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

Merge branch 'feat/growi-bot' into feat/GW-5859-generate-proxy-access-token

Shun Miyazawa 4 лет назад
Родитель
Сommit
8c8a3a405e
22 измененных файлов с 342 добавлено и 253 удалено
  1. 1 0
      packages/slack/package.json
  2. 3 0
      packages/slack/src/index.ts
  3. 4 0
      packages/slack/src/interfaces/connection-status.ts
  4. 17 0
      packages/slack/src/interfaces/request-between-growi-and-proxy.ts
  5. 30 0
      packages/slack/src/middlewares/verify-growi-to-slack-request.ts
  6. 91 0
      packages/slack/src/utils/check-communicable.ts
  7. 55 0
      packages/slackbot-proxy/src/controllers/growi-to-slack.ts
  8. 7 7
      packages/slackbot-proxy/src/controllers/slack.ts
  9. 1 1
      packages/slackbot-proxy/src/entities/relation.ts
  10. 4 0
      packages/slackbot-proxy/src/interfaces/growi-to-slack/growi-req.ts
  11. 0 0
      packages/slackbot-proxy/src/interfaces/slack-to-growi/growi-command-processor.ts
  12. 1 1
      packages/slackbot-proxy/src/interfaces/slack-to-growi/slack-oauth-req.ts
  13. 0 0
      packages/slackbot-proxy/src/middlewares/slack-to-growi/add-signing-secret-to-req.ts
  14. 4 4
      packages/slackbot-proxy/src/middlewares/slack-to-growi/authorizer.ts
  15. 1 1
      packages/slackbot-proxy/src/services/RegisterService.ts
  16. 23 1
      src/client/js/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx
  17. 10 7
      src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  18. 1 1
      src/client/js/components/Admin/SlackIntegration/DeleteSlackBotSettingsModal.jsx
  19. 8 22
      src/client/js/components/Admin/SlackIntegration/SlackIntegration.jsx
  20. 60 176
      src/server/routes/apiv3/slack-integration-settings.js
  21. 0 6
      src/server/service/config-loader.js
  22. 21 26
      src/server/service/slackbot.js

+ 1 - 0
packages/slack/package.json

@@ -15,6 +15,7 @@
     "test:lint:fix": "eslint src --ext .ts --fix"
   },
   "dependencies": {
+    "axios": "^0.21.1",
     "browser-bunyan": "^1.6.3",
     "bunyan": "^1.8.15",
     "dotenv-flow": "^3.2.0",

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

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

+ 4 - 0
packages/slack/src/interfaces/connection-status.ts

@@ -0,0 +1,4 @@
+export type ConnectionStatus = {
+  error?: Error,
+  workspaceName?: string,
+}

+ 17 - 0
packages/slack/src/interfaces/request-between-growi-and-proxy.ts

@@ -0,0 +1,17 @@
+import { Request } from 'express';
+
+export type RequestFromGrowi = Request & {
+  // appended by GROWI
+  headers:{'x-growi-gtop-tokens'?:string},
+
+  // will be extracted from header
+  tokenGtoPs: string[],
+};
+
+export type RequestFromProxy = Request & {
+  // appended by Proxy
+  headers:{'x-growi-ptog-token'?:string},
+
+  // will be extracted from header
+  tokenPtoG: string[],
+};

+ 30 - 0
packages/slack/src/middlewares/verify-growi-to-slack-request.ts

@@ -0,0 +1,30 @@
+import { Response, NextFunction } from 'express';
+
+import loggerFactory from '../utils/logger';
+import { RequestFromGrowi } from '../interfaces/request-between-growi-and-proxy';
+
+const logger = loggerFactory('@growi/slack:middlewares:verify-slack-request');
+
+/**
+ * Verify if the request came from slack
+ * See: https://api.slack.com/authentication/verifying-requests-from-slack
+ */
+export const verifyGrowiToSlackRequest = (req: RequestFromGrowi, res: Response, next: NextFunction): Record<string, any> | void => {
+  const str = req.headers['x-growi-gtop-tokens'];
+
+  if (str == null) {
+    const message = 'The value of header \'x-growi-gtop-tokens\' must not be empty.';
+    logger.warn(message, { body: req.body });
+    return res.status(400).send({ message });
+  }
+
+  const tokens = str.split(',').map(value => value.trim());
+  if (tokens.length === 0) {
+    const message = 'The value of header \'x-growi-gtop-tokens\' must include at least one or more tokens.';
+    logger.warn(message, { body: req.body });
+    return res.status(400).send({ message });
+  }
+
+  req.tokenGtoPs = tokens;
+  return next();
+};

+ 91 - 0
packages/slack/src/utils/check-communicable.ts

@@ -0,0 +1,91 @@
+import axios, { AxiosError } from 'axios';
+
+import { WebClient } from '@slack/web-api';
+
+import { generateWebClient } from './webclient-factory';
+import { ConnectionStatus } from '../interfaces/connection-status';
+
+/**
+ * Check whether the HTTP server responds or not.
+ *
+ * @param serverUri Server URI to connect
+ * @returns AxiosError when error is occured
+ */
+export const connectToHttpServer = async(serverUri: string): Promise<void|AxiosError> => {
+  try {
+    await axios.get(serverUri, { maxRedirects: 0, timeout: 3000 });
+  }
+  catch (err) {
+    return err as AxiosError;
+  }
+};
+
+/**
+ * Check whether the Slack API server responds or not.
+ *
+ * @returns AxiosError when error is occured
+ */
+export const connectToSlackApiServer = async(): Promise<void|AxiosError> => {
+  return connectToHttpServer('https://slack.com/api/');
+};
+
+/**
+ * Test Slack API
+ * @param client
+ */
+const testSlackApiServer = async(client: WebClient): Promise<void> => {
+  const result = await client.api.test();
+
+  if (!result.ok) {
+    throw new Error(result.error);
+  }
+};
+
+/**
+ * Retrieve Slack workspace name
+ * @param client
+ */
+const retrieveWorkspaceName = async(client: WebClient): Promise<string> => {
+  const result = await client.team.info();
+
+  if (!result.ok) {
+    throw new Error(result.error);
+  }
+
+  return (result as any).team?.name;
+};
+
+/**
+ * Get token string to ConnectionStatus map
+ * @param tokens Array of bot OAuth token
+ * @returns
+ */
+export const getConnectionStatuses = async(tokens: string[]): Promise<{[key: string]: ConnectionStatus}> => {
+  const map = tokens
+    .reduce<Promise<Map<string, ConnectionStatus>>>(
+      async(acc, token) => {
+        const client = generateWebClient(token);
+
+        const status: ConnectionStatus = {};
+        try {
+          // try to connect
+          await testSlackApiServer(client);
+          // retrieve workspace name
+          status.workspaceName = await retrieveWorkspaceName(client);
+        }
+        catch (err) {
+          status.error = err;
+        }
+
+        (await acc).set(token, status);
+
+        return acc;
+
+      },
+      // define initial accumulator
+      Promise.resolve(new Map<string, ConnectionStatus>()),
+    );
+
+  // convert to object
+  return Object.fromEntries(await map);
+};

+ 55 - 0
packages/slackbot-proxy/src/controllers/growi-to-slack.ts

@@ -0,0 +1,55 @@
+import {
+  Controller, Get, Inject, Req, Res, UseBefore,
+} from '@tsed/common';
+
+import { WebAPICallResult } from '@slack/web-api';
+
+import { verifyGrowiToSlackRequest, getConnectionStatuses } from '@growi/slack';
+
+import { GrowiReq } from '~/interfaces/growi-to-slack/growi-req';
+import { InstallationRepository } from '~/repositories/installation';
+import { RelationRepository } from '~/repositories/relation';
+import { InstallerService } from '~/services/InstallerService';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('slackbot-proxy:controllers:growi-to-slack');
+
+
+@Controller('/g2s')
+export class GrowiToSlackCtrl {
+
+  @Inject()
+  installerService: InstallerService;
+
+  @Inject()
+  installationRepository: InstallationRepository;
+
+  @Inject()
+  relationRepository: RelationRepository;
+
+  @Get('/connection-status')
+  @UseBefore(verifyGrowiToSlackRequest)
+  async getConnectionStatuses(@Req() req: GrowiReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
+    // asserted (tokenGtoPs.length > 0) by verifyGrowiToSlackRequest
+    const { tokenGtoPs } = req;
+
+    // retrieve Relation with Installation
+    const relations = await this.relationRepository.createQueryBuilder('relation')
+      .where('relation.tokenGtoP IN (:...tokens)', { tokens: tokenGtoPs })
+      .leftJoinAndSelect('relation.installation', 'installation')
+      .getMany();
+
+    logger.debug(`${relations.length} relations found`, relations);
+
+    // extract bot token
+    const tokens: string[] = relations
+      .map(relation => relation.installation?.data?.bot?.token)
+      .filter((v): v is string => v != null); // filter out null values
+
+    const connectionStatuses = await getConnectionStatuses(tokens);
+
+    return res.send({ connectionStatuses });
+  }
+
+}

+ 7 - 7
packages/slackbot-proxy/src/controllers/slack.ts

@@ -11,12 +11,12 @@ import {
 } from '@growi/slack';
 
 import { Relation } from '~/entities/relation';
-import { AuthedReq } from '~/interfaces/authorized-req';
+import { SlackOauthReq } from '~/interfaces/slack-to-growi/slack-oauth-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 { AddSigningSecretToReq } from '~/middlewares/slack-to-growi/add-signing-secret-to-req';
+import { AuthorizeCommandMiddleware, AuthorizeInteractionMiddleware } from '~/middlewares/slack-to-growi/authorizer';
 import { InstallerService } from '~/services/InstallerService';
 import { RegisterService } from '~/services/RegisterService';
 import loggerFactory from '~/utils/logger';
@@ -65,7 +65,7 @@ export class SlackCtrl {
 
   @Post('/commands')
   @UseBefore(AddSigningSecretToReq, verifySlackRequest, AuthorizeCommandMiddleware)
-  async handleCommand(@Req() req: AuthedReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
+  async handleCommand(@Req() req: SlackOauthReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
     const { body, authorizeResult } = req;
 
     if (body.text == null) {
@@ -89,7 +89,7 @@ export class SlackCtrl {
     const installationId = authorizeResult.enterpriseId || authorizeResult.teamId;
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     const installation = await this.installationRepository.findByTeamIdOrEnterpriseId(installationId!);
-    const relations = await this.relationRepository.find({ installation: installation?.id });
+    const relations = await this.relationRepository.find({ installation });
 
     if (relations.length === 0) {
       return res.json({
@@ -130,7 +130,7 @@ export class SlackCtrl {
 
   @Post('/interactions')
   @UseBefore(AuthorizeInteractionMiddleware)
-  async handleInteraction(@Req() req: AuthedReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
+  async handleInteraction(@Req() req: SlackOauthReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
     logger.info('receive interaction', req.body);
     logger.info('receive interaction', req.authorizeResult);
 
@@ -163,7 +163,7 @@ export class SlackCtrl {
     /*
      * forward to GROWI server
      */
-    const relations = await this.relationRepository.find({ installation: installation?.id });
+    const relations = await this.relationRepository.find({ installation });
 
     const promises = relations.map((relation: Relation) => {
       // generate API URL

+ 1 - 1
packages/slackbot-proxy/src/entities/relation.ts

@@ -17,7 +17,7 @@ export class Relation {
   readonly updatedAt: Date;
 
   @ManyToOne(() => Installation)
-  readonly installation: number;
+  readonly installation: Installation;
 
   @Column()
   @Index({ unique: true })

+ 4 - 0
packages/slackbot-proxy/src/interfaces/growi-to-slack/growi-req.ts

@@ -0,0 +1,4 @@
+import { Req } from '@tsed/common';
+import { RequestFromGrowi } from '@growi/slack';
+
+export type GrowiReq = Req & RequestFromGrowi;

+ 0 - 0
packages/slackbot-proxy/src/interfaces/growi-command-processor.ts → packages/slackbot-proxy/src/interfaces/slack-to-growi/growi-command-processor.ts


+ 1 - 1
packages/slackbot-proxy/src/interfaces/authorized-req.ts → packages/slackbot-proxy/src/interfaces/slack-to-growi/slack-oauth-req.ts

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

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


+ 4 - 4
packages/slackbot-proxy/src/middlewares/authorizer.ts → packages/slackbot-proxy/src/middlewares/slack-to-growi/authorizer.ts

@@ -5,7 +5,7 @@ import {
 
 import Logger from 'bunyan';
 
-import { AuthedReq } from '~/interfaces/authorized-req';
+import { SlackOauthReq } from '~/interfaces/slack-to-growi/slack-oauth-req';
 import { InstallationRepository } from '~/repositories/installation';
 import { InstallerService } from '~/services/InstallerService';
 import loggerFactory from '~/utils/logger';
@@ -25,12 +25,12 @@ export class AuthorizeCommandMiddleware implements IMiddleware {
     this.logger = loggerFactory('slackbot-proxy:middlewares:AuthorizeCommandMiddleware');
   }
 
-  async use(@Req() req: AuthedReq, @Res() res: Res): Promise<void> {
+  async use(@Req() req: SlackOauthReq, @Res() res: Res): Promise<void> {
     const { body } = req;
 
     // extract id from body
     const teamId = body.team_id;
-    const enterpriseId = body.enterprize_id;
+    const enterpriseId = body.enterprise_id;
     const isEnterpriseInstall = body.is_enterprise_install === 'true';
 
     if (teamId == null && enterpriseId == null) {
@@ -83,7 +83,7 @@ export class AuthorizeInteractionMiddleware implements IMiddleware {
     this.logger = loggerFactory('slackbot-proxy:middlewares:AuthorizeInteractionMiddleware');
   }
 
-  async use(@Req() req: AuthedReq, @Res() res: Res): Promise<void> {
+  async use(@Req() req: SlackOauthReq, @Res() res: Res): Promise<void> {
     const { body } = req;
 
     if (body.payload == null) {

+ 1 - 1
packages/slackbot-proxy/src/services/RegisterService.ts

@@ -2,7 +2,7 @@ import { Service } from '@tsed/di';
 import { WebClient, LogLevel } from '@slack/web-api';
 import { generateInputSectionBlock, GrowiCommand, generateMarkdownSectionBlock } from '@growi/slack';
 import { AuthorizeResult } from '@slack/oauth';
-import { GrowiCommandProcessor } from '~/interfaces/growi-command-processor';
+import { GrowiCommandProcessor } from '~/interfaces/slack-to-growi/growi-command-processor';
 import { OrderRepository } from '~/repositories/order';
 import { Installation } from '~/entities/installation';
 

+ 23 - 1
src/client/js/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx

@@ -4,12 +4,16 @@ import PropTypes from 'prop-types';
 import AppContainer from '../../../services/AppContainer';
 import AdminAppContainer from '../../../services/AdminAppContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
 import CustomBotWithProxyIntegrationCard from './CustomBotWithProxyIntegrationCard';
 import CustomBotWithProxySettingsAccordion from './CustomBotWithProxySettingsAccordion';
+import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
 
 const CustomBotWithProxySettings = (props) => {
   // eslint-disable-next-line no-unused-vars
   const { appContainer, adminAppContainer } = props;
+  const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] = useState(false);
+
   const { t } = useTranslation();
 
   // TODO: Multiple accordion logic
@@ -26,6 +30,19 @@ const CustomBotWithProxySettings = (props) => {
     );
   };
 
+  const deleteSlackSettingsHandler = async() => {
+    try {
+      // TODO imple delete PtoG and GtoP Token at GW 5861
+      await appContainer.apiv3.put('/slack-integration-settings/custom-bot-with-proxy', {
+      });
+      deleteAccordionHandler();
+      toastSuccess('success');
+    }
+    catch (err) {
+      toastError(err);
+    }
+  };
+
   return (
     <>
       <h2 className="admin-setting-header mb-2">{t('admin:slack_integration.custom_bot_with_proxy_integration')}</h2>
@@ -57,7 +74,7 @@ const CustomBotWithProxySettings = (props) => {
               <button
                 className="my-3 btn btn-outline-danger"
                 type="button"
-                onClick={deleteAccordionHandler}
+                onClick={() => setIsDeleteConfirmModalShown(true)}
               >
                 <i className="icon-trash mr-1" />
                 {t('admin:slack_integration.delete')}
@@ -79,6 +96,11 @@ const CustomBotWithProxySettings = (props) => {
           </button>
         </div>
       </div>
+      <DeleteSlackBotSettingsModal
+        isOpen={isDeleteConfirmModalShown}
+        onClose={() => setIsDeleteConfirmModalShown(false)}
+        onClickDeleteButton={deleteSlackSettingsHandler}
+      />
     </>
   );
 };

+ 10 - 7
src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx

@@ -67,17 +67,20 @@ const CustomBotWithoutProxySettingsAccordion = ({
     setConnectionErrorCode(null);
     setConnectionErrorMessage(null);
     setConnectionSuccessMessage(null);
+    // TODO: 5921 Add new Test endpoint
     try {
-      const res = await appContainer.apiv3.post('/slack-integration-settings/notification-test-to-slack-work-space', {
-        channel: testChannel,
-      });
-      setConnectionSuccessMessage(res.data.message);
-      onSetIsSendTestMessage(true);
+      // eslint-disable-next-line no-console
+      console.log('Test');
+      // const res = await appContainer.apiv3.post('/slack-integration-settings/notification-test-to-slack-work-space', {
+      //   channel: testChannel,
+      // });
+      // setConnectionSuccessMessage(res.data.message);
+      // onSetIsSendTestMessage(true);
     }
     catch (err) {
       onSetIsSendTestMessage(false);
-      setConnectionErrorCode(err[0].code);
-      setConnectionErrorMessage(err[0].message);
+      setConnectionErrorCode('dummy-error-code');
+      setConnectionErrorMessage('This is a sample error message');
     }
   };
 

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

@@ -46,7 +46,7 @@ const DeleteSlackBotSettingsModal = React.memo((props) => {
         <Button onClick={closeButtonHandler}>{t('Cancel')}</Button>
         <Button color="danger" onClick={deleteSlackCredentialsHandler}>
           <i className="icon icon-fire"></i>
-          {t('Reset')}
+          {t('admin:slack_integration.reset')}
         </Button>
       </ModalFooter>
     </Modal>

+ 8 - 22
src/client/js/components/Admin/SlackIntegration/SlackIntegration.jsx

@@ -26,42 +26,28 @@ const SlackIntegration = (props) => {
   const [isSendTestMessage, setIsSendTestMessage] = useState(false);
   const [slackWSNameInWithoutProxy, setSlackWSNameInWithoutProxy] = useState(null);
 
-  const fetchSlackWorkSpaceNameInWithoutProxy = useCallback(async() => {
-
-    try {
-      const res = await appContainer.apiv3.get('/slack-integration-settings/custom-bot-without-proxy/slack-workspace-name');
-      setSlackWSNameInWithoutProxy(res.data.slackWorkSpaceName);
-    }
-    catch (err) {
-      if (err[0].message === 'missing_scope') {
-        setSlackWSNameInWithoutProxy(null);
-        toastError(err, t('admin:slack_integration.set_scope'));
-      }
-      else {
-        toastError(err);
-      }
-    }
-  }, [appContainer.apiv3, t]);
-
   const fetchSlackIntegrationData = useCallback(async() => {
     try {
-      const response = await appContainer.apiv3.get('/slack-integration-settings');
-      const { currentBotType, customBotWithoutProxySettings } = response.data.slackBotSettingParams;
+      const { data } = await appContainer.apiv3.get('/slack-integration-settings');
       const {
         slackSigningSecret, slackBotToken, slackSigningSecretEnvVars, slackBotTokenEnvVars,
-      } = customBotWithoutProxySettings;
+      } = data.settings;
+
+      if (data.connectionStatuses != null) {
+        const { workspaceName } = data.connectionStatuses[slackBotToken];
+        setSlackWSNameInWithoutProxy(workspaceName);
+      }
 
       setCurrentBotType(currentBotType);
       setSlackSigningSecret(slackSigningSecret);
       setSlackBotToken(slackBotToken);
       setSlackSigningSecretEnv(slackSigningSecretEnvVars);
       setSlackBotTokenEnv(slackBotTokenEnvVars);
-      fetchSlackWorkSpaceNameInWithoutProxy();
     }
     catch (err) {
       toastError(err);
     }
-  }, [appContainer.apiv3, fetchSlackWorkSpaceNameInWithoutProxy]);
+  }, [appContainer.apiv3, currentBotType]);
 
 
   useEffect(() => {

+ 60 - 176
src/server/routes/apiv3/slack-integration-settings.js

@@ -1,12 +1,16 @@
-const loggerFactory = require('@alias/logger');
-
-const logger = loggerFactory('growi:routes:apiv3:notification-setting');
 const express = require('express');
 const { body } = require('express-validator');
+const axios = require('axios');
 const crypto = require('crypto');
-const { WebClient, LogLevel } = require('@slack/web-api');
+
+const loggerFactory = require('@alias/logger');
+
+const { getConnectionStatuses } = require('@growi/slack');
+
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
+const logger = loggerFactory('growi:routes:apiv3:notification-setting');
+
 const router = express.Router();
 
 /**
@@ -68,7 +72,7 @@ module.exports = (crowi) => {
     return configManager.updateConfigsInTheSameNamespace('crowi', params, true);
   }
 
-
+  // eslint-disable-next-line no-unused-vars
   function generateAccessToken(user) {
     const hasher = crypto.createHash('sha512');
     hasher.update(new Date().getTime() + user._id);
@@ -76,6 +80,19 @@ module.exports = (crowi) => {
     return hasher.digest('base64');
   }
 
+  async function getConnectionStatusesFromProxy(tokens) {
+    const csv = tokens.join(',');
+
+    // TODO: retrieve proxy url from configManager
+    const result = await axios.get('http://localhost:8080/g2s/connection-status', {
+      headers: {
+        'x-growi-gtop-tokens': csv,
+      },
+    });
+
+    return result.data;
+  }
+
   /**
    * @swagger
    *
@@ -84,36 +101,48 @@ module.exports = (crowi) => {
    *        tags: [SlackBotSettingParams]
    *        operationId: getSlackBotSettingParams
    *        summary: get /slack-integration
-   *        description: Get slackBot setting params.
+   *        description: Get current settings and connection statuses.
    *        responses:
    *          200:
-   *            description: Succeeded to get slackBot setting params.
+   *            description: Succeeded to get info.
    */
   router.get('/', accessTokenParser, loginRequiredStrictly, adminRequired, async(req, res) => {
-    const slackBotSettingParams = {
-      accessToken: crowi.configManager.getConfig('crowi', 'slackbot:access-token'),
-      currentBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType'),
-      // TODO impl when creating official bot
-      officialBotSettings: {
-        // TODO impl this after GW-4939
-        // AccessToken: "tempaccessdatahogehoge",
-      },
-      customBotWithoutProxySettings: {
-        // TODO impl this after GW-4939
-        // AccessToken: "tempaccessdatahogehoge",
-        slackSigningSecretEnvVars: crowi.configManager.getConfigFromEnvVars('crowi', 'slackbot:signingSecret'),
-        slackBotTokenEnvVars: crowi.configManager.getConfigFromEnvVars('crowi', 'slackbot:token'),
-        slackSigningSecret: crowi.configManager.getConfig('crowi', 'slackbot:signingSecret'),
-        slackBotToken: crowi.configManager.getConfig('crowi', 'slackbot:token'),
-        isConnectedToSlack: crowi.slackBotService.isConnectedToSlack,
-      },
-      // TODO imple when creating with proxy
-      customBotWithProxySettings: {
-        // TODO impl this after GW-4939
-        // AccessToken: "tempaccessdatahogehoge",
-      },
-    };
-    return res.apiv3({ slackBotSettingParams });
+    const { configManager } = crowi;
+    const currentBotType = configManager.getConfig('crowi', 'slackbot:currentBotType');
+
+    // retrieve settings
+    const settings = {};
+    if (currentBotType === 'customBotWithoutProxy') {
+      settings.slackSigningSecretEnvVars = configManager.getConfigFromEnvVars('crowi', 'slackbot:signingSecret');
+      settings.slackBotTokenEnvVars = configManager.getConfigFromEnvVars('crowi', 'slackbot:token');
+      settings.slackSigningSecret = configManager.getConfig('crowi', 'slackbot:signingSecret');
+      settings.slackBotToken = configManager.getConfig('crowi', 'slackbot:token');
+    }
+    else {
+      // settings.proxyUriEnvVars = ;
+      // settings.proxyUri = ;
+      // settings.tokenPtoG = ;
+      // settings.tokenGtoP = ;
+    }
+
+    // TODO: try-catch
+
+    // retrieve connection statuses
+    let connectionStatuses;
+    if (currentBotType === 'customBotWithoutProxy') {
+      const token = settings.slackBotToken;
+      // check the token is not null
+      if (token != null) {
+        connectionStatuses = await getConnectionStatuses([token]);
+      }
+    }
+    else {
+      // TODO: retrieve tokenGtoPs from DB
+      const tokenGtoPs = ['gtop1'];
+      connectionStatuses = (await getConnectionStatusesFromProxy(tokenGtoPs)).connectionStatuses;
+    }
+
+    return res.apiv3({ currentBotType, settings, connectionStatuses });
   });
 
   /**
@@ -145,9 +174,6 @@ module.exports = (crowi) => {
 
       try {
         await updateSlackBotSettings(requestParams);
-
-        // initialize slack service
-        await crowi.slackBotService.initialize();
         crowi.slackBotService.publishUpdatedMessage();
 
         const slackIntegrationSettingsParams = {
@@ -191,9 +217,6 @@ module.exports = (crowi) => {
       };
       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.
@@ -211,144 +234,5 @@ module.exports = (crowi) => {
       }
     });
 
-  /**
-   * @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;
 };

+ 0 - 6
src/server/service/config-loader.js

@@ -410,12 +410,6 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    TYPES.STRING,
     default: null,
   },
-  SLACK_BOT_ACCESS_TOKEN: {
-    ns:      'crowi',
-    key:     'slackbot:access-token',
-    type:    TYPES.STRING,
-    default: null,
-  },
   SLACK_BOT_TYPE: {
     ns:      'crowi',
     key:     'slackbot:currentBotType', // 'officialBot' || 'customBotWithoutProxy' || 'customBotWithProxy'

+ 21 - 26
src/server/service/slackbot.js

@@ -1,10 +1,10 @@
 const logger = require('@alias/logger')('growi:service:SlackBotService');
 const mongoose = require('mongoose');
 
-const PAGINGLIMIT = 10;
-
 const { generateWebClient } = require('@growi/slack');
 
+const PAGINGLIMIT = 10;
+
 const S2sMessage = require('../models/vo/s2s-message');
 const S2sMessageHandlable = require('./s2s-messaging/handlable');
 
@@ -16,40 +16,36 @@ class SlackBotService extends S2sMessageHandlable {
     this.crowi = crowi;
     this.s2sMessagingService = crowi.s2sMessagingService;
 
-    this.client = null;
-    this.searchService = null;
-
-    this.isConnectedToSlack = false;
-
     this.lastLoadedAt = null;
 
     this.initialize();
   }
 
-  async initialize() {
-    this.isConnectedToSlack = false;
+  initialize() {
+    this.lastLoadedAt = new Date();
+  }
+
+  get client() {
     const currentBotType = this.crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
 
-    if (currentBotType != null) {
-      let serverUri;
-      let token;
+    if (currentBotType == null) {
+      throw new Error('The config \'SLACK_BOT_TYPE\'(ns: \'crowi\', key: \'slackbot:currentBotType\') must be set.');
+    }
 
-      // connect to proxy
-      if (currentBotType !== 'customBotWithoutProxy') {
-        // TODO: https://youtrack.weseek.co.jp/issue/GW-5896
-        serverUri = 'http://localhost:8080/slack-api-proxy/';
-      }
-      // connect directly
-      else {
-        token = this.crowi.configManager.getConfig('crowi', 'slackbot:token');
-      }
+    let serverUri;
+    let token;
 
-      this.client = generateWebClient(token, serverUri);
-      logger.debug('SlackBot: setup is done');
-      await this.sendAuthTest();
+    // connect to proxy
+    if (currentBotType !== 'customBotWithoutProxy') {
+      // TODO: https://youtrack.weseek.co.jp/issue/GW-5896
+      serverUri = 'http://localhost:8080/slack-api-proxy/';
+    }
+    // connect directly
+    else {
+      token = this.crowi.configManager.getConfig('crowi', 'slackbot:token');
     }
 
-    this.lastLoadedAt = new Date();
+    return generateWebClient(token, serverUri);
   }
 
   /**
@@ -93,7 +89,6 @@ class SlackBotService extends S2sMessageHandlable {
 
   async sendAuthTest() {
     await this.client.api.test();
-    this.isConnectedToSlack = true;
   }
 
   notCommand(body) {