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

Merge branch 'feat/growi-bot' into feat/5923-delete-accordion

Steven Fukase 4 лет назад
Родитель
Сommit
bd6451c417

+ 24 - 3
packages/slack/src/utils/check-communicable.ts

@@ -33,12 +33,24 @@ export const connectToSlackApiServer = async(): Promise<void|AxiosError> => {
  * Test Slack API
  * Test Slack API
  * @param client
  * @param client
  */
  */
-const testSlackApiServer = async(client: WebClient): Promise<void> => {
+const testSlackApiServer = async(client: WebClient): Promise<any> => {
   const result = await client.api.test();
   const result = await client.api.test();
 
 
   if (!result.ok) {
   if (!result.ok) {
     throw new Error(result.error);
     throw new Error(result.error);
   }
   }
+
+  return result;
+};
+
+const checkSlackScopes = (resultTestSlackApiServer: any) => {
+  const slackScopes = resultTestSlackApiServer.response_metadata.scopes;
+  const correctScopes = ['commands', 'team:read', 'chat:write'];
+  const isPassedScopeCheck = correctScopes.every(e => slackScopes.includes(e));
+
+  if (!isPassedScopeCheck) {
+    throw new Error('The scopes is not appropriate. Required scopes is [\'commands\', \'team:read\', \'chat:write\']');
+  }
 };
 };
 
 
 /**
 /**
@@ -94,7 +106,16 @@ export const getConnectionStatuses = async(tokens: string[]): Promise<{[key: str
  * @param token bot OAuth token
  * @param token bot OAuth token
  * @returns
  * @returns
  */
  */
-export const relationTestToSlack = async(token:string): Promise<void> => {
+export const testToSlack = async(token:string): Promise<void> => {
+  const client = generateWebClient(token);
+  const res = await testSlackApiServer(client);
+  await checkSlackScopes(res);
+};
+
+export const sendSuccessMessage = async(token:string, channel:string, appSiteUrl:string): Promise<void> => {
   const client = generateWebClient(token);
   const client = generateWebClient(token);
-  await testSlackApiServer(client);
+  await client.chat.postMessage({
+    channel,
+    text: `Successfully tested with ${appSiteUrl}.`,
+  });
 };
 };

+ 2 - 2
packages/slack/src/utils/webclient-factory.ts

@@ -7,6 +7,6 @@ const isProduction = process.env.NODE_ENV === 'production';
  * @param token Slack Bot Token or Proxy Server URI
  * @param token Slack Bot Token or Proxy Server URI
  * @returns
  * @returns
  */
  */
-export const generateWebClient = (token: string, serverUri?: string): WebClient => {
-  return new WebClient(token, { slackApiUrl: serverUri, logLevel: isProduction ? LogLevel.DEBUG : LogLevel.INFO });
+export const generateWebClient = (token: string, serverUri?: string, headers?:{[key:string]:string}): WebClient => {
+  return new WebClient(token, { slackApiUrl: serverUri, logLevel: isProduction ? LogLevel.DEBUG : LogLevel.INFO, headers });
 };
 };

+ 56 - 4
packages/slackbot-proxy/src/controllers/growi-to-slack.ts

@@ -1,11 +1,13 @@
 import {
 import {
-  Controller, Get, Inject, Req, Res, UseBefore,
+  Controller, Get, Post, Inject, Req, Res, UseBefore,
 } from '@tsed/common';
 } from '@tsed/common';
 import axios from 'axios';
 import axios from 'axios';
 
 
 import { WebAPICallResult } from '@slack/web-api';
 import { WebAPICallResult } from '@slack/web-api';
 
 
-import { verifyGrowiToSlackRequest, getConnectionStatuses, relationTestToSlack } from '@growi/slack';
+import {
+  verifyGrowiToSlackRequest, getConnectionStatuses, testToSlack, generateWebClient,
+} from '@growi/slack';
 
 
 import { GrowiReq } from '~/interfaces/growi-to-slack/growi-req';
 import { GrowiReq } from '~/interfaces/growi-to-slack/growi-req';
 import { InstallationRepository } from '~/repositories/installation';
 import { InstallationRepository } from '~/repositories/installation';
@@ -105,7 +107,14 @@ export class GrowiToSlackCtrl {
         return res.status(400).send({ message: `failed to request to GROWI. err: ${err.message}` });
         return res.status(400).send({ message: `failed to request to GROWI. err: ${err.message}` });
       }
       }
 
 
-      await relationTestToSlack(token);
+      try {
+        await testToSlack(token);
+      }
+      catch (err) {
+        logger.error(err);
+        return res.status(400).send({ message: `failed to test. err: ${err.message}` });
+      }
+
       return res.send({ relation });
       return res.send({ relation });
     }
     }
 
 
@@ -136,7 +145,13 @@ export class GrowiToSlackCtrl {
       return res.status(400).send({ message: 'installation is invalid' });
       return res.status(400).send({ message: 'installation is invalid' });
     }
     }
 
 
-    await relationTestToSlack(token);
+    try {
+      await testToSlack(token);
+    }
+    catch (err) {
+      logger.error(err);
+      return res.status(400).send({ message: `failed to test. err: ${err.message}` });
+    }
 
 
     logger.debug('relation test is success', order);
     logger.debug('relation test is success', order);
 
 
@@ -148,4 +163,41 @@ export class GrowiToSlackCtrl {
     return res.send({ relation: createdRelation });
     return res.send({ relation: createdRelation });
   }
   }
 
 
+  @Post('/*')
+  @UseBefore(verifyGrowiToSlackRequest)
+  async postResult(@Req() req: GrowiReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
+    const { tokenGtoPs } = req;
+
+    if (tokenGtoPs.length !== 1) {
+      return res.status(400).send({ message: 'tokenGtoPs is invalid' });
+    }
+
+    const tokenGtoP = tokenGtoPs[0];
+
+    // retrieve relation with Installation
+    const relation = await this.relationRepository.createQueryBuilder('relation')
+      .where('tokenGtoP = :token', { token: tokenGtoP })
+      .leftJoinAndSelect('relation.installation', 'installation')
+      .getOne();
+
+    if (relation == null) {
+      return res.status(400).send({ message: 'relation is invalid' });
+    }
+
+    const token = relation.installation.data.bot?.token;
+    if (token == null) {
+      return res.status(400).send({ message: 'installation is invalid' });
+    }
+
+    const client = generateWebClient(token);
+    await client.chat.postMessage({
+      channel: req.body.channel,
+      blocks: req.body.blocks,
+    });
+
+    logger.debug('postMessage is success');
+
+    return res.end();
+  }
+
 }
 }

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

@@ -301,6 +301,7 @@
     "reset_all_settings": "Reset all settings",
     "reset_all_settings": "Reset all settings",
     "delete_slackbot_settings": "Reset Slack Bot settings",
     "delete_slackbot_settings": "Reset Slack Bot settings",
     "slackbot_settings_notice": "Reset",
     "slackbot_settings_notice": "Reset",
+    "all_settings_of_the_bot_will_be_reset": "All settings of the Bot will be reset.<br>Are you sure?",
     "accordion": {
     "accordion": {
       "create_bot": "Create Bot",
       "create_bot": "Create Bot",
       "how_to_create_a_bot": "How to create a bot",
       "how_to_create_a_bot": "How to create a bot",

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

@@ -298,6 +298,7 @@
     "reset_all_settings": "全ての設定をリセット",
     "reset_all_settings": "全ての設定をリセット",
     "delete_slackbot_settings": "Slack Bot 設定をリセットする",
     "delete_slackbot_settings": "Slack Bot 設定をリセットする",
     "slackbot_settings_notice": "リセットします",
     "slackbot_settings_notice": "リセットします",
+    "all_settings_of_the_bot_will_be_reset": "Botの全ての設定がリセットされます。<br>よろしいですか?",
     "accordion": {
     "accordion": {
       "create_bot": "Bot を作成する",
       "create_bot": "Bot を作成する",
       "how_to_create_a_bot": "作成手順はこちら",
       "how_to_create_a_bot": "作成手順はこちら",

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

@@ -308,6 +308,7 @@
     "reset_all_settings": "重置所有设置",
     "reset_all_settings": "重置所有设置",
     "delete_slackbot_settings": "重置 Slack Bot 设置",
     "delete_slackbot_settings": "重置 Slack Bot 设置",
     "slackbot_settings_notice": "重置",
     "slackbot_settings_notice": "重置",
+    "all_settings_of_the_bot_will_be_reset": "bot的所有设置将被重置。<br>你确定吗?",
     "accordion": {
     "accordion": {
       "create_bot": "创建 Bot",
       "create_bot": "创建 Bot",
       "how_to_create_a_bot": "如何创建一个 Bot",
       "how_to_create_a_bot": "如何创建一个 Bot",

+ 58 - 40
src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxyIntegrationCard.jsx

@@ -4,16 +4,70 @@ import PropTypes from 'prop-types';
 
 
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
-const CustomBotWithoutProxyIntegrationCard = (props) => {
+const IntegrationSuccess = () => {
+  const { t } = useTranslation();
+
+  return (
+    <>
+      <div className="d-none d-lg-block">
+        <p className="text-success small mt-5">
+          <i className="fa fa-check mr-1" />
+          {t('admin:slack_integration.integration_sentence.integration_successful')}
+        </p>
+        <hr className="align-self-center admin-border-success border-success"></hr>
+      </div>
+      <div id="integration-line-for-tooltip" className="d-block d-lg-none mt-5">
+        <i className="fa fa-check mr-1 text-success" />
+        <hr className="align-self-center admin-border-success border-success"></hr>
+      </div>
+      <UncontrolledTooltip placement="top" fade={false} target="integration-line-for-tooltip">
+        <small>
+          {t('admin:slack_integration.integration_sentence.integration_successful')}
+        </small>
+      </UncontrolledTooltip>
+    </>
+  );
+};
 
 
+const IntegrationFailed = () => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
+  return (
+    <>
+      <div className="d-none d-lg-block">
+        <p className="mt-4">
+          <small
+            className="text-danger m-0"
+          // eslint-disable-next-line react/no-danger
+            dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.integration_sentence.integration_is_not_complete') }}
+          />
+        </p>
+        <hr className="align-self-center admin-border-danger border-danger"></hr>
+
+      </div>
+      <div id="integration-line-for-tooltip" className="d-block d-lg-none mt-5">
+        <i className="icon-info text-danger" />
+        <hr className="align-self-center admin-border-danger border-danger"></hr>
+      </div>
+      <UncontrolledTooltip placement="top" fade={false} target="integration-line-for-tooltip">
+        <small
+          className="m-0"
+        // eslint-disable-next-line react/no-danger
+          dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.integration_sentence.integration_is_not_complete') }}
+        />
+      </UncontrolledTooltip>
+    </>
+  );
+};
+
+const CustomBotWithoutProxyIntegrationCard = (props) => {
+
   return (
   return (
     <div className="d-flex justify-content-center my-5 bot-integration">
     <div className="d-flex justify-content-center my-5 bot-integration">
       <div className="card rounded shadow border-0 w-50 admin-bot-card mb-0">
       <div className="card rounded shadow border-0 w-50 admin-bot-card mb-0">
         <h5 className="card-title font-weight-bold mt-3 ml-4">Slack</h5>
         <h5 className="card-title font-weight-bold mt-3 ml-4">Slack</h5>
         <div className="card-body p-2 w-50 mx-auto">
         <div className="card-body p-2 w-50 mx-auto">
-          {props.slackWSNameInWithoutProxy != null && (
+          {props.isIntegrationSuccess && props.slackWSNameInWithoutProxy != null && (
             <div className="card slack-work-space-name-card">
             <div className="card slack-work-space-name-card">
               <div className="m-2 text-center">
               <div className="m-2 text-center">
                 <h5 className="font-weight-bold">{props.slackWSNameInWithoutProxy}</h5>
                 <h5 className="font-weight-bold">{props.slackWSNameInWithoutProxy}</h5>
@@ -25,32 +79,7 @@ const CustomBotWithoutProxyIntegrationCard = (props) => {
       </div>
       </div>
 
 
       <div className="text-center w-25">
       <div className="text-center w-25">
-
-        <div className="d-none d-lg-block">
-          {/* TODO GW-5998 switching logic */}
-          {/* <p className="text-success small mt-5">
-            <i className="fa fa-check mr-1" />
-            {t('admin:slack_integration.integration_sentence.integration_successful')}
-          </p> */}
-
-          <p className="mt-4">
-            <small
-              className="text-danger m-0"
-              // eslint-disable-next-line react/no-danger
-              dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.integration_sentence.integration_is_not_complete') }}
-            />
-          </p>
-        </div>
-
-        {/* TODO GW-5998  */}
-        <div id="integration-line-for-tooltip" className="d-block d-lg-none mt-5">
-          {/* <i className="fa fa-check mr-1 text-success" /> */}
-          <i className="icon-info text-danger" />
-        </div>
-
-        {/* TODO GW-5998 */}
-        {/* <hr className="align-self-center admin-border-success border-success"></hr> */}
-        <hr className="align-self-center admin-border-danger border-danger"></hr>
+        {props.isIntegrationSuccess ? <IntegrationSuccess /> : <IntegrationFailed />}
       </div>
       </div>
 
 
       <div className="card rounded-lg shadow border-0 w-50 admin-bot-card mb-0">
       <div className="card rounded-lg shadow border-0 w-50 admin-bot-card mb-0">
@@ -59,18 +88,6 @@ const CustomBotWithoutProxyIntegrationCard = (props) => {
           <div className="btn btn-primary">{ props.siteName }</div>
           <div className="btn btn-primary">{ props.siteName }</div>
         </div>
         </div>
       </div>
       </div>
-
-      <UncontrolledTooltip placement="top" fade={false} target="integration-line-for-tooltip">
-        {/* TODO GW-5998 */}
-        {/* <small>
-          {t('admin:slack_integration.integration_sentence.integration_successful')}
-        </small> */}
-        <small
-          className="m-0"
-          // eslint-disable-next-line react/no-danger
-          dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.integration_sentence.integration_is_not_complete') }}
-        />
-      </UncontrolledTooltip>
     </div>
     </div>
   );
   );
 };
 };
@@ -78,6 +95,7 @@ const CustomBotWithoutProxyIntegrationCard = (props) => {
 CustomBotWithoutProxyIntegrationCard.propTypes = {
 CustomBotWithoutProxyIntegrationCard.propTypes = {
   siteName: PropTypes.string.isRequired,
   siteName: PropTypes.string.isRequired,
   slackWSNameInWithoutProxy: PropTypes.string,
   slackWSNameInWithoutProxy: PropTypes.string,
+  isIntegrationSuccess: PropTypes.bool,
 };
 };
 
 
 export default CustomBotWithoutProxyIntegrationCard;
 export default CustomBotWithoutProxyIntegrationCard;

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

@@ -14,6 +14,10 @@ const CustomBotWithoutProxySettings = (props) => {
 
 
   const [siteName, setSiteName] = useState('');
   const [siteName, setSiteName] = useState('');
   const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] = useState(false);
   const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] = useState(false);
+  const [isIntegrationSuccess, setIsIntegrationSuccess] = useState(false);
+  const [connectionMessage, setConnectionMessage] = useState('');
+  const [connectionErrorCode, setConnectionErrorCode] = useState(null);
+  const [testChannel, setTestChannel] = useState('');
 
 
   const resetSettings = async() => {
   const resetSettings = async() => {
     if (onResetSettings == null) {
     if (onResetSettings == null) {
@@ -22,6 +26,25 @@ const CustomBotWithoutProxySettings = (props) => {
     onResetSettings();
     onResetSettings();
   };
   };
 
 
+  const testConnection = async() => {
+    setConnectionErrorCode(null);
+    setConnectionMessage(null);
+    try {
+      await appContainer.apiv3.post('/slack-integration-settings/without-proxy/test', { channel: testChannel });
+      setConnectionMessage('Send the message to slack work space.');
+      setIsIntegrationSuccess(true);
+    }
+    catch (err) {
+      setConnectionErrorCode(err[0].code);
+      setConnectionMessage(err[0].message);
+      setIsIntegrationSuccess(false);
+    }
+  };
+
+  const inputTestChannelHandler = (channel) => {
+    setTestChannel(channel);
+  };
+
   useEffect(() => {
   useEffect(() => {
     const siteName = appContainer.config.crowi.title;
     const siteName = appContainer.config.crowi.title;
     setSiteName(siteName);
     setSiteName(siteName);
@@ -34,6 +57,7 @@ const CustomBotWithoutProxySettings = (props) => {
       <CustomBotWithoutProxyIntegrationCard
       <CustomBotWithoutProxyIntegrationCard
         siteName={siteName}
         siteName={siteName}
         slackWSNameInWithoutProxy={props.slackWSNameInWithoutProxy}
         slackWSNameInWithoutProxy={props.slackWSNameInWithoutProxy}
+        isIntegrationSuccess={isIntegrationSuccess}
       />
       />
 
 
       <h2 className="admin-setting-header">{t('admin:slack_integration.integration_procedure')}</h2>
       <h2 className="admin-setting-header">{t('admin:slack_integration.integration_procedure')}</h2>
@@ -51,6 +75,13 @@ const CustomBotWithoutProxySettings = (props) => {
         <CustomBotWithoutProxySettingsAccordion
         <CustomBotWithoutProxySettingsAccordion
           {...props}
           {...props}
           activeStep={botInstallationStep.CREATE_BOT}
           activeStep={botInstallationStep.CREATE_BOT}
+          connectionMessage={connectionMessage}
+          connectionErrorCode={connectionErrorCode}
+          isIntegrationSuccess={isIntegrationSuccess}
+          testChannel={testChannel}
+          onTestFormSubmitted={testConnection}
+          inputTestChannelHandler={inputTestChannelHandler}
+
         />
         />
       </div>
       </div>
       <DeleteSlackBotSettingsModal
       <DeleteSlackBotSettingsModal
@@ -73,6 +104,7 @@ CustomBotWithoutProxySettings.propTypes = {
   slackBotToken: PropTypes.string,
   slackBotToken: PropTypes.string,
   slackBotTokenEnv: PropTypes.string,
   slackBotTokenEnv: PropTypes.string,
   isRgisterSlackCredentials: PropTypes.bool,
   isRgisterSlackCredentials: PropTypes.bool,
+  isIntegrationSuccess: PropTypes.bool,
   slackWSNameInWithoutProxy: PropTypes.string,
   slackWSNameInWithoutProxy: PropTypes.string,
   onResetSettings: PropTypes.func,
   onResetSettings: PropTypes.func,
 };
 };

+ 31 - 47
src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx

@@ -16,19 +16,15 @@ export const botInstallationStep = {
 };
 };
 
 
 const CustomBotWithoutProxySettingsAccordion = ({
 const CustomBotWithoutProxySettingsAccordion = ({
-  appContainer, activeStep, fetchSlackIntegrationData,
-  slackSigningSecret, slackSigningSecretEnv, slackBotToken, slackBotTokenEnv,
-  isRegisterSlackCredentials, isSendTestMessage,
-  onSetSlackSigningSecret, onSetSlackBotToken, onSetIsSendTestMessage,
+  appContainer, activeStep,
+  connectionMessage, connectionErrorCode, testChannel, slackSigningSecret, slackSigningSecretEnv, slackBotToken, slackBotTokenEnv,
+  isRegisterSlackCredentials, isIntegrationSuccess,
+  fetchSlackIntegrationData, inputTestChannelHandler, onTestFormSubmitted, onSetSlackSigningSecret, onSetSlackBotToken,
 }) => {
 }) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   // TODO: GW-5644 Store default open accordion
   // TODO: GW-5644 Store default open accordion
   // eslint-disable-next-line no-unused-vars
   // eslint-disable-next-line no-unused-vars
   const [defaultOpenAccordionKeys, setDefaultOpenAccordionKeys] = useState(new Set([activeStep]));
   const [defaultOpenAccordionKeys, setDefaultOpenAccordionKeys] = useState(new Set([activeStep]));
-  const [connectionErrorCode, setConnectionErrorCode] = useState(null);
-  const [connectionErrorMessage, setConnectionErrorMessage] = useState(null);
-  const [connectionSuccessMessage, setConnectionSuccessMessage] = useState(null);
-  const [testChannel, setTestChannel] = useState('');
   const currentBotType = 'customBotWithoutProxy';
   const currentBotType = 'customBotWithoutProxy';
 
 
 
 
@@ -63,42 +59,22 @@ const CustomBotWithoutProxySettingsAccordion = ({
     }
     }
   };
   };
 
 
-  const testConnection = async() => {
-    setConnectionErrorCode(null);
-    setConnectionErrorMessage(null);
-    setConnectionSuccessMessage(null);
-    // TODO: 5921 Add new Test endpoint
-    try {
-      // eslint-disable-next-line no-console
-      console.log('Test');
-      // const res = await appContainer.apiv3.post('/slack-integration-settings//without-proxy/test', {
-      //   channel: testChannel,
-      // });
-      // setConnectionSuccessMessage(res.data.message);
-      // onSetIsSendTestMessage(true);
-    }
-    catch (err) {
-      onSetIsSendTestMessage(false);
-      setConnectionErrorCode('dummy-error-code');
-      setConnectionErrorMessage('This is a sample error message');
-    }
-  };
-
   const submitForm = (e) => {
   const submitForm = (e) => {
     e.preventDefault();
     e.preventDefault();
-    testConnection();
-  };
 
 
-  const inputTestChannelHandler = (channel) => {
-    setTestChannel(channel);
+    if (onTestFormSubmitted == null) {
+      return;
+    }
+    onTestFormSubmitted();
   };
   };
 
 
+
   let value = '';
   let value = '';
-  if (connectionErrorMessage != null) {
-    value = [connectionErrorCode, connectionErrorMessage];
+  if (connectionMessage === 'Send the message to slack work space.' || connectionMessage === '') {
+    value = connectionMessage;
   }
   }
-  if (connectionSuccessMessage != null) {
-    value = connectionSuccessMessage;
+  else {
+    value = [connectionErrorCode, connectionMessage];
   }
   }
 
 
   return (
   return (
@@ -159,7 +135,7 @@ const CustomBotWithoutProxySettingsAccordion = ({
       <Accordion
       <Accordion
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.CONNECTION_TEST)}
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.CONNECTION_TEST)}
         // eslint-disable-next-line max-len
         // eslint-disable-next-line max-len
-        title={<><span className="mr-2">④</span>{t('admin:slack_integration.accordion.test_connection')}{isSendTestMessage && <i className="ml-3 text-success fa fa-check"></i>}</>}
+        title={<><span className="mr-2">④</span>{t('admin:slack_integration.accordion.test_connection')}{isIntegrationSuccess && <i className="ml-3 text-success fa fa-check"></i>}</>}
       >
       >
         <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
         <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
         <div className="d-flex justify-content-center">
         <div className="d-flex justify-content-center">
@@ -184,10 +160,17 @@ const CustomBotWithoutProxySettingsAccordion = ({
             </button>
             </button>
           </form>
           </form>
         </div>
         </div>
-        {connectionErrorMessage != null
-          && <p className="text-danger text-center my-4">{t('admin:slack_integration.accordion.error_check_logs_below')}</p>}
-        {connectionSuccessMessage != null
-          && <p className="text-info text-center my-4">{t('admin:slack_integration.accordion.send_message_to_slack_work_space')}</p>}
+        {connectionMessage === ''
+          ? <p></p>
+          : (
+            <>
+              {connectionMessage === 'Send the message to slack work space.'
+                ? <p className="text-info text-center my-4">{t('admin:slack_integration.accordion.send_message_to_slack_work_space')}</p>
+                : <p className="text-danger text-center my-4">{t('admin:slack_integration.accordion.error_check_logs_below')}</p>
+              }
+            </>
+          )
+        }
         <form>
         <form>
           <div className="row my-3 justify-content-center">
           <div className="row my-3 justify-content-center">
             <div className="form-group slack-connection-log col-md-4">
             <div className="form-group slack-connection-log col-md-4">
@@ -214,15 +197,16 @@ CustomBotWithoutProxySettingsAccordion.propTypes = {
   slackSigningSecretEnv: PropTypes.string,
   slackSigningSecretEnv: PropTypes.string,
   slackBotToken: PropTypes.string,
   slackBotToken: PropTypes.string,
   slackBotTokenEnv: PropTypes.string,
   slackBotTokenEnv: PropTypes.string,
+  testChannel: PropTypes.string,
   isRegisterSlackCredentials: PropTypes.bool,
   isRegisterSlackCredentials: PropTypes.bool,
-  isSendTestMessage: PropTypes.bool,
+  isIntegrationSuccess: PropTypes.bool,
   fetchSlackIntegrationData: PropTypes.func,
   fetchSlackIntegrationData: PropTypes.func,
+  inputTestChannelHandler: PropTypes.func,
+  onTestFormSubmitted: PropTypes.func,
   onSetSlackSigningSecret: PropTypes.func,
   onSetSlackSigningSecret: PropTypes.func,
   onSetSlackBotToken: PropTypes.func,
   onSetSlackBotToken: PropTypes.func,
-  onSetIsSendTestMessage: PropTypes.func,
-  onSetIsRegisterSlackCredentials: PropTypes.func,
-  setSlackWSNameInWithoutProxy: PropTypes.func,
-
+  connectionMessage: PropTypes.string,
+  connectionErrorCode: PropTypes.string,
   adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
   adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
   activeStep: PropTypes.oneOf(Object.values(botInstallationStep)).isRequired,
   activeStep: PropTypes.oneOf(Object.values(botInstallationStep)).isRequired,
 };
 };

+ 13 - 1
src/client/js/components/Admin/SlackIntegration/DeleteSlackBotSettingsModal.jsx

@@ -49,7 +49,19 @@ const DeleteSlackBotSettingsModal = React.memo((props) => {
         </span>
         </span>
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>
-        {t('admin:slack_integration.slackbot_settings_notice')}
+        {props.isResetAll && (
+          <>
+            <span
+              // eslint-disable-next-line react/no-danger
+              dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.all_settings_of_the_bot_will_be_reset') }}
+            />
+          </>
+        )}
+        {!props.isResetAll && (
+          <>
+            {t('admin:slack_integration.slackbot_settings_notice')}
+          </>
+        )}
       </ModalBody>
       </ModalBody>
       <ModalFooter>
       <ModalFooter>
         <Button onClick={closeButtonHandler}>{t('Cancel')}</Button>
         <Button onClick={closeButtonHandler}>{t('Cancel')}</Button>

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

@@ -111,7 +111,7 @@ const OfficialBotSettings = (props) => {
         </div>
         </div>
       </div>
       </div>
 
 
-      <h2 className="admin-setting-header">{t('admin:slack_integration.official_bot_settings')}</h2>
+      <h2 className="admin-setting-header">{t('admin:slack_integration.integration_procedure')}</h2>
 
 
       <div className="mx-3">
       <div className="mx-3">
         {slackAppIntegrations.map((slackAppIntegration) => {
         {slackAppIntegrations.map((slackAppIntegration) => {

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

@@ -24,7 +24,6 @@ const SlackIntegration = (props) => {
   const [slackSigningSecretEnv, setSlackSigningSecretEnv] = useState('');
   const [slackSigningSecretEnv, setSlackSigningSecretEnv] = useState('');
   const [slackBotTokenEnv, setSlackBotTokenEnv] = useState('');
   const [slackBotTokenEnv, setSlackBotTokenEnv] = useState('');
   const [isRegisterSlackCredentials, setIsRegisterSlackCredentials] = useState(false);
   const [isRegisterSlackCredentials, setIsRegisterSlackCredentials] = useState(false);
-  const [isSendTestMessage, setIsSendTestMessage] = useState(false);
   const [slackWSNameInWithoutProxy, setSlackWSNameInWithoutProxy] = useState(null);
   const [slackWSNameInWithoutProxy, setSlackWSNameInWithoutProxy] = useState(null);
   const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] = useState(false);
   const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] = useState(false);
   const [slackAppIntegrations, setSlackAppIntegrations] = useState();
   const [slackAppIntegrations, setSlackAppIntegrations] = useState();
@@ -93,7 +92,6 @@ const SlackIntegration = (props) => {
       setIsRegisterSlackCredentials(false);
       setIsRegisterSlackCredentials(false);
       setSlackSigningSecret(null);
       setSlackSigningSecret(null);
       setSlackBotToken(null);
       setSlackBotToken(null);
-      setIsSendTestMessage(false);
       setSlackWSNameInWithoutProxy(null);
       setSlackWSNameInWithoutProxy(null);
     }
     }
     catch (err) {
     catch (err) {
@@ -129,7 +127,6 @@ const SlackIntegration = (props) => {
     case 'customBotWithoutProxy':
     case 'customBotWithoutProxy':
       settingsComponent = (
       settingsComponent = (
         <CustomBotWithoutProxySettings
         <CustomBotWithoutProxySettings
-          isSendTestMessage={isSendTestMessage}
           isRegisterSlackCredentials={isRegisterSlackCredentials}
           isRegisterSlackCredentials={isRegisterSlackCredentials}
           slackBotTokenEnv={slackBotTokenEnv}
           slackBotTokenEnv={slackBotTokenEnv}
           slackBotToken={slackBotToken}
           slackBotToken={slackBotToken}
@@ -138,7 +135,6 @@ const SlackIntegration = (props) => {
           slackWSNameInWithoutProxy={slackWSNameInWithoutProxy}
           slackWSNameInWithoutProxy={slackWSNameInWithoutProxy}
           onSetSlackSigningSecret={setSlackSigningSecret}
           onSetSlackSigningSecret={setSlackSigningSecret}
           onSetSlackBotToken={setSlackBotToken}
           onSetSlackBotToken={setSlackBotToken}
-          onSetIsSendTestMessage={setIsSendTestMessage}
           onResetSettings={resetWithOutSettings}
           onResetSettings={resetWithOutSettings}
           fetchSlackIntegrationData={fetchSlackIntegrationData}
           fetchSlackIntegrationData={fetchSlackIntegrationData}
         />
         />

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

@@ -5,7 +5,7 @@ const axios = require('axios');
 const urljoin = require('url-join');
 const urljoin = require('url-join');
 const loggerFactory = require('@alias/logger');
 const loggerFactory = require('@alias/logger');
 
 
-const { getConnectionStatuses, relationTestToSlack } = require('@growi/slack');
+const { getConnectionStatuses, testToSlack, sendSuccessMessage } = require('@growi/slack');
 
 
 const ErrorV3 = require('../../models/vo/error-apiv3');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 
@@ -510,17 +510,26 @@ module.exports = (crowi) => {
       const msg = 'Select Without Proxy Type';
       const msg = 'Select Without Proxy Type';
       return res.apiv3Err(new ErrorV3(msg, 'select-not-proxy-type'), 400);
       return res.apiv3Err(new ErrorV3(msg, 'select-not-proxy-type'), 400);
     }
     }
-    // TODO impl req.body at GW-5998
-    // const { channel } = req.body;
+
     const slackBotToken = crowi.configManager.getConfig('crowi', 'slackbot:token');
     const slackBotToken = crowi.configManager.getConfig('crowi', 'slackbot:token');
     try {
     try {
-      await relationTestToSlack(slackBotToken);
-      // TODO impl return response after imple 5996, 6002
+      await testToSlack(slackBotToken);
     }
     }
     catch (error) {
     catch (error) {
       logger.error('Error', error);
       logger.error('Error', error);
       return res.apiv3Err(new ErrorV3(`Error occured while testing. Cause: ${error.message}`, 'test-failed', error.stack));
       return res.apiv3Err(new ErrorV3(`Error occured while testing. Cause: ${error.message}`, 'test-failed', error.stack));
     }
     }
+
+    const { channel } = req.body;
+    const appSiteURL = crowi.configManager.getConfig('crowi', 'app:siteUrl');
+    try {
+      await sendSuccessMessage(slackBotToken, channel, appSiteURL);
+    }
+    catch (error) {
+      return res.apiv3Err(new ErrorV3(`Error occured while sending message. Cause: ${error.message}`, 'send-message-failed', error.stack));
+    }
+
+    return res.apiv3();
   });
   });
 
 
   return router;
   return router;

+ 61 - 11
src/server/routes/apiv3/slack-integration.js

@@ -1,9 +1,10 @@
 const express = require('express');
 const express = require('express');
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
+const urljoin = require('url-join');
 
 
 const loggerFactory = require('@alias/logger');
 const loggerFactory = require('@alias/logger');
 
 
-const { verifySlackRequest } = require('@growi/slack');
+const { verifySlackRequest, generateWebClient } = require('@growi/slack');
 
 
 const logger = loggerFactory('growi:routes:apiv3:slack-integration');
 const logger = loggerFactory('growi:routes:apiv3:slack-integration');
 const router = express.Router();
 const router = express.Router();
@@ -24,13 +25,13 @@ module.exports = (crowi) => {
       return res.status(400).send({ message });
       return res.status(400).send({ message });
     }
     }
 
 
-    const slackAppIntegration = await SlackAppIntegration.estimatedDocumentCount({ tokenPtoG });
+    const slackAppIntegrationCount = await SlackAppIntegration.estimatedDocumentCount({ tokenPtoG });
 
 
     logger.debug('verifyAccessTokenFromProxy', {
     logger.debug('verifyAccessTokenFromProxy', {
       tokenPtoG,
       tokenPtoG,
     });
     });
 
 
-    if (slackAppIntegration === 0) {
+    if (slackAppIntegrationCount === 0) {
       return res.status(403).send({
       return res.status(403).send({
         message: 'The access token that identifies the request source is slackbot-proxy is invalid. Did you setup with `/growi register`?',
         message: 'The access token that identifies the request source is slackbot-proxy is invalid. Did you setup with `/growi register`?',
       });
       });
@@ -44,6 +45,31 @@ module.exports = (crowi) => {
     return next();
     return next();
   };
   };
 
 
+  const generateClientForResponse = (tokenGtoP) => {
+    const currentBotType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
+
+    if (currentBotType == null) {
+      throw new Error('The config \'SLACK_BOT_TYPE\'(ns: \'crowi\', key: \'slackbot:currentBotType\') must be set.');
+    }
+
+    let token;
+
+    // connect directly
+    if (tokenGtoP == null) {
+      token = crowi.configManager.getConfig('crowi', 'slackbot:token');
+      return generateWebClient(token);
+    }
+
+    // connect to proxy
+    const proxyServerUri = crowi.configManager.getConfig('crowi', 'slackbot:proxyServerUri');
+    const serverUri = urljoin(proxyServerUri, '/g2s');
+    const headers = {
+      'x-growi-gtop-tokens': tokenGtoP,
+    };
+
+    return generateWebClient(token, serverUri, headers);
+  };
+
   async function handleCommands(req, res) {
   async function handleCommands(req, res) {
     const { body } = req;
     const { body } = req;
 
 
@@ -59,19 +85,31 @@ module.exports = (crowi) => {
     // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
     // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
     res.send();
     res.send();
 
 
+    const tokenPtoG = req.headers['x-growi-ptog-tokens'];
+
+    // generate client
+    let client;
+    if (tokenPtoG == null) {
+      client = generateClientForResponse();
+    }
+    else {
+      const slackAppIntegration = await SlackAppIntegration.findOne({ tokenPtoG });
+      client = generateClientForResponse(slackAppIntegration.tokenGtoP);
+    }
+
     const args = body.text.split(' ');
     const args = body.text.split(' ');
     const command = args[0];
     const command = args[0];
 
 
     try {
     try {
       switch (command) {
       switch (command) {
         case 'search':
         case 'search':
-          await crowi.slackBotService.showEphemeralSearchResults(body, args);
+          await crowi.slackBotService.showEphemeralSearchResults(client, body, args);
           break;
           break;
         case 'create':
         case 'create':
-          await crowi.slackBotService.createModal(body);
+          await crowi.slackBotService.createModal(client, body);
           break;
           break;
         default:
         default:
-          await crowi.slackBotService.notCommand(body);
+          await crowi.slackBotService.notCommand(client, body);
           break;
           break;
       }
       }
     }
     }
@@ -98,12 +136,12 @@ module.exports = (crowi) => {
   });
   });
 
 
 
 
-  const handleBlockActions = async(payload) => {
+  const handleBlockActions = async(client, payload) => {
     const { action_id: actionId } = payload.actions[0];
     const { action_id: actionId } = payload.actions[0];
 
 
     switch (actionId) {
     switch (actionId) {
       case 'shareSearchResults': {
       case 'shareSearchResults': {
-        await crowi.slackBotService.shareSearchResults(payload);
+        await crowi.slackBotService.shareSearchResults(client, payload);
         break;
         break;
       }
       }
       case 'showNextResults': {
       case 'showNextResults': {
@@ -111,7 +149,7 @@ module.exports = (crowi) => {
 
 
         const { body, args, offset } = parsedValue;
         const { body, args, offset } = parsedValue;
         const newOffset = offset + 10;
         const newOffset = offset + 10;
-        await crowi.slackBotService.showEphemeralSearchResults(body, args, newOffset);
+        await crowi.slackBotService.showEphemeralSearchResults(client, body, args, newOffset);
         break;
         break;
       }
       }
       default:
       default:
@@ -137,16 +175,28 @@ module.exports = (crowi) => {
     // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
     // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
     res.send();
     res.send();
 
 
+
+    const tokenPtoG = req.headers['x-growi-ptog-tokens'];
+    // generate client
+    let client;
+    if (tokenPtoG == null) {
+      client = generateClientForResponse();
+    }
+    else {
+      const slackAppIntegration = await SlackAppIntegration.findOne({ tokenPtoG });
+      client = generateClientForResponse(slackAppIntegration.tokenGtoP);
+    }
+
     const payload = JSON.parse(req.body.payload);
     const payload = JSON.parse(req.body.payload);
     const { type } = payload;
     const { type } = payload;
 
 
     try {
     try {
       switch (type) {
       switch (type) {
         case 'block_actions':
         case 'block_actions':
-          await handleBlockActions(payload);
+          await handleBlockActions(client, payload);
           break;
           break;
         case 'view_submission':
         case 'view_submission':
-          await handleViewSubmission(payload);
+          await handleViewSubmission(client, payload);
           break;
           break;
         default:
         default:
           break;
           break;

+ 16 - 50
src/server/service/slackbot.js

@@ -1,8 +1,6 @@
 const logger = require('@alias/logger')('growi:service:SlackBotService');
 const logger = require('@alias/logger')('growi:service:SlackBotService');
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
 
 
-const { generateWebClient } = require('@growi/slack');
-
 const PAGINGLIMIT = 10;
 const PAGINGLIMIT = 10;
 
 
 const S2sMessage = require('../models/vo/s2s-message');
 const S2sMessage = require('../models/vo/s2s-message');
@@ -25,29 +23,6 @@ class SlackBotService extends S2sMessageHandlable {
     this.lastLoadedAt = new Date();
     this.lastLoadedAt = new Date();
   }
   }
 
 
-  get client() {
-    const currentBotType = this.crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
-
-    if (currentBotType == null) {
-      throw new Error('The config \'SLACK_BOT_TYPE\'(ns: \'crowi\', key: \'slackbot:currentBotType\') must be set.');
-    }
-
-    let serverUri;
-    let token;
-
-    // connect to proxy
-    if (currentBotType !== 'customBotWithoutProxy') {
-      // TODO: https://youtrack.weseek.co.jp/issue/GW-5896
-      serverUri = 'http://localhost:8080/slack-api-proxy/';
-    }
-    // connect directly
-    else {
-      token = this.crowi.configManager.getConfig('crowi', 'slackbot:token');
-    }
-
-    return generateWebClient(token, serverUri);
-  }
-
   /**
   /**
    * @inheritdoc
    * @inheritdoc
    */
    */
@@ -87,13 +62,9 @@ class SlackBotService extends S2sMessageHandlable {
     }
     }
   }
   }
 
 
-  async sendAuthTest() {
-    await this.client.api.test();
-  }
-
-  notCommand(body) {
+  async notCommand(client, body) {
     logger.error('Invalid first argument');
     logger.error('Invalid first argument');
-    this.client.chat.postEphemeral({
+    client.chat.postEphemeral({
       channel: body.channel_id,
       channel: body.channel_id,
       user: body.user_id,
       user: body.user_id,
       blocks: [
       blocks: [
@@ -109,10 +80,10 @@ class SlackBotService extends S2sMessageHandlable {
     return keywords;
     return keywords;
   }
   }
 
 
-  async getSearchResultPaths(body, args, offset = 0) {
+  async getSearchResultPaths(client, body, args, offset = 0) {
     const firstKeyword = args[1];
     const firstKeyword = args[1];
     if (firstKeyword == null) {
     if (firstKeyword == null) {
-      this.client.chat.postEphemeral({
+      client.chat.postEphemeral({
         channel: body.channel_id,
         channel: body.channel_id,
         user: body.user_id,
         user: body.user_id,
         blocks: [
         blocks: [
@@ -132,7 +103,7 @@ class SlackBotService extends S2sMessageHandlable {
     // no search results
     // no search results
     if (results.data.length === 0) {
     if (results.data.length === 0) {
       logger.info(`No page found with "${keywords}"`);
       logger.info(`No page found with "${keywords}"`);
-      this.client.chat.postEphemeral({
+      client.chat.postEphemeral({
         channel: body.channel_id,
         channel: body.channel_id,
         user: body.user_id,
         user: body.user_id,
         blocks: [
         blocks: [
@@ -166,22 +137,17 @@ class SlackBotService extends S2sMessageHandlable {
     };
     };
   }
   }
 
 
-  async getSlackChannelName() {
-    const slackTeamInfo = await this.client.team.info();
-    return slackTeamInfo.team.name;
-  }
-
-  shareSearchResults(payload) {
-    this.client.chat.postMessage({
+  async shareSearchResults(client, payload) {
+    client.chat.postMessage({
       channel: payload.channel.id,
       channel: payload.channel.id,
       text: payload.actions[0].value,
       text: payload.actions[0].value,
     });
     });
   }
   }
 
 
-  async showEphemeralSearchResults(body, args, offsetNum) {
+  async showEphemeralSearchResults(client, body, args, offsetNum) {
     const {
     const {
       resultPaths, offset, resultsTotal,
       resultPaths, offset, resultsTotal,
-    } = await this.getSearchResultPaths(body, args, offsetNum);
+    } = await this.getSearchResultPaths(client, body, args, offsetNum);
 
 
     const keywords = this.getKeywords(args);
     const keywords = this.getKeywords(args);
 
 
@@ -246,7 +212,7 @@ class SlackBotService extends S2sMessageHandlable {
           },
           },
         );
         );
       }
       }
-      await this.client.chat.postEphemeral({
+      await client.chat.postEphemeral({
         channel: body.channel_id,
         channel: body.channel_id,
         user: body.user_id,
         user: body.user_id,
         blocks: [
         blocks: [
@@ -258,7 +224,7 @@ class SlackBotService extends S2sMessageHandlable {
     }
     }
     catch {
     catch {
       logger.error('Failed to get search results.');
       logger.error('Failed to get search results.');
-      await this.client.chat.postEphemeral({
+      await client.chat.postEphemeral({
         channel: body.channel_id,
         channel: body.channel_id,
         user: body.user_id,
         user: body.user_id,
         blocks: [
         blocks: [
@@ -269,9 +235,9 @@ class SlackBotService extends S2sMessageHandlable {
     }
     }
   }
   }
 
 
-  async createModal(body) {
+  async createModal(client, body) {
     try {
     try {
-      await this.client.views.open({
+      await client.views.open({
         trigger_id: body.trigger_id,
         trigger_id: body.trigger_id,
 
 
         view: {
         view: {
@@ -299,7 +265,7 @@ class SlackBotService extends S2sMessageHandlable {
     }
     }
     catch (err) {
     catch (err) {
       logger.error('Failed to create a page.');
       logger.error('Failed to create a page.');
-      await this.client.chat.postEphemeral({
+      await client.chat.postEphemeral({
         channel: body.channel_id,
         channel: body.channel_id,
         user: body.user_id,
         user: body.user_id,
         blocks: [
         blocks: [
@@ -311,7 +277,7 @@ class SlackBotService extends S2sMessageHandlable {
   }
   }
 
 
   // Submit action in create Modal
   // Submit action in create Modal
-  async createPageInGrowi(payload) {
+  async createPageInGrowi(client, payload) {
     const Page = this.crowi.model('Page');
     const Page = this.crowi.model('Page');
     const pathUtils = require('growi-commons').pathUtils;
     const pathUtils = require('growi-commons').pathUtils;
 
 
@@ -328,7 +294,7 @@ class SlackBotService extends S2sMessageHandlable {
       await Page.create(path, contentsBody, dummyObjectIdOfUser, {});
       await Page.create(path, contentsBody, dummyObjectIdOfUser, {});
     }
     }
     catch (err) {
     catch (err) {
-      this.client.chat.postMessage({
+      client.chat.postMessage({
         channel: payload.user.id,
         channel: payload.user.id,
         blocks: [
         blocks: [
           this.generateMarkdownSectionBlock(`Cannot create new page to existed path\n *Contents* :memo:\n ${contentsBody}`)],
           this.generateMarkdownSectionBlock(`Cannot create new page to existed path\n *Contents* :memo:\n ${contentsBody}`)],