updateChannel.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import { getInputJson } from '../../helpers/InputOutput'
  2. import { asValidatedMetadata, metadataToBytes } from '../../helpers/serialization'
  3. import { ChannelInputParameters } from '../../Types'
  4. import { flags } from '@oclif/command'
  5. import UploadCommandBase from '../../base/UploadCommandBase'
  6. import { CreateInterface, createType } from '@joystream/types'
  7. import { ChannelUpdateParameters } from '@joystream/types/content'
  8. import { ChannelInputSchema } from '../../schemas/ContentDirectory'
  9. import { ChannelMetadata } from '@joystream/metadata-protobuf'
  10. import { DataObjectInfoFragment } from '../../graphql/generated/queries'
  11. import BN from 'bn.js'
  12. import { formatBalance } from '@polkadot/util'
  13. import chalk from 'chalk'
  14. import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
  15. import ExitCodes from '../../ExitCodes'
  16. export default class UpdateChannelCommand extends UploadCommandBase {
  17. static description = 'Update existing content directory channel.'
  18. static flags = {
  19. context: ContentDirectoryCommandBase.channelManagementContextFlag,
  20. input: flags.string({
  21. char: 'i',
  22. required: true,
  23. description: `Path to JSON file to use as input`,
  24. }),
  25. }
  26. static args = [
  27. {
  28. name: 'channelId',
  29. required: true,
  30. description: 'ID of the Channel',
  31. },
  32. ]
  33. parseRewardAccountInput(rewardAccount?: string | null): string | null | Uint8Array {
  34. if (rewardAccount === undefined) {
  35. // Reward account remains unchanged
  36. return null
  37. } else if (rewardAccount === null) {
  38. // Reward account changed to empty
  39. return new Uint8Array([1, 0])
  40. } else {
  41. // Reward account set to new account
  42. return rewardAccount
  43. }
  44. }
  45. async getAssetsToRemove(
  46. channelId: number,
  47. coverPhotoIndex: number | undefined,
  48. avatarPhotoIndex: number | undefined
  49. ): Promise<string[]> {
  50. let assetsToRemove: DataObjectInfoFragment[] = []
  51. if (coverPhotoIndex !== undefined || avatarPhotoIndex !== undefined) {
  52. const currentAssets = await this.getQNApi().dataObjectsByChannelId(channelId.toString())
  53. const currentCovers = currentAssets.filter((a) => a.type.__typename === 'DataObjectTypeChannelCoverPhoto')
  54. const currentAvatars = currentAssets.filter((a) => a.type.__typename === 'DataObjectTypeChannelAvatar')
  55. if (currentCovers.length && coverPhotoIndex !== undefined) {
  56. assetsToRemove = assetsToRemove.concat(currentCovers)
  57. }
  58. if (currentAvatars.length && avatarPhotoIndex !== undefined) {
  59. assetsToRemove = assetsToRemove.concat(currentAvatars)
  60. }
  61. if (assetsToRemove.length) {
  62. this.log(`\nData objects to be removed due to replacement:`)
  63. assetsToRemove.forEach((a) => this.log(`- ${a.id} (${a.type.__typename})`))
  64. const totalPrize = assetsToRemove.reduce((sum, { deletionPrize }) => sum.add(new BN(deletionPrize)), new BN(0))
  65. this.log(`Total deletion prize: ${chalk.cyanBright(formatBalance(totalPrize))}\n`)
  66. }
  67. }
  68. return assetsToRemove.map((a) => a.id)
  69. }
  70. async run(): Promise<void> {
  71. const {
  72. flags: { input, context },
  73. args: { channelId },
  74. } = this.parse(UpdateChannelCommand)
  75. // Context
  76. const channel = await this.getApi().channelById(channelId)
  77. const [actor, address] = await this.getChannelManagementActor(channel, context)
  78. const [memberId] = await this.getRequiredMemberContext(true)
  79. const keypair = await this.getDecodedPair(address)
  80. const channelInput = await getInputJson<ChannelInputParameters>(input, ChannelInputSchema)
  81. const meta = asValidatedMetadata(ChannelMetadata, channelInput)
  82. if (channelInput.rewardAccount !== undefined && actor.type === 'Collaborator') {
  83. this.error("Collaborators are not allowed to update channel's reward account!", { exit: ExitCodes.AccessDenied })
  84. }
  85. if (channelInput.collaborators !== undefined && actor.type === 'Collaborator') {
  86. this.error("Collaborators are not allowed to update channel's collaborators!", { exit: ExitCodes.AccessDenied })
  87. }
  88. if (channelInput.collaborators) {
  89. await this.validateCollaborators(channelInput.collaborators)
  90. }
  91. const { coverPhotoPath, avatarPhotoPath, rewardAccount } = channelInput
  92. const [resolvedAssets, assetIndices] = await this.resolveAndValidateAssets(
  93. { coverPhotoPath, avatarPhotoPath },
  94. input
  95. )
  96. // Set assets indices in the metadata
  97. // "undefined" values will be omitted when the metadata is encoded. It's not possible to "unset" an asset this way.
  98. meta.coverPhoto = assetIndices.coverPhotoPath
  99. meta.avatarPhoto = assetIndices.avatarPhotoPath
  100. // Preare and send the extrinsic
  101. const assetsToUpload = await this.prepareAssetsForExtrinsic(resolvedAssets)
  102. const assetsToRemove = await this.getAssetsToRemove(
  103. channelId,
  104. assetIndices.coverPhotoPath,
  105. assetIndices.avatarPhotoPath
  106. )
  107. const collaborators = createType('Option<BTreeSet<MemberId>>', channelInput.collaborators)
  108. const channelUpdateParameters: CreateInterface<ChannelUpdateParameters> = {
  109. assets_to_upload: assetsToUpload,
  110. assets_to_remove: createType('BTreeSet<DataObjectId>', assetsToRemove),
  111. new_meta: metadataToBytes(ChannelMetadata, meta),
  112. reward_account: this.parseRewardAccountInput(rewardAccount),
  113. collaborators,
  114. }
  115. this.jsonPrettyPrint(
  116. JSON.stringify({ assetsToUpload: assetsToUpload?.toJSON(), assetsToRemove, metadata: meta, rewardAccount })
  117. )
  118. await this.requireConfirmation('Do you confirm the provided input?', true)
  119. const result = await this.sendAndFollowNamedTx(keypair, 'content', 'updateChannel', [
  120. actor,
  121. channelId,
  122. channelUpdateParameters,
  123. ])
  124. const dataObjectsUploadedEvent = this.findEvent(result, 'storage', 'DataObjectsUploaded')
  125. if (dataObjectsUploadedEvent) {
  126. const [objectIds] = dataObjectsUploadedEvent.data
  127. await this.uploadAssets(
  128. keypair,
  129. memberId.toNumber(),
  130. `dynamic:channel:${channelId.toString()}`,
  131. objectIds.map((id, index) => ({ dataObjectId: id, path: resolvedAssets[index].path })),
  132. input
  133. )
  134. }
  135. }
  136. }