RelationsService.ts 7.0 KB

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