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

Merge branch 'feat/growi-bot' into feat/5540-create-put-route-for-slack-integration

Steven Fukase 5 лет назад
Родитель
Сommit
cdf7ac1555

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

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

+ 32 - 37
src/client/js/components/Admin/SlackIntegration/ConfirmBotChangeModal.jsx

@@ -7,51 +7,46 @@ 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}>
-          {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={onCancelClick}>
-            {t('slack_integration.modal.cancel')}
-          </button>
-          <button type="button" className="btn btn-primary" onClick={onConfirmClick}>
-            {t('slack_integration.modal.change')}
-          </button>
-        </ModalFooter>
-      </Modal>
-    </>
+    <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,
+  isOpen: PropTypes.bool.isRequired,
   onConfirmClick: PropTypes.func,
   onCancelClick: PropTypes.func,
 };

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

@@ -91,6 +91,7 @@ const CustomBotWithoutProxySettings = (props) => {
                 readOnly
               />
               <p className="form-text text-muted">
+                {/* eslint-disable-next-line react/no-danger */}
                 <small dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.use_env_var_if_empty', { variable: 'SLACK_SIGNING_SECRET' }) }} />
               </p>
             </td>
@@ -113,6 +114,7 @@ const CustomBotWithoutProxySettings = (props) => {
                 readOnly
               />
               <p className="form-text text-muted">
+                {/* eslint-disable-next-line react/no-danger */}
                 <small dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.use_env_var_if_empty', { variable: 'SLACK_BOT_TOKEN' }) }} />
               </p>
             </td>

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

@@ -100,9 +100,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>
@@ -111,9 +110,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>
@@ -122,9 +120,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;

+ 44 - 3
src/server/routes/apiv3/slack-integration.js

@@ -182,6 +182,12 @@ 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'),
           slackBotToken: crowi.configManager.getConfig('crowi', 'slackbot:token'),
@@ -192,7 +198,7 @@ module.exports = (crowi) => {
       catch (error) {
         const msg = 'Error occured in updating Custom bot setting';
         logger.error('Error', error);
-        return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'));
+        return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
       }
     });
 
@@ -209,18 +215,53 @@ module.exports = (crowi) => {
    *          200:
    *            description: Succeeded to update access token for slack
    */
-  router.put('/access-token', loginRequiredStrictly, adminRequired, async(req, res) => {
+  router.put('/access-token', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
 
     try {
       const accessToken = generateAccessToken(req.user);
       await updateSlackBotSettings({ 'slackbot:access-token': accessToken });
 
+      // initialize bolt service
+      crowi.boltService.initialize();
+      crowi.boltService.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'));
+      return res.apiv3Err(new ErrorV3(msg, 'update-accessToken-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 bolt service
+      crowi.boltService.initialize();
+      crowi.boltService.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);
     }
   });
 

+ 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,
     }) => {