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

Merge branch 'feat/growi-bot' into feat/get-slack-work-space-name

zahmis 5 лет назад
Родитель
Сommit
50dae380d9

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

@@ -253,12 +253,19 @@
     "delete": "Delete"
   },
   "slack_integration": {
+    "modal": {
+      "warning": "Warning",
+      "sure_change_bot_type": "Are you sure you want to change the bot type?",
+      "changes_will_be_deleted": "Settings for other bot types will be deleted.",
+      "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"
     },
-    "custom_bot_without_proxy_settings": "Custom bot (without-proxy) Settings",
+    "custom_bot_without_proxy_settings": "Custom Bot (Without-Proxy) Settings",
     "without_proxy": {
       "create_bot": "Create Bot"
     }

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

@@ -251,12 +251,19 @@
     "Directory_hierarchy_tag": "ディレクトリ階層タグ"
   },
   "slack_integration": {
+    "modal": {
+      "warning": "注意",
+      "sure_change_bot_type": "Botの種類を変更しますか?",
+      "changes_will_be_deleted": "他のBotの設定が消去されます。",
+      "cancel": "取消",
+      "change": "変更する"
+    },
     "use_env_var_if_empty": "データベース側の値が空の場合、環境変数 <code>{{variable}}</code> の値を利用します",
     "access_token_settings": {
       "discard": "破棄",
       "generate": "発行"
     },
-    "custom_bot_without_proxy_settings": "Custom bot (without-proxy) 設定",
+    "custom_bot_without_proxy_settings": "Custom Bot (Without-Proxy) 設定",
     "without_proxy": {
       "create_bot": "Bot を作成する"
     }

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

@@ -192,7 +192,7 @@
 			"upload": "Upload",
 			"discard": "Discard uploaded data",
 			"errors": {
-        "versions_not_met": "this growi and the uploarded data versions are not met",
+        "versions_not_met": "this growi and the uploaded data versions are not met",
 				"at_least_one": "Select one or more collections.",
 				"page_and_revision": "'Pages' and 'Revisions' must be imported both.",
 				"depends": "'{{target}}' must be selected when '{{condition}}' is selected."
@@ -261,12 +261,19 @@
 		"delete": "删除"
   },
   "slack_integration": {
+    "modal": {
+      "warning": "警告",
+      "sure_change_bot_type": "您确定要更改设置吗?",
+      "changes_will_be_deleted": "其他Bot类型的设置将被删除。",
+      "cancel": "取消",
+      "change": "改变"
+    },
     "use_env_var_if_empty": "如果数据库中的值为空,则环境变量的值 <cod>{{variable}}</code> 启用。",
     "access_token_settings": {
       "discard": "丢弃",
       "generate": "生成"
     },
-    "custom_bot_without_proxy_settings": "Custom bot (without-proxy) 设置",
+    "custom_bot_without_proxy_settings": "Custom Bot (Without-Proxy) 设置",
     "without_proxy": {
       "create_bot": "创建 Bot"
     }

+ 54 - 0
src/client/js/components/Admin/SlackIntegration/ConfirmBotChangeModal.jsx

@@ -0,0 +1,54 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
+import {
+  Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
+
+const ConfirmBotChangeModal = (props) => {
+  const { t } = useTranslation('admin');
+
+  const handleCancelButton = () => {
+    if (props.onCancelClick != null) {
+      props.onCancelClick();
+    }
+  };
+
+  const handleChangeButton = () => {
+    if (props.onConfirmClick != null) {
+      props.onConfirmClick();
+    }
+  };
+
+  return (
+    <Modal isOpen={props.isOpen} centered>
+      <ModalHeader toggle={handleCancelButton}>
+        {t('slack_integration.modal.warning')}
+      </ModalHeader>
+      <ModalBody>
+        <div>
+          <h4>{t('slack_integration.modal.sure_change_bot_type')}</h4>
+        </div>
+        <div>
+          <p>{t('slack_integration.modal.changes_will_be_deleted')}</p>
+        </div>
+      </ModalBody>
+      <ModalFooter>
+        <button type="button" className="btn btn-secondary" onClick={handleCancelButton}>
+          {t('slack_integration.modal.cancel')}
+        </button>
+        <button type="button" className="btn btn-primary" onClick={handleChangeButton}>
+          {t('slack_integration.modal.change')}
+        </button>
+      </ModalFooter>
+    </Modal>
+  );
+};
+
+ConfirmBotChangeModal.propTypes = {
+  isOpen: PropTypes.bool.isRequired,
+  onConfirmClick: PropTypes.func,
+  onCancelClick: PropTypes.func,
+};
+
+export default ConfirmBotChangeModal;

+ 12 - 0
src/client/js/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx

@@ -0,0 +1,12 @@
+import React from 'react';
+
+const CustomBotWithProxySettings = () => {
+
+  return (
+    <div className="row my-5">
+      <h1>With Proxy Component</h1>
+    </div>
+  );
+};
+
+export default CustomBotWithProxySettings;

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

@@ -59,6 +59,7 @@ const CustomBotWithoutProxySettings = (props) => {
 
   return (
     <>
+      <h2 className="admin-setting-header">{t('admin:slack_integration.custom_bot_without_proxy_settings')}</h2>
       <div className="row my-5">
         <div className="mx-auto">
           <button

+ 12 - 0
src/client/js/components/Admin/SlackIntegration/OfficialBotSettings.jsx

@@ -0,0 +1,12 @@
+import React from 'react';
+
+const OfficialBotSettings = () => {
+
+  return (
+    <div className="row my-5">
+      <h1>Official Bot Settings Component</h1>
+    </div>
+  );
+};
+
+export default OfficialBotSettings;

+ 86 - 9
src/client/js/components/Admin/SlackIntegration/SlackIntegration.jsx

@@ -1,14 +1,59 @@
-import React from 'react';
-import { useTranslation } from 'react-i18next';
+import React, { useState } from 'react';
 
 import AccessTokenSettings from './AccessTokenSettings';
+import OfficialBotSettings from './OfficialBotSettings';
 import CustomBotWithoutProxySettings from './CustomBotWithoutProxySettings';
+import CustomBotWithProxySettings from './CustomBotWithProxySettings';
+import ConfirmBotChangeModal from './ConfirmBotChangeModal';
 
-function SlackIntegration() {
+const SlackIntegration = () => {
+  const [currentBotType, setCurrentBotType] = useState(null);
+  const [selectedBotType, setSelectedBotType] = useState(null);
+
+  const handleBotTypeSelect = (clickedBotType) => {
+    if (clickedBotType === currentBotType) {
+      return;
+    }
+    if (currentBotType === null) {
+      setCurrentBotType(clickedBotType);
+      return;
+    }
+    setSelectedBotType(clickedBotType);
+  };
+
+  const handleCancelBotChange = () => {
+    setSelectedBotType(null);
+  };
+
+  const changeCurrentBotSettings = () => {
+    setCurrentBotType(selectedBotType);
+    setSelectedBotType(null);
+  };
+
+  let settingsComponent = null;
+
+  switch (currentBotType) {
+    case 'official-bot':
+      settingsComponent = <OfficialBotSettings />;
+      break;
+    case 'custom-bot-without-proxy':
+      settingsComponent = <CustomBotWithoutProxySettings />;
+      break;
+    case 'custom-bot-with-proxy':
+      settingsComponent = <CustomBotWithProxySettings />;
+      break;
+  }
 
-  const { t } = useTranslation('admin');
   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>
@@ -16,14 +61,46 @@ function SlackIntegration() {
         </div>
       </div>
 
-      <div className="row">
-        <div className="col-lg-12">
-          <h2 className="admin-setting-header">{t('slack_integration.custom_bot_without_proxy_settings')}</h2>
-          <CustomBotWithoutProxySettings />
+
+      <div className="row my-5">
+        <div className="card-deck mx-auto">
+
+          <div
+            className={`card admin-bot-card mx-3 py-5 rounded ${currentBotType === 'official-bot' ? 'border-info' : ''}`}
+            onClick={() => handleBotTypeSelect('official-bot')}
+          >
+            <div className="card-body">
+              <h5 className="card-title">Official Bot</h5>
+              <p className="card-text">This is a wider card with supporting text below as a natural lead-in to additional content.</p>
+            </div>
+          </div>
+
+          <div
+            className={`card admin-bot-card mx-3 py-5 rounded ${currentBotType === 'custom-bot-without-proxy' ? 'border-info' : ''}`}
+            onClick={() => handleBotTypeSelect('custom-bot-without-proxy')}
+          >
+            <div className="card-body">
+              <h5 className="card-title">Custom Bot (Without Proxy)</h5>
+              <p className="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. </p>
+            </div>
+          </div>
+
+          <div
+            className={`card admin-bot-card mx-3 py-5 rounded ${currentBotType === 'custom-bot-with-proxy' ? 'border-info' : ''}`}
+            onClick={() => handleBotTypeSelect('custom-bot-with-proxy')}
+          >
+            <div className="card-body">
+              <h5 className="card-title">Custom Bot (With Proxy)</h5>
+              <p className="card-text">This is a wider card with supporting text below as a natural lead-in to additional content.</p>
+            </div>
+          </div>
+
         </div>
       </div>
+
+      {settingsComponent}
     </>
   );
-}
+};
 
 export default SlackIntegration;

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

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

@@ -142,6 +142,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'),

+ 75 - 14
src/server/service/bolt.js

@@ -50,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;
+
+    this.isBoltSetup = false;
+    this.lastLoadedAt = null;
 
-    const signingSecret = crowi.configManager.getConfig('crowi', 'slackbot:signingSecret');
-    const token = crowi.configManager.getConfig('crowi', 'slackbot:token');
+    this.initialize();
+  }
 
-    const client = new WebClient(token, { logLevel: LogLevel.DEBUG });
-    this.client = client;
+  initialize() {
+    this.isBoltSetup = false;
 
-    if (token != null || signingSecret != null) {
-      logger.debug('SlackBot: setup is done');
-      this.bolt = new App({
-        token,
-        signingSecret,
-        receiver: this.receiver,
-      });
-      this.init();
+    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();
   }
 
-  init() {
+  /**
+   * @inheritdoc
+   */
+  shouldHandleS2sMessage(s2sMessage) {
+    const { eventName, updatedAt } = s2sMessage;
+    if (eventName !== 'boltServiceUpdated' || updatedAt == null) {
+      return false;
+    }
+
+    return this.lastLoadedAt == null || this.lastLoadedAt < new Date(s2sMessage.updatedAt);
+  }
+
+
+  /**
+   * @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,
     }) => {