SelectGrowiService.ts 7.8 KB

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