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

Merge remote-tracking branch 'origin/feat/growi-bot' into feat/growi-bot-proxy

Yuki Takei 5 лет назад
Родитель
Сommit
0473b366fa

+ 0 - 1
package.json

@@ -84,7 +84,6 @@
     "@kobalab/socket.io-session": "^1.0.3",
     "@promster/express": "^5.0.1",
     "@promster/server": "^6.0.0",
-    "@slack/bolt": "^3.0.0",
     "@slack/events-api": "^3.0.0",
     "@slack/web-api": "^6.1.0",
     "JSONStream": "^1.3.5",

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

@@ -254,6 +254,7 @@
   },
   "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?",

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

@@ -252,6 +252,7 @@
   },
   "slack_integration": {
     "bot_reset_successful": "Botの設定を消去しました。",
+    "copied_to_clipboard": "クリップボードにコピーされました。",
     "modal": {
       "warning": "注意",
       "sure_change_bot_type": "Botの種類を変更しますか?",

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

@@ -262,6 +262,7 @@
   },
   "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;

+ 21 - 10
src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx

@@ -21,30 +21,41 @@ const CustomBotWithoutProxySettings = (props) => {
   // 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 [isSetupSlackBot, setIsSetupSlackBot] = useState(null);
+  const [isConnectedToSlack, setIsConnectedToSlack] = 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);
+  useEffect(() => {
+    const fetchData = 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);
+      }
+    };
+    setSlackWSNameInWithoutProxy(null);
+    if (isConnectedToSlack) {
+      fetchData();
     }
-  }, [appContainer]);
+  }, [appContainer, isConnectedToSlack]);
 
   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, isSetupSlackBot, isConnectedToSlack,
       } = res.data.slackBotSettingParams.customBotWithoutProxySettings;
       setSlackSigningSecret(slackSigningSecret);
       setSlackBotToken(slackBotToken);
       setSlackSigningSecretEnv(slackSigningSecretEnvVars);
       setSlackBotTokenEnv(slackBotTokenEnvVars);
       setSiteName(adminAppContainer.state.title);
+      setIsSetupSlackBot(isSetupSlackBot);
+      setIsConnectedToSlack(isConnectedToSlack);
     }
     catch (err) {
       toastError(err);
@@ -62,7 +73,7 @@ const CustomBotWithoutProxySettings = (props) => {
         slackBotToken,
         currentBotType,
       });
-      getSlackWSInWithoutProxy();
+      fetchData();
       toastSuccess(t('toaster.update_successed', { target: t('admin:slack_integration.custom_bot_without_proxy_settings') }));
     }
     catch (err) {

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

@@ -4,6 +4,7 @@ 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';
 import CustomBotWithoutProxySettings from './CustomBotWithoutProxySettings';
@@ -16,12 +17,14 @@ const SlackIntegration = (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 } = response.data.slackBotSettingParams;
+      const { currentBotType, accessToken } = response.data.slackBotSettingParams;
       setCurrentBotType(currentBotType);
+      setAccessToken(accessToken);
     }
     catch (err) {
       toastError(err);
@@ -43,11 +46,11 @@ const SlackIntegration = (props) => {
     setSelectedBotType(clickedBotType);
   };
 
-  const handleCancelBotChange = () => {
+  const cancelBotChangeHandler = () => {
     setSelectedBotType(null);
   };
 
-  const handleChangeCurrentBotSettings = async() => {
+  const changeCurrentBotSettingsHandler = async() => {
     try {
       const res = await appContainer.apiv3.put('slack-integration/custom-bot-without-proxy', {
         slackSigningSecret: '',
@@ -63,6 +66,27 @@ const SlackIntegration = (props) => {
     }
   };
 
+  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;
 
   switch (currentBotType) {
@@ -81,20 +105,17 @@ const SlackIntegration = (props) => {
 
   return (
     <>
-      <div className="container">
-        <ConfirmBotChangeModal
-          isOpen={selectedBotType != null}
-          onConfirmClick={handleChangeCurrentBotSettings}
-          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">

+ 7 - 7
src/server/crowi/index.js

@@ -57,7 +57,7 @@ function Crowi(rootdir) {
   this.syncPageStatusService = null;
   this.cdnResourcesService = new CdnResourcesService();
   this.interceptorManager = new InterceptorManager();
-  this.boltService = null;
+  this.slackBotService = null;
   this.xss = new Xss();
 
   this.tokens = null;
@@ -118,7 +118,7 @@ Crowi.prototype.init = async function() {
     this.setupImport(),
     this.setupPageService(),
     this.setupSyncPageStatusService(),
-    this.setupBoltService(),
+    this.setupSlackBotService(),
   ]);
 
   // globalNotification depends on slack and mailer
@@ -670,15 +670,15 @@ Crowi.prototype.setupSyncPageStatusService = async function() {
   }
 };
 
-Crowi.prototype.setupBoltService = async function() {
-  const BoltService = require('../service/bolt');
-  if (this.boltService == null) {
-    this.boltService = new BoltService(this);
+Crowi.prototype.setupSlackBotService = async function() {
+  const SlackBotService = require('../service/slackbot');
+  if (this.slackBotService == null) {
+    this.slackBotService = new SlackBotService(this);
   }
 
   // add as a message handler
   if (this.s2sMessagingService != null) {
-    this.s2sMessagingService.addMessageHandler(this.boltService);
+    this.s2sMessagingService.addMessageHandler(this.slackBotService);
   }
 };
 

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

@@ -9,8 +9,6 @@ const router = express.Router();
 
 module.exports = (crowi) => {
   this.app = crowi.express;
-  const { boltService } = crowi;
-  const requestHandler = boltService.receiver.requestHandler.bind(boltService.receiver);
 
 
   // Check if the access token is correct
@@ -40,8 +38,90 @@ module.exports = (crowi) => {
     // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
     res.send();
 
-    await requestHandler(req.body);
+    const { body } = req;
+    const args = body.text.split(' ');
+    const command = args[0];
+
+    try {
+      switch (command) {
+        case 'search':
+          await crowi.slackBotService.showEphemeralSearchResults(body, args);
+          break;
+        case 'create':
+          await crowi.slackBotService.createModal(body);
+          break;
+        default:
+          await crowi.slackBotService.notCommand(body);
+          break;
+      }
+    }
+    catch (error) {
+      logger.error(error);
+      return res.send(error.message);
+    }
+  });
+
+  const handleBlockActions = async(payload) => {
+    const { action_id: actionId } = payload.actions[0];
+
+    switch (actionId) {
+      case 'shareSearchResults': {
+        await crowi.slackBotService.shareSearchResults(payload);
+        break;
+      }
+      case 'showNextResults': {
+        const parsedValue = JSON.parse(payload.actions[0].value);
+
+        const { body, args, offset } = parsedValue;
+        const newOffset = offset + 10;
+        await crowi.slackBotService.showEphemeralSearchResults(body, args, newOffset);
+        break;
+      }
+      default:
+        break;
+    }
+  };
+
+  const handleViewSubmission = async(payload) => {
+    const { callback_id: callbackId } = payload.view;
+
+    switch (callbackId) {
+      case 'createPage':
+        await crowi.slackBotService.createPageInGrowi(payload);
+        break;
+      default:
+        break;
+    }
+  };
+
+  router.post('/interactive', verificationRequestUrl, 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();
+
+    const payload = JSON.parse(req.body.payload);
+    const { type } = payload;
+
+    try {
+      switch (type) {
+        case 'block_actions':
+          await handleBlockActions(payload);
+          break;
+        case 'view_submission':
+          await handleViewSubmission(payload);
+          break;
+        default:
+          break;
+      }
+    }
+    catch (error) {
+      logger.error(error);
+      return res.send(error.message);
+    }
+
   });
 
+
   return router;
 };

+ 25 - 30
src/server/routes/apiv3/slack-integration.js

@@ -3,7 +3,6 @@ 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');
 
@@ -87,6 +86,7 @@ module.exports = (crowi) => {
    */
   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: {
@@ -100,6 +100,8 @@ module.exports = (crowi) => {
         slackBotTokenEnvVars: crowi.configManager.getConfigFromEnvVars('crowi', 'slackbot:token'),
         slackSigningSecret: crowi.configManager.getConfig('crowi', 'slackbot:signingSecret'),
         slackBotToken: crowi.configManager.getConfig('crowi', 'slackbot:token'),
+        isSetupSlackBot: crowi.slackBotService.isSetupSlackBot,
+        isConnectedToSlack: crowi.slackBotService.isConnectedToSlack,
       },
       // TODO imple when creating with proxy
       customBotWithProxySettings: {
@@ -140,9 +142,9 @@ module.exports = (crowi) => {
       try {
         await updateSlackBotSettings(requestParams);
 
-        // initialize bolt service
-        crowi.boltService.initialize();
-        crowi.boltService.publishUpdatedMessage();
+        // initialize slack service
+        await crowi.slackBotService.initialize();
+        crowi.slackBotService.publishUpdatedMessage();
 
         const slackIntegrationSettingsParams = {
           currentBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType'),
@@ -152,7 +154,7 @@ module.exports = (crowi) => {
       catch (error) {
         const msg = 'Error occured in updating Slack bot setting';
         logger.error('Error', error);
-        return res.apiv3Err(new ErrorV3(msg, 'update-SlackIntegrationSetting-failed'));
+        return res.apiv3Err(new ErrorV3(msg, 'update-SlackIntegrationSetting-failed'), 500);
       }
     });
 
@@ -188,9 +190,9 @@ module.exports = (crowi) => {
       try {
         await updateSlackBotSettings(requestParams);
 
-        // initialize bolt service
-        crowi.boltService.initialize();
-        crowi.boltService.publishUpdatedMessage();
+        // initialize slack service
+        await crowi.slackBotService.initialize();
+        crowi.slackBotService.publishUpdatedMessage();
 
         // TODO Impl to delete AccessToken both of Proxy and GROWI when botType changes.
         const customBotWithoutProxySettingParams = {
@@ -220,25 +222,18 @@ module.exports = (crowi) => {
    *          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');
+  router.get('/custom-bot-without-proxy/slack-workspace-name', loginRequiredStrictly, adminRequired, async(req, res) => {
 
-    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'));
-      }
+    try {
+      const slackWorkSpaceName = await crowi.slackBotService.getSlackChannelName();
+      return res.apiv3({ slackWorkSpaceName });
+    }
+    catch (error) {
+      const msg = 'Error occured in slack_bot_token';
+      logger.error('Error', error);
+      return res.apiv3Err(new ErrorV3(msg, 'get-SlackWorkSpaceName-failed'), 500);
     }
 
-    return res.apiv3({ slackWorkSpaceName });
   });
 
   /**
@@ -260,9 +255,9 @@ module.exports = (crowi) => {
       const accessToken = generateAccessToken(req.user);
       await updateSlackBotSettings({ 'slackbot:access-token': accessToken });
 
-      // initialize bolt service
-      crowi.boltService.initialize();
-      crowi.boltService.publishUpdatedMessage();
+      // initialize slack service
+      await crowi.slackBotService.initialize();
+      crowi.slackBotService.publishUpdatedMessage();
 
       return res.apiv3({ accessToken });
     }
@@ -291,9 +286,9 @@ module.exports = (crowi) => {
     try {
       await updateSlackBotSettings({ 'slackbot:access-token': null });
 
-      // initialize bolt service
-      crowi.boltService.initialize();
-      crowi.boltService.publishUpdatedMessage();
+      // initialize slack service
+      await crowi.slackBotService.initialize();
+      crowi.slackBotService.publishUpdatedMessage();
 
       return res.apiv3({});
     }

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

@@ -410,6 +410,12 @@ 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', // 'official-bot' || 'custom-bot-without-proxy' || 'custom-bot-with-proxy'

+ 58 - 146
src/server/service/bolt.js → src/server/service/slackbot.js

@@ -1,97 +1,44 @@
-const logger = require('@alias/logger')('growi:service:BoltService');
+const logger = require('@alias/logger')('growi:service:SlackBotService');
 const mongoose = require('mongoose');
 
 const PAGINGLIMIT = 10;
 
-class BoltReciever {
-
-  init(app) {
-    this.bolt = app;
-  }
-
-  async requestHandler(body) {
-    if (this.bolt === undefined) {
-      throw new Error('Slack Bot service is not setup');
-    }
-
-    let ackCalled = false;
-
-    const payload = body.payload;
-    let reqBody;
-
-    if (payload != null) {
-      reqBody = JSON.parse(payload);
-    }
-    else {
-      reqBody = body;
-    }
-
-    const event = {
-      body: reqBody,
-      ack: (response) => {
-        if (ackCalled) {
-          return;
-        }
-
-        ackCalled = true;
-
-        if (response instanceof Error) {
-          const message = response.message || 'Error occurred';
-          throw new Error(message);
-        }
-        return;
-      },
-    };
-
-    await this.bolt.processEvent(event);
-  }
-
-}
-
-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 extends S2sMessageHandlable {
+class SlackBotService extends S2sMessageHandlable {
 
   constructor(crowi) {
     super();
 
     this.crowi = crowi;
     this.s2sMessagingService = crowi.s2sMessagingService;
-    this.receiver = new BoltReciever();
+
     this.client = null;
+    this.searchService = null;
+
+    this.isSetupSlackBot = false;
+    this.isConnectedToSlack = false;
 
-    this.isBoltSetup = false;
     this.lastLoadedAt = null;
 
     this.initialize();
   }
 
-  initialize() {
-    this.isBoltSetup = false;
+  async initialize() {
+    this.isSetupSlackBot = 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;
+    if (token != null) {
+      this.client = new WebClient(token, { logLevel: LogLevel.DEBUG });
+      logger.debug('SlackBot: setup is done');
+      this.isSetupSlackBot = true;
+      await this.sendAuthTest();
     }
 
-    this.bolt = new App({
-      token,
-      signingSecret,
-      receiver: this.receiver,
-    });
-    this.setupRoute();
-
-    logger.debug('SlackBot: setup is done');
-
-    this.isBoltSetup = true;
     this.lastLoadedAt = new Date();
   }
 
@@ -100,7 +47,7 @@ class BoltService extends S2sMessageHandlable {
    */
   shouldHandleS2sMessage(s2sMessage) {
     const { eventName, updatedAt } = s2sMessage;
-    if (eventName !== 'boltServiceUpdated' || updatedAt == null) {
+    if (eventName !== 'slackBotServiceUpdated' || updatedAt == null) {
       return false;
     }
 
@@ -114,7 +61,7 @@ class BoltService extends S2sMessageHandlable {
   async handleS2sMessage() {
     const { configManager } = this.crowi;
 
-    logger.info('Reset bolt by pubsub notification');
+    logger.info('Reset slack bot by pubsub notification');
     await configManager.loadConfigs();
     this.initialize();
   }
@@ -123,7 +70,7 @@ class BoltService extends S2sMessageHandlable {
     const { s2sMessagingService } = this;
 
     if (s2sMessagingService != null) {
-      const s2sMessage = new S2sMessage('boltServiceUpdated', { updatedAt: new Date() });
+      const s2sMessage = new S2sMessage('slackBotServiceUpdated', { updatedAt: new Date() });
 
       try {
         await s2sMessagingService.publish(s2sMessage);
@@ -134,65 +81,18 @@ class BoltService extends S2sMessageHandlable {
     }
   }
 
+  async sendAuthTest() {
+    this.isConnectedToSlack = false;
 
-  setupRoute() {
-    this.bolt.command('/growi', async({
-      command, client, body, ack,
-    }) => {
-      await ack();
-      const args = command.text.split(' ');
-      const firstArg = args[0];
-
-      switch (firstArg) {
-        case 'search':
-          await this.showEphemeralSearchResults(command, args);
-          break;
-
-        case 'create':
-          await this.createModal(command, client, body);
-          break;
-
-        default:
-          this.notCommand(command);
-          break;
-      }
-    });
-
-    this.bolt.view('createPage', async({
-      ack, view, body, client,
-    }) => {
-      await ack();
-      await this.createPageInGrowi(view, body);
-    });
-
-    this.bolt.action('showNextResults', async({
-      ack, action,
-    }) => {
-      await ack();
-      const parsedValue = JSON.parse(action.value);
-
-      const command = parsedValue.command;
-      const args = parsedValue.args;
-      const offset = parsedValue.offset;
-
-      const newOffset = offset + 10;
-      this.showEphemeralSearchResults(command, args, newOffset);
-    });
-
-    this.bolt.action('shareSearchResults', async({
-      body, ack, say, action,
-    }) => {
-      await ack();
-      await say(action.value);
-    });
-
+    await this.client.api.test();
+    this.isConnectedToSlack = true;
   }
 
-  notCommand(command) {
+  notCommand(body) {
     logger.error('Invalid first argument');
     this.client.chat.postEphemeral({
-      channel: command.channel_id,
-      user: command.user_id,
+      channel: body.channel_id,
+      user: body.user_id,
       blocks: [
         this.generateMarkdownSectionBlock('*No command.*\n Hint\n `/growi [command] [keyword]`'),
       ],
@@ -206,12 +106,12 @@ class BoltService extends S2sMessageHandlable {
     return keywords;
   }
 
-  async getSearchResultPaths(command, args, offset = 0) {
+  async getSearchResultPaths(body, args, offset = 0) {
     const firstKeyword = args[1];
     if (firstKeyword == null) {
       this.client.chat.postEphemeral({
-        channel: command.channel_id,
-        user: command.user_id,
+        channel: body.channel_id,
+        user: body.user_id,
         blocks: [
           this.generateMarkdownSectionBlock('*Input keywords.*\n Hint\n `/growi search [keyword]`'),
         ],
@@ -230,8 +130,8 @@ class BoltService extends S2sMessageHandlable {
     if (results.data.length === 0) {
       logger.info(`No page found with "${keywords}"`);
       this.client.chat.postEphemeral({
-        channel: command.channel_id,
-        user: command.user_id,
+        channel: body.channel_id,
+        user: body.user_id,
         blocks: [
           this.generateMarkdownSectionBlock(`*No page that matches your keyword(s) "${keywords}".*`),
           this.generateMarkdownSectionBlock(':mag: *Help: Searching*'),
@@ -263,10 +163,22 @@ class BoltService extends S2sMessageHandlable {
     };
   }
 
-  async showEphemeralSearchResults(command, args, offsetNum) {
+  async getSlackChannelName() {
+    const slackTeamInfo = await this.client.team.info();
+    return slackTeamInfo.team.name;
+  }
+
+  shareSearchResults(payload) {
+    this.client.chat.postMessage({
+      channel: payload.channel.id,
+      text: payload.actions[0].value,
+    });
+  }
+
+  async showEphemeralSearchResults(body, args, offsetNum) {
     const {
       resultPaths, offset, resultsTotal,
-    } = await this.getSearchResultPaths(command, args, offsetNum);
+    } = await this.getSearchResultPaths(body, args, offsetNum);
 
     const keywords = this.getKeywords(args);
 
@@ -327,13 +239,13 @@ class BoltService extends S2sMessageHandlable {
               text: 'Next',
             },
             action_id: 'showNextResults',
-            value: JSON.stringify({ offset, command, args }),
+            value: JSON.stringify({ offset, body, args }),
           },
         );
       }
       await this.client.chat.postEphemeral({
-        channel: command.channel_id,
-        user: command.user_id,
+        channel: body.channel_id,
+        user: body.user_id,
         blocks: [
           this.generateMarkdownSectionBlock(keywordsAndDesc),
           this.generateMarkdownSectionBlock(`${urls.join('\n')}`),
@@ -344,8 +256,8 @@ class BoltService extends S2sMessageHandlable {
     catch {
       logger.error('Failed to get search results.');
       await this.client.chat.postEphemeral({
-        channel: command.channel_id,
-        user: command.user_id,
+        channel: body.channel_id,
+        user: body.user_id,
         blocks: [
           this.generateMarkdownSectionBlock('*Failed to search.*\n Hint\n `/growi search [keyword]`'),
         ],
@@ -354,9 +266,9 @@ class BoltService extends S2sMessageHandlable {
     }
   }
 
-  async createModal(command, client, body) {
+  async createModal(body) {
     try {
-      await client.views.open({
+      await this.client.views.open({
         trigger_id: body.trigger_id,
 
         view: {
@@ -385,8 +297,8 @@ class BoltService extends S2sMessageHandlable {
     catch (err) {
       logger.error('Failed to create a page.');
       await this.client.chat.postEphemeral({
-        channel: command.channel_id,
-        user: command.user_id,
+        channel: body.channel_id,
+        user: body.user_id,
         blocks: [
           this.generateMarkdownSectionBlock(`*Failed to create new page.*\n ${err}`),
         ],
@@ -396,14 +308,14 @@ class BoltService extends S2sMessageHandlable {
   }
 
   // Submit action in create Modal
-  async createPageInGrowi(view, body) {
+  async createPageInGrowi(payload) {
     const Page = this.crowi.model('Page');
     const pathUtils = require('growi-commons').pathUtils;
 
-    const contentsBody = view.state.values.contents.contents_input.value;
+    const contentsBody = payload.view.state.values.contents.contents_input.value;
 
     try {
-      let path = view.state.values.path.path_input.value;
+      let path = payload.view.state.values.path.path_input.value;
       // sanitize path
       path = this.crowi.xss.process(path);
       path = pathUtils.normalizePath(path);
@@ -414,7 +326,7 @@ class BoltService extends S2sMessageHandlable {
     }
     catch (err) {
       this.client.chat.postMessage({
-        channel: body.user.id,
+        channel: payload.user.id,
         blocks: [
           this.generateMarkdownSectionBlock(`Cannot create new page to existed path\n *Contents* :memo:\n ${contentsBody}`)],
       });
@@ -461,4 +373,4 @@ class BoltService extends S2sMessageHandlable {
 
 }
 
-module.exports = BoltService;
+module.exports = SlackBotService;

+ 1 - 38
yarn.lock

@@ -1843,27 +1843,6 @@
   dependencies:
     type-detect "4.0.8"
 
-"@slack/bolt@^3.0.0":
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/@slack/bolt/-/bolt-3.0.0.tgz#3fb9cf4669178727b0345feb91a9c99038243784"
-  integrity sha512-AM+ZHVWkn9tqI0JKk6yzTnQxpJuk/b3umaPyiW0BtM7nt37ZUli07xL8MvhlPBD7blh9Ow1VRwJw/ufzASUNsQ==
-  dependencies:
-    "@slack/logger" "^3.0.0"
-    "@slack/oauth" "^2.0.0"
-    "@slack/socket-mode" "1.0.0"
-    "@slack/types" "^2.0.0"
-    "@slack/web-api" "^6.0.0"
-    "@types/express" "^4.16.1"
-    "@types/node" ">=12"
-    "@types/promise.allsettled" "^1.0.3"
-    "@types/tsscmp" "^1.0.0"
-    axios "^0.21.1"
-    express "^4.16.4"
-    please-upgrade-node "^3.2.0"
-    promise.allsettled "^1.0.2"
-    raw-body "^2.3.3"
-    tsscmp "^1.0.6"
-
 "@slack/bolt@^3.3.0":
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/@slack/bolt/-/bolt-3.3.0.tgz#9bfb9252091f845ab20cac20d6801edae794a169"
@@ -1941,22 +1920,6 @@
     jsonwebtoken "^8.5.1"
     lodash.isstring "^4.0.1"
 
-"@slack/socket-mode@1.0.0":
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/@slack/socket-mode/-/socket-mode-1.0.0.tgz#b774e2e19b90b6a33f652afbd2cf2189eb7d497c"
-  integrity sha512-uDhFWljDsDjS1aME2gxzmbLvBBmPfOsEipI1+dPWEQdkcfGtM0Cg/TnqjbarHm58yNwAV6iS/RebJMC6CkeGEw==
-  dependencies:
-    "@slack/logger" "^2.0.0"
-    "@slack/web-api" "^5.14.0"
-    "@types/node" ">=12.0.0"
-    "@types/p-queue" "^2.3.2"
-    "@types/ws" "^7.2.5"
-    eventemitter3 "^3.1.0"
-    finity "^0.5.4"
-    p-cancelable "^1.1.0"
-    p-queue "^2.4.2"
-    ws "^7.3.1"
-
 "@slack/socket-mode@^1.0.0":
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/@slack/socket-mode/-/socket-mode-1.0.2.tgz#c805a627aa6528b888ad236872d082b40ba4247a"
@@ -1983,7 +1946,7 @@
   resolved "https://registry.yarnpkg.com/@slack/types/-/types-2.0.0.tgz#7b938ab576cd1d6c9ff9ad67a96f8058d101af10"
   integrity sha512-Nu4jWC39mDY5egAX4oElwOypdu8Cx9tmR7bo3ghaHYaC7mkKM1+b+soanW5s2ssu4yOLxMdFExMh6wlR34B6CA==
 
-"@slack/web-api@^5.14.0", "@slack/web-api@^5.7.0":
+"@slack/web-api@^5.7.0":
   version "5.15.0"
   resolved "https://registry.yarnpkg.com/@slack/web-api/-/web-api-5.15.0.tgz#6bcf1d0a833c0e87e45150c2fd1f9657e3ec0b0b"
   integrity sha512-tjQ8Zqv/Fmj9SOL9yIEd7IpTiKfKHi9DKAkfRVeotoX0clMr3SqQtBqO+KZMX27gm7dmgJsQaDKlILyzdCO+IA==