RegisterService.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import type {
  2. GrowiCommand, GrowiCommandProcessor, GrowiInteractionProcessor, InteractionHandledResult,
  3. } from '@growi/slack';
  4. import {
  5. markdownSectionBlock, markdownHeaderBlock, inputSectionBlock, inputBlock,
  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 } from '@growi/slack/dist/utils/response-url';
  10. import { AuthorizeResult } from '@slack/oauth';
  11. import {
  12. WebClient, LogLevel, Block, ConversationsSelect,
  13. } from '@slack/web-api';
  14. import { Inject, Service } from '@tsed/di';
  15. import { InstallationRepository } from '~/repositories/installation';
  16. import { OrderRepository } from '~/repositories/order';
  17. import loggerFactory from '~/utils/logger';
  18. import { InvalidUrlError } from '../models/errors';
  19. const logger = loggerFactory('slackbot-proxy:services:RegisterService');
  20. const isProduction = process.env.NODE_ENV === 'production';
  21. const isOfficialMode = process.env.OFFICIAL_MODE === 'true';
  22. export type RegisterCommandBody = {
  23. // eslint-disable-next-line camelcase
  24. trigger_id: string,
  25. // eslint-disable-next-line camelcase
  26. channel_name: string,
  27. }
  28. @Service()
  29. export class RegisterService implements GrowiCommandProcessor<RegisterCommandBody>, GrowiInteractionProcessor<void> {
  30. @Inject()
  31. orderRepository: OrderRepository;
  32. @Inject()
  33. installationRepository: InstallationRepository;
  34. shouldHandleCommand(growiCommand: GrowiCommand): boolean {
  35. return growiCommand.growiCommandType === 'register';
  36. }
  37. async processCommand(growiCommand: GrowiCommand, authorizeResult: AuthorizeResult, context: RegisterCommandBody): Promise<void> {
  38. const { botToken } = authorizeResult;
  39. const client = new WebClient(botToken, { logLevel: isProduction ? LogLevel.DEBUG : LogLevel.INFO });
  40. const conversationsSelectElement: ConversationsSelect = {
  41. action_id: 'conversation',
  42. type: 'conversations_select',
  43. response_url_enabled: true,
  44. default_to_current_conversation: true,
  45. };
  46. await client.views.open({
  47. trigger_id: context.trigger_id,
  48. view: {
  49. type: 'modal',
  50. callback_id: 'register:register',
  51. title: {
  52. type: 'plain_text',
  53. text: 'Register Credentials',
  54. },
  55. submit: {
  56. type: 'plain_text',
  57. text: 'Submit',
  58. },
  59. close: {
  60. type: 'plain_text',
  61. text: 'Close',
  62. },
  63. private_metadata: JSON.stringify({ channel: context.channel_name }),
  64. blocks: [
  65. inputBlock(conversationsSelectElement, 'conversation', 'Channel to which you want to add'),
  66. inputSectionBlock('growiUrl', 'GROWI domain', 'contents_input', false, 'https://example.com'),
  67. inputSectionBlock('tokenPtoG', 'Access Token Proxy to GROWI', 'contents_input', false, 'jBMZvpk.....'),
  68. inputSectionBlock('tokenGtoP', 'Access Token GROWI to Proxy', 'contents_input', false, 'sdg15av.....'),
  69. ],
  70. },
  71. });
  72. }
  73. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  74. shouldHandleInteraction(interactionPayloadAccessor: InteractionPayloadAccessor): boolean {
  75. const { actionId, callbackId } = interactionPayloadAccessor.getActionIdAndCallbackIdFromPayLoad();
  76. const registerRegexp: RegExp = getInteractionIdRegexpFromCommandName('register');
  77. return registerRegexp.test(actionId) || registerRegexp.test(callbackId);
  78. }
  79. async processInteraction(
  80. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  81. authorizeResult: AuthorizeResult, interactionPayload: any, interactionPayloadAccessor: InteractionPayloadAccessor,
  82. ): Promise<InteractionHandledResult<void>> {
  83. const interactionHandledResult: InteractionHandledResult<void> = {
  84. isTerminated: false,
  85. };
  86. if (!this.shouldHandleInteraction(interactionPayloadAccessor)) return interactionHandledResult;
  87. interactionHandledResult.result = await this.handleRegisterInteraction(authorizeResult, interactionPayload, interactionPayloadAccessor);
  88. interactionHandledResult.isTerminated = true;
  89. return interactionHandledResult as InteractionHandledResult<void>;
  90. }
  91. async handleRegisterInteraction(
  92. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  93. authorizeResult: AuthorizeResult, interactionPayload: any, interactionPayloadAccessor: InteractionPayloadAccessor,
  94. ): Promise<void> {
  95. try {
  96. await this.insertOrderRecord(authorizeResult, interactionPayloadAccessor);
  97. }
  98. catch (err) {
  99. if (err instanceof InvalidUrlError) {
  100. logger.error('Failed to register:\n', err);
  101. await respond(interactionPayloadAccessor.getResponseUrl(), {
  102. text: 'Invalid URL',
  103. blocks: [
  104. markdownSectionBlock('Please enter a valid URL'),
  105. ],
  106. });
  107. return;
  108. }
  109. logger.error('Error occurred while insertOrderRecord:\n', err);
  110. }
  111. await this.notifyServerUriToSlack(interactionPayloadAccessor);
  112. }
  113. async insertOrderRecord(
  114. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  115. authorizeResult: AuthorizeResult, interactionPayloadAccessor: InteractionPayloadAccessor,
  116. ): Promise<void> {
  117. const inputValues = interactionPayloadAccessor.getStateValues();
  118. const growiUrl = inputValues.growiUrl.contents_input.value;
  119. const tokenPtoG = inputValues.tokenPtoG.contents_input.value;
  120. const tokenGtoP = inputValues.tokenGtoP.contents_input.value;
  121. try {
  122. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  123. const url = new URL(growiUrl);
  124. }
  125. catch (error) {
  126. throw new InvalidUrlError(growiUrl);
  127. }
  128. const installationId = authorizeResult.enterpriseId || authorizeResult.teamId;
  129. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  130. const installation = await this.installationRepository.findByTeamIdOrEnterpriseId(installationId!);
  131. this.orderRepository.save({
  132. installation, growiUrl, tokenPtoG, tokenGtoP,
  133. });
  134. }
  135. async notifyServerUriToSlack(
  136. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  137. interactionPayloadAccessor: InteractionPayloadAccessor,
  138. ): Promise<void> {
  139. const serverUri = process.env.SERVER_URI;
  140. const responseUrl = interactionPayloadAccessor.getResponseUrl();
  141. const blocks: Block[] = [];
  142. if (isOfficialMode) {
  143. blocks.push(markdownHeaderBlock(':white_check_mark: 1. Install Official bot to Slack'));
  144. blocks.push(markdownHeaderBlock(':white_check_mark: 2. Register for GROWI Official Bot Proxy Service'));
  145. blocks.push(markdownSectionBlock('The request has been successfully accepted. However, registration has *NOT been completed* yet.'));
  146. blocks.push(markdownHeaderBlock(':arrow_right: 3. Test Connection'));
  147. blocks.push(markdownSectionBlock('*Test Connection* to complete the registration in your GROWI.'));
  148. blocks.push(markdownHeaderBlock(':white_large_square: 4. (Opt) Manage Permission'));
  149. blocks.push(markdownSectionBlock('Modify permission settings if you need.'));
  150. await respond(responseUrl, {
  151. text: 'Proxy URL',
  152. blocks,
  153. });
  154. return;
  155. }
  156. blocks.push(markdownHeaderBlock(':white_check_mark: 1. Create Bot'));
  157. blocks.push(markdownHeaderBlock(':white_check_mark: 2. Install bot to Slack'));
  158. blocks.push(markdownHeaderBlock(':white_check_mark: 3. Register for your GROWI Custom Bot Proxy'));
  159. blocks.push(markdownSectionBlock('The request has been successfully accepted. However, registration has *NOT been completed* yet.'));
  160. blocks.push(markdownHeaderBlock(':arrow_right: 4. Set Proxy URL on GROWI'));
  161. blocks.push(markdownSectionBlock('Please enter and update the following Proxy URL to slack bot setting form in your GROWI'));
  162. blocks.push(markdownSectionBlock(`Proxy URL: ${serverUri}`));
  163. blocks.push(markdownHeaderBlock(':arrow_right: 5. Test Connection'));
  164. blocks.push(markdownSectionBlock('And *Test Connection* to complete the registration in your GROWI.'));
  165. blocks.push(markdownHeaderBlock(':white_large_square: 6. (Opt) Manage Permission'));
  166. blocks.push(markdownSectionBlock('Modify permission settings if you need.'));
  167. await respond(responseUrl, {
  168. text: 'Proxy URL',
  169. blocks,
  170. });
  171. return;
  172. }
  173. }