LinkSharedService.ts 3.8 KB

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