فهرست منبع

make delegators typesafe

Yuki Takei 4 سال پیش
والد
کامیت
0c5160bf16

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

@@ -3,7 +3,7 @@ import {
 } from '@tsed/common';
 } from '@tsed/common';
 import axios from 'axios';
 import axios from 'axios';
 
 
-import { WebAPICallOptions, WebAPICallResult } from '@slack/web-api';
+import { WebAPICallResult } from '@slack/web-api';
 
 
 import {
 import {
   verifyGrowiToSlackRequest, getConnectionStatuses, getConnectionStatus, generateWebClient,
   verifyGrowiToSlackRequest, getConnectionStatuses, getConnectionStatus, generateWebClient,
@@ -20,7 +20,6 @@ import { InstallerService } from '~/services/InstallerService';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { ViewInteractionPayloadDelegator } from '~/services/growi-uri-injector/ViewInteractionPayloadDelegator';
 import { ViewInteractionPayloadDelegator } from '~/services/growi-uri-injector/ViewInteractionPayloadDelegator';
 import { ActionsBlockPayloadDelegator } from '~/services/growi-uri-injector/ActionsBlockPayloadDelegator';
 import { ActionsBlockPayloadDelegator } from '~/services/growi-uri-injector/ActionsBlockPayloadDelegator';
-import { BlockElement, ViewElement } from '~/interfaces/growi-uri-injector';
 
 
 
 
 const logger = loggerFactory('slackbot-proxy:controllers:growi-to-slack');
 const logger = loggerFactory('slackbot-proxy:controllers:growi-to-slack');
@@ -178,26 +177,27 @@ export class GrowiToSlackCtrl {
     return res.send({ relation: createdRelation, slackBotToken: token });
     return res.send({ relation: createdRelation, slackBotToken: token });
   }
   }
 
 
-  injectGrowiUri(req: GrowiReq, growiUri: string):WebAPICallOptions {
-
-    // delegate to ViewInteractionPayloadDelegator
-    if (this.viewInteractionPayloadDelegator.shouldHandleToInject(req)) {
-      const parsedView = JSON.parse(req.body.view) as ViewElement;
-      this.viewInteractionPayloadDelegator.inject(parsedView, growiUri);
-      req.body.view = JSON.stringify(parsedView);
+  injectGrowiUri(req: GrowiReq, growiUri: string): void {
+    if (req.body.view == null && req.body.blocks == null) {
+      return;
     }
     }
 
 
-    // delegate to ActionsBlockPayloadDelegator
-    if (this.actionsBlockPayloadDelegator.shouldHandleToInject(req)) {
-      const parsedBlocks = JSON.parse(req.body.blocks) as BlockElement[];
-      this.actionsBlockPayloadDelegator.inject(parsedBlocks, growiUri);
-      req.body.blocks = JSON.stringify(parsedBlocks);
+    if (req.body.view != null) {
+      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);
+      }
+    }
+    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')
   @Post('/:method')
@@ -231,7 +231,10 @@ export class GrowiToSlackCtrl {
     const client = generateWebClient(token);
     const client = generateWebClient(token);
 
 
     try {
     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);
       await client.apiCall(method, opt);
     }
     }

+ 9 - 31
packages/slackbot-proxy/src/interfaces/growi-uri-injector.ts

@@ -1,34 +1,12 @@
-// see: https://api.slack.com/reference/interaction-payloads/views
-export type ViewElement = {
-  type: string,
-  'private_metadata'?: any,
-}
-
-// see: https://api.slack.com/reference/interaction-payloads/views
-export type ViewInteractionPayload = {
-  type: string,
-  view: {
-    'private_metadata'?: any,
-  },
-}
-
-// see: https://api.slack.com/reference/block-kit/blocks
-export type BlockElement = {
-  type: string,
-  element?: { type: string } & any,
-  elements?: ({ type: string } & any)[],
-}
-
-// see: https://api.slack.com/reference/interaction-payloads/block-actions
-export type BlockActionsPayload = {
-  type: string,
-}
-
 export type GrowiUriWithOriginalData = {
 export type GrowiUriWithOriginalData = {
   growiUri: string,
   growiUri: string,
   originalData: string,
   originalData: string,
 }
 }
 
 
+export type TypedBlock = {
+  type: string,
+}
+
 /**
 /**
  * Type guard for GrowiUriWithOriginalData
  * Type guard for GrowiUriWithOriginalData
  * @param data
  * @param data
@@ -39,13 +17,13 @@ export const isGrowiUriWithOriginalData = (data: any): data is GrowiUriWithOrigi
   return data.growiUri != null && data.originalData != null;
   return data.growiUri != null && data.originalData != null;
 };
 };
 
 
-export interface GrowiUriInjector<IDATA extends ViewElement|BlockElement[], EDATA extends (ViewInteractionPayload|BlockActionsPayload)> {
+export interface GrowiUriInjector<ISDATA, IDATA, ESDATA, EDATA> {
 
 
-  shouldHandleToInject(data: unknown): boolean;
-  inject(data: unknown, growiUri:string): void;
+  shouldHandleToInject(data: ISDATA): boolean;
+  inject(data: IDATA, growiUri:string): void;
 
 
-  shouldHandleToExtract(data: EDATA): boolean;
-  extract(data: unknown): GrowiUriWithOriginalData;
+  shouldHandleToExtract(data: ESDATA): boolean;
+  extract(data: EDATA): GrowiUriWithOriginalData;
 
 
 }
 }
 
 

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

@@ -25,15 +25,13 @@ export class ExtractGrowiUriFromReq implements IMiddleware {
 
 
     const parsedPayload = JSON.parse(req.body.payload);
     const parsedPayload = JSON.parse(req.body.payload);
 
 
-    // iterate
-    for (const delegator of [this.viewInteractionPayloadDelegator, this.actionsBlockPayloadDelegator]) {
-      if (delegator.shouldHandleToExtract(parsedPayload)) {
-        const data = delegator.extract(parsedPayload);
-        req.growiUri = data.growiUri;
-
-        // break if growiUri discovered
-        break;
-      }
+    if (this.viewInteractionPayloadDelegator.shouldHandleToExtract(parsedPayload)) {
+      const data = this.viewInteractionPayloadDelegator.extract(parsedPayload);
+      req.growiUri = data.growiUri;
+    }
+    else if (this.actionsBlockPayloadDelegator.shouldHandleToExtract(parsedPayload)) {
+      const data = this.actionsBlockPayloadDelegator.extract(parsedPayload);
+      req.growiUri = data.growiUri;
     }
     }
 
 
     req.body.payload = JSON.stringify(parsedPayload);
     req.body.payload = JSON.stringify(parsedPayload);

+ 20 - 9
packages/slackbot-proxy/src/services/growi-uri-injector/ActionsBlockPayloadDelegator.ts

@@ -1,25 +1,36 @@
 import { Inject, OnInit, Service } from '@tsed/di';
 import { Inject, OnInit, Service } from '@tsed/di';
-import { GrowiReq } from '~/interfaces/growi-to-slack/growi-req';
 import {
 import {
-  BlockActionsPayload, BlockElement, GrowiUriInjector, GrowiUriWithOriginalData,
+  GrowiUriInjector, GrowiUriWithOriginalData, TypedBlock,
 } from '~/interfaces/growi-uri-injector';
 } from '~/interfaces/growi-uri-injector';
 import { ButtonActionPayloadDelegator } from './block-elements/ButtonActionPayloadDelegator';
 import { ButtonActionPayloadDelegator } from './block-elements/ButtonActionPayloadDelegator';
 
 
 
 
+// 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()
 @Service()
-export class ActionsBlockPayloadDelegator implements GrowiUriInjector<BlockElement[], BlockActionsPayload & {actions: any}>, OnInit {
+export class ActionsBlockPayloadDelegator implements GrowiUriInjector<any, BlockElement[], any, BlockActionsPayload>, OnInit {
 
 
   @Inject()
   @Inject()
   buttonActionPayloadDelegator: ButtonActionPayloadDelegator;
   buttonActionPayloadDelegator: ButtonActionPayloadDelegator;
 
 
-  private childDelegators: GrowiUriInjector<any, any>[] = [];
+  private childDelegators: (ButtonActionPayloadDelegator)[] = [];
 
 
   $onInit(): void | Promise<any> {
   $onInit(): void | Promise<any> {
     this.childDelegators.push(this.buttonActionPayloadDelegator);
     this.childDelegators.push(this.buttonActionPayloadDelegator);
   }
   }
 
 
-  shouldHandleToInject(req: GrowiReq): boolean {
-    return req.body.blocks != null;
+  // 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 {
   inject(data: BlockElement[], growiUri: string): void {
@@ -36,8 +47,8 @@ export class ActionsBlockPayloadDelegator implements GrowiUriInjector<BlockEleme
     });
     });
   }
   }
 
 
-  // eslint-disable-next-line @typescript-eslint/no-unused-vars
-  shouldHandleToExtract(data: BlockActionsPayload & {actions?: any}): boolean {
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  shouldHandleToExtract(data: any): data is BlockActionsPayload {
     if (data.actions == null || data.actions.length === 0) {
     if (data.actions == null || data.actions.length === 0) {
       return false;
       return false;
     }
     }
@@ -48,7 +59,7 @@ export class ActionsBlockPayloadDelegator implements GrowiUriInjector<BlockEleme
       .includes(true);
       .includes(true);
   }
   }
 
 
-  extract(data: BlockActionsPayload & {actions: any}): GrowiUriWithOriginalData {
+  extract(data: BlockActionsPayload): GrowiUriWithOriginalData {
     let growiUriWithOriginalData: GrowiUriWithOriginalData;
     let growiUriWithOriginalData: GrowiUriWithOriginalData;
 
 
     const action = data.actions[0];
     const action = data.actions[0];

+ 22 - 6
packages/slackbot-proxy/src/services/growi-uri-injector/ViewInteractionPayloadDelegator.ts

@@ -1,14 +1,26 @@
 import { Service } from '@tsed/di';
 import { Service } from '@tsed/di';
-import { GrowiReq } from '~/interfaces/growi-to-slack/growi-req';
 import {
 import {
-  GrowiUriInjector, GrowiUriWithOriginalData, isGrowiUriWithOriginalData, ViewElement, ViewInteractionPayload,
+  GrowiUriInjector, GrowiUriWithOriginalData, isGrowiUriWithOriginalData, TypedBlock,
 } from '~/interfaces/growi-uri-injector';
 } 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()
 @Service()
-export class ViewInteractionPayloadDelegator implements GrowiUriInjector<ViewElement, ViewInteractionPayload> {
+export class ViewInteractionPayloadDelegator implements GrowiUriInjector<any, ViewElement, any, ViewInteractionPayload> {
 
 
-  shouldHandleToInject(req: GrowiReq): boolean {
-    return req.body.view != null;
+  // 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 {
   inject(data: ViewElement, growiUri :string): void {
@@ -19,11 +31,15 @@ export class ViewInteractionPayloadDelegator implements GrowiUriInjector<ViewEle
     data.private_metadata = JSON.stringify(urlWithOrgData);
     data.private_metadata = JSON.stringify(urlWithOrgData);
   }
   }
 
 
-  shouldHandleToExtract(data: ViewInteractionPayload): boolean {
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  shouldHandleToExtract(data: any): data is ViewInteractionPayload {
     const { type, view } = data;
     const { type, view } = data;
     if (type !== 'view_submission') {
     if (type !== 'view_submission') {
       return false;
       return false;
     }
     }
+    if (view.private_metadata == null) {
+      return false;
+    }
 
 
     try {
     try {
       const restoredData: any = JSON.parse(view.private_metadata);
       const restoredData: any = JSON.parse(view.private_metadata);

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

@@ -1,15 +1,24 @@
 import { Service } from '@tsed/di';
 import { Service } from '@tsed/di';
-import { GrowiUriWithOriginalData, GrowiUriInjector } from '~/interfaces/growi-uri-injector';
+import { GrowiUriWithOriginalData, GrowiUriInjector, TypedBlock } from '~/interfaces/growi-uri-injector';
+
+
+type ButtonElement = TypedBlock & {
+  value: string,
+}
+
+type ButtonActionPayload = TypedBlock & {
+  value: string,
+}
 
 
 @Service()
 @Service()
-export class ButtonActionPayloadDelegator implements GrowiUriInjector<{type: string, value: string}[], {type: string, value: string}> {
+export class ButtonActionPayloadDelegator implements GrowiUriInjector<TypedBlock[], ButtonElement[], TypedBlock, ButtonActionPayload> {
 
 
-  shouldHandleToInject(elements: {type: string}[]): boolean {
+  shouldHandleToInject(elements: TypedBlock[]): elements is ButtonElement[] {
     const buttonElements = elements.filter(element => element.type === 'button');
     const buttonElements = elements.filter(element => element.type === 'button');
     return buttonElements.length > 0;
     return buttonElements.length > 0;
   }
   }
 
 
-  inject(elements: {type: string, value: string}[], growiUri: string): void {
+  inject(elements: ButtonElement[], growiUri: string): void {
     const buttonElements = elements.filter(blockElement => blockElement.type === 'button');
     const buttonElements = elements.filter(blockElement => blockElement.type === 'button');
 
 
     buttonElements
     buttonElements
@@ -19,11 +28,11 @@ export class ButtonActionPayloadDelegator implements GrowiUriInjector<{type: str
       });
       });
   }
   }
 
 
-  shouldHandleToExtract(action: {type: string, value: string}): boolean {
+  shouldHandleToExtract(action: TypedBlock): action is ButtonActionPayload {
     return action.type === 'button';
     return action.type === 'button';
   }
   }
 
 
-  extract(action: {type: string, value: string}): GrowiUriWithOriginalData {
+  extract(action: ButtonActionPayload): GrowiUriWithOriginalData {
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     const restoredData: GrowiUriWithOriginalData = JSON.parse(action.value);
     const restoredData: GrowiUriWithOriginalData = JSON.parse(action.value);
     action.value = restoredData.originalData;
     action.value = restoredData.originalData;