Просмотр исходного кода

Merge branch 'feat/growi-bot' into feat/GW-5798-enable-display-mulutiple-ws-names-and-growi-app-names

Shun Miyazawa 5 лет назад
Родитель
Сommit
06b85b479f

+ 1 - 0
packages/slack/package.json

@@ -22,6 +22,7 @@
   },
   "devDependencies": {
     "@slack/bolt": "^3.3.0",
+    "@types/express": "^4.17.11",
     "@types/jest": "^26.0.22",
     "@typescript-eslint/eslint-plugin": "^4.18.0",
     "@typescript-eslint/parser": "^4.18.0",

+ 3 - 1
packages/slack/src/index.ts

@@ -8,8 +8,10 @@ export const supportedGrowiCommands: string[] = [
 ];
 
 export * from './interfaces/growi-command';
+export * from './interfaces/request-from-slack';
 export * from './models/errors';
-export * from './middlewares/verification-slack-request';
+export * from './middlewares/verify-slack-request';
 export * from './utils/block-creater';
+export * from './utils/post-ephemeral-errors';
 export * from './utils/slash-command-parser';
 export * from './utils/webclient-factory';

+ 9 - 0
packages/slack/src/interfaces/request-from-slack.ts

@@ -0,0 +1,9 @@
+import { Request } from 'express';
+
+export type RequestFromSlack = Request & {
+  // appended by slack
+  headers:{'x-slack-signature'?:string, 'x-slack-request-timestamp':number},
+
+  // appended by GROWI or slackbot-proxy
+  slackSigningSecret?:string,
+};

+ 13 - 15
packages/slack/src/middlewares/verification-slack-request.ts → packages/slack/src/middlewares/verify-slack-request.ts

@@ -1,19 +1,18 @@
 import { createHmac, timingSafeEqual } from 'crypto';
 import { stringify } from 'qs';
-import { Request, Response, NextFunction } from 'express';
+import { Response, NextFunction } from 'express';
+
+import { RequestFromSlack } from '../interfaces/request-from-slack';
+
 /**
-   * Verify if the request came from slack
-   * See: https://api.slack.com/authentication/verifying-requests-from-slack
-   */
-
-type signingSecretType = {
-  signingSecret?:string; headers:{'x-slack-signature'?:string, 'x-slack-request-timestamp':number}
-}
-
-// eslint-disable-next-line max-len
-export const verificationSlackRequest = (req : Request & signingSecretType, res:Response, next:NextFunction):Record<string, any>| void => {
-  if (req.signingSecret == null) {
-    return res.send('No signing secret.');
+ * Verify if the request came from slack
+ * See: https://api.slack.com/authentication/verifying-requests-from-slack
+ */
+export const verifySlackRequest = (req: RequestFromSlack, res: Response, next: NextFunction): Record<string, any> | void => {
+  const signingSecret = req.slackSigningSecret;
+
+  if (signingSecret == null) {
+    return res.status(400).send({ message: 'No signing secret.' });
   }
 
   // take out slackSignature and timestamp from header
@@ -32,7 +31,7 @@ export const verificationSlackRequest = (req : Request & signingSecretType, res:
 
   // generate growi signature
   const sigBaseString = `v0:${timestamp}:${stringify(req.body, { format: 'RFC1738' })}`;
-  const hasher = createHmac('sha256', req.signingSecret);
+  const hasher = createHmac('sha256', signingSecret);
   hasher.update(sigBaseString, 'utf8');
   const hashedSigningSecret = hasher.digest('hex');
   const growiSignature = `v0=${hashedSigningSecret}`;
@@ -40,7 +39,6 @@ export const verificationSlackRequest = (req : Request & signingSecretType, res:
   // compare growiSignature and slackSignature
   if (timingSafeEqual(Buffer.from(growiSignature, 'utf8'), Buffer.from(slackSignature, 'utf8'))) {
     return next();
-
   }
 
   return res.send('Verification failed.');

+ 40 - 0
packages/slack/src/utils/post-ephemeral-errors.ts

@@ -0,0 +1,40 @@
+import { WebAPICallResult } from '@slack/web-api';
+
+import { generateMarkdownSectionBlock } from './block-creater';
+import { generateWebClient } from './webclient-factory';
+
+export const postEphemeralErrors = async(
+  rejectedResults: PromiseRejectedResult[],
+  channelId: string,
+  userId: string,
+  botToken: string,
+): Promise<WebAPICallResult|void> => {
+
+  if (rejectedResults.length > 0) {
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    const client = generateWebClient(botToken);
+
+    return client.chat.postEphemeral({
+      text: 'Error occured.',
+      channel: channelId,
+      user: userId,
+      blocks: [
+        generateMarkdownSectionBlock('*Error occured:*'),
+        ...rejectedResults.map((rejectedResult) => {
+          const reason = rejectedResult.reason.toString();
+          const resData = rejectedResult.reason.response?.data;
+          const resDataMessage = resData?.message || resData?.toString();
+
+          let errorMessage = reason;
+          if (resDataMessage != null) {
+            errorMessage += ` (${resDataMessage})`;
+          }
+
+          return generateMarkdownSectionBlock(errorMessage);
+        }),
+      ],
+    });
+  }
+
+  return;
+};

+ 30 - 79
packages/slackbot-proxy/src/controllers/slack.ts

@@ -4,22 +4,27 @@ import {
 
 import axios from 'axios';
 
-import { generateMarkdownSectionBlock, generateWebClient, parseSlashCommand } from '@growi/slack';
-import { Installation } from '~/entities/installation';
+import { WebAPICallResult } from '@slack/web-api';
 
+import {
+  generateMarkdownSectionBlock, parseSlashCommand, postEphemeralErrors, verifySlackRequest,
+} from '@growi/slack';
+
+import { Relation } from '~/entities/relation';
+import { AuthedReq } from '~/interfaces/authorized-req';
 import { InstallationRepository } from '~/repositories/installation';
 import { RelationRepository } from '~/repositories/relation';
 import { OrderRepository } from '~/repositories/order';
+import { AddSigningSecretToReq } from '~/middlewares/add-signing-secret-to-req';
+import { AuthorizeCommandMiddleware, AuthorizeInteractionMiddleware } from '~/middlewares/authorizer';
 import { InstallerService } from '~/services/InstallerService';
 import { RegisterService } from '~/services/RegisterService';
-
 import loggerFactory from '~/utils/logger';
-import { AuthorizeCommandMiddleware, AuthorizeInteractionMiddleware } from '~/middlewares/authorizer';
-import { AuthedReq } from '~/interfaces/authorized-req';
-import { Relation } from '~/entities/relation';
+
 
 const logger = loggerFactory('slackbot-proxy:controllers:slack');
 
+
 @Controller('/slack')
 export class SlackCtrl {
 
@@ -38,25 +43,6 @@ export class SlackCtrl {
   @Inject()
   registerService: RegisterService;
 
-  @Get('/testsave')
-  testsave(): void {
-    const installation = new Installation();
-    installation.data = {
-      team: undefined,
-      enterprise: undefined,
-      user: {
-        id: '',
-        token: undefined,
-        scopes: undefined,
-      },
-    };
-
-    // const installationRepository = getRepository(Installation);
-
-    this.installationRepository.save(installation);
-  }
-
-
   @Get('/install')
   async install(): Promise<string> {
     const url = await this.installerService.installer.generateInstallUrl({
@@ -78,8 +64,8 @@ export class SlackCtrl {
   }
 
   @Post('/commands')
-  @UseBefore(AuthorizeCommandMiddleware)
-  async handleCommand(@Req() req: AuthedReq, @Res() res: Res): Promise<void|string|Res> {
+  @UseBefore(AddSigningSecretToReq, verifySlackRequest, AuthorizeCommandMiddleware)
+  async handleCommand(@Req() req: AuthedReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
     const { body, authorizeResult } = req;
 
     if (body.text == null) {
@@ -120,7 +106,7 @@ export class SlackCtrl {
 
     const promises = relations.map((relation: Relation) => {
       // generate API URL
-      const url = new URL('/_api/v3/slack-bot/commands', relation.growiUri);
+      const url = new URL('/_api/v3/slack-integration/proxied/commands', relation.growiUri);
       return axios.post(url.toString(), {
         ...body,
         tokenPtoG: relation.tokenPtoG,
@@ -131,27 +117,14 @@ export class SlackCtrl {
     // pickup PromiseRejectedResult only
     const results = await Promise.allSettled(promises);
     const rejectedResults: PromiseRejectedResult[] = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected');
+    const botToken = installation?.data.bot?.token;
 
-    if (rejectedResults.length > 0) {
-      const botToken = installation?.data.bot?.token;
-
+    try {
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      const client = generateWebClient(botToken!);
-
-      try {
-        client.chat.postEphemeral({
-          text: 'Error occured.',
-          channel: body.channel_id,
-          user: body.user_id,
-          blocks: [
-            generateMarkdownSectionBlock('*Error occured:*'),
-            ...rejectedResults.map(result => generateMarkdownSectionBlock(result.reason.toString())),
-          ],
-        });
-      }
-      catch (err) {
-        logger.error(err);
-      }
+      return postEphemeralErrors(rejectedResults, body.channel_id, body.user_id, botToken!);
+    }
+    catch (err) {
+      logger.error(err);
     }
   }
 
@@ -176,43 +149,21 @@ export class SlackCtrl {
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     const installation = await this.installationRepository.findByTeamIdOrEnterpriseId(installationId!);
 
-    const handleViewSubmission = async(inputValues) => {
-
-      const inputGrowiUrl = inputValues.growiDomain.contents_input.value;
-      const inputGrowiAccessToken = inputValues.growiAccessToken.contents_input.value;
-      const inputProxyAccessToken = inputValues.proxyToken.contents_input.value;
-
-      const order = await this.orderRepository.findOne({ installation: installation?.id, growiUrl: inputGrowiUrl });
-      if (order != null) {
-        this.orderRepository.update(
-          { installation: installation?.id, growiUrl: inputGrowiUrl },
-          { growiAccessToken: inputGrowiAccessToken, proxyAccessToken: inputProxyAccessToken },
-        );
-      }
-      else {
-        this.orderRepository.save({
-          installation: installation?.id, growiUrl: inputGrowiUrl, growiAccessToken: inputGrowiAccessToken, proxyAccessToken: inputProxyAccessToken,
-        });
-      }
-    };
-
     const payload = JSON.parse(body.payload);
     const { type } = payload;
-    const inputValues = payload.view.state.values;
 
-    try {
-      switch (type) {
-        case 'view_submission':
-          await handleViewSubmission(inputValues);
-          break;
-        default:
-          break;
-      }
-    }
-    catch (error) {
-      logger.error(error);
+    // register
+    if (type === 'view_submission' && payload.response_urls[0].action_id === 'submit_growi_url_and_access_tokens') {
+      await this.registerService.upsertOrderRecord(this.orderRepository, installation, payload);
+      await this.registerService.showProxyUrl(authorizeResult, payload);
+      return;
     }
 
+    /*
+     * forward to GROWI server
+     */
+    // TODO: forward to GROWI server by GW-5866
+
   }
 
   @Post('/events')

+ 14 - 0
packages/slackbot-proxy/src/middlewares/add-signing-secret-to-req.ts

@@ -0,0 +1,14 @@
+import { RequestFromSlack } from '@growi/slack';
+import {
+  IMiddleware, Middleware, Next, Req, Res,
+} from '@tsed/common';
+
+@Middleware()
+export class AddSigningSecretToReq implements IMiddleware {
+
+  use(@Req() req: Req & RequestFromSlack, @Res() res: Res, @Next() next: Next): void {
+    req.slackSigningSecret = process.env.SLACK_SIGNING_SECRET;
+    next();
+  }
+
+}

+ 69 - 3
packages/slackbot-proxy/src/services/RegisterService.ts

@@ -1,9 +1,10 @@
 import { Service } from '@tsed/di';
 import { WebClient, LogLevel } from '@slack/web-api';
-import { generateInputSectionBlock, GrowiCommand } from '@growi/slack';
+import { generateInputSectionBlock, GrowiCommand, generateMarkdownSectionBlock } from '@growi/slack';
 import { AuthorizeResult } from '@slack/oauth';
-
 import { GrowiCommandProcessor } from '~/interfaces/growi-command-processor';
+import { OrderRepository } from '~/repositories/order';
+import { Installation } from '~/entities/installation';
 
 
 const isProduction = process.env.NODE_ENV === 'production';
@@ -12,7 +13,6 @@ const isProduction = process.env.NODE_ENV === 'production';
 export class RegisterService implements GrowiCommandProcessor {
 
   async process(growiCommand: GrowiCommand, authorizeResult: AuthorizeResult, body: {[key:string]:string}): Promise<void> {
-
     const { botToken } = authorizeResult;
 
     // tmp use process.env
@@ -37,9 +37,75 @@ export class RegisterService implements GrowiCommandProcessor {
           generateInputSectionBlock('growiDomain', 'GROWI domain', 'contents_input', false, 'https://example.com'),
           generateInputSectionBlock('growiAccessToken', 'GROWI ACCESS_TOKEN', 'contents_input', false, 'jBMZvpk.....'),
           generateInputSectionBlock('proxyToken', 'PROXY ACCESS_TOKEN', 'contents_input', false, 'jBMZvpk.....'),
+          {
+            block_id: 'channel_to_post_proxy_url',
+            type: 'input',
+            label: {
+              type: 'plain_text',
+              text: 'Select a channel to post the proxy URL on',
+            },
+            element: {
+              action_id: 'submit_growi_url_and_access_tokens',
+              type: 'conversations_select',
+              response_url_enabled: true,
+              default_to_current_conversation: true,
+            },
+          },
         ],
       },
     });
   }
 
+  async upsertOrderRecord(
+      // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+      orderRepository: OrderRepository, installation: Installation | undefined, payload: any,
+  ): Promise<void> {
+    const inputValues = payload.view.state.values;
+    const inputGrowiUrl = inputValues.growiDomain.contents_input.value;
+    const inputGrowiAccessToken = inputValues.growiAccessToken.contents_input.value;
+    const inputProxyAccessToken = inputValues.proxyToken.contents_input.value;
+
+    const order = await orderRepository.findOne({ installation: installation?.id, growiUrl: inputGrowiUrl });
+    if (order != null) {
+      orderRepository.update(
+        { installation: installation?.id, growiUrl: inputGrowiUrl },
+        { growiAccessToken: inputGrowiAccessToken, proxyAccessToken: inputProxyAccessToken },
+      );
+    }
+    else {
+      orderRepository.save({
+        installation: installation?.id, growiUrl: inputGrowiUrl, growiAccessToken: inputGrowiAccessToken, proxyAccessToken: inputProxyAccessToken,
+      });
+    }
+  }
+
+  async showProxyUrl(
+      // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+      authorizeResult:AuthorizeResult, payload: any,
+  ): Promise<void> {
+
+    // TODO: implement for when proxy URL is undefined by GW-5834
+    let proxyURL;
+    if (process.env.PROXY_URL != null) {
+      proxyURL = process.env.PROXY_URL;
+    }
+
+    const { botToken } = authorizeResult;
+
+    const client = new WebClient(botToken, { logLevel: isProduction ? LogLevel.DEBUG : LogLevel.INFO });
+
+    await client.chat.postEphemeral({
+      channel: payload.response_urls[0].channel_id,
+      user: payload.user.id,
+      // Recommended including 'text' to provide a fallback when using blocks
+      // refer to https://api.slack.com/methods/chat.postEphemeral#text_usage
+      text: 'Proxy URL',
+      blocks: [
+        generateMarkdownSectionBlock('Please enter and update the following Proxy URL to slack bot setting form in your GROWI'),
+        generateMarkdownSectionBlock(`Proxy URL: ${proxyURL}`),
+      ],
+    });
+    return;
+  }
+
 }

+ 10 - 3
src/client/js/components/Admin/Common/AdminNavigation.jsx

@@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import urljoin from 'url-join';
 
+import { pathUtils } from 'growi-commons';
 
 const AdminNavigation = (props) => {
   const { t } = props;
@@ -21,7 +22,7 @@ const AdminNavigation = (props) => {
       case 'importer':                 return <><i className="icon-fw icon-cloud-upload"></i>    { t('Import Data') }</>;
       case 'export':                   return <><i className="icon-fw icon-cloud-download"></i>  { t('Export Archive Data') }</>;
       case 'notification':             return <><i className="icon-fw icon-bell"></i>            { t('External_Notification')}</>;
-      case 'legacy-slack-integration': return <><i className="fa fa-slack mr-2"></i>             { t('Legacy_Slack_Integration')}</>;
+      case 'slack-integration-legacy': return <><i className="fa fa-slack mr-2"></i>             { t('Legacy_Slack_Integration')}</>;
       case 'slack-integration':        return <><i className="fa fa-slack mr-2"></i>             { t('slack_integration') }</>;
       case 'users':                    return <><i className="icon-fw icon-user"></i>            { t('User_Management') }</>;
       case 'user-groups':              return <><i className="icon-fw icon-people"></i>          { t('UserGroup Management') }</>;
@@ -49,7 +50,13 @@ const AdminNavigation = (props) => {
   };
 
   const isActiveMenu = (path) => {
-    return (pathname.startsWith(urljoin('/admin', path)));
+    const basisPath = pathUtils.normalizePath(urljoin('/admin', path));
+    const basisParentPath = pathUtils.addTrailingSlash(basisPath);
+
+    return (
+      pathname === basisPath
+      || pathname.startsWith(basisParentPath)
+    );
   };
 
   const getListGroupItemOrDropdownItemList = (isListGroupItems) => {
@@ -64,7 +71,7 @@ const AdminNavigation = (props) => {
         <MenuLink menu="export"       isListGroupItems isActive={isActiveMenu('/export')} />
         <MenuLink menu="notification" isListGroupItems isActive={isActiveMenu('/notification') || isActiveMenu('/global-notification')} />
         <MenuLink menu="slack-integration" isListGroupItems isActive={isActiveMenu('/slack-integration')} />
-        <MenuLink menu="legacy-slack-integration" isListGroupItems isActive={isActiveMenu('/legacy-slack-integration')} />
+        <MenuLink menu="slack-integration-legacy" isListGroupItems isActive={isActiveMenu('/slack-integration-legacy')} />
         <MenuLink menu="users"        isListGroupItems isActive={isActiveMenu('/users')} />
         <MenuLink menu="user-groups"  isListGroupItems isActive={isActiveMenu('/user-groups')} />
         <MenuLink menu="search"       isListGroupItems isActive={isActiveMenu('/search')} />

+ 3 - 3
src/server/routes/admin.js

@@ -223,9 +223,9 @@ module.exports = function(crowi, app) {
     return res.render('admin/external-accounts');
   };
 
-  actions.legacySlackIntegration = {};
-  actions.legacySlackIntegration = function(req, res) {
-    return res.render('admin/legacy-slack-integration');
+  actions.slackIntegrationLegacy = {};
+  actions.slackIntegrationLegacy = function(req, res) {
+    return res.render('admin/slack-integration-legacy');
   };
 
   actions.slackIntegration = {};

+ 1 - 1
src/server/routes/apiv3/index.js

@@ -46,8 +46,8 @@ module.exports = (crowi) => {
   router.use('/bookmarks', require('./bookmarks')(crowi));
   router.use('/attachment', require('./attachment')(crowi));
 
-  router.use('/slack-bot', require('./slack-bot')(crowi));
   router.use('/slack-integration', require('./slack-integration')(crowi));
+  router.use('/slack-integration-legacy', require('./slack-integration-legacy')(crowi));
   router.use('/staffs', require('./staffs')(crowi));
 
   return router;

+ 0 - 135
src/server/routes/apiv3/slack-bot.js

@@ -1,135 +0,0 @@
-const express = require('express');
-
-const loggerFactory = require('@alias/logger');
-
-const logger = loggerFactory('growi:routes:apiv3:slack-bot');
-
-const router = express.Router();
-const { verificationSlackRequest } = require('@growi/slack');
-
-module.exports = (crowi) => {
-  this.app = crowi.express;
-
-  // Check if the access token is correct
-  function verificationAccessToken(req, res, next) {
-    const botType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
-    if (botType === 'customBotWithoutProxy') {
-      return next();
-    }
-    const slackBotAccessToken = req.body.slack_bot_access_token || null;
-
-    if (slackBotAccessToken == null || slackBotAccessToken !== this.crowi.configManager.getConfig('crowi', 'slackbot:access-token')) {
-      logger.error('slack_bot_access_token is invalid.');
-      return res.send('*Access token is inValid*');
-    }
-
-    return next();
-  }
-
-  function verificationRequestUrl(req, res, next) {
-    // for verification request URL on Event Subscriptions
-    if (req.body.type === 'url_verification') {
-      return res.send(req.body);
-    }
-
-    return next();
-  }
-
-  const addSlackBotSigningSecret = (req, res, next) => {
-    req.signingSecret = crowi.configManager.getConfig('crowi', 'slackbot:signingSecret');
-    return next();
-  };
-
-  router.post('/commands', verificationRequestUrl, addSlackBotSigningSecret, verificationSlackRequest, verificationAccessToken, async(req, res) => {
-
-    // Send response immediately to avoid opelation_timeout error
-    // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
-    res.send();
-
-    const { body } = req;
-    const args = body.text.split(' ');
-    const command = args[0];
-
-    try {
-      switch (command) {
-        case 'search':
-          await crowi.slackBotService.showEphemeralSearchResults(body, args);
-          break;
-        case 'create':
-          await crowi.slackBotService.createModal(body);
-          break;
-        default:
-          await crowi.slackBotService.notCommand(body);
-          break;
-      }
-    }
-    catch (error) {
-      logger.error(error);
-      return res.send(error.message);
-    }
-  });
-
-  const handleBlockActions = async(payload) => {
-    const { action_id: actionId } = payload.actions[0];
-
-    switch (actionId) {
-      case 'shareSearchResults': {
-        await crowi.slackBotService.shareSearchResults(payload);
-        break;
-      }
-      case 'showNextResults': {
-        const parsedValue = JSON.parse(payload.actions[0].value);
-
-        const { body, args, offset } = parsedValue;
-        const newOffset = offset + 10;
-        await crowi.slackBotService.showEphemeralSearchResults(body, args, newOffset);
-        break;
-      }
-      default:
-        break;
-    }
-  };
-
-  const handleViewSubmission = async(payload) => {
-    const { callback_id: callbackId } = payload.view;
-
-    switch (callbackId) {
-      case 'createPage':
-        await crowi.slackBotService.createPageInGrowi(payload);
-        break;
-      default:
-        break;
-    }
-  };
-
-  router.post('/interactions', verificationRequestUrl, addSlackBotSigningSecret, verificationSlackRequest, async(req, res) => {
-
-    // Send response immediately to avoid opelation_timeout error
-    // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
-    res.send();
-
-    const payload = JSON.parse(req.body.payload);
-    const { type } = payload;
-
-    try {
-      switch (type) {
-        case 'block_actions':
-          await handleBlockActions(payload);
-          break;
-        case 'view_submission':
-          await handleViewSubmission(payload);
-          break;
-        default:
-          break;
-      }
-    }
-    catch (error) {
-      logger.error(error);
-      return res.send(error.message);
-    }
-
-  });
-
-
-  return router;
-};

+ 354 - 0
src/server/routes/apiv3/slack-integration-legacy.js

@@ -0,0 +1,354 @@
+const loggerFactory = require('@alias/logger');
+
+const logger = loggerFactory('growi:routes:apiv3:notification-setting');
+const express = require('express');
+const { body } = require('express-validator');
+const crypto = require('crypto');
+const { WebClient, LogLevel } = require('@slack/web-api');
+const ErrorV3 = require('../../models/vo/error-apiv3');
+
+const router = express.Router();
+
+/**
+ * @swagger
+ *  tags:
+ *    name: SlackIntegration
+ */
+
+/**
+ * @swagger
+ *
+ *  components:
+ *    schemas:
+ *      CustomBotWithoutProxy:
+ *        description: CustomBotWithoutProxy
+ *        type: object
+ *        properties:
+ *          slackSigningSecret:
+ *            type: string
+ *          slackBotToken:
+ *            type: string
+ *          currentBotType:
+ *            type: string
+ *      SlackIntegration:
+ *        description: SlackIntegration
+ *        type: object
+ *        properties:
+ *          currentBotType:
+ *            type: string
+ */
+
+
+module.exports = (crowi) => {
+  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
+  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
+  const adminRequired = require('../../middlewares/admin-required')(crowi);
+  const csrf = require('../../middlewares/csrf')(crowi);
+  const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
+
+  const validator = {
+    CustomBotWithoutProxy: [
+      body('slackSigningSecret').isString(),
+      body('slackBotToken').isString(),
+      body('currentBotType').isString(),
+    ],
+    SlackIntegration: [
+      body('currentBotType')
+        .isIn(['officialBot', 'customBotWithoutProxy', 'customBotWithProxy']),
+    ],
+    NotificationTestToSlackWorkSpace: [
+      body('channel').trim().not().isEmpty()
+        .isString(),
+    ],
+  };
+
+  async function updateSlackBotSettings(params) {
+    const { configManager } = crowi;
+    // update config without publishing S2sMessage
+    return configManager.updateConfigsInTheSameNamespace('crowi', params, true);
+  }
+
+
+  function generateAccessToken(user) {
+    const hasher = crypto.createHash('sha512');
+    hasher.update(new Date().getTime() + user._id);
+
+    return hasher.digest('base64');
+  }
+
+  /**
+   * @swagger
+   *
+   *    /slack-integration/:
+   *      get:
+   *        tags: [SlackBotSettingParams]
+   *        operationId: getSlackBotSettingParams
+   *        summary: get /slack-integration
+   *        description: Get slackBot setting params.
+   *        responses:
+   *          200:
+   *            description: Succeeded to get slackBot setting params.
+   */
+  router.get('/', accessTokenParser, loginRequiredStrictly, adminRequired, async(req, res) => {
+    const slackBotSettingParams = {
+      accessToken: crowi.configManager.getConfig('crowi', 'slackbot:access-token'),
+      currentBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType'),
+      // TODO impl when creating official bot
+      officialBotSettings: {
+        // TODO impl this after GW-4939
+        // AccessToken: "tempaccessdatahogehoge",
+      },
+      customBotWithoutProxySettings: {
+        // TODO impl this after GW-4939
+        // AccessToken: "tempaccessdatahogehoge",
+        slackSigningSecretEnvVars: crowi.configManager.getConfigFromEnvVars('crowi', 'slackbot:signingSecret'),
+        slackBotTokenEnvVars: crowi.configManager.getConfigFromEnvVars('crowi', 'slackbot:token'),
+        slackSigningSecret: crowi.configManager.getConfig('crowi', 'slackbot:signingSecret'),
+        slackBotToken: crowi.configManager.getConfig('crowi', 'slackbot:token'),
+        isConnectedToSlack: crowi.slackBotService.isConnectedToSlack,
+      },
+      // TODO imple when creating with proxy
+      customBotWithProxySettings: {
+        // TODO impl this after GW-4939
+        // AccessToken: "tempaccessdatahogehoge",
+      },
+    };
+    return res.apiv3({ slackBotSettingParams });
+  });
+
+  /**
+   * @swagger
+   *
+   *    /slack-integration/:
+   *      put:
+   *        tags: [SlackIntegration]
+   *        operationId: putSlackIntegration
+   *        summary: put /slack-integration
+   *        description: Put SlackIntegration setting.
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/SlackIntegration'
+   *        responses:
+   *           200:
+   *             description: Succeeded to put Slack Integration setting.
+   */
+  router.put('/',
+    accessTokenParser, loginRequiredStrictly, adminRequired, csrf, validator.SlackIntegration, apiV3FormValidator, async(req, res) => {
+      const { currentBotType } = req.body;
+
+      const requestParams = {
+        'slackbot:currentBotType': currentBotType,
+      };
+
+      try {
+        await updateSlackBotSettings(requestParams);
+
+        // initialize slack service
+        await crowi.slackBotService.initialize();
+        crowi.slackBotService.publishUpdatedMessage();
+
+        const slackIntegrationSettingsParams = {
+          currentBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType'),
+        };
+        return res.apiv3({ slackIntegrationSettingsParams });
+      }
+      catch (error) {
+        const msg = 'Error occured in updating Slack bot setting';
+        logger.error('Error', error);
+        return res.apiv3Err(new ErrorV3(msg, 'update-SlackIntegrationSetting-failed'), 500);
+      }
+    });
+
+  /**
+   * @swagger
+   *
+   *    /slack-integration/custom-bot-without-proxy/:
+   *      put:
+   *        tags: [CustomBotWithoutProxy]
+   *        operationId: putCustomBotWithoutProxy
+   *        summary: /slack-integration/custom-bot-without-proxy
+   *        description: Put customBotWithoutProxy setting.
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/CustomBotWithoutProxy'
+   *        responses:
+   *           200:
+   *             description: Succeeded to put CustomBotWithoutProxy setting.
+   */
+  router.put('/custom-bot-without-proxy',
+    accessTokenParser, loginRequiredStrictly, adminRequired, csrf, validator.CustomBotWithoutProxy, apiV3FormValidator, async(req, res) => {
+      const { slackSigningSecret, slackBotToken, currentBotType } = req.body;
+      const requestParams = {
+        'slackbot:signingSecret': slackSigningSecret,
+        'slackbot:token': slackBotToken,
+        'slackbot:currentBotType': currentBotType,
+      };
+      try {
+        await updateSlackBotSettings(requestParams);
+
+        // initialize slack service
+        await crowi.slackBotService.initialize();
+        crowi.slackBotService.publishUpdatedMessage();
+
+        // TODO Impl to delete AccessToken both of Proxy and GROWI when botType changes.
+        const customBotWithoutProxySettingParams = {
+          slackSigningSecret: crowi.configManager.getConfig('crowi', 'slackbot:signingSecret'),
+          slackBotToken: crowi.configManager.getConfig('crowi', 'slackbot:token'),
+          slackBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType'),
+        };
+        return res.apiv3({ customBotWithoutProxySettingParams });
+      }
+      catch (error) {
+        const msg = 'Error occured in updating Custom bot setting';
+        logger.error('Error', error);
+        return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
+      }
+    });
+
+  /**
+   * @swagger
+   *
+   *    /slack-integration/custom-bot-without-proxy/slack-workspace-name:
+   *      get:
+   *        tags: [slackWorkSpaceName]
+   *        operationId: getSlackWorkSpaceName
+   *        summary: Get slack work space name for custom bot without proxy
+   *        description: get slack WS name in custom bot without proxy
+   *        responses:
+   *          200:
+   *            description: Succeeded to get slack ws name for custom bot without proxy
+   */
+  router.get('/custom-bot-without-proxy/slack-workspace-name', loginRequiredStrictly, adminRequired, async(req, res) => {
+
+    try {
+      const slackWorkSpaceName = await crowi.slackBotService.getSlackChannelName();
+      return res.apiv3({ slackWorkSpaceName });
+    }
+    catch (error) {
+      let msg = 'Error occured in slack_bot_token';
+      if (error.data.error === 'missing_scope') {
+        msg = 'missing_scope';
+      }
+      logger.error('Error', error);
+      return res.apiv3Err(new ErrorV3(msg, 'get-SlackWorkSpaceName-failed'), 500);
+    }
+
+  });
+
+  /**
+   * @swagger
+   *
+   *    /slack-integration/access-token:
+   *      put:
+   *        tags: [SlackIntegration]
+   *        operationId: getCustomBotSetting
+   *        summary: /slack-integration
+   *        description: Generate accessToken
+   *        responses:
+   *          200:
+   *            description: Succeeded to update access token for slack
+   */
+  router.put('/access-token', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
+
+    try {
+      const accessToken = generateAccessToken(req.user);
+      await updateSlackBotSettings({ 'slackbot:access-token': accessToken });
+
+      // initialize slack service
+      await crowi.slackBotService.initialize();
+      crowi.slackBotService.publishUpdatedMessage();
+
+      return res.apiv3({ accessToken });
+    }
+    catch (error) {
+      const msg = 'Error occured in updating access token for access token';
+      logger.error('Error', error);
+      return res.apiv3Err(new ErrorV3(msg, 'update-accessToken-failed'), 500);
+    }
+  });
+
+  /**
+   * @swagger
+   *
+   *    /slack-integration/test-notification-to-slack-work-space:
+   *      post:
+   *        tags: [SlackTestToWorkSpace]
+   *        operationId: postSlackMessageToSlackWorkSpace
+   *        summary: test to send message to slack work space
+   *        description: post message to slack work space
+   *        responses:
+   *          200:
+   *            description: Succeeded to send a message to slack work space
+   */
+  router.post('/notification-test-to-slack-work-space',
+    loginRequiredStrictly, adminRequired, csrf, validator.NotificationTestToSlackWorkSpace, apiV3FormValidator, async(req, res) => {
+      const isConnectedToSlack = crowi.slackBotService.isConnectedToSlack;
+      const { channel } = req.body;
+
+      if (isConnectedToSlack === false) {
+        const msg = 'Bot User OAuth Token is not setup.';
+        logger.error('Error', msg);
+        return res.apiv3Err(new ErrorV3(msg, 'not-setup-slack-bot-token', 400));
+      }
+
+      const slackBotToken = crowi.configManager.getConfig('crowi', 'slackbot:token');
+      this.client = new WebClient(slackBotToken, { logLevel: LogLevel.DEBUG });
+      logger.debug('SlackBot: setup is done');
+
+      try {
+        await this.client.chat.postMessage({
+          channel: `#${channel}`,
+          text: 'Your test was successful!',
+        });
+        logger.info(`SlackTest: send success massage to slack work space at #${channel}.`);
+        logger.info(`If you do not receive a message, you may not have invited the bot to the #${channel} channel.`);
+        // eslint-disable-next-line max-len
+        const message = `Successfully send message to Slack work space. See #general channel. If you do not receive a message, you may not have invited the bot to the #${channel} channel.`;
+        return res.apiv3({ message });
+      }
+      catch (error) {
+        const msg = `Error: ${error.data.error}. Needed:${error.data.needed}`;
+        logger.error('Error', error);
+        return res.apiv3Err(new ErrorV3(msg, 'notification-test-slack-work-space-failed'), 500);
+      }
+    });
+
+  /**
+   * @swagger
+   *
+   *    /slack-integration/access-token:
+   *      delete:
+   *        tags: [SlackIntegration]
+   *        operationId: deleteAccessTokenForSlackBot
+   *        summary: /slack-integration
+   *        description: Delete accessToken
+   *        responses:
+   *          200:
+   *            description: Succeeded to delete accessToken
+   */
+  router.delete('/access-token', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
+
+    try {
+      await updateSlackBotSettings({ 'slackbot:access-token': null });
+
+      // initialize slack service
+      await crowi.slackBotService.initialize();
+      crowi.slackBotService.publishUpdatedMessage();
+
+      return res.apiv3({});
+    }
+    catch (error) {
+      const msg = 'Error occured in discard of slackbotAccessToken';
+      logger.error('Error', error);
+      return res.apiv3Err(new ErrorV3(msg, 'discard-slackbotAccessToken-failed'), 500);
+    }
+  });
+
+  return router;
+};

+ 121 - 312
src/server/routes/apiv3/slack-integration.js

@@ -1,353 +1,162 @@
+const express = require('express');
+
 const loggerFactory = require('@alias/logger');
 
-const logger = loggerFactory('growi:routes:apiv3:notification-setting');
-const express = require('express');
-const { body } = require('express-validator');
-const crypto = require('crypto');
-const { WebClient, LogLevel } = require('@slack/web-api');
-const ErrorV3 = require('../../models/vo/error-apiv3');
+const { verifySlackRequest } = require('@growi/slack');
 
+const logger = loggerFactory('growi:routes:apiv3:slack-integration');
 const router = express.Router();
 
-/**
- * @swagger
- *  tags:
- *    name: SlackIntegration
- */
-
-/**
- * @swagger
- *
- *  components:
- *    schemas:
- *      CustomBotWithoutProxy:
- *        description: CustomBotWithoutProxy
- *        type: object
- *        properties:
- *          slackSigningSecret:
- *            type: string
- *          slackBotToken:
- *            type: string
- *          currentBotType:
- *            type: string
- *      SlackIntegration:
- *        description: SlackIntegration
- *        type: object
- *        properties:
- *          currentBotType:
- *            type: string
- */
+module.exports = (crowi) => {
+  this.app = crowi.express;
 
+  const { configManager } = crowi;
 
-module.exports = (crowi) => {
-  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
-  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
-  const adminRequired = require('../../middlewares/admin-required')(crowi);
-  const csrf = require('../../middlewares/csrf')(crowi);
-  const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
-
-  const validator = {
-    CustomBotWithoutProxy: [
-      body('slackSigningSecret').isString(),
-      body('slackBotToken').isString(),
-      body('currentBotType').isString(),
-    ],
-    SlackIntegration: [
-      body('currentBotType')
-        .isIn(['officialBot', 'customBotWithoutProxy', 'customBotWithProxy']),
-    ],
-    NotificationTestToSlackWorkSpace: [
-      body('channel').trim().not().isEmpty()
-        .isString(),
-    ],
-  };
+  // Check if the access token is correct
+  function verifyAccessTokenFromProxy(req, res, next) {
+    const { body } = req;
+    const { tokenPtoG } = body;
 
-  async function updateSlackBotSettings(params) {
-    const { configManager } = crowi;
-    // update config without publishing S2sMessage
-    return configManager.updateConfigsInTheSameNamespace('crowi', params, true);
-  }
+    const correctToken = configManager.getConfig('crowi', 'slackbot:access-token');
 
+    logger.debug('verifyAccessTokenFromProxy', {
+      tokenPtoG,
+      correctToken,
+    });
 
-  function generateAccessToken(user) {
-    const hasher = crypto.createHash('sha512');
-    hasher.update(new Date().getTime() + user._id);
+    if (tokenPtoG == null || tokenPtoG !== correctToken) {
+      return res.status(403).send({ message: 'The access token that identifies the request source is slackbot-proxy is invalid.' });
+    }
 
-    return hasher.digest('base64');
+    next();
   }
 
-  /**
-   * @swagger
-   *
-   *    /slack-integration/:
-   *      get:
-   *        tags: [SlackBotSettingParams]
-   *        operationId: getSlackBotSettingParams
-   *        summary: get /slack-integration
-   *        description: Get slackBot setting params.
-   *        responses:
-   *          200:
-   *            description: Succeeded to get slackBot setting params.
-   */
-  router.get('/', accessTokenParser, loginRequiredStrictly, adminRequired, async(req, res) => {
-    const slackBotSettingParams = {
-      accessToken: crowi.configManager.getConfig('crowi', 'slackbot:access-token'),
-      currentBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType'),
-      // TODO impl when creating official bot
-      officialBotSettings: {
-        // TODO impl this after GW-4939
-        // AccessToken: "tempaccessdatahogehoge",
-      },
-      customBotWithoutProxySettings: {
-        // TODO impl this after GW-4939
-        // AccessToken: "tempaccessdatahogehoge",
-        slackSigningSecretEnvVars: crowi.configManager.getConfigFromEnvVars('crowi', 'slackbot:signingSecret'),
-        slackBotTokenEnvVars: crowi.configManager.getConfigFromEnvVars('crowi', 'slackbot:token'),
-        slackSigningSecret: crowi.configManager.getConfig('crowi', 'slackbot:signingSecret'),
-        slackBotToken: crowi.configManager.getConfig('crowi', 'slackbot:token'),
-        isConnectedToSlack: crowi.slackBotService.isConnectedToSlack,
-      },
-      // TODO imple when creating with proxy
-      customBotWithProxySettings: {
-        // TODO impl this after GW-4939
-        // AccessToken: "tempaccessdatahogehoge",
-      },
-    };
-    return res.apiv3({ slackBotSettingParams });
-  });
+  const addSigningSecretToReq = (req, res, next) => {
+    req.slackSigningSecret = configManager.getConfig('crowi', 'slackbot:signingSecret');
+    return next();
+  };
 
-  /**
-   * @swagger
-   *
-   *    /slack-integration/:
-   *      put:
-   *        tags: [SlackIntegration]
-   *        operationId: putSlackIntegration
-   *        summary: put /slack-integration
-   *        description: Put SlackIntegration setting.
-   *        requestBody:
-   *          required: true
-   *          content:
-   *            application/json:
-   *              schema:
-   *                $ref: '#/components/schemas/SlackIntegration'
-   *        responses:
-   *           200:
-   *             description: Succeeded to put Slack Integration setting.
-   */
-  router.put('/',
-    accessTokenParser, loginRequiredStrictly, adminRequired, csrf, validator.SlackIntegration, apiV3FormValidator, async(req, res) => {
-      const { currentBotType } = req.body;
-
-      const requestParams = {
-        'slackbot:currentBotType': currentBotType,
-      };
-
-      try {
-        await updateSlackBotSettings(requestParams);
-
-        // initialize slack service
-        await crowi.slackBotService.initialize();
-        crowi.slackBotService.publishUpdatedMessage();
-
-        const slackIntegrationSettingsParams = {
-          currentBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType'),
-        };
-        return res.apiv3({ slackIntegrationSettingsParams });
-      }
-      catch (error) {
-        const msg = 'Error occured in updating Slack bot setting';
-        logger.error('Error', error);
-        return res.apiv3Err(new ErrorV3(msg, 'update-SlackIntegrationSetting-failed'), 500);
-      }
-    });
+  async function handleCommands(req, res) {
+    const { body } = req;
 
-  /**
-   * @swagger
-   *
-   *    /slack-integration/custom-bot-without-proxy/:
-   *      put:
-   *        tags: [CustomBotWithoutProxy]
-   *        operationId: putCustomBotWithoutProxy
-   *        summary: /slack-integration/custom-bot-without-proxy
-   *        description: Put customBotWithoutProxy setting.
-   *        requestBody:
-   *          required: true
-   *          content:
-   *            application/json:
-   *              schema:
-   *                $ref: '#/components/schemas/CustomBotWithoutProxy'
-   *        responses:
-   *           200:
-   *             description: Succeeded to put CustomBotWithoutProxy setting.
-   */
-  router.put('/custom-bot-without-proxy',
-    accessTokenParser, loginRequiredStrictly, adminRequired, csrf, validator.CustomBotWithoutProxy, apiV3FormValidator, async(req, res) => {
-      const { slackSigningSecret, slackBotToken, currentBotType } = req.body;
-      const requestParams = {
-        'slackbot:signingSecret': slackSigningSecret,
-        'slackbot:token': slackBotToken,
-        'slackbot:currentBotType': currentBotType,
-      };
-      try {
-        await updateSlackBotSettings(requestParams);
-
-        // initialize slack service
-        await crowi.slackBotService.initialize();
-        crowi.slackBotService.publishUpdatedMessage();
-
-        // TODO Impl to delete AccessToken both of Proxy and GROWI when botType changes.
-        const customBotWithoutProxySettingParams = {
-          slackSigningSecret: crowi.configManager.getConfig('crowi', 'slackbot:signingSecret'),
-          slackBotToken: crowi.configManager.getConfig('crowi', 'slackbot:token'),
-          slackBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType'),
-        };
-        return res.apiv3({ customBotWithoutProxySettingParams });
-      }
-      catch (error) {
-        const msg = 'Error occured in updating Custom bot setting';
-        logger.error('Error', error);
-        return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
-      }
-    });
+    if (body.text == null) {
+      return 'No text.';
+    }
 
-  /**
-   * @swagger
-   *
-   *    /slack-integration/custom-bot-without-proxy/slack-workspace-name:
-   *      get:
-   *        tags: [slackWorkSpaceName]
-   *        operationId: getSlackWorkSpaceName
-   *        summary: Get slack work space name for custom bot without proxy
-   *        description: get slack WS name in custom bot without proxy
-   *        responses:
-   *          200:
-   *            description: Succeeded to get slack ws name for custom bot without proxy
-   */
-  router.get('/custom-bot-without-proxy/slack-workspace-name', loginRequiredStrictly, adminRequired, async(req, res) => {
+    /*
+     * TODO: use parseSlashCommand
+     */
+
+    // Send response immediately to avoid opelation_timeout error
+    // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
+    res.send();
+
+    const args = body.text.split(' ');
+    const command = args[0];
 
     try {
-      const slackWorkSpaceName = await crowi.slackBotService.getSlackChannelName();
-      return res.apiv3({ slackWorkSpaceName });
+      switch (command) {
+        case 'search':
+          await crowi.slackBotService.showEphemeralSearchResults(body, args);
+          break;
+        case 'create':
+          await crowi.slackBotService.createModal(body);
+          break;
+        default:
+          await crowi.slackBotService.notCommand(body);
+          break;
+      }
     }
     catch (error) {
-      let msg = 'Error occured in slack_bot_token';
-      if (error.data.error === 'missing_scope') {
-        msg = 'missing_scope';
-      }
-      logger.error('Error', error);
-      return res.apiv3Err(new ErrorV3(msg, 'get-SlackWorkSpaceName-failed'), 500);
+      logger.error(error);
+      return res.send(error.message);
     }
+  }
 
+  router.post('/commands', addSigningSecretToReq, verifySlackRequest, async(req, res) => {
+    return handleCommands(req, res);
   });
 
-  /**
-   * @swagger
-   *
-   *    /slack-integration/access-token:
-   *      put:
-   *        tags: [SlackIntegration]
-   *        operationId: getCustomBotSetting
-   *        summary: /slack-integration
-   *        description: Generate accessToken
-   *        responses:
-   *          200:
-   *            description: Succeeded to update access token for slack
-   */
-  router.put('/access-token', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
+  router.post('/proxied/commands', verifyAccessTokenFromProxy, async(req, res) => {
+    const { body } = req;
 
-    try {
-      const accessToken = generateAccessToken(req.user);
-      await updateSlackBotSettings({ 'slackbot:access-token': accessToken });
-
-      // initialize slack service
-      await crowi.slackBotService.initialize();
-      crowi.slackBotService.publishUpdatedMessage();
-
-      return res.apiv3({ accessToken });
-    }
-    catch (error) {
-      const msg = 'Error occured in updating access token for access token';
-      logger.error('Error', error);
-      return res.apiv3Err(new ErrorV3(msg, 'update-accessToken-failed'), 500);
+    // eslint-disable-next-line max-len
+    // see: https://api.slack.com/apis/connections/events-api#the-events-api__subscribing-to-event-types__events-api-request-urls__request-url-configuration--verification
+    if (body.type === 'url_verification') {
+      return body.challenge;
     }
+
+    return handleCommands(req, res);
   });
 
-  /**
-   * @swagger
-   *
-   *    /slack-integration/test-notification-to-slack-work-space:
-   *      post:
-   *        tags: [SlackTestToWorkSpace]
-   *        operationId: postSlackMessageToSlackWorkSpace
-   *        summary: test to send message to slack work space
-   *        description: post message to slack work space
-   *        responses:
-   *          200:
-   *            description: Succeeded to send a message to slack work space
-   */
-  router.post('/notification-test-to-slack-work-space',
-    loginRequiredStrictly, adminRequired, csrf, validator.NotificationTestToSlackWorkSpace, apiV3FormValidator, async(req, res) => {
-      const isConnectedToSlack = crowi.slackBotService.isConnectedToSlack;
-      const { channel } = req.body;
-
-      if (isConnectedToSlack === false) {
-        const msg = 'Bot User OAuth Token is not setup.';
-        logger.error('Error', msg);
-        return res.apiv3Err(new ErrorV3(msg, 'not-setup-slack-bot-token', 400));
-      }
 
-      const slackBotToken = crowi.configManager.getConfig('crowi', 'slackbot:token');
-      this.client = new WebClient(slackBotToken, { logLevel: LogLevel.DEBUG });
-      logger.debug('SlackBot: setup is done');
-
-      try {
-        await this.client.chat.postMessage({
-          channel: `#${channel}`,
-          text: 'Your test was successful!',
-        });
-        logger.info(`SlackTest: send success massage to slack work space at #${channel}.`);
-        logger.info(`If you do not receive a message, you may not have invited the bot to the #${channel} channel.`);
-        // eslint-disable-next-line max-len
-        const message = `Successfully send message to Slack work space. See #general channel. If you do not receive a message, you may not have invited the bot to the #${channel} channel.`;
-        return res.apiv3({ message });
+  const handleBlockActions = async(payload) => {
+    const { action_id: actionId } = payload.actions[0];
+
+    switch (actionId) {
+      case 'shareSearchResults': {
+        await crowi.slackBotService.shareSearchResults(payload);
+        break;
       }
-      catch (error) {
-        const msg = `Error: ${error.data.error}. Needed:${error.data.needed}`;
-        logger.error('Error', error);
-        return res.apiv3Err(new ErrorV3(msg, 'notification-test-slack-work-space-failed'), 500);
+      case 'showNextResults': {
+        const parsedValue = JSON.parse(payload.actions[0].value);
+
+        const { body, args, offset } = parsedValue;
+        const newOffset = offset + 10;
+        await crowi.slackBotService.showEphemeralSearchResults(body, args, newOffset);
+        break;
       }
-    });
+      default:
+        break;
+    }
+  };
 
-  /**
-   * @swagger
-   *
-   *    /slack-integration/access-token:
-   *      delete:
-   *        tags: [SlackIntegration]
-   *        operationId: deleteAccessTokenForSlackBot
-   *        summary: /slack-integration
-   *        description: Delete accessToken
-   *        responses:
-   *          200:
-   *            description: Succeeded to delete accessToken
-   */
-  router.delete('/access-token', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
+  const handleViewSubmission = async(payload) => {
+    const { callback_id: callbackId } = payload.view;
 
-    try {
-      await updateSlackBotSettings({ 'slackbot:access-token': null });
+    switch (callbackId) {
+      case 'createPage':
+        await crowi.slackBotService.createPageInGrowi(payload);
+        break;
+      default:
+        break;
+    }
+  };
+
+  async function handleInteractions(req, res) {
 
-      // initialize slack service
-      await crowi.slackBotService.initialize();
-      crowi.slackBotService.publishUpdatedMessage();
+    // Send response immediately to avoid opelation_timeout error
+    // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
+    res.send();
 
-      return res.apiv3({});
+    const payload = JSON.parse(req.body.payload);
+    const { type } = payload;
+
+    try {
+      switch (type) {
+        case 'block_actions':
+          await handleBlockActions(payload);
+          break;
+        case 'view_submission':
+          await handleViewSubmission(payload);
+          break;
+        default:
+          break;
+      }
     }
     catch (error) {
-      const msg = 'Error occured in discard of slackbotAccessToken';
-      logger.error('Error', error);
-      return res.apiv3Err(new ErrorV3(msg, 'discard-slackbotAccessToken-failed'), 500);
+      logger.error(error);
+      return res.send(error.message);
     }
+
+  }
+
+  router.post('/interactions', addSigningSecretToReq, verifySlackRequest, async(req, res) => {
+    return handleInteractions(req, res);
+  });
+
+  router.post('/proxied/interactions', verifyAccessTokenFromProxy, async(req, res) => {
+    return handleInteractions(req, res);
   });
 
   return router;

+ 1 - 1
src/server/routes/index.js

@@ -93,7 +93,7 @@ module.exports = function(crowi, app) {
   app.get('/admin/notification/slackSetting/disconnect' , loginRequiredStrictly , adminRequired , admin.notification.disconnectFromSlack);
   app.get('/admin/global-notification/new'              , loginRequiredStrictly , adminRequired , admin.globalNotification.detail);
   app.get('/admin/global-notification/:id'              , loginRequiredStrictly , adminRequired , admin.globalNotification.detail);
-  app.get('/admin/legacy-slack-integration'         , loginRequiredStrictly , adminRequired,  admin.legacySlackIntegration);
+  app.get('/admin/slack-integration-legacy'             , loginRequiredStrictly , adminRequired,  admin.slackIntegrationLegacy);
   app.get('/admin/slack-integration'                    , loginRequiredStrictly , adminRequired,  admin.slackIntegration);
 
   app.get('/admin/users'                                , loginRequiredStrictly , adminRequired , admin.user.index);

+ 0 - 0
src/server/views/admin/legacy-slack-integration.html → src/server/views/admin/slack-integration-legacy.html