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

Merge branch 'feat/growi-bot' into feat/5529-delete-info-from-backend

深瀬スティーヴン 5 лет назад
Родитель
Сommit
27e66c02f3

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

@@ -66,7 +66,7 @@
     "load_plugins": "Load_plugins",
     "enable": "Enable",
     "disable": "Disable",
-    "use_env_var_if_empty": "If the value in the database is empty, the value of the environment variable <cod>{{variable}}</code> is used.",
+    "use_env_var_if_empty": "If the value in the database is empty, the value of the environment variable <code>{{variable}}</code> is used.",
     "note_for_the_only_env_option": "The GCS Settings is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> ."
   },
   "markdown_setting": {
@@ -260,6 +260,7 @@
       "cancel": "Cancel",
       "change": "Change"
     },
+    "use_env_var_if_empty": "If the value in the database is empty, the value of the environment variable <code>{{variable}}</code> is used.",
     "access_token_settings": {
       "discard": "Discard",
       "generate": "Generate"

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

@@ -258,6 +258,7 @@
       "cancel": "取消",
       "change": "変更する"
     },
+    "use_env_var_if_empty": "データベース側の値が空の場合、環境変数 <code>{{variable}}</code> の値を利用します",
     "access_token_settings": {
       "discard": "破棄",
       "generate": "発行"

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

@@ -268,6 +268,7 @@
       "cancel": "取消",
       "change": "改变"
     },
+    "use_env_var_if_empty": "如果数据库中的值为空,则环境变量的值 <cod>{{variable}}</code> 启用。",
     "access_token_settings": {
       "discard": "丢弃",
       "generate": "生成"

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

@@ -7,25 +7,22 @@ import {
 
 const ConfirmBotChangeModal = (props) => {
   const { t } = useTranslation('admin');
-  let isOpen = false;
-  let onConfirmClick = null;
-  let onCancelClick = null;
 
-  if (props.isOpen != null) {
-    isOpen = props.isOpen;
-  }
+  const handleCancelButton = () => {
+    if (props.onCancelClick != null) {
+      props.onCancelClick();
+    }
+  };
 
-  if (props.onConfirmClick != null) {
-    onConfirmClick = props.onConfirmClick;
-  }
-
-  if (props.onCancelClick != null) {
-    onCancelClick = props.onCancelClick;
-  }
+  const handleChangeButton = () => {
+    if (props.onConfirmClick != null) {
+      props.onConfirmClick();
+    }
+  };
 
   return (
-    <Modal isOpen={isOpen} centered>
-      <ModalHeader toggle={onCancelClick}>
+    <Modal isOpen={props.isOpen} centered>
+      <ModalHeader toggle={handleCancelButton}>
         {t('slack_integration.modal.warning')}
       </ModalHeader>
       <ModalBody>
@@ -37,10 +34,10 @@ const ConfirmBotChangeModal = (props) => {
         </div>
       </ModalBody>
       <ModalFooter>
-        <button type="button" className="btn btn-secondary" onClick={onCancelClick}>
+        <button type="button" className="btn btn-secondary" onClick={handleCancelButton}>
           {t('slack_integration.modal.cancel')}
         </button>
-        <button type="button" className="btn btn-primary" onClick={onConfirmClick}>
+        <button type="button" className="btn btn-primary" onClick={handleChangeButton}>
           {t('slack_integration.modal.change')}
         </button>
       </ModalFooter>
@@ -49,7 +46,7 @@ const ConfirmBotChangeModal = (props) => {
 };
 
 ConfirmBotChangeModal.propTypes = {
-  isOpen: PropTypes.bool,
+  isOpen: PropTypes.bool.isRequired,
   onConfirmClick: PropTypes.func,
   onCancelClick: PropTypes.func,
 };

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

@@ -63,30 +63,65 @@ const CustomBotWithoutProxySettings = (props) => {
           </button>
         </div>
       </div>
+      <table className="table settings-table">
+        <colgroup>
+          <col className="item-name" />
+          <col className="from-db" />
+          <col className="from-env-vars" />
+        </colgroup>
+        <thead>
+          <tr><th></th><th>Database</th><th>Environment variables</th></tr>
+        </thead>
+        <tbody>
+          <tr>
+            <th>Signing Secret</th>
+            <td>
+              <input
+                className="form-control"
+                type="text"
+                value={slackSigningSecret || ''}
+                onChange={e => setSlackSigningSecret(e.target.value)}
+              />
+            </td>
+            <td>
+              <input
+                className="form-control"
+                type="text"
+                value={slackSigningSecretEnv || ''}
+                readOnly
+              />
+              <p className="form-text text-muted">
+                <small dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.use_env_var_if_empty', { variable: 'SLACK_SIGNING_SECRET' }) }} />
+              </p>
+            </td>
+          </tr>
+          <tr>
+            <th>Bot User OAuth Token</th>
+            <td>
+              <input
+                className="form-control"
+                type="text"
+                value={slackBotToken || ''}
+                onChange={e => setSlackBotToken(e.target.value)}
+              />
+            </td>
+            <td>
+              <input
+                className="form-control"
+                type="text"
+                value={slackBotTokenEnv || ''}
+                readOnly
+              />
+              <p className="form-text text-muted">
+                <small dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.use_env_var_if_empty', { variable: 'SLACK_BOT_TOKEN' }) }} />
+              </p>
+            </td>
+
+          </tr>
+        </tbody>
+      </table>
 
-      <div className="form-group row">
-        <label className="text-left text-md-right col-md-3 col-form-label">Signing Secret</label>
-        <div className="col-md-6">
-          <input
-            className="form-control"
-            type="text"
-            value={slackSigningSecret || slackSigningSecretEnv || ''}
-            onChange={e => setSlackSigningSecret(e.target.value)}
-          />
-        </div>
-      </div>
 
-      <div className="form-group row mb-5">
-        <label className="text-left text-md-right col-md-3 col-form-label">Bot User OAuth Token</label>
-        <div className="col-md-6">
-          <input
-            className="form-control"
-            type="text"
-            value={slackBotToken || slackBotTokenEnv || ''}
-            onChange={e => setSlackBotToken(e.target.value)}
-          />
-        </div>
-      </div>
       <AdminUpdateButtonRow onClick={updateHandler} disabled={false} />
     </>
   );

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

@@ -85,9 +85,8 @@ const SlackIntegration = (props) => {
         <div className="card-deck mx-auto">
 
           <div
-            className={`card mx-3 py-5 rounded ${currentBotType === 'official-bot' ? 'border-info' : ''}`}
+            className={`card admin-bot-card mx-3 py-5 rounded ${currentBotType === 'official-bot' ? 'border-info' : ''}`}
             onClick={() => handleBotTypeSelect('official-bot')}
-            style={{ cursor: 'pointer' }}
           >
             <div className="card-body">
               <h5 className="card-title">Official Bot</h5>
@@ -96,9 +95,8 @@ const SlackIntegration = (props) => {
           </div>
 
           <div
-            className={`card mx-3 py-5 rounded ${currentBotType === 'custom-bot-without-proxy' ? 'border-info' : ''}`}
+            className={`card admin-bot-card mx-3 py-5 rounded ${currentBotType === 'custom-bot-without-proxy' ? 'border-info' : ''}`}
             onClick={() => handleBotTypeSelect('custom-bot-without-proxy')}
-            style={{ cursor: 'pointer' }}
           >
             <div className="card-body">
               <h5 className="card-title">Custom Bot (Without Proxy)</h5>
@@ -107,9 +105,8 @@ const SlackIntegration = (props) => {
           </div>
 
           <div
-            className={`card mx-3 py-5 rounded ${currentBotType === 'custom-bot-with-proxy' ? 'border-info' : ''}`}
+            className={`card admin-bot-card mx-3 py-5 rounded ${currentBotType === 'custom-bot-with-proxy' ? 'border-info' : ''}`}
             onClick={() => handleBotTypeSelect('custom-bot-with-proxy')}
-            style={{ cursor: 'pointer' }}
           >
             <div className="card-body">
               <h5 className="card-title">Custom Bot (With Proxy)</h5>

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

@@ -83,6 +83,10 @@
     }
   }
 
+  .admin-bot-card {
+    cursor: pointer;
+  }
+
   //// TODO: migrate to Bootstrap 4
   //// omit all .btn-toggle and use Switches
   //// https://getbootstrap.com/docs/4.2/components/forms/#switches

+ 5 - 0
src/server/crowi/index.js

@@ -675,6 +675,11 @@ Crowi.prototype.setupBoltService = async function() {
   if (this.boltService == null) {
     this.boltService = new BoltService(this);
   }
+
+  // add as a message handler
+  if (this.s2sMessagingService != null) {
+    this.s2sMessagingService.addMessageHandler(this.boltService);
+  }
 };
 
 module.exports = Crowi;

+ 24 - 3
src/server/routes/apiv3/slack-bot.js

@@ -1,6 +1,10 @@
 
 const express = require('express');
 
+const loggerFactory = require('@alias/logger');
+
+const logger = loggerFactory('growi:routes:apiv3:slack-bot');
+
 const router = express.Router();
 
 module.exports = (crowi) => {
@@ -8,13 +12,30 @@ module.exports = (crowi) => {
   const { boltService } = crowi;
   const requestHandler = boltService.receiver.requestHandler.bind(boltService.receiver);
 
-  router.post('/', async(req, res) => {
+
+  // Check if the access token is correct
+  function verificationAccessToken(req, res, next) {
+    const slackBotAccessToken = req.body.slack_bot_access_token || null;
+
+    if (slackBotAccessToken == null || slackBotAccessToken !== this.crowi.configManager.getConfig('crowi', 'slackbot:access-token')) {
+      logger.error('slack_bot_access_token is invalid.');
+      return res.send('*Access token is inValid*');
+    }
+
+    return next();
+  }
+
+  function verificationRequestUrl(req, res, next) {
     // for verification request URL on Event Subscriptions
     if (req.body.type === 'url_verification') {
-      res.send(req.body);
-      return;
+      return res.send(req.body);
     }
 
+    return next();
+  }
+
+  router.post('/', verificationRequestUrl, verificationAccessToken, async(req, res) => {
+
     // Send response immediately to avoid opelation_timeout error
     // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
     res.send();

+ 42 - 0
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 crypto = require('crypto');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 const router = express.Router();
@@ -53,6 +54,14 @@ module.exports = (crowi) => {
     return configManager.updateConfigsInTheSameNamespace('crowi', params, true);
   }
 
+
+  function generateAccessToken(user) {
+    const hasher = crypto.createHash('sha512');
+    hasher.update(new Date().getTime() + user._id);
+
+    return hasher.digest('base64');
+  }
+
   /**
    * @swagger
    *
@@ -123,6 +132,11 @@ module.exports = (crowi) => {
 
       try {
         await updateSlackBotSettings(requestParams);
+
+        // initialize bolt service
+        crowi.boltService.initialize();
+        crowi.boltService.publishUpdatedMessage();
+
         // TODO Impl to delete AccessToken both of Proxy and GROWI when botType changes.
         const customBotWithoutProxySettingParams = {
           slackSigningSecret: crowi.configManager.getConfig('crowi', 'slackbot:signingSecret'),
@@ -138,5 +152,33 @@ module.exports = (crowi) => {
       }
     });
 
+  /**
+   * @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, async(req, res) => {
+
+    try {
+      const accessToken = generateAccessToken(req.user);
+      await updateSlackBotSettings({ 'slackbot:access-token': accessToken });
+
+      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'));
+    }
+  });
+
   return router;
 };

+ 79 - 34
src/server/service/bolt.js

@@ -1,4 +1,5 @@
 const logger = require('@alias/logger')('growi:service:BoltService');
+const mongoose = require('mongoose');
 
 const PAGINGLIMIT = 10;
 
@@ -49,31 +50,92 @@ class BoltReciever {
 
 const { App } = require('@slack/bolt');
 const { WebClient, LogLevel } = require('@slack/web-api');
+const S2sMessage = require('../models/vo/s2s-message');
+const S2sMessageHandlable = require('./s2s-messaging/handlable');
 
-class BoltService {
+class BoltService extends S2sMessageHandlable {
 
   constructor(crowi) {
+    super();
+
     this.crowi = crowi;
+    this.s2sMessagingService = crowi.s2sMessagingService;
     this.receiver = new BoltReciever();
+    this.client = null;
 
-    const signingSecret = crowi.configManager.getConfig('crowi', 'slackbot:signingSecret');
-    const token = crowi.configManager.getConfig('crowi', 'slackbot:token');
+    this.isBoltSetup = false;
+    this.lastLoadedAt = null;
 
-    const client = new WebClient(token, { logLevel: LogLevel.DEBUG });
-    this.client = client;
+    this.initialize();
+  }
 
-    if (token != null || signingSecret != null) {
-      logger.debug('SlackBot: setup is done');
-      this.bolt = new App({
-        token,
-        signingSecret,
-        receiver: this.receiver,
-      });
-      this.init();
+  initialize() {
+    this.isBoltSetup = false;
+
+    const token = this.crowi.configManager.getConfig('crowi', 'slackbot:token');
+    const signingSecret = this.crowi.configManager.getConfig('crowi', 'slackbot:signingSecret');
+
+    this.client = new WebClient(token, { logLevel: LogLevel.DEBUG });
+
+    if (token == null || signingSecret == null) {
+      this.bolt = null;
+      return;
+    }
+
+    this.bolt = new App({
+      token,
+      signingSecret,
+      receiver: this.receiver,
+    });
+    this.setupRoute();
+
+    logger.debug('SlackBot: setup is done');
+
+    this.isBoltSetup = true;
+    this.lastLoadedAt = new Date();
+  }
+
+  /**
+   * @inheritdoc
+   */
+  shouldHandleS2sMessage(s2sMessage) {
+    const { eventName, updatedAt } = s2sMessage;
+    if (eventName !== 'boltServiceUpdated' || updatedAt == null) {
+      return false;
     }
+
+    return this.lastLoadedAt == null || this.lastLoadedAt < new Date(s2sMessage.updatedAt);
   }
 
-  init() {
+
+  /**
+   * @inheritdoc
+   */
+  async handleS2sMessage() {
+    const { configManager } = this.crowi;
+
+    logger.info('Reset bolt by pubsub notification');
+    await configManager.loadConfigs();
+    this.initialize();
+  }
+
+  async publishUpdatedMessage() {
+    const { s2sMessagingService } = this;
+
+    if (s2sMessagingService != null) {
+      const s2sMessage = new S2sMessage('boltServiceUpdated', { updatedAt: new Date() });
+
+      try {
+        await s2sMessagingService.publish(s2sMessage);
+      }
+      catch (e) {
+        logger.error('Failed to publish update message with S2sMessagingService: ', e.message);
+      }
+    }
+  }
+
+
+  setupRoute() {
     this.bolt.command('/growi', async({
       command, client, body, ack,
     }) => {
@@ -293,20 +355,6 @@ class BoltService {
   }
 
   async createModal(command, client, body) {
-    const User = this.crowi.model('User');
-    const slackUser = await User.findUserByUsername('slackUser');
-
-    // if "slackUser" is null, don't show create Modal
-    if (slackUser == null) {
-      logger.error('Failed to create a page because slackUser is not found.');
-      this.client.chat.postEphemeral({
-        channel: command.channel_id,
-        user: command.user_id,
-        blocks: [this.generateMarkdownSectionBlock('*slackUser does not exist.*')],
-      });
-      throw new Error('/growi command:create: slackUser is not found');
-    }
-
     try {
       await client.views.open({
         trigger_id: body.trigger_id,
@@ -349,23 +397,20 @@ class BoltService {
 
   // Submit action in create Modal
   async createPageInGrowi(view, body) {
-    const User = this.crowi.model('User');
     const Page = this.crowi.model('Page');
     const pathUtils = require('growi-commons').pathUtils;
 
     const contentsBody = view.state.values.contents.contents_input.value;
 
     try {
-      // search "slackUser" to create page in slack
-      const slackUser = await User.findUserByUsername('slackUser');
-
       let path = view.state.values.path.path_input.value;
       // sanitize path
       path = this.crowi.xss.process(path);
       path = pathUtils.normalizePath(path);
 
-      const user = slackUser._id;
-      await Page.create(path, contentsBody, user, {});
+      // generate a dummy id because Operation to create a page needs ObjectId
+      const dummyObjectIdOfUser = new mongoose.Types.ObjectId();
+      await Page.create(path, contentsBody, dummyObjectIdOfUser, {});
     }
     catch (err) {
       this.client.chat.postMessage({