LinkSharedService.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. import {
  2. type GrowiEventProcessor,
  3. REQUEST_TIMEOUT_FOR_PTOG,
  4. } from '@growi/slack';
  5. import type { WebClient } from '@slack/web-api';
  6. import { Inject, Service } from '@tsed/di';
  7. import axios from 'axios';
  8. // needed to import class (not type) for injection
  9. import { RelationRepository } from '~/repositories/relation';
  10. import loggerFactory from '~/utils/logger';
  11. const logger = loggerFactory('slackbot-proxy:services:LinkSharedService');
  12. type LinkSharedEventLink = {
  13. url: string;
  14. domain: string;
  15. };
  16. // aliases
  17. type GrowiOrigin = string;
  18. type TokenPtoG = string;
  19. export type LinkSharedRequestEvent = {
  20. channel: string;
  21. message_ts: string;
  22. links: LinkSharedEventLink[];
  23. };
  24. type PrivateData = {
  25. isPublic: false;
  26. path: string;
  27. };
  28. type PublicData = {
  29. isPublic: true;
  30. path: string;
  31. pageBody: string;
  32. updatedAt: string;
  33. commentCount: number;
  34. };
  35. export type DataForLinkShared = PrivateData | PublicData;
  36. @Service()
  37. export class LinkSharedService
  38. implements GrowiEventProcessor<LinkSharedRequestEvent>
  39. {
  40. @Inject()
  41. relationRepository: RelationRepository;
  42. shouldHandleEvent(eventType: string): boolean {
  43. return eventType === 'link_shared';
  44. }
  45. async processEvent(
  46. client: WebClient,
  47. event: LinkSharedRequestEvent,
  48. ): Promise<void> {
  49. const { links } = event;
  50. const origins: string[] = links.map(
  51. (link: LinkSharedEventLink) => new URL(link.url).origin,
  52. );
  53. const originToTokenPtoGMap: Map<GrowiOrigin, TokenPtoG> =
  54. await this.generateOriginToTokenPtoGMapFromOrigins(origins); // get tokenPtoG at once
  55. // forward to GROWI
  56. const result = await this.forwardToEachGrowiOrigin(
  57. origins,
  58. event,
  59. originToTokenPtoGMap,
  60. );
  61. // log error
  62. this.logErrorRejectedResults(result);
  63. }
  64. // generate Map<GrowiOrigin, TokenPtoG>
  65. async generateOriginToTokenPtoGMapFromOrigins(
  66. origins: GrowiOrigin[],
  67. ): Promise<Map<GrowiOrigin, TokenPtoG>> {
  68. const originToTokenPtoGMap: Map<GrowiOrigin, TokenPtoG> = new Map();
  69. // get relations using origins at once
  70. const relations = await this.relationRepository.findAllByGrowiUris(origins);
  71. // increment map using relation.growiUri & relation.tokenPtoG
  72. relations.forEach((relation) => {
  73. originToTokenPtoGMap.set(relation.growiUri, relation.tokenPtoG);
  74. });
  75. return originToTokenPtoGMap;
  76. }
  77. async forwardToEachGrowiOrigin(
  78. origins: string[],
  79. event: LinkSharedRequestEvent,
  80. originToTokenPtoGMap: Map<GrowiOrigin, TokenPtoG>,
  81. ): Promise<PromiseSettledResult<void>[]> {
  82. return Promise.allSettled(
  83. origins.map(async (origin) => {
  84. const requestBody = {
  85. growiBotEvent: {
  86. eventType: 'link_shared',
  87. event,
  88. },
  89. data: {
  90. origin,
  91. },
  92. };
  93. try {
  94. // ensure tokenPtoG exists
  95. const tokenPtoG = originToTokenPtoGMap.get(origin);
  96. if (tokenPtoG == null) throw new Error('tokenPtoG is null');
  97. const url = new URL(
  98. '/_api/v3/slack-integration/proxied/events',
  99. origin,
  100. );
  101. await axios.post(url.toString(), requestBody, {
  102. headers: {
  103. 'x-growi-ptog-tokens': tokenPtoG,
  104. },
  105. timeout: REQUEST_TIMEOUT_FOR_PTOG,
  106. });
  107. } catch (err) {
  108. logger.error(
  109. `Error occurred while request to growi (origin=${origin}):`,
  110. err,
  111. );
  112. throw err;
  113. }
  114. }),
  115. );
  116. }
  117. // Promise util method to output rejected results
  118. private logErrorRejectedResults<T>(results: PromiseSettledResult<T>[]): void {
  119. const rejectedResults: PromiseRejectedResult[] = results.filter(
  120. (result): result is PromiseRejectedResult => result.status === 'rejected',
  121. );
  122. rejectedResults.forEach((rejected, i) => {
  123. logger.error(
  124. `Error occurred (count: ${i}): `,
  125. rejected.reason.toString(),
  126. );
  127. });
  128. }
  129. }