WorkingGroupsCommandBase.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import ExitCodes from '../ExitCodes'
  2. import AccountsCommandBase from './AccountsCommandBase'
  3. import { flags } from '@oclif/command'
  4. import {
  5. WorkingGroups,
  6. AvailableGroups,
  7. NamedKeyringPair,
  8. GroupMember,
  9. GroupOpening,
  10. OpeningStatus,
  11. GroupApplication,
  12. } from '../Types'
  13. import _ from 'lodash'
  14. import { ApplicationStageKeys } from '@joystream/types/hiring'
  15. import chalk from 'chalk'
  16. import { IConfig } from '@oclif/config'
  17. /**
  18. * Abstract base class for commands that need to use gates based on user's roles
  19. */
  20. export abstract class RolesCommandBase extends AccountsCommandBase {
  21. group: WorkingGroups
  22. constructor(argv: string[], config: IConfig) {
  23. super(argv, config)
  24. // Can be modified by child class constructor
  25. this.group = this.getPreservedState().defaultWorkingGroup
  26. }
  27. // Use when lead access is required in given command
  28. async getRequiredLead(): Promise<GroupMember> {
  29. const selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount()
  30. const lead = await this.getApi().groupLead(this.group)
  31. if (!lead || lead.roleAccount.toString() !== selectedAccount.address) {
  32. this.error(`${_.startCase(this.group)} Group Lead access required for this command!`, {
  33. exit: ExitCodes.AccessDenied,
  34. })
  35. }
  36. return lead
  37. }
  38. // Use when worker access is required in given command
  39. async getRequiredWorker(): Promise<GroupMember> {
  40. const selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount()
  41. const groupMembers = await this.getApi().groupMembers(this.group)
  42. const groupMembersByAccount = groupMembers.filter((m) => m.roleAccount.toString() === selectedAccount.address)
  43. if (!groupMembersByAccount.length) {
  44. this.error(`${_.startCase(this.group)} Group Worker access required for this command!`, {
  45. exit: ExitCodes.AccessDenied,
  46. })
  47. } else if (groupMembersByAccount.length === 1) {
  48. return groupMembersByAccount[0]
  49. } else {
  50. return await this.promptForWorker(groupMembersByAccount)
  51. }
  52. }
  53. // Use when member controller access is required, but one of the associated roles is expected to be selected
  54. async getRequiredWorkerByMemberController(): Promise<GroupMember> {
  55. const selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount()
  56. const memberIds = await this.getApi().getMemberIdsByControllerAccount(selectedAccount.address)
  57. const controlledWorkers = (await this.getApi().groupMembers(this.group)).filter((groupMember) =>
  58. memberIds.some((memberId) => groupMember.memberId.eq(memberId))
  59. )
  60. if (!controlledWorkers.length) {
  61. this.error(`Member controller account with some associated ${this.group} group roles needs to be selected!`, {
  62. exit: ExitCodes.AccessDenied,
  63. })
  64. } else if (controlledWorkers.length === 1) {
  65. return controlledWorkers[0]
  66. } else {
  67. return await this.promptForWorker(controlledWorkers)
  68. }
  69. }
  70. async promptForWorker(groupMembers: GroupMember[]): Promise<GroupMember> {
  71. const chosenWorkerIndex = await this.simplePrompt({
  72. message: `Choose the intended ${_.startCase(this.group)} Group Worker context:`,
  73. type: 'list',
  74. choices: groupMembers.map((groupMember, index) => ({
  75. name: `Worker ID ${groupMember.workerId.toString()}`,
  76. value: index,
  77. })),
  78. })
  79. return groupMembers[chosenWorkerIndex]
  80. }
  81. }
  82. /**
  83. * Abstract base class for commands directly related to working groups
  84. */
  85. export default abstract class WorkingGroupsCommandBase extends RolesCommandBase {
  86. group: WorkingGroups
  87. constructor(argv: string[], config: IConfig) {
  88. super(argv, config)
  89. this.group = this.getPreservedState().defaultWorkingGroup
  90. }
  91. static flags = {
  92. group: flags.enum({
  93. char: 'g',
  94. description:
  95. 'The working group context in which the command should be executed\n' +
  96. `Available values are: ${AvailableGroups.join(', ')}.`,
  97. required: false,
  98. options: [...AvailableGroups],
  99. }),
  100. }
  101. async promptForApplicationsToAccept(opening: GroupOpening): Promise<number[]> {
  102. const acceptableApplications = opening.applications.filter((a) => a.stage === ApplicationStageKeys.Active)
  103. const acceptedApplications = await this.simplePrompt({
  104. message: 'Select succesful applicants',
  105. type: 'checkbox',
  106. choices: acceptableApplications.map((a) => ({
  107. name: ` ${a.wgApplicationId}: ${a.member?.handle.toString()}`,
  108. value: a.wgApplicationId,
  109. })),
  110. })
  111. return acceptedApplications
  112. }
  113. async getOpeningForLeadAction(id: number, requiredStatus?: OpeningStatus): Promise<GroupOpening> {
  114. const opening = await this.getApi().groupOpening(this.group, id)
  115. if (!opening.type.isOfType('Worker')) {
  116. this.error('A lead can only manage Worker openings!', { exit: ExitCodes.AccessDenied })
  117. }
  118. if (requiredStatus && opening.stage.status !== requiredStatus) {
  119. this.error(
  120. `The opening needs to be in "${_.startCase(requiredStatus)}" stage! ` +
  121. `This one is: "${_.startCase(opening.stage.status)}"`,
  122. { exit: ExitCodes.InvalidInput }
  123. )
  124. }
  125. return opening
  126. }
  127. // An alias for better code readibility in case we don't need the actual return value
  128. validateOpeningForLeadAction = this.getOpeningForLeadAction
  129. async getApplicationForLeadAction(id: number, requiredStatus?: ApplicationStageKeys): Promise<GroupApplication> {
  130. const application = await this.getApi().groupApplication(this.group, id)
  131. const opening = await this.getApi().groupOpening(this.group, application.wgOpeningId)
  132. if (!opening.type.isOfType('Worker')) {
  133. this.error('A lead can only manage Worker opening applications!', { exit: ExitCodes.AccessDenied })
  134. }
  135. if (requiredStatus && application.stage !== requiredStatus) {
  136. this.error(
  137. `The application needs to have "${_.startCase(requiredStatus)}" status! ` +
  138. `This one has: "${_.startCase(application.stage)}"`,
  139. { exit: ExitCodes.InvalidInput }
  140. )
  141. }
  142. return application
  143. }
  144. async getWorkerForLeadAction(id: number, requireStakeProfile = false) {
  145. const groupMember = await this.getApi().groupMember(this.group, id)
  146. const groupLead = await this.getApi().groupLead(this.group)
  147. if (groupLead?.workerId.eq(groupMember.workerId)) {
  148. this.error('A lead cannot manage his own role this way!', { exit: ExitCodes.AccessDenied })
  149. }
  150. if (requireStakeProfile && !groupMember.stake) {
  151. this.error('This worker has no associated role stake profile!', { exit: ExitCodes.InvalidInput })
  152. }
  153. return groupMember
  154. }
  155. // Helper for better TS handling.
  156. // We could also use some magic with conditional types instead, but those don't seem be very well supported yet.
  157. async getWorkerWithStakeForLeadAction(id: number) {
  158. return (await this.getWorkerForLeadAction(id, true)) as GroupMember & Required<Pick<GroupMember, 'stake'>>
  159. }
  160. async init() {
  161. await super.init()
  162. const { flags } = this.parse(this.constructor as typeof WorkingGroupsCommandBase)
  163. if (flags.group) {
  164. this.group = flags.group
  165. }
  166. this.log(chalk.white('Current Group: ' + this.group))
  167. }
  168. }