فهرست منبع

Merge branch 'feat/growi-bot' into imprv/gw5547-new

kaori 5 سال پیش
والد
کامیت
9cb62c64e6
30فایلهای تغییر یافته به همراه414 افزوده شده و 128 حذف شده
  1. 5 4
      packages/app-for-hoisting/package.json
  2. 5 4
      packages/app/package.json
  3. 5 1
      packages/slack/package.json
  4. 5 1
      packages/slackbot-proxy/package.json
  5. 16 0
      packages/slackbot-proxy/src/config/logger/config.dev.ts
  6. 16 0
      packages/slackbot-proxy/src/config/logger/config.prod.ts
  7. 58 22
      packages/slackbot-proxy/src/controllers/slack.ts
  8. 6 0
      packages/slackbot-proxy/src/interfaces/authorized-req.ts
  9. 6 0
      packages/slackbot-proxy/src/interfaces/growi-command-processor.ts
  10. 0 3
      packages/slackbot-proxy/src/interfaces/growi-commands-mappings.ts
  11. 49 0
      packages/slackbot-proxy/src/middlewares/authorizer.ts
  12. 10 10
      packages/slackbot-proxy/src/services/InstallerService.ts
  13. 0 16
      packages/slackbot-proxy/src/services/RecieveService.ts
  14. 10 5
      packages/slackbot-proxy/src/services/RegisterService.ts
  15. 17 0
      packages/slackbot-proxy/src/utils/logger/index.ts
  16. 36 0
      public/images/slack-integration/slackbot-difficulty-level-easy.svg
  17. 32 0
      public/images/slack-integration/slackbot-difficulty-level-hard.svg
  18. 32 0
      public/images/slack-integration/slackbot-difficulty-level-normal.svg
  19. 3 2
      resource/locales/en_US/admin/admin.json
  20. 3 2
      resource/locales/ja_JP/admin/admin.json
  21. 3 2
      resource/locales/zh_CN/admin/admin.json
  22. 1 7
      src/client/js/components/Admin/SlackIntegration/BotTypeCard.jsx
  23. 23 12
      src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx
  24. 26 13
      src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  25. 16 15
      src/client/js/components/Admin/SlackIntegration/SlackIntegration.jsx
  26. 4 1
      src/client/styles/scss/_admin.scss
  27. 2 2
      src/server/routes/apiv3/slack-bot.js
  28. 0 2
      src/server/routes/apiv3/slack-integration.js
  29. 1 3
      src/server/service/slackbot.js
  30. 24 1
      yarn.lock

+ 5 - 4
packages/app-for-hoisting/package.json

@@ -2,8 +2,7 @@
   "name": "@growi/app-for-hoisting",
   "version": "0.9.0-RC",
   "license": "MIT",
-  "scripts": {
-  },
+  "scripts": {},
   "// comments for dependencies": {
     "openid-client": "Node.js 12 or higher is required for openid-client@3 and above."
   },
@@ -21,7 +20,8 @@
     "aws-sdk": "^2.88.0",
     "axios": "^0.21.1",
     "body-parser": "^1.18.2",
-    "bunyan": "^1.8.12",
+    "browser-bunyan": "^1.6.3",
+    "bunyan": "^1.8.15",
     "bunyan-format": "^0.2.1",
     "check-node-version": "^4.1.0",
     "connect-flash": "~0.1.1",
@@ -89,6 +89,7 @@
     "string-width": "^4.1.0",
     "swig-templates": "^2.0.2",
     "uglifycss": "^0.0.29",
+    "universal-bunyan": "^0.9.2",
     "unzipper": "^0.10.5",
     "url-join": "^4.0.0",
     "validator": "^12.0.0",
@@ -120,7 +121,7 @@
     "babel-plugin-lodash": "^3.3.4",
     "babel-plugin-transform-imports": "^2.0.0",
     "bootstrap": "^4.5.0",
-    "browser-bunyan": "^1.3.0",
+    "browser-bunyan": "^1.6.3",
     "browser-sync": "^2.26.3",
     "bunyan-debug": "^2.0.0",
     "cli": "~1.0.1",

+ 5 - 4
packages/app/package.json

@@ -2,8 +2,7 @@
   "name": "@growi/app",
   "version": "0.9.0-RC",
   "license": "MIT",
-  "scripts": {
-  },
+  "scripts": {},
   "// comments for dependencies": {
     "openid-client": "Node.js 12 or higher is required for openid-client@3 and above."
   },
@@ -21,7 +20,8 @@
     "aws-sdk": "^2.88.0",
     "axios": "^0.21.1",
     "body-parser": "^1.18.2",
-    "bunyan": "^1.8.12",
+    "browser-bunyan": "^1.6.3",
+    "bunyan": "^1.8.15",
     "bunyan-format": "^0.2.1",
     "check-node-version": "^4.1.0",
     "connect-flash": "~0.1.1",
@@ -89,6 +89,7 @@
     "string-width": "^4.1.0",
     "swig-templates": "^2.0.2",
     "uglifycss": "^0.0.29",
+    "universal-bunyan": "^0.9.2",
     "unzipper": "^0.10.5",
     "url-join": "^4.0.0",
     "validator": "^12.0.0",
@@ -120,7 +121,7 @@
     "babel-plugin-lodash": "^3.3.4",
     "babel-plugin-transform-imports": "^2.0.0",
     "bootstrap": "^4.5.0",
-    "browser-bunyan": "^1.3.0",
+    "browser-bunyan": "^1.6.3",
     "browser-sync": "^2.26.3",
     "bunyan-debug": "^2.0.0",
     "cli": "~1.0.1",

+ 5 - 1
packages/slack/package.json

@@ -15,13 +15,17 @@
     "test:lint:fix": "eslint src --ext .ts --fix"
   },
   "dependencies": {
-    "dotenv-flow": "^3.2.0"
+    "browser-bunyan": "^1.6.3",
+    "bunyan": "^1.8.15",
+    "dotenv-flow": "^3.2.0",
+    "universal-bunyan": "^0.9.2"
   },
   "devDependencies": {
     "@slack/bolt": "^3.3.0",
     "@types/jest": "^26.0.22",
     "@typescript-eslint/eslint-plugin": "^4.18.0",
     "@typescript-eslint/parser": "^4.18.0",
+    "browser-bunyan": "^1.6.3",
     "cross-env": "^7.0.0",
     "eslint-import-resolver-typescript": "^2.4.0",
     "eslint-plugin-jest": "^24.3.2",

+ 5 - 1
packages/slackbot-proxy/package.json

@@ -26,13 +26,16 @@
     "@tsed/platform-express": "^6.43.0",
     "@tsed/swagger": "^6.43.0",
     "@tsed/typeorm": "^6.43.0",
+    "browser-bunyan": "^1.6.3",
+    "bunyan": "^1.8.15",
     "compression": "^1.7.4",
     "cookie-parser": "^1.4.5",
     "cross-env": "^7.0.0",
     "dotenv-flow": "^3.2.0",
     "method-override": "^3.0.0",
     "mysql2": "^2.2.5",
-    "typeorm": "^0.2.31"
+    "typeorm": "^0.2.31",
+    "universal-bunyan": "^0.9.2"
   },
   "devDependencies": {
     "@tsed/core": "^6.43.0",
@@ -41,6 +44,7 @@
     "@tsed/schema": "^6.43.0",
     "@typescript-eslint/eslint-plugin": "^4.18.0",
     "@typescript-eslint/parser": "^4.18.0",
+    "browser-bunyan": "^1.6.3",
     "eslint-import-resolver-typescript": "^2.4.0",
     "ts-jest": "^26.5.4",
     "ts-node": "^9.1.1",

+ 16 - 0
packages/slackbot-proxy/src/config/logger/config.dev.ts

@@ -0,0 +1,16 @@
+import { UniversalBunyanConfig } from 'universal-bunyan';
+
+const config: UniversalBunyanConfig = {
+  default: 'info',
+
+  // 'express-session': 'debug',
+
+  /*
+   * configure level for server
+   */
+  // 'express:*': 'debug',
+  // 'slackbot-proxy:*': 'debug',
+
+};
+
+export default config;

+ 16 - 0
packages/slackbot-proxy/src/config/logger/config.prod.ts

@@ -0,0 +1,16 @@
+import { UniversalBunyanConfig } from 'universal-bunyan';
+
+const config: UniversalBunyanConfig = {
+  default: 'info',
+
+  // 'express-session': 'debug',
+
+  /*
+   * configure level for server
+   */
+  // 'express:*': 'debug',
+  // 'slackbot-proxy:*': 'debug',
+
+};
+
+export default config;

+ 58 - 22
packages/slackbot-proxy/src/controllers/slack.ts

@@ -1,5 +1,5 @@
 import {
-  BodyParams, Controller, Get, Inject, Post, Req, Res,
+  BodyParams, Controller, Get, Inject, Post, Req, Res, UseBefore,
 } from '@tsed/common';
 import { parseSlashCommand } from '@growi/slack';
 import { Installation } from '~/entities/installation';
@@ -10,6 +10,11 @@ import { OrderRepository } from '~/repositories/order';
 import { InstallerService } from '~/services/InstallerService';
 import { RegisterService } from '~/services/RegisterService';
 
+import loggerFactory from '~/utils/logger';
+import { AuthorizeMiddleware } from '~/middlewares/authorizer';
+import { AuthedReq } from '~/interfaces/authorized-req';
+
+const logger = loggerFactory('slackbot-proxy:controllers:slack');
 
 @Controller('/slack')
 export class SlackCtrl {
@@ -29,10 +34,6 @@ export class SlackCtrl {
   @Inject()
   registerService: RegisterService;
 
-  growiCommandsMappings = {
-    register: async(body:{[key:string]:string}):Promise<void> => this.registerService.execSlashCommand(body),
-  };
-
   @Get('/testsave')
   testsave(): void {
     const installation = new Installation();
@@ -72,10 +73,10 @@ export class SlackCtrl {
       + '</a>';
   }
 
-  @Post('/events')
-  async handleEvent(@BodyParams() body:{[key:string]:string}, @Res() res: Res): Promise<string> {
-    // Send response immediately to avoid opelation_timeout error
-    // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
+  @Post('/commands')
+  @UseBefore(AuthorizeMiddleware)
+  async handleCommand(@Req() req: AuthedReq, @Res() res: Res): Promise<void|string> {
+    const { body, authorizeResult } = req;
 
     const handleViewSubmission = async(inputValues) => {
 
@@ -124,14 +125,17 @@ export class SlackCtrl {
       return 'No text.';
     }
 
-    const parsedBody = parseSlashCommand(body);
-    const executeGrowiCommand = this.growiCommandsMappings[parsedBody.growiCommandType];
+    // 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);
 
-    if (executeGrowiCommand == null) {
-      return 'No executeGrowiCommand';
+    // register
+    if (growiCommand.growiCommandType === 'register') {
+      await this.registerService.process(growiCommand, authorizeResult, body as {[key:string]:string});
+      return;
     }
-    await executeGrowiCommand(body);
-    res.send();
 
     const installation = await this.installationRepository.findByID('1');
     if (installation == null) {
@@ -151,28 +155,60 @@ export class SlackCtrl {
       order = await this.orderRepository.save({ installation: installation.id });
     }
 
-    return 'This action will be handled by bolt service.';
+    return;
+  }
+
+  @Post('/interactions')
+  async handleInteraction(@BodyParams() body:{[key:string]:string}, @Res() res: Res): Promise<void|string> {
+    logger.info('receive interaction', body);
+    return;
+  }
+
+  @Post('/events')
+  async handleEvent(@BodyParams() body:{[key:string]:string}, @Res() res: Res): Promise<void|string> {
+    // 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;
+    }
+
+    logger.info('receive event', body);
+
+    return;
   }
 
   @Get('/oauth_redirect')
   async handleOauthRedirect(@Req() req: Req, @Res() res: Res): Promise<void> {
 
-    // illegal state
-    // TODO: https://youtrack.weseek.co.jp/issue/GW-5543
-    if (req.query.state !== 'init') {
+    if (req.query.state === '') {
       res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
       res.end('<html>'
       + '<head><meta name="viewport" content="width=device-width,initial-scale=1"></head>'
       + '<body style="text-align:center; padding-top:20%;">'
       + '<h1>Illegal state, try it again.</h1>'
       + '<a href="/slack/install">'
-      + 'go to install page'
+      + 'Go to install page'
       + '</a>'
       + '</body></html>');
     }
 
-    this.installerService.installer.handleCallback(req, res, {
-      // success: (installation, metadata, req, res) => {},
+    await this.installerService.installer.handleCallback(req, res, {
+      success: (installation, metadata, req, res) => {
+        logger.info('Success to install', { installation, metadata });
+
+        const appPageUrl = `https://slack.com/apps/${installation.appId}`;
+
+        res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
+        res.end('<html>'
+        + '<head><meta name="viewport" content="width=device-width,initial-scale=1"></head>'
+        + '<body style="text-align:center; padding-top:20%;">'
+        + '<h1>Congratulations!</h1>'
+        + '<p>GROWI Bot installation has succeeded.</p>'
+        + `<a href="${appPageUrl}">`
+        + 'Access to Slack App detail page.'
+        + '</a>'
+        + '</body></html>');
+      },
       failure: (error, installOptions, req, res) => {
         res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
         res.end('<html>'

+ 6 - 0
packages/slackbot-proxy/src/interfaces/authorized-req.ts

@@ -0,0 +1,6 @@
+import { AuthorizeResult } from '@slack/oauth';
+import { Req } from '@tsed/common';
+
+export type AuthedReq = Req & {
+  authorizeResult: AuthorizeResult,
+};

+ 6 - 0
packages/slackbot-proxy/src/interfaces/growi-command-processor.ts

@@ -0,0 +1,6 @@
+import { AuthorizeResult } from '@slack/oauth';
+import { GrowiCommand } from '@growi/slack';
+
+export interface GrowiCommandProcessor {
+  process(growiCommand: GrowiCommand, authorizeResult: AuthorizeResult, body: {[key:string]:string}): Promise<void>
+}

+ 0 - 3
packages/slackbot-proxy/src/interfaces/growi-commands-mappings.ts

@@ -1,3 +0,0 @@
-export interface GrowiCommandsMappings{
-  execSlashCommand(body:{[key:string]:string}):Promise<void>
-}

+ 49 - 0
packages/slackbot-proxy/src/middlewares/authorizer.ts

@@ -0,0 +1,49 @@
+import { InstallationQuery } from '@slack/oauth';
+import {
+  IMiddleware, Inject, Middleware, Req, Res,
+} from '@tsed/common';
+
+import { AuthedReq } from '~/interfaces/authorized-req';
+import { InstallationRepository } from '~/repositories/installation';
+import { InstallerService } from '~/services/InstallerService';
+
+@Middleware()
+export class AuthorizeMiddleware implements IMiddleware {
+
+  @Inject()
+  installerService: InstallerService;
+
+  @Inject()
+  installationRepository: InstallationRepository;
+
+  async use(@Req() req: AuthedReq, @Res() res: Res): Promise<void> {
+    const { body } = req;
+
+    // extract id from body
+    const teamId = body.team_id;
+    const enterpriseId = body.enterprize_id;
+
+    if (teamId == null && enterpriseId == null) {
+      res.writeHead(400);
+      return res.end();
+    }
+
+    // create query from body
+    const query: InstallationQuery<boolean> = {
+      teamId,
+      enterpriseId,
+      isEnterpriseInstall: body.is_enterprise_install === 'true',
+    };
+
+    const result = await this.installerService.installer.authorize(query);
+
+    if (result.botToken == null) {
+      res.writeHead(403);
+      return res.end();
+    }
+
+    // set authorized data
+    req.authorizeResult = result;
+  }
+
+}

+ 10 - 10
packages/slackbot-proxy/src/services/InstallerService.ts

@@ -55,16 +55,16 @@ export class InstallerService {
           return;
         },
         fetchInstallation: async(installQuery: InstallationQuery<boolean>) => {
-          const installation: SlackInstallation<'v1' | 'v2', boolean> = {
-            team: undefined,
-            enterprise: undefined,
-            user: {
-              id: '',
-              token: undefined,
-              scopes: undefined,
-            },
-          };
-          return installation;
+          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+          const id = installQuery.enterpriseId || installQuery.teamId!;
+
+          const installation = await repository.findByTeamIdOrEnterpriseId(id);
+
+          if (installation == null) {
+            throw new Error('Failed fetching installation');
+          }
+
+          return installation.data;
         },
       },
     });

+ 0 - 16
packages/slackbot-proxy/src/services/RecieveService.ts

@@ -1,16 +0,0 @@
-import { Service } from '@tsed/di';
-import { parseSlashCommand } from '@growi/slack';
-
-@Service()
-export class ReceiveService {
-
-  receiveContentsFromSlack(body:{[key:string]:string}) : string {
-    const parseBody = parseSlashCommand(body);
-    if (parseBody.growiCommandType === 'register') {
-      console.log('register action occured');
-      return 'register action occurd';
-    }
-    return 'return receiveContentsFromSlack';
-  }
-
-}

+ 10 - 5
packages/slackbot-proxy/src/services/RegisterService.ts

@@ -1,14 +1,19 @@
 import { Service } from '@tsed/di';
 import { WebClient, LogLevel } from '@slack/web-api';
-import { generateInputSectionBlock } from '@growi/slack';
-import { GrowiCommandsMappings } from '../interfaces/growi-commands-mappings';
+import { generateInputSectionBlock, GrowiCommand } from '@growi/slack';
+import { AuthorizeResult } from '@slack/oauth';
+
+import { GrowiCommandProcessor } from '~/interfaces/growi-command-processor';
 
 @Service()
-export class RegisterService implements GrowiCommandsMappings {
+export class RegisterService implements GrowiCommandProcessor {
+
+  async process(growiCommand: GrowiCommand, authorizeResult: AuthorizeResult, body: {[key:string]:string}): Promise<void> {
+
+    const { botToken } = authorizeResult;
 
-  async execSlashCommand(body:{[key:string]:string}):Promise<void> {
     // tmp use process.env
-    const client = new WebClient(process.env.SLACK_BOT_USER_OAUTH_TOKEN, { logLevel: LogLevel.DEBUG });
+    const client = new WebClient(botToken, { logLevel: LogLevel.DEBUG });
     await client.views.open({
       trigger_id: body.trigger_id,
       view: {

+ 17 - 0
packages/slackbot-proxy/src/utils/logger/index.ts

@@ -0,0 +1,17 @@
+import Logger from 'bunyan';
+import { createLogger } from 'universal-bunyan';
+
+import configForDev from '~/config/logger/config.dev';
+import configForProd from '~/config/logger/config.prod';
+
+const isProduction = process.env.NODE_ENV === 'production';
+const config = isProduction ? configForProd : configForDev;
+
+const loggerFactory = function(name: string): Logger {
+  return createLogger({
+    name,
+    config,
+  });
+};
+
+export default loggerFactory;

+ 36 - 0
public/images/slack-integration/slackbot-difficulty-level-easy.svg

@@ -0,0 +1,36 @@
+<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 {
+        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;
+      }
+    </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>
+  </g>
+</svg>

+ 32 - 0
public/images/slack-integration/slackbot-difficulty-level-hard.svg

@@ -0,0 +1,32 @@
+<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 {
+        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;
+      }
+    </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)">
+      <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>
+  </g>
+</svg>

+ 32 - 0
public/images/slack-integration/slackbot-difficulty-level-normal.svg

@@ -0,0 +1,32 @@
+<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 {
+        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;
+      }
+    </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)">
+      <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>
+  </g>
+</svg>

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

@@ -305,8 +305,9 @@
     },
     "custom_bot_without_proxy_integration": "Custom bot without proxy integration",
     "integration_sentence": {
-      "integration_is_not_complete": "Integration is not complete.",
-      "proceed_with_the_following_integration_procedure": "Proceed with the following integration procedure."
+      "integration_is_not_complete": "Integration is not complete.<br>Proceed with the following integration procedure.",
+      "proceed_with_the_following_integration_procedure": "Proceed with the following integration procedure.",
+      "integration_sucessed": "integration sucessed"
     },
     "custom_bot_with_proxy_integration": "Custom bot with proxy integration"
   },

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

@@ -303,8 +303,9 @@
     },
     "custom_bot_without_proxy_integration": "Custom bot without proxy 連携",
     "integration_sentence": {
-      "integration_is_not_complete": "連携は完了していません。",
-      "proceed_with_the_following_integration_procedure": "下記の連携手順を進めてください。"
+      "integration_is_not_complete": "連携は完了していません。<br>下記の連携手順を進めてください。",
+      "proceed_with_the_following_integration_procedure": "下記の連携手順を進めてください。",
+      "integration_sucessed": "連携が完了しました。"
     },
     "custom_bot_with_proxy_integration": "Custom bot with proxy 連携"
   },

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

@@ -313,8 +313,9 @@
     },
     "custom_bot_without_proxy_integration": "Custom bot without proxy 一体化",
     "integration_sentence": {
-      "integration_is_not_complete": "一体化未完成。",
-      "proceed_with_the_following_integration_procedure": "进行以下一体化程序。"
+      "integration_is_not_complete": "一体化未完成。<br>进行以下一体化程序。",
+      "proceed_with_the_following_integration_procedure": "进行以下一体化程序。",
+      "integration_sucessed": "一体化成功"
     },
     "custom_bot_with_proxy_integration": "Custom bot with proxy 一体化"
   },

+ 1 - 7
src/client/js/components/Admin/SlackIntegration/BotTypeCard.jsx

@@ -67,13 +67,7 @@ const BotTypeCard = (props) => {
       <div className="card-body p-4">
         <div className="card-text">
           <div className="my-2">
-            <div className="d-flex justify-content-between mb-3">
-              {/* TODO add image of difficulties by GW-5638
-               <span>{t('admin:slack_integration.selecting_bot_types.set_up')}</span>
-               <span className={`bot-type-disc-${value.setUp}`}>{t(`admin:slack_integration.selecting_bot_types.${value.setUp}`)}</span>  */}
-
-
-            </div>
+            <img className="d-block mx-auto mb-4" src={`/images/slack-integration/slackbot-difficulty-level-${botDetails[props.botType].setUp}.svg`}></img>
             <div className="d-flex justify-content-between mb-3">
               <span>{t('admin:slack_integration.selecting_bot_types.multiple_workspaces_integration')}</span>
               <img className="bot-type-disc" src={`/images/slack-integration/${botDetails[props.botType].multiWSIntegration}.png`} alt="" />

+ 23 - 12
src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx

@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
 import { useTranslation } from 'react-i18next';
 import PropTypes from 'prop-types';
 import AppContainer from '../../../services/AppContainer';
@@ -15,7 +15,7 @@ const CustomBotWithoutProxySettings = (props) => {
 
   const [siteName, setSiteName] = useState('');
 
-  const fetchSlackWorkSpaceName = async() => {
+  const fetchSlackWorkSpaceName = useCallback(async() => {
     try {
       const res = await appContainer.apiv3.get('/slack-integration/custom-bot-without-proxy/slack-workspace-name');
       setSlackWSNameInWithoutProxy(res.data.slackWorkSpaceName);
@@ -23,19 +23,17 @@ const CustomBotWithoutProxySettings = (props) => {
     catch (err) {
       toastError(err);
     }
-  };
+  }, [appContainer.apiv3]);
+
+  useEffect(() => {
 
-  const fetchSiteName = () => {
     const siteName = appContainer.config.crowi.title;
     setSiteName(siteName);
-  };
 
-  useEffect(() => {
-    fetchSiteName();
     if (props.isSetupSlackBot) {
       fetchSlackWorkSpaceName();
     }
-  }, [appContainer, props.isSetupSlackBot]);
+  }, [appContainer, fetchSlackWorkSpaceName, props.isSetupSlackBot]);
 
   return (
     <>
@@ -57,10 +55,23 @@ const CustomBotWithoutProxySettings = (props) => {
           </div>
         </div>
 
-        <div className="text-center w-25 mt-4">
-          <p className="text-secondary m-0"><small>{t('admin:slack_integration.integration_sentence.integration_is_not_complete')}</small></p>
-          <p className="text-secondary"><small>{t('admin:slack_integration.integration_sentence.proceed_with_the_following_integration_procedure')}</small></p>
-          <hr className="border-danger align-self-center admin-border"></hr>
+        <div className="text-center w-25">
+          {props.isSetupSlackBot && (
+            <div className="mt-5">
+              <p className="text-success"><small className="fa fa-check"> {t('admin:slack_integration.integration_sentence.integration_sucessed')}</small></p>
+              <hr className="align-self-center admin-border-success border-success"></hr>
+            </div>
+          )}
+          {!props.isSetupSlackBot && (
+            <div className="mt-4">
+              <small
+                className="text-secondary m-0"
+                // eslint-disable-next-line react/no-danger
+                dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.integration_sentence.integration_is_not_complete') }}
+              />
+              <hr className="align-self-center admin-border-danger border-danger"></hr>
+            </div>
+          )}
         </div>
 
         <div className="card rounded-lg shadow border-0 w-50 admin-bot-card">

+ 26 - 13
src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx

@@ -16,10 +16,10 @@ export const botInstallationStep = {
 };
 
 const CustomBotWithoutProxySettingsAccordion = ({
-  appContainer,
-  activeStep, slackSigningSecret, slackSigningSecretEnv, slackBotToken,
-  slackBotTokenEnv, isRegisterSlackCredentials, isSendTestMessage,
-  setSlackSigningSecret, setSlackBotToken, setIsSendTestMessage, setIsRegisterSlackCredentials,
+  appContainer, activeStep,
+  slackSigningSecret, slackSigningSecretEnv, slackBotToken, slackBotTokenEnv,
+  isRegisterSlackCredentials, isSendTestMessage, isConnectedToSlack,
+  onSetSlackSigningSecret, onSetSlackBotToken, onSetIsSendTestMessage, onSetIsRegisterSlackCredentials,
 }) => {
   const { t } = useTranslation();
   // TODO: GW-5644 Store default open accordion
@@ -38,20 +38,32 @@ const CustomBotWithoutProxySettingsAccordion = ({
         slackBotToken,
         currentBotType,
       });
+
+      if (isConnectedToSlack) {
+        onSetIsRegisterSlackCredentials(true);
+      }
+      else {
+        onSetIsRegisterSlackCredentials(false);
+        onSetIsSendTestMessage(false);
+      }
       toastSuccess(t('toaster.update_successed', { target: t('admin:slack_integration.custom_bot_without_proxy_settings') }));
     }
     catch (err) {
-      setIsRegisterSlackCredentials(false);
+      onSetIsRegisterSlackCredentials(false);
       toastError(err);
     }
   };
 
   const onChangeSigningSecretHandler = (signingSecretInput) => {
-    setSlackSigningSecret(signingSecretInput);
+    if (onSetSlackSigningSecret != null) {
+      onSetSlackSigningSecret(signingSecretInput);
+    }
   };
 
   const onChangeBotTokenHandler = (botTokenInput) => {
-    setSlackBotToken(botTokenInput);
+    if (onSetSlackBotToken != null) {
+      onSetSlackBotToken(botTokenInput);
+    }
   };
 
   const onTestConnectionHandler = async() => {
@@ -63,10 +75,10 @@ const CustomBotWithoutProxySettingsAccordion = ({
         channel: testChannel,
       });
       setConnectionSuccessMessage(res.data.message);
-      setIsSendTestMessage(true);
+      onSetIsSendTestMessage(true);
     }
     catch (err) {
-      setIsSendTestMessage(false);
+      onSetIsSendTestMessage(false);
       setConnectionErrorCode(err[0].code);
       setConnectionErrorMessage(err[0].message);
     }
@@ -202,10 +214,11 @@ CustomBotWithoutProxySettingsAccordion.propTypes = {
   slackBotTokenEnv: PropTypes.string,
   isRegisterSlackCredentials: PropTypes.bool,
   isSendTestMessage: PropTypes.bool,
-  setSlackSigningSecret: PropTypes.string,
-  setSlackBotToken: PropTypes.string,
-  setIsSendTestMessage: PropTypes.func,
-  setIsRegisterSlackCredentials: PropTypes.func,
+  isConnectedToSlack: PropTypes.bool,
+  onSetSlackSigningSecret: PropTypes.func,
+  onSetSlackBotToken: PropTypes.func,
+  onSetIsSendTestMessage: PropTypes.func,
+  onSetIsRegisterSlackCredentials: PropTypes.func,
   adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
   activeStep: PropTypes.oneOf(Object.values(botInstallationStep)).isRequired,
 };

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

@@ -18,11 +18,11 @@ const SlackIntegration = (props) => {
   const { t } = useTranslation();
   const [currentBotType, setCurrentBotType] = useState(null);
   const [selectedBotType, setSelectedBotType] = useState(null);
-  const [slackSigningSecret, setSlackSigningSecret] = useState('');
-  const [slackBotToken, setSlackBotToken] = useState('');
+  const [slackSigningSecret, setSlackSigningSecret] = useState(null);
+  const [slackBotToken, setSlackBotToken] = useState(null);
   const [slackSigningSecretEnv, setSlackSigningSecretEnv] = useState('');
   const [slackBotTokenEnv, setSlackBotTokenEnv] = useState('');
-  const [isConnectedToSlack, setIsConnectedToSlack] = useState(null);
+  const [isConnectedToSlack, setIsConnectedToSlack] = useState(false);
   const [isRegisterSlackCredentials, setIsRegisterSlackCredentials] = useState(false);
   const [isSendTestMessage, setIsSendTestMessage] = useState(false);
   const [isSetupSlackBot, setIsSetupSlackBot] = useState(false);
@@ -33,7 +33,8 @@ const SlackIntegration = (props) => {
       const response = await appContainer.apiv3.get('slack-integration/');
       const { currentBotType, customBotWithoutProxySettings } = response.data.slackBotSettingParams;
       const {
-        slackSigningSecret, slackBotToken, slackSigningSecretEnvVars, slackBotTokenEnvVars, isSetupSlackBot,
+        slackSigningSecret, slackBotToken, slackSigningSecretEnvVars, slackBotTokenEnvVars,
+        isSetupSlackBot, isConnectedToSlack,
       } = customBotWithoutProxySettings;
 
       setCurrentBotType(currentBotType);
@@ -41,18 +42,17 @@ const SlackIntegration = (props) => {
       setSlackBotToken(slackBotToken);
       setSlackSigningSecretEnv(slackSigningSecretEnvVars);
       setSlackBotTokenEnv(slackBotTokenEnvVars);
-      setIsConnectedToSlack(isConnectedToSlack);
       setIsSetupSlackBot(isSetupSlackBot);
+      setIsConnectedToSlack(isConnectedToSlack);
 
-      if ((slackBotToken && slackSigningSecret) || (slackBotTokenEnv && slackSigningSecretEnv)) {
+      if (isConnectedToSlack) {
         setIsRegisterSlackCredentials(true);
       }
-
     }
     catch (err) {
       toastError(err);
     }
-  }, [appContainer.apiv3, isConnectedToSlack, slackBotTokenEnv, slackSigningSecretEnv]);
+  }, [appContainer.apiv3]);
 
   useEffect(() => {
     fetchData();
@@ -66,9 +66,6 @@ const SlackIntegration = (props) => {
       setCurrentBotType(clickedBotType);
       return;
     }
-    setIsRegisterSlackCredentials(false);
-    setSlackSigningSecret('');
-    setSlackBotToken('');
     setSelectedBotType(clickedBotType);
   };
 
@@ -86,6 +83,10 @@ const SlackIntegration = (props) => {
       setCurrentBotType(res.data.customBotWithoutProxySettingParams.slackBotType);
       setSelectedBotType(null);
       toastSuccess(t('admin:slack_integration.bot_reset_successful'));
+      setIsRegisterSlackCredentials(false);
+      setSlackSigningSecret(null);
+      setSlackBotToken(null);
+      setIsSendTestMessage(false);
     }
     catch (err) {
       toastError(err);
@@ -108,11 +109,11 @@ const SlackIntegration = (props) => {
           slackBotToken={slackBotToken}
           slackSigningSecretEnv={slackSigningSecretEnv}
           slackSigningSecret={slackSigningSecret}
-          setSlackSigningSecret={setSlackSigningSecret}
-          setSlackBotToken={setSlackBotToken}
-          setIsSendTestMessage={setIsSendTestMessage}
-          setIsRegisterSlackCredentials={setIsRegisterSlackCredentials}
           isSetupSlackBot={isSetupSlackBot}
+          onSetSlackSigningSecret={setSlackSigningSecret}
+          onSetSlackBotToken={setSlackBotToken}
+          onSetIsSendTestMessage={setIsSendTestMessage}
+          onSetIsRegisterSlackCredentials={setIsRegisterSlackCredentials}
         />
       );
       break;

+ 4 - 1
src/client/styles/scss/_admin.scss

@@ -122,10 +122,13 @@ $slack-work-space-name-card-border: #efc1f6;
     .admin-bot-card {
       border-radius: 8px !important;
     }
-    .admin-border {
+    .admin-border-danger {
       border-style : dashed;
       border-width : 2px;
     }
+    .admin-border-success {
+      border-width : 3px;
+    }
     .circle{
       width: 100px;
       height: 100px;

+ 2 - 2
src/server/routes/apiv3/slack-bot.js

@@ -36,7 +36,7 @@ module.exports = (crowi) => {
     return next();
   };
 
-  router.post('/', verificationRequestUrl, addSlackBotSigningSecret, verificationSlackRequest, verificationAccessToken, async(req, res) => {
+  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
@@ -98,7 +98,7 @@ module.exports = (crowi) => {
     }
   };
 
-  router.post('/interactive', verificationRequestUrl, addSlackBotSigningSecret, verificationSlackRequest, async(req, res) => {
+  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

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

@@ -185,13 +185,11 @@ module.exports = (crowi) => {
   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);
 

+ 1 - 3
src/server/service/slackbot.js

@@ -29,7 +29,7 @@ class SlackBotService extends S2sMessageHandlable {
 
   async initialize() {
     this.isSetupSlackBot = false;
-
+    this.isConnectedToSlack = false;
     const token = this.crowi.configManager.getConfig('crowi', 'slackbot:token');
 
     if (token != null) {
@@ -82,8 +82,6 @@ class SlackBotService extends S2sMessageHandlable {
   }
 
   async sendAuthTest() {
-    this.isConnectedToSlack = false;
-
     await this.client.api.test();
     this.isConnectedToSlack = true;
   }

+ 24 - 1
yarn.lock

@@ -4679,7 +4679,7 @@ brorand@^1.0.1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
 
-browser-bunyan@^1.3.0:
+browser-bunyan@^1.6.3:
   version "1.6.3"
   resolved "https://registry.yarnpkg.com/browser-bunyan/-/browser-bunyan-1.6.3.tgz#0e58c51ff48507317ba8e5cf579e8b6bad7281e0"
   integrity sha512-HRg+acpwO3dsY2RWgtjw2wPVHV+uzbCrdhUxD25+qo5NFSTpbfJekrRP0yFNypAhG5LwXFV1Dc5FIc8cxwU5rQ==
@@ -4950,6 +4950,16 @@ bunyan@^1.8.12, bunyan@^1.8.3:
     mv "~2"
     safe-json-stringify "~1"
 
+bunyan@^1.8.15:
+  version "1.8.15"
+  resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.8.15.tgz#8ce34ca908a17d0776576ca1b2f6cbd916e93b46"
+  integrity sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==
+  optionalDependencies:
+    dtrace-provider "~0.8"
+    moment "^2.19.3"
+    mv "~2"
+    safe-json-stringify "~1"
+
 busboy@^0.2.11:
   version "0.2.14"
   resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453"
@@ -12726,6 +12736,11 @@ moment@>=2.26.0:
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a"
   integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw==
 
+moment@^2.19.3:
+  version "2.29.1"
+  resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
+  integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
+
 mongodb@3.6.2, mongodb@^3.1.0, mongodb@^3.6.2:
   version "3.6.2"
   resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.6.2.tgz#1154a4ac107bf1375112d83a29c5cf97704e96b6"
@@ -19215,6 +19230,14 @@ unist-util-visit@^1.1.0:
   dependencies:
     unist-util-visit-parents "^2.0.0"
 
+universal-bunyan@^0.9.2:
+  version "0.9.2"
+  resolved "https://registry.yarnpkg.com/universal-bunyan/-/universal-bunyan-0.9.2.tgz#4cf09dc34070390d8f5df4fe9af6a80fcd0dd574"
+  integrity sha512-MkyO17+5AVCpFfhMtYLODvSZmPxV8eHcoOAWobEXXzlXrSnf5YgCV5lBWcMV3VPaaKyZPQ0oG5PSWYmGSBGtIg==
+  dependencies:
+    bunyan-format "^0.2.1"
+    minimatch "^3.0.4"
+
 universal-user-agent@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee"