RegisterService.ts 8.1 KB

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