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

Merge branch 'feat/growi-bot' into imprv/create-reset-button

zahmis 4 лет назад
Родитель
Сommit
c31c80321e
29 измененных файлов с 879 добавлено и 659 удалено
  1. 1 0
      packages/slack/package.json
  2. 3 1
      packages/slack/src/index.ts
  3. 9 0
      packages/slack/src/interfaces/request-from-slack.ts
  4. 13 15
      packages/slack/src/middlewares/verify-slack-request.ts
  5. 40 0
      packages/slack/src/utils/post-ephemeral-errors.ts
  6. 1 0
      packages/slackbot-proxy/docker-compose.dev.yml
  7. 48 85
      packages/slackbot-proxy/src/controllers/slack.ts
  8. 14 0
      packages/slackbot-proxy/src/middlewares/add-signing-secret-to-req.ts
  9. 69 3
      packages/slackbot-proxy/src/services/RegisterService.ts
  10. 10 16
      public/images/slack-integration/slackbot-difficulty-level-easy.svg
  11. 8 10
      public/images/slack-integration/slackbot-difficulty-level-hard.svg
  12. 8 10
      public/images/slack-integration/slackbot-difficulty-level-normal.svg
  13. 2 0
      resource/locales/en_US/admin/admin.json
  14. 2 0
      resource/locales/ja_JP/admin/admin.json
  15. 2 0
      resource/locales/zh_CN/admin/admin.json
  16. 10 3
      src/client/js/components/Admin/Common/AdminNavigation.jsx
  17. 50 27
      src/client/js/components/Admin/SlackIntegration/CustomBotWithProxyIntegrationCard.jsx
  18. 6 2
      src/client/js/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx
  19. 35 5
      src/client/js/components/Admin/SlackIntegration/CustomBotWithProxySettingsAccordion.jsx
  20. 6 6
      src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxyIntegrationCard.jsx
  21. 15 19
      src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  22. 47 4
      src/client/js/components/Admin/SlackIntegration/OfficialbotSettingsAccordion.jsx
  23. 3 3
      src/server/routes/admin.js
  24. 1 1
      src/server/routes/apiv3/index.js
  25. 0 135
      src/server/routes/apiv3/slack-bot.js
  26. 354 0
      src/server/routes/apiv3/slack-integration-legacy.js
  27. 121 313
      src/server/routes/apiv3/slack-integration.js
  28. 1 1
      src/server/routes/index.js
  29. 0 0
      src/server/views/admin/slack-integration-legacy.html

+ 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;
+};

+ 1 - 0
packages/slackbot-proxy/docker-compose.dev.yml

@@ -11,6 +11,7 @@ services:
       - MYSQL_USER=growi-slackbot-proxy
       - MYSQL_PASSWORD=YrkUi7rCW46Z2N6EXSFUBwaQTUR8biCU
       - MYSQL_DATABASE=growi-slackbot-proxy
+    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
     volumes:
       - /data/db
 

+ 48 - 85
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,24 +64,23 @@ export class SlackCtrl {
   }
 
   @Post('/commands')
-  @UseBefore(AuthorizeCommandMiddleware)
-  async handleCommand(@Req() req: AuthedReq, @Res() res: Res): Promise<void|string> {
+  @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) {
       return 'No text.';
     }
 
-    // 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 growiCommand = parseSlashCommand(body);
 
     // register
     if (growiCommand.growiCommandType === 'register') {
-      await this.registerService.process(growiCommand, authorizeResult, body as {[key:string]:string});
-      return;
+      // 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 this.registerService.process(growiCommand, authorizeResult, body as {[key:string]:string});
     }
 
     /*
@@ -106,9 +91,22 @@ export class SlackCtrl {
     const installation = await this.installationRepository.findByTeamIdOrEnterpriseId(installationId!);
     const relations = await this.relationRepository.find({ installation: installation?.id });
 
+    if (relations.length === 0) {
+      return res.json({
+        blocks: [
+          generateMarkdownSectionBlock('*No relation found.*'),
+          generateMarkdownSectionBlock('Run `/growi register` first.'),
+        ],
+      });
+    }
+
+    // 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 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,
@@ -119,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);
     }
   }
 
@@ -164,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 - 16
public/images/slack-integration/slackbot-difficulty-level-easy.svg

@@ -1,36 +1,30 @@
 <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 60 60">
   <defs>
     <style>
-      .cls-1, .cls-4 {
-        fill: none;
-      }
-
       .cls-1 {
+        fill: #fff;
         stroke: #81d5b8;
         stroke-width: 3px;
       }
 
       .cls-2 {
-        font-family: NotoSansCJKjp-Bold, Noto Sans CJK JP;
-        font-size: 16px;
-        font-weight: 700;
         fill: #81d5b8;
       }
 
       .cls-3 {
         stroke: none;
       }
+
+      .cls-4 {
+        fill: none;
+      }
     </style>
   </defs>
-  <g id="Group_4361" data-name="Group 4361" transform="translate(-71.69 -49.04)">
-    <g id="Group_4358" data-name="Group 4358">
-      <g id="Group_4357" data-name="Group 4357">
-        <g id="Ellipse_97" data-name="Ellipse 97" class="cls-1" transform="translate(71.69 49.04)">
-          <circle class="cls-3" cx="30" cy="30" r="30"/>
-          <circle class="cls-4" cx="30" cy="30" r="28.5"/>
-        </g>
-        <text id="EASY" class="cls-2" transform="translate(83.884 89.632) rotate(-11)"><tspan x="0" y="0">EASY</tspan></text>
-      </g>
+  <g id="Group_4362" data-name="Group 4362" transform="translate(-538 -44)">
+    <g id="Ellipse_101" data-name="Ellipse 101" class="cls-1" transform="translate(538 44)">
+      <circle class="cls-3" cx="30" cy="30" r="30"/>
+      <circle class="cls-4" cx="30" cy="30" r="28.5"/>
     </g>
+    <path id="Path_708" data-name="Path 708" class="cls-2" d="M1.456,0H8.9V-1.984H3.824V-5.152h4.16V-7.136H3.824V-9.872h4.9V-11.84H1.456ZM13.52-4.88l.352-1.3c.352-1.232.7-2.576,1.008-3.872h.064c.352,1.28.672,2.64,1.04,3.872l.352,1.3ZM17.68,0h2.48L16.352-11.84H13.568L9.776,0h2.4l.832-3.04h3.84Zm7.408.224c2.736,0,4.352-1.648,4.352-3.584a3.271,3.271,0,0,0-2.384-3.216L25.5-7.232c-1.008-.4-1.856-.7-1.856-1.552,0-.784.672-1.248,1.712-1.248a3.777,3.777,0,0,1,2.512.976l1.2-1.488a5.254,5.254,0,0,0-3.712-1.52c-2.4,0-4.1,1.488-4.1,3.424a3.43,3.43,0,0,0,2.4,3.184l1.584.672c1.056.448,1.776.72,1.776,1.6,0,.832-.656,1.36-1.888,1.36a4.658,4.658,0,0,1-3.008-1.312L20.768-1.5A6.309,6.309,0,0,0,25.088.224ZM33.232,0H35.6V-4.336l3.568-7.5H36.7L35.52-8.96c-.336.88-.688,1.712-1.056,2.624H34.4c-.368-.912-.688-1.744-1.024-2.624l-1.184-2.88H29.68l3.552,7.5Z" transform="translate(549.872 85.316) rotate(-11)"/>
   </g>
 </svg>

+ 8 - 10
public/images/slack-integration/slackbot-difficulty-level-hard.svg

@@ -1,32 +1,30 @@
 <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 60 60">
   <defs>
     <style>
-      .cls-1, .cls-4 {
-        fill: none;
-      }
-
       .cls-1 {
+        fill: #fff;
         stroke: #ff8080;
         stroke-width: 3px;
       }
 
       .cls-2 {
         fill: #ff8080;
-        font-size: 16px;
-        font-family: NotoSansCJKjp-Bold, Noto Sans CJK JP;
-        font-weight: 700;
       }
 
       .cls-3 {
         stroke: none;
       }
+
+      .cls-4 {
+        fill: none;
+      }
     </style>
   </defs>
-  <g id="Group_4360" data-name="Group 4360" transform="translate(-303.305 -23.039)">
-    <g id="Ellipse_99" data-name="Ellipse 99" class="cls-1" transform="translate(303.305 23.039)">
+  <g id="Group_4364" data-name="Group 4364" transform="translate(-782.69 -44.039)">
+    <g id="Ellipse_103" data-name="Ellipse 103" class="cls-1" transform="translate(782.69 44.039)">
       <circle class="cls-3" cx="30" cy="30" r="30"/>
       <circle class="cls-4" cx="30" cy="30" r="28.5"/>
     </g>
-    <text id="HARD" class="cls-2" transform="translate(312.93 64.277) rotate(-11)"><tspan x="0" y="0">HARD</tspan></text>
+    <path id="Path_710" data-name="Path 710" class="cls-2" d="M1.456,0H3.824V-5.12H8.3V0h2.352V-11.84H8.3v4.656H3.824V-11.84H1.456ZM15.792-4.88l.352-1.3c.352-1.232.7-2.576,1.008-3.872h.064c.352,1.28.672,2.64,1.04,3.872l.352,1.3ZM19.952,0h2.48L18.624-11.84H15.84L12.048,0h2.4l.832-3.04h3.84Zm6.24-9.968h1.536c1.52,0,2.352.432,2.352,1.712,0,1.264-.832,1.9-2.352,1.9H26.192ZM32.912,0,30.144-4.848A3.389,3.389,0,0,0,32.4-8.256c0-2.72-1.968-3.584-4.448-3.584H23.824V0h2.368V-4.48H27.84L30.272,0Zm1.824,0h3.376C41.6,0,43.84-1.984,43.84-5.968c0-4-2.24-5.872-5.856-5.872H34.736ZM37.1-1.9V-9.952h.736c2.208,0,3.584,1.088,3.584,3.984,0,2.88-1.376,4.064-3.584,4.064Z" transform="translate(792.315 85.277) rotate(-11)"/>
   </g>
 </svg>

+ 8 - 10
public/images/slack-integration/slackbot-difficulty-level-normal.svg

@@ -1,32 +1,30 @@
 <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 60 60">
   <defs>
     <style>
-      .cls-1, .cls-4 {
-        fill: none;
-      }
-
       .cls-1 {
+        fill: #fff;
         stroke: #ffce60;
         stroke-width: 3px;
       }
 
       .cls-2 {
         fill: #ffce60;
-        font-size: 11px;
-        font-family: NotoSansCJKjp-Bold, Noto Sans CJK JP;
-        font-weight: 700;
       }
 
       .cls-3 {
         stroke: none;
       }
+
+      .cls-4 {
+        fill: none;
+      }
     </style>
   </defs>
-  <g id="Group_4359" data-name="Group 4359" transform="translate(-163.69 -44.039)">
-    <g id="Ellipse_98" data-name="Ellipse 98" class="cls-1" transform="translate(163.69 44.039)">
+  <g id="Group_4363" data-name="Group 4363" transform="translate(-664.69 -44.039)">
+    <g id="Ellipse_102" data-name="Ellipse 102" class="cls-1" transform="translate(664.69 44.039)">
       <circle class="cls-3" cx="30" cy="30" r="30"/>
       <circle class="cls-4" cx="30" cy="30" r="28.5"/>
     </g>
-    <text id="NORMAL" class="cls-2" transform="translate(171.836 83.813) rotate(-11)"><tspan x="0" y="0">NORMAL</tspan></text>
+    <path id="Path_709" data-name="Path 709" class="cls-2" d="M1,0h1.54V-3.267c0-.935-.121-1.958-.2-2.838H2.4l.825,1.749L5.577,0h1.65V-8.14H5.687v3.245c0,.924.121,2,.209,2.849H5.841l-.814-1.76L2.662-8.14H1ZM12.474.154c2.156,0,3.641-1.617,3.641-4.257S14.63-8.294,12.474-8.294,8.833-6.754,8.833-4.1,10.318.154,12.474.154Zm0-1.408c-1.21,0-1.98-1.111-1.98-2.849s.77-2.794,1.98-2.794,1.98,1.045,1.98,2.794S13.684-1.254,12.474-1.254Zm6.864-5.6h1.056c1.045,0,1.617.3,1.617,1.177s-.572,1.309-1.617,1.309H19.338ZM23.958,0l-1.9-3.333a2.33,2.33,0,0,0,1.551-2.343c0-1.87-1.353-2.464-3.058-2.464H17.71V0h1.628V-3.08h1.133L22.143,0Zm1.254,0h1.463V-3.4c0-.77-.132-1.9-.209-2.673h.044l.649,1.914L28.424-.737h.935l1.254-3.421.66-1.914h.044c-.077.77-.2,1.9-.2,2.673V0H32.6V-8.14H30.8L29.447-4.334c-.176.506-.319,1.045-.5,1.573H28.9c-.165-.528-.319-1.067-.495-1.573L27.016-8.14h-1.8ZM36.135-3.355l.242-.891c.242-.847.484-1.771.693-2.662h.044c.242.88.462,1.815.715,2.662l.242.891ZM38.995,0H40.7L38.082-8.14H36.168L33.561,0h1.65l.572-2.09h2.64Zm2.662,0h4.928V-1.364h-3.3V-8.14H41.657Z" transform="translate(672.836 83.813) rotate(-11)"/>
   </g>
 </svg>

+ 2 - 0
resource/locales/en_US/admin/admin.json

@@ -296,7 +296,9 @@
     "accordion": {
       "create_bot": "Create Bot",
       "how_to_create_a_bot": "How to create a bot",
+      "how_to_install": "How to install",
       "install_bot_to_slack": "Install Bot To Slack",
+      "install_now": "Install now",
       "select_install_your_app": "Select \"Install your app\".",
       "select_install_to_workspace": "Select \"Install to Workspace\".",
       "register_official_bot_proxy_service": "Issue Access Token / Register GROWI Official Bot Proxy Service",

+ 2 - 0
resource/locales/ja_JP/admin/admin.json

@@ -293,7 +293,9 @@
     "accordion": {
       "create_bot": "Bot を作成する",
       "how_to_create_a_bot": "作成方法はこちら",
+      "how_to_install": "インストール方法はこちら",
       "install_bot_to_slack": "Bot を Slack にインストールする",
+      "install_now": "今すぐインストール",
       "select_install_your_app": "Install your app をクリックします。",
       "select_install_to_workspace": "Install to Workspace をクリックします。",
       "register_official_bot_proxy_service": "アクセストークンの発行 / GROWI Official Bot Proxy サービスへの登録",

+ 2 - 0
resource/locales/zh_CN/admin/admin.json

@@ -303,7 +303,9 @@
     "accordion": {
       "create_bot": "创建 Bot",
       "how_to_create_a_bot": "如何创建一个 Bot",
+      "how_to_install": "点击这里查看安装说明",
       "install_bot_to_slack": "将 Bot 安装到 Slack",
+      "install_now": "现在安装",
       "select_install_your_app": "选择 \"Install your app\"。",
       "select_install_to_workspace": "选择 \"Install to Workspace\"。",
       "register_official_bot_proxy_service": "发行访问令牌 / 注册 GROWI 官方 Bot 代理服务",

+ 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')} />

+ 50 - 27
src/client/js/components/Admin/SlackIntegration/CustomBotWithProxyIntegrationCard.jsx

@@ -1,52 +1,75 @@
 import React from 'react';
 import { useTranslation } from 'react-i18next';
+import PropTypes from 'prop-types';
 
-const CustomBotWithProxyIntegrationCard = () => {
-
+const CustomBotWithProxyIntegrationCard = (props) => {
   const { t } = useTranslation();
 
   return (
-    <>
-
-      <div className="d-flex justify-content-center my-5 bot-integration">
+    <div className="d-flex justify-content-center my-5 bot-integration">
 
-        <div className="card rounded shadow border-0 w-50 admin-bot-card">
-          <h5 className="card-title font-weight-bold mt-3 ml-4">Slack</h5>
-          <div className="card-body p-4"></div>
+      <div className="card rounded shadow border-0 w-50 admin-bot-card">
+        <h5 className="card-title font-weight-bold mt-3 ml-4">Slack</h5>
+        <div className="card-body px-5">
+          {props.slackWSNameInWithProxy != null && (
+            <div className="card slack-work-space-name-card">
+              <div className="m-2 text-center">
+                <h5 className="font-weight-bold">{props.slackWSNameInWithProxy}</h5>
+                <img width={20} height={20} src="/images/slack-integration/growi-bot-kun-icon.png" />
+              </div>
+            </div>
+          )}
         </div>
+      </div>
 
-        <div className="text-center w-25">
+      <div className="text-center w-25">
+        {props.isSlackScopeSet && (
+          <p className="text-success small">
+            <i className="fa fa-check mr-1" />
+            {t('admin:slack_integration.integration_sentence.integration_successful')}
+          </p>
+        )}
+        {!props.isSlackScopeSet && (
           <small
             className="text-secondary"
             // eslint-disable-next-line react/no-danger
             dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.integration_sentence.integration_is_not_complete') }}
           />
-          <div className="pt-2">
-            <div className="position-relative mt-5">
-              <div className="circle position-absolute bg-primary border-light">
-                <p className="circle-inner text-light font-weight-bold">Proxy Server</p>
-              </div>
-              <hr className="align-self-center admin-border-danger border-danger"></hr>
+        )}
+        <div className="pt-2">
+          <div className="position-relative mt-5">
+            <div className="circle position-absolute bg-primary border-light">
+              <p className="circle-inner text-light font-weight-bold">Proxy Server</p>
             </div>
+            {props.isSlackScopeSet && (
+              <hr className="align-self-center border-success admin-border-success"></hr>
+            )}
+            {!props.isSlackScopeSet && (
+              <hr className="align-self-center border-danger admin-border-danger"></hr>
+            )}
           </div>
         </div>
+      </div>
 
-        <div className="card rounded-lg shadow border-0 w-50 admin-bot-card">
-          <div className="row">
-            <h5 className="card-title font-weight-bold mt-3 ml-4 col">GROWI App</h5>
-            <div className="pull-right mt-3 mr-3">
-              <a className="icon-fw fa fa-repeat fa-2x"></a>
-            </div>
-          </div>
-          <div className="card-body p-4 mb-5 text-center">
-            <a className="btn btn-primary">WESEEK Inner Wiki</a>
+      <div className="card rounded-lg shadow border-0 w-50 admin-bot-card">
+        <div className="row">
+          <h5 className="card-title font-weight-bold mt-3 ml-4 col">GROWI App</h5>
+          <div className="pull-right mt-3 mr-3">
+            <a className="icon-fw fa fa-repeat fa-2x"></a>
           </div>
         </div>
-
+        <div className="card-body p-4 mb-5 text-center">
+          <a className="btn btn-primary">{props.siteName}</a>
+        </div>
       </div>
-
-    </>
+    </div>
   );
 };
 
+CustomBotWithProxyIntegrationCard.propTypes = {
+  siteName: PropTypes.string.isRequired,
+  slackWSNameInWithProxy: PropTypes.string,
+  isSlackScopeSet: PropTypes.bool.isRequired,
+};
+
 export default CustomBotWithProxyIntegrationCard;

+ 6 - 2
src/client/js/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx

@@ -15,10 +15,14 @@ const CustomBotWithProxySettings = (props) => {
   return (
     <>
 
-      {/* TODO: GW-5768 */}
       <h2 className="admin-setting-header">{t('admin:slack_integration.custom_bot_with_proxy_integration')}</h2>
 
-      <CustomBotWithProxyIntegrationCard />
+      {/* TODO delete tmp props */}
+      <CustomBotWithProxyIntegrationCard
+        siteName="GROWI"
+        slackWSNameInWithProxy="SlackWorkSpaceName"
+        isSlackScopeSet
+      />
 
       <div className="my-5 mx-3">
         <CustomBotWithProxySettingsAccordion />

+ 35 - 5
src/client/js/components/Admin/SlackIntegration/CustomBotWithProxySettingsAccordion.jsx

@@ -1,19 +1,49 @@
 import React from 'react';
+import { useTranslation } from 'react-i18next';
 import Accordion from '../Common/Accordion';
 
 const CustomBotWithProxySettingsAccordion = () => {
-
+  const { t } = useTranslation();
   return (
     <div className="card border-0 rounded-lg shadow overflow-hidden">
       <Accordion
-        title={<><span className="mr-2">①</span>First Accordion</>}
+        title={<><span className="mr-2">①</span>{t('admin:slack_integration.accordion.create_bot')}</>}
       >
-        1
+        <div className="my-5 d-flex flex-column align-items-center">
+          <button type="button" className="btn btn-primary text-nowrap" onClick={() => window.open('https://api.slack.com/apps', '_blank')}>
+            {t('admin:slack_integration.accordion.create_bot')}
+            <i className="fa fa-external-link ml-2" aria-hidden="true" />
+          </button>
+          {/* TODO: Insert DOCS link */}
+          <a href="#">
+            <p className="text-center mt-1">
+              <small>
+                {t('admin:slack_integration.accordion.how_to_create_a_bot')}
+                <i className="fa fa-external-link ml-2" aria-hidden="true" />
+              </small>
+            </p>
+          </a>
+        </div>
       </Accordion>
       <Accordion
-        title={<><span className="mr-2">②</span>Second Accordion</>}
+        title={<><span className="mr-2">②</span>{t('admin:slack_integration.accordion.install_bot_to_slack')}</>}
       >
-        2
+        <div className="my-5 d-flex flex-column align-items-center">
+          {/* TODO: Insert install link */}
+          <button type="button" className="btn btn-primary text-nowrap" onClick={() => window.open('https://api.slack.com/apps', '_blank')}>
+            {t('admin:slack_integration.accordion.install_now')}
+            <i className="fa fa-external-link ml-2" aria-hidden="true" />
+          </button>
+          {/* TODO: Insert DOCS link */}
+          <a href="#">
+            <p className="text-center mt-1">
+              <small>
+                {t('admin:slack_integration.accordion.how_to_install')}
+                <i className="fa fa-external-link ml-2" aria-hidden="true" />
+              </small>
+            </p>
+          </a>
+        </div>
       </Accordion>
       <Accordion
         title={<><span className="mr-2">③</span>Third Accordion</>}

+ 6 - 6
src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxyIntegrationCard.jsx

@@ -12,13 +12,13 @@ const CustomBotWithoutProxyIntegrationCard = (props) => {
         <h5 className="card-title font-weight-bold mt-3 ml-4">Slack</h5>
         <div className="card-body p-2 w-50 mx-auto">
           {props.slackWSNameInWithoutProxy != null && (
-          <div className="card slack-work-space-name-card">
-            <div className="m-2 text-center">
-              <h5 className="font-weight-bold">{props.slackWSNameInWithoutProxy}</h5>
-              <img width={20} height={20} src="/images/slack-integration/growi-bot-kun-icon.png" />
+            <div className="card slack-work-space-name-card">
+              <div className="m-2 text-center">
+                <h5 className="font-weight-bold">{props.slackWSNameInWithoutProxy}</h5>
+                <img width={20} height={20} src="/images/slack-integration/growi-bot-kun-icon.png" />
+              </div>
             </div>
-          </div>
-            )}
+          )}
         </div>
       </div>
 

+ 15 - 19
src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx

@@ -104,24 +104,20 @@ const CustomBotWithoutProxySettingsAccordion = ({
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.CREATE_BOT)}
         title={<><span className="mr-2">①</span>{t('admin:slack_integration.accordion.create_bot')}</>}
       >
-        <div className="row my-5">
-          <div className="mx-auto">
-            <div>
-              <button type="button" className="btn btn-primary text-nowrap mx-1" onClick={() => window.open('https://api.slack.com/apps', '_blank')}>
-                {t('admin:slack_integration.accordion.create_bot')}
+        <div className="my-5 d-flex flex-column align-items-center">
+          <button type="button" className="btn btn-primary text-nowrap" onClick={() => window.open('https://api.slack.com/apps', '_blank')}>
+            {t('admin:slack_integration.accordion.create_bot')}
+            <i className="fa fa-external-link ml-2" aria-hidden="true" />
+          </button>
+          {/* TODO: Insert DOCS link */}
+          <a href="#">
+            <p className="text-center mt-1">
+              <small>
+                {t('admin:slack_integration.accordion.how_to_create_a_bot')}
                 <i className="fa fa-external-link ml-2" aria-hidden="true" />
-              </button>
-            </div>
-            {/* TODO: Insert DOCS link */}
-            <a href="#">
-              <p className="text-center mt-1">
-                <small>
-                  {t('admin:slack_integration.accordion.how_to_create_a_bot')}
-                  <i className="fa fa-external-link ml-2" aria-hidden="true" />
-                </small>
-              </p>
-            </a>
-          </div>
+              </small>
+            </p>
+          </a>
         </div>
       </Accordion>
       <Accordion
@@ -186,9 +182,9 @@ const CustomBotWithoutProxySettingsAccordion = ({
           </form>
         </div>
         {connectionErrorMessage != null
-        && <p className="text-danger text-center my-4">{t('admin:slack_integration.accordion.error_check_logs_below')}</p>}
+          && <p className="text-danger text-center my-4">{t('admin:slack_integration.accordion.error_check_logs_below')}</p>}
         {connectionSuccessMessage != null
-        && <p className="text-info text-center my-4">{t('admin:slack_integration.accordion.send_message_to_slack_work_space')}</p>}
+          && <p className="text-info text-center my-4">{t('admin:slack_integration.accordion.send_message_to_slack_work_space')}</p>}
         <form>
           <div className="row my-3 justify-content-center">
             <div className="form-group slack-connection-log w-25">

+ 47 - 4
src/client/js/components/Admin/SlackIntegration/OfficialbotSettingsAccordion.jsx

@@ -10,8 +10,22 @@ const OfficialBotSettingsAccordion = () => {
       <Accordion
         title={<><span className="mr-2">①</span>{t('admin:slack_integration.accordion.install_bot_to_slack')}</>}
       >
-        {/* TODO: GW-5824 add accordion contents  */}
-        hoge
+        <div className="my-5 d-flex flex-column align-items-center">
+          {/* TODO: Insert install link */}
+          <button type="button" className="btn btn-primary text-nowrap" onClick={() => window.open('https://api.slack.com/apps', '_blank', 'noreferrer')}>
+            {t('admin:slack_integration.accordion.install_now')}
+            <i className="fa fa-external-link ml-2" aria-hidden="true" />
+          </button>
+          {/* TODO: Insert DOCS link */}
+          <a href="#">
+            <p className="text-center mt-1">
+              <small>
+                {t('admin:slack_integration.accordion.how_to_install')}
+                <i className="fa fa-external-link ml-2" aria-hidden="true" />
+              </small>
+            </p>
+          </a>
+        </div>
       </Accordion>
       <Accordion
         title={<><span className="mr-2">②</span>{t('admin:slack_integration.accordion.register_official_bot_proxy_service')}</>}
@@ -28,8 +42,37 @@ const OfficialBotSettingsAccordion = () => {
       <Accordion
         title={<><span className="mr-2">④</span>{t('admin:slack_integration.accordion.test_connection')}</>}
       >
-        {/* TODO: GW-5824 add accordion contents  */}
-        hoge
+        <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
+        <div className="d-flex justify-content-center">
+          <form className="form-row align-items-center w-25">
+            <div className="col-8 input-group-prepend">
+              <span className="input-group-text" id="slack-channel-addon"><i className="fa fa-hashtag" /></span>
+              <input
+                className="form-control w-100"
+                type="text"
+                placeholder="Slack Channel"
+              />
+            </div>
+            <div className="col-4">
+              <button
+                type="submit"
+                className="btn btn-info mx-3 font-weight-bold"
+              >Test
+              </button>
+            </div>
+          </form>
+        </div>
+        <form>
+          <div className="row my-3 justify-content-center">
+            <div className="form-group slack-connection-log w-25">
+              <label className="mb-1"><p className="border-info slack-connection-log-title pl-2">Logs</p></label>
+              <textarea
+                className="form-control card border-info slack-connection-log-body rounded-lg"
+                readOnly
+              />
+            </div>
+          </div>
+        </form>
       </Accordion>
     </div>
   );

+ 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 - 313
src/server/routes/apiv3/slack-integration.js

@@ -1,354 +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,
-        isSetupSlackBot: crowi.slackBotService.isSetupSlackBot,
-      },
-      // 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