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

Merge remote-tracking branch 'origin/feat/growi-bot' into support/growi-bot-update-dockerfile

Yuki Takei 4 лет назад
Родитель
Сommit
e86ca1eed2

+ 1 - 0
packages/slack/src/index.ts

@@ -5,6 +5,7 @@ export const supportedSlackCommands: string[] = [
 export const supportedGrowiCommands: string[] = [
   'search',
   'create',
+  'help',
 ];
 
 export * from './interfaces/growi-command';

+ 36 - 2
packages/slackbot-proxy/src/controllers/growi-to-slack.ts

@@ -18,6 +18,8 @@ import { OrderRepository } from '~/repositories/order';
 
 import { InstallerService } from '~/services/InstallerService';
 import loggerFactory from '~/utils/logger';
+import { findInjectorByType } from '~/services/growi-uri-injector/GrowiUriInjectorFactory';
+import { injectGrowiUriToView } from '~/utils/injectGrowiUriToView';
 
 
 const logger = loggerFactory('slackbot-proxy:controllers:growi-to-slack');
@@ -163,6 +165,38 @@ export class GrowiToSlackCtrl {
     return res.send({ relation: createdRelation, slackBotToken: token });
   }
 
+  injectGrowiUri(req:GrowiReq, growiUri:string):WebAPICallOptions {
+
+    if (req.body.view != null) {
+      injectGrowiUriToView(req.body, growiUri);
+    }
+
+    if (req.body.blocks != null) {
+      const parsedBlocks = JSON.parse(req.body.blocks as string);
+
+      parsedBlocks.forEach((parsedBlock) => {
+        if (parsedBlock.type !== 'actions') {
+          return;
+        }
+        parsedBlock.elements.forEach((element) => {
+          const growiUriInjector = findInjectorByType(element.type);
+          if (growiUriInjector != null) {
+            growiUriInjector.inject(element, growiUri);
+          }
+        });
+
+        return;
+      });
+
+      req.body.blocks = JSON.stringify(parsedBlocks);
+    }
+
+    const opt = req.body;
+    opt.headers = req.headers;
+
+    return opt;
+  }
+
   @Post('/:method')
   @UseBefore(AddWebclientResponseToRes, verifyGrowiToSlackRequest)
   async postResult(
@@ -194,8 +228,8 @@ export class GrowiToSlackCtrl {
     const client = generateWebClient(token);
 
     try {
-      const opt = req.body as WebAPICallOptions;
-      opt.headers = req.headers;
+      const opt = this.injectGrowiUri(req, relation.growiUri);
+
       await client.apiCall(method, opt);
     }
     catch (err) {

+ 13 - 17
packages/slackbot-proxy/src/controllers/slack.ts

@@ -17,6 +17,7 @@ import { RelationRepository } from '~/repositories/relation';
 import { OrderRepository } from '~/repositories/order';
 import { AddSigningSecretToReq } from '~/middlewares/slack-to-growi/add-signing-secret-to-req';
 import { AuthorizeCommandMiddleware, AuthorizeInteractionMiddleware } from '~/middlewares/slack-to-growi/authorizer';
+import { ExtractGrowiUriFromReq } from '~/middlewares/slack-to-growi/extract-growi-uri-from-req';
 import { InstallerService } from '~/services/InstallerService';
 import { RegisterService } from '~/services/RegisterService';
 import { UnregisterService } from '~/services/UnregisterService';
@@ -164,7 +165,7 @@ export class SlackCtrl {
   }
 
   @Post('/interactions')
-  @UseBefore(AuthorizeInteractionMiddleware)
+  @UseBefore(AuthorizeInteractionMiddleware, ExtractGrowiUriFromReq)
   async handleInteraction(@Req() req: SlackOauthReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
     logger.info('receive interaction', req.body);
     logger.info('receive interaction', req.authorizeResult);
@@ -211,30 +212,25 @@ export class SlackCtrl {
     }
 
     /*
-     * forward to GROWI server
-     */
-    const relations = await this.relationRepository.find({ installation });
+    * forward to GROWI server
+    */
+    const relation = await this.relationRepository.findOne({ installation, growiUri: req.growiUri });
 
-    const promises = relations.map((relation: Relation) => {
+    if (relation == null) {
+      logger.error('*No relation found.*');
+      return;
+    }
+
+    try {
       // generate API URL
-      const url = new URL('/_api/v3/slack-integration/proxied/interactions', relation.growiUri);
-      return axios.post(url.toString(), {
+      const url = new URL('/_api/v3/slack-integration/proxied/interactions', req.growiUri);
+      await axios.post(url.toString(), {
         ...body,
       }, {
         headers: {
           'x-growi-ptog-tokens': relation.tokenPtoG,
         },
       });
-    });
-
-    // pickup PromiseRejectedResult only
-    const results = await Promise.allSettled(promises);
-    const rejectedResults: PromiseRejectedResult[] = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected');
-    const botToken = installation?.data.bot?.token;
-
-    try {
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      return postEphemeralErrors(rejectedResults, body.channel_id, body.user_id, botToken!);
     }
     catch (err) {
       logger.error(err);

+ 1 - 0
packages/slackbot-proxy/src/interfaces/slack-to-growi/slack-oauth-req.ts

@@ -3,4 +3,5 @@ import { Req } from '@tsed/common';
 
 export type SlackOauthReq = Req & {
   authorizeResult: AuthorizeResult,
+  growiUri?: string,
 };

+ 40 - 0
packages/slackbot-proxy/src/middlewares/slack-to-growi/extract-growi-uri-from-req.ts

@@ -0,0 +1,40 @@
+import {
+  IMiddleware, Middleware, Next, Req, Res,
+} from '@tsed/common';
+import { SlackOauthReq } from '~/interfaces/slack-to-growi/slack-oauth-req';
+import { growiUriInjectorFactory } from '~/services/growi-uri-injector/GrowiUriInjectorFactory';
+import { extractGrowiUriFromView } from '~/utils/extractGrowiUriFromView';
+
+@Middleware()
+export class ExtractGrowiUriFromReq implements IMiddleware {
+
+  use(@Req() req: Req & SlackOauthReq, @Res() res: Res, @Next() next: Next): void {
+
+    const payload = JSON.parse(req.body.payload);
+
+    // extract for modal
+    if (payload.view != null) {
+      const extractedValues = extractGrowiUriFromView(payload.view);
+      req.growiUri = extractedValues.growiUri;
+      payload.view.private_metadata = extractedValues.originalData;
+    }
+    else {
+      // break when uri is found
+      for (const type of Object.keys(growiUriInjectorFactory)) {
+        const growiUriInjector = growiUriInjectorFactory[type]();
+        const extractedValues = growiUriInjector.extract(payload.actions[0]);
+
+        if (extractedValues.growiUri != null) {
+          req.growiUri = extractedValues.growiUri;
+          payload.actions[0].value = JSON.stringify(extractedValues.originalData);
+          break;
+        }
+      }
+    }
+
+    req.body.payload = JSON.stringify(payload);
+
+    return next();
+  }
+
+}

+ 19 - 0
packages/slackbot-proxy/src/services/growi-uri-injector/GrowiUriInjectionButtonDelegator.ts

@@ -0,0 +1,19 @@
+import { GrowiUriInjector } from './GrowiUriInjector';
+
+export class GrowiUriInjectionButtonDelegator implements GrowiUriInjector {
+
+  inject(element: {value:string}, growiUri:string): void {
+    const parsedValue = JSON.parse(element.value);
+    const originalData = JSON.stringify(parsedValue);
+    element.value = JSON.stringify({ growiUri, originalData });
+  }
+
+  extract(action: {value:string}): {growiUri?:string, originalData:any} {
+    const parsedValues = JSON.parse(action.value);
+    if (parsedValues.originalData != null) {
+      parsedValues.originalData = JSON.parse(parsedValues.originalData);
+    }
+    return parsedValues;
+  }
+
+}

+ 7 - 0
packages/slackbot-proxy/src/services/growi-uri-injector/GrowiUriInjector.ts

@@ -0,0 +1,7 @@
+
+export interface GrowiUriInjector {
+
+  inject(body: any, growiUri:string): void;
+
+  extract(body: any):any;
+}

+ 18 - 0
packages/slackbot-proxy/src/services/growi-uri-injector/GrowiUriInjectorFactory.ts

@@ -0,0 +1,18 @@
+import { GrowiUriInjector } from './GrowiUriInjector';
+import { GrowiUriInjectionButtonDelegator } from './GrowiUriInjectionButtonDelegator';
+
+/**
+ * Instanciate GrowiUriInjector
+ */
+export const growiUriInjectorFactory = {
+  button: (): GrowiUriInjector => {
+    return new GrowiUriInjectionButtonDelegator();
+  },
+};
+
+export const findInjectorByType = (type:string): null|GrowiUriInjector => {
+  if (!Object.keys(growiUriInjectorFactory).includes(type)) {
+    return null;
+  }
+  return growiUriInjectorFactory[type]();
+};

+ 10 - 0
packages/slackbot-proxy/src/utils/extractGrowiUriFromView.ts

@@ -0,0 +1,10 @@
+export const extractGrowiUriFromView = (view:{'private_metadata': string}): {growiUri?:string, originalData:{[key:string]:any}} => {
+  const parsedValues = JSON.parse(view.private_metadata);
+  if (parsedValues.originalData != null) {
+    parsedValues.originalData = JSON.parse(parsedValues.originalData);
+  }
+  else {
+    parsedValues.originalData = view.private_metadata;
+  }
+  return parsedValues;
+};

+ 7 - 0
packages/slackbot-proxy/src/utils/injectGrowiUriToView.ts

@@ -0,0 +1,7 @@
+export const injectGrowiUriToView = (body: {view:string}, growiUri:string): void => {
+  const parsedView = JSON.parse(body.view);
+  const originalData = JSON.stringify(parsedView.private_metadata);
+
+  parsedView.private_metadata = JSON.stringify({ growiUri, originalData });
+  body.view = JSON.stringify(parsedView);
+};

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

@@ -20,10 +20,9 @@ const CustomBotWithProxySettings = (props) => {
   const [siteName, setSiteName] = useState('');
   const { t } = useTranslation();
 
+  // componentDidUpdate
   useEffect(() => {
-    if (proxyServerUri != null) {
-      setNewProxyServerUri(proxyServerUri);
-    }
+    setNewProxyServerUri(proxyServerUri);
   }, [proxyServerUri]);
 
   const addSlackAppIntegrationHandler = async() => {

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

@@ -13,7 +13,7 @@ const logger = loggerFactory('growi:SlackBotSettings');
 
 const OfficialBotSettings = (props) => {
   const {
-    appContainer, slackAppIntegrations, proxyServerUri, onClickAddSlackWorkspaceBtn, connectionStatuses, onUpdateTokens,
+    appContainer, slackAppIntegrations, proxyServerUri, onClickAddSlackWorkspaceBtn, connectionStatuses, onUpdateTokens, onSubmitForm,
   } = props;
   const [siteName, setSiteName] = useState('');
   const [integrationIdToDelete, setIntegrationIdToDelete] = useState(null);
@@ -21,10 +21,9 @@ const OfficialBotSettings = (props) => {
 
   const [newProxyServerUri, setNewProxyServerUri] = useState();
 
+  // componentDidUpdate
   useEffect(() => {
-    if (proxyServerUri != null) {
-      setNewProxyServerUri(proxyServerUri);
-    }
+    setNewProxyServerUri(proxyServerUri);
   }, [proxyServerUri, slackAppIntegrations]);
 
   const addSlackAppIntegrationHandler = async() => {
@@ -124,6 +123,7 @@ const OfficialBotSettings = (props) => {
                 tokenGtoP={tokenGtoP}
                 tokenPtoG={tokenPtoG}
                 onUpdateTokens={onUpdateTokens}
+                onSubmitForm={onSubmitForm}
               />
             </React.Fragment>
           );
@@ -164,6 +164,7 @@ OfficialBotSettings.propTypes = {
   onDeleteSlackAppIntegration: PropTypes.func,
   connectionStatuses: PropTypes.object.isRequired,
   onUpdateTokens: PropTypes.func,
+  onSubmitForm: PropTypes.func,
 };
 
 export default OfficialBotSettingsWrapper;

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

@@ -125,6 +125,7 @@ const SlackIntegration = (props) => {
           onDeleteSlackAppIntegration={fetchSlackIntegrationData}
           connectionStatuses={connectionStatuses}
           onUpdateTokens={fetchSlackIntegrationData}
+          onSubmitForm={fetchSlackIntegrationData}
         />
       );
       break;

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

@@ -290,7 +290,13 @@ const WithProxyAccordions = (props) => {
     },
     '④': {
       title: 'test_connection',
-      content: <TestProcess />,
+      content: <TestProcess
+        apiv3Post={props.appContainer.apiv3.post}
+        slackAppIntegrationId={props.slackAppIntegrationId}
+        onSubmitForm={submitForm}
+        onSubmitFormFailed={submitFormFailed}
+        isLatestConnectionSuccess={isLatestConnectionSuccess}
+      />,
     },
   };
 

+ 3 - 6
src/server/models/page.js

@@ -985,9 +985,8 @@ module.exports = function(crowi) {
     savedPage = await this.findByPath(revision.path);
     await savedPage.populateDataToShowRevision();
 
-    if (socketClientId != null) {
-      pageEvent.emit('create', savedPage, user, socketClientId);
-    }
+    pageEvent.emit('create', savedPage, user, socketClientId);
+
     return savedPage;
   };
 
@@ -1014,9 +1013,7 @@ module.exports = function(crowi) {
       savedPage = await this.syncRevisionToHackmd(savedPage);
     }
 
-    if (socketClientId != null) {
-      pageEvent.emit('update', savedPage, user, socketClientId);
-    }
+    pageEvent.emit('update', savedPage, user, socketClientId);
 
     return savedPage;
   };

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

@@ -108,6 +108,9 @@ module.exports = (crowi) => {
         case 'create':
           await crowi.slackBotService.createModal(client, body);
           break;
+        case 'help':
+          await crowi.slackBotService.helpCommand(client, body);
+          break;
         default:
           await crowi.slackBotService.notCommand(client, body);
           break;

+ 28 - 3
src/server/service/slackbot.js

@@ -75,6 +75,19 @@ class SlackBotService extends S2sMessageHandlable {
     return;
   }
 
+  async helpCommand(client, body) {
+    const message = '*Help*\n growi-bot usage\n `/growi [command] [args]`\n\n Create new page\n `create`\n\n Search pages\n `search [keyword]`';
+    client.chat.postEphemeral({
+      channel: body.channel_id,
+      user: body.user_id,
+      text: 'Help',
+      blocks: [
+        this.generateMarkdownSectionBlock(message),
+      ],
+    });
+    return;
+  }
+
   getKeywords(args) {
     const keywordsArr = args.slice(1);
     const keywords = keywordsArr.join(' ');
@@ -143,7 +156,7 @@ class SlackBotService extends S2sMessageHandlable {
   async shareSearchResults(client, payload) {
     client.chat.postMessage({
       channel: payload.channel.id,
-      text: payload.actions[0].value,
+      text: JSON.parse(payload.actions[0].value).pageList,
     });
   }
 
@@ -197,7 +210,9 @@ class SlackBotService extends S2sMessageHandlable {
             },
             style: 'primary',
             action_id: 'shareSearchResults',
-            value: `${keywordsAndDesc} \n\n ${urls.join('\n')}`,
+            value: JSON.stringify({
+              offset, body, args, pageList: `${keywordsAndDesc} \n\n ${urls.join('\n')}`,
+            }),
           },
         ],
       };
@@ -265,6 +280,7 @@ class SlackBotService extends S2sMessageHandlable {
             this.generateInputSectionBlock('path', 'Path', 'path_input', false, '/path'),
             this.generateInputSectionBlock('contents', 'Contents', 'contents_input', true, 'Input with Markdown...'),
           ],
+          private_metadata: JSON.stringify({ channelId: body.channel_id }),
         },
       });
     }
@@ -297,7 +313,16 @@ class SlackBotService extends S2sMessageHandlable {
 
       // generate a dummy id because Operation to create a page needs ObjectId
       const dummyObjectIdOfUser = new mongoose.Types.ObjectId();
-      await Page.create(path, contentsBody, dummyObjectIdOfUser, {});
+      const page = await Page.create(path, contentsBody, dummyObjectIdOfUser, {});
+
+      // Send a message when page creation is complete
+      const growiUri = this.crowi.appService.getSiteUrl();
+      const channelId = JSON.parse(payload.view.private_metadata).channelId;
+      await client.chat.postEphemeral({
+        channel: channelId,
+        user: payload.user.id,
+        text: `The page <${decodeURI(growiUri + path)} | ${decodeURI(`${growiUri}/${page._id}`)}> has been created.`,
+      });
     }
     catch (err) {
       client.chat.postMessage({