SelectGrowiService.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import type {
  2. GrowiCommand, GrowiCommandProcessor, GrowiInteractionProcessor,
  3. InteractionHandledResult,
  4. } from '@growi/slack';
  5. import { markdownSectionBlock } from '@growi/slack/dist/utils/block-kit-builder';
  6. import { InteractionPayloadAccessor } from '@growi/slack/dist/utils/interaction-payload-accessor';
  7. import { getInteractionIdRegexpFromCommandName } from '@growi/slack/dist/utils/payload-interaction-id-helpers';
  8. import { replaceOriginal, respond } from '@growi/slack/dist/utils/response-url';
  9. import { AuthorizeResult } from '@slack/oauth';
  10. import { Inject, Service } from '@tsed/di';
  11. import { Installation } from '~/entities/installation';
  12. import { Relation } from '~/entities/relation';
  13. import { InstallationRepository } from '~/repositories/installation';
  14. import { RelationRepository } from '~/repositories/relation';
  15. import loggerFactory from '~/utils/logger';
  16. const logger = loggerFactory('slackbot-proxy:services:UnregisterService');
  17. export type SelectGrowiCommandBody = {
  18. growiUrisForSingleUse: string[],
  19. }
  20. type SelectValue = {
  21. growiCommand: GrowiCommand,
  22. growiUri: any,
  23. }
  24. type SendCommandBody = {
  25. // eslint-disable-next-line camelcase
  26. trigger_id: string,
  27. // eslint-disable-next-line camelcase
  28. channel_id: string,
  29. // eslint-disable-next-line camelcase
  30. channel_name: string,
  31. }
  32. export type SelectedGrowiInformation = {
  33. relation: Relation,
  34. growiCommand: GrowiCommand,
  35. sendCommandBody: SendCommandBody,
  36. }
  37. @Service()
  38. export class SelectGrowiService implements GrowiCommandProcessor<SelectGrowiCommandBody | null>, GrowiInteractionProcessor<SelectedGrowiInformation> {
  39. @Inject()
  40. relationRepository: RelationRepository;
  41. @Inject()
  42. installationRepository: InstallationRepository;
  43. private generateGrowiSelectValue(growiCommand: GrowiCommand, growiUri: string): SelectValue {
  44. return {
  45. growiCommand,
  46. growiUri,
  47. };
  48. }
  49. shouldHandleCommand(): boolean {
  50. // TODO: consider to use the default supported commands for single use
  51. return true;
  52. }
  53. async processCommand(
  54. growiCommand: GrowiCommand, authorizeResult: AuthorizeResult, context: SelectGrowiCommandBody,
  55. ): Promise<void> {
  56. const growiUrls = context.growiUrisForSingleUse;
  57. const chooseSection = growiUrls.map((growiUri) => {
  58. const value = this.generateGrowiSelectValue(growiCommand, growiUri);
  59. return ({
  60. type: 'section',
  61. text: {
  62. type: 'mrkdwn',
  63. text: growiUri,
  64. },
  65. accessory: {
  66. type: 'button',
  67. action_id: 'select_growi:select_growi',
  68. text: {
  69. type: 'plain_text',
  70. text: 'Choose',
  71. },
  72. value: JSON.stringify(value),
  73. },
  74. });
  75. });
  76. return respond(growiCommand.responseUrl, {
  77. blocks: [
  78. {
  79. type: 'header',
  80. text: {
  81. type: 'plain_text',
  82. text: 'Select target GROWI',
  83. },
  84. },
  85. {
  86. type: 'context',
  87. elements: [
  88. {
  89. type: 'mrkdwn',
  90. text: `Request: \`/growi ${growiCommand.text}\` to:`,
  91. },
  92. ],
  93. },
  94. {
  95. type: 'divider',
  96. },
  97. ...chooseSection,
  98. ],
  99. });
  100. }
  101. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  102. shouldHandleInteraction(interactionPayloadAccessor: InteractionPayloadAccessor): boolean {
  103. const { actionId, callbackId } = interactionPayloadAccessor.getActionIdAndCallbackIdFromPayLoad();
  104. const registerRegexp: RegExp = getInteractionIdRegexpFromCommandName('select_growi');
  105. return registerRegexp.test(actionId) || registerRegexp.test(callbackId);
  106. }
  107. async processInteraction(
  108. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  109. authorizeResult: AuthorizeResult, interactionPayload: any, interactionPayloadAccessor: InteractionPayloadAccessor,
  110. ): Promise<InteractionHandledResult<SelectedGrowiInformation>> {
  111. const interactionHandledResult: InteractionHandledResult<SelectedGrowiInformation> = {
  112. isTerminated: false,
  113. };
  114. if (!this.shouldHandleInteraction(interactionPayloadAccessor)) return interactionHandledResult;
  115. const selectGrowiInformation = await this.handleSelectInteraction(authorizeResult, interactionPayload, interactionPayloadAccessor);
  116. if (selectGrowiInformation != null) {
  117. interactionHandledResult.result = selectGrowiInformation;
  118. }
  119. interactionHandledResult.isTerminated = false;
  120. return interactionHandledResult as InteractionHandledResult<SelectedGrowiInformation>;
  121. }
  122. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  123. async handleSelectInteraction(
  124. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  125. authorizeResult: AuthorizeResult, interactionPayload: any, interactionPayloadAccessor: InteractionPayloadAccessor,
  126. ): Promise<SelectedGrowiInformation | null> {
  127. const responseUrl = interactionPayloadAccessor.getResponseUrl();
  128. const selectGrowiValue = interactionPayloadAccessor.firstAction()?.value;
  129. if (selectGrowiValue == null) {
  130. logger.error('GROWI command failed: The first action element must have the value parameter.');
  131. await respond(responseUrl, {
  132. text: 'GROWI command failed',
  133. blocks: [
  134. markdownSectionBlock('Error occurred while processing GROWI command.'),
  135. ],
  136. });
  137. return null;
  138. }
  139. const { growiUri, growiCommand } = JSON.parse(selectGrowiValue);
  140. if (growiCommand == null) {
  141. logger.error('GROWI command failed: The first action value must have growiCommand parameter.');
  142. await respond(responseUrl, {
  143. text: 'GROWI command failed',
  144. blocks: [
  145. markdownSectionBlock('Error occurred while processing GROWI command.'),
  146. ],
  147. });
  148. return null;
  149. }
  150. await replaceOriginal(responseUrl, {
  151. text: `Accepted ${growiCommand.growiCommandType} command.`,
  152. blocks: [
  153. markdownSectionBlock(`Forwarding your request *"/growi ${growiCommand.growiCommandType}"* on GROWI to ${growiUri} ...`),
  154. ],
  155. });
  156. const installationId = authorizeResult.enterpriseId || authorizeResult.teamId;
  157. let installation: Installation | undefined;
  158. try {
  159. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  160. installation = await this.installationRepository.findByTeamIdOrEnterpriseId(installationId!);
  161. }
  162. catch (err) {
  163. logger.error('GROWI command failed: No installation found.\n', err);
  164. await respond(responseUrl, {
  165. text: 'GROWI command failed',
  166. blocks: [
  167. markdownSectionBlock('Error occurred while processing GROWI command.'),
  168. ],
  169. });
  170. return null;
  171. }
  172. const relation = await this.relationRepository.createQueryBuilder('relation')
  173. .where('relation.growiUri =:growiUri', { growiUri })
  174. .andWhere('relation.installationId = :id', { id: installation?.id })
  175. .leftJoinAndSelect('relation.installation', 'installation')
  176. .getOne();
  177. if (relation == null) {
  178. logger.error('GROWI command failed: No installation found.');
  179. await respond(responseUrl, {
  180. text: 'GROWI command failed',
  181. blocks: [
  182. markdownSectionBlock('Error occurred while processing GROWI command.'),
  183. ],
  184. });
  185. return null;
  186. }
  187. // increment sendCommandBody
  188. const channel = interactionPayloadAccessor.getChannel();
  189. if (channel == null) {
  190. logger.error('GROWI command failed: channel not found.');
  191. await respond(responseUrl, {
  192. text: 'GROWI command failed',
  193. blocks: [
  194. markdownSectionBlock('Error occurred while processing GROWI command.'),
  195. ],
  196. });
  197. return null;
  198. }
  199. const sendCommandBody: SendCommandBody = {
  200. trigger_id: interactionPayload.trigger_id,
  201. channel_id: channel.id,
  202. channel_name: channel.name,
  203. };
  204. return {
  205. relation,
  206. growiCommand,
  207. sendCommandBody,
  208. };
  209. }
  210. }