SelectGrowiService.ts 7.6 KB

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