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

Merge pull request #4004 from weseek/imprv/6533-refactoring

Imprv/6533 refactoring
Yuki Takei 4 лет назад
Родитель
Сommit
a7b5703cb4

+ 6 - 0
packages/slack/src/interfaces/request-between-growi-and-proxy.ts

@@ -6,6 +6,12 @@ export type RequestFromGrowi = Request & {
 
   // will be extracted from header
   tokenGtoPs: string[],
+
+  // Block Kit properties
+  body: {
+    view?: string,
+    blocks?: string
+  },
 };
 
 export type RequestFromProxy = Request & {

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

@@ -3,7 +3,7 @@ import {
 } from '@tsed/common';
 import axios from 'axios';
 
-import { WebAPICallOptions, WebAPICallResult } from '@slack/web-api';
+import { WebAPICallResult } from '@slack/web-api';
 
 import {
   verifyGrowiToSlackRequest, getConnectionStatuses, getConnectionStatus, generateWebClient,
@@ -18,8 +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';
+import { ViewInteractionPayloadDelegator } from '~/services/growi-uri-injector/ViewInteractionPayloadDelegator';
+import { ActionsBlockPayloadDelegator } from '~/services/growi-uri-injector/ActionsBlockPayloadDelegator';
 
 
 const logger = loggerFactory('slackbot-proxy:controllers:growi-to-slack');
@@ -42,6 +42,12 @@ export class GrowiToSlackCtrl {
   @Inject()
   orderRepository: OrderRepository;
 
+  @Inject()
+  viewInteractionPayloadDelegator: ViewInteractionPayloadDelegator;
+
+  @Inject()
+  actionsBlockPayloadDelegator: ActionsBlockPayloadDelegator;
+
   async requestToGrowi(growiUrl:string, tokenPtoG:string):Promise<void> {
     const url = new URL('/_api/v3/slack-integration/proxied/commands', growiUrl);
     await axios.post(url.toString(), {
@@ -171,41 +177,32 @@ export class GrowiToSlackCtrl {
     return res.send({ relation: createdRelation, slackBotToken: token });
   }
 
-  injectGrowiUri(req:GrowiReq, growiUri:string):WebAPICallOptions {
+  injectGrowiUri(req: GrowiReq, growiUri: string): void {
+    if (req.body.view == null && req.body.blocks == null) {
+      return;
+    }
 
     if (req.body.view != null) {
-      injectGrowiUriToView(req.body, growiUri);
+      const parsedElement = JSON.parse(req.body.view);
+      // delegate to ViewInteractionPayloadDelegator
+      if (this.viewInteractionPayloadDelegator.shouldHandleToInject(parsedElement)) {
+        this.viewInteractionPayloadDelegator.inject(parsedElement, growiUri);
+        req.body.view = JSON.stringify(parsedElement);
+      }
     }
-
-    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);
+    else if (req.body.blocks != null) {
+      const parsedElement = JSON.parse(req.body.blocks);
+      // delegate to ActionsBlockPayloadDelegator
+      if (this.actionsBlockPayloadDelegator.shouldHandleToInject(parsedElement)) {
+        this.actionsBlockPayloadDelegator.inject(parsedElement, growiUri);
+        req.body.blocks = JSON.stringify(parsedElement);
+      }
     }
-
-    const opt = req.body;
-    opt.headers = req.headers;
-
-    return opt;
   }
 
   @Post('/:method')
   @UseBefore(AddWebclientResponseToRes, verifyGrowiToSlackRequest)
-  async postResult(
+  async callSlackApi(
     @PathParams('method') method: string, @Req() req: GrowiReq, @Res() res: WebclientRes,
   ): Promise<void|string|Res|WebAPICallResult> {
     const { tokenGtoPs } = req;
@@ -234,7 +231,10 @@ export class GrowiToSlackCtrl {
     const client = generateWebClient(token);
 
     try {
-      const opt = this.injectGrowiUri(req, relation.growiUri);
+      this.injectGrowiUri(req, relation.growiUri);
+
+      const opt = req.body;
+      opt.headers = req.headers;
 
       await client.apiCall(method, opt);
     }

+ 28 - 0
packages/slackbot-proxy/src/interfaces/growi-uri-injector.ts

@@ -0,0 +1,28 @@
+export type GrowiUriWithOriginalData = {
+  growiUri: string,
+  originalData: string,
+}
+
+export type TypedBlock = {
+  type: string,
+}
+
+/**
+ * Type guard for GrowiUriWithOriginalData
+ * @param data
+ * @returns
+ */
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+export const isGrowiUriWithOriginalData = (data: any): data is GrowiUriWithOriginalData => {
+  return data.growiUri != null && data.originalData != null;
+};
+
+export interface GrowiUriInjector<ISDATA, IDATA, ESDATA, EDATA> {
+
+  shouldHandleToInject(data: ISDATA & any): data is IDATA;
+  inject(data: IDATA, growiUri:string): void;
+
+  shouldHandleToExtract(data: ESDATA & any): data is EDATA;
+  extract(data: EDATA): GrowiUriWithOriginalData;
+
+}

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

@@ -1,43 +1,40 @@
 import {
-  IMiddleware, Middleware, Next, Req, Res,
+  IMiddleware, Inject, 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';
+import { ViewInteractionPayloadDelegator } from '~/services/growi-uri-injector/ViewInteractionPayloadDelegator';
+import { ActionsBlockPayloadDelegator } from '~/services/growi-uri-injector/ActionsBlockPayloadDelegator';
+
 
 @Middleware()
 export class ExtractGrowiUriFromReq implements IMiddleware {
 
-  use(@Req() req: Req & SlackOauthReq, @Res() res: Res, @Next() next: Next): void {
+  @Inject()
+  viewInteractionPayloadDelegator: ViewInteractionPayloadDelegator;
+
+  @Inject()
+  actionsBlockPayloadDelegator: ActionsBlockPayloadDelegator;
+
+  use(@Req() req: SlackOauthReq, @Res() res: Res, @Next() next: Next): void {
 
     // There is no payload in the request from slack
     if (req.body.payload == null) {
       return next();
     }
 
-    const payload = JSON.parse(req.body.payload);
+    const parsedPayload = 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;
+    if (this.viewInteractionPayloadDelegator.shouldHandleToExtract(parsedPayload)) {
+      const data = this.viewInteractionPayloadDelegator.extract(parsedPayload);
+      req.growiUri = data.growiUri;
     }
-    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;
-        }
-      }
+    else if (this.actionsBlockPayloadDelegator.shouldHandleToExtract(parsedPayload)) {
+      const data = this.actionsBlockPayloadDelegator.extract(parsedPayload);
+      req.growiUri = data.growiUri;
     }
 
-    req.body.payload = JSON.stringify(payload);
+    req.body.payload = JSON.stringify(parsedPayload);
 
     return next();
   }

+ 84 - 0
packages/slackbot-proxy/src/services/growi-uri-injector/ActionsBlockPayloadDelegator.ts

@@ -0,0 +1,84 @@
+import { Inject, OnInit, Service } from '@tsed/di';
+import {
+  GrowiUriInjector, GrowiUriWithOriginalData, TypedBlock,
+} from '~/interfaces/growi-uri-injector';
+import { ButtonActionPayloadDelegator } from './block-elements/ButtonActionPayloadDelegator';
+import { CheckboxesActionPayloadDelegator } from './block-elements/CheckboxesActionPayloadDelegator';
+
+
+// see: https://api.slack.com/reference/block-kit/blocks
+type BlockElement = TypedBlock & {
+  elements: (TypedBlock & any)[],
+}
+
+// see: https://api.slack.com/reference/interaction-payloads/block-actions
+type BlockActionsPayload = TypedBlock & {
+  actions: TypedBlock[],
+}
+
+@Service()
+export class ActionsBlockPayloadDelegator implements GrowiUriInjector<any, BlockElement[], any, BlockActionsPayload>, OnInit {
+
+  @Inject()
+  buttonActionPayloadDelegator: ButtonActionPayloadDelegator;
+
+  @Inject()
+  checkboxesActionPayloadDelegator: CheckboxesActionPayloadDelegator;
+
+  private childDelegators: GrowiUriInjector<TypedBlock[], any, TypedBlock, any>[] = [];
+
+  $onInit(): void | Promise<any> {
+    this.childDelegators.push(
+      this.buttonActionPayloadDelegator,
+      this.checkboxesActionPayloadDelegator,
+    );
+  }
+
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  shouldHandleToInject(data: any): data is BlockElement[] {
+    const actionsBlocks = data.filter(blockElement => blockElement.type === 'actions');
+    return actionsBlocks.length > 0;
+  }
+
+  inject(data: BlockElement[], growiUri: string): void {
+    const actionsBlocks = data.filter(blockElement => blockElement.type === 'actions');
+
+    // collect elements
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    const elements = actionsBlocks.flatMap(actionBlock => actionBlock.elements!);
+
+    this.childDelegators.forEach((delegator) => {
+      if (delegator.shouldHandleToInject(elements)) {
+        delegator.inject(elements, growiUri);
+      }
+    });
+  }
+
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  shouldHandleToExtract(data: any): data is BlockActionsPayload {
+    if (data.actions == null || data.actions.length === 0) {
+      return false;
+    }
+
+    const action = data.actions[0];
+    return this.childDelegators
+      .map(delegator => delegator.shouldHandleToExtract(action))
+      .includes(true);
+  }
+
+  extract(data: BlockActionsPayload): GrowiUriWithOriginalData {
+    let growiUriWithOriginalData: GrowiUriWithOriginalData;
+
+    const action = data.actions[0];
+    for (const delegator of this.childDelegators) {
+      if (delegator.shouldHandleToExtract(action)) {
+        growiUriWithOriginalData = delegator.extract(action);
+        break;
+      }
+    }
+
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    return growiUriWithOriginalData!;
+  }
+
+}

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

@@ -1,19 +0,0 @@
-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;
-  }
-
-}

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

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

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

@@ -1,18 +0,0 @@
-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]();
-};

+ 62 - 0
packages/slackbot-proxy/src/services/growi-uri-injector/ViewInteractionPayloadDelegator.ts

@@ -0,0 +1,62 @@
+import { Service } from '@tsed/di';
+import {
+  GrowiUriInjector, GrowiUriWithOriginalData, isGrowiUriWithOriginalData, TypedBlock,
+} from '~/interfaces/growi-uri-injector';
+
+// see: https://api.slack.com/reference/interaction-payloads/views
+type ViewElement = TypedBlock & {
+  'private_metadata'?: any,
+}
+
+// see: https://api.slack.com/reference/interaction-payloads/views
+type ViewInteractionPayload = TypedBlock & {
+  view: {
+    'private_metadata'?: any,
+  },
+}
+
+@Service()
+export class ViewInteractionPayloadDelegator implements GrowiUriInjector<any, ViewElement, any, ViewInteractionPayload> {
+
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  shouldHandleToInject(data: any): data is ViewElement {
+    return data.type != null && data.private_metadata != null;
+  }
+
+  inject(data: ViewElement, growiUri :string): void {
+    const originalData = data.private_metadata;
+
+    const urlWithOrgData: GrowiUriWithOriginalData = { growiUri, originalData };
+
+    data.private_metadata = JSON.stringify(urlWithOrgData);
+  }
+
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  shouldHandleToExtract(data: any): data is ViewInteractionPayload {
+    const { type, view } = data;
+    if (type !== 'view_submission') {
+      return false;
+    }
+    if (view.private_metadata == null) {
+      return false;
+    }
+
+    try {
+      const restoredData: any = JSON.parse(view.private_metadata);
+      return isGrowiUriWithOriginalData(restoredData);
+    }
+    // when parsing failed
+    catch (err) {
+      return false;
+    }
+  }
+
+  extract(data: ViewInteractionPayload): GrowiUriWithOriginalData {
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    const restoredData: GrowiUriWithOriginalData = JSON.parse(data.view.private_metadata!); // private_metadata must not be null at this moment
+    data.view.private_metadata = restoredData.originalData;
+
+    return restoredData;
+  }
+
+}

+ 43 - 0
packages/slackbot-proxy/src/services/growi-uri-injector/block-elements/ButtonActionPayloadDelegator.ts

@@ -0,0 +1,43 @@
+import { Service } from '@tsed/di';
+import { GrowiUriWithOriginalData, GrowiUriInjector, TypedBlock } from '~/interfaces/growi-uri-injector';
+
+
+type ButtonElement = TypedBlock & {
+  value: string,
+}
+
+type ButtonActionPayload = TypedBlock & {
+  value: string,
+}
+
+@Service()
+export class ButtonActionPayloadDelegator implements GrowiUriInjector<TypedBlock[], ButtonElement[], TypedBlock, ButtonActionPayload> {
+
+  shouldHandleToInject(elements: TypedBlock[]): elements is ButtonElement[] {
+    const buttonElements = elements.filter(element => element.type === 'button');
+    return buttonElements.length > 0;
+  }
+
+  inject(elements: ButtonElement[], growiUri: string): void {
+    const buttonElements = elements.filter(blockElement => blockElement.type === 'button');
+
+    buttonElements
+      .forEach((element) => {
+        const urlWithOrgData: GrowiUriWithOriginalData = { growiUri, originalData: element.value };
+        element.value = JSON.stringify(urlWithOrgData);
+      });
+  }
+
+  shouldHandleToExtract(action: TypedBlock): action is ButtonActionPayload {
+    return action.type === 'button';
+  }
+
+  extract(action: ButtonActionPayload): GrowiUriWithOriginalData {
+    const restoredData: GrowiUriWithOriginalData = JSON.parse(action.value);
+    action.value = restoredData.originalData;
+
+    return restoredData;
+  }
+
+
+}

+ 57 - 0
packages/slackbot-proxy/src/services/growi-uri-injector/block-elements/CheckboxesActionPayloadDelegator.ts

@@ -0,0 +1,57 @@
+import { Service } from '@tsed/di';
+import { GrowiUriWithOriginalData, GrowiUriInjector, TypedBlock } from '~/interfaces/growi-uri-injector';
+
+
+type CheckboxesElement = TypedBlock & {
+  options: { value: string }[],
+}
+
+type CheckboxesActionPayload = TypedBlock & {
+  'selected_options': { value: string }[],
+}
+
+@Service()
+export class CheckboxesActionPayloadDelegator implements GrowiUriInjector<TypedBlock[], CheckboxesElement[], TypedBlock, CheckboxesActionPayload> {
+
+  shouldHandleToInject(elements: TypedBlock[]): elements is CheckboxesElement[] {
+    const buttonElements = elements.filter(element => element.type === 'checkboxes');
+    return buttonElements.length > 0;
+  }
+
+  inject(elements: CheckboxesElement[], growiUri: string): void {
+    const cbElements = elements.filter(blockElement => blockElement.type === 'checkboxes');
+
+    cbElements.forEach((element) => {
+      element.options.forEach((option) => {
+        const urlWithOrgData: GrowiUriWithOriginalData = { growiUri, originalData: option.value };
+        option.value = JSON.stringify(urlWithOrgData);
+      });
+    });
+  }
+
+  shouldHandleToExtract(action: TypedBlock): action is CheckboxesActionPayload {
+    return (
+      action.type === 'checkboxes'
+      && (action as CheckboxesActionPayload).selected_options != null
+      // ...Unsolved problem...
+      // slackbot-proxy can't determine growiUri when selected_options is empty -- 2021.07.12 Yuki Takei
+      && (action as CheckboxesActionPayload).selected_options.length > 0
+    );
+  }
+
+  extract(action: CheckboxesActionPayload): GrowiUriWithOriginalData {
+    let oneOfRestoredData: GrowiUriWithOriginalData;
+
+    action.selected_options.forEach((selectedOption) => {
+      const restoredData = JSON.parse(selectedOption.value);
+      selectedOption.value = restoredData.originalData;
+
+      // update oneOfRestoredData
+      oneOfRestoredData = restoredData;
+    });
+
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    return oneOfRestoredData!;
+  }
+
+}

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

@@ -1,10 +0,0 @@
-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;
-};

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

@@ -1,7 +0,0 @@
-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);
-};