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

Merge pull request #3721 from weseek/feat/get-slack-connection-statuses

Feat/get slack connection statuses
Yuki Takei 4 лет назад
Родитель
Сommit
51f40ecf59

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

@@ -8,8 +8,10 @@ export const supportedGrowiCommands: string[] = [
 ];
 
 export * from './interfaces/growi-command';
+export * from './interfaces/request-between-growi-and-proxy';
 export * from './interfaces/request-from-slack';
 export * from './models/errors';
+export * from './middlewares/verify-growi-to-slack-request';
 export * from './middlewares/verify-slack-request';
 export * from './utils/block-creater';
 export * from './utils/check-communicable';

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

@@ -0,0 +1,17 @@
+import { Request } from 'express';
+
+export type RequestFromGrowi = Request & {
+  // appended by GROWI
+  headers:{'x-growi-gtop-tokens'?:string},
+
+  // will be extracted from header
+  tokenGtoPs: string[],
+};
+
+export type RequestFromProxy = Request & {
+  // appended by Proxy
+  headers:{'x-growi-ptog-token'?:string},
+
+  // will be extracted from header
+  tokenPtoG: string[],
+};

+ 30 - 0
packages/slack/src/middlewares/verify-growi-to-slack-request.ts

@@ -0,0 +1,30 @@
+import { Response, NextFunction } from 'express';
+
+import loggerFactory from '../utils/logger';
+import { RequestFromGrowi } from '../interfaces/request-between-growi-and-proxy';
+
+const logger = loggerFactory('@growi/slack:middlewares:verify-slack-request');
+
+/**
+ * Verify if the request came from slack
+ * See: https://api.slack.com/authentication/verifying-requests-from-slack
+ */
+export const verifyGrowiToSlackRequest = (req: RequestFromGrowi, res: Response, next: NextFunction): Record<string, any> | void => {
+  const str = req.headers['x-growi-gtop-tokens'];
+
+  if (str == null) {
+    const message = 'The value of header \'x-growi-gtop-tokens\' must not be empty.';
+    logger.warn(message, { body: req.body });
+    return res.status(400).send({ message });
+  }
+
+  const tokens = str.split(',').map(value => value.trim());
+  if (tokens.length === 0) {
+    const message = 'The value of header \'x-growi-gtop-tokens\' must include at least one or more tokens.';
+    logger.warn(message, { body: req.body });
+    return res.status(400).send({ message });
+  }
+
+  req.tokenGtoPs = tokens;
+  return next();
+};

+ 5 - 2
packages/slack/src/utils/check-communicable.ts

@@ -60,8 +60,8 @@ const retrieveWorkspaceName = async(client: WebClient): Promise<string> => {
  * @param tokens Array of bot OAuth token
  * @returns
  */
-export const getConnectionStatuses = async(tokens: string[]): Promise<Map<string, ConnectionStatus>> => {
-  return tokens
+export const getConnectionStatuses = async(tokens: string[]): Promise<{[key: string]: ConnectionStatus}> => {
+  const map = tokens
     .reduce<Promise<Map<string, ConnectionStatus>>>(
       async(acc, token) => {
         const client = generateWebClient(token);
@@ -85,4 +85,7 @@ export const getConnectionStatuses = async(tokens: string[]): Promise<Map<string
       // define initial accumulator
       Promise.resolve(new Map<string, ConnectionStatus>()),
     );
+
+  // convert to object
+  return Object.fromEntries(await map);
 };

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

@@ -0,0 +1,55 @@
+import {
+  Controller, Get, Inject, Req, Res, UseBefore,
+} from '@tsed/common';
+
+import { WebAPICallResult } from '@slack/web-api';
+
+import { verifyGrowiToSlackRequest, getConnectionStatuses } from '@growi/slack';
+
+import { GrowiReq } from '~/interfaces/growi-to-slack/growi-req';
+import { InstallationRepository } from '~/repositories/installation';
+import { RelationRepository } from '~/repositories/relation';
+import { InstallerService } from '~/services/InstallerService';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('slackbot-proxy:controllers:growi-to-slack');
+
+
+@Controller('/g2s')
+export class GrowiToSlackCtrl {
+
+  @Inject()
+  installerService: InstallerService;
+
+  @Inject()
+  installationRepository: InstallationRepository;
+
+  @Inject()
+  relationRepository: RelationRepository;
+
+  @Get('/connection-status')
+  @UseBefore(verifyGrowiToSlackRequest)
+  async getConnectionStatuses(@Req() req: GrowiReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
+    // asserted (tokenGtoPs.length > 0) by verifyGrowiToSlackRequest
+    const { tokenGtoPs } = req;
+
+    // retrieve Relation with Installation
+    const relations = await this.relationRepository.createQueryBuilder('relation')
+      .where('relation.tokenGtoP IN (:...tokens)', { tokens: tokenGtoPs })
+      .leftJoinAndSelect('relation.installation', 'installation')
+      .getMany();
+
+    logger.debug(`${relations.length} relations found`, relations);
+
+    // extract bot token
+    const tokens: string[] = relations
+      .map(relation => relation.installation?.data?.bot?.token)
+      .filter((v): v is string => v != null); // filter out null values
+
+    const connectionStatuses = await getConnectionStatuses(tokens);
+
+    return res.send({ connectionStatuses });
+  }
+
+}

+ 7 - 7
packages/slackbot-proxy/src/controllers/slack.ts

@@ -11,12 +11,12 @@ import {
 } from '@growi/slack';
 
 import { Relation } from '~/entities/relation';
-import { AuthedReq } from '~/interfaces/authorized-req';
+import { SlackOauthReq } from '~/interfaces/slack-to-growi/slack-oauth-req';
 import { InstallationRepository } from '~/repositories/installation';
 import { RelationRepository } from '~/repositories/relation';
 import { OrderRepository } from '~/repositories/order';
-import { AddSigningSecretToReq } from '~/middlewares/add-signing-secret-to-req';
-import { AuthorizeCommandMiddleware, AuthorizeInteractionMiddleware } from '~/middlewares/authorizer';
+import { AddSigningSecretToReq } from '~/middlewares/slack-to-growi/add-signing-secret-to-req';
+import { AuthorizeCommandMiddleware, AuthorizeInteractionMiddleware } from '~/middlewares/slack-to-growi/authorizer';
 import { InstallerService } from '~/services/InstallerService';
 import { RegisterService } from '~/services/RegisterService';
 import loggerFactory from '~/utils/logger';
@@ -65,7 +65,7 @@ export class SlackCtrl {
 
   @Post('/commands')
   @UseBefore(AddSigningSecretToReq, verifySlackRequest, AuthorizeCommandMiddleware)
-  async handleCommand(@Req() req: AuthedReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
+  async handleCommand(@Req() req: SlackOauthReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
     const { body, authorizeResult } = req;
 
     if (body.text == null) {
@@ -89,7 +89,7 @@ export class SlackCtrl {
     const installationId = authorizeResult.enterpriseId || authorizeResult.teamId;
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     const installation = await this.installationRepository.findByTeamIdOrEnterpriseId(installationId!);
-    const relations = await this.relationRepository.find({ installation: installation?.id });
+    const relations = await this.relationRepository.find({ installation });
 
     if (relations.length === 0) {
       return res.json({
@@ -130,7 +130,7 @@ export class SlackCtrl {
 
   @Post('/interactions')
   @UseBefore(AuthorizeInteractionMiddleware)
-  async handleInteraction(@Req() req: AuthedReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
+  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);
 
@@ -163,7 +163,7 @@ export class SlackCtrl {
     /*
      * forward to GROWI server
      */
-    const relations = await this.relationRepository.find({ installation: installation?.id });
+    const relations = await this.relationRepository.find({ installation });
 
     const promises = relations.map((relation: Relation) => {
       // generate API URL

+ 1 - 1
packages/slackbot-proxy/src/entities/relation.ts

@@ -17,7 +17,7 @@ export class Relation {
   readonly updatedAt: Date;
 
   @ManyToOne(() => Installation)
-  readonly installation: number;
+  readonly installation: Installation;
 
   @Column()
   @Index({ unique: true })

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

@@ -0,0 +1,4 @@
+import { Req } from '@tsed/common';
+import { RequestFromGrowi } from '@growi/slack';
+
+export type GrowiReq = Req & RequestFromGrowi;

+ 0 - 0
packages/slackbot-proxy/src/interfaces/growi-command-processor.ts → packages/slackbot-proxy/src/interfaces/slack-to-growi/growi-command-processor.ts


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

@@ -1,6 +1,6 @@
 import { AuthorizeResult } from '@slack/oauth';
 import { Req } from '@tsed/common';
 
-export type AuthedReq = Req & {
+export type SlackOauthReq = Req & {
   authorizeResult: AuthorizeResult,
 };

+ 0 - 0
packages/slackbot-proxy/src/middlewares/add-signing-secret-to-req.ts → packages/slackbot-proxy/src/middlewares/slack-to-growi/add-signing-secret-to-req.ts


+ 3 - 3
packages/slackbot-proxy/src/middlewares/authorizer.ts → packages/slackbot-proxy/src/middlewares/slack-to-growi/authorizer.ts

@@ -5,7 +5,7 @@ import {
 
 import Logger from 'bunyan';
 
-import { AuthedReq } from '~/interfaces/authorized-req';
+import { SlackOauthReq } from '~/interfaces/slack-to-growi/slack-oauth-req';
 import { InstallationRepository } from '~/repositories/installation';
 import { InstallerService } from '~/services/InstallerService';
 import loggerFactory from '~/utils/logger';
@@ -25,7 +25,7 @@ export class AuthorizeCommandMiddleware implements IMiddleware {
     this.logger = loggerFactory('slackbot-proxy:middlewares:AuthorizeCommandMiddleware');
   }
 
-  async use(@Req() req: AuthedReq, @Res() res: Res): Promise<void> {
+  async use(@Req() req: SlackOauthReq, @Res() res: Res): Promise<void> {
     const { body } = req;
 
     // extract id from body
@@ -83,7 +83,7 @@ export class AuthorizeInteractionMiddleware implements IMiddleware {
     this.logger = loggerFactory('slackbot-proxy:middlewares:AuthorizeInteractionMiddleware');
   }
 
-  async use(@Req() req: AuthedReq, @Res() res: Res): Promise<void> {
+  async use(@Req() req: SlackOauthReq, @Res() res: Res): Promise<void> {
     const { body } = req;
 
     if (body.payload == null) {

+ 1 - 1
packages/slackbot-proxy/src/services/RegisterService.ts

@@ -2,7 +2,7 @@ import { Service } from '@tsed/di';
 import { WebClient, LogLevel } from '@slack/web-api';
 import { generateInputSectionBlock, GrowiCommand, generateMarkdownSectionBlock } from '@growi/slack';
 import { AuthorizeResult } from '@slack/oauth';
-import { GrowiCommandProcessor } from '~/interfaces/growi-command-processor';
+import { GrowiCommandProcessor } from '~/interfaces/slack-to-growi/growi-command-processor';
 import { OrderRepository } from '~/repositories/order';
 import { Installation } from '~/entities/installation';
 

+ 26 - 6
src/server/routes/apiv3/slack-integration-settings.js

@@ -1,13 +1,16 @@
+const express = require('express');
+const { body } = require('express-validator');
+const axios = require('axios');
+const crypto = require('crypto');
+
 const loggerFactory = require('@alias/logger');
 
 const { getConnectionStatuses } = require('@growi/slack');
 
-const logger = loggerFactory('growi:routes:apiv3:notification-setting');
-const express = require('express');
-const { body } = require('express-validator');
-const crypto = require('crypto');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
+const logger = loggerFactory('growi:routes:apiv3:notification-setting');
+
 const router = express.Router();
 
 /**
@@ -77,6 +80,19 @@ module.exports = (crowi) => {
     return hasher.digest('base64');
   }
 
+  async function getConnectionStatusesFromProxy(tokens) {
+    const csv = tokens.join(',');
+
+    // TODO: retrieve proxy url from configManager
+    const result = await axios.get('http://localhost:8080/g2s/connection-status', {
+      headers: {
+        'x-growi-gtop-tokens': csv,
+      },
+    });
+
+    return result.data;
+  }
+
   /**
    * @swagger
    *
@@ -109,17 +125,21 @@ module.exports = (crowi) => {
       // settings.tokenGtoP = ;
     }
 
+    // TODO: try-catch
+
     // retrieve connection statuses
     let connectionStatuses;
     if (currentBotType === 'customBotWithoutProxy') {
       const token = settings.slackBotToken;
       // check the token is not null
       if (token != null) {
-        connectionStatuses = Object.fromEntries(await getConnectionStatuses([token]));
+        connectionStatuses = await getConnectionStatuses([token]);
       }
     }
     else {
-      // connectionStatuses = getConnectionStatusesFromProxy();
+      // TODO: retrieve tokenGtoPs from DB
+      const tokenGtoPs = ['gtop1'];
+      connectionStatuses = (await getConnectionStatusesFromProxy(tokenGtoPs)).connectionStatuses;
     }
 
     return res.apiv3({ currentBotType, settings, connectionStatuses });