Browse Source

Merge remote-tracking branch 'arsen/cli_videos' into sumer-cli

Leszek Wiesner 3 years ago
parent
commit
5f9df2bf4f

+ 1 - 1
cli/examples/content/UpdateChannel.json

@@ -4,7 +4,7 @@
       "Urls": ["https://joystream.org/WorkingGroupOpening.schema.json"]
     }
   ],
-  "new_meta": {
+  "meta": {
     "title": "Channel Title",
     "description": "Cool Description",
     "isPublic": true,

+ 36 - 0
cli/examples/content/createVideo.json

@@ -0,0 +1,36 @@
+{
+  "assets": [
+    {
+      "Urls": ["https://joystream.org/WorkingGroupOpening.schema.json"]
+    }
+  ],
+  "meta": {
+    "title": "Title",
+    "description": "Description",
+    "video": 1,
+    "thumbnailPhoto": 1,
+    "duration": 10,
+    "mediaPixelHeight": 20,
+    "mediaPixelWidth": 50,
+    "language": "en",
+    "hasMarketing": true,
+    "isPublic": true,
+    "isExplicit": true,
+    "personsList": [1, 2, 5],
+    "category": 2,
+    "mediaType": {
+      "codecName": "mpeg4",
+      "container": "avi",
+      "mimeMediaType": "videp/mp4"
+    },
+    "license": {
+      "code": 1001,
+      "attribution": "first",
+      "customText": "text"
+    },
+    "publishedBeforeJoystream": {
+      "isPublished": true,
+      "date": "2012-09-27"
+    }
+  }
+}

+ 36 - 0
cli/examples/content/updateVideo.json

@@ -0,0 +1,36 @@
+{
+  "assets": [
+    {
+      "Urls": ["https://joystream.org/WorkingGroupOpening.schema.json"]
+    }
+  ],
+  "meta": {
+    "title": "Title",
+    "description": "Description",
+    "video": 1,
+    "thumbnailPhoto": "1",
+    "duration": 10,
+    "mediaPixelHeight": 20,
+    "mediaPixelWidth": 50,
+    "language": "en",
+    "hasMarketing": true,
+    "isPublic": true,
+    "isExplicit": true,
+    "personsList": [1, 2, 5],
+    "category": 2,
+    "mediaType": {
+      "codecName": "mpeg4",
+      "container": "avi",
+      "mimeMediaType": "videp/mp4"
+    },
+    "license": {
+      "code": 1001,
+      "attribution": "first",
+      "customText": "text"
+    },
+    "publishedBeforeJoystream": {
+      "isPublished": true,
+      "date": "2012-09-27"
+    }
+  }
+}

+ 15 - 3
cli/src/Api.ts

@@ -498,6 +498,10 @@ export default class Api {
     return await this.entriesByIds<ChannelId, Channel>(this._api.query.content.channelById)
   }
 
+  async availableVideos(): Promise<[VideoId, Video][]> {
+    return await this.entriesByIds<VideoId, Video>(this._api.query.content.videoById)
+  }
+
   availableCuratorGroups(): Promise<[CuratorGroupId, CuratorGroup][]> {
     return this.entriesByIds<CuratorGroupId, CuratorGroup>(this._api.query.content.curatorGroupById)
   }
@@ -517,12 +521,20 @@ export default class Api {
   }
 
   async videosByChannelId(channelId: number): Promise<[VideoId, Video][]> {
-    const videoEntries = await this.entriesByIds<VideoId, Video>(this._api.query.content.videoById)
-    return videoEntries.filter(([, video]) => video.in_channel.toNumber() === channelId)
+    const channel = await this.channelById(channelId)
+    if (channel) {
+      return Promise.all(
+        channel.videos.map(
+          async (videoId) => [videoId, await this._api.query.content.videoById<Video>(videoId)] as [VideoId, Video]
+        )
+      )
+    } else {
+      return []
+    }
   }
 
   async videoById(videoId: number): Promise<Video | null> {
-    const exists = !!(await this._api.query.content.entityById.size(videoId)).toNumber()
+    const exists = !!(await this._api.query.content.videoById.size(videoId)).toNumber()
     return exists ? await this._api.query.content.videoById<Video>(videoId) : null
   }
 

+ 48 - 1
cli/src/Types.ts

@@ -1,6 +1,6 @@
 import BN from 'bn.js'
 import { ElectionStage, Seat } from '@joystream/types/council'
-import { Option } from '@polkadot/types'
+import { Vec, Option } from '@polkadot/types'
 import { Codec } from '@polkadot/types/types'
 import { BlockNumber, Balance, AccountId } from '@polkadot/types/interfaces'
 import { DeriveBalancesAll } from '@polkadot/api-derive/types'
@@ -9,6 +9,9 @@ import { WorkerId, OpeningType } from '@joystream/types/working-group'
 import { Membership, MemberId } from '@joystream/types/members'
 import { Opening, StakingPolicy, ApplicationStageKeys } from '@joystream/types/hiring'
 import { Validator } from 'inquirer'
+import { NewAsset } from '@joystream/types/content'
+import { Bytes } from '@polkadot/types/primitive'
+import { VideoMetadata, ChannelMetadata } from '@joystream/content-metadata-protobuf'
 
 // KeyringPair type extended with mandatory "meta.name"
 // It's used for accounts/keys management within CLI.
@@ -197,3 +200,47 @@ export type ApiMethodNamedArg = {
   value: ApiMethodArg
 }
 export type ApiMethodNamedArgs = ApiMethodNamedArg[]
+
+export type VideoUpdateParametersInput = {
+  assets: Option<Vec<NewAsset>>
+  meta: VideoMetadata.AsObject
+}
+
+export type VideoUpdateParameters = {
+  assets: Option<Vec<NewAsset>>
+  meta: Bytes
+}
+
+export type VideoCreationParametersInput = {
+  assets: Vec<NewAsset>
+  meta: VideoMetadata.AsObject
+}
+
+export type VideoCreationParameters = {
+  assets: Vec<NewAsset>
+  meta: Bytes
+}
+
+export type ChannelCreationParametersInput = {
+  assets: Vec<NewAsset>
+  meta: ChannelMetadata.AsObject
+  reward_account: Option<AccountId>
+}
+
+export type ChannelCreationParameters = {
+  assets: Vec<NewAsset>
+  meta: Bytes
+  reward_account: Option<AccountId>
+}
+
+export type ChannelUpdateParametersInput = {
+  assets: Option<Vec<NewAsset>>
+  meta: ChannelMetadata.AsObject
+  reward_account: Option<AccountId>
+}
+
+export type ChannelUpdateParameters = {
+  assets: Option<Vec<NewAsset>>
+  new_meta: Bytes
+  reward_account: Option<AccountId>
+}

+ 4 - 28
cli/src/commands/content/createChannel.ts

@@ -1,22 +1,7 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import { IOFlags, getInputJson } from '../../helpers/InputOutput'
-import { NewAsset } from '@joystream/types/content'
-import { ChannelMetadata } from '@joystream/content-metadata-protobuf'
-import { Vec, Option } from '@polkadot/types'
-import AccountId from '@polkadot/types/generic/AccountId'
-import { Bytes } from '@polkadot/types/primitive'
-
-type ChannelCreationParametersInput = {
-  assets: Vec<NewAsset>
-  meta: ChannelMetadata.AsObject
-  reward_account: Option<AccountId>
-}
-
-type ChannelCreationParameters = {
-  assets: Vec<NewAsset>
-  meta: Bytes
-  reward_account: Option<AccountId>
-}
+import { ChannelCreationParameters, ChannelCreationParametersInput } from '../../Types'
+import { channelMetadataFromInput } from '../../helpers/serialization'
 
 export default class CreateChannelCommand extends ContentDirectoryCommandBase {
   static description = 'Create channel inside content directory.'
@@ -40,18 +25,9 @@ export default class CreateChannelCommand extends ContentDirectoryCommandBase {
     if (input) {
       const channelCreationParametersInput = await getInputJson<ChannelCreationParametersInput>(input)
 
-      const channelMetadata = new ChannelMetadata()
-      channelMetadata.setTitle(channelCreationParametersInput.meta.title!)
-      channelMetadata.setDescription(channelCreationParametersInput.meta.description!)
-      channelMetadata.setIsPublic(channelCreationParametersInput.meta.isPublic!)
-      channelMetadata.setLanguage(channelCreationParametersInput.meta.language!)
-      channelMetadata.setCoverPhoto(channelCreationParametersInput.meta.coverPhoto!)
-      channelMetadata.setAvatarPhoto(channelCreationParametersInput.meta.avatarPhoto!)
-      channelMetadata.setCategory(channelCreationParametersInput.meta.category!)
-
-      const serialized = channelMetadata.serializeBinary()
+      const api = await this.getOriginalApi()
 
-      const meta = this.createType('Bytes', '0x' + Buffer.from(serialized).toString('hex'))
+      const meta = channelMetadataFromInput(api, channelCreationParametersInput)
 
       const channelCreationParameters: ChannelCreationParameters = {
         assets: channelCreationParametersInput.assets,

+ 65 - 0
cli/src/commands/content/createVideo.ts

@@ -0,0 +1,65 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+import { IOFlags, getInputJson } from '../../helpers/InputOutput'
+import { videoMetadataFromInput } from '../../helpers/serialization'
+import { VideoCreationParameters, VideoCreationParametersInput } from '../../Types'
+
+export default class CreateVideoCommand extends ContentDirectoryCommandBase {
+  static description = 'Create video under specific channel inside content directory.'
+  static flags = {
+    context: ContentDirectoryCommandBase.ownerContextFlag,
+    input: IOFlags.input,
+  }
+
+  static args = [
+    {
+      name: 'channelId',
+      required: true,
+      description: 'ID of the Channel',
+    },
+  ]
+
+  async run() {
+    let { context, input } = this.parse(CreateVideoCommand).flags
+
+    const { channelId } = this.parse(CreateVideoCommand).args
+
+    if (!context) {
+      context = await this.promptForOwnerContext()
+    }
+
+    const currentAccount = await this.getRequiredSelectedAccount()
+    await this.requestAccountDecoding(currentAccount)
+
+    const actor = await this.getActor(context)
+
+    if (input) {
+      const videoCreationParametersInput = await getInputJson<VideoCreationParametersInput>(input)
+
+      const api = await this.getOriginalApi()
+
+      const meta = videoMetadataFromInput(api, videoCreationParametersInput)
+
+      const videoCreationParameters: VideoCreationParameters = {
+        assets: videoCreationParametersInput.assets,
+        meta,
+      }
+
+      this.jsonPrettyPrint(JSON.stringify(videoCreationParametersInput))
+      this.log('Meta: ' + meta)
+
+      const confirmed = await this.simplePrompt({ type: 'confirm', message: 'Do you confirm the provided input?' })
+
+      if (confirmed) {
+        this.log('Sending the extrinsic...')
+
+        await this.sendAndFollowNamedTx(currentAccount, 'content', 'createVideo', [
+          actor,
+          channelId,
+          videoCreationParameters,
+        ])
+      }
+    } else {
+      this.error('Input invalid or was not provided...')
+    }
+  }
+}

+ 4 - 29
cli/src/commands/content/updateChannel.ts

@@ -1,22 +1,7 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import { IOFlags, getInputJson } from '../../helpers/InputOutput'
-import { NewAsset } from '@joystream/types/content'
-import { ChannelMetadata } from '@joystream/content-metadata-protobuf'
-import { Vec, Option } from '@polkadot/types'
-import AccountId from '@polkadot/types/generic/AccountId'
-import { Bytes } from '@polkadot/types/primitive'
-
-type ChannelUpdateParametersInput = {
-  assets: Option<Vec<NewAsset>>
-  new_meta: ChannelMetadata.AsObject
-  reward_account: Option<AccountId>
-}
-
-type ChannelUpdateParameters = {
-  assets: Option<Vec<NewAsset>>
-  new_meta: Bytes
-  reward_account: Option<AccountId>
-}
+import { channelMetadataFromInput } from '../../helpers/serialization'
+import { ChannelUpdateParameters, ChannelUpdateParametersInput } from '../../Types'
 
 export default class UpdateChannelCommand extends ContentDirectoryCommandBase {
   static description = 'Update existing content directory channel.'
@@ -50,18 +35,9 @@ export default class UpdateChannelCommand extends ContentDirectoryCommandBase {
     if (input) {
       const channelUpdateParametersInput = await getInputJson<ChannelUpdateParametersInput>(input)
 
-      const channelMetadata = new ChannelMetadata()
-      channelMetadata.setTitle(channelUpdateParametersInput.new_meta.title!)
-      channelMetadata.setDescription(channelUpdateParametersInput.new_meta.description!)
-      channelMetadata.setIsPublic(channelUpdateParametersInput.new_meta.isPublic!)
-      channelMetadata.setLanguage(channelUpdateParametersInput.new_meta.language!)
-      channelMetadata.setCoverPhoto(channelUpdateParametersInput.new_meta.coverPhoto!)
-      channelMetadata.setAvatarPhoto(channelUpdateParametersInput.new_meta.avatarPhoto!)
-      channelMetadata.setCategory(channelUpdateParametersInput.new_meta.category!)
-
-      const serialized = channelMetadata.serializeBinary()
+      const api = await this.getOriginalApi()
 
-      const meta = this.createType('Bytes', '0x' + Buffer.from(serialized).toString('hex'))
+      const meta = channelMetadataFromInput(api, channelUpdateParametersInput)
 
       const channelUpdateParameters: ChannelUpdateParameters = {
         assets: channelUpdateParametersInput.assets,
@@ -73,7 +49,6 @@ export default class UpdateChannelCommand extends ContentDirectoryCommandBase {
 
       this.log('Meta: ' + meta)
 
-      this.jsonPrettyPrint(JSON.stringify(channelUpdateParameters))
       const confirmed = await this.simplePrompt({ type: 'confirm', message: 'Do you confirm the provided input?' })
 
       if (confirmed) {

+ 65 - 0
cli/src/commands/content/updateVideo.ts

@@ -0,0 +1,65 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+import { IOFlags, getInputJson } from '../../helpers/InputOutput'
+import { VideoUpdateParameters, VideoUpdateParametersInput } from '../../Types'
+import { videoMetadataFromInput } from '../../helpers/serialization'
+
+export default class UpdateVideoCommand extends ContentDirectoryCommandBase {
+  static description = 'Update video under specific id.'
+  static flags = {
+    context: ContentDirectoryCommandBase.ownerContextFlag,
+    input: IOFlags.input,
+  }
+
+  static args = [
+    {
+      name: 'videoId',
+      required: true,
+      description: 'ID of the Video',
+    },
+  ]
+
+  async run() {
+    let { context, input } = this.parse(UpdateVideoCommand).flags
+
+    const { videoId } = this.parse(UpdateVideoCommand).args
+
+    if (!context) {
+      context = await this.promptForOwnerContext()
+    }
+
+    const currentAccount = await this.getRequiredSelectedAccount()
+    await this.requestAccountDecoding(currentAccount)
+
+    const actor = await this.getActor(context)
+
+    if (input) {
+      const videoUpdateParametersInput = await getInputJson<VideoUpdateParametersInput>(input)
+
+      const api = await this.getOriginalApi()
+
+      const meta = videoMetadataFromInput(api, videoUpdateParametersInput)
+
+      const videoUpdateParameters: VideoUpdateParameters = {
+        assets: videoUpdateParametersInput.assets,
+        meta,
+      }
+
+      this.jsonPrettyPrint(JSON.stringify(videoUpdateParametersInput))
+      this.log('Meta: ' + meta)
+
+      const confirmed = await this.simplePrompt({ type: 'confirm', message: 'Do you confirm the provided input?' })
+
+      if (confirmed) {
+        this.log('Sending the extrinsic...')
+
+        await this.sendAndFollowNamedTx(currentAccount, 'content', 'updateVideo', [
+          actor,
+          videoId,
+          videoUpdateParameters,
+        ])
+      }
+    } else {
+      this.error('Input invalid or was not provided...')
+    }
+  }
+}

+ 28 - 0
cli/src/commands/content/video.ts

@@ -0,0 +1,28 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+import { displayCollapsedRow } from '../../helpers/display'
+
+export default class VideoCommand extends ContentDirectoryCommandBase {
+  static description = 'Show Video details by id.'
+  static args = [
+    {
+      name: 'videoId',
+      required: true,
+      description: 'ID of the Video',
+    },
+  ]
+
+  async run() {
+    const { videoId } = this.parse(VideoCommand).args
+    const aVideo = await this.getApi().videoById(videoId)
+    if (aVideo) {
+      displayCollapsedRow({
+        'ID': videoId.toString(),
+        'InChannel': aVideo.in_channel.toString(),
+        'InSeries': aVideo.in_series.toString(),
+        'IsCensored': aVideo.is_censored.toString(),
+      })
+    } else {
+      this.error(`Video not found by channel id: "${videoId}"!`)
+    }
+  }
+}

+ 40 - 0
cli/src/commands/content/videos.ts

@@ -0,0 +1,40 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+import { Video, VideoId } from '@joystream/types/content'
+import { displayTable } from '../../helpers/display'
+
+export default class VideosCommand extends ContentDirectoryCommandBase {
+  static description = 'List existing content directory videos.'
+
+  static args = [
+    {
+      name: 'channelId',
+      required: true,
+      description: 'ID of the Channel',
+    },
+  ]
+
+  async run() {
+    const { channelId } = this.parse(VideosCommand).args
+
+    let videos: [VideoId, Video][]
+    if (channelId) {
+      videos = await this.getApi().videosByChannelId(channelId)
+    } else {
+      videos = await this.getApi().availableVideos()
+    }
+
+    if (videos.length > 0) {
+      displayTable(
+        videos.map(([id, v]) => ({
+          'ID': id.toString(),
+          'InChannel': v.in_channel.toString(),
+          'InSeries': v.in_series.toString(),
+          'IsCensored': v.is_censored.toString(),
+        })),
+        3
+      )
+    } else {
+      this.log('There are no videos yet')
+    }
+  }
+}

+ 77 - 0
cli/src/helpers/serialization.ts

@@ -0,0 +1,77 @@
+import {
+  VideoMetadata,
+  PublishedBeforeJoystream,
+  License,
+  MediaType,
+  ChannelMetadata,
+} from '@joystream/content-metadata-protobuf'
+import {
+  VideoUpdateParametersInput,
+  VideoCreationParametersInput,
+  ChannelUpdateParametersInput,
+  ChannelCreationParametersInput,
+} from '../Types'
+import { ApiPromise } from '@polkadot/api'
+import { Bytes } from '@polkadot/types/primitive'
+
+export function binaryToMeta(api: ApiPromise, serialized: Uint8Array): Bytes {
+  return api.createType('Bytes', '0x' + Buffer.from(serialized).toString('hex'))
+}
+
+export function videoMetadataFromInput(
+  api: ApiPromise,
+  videoParametersInput: VideoCreationParametersInput | VideoUpdateParametersInput
+): Bytes {
+  const mediaType = new MediaType()
+  mediaType.setCodecName(videoParametersInput.meta.mediaType!.codecName!)
+  mediaType.setContainer(videoParametersInput.meta.mediaType!.container!)
+  mediaType.setMimeMediaType(videoParametersInput.meta.mediaType!.mimeMediaType!)
+
+  const license = new License()
+  license.setCode(videoParametersInput.meta.license!.code!)
+  license.setAttribution(videoParametersInput.meta.license!.attribution!)
+  license.setCustomText(videoParametersInput.meta.license!.customText!)
+
+  const publishedBeforeJoystream = new PublishedBeforeJoystream()
+  publishedBeforeJoystream.setIsPublished(videoParametersInput.meta.publishedBeforeJoystream!.isPublished!)
+  publishedBeforeJoystream.setDate(videoParametersInput.meta.publishedBeforeJoystream!.date!)
+
+  const videoMetadata = new VideoMetadata()
+  videoMetadata.setTitle(videoParametersInput.meta.title!)
+  videoMetadata.setDescription(videoParametersInput.meta.description!)
+  videoMetadata.setVideo(videoParametersInput.meta.video!)
+  videoMetadata.setThumbnailPhoto(videoParametersInput.meta.thumbnailPhoto!)
+  videoMetadata.setDuration(videoParametersInput.meta.duration!)
+  videoMetadata.setMediaPixelHeight(videoParametersInput.meta.mediaPixelHeight!)
+  videoMetadata.setMediaPixelWidth(videoParametersInput.meta.mediaPixelWidth!)
+  videoMetadata.setLanguage(videoParametersInput.meta.language!)
+  videoMetadata.setHasMarketing(videoParametersInput.meta.hasMarketing!)
+  videoMetadata.setIsPublic(videoParametersInput.meta.isPublic!)
+  videoMetadata.setIsExplicit(videoParametersInput.meta.isExplicit!)
+  videoMetadata.setPersonsList(videoParametersInput.meta.personsList!)
+  videoMetadata.setCategory(videoParametersInput.meta.category!)
+
+  videoMetadata.setMediaType(mediaType)
+  videoMetadata.setLicense(license)
+  videoMetadata.setPublishedBeforeJoystream(publishedBeforeJoystream)
+
+  const serialized = videoMetadata.serializeBinary()
+  return binaryToMeta(api, serialized)
+}
+
+export function channelMetadataFromInput(
+  api: ApiPromise,
+  channelParametersInput: ChannelCreationParametersInput | ChannelUpdateParametersInput
+): Bytes {
+  const channelMetadata = new ChannelMetadata()
+  channelMetadata.setTitle(channelParametersInput.meta.title!)
+  channelMetadata.setDescription(channelParametersInput.meta.description!)
+  channelMetadata.setIsPublic(channelParametersInput.meta.isPublic!)
+  channelMetadata.setLanguage(channelParametersInput.meta.language!)
+  channelMetadata.setCoverPhoto(channelParametersInput.meta.coverPhoto!)
+  channelMetadata.setAvatarPhoto(channelParametersInput.meta.avatarPhoto!)
+  channelMetadata.setCategory(channelParametersInput.meta.category!)
+
+  const serialized = channelMetadata.serializeBinary()
+  return binaryToMeta(api, serialized)
+}

+ 3 - 3
content-metadata-protobuf/src/index.ts

@@ -1,6 +1,6 @@
-// Some helpers for constructing known licences
-import licences from './licenses'
-export { licences }
+// Some helpers for constructing known licenses
+import licenses from './licenses'
+export { licenses }
 
 // protobuf message constructors
 export * from '../compiled/proto/Video_pb'

+ 1 - 1
content-metadata-protobuf/src/licenses.ts

@@ -1,4 +1,4 @@
-// Helper methods to handle joystream defined licence types
+// Helper methods to handle joystream defined license types
 // This should be factored out into a separate package
 
 import LICENSES from './KnownLicenses.json'

+ 2 - 2
content-metadata-protobuf/test/license-codes.ts

@@ -14,7 +14,7 @@ describe('Known License Codes', () => {
   })
 
   it('Pre-defined Joystream license codes', () => {
-    // Make sure we have correct known custom licence
+    // Make sure we have correct known custom license
     assert(KnownLicenses.has(CUSTOM_LICENSE_CODE))
     assert.equal(KnownLicenses.get(CUSTOM_LICENSE_CODE)!.name, 'CUSTOM')
 
@@ -33,7 +33,7 @@ describe('Known License Codes', () => {
     assert.equal(license.getCode(), CUSTOM_LICENSE_CODE)
   })
 
-  it('createKnownLicenseFromCode(): Licence can be created by name', () => {
+  it('createKnownLicenseFromCode(): License can be created by name', () => {
     const licenseCode = getLicenseCodeByName('CC_BY') as number
     const license = createKnownLicenseFromCode(licenseCode as number, 'Attribution: Joystream')
     const videoMeta = new VideoMetadata()

+ 1 - 1
content-metadata-protobuf/test/video.ts

@@ -78,7 +78,7 @@ describe('Video Metadata', () => {
     })
   })
 
-  it('Message: Licence', () => {
+  it('Message: License', () => {
     const license = new License()
 
     const code = 1000

+ 28 - 70
runtime-modules/content/src/lib.rs

@@ -770,15 +770,20 @@ decl_module! {
         }
 
         #[weight = 10_000_000] // TODO: adjust weight
-        pub fn censor_channel(
+        pub fn update_channel_censorship_status(
             origin,
             actor: ContentActor<T::CuratorGroupId, T::CuratorId, T::MemberId>,
             channel_id: T::ChannelId,
+            is_censored: bool,
             rationale: Vec<u8>,
         ) {
             // check that channel exists
             let channel = Self::ensure_channel_exists(&channel_id)?;
 
+            if channel.is_censored == is_censored {
+                return Ok(())
+            }
+
             ensure_actor_authorized_to_censor::<T>(
                 origin,
                 &actor,
@@ -791,44 +796,14 @@ decl_module! {
 
             let mut channel = channel;
 
-            channel.is_censored = true;
+            channel.is_censored = is_censored;
 
             // TODO: unset the reward account ? so no revenue can be earned for censored channels?
 
             // Update the channel
             ChannelById::<T>::insert(channel_id, channel);
 
-            Self::deposit_event(RawEvent::ChannelCensored(actor, channel_id, rationale));
-        }
-
-        #[weight = 10_000_000] // TODO: adjust weight
-        pub fn uncensor_channel(
-            origin,
-            actor: ContentActor<T::CuratorGroupId, T::CuratorId, T::MemberId>,
-            channel_id: T::ChannelId,
-            rationale: Vec<u8>,
-        ) {
-            // check that channel exists
-            let channel = Self::ensure_channel_exists(&channel_id)?;
-
-            ensure_actor_authorized_to_censor::<T>(
-                origin,
-                &actor,
-                &channel.owner,
-            )?;
-
-            //
-            // == MUTATION SAFE ==
-            //
-
-            let mut channel = channel;
-
-            channel.is_censored = false;
-
-            // Update the channel
-            ChannelById::<T>::insert(channel_id, channel);
-
-            Self::deposit_event(RawEvent::ChannelUncensored(actor, channel_id, rationale));
+            Self::deposit_event(RawEvent::ChannelCensorshipStatusUpdated(actor, channel_id, is_censored, rationale));
         }
 
         #[weight = 10_000_000] // TODO: adjust weight
@@ -1221,45 +1196,19 @@ decl_module! {
         }
 
         #[weight = 10_000_000] // TODO: adjust weight
-        pub fn censor_video(
+        pub fn update_video_censorship_status(
             origin,
             actor: ContentActor<T::CuratorGroupId, T::CuratorId, T::MemberId>,
             video_id: T::VideoId,
+            is_censored: bool,
             rationale: Vec<u8>,
         ) {
             // check that video exists
             let video = Self::ensure_video_exists(&video_id)?;
 
-            ensure_actor_authorized_to_censor::<T>(
-                origin,
-                &actor,
-                // The channel owner will be..
-                &Self::channel_by_id(video.in_channel).owner,
-            )?;
-
-            //
-            // == MUTATION SAFE ==
-            //
-
-            let mut video = video;
-
-            video.is_censored = true;
-
-            // Update the video
-            VideoById::<T>::insert(video_id, video);
-
-            Self::deposit_event(RawEvent::VideoCensored(actor, video_id, rationale));
-        }
-
-        #[weight = 10_000_000] // TODO: adjust weight
-        pub fn uncensor_video(
-            origin,
-            actor: ContentActor<T::CuratorGroupId, T::CuratorId, T::MemberId>,
-            video_id: T::VideoId,
-            rationale: Vec<u8>
-        ) {
-            // check that video exists
-            let video = Self::ensure_video_exists(&video_id)?;
+            if video.is_censored == is_censored {
+                return Ok(())
+            }
 
             ensure_actor_authorized_to_censor::<T>(
                 origin,
@@ -1274,12 +1223,12 @@ decl_module! {
 
             let mut video = video;
 
-            video.is_censored = false;
+            video.is_censored = is_censored;
 
             // Update the video
             VideoById::<T>::insert(video_id, video);
 
-            Self::deposit_event(RawEvent::VideoUncensored(actor, video_id, rationale));
+            Self::deposit_event(RawEvent::VideoCensorshipStatusUpdated(actor, video_id, is_censored, rationale));
         }
 
         #[weight = 10_000_000] // TODO: adjust weight
@@ -1453,6 +1402,7 @@ decl_event!(
         ContentParameters = ContentParameters<T>,
         AccountId = <T as system::Trait>::AccountId,
         ContentId = ContentId<T>,
+        IsCensored = bool,
     {
         // Curators
         CuratorGroupCreated(CuratorGroupId),
@@ -1475,8 +1425,12 @@ decl_event!(
         ),
         ChannelAssetsRemoved(ContentActor, ChannelId, Vec<ContentId>),
 
-        ChannelCensored(ContentActor, ChannelId, Vec<u8> /* rationale */),
-        ChannelUncensored(ContentActor, ChannelId, Vec<u8> /* rationale */),
+        ChannelCensorshipStatusUpdated(
+            ContentActor,
+            ChannelId,
+            IsCensored,
+            Vec<u8>, /* rationale */
+        ),
 
         // Channel Ownership Transfers
         ChannelOwnershipTransferRequested(
@@ -1522,8 +1476,12 @@ decl_event!(
         ),
         VideoDeleted(ContentActor, VideoId),
 
-        VideoCensored(ContentActor, VideoId, Vec<u8> /* rationale */),
-        VideoUncensored(ContentActor, VideoId, Vec<u8> /* rationale */),
+        VideoCensorshipStatusUpdated(
+            ContentActor,
+            VideoId,
+            IsCensored,
+            Vec<u8>, /* rationale */
+        ),
 
         // Featured Videos
         FeaturedVideosSet(ContentActor, Vec<VideoId>),

+ 17 - 7
runtime-modules/content/src/tests/channels.rs

@@ -292,18 +292,21 @@ fn channel_censoring() {
         let group_id = curators::add_curator_to_new_group(FIRST_CURATOR_ID);
 
         // Curator can censor channels
-        assert_ok!(Content::censor_channel(
+        let is_censored = true;
+        assert_ok!(Content::update_channel_censorship_status(
             Origin::signed(FIRST_CURATOR_ORIGIN),
             ContentActor::Curator(group_id, FIRST_CURATOR_ID),
             channel_id,
+            is_censored,
             vec![]
         ));
 
         assert_eq!(
             System::events().last().unwrap().event,
-            MetaEvent::content(RawEvent::ChannelCensored(
+            MetaEvent::content(RawEvent::ChannelCensorshipStatusUpdated(
                 ContentActor::Curator(group_id, FIRST_CURATOR_ID),
                 channel_id,
+                is_censored,
                 vec![]
             ))
         );
@@ -313,18 +316,21 @@ fn channel_censoring() {
         assert!(channel.is_censored);
 
         // Curator can un-censor channels
-        assert_ok!(Content::uncensor_channel(
+        let is_censored = false;
+        assert_ok!(Content::update_channel_censorship_status(
             Origin::signed(FIRST_CURATOR_ORIGIN),
             ContentActor::Curator(group_id, FIRST_CURATOR_ID),
             channel_id,
+            is_censored,
             vec![]
         ));
 
         assert_eq!(
             System::events().last().unwrap().event,
-            MetaEvent::content(RawEvent::ChannelUncensored(
+            MetaEvent::content(RawEvent::ChannelCensorshipStatusUpdated(
                 ContentActor::Curator(group_id, FIRST_CURATOR_ID),
                 channel_id,
+                is_censored,
                 vec![]
             ))
         );
@@ -334,11 +340,13 @@ fn channel_censoring() {
         assert!(!channel.is_censored);
 
         // Member cannot censor channels
+        let is_censored = true;
         assert_err!(
-            Content::censor_channel(
+            Content::update_channel_censorship_status(
                 Origin::signed(FIRST_MEMBER_ORIGIN),
                 ContentActor::Member(FIRST_MEMBER_ID),
                 channel_id,
+                is_censored,
                 vec![]
             ),
             Error::<Test>::ActorNotAuthorized
@@ -359,20 +367,22 @@ fn channel_censoring() {
 
         // Curator cannot censor curator group channels
         assert_err!(
-            Content::censor_channel(
+            Content::update_channel_censorship_status(
                 Origin::signed(FIRST_CURATOR_ORIGIN),
                 ContentActor::Curator(group_id, FIRST_CURATOR_ID),
                 curator_channel_id,
+                is_censored,
                 vec![]
             ),
             Error::<Test>::CannotCensoreCuratorGroupOwnedChannels
         );
 
         // Lead can still censor curator group channels
-        assert_ok!(Content::censor_channel(
+        assert_ok!(Content::update_channel_censorship_status(
             Origin::signed(LEAD_ORIGIN),
             ContentActor::Lead,
             curator_channel_id,
+            is_censored,
             vec![]
         ));
     })

+ 12 - 5
runtime-modules/content/src/tests/videos.rs

@@ -160,18 +160,21 @@ fn curators_can_censor_videos() {
         let group_id = curators::add_curator_to_new_group(FIRST_CURATOR_ID);
 
         // Curator can censor videos
-        assert_ok!(Content::censor_video(
+        let is_censored = true;
+        assert_ok!(Content::update_video_censorship_status(
             Origin::signed(FIRST_CURATOR_ORIGIN),
             ContentActor::Curator(group_id, FIRST_CURATOR_ID),
             video_id,
+            is_censored,
             vec![]
         ));
 
         assert_eq!(
             System::events().last().unwrap().event,
-            MetaEvent::content(RawEvent::VideoCensored(
+            MetaEvent::content(RawEvent::VideoCensorshipStatusUpdated(
                 ContentActor::Curator(group_id, FIRST_CURATOR_ID),
                 video_id,
+                is_censored,
                 vec![]
             ))
         );
@@ -181,18 +184,21 @@ fn curators_can_censor_videos() {
         assert!(video.is_censored);
 
         // Curator can un-censor videos
-        assert_ok!(Content::uncensor_video(
+        let is_censored = false;
+        assert_ok!(Content::update_video_censorship_status(
             Origin::signed(FIRST_CURATOR_ORIGIN),
             ContentActor::Curator(group_id, FIRST_CURATOR_ID),
             video_id,
+            is_censored,
             vec![]
         ));
 
         assert_eq!(
             System::events().last().unwrap().event,
-            MetaEvent::content(RawEvent::VideoUncensored(
+            MetaEvent::content(RawEvent::VideoCensorshipStatusUpdated(
                 ContentActor::Curator(group_id, FIRST_CURATOR_ID),
                 video_id,
+                is_censored,
                 vec![]
             ))
         );
@@ -203,10 +209,11 @@ fn curators_can_censor_videos() {
 
         // Members cannot censor videos
         assert_err!(
-            Content::censor_video(
+            Content::update_video_censorship_status(
                 Origin::signed(FIRST_MEMBER_ORIGIN),
                 ContentActor::Member(FIRST_MEMBER_ORIGIN),
                 channel_id,
+                true,
                 vec![]
             ),
             Error::<Test>::ActorNotAuthorized