Browse Source

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

Leszek Wiesner 3 years ago
parent
commit
de0ec5fdeb

+ 5 - 0
cli/examples/content/CreateCategory.json

@@ -0,0 +1,5 @@
+{
+  "meta": {
+    "name": "Nature"
+  }
+}

+ 5 - 0
cli/examples/content/UpdateCategory.json

@@ -0,0 +1,5 @@
+{
+  "meta": {
+    "name": "Science"
+  }
+}

+ 19 - 1
cli/src/Api.ts

@@ -48,7 +48,15 @@ import { RewardRelationship, RewardRelationshipId } from '@joystream/types/recur
 import { Stake, StakeId } from '@joystream/types/stake'
 
 import { InputValidationLengthConstraint, ChannelId, Url } from '@joystream/types/common'
-import { CuratorGroup, CuratorGroupId, Channel, Video, VideoId } from '@joystream/types/content'
+import {
+  CuratorGroup,
+  CuratorGroupId,
+  Channel,
+  Video,
+  VideoId,
+  ChannelCategory,
+  VideoCategory,
+} from '@joystream/types/content'
 import { ContentId, DataObject } from '@joystream/types/storage'
 import { ServiceProviderRecord } from '@joystream/types/discovery'
 import _ from 'lodash'
@@ -538,6 +546,16 @@ export default class Api {
     return exists ? await this._api.query.content.videoById<Video>(videoId) : null
   }
 
+  async channelCategoryById(channelCategoryId: number): Promise<ChannelCategory | null> {
+    const exists = !!(await this._api.query.content.channelCategoryById.size(channelCategoryId)).toNumber()
+    return exists ? await this._api.query.content.channelCategoryById<ChannelCategory>(channelCategoryId) : null
+  }
+
+  async videoCategoryById(videoCategoryId: number): Promise<VideoCategory | null> {
+    const exists = !!(await this._api.query.content.videoCategoryById.size(videoCategoryId)).toNumber()
+    return exists ? await this._api.query.content.videoCategoryById<VideoCategory>(videoCategoryId) : null
+  }
+
   async dataByContentId(contentId: ContentId): Promise<DataObject | null> {
     const dataObject = await this._api.query.dataDirectory.dataByContentId<DataObject>(contentId)
     return dataObject.isEmpty ? null : dataObject

+ 38 - 1
cli/src/Types.ts

@@ -11,7 +11,12 @@ import { Opening, StakingPolicy, ApplicationStageKeys } from '@joystream/types/h
 import { Validator } from 'inquirer'
 import { NewAsset } from '@joystream/types/content'
 import { Bytes } from '@polkadot/types/primitive'
-import { VideoMetadata, ChannelMetadata } from '@joystream/content-metadata-protobuf'
+import {
+  VideoMetadata,
+  ChannelMetadata,
+  ChannelCategoryMetadata,
+  VideoCategoryMetadata,
+} from '@joystream/content-metadata-protobuf'
 
 // KeyringPair type extended with mandatory "meta.name"
 // It's used for accounts/keys management within CLI.
@@ -244,3 +249,35 @@ export type ChannelUpdateParameters = {
   new_meta: Bytes
   reward_account: Option<AccountId>
 }
+
+export type ChannelCategoryCreationParametersInput = {
+  meta: ChannelCategoryMetadata.AsObject
+}
+
+export type ChannelCategoryCreationParameters = {
+  meta: Bytes
+}
+
+export type ChannelCategoryUpdateParametersInput = {
+  meta: ChannelCategoryMetadata.AsObject
+}
+
+export type ChannelCategoryUpdateParameters = {
+  new_meta: Bytes
+}
+
+export type VideoCategoryCreationParametersInput = {
+  meta: VideoCategoryMetadata.AsObject
+}
+
+export type VideoCategoryCreationParameters = {
+  meta: Bytes
+}
+
+export type VideoCategoryUpdateParametersInput = {
+  meta: VideoCategoryMetadata.AsObject
+}
+
+export type VideoCategoryUpdateParameters = {
+  new_meta: Bytes
+}

+ 20 - 1
cli/src/base/ContentDirectoryCommandBase.ts

@@ -9,9 +9,11 @@ import { flags } from '@oclif/command'
 
 const CONTEXTS = ['Member', 'Curator', 'Lead'] as const
 const OWNER_CONTEXTS = ['Member', 'Curator'] as const
+const CATEGORIES_CONTEXTS = ['Lead', 'Curator'] as const
 
 type Context = typeof CONTEXTS[number]
 type OwnerContext = typeof OWNER_CONTEXTS[number]
+type CategoriesContext = typeof CATEGORIES_CONTEXTS[number]
 
 /**
  * Abstract base class for commands related to content directory
@@ -27,12 +29,19 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
   })
 
   static ownerContextFlag = flags.enum({
-    name: 'context',
+    name: 'ownerContext',
     required: false,
     description: `Actor context to execute the command in (${OWNER_CONTEXTS.join('/')})`,
     options: [...OWNER_CONTEXTS],
   })
 
+  static categoriesContextFlag = flags.enum({
+    name: 'categoriesContext',
+    required: false,
+    description: `Actor context to execute the command in (${CATEGORIES_CONTEXTS.join('/')})`,
+    options: [...CATEGORIES_CONTEXTS],
+  })
+
   async promptForContext(message = 'Choose in which context you wish to execute the command'): Promise<Context> {
     return this.simplePrompt({
       message,
@@ -51,6 +60,16 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
     })
   }
 
+  async promptForCategoriesContext(
+    message = 'Choose in which context you wish to execute the command'
+  ): Promise<CategoriesContext> {
+    return this.simplePrompt({
+      message,
+      type: 'list',
+      choices: CATEGORIES_CONTEXTS.map((c) => ({ name: c, value: c })),
+    })
+  }
+
   // Use when lead access is required in given command
   async requireLead(): Promise<void> {
     await this.getRequiredLead()

+ 54 - 0
cli/src/commands/content/createChannelCategory.ts

@@ -0,0 +1,54 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+import { IOFlags, getInputJson } from '../../helpers/InputOutput'
+import { ChannelCategoryCreationParameters, ChannelCategoryCreationParametersInput } from '../../Types'
+import { channelCategoryMetadataFromInput } from '../../helpers/serialization'
+
+export default class CreateChannelCategoryCommand extends ContentDirectoryCommandBase {
+  static description = 'Create channel category inside content directory.'
+  static flags = {
+    context: ContentDirectoryCommandBase.categoriesContextFlag,
+    input: IOFlags.input,
+  }
+
+  async run() {
+    let { context, input } = this.parse(CreateChannelCategoryCommand).flags
+
+    if (!context) {
+      context = await this.promptForCategoriesContext()
+    }
+
+    const currentAccount = await this.getRequiredSelectedAccount()
+    await this.requestAccountDecoding(currentAccount)
+
+    const actor = await this.getActor(context)
+
+    if (input) {
+      const channelCategoryCreationParametersInput = await getInputJson<ChannelCategoryCreationParametersInput>(input)
+
+      const api = await this.getOriginalApi()
+
+      const meta = channelCategoryMetadataFromInput(api, channelCategoryCreationParametersInput)
+
+      const channelCategoryCreationParameters: ChannelCategoryCreationParameters = {
+        meta,
+      }
+
+      this.jsonPrettyPrint(JSON.stringify(channelCategoryCreationParametersInput))
+
+      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', 'createChannelCategory', [
+          actor,
+          channelCategoryCreationParameters,
+        ])
+      }
+    } else {
+      this.error('Input invalid or was not provided...')
+    }
+  }
+}

+ 54 - 0
cli/src/commands/content/createVideoCategory.ts

@@ -0,0 +1,54 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+import { IOFlags, getInputJson } from '../../helpers/InputOutput'
+import { VideoCategoryCreationParameters, VideoCategoryCreationParametersInput } from '../../Types'
+import { videoCategoryMetadataFromInput } from '../../helpers/serialization'
+
+export default class CreateVideoCategoryCommand extends ContentDirectoryCommandBase {
+  static description = 'Create video category inside content directory.'
+  static flags = {
+    context: ContentDirectoryCommandBase.categoriesContextFlag,
+    input: IOFlags.input,
+  }
+
+  async run() {
+    let { context, input } = this.parse(CreateVideoCategoryCommand).flags
+
+    if (!context) {
+      context = await this.promptForCategoriesContext()
+    }
+
+    const currentAccount = await this.getRequiredSelectedAccount()
+    await this.requestAccountDecoding(currentAccount)
+
+    const actor = await this.getActor(context)
+
+    if (input) {
+      const videoCategoryCreationParametersInput = await getInputJson<VideoCategoryCreationParametersInput>(input)
+
+      const api = await this.getOriginalApi()
+
+      const meta = videoCategoryMetadataFromInput(api, videoCategoryCreationParametersInput)
+
+      const videoCategoryCreationParameters: VideoCategoryCreationParameters = {
+        meta,
+      }
+
+      this.jsonPrettyPrint(JSON.stringify(videoCategoryCreationParametersInput))
+
+      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', 'createVideoCategory', [
+          actor,
+          videoCategoryCreationParameters,
+        ])
+      }
+    } else {
+      this.error('Input invalid or was not provided...')
+    }
+  }
+}

+ 39 - 0
cli/src/commands/content/deleteChannelCategory.ts

@@ -0,0 +1,39 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+
+export default class DeleteChannelCategoryCommand extends ContentDirectoryCommandBase {
+  static description = 'Delete channel category.'
+  static flags = {
+    context: ContentDirectoryCommandBase.categoriesContextFlag,
+  }
+
+  static args = [
+    {
+      name: 'channelCategoryId',
+      required: true,
+      description: 'ID of the Channel Category',
+    },
+  ]
+
+  async run() {
+    let { context } = this.parse(DeleteChannelCategoryCommand).flags
+
+    const { channelCategoryId } = this.parse(DeleteChannelCategoryCommand).args
+
+    const channelCategory = await this.getApi().channelCategoryById(channelCategoryId)
+
+    if (channelCategory) {
+      if (!context) {
+        context = await this.promptForCategoriesContext()
+      }
+
+      const currentAccount = await this.getRequiredSelectedAccount()
+      await this.requestAccountDecoding(currentAccount)
+
+      const actor = await this.getActor(context)
+
+      await this.sendAndFollowNamedTx(currentAccount, 'content', 'deleteChannelCategory', [actor, channelCategoryId])
+    } else {
+      this.error('Channel category under given id does not exist...')
+    }
+  }
+}

+ 39 - 0
cli/src/commands/content/deleteVideoCategory.ts

@@ -0,0 +1,39 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+
+export default class DeleteVideoCategoryCommand extends ContentDirectoryCommandBase {
+  static description = 'Delete video category.'
+  static flags = {
+    context: ContentDirectoryCommandBase.categoriesContextFlag,
+  }
+
+  static args = [
+    {
+      name: 'videoCategoryId',
+      required: true,
+      description: 'ID of the Video Category',
+    },
+  ]
+
+  async run() {
+    let { context } = this.parse(DeleteVideoCategoryCommand).flags
+
+    const { videoCategoryId } = this.parse(DeleteVideoCategoryCommand).args
+
+    const videoCategory = await this.getApi().videoCategoryById(videoCategoryId)
+
+    if (videoCategory) {
+      if (!context) {
+        context = await this.promptForCategoriesContext()
+      }
+
+      const currentAccount = await this.getRequiredSelectedAccount()
+      await this.requestAccountDecoding(currentAccount)
+
+      const actor = await this.getActor(context)
+
+      await this.sendAndFollowNamedTx(currentAccount, 'content', 'deleteVideoCategory', [actor, videoCategoryId])
+    } else {
+      this.error('Video category under given id does not exist...')
+    }
+  }
+}

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

@@ -0,0 +1,65 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+import { IOFlags, getInputJson } from '../../helpers/InputOutput'
+import { ChannelCategoryUpdateParameters, ChannelCategoryUpdateParametersInput } from '../../Types'
+import { channelCategoryMetadataFromInput } from '../../helpers/serialization'
+
+export default class UpdateChannelCategoryCommand extends ContentDirectoryCommandBase {
+  static description = 'Update channel category inside content directory.'
+  static flags = {
+    context: ContentDirectoryCommandBase.categoriesContextFlag,
+    input: IOFlags.input,
+  }
+
+  static args = [
+    {
+      name: 'channelCategoryId',
+      required: true,
+      description: 'ID of the Channel Category',
+    },
+  ]
+
+  async run() {
+    let { context, input } = this.parse(UpdateChannelCategoryCommand).flags
+
+    const { channelCategoryId } = this.parse(UpdateChannelCategoryCommand).args
+
+    if (!context) {
+      context = await this.promptForCategoriesContext()
+    }
+
+    const currentAccount = await this.getRequiredSelectedAccount()
+    await this.requestAccountDecoding(currentAccount)
+
+    const actor = await this.getActor(context)
+
+    if (input) {
+      const channelCategoryUpdateParametersInput = await getInputJson<ChannelCategoryUpdateParametersInput>(input)
+
+      const api = await this.getOriginalApi()
+
+      const meta = channelCategoryMetadataFromInput(api, channelCategoryUpdateParametersInput)
+
+      const channelCategoryUpdateParameters: ChannelCategoryUpdateParameters = {
+        new_meta: meta,
+      }
+
+      this.jsonPrettyPrint(JSON.stringify(channelCategoryUpdateParametersInput))
+
+      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', 'updateChannelCategory', [
+          actor,
+          channelCategoryId,
+          channelCategoryUpdateParameters,
+        ])
+      }
+    } else {
+      this.error('Input invalid or was not provided...')
+    }
+  }
+}

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

@@ -0,0 +1,65 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+import { IOFlags, getInputJson } from '../../helpers/InputOutput'
+import { VideoCategoryUpdateParameters, VideoCategoryUpdateParametersInput } from '../../Types'
+import { videoCategoryMetadataFromInput } from '../../helpers/serialization'
+
+export default class UpdateVideoCategoryCommand extends ContentDirectoryCommandBase {
+  static description = 'Update video category inside content directory.'
+  static flags = {
+    context: ContentDirectoryCommandBase.categoriesContextFlag,
+    input: IOFlags.input,
+  }
+
+  static args = [
+    {
+      name: 'videoCategoryId',
+      required: true,
+      description: 'ID of the Video Category',
+    },
+  ]
+
+  async run() {
+    let { context, input } = this.parse(UpdateVideoCategoryCommand).flags
+
+    const { videoCategoryId } = this.parse(UpdateVideoCategoryCommand).args
+
+    if (!context) {
+      context = await this.promptForCategoriesContext()
+    }
+
+    const currentAccount = await this.getRequiredSelectedAccount()
+    await this.requestAccountDecoding(currentAccount)
+
+    const actor = await this.getActor(context)
+
+    if (input) {
+      const videoCategoryUpdateParametersInput = await getInputJson<VideoCategoryUpdateParametersInput>(input)
+
+      const api = await this.getOriginalApi()
+
+      const meta = videoCategoryMetadataFromInput(api, videoCategoryUpdateParametersInput)
+
+      const videoCategoryUpdateParameters: VideoCategoryUpdateParameters = {
+        new_meta: meta,
+      }
+
+      this.jsonPrettyPrint(JSON.stringify(videoCategoryUpdateParameters))
+
+      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', 'updateVideoCategory', [
+          actor,
+          videoCategoryId,
+          videoCategoryUpdateParameters,
+        ])
+      }
+    } else {
+      this.error('Input invalid or was not provided...')
+    }
+  }
+}

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

@@ -4,12 +4,18 @@ import {
   License,
   MediaType,
   ChannelMetadata,
+  ChannelCategoryMetadata,
+  VideoCategoryMetadata,
 } from '@joystream/content-metadata-protobuf'
 import {
   VideoUpdateParametersInput,
   VideoCreationParametersInput,
   ChannelUpdateParametersInput,
   ChannelCreationParametersInput,
+  ChannelCategoryCreationParametersInput,
+  ChannelCategoryUpdateParametersInput,
+  VideoCategoryCreationParametersInput,
+  VideoCategoryUpdateParametersInput,
 } from '../Types'
 import { ApiPromise } from '@polkadot/api'
 import { Bytes } from '@polkadot/types/primitive'
@@ -75,3 +81,25 @@ export function channelMetadataFromInput(
   const serialized = channelMetadata.serializeBinary()
   return binaryToMeta(api, serialized)
 }
+
+export function channelCategoryMetadataFromInput(
+  api: ApiPromise,
+  channelCategoryParametersInput: ChannelCategoryCreationParametersInput | ChannelCategoryUpdateParametersInput
+): Bytes {
+  const channelCategoryMetadata = new ChannelCategoryMetadata()
+  channelCategoryMetadata.setName(channelCategoryParametersInput.meta.name!)
+
+  const serialized = channelCategoryMetadata.serializeBinary()
+  return binaryToMeta(api, serialized)
+}
+
+export function videoCategoryMetadataFromInput(
+  api: ApiPromise,
+  videoCategoryParametersInput: VideoCategoryCreationParametersInput | VideoCategoryUpdateParametersInput
+): Bytes {
+  const videoCategoryMetadata = new VideoCategoryMetadata()
+  videoCategoryMetadata.setName(videoCategoryParametersInput.meta.name!)
+
+  const serialized = videoCategoryMetadata.serializeBinary()
+  return binaryToMeta(api, serialized)
+}