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

Merge branch 'feat/growi-bot' into feat/4937-5433-Discard-access-token

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

+ 3 - 1
package.json

@@ -77,9 +77,11 @@
     ],
     "@google-cloud/storage": "^3.3.0",
     "@kobalab/socket.io-session": "^1.0.3",
-    "@slack/bolt": "^3.0.0",
     "@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",
     "archiver": "^3.1.1",
     "array.prototype.flatmap": "^1.2.2",

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

@@ -257,8 +257,8 @@
       "discard": "Discard",
       "generate": "Generate"
     },
-    "custom_bot_non_proxy_settings": "Custom bot (non-proxy) Settings",
-    "non_proxy": {
+    "custom_bot_without_proxy_settings": "Custom bot (without-proxy) Settings",
+    "without_proxy": {
       "create_bot": "Create Bot"
     }
   },

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

@@ -255,8 +255,8 @@
       "discard": "破棄",
       "generate": "発行"
     },
-    "custom_bot_non_proxy_settings": "Custom bot (non-proxy) 設定",
-    "non_proxy": {
+    "custom_bot_without_proxy_settings": "Custom bot (without-proxy) 設定",
+    "without_proxy": {
       "create_bot": "Bot を作成する"
     }
   },

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

@@ -265,8 +265,8 @@
       "discard": "丢弃",
       "generate": "生成"
     },
-    "custom_bot_non_proxy_settings": "Custom bot (non-proxy) 设置",
-    "non_proxy": {
+    "custom_bot_without_proxy_settings": "Custom bot (without-proxy) 设置",
+    "without_proxy": {
       "create_bot": "创建 Bot"
     }
   },

+ 2 - 0
src/client/js/admin.jsx

@@ -10,6 +10,7 @@ import ErrorBoundary from './components/ErrorBoudary';
 import AdminHome from './components/Admin/AdminHome/AdminHome';
 import UserGroupDetailPage from './components/Admin/UserGroupDetail/UserGroupDetailPage';
 import NotificationSetting from './components/Admin/Notification/NotificationSetting';
+import SlackIntegrationNotificationSetting from './components/Admin/Notification/SlackIntegrationNotificationSetting';
 import SlackIntegration from './components/Admin/SlackIntegration/SlackIntegration';
 import ManageGlobalNotification from './components/Admin/Notification/ManageGlobalNotification';
 import MarkdownSetting from './components/Admin/MarkdownSetting/MarkDownSetting';
@@ -98,6 +99,7 @@ Object.assign(componentMappings, {
   'admin-export-page': <ExportArchiveDataPage />,
   'admin-notification-setting': <NotificationSetting />,
   'admin-slack-integration': <SlackIntegration />,
+  'admin-slack-integration-notification-setting': <SlackIntegrationNotificationSetting />,
   'admin-global-notification-setting': <ManageGlobalNotification />,
   'admin-user-page': <UserManagement />,
   'admin-external-account-setting': <ManageExternalAccount />,

+ 13 - 14
src/client/js/components/Admin/Common/AdminNavigation.jsx

@@ -14,20 +14,19 @@ const AdminNavigation = (props) => {
   // eslint-disable-next-line react/prop-types
   const MenuLabel = ({ menu }) => {
     switch (menu) {
-      case 'app':               return <><i className="icon-fw icon-settings"></i>        { t('App Settings') }</>;
-      case 'security':          return <><i className="icon-fw icon-shield"></i>          { t('security_settings') }</>;
-      case 'markdown':          return <><i className="icon-fw icon-note"></i>            { t('Markdown Settings') }</>;
-      case 'customize':         return <><i className="icon-fw icon-wrench"></i>          { t('Customize') }</>;
-      case 'importer':          return <><i className="icon-fw icon-cloud-upload"></i>    { t('Import Data') }</>;
-      case 'export':            return <><i className="icon-fw icon-cloud-download"></i>  { t('Export Archive Data') }</>;
-      case 'notification':      return <><i className="icon-fw icon-bell"></i>            { t('External_Notification') }</>;
-      // TODO change icon for legacy-slack-integration by GW-5466
-      case 'legacy-slack-integration':  return <> <i className="icon-fw icon-paper-plane"></i>    { t('Legacy_Slack_Integration') }</>;
-      case 'slack-integration': return <><i className="icon-fw icon-paper-plane"></i>     { t('slack_integration') }</>;
-      case 'users':             return <><i className="icon-fw icon-user"></i>            { t('User_Management') }</>;
-      case 'user-groups':       return <><i className="icon-fw icon-people"></i>          { t('UserGroup Management') }</>;
-      case 'search':            return <><i className="icon-fw icon-magnifier"></i>       { t('Full Text Search Management') }</>;
-      default:                  return <><i className="icon-fw icon-home"></i>            { t('Wiki Management Home Page') }</>;
+      case 'app':                      return <><i className="icon-fw icon-settings"></i>        { t('App Settings') }</>;
+      case 'security':                 return <><i className="icon-fw icon-shield"></i>          { t('security_settings') }</>;
+      case 'markdown':                 return <><i className="icon-fw icon-note"></i>            { t('Markdown Settings') }</>;
+      case 'customize':                return <><i className="icon-fw icon-wrench"></i>          { t('Customize') }</>;
+      case 'importer':                 return <><i className="icon-fw icon-cloud-upload"></i>    { t('Import Data') }</>;
+      case 'export':                   return <><i className="icon-fw icon-cloud-download"></i>  { t('Export Archive Data') }</>;
+      case 'notification':             return <><i className="icon-fw icon-bell"></i>            { t('External_Notification')}</>;
+      case 'legacy-slack-integration': return <><i className="fa fa-slack mr-2"></i>             { t('Legacy_Slack_Integration')}</>;
+      case 'slack-integration':        return <><i className="fa fa-slack mr-2"></i>             { t('slack_integration') }</>;
+      case 'users':                    return <><i className="icon-fw icon-user"></i>            { t('User_Management') }</>;
+      case 'user-groups':              return <><i className="icon-fw icon-people"></i>          { t('UserGroup Management') }</>;
+      case 'search':                   return <><i className="icon-fw icon-magnifier"></i>       { t('Full Text Search Management') }</>;
+      default:                         return <><i className="icon-fw icon-home"></i>            { t('Wiki Management Home Page') }</>;
     }
   };
 

+ 4 - 13
src/client/js/components/Admin/Notification/NotificationSetting.jsx

@@ -13,7 +13,6 @@ import AdminNotificationContainer from '../../../services/AdminNotificationConta
 
 import { CustomNavTab } from '../../CustomNavigation/CustomNav';
 
-import SlackAppConfiguration from './SlackAppConfiguration';
 import UserTriggerNotification from './UserTriggerNotification';
 import GlobalNotification from './GlobalNotification';
 
@@ -23,8 +22,8 @@ let retrieveErrors = null;
 function NotificationSetting(props) {
   const { adminNotificationContainer } = props;
 
-  const [activeTab, setActiveTab] = useState('slack_configuration');
-  const [activeComponents, setActiveComponents] = useState(new Set(['slack_configuration']));
+  const [activeTab, setActiveTab] = useState('user_trigger_notification');
+  const [activeComponents, setActiveComponents] = useState(new Set(['user_trigger_notification']));
 
   const switchActiveTab = (selectedTab) => {
     setActiveTab(selectedTab);
@@ -52,20 +51,15 @@ function NotificationSetting(props) {
 
   const navTabMapping = useMemo(() => {
     return {
-      slack_configuration: {
-        Icon: () => <i className="icon-settings" />,
-        i18n: 'Slack configuration',
-        index: 0,
-      },
       user_trigger_notification: {
         Icon: () => <i className="icon-settings" />,
         i18n: 'User trigger notification',
-        index: 1,
+        index: 0,
       },
       global_notification: {
         Icon: () => <i className="icon-settings" />,
         i18n: 'Global notification',
-        index: 2,
+        index: 1,
       },
     };
   }, []);
@@ -75,9 +69,6 @@ function NotificationSetting(props) {
       <CustomNavTab activeTab={activeTab} navTabMapping={navTabMapping} onNavSelected={switchActiveTab} hideBorderBottom />
 
       <TabContent activeTab={activeTab} className="p-5">
-        <TabPane tabId="slack_configuration">
-          {activeComponents.has('slack_configuration') && <SlackAppConfiguration />}
-        </TabPane>
         <TabPane tabId="user_trigger_notification">
           {activeComponents.has('user_trigger_notification') && <UserTriggerNotification />}
         </TabPane>

+ 80 - 0
src/client/js/components/Admin/Notification/SlackIntegrationNotificationSetting.jsx

@@ -0,0 +1,80 @@
+import React, { useMemo, useState } from 'react';
+import PropTypes from 'prop-types';
+
+import loggerFactory from '@alias/logger';
+
+import { TabContent, TabPane } from 'reactstrap';
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import { toastError } from '../../../util/apiNotification';
+import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
+
+import AdminNotificationContainer from '../../../services/AdminNotificationContainer';
+
+import { CustomNavTab } from '../../CustomNavigation/CustomNav';
+
+import SlackAppConfiguration from './SlackAppConfiguration';
+
+const logger = loggerFactory('growi:NotificationSetting');
+
+let retrieveErrors = null;
+function NotificationSetting(props) {
+  const { adminNotificationContainer } = props;
+
+  const [activeTab, setActiveTab] = useState('slack_configuration');
+  const [activeComponents, setActiveComponents] = useState(new Set(['slack_configuration']));
+
+  const switchActiveTab = (selectedTab) => {
+    setActiveTab(selectedTab);
+    setActiveComponents(activeComponents.add(selectedTab));
+  };
+
+  if (adminNotificationContainer.state.webhookUrl === adminNotificationContainer.dummyWebhookUrl) {
+    throw (async() => {
+      try {
+        await adminNotificationContainer.retrieveNotificationData();
+      }
+      catch (err) {
+        const errs = toArrayIfNot(err);
+        toastError(errs);
+        logger.error(errs);
+        retrieveErrors = errs;
+        adminNotificationContainer.setState({ webhookUrl: adminNotificationContainer.dummyWebhookUrlForError });
+      }
+    })();
+  }
+
+  if (adminNotificationContainer.state.webhookUrl === adminNotificationContainer.dummyWebhookUrlForError) {
+    throw new Error(`${retrieveErrors.length} errors occured`);
+  }
+
+  const navTabMapping = useMemo(() => {
+    return {
+      slack_configuration: {
+        Icon: () => <i className="icon-settings" />,
+        i18n: 'Slack configuration',
+        index: 0,
+      },
+    };
+  }, []);
+
+  return (
+    <>
+      <CustomNavTab activeTab={activeTab} navTabMapping={navTabMapping} onNavSelected={switchActiveTab} hideBorderBottom />
+
+      <TabContent activeTab={activeTab} className="p-5">
+        <TabPane tabId="slack_configuration">
+          {activeComponents.has('slack_configuration') && <SlackAppConfiguration />}
+        </TabPane>
+      </TabContent>
+    </>
+  );
+}
+
+const NotificationSettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(NotificationSetting), [AdminNotificationContainer]);
+
+NotificationSetting.propTypes = {
+  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
+};
+
+export default NotificationSettingWithUnstatedContainer;

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

@@ -1,54 +0,0 @@
-/* eslint-disable no-console */
-import React, { useState } from 'react';
-import { useTranslation } from 'react-i18next';
-
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
-function CustomBotNonProxySettings() {
-
-  const { t } = useTranslation('admin');
-  const [secret, setSecret] = useState('');
-  const [token, setToken] = useState('');
-
-  function updateHandler() {
-    console.log(`Signing Secret: ${secret}, Bot User OAuth Token: ${token}`);
-  }
-
-  return (
-    <>
-      <div className="row my-5">
-        <div className="mx-auto">
-          <button type="button" className="btn btn-primary text-nowrap mx-1" onClick={() => window.open('https://api.slack.com/apps', '_blank')}>
-            {t('slack_integration.non_proxy.create_bot')}
-          </button>
-        </div>
-      </div>
-
-      <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"
-            onChange={e => setSecret(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"
-            onChange={e => setToken(e.target.value)}
-          />
-        </div>
-      </div>
-
-      <AdminUpdateButtonRow onClick={updateHandler} disabled={false} />
-    </>
-  );
-}
-
-export default CustomBotNonProxySettings;

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

@@ -0,0 +1,101 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import PropTypes from 'prop-types';
+import AppContainer from '../../../services/AppContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+
+const CustomBotWithoutProxySettings = (props) => {
+  const { appContainer } = props;
+  const { t } = useTranslation();
+
+  const [slackSigningSecret, setSlackSigningSecret] = useState('');
+  const [slackBotToken, setSlackBotToken] = useState('');
+  const [slackSigningSecretEnv, setSlackSigningSecretEnv] = useState('');
+  const [slackBotTokenEnv, setSlackBotTokenEnv] = useState('');
+  const botType = 'without-proxy';
+
+  const fetchData = useCallback(async() => {
+    try {
+      const res = await appContainer.apiv3.get('/slack-integration/');
+      const {
+        slackSigningSecret, slackBotToken, slackSigningSecretEnvVars, slackBotTokenEnvVars,
+      } = res.data.slackBotSettingParams.customBotWithoutProxySettings;
+      setSlackSigningSecret(slackSigningSecret);
+      setSlackBotToken(slackBotToken);
+      setSlackSigningSecretEnv(slackSigningSecretEnvVars);
+      setSlackBotTokenEnv(slackBotTokenEnvVars);
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [appContainer]);
+
+  useEffect(() => {
+    fetchData();
+  }, [fetchData]);
+
+  async function updateHandler() {
+    try {
+      await appContainer.apiv3.put('/slack-integration/custom-bot-without-proxy', {
+        slackSigningSecret,
+        slackBotToken,
+        botType,
+      });
+      toastSuccess(t('toaster.update_successed', { target: t('admin:slack_integration.custom_bot_without_proxy_settings') }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  return (
+    <>
+      <div className="row my-5">
+        <div className="mx-auto">
+          <button
+            type="button"
+            className="btn btn-primary text-nowrap mx-1"
+            onClick={() => window.open('https://api.slack.com/apps', '_blank')}
+          >
+            {t('slack_integration.without_proxy.create_bot')}
+          </button>
+        </div>
+      </div>
+
+      <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} />
+    </>
+  );
+};
+
+const CustomBotWithoutProxySettingsWrapper = withUnstatedContainers(CustomBotWithoutProxySettings, [AppContainer]);
+
+CustomBotWithoutProxySettings.propTypes = {
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+};
+
+export default CustomBotWithoutProxySettingsWrapper;

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

@@ -2,7 +2,7 @@ import React from 'react';
 import { useTranslation } from 'react-i18next';
 
 import AccessTokenSettings from './AccessTokenSettings';
-import CustomBotNonProxySettings from './CustomBotNonProxySettings';
+import CustomBotWithoutProxySettings from './CustomBotWithoutProxySettings';
 
 function SlackIntegration() {
 
@@ -18,8 +18,8 @@ function SlackIntegration() {
 
       <div className="row">
         <div className="col-lg-12">
-          <h2 className="admin-setting-header">{t('slack_integration.custom_bot_non_proxy_settings')}</h2>
-          <CustomBotNonProxySettings />
+          <h2 className="admin-setting-header">{t('slack_integration.custom_bot_without_proxy_settings')}</h2>
+          <CustomBotWithoutProxySettings />
         </div>
       </div>
     </>

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

@@ -105,6 +105,7 @@ Crowi.prototype.init = async function() {
     this.setupSearcher(),
     this.setupMailer(),
     this.setupSlack(),
+    this.setupSlackLegacy(),
     this.setupCsrf(),
     this.setUpFileUpload(),
     this.setUpFileUploaderSwitchService(),
@@ -313,6 +314,10 @@ Crowi.prototype.getSlack = function() {
   return this.slack;
 };
 
+Crowi.prototype.getSlackLegacy = function() {
+  return this.slackLegacy;
+};
+
 Crowi.prototype.getInterceptorManager = function() {
   return this.interceptorManager;
 };
@@ -385,6 +390,15 @@ Crowi.prototype.setupSlack = async function() {
   }));
 };
 
+Crowi.prototype.setupSlackLegacy = async function() {
+  const self = this;
+
+  return new Promise(((resolve, reject) => {
+    self.slackLegacy = require('../util/slack-legacy')(self);
+    resolve();
+  }));
+};
+
 Crowi.prototype.setupCsrf = async function() {
   const Tokens = require('csrf');
   this.tokens = new Tokens();

+ 1 - 1
src/server/routes/apiv3/notification-setting.js

@@ -181,7 +181,7 @@ module.exports = (crowi) => {
         isIncomingWebhookPrioritized: await crowi.configManager.getConfig('notification', 'slack:isIncomingWebhookPrioritized'),
         slackToken: await crowi.configManager.getConfig('notification', 'slack:token'),
       };
-      await crowi.setupSlack();
+      await crowi.setupSlackLegacy();
       return res.apiv3({ responseParams });
     }
     catch (err) {

+ 18 - 16
src/server/routes/apiv3/slack-integration.js

@@ -19,8 +19,8 @@ const router = express.Router();
  *
  *  components:
  *    schemas:
- *      CustomBotNonProxy:
- *        description: CustomBotNonProxy
+ *      CustomBotWithoutProxy:
+ *        description: CustomBotWithoutProxy
  *        type: object
  *        properties:
  *          slackSigningSecret:
@@ -41,7 +41,7 @@ module.exports = (crowi) => {
 
 
   const validator = {
-    CusotmBotNonProxy: [
+    CusotmBotWithoutProxy: [
       body('slackSigningSecret').isString(),
       body('slackBotToken').isString(),
       body('botType').isString(),
@@ -84,14 +84,16 @@ module.exports = (crowi) => {
         // TODO impl this after GW-4939
         // AccessToken: "tempaccessdatahogehoge",
       },
-      cusotmBotNonProxySettings: {
+      customBotWithoutProxySettings: {
         // TODO impl this after GW-4939
         // AccessToken: "tempaccessdatahogehoge",
+        slackSigningSecretEnvVars: crowi.configManager.getConfigFromEnvVars('crowi', 'slackbot:signingSecret'),
+        slackBotTokenEnvVars: crowi.configManager.getConfigFromEnvVars('crowi', 'slackbot:token'),
         slackSigningSecret: crowi.configManager.getConfig('crowi', 'slackbot:signingSecret'),
         slackBotToken: crowi.configManager.getConfig('crowi', 'slackbot:token'),
       },
       // TODO imple when creating with proxy
-      cusotmBotWithProxySettings: {
+      customBotWithProxySettings: {
         // TODO impl this after GW-4939
         // AccessToken: "tempaccessdatahogehoge",
       },
@@ -102,24 +104,24 @@ module.exports = (crowi) => {
   /**
    * @swagger
    *
-   *    /slack-integration/custom-bot-non-proxy/:
+   *    /slack-integration/custom-bot-without-proxy/:
    *      put:
-   *        tags: [CustomBotNonProxy]
-   *        operationId: putCustomBotNonProxy
-   *        summary: /slack-integration/custom-bot-non-proxy
-   *        description: Put customBotNonProxy setting.
+   *        tags: [CustomBotWithoutProxy]
+   *        operationId: putCustomBotWithoutProxy
+   *        summary: /slack-integration/custom-bot-without-proxy
+   *        description: Put customBotWithoutProxy setting.
    *        requestBody:
    *          required: true
    *          content:
    *            application/json:
    *              schema:
-   *                $ref: '#/components/schemas/CustomBotNonProxy'
+   *                $ref: '#/components/schemas/CustomBotWithoutProxy'
    *        responses:
    *           200:
-   *             description: Succeeded to put CustomBotNonProxy setting.
+   *             description: Succeeded to put CustomBotWithoutProxy setting.
    */
-  router.put('/custom-bot-non-proxy',
-    accessTokenParser, loginRequiredStrictly, adminRequired, csrf, validator.CusotmBotNonProxy, apiV3FormValidator, async(req, res) => {
+  router.put('/custom-bot-without-proxy',
+    accessTokenParser, loginRequiredStrictly, adminRequired, csrf, validator.CusotmBotWithoutProxy, apiV3FormValidator, async(req, res) => {
       const { slackSigningSecret, slackBotToken, botType } = req.body;
 
       const requestParams = {
@@ -131,12 +133,12 @@ module.exports = (crowi) => {
       try {
         await updateSlackBotSettings(requestParams);
         // TODO Impl to delete AccessToken both of Proxy and GROWI when botType changes.
-        const customBotNonProxySettingParams = {
+        const customBotWithoutProxySettingParams = {
           slackSigningSecret: crowi.configManager.getConfig('crowi', 'slackbot:signingSecret'),
           slackBotToken: crowi.configManager.getConfig('crowi', 'slackbot:token'),
           slackBotType: crowi.configManager.getConfig('crowi', 'slackbot:type'),
         };
-        return res.apiv3({ customBotNonProxySettingParams });
+        return res.apiv3({ customBotWithoutProxySettingParams });
       }
       catch (error) {
         const msg = 'Error occured in updating Custom bot setting';

+ 3 - 2
src/server/service/bolt.js

@@ -205,7 +205,7 @@ class BoltService {
           this.generateMarkdownSectionBlock('`-tag:wiki` \n Exclude pages with wiki tag'),
         ],
       });
-      return;
+      return { resultPaths: [] };
     }
 
     const resultPaths = results.data.map((data) => {
@@ -224,9 +224,10 @@ class BoltService {
 
     const keywords = this.getKeywords(args);
 
-    if (resultPaths == null) {
+    if (resultPaths.length === 0) {
       return;
     }
+
     const base = this.crowi.appService.getSiteUrl();
 
     const urls = resultPaths.map((path) => {

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

@@ -412,7 +412,7 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
   },
   SLACK_BOT_TYPE: {
     ns:      'crowi',
-    key:     'slackbot:type', // eg. official || custom-non-proxy || custom-with-proxy
+    key:     'slackbot:type', // eg. official || custom-without-proxy || custom-with-proxy
     type:    TYPES.STRING,
     default: null,
   },

+ 7 - 1
src/server/service/global-notification/global-notification-slack.js

@@ -9,6 +9,7 @@ class GlobalNotificationSlackService {
   constructor(crowi) {
     this.crowi = crowi;
     this.slack = crowi.getSlack();
+    this.slackLegacy = crowi.getSlackLegacy();
     this.type = crowi.model('GlobalNotificationSetting').TYPE.SLACK;
     this.event = crowi.model('GlobalNotificationSetting').EVENT;
   }
@@ -31,8 +32,13 @@ class GlobalNotificationSlackService {
     const attachmentBody = this.generateAttachmentBody(event, path, triggeredBy, vars);
 
     await Promise.all(notifications.map((notification) => {
-      return this.slack.sendGlobalNotification(messageBody, attachmentBody, notification.slackChannels);
+      return [
+        this.slack.sendGlobalNotification(messageBody, attachmentBody, notification.slackChannels),
+        this.slackLegacy.sendGlobalNotification(messageBody, attachmentBody, notification.slackChannels),
+      ];
     }));
+
+
   }
 
   /**

+ 5 - 1
src/server/service/user-notification/index.js

@@ -24,7 +24,9 @@ class UserNotificationService {
    * @param {Comment} comment
    */
   async fire(page, user, slackChannelsStr, mode, option, comment = {}) {
-    const { slackNotificationService, slack } = this.crowi;
+    const {
+      slackNotificationService, slackLegacy, slack,
+    } = this.crowi;
 
     const opt = option || {};
     const previousRevision = opt.previousRevision || '';
@@ -42,9 +44,11 @@ class UserNotificationService {
       let res;
       if (mode === 'comment') {
         res = await slack.postComment(comment, user, chan, page.path);
+        res = await slackLegacy.postComment(comment, user, chan, page.path);
       }
       else {
         res = await slack.postPage(page, user, chan, mode, previousRevision);
+        res = await slackLegacy.postPage(page, user, chan, mode, previousRevision);
       }
       if (res.status !== 'ok') {
         throw new Error(`fail to send slack notification to #${chan} channel`);

+ 96 - 0
src/server/util/slack-legacy.js

@@ -0,0 +1,96 @@
+const debug = require('debug')('growi:util:slack');
+// const slack = require('./slack');
+
+/**
+ * slack
+ */
+
+/* eslint-disable no-use-before-define */
+
+module.exports = function(crowi) {
+  const Slack = require('slack-node');
+
+  const { configManager } = crowi;
+  const slack = crowi.getSlack();
+
+  const slackLegacy = {};
+
+  const postWithIwh = function(messageObj) {
+    return new Promise((resolve, reject) => {
+      const client = new Slack();
+      client.setWebhook(configManager.getConfig('notification', 'slack:incomingWebhookUrl'));
+      client.webhook(messageObj, (err, res) => {
+        if (err) {
+          debug('Post error', err, res);
+          debug('Sent data to slack is:', messageObj);
+          return reject(err);
+        }
+        resolve(res);
+      });
+    });
+  };
+
+  const postWithWebApi = function(messageObj) {
+    return new Promise((resolve, reject) => {
+      const client = new Slack(configManager.getConfig('notification', 'slack:token'));
+      // stringify attachments
+      if (messageObj.attachments != null) {
+        messageObj.attachments = JSON.stringify(messageObj.attachments);
+      }
+      client.api('chat.postMessage', messageObj, (err, res) => {
+        if (err) {
+          debug('Post error', err, res);
+          debug('Sent data to slack is:', messageObj);
+          return reject(err);
+        }
+        resolve(res);
+      });
+    });
+  };
+
+  // slackLegacy.post = function (channel, message, opts) {
+  slackLegacy.postPage = (page, user, channel, updateType, previousRevision) => {
+    const messageObj = slack.prepareSlackMessageForPage(page, user, channel, updateType, previousRevision);
+
+    return slackPost(messageObj);
+  };
+
+  slackLegacy.postComment = (comment, user, channel, path) => {
+    const messageObj = slack.prepareSlackMessageForComment(comment, user, channel, path);
+
+    return slackPost(messageObj);
+  };
+
+  slackLegacy.sendGlobalNotification = async(messageBody, attachmentBody, slackChannel) => {
+    const messageObj = await slack.prepareSlackMessageForGlobalNotification(messageBody, attachmentBody, slackChannel);
+
+    return slackPost(messageObj);
+  };
+
+  const slackPost = (messageObj) => {
+    // when incoming Webhooks is prioritized
+    if (configManager.getConfig('notification', 'slack:isIncomingWebhookPrioritized')) {
+      if (configManager.getConfig('notification', 'slack:incomingWebhookUrl')) {
+        debug('posting message with IncomingWebhook');
+        return postWithIwh(messageObj);
+      }
+      if (configManager.getConfig('notification', 'slack:token')) {
+        debug('posting message with Web API');
+        return postWithWebApi(messageObj);
+      }
+    }
+    // else
+    else {
+      if (configManager.getConfig('notification', 'slack:token')) {
+        debug('posting message with Web API');
+        return postWithWebApi(messageObj);
+      }
+      if (configManager.getConfig('notification', 'slack:incomingWebhookUrl')) {
+        debug('posting message with IncomingWebhook');
+        return postWithIwh(messageObj);
+      }
+    }
+  };
+
+  return slackLegacy;
+};

+ 13 - 53
src/server/util/slack.js

@@ -8,34 +8,17 @@ const urljoin = require('url-join');
 /* eslint-disable no-use-before-define */
 
 module.exports = function(crowi) {
-  const Slack = require('slack-node');
-  const { configManager } = crowi;
+  const { WebClient, LogLevel } = require('@slack/web-api');
 
+  const { configManager } = crowi;
   const slack = {};
 
-  const postWithIwh = function(messageObj) {
+  const postWithSlackBot = function(messageObj) {
     return new Promise((resolve, reject) => {
-      const client = new Slack();
-      client.setWebhook(configManager.getConfig('notification', 'slack:incomingWebhookUrl'));
-      client.webhook(messageObj, (err, res) => {
-        if (err) {
-          debug('Post error', err, res);
-          debug('Sent data to slack is:', messageObj);
-          return reject(err);
-        }
-        resolve(res);
+      const client = new WebClient(configManager.getConfig('crowi', 'slackbot:token'), {
+        logLevel: LogLevel.DEBUG,
       });
-    });
-  };
-
-  const postWithWebApi = function(messageObj) {
-    return new Promise((resolve, reject) => {
-      const client = new Slack(configManager.getConfig('notification', 'slack:token'));
-      // stringify attachments
-      if (messageObj.attachments != null) {
-        messageObj.attachments = JSON.stringify(messageObj.attachments);
-      }
-      client.api('chat.postMessage', messageObj, (err, res) => {
+      client.chat.postMessage(messageObj, (err, res) => {
         if (err) {
           debug('Post error', err, res);
           debug('Sent data to slack is:', messageObj);
@@ -105,7 +88,7 @@ module.exports = function(crowi) {
     return body;
   };
 
-  const prepareSlackMessageForPage = function(page, user, channel, updateType, previousRevision) {
+  slack.prepareSlackMessageForPage = (page, user, channel, updateType, previousRevision) => {
     const appTitle = crowi.appService.getAppTitle();
     const url = crowi.appService.getSiteUrl();
     let body = page.revision.body;
@@ -141,7 +124,7 @@ module.exports = function(crowi) {
     return message;
   };
 
-  const prepareSlackMessageForComment = function(comment, user, channel, path) {
+  slack.prepareSlackMessageForComment = (comment, user, channel, path) => {
     const appTitle = crowi.appService.getAppTitle();
     const url = crowi.appService.getSiteUrl();
     const body = prepareAttachmentTextForComment(comment);
@@ -175,7 +158,7 @@ module.exports = function(crowi) {
    * @param {string} attachmentBody
    * @param {string} slackChannel
   */
-  const prepareSlackMessageForGlobalNotification = async(messageBody, attachmentBody, slackChannel) => {
+  slack.prepareSlackMessageForGlobalNotification = async(messageBody, attachmentBody, slackChannel) => {
     const appTitle = crowi.appService.getAppTitle();
 
     const attachment = {
@@ -217,48 +200,25 @@ module.exports = function(crowi) {
     return text;
   };
 
-  // slack.post = function (channel, message, opts) {
   slack.postPage = (page, user, channel, updateType, previousRevision) => {
-    const messageObj = prepareSlackMessageForPage(page, user, channel, updateType, previousRevision);
+    const messageObj = slack.prepareSlackMessageForPage(page, user, channel, updateType, previousRevision);
 
     return slackPost(messageObj);
   };
 
   slack.postComment = (comment, user, channel, path) => {
-    const messageObj = prepareSlackMessageForComment(comment, user, channel, path);
+    const messageObj = slack.prepareSlackMessageForComment(comment, user, channel, path);
 
     return slackPost(messageObj);
   };
 
   slack.sendGlobalNotification = async(messageBody, attachmentBody, slackChannel) => {
-    const messageObj = await prepareSlackMessageForGlobalNotification(messageBody, attachmentBody, slackChannel);
-
+    const messageObj = await slack.prepareSlackMessageForGlobalNotification(messageBody, attachmentBody, slackChannel);
     return slackPost(messageObj);
   };
 
   const slackPost = (messageObj) => {
-    // when incoming Webhooks is prioritized
-    if (configManager.getConfig('notification', 'slack:isIncomingWebhookPrioritized')) {
-      if (configManager.getConfig('notification', 'slack:incomingWebhookUrl')) {
-        debug('posting message with IncomingWebhook');
-        return postWithIwh(messageObj);
-      }
-      if (configManager.getConfig('notification', 'slack:token')) {
-        debug('posting message with Web API');
-        return postWithWebApi(messageObj);
-      }
-    }
-    // else
-    else {
-      if (configManager.getConfig('notification', 'slack:token')) {
-        debug('posting message with Web API');
-        return postWithWebApi(messageObj);
-      }
-      if (configManager.getConfig('notification', 'slack:incomingWebhookUrl')) {
-        debug('posting message with IncomingWebhook');
-        return postWithIwh(messageObj);
-      }
-    }
+    return postWithSlackBot(messageObj);
   };
 
   return slack;

+ 1 - 1
src/server/views/admin/legacy-slack-integration.html

@@ -7,6 +7,6 @@
 {% endblock %}
 
 {% block content_main %}
-<!-- TODO: move contents from notification settings by GW-5467  -->
+<div id="admin-slack-integration-notification-setting" class="admin-slack-integration-notification-setting"></div>
 {% endblock content_main %}
 

+ 4 - 4
src/test/util/slack.test.js → src/test/util/slack-legacy.test.js

@@ -3,19 +3,19 @@ const { getInstance } = require('../setup-crowi');
 describe('Slack Util', () => {
 
   let crowi;
-  let slack;
+  let slackLegacy;
 
   beforeEach(async(done) => {
     crowi = await getInstance();
-    slack = require(`${crowi.libDir}/util/slack`)(crowi);
+    slackLegacy = require(`${crowi.libDir}/util/slack-legacy`)(crowi);
     done();
   });
 
   test('post comment method exists', () => {
-    expect(slack.postComment).toBeInstanceOf(Function);
+    expect(slackLegacy.postComment).toBeInstanceOf(Function);
   });
 
   test('post page method exists', () => {
-    expect(slack.postPage).toBeInstanceOf(Function);
+    expect(slackLegacy.postPage).toBeInstanceOf(Function);
   });
 });

+ 126 - 38
yarn.lock

@@ -1827,6 +1827,24 @@
     raw-body "^2.3.3"
     tsscmp "^1.0.6"
 
+"@slack/events-api@^3.0.0":
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/@slack/events-api/-/events-api-3.0.0.tgz#3e7626ceb5700cb1cce2fbfe6e1583c23a3626fd"
+  integrity sha512-4LdeVuyBCoESSMryACZNafSM4GlX9o881WShABcW+nonh97PnxUKI+hZwuYdTLQpyKu2HSp1zcWsi8QLaiXkrw==
+  dependencies:
+    "@types/debug" "^4.1.4"
+    "@types/express" "^4.17.0"
+    "@types/lodash.isstring" "^4.0.6"
+    "@types/node" ">=12.13.0 < 13"
+    "@types/yargs" "^15.0.4"
+    debug "^2.6.1"
+    lodash.isstring "^4.0.1"
+    raw-body "^2.3.3"
+    tsscmp "^1.0.6"
+    yargs "^15.3.1"
+  optionalDependencies:
+    express "^4.0.0"
+
 "@slack/logger@>=1.0.0 <3.0.0", "@slack/logger@^2.0.0":
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/@slack/logger/-/logger-2.0.0.tgz#6a4e1c755849bc0f66dac08a8be54ce790ec0e6b"
@@ -1911,6 +1929,22 @@
     p-queue "^6.6.1"
     p-retry "^4.0.0"
 
+"@slack/web-api@^6.1.0":
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/@slack/web-api/-/web-api-6.1.0.tgz#27a17f61eb72100d6722ff17f581349c41d19b5f"
+  integrity sha512-9MVHw+rDBaFvkvzm8lDNH/nlkvJCDKRIjFGMdpbyZlVLsm4rcht4qyiL71bqdyLATHXJnWknb/sl0FQGLLobIA==
+  dependencies:
+    "@slack/logger" ">=1.0.0 <3.0.0"
+    "@slack/types" "^1.7.0"
+    "@types/is-stream" "^1.1.0"
+    "@types/node" ">=12.0.0"
+    axios "^0.21.1"
+    eventemitter3 "^3.1.0"
+    form-data "^2.5.0"
+    is-stream "^1.1.0"
+    p-queue "^6.6.1"
+    p-retry "^4.0.0"
+
 "@types/babel__core@^7.1.0":
   version "7.1.2"
   resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.2.tgz#608c74f55928033fce18b99b213c16be4b3d114f"
@@ -1956,6 +1990,11 @@
   resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
   integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
 
+"@types/debug@^4.1.4":
+  version "4.1.5"
+  resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd"
+  integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==
+
 "@types/express-serve-static-core@*":
   version "4.11.0"
   resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.11.0.tgz#aaaf472777191c3e56ec7aa160034c6b55ebdd59"
@@ -1979,7 +2018,7 @@
     "@types/express-serve-static-core" "*"
     "@types/serve-static" "*"
 
-"@types/express@^4.16.1":
+"@types/express@^4.16.1", "@types/express@^4.17.0":
   version "4.17.11"
   resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.11.tgz#debe3caa6f8e5fcda96b47bd54e2f40c4ee59545"
   integrity sha512-no+R6rW60JEc59977wIxreQVsIEOAYwgCqldrA/vkpCnbD7MqTefO97lmoBe4WE0F156bC4uLSP1XHDOySnChg==
@@ -2039,6 +2078,18 @@
   dependencies:
     "@types/node" "*"
 
+"@types/lodash.isstring@^4.0.6":
+  version "4.0.6"
+  resolved "https://registry.yarnpkg.com/@types/lodash.isstring/-/lodash.isstring-4.0.6.tgz#1534d0c19a2ad79caa17558a298e366893ffd08c"
+  integrity sha512-uUGvF9G1G7jQ5H42Y38GA9rZmUoY8wI/OMSwnW0BZA+Ra0uxzpuQf4CixXl3yG3TvF6LjuduMyt1WvKl+je8QA==
+  dependencies:
+    "@types/lodash" "*"
+
+"@types/lodash@*":
+  version "4.14.168"
+  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008"
+  integrity sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==
+
 "@types/mime@*":
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b"
@@ -2057,6 +2108,11 @@
   resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.21.tgz#d934aacc22424fe9622ebf6857370c052eae464e"
   integrity sha512-cHYfKsnwllYhjOzuC5q1VpguABBeecUp24yFluHpn/BQaVxB1CuQ1FSRZCzrPxrkIfWISXV2LbeoBthLWg0+0A==
 
+"@types/node@>=12.13.0 < 13":
+  version "12.20.6"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.6.tgz#7b73cce37352936e628c5ba40326193443cfba25"
+  integrity sha512-sRVq8d+ApGslmkE9e3i+D3gFGk7aZHAT+G4cIpIEdLJYPsWiSPwcAnJEjddLQQDqV3Ra2jOclX/Sv6YrvGYiWA==
+
 "@types/node@^7.0.21", "@types/node@^7.0.23":
   version "7.0.52"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.52.tgz#8990d3350375542b0c21a83cd0331e6a8fc86716"
@@ -2195,6 +2251,13 @@
   dependencies:
     "@types/yargs-parser" "*"
 
+"@types/yargs@^15.0.4":
+  version "15.0.13"
+  resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.13.tgz#34f7fec8b389d7f3c1fd08026a5763e072d3c6dc"
+  integrity sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==
+  dependencies:
+    "@types/yargs-parser" "*"
+
 "@typescript-eslint/experimental-utils@^2.5.0":
   version "2.7.0"
   resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.7.0.tgz#58d790a3884df3041b5a5e08f9e5e6b7c41864b5"
@@ -5036,7 +5099,7 @@ debounce@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.1.0.tgz#6a1a4ee2a9dc4b7c24bb012558dbcdb05b37f408"
 
-debug@2, debug@2.6.9, debug@^2.0.0, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
+debug@2, debug@2.6.9, debug@^2.0.0, debug@^2.2.0, debug@^2.3.3, debug@^2.6.1, debug@^2.6.8, debug@^2.6.9:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
   dependencies:
@@ -6285,6 +6348,42 @@ express-webpack-assets@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/express-webpack-assets/-/express-webpack-assets-0.1.0.tgz#000fb3413eb0d512cbd6cd3f6a10b5e70dbe0079"
 
+express@^4.0.0, express@^4.16.4:
+  version "4.17.1"
+  resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
+  integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
+  dependencies:
+    accepts "~1.3.7"
+    array-flatten "1.1.1"
+    body-parser "1.19.0"
+    content-disposition "0.5.3"
+    content-type "~1.0.4"
+    cookie "0.4.0"
+    cookie-signature "1.0.6"
+    debug "2.6.9"
+    depd "~1.1.2"
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    etag "~1.8.1"
+    finalhandler "~1.1.2"
+    fresh "0.5.2"
+    merge-descriptors "1.0.1"
+    methods "~1.1.2"
+    on-finished "~2.3.0"
+    parseurl "~1.3.3"
+    path-to-regexp "0.1.7"
+    proxy-addr "~2.0.5"
+    qs "6.7.0"
+    range-parser "~1.2.1"
+    safe-buffer "5.1.2"
+    send "0.17.1"
+    serve-static "1.14.1"
+    setprototypeof "1.1.1"
+    statuses "~1.5.0"
+    type-is "~1.6.18"
+    utils-merge "1.0.1"
+    vary "~1.1.2"
+
 express@^4.16.1:
   version "4.16.2"
   resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c"
@@ -6355,42 +6454,6 @@ express@^4.16.3:
     utils-merge "1.0.1"
     vary "~1.1.2"
 
-express@^4.16.4:
-  version "4.17.1"
-  resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
-  integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
-  dependencies:
-    accepts "~1.3.7"
-    array-flatten "1.1.1"
-    body-parser "1.19.0"
-    content-disposition "0.5.3"
-    content-type "~1.0.4"
-    cookie "0.4.0"
-    cookie-signature "1.0.6"
-    debug "2.6.9"
-    depd "~1.1.2"
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    etag "~1.8.1"
-    finalhandler "~1.1.2"
-    fresh "0.5.2"
-    merge-descriptors "1.0.1"
-    methods "~1.1.2"
-    on-finished "~2.3.0"
-    parseurl "~1.3.3"
-    path-to-regexp "0.1.7"
-    proxy-addr "~2.0.5"
-    qs "6.7.0"
-    range-parser "~1.2.1"
-    safe-buffer "5.1.2"
-    send "0.17.1"
-    serve-static "1.14.1"
-    setprototypeof "1.1.1"
-    statuses "~1.5.0"
-    type-is "~1.6.18"
-    utils-merge "1.0.1"
-    vary "~1.1.2"
-
 extend-shallow@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
@@ -16760,6 +16823,14 @@ yargs-parser@^16.1.0:
     camelcase "^5.0.0"
     decamelize "^1.2.0"
 
+yargs-parser@^18.1.2:
+  version "18.1.3"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
+  integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
+  dependencies:
+    camelcase "^5.0.0"
+    decamelize "^1.2.0"
+
 yargs-parser@^4.1.0, yargs-parser@^4.2.0:
   version "4.2.1"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c"
@@ -16871,6 +16942,23 @@ yargs@^15.0.0:
     y18n "^4.0.0"
     yargs-parser "^16.1.0"
 
+yargs@^15.3.1:
+  version "15.4.1"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
+  integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
+  dependencies:
+    cliui "^6.0.0"
+    decamelize "^1.2.0"
+    find-up "^4.1.0"
+    get-caller-file "^2.0.1"
+    require-directory "^2.1.1"
+    require-main-filename "^2.0.0"
+    set-blocking "^2.0.0"
+    string-width "^4.2.0"
+    which-module "^2.0.0"
+    y18n "^4.0.0"
+    yargs-parser "^18.1.2"
+
 yargs@~1.2.6:
   version "1.2.6"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-1.2.6.tgz#9c7b4a82fd5d595b2bf17ab6dcc43135432fe34b"