ContentDirectoryCommandBase.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  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 { RolesCommandBase } from './WorkingGroupsCommandBase'
  7. import { createTypeFromConstructor } from '@joystream/types'
  8. import { flags } from '@oclif/command'
  9. // TODO: Rework the contexts
  10. const CONTEXTS = ['Member', 'Curator', 'Lead'] as const
  11. const OWNER_CONTEXTS = ['Member', 'Curator'] as const
  12. const CATEGORIES_CONTEXTS = ['Lead', 'Curator'] as const
  13. type Context = typeof CONTEXTS[number]
  14. type OwnerContext = typeof OWNER_CONTEXTS[number]
  15. type CategoriesContext = typeof CATEGORIES_CONTEXTS[number]
  16. /**
  17. * Abstract base class for commands related to content directory
  18. */
  19. export default abstract class ContentDirectoryCommandBase extends RolesCommandBase {
  20. group = WorkingGroups.Curators // override group for RolesCommandBase
  21. static contextFlag = flags.enum({
  22. name: 'context',
  23. required: false,
  24. description: `Actor context to execute the command in (${CONTEXTS.join('/')})`,
  25. options: [...CONTEXTS],
  26. })
  27. static ownerContextFlag = flags.enum({
  28. name: 'ownerContext',
  29. required: false,
  30. description: `Actor context to execute the command in (${OWNER_CONTEXTS.join('/')})`,
  31. options: [...OWNER_CONTEXTS],
  32. })
  33. static categoriesContextFlag = flags.enum({
  34. name: 'categoriesContext',
  35. required: false,
  36. description: `Actor context to execute the command in (${CATEGORIES_CONTEXTS.join('/')})`,
  37. options: [...CATEGORIES_CONTEXTS],
  38. })
  39. async promptForContext(message = 'Choose in which context you wish to execute the command'): Promise<Context> {
  40. return this.simplePrompt({
  41. message,
  42. type: 'list',
  43. choices: CONTEXTS.map((c) => ({ name: c, value: c })),
  44. })
  45. }
  46. async promptForOwnerContext(
  47. message = 'Choose in which context you wish to execute the command'
  48. ): Promise<OwnerContext> {
  49. return this.simplePrompt({
  50. message,
  51. type: 'list',
  52. choices: OWNER_CONTEXTS.map((c) => ({ name: c, value: c })),
  53. })
  54. }
  55. async promptForCategoriesContext(
  56. message = 'Choose in which context you wish to execute the command'
  57. ): Promise<CategoriesContext> {
  58. return this.simplePrompt({
  59. message,
  60. type: 'list',
  61. choices: CATEGORIES_CONTEXTS.map((c) => ({ name: c, value: c })),
  62. })
  63. }
  64. // Use when lead access is required in given command
  65. async requireLead(): Promise<void> {
  66. await this.getRequiredLead()
  67. }
  68. async getCurationActorByChannel(channel: Channel): Promise<ContentActor> {
  69. return channel.owner.isOfType('Curators') ? await this.getActor('Lead') : await this.getActor('Curator')
  70. }
  71. async getChannelOwnerActor(channel: Channel): Promise<ContentActor> {
  72. if (channel.owner.isOfType('Curators')) {
  73. try {
  74. return await this.getActor('Lead')
  75. } catch (e) {
  76. return await this.getCuratorContext(channel.owner.asType('Curators'))
  77. }
  78. } else {
  79. return await this.getActor('Member')
  80. }
  81. }
  82. async getCategoryManagementActor(): Promise<ContentActor> {
  83. try {
  84. return await this.getActor('Lead')
  85. } catch (e) {
  86. return await this.getActor('Curator')
  87. }
  88. }
  89. async getCuratorContext(requiredGroupId?: CuratorGroupId): Promise<ContentActor> {
  90. const curator = await this.getRequiredWorker()
  91. let groupId: number
  92. if (requiredGroupId) {
  93. const group = await this.getCuratorGroup(requiredGroupId.toNumber())
  94. if (!group.active.valueOf()) {
  95. this.error(`Curator group ${requiredGroupId.toString()} is no longer active`, { exit: ExitCodes.AccessDenied })
  96. }
  97. if (!Array.from(group.curators).some((curatorId) => curatorId.eq(curator.workerId))) {
  98. this.error(`You don't belong to required curator group (ID: ${requiredGroupId.toString()})`, {
  99. exit: ExitCodes.AccessDenied,
  100. })
  101. }
  102. groupId = requiredGroupId.toNumber()
  103. } else {
  104. const groups = await this.getApi().availableCuratorGroups()
  105. const availableGroupIds = groups
  106. .filter(
  107. ([, group]) =>
  108. group.active.valueOf() && Array.from(group.curators).some((curatorId) => curatorId.eq(curator.workerId))
  109. )
  110. .map(([id]) => id)
  111. if (!availableGroupIds.length) {
  112. this.error("You don't belong to any active curator group!", { exit: ExitCodes.AccessDenied })
  113. } else if (availableGroupIds.length === 1) {
  114. groupId = availableGroupIds[0].toNumber()
  115. } else {
  116. groupId = await this.promptForCuratorGroup('Select Curator Group context', availableGroupIds)
  117. }
  118. }
  119. return createTypeFromConstructor(ContentActor, { Curator: [groupId, curator.workerId.toNumber()] })
  120. }
  121. private async curatorGroupChoices(ids?: CuratorGroupId[]) {
  122. const groups = await this.getApi().availableCuratorGroups()
  123. return groups
  124. .filter(([id]) => (ids ? ids.some((allowedId) => allowedId.eq(id)) : true))
  125. .map(([id, group]) => ({
  126. name:
  127. `Group ${id.toString()} (` +
  128. `${group.active.valueOf() ? 'Active' : 'Inactive'}, ` +
  129. `${Array.from(group.curators).length} member(s)), `,
  130. value: id.toNumber(),
  131. }))
  132. }
  133. async promptForCuratorGroup(message = 'Select a Curator Group', ids?: CuratorGroupId[]): Promise<number> {
  134. const choices = await this.curatorGroupChoices(ids)
  135. if (!choices.length) {
  136. this.warn('No Curator Groups to choose from!')
  137. this.exit(ExitCodes.InvalidInput)
  138. }
  139. const selectedId = await this.simplePrompt({ message, type: 'list', choices })
  140. return selectedId
  141. }
  142. async promptForCuratorGroups(message = 'Select Curator Groups'): Promise<number[]> {
  143. const choices = await this.curatorGroupChoices()
  144. if (!choices.length) {
  145. return []
  146. }
  147. const selectedIds = await this.simplePrompt({ message, type: 'checkbox', choices })
  148. return selectedIds
  149. }
  150. async promptForCurator(message = 'Choose a Curator', ids?: number[]): Promise<number> {
  151. const curators = await this.getApi().groupMembers(WorkingGroups.Curators)
  152. const choices = curators
  153. .filter((c) => (ids ? ids.includes(c.workerId.toNumber()) : true))
  154. .map((c) => ({
  155. name: `${c.profile.handle.toString()} (Worker ID: ${c.workerId})`,
  156. value: c.workerId.toNumber(),
  157. }))
  158. if (!choices.length) {
  159. this.warn('No Curators to choose from!')
  160. this.exit(ExitCodes.InvalidInput)
  161. }
  162. const selectedCuratorId = await this.simplePrompt({
  163. message,
  164. type: 'list',
  165. choices,
  166. })
  167. return selectedCuratorId
  168. }
  169. async getCurator(id: string | number): Promise<Worker> {
  170. if (typeof id === 'string') {
  171. id = parseInt(id)
  172. }
  173. let curator
  174. try {
  175. curator = await this.getApi().workerByWorkerId(WorkingGroups.Curators, id)
  176. } catch (e) {
  177. if (e instanceof CLIError) {
  178. throw new CLIError('Invalid Curator id!')
  179. }
  180. throw e
  181. }
  182. return curator
  183. }
  184. async getCuratorGroup(id: string | number): Promise<CuratorGroup> {
  185. if (typeof id === 'string') {
  186. id = parseInt(id)
  187. }
  188. const group = await this.getApi().curatorGroupById(id)
  189. if (!group) {
  190. this.error('Invalid Curator Group id!', { exit: ExitCodes.InvalidInput })
  191. }
  192. return group
  193. }
  194. async getActor(context: typeof CONTEXTS[number]) {
  195. let actor: ContentActor
  196. if (context === 'Member') {
  197. const memberId = await this.getRequiredMemberId()
  198. actor = this.createType('ContentActor', { Member: memberId })
  199. } else if (context === 'Curator') {
  200. actor = await this.getCuratorContext()
  201. } else {
  202. await this.getRequiredLead()
  203. actor = this.createType('ContentActor', { Lead: null })
  204. }
  205. return actor
  206. }
  207. }