@oneuptime/common 9.2.12 → 9.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/Models/DatabaseModels/AlertInternalNote.ts +29 -0
  2. package/Models/DatabaseModels/IncidentInternalNote.ts +29 -0
  3. package/Models/DatabaseModels/IncidentPublicNote.ts +29 -0
  4. package/Models/DatabaseModels/Index.ts +3 -11
  5. package/Models/DatabaseModels/{CopilotPullRequest.ts → LlmProvider.ts} +243 -248
  6. package/Models/DatabaseModels/ScheduledMaintenanceInternalNote.ts +29 -0
  7. package/Models/DatabaseModels/ScheduledMaintenancePublicNote.ts +29 -0
  8. package/Server/API/LlmProviderAPI.ts +57 -0
  9. package/Server/API/MicrosoftTeamsAPI.ts +2 -146
  10. package/Server/API/SlackAPI.ts +105 -0
  11. package/Server/API/StatusPageAPI.ts +6 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1765477339178-MigrationName.ts +71 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1765540325149-MigrationName.ts +45 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1765540549739-MigrationName.ts +61 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1765544010078-MigrationName.ts +35 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  17. package/Server/Middleware/SlackAuthorization.ts +4 -1
  18. package/Server/Services/AlertInternalNoteService.ts +26 -0
  19. package/Server/Services/IncidentInternalNoteService.ts +26 -0
  20. package/Server/Services/IncidentPublicNoteService.ts +26 -0
  21. package/Server/Services/Index.ts +2 -11
  22. package/Server/Services/LlmProviderService.ts +100 -0
  23. package/Server/Services/ScheduledMaintenanceInternalNoteService.ts +26 -0
  24. package/Server/Services/ScheduledMaintenancePublicNoteService.ts +26 -0
  25. package/Server/Services/StatusPageService.ts +3 -0
  26. package/Server/Utils/Express.ts +1 -0
  27. package/Server/Utils/StartServer.ts +5 -0
  28. package/Server/Utils/StatusPageResource.ts +89 -0
  29. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +96 -55
  30. package/Server/Utils/Workspace/Slack/Actions/ActionTypes.ts +16 -0
  31. package/Server/Utils/Workspace/Slack/Actions/Alert.ts +184 -1
  32. package/Server/Utils/Workspace/Slack/Actions/Incident.ts +224 -1
  33. package/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.ts +232 -1
  34. package/Server/Utils/Workspace/Slack/Slack.ts +113 -0
  35. package/Server/Utils/Workspace/Slack/app-manifest.json +13 -2
  36. package/Tests/Server/Utils/StatusPageResource.test.ts +161 -0
  37. package/Types/Icon/IconProp.ts +1 -0
  38. package/Types/LLM/Index.ts +4 -0
  39. package/Types/LLM/LlmType.ts +7 -0
  40. package/Types/Permission.ts +38 -113
  41. package/UI/Components/Icon/Icon.tsx +8 -0
  42. package/UI/Components/Link/Link.tsx +5 -1
  43. package/build/dist/Models/DatabaseModels/AlertInternalNote.js +30 -0
  44. package/build/dist/Models/DatabaseModels/AlertInternalNote.js.map +1 -1
  45. package/build/dist/Models/DatabaseModels/IncidentInternalNote.js +30 -0
  46. package/build/dist/Models/DatabaseModels/IncidentInternalNote.js.map +1 -1
  47. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +30 -0
  48. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
  49. package/build/dist/Models/DatabaseModels/Index.js +2 -10
  50. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  51. package/build/dist/Models/DatabaseModels/{CopilotPullRequest.js → LlmProvider.js} +263 -255
  52. package/build/dist/Models/DatabaseModels/LlmProvider.js.map +1 -0
  53. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceInternalNote.js +30 -0
  54. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceInternalNote.js.map +1 -1
  55. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js +30 -0
  56. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js.map +1 -1
  57. package/build/dist/Server/API/LlmProviderAPI.js +36 -0
  58. package/build/dist/Server/API/LlmProviderAPI.js.map +1 -0
  59. package/build/dist/Server/API/MicrosoftTeamsAPI.js +2 -91
  60. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  61. package/build/dist/Server/API/SlackAPI.js +74 -0
  62. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  63. package/build/dist/Server/API/StatusPageAPI.js +6 -0
  64. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  65. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765477339178-MigrationName.js +30 -0
  66. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765477339178-MigrationName.js.map +1 -0
  67. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765540325149-MigrationName.js +22 -0
  68. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765540325149-MigrationName.js.map +1 -0
  69. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765540549739-MigrationName.js +39 -0
  70. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765540549739-MigrationName.js.map +1 -0
  71. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765544010078-MigrationName.js +18 -0
  72. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765544010078-MigrationName.js.map +1 -0
  73. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  74. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  75. package/build/dist/Server/Middleware/SlackAuthorization.js +4 -1
  76. package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
  77. package/build/dist/Server/Services/AlertInternalNoteService.js +24 -0
  78. package/build/dist/Server/Services/AlertInternalNoteService.js.map +1 -1
  79. package/build/dist/Server/Services/IncidentInternalNoteService.js +24 -0
  80. package/build/dist/Server/Services/IncidentInternalNoteService.js.map +1 -1
  81. package/build/dist/Server/Services/IncidentPublicNoteService.js +24 -0
  82. package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
  83. package/build/dist/Server/Services/Index.js +2 -10
  84. package/build/dist/Server/Services/Index.js.map +1 -1
  85. package/build/dist/Server/Services/LlmProviderService.js +85 -0
  86. package/build/dist/Server/Services/LlmProviderService.js.map +1 -0
  87. package/build/dist/Server/Services/ScheduledMaintenanceInternalNoteService.js +24 -0
  88. package/build/dist/Server/Services/ScheduledMaintenanceInternalNoteService.js.map +1 -1
  89. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +24 -0
  90. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
  91. package/build/dist/Server/Services/StatusPageService.js +3 -0
  92. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  93. package/build/dist/Server/Utils/Express.js.map +1 -1
  94. package/build/dist/Server/Utils/StartServer.js +5 -0
  95. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  96. package/build/dist/Server/Utils/StatusPageResource.js +68 -0
  97. package/build/dist/Server/Utils/StatusPageResource.js.map +1 -0
  98. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +73 -42
  99. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  100. package/build/dist/Server/Utils/Workspace/Slack/Actions/ActionTypes.js +13 -0
  101. package/build/dist/Server/Utils/Workspace/Slack/Actions/ActionTypes.js.map +1 -1
  102. package/build/dist/Server/Utils/Workspace/Slack/Actions/Alert.js +143 -1
  103. package/build/dist/Server/Utils/Workspace/Slack/Actions/Alert.js.map +1 -1
  104. package/build/dist/Server/Utils/Workspace/Slack/Actions/Incident.js +173 -1
  105. package/build/dist/Server/Utils/Workspace/Slack/Actions/Incident.js.map +1 -1
  106. package/build/dist/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.js +173 -1
  107. package/build/dist/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.js.map +1 -1
  108. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +92 -0
  109. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  110. package/build/dist/Server/Utils/Workspace/Slack/app-manifest.json +12 -2
  111. package/build/dist/Tests/Server/Utils/StatusPageResource.test.js +122 -0
  112. package/build/dist/Tests/Server/Utils/StatusPageResource.test.js.map +1 -0
  113. package/build/dist/Types/Icon/IconProp.js +1 -0
  114. package/build/dist/Types/Icon/IconProp.js.map +1 -1
  115. package/build/dist/Types/LLM/Index.js +4 -0
  116. package/build/dist/Types/LLM/Index.js.map +1 -0
  117. package/build/dist/Types/LLM/LlmType.js +8 -0
  118. package/build/dist/Types/LLM/LlmType.js.map +1 -0
  119. package/build/dist/Types/Permission.js +32 -97
  120. package/build/dist/Types/Permission.js.map +1 -1
  121. package/build/dist/UI/Components/Icon/Icon.js +3 -0
  122. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  123. package/build/dist/UI/Components/Link/Link.js +4 -1
  124. package/build/dist/UI/Components/Link/Link.js.map +1 -1
  125. package/package.json +5 -1
  126. package/Models/DatabaseModels/CopilotAction.ts +0 -772
  127. package/Models/DatabaseModels/CopilotActionTypePriority.ts +0 -340
  128. package/Models/DatabaseModels/CopilotCodeRepository.ts +0 -637
  129. package/Models/DatabaseModels/ServiceCopilotCodeRepository.ts +0 -544
  130. package/Server/API/CopilotActionAPI.ts +0 -418
  131. package/Server/API/CopilotCodeRepositoryAPI.ts +0 -127
  132. package/Server/API/CopilotPullRequestAPI.ts +0 -243
  133. package/Server/Docs/CodeRepository.md +0 -43
  134. package/Server/Middleware/CodeRepositoryAuthorization.ts +0 -50
  135. package/Server/Services/CopilotActionService.ts +0 -10
  136. package/Server/Services/CopilotActionTypePriorityService.ts +0 -67
  137. package/Server/Services/CopilotCodeRepositoryService.ts +0 -62
  138. package/Server/Services/CopilotPullRequestService.ts +0 -10
  139. package/Server/Services/ServiceCopilotCodeRepositoryService.ts +0 -10
  140. package/Types/Copilot/CopilotActionProps/DirectoryActionProp.ts +0 -3
  141. package/Types/Copilot/CopilotActionProps/ExceptionActionProp.ts +0 -4
  142. package/Types/Copilot/CopilotActionProps/FileActionProp.ts +0 -7
  143. package/Types/Copilot/CopilotActionProps/FunctionActionProp.ts +0 -5
  144. package/Types/Copilot/CopilotActionProps/Index.ts +0 -96
  145. package/Types/Copilot/CopilotActionProps/SpanActionProp.ts +0 -4
  146. package/Types/Copilot/CopilotActionStatus.ts +0 -114
  147. package/Types/Copilot/CopilotActionType.ts +0 -212
  148. package/build/dist/Models/DatabaseModels/CopilotAction.js +0 -793
  149. package/build/dist/Models/DatabaseModels/CopilotAction.js.map +0 -1
  150. package/build/dist/Models/DatabaseModels/CopilotActionTypePriority.js +0 -358
  151. package/build/dist/Models/DatabaseModels/CopilotActionTypePriority.js.map +0 -1
  152. package/build/dist/Models/DatabaseModels/CopilotCodeRepository.js +0 -656
  153. package/build/dist/Models/DatabaseModels/CopilotCodeRepository.js.map +0 -1
  154. package/build/dist/Models/DatabaseModels/CopilotPullRequest.js.map +0 -1
  155. package/build/dist/Models/DatabaseModels/ServiceCopilotCodeRepository.js +0 -561
  156. package/build/dist/Models/DatabaseModels/ServiceCopilotCodeRepository.js.map +0 -1
  157. package/build/dist/Server/API/CopilotActionAPI.js +0 -295
  158. package/build/dist/Server/API/CopilotActionAPI.js.map +0 -1
  159. package/build/dist/Server/API/CopilotCodeRepositoryAPI.js +0 -91
  160. package/build/dist/Server/API/CopilotCodeRepositoryAPI.js.map +0 -1
  161. package/build/dist/Server/API/CopilotPullRequestAPI.js +0 -166
  162. package/build/dist/Server/API/CopilotPullRequestAPI.js.map +0 -1
  163. package/build/dist/Server/Middleware/CodeRepositoryAuthorization.js +0 -48
  164. package/build/dist/Server/Middleware/CodeRepositoryAuthorization.js.map +0 -1
  165. package/build/dist/Server/Services/CopilotActionService.js +0 -9
  166. package/build/dist/Server/Services/CopilotActionService.js.map +0 -1
  167. package/build/dist/Server/Services/CopilotActionTypePriorityService.js +0 -61
  168. package/build/dist/Server/Services/CopilotActionTypePriorityService.js.map +0 -1
  169. package/build/dist/Server/Services/CopilotCodeRepositoryService.js +0 -61
  170. package/build/dist/Server/Services/CopilotCodeRepositoryService.js.map +0 -1
  171. package/build/dist/Server/Services/CopilotPullRequestService.js +0 -9
  172. package/build/dist/Server/Services/CopilotPullRequestService.js.map +0 -1
  173. package/build/dist/Server/Services/ServiceCopilotCodeRepositoryService.js +0 -9
  174. package/build/dist/Server/Services/ServiceCopilotCodeRepositoryService.js.map +0 -1
  175. package/build/dist/Types/Copilot/CopilotActionProps/DirectoryActionProp.js +0 -2
  176. package/build/dist/Types/Copilot/CopilotActionProps/DirectoryActionProp.js.map +0 -1
  177. package/build/dist/Types/Copilot/CopilotActionProps/ExceptionActionProp.js +0 -2
  178. package/build/dist/Types/Copilot/CopilotActionProps/ExceptionActionProp.js.map +0 -1
  179. package/build/dist/Types/Copilot/CopilotActionProps/FileActionProp.js +0 -2
  180. package/build/dist/Types/Copilot/CopilotActionProps/FileActionProp.js.map +0 -1
  181. package/build/dist/Types/Copilot/CopilotActionProps/FunctionActionProp.js +0 -2
  182. package/build/dist/Types/Copilot/CopilotActionProps/FunctionActionProp.js.map +0 -1
  183. package/build/dist/Types/Copilot/CopilotActionProps/Index.js +0 -64
  184. package/build/dist/Types/Copilot/CopilotActionProps/Index.js.map +0 -1
  185. package/build/dist/Types/Copilot/CopilotActionProps/SpanActionProp.js +0 -2
  186. package/build/dist/Types/Copilot/CopilotActionProps/SpanActionProp.js.map +0 -1
  187. package/build/dist/Types/Copilot/CopilotActionStatus.js +0 -96
  188. package/build/dist/Types/Copilot/CopilotActionStatus.js.map +0 -1
  189. package/build/dist/Types/Copilot/CopilotActionType.js +0 -175
  190. package/build/dist/Types/Copilot/CopilotActionType.js.map +0 -1
@@ -29,6 +29,8 @@ import CaptureSpan from "../../Telemetry/CaptureSpan";
29
29
  import BadDataException from "../../../../Types/Exception/BadDataException";
30
30
  import ObjectID from "../../../../Types/ObjectID";
31
31
  import WorkspaceProjectAuthTokenService from "../../../Services/WorkspaceProjectAuthTokenService";
32
+ import WorkspaceUserAuthTokenService from "../../../Services/WorkspaceUserAuthTokenService";
33
+ import WorkspaceUserAuthToken from "../../../../Models/DatabaseModels/WorkspaceUserAuthToken";
32
34
  import WorkspaceProjectAuthToken, {
33
35
  MicrosoftTeamsMiscData,
34
36
  MicrosoftTeamsTeam,
@@ -2815,6 +2817,9 @@ All monitoring checks are passing normally.`;
2815
2817
  @CaptureSpan()
2816
2818
  public static async refreshTeams(data: {
2817
2819
  projectId: ObjectID;
2820
+ // optional: prefer a user-scoped token when provided
2821
+ userId?: ObjectID;
2822
+ userAccessToken?: string;
2818
2823
  }): Promise<Record<string, { id: string; name: string }>> {
2819
2824
  logger.debug("=== refreshTeams called ===");
2820
2825
 
@@ -2848,75 +2853,111 @@ All monitoring checks are passing normally.`;
2848
2853
  );
2849
2854
  }
2850
2855
 
2851
- // Get a valid app access token
2852
- const accessToken: string | null = await this.refreshAccessToken({
2853
- projectId: data.projectId,
2854
- miscData: projectAuth.miscData as MicrosoftTeamsMiscData,
2855
- tenantId,
2856
- });
2856
+ // Try using user scoped token first when available
2857
+ let allTeams: Array<JSONObject> = [];
2858
+ let usedAccessToken: string | null = null;
2857
2859
 
2858
- if (!accessToken) {
2859
- throw new BadDataException(
2860
- "Could not obtain valid access token for Microsoft Teams",
2861
- );
2862
- }
2863
-
2864
- /*
2865
- * Fetch all teams from Microsoft Graph API using app permissions
2866
- * Handle pagination to get all teams
2867
- */
2868
- const allTeams: Array<JSONObject> = [];
2869
- let nextLink: string | null = "https://graph.microsoft.com/v1.0/teams";
2870
- let pageCount: number = 0;
2871
- const MAX_PAGES: number = MICROSOFT_TEAMS_MAX_PAGES; // Prevent infinite loop
2872
-
2873
- while (nextLink) {
2874
- pageCount++;
2875
- if (pageCount > MAX_PAGES) {
2876
- logger.error(
2877
- `Maximum page limit (${MAX_PAGES}) reached while paginating teams. Breaking out to prevent infinite loop.`,
2860
+ try {
2861
+ // If caller provided a userAccessToken directly, use it
2862
+ if (data.userAccessToken) {
2863
+ logger.debug(
2864
+ "Using provided user access token to fetch joined teams",
2878
2865
  );
2879
- break;
2866
+ usedAccessToken = data.userAccessToken;
2867
+ const userTeams: Record<string, { id: string; name: string }> =
2868
+ await this.getUserJoinedTeams(usedAccessToken);
2869
+ allTeams = Object.values(userTeams) as any;
2870
+ } else if (data.userId) {
2871
+ // Try to fetch stored user auth for this project + user
2872
+ logger.debug("Looking up stored user auth token for provided userId");
2873
+ const userAuth: WorkspaceUserAuthToken | null =
2874
+ await WorkspaceUserAuthTokenService.getUserAuth({
2875
+ projectId: data.projectId,
2876
+ userId: data.userId,
2877
+ workspaceType: WorkspaceType.MicrosoftTeams,
2878
+ });
2879
+
2880
+ if (userAuth && userAuth.authToken) {
2881
+ usedAccessToken = userAuth.authToken;
2882
+ logger.debug(
2883
+ "Found user auth token; using it to fetch joined teams",
2884
+ );
2885
+ const userTeams: Record<string, { id: string; name: string }> =
2886
+ await this.getUserJoinedTeams(usedAccessToken);
2887
+ allTeams = Object.values(userTeams) as any;
2888
+ }
2880
2889
  }
2881
- logger.debug(`Fetching teams page ${pageCount}: ${nextLink}`);
2890
+ } catch (err) {
2891
+ logger.warn(
2892
+ "Failed to fetch teams using user-scoped token, falling back to app token:",
2893
+ );
2894
+ logger.warn(err);
2895
+ allTeams = [];
2896
+ }
2882
2897
 
2883
- const teamsResponse: HTTPErrorResponse | HTTPResponse<JSONObject> =
2884
- await API.get<JSONObject>({
2885
- url: URL.fromString(nextLink),
2886
- headers: {
2887
- Authorization: `Bearer ${accessToken}`,
2888
- },
2889
- });
2898
+ // If we couldn't obtain teams via user token, fall back to app-scoped token + existing behavior
2899
+ if (!allTeams || allTeams.length === 0) {
2900
+ // Get a valid app access token
2901
+ const accessToken: string | null = await this.refreshAccessToken({
2902
+ projectId: data.projectId,
2903
+ miscData: projectAuth.miscData as MicrosoftTeamsMiscData,
2904
+ tenantId,
2905
+ });
2890
2906
 
2891
- if (teamsResponse instanceof HTTPErrorResponse) {
2892
- logger.error("Error fetching teams from Microsoft Teams:");
2893
- logger.error(teamsResponse);
2907
+ if (!accessToken) {
2894
2908
  throw new BadDataException(
2895
- "Failed to fetch teams from Microsoft Teams",
2909
+ "Could not obtain valid access token for Microsoft Teams",
2896
2910
  );
2897
2911
  }
2898
2912
 
2899
- const teams: Array<JSONObject> =
2900
- (teamsResponse.data as any)["value"] || [];
2901
- allTeams.push(...teams);
2913
+ /*
2914
+ * Fetch all teams from Microsoft Graph API using app permissions
2915
+ * Handle pagination to get all teams
2916
+ */
2917
+ allTeams = [];
2918
+ let nextLink: string | null = "https://graph.microsoft.com/v1.0/teams";
2919
+ let pageCount: number = 0;
2920
+ const MAX_PAGES: number = MICROSOFT_TEAMS_MAX_PAGES; // Prevent infinite loop
2921
+
2922
+ while (nextLink) {
2923
+ pageCount++;
2924
+ if (pageCount > MAX_PAGES) {
2925
+ logger.error(
2926
+ `Maximum page limit (${MAX_PAGES}) reached while paginating teams. Breaking out to prevent infinite loop.`,
2927
+ );
2928
+ break;
2929
+ }
2930
+ logger.debug(`Fetching teams page ${pageCount}: ${nextLink}`);
2931
+
2932
+ const teamsResponse: HTTPErrorResponse | HTTPResponse<JSONObject> =
2933
+ await API.get<JSONObject>({
2934
+ url: URL.fromString(nextLink),
2935
+ headers: {
2936
+ Authorization: `Bearer ${accessToken}`,
2937
+ },
2938
+ });
2939
+
2940
+ if (teamsResponse instanceof HTTPErrorResponse) {
2941
+ logger.error("Error fetching teams from Microsoft Teams:");
2942
+ logger.error(teamsResponse);
2943
+ throw new BadDataException(
2944
+ "Failed to fetch teams from Microsoft Teams",
2945
+ );
2946
+ }
2902
2947
 
2903
- // Check for next page
2904
- nextLink = (teamsResponse.data as any)["@odata.nextLink"] || null;
2948
+ const teams: Array<JSONObject> =
2949
+ (teamsResponse.data as any)["value"] || [];
2950
+ allTeams.push(...teams);
2905
2951
 
2906
- logger.debug(
2907
- `Page ${pageCount}: Fetched ${teams.length} teams. Total so far: ${allTeams.length}`,
2908
- );
2909
- }
2952
+ // Check for next page
2953
+ nextLink = (teamsResponse.data as any)["@odata.nextLink"] || null;
2910
2954
 
2911
- if (allTeams.length === 0) {
2912
- logger.debug("No teams found in organization");
2913
- return {};
2955
+ logger.debug(
2956
+ `Page ${pageCount}: Fetched ${teams.length} teams. Total so far: ${allTeams.length}`,
2957
+ );
2958
+ }
2914
2959
  }
2915
2960
 
2916
- logger.debug(
2917
- `Completed fetching all teams. Total pages: ${pageCount}, Total teams: ${allTeams.length}`,
2918
- );
2919
-
2920
2961
  // Process teams
2921
2962
  const availableTeams: Record<string, { id: string; name: string }> =
2922
2963
  allTeams.reduce(
@@ -12,6 +12,9 @@ enum SlackActionType {
12
12
  NewIncident = "/incident", // new incident slash command
13
13
  SubmitNewIncident = "SubmitNewIncident",
14
14
 
15
+ // Emoji Reaction Actions
16
+ EmojiReactionAdded = "EmojiReactionAdded",
17
+
15
18
  // Alert Actions just like Incident Actions
16
19
  AcknowledgeAlert = "AcknowledgeAlert",
17
20
  ResolveAlert = "ResolveAlert",
@@ -41,4 +44,17 @@ enum SlackActionType {
41
44
  ViewOnCallPolicy = "ViewOnCallPolicy",
42
45
  }
43
46
 
47
+ // Emoji names that trigger saving a message as a Private Note (Internal Note)
48
+ export const PrivateNoteEmojis: string[] = ["pushpin", "round_pushpin", "pin"];
49
+
50
+ // Emoji names that trigger saving a message as a Public Note
51
+ export const PublicNoteEmojis: string[] = [
52
+ "mega",
53
+ "loudspeaker",
54
+ "megaphone",
55
+ "announcement",
56
+ "speaking_head_in_silhouette",
57
+ "speaking_head",
58
+ ];
59
+
44
60
  export default SlackActionType;
@@ -3,7 +3,7 @@ import ObjectID from "../../../../../Types/ObjectID";
3
3
  import AlertService from "../../../../Services/AlertService";
4
4
  import { ExpressRequest, ExpressResponse } from "../../../Express";
5
5
  import SlackUtil from "../Slack";
6
- import SlackActionType from "./ActionTypes";
6
+ import SlackActionType, { PrivateNoteEmojis } from "./ActionTypes";
7
7
  import { SlackAction, SlackRequest } from "./Auth";
8
8
  import Response from "../../../Response";
9
9
  import {
@@ -24,7 +24,12 @@ import logger from "../../../Logger";
24
24
  import AccessTokenService from "../../../../Services/AccessTokenService";
25
25
  import CaptureSpan from "../../../Telemetry/CaptureSpan";
26
26
  import WorkspaceNotificationLogService from "../../../../Services/WorkspaceNotificationLogService";
27
+ import WorkspaceUserAuthTokenService from "../../../../Services/WorkspaceUserAuthTokenService";
27
28
  import WorkspaceType from "../../../../../Types/Workspace/WorkspaceType";
29
+ import WorkspaceProjectAuthTokenService from "../../../../Services/WorkspaceProjectAuthTokenService";
30
+ import WorkspaceNotificationLog from "../../../../../Models/DatabaseModels/WorkspaceNotificationLog";
31
+ import WorkspaceProjectAuthToken from "../../../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
32
+ import WorkspaceUserAuthToken from "../../../../../Models/DatabaseModels/WorkspaceUserAuthToken";
28
33
 
29
34
  export default class SlackAlertActions {
30
35
  @CaptureSpan()
@@ -773,4 +778,182 @@ export default class SlackAlertActions {
773
778
  new BadDataException("Invalid Action Type"),
774
779
  );
775
780
  }
781
+
782
+ @CaptureSpan()
783
+ public static async handleEmojiReaction(data: {
784
+ teamId: string;
785
+ reaction: string;
786
+ userId: string;
787
+ channelId: string;
788
+ messageTs: string;
789
+ }): Promise<void> {
790
+ logger.debug("Handling emoji reaction for Alert with data:");
791
+ logger.debug(data);
792
+
793
+ const { teamId, reaction, userId, channelId, messageTs } = data;
794
+
795
+ // Alerts only support private notes, so only pushpin emojis work
796
+ const isPrivateNoteEmoji: boolean = PrivateNoteEmojis.includes(reaction);
797
+
798
+ if (!isPrivateNoteEmoji) {
799
+ logger.debug(
800
+ `Emoji "${reaction}" is not a supported private note emoji for alerts. Ignoring.`,
801
+ );
802
+ return;
803
+ }
804
+
805
+ // Get the project auth token using the team ID
806
+ const projectAuth: WorkspaceProjectAuthToken | null =
807
+ await WorkspaceProjectAuthTokenService.findOneBy({
808
+ query: {
809
+ workspaceProjectId: teamId,
810
+ },
811
+ select: {
812
+ projectId: true,
813
+ authToken: true,
814
+ },
815
+ props: {
816
+ isRoot: true,
817
+ },
818
+ });
819
+
820
+ if (!projectAuth || !projectAuth.projectId || !projectAuth.authToken) {
821
+ logger.debug(
822
+ "No project auth found for team ID. Ignoring emoji reaction.",
823
+ );
824
+ return;
825
+ }
826
+
827
+ const projectId: ObjectID = projectAuth.projectId;
828
+ const authToken: string = projectAuth.authToken;
829
+
830
+ // Find the alert linked to this channel
831
+ const workspaceLog: WorkspaceNotificationLog | null =
832
+ await WorkspaceNotificationLogService.findOneBy({
833
+ query: {
834
+ channelId: channelId,
835
+ workspaceType: WorkspaceType.Slack,
836
+ projectId: projectId,
837
+ },
838
+ select: {
839
+ alertId: true,
840
+ },
841
+ props: {
842
+ isRoot: true,
843
+ },
844
+ });
845
+
846
+ if (!workspaceLog || !workspaceLog.alertId) {
847
+ logger.debug(
848
+ "No alert found linked to this channel. Ignoring emoji reaction.",
849
+ );
850
+ return;
851
+ }
852
+
853
+ const alertId: ObjectID = workspaceLog.alertId;
854
+
855
+ // Get the alert number for the confirmation message
856
+ const alertNumber: number | null = await AlertService.getAlertNumber({
857
+ alertId: alertId,
858
+ });
859
+
860
+ // Get the user ID in OneUptime based on Slack user ID
861
+ const userAuth: WorkspaceUserAuthToken | null =
862
+ await WorkspaceUserAuthTokenService.findOneBy({
863
+ query: {
864
+ workspaceUserId: userId,
865
+ workspaceType: WorkspaceType.Slack,
866
+ projectId: projectId,
867
+ },
868
+ select: {
869
+ userId: true,
870
+ },
871
+ props: {
872
+ isRoot: true,
873
+ },
874
+ });
875
+
876
+ if (!userAuth || !userAuth.userId) {
877
+ logger.debug(
878
+ "No OneUptime user found for Slack user. Ignoring emoji reaction.",
879
+ );
880
+ return;
881
+ }
882
+
883
+ const oneUptimeUserId: ObjectID = userAuth.userId;
884
+
885
+ // Fetch the message text using the timestamp
886
+ let messageText: string | null = null;
887
+ try {
888
+ messageText = await SlackUtil.getMessageByTimestamp({
889
+ authToken: authToken,
890
+ channelId: channelId,
891
+ messageTs: messageTs,
892
+ });
893
+ } catch (err) {
894
+ logger.error("Error fetching message text:");
895
+ logger.error(err);
896
+ return;
897
+ }
898
+
899
+ if (!messageText) {
900
+ logger.debug("No message text found. Ignoring emoji reaction.");
901
+ return;
902
+ }
903
+
904
+ // Create a unique identifier for this Slack message to prevent duplicate notes
905
+ const postedFromSlackMessageId: string = `${channelId}:${messageTs}`;
906
+
907
+ // Check if a note from this Slack message already exists
908
+ const hasExistingNote: boolean =
909
+ await AlertInternalNoteService.hasNoteFromSlackMessage({
910
+ alertId: alertId,
911
+ postedFromSlackMessageId: postedFromSlackMessageId,
912
+ });
913
+
914
+ if (hasExistingNote) {
915
+ logger.debug(
916
+ "Private note from this Slack message already exists. Skipping duplicate.",
917
+ );
918
+ return;
919
+ }
920
+
921
+ // Save as private note (Alerts only support private notes)
922
+ try {
923
+ await AlertInternalNoteService.addNote({
924
+ alertId: alertId,
925
+ note: messageText,
926
+ projectId: projectId,
927
+ userId: oneUptimeUserId,
928
+ postedFromSlackMessageId: postedFromSlackMessageId,
929
+ });
930
+ logger.debug("Private note added to alert successfully.");
931
+ } catch (err) {
932
+ logger.error("Error saving note:");
933
+ logger.error(err);
934
+ return;
935
+ }
936
+
937
+ // Send confirmation message as a reply to the original message thread
938
+ try {
939
+ const alertLink: string = (
940
+ await AlertService.getAlertLinkInDashboard(projectId, alertId)
941
+ ).toString();
942
+
943
+ const confirmationMessage: string = `✅ Message saved as *private note* to <${alertLink}|Alert #${alertNumber}>.`;
944
+
945
+ await SlackUtil.sendMessageToThread({
946
+ authToken: authToken,
947
+ channelId: channelId,
948
+ threadTs: messageTs,
949
+ text: confirmationMessage,
950
+ });
951
+
952
+ logger.debug("Confirmation message sent successfully.");
953
+ } catch (err) {
954
+ logger.error("Error sending confirmation message:");
955
+ logger.error(err);
956
+ // Don't throw - note was saved successfully, confirmation is best effort
957
+ }
958
+ }
776
959
  }
@@ -3,7 +3,10 @@ import ObjectID from "../../../../../Types/ObjectID";
3
3
  import IncidentService from "../../../../Services/IncidentService";
4
4
  import { ExpressRequest, ExpressResponse } from "../../../Express";
5
5
  import SlackUtil from "../Slack";
6
- import SlackActionType from "./ActionTypes";
6
+ import SlackActionType, {
7
+ PrivateNoteEmojis,
8
+ PublicNoteEmojis,
9
+ } from "./ActionTypes";
7
10
  import { SlackAction, SlackRequest } from "./Auth";
8
11
  import Response from "../../../Response";
9
12
  import {
@@ -38,6 +41,11 @@ import LabelService from "../../../../Services/LabelService";
38
41
  import Incident from "../../../../../Models/DatabaseModels/Incident";
39
42
  import AccessTokenService from "../../../../Services/AccessTokenService";
40
43
  import CaptureSpan from "../../../Telemetry/CaptureSpan";
44
+ import WorkspaceProjectAuthTokenService from "../../../../Services/WorkspaceProjectAuthTokenService";
45
+ import WorkspaceUserAuthTokenService from "../../../../Services/WorkspaceUserAuthTokenService";
46
+ import WorkspaceNotificationLog from "../../../../../Models/DatabaseModels/WorkspaceNotificationLog";
47
+ import WorkspaceProjectAuthToken from "../../../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
48
+ import WorkspaceUserAuthToken from "../../../../../Models/DatabaseModels/WorkspaceUserAuthToken";
41
49
 
42
50
  export default class SlackIncidentActions {
43
51
  @CaptureSpan()
@@ -1285,4 +1293,219 @@ export default class SlackIncidentActions {
1285
1293
  new BadDataException("Invalid Action Type"),
1286
1294
  );
1287
1295
  }
1296
+
1297
+ @CaptureSpan()
1298
+ public static async handleEmojiReaction(data: {
1299
+ teamId: string;
1300
+ reaction: string;
1301
+ userId: string;
1302
+ channelId: string;
1303
+ messageTs: string;
1304
+ }): Promise<void> {
1305
+ logger.debug("Handling emoji reaction with data:");
1306
+ logger.debug(data);
1307
+
1308
+ const { teamId, reaction, userId, channelId, messageTs } = data;
1309
+
1310
+ // Check if the emoji is a supported private or public note emoji
1311
+ const isPrivateNoteEmoji: boolean = PrivateNoteEmojis.includes(reaction);
1312
+ const isPublicNoteEmoji: boolean = PublicNoteEmojis.includes(reaction);
1313
+
1314
+ if (!isPrivateNoteEmoji && !isPublicNoteEmoji) {
1315
+ logger.debug(
1316
+ `Emoji "${reaction}" is not a supported note emoji. Ignoring.`,
1317
+ );
1318
+ return;
1319
+ }
1320
+
1321
+ // Get the project auth token using the team ID
1322
+ const projectAuth: WorkspaceProjectAuthToken | null =
1323
+ await WorkspaceProjectAuthTokenService.findOneBy({
1324
+ query: {
1325
+ workspaceProjectId: teamId,
1326
+ },
1327
+ select: {
1328
+ projectId: true,
1329
+ authToken: true,
1330
+ },
1331
+ props: {
1332
+ isRoot: true,
1333
+ },
1334
+ });
1335
+
1336
+ if (!projectAuth || !projectAuth.projectId || !projectAuth.authToken) {
1337
+ logger.debug(
1338
+ "No project auth found for team ID. Ignoring emoji reaction.",
1339
+ );
1340
+ return;
1341
+ }
1342
+
1343
+ const projectId: ObjectID = projectAuth.projectId;
1344
+ const authToken: string = projectAuth.authToken;
1345
+
1346
+ // Find the incident linked to this channel
1347
+ const workspaceLog: WorkspaceNotificationLog | null =
1348
+ await WorkspaceNotificationLogService.findOneBy({
1349
+ query: {
1350
+ channelId: channelId,
1351
+ workspaceType: WorkspaceType.Slack,
1352
+ projectId: projectId,
1353
+ },
1354
+ select: {
1355
+ incidentId: true,
1356
+ },
1357
+ props: {
1358
+ isRoot: true,
1359
+ },
1360
+ });
1361
+
1362
+ if (!workspaceLog || !workspaceLog.incidentId) {
1363
+ logger.debug(
1364
+ "No incident found linked to this channel. Ignoring emoji reaction.",
1365
+ );
1366
+ return;
1367
+ }
1368
+
1369
+ const incidentId: ObjectID = workspaceLog.incidentId;
1370
+
1371
+ // Get the incident number for the confirmation message
1372
+ const incidentNumber: number | null =
1373
+ await IncidentService.getIncidentNumber({
1374
+ incidentId: incidentId,
1375
+ });
1376
+
1377
+ // Get the user ID in OneUptime based on Slack user ID
1378
+ const userAuth: WorkspaceUserAuthToken | null =
1379
+ await WorkspaceUserAuthTokenService.findOneBy({
1380
+ query: {
1381
+ workspaceUserId: userId,
1382
+ workspaceType: WorkspaceType.Slack,
1383
+ projectId: projectId,
1384
+ },
1385
+ select: {
1386
+ userId: true,
1387
+ },
1388
+ props: {
1389
+ isRoot: true,
1390
+ },
1391
+ });
1392
+
1393
+ if (!userAuth || !userAuth.userId) {
1394
+ logger.debug(
1395
+ "No OneUptime user found for Slack user. Ignoring emoji reaction.",
1396
+ );
1397
+ return;
1398
+ }
1399
+
1400
+ const oneUptimeUserId: ObjectID = userAuth.userId;
1401
+
1402
+ // Fetch the message text using the timestamp
1403
+ let messageText: string | null = null;
1404
+ try {
1405
+ messageText = await SlackUtil.getMessageByTimestamp({
1406
+ authToken: authToken,
1407
+ channelId: channelId,
1408
+ messageTs: messageTs,
1409
+ });
1410
+ } catch (err) {
1411
+ logger.error("Error fetching message text:");
1412
+ logger.error(err);
1413
+ return;
1414
+ }
1415
+
1416
+ if (!messageText) {
1417
+ logger.debug("No message text found. Ignoring emoji reaction.");
1418
+ return;
1419
+ }
1420
+
1421
+ // Create a unique identifier for this Slack message to prevent duplicate notes
1422
+ const postedFromSlackMessageId: string = `${channelId}:${messageTs}`;
1423
+
1424
+ // Save the note based on the emoji type
1425
+ let noteType: string;
1426
+ try {
1427
+ if (isPrivateNoteEmoji) {
1428
+ noteType = "private";
1429
+
1430
+ // Check if a note from this Slack message already exists
1431
+ const hasExistingNote: boolean =
1432
+ await IncidentInternalNoteService.hasNoteFromSlackMessage({
1433
+ incidentId: incidentId,
1434
+ postedFromSlackMessageId: postedFromSlackMessageId,
1435
+ });
1436
+
1437
+ if (hasExistingNote) {
1438
+ logger.debug(
1439
+ "Private note from this Slack message already exists. Skipping duplicate.",
1440
+ );
1441
+ return;
1442
+ }
1443
+
1444
+ await IncidentInternalNoteService.addNote({
1445
+ incidentId: incidentId,
1446
+ note: messageText,
1447
+ projectId: projectId,
1448
+ userId: oneUptimeUserId,
1449
+ postedFromSlackMessageId: postedFromSlackMessageId,
1450
+ });
1451
+ logger.debug("Private note added successfully.");
1452
+ } else if (isPublicNoteEmoji) {
1453
+ noteType = "public";
1454
+
1455
+ // Check if a note from this Slack message already exists
1456
+ const hasExistingNote: boolean =
1457
+ await IncidentPublicNoteService.hasNoteFromSlackMessage({
1458
+ incidentId: incidentId,
1459
+ postedFromSlackMessageId: postedFromSlackMessageId,
1460
+ });
1461
+
1462
+ if (hasExistingNote) {
1463
+ logger.debug(
1464
+ "Public note from this Slack message already exists. Skipping duplicate.",
1465
+ );
1466
+ return;
1467
+ }
1468
+
1469
+ await IncidentPublicNoteService.addNote({
1470
+ incidentId: incidentId,
1471
+ note: messageText,
1472
+ projectId: projectId,
1473
+ userId: oneUptimeUserId,
1474
+ postedFromSlackMessageId: postedFromSlackMessageId,
1475
+ });
1476
+ logger.debug("Public note added successfully.");
1477
+ } else {
1478
+ return;
1479
+ }
1480
+ } catch (err) {
1481
+ logger.error("Error saving note:");
1482
+ logger.error(err);
1483
+ return;
1484
+ }
1485
+
1486
+ // Send confirmation message as a reply to the original message thread
1487
+ try {
1488
+ const incidentLink: string = (
1489
+ await IncidentService.getIncidentLinkInDashboard(projectId, incidentId)
1490
+ ).toString();
1491
+
1492
+ const confirmationMessage: string =
1493
+ noteType === "private"
1494
+ ? `✅ Message saved as *private note* to <${incidentLink}|Incident #${incidentNumber}>.`
1495
+ : `✅ Message saved as *public note* to <${incidentLink}|Incident #${incidentNumber}>. This note will be visible on the status page.`;
1496
+
1497
+ await SlackUtil.sendMessageToThread({
1498
+ authToken: authToken,
1499
+ channelId: channelId,
1500
+ threadTs: messageTs,
1501
+ text: confirmationMessage,
1502
+ });
1503
+
1504
+ logger.debug("Confirmation message sent successfully.");
1505
+ } catch (err) {
1506
+ logger.error("Error sending confirmation message:");
1507
+ logger.error(err);
1508
+ // Don't throw - note was saved successfully, confirmation is best effort
1509
+ }
1510
+ }
1288
1511
  }