growi-to-slack.ts 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. import {
  2. Controller, Get, Post, Inject, Req, Res, UseBefore, PathParams,
  3. } from '@tsed/common';
  4. import axios from 'axios';
  5. import { WebAPICallOptions, WebAPICallResult } from '@slack/web-api';
  6. import {
  7. verifyGrowiToSlackRequest, getConnectionStatuses, getConnectionStatus, generateWebClient,
  8. } from '@growi/slack';
  9. import { WebclientRes, AddWebclientResponseToRes } from '~/middlewares/slack-to-growi/add-webclient-response-to-res';
  10. import { GrowiReq } from '~/interfaces/growi-to-slack/growi-req';
  11. import { InstallationRepository } from '~/repositories/installation';
  12. import { RelationRepository } from '~/repositories/relation';
  13. import { OrderRepository } from '~/repositories/order';
  14. import { InstallerService } from '~/services/InstallerService';
  15. import loggerFactory from '~/utils/logger';
  16. import { findInjectorByType } from '~/services/growi-uri-injector/GrowiUriInjectorFactory';
  17. import { injectGrowiUriToView } from '~/utils/injectGrowiUriToView';
  18. const logger = loggerFactory('slackbot-proxy:controllers:growi-to-slack');
  19. // temporarily save for selection to growi
  20. const temporarySinglePostCommands = ['create'];
  21. @Controller('/g2s')
  22. export class GrowiToSlackCtrl {
  23. @Inject()
  24. installerService: InstallerService;
  25. @Inject()
  26. installationRepository: InstallationRepository;
  27. @Inject()
  28. relationRepository: RelationRepository;
  29. @Inject()
  30. orderRepository: OrderRepository;
  31. async requestToGrowi(growiUrl:string, tokenPtoG:string):Promise<void> {
  32. const url = new URL('/_api/v3/slack-integration/proxied/commands', growiUrl);
  33. await axios.post(url.toString(), {
  34. type: 'url_verification',
  35. challenge: 'this_is_my_challenge_token',
  36. },
  37. {
  38. headers: {
  39. 'x-growi-ptog-tokens': tokenPtoG,
  40. },
  41. });
  42. }
  43. @Get('/connection-status')
  44. @UseBefore(verifyGrowiToSlackRequest)
  45. async getConnectionStatuses(@Req() req: GrowiReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
  46. // asserted (tokenGtoPs.length > 0) by verifyGrowiToSlackRequest
  47. const { tokenGtoPs } = req;
  48. // retrieve Relation with Installation
  49. const relations = await this.relationRepository.createQueryBuilder('relation')
  50. .where('relation.tokenGtoP IN (:...tokens)', { tokens: tokenGtoPs })
  51. .leftJoinAndSelect('relation.installation', 'installation')
  52. .getMany();
  53. logger.debug(`${relations.length} relations found`, relations);
  54. // key: tokenGtoP, value: botToken
  55. const botTokenResolverMapping: {[tokenGtoP:string]:string} = {};
  56. relations.forEach((relation) => {
  57. const botToken = relation.installation?.data?.bot?.token;
  58. if (botToken != null) {
  59. botTokenResolverMapping[relation.tokenGtoP] = botToken;
  60. }
  61. });
  62. const connectionStatuses = await getConnectionStatuses(Object.keys(botTokenResolverMapping), (tokenGtoP:string) => botTokenResolverMapping[tokenGtoP]);
  63. return res.send({ connectionStatuses });
  64. }
  65. @Get('/relation-test')
  66. @UseBefore(verifyGrowiToSlackRequest)
  67. async postRelation(@Req() req: GrowiReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
  68. const { tokenGtoPs } = req;
  69. if (tokenGtoPs.length !== 1) {
  70. return res.status(400).send({ message: 'installation is invalid' });
  71. }
  72. const tokenGtoP = tokenGtoPs[0];
  73. // retrieve relation with Installation
  74. const relation = await this.relationRepository.createQueryBuilder('relation')
  75. .where('tokenGtoP = :token', { token: tokenGtoP })
  76. .leftJoinAndSelect('relation.installation', 'installation')
  77. .getOne();
  78. // Returns the result of the test if it already exists
  79. if (relation != null) {
  80. logger.debug('relation found', relation);
  81. const token = relation.installation.data.bot?.token;
  82. if (token == null) {
  83. return res.status(400).send({ message: 'installation is invalid' });
  84. }
  85. try {
  86. await this.requestToGrowi(relation.growiUri, relation.tokenPtoG);
  87. }
  88. catch (err) {
  89. logger.error(err);
  90. return res.status(400).send({ message: `failed to request to GROWI. err: ${err.message}` });
  91. }
  92. const status = await getConnectionStatus(token);
  93. if (status.error != null) {
  94. return res.status(400).send({ message: `failed to get connection. err: ${status.error}` });
  95. }
  96. return res.send({ relation, slackBotToken: token });
  97. }
  98. // retrieve latest Order with Installation
  99. const order = await this.orderRepository.createQueryBuilder('order')
  100. .orderBy('order.createdAt', 'DESC')
  101. .where('tokenGtoP = :token', { token: tokenGtoP })
  102. .leftJoinAndSelect('order.installation', 'installation')
  103. .getOne();
  104. if (order == null || order.isExpired()) {
  105. return res.status(400).send({ message: 'order has expired or does not exist.' });
  106. }
  107. // Access the GROWI URL saved in the Order record and check if the GtoP token is valid.
  108. try {
  109. await this.requestToGrowi(order.growiUrl, order.tokenPtoG);
  110. }
  111. catch (err) {
  112. logger.error(err);
  113. return res.status(400).send({ message: `failed to request to GROWI. err: ${err.message}` });
  114. }
  115. logger.debug('order found', order);
  116. const token = order.installation.data.bot?.token;
  117. if (token == null) {
  118. return res.status(400).send({ message: 'installation is invalid' });
  119. }
  120. const status = await getConnectionStatus(token);
  121. if (status.error != null) {
  122. return res.status(400).send({ message: `failed to get connection. err: ${status.error}` });
  123. }
  124. logger.debug('relation test is success', order);
  125. // Transaction is not considered because it is used infrequently,
  126. const createdRelation = await this.relationRepository.save({
  127. installation: order.installation,
  128. tokenGtoP: order.tokenGtoP,
  129. tokenPtoG: order.tokenPtoG,
  130. growiUri: order.growiUrl,
  131. siglePostCommands: temporarySinglePostCommands,
  132. });
  133. return res.send({ relation: createdRelation, slackBotToken: token });
  134. }
  135. injectGrowiUri(req:GrowiReq, growiUri:string):WebAPICallOptions {
  136. if (req.body.view != null) {
  137. injectGrowiUriToView(req.body, growiUri);
  138. }
  139. if (req.body.blocks != null) {
  140. const parsedBlocks = JSON.parse(req.body.blocks as string);
  141. parsedBlocks.forEach((parsedBlock) => {
  142. if (parsedBlock.type !== 'actions') {
  143. return;
  144. }
  145. parsedBlock.elements.forEach((element) => {
  146. const growiUriInjector = findInjectorByType(element.type);
  147. if (growiUriInjector != null) {
  148. growiUriInjector.inject(element, growiUri);
  149. }
  150. });
  151. return;
  152. });
  153. req.body.blocks = JSON.stringify(parsedBlocks);
  154. }
  155. const opt = req.body;
  156. opt.headers = req.headers;
  157. return opt;
  158. }
  159. @Post('/:method')
  160. @UseBefore(AddWebclientResponseToRes, verifyGrowiToSlackRequest)
  161. async postResult(
  162. @PathParams('method') method: string, @Req() req: GrowiReq, @Res() res: WebclientRes,
  163. ): Promise<void|string|Res|WebAPICallResult> {
  164. const { tokenGtoPs } = req;
  165. if (tokenGtoPs.length !== 1) {
  166. return res.webClientErr('tokenGtoPs is invalid', 'invalid_tokenGtoP');
  167. }
  168. const tokenGtoP = tokenGtoPs[0];
  169. // retrieve relation with Installation
  170. const relation = await this.relationRepository.createQueryBuilder('relation')
  171. .where('tokenGtoP = :token', { token: tokenGtoP })
  172. .leftJoinAndSelect('relation.installation', 'installation')
  173. .getOne();
  174. if (relation == null) {
  175. return res.webClientErr('relation is invalid', 'invalid_relation');
  176. }
  177. const token = relation.installation.data.bot?.token;
  178. if (token == null) {
  179. return res.webClientErr('installation is invalid', 'invalid_installation');
  180. }
  181. const client = generateWebClient(token);
  182. try {
  183. const opt = this.injectGrowiUri(req, relation.growiUri);
  184. await client.apiCall(method, opt);
  185. }
  186. catch (err) {
  187. logger.error(err);
  188. return res.webClientErr(`failed to send to slack. err: ${err.message}`, 'fail_api_call');
  189. }
  190. logger.debug('send to slack is success');
  191. // required to return ok for apiCall
  192. return res.webClient();
  193. }
  194. }