UnregisterService.ts 7.6 KB

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