UnregisterService.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  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. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  104. shouldHandleInteraction(
  105. interactionPayloadAccessor: InteractionPayloadAccessor,
  106. ): boolean {
  107. const { actionId, callbackId } =
  108. interactionPayloadAccessor.getActionIdAndCallbackIdFromPayLoad();
  109. const registerRegexp: RegExp =
  110. getInteractionIdRegexpFromCommandName('unregister');
  111. return registerRegexp.test(actionId) || registerRegexp.test(callbackId);
  112. }
  113. async processInteraction(
  114. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  115. authorizeResult: AuthorizeResult,
  116. interactionPayload: any,
  117. interactionPayloadAccessor: InteractionPayloadAccessor,
  118. ): Promise<InteractionHandledResult<void>> {
  119. const interactionHandledResult: InteractionHandledResult<void> = {
  120. isTerminated: false,
  121. };
  122. if (!this.shouldHandleInteraction(interactionPayloadAccessor))
  123. return interactionHandledResult;
  124. const { actionId } =
  125. interactionPayloadAccessor.getActionIdAndCallbackIdFromPayLoad();
  126. switch (actionId) {
  127. case 'unregister:unregister':
  128. interactionHandledResult.result =
  129. await this.handleUnregisterInteraction(
  130. authorizeResult,
  131. interactionPayload,
  132. interactionPayloadAccessor,
  133. );
  134. break;
  135. case 'unregister:cancel':
  136. interactionHandledResult.result =
  137. await this.handleUnregisterCancelInteraction(
  138. interactionPayloadAccessor,
  139. );
  140. break;
  141. case 'unregister:selectedGrowiUris':
  142. break;
  143. default:
  144. logger.error('This unregister interaction is not implemented.');
  145. break;
  146. }
  147. interactionHandledResult.isTerminated = true;
  148. return interactionHandledResult as InteractionHandledResult<void>;
  149. }
  150. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  151. async handleUnregisterInteraction(
  152. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  153. authorizeResult: AuthorizeResult,
  154. interactionPayload: any,
  155. interactionPayloadAccessor: InteractionPayloadAccessor,
  156. ): Promise<void> {
  157. const responseUrl = interactionPayloadAccessor.getResponseUrl();
  158. const selectedOptions =
  159. interactionPayloadAccessor.getStateValues()?.growiUris?.[
  160. 'unregister:selectedGrowiUris'
  161. ]?.selected_options;
  162. if (!Array.isArray(selectedOptions)) {
  163. logger.error('Unregisteration failed: Mulformed object was detected\n');
  164. await respond(responseUrl, {
  165. text: 'Unregistration failed',
  166. blocks: [
  167. markdownSectionBlock('Error occurred while unregistering GROWI.'),
  168. ],
  169. });
  170. return;
  171. }
  172. const growiUris = selectedOptions.map(
  173. (selectedOption) => selectedOption.value,
  174. );
  175. const installationId =
  176. authorizeResult.enterpriseId || authorizeResult.teamId;
  177. let installation: Installation | undefined;
  178. try {
  179. installation =
  180. await this.installationRepository.findByTeamIdOrEnterpriseId(
  181. // biome-ignore lint/style/noNonNullAssertion: installationId must be set --- IGNORE ---
  182. installationId!,
  183. );
  184. } catch (err) {
  185. logger.error('Unregisteration failed:\n', err);
  186. await respond(responseUrl, {
  187. text: 'Unregistration failed',
  188. blocks: [
  189. markdownSectionBlock('Error occurred while unregistering GROWI.'),
  190. ],
  191. });
  192. return;
  193. }
  194. let deleteResult: DeleteResult;
  195. try {
  196. deleteResult = await this.relationRepository
  197. .createQueryBuilder('relation')
  198. .where('relation.growiUri IN (:uris)', { uris: growiUris })
  199. .andWhere('relation.installationId = :installationId', {
  200. installationId: installation?.id,
  201. })
  202. .delete()
  203. .execute();
  204. } catch (err) {
  205. logger.error('Unregisteration failed\n', err);
  206. await respond(responseUrl, {
  207. text: 'Unregistration failed',
  208. blocks: [
  209. markdownSectionBlock('Error occurred while unregistering GROWI.'),
  210. ],
  211. });
  212. return;
  213. }
  214. await replaceOriginal(responseUrl, {
  215. text: 'Unregistration completed',
  216. blocks: [
  217. markdownSectionBlock(
  218. `Unregistered *${deleteResult.affected}* GROWI from this workspace.`,
  219. ),
  220. ],
  221. });
  222. return;
  223. }
  224. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  225. async handleUnregisterCancelInteraction(
  226. interactionPayloadAccessor: InteractionPayloadAccessor,
  227. ): Promise<void> {
  228. await axios.post(interactionPayloadAccessor.getResponseUrl(), {
  229. delete_original: true,
  230. });
  231. }
  232. }