123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 |
- import ExitCodes from '../ExitCodes'
- import { WorkingGroups } from '../Types'
- import { CuratorGroup, CuratorGroupId, ContentActor, Channel } from '@joystream/types/content'
- import { Worker } from '@joystream/types/working-group'
- import { CLIError } from '@oclif/errors'
- import { flags } from '@oclif/command'
- import { memberHandle } from '../helpers/display'
- import { MemberId } from '@joystream/types/common'
- import { createType } from '@joystream/types'
- import WorkingGroupCommandBase from './WorkingGroupCommandBase'
- const CHANNEL_CREATION_CONTEXTS = ['Member', 'Curator'] as const
- const CATEGORIES_CONTEXTS = ['Lead', 'Curator'] as const
- const CHANNEL_MANAGEMENT_CONTEXTS = ['Owner', 'Collaborator'] as const
- type ChannelManagementContext = typeof CHANNEL_MANAGEMENT_CONTEXTS[number]
- type ChannelCreationContext = typeof CHANNEL_CREATION_CONTEXTS[number]
- type CategoriesContext = typeof CATEGORIES_CONTEXTS[number]
- /**
- * Abstract base class for commands related to content directory
- */
- export default abstract class ContentDirectoryCommandBase extends WorkingGroupCommandBase {
- static flags = {
- ...WorkingGroupCommandBase.flags,
- }
- static channelCreationContextFlag = flags.enum({
- required: false,
- description: `Actor context to execute the command in (${CHANNEL_CREATION_CONTEXTS.join('/')})`,
- options: [...CHANNEL_CREATION_CONTEXTS],
- })
- static channelManagementContextFlag = flags.enum({
- required: false,
- description: `Actor context to execute the command in (${CHANNEL_MANAGEMENT_CONTEXTS.join('/')})`,
- options: [...CHANNEL_MANAGEMENT_CONTEXTS],
- })
- static categoriesContextFlag = flags.enum({
- required: false,
- description: `Actor context to execute the command in (${CATEGORIES_CONTEXTS.join('/')})`,
- options: [...CATEGORIES_CONTEXTS],
- })
- async init(): Promise<void> {
- await super.init()
- this._group = WorkingGroups.Curators // override group for RolesCommandBase
- }
- async promptForChannelCreationContext(
- message = 'Choose in which context you wish to execute the command'
- ): Promise<ChannelCreationContext> {
- return this.simplePrompt({
- message,
- type: 'list',
- choices: CHANNEL_CREATION_CONTEXTS.map((c) => ({ name: c, value: c })),
- })
- }
- 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.getRequiredLeadContext()
- }
- getCurationActorByChannel(channel: Channel): Promise<[ContentActor, string]> {
- return channel.owner.isOfType('Curators') ? this.getContentActor('Lead') : this.getContentActor('Curator')
- }
- async getChannelOwnerActor(channel: Channel): Promise<[ContentActor, string]> {
- if (channel.owner.isOfType('Curators')) {
- try {
- return this.getContentActor('Lead')
- } catch (e) {
- return this.getCuratorContext(channel.owner.asType('Curators'))
- }
- } else {
- const { id, membership } = await this.getRequiredMemberContext(false, [channel.owner.asType('Member')])
- return [
- createType<ContentActor, 'ContentActor'>('ContentActor', { Member: id }),
- membership.controller_account.toString(),
- ]
- }
- }
- async getChannelCollaboratorActor(channel: Channel): Promise<[ContentActor, string]> {
- const { id, membership } = await this.getRequiredMemberContext(false, Array.from(channel.collaborators))
- return [
- createType<ContentActor, 'ContentActor'>('ContentActor', { Member: id }),
- membership.controller_account.toString(),
- ]
- }
- isChannelOwner(channel: Channel, actor: ContentActor): boolean {
- return channel.owner.isOfType('Curators')
- ? (actor.isOfType('Curator') && actor.asType('Curator')[0].eq(channel.owner.asType('Curators'))) ||
- actor.isOfType('Lead')
- : actor.isOfType('Member') && actor.asType('Member').eq(channel.owner.asType('Member'))
- }
- async getChannelManagementActor(
- channel: Channel,
- context: ChannelManagementContext
- ): Promise<[ContentActor, string]> {
- if (context && context === 'Owner') {
- return this.getChannelOwnerActor(channel)
- }
- if (context && context === 'Collaborator') {
- return this.getChannelCollaboratorActor(channel)
- }
- // Context not set - derive
- try {
- const owner = await this.getChannelOwnerActor(channel)
- this.log('Derived context: Channel owner')
- return owner
- } catch (e) {
- // continue
- }
- try {
- const collaborator = await this.getChannelCollaboratorActor(channel)
- this.log('Derived context: Channel collaborator')
- return collaborator
- } catch (e) {
- // continue
- }
- this.error('No account found with access to manage the provided channel', { exit: ExitCodes.AccessDenied })
- }
- async getCategoryManagementActor(): Promise<[ContentActor, string]> {
- try {
- const lead = await this.getContentActor('Lead')
- this.log('Derived context: Lead')
- return lead
- } catch (e) {
- // continue
- }
- try {
- const curator = await this.getContentActor('Curator')
- this.log('Derived context: Curator')
- return curator
- } catch (e) {
- // continue
- }
- this.error('Lead / Curator Group member permissions are required for this action', { exit: ExitCodes.AccessDenied })
- }
- async getCuratorContext(requiredGroupId?: CuratorGroupId): Promise<[ContentActor, string]> {
- const curator = await this.getRequiredWorkerContext()
- let groupId: number
- if (requiredGroupId) {
- const group = await this.getCuratorGroup(requiredGroupId.toNumber())
- if (!group.active.valueOf()) {
- this.error(`Curator group ${requiredGroupId.toString()} is no longer active`, { exit: ExitCodes.AccessDenied })
- }
- if (!Array.from(group.curators).some((curatorId) => curatorId.eq(curator.workerId))) {
- this.error(`You don't belong to required curator group (ID: ${requiredGroupId.toString()})`, {
- exit: ExitCodes.AccessDenied,
- })
- }
- groupId = requiredGroupId.toNumber()
- } else {
- const groups = await this.getApi().availableCuratorGroups()
- const availableGroupIds = groups
- .filter(
- ([, group]) =>
- group.active.valueOf() && Array.from(group.curators).some((curatorId) => curatorId.eq(curator.workerId))
- )
- .map(([id]) => id)
- if (!availableGroupIds.length) {
- this.error("You don't belong to any active curator group!", { exit: ExitCodes.AccessDenied })
- } else if (availableGroupIds.length === 1) {
- groupId = availableGroupIds[0].toNumber()
- } else {
- groupId = await this.promptForCuratorGroup('Select Curator Group context', availableGroupIds)
- }
- }
- return [
- createType<ContentActor, 'ContentActor'>('ContentActor', { Curator: [groupId, curator.workerId.toNumber()] }),
- curator.roleAccount.toString(),
- ]
- }
- private async curatorGroupChoices(ids?: CuratorGroupId[]) {
- const groups = await this.getApi().availableCuratorGroups()
- return groups
- .filter(([id]) => (ids ? ids.some((allowedId) => allowedId.eq(id)) : true))
- .map(([id, group]) => ({
- name:
- `Group ${id.toString()} (` +
- `${group.active.valueOf() ? 'Active' : 'Inactive'}, ` +
- `${Array.from(group.curators).length} member(s)), `,
- value: id.toNumber(),
- }))
- }
- async promptForCuratorGroup(message = 'Select a Curator Group', ids?: CuratorGroupId[]): Promise<number> {
- const choices = await this.curatorGroupChoices(ids)
- if (!choices.length) {
- this.warn('No Curator Groups to choose from!')
- this.exit(ExitCodes.InvalidInput)
- }
- const selectedId = await this.simplePrompt<number>({ message, type: 'list', choices })
- return selectedId
- }
- async promptForCuratorGroups(message = 'Select Curator Groups'): Promise<number[]> {
- const choices = await this.curatorGroupChoices()
- if (!choices.length) {
- return []
- }
- const selectedIds = await this.simplePrompt<number[]>({ message, type: 'checkbox', choices })
- return selectedIds
- }
- async promptForCurator(message = 'Choose a Curator', ids?: number[]): Promise<number> {
- const curators = await this.getApi().groupMembers(WorkingGroups.Curators)
- const choices = curators
- .filter((c) => (ids ? ids.includes(c.workerId.toNumber()) : true))
- .map((c) => ({
- name: `${memberHandle(c.profile)} (Worker ID: ${c.workerId})`,
- value: c.workerId.toNumber(),
- }))
- if (!choices.length) {
- this.warn('No Curators to choose from!')
- this.exit(ExitCodes.InvalidInput)
- }
- const selectedCuratorId = await this.simplePrompt<number>({
- message,
- type: 'list',
- choices,
- })
- return selectedCuratorId
- }
- async getCurator(id: string | number): Promise<Worker> {
- if (typeof id === 'string') {
- id = parseInt(id)
- }
- let curator
- try {
- curator = await this.getApi().workerByWorkerId(WorkingGroups.Curators, id)
- } catch (e) {
- if (e instanceof CLIError) {
- throw new CLIError('Invalid Curator id!')
- }
- throw e
- }
- return curator
- }
- async getCuratorGroup(id: string | number): Promise<CuratorGroup> {
- if (typeof id === 'string') {
- id = parseInt(id)
- }
- const group = await this.getApi().curatorGroupById(id)
- if (!group) {
- this.error('Invalid Curator Group id!', { exit: ExitCodes.InvalidInput })
- }
- return group
- }
- async getContentActor(
- context: Exclude<keyof typeof ContentActor.typeDefinitions, 'Collaborator'>
- ): Promise<[ContentActor, string]> {
- if (context === 'Member') {
- const { id, membership } = await this.getRequiredMemberContext()
- return [
- createType<ContentActor, 'ContentActor'>('ContentActor', { Member: id }),
- membership.controller_account.toString(),
- ]
- }
- if (context === 'Curator') {
- return this.getCuratorContext()
- }
- if (context === 'Lead') {
- const lead = await this.getRequiredLeadContext()
- return [createType<ContentActor, 'ContentActor'>('ContentActor', { Lead: null }), lead.roleAccount.toString()]
- }
- throw new Error(`Unrecognized context: ${context}`)
- }
- async validateMemberIdsSet(ids: number[] | MemberId[], setName: 'collaborator' | 'moderator'): Promise<void> {
- const members = await this.getApi().getMembers(ids)
- if (members.length < ids.length || members.some((m) => m.isEmpty)) {
- this.error(`Invalid ${setName} set! All ${setName} set members must be existing members!`, {
- exit: ExitCodes.InvalidInput,
- })
- }
- }
- }
|