UnregisterService.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. import type {
  2. GrowiCommand,
  3. GrowiCommandProcessor,
  4. GrowiInteractionProcessor,
  5. InteractionHandledResult,
  6. } from '@growi/slack';
  7. import {
  8. actionsBlock,
  9. buttonElement,
  10. inputBlock,
  11. markdownSectionBlock,
  12. } from '@growi/slack/dist/utils/block-kit-builder';
  13. import { InteractionPayloadAccessor } from '@growi/slack/dist/utils/interaction-payload-accessor';
  14. import { getInteractionIdRegexpFromCommandName } from '@growi/slack/dist/utils/payload-interaction-id-helpers';
  15. import { replaceOriginal, respond } from '@growi/slack/dist/utils/response-url';
  16. import { AuthorizeResult } from '@slack/oauth';
  17. import { MultiStaticSelect } from '@slack/web-api';
  18. import { Inject, Service } from '@tsed/di';
  19. import axios from 'axios';
  20. import { DeleteResult } from 'typeorm';
  21. import { Installation } from '~/entities/installation';
  22. import { InstallationRepository } from '~/repositories/installation';
  23. import { RelationRepository } from '~/repositories/relation';
  24. import loggerFactory from '~/utils/logger';
  25. const logger = loggerFactory('slackbot-proxy:services:UnregisterService');
  26. @Service()
  27. export class UnregisterService
  28. implements GrowiCommandProcessor, GrowiInteractionProcessor<void>
  29. {
  30. @Inject()
  31. relationRepository: RelationRepository;
  32. @Inject()
  33. installationRepository: InstallationRepository;
  34. shouldHandleCommand(growiCommand: GrowiCommand): boolean {
  35. return growiCommand.growiCommandType === 'unregister';
  36. }
  37. async processCommand(
  38. growiCommand: GrowiCommand,
  39. authorizeResult: AuthorizeResult,
  40. ): Promise<void> {
  41. // get growi urls
  42. const installationId =
  43. authorizeResult.enterpriseId || authorizeResult.teamId;
  44. const installation =
  45. await this.installationRepository.findByTeamIdOrEnterpriseId(
  46. // biome-ignore lint/style/noNonNullAssertion: installationId must be set --- IGNORE ---
  47. installationId!,
  48. );
  49. const relations = await this.relationRepository
  50. .createQueryBuilder('relation')
  51. .where('relation.installationId = :id', { id: installation?.id })
  52. .leftJoinAndSelect('relation.installation', 'installation')
  53. .getMany();
  54. if (relations.length === 0) {
  55. await respond(growiCommand.responseUrl, {
  56. text: 'No GROWI found to unregister.',
  57. blocks: [
  58. markdownSectionBlock(
  59. "You haven't registered any GROWI to this workspace.",
  60. ),
  61. markdownSectionBlock('Send `/growi register` to register.'),
  62. ],
  63. });
  64. return;
  65. }
  66. const staticSelectElement: MultiStaticSelect = {
  67. action_id: 'unregister:selectedGrowiUris',
  68. type: 'multi_static_select',
  69. placeholder: {
  70. type: 'plain_text',
  71. text: 'Select GROWI URLs to unregister',
  72. },
  73. options: relations.map((relation) => {
  74. return {
  75. text: {
  76. type: 'plain_text',
  77. text: relation.growiUri,
  78. },
  79. value: relation.growiUri,
  80. };
  81. }),
  82. };
  83. await respond(growiCommand.responseUrl, {
  84. text: 'Select GROWI URLs to unregister.',
  85. blocks: [
  86. inputBlock(staticSelectElement, 'growiUris', 'GROWI URL to unregister'),
  87. actionsBlock(
  88. buttonElement({
  89. text: 'Cancel',
  90. actionId: 'unregister:cancel',
  91. value: JSON.stringify({}),
  92. }),
  93. buttonElement({
  94. text: 'Unregister',
  95. actionId: 'unregister:unregister',
  96. style: 'danger',
  97. value: JSON.stringify({}),
  98. }),
  99. ),
  100. ],
  101. });
  102. }
  103. shouldHandleInteraction(
  104. interactionPayloadAccessor: InteractionPayloadAccessor,
  105. ): boolean {
  106. const { actionId, callbackId } =
  107. interactionPayloadAccessor.getActionIdAndCallbackIdFromPayLoad();
  108. const registerRegexp: RegExp =
  109. getInteractionIdRegexpFromCommandName('unregister');
  110. return registerRegexp.test(actionId) || registerRegexp.test(callbackId);
  111. }
  112. async processInteraction(
  113. authorizeResult: AuthorizeResult,
  114. interactionPayload: any,
  115. interactionPayloadAccessor: InteractionPayloadAccessor,
  116. ): Promise<InteractionHandledResult<void>> {
  117. const interactionHandledResult: InteractionHandledResult<void> = {
  118. isTerminated: false,
  119. };
  120. if (!this.shouldHandleInteraction(interactionPayloadAccessor))
  121. return interactionHandledResult;
  122. const { actionId } =
  123. interactionPayloadAccessor.getActionIdAndCallbackIdFromPayLoad();
  124. switch (actionId) {
  125. case 'unregister:unregister':
  126. interactionHandledResult.result =
  127. await this.handleUnregisterInteraction(
  128. authorizeResult,
  129. interactionPayload,
  130. interactionPayloadAccessor,
  131. );
  132. break;
  133. case 'unregister:cancel':
  134. interactionHandledResult.result =
  135. await this.handleUnregisterCancelInteraction(
  136. interactionPayloadAccessor,
  137. );
  138. break;
  139. case 'unregister:selectedGrowiUris':
  140. break;
  141. default:
  142. logger.error('This unregister interaction is not implemented.');
  143. break;
  144. }
  145. interactionHandledResult.isTerminated = true;
  146. return interactionHandledResult as InteractionHandledResult<void>;
  147. }
  148. // biome-ignore lint:*:noExplicitModuleBoundaryTypes: Temporary Alternative to @typescript-eslint/explicit-module-boundary-types
  149. async handleUnregisterInteraction(
  150. // biome-ignore lint:*:noExplicitModuleBoundaryTypes: Temporary Alternative to @typescript-eslint/explicit-module-boundary-types
  151. authorizeResult: AuthorizeResult,
  152. interactionPayload: any,
  153. interactionPayloadAccessor: InteractionPayloadAccessor,
  154. ): Promise<void> {
  155. const responseUrl = interactionPayloadAccessor.getResponseUrl();
  156. const selectedOptions =
  157. interactionPayloadAccessor.getStateValues()?.growiUris?.[
  158. 'unregister:selectedGrowiUris'
  159. ]?.selected_options;
  160. if (!Array.isArray(selectedOptions)) {
  161. logger.error('Unregisteration failed: Mulformed object was detected\n');
  162. await respond(responseUrl, {
  163. text: 'Unregistration failed',
  164. blocks: [
  165. markdownSectionBlock('Error occurred while unregistering GROWI.'),
  166. ],
  167. });
  168. return;
  169. }
  170. const growiUris = selectedOptions.map(
  171. (selectedOption) => selectedOption.value,
  172. );
  173. const installationId =
  174. authorizeResult.enterpriseId || authorizeResult.teamId;
  175. let installation: Installation | undefined;
  176. try {
  177. installation =
  178. await this.installationRepository.findByTeamIdOrEnterpriseId(
  179. // biome-ignore lint/style/noNonNullAssertion: installationId must be set --- IGNORE ---
  180. installationId!,
  181. );
  182. } catch (err) {
  183. logger.error('Unregisteration failed:\n', err);
  184. await respond(responseUrl, {
  185. text: 'Unregistration failed',
  186. blocks: [
  187. markdownSectionBlock('Error occurred while unregistering GROWI.'),
  188. ],
  189. });
  190. return;
  191. }
  192. let deleteResult: DeleteResult;
  193. try {
  194. deleteResult = await this.relationRepository
  195. .createQueryBuilder('relation')
  196. .where('relation.growiUri IN (:uris)', { uris: growiUris })
  197. .andWhere('relation.installationId = :installationId', {
  198. installationId: installation?.id,
  199. })
  200. .delete()
  201. .execute();
  202. } catch (err) {
  203. logger.error('Unregisteration failed\n', err);
  204. await respond(responseUrl, {
  205. text: 'Unregistration failed',
  206. blocks: [
  207. markdownSectionBlock('Error occurred while unregistering GROWI.'),
  208. ],
  209. });
  210. return;
  211. }
  212. await replaceOriginal(responseUrl, {
  213. text: 'Unregistration completed',
  214. blocks: [
  215. markdownSectionBlock(
  216. `Unregistered *${deleteResult.affected}* GROWI from this workspace.`,
  217. ),
  218. ],
  219. });
  220. return;
  221. }
  222. // biome-ignore lint:*:noExplicitModuleBoundaryTypes: Temporary Alternative to @typescript-eslint/explicit-module-boundary-types
  223. async handleUnregisterCancelInteraction(
  224. interactionPayloadAccessor: InteractionPayloadAccessor,
  225. ): Promise<void> {
  226. await axios.post(interactionPayloadAccessor.getResponseUrl(), {
  227. delete_original: true,
  228. });
  229. }
  230. }