slack-integration.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. const express = require('express');
  2. const mongoose = require('mongoose');
  3. const urljoin = require('url-join');
  4. const loggerFactory = require('@alias/logger');
  5. const { verifySlackRequest, generateWebClient } = require('@growi/slack');
  6. const logger = loggerFactory('growi:routes:apiv3:slack-integration');
  7. const router = express.Router();
  8. const SlackAppIntegration = mongoose.model('SlackAppIntegration');
  9. module.exports = (crowi) => {
  10. this.app = crowi.express;
  11. const { configManager } = crowi;
  12. // Check if the access token is correct
  13. async function verifyAccessTokenFromProxy(req, res, next) {
  14. const tokenPtoG = req.headers['x-growi-ptog-tokens'];
  15. if (tokenPtoG == null) {
  16. const message = 'The value of header \'x-growi-ptog-tokens\' must not be empty.';
  17. logger.warn(message, { body: req.body });
  18. return res.status(400).send({ message });
  19. }
  20. const slackAppIntegrationCount = await SlackAppIntegration.estimatedDocumentCount({ tokenPtoG });
  21. logger.debug('verifyAccessTokenFromProxy', {
  22. tokenPtoG,
  23. });
  24. if (slackAppIntegrationCount === 0) {
  25. return res.status(403).send({
  26. message: 'The access token that identifies the request source is slackbot-proxy is invalid. Did you setup with `/growi register`.\n'
  27. + 'Or did you delete registration for GROWI ? if so, the link with GROWI has been disconnected. '
  28. + 'Please unregister the information registered in the proxy and setup `/growi register` again.',
  29. });
  30. }
  31. next();
  32. }
  33. const addSigningSecretToReq = (req, res, next) => {
  34. req.slackSigningSecret = configManager.getConfig('crowi', 'slackbot:signingSecret');
  35. return next();
  36. };
  37. const generateClientForResponse = (tokenGtoP) => {
  38. const currentBotType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
  39. if (currentBotType == null) {
  40. throw new Error('The config \'SLACK_BOT_TYPE\'(ns: \'crowi\', key: \'slackbot:currentBotType\') must be set.');
  41. }
  42. let token;
  43. // connect directly
  44. if (tokenGtoP == null) {
  45. token = crowi.configManager.getConfig('crowi', 'slackbot:token');
  46. return generateWebClient(token);
  47. }
  48. // connect to proxy
  49. const proxyServerUri = crowi.configManager.getConfig('crowi', 'slackbot:proxyServerUri');
  50. const serverUri = urljoin(proxyServerUri, '/g2s');
  51. const headers = {
  52. 'x-growi-gtop-tokens': tokenGtoP,
  53. };
  54. return generateWebClient(token, serverUri, headers);
  55. };
  56. async function handleCommands(req, res) {
  57. const { body } = req;
  58. if (body.text == null) {
  59. return 'No text.';
  60. }
  61. /*
  62. * TODO: use parseSlashCommand
  63. */
  64. // Send response immediately to avoid opelation_timeout error
  65. // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
  66. res.send();
  67. const tokenPtoG = req.headers['x-growi-ptog-tokens'];
  68. // generate client
  69. let client;
  70. if (tokenPtoG == null) {
  71. client = generateClientForResponse();
  72. }
  73. else {
  74. const slackAppIntegration = await SlackAppIntegration.findOne({ tokenPtoG });
  75. client = generateClientForResponse(slackAppIntegration.tokenGtoP);
  76. }
  77. const args = body.text.split(' ');
  78. const command = args[0];
  79. try {
  80. switch (command) {
  81. case 'search':
  82. await crowi.slackBotService.showEphemeralSearchResults(client, body, args);
  83. break;
  84. case 'create':
  85. await crowi.slackBotService.createModal(client, body);
  86. break;
  87. case 'help':
  88. await crowi.slackBotService.helpCommand(client, body);
  89. break;
  90. default:
  91. await crowi.slackBotService.notCommand(client, body);
  92. break;
  93. }
  94. }
  95. catch (error) {
  96. logger.error(error);
  97. return res.send(error.message);
  98. }
  99. }
  100. router.post('/commands', addSigningSecretToReq, verifySlackRequest, async(req, res) => {
  101. return handleCommands(req, res);
  102. });
  103. router.post('/proxied/commands', verifyAccessTokenFromProxy, async(req, res) => {
  104. const { body } = req;
  105. // eslint-disable-next-line max-len
  106. // see: https://api.slack.com/apis/connections/events-api#the-events-api__subscribing-to-event-types__events-api-request-urls__request-url-configuration--verification
  107. if (body.type === 'url_verification') {
  108. return res.send({ challenge: body.challenge });
  109. }
  110. return handleCommands(req, res);
  111. });
  112. const handleBlockActions = async(client, payload) => {
  113. const { action_id: actionId } = payload.actions[0];
  114. switch (actionId) {
  115. case 'shareSearchResults': {
  116. await crowi.slackBotService.shareSearchResults(client, payload);
  117. break;
  118. }
  119. case 'showNextResults': {
  120. const parsedValue = JSON.parse(payload.actions[0].value);
  121. const { body, args, offset } = parsedValue;
  122. const newOffset = offset + 10;
  123. await crowi.slackBotService.showEphemeralSearchResults(client, body, args, newOffset);
  124. break;
  125. }
  126. default:
  127. break;
  128. }
  129. };
  130. const handleViewSubmission = async(client, payload) => {
  131. const { callback_id: callbackId } = payload.view;
  132. switch (callbackId) {
  133. case 'createPage':
  134. await crowi.slackBotService.createPageInGrowi(client, payload);
  135. break;
  136. default:
  137. break;
  138. }
  139. };
  140. async function handleInteractions(req, res) {
  141. // Send response immediately to avoid opelation_timeout error
  142. // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
  143. res.send();
  144. const tokenPtoG = req.headers['x-growi-ptog-tokens'];
  145. // generate client
  146. let client;
  147. if (tokenPtoG == null) {
  148. client = generateClientForResponse();
  149. }
  150. else {
  151. const slackAppIntegration = await SlackAppIntegration.findOne({ tokenPtoG });
  152. client = generateClientForResponse(slackAppIntegration.tokenGtoP);
  153. }
  154. const payload = JSON.parse(req.body.payload);
  155. const { type } = payload;
  156. try {
  157. switch (type) {
  158. case 'block_actions':
  159. await handleBlockActions(client, payload);
  160. break;
  161. case 'view_submission':
  162. await handleViewSubmission(client, payload);
  163. break;
  164. default:
  165. break;
  166. }
  167. }
  168. catch (error) {
  169. logger.error(error);
  170. return res.send(error.message);
  171. }
  172. }
  173. router.post('/interactions', addSigningSecretToReq, verifySlackRequest, async(req, res) => {
  174. return handleInteractions(req, res);
  175. });
  176. router.post('/proxied/interactions', verifyAccessTokenFromProxy, async(req, res) => {
  177. return handleInteractions(req, res);
  178. });
  179. return router;
  180. };