ContentDirectoryCommandBase.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. import ExitCodes from '../ExitCodes'
  2. import { WorkingGroups } from '../Types'
  3. import { CuratorGroup, CuratorGroupId, ContentActor, Channel } from '@joystream/types/content'
  4. import { Worker } from '@joystream/types/working-group'
  5. import { CLIError } from '@oclif/errors'
  6. import { flags } from '@oclif/command'
  7. import { memberHandle } from '../helpers/display'
  8. import { MemberId } from '@joystream/types/common'
  9. import { createType } from '@joystream/types'
  10. import WorkingGroupCommandBase from './WorkingGroupCommandBase'
  11. const CHANNEL_CREATION_CONTEXTS = ['Member', 'Curator'] as const
  12. const CATEGORIES_CONTEXTS = ['Lead', 'Curator'] as const
  13. const CHANNEL_MANAGEMENT_CONTEXTS = ['Owner', 'Collaborator'] as const
  14. type ChannelManagementContext = typeof CHANNEL_MANAGEMENT_CONTEXTS[number]
  15. type ChannelCreationContext = typeof CHANNEL_CREATION_CONTEXTS[number]
  16. type CategoriesContext = typeof CATEGORIES_CONTEXTS[number]
  17. /**
  18. * Abstract base class for commands related to content directory
  19. */
  20. export default abstract class ContentDirectoryCommandBase extends WorkingGroupCommandBase {
  21. static flags = {
  22. ...WorkingGroupCommandBase.flags,
  23. }
  24. static channelCreationContextFlag = flags.enum({
  25. required: false,
  26. description: `Actor context to execute the command in (${CHANNEL_CREATION_CONTEXTS.join('/')})`,
  27. options: [...CHANNEL_CREATION_CONTEXTS],
  28. })
  29. static channelManagementContextFlag = flags.enum({
  30. required: false,
  31. description: `Actor context to execute the command in (${CHANNEL_MANAGEMENT_CONTEXTS.join('/')})`,
  32. options: [...CHANNEL_MANAGEMENT_CONTEXTS],
  33. })
  34. static categoriesContextFlag = flags.enum({
  35. required: false,
  36. description: `Actor context to execute the command in (${CATEGORIES_CONTEXTS.join('/')})`,
  37. options: [...CATEGORIES_CONTEXTS],
  38. })
  39. async init(): Promise<void> {
  40. await super.init()
  41. this._group = WorkingGroups.Curators // override group for RolesCommandBase
  42. }
  43. async promptForChannelCreationContext(
  44. message = 'Choose in which context you wish to execute the command'
  45. ): Promise<ChannelCreationContext> {
  46. return this.simplePrompt({
  47. message,
  48. type: 'list',
  49. choices: CHANNEL_CREATION_CONTEXTS.map((c) => ({ name: c, value: c })),
  50. })
  51. }
  52. async promptForCategoriesContext(
  53. message = 'Choose in which context you wish to execute the command'
  54. ): Promise<CategoriesContext> {
  55. return this.simplePrompt({
  56. message,
  57. type: 'list',
  58. choices: CATEGORIES_CONTEXTS.map((c) => ({ name: c, value: c })),
  59. })
  60. }
  61. // Use when lead access is required in given command
  62. async requireLead(): Promise<void> {
  63. await this.getRequiredLeadContext()
  64. }
  65. getCurationActorByChannel(channel: Channel): Promise<[ContentActor, string]> {
  66. return channel.owner.isOfType('Curators') ? this.getContentActor('Lead') : this.getContentActor('Curator')
  67. }
  68. async getChannelOwnerActor(channel: Channel): Promise<[ContentActor, string]> {
  69. if (channel.owner.isOfType('Curators')) {
  70. try {
  71. return this.getContentActor('Lead')
  72. } catch (e) {
  73. return this.getCuratorContext(channel.owner.asType('Curators'))
  74. }
  75. } else {
  76. const { id, membership } = await this.getRequiredMemberContext(false, [channel.owner.asType('Member')])
  77. return [
  78. createType<ContentActor, 'ContentActor'>('ContentActor', { Member: id }),
  79. membership.controller_account.toString(),
  80. ]
  81. }
  82. }
  83. async getChannelCollaboratorActor(channel: Channel): Promise<[ContentActor, string]> {
  84. const { id, membership } = await this.getRequiredMemberContext(false, Array.from(channel.collaborators))
  85. return [
  86. createType<ContentActor, 'ContentActor'>('ContentActor', { Member: id }),
  87. membership.controller_account.toString(),
  88. ]
  89. }
  90. isChannelOwner(channel: Channel, actor: ContentActor): boolean {
  91. return channel.owner.isOfType('Curators')
  92. ? (actor.isOfType('Curator') && actor.asType('Curator')[0].eq(channel.owner.asType('Curators'))) ||
  93. actor.isOfType('Lead')
  94. : actor.isOfType('Member') && actor.asType('Member').eq(channel.owner.asType('Member'))
  95. }
  96. async getChannelManagementActor(
  97. channel: Channel,
  98. context: ChannelManagementContext
  99. ): Promise<[ContentActor, string]> {
  100. if (context && context === 'Owner') {
  101. return this.getChannelOwnerActor(channel)
  102. }
  103. if (context && context === 'Collaborator') {
  104. return this.getChannelCollaboratorActor(channel)
  105. }
  106. // Context not set - derive
  107. try {
  108. const owner = await this.getChannelOwnerActor(channel)
  109. this.log('Derived context: Channel owner')
  110. return owner
  111. } catch (e) {
  112. // continue
  113. }
  114. try {
  115. const collaborator = await this.getChannelCollaboratorActor(channel)
  116. this.log('Derived context: Channel collaborator')
  117. return collaborator
  118. } catch (e) {
  119. // continue
  120. }
  121. this.error('No account found with access to manage the provided channel', { exit: ExitCodes.AccessDenied })
  122. }
  123. async getCategoryManagementActor(): Promise<[ContentActor, string]> {
  124. try {
  125. const lead = await this.getContentActor('Lead')
  126. this.log('Derived context: Lead')
  127. return lead
  128. } catch (e) {
  129. // continue
  130. }
  131. try {
  132. const curator = await this.getContentActor('Curator')
  133. this.log('Derived context: Curator')
  134. return curator
  135. } catch (e) {
  136. // continue
  137. }
  138. this.error('Lead / Curator Group member permissions are required for this action', { exit: ExitCodes.AccessDenied })
  139. }
  140. async getCuratorContext(requiredGroupId?: CuratorGroupId): Promise<[ContentActor, string]> {
  141. const curator = await this.getRequiredWorkerContext()
  142. let groupId: number
  143. if (requiredGroupId) {
  144. const group = await this.getCuratorGroup(requiredGroupId.toNumber())
  145. if (!group.active.valueOf()) {
  146. this.error(`Curator group ${requiredGroupId.toString()} is no longer active`, { exit: ExitCodes.AccessDenied })
  147. }
  148. if (!Array.from(group.curators).some((curatorId) => curatorId.eq(curator.workerId))) {
  149. this.error(`You don't belong to required curator group (ID: ${requiredGroupId.toString()})`, {
  150. exit: ExitCodes.AccessDenied,
  151. })
  152. }
  153. groupId = requiredGroupId.toNumber()
  154. } else {
  155. const groups = await this.getApi().availableCuratorGroups()
  156. const availableGroupIds = groups
  157. .filter(
  158. ([, group]) =>
  159. group.active.valueOf() && Array.from(group.curators).some((curatorId) => curatorId.eq(curator.workerId))
  160. )
  161. .map(([id]) => id)
  162. if (!availableGroupIds.length) {
  163. this.error("You don't belong to any active curator group!", { exit: ExitCodes.AccessDenied })
  164. } else if (availableGroupIds.length === 1) {
  165. groupId = availableGroupIds[0].toNumber()
  166. } else {
  167. groupId = await this.promptForCuratorGroup('Select Curator Group context', availableGroupIds)
  168. }
  169. }
  170. return [
  171. createType<ContentActor, 'ContentActor'>('ContentActor', { Curator: [groupId, curator.workerId.toNumber()] }),
  172. curator.roleAccount.toString(),
  173. ]
  174. }
  175. private async curatorGroupChoices(ids?: CuratorGroupId[]) {
  176. const groups = await this.getApi().availableCuratorGroups()
  177. return groups
  178. .filter(([id]) => (ids ? ids.some((allowedId) => allowedId.eq(id)) : true))
  179. .map(([id, group]) => ({
  180. name:
  181. `Group ${id.toString()} (` +
  182. `${group.active.valueOf() ? 'Active' : 'Inactive'}, ` +
  183. `${Array.from(group.curators).length} member(s)), `,
  184. value: id.toNumber(),
  185. }))
  186. }
  187. async promptForCuratorGroup(message = 'Select a Curator Group', ids?: CuratorGroupId[]): Promise<number> {
  188. const choices = await this.curatorGroupChoices(ids)
  189. if (!choices.length) {
  190. this.warn('No Curator Groups to choose from!')
  191. this.exit(ExitCodes.InvalidInput)
  192. }
  193. const selectedId = await this.simplePrompt<number>({ message, type: 'list', choices })
  194. return selectedId
  195. }
  196. async promptForCuratorGroups(message = 'Select Curator Groups'): Promise<number[]> {
  197. const choices = await this.curatorGroupChoices()
  198. if (!choices.length) {
  199. return []
  200. }
  201. const selectedIds = await this.simplePrompt<number[]>({ message, type: 'checkbox', choices })
  202. return selectedIds
  203. }
  204. async promptForCurator(message = 'Choose a Curator', ids?: number[]): Promise<number> {
  205. const curators = await this.getApi().groupMembers(WorkingGroups.Curators)
  206. const choices = curators
  207. .filter((c) => (ids ? ids.includes(c.workerId.toNumber()) : true))
  208. .map((c) => ({
  209. name: `${memberHandle(c.profile)} (Worker ID: ${c.workerId})`,
  210. value: c.workerId.toNumber(),
  211. }))
  212. if (!choices.length) {
  213. this.warn('No Curators to choose from!')
  214. this.exit(ExitCodes.InvalidInput)
  215. }
  216. const selectedCuratorId = await this.simplePrompt<number>({
  217. message,
  218. type: 'list',
  219. choices,
  220. })
  221. return selectedCuratorId
  222. }
  223. async getCurator(id: string | number): Promise<Worker> {
  224. if (typeof id === 'string') {
  225. id = parseInt(id)
  226. }
  227. let curator
  228. try {
  229. curator = await this.getApi().workerByWorkerId(WorkingGroups.Curators, id)
  230. } catch (e) {
  231. if (e instanceof CLIError) {
  232. throw new CLIError('Invalid Curator id!')
  233. }
  234. throw e
  235. }
  236. return curator
  237. }
  238. async getCuratorGroup(id: string | number): Promise<CuratorGroup> {
  239. if (typeof id === 'string') {
  240. id = parseInt(id)
  241. }
  242. const group = await this.getApi().curatorGroupById(id)
  243. if (!group) {
  244. this.error('Invalid Curator Group id!', { exit: ExitCodes.InvalidInput })
  245. }
  246. return group
  247. }
  248. async getContentActor(
  249. context: Exclude<keyof typeof ContentActor.typeDefinitions, 'Collaborator'>
  250. ): Promise<[ContentActor, string]> {
  251. if (context === 'Member') {
  252. const { id, membership } = await this.getRequiredMemberContext()
  253. return [
  254. createType<ContentActor, 'ContentActor'>('ContentActor', { Member: id }),
  255. membership.controller_account.toString(),
  256. ]
  257. }
  258. if (context === 'Curator') {
  259. return this.getCuratorContext()
  260. }
  261. if (context === 'Lead') {
  262. const lead = await this.getRequiredLeadContext()
  263. return [createType<ContentActor, 'ContentActor'>('ContentActor', { Lead: null }), lead.roleAccount.toString()]
  264. }
  265. throw new Error(`Unrecognized context: ${context}`)
  266. }
  267. async validateMemberIdsSet(ids: number[] | MemberId[], setName: 'collaborator' | 'moderator'): Promise<void> {
  268. const members = await this.getApi().getMembers(ids)
  269. if (members.length < ids.length || members.some((m) => m.isEmpty)) {
  270. this.error(`Invalid ${setName} set! All ${setName} set members must be existing members!`, {
  271. exit: ExitCodes.InvalidInput,
  272. })
  273. }
  274. }
  275. }