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

Merge branch 'feat/growi-bot' into support/5544-5545-use-slack-web-api-instead-of-bolt

itizawa 5 лет назад
Родитель
Сommit
a213ce83d0

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

@@ -253,6 +253,8 @@
     "delete": "Delete"
   },
   "slack_integration": {
+    "bot_reset_successful": "Bot settings have been reset.",
+    "copied_to_clipboard": "Copied to clipboard",
     "modal": {
       "warning": "Warning",
       "sure_change_bot_type": "Are you sure you want to change the bot type?",

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

@@ -251,6 +251,8 @@
     "Directory_hierarchy_tag": "ディレクトリ階層タグ"
   },
   "slack_integration": {
+    "bot_reset_successful": "Botの設定を消去しました。",
+    "copied_to_clipboard": "クリップボードにコピーされました。",
     "modal": {
       "warning": "注意",
       "sure_change_bot_type": "Botの種類を変更しますか?",

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

@@ -261,6 +261,8 @@
 		"delete": "删除"
   },
   "slack_integration": {
+    "bot_reset_successful": "删除了BOT设置。",
+    "copied_to_clipboard": "它已复制到剪贴板。",
     "modal": {
       "warning": "警告",
       "sure_change_bot_type": "您确定要更改设置吗?",

+ 51 - 25
src/client/js/components/Admin/SlackIntegration/AccessTokenSettings.jsx

@@ -1,39 +1,65 @@
-/* eslint-disable no-console */
 import React from 'react';
+import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
+import { CopyToClipboard } from 'react-copy-to-clipboard';
+import { toastSuccess } from '../../../util/apiNotification';
 
-function AccessTokenSettings() {
-
+const AccessTokenSettings = (props) => {
   const { t } = useTranslation('admin');
-  function discardHandler() {
-    console.log('Discard button pressed');
-  }
 
-  function generateHandler() {
-    console.log('Generate button pressed');
-  }
+  const onClickDiscardButton = () => {
+    if (props.onClickDiscardButton != null) {
+      props.onClickDiscardButton();
+    }
+  };
+
+  const onClickGenerateToken = () => {
+    if (props.onClickGenerateToken != null) {
+      props.onClickGenerateToken();
+    }
+  };
+
+  const accessToken = props.accessToken ? props.accessToken : '';
 
   return (
-    <>
-      <div className="form-group row my-5">
-        <label className="text-left text-md-right col-md-3 col-form-label">Access Token</label>
-        <div className="col-md-6">
-          <input className="form-control" type="text" placeholder="access-token" />
+    <div className="row">
+      <div className="col-lg-12">
+
+        <h2 className="admin-setting-header">Access Token</h2>
+
+        <div className="form-group row my-5">
+          <label className="text-left text-md-right col-md-3 col-form-label">Access Token</label>
+          <div className="col-md-6">
+            {accessToken.length === 0 ? (
+              <input className="form-control" type="text" value={accessToken} readOnly />
+            ) : (
+              <CopyToClipboard text={accessToken} onCopy={() => toastSuccess(t('slack_integration.copied_to_clipboard'))}>
+                <input className="form-control" type="text" value={accessToken} readOnly />
+              </CopyToClipboard>
+            )}
+          </div>
         </div>
-      </div>
 
-      <div className="row">
-        <div className="mx-auto">
-          <button type="button" className="btn btn-outline-secondary text-nowrap mx-1" onClick={discardHandler}>
-            {t('slack_integration.access_token_settings.discard')}
-          </button>
-          <button type="button" className="btn btn-primary text-nowrap mx-1" onClick={generateHandler}>
-            {t('slack_integration.access_token_settings.generate')}
-          </button>
+        <div className="row">
+          <div className="mx-auto">
+            <button type="button" className="btn btn-outline-secondary text-nowrap mx-1" onClick={onClickDiscardButton} disabled={accessToken.length === 0}>
+              {t('slack_integration.access_token_settings.discard')}
+            </button>
+            <button type="button" className="btn btn-primary text-nowrap mx-1" onClick={onClickGenerateToken}>
+              {t('slack_integration.access_token_settings.generate')}
+            </button>
+          </div>
         </div>
+
       </div>
-    </>
+    </div>
   );
-}
+};
+
+AccessTokenSettings.propTypes = {
+  accessToken: PropTypes.string,
+  onClickDiscardButton: PropTypes.func,
+  onClickGenerateToken: PropTypes.func,
+};
 
 export default AccessTokenSettings;

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

@@ -2,34 +2,57 @@ import React, { useState, useEffect, useCallback } from 'react';
 import { useTranslation } from 'react-i18next';
 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 AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+import SlackGrowiBridging from './SlackGrowiBridging';
+
 
 const CustomBotWithoutProxySettings = (props) => {
-  const { appContainer } = props;
+  const { appContainer, adminAppContainer } = props;
   const { t } = useTranslation();
 
   const [slackSigningSecret, setSlackSigningSecret] = useState('');
   const [slackBotToken, setSlackBotToken] = useState('');
   const [slackSigningSecretEnv, setSlackSigningSecretEnv] = useState('');
   const [slackBotTokenEnv, setSlackBotTokenEnv] = useState('');
-  const botType = 'custom-bot-without-proxy';
+  const [slackWSNameInWithoutProxy, setSlackWSNameInWithoutProxy] = useState(null);
+  // get site name from this GROWI
+  // eslint-disable-next-line no-unused-vars
+  const [siteName, setSiteName] = useState('');
+  // eslint-disable-next-line no-unused-vars
+  const [isBoltSetup, setIsBoltSetup] = useState(null);
+  const currentBotType = 'custom-bot-without-proxy';
+
+  const getSlackWSInWithoutProxy = useCallback(async() => {
+    try {
+      const res = await appContainer.apiv3.get('/slack-integration/custom-bot-without-proxy/slack-workspace-name');
+      setSlackWSNameInWithoutProxy(res.data.slackWorkSpaceName);
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [appContainer]);
+
   const fetchData = useCallback(async() => {
     try {
+      await adminAppContainer.retrieveAppSettingsData();
       const res = await appContainer.apiv3.get('/slack-integration/');
       const {
-        slackSigningSecret, slackBotToken, slackSigningSecretEnvVars, slackBotTokenEnvVars,
+        slackSigningSecret, slackBotToken, slackSigningSecretEnvVars, slackBotTokenEnvVars, isBoltSetup,
       } = res.data.slackBotSettingParams.customBotWithoutProxySettings;
       setSlackSigningSecret(slackSigningSecret);
       setSlackBotToken(slackBotToken);
       setSlackSigningSecretEnv(slackSigningSecretEnvVars);
       setSlackBotTokenEnv(slackBotTokenEnvVars);
+      setSiteName(adminAppContainer.state.title);
+      setIsBoltSetup(isBoltSetup);
     }
     catch (err) {
       toastError(err);
     }
-  }, [appContainer]);
+  }, [appContainer, adminAppContainer]);
 
   useEffect(() => {
     fetchData();
@@ -40,8 +63,9 @@ const CustomBotWithoutProxySettings = (props) => {
       await appContainer.apiv3.put('/slack-integration/custom-bot-without-proxy', {
         slackSigningSecret,
         slackBotToken,
-        botType,
+        currentBotType,
       });
+      getSlackWSInWithoutProxy();
       toastSuccess(t('toaster.update_successed', { target: t('admin:slack_integration.custom_bot_without_proxy_settings') }));
     }
     catch (err) {
@@ -52,6 +76,11 @@ const CustomBotWithoutProxySettings = (props) => {
   return (
     <>
       <h2 className="admin-setting-header">{t('admin:slack_integration.custom_bot_without_proxy_settings')}</h2>
+      {/* temporarily put bellow component */}
+      <SlackGrowiBridging
+        siteName={siteName}
+        slackWorkSpaceName={slackWSNameInWithoutProxy}
+      />
       <div className="row my-5">
         <div className="mx-auto">
           <button
@@ -129,10 +158,11 @@ const CustomBotWithoutProxySettings = (props) => {
   );
 };
 
-const CustomBotWithoutProxySettingsWrapper = withUnstatedContainers(CustomBotWithoutProxySettings, [AppContainer]);
+const CustomBotWithoutProxySettingsWrapper = withUnstatedContainers(CustomBotWithoutProxySettings, [AppContainer, AdminAppContainer]);
 
 CustomBotWithoutProxySettings.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
 };
 
 export default CustomBotWithoutProxySettingsWrapper;

+ 17 - 0
src/client/js/components/Admin/SlackIntegration/SlackGrowiBridging.jsx

@@ -0,0 +1,17 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const SlackGrowiBridging = (props) => {
+  return (
+    <>
+      {props.slackWorkSpaceName}{props.siteName}
+    </>
+  );
+};
+
+SlackGrowiBridging.propTypes = {
+  slackWorkSpaceName: PropTypes.string,
+  siteName: PropTypes.string,
+};
+
+export default SlackGrowiBridging;

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

@@ -1,4 +1,9 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
+import AppContainer from '../../../services/AppContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 import AccessTokenSettings from './AccessTokenSettings';
 import OfficialBotSettings from './OfficialBotSettings';
@@ -6,9 +11,29 @@ import CustomBotWithoutProxySettings from './CustomBotWithoutProxySettings';
 import CustomBotWithProxySettings from './CustomBotWithProxySettings';
 import ConfirmBotChangeModal from './ConfirmBotChangeModal';
 
-const SlackIntegration = () => {
+
+const SlackIntegration = (props) => {
+  const { appContainer } = props;
+  const { t } = useTranslation();
   const [currentBotType, setCurrentBotType] = useState(null);
   const [selectedBotType, setSelectedBotType] = useState(null);
+  const [accessToken, setAccessToken] = useState('');
+
+  const fetchData = useCallback(async() => {
+    try {
+      const response = await appContainer.apiv3.get('slack-integration/');
+      const { currentBotType, accessToken } = response.data.slackBotSettingParams;
+      setCurrentBotType(currentBotType);
+      setAccessToken(accessToken);
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [appContainer.apiv3]);
+
+  useEffect(() => {
+    fetchData();
+  }, [fetchData]);
 
   const handleBotTypeSelect = (clickedBotType) => {
     if (clickedBotType === currentBotType) {
@@ -21,13 +46,45 @@ const SlackIntegration = () => {
     setSelectedBotType(clickedBotType);
   };
 
-  const handleCancelBotChange = () => {
+  const cancelBotChangeHandler = () => {
     setSelectedBotType(null);
   };
 
-  const changeCurrentBotSettings = () => {
-    setCurrentBotType(selectedBotType);
-    setSelectedBotType(null);
+  const changeCurrentBotSettingsHandler = async() => {
+    try {
+      const res = await appContainer.apiv3.put('slack-integration/custom-bot-without-proxy', {
+        slackSigningSecret: '',
+        slackBotToken: '',
+        currentBotType: selectedBotType,
+      });
+      setCurrentBotType(res.data.customBotWithoutProxySettingParams.slackBotType);
+      setSelectedBotType(null);
+      toastSuccess(t('admin:slack_integration.bot_reset_successful'));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  };
+
+  const generateTokenHandler = async() => {
+    try {
+      await appContainer.apiv3.put('slack-integration/access-token');
+      fetchData();
+    }
+    catch (err) {
+      toastError(err);
+    }
+  };
+
+  const discardTokenHandler = async() => {
+    try {
+      await appContainer.apiv3.delete('slack-integration/access-token');
+      fetchData();
+      toastSuccess(t('admin:slack_integration.bot_reset_successful'));
+    }
+    catch (err) {
+      toastError(err);
+    }
   };
 
   let settingsComponent = null;
@@ -37,7 +94,9 @@ const SlackIntegration = () => {
       settingsComponent = <OfficialBotSettings />;
       break;
     case 'custom-bot-without-proxy':
-      settingsComponent = <CustomBotWithoutProxySettings />;
+      settingsComponent = (
+        <CustomBotWithoutProxySettings />
+      );
       break;
     case 'custom-bot-with-proxy':
       settingsComponent = <CustomBotWithProxySettings />;
@@ -46,21 +105,17 @@ const SlackIntegration = () => {
 
   return (
     <>
-      <div className="container">
-        <ConfirmBotChangeModal
-          isOpen={selectedBotType != null}
-          onConfirmClick={changeCurrentBotSettings}
-          onCancelClick={handleCancelBotChange}
-        />
-      </div>
-
-      <div className="row">
-        <div className="col-lg-12">
-          <h2 className="admin-setting-header">Access Token</h2>
-          <AccessTokenSettings />
-        </div>
-      </div>
-
+      <ConfirmBotChangeModal
+        isOpen={selectedBotType != null}
+        onConfirmClick={changeCurrentBotSettingsHandler}
+        onCancelClick={cancelBotChangeHandler}
+      />
+
+      <AccessTokenSettings
+        accessToken={accessToken}
+        onClickDiscardButton={discardTokenHandler}
+        onClickGenerateToken={generateTokenHandler}
+      />
 
       <div className="row my-5">
         <div className="card-deck mx-auto">
@@ -103,4 +158,10 @@ const SlackIntegration = () => {
   );
 };
 
-export default SlackIntegration;
+const SlackIntegrationWrapper = withUnstatedContainers(SlackIntegration, [AppContainer]);
+
+SlackIntegration.propTypes = {
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+};
+
+export default SlackIntegrationWrapper;

+ 102 - 11
src/server/routes/apiv3/slack-integration.js

@@ -3,6 +3,7 @@ const loggerFactory = require('@alias/logger');
 const logger = loggerFactory('growi:routes:apiv3:notification-setting');
 const express = require('express');
 const { body } = require('express-validator');
+const { WebClient } = require('@slack/web-api');
 const crypto = require('crypto');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
@@ -27,7 +28,13 @@ const router = express.Router();
  *            type: string
  *          slackBotToken:
  *            type: string
- *          botType:
+ *          currentBotType:
+ *            type: string
+ *      SlackIntegration:
+ *        description: SlackIntegration
+ *        type: object
+ *        properties:
+ *          currentBotType:
  *            type: string
  */
 
@@ -39,12 +46,15 @@ module.exports = (crowi) => {
   const csrf = require('../../middlewares/csrf')(crowi);
   const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
 
-
   const validator = {
-    CusotmBotWithoutProxy: [
+    CustomBotWithoutProxy: [
       body('slackSigningSecret').isString(),
       body('slackBotToken').isString(),
-      body('botType').isString(),
+      body('currentBotType').isString(),
+    ],
+    SlackIntegration: [
+      body('currentBotType')
+        .isIn(['official-bot', 'custom-bot-without-proxy', 'custom-bot-with-proxy']),
     ],
   };
 
@@ -69,16 +79,16 @@ module.exports = (crowi) => {
    *      get:
    *        tags: [SlackBotSettingParams]
    *        operationId: getSlackBotSettingParams
-   *        summary: /slack-integration
+   *        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 = {
-      slackBotType: crowi.configManager.getConfig('crowi', 'slackbot:type'),
+      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
@@ -91,6 +101,7 @@ module.exports = (crowi) => {
         slackBotTokenEnvVars: crowi.configManager.getConfigFromEnvVars('crowi', 'slackbot:token'),
         slackSigningSecret: crowi.configManager.getConfig('crowi', 'slackbot:signingSecret'),
         slackBotToken: crowi.configManager.getConfig('crowi', 'slackbot:token'),
+        isBoltSetup: crowi.boltService.isBoltSetup,
       },
       // TODO imple when creating with proxy
       customBotWithProxySettings: {
@@ -101,6 +112,52 @@ module.exports = (crowi) => {
     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 bolt service
+        crowi.boltService.initialize();
+        crowi.boltService.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'));
+      }
+    });
+
   /**
    * @swagger
    *
@@ -121,13 +178,13 @@ module.exports = (crowi) => {
    *             description: Succeeded to put CustomBotWithoutProxy setting.
    */
   router.put('/custom-bot-without-proxy',
-    accessTokenParser, loginRequiredStrictly, adminRequired, csrf, validator.CusotmBotWithoutProxy, apiV3FormValidator, async(req, res) => {
-      const { slackSigningSecret, slackBotToken, botType } = req.body;
+    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:type': botType,
+        'slackbot:currentBotType': currentBotType,
       };
 
       try {
@@ -141,7 +198,7 @@ module.exports = (crowi) => {
         const customBotWithoutProxySettingParams = {
           slackSigningSecret: crowi.configManager.getConfig('crowi', 'slackbot:signingSecret'),
           slackBotToken: crowi.configManager.getConfig('crowi', 'slackbot:token'),
-          slackBotType: crowi.configManager.getConfig('crowi', 'slackbot:type'),
+          slackBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType'),
         };
         return res.apiv3({ customBotWithoutProxySettingParams });
       }
@@ -152,6 +209,40 @@ 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', async(req, res) => {
+    // get ws name in custom bot from slackbot token
+    const slackBotToken = crowi.configManager.getConfig('crowi', 'slackbot:token');
+
+    let slackWorkSpaceName = null;
+    if (slackBotToken != null) {
+      const web = new WebClient(slackBotToken);
+      try {
+        const slackTeamInfo = await web.team.info();
+        slackWorkSpaceName = slackTeamInfo.team.name;
+      }
+      catch (error) {
+        const msg = 'Error occured in slack_bot_token';
+        logger.error('Error', msg);
+        return res.apiv3Err(new ErrorV3(msg, 'get-SlackWorkSpaceName-failed'));
+      }
+    }
+
+    return res.apiv3({ slackWorkSpaceName });
+  });
+
   /**
    * @swagger
    *

+ 7 - 1
src/server/service/config-loader.js

@@ -410,9 +410,15 @@ 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:type', // eg. official || custom-without-proxy || custom-with-proxy
+    key:     'slackbot:currentBotType', // 'official-bot' || 'custom-bot-without-proxy' || 'custom-bot-with-proxy'
     type:    TYPES.STRING,
     default: null,
   },