2
0

RelationsService.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import { REQUEST_TIMEOUT_FOR_PTOG, type IChannelOptionalId } from '@growi/slack';
  2. import { getSupportedGrowiActionsRegExp } from '@growi/slack/dist/utils/get-supported-growi-actions-regexps';
  3. import { permissionParser } from '@growi/slack/dist/utils/permission-parser';
  4. import { Inject, Service } from '@tsed/di';
  5. import axios from 'axios';
  6. import { addHours } from 'date-fns/addHours';
  7. import { Relation, PermissionSettingsInterface } from '~/entities/relation';
  8. import { RelationRepository } from '~/repositories/relation';
  9. import loggerFactory from '~/utils/logger';
  10. const logger = loggerFactory('slackbot-proxy:services:RelationsService');
  11. type CheckPermissionForInteractionsResults = {
  12. allowedRelations:Relation[],
  13. disallowedGrowiUrls:Set<string>,
  14. commandName:string,
  15. rejectedResults:PromiseRejectedResult[]
  16. }
  17. type CheckEachRelationResult = {
  18. allowedRelation:Relation|null,
  19. disallowedGrowiUrl:string|null,
  20. eachRelationCommandName:string,
  21. }
  22. @Service()
  23. export class RelationsService {
  24. @Inject()
  25. relationRepository: RelationRepository;
  26. async resetAllExpiredAtCommands(): Promise<void> {
  27. await this.relationRepository.update({}, { expiredAtCommands: new Date('2000-01-01') });
  28. }
  29. private async getSupportedGrowiCommands(relation:Relation):Promise<any> {
  30. // generate API URL
  31. const url = new URL('/_api/v3/slack-integration/supported-commands', relation.growiUri);
  32. return axios.get(url.toString(), {
  33. headers: {
  34. 'x-growi-ptog-tokens': relation.tokenPtoG,
  35. },
  36. timeout: REQUEST_TIMEOUT_FOR_PTOG,
  37. });
  38. }
  39. private async syncSupportedGrowiCommands(relation:Relation): Promise<Relation> {
  40. const res = await this.getSupportedGrowiCommands(relation);
  41. // support both of v4.4.x and v4.5.x
  42. // see: https://redmine.weseek.co.jp/issues/82985
  43. const { permissionsForBroadcastUseCommands, permissionsForSingleUseCommands } = res.data.data ?? res.data;
  44. if (relation !== null) {
  45. relation.permissionsForBroadcastUseCommands = permissionsForBroadcastUseCommands;
  46. relation.permissionsForSingleUseCommands = permissionsForSingleUseCommands;
  47. relation.expiredAtCommands = addHours(new Date(), 48);
  48. return this.relationRepository.save(relation);
  49. }
  50. throw Error('No relation exists.');
  51. }
  52. private async syncRelation(relation: Relation): Promise<Relation> {
  53. // TODO use assert (relation != null)
  54. const isDataNull = relation.permissionsForBroadcastUseCommands == null || relation.permissionsForBroadcastUseCommands == null;
  55. const distanceMillisecondsToExpiredAt = relation.getDistanceInMillisecondsToExpiredAt(new Date());
  56. const isExpired = distanceMillisecondsToExpiredAt < 0;
  57. if (isDataNull || isExpired) {
  58. return this.syncSupportedGrowiCommands(relation);
  59. }
  60. // 24 hours
  61. const isLimitUnder24Hours = distanceMillisecondsToExpiredAt < 24 * 60 * 60 * 1000;
  62. if (isLimitUnder24Hours) {
  63. this.syncSupportedGrowiCommands(relation);
  64. }
  65. return relation;
  66. }
  67. private isPermitted(permissionSettings: PermissionSettingsInterface, growiCommandType: string, channel: IChannelOptionalId): boolean {
  68. // TODO assert (permissionSettings != null)
  69. const permissionForCommand = permissionSettings[growiCommandType];
  70. return permissionParser(permissionForCommand, channel);
  71. }
  72. async isPermissionsForSingleUseCommands(relation: Relation, growiCommandType: string, channel: IChannelOptionalId): Promise<boolean> {
  73. // TODO assert (relation != null)
  74. if (relation == null) {
  75. return false;
  76. }
  77. let relationToEval = relation;
  78. try {
  79. relationToEval = await this.syncRelation(relation);
  80. }
  81. catch (err) {
  82. logger.error('failed to sync', err);
  83. return false;
  84. }
  85. // TODO assert (relationToEval.permissionsForSingleUseCommands != null) because syncRelation success
  86. return this.isPermitted(relationToEval.permissionsForSingleUseCommands, growiCommandType, channel);
  87. }
  88. async isPermissionsUseBroadcastCommands(relation: Relation, growiCommandType: string, channel: IChannelOptionalId):Promise<boolean> {
  89. // TODO assert (relation != null)
  90. if (relation == null) {
  91. return false;
  92. }
  93. let relationToEval = relation;
  94. try {
  95. relationToEval = await this.syncRelation(relation);
  96. }
  97. catch (err) {
  98. logger.error('failed to sync', err);
  99. return false;
  100. }
  101. // TODO assert (relationToEval.permissionsForSingleUseCommands != null) because syncRelation success
  102. return this.isPermitted(relationToEval.permissionsForBroadcastUseCommands, growiCommandType, channel);
  103. }
  104. async checkPermissionForInteractions(
  105. relations: Relation[], actionId: string, callbackId: string, channel: IChannelOptionalId,
  106. ):Promise<CheckPermissionForInteractionsResults> {
  107. const allowedRelations:Relation[] = [];
  108. const disallowedGrowiUrls:Set<string> = new Set();
  109. let commandName = '';
  110. const results = await Promise.allSettled(relations.map((relation) => {
  111. const relationResult = this.checkEachRelation(relation, actionId, callbackId, channel);
  112. const { allowedRelation, disallowedGrowiUrl, eachRelationCommandName } = relationResult;
  113. if (allowedRelation != null) {
  114. allowedRelations.push(allowedRelation);
  115. }
  116. if (disallowedGrowiUrl != null) {
  117. disallowedGrowiUrls.add(disallowedGrowiUrl);
  118. }
  119. commandName = eachRelationCommandName;
  120. return relationResult;
  121. }));
  122. // Pick up only a relation which status is "rejected" in results. Like bellow
  123. const rejectedResults: PromiseRejectedResult[] = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected');
  124. return {
  125. allowedRelations, disallowedGrowiUrls, commandName, rejectedResults,
  126. };
  127. }
  128. checkEachRelation(relation:Relation, actionId:string, callbackId:string, channel: IChannelOptionalId): CheckEachRelationResult {
  129. let allowedRelation:Relation|null = null;
  130. let disallowedGrowiUrl:string|null = null;
  131. let eachRelationCommandName = '';
  132. let permissionForInteractions:boolean|string[];
  133. const singleUse = Object.keys(relation.permissionsForSingleUseCommands);
  134. const broadCastUse = Object.keys(relation.permissionsForBroadcastUseCommands);
  135. [...singleUse, ...broadCastUse].forEach(async(tempCommandName) => {
  136. // ex. search OR search:handlerName
  137. const commandRegExp = getSupportedGrowiActionsRegExp(tempCommandName);
  138. // skip this forEach loop if the requested command is not in permissionsForBroadcastUseCommands and permissionsForSingleUseCommands
  139. if (!commandRegExp.test(actionId) && !commandRegExp.test(callbackId)) {
  140. return;
  141. }
  142. eachRelationCommandName = tempCommandName;
  143. // case: singleUse
  144. permissionForInteractions = relation.permissionsForSingleUseCommands[tempCommandName];
  145. // case: broadcastUse
  146. if (permissionForInteractions == null) {
  147. permissionForInteractions = relation.permissionsForBroadcastUseCommands[tempCommandName];
  148. }
  149. if (permissionForInteractions === true) {
  150. allowedRelation = relation;
  151. return;
  152. }
  153. // check permission at channel level
  154. if (Array.isArray(permissionForInteractions)) {
  155. if (permissionForInteractions.includes(channel.name)) {
  156. allowedRelation = relation;
  157. return;
  158. }
  159. if (channel.id == null) return;
  160. if (permissionForInteractions.includes(channel.id)) {
  161. allowedRelation = relation;
  162. return;
  163. }
  164. }
  165. disallowedGrowiUrl = relation.growiUri;
  166. });
  167. return { allowedRelation, disallowedGrowiUrl, eachRelationCommandName };
  168. }
  169. }