LinkSharedService.ts 4.0 KB

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