SelectGrowiService.ts 7.7 KB

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