slack-integration.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. import loggerFactory from '~/utils/logger';
  2. const express = require('express');
  3. const mongoose = require('mongoose');
  4. const urljoin = require('url-join');
  5. const { verifySlackRequest, generateWebClient, getSupportedGrowiActionsRegExps } = require('@growi/slack');
  6. const logger = loggerFactory('growi:routes:apiv3:slack-integration');
  7. const router = express.Router();
  8. const SlackAppIntegration = mongoose.model('SlackAppIntegration');
  9. const { respondIfSlackbotError } = require('../../service/slack-command-handler/respond-if-slackbot-error');
  10. module.exports = (crowi) => {
  11. this.app = crowi.express;
  12. const { configManager, slackIntegrationService } = crowi;
  13. // Check if the access token is correct
  14. async function verifyAccessTokenFromProxy(req, res, next) {
  15. const tokenPtoG = req.headers['x-growi-ptog-tokens'];
  16. if (tokenPtoG == null) {
  17. const message = 'The value of header \'x-growi-ptog-tokens\' must not be empty.';
  18. logger.warn(message, { body: req.body });
  19. return res.status(400).send({ message });
  20. }
  21. const slackAppIntegrationCount = await SlackAppIntegration.countDocuments({ tokenPtoG });
  22. logger.debug('verifyAccessTokenFromProxy', {
  23. tokenPtoG,
  24. slackAppIntegrationCount,
  25. });
  26. if (slackAppIntegrationCount === 0) {
  27. return res.status(403).send({
  28. message: 'The access token that identifies the request source is slackbot-proxy is invalid. Did you setup with `/growi register`.\n'
  29. + 'Or did you delete registration for GROWI ? if so, the link with GROWI has been disconnected. '
  30. + 'Please unregister the information registered in the proxy and setup `/growi register` again.',
  31. });
  32. }
  33. next();
  34. }
  35. async function checkCommandPermission(req, res, next) {
  36. const tokenPtoG = req.headers['x-growi-ptog-tokens'];
  37. const relation = await SlackAppIntegration.findOne({ tokenPtoG });
  38. const { supportedCommandsForBroadcastUse, supportedCommandsForSingleUse } = relation;
  39. const supportedCommands = supportedCommandsForBroadcastUse.concat(supportedCommandsForSingleUse);
  40. const supportedGrowiActionsRegExps = getSupportedGrowiActionsRegExps(supportedCommands);
  41. // get command name from req.body
  42. let command = '';
  43. let actionId = '';
  44. let callbackId = '';
  45. let payload;
  46. if (req.body.payload) {
  47. payload = JSON.parse(req.body.payload);
  48. }
  49. if (req.body.text == null && !payload) { // when /relation-test
  50. return next();
  51. }
  52. if (!payload) { // when request is to /commands
  53. command = req.body.text.split(' ')[0];
  54. }
  55. else if (payload.actions) { // when request is to /interactions && block_actions
  56. actionId = payload.actions[0].action_id;
  57. }
  58. else { // when request is to /interactions && view_submission
  59. callbackId = payload.view.callback_id;
  60. }
  61. let isActionSupported = false;
  62. supportedGrowiActionsRegExps.forEach((regexp) => {
  63. if (regexp.test(actionId) || regexp.test(callbackId)) {
  64. isActionSupported = true;
  65. }
  66. });
  67. // validate
  68. if (command && !supportedCommands.includes(command)) {
  69. return res.status(403).send(`It is not allowed to run '${command}' command to this GROWI.`);
  70. }
  71. if ((actionId || callbackId) && !isActionSupported) {
  72. return res.status(403).send(`It is not allowed to run '${command}' command to this GROWI.`);
  73. }
  74. next();
  75. }
  76. const addSigningSecretToReq = (req, res, next) => {
  77. req.slackSigningSecret = configManager.getConfig('crowi', 'slackbot:signingSecret');
  78. return next();
  79. };
  80. async function handleCommands(req, res, client) {
  81. const { body } = req;
  82. if (body.text == null) {
  83. return 'No text.';
  84. }
  85. /*
  86. * TODO: use parseSlashCommand
  87. */
  88. // Send response immediately to avoid opelation_timeout error
  89. // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
  90. res.json({
  91. response_type: 'ephemeral',
  92. text: 'Processing your request ...',
  93. });
  94. const args = body.text.split(' ');
  95. const command = args[0];
  96. try {
  97. await crowi.slackIntegrationService.handleCommandRequest(command, client, body, args);
  98. }
  99. catch (err) {
  100. await respondIfSlackbotError(client, body, err);
  101. }
  102. }
  103. router.post('/commands', addSigningSecretToReq, verifySlackRequest, async(req, res) => {
  104. const client = await slackIntegrationService.generateClientForCustomBotWithoutProxy();
  105. return handleCommands(req, res, client);
  106. });
  107. router.post('/proxied/commands', verifyAccessTokenFromProxy, checkCommandPermission, async(req, res) => {
  108. const { body } = req;
  109. // eslint-disable-next-line max-len
  110. // see: https://api.slack.com/apis/connections/events-api#the-events-api__subscribing-to-event-types__events-api-request-urls__request-url-configuration--verification
  111. if (body.type === 'url_verification') {
  112. return res.send({ challenge: body.challenge });
  113. }
  114. const tokenPtoG = req.headers['x-growi-ptog-tokens'];
  115. const client = await slackIntegrationService.generateClientByTokenPtoG(tokenPtoG);
  116. return handleCommands(req, res, client);
  117. });
  118. async function handleInteractions(req, res, client) {
  119. // Send response immediately to avoid opelation_timeout error
  120. // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
  121. res.send();
  122. const payload = JSON.parse(req.body.payload);
  123. const { type } = payload;
  124. try {
  125. switch (type) {
  126. case 'block_actions':
  127. try {
  128. await crowi.slackIntegrationService.handleBlockActionsRequest(client, payload);
  129. }
  130. catch (err) {
  131. await respondIfSlackbotError(client, req.body, err);
  132. }
  133. break;
  134. case 'view_submission':
  135. try {
  136. await crowi.slackIntegrationService.handleViewSubmissionRequest(client, payload);
  137. }
  138. catch (err) {
  139. await respondIfSlackbotError(client, req.body, err);
  140. }
  141. break;
  142. default:
  143. break;
  144. }
  145. }
  146. catch (error) {
  147. logger.error(error);
  148. }
  149. }
  150. router.post('/interactions', addSigningSecretToReq, verifySlackRequest, async(req, res) => {
  151. const client = await slackIntegrationService.generateClientForCustomBotWithoutProxy();
  152. return handleInteractions(req, res, client);
  153. });
  154. router.post('/proxied/interactions', verifyAccessTokenFromProxy, checkCommandPermission, async(req, res) => {
  155. const tokenPtoG = req.headers['x-growi-ptog-tokens'];
  156. const client = await slackIntegrationService.generateClientByTokenPtoG(tokenPtoG);
  157. return handleInteractions(req, res, client);
  158. });
  159. router.get('/supported-commands', verifyAccessTokenFromProxy, async(req, res) => {
  160. const tokenPtoG = req.headers['x-growi-ptog-tokens'];
  161. const slackAppIntegration = await SlackAppIntegration.findOne({ tokenPtoG });
  162. return res.send(slackAppIntegration);
  163. });
  164. return router;
  165. };