slack-integration.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. import mongoose from 'mongoose';
  2. import { IncomingWebhookSendArguments } from '@slack/webhook';
  3. import { ChatPostMessageArguments, WebClient } from '@slack/web-api';
  4. import { generateWebClient, markdownSectionBlock, SlackbotType } from '@growi/slack';
  5. import loggerFactory from '~/utils/logger';
  6. import S2sMessage from '../models/vo/s2s-message';
  7. import ConfigManager from './config-manager';
  8. import { S2sMessagingService } from './s2s-messaging/base';
  9. import { S2sMessageHandlable } from './s2s-messaging/handlable';
  10. const logger = loggerFactory('growi:service:SlackBotService');
  11. type S2sMessageForSlackIntegration = S2sMessage & { updatedAt: Date };
  12. export class SlackIntegrationService implements S2sMessageHandlable {
  13. crowi!: any;
  14. configManager!: ConfigManager;
  15. s2sMessagingService!: S2sMessagingService;
  16. lastLoadedAt?: Date;
  17. constructor(crowi) {
  18. this.crowi = crowi;
  19. this.configManager = crowi.configManager;
  20. this.s2sMessagingService = crowi.s2sMessagingService;
  21. this.initialize();
  22. }
  23. initialize() {
  24. this.lastLoadedAt = new Date();
  25. }
  26. /**
  27. * @inheritdoc
  28. */
  29. shouldHandleS2sMessage(s2sMessage: S2sMessageForSlackIntegration): boolean {
  30. const { eventName, updatedAt } = s2sMessage;
  31. if (eventName !== 'slackIntegrationServiceUpdated' || updatedAt == null) {
  32. return false;
  33. }
  34. return this.lastLoadedAt == null || this.lastLoadedAt < new Date(s2sMessage.updatedAt);
  35. }
  36. /**
  37. * @inheritdoc
  38. */
  39. async handleS2sMessage(): Promise<void> {
  40. const { configManager } = this.crowi;
  41. logger.info('Reset slack bot by pubsub notification');
  42. await configManager.loadConfigs();
  43. this.initialize();
  44. }
  45. async publishUpdatedMessage(): Promise<void> {
  46. const { s2sMessagingService } = this;
  47. if (s2sMessagingService != null) {
  48. const s2sMessage = new S2sMessage('slackIntegrationServiceUpdated', { updatedAt: new Date() });
  49. try {
  50. await s2sMessagingService.publish(s2sMessage);
  51. }
  52. catch (e) {
  53. logger.error('Failed to publish update message with S2sMessagingService: ', e.message);
  54. }
  55. }
  56. }
  57. get isSlackConfigured(): boolean {
  58. return this.isSlackbotConfigured || this.isSlackLegacyConfigured;
  59. }
  60. get isSlackbotConfigured(): boolean {
  61. const hasSlackbotType = !!this.configManager.getConfig('crowi', 'slackbot:currentBotType');
  62. return hasSlackbotType;
  63. }
  64. get isSlackLegacyConfigured(): boolean {
  65. // for legacy util
  66. const hasSlackToken = !!this.configManager.getConfig('notification', 'slack:token');
  67. const hasSlackIwhUrl = !!this.configManager.getConfig('notification', 'slack:incomingWebhookUrl');
  68. return hasSlackToken || hasSlackIwhUrl;
  69. }
  70. private isCheckTypeValid(): boolean {
  71. const currentBotType = this.configManager.getConfig('crowi', 'slackbot:currentBotType');
  72. if (currentBotType == null) {
  73. throw new Error('The config \'SLACKBOT_TYPE\'(ns: \'crowi\', key: \'slackbot:currentBotType\') must be set.');
  74. }
  75. return true;
  76. }
  77. /**
  78. * generate WebClient instance for CUSTOM_WITHOUT_PROXY type
  79. */
  80. async generateClientForCustomBotWithoutProxy(): Promise<WebClient> {
  81. this.isCheckTypeValid();
  82. const token = this.configManager.getConfig('crowi', 'slackbot:withoutProxy:botToken');
  83. if (token == null) {
  84. throw new Error('The config \'SLACK_BOT_TOKEN\'(ns: \'crowi\', key: \'slackbot:withoutProxy:botToken\') must be set.');
  85. }
  86. return generateWebClient(token);
  87. }
  88. /**
  89. * generate WebClient instance by tokenPtoG
  90. * @param tokenPtoG
  91. */
  92. async generateClientByTokenPtoG(tokenPtoG: string): Promise<WebClient> {
  93. this.isCheckTypeValid();
  94. // const SlackAppIntegration = mongoose.model('SlackAppIntegration');
  95. const SlackAppIntegrationMock = mongoose.model('SlackAppIntegrationMock');
  96. const slackAppIntegration = await SlackAppIntegrationMock.findOne({ tokenPtoG });
  97. if (slackAppIntegration == null) {
  98. throw new Error('No SlackAppIntegration exists that corresponds to the tokenPtoG specified.');
  99. }
  100. return this.generateClientBySlackAppIntegration(slackAppIntegration as unknown as { tokenGtoP: string; });
  101. }
  102. /**
  103. * generate WebClient instance by tokenPtoG
  104. * @param tokenPtoG
  105. */
  106. async generateClientForPrimaryWorkspace(): Promise<WebClient> {
  107. this.isCheckTypeValid();
  108. const currentBotType = this.configManager.getConfig('crowi', 'slackbot:currentBotType');
  109. if (currentBotType === SlackbotType.CUSTOM_WITHOUT_PROXY) {
  110. return this.generateClientForCustomBotWithoutProxy();
  111. }
  112. // retrieve primary SlackAppIntegration
  113. const SlackAppIntegration = mongoose.model('SlackAppIntegration');
  114. const slackAppIntegration = await SlackAppIntegration.findOne({ isPrimary: true });
  115. if (slackAppIntegration == null) {
  116. throw new Error('None of the primary SlackAppIntegration exists.');
  117. }
  118. return this.generateClientBySlackAppIntegration(slackAppIntegration as unknown as { tokenGtoP: string; });
  119. }
  120. /**
  121. * generate WebClient instance by SlackAppIntegration
  122. * @param slackAppIntegration
  123. */
  124. async generateClientBySlackAppIntegration(slackAppIntegration: { tokenGtoP: string }): Promise<WebClient> {
  125. this.isCheckTypeValid();
  126. // connect to proxy
  127. const proxyServerUri = this.configManager.getConfig('crowi', 'slackbot:proxyUri');
  128. const serverUri = new URL('/g2s', proxyServerUri);
  129. const headers = {
  130. 'x-growi-gtop-tokens': slackAppIntegration.tokenGtoP,
  131. };
  132. return generateWebClient(undefined, serverUri.toString(), headers);
  133. }
  134. async postMessage(messageArgs: ChatPostMessageArguments, slackAppIntegration?: { tokenGtoP: string; }): Promise<void> {
  135. // use legacy slack configuration
  136. if (this.isSlackLegacyConfigured && !this.isSlackbotConfigured) {
  137. return this.postMessageWithLegacyUtil(messageArgs);
  138. }
  139. const client = slackAppIntegration == null
  140. ? await this.generateClientForPrimaryWorkspace()
  141. : await this.generateClientBySlackAppIntegration(slackAppIntegration);
  142. try {
  143. await client.chat.postMessage(messageArgs);
  144. }
  145. catch (error) {
  146. logger.debug('Post error', error);
  147. logger.debug('Sent data to slack is:', messageArgs);
  148. throw error;
  149. }
  150. }
  151. private async postMessageWithLegacyUtil(messageArgs: ChatPostMessageArguments | IncomingWebhookSendArguments): Promise<void> {
  152. const slackLegacyUtil = require('../util/slack-legacy')(this.crowi);
  153. try {
  154. await slackLegacyUtil.postMessage(messageArgs);
  155. }
  156. catch (error) {
  157. logger.debug('Post error', error);
  158. logger.debug('Sent data to slack is:', messageArgs);
  159. throw error;
  160. }
  161. }
  162. /**
  163. * Handle /commands endpoint
  164. */
  165. async handleCommandRequest(command, client, body, ...opt) {
  166. let module;
  167. try {
  168. module = `./slack-command-handler/${command}`;
  169. }
  170. catch (err) {
  171. await this.notCommand(client, body);
  172. }
  173. try {
  174. const handler = require(module)(this.crowi);
  175. await handler.handleCommand(client, body, ...opt);
  176. }
  177. catch (err) {
  178. throw err;
  179. }
  180. }
  181. async handleBlockActionsRequest(client, payload) {
  182. const { action_id: actionId } = payload.actions[0];
  183. const commandName = actionId.split(':')[0];
  184. const handlerMethodName = actionId.split(':')[1];
  185. const module = `./slack-command-handler/${commandName}`;
  186. try {
  187. const handler = require(module)(this.crowi);
  188. await handler.handleBlockActions(client, payload, handlerMethodName);
  189. }
  190. catch (err) {
  191. throw err;
  192. }
  193. return;
  194. }
  195. async handleViewSubmissionRequest(client, payload) {
  196. const { callback_id: callbackId } = payload.view;
  197. const commandName = callbackId.split(':')[0];
  198. const handlerMethodName = callbackId.split(':')[1];
  199. const module = `./slack-command-handler/${commandName}`;
  200. try {
  201. const handler = require(module)(this.crowi);
  202. await handler.handleBlockActions(client, payload, handlerMethodName);
  203. }
  204. catch (err) {
  205. throw err;
  206. }
  207. return;
  208. }
  209. async notCommand(client, body) {
  210. logger.error('Invalid first argument');
  211. client.chat.postEphemeral({
  212. channel: body.channel_id,
  213. user: body.user_id,
  214. text: 'No command',
  215. blocks: [
  216. markdownSectionBlock('*No command.*\n Hint\n `/growi [command] [keyword]`'),
  217. ],
  218. });
  219. return;
  220. }
  221. }