growi-to-slack.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. import {
  2. Controller, Get, Post, Inject, Req, Res, UseBefore, PathParams, Put,
  3. } from '@tsed/common';
  4. import axios from 'axios';
  5. import createError from 'http-errors';
  6. import { addHours } from 'date-fns';
  7. import { ErrorCode, WebAPICallResult } from '@slack/web-api';
  8. import {
  9. verifyGrowiToSlackRequest, getConnectionStatuses, getConnectionStatus, REQUEST_TIMEOUT_FOR_PTOG, generateWebClient,
  10. } from '@growi/slack';
  11. import { WebclientRes, AddWebclientResponseToRes } from '~/middlewares/growi-to-slack/add-webclient-response-to-res';
  12. import { GrowiReq } from '~/interfaces/growi-to-slack/growi-req';
  13. import { InstallationRepository } from '~/repositories/installation';
  14. import { RelationRepository } from '~/repositories/relation';
  15. import { OrderRepository } from '~/repositories/order';
  16. import { InstallerService } from '~/services/InstallerService';
  17. import loggerFactory from '~/utils/logger';
  18. import { ViewInteractionPayloadDelegator } from '~/services/growi-uri-injector/ViewInteractionPayloadDelegator';
  19. import { ActionsBlockPayloadDelegator } from '~/services/growi-uri-injector/ActionsBlockPayloadDelegator';
  20. import { SectionBlockPayloadDelegator } from '~/services/growi-uri-injector/SectionBlockPayloadDelegator';
  21. const logger = loggerFactory('slackbot-proxy:controllers:growi-to-slack');
  22. @Controller('/g2s')
  23. export class GrowiToSlackCtrl {
  24. @Inject()
  25. installerService: InstallerService;
  26. @Inject()
  27. installationRepository: InstallationRepository;
  28. @Inject()
  29. relationRepository: RelationRepository;
  30. @Inject()
  31. orderRepository: OrderRepository;
  32. @Inject()
  33. viewInteractionPayloadDelegator: ViewInteractionPayloadDelegator;
  34. @Inject()
  35. actionsBlockPayloadDelegator: ActionsBlockPayloadDelegator;
  36. @Inject()
  37. sectionBlockPayloadDelegator: SectionBlockPayloadDelegator;
  38. async requestToGrowi(growiUrl:string, tokenPtoG:string):Promise<void> {
  39. console.log(55);
  40. const url = new URL('/_api/v3/slack-integration/proxied/commands', growiUrl);
  41. await axios.post(url.toString(), {
  42. type: 'url_verification',
  43. challenge: 'this_is_my_challenge_token',
  44. },
  45. {
  46. headers: {
  47. 'x-growi-ptog-tokens': tokenPtoG,
  48. },
  49. timeout: REQUEST_TIMEOUT_FOR_PTOG,
  50. });
  51. }
  52. @Get('/connection-status')
  53. @UseBefore(verifyGrowiToSlackRequest)
  54. async getConnectionStatuses(@Req() req: GrowiReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
  55. // asserted (tokenGtoPs.length > 0) by verifyGrowiToSlackRequest
  56. const { tokenGtoPs } = req;
  57. // retrieve Relation with Installation
  58. const relations = await this.relationRepository.createQueryBuilder('relation')
  59. .where('relation.tokenGtoP IN (:...tokens)', { tokens: tokenGtoPs })
  60. .leftJoinAndSelect('relation.installation', 'installation')
  61. .getMany();
  62. logger.debug(`${relations.length} relations found`, relations);
  63. // key: tokenGtoP, value: botToken
  64. const botTokenResolverMapping: {[tokenGtoP:string]:string} = {};
  65. relations.forEach((relation) => {
  66. const botToken = relation.installation?.data?.bot?.token;
  67. if (botToken != null) {
  68. botTokenResolverMapping[relation.tokenGtoP] = botToken;
  69. }
  70. });
  71. const connectionStatuses = await getConnectionStatuses(Object.keys(botTokenResolverMapping), (tokenGtoP:string) => botTokenResolverMapping[tokenGtoP]);
  72. return res.send({ connectionStatuses });
  73. }
  74. @Put('/supported-commands')
  75. @UseBefore(verifyGrowiToSlackRequest)
  76. async putSupportedCommands(@Req() req: GrowiReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
  77. // asserted (tokenGtoPs.length > 0) by verifyGrowiToSlackRequest
  78. const { tokenGtoPs } = req;
  79. // MOCK DATA SO FAR BUT THIS CAN BE USED AS AN ACTUAL CODE AS WELL GW 6972 -----------
  80. const { permissionsForBroadcastUseCommands, permissionsForSingleUseCommands } = req.body;
  81. // MOCK DATA SO FAR BUT THIS CAN BE USED AS AN ACTUAL CODE AS WELL GW 6972 -----------
  82. if (tokenGtoPs.length !== 1) {
  83. throw createError(400, 'installation is invalid');
  84. }
  85. const tokenGtoP = tokenGtoPs[0];
  86. // MOCK DATA MODIFY THIS GW 6972 -----------
  87. const relation = await this.relationRepository.update(
  88. { tokenGtoP }, { permissionsForBroadcastUseCommands, permissionsForSingleUseCommands },
  89. );
  90. // MOCK DATA MODIFY THIS GW 6972 -----------
  91. return res.send({ relation });
  92. }
  93. @Post('/relation-test')
  94. @UseBefore(verifyGrowiToSlackRequest)
  95. async postRelation(@Req() req: GrowiReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
  96. console.log(126);
  97. const { tokenGtoPs } = req;
  98. if (tokenGtoPs.length !== 1) {
  99. throw createError(400, 'installation is invalid');
  100. }
  101. const tokenGtoP = tokenGtoPs[0];
  102. // retrieve relation with Installation
  103. console.log(tokenGtoP, 133);
  104. const relation = await this.relationRepository.createQueryBuilder('relation')
  105. .where('tokenGtoP = :token', { token: tokenGtoP })
  106. .leftJoinAndSelect('relation.installation', 'installation')
  107. .getOne();
  108. console.log(138, relation);
  109. // Returns the result of the test if it already exists
  110. if (relation != null) {
  111. logger.debug('relation found', relation);
  112. const token = relation.installation.data.bot?.token;
  113. console.log(token, 145);
  114. if (token == null) {
  115. throw createError(400, 'installation is invalid');
  116. }
  117. try {
  118. await this.requestToGrowi(relation.growiUri, relation.tokenPtoG);
  119. }
  120. catch (err) {
  121. console.log(157);
  122. logger.error(err);
  123. throw createError(400, `failed to request to GROWI. err: ${err.message}`);
  124. }
  125. console.log(162);
  126. const status = await getConnectionStatus(token);
  127. if (status.error != null) {
  128. throw createError(400, `failed to get connection. err: ${status.error}`);
  129. }
  130. return res.send({ relation, slackBotToken: token });
  131. }
  132. // retrieve latest Order with Installation
  133. const order = await this.orderRepository.createQueryBuilder('order')
  134. .orderBy('order.createdAt', 'DESC')
  135. .where('tokenGtoP = :token', { token: tokenGtoP })
  136. .leftJoinAndSelect('order.installation', 'installation')
  137. .getOne();
  138. console.log(order);// 存在する
  139. if (order == null || order.isExpired()) {
  140. throw createError(400, 'order has expired or does not exist.');
  141. }
  142. // Access the GROWI URL saved in the Order record and check if the GtoP token is valid.
  143. try {
  144. await this.requestToGrowi(order.growiUrl, order.tokenPtoG);
  145. }
  146. catch (err) {
  147. console.log(192, err);
  148. logger.error(err);
  149. throw createError(400, `failed to request to GROWI. err: ${err.message}`);
  150. }
  151. logger.debug('order found', order);
  152. const token = order.installation.data.bot?.token;
  153. if (token == null) {
  154. throw createError(400, 'installation is invalid');
  155. }
  156. const status = await getConnectionStatus(token);
  157. if (status.error != null) {
  158. throw createError(400, `failed to get connection. err: ${status.error}`);
  159. }
  160. logger.debug('relation test is success', order);
  161. // temporary cache for 48 hours
  162. const expiredAtCommands = addHours(new Date(), 48);
  163. const response = await this.relationRepository.createQueryBuilder('relation')
  164. .insert()
  165. .values({
  166. installation: order.installation,
  167. tokenGtoP: order.tokenGtoP,
  168. tokenPtoG: order.tokenPtoG,
  169. growiUri: order.growiUrl,
  170. permissionsForBroadcastUseCommands: req.body.permissionsForBroadcastUseCommands,
  171. permissionsForSingleUseCommands: req.body.permissionsForSingleUseCommands,
  172. expiredAtCommands,
  173. })
  174. // https://github.com/typeorm/typeorm/issues/1090#issuecomment-634391487
  175. .orUpdate({
  176. conflict_target: ['installation', 'growiUri'],
  177. overwrite: ['tokenGtoP', 'tokenPtoG', 'permissionsForBroadcastUseCommands', 'permissionsForSingleUseCommands'],
  178. })
  179. .execute();
  180. const generatedRelation = await this.relationRepository.findOne({ id: response.identifiers[0].id });
  181. return res.send({ relation: generatedRelation, slackBotToken: token });
  182. }
  183. injectGrowiUri(req: GrowiReq, growiUri: string): void {
  184. if (req.body.view == null && req.body.blocks == null) {
  185. return;
  186. }
  187. if (req.body.view != null) {
  188. const parsedElement = JSON.parse(req.body.view);
  189. // delegate to ViewInteractionPayloadDelegator
  190. if (this.viewInteractionPayloadDelegator.shouldHandleToInject(parsedElement)) {
  191. this.viewInteractionPayloadDelegator.inject(parsedElement, growiUri);
  192. req.body.view = JSON.stringify(parsedElement);
  193. }
  194. }
  195. else if (req.body.blocks != null) {
  196. const parsedElement = JSON.parse(req.body.blocks);
  197. // delegate to ActionsBlockPayloadDelegator
  198. if (this.actionsBlockPayloadDelegator.shouldHandleToInject(parsedElement)) {
  199. this.actionsBlockPayloadDelegator.inject(parsedElement, growiUri);
  200. req.body.blocks = JSON.stringify(parsedElement);
  201. }
  202. // delegate to SectionBlockPayloadDelegator
  203. if (this.sectionBlockPayloadDelegator.shouldHandleToInject(parsedElement)) {
  204. this.sectionBlockPayloadDelegator.inject(parsedElement, growiUri);
  205. req.body.blocks = JSON.stringify(parsedElement);
  206. }
  207. }
  208. }
  209. @Post('/:method')
  210. @UseBefore(AddWebclientResponseToRes, verifyGrowiToSlackRequest)
  211. async callSlackApi(
  212. @PathParams('method') method: string, @Req() req: GrowiReq, @Res() res: WebclientRes,
  213. ): Promise<WebclientRes> {
  214. const { tokenGtoPs } = req;
  215. logger.debug('Slack API called: ', { method });
  216. if (tokenGtoPs.length !== 1) {
  217. return res.simulateWebAPIPlatformError('tokenGtoPs is invalid', 'invalid_tokenGtoP');
  218. }
  219. const tokenGtoP = tokenGtoPs[0];
  220. // retrieve relation with Installation
  221. const relation = await this.relationRepository.createQueryBuilder('relation')
  222. .where('tokenGtoP = :token', { token: tokenGtoP })
  223. .leftJoinAndSelect('relation.installation', 'installation')
  224. .getOne();
  225. if (relation == null) {
  226. return res.simulateWebAPIPlatformError('relation is invalid', 'invalid_relation');
  227. }
  228. const token = relation.installation.data.bot?.token;
  229. if (token == null) {
  230. return res.simulateWebAPIPlatformError('installation is invalid', 'invalid_installation');
  231. }
  232. // generate WebClient with no retry because GROWI main side will do
  233. const client = generateWebClient(token, {
  234. retryConfig: { retries: 0 },
  235. });
  236. try {
  237. this.injectGrowiUri(req, relation.growiUri);
  238. const opt = req.body;
  239. opt.headers = req.headers;
  240. logger.debug({ method, opt });
  241. // !! DO NOT REMOVE `await ` or it does not enter catch block even when axios error occured !! -- 2021.08.22 Yuki Takei
  242. const result = await client.apiCall(method, opt);
  243. return res.send(result);
  244. }
  245. catch (err) {
  246. logger.error(err);
  247. if (err.code === ErrorCode.PlatformError) {
  248. return res.simulateWebAPIPlatformError(err.message, err.code);
  249. }
  250. return res.simulateWebAPIRequestError(err.message, err.response?.status);
  251. }
  252. }
  253. }