Jelajahi Sumber

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

Yuki Takei 5 tahun lalu
induk
melakukan
6c09283a65
32 mengubah file dengan 983 tambahan dan 295 penghapusan
  1. 5 5
      package.json
  2. 11 3
      resource/locales/en_US/admin/admin.json
  3. 10 2
      resource/locales/ja_JP/admin/admin.json
  4. 11 3
      resource/locales/zh_CN/admin/admin.json
  5. 2 0
      src/client/js/admin.jsx
  6. 13 14
      src/client/js/components/Admin/Common/AdminNavigation.jsx
  7. 4 13
      src/client/js/components/Admin/Notification/NotificationSetting.jsx
  8. 80 0
      src/client/js/components/Admin/Notification/SlackIntegrationNotificationSetting.jsx
  9. 54 0
      src/client/js/components/Admin/SlackIntegration/ConfirmBotChangeModal.jsx
  10. 0 54
      src/client/js/components/Admin/SlackIntegration/CustomBotNonProxySettings.jsx
  11. 12 0
      src/client/js/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx
  12. 136 0
      src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx
  13. 12 0
      src/client/js/components/Admin/SlackIntegration/OfficialBotSettings.jsx
  14. 87 10
      src/client/js/components/Admin/SlackIntegration/SlackIntegration.jsx
  15. 4 0
      src/client/styles/scss/_admin.scss
  16. 1 3
      src/lib/service/logger/alias-for-debug.js
  17. 63 11
      src/lib/service/logger/index.js
  18. 16 0
      src/lib/service/logger/stream.dev.js
  19. 25 0
      src/lib/service/logger/stream.prod.js
  20. 19 0
      src/server/crowi/index.js
  21. 1 1
      src/server/routes/apiv3/notification-setting.js
  22. 24 3
      src/server/routes/apiv3/slack-bot.js
  23. 60 16
      src/server/routes/apiv3/slack-integration.js
  24. 79 34
      src/server/service/bolt.js
  25. 1 1
      src/server/service/config-loader.js
  26. 7 1
      src/server/service/global-notification/global-notification-slack.js
  27. 5 1
      src/server/service/user-notification/index.js
  28. 96 0
      src/server/util/slack-legacy.js
  29. 13 53
      src/server/util/slack.js
  30. 1 1
      src/server/views/admin/legacy-slack-integration.html
  31. 4 4
      src/test/util/slack-legacy.test.js
  32. 127 62
      yarn.lock

+ 5 - 5
package.json

@@ -82,9 +82,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",
@@ -92,7 +94,7 @@
     "aws-sdk": "^2.88.0",
     "axios": "^0.21.1",
     "body-parser": "^1.18.2",
-    "bunyan": "^1.8.15",
+    "bunyan": "^1.8.12",
     "bunyan-format": "^0.2.1",
     "check-node-version": "^4.0.2",
     "connect-flash": "~0.1.1",
@@ -160,7 +162,6 @@
     "string-width": "^4.1.0",
     "swig-templates": "^2.0.2",
     "uglifycss": "^0.0.29",
-    "universal-bunyan": "^0.9.2",
     "unzipper": "^0.10.5",
     "url-join": "^4.0.0",
     "validator": "^12.0.0",
@@ -181,7 +182,6 @@
     "@babel/polyfill": "^7.4.4",
     "@babel/preset-env": "^7.4.5",
     "@babel/preset-react": "^7.0.0",
-    "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
@@ -193,7 +193,7 @@
     "babel-plugin-lodash": "^3.3.4",
     "babel-plugin-transform-imports": "^2.0.0",
     "bootstrap": "^4.5.0",
-    "browser-bunyan": "^1.6.3",
+    "browser-bunyan": "^1.3.0",
     "browser-sync": "^2.26.3",
     "bunyan-debug": "^2.0.0",
     "cli": "~1.0.1",

+ 11 - 3
resource/locales/en_US/admin/admin.json

@@ -66,7 +66,7 @@
     "load_plugins": "Load_plugins",
     "enable": "Enable",
     "disable": "Disable",
-    "use_env_var_if_empty": "If the value in the database is empty, the value of the environment variable <cod>{{variable}}</code> is used.",
+    "use_env_var_if_empty": "If the value in the database is empty, the value of the environment variable <code>{{variable}}</code> is used.",
     "note_for_the_only_env_option": "The GCS Settings is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> ."
   },
   "markdown_setting": {
@@ -253,12 +253,20 @@
     "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_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"
     }
   },

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

@@ -251,12 +251,20 @@
     "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_non_proxy_settings": "Custom bot (non-proxy) 設定",
-    "non_proxy": {
+    "custom_bot_without_proxy_settings": "Custom Bot (Without-Proxy) 設定",
+    "without_proxy": {
       "create_bot": "Bot を作成する"
     }
   },

+ 11 - 3
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,20 @@
 		"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_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;

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

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

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

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

@@ -0,0 +1,136 @@
+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 = 'custom-bot-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 (
+    <>
+      <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
+            type="button"
+            className="btn btn-primary text-nowrap mx-1"
+            onClick={() => window.open('https://api.slack.com/apps', '_blank')}
+          >
+            {t('admin:slack_integration.without_proxy.create_bot')}
+          </button>
+        </div>
+      </div>
+      <table className="table settings-table">
+        <colgroup>
+          <col className="item-name" />
+          <col className="from-db" />
+          <col className="from-env-vars" />
+        </colgroup>
+        <thead>
+          <tr><th></th><th>Database</th><th>Environment variables</th></tr>
+        </thead>
+        <tbody>
+          <tr>
+            <th>Signing Secret</th>
+            <td>
+              <input
+                className="form-control"
+                type="text"
+                value={slackSigningSecret || ''}
+                onChange={e => setSlackSigningSecret(e.target.value)}
+              />
+            </td>
+            <td>
+              <input
+                className="form-control"
+                type="text"
+                value={slackSigningSecretEnv || ''}
+                readOnly
+              />
+              <p className="form-text text-muted">
+                <small dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.use_env_var_if_empty', { variable: 'SLACK_SIGNING_SECRET' }) }} />
+              </p>
+            </td>
+          </tr>
+          <tr>
+            <th>Bot User OAuth Token</th>
+            <td>
+              <input
+                className="form-control"
+                type="text"
+                value={slackBotToken || ''}
+                onChange={e => setSlackBotToken(e.target.value)}
+              />
+            </td>
+            <td>
+              <input
+                className="form-control"
+                type="text"
+                value={slackBotTokenEnv || ''}
+                readOnly
+              />
+              <p className="form-text text-muted">
+                <small dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.use_env_var_if_empty', { variable: 'SLACK_BOT_TOKEN' }) }} />
+              </p>
+            </td>
+
+          </tr>
+        </tbody>
+      </table>
+
+
+      <AdminUpdateButtonRow onClick={updateHandler} disabled={false} />
+    </>
+  );
+};
+
+const CustomBotWithoutProxySettingsWrapper = withUnstatedContainers(CustomBotWithoutProxySettings, [AppContainer]);
+
+CustomBotWithoutProxySettings.propTypes = {
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+};
+
+export default CustomBotWithoutProxySettingsWrapper;

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

+ 87 - 10
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 CustomBotNonProxySettings from './CustomBotNonProxySettings';
+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_non_proxy_settings')}</h2>
-          <CustomBotNonProxySettings />
+
+      <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

+ 1 - 3
src/lib/service/logger/alias-for-debug.js

@@ -1,5 +1,3 @@
-const generateBunyanLogger = require('./index');
-
 /**
  * return 'debug' method of bunyan logger
  *
@@ -8,6 +6,6 @@ const generateBunyanLogger = require('./index');
  * @param {string} name
  */
 module.exports = (name) => {
-  const bunyanLogger = generateBunyanLogger(name);
+  const bunyanLogger = require('./index')(name);
   return bunyanLogger.debug.bind(bunyanLogger);
 };

+ 63 - 11
src/lib/service/logger/index.js

@@ -1,16 +1,68 @@
-const { createLogger } = require('universal-bunyan');
+const bunyan = require('bunyan'); // will be replaced to browser-bunyan on browser by webpack
+const minimatch = require('minimatch');
 
-const configForDev = require('@root/config/logger/config.dev');
-const configForProd = require('@root/config/logger/config.prod');
+const isBrowser = typeof window !== 'undefined';
+const isProd = process.env.NODE_ENV === 'production';
 
-const isProduction = process.env.NODE_ENV === 'production';
-const config = isProduction ? configForProd : configForDev;
+const config = require('@root/config').logger;
+const stream = isProd ? require('./stream.prod') : require('./stream.dev');
 
-const loggerFactory = function(name) {
-  return createLogger({
-    name,
-    config,
-  });
+// logger store
+const loggers = {};
+
+
+// merge configuration from environment variables
+const envLevelMap = {
+  INFO:   'info',
+  DEBUG:  'debug',
+  WARN:   'warn',
+  TRACE:  'trace',
+  ERROR:  'error',
 };
+Object.keys(envLevelMap).forEach((envName) => { // ['INFO', 'DEBUG', ...].forEach
+  const envVars = process.env[envName]; // process.env.DEBUG should have a value like 'growi:routes:page,growi:models.page,...'
+  if (envVars != null) {
+    const level = envLevelMap[envName];
+    envVars.split(',').forEach((ns) => { // ['growi:routes:page', 'growi:models.page', ...].forEach
+      config[ns.trim()] = level;
+    });
+  }
+});
+
+
+/**
+ * determine logger level
+ * @param {string} name Logger name
+ */
+function determineLoggerLevel(name) {
+  if (isBrowser && isProd) {
+    return 'error';
+  }
+
+  let level = config.default;
 
-module.exports = loggerFactory;
+  /* eslint-disable array-callback-return, no-useless-return */
+  // retrieve configured level
+  Object.keys(config).some((key) => { //  breakable forEach
+    // test whether 'name' matches to 'key'(blob)
+    if (minimatch(name, key)) {
+      level = config[key];
+      return; //                          break if match
+    }
+  });
+
+  return level;
+}
+
+module.exports = (name) => {
+  // create logger instance if absent
+  if (loggers[name] == null) {
+    loggers[name] = bunyan.createLogger({
+      name,
+      stream,
+      level: determineLoggerLevel(name),
+    });
+  }
+
+  return loggers[name];
+};

+ 16 - 0
src/lib/service/logger/stream.dev.js

@@ -0,0 +1,16 @@
+const isBrowser = typeof window !== 'undefined';
+
+let stream;
+
+// browser settings
+if (isBrowser) {
+  const ConsoleFormattedStream = require('@browser-bunyan/console-formatted-stream').ConsoleFormattedStream;
+  stream = new ConsoleFormattedStream();
+}
+// node settings
+else {
+  const bunyanFormat = require('bunyan-format');
+  stream = bunyanFormat({ outputMode: 'short' });
+}
+
+module.exports = stream;

+ 25 - 0
src/lib/service/logger/stream.prod.js

@@ -0,0 +1,25 @@
+const { envUtils } = require('growi-commons');
+
+const isBrowser = typeof window !== 'undefined';
+
+let stream;
+
+// browser settings
+if (isBrowser) {
+  const ConsoleFormattedStream = require('@browser-bunyan/console-formatted-stream').ConsoleFormattedStream;
+  stream = new ConsoleFormattedStream();
+}
+// node settings
+else {
+  const isFormat = (process.env.FORMAT_NODE_LOG == null) || envUtils.toBoolean(process.env.FORMAT_NODE_LOG);
+
+  if (isFormat) {
+    const bunyanFormat = require('bunyan-format');
+    stream = bunyanFormat({ outputMode: 'long' });
+  }
+  else {
+    stream = process.stdout;
+  }
+}
+
+module.exports = stream;

+ 19 - 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();
@@ -661,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;

+ 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) {

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

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

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

@@ -3,6 +3,7 @@ const loggerFactory = require('@alias/logger');
 const logger = loggerFactory('growi:routes:apiv3:notification-setting');
 const express = require('express');
 const { body } = require('express-validator');
+const crypto = require('crypto');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 const router = express.Router();
@@ -18,8 +19,8 @@ const router = express.Router();
  *
  *  components:
  *    schemas:
- *      CustomBotNonProxy:
- *        description: CustomBotNonProxy
+ *      CustomBotWithoutProxy:
+ *        description: CustomBotWithoutProxy
  *        type: object
  *        properties:
  *          slackSigningSecret:
@@ -40,7 +41,7 @@ module.exports = (crowi) => {
 
 
   const validator = {
-    CusotmBotNonProxy: [
+    CusotmBotWithoutProxy: [
       body('slackSigningSecret').isString(),
       body('slackBotToken').isString(),
       body('botType').isString(),
@@ -53,6 +54,14 @@ module.exports = (crowi) => {
     return configManager.updateConfigsInTheSameNamespace('crowi', params, true);
   }
 
+
+  function generateAccessToken(user) {
+    const hasher = crypto.createHash('sha512');
+    hasher.update(new Date().getTime() + user._id);
+
+    return hasher.digest('base64');
+  }
+
   /**
    * @swagger
    *
@@ -75,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",
       },
@@ -93,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 = {
@@ -121,13 +132,18 @@ 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 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';
@@ -136,5 +152,33 @@ module.exports = (crowi) => {
       }
     });
 
+  /**
+   * @swagger
+   *
+   *    /slack-integration/access-token:
+   *      put:
+   *        tags: [SlackIntegration]
+   *        operationId: getCustomBotSetting
+   *        summary: /slack-integration
+   *        description: Generate accessToken
+   *        responses:
+   *          200:
+   *            description: Succeeded to update access token for slack
+   */
+  router.put('/access-token', loginRequiredStrictly, adminRequired, async(req, res) => {
+
+    try {
+      const accessToken = generateAccessToken(req.user);
+      await updateSlackBotSettings({ 'slackbot:access-token': accessToken });
+
+      return res.apiv3({ accessToken });
+    }
+    catch (error) {
+      const msg = 'Error occured in updating access token for access token';
+      logger.error('Error', error);
+      return res.apiv3Err(new ErrorV3(msg, 'update-accessToken-failed'));
+    }
+  });
+
   return router;
 };

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

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

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

+ 127 - 62
yarn.lock

@@ -1885,6 +1885,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"
@@ -1997,6 +2015,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"
+
 "@sqltools/formatter@1.2.2":
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.2.tgz#9390a8127c0dcba61ebd7fdcc748655e191bdd68"
@@ -2167,6 +2201,11 @@
   dependencies:
     "@types/express" "*"
 
+"@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@^4.17.18":
   version "4.17.19"
   resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.19.tgz#00acfc1632e729acac4f1530e9e16f6dd1508a1d"
@@ -2176,7 +2215,7 @@
     "@types/qs" "*"
     "@types/range-parser" "*"
 
-"@types/express@*", "@types/express@^4.16.1", "@types/express@^4.17.11":
+"@types/express@*", "@types/express@^4.16.1", "@types/express@^4.17.0", "@types/express@^4.17.11":
   version "4.17.11"
   resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.11.tgz#debe3caa6f8e5fcda96b47bd54e2f40c4ee59545"
   integrity sha512-no+R6rW60JEc59977wIxreQVsIEOAYwgCqldrA/vkpCnbD7MqTefO97lmoBe4WE0F156bC4uLSP1XHDOySnChg==
@@ -2261,6 +2300,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@^1":
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
@@ -2283,6 +2334,11 @@
   resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.35.tgz#42c953a4e2b18ab931f72477e7012172f4ffa313"
   integrity sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag==
 
+"@types/node@>=12.13.0 < 13":
+  version "12.20.7"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.7.tgz#1cb61fd0c85cb87e728c43107b5fd82b69bc9ef8"
+  integrity sha512-gWL8VUkg8VRaCAUgG9WmhefMqHmMblxe2rVpMF86nZY/+ZysU+BkAp+3cz03AixWDSSz0ks5WX59yAhv/cDwFA==
+
 "@types/node@^7.0.21", "@types/node@^7.0.23":
   version "7.10.14"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-7.10.14.tgz#06fa7319b8131b969a8da4a14c487e6f28abacf7"
@@ -2433,6 +2489,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/eslint-plugin@^4.18.0":
   version "4.18.0"
   resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.18.0.tgz#50fbce93211b5b690895d20ebec6fe8db48af1f6"
@@ -3817,7 +3880,7 @@ brorand@^1.0.1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
 
-browser-bunyan@^1.6.3:
+browser-bunyan@^1.3.0:
   version "1.6.3"
   resolved "https://registry.yarnpkg.com/browser-bunyan/-/browser-bunyan-1.6.3.tgz#0e58c51ff48507317ba8e5cf579e8b6bad7281e0"
   integrity sha512-HRg+acpwO3dsY2RWgtjw2wPVHV+uzbCrdhUxD25+qo5NFSTpbfJekrRP0yFNypAhG5LwXFV1Dc5FIc8cxwU5rQ==
@@ -4083,16 +4146,6 @@ bunyan@^1.8.12, bunyan@^1.8.3:
     mv "~2"
     safe-json-stringify "~1"
 
-bunyan@^1.8.15:
-  version "1.8.15"
-  resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.8.15.tgz#8ce34ca908a17d0776576ca1b2f6cbd916e93b46"
-  integrity sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==
-  optionalDependencies:
-    dtrace-provider "~0.8"
-    moment "^2.19.3"
-    mv "~2"
-    safe-json-stringify "~1"
-
 busboy@^0.2.11:
   version "0.2.14"
   resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453"
@@ -5592,7 +5645,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:
@@ -6908,6 +6961,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, express@^4.17.1:
+  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"
@@ -6978,42 +7067,6 @@ express@^4.16.3:
     utils-merge "1.0.1"
     vary "~1.1.2"
 
-express@^4.16.4, express@^4.17.1:
-  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"
@@ -11181,11 +11234,6 @@ moment@>=2.26.0:
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a"
   integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw==
 
-moment@^2.19.3:
-  version "2.29.1"
-  resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
-  integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
-
 mongodb@3.6.2, mongodb@^3.1.0, mongodb@^3.6.2:
   version "3.6.2"
   resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.6.2.tgz#1154a4ac107bf1375112d83a29c5cf97704e96b6"
@@ -17126,14 +17174,6 @@ unist-util-visit@^1.1.0:
   dependencies:
     unist-util-visit-parents "^2.0.0"
 
-universal-bunyan@^0.9.2:
-  version "0.9.2"
-  resolved "https://registry.yarnpkg.com/universal-bunyan/-/universal-bunyan-0.9.2.tgz#4cf09dc34070390d8f5df4fe9af6a80fcd0dd574"
-  integrity sha512-MkyO17+5AVCpFfhMtYLODvSZmPxV8eHcoOAWobEXXzlXrSnf5YgCV5lBWcMV3VPaaKyZPQ0oG5PSWYmGSBGtIg==
-  dependencies:
-    bunyan-format "^0.2.1"
-    minimatch "^3.0.4"
-
 universalify@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7"
@@ -18028,6 +18068,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"
@@ -18139,6 +18187,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@^16.0.0, yargs@^16.0.3:
   version "16.2.0"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"