@oneuptime/common 9.2.16 → 9.2.18

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 (133) hide show
  1. package/Models/DatabaseModels/CodeRepository.ts +664 -0
  2. package/Models/DatabaseModels/Index.ts +8 -0
  3. package/Models/DatabaseModels/LlmLog.ts +818 -0
  4. package/Models/DatabaseModels/LlmProvider.ts +21 -0
  5. package/Models/DatabaseModels/Project.ts +206 -0
  6. package/Models/DatabaseModels/ServiceCatalogCodeRepository.ts +549 -0
  7. package/Server/API/AIBillingAPI.ts +126 -0
  8. package/Server/API/AlertAPI.ts +139 -0
  9. package/Server/API/GitHubAPI.ts +360 -0
  10. package/Server/API/IncidentAPI.ts +258 -0
  11. package/Server/API/ScheduledMaintenanceAPI.ts +164 -0
  12. package/Server/EnvironmentConfig.ts +44 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1765580181582-MigrationName.ts +79 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1765633554715-MigrationName.ts +75 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1765801357168-MigrationName.ts +32 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1765810218488-MigrationName.ts +69 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1765830758857-MigrationName.ts +111 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1765834537501-MigrationName.ts +39 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
  20. package/Server/Services/AIBillingService.ts +247 -0
  21. package/Server/Services/AIService.ts +238 -0
  22. package/Server/Services/CodeRepositoryService.ts +10 -0
  23. package/Server/Services/IncidentService.ts +88 -0
  24. package/Server/Services/Index.ts +2 -0
  25. package/Server/Services/LlmLogService.ts +14 -0
  26. package/Server/Services/LlmProviderService.ts +58 -0
  27. package/Server/Services/ServiceCatalogCodeRepositoryService.ts +55 -0
  28. package/Server/Utils/AI/AlertAIContextBuilder.ts +264 -0
  29. package/Server/Utils/AI/IncidentAIContextBuilder.ts +710 -0
  30. package/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.ts +345 -0
  31. package/Server/Utils/CodeRepository/GitHub/GitHub.ts +226 -0
  32. package/Server/Utils/LLM/LLMService.ts +276 -0
  33. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +166 -0
  34. package/Server/Utils/Workspace/Slack/Slack.ts +134 -0
  35. package/Server/Utils/Workspace/Workspace.ts +126 -0
  36. package/Tests/Types/Domain.test.ts +24 -3
  37. package/Types/CodeRepository/CodeRepositoryType.ts +1 -1
  38. package/Types/Domain.ts +21 -24
  39. package/Types/LlmLogStatus.ts +7 -0
  40. package/Types/Permission.ts +87 -0
  41. package/Types/ServiceCatalog/CodeRepositoryImprovementAction.ts +9 -0
  42. package/UI/Components/AI/AILoader.tsx +95 -0
  43. package/UI/Components/AI/GenerateFromAIModal.tsx +432 -0
  44. package/UI/Components/Modal/Modal.tsx +6 -1
  45. package/build/dist/Models/DatabaseModels/CodeRepository.js +689 -0
  46. package/build/dist/Models/DatabaseModels/CodeRepository.js.map +1 -0
  47. package/build/dist/Models/DatabaseModels/Index.js +7 -0
  48. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  49. package/build/dist/Models/DatabaseModels/LlmLog.js +856 -0
  50. package/build/dist/Models/DatabaseModels/LlmLog.js.map +1 -0
  51. package/build/dist/Models/DatabaseModels/LlmProvider.js +22 -0
  52. package/build/dist/Models/DatabaseModels/LlmProvider.js.map +1 -1
  53. package/build/dist/Models/DatabaseModels/Project.js +220 -0
  54. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  55. package/build/dist/Models/DatabaseModels/ServiceCatalogCodeRepository.js +565 -0
  56. package/build/dist/Models/DatabaseModels/ServiceCatalogCodeRepository.js.map +1 -0
  57. package/build/dist/Server/API/AIBillingAPI.js +58 -0
  58. package/build/dist/Server/API/AIBillingAPI.js.map +1 -0
  59. package/build/dist/Server/API/AlertAPI.js +94 -0
  60. package/build/dist/Server/API/AlertAPI.js.map +1 -0
  61. package/build/dist/Server/API/GitHubAPI.js +207 -0
  62. package/build/dist/Server/API/GitHubAPI.js.map +1 -0
  63. package/build/dist/Server/API/IncidentAPI.js +171 -1
  64. package/build/dist/Server/API/IncidentAPI.js.map +1 -1
  65. package/build/dist/Server/API/ScheduledMaintenanceAPI.js +103 -0
  66. package/build/dist/Server/API/ScheduledMaintenanceAPI.js.map +1 -0
  67. package/build/dist/Server/EnvironmentConfig.js +31 -0
  68. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  69. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765580181582-MigrationName.js +34 -0
  70. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765580181582-MigrationName.js.map +1 -0
  71. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765633554715-MigrationName.js +32 -0
  72. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765633554715-MigrationName.js.map +1 -0
  73. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765801357168-MigrationName.js +38 -0
  74. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765801357168-MigrationName.js.map +1 -0
  75. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765810218488-MigrationName.js +30 -0
  76. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765810218488-MigrationName.js.map +1 -0
  77. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765830758857-MigrationName.js +44 -0
  78. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765830758857-MigrationName.js.map +1 -0
  79. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765834537501-MigrationName.js +22 -0
  80. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1765834537501-MigrationName.js.map +1 -0
  81. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
  82. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  83. package/build/dist/Server/Services/AIBillingService.js +187 -0
  84. package/build/dist/Server/Services/AIBillingService.js.map +1 -0
  85. package/build/dist/Server/Services/AIService.js +184 -0
  86. package/build/dist/Server/Services/AIService.js.map +1 -0
  87. package/build/dist/Server/Services/CodeRepositoryService.js +9 -0
  88. package/build/dist/Server/Services/CodeRepositoryService.js.map +1 -0
  89. package/build/dist/Server/Services/IncidentService.js +60 -0
  90. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  91. package/build/dist/Server/Services/Index.js +2 -0
  92. package/build/dist/Server/Services/Index.js.map +1 -1
  93. package/build/dist/Server/Services/LlmLogService.js +13 -0
  94. package/build/dist/Server/Services/LlmLogService.js.map +1 -0
  95. package/build/dist/Server/Services/LlmProviderService.js +65 -0
  96. package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
  97. package/build/dist/Server/Services/ServiceCatalogCodeRepositoryService.js +54 -0
  98. package/build/dist/Server/Services/ServiceCatalogCodeRepositoryService.js.map +1 -0
  99. package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js +238 -0
  100. package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js.map +1 -0
  101. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js +597 -0
  102. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js.map +1 -0
  103. package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js +311 -0
  104. package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js.map +1 -0
  105. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +163 -0
  106. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
  107. package/build/dist/Server/Utils/LLM/LLMService.js +225 -0
  108. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -0
  109. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +110 -0
  110. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  111. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +89 -0
  112. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  113. package/build/dist/Server/Utils/Workspace/Workspace.js +80 -0
  114. package/build/dist/Server/Utils/Workspace/Workspace.js.map +1 -1
  115. package/build/dist/Tests/Types/Domain.test.js +19 -3
  116. package/build/dist/Tests/Types/Domain.test.js.map +1 -1
  117. package/build/dist/Types/CodeRepository/CodeRepositoryType.js +1 -1
  118. package/build/dist/Types/CodeRepository/CodeRepositoryType.js.map +1 -1
  119. package/build/dist/Types/Domain.js +18 -16
  120. package/build/dist/Types/Domain.js.map +1 -1
  121. package/build/dist/Types/LlmLogStatus.js +8 -0
  122. package/build/dist/Types/LlmLogStatus.js.map +1 -0
  123. package/build/dist/Types/Permission.js +74 -0
  124. package/build/dist/Types/Permission.js.map +1 -1
  125. package/build/dist/Types/ServiceCatalog/CodeRepositoryImprovementAction.js +10 -0
  126. package/build/dist/Types/ServiceCatalog/CodeRepositoryImprovementAction.js.map +1 -0
  127. package/build/dist/UI/Components/AI/AILoader.js +64 -0
  128. package/build/dist/UI/Components/AI/AILoader.js.map +1 -0
  129. package/build/dist/UI/Components/AI/GenerateFromAIModal.js +320 -0
  130. package/build/dist/UI/Components/AI/GenerateFromAIModal.js.map +1 -0
  131. package/build/dist/UI/Components/Modal/Modal.js +6 -1
  132. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  133. package/package.json +1 -1
@@ -0,0 +1,276 @@
1
+ import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
2
+ import HTTPResponse from "../../../Types/API/HTTPResponse";
3
+ import URL from "../../../Types/API/URL";
4
+ import { JSONObject } from "../../../Types/JSON";
5
+ import API from "../../../Utils/API";
6
+ import LlmType from "../../../Types/LLM/LlmType";
7
+ import BadDataException from "../../../Types/Exception/BadDataException";
8
+ import logger from "../Logger";
9
+ import CaptureSpan from "../Telemetry/CaptureSpan";
10
+
11
+ export interface LLMMessage {
12
+ role: "system" | "user" | "assistant";
13
+ content: string;
14
+ }
15
+
16
+ export interface LLMCompletionRequest {
17
+ messages: Array<LLMMessage>;
18
+ temperature?: number;
19
+ llmProviderConfig: LLMProviderConfig;
20
+ }
21
+
22
+ export interface LLMUsage {
23
+ promptTokens: number;
24
+ completionTokens: number;
25
+ totalTokens: number;
26
+ }
27
+
28
+ export interface LLMCompletionResponse {
29
+ content: string;
30
+ usage: LLMUsage | undefined;
31
+ }
32
+
33
+ export interface LLMProviderConfig {
34
+ llmType: LlmType;
35
+ apiKey?: string;
36
+ baseUrl?: string;
37
+ modelName?: string;
38
+ }
39
+
40
+ export default class LLMService {
41
+ @CaptureSpan()
42
+ public static async getCompletion(
43
+ request: LLMCompletionRequest,
44
+ ): Promise<LLMCompletionResponse> {
45
+ const config: LLMProviderConfig = request.llmProviderConfig;
46
+
47
+ switch (config.llmType) {
48
+ case LlmType.OpenAI:
49
+ return await this.getOpenAICompletion(config, request);
50
+ case LlmType.Anthropic:
51
+ return await this.getAnthropicCompletion(config, request);
52
+ case LlmType.Ollama:
53
+ return await this.getOllamaCompletion(config, request);
54
+ default:
55
+ throw new BadDataException(`Unsupported LLM type: ${config.llmType}`);
56
+ }
57
+ }
58
+
59
+ @CaptureSpan()
60
+ private static async getOpenAICompletion(
61
+ config: LLMProviderConfig,
62
+ request: LLMCompletionRequest,
63
+ ): Promise<LLMCompletionResponse> {
64
+ if (!config.apiKey) {
65
+ throw new BadDataException("OpenAI API key is required");
66
+ }
67
+
68
+ const baseUrl: string = config.baseUrl || "https://api.openai.com/v1";
69
+ const modelName: string = config.modelName || "gpt-4o";
70
+
71
+ const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
72
+ await API.post<JSONObject>({
73
+ url: URL.fromString(`${baseUrl}/chat/completions`),
74
+ data: {
75
+ model: modelName,
76
+ messages: request.messages.map((msg: LLMMessage) => {
77
+ return {
78
+ role: msg.role,
79
+ content: msg.content,
80
+ };
81
+ }),
82
+ temperature: request.temperature ?? 0.7,
83
+ },
84
+ headers: {
85
+ Authorization: `Bearer ${config.apiKey}`,
86
+ "Content-Type": "application/json",
87
+ },
88
+ options: {
89
+ retries: 2,
90
+ exponentialBackoff: true,
91
+ timeout: 120000, // 2 minutes timeout for LLM calls
92
+ },
93
+ });
94
+
95
+ if (response instanceof HTTPErrorResponse) {
96
+ logger.error("Error from OpenAI API:");
97
+ logger.error(response);
98
+ throw new BadDataException(
99
+ `OpenAI API error: ${JSON.stringify(response.jsonData)}`,
100
+ );
101
+ }
102
+
103
+ const jsonData: JSONObject = response.jsonData as JSONObject;
104
+ const choices: Array<JSONObject> = jsonData["choices"] as Array<JSONObject>;
105
+
106
+ if (!choices || choices.length === 0) {
107
+ throw new BadDataException("No response from OpenAI");
108
+ }
109
+
110
+ const message: JSONObject = choices[0]!["message"] as JSONObject;
111
+ const usage: JSONObject = jsonData["usage"] as JSONObject;
112
+
113
+ return {
114
+ content: message["content"] as string,
115
+ usage: usage
116
+ ? {
117
+ promptTokens: usage["prompt_tokens"] as number,
118
+ completionTokens: usage["completion_tokens"] as number,
119
+ totalTokens: usage["total_tokens"] as number,
120
+ }
121
+ : undefined,
122
+ };
123
+ }
124
+
125
+ @CaptureSpan()
126
+ private static async getAnthropicCompletion(
127
+ config: LLMProviderConfig,
128
+ request: LLMCompletionRequest,
129
+ ): Promise<LLMCompletionResponse> {
130
+ if (!config.apiKey) {
131
+ throw new BadDataException("Anthropic API key is required");
132
+ }
133
+
134
+ const baseUrl: string = config.baseUrl || "https://api.anthropic.com/v1";
135
+ const modelName: string = config.modelName || "claude-sonnet-4-20250514";
136
+
137
+ // Anthropic requires system message to be separate
138
+ let systemMessage: string = "";
139
+ const userMessages: Array<{ role: string; content: string }> = [];
140
+
141
+ for (const msg of request.messages) {
142
+ if (msg.role === "system") {
143
+ systemMessage = msg.content;
144
+ } else {
145
+ userMessages.push({
146
+ role: msg.role,
147
+ content: msg.content,
148
+ });
149
+ }
150
+ }
151
+
152
+ const requestData: JSONObject = {
153
+ model: modelName,
154
+ messages: userMessages,
155
+ temperature: request.temperature ?? 0.7,
156
+ };
157
+
158
+ if (systemMessage) {
159
+ requestData["system"] = systemMessage;
160
+ }
161
+
162
+ const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
163
+ await API.post<JSONObject>({
164
+ url: URL.fromString(`${baseUrl}/messages`),
165
+ data: requestData,
166
+ headers: {
167
+ "x-api-key": config.apiKey,
168
+ "anthropic-version": "2023-06-01",
169
+ "Content-Type": "application/json",
170
+ },
171
+ options: {
172
+ retries: 2,
173
+ exponentialBackoff: true,
174
+ timeout: 120000,
175
+ },
176
+ });
177
+
178
+ if (response instanceof HTTPErrorResponse) {
179
+ logger.error("Error from Anthropic API:");
180
+ logger.error(response);
181
+ throw new BadDataException(
182
+ `Anthropic API error: ${JSON.stringify(response.jsonData)}`,
183
+ );
184
+ }
185
+
186
+ const jsonData: JSONObject = response.jsonData as JSONObject;
187
+ const content: Array<JSONObject> = jsonData["content"] as Array<JSONObject>;
188
+
189
+ if (!content || content.length === 0) {
190
+ throw new BadDataException("No response from Anthropic");
191
+ }
192
+
193
+ const textContent: JSONObject | undefined = content.find(
194
+ (c: JSONObject) => {
195
+ return c["type"] === "text";
196
+ },
197
+ );
198
+
199
+ if (!textContent) {
200
+ throw new BadDataException("No text content in Anthropic response");
201
+ }
202
+
203
+ const usage: JSONObject = jsonData["usage"] as JSONObject;
204
+
205
+ return {
206
+ content: textContent["text"] as string,
207
+ usage: usage
208
+ ? {
209
+ promptTokens: usage["input_tokens"] as number,
210
+ completionTokens: usage["output_tokens"] as number,
211
+ totalTokens:
212
+ ((usage["input_tokens"] as number) || 0) +
213
+ ((usage["output_tokens"] as number) || 0),
214
+ }
215
+ : undefined,
216
+ };
217
+ }
218
+
219
+ @CaptureSpan()
220
+ private static async getOllamaCompletion(
221
+ config: LLMProviderConfig,
222
+ request: LLMCompletionRequest,
223
+ ): Promise<LLMCompletionResponse> {
224
+ if (!config.baseUrl) {
225
+ throw new BadDataException("Ollama base URL is required");
226
+ }
227
+
228
+ const modelName: string = config.modelName || "llama2";
229
+
230
+ const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
231
+ await API.post<JSONObject>({
232
+ url: URL.fromString(`${config.baseUrl}/api/chat`),
233
+ data: {
234
+ model: modelName,
235
+ messages: request.messages.map((msg: LLMMessage) => {
236
+ return {
237
+ role: msg.role,
238
+ content: msg.content,
239
+ };
240
+ }),
241
+ stream: false,
242
+ options: {
243
+ temperature: request.temperature ?? 0.7,
244
+ },
245
+ },
246
+ headers: {
247
+ "Content-Type": "application/json",
248
+ },
249
+ options: {
250
+ retries: 2,
251
+ exponentialBackoff: true,
252
+ timeout: 300000, // 5 minutes for Ollama as it may be slower
253
+ },
254
+ });
255
+
256
+ if (response instanceof HTTPErrorResponse) {
257
+ logger.error("Error from Ollama API:");
258
+ logger.error(response);
259
+ throw new BadDataException(
260
+ `Ollama API error: ${JSON.stringify(response.jsonData)}`,
261
+ );
262
+ }
263
+
264
+ const jsonData: JSONObject = response.jsonData as JSONObject;
265
+ const message: JSONObject = jsonData["message"] as JSONObject;
266
+
267
+ if (!message) {
268
+ throw new BadDataException("No response from Ollama");
269
+ }
270
+
271
+ return {
272
+ content: message["content"] as string,
273
+ usage: undefined, // Ollama doesn't provide token usage in the same way
274
+ };
275
+ }
276
+ }
@@ -1,3 +1,4 @@
1
+ import { WorkspaceChannelMessage } from "../Workspace";
1
2
  import HTTPErrorResponse from "../../../../Types/API/HTTPErrorResponse";
2
3
  import HTTPResponse from "../../../../Types/API/HTTPResponse";
3
4
  import URL from "../../../../Types/API/URL";
@@ -3062,4 +3063,169 @@ All monitoring checks are passing normally.`;
3062
3063
  throw error;
3063
3064
  }
3064
3065
  }
3066
+
3067
+ @CaptureSpan()
3068
+ public static async getChannelMessages(params: {
3069
+ channelId: string;
3070
+ teamId: string;
3071
+ projectId: ObjectID;
3072
+ limit?: number;
3073
+ oldestTimestamp?: Date;
3074
+ }): Promise<
3075
+ Array<{
3076
+ messageId: string;
3077
+ text: string;
3078
+ userId?: string;
3079
+ username?: string;
3080
+ timestamp: Date;
3081
+ isBot: boolean;
3082
+ }>
3083
+ > {
3084
+ const messages: Array<{
3085
+ messageId: string;
3086
+ text: string;
3087
+ userId?: string;
3088
+ username?: string;
3089
+ timestamp: Date;
3090
+ isBot: boolean;
3091
+ }> = [];
3092
+
3093
+ try {
3094
+ // Get valid access token
3095
+ const projectAuth: WorkspaceProjectAuthToken | null =
3096
+ await WorkspaceProjectAuthTokenService.getProjectAuth({
3097
+ projectId: params.projectId,
3098
+ workspaceType: WorkspaceType.MicrosoftTeams,
3099
+ });
3100
+
3101
+ if (!projectAuth || !projectAuth.miscData) {
3102
+ logger.error("Microsoft Teams integration not found for this project");
3103
+ return messages;
3104
+ }
3105
+
3106
+ const miscData: JSONObject = projectAuth.miscData as JSONObject;
3107
+ const accessToken: string = miscData["appAccessToken"] as string;
3108
+ const tokenExpiresAt: string = miscData[
3109
+ "appAccessTokenExpiresAt"
3110
+ ] as string;
3111
+
3112
+ // Check if token is expired
3113
+ if (
3114
+ !accessToken ||
3115
+ (tokenExpiresAt &&
3116
+ OneUptimeDate.isInThePast(OneUptimeDate.fromString(tokenExpiresAt)))
3117
+ ) {
3118
+ logger.debug(
3119
+ "Microsoft Teams access token expired or missing, skipping message fetch",
3120
+ );
3121
+ return messages;
3122
+ }
3123
+
3124
+ // Fetch messages from Microsoft Teams channel
3125
+ let nextLink: string | undefined = undefined;
3126
+ const maxMessages: number = params.limit || 1000;
3127
+ const maxPages: number = 10;
3128
+ let pageCount: number = 0;
3129
+
3130
+ do {
3131
+ let requestUrl: string;
3132
+
3133
+ if (nextLink) {
3134
+ requestUrl = nextLink;
3135
+ } else {
3136
+ requestUrl = `https://graph.microsoft.com/v1.0/teams/${params.teamId}/channels/${params.channelId}/messages`;
3137
+ requestUrl += `?$top=${Math.min(50, maxMessages - messages.length)}`;
3138
+ }
3139
+
3140
+ const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
3141
+ await API.get<JSONObject>({
3142
+ url: URL.fromString(requestUrl),
3143
+ headers: {
3144
+ Authorization: `Bearer ${accessToken}`,
3145
+ "Content-Type": "application/json",
3146
+ },
3147
+ options: {
3148
+ retries: 2,
3149
+ exponentialBackoff: true,
3150
+ },
3151
+ });
3152
+
3153
+ if (response instanceof HTTPErrorResponse) {
3154
+ logger.error(
3155
+ "Error response from Microsoft Teams API for channel messages:",
3156
+ );
3157
+ logger.error(response);
3158
+ break;
3159
+ }
3160
+
3161
+ const jsonData: JSONObject = response.jsonData as JSONObject;
3162
+ const teamsMessages: Array<JSONObject> =
3163
+ (jsonData["value"] as Array<JSONObject>) || [];
3164
+
3165
+ for (const msg of teamsMessages) {
3166
+ // Skip system messages
3167
+ if (msg["messageType"] !== "message") {
3168
+ continue;
3169
+ }
3170
+
3171
+ const body: JSONObject = msg["body"] as JSONObject;
3172
+ let text: string = (body?.["content"] as string) || "";
3173
+
3174
+ // Remove HTML tags if present (Teams uses HTML)
3175
+ text = text.replace(/<[^>]*>/g, "");
3176
+ text = text.trim();
3177
+
3178
+ // Skip empty messages
3179
+ if (!text) {
3180
+ continue;
3181
+ }
3182
+
3183
+ const from: JSONObject = msg["from"] as JSONObject;
3184
+ const user: JSONObject = from?.["user"] as JSONObject;
3185
+ const isBot: boolean = Boolean(from?.["application"]);
3186
+
3187
+ const createdDateTime: string = msg["createdDateTime"] as string;
3188
+ const timestamp: Date = createdDateTime
3189
+ ? new Date(createdDateTime)
3190
+ : new Date();
3191
+
3192
+ // Check if message is older than the oldest timestamp filter
3193
+ if (params.oldestTimestamp && timestamp < params.oldestTimestamp) {
3194
+ continue;
3195
+ }
3196
+
3197
+ messages.push({
3198
+ messageId: msg["id"] as string,
3199
+ text: text,
3200
+ userId: user?.["id"] as string,
3201
+ username: user?.["displayName"] as string,
3202
+ timestamp: timestamp,
3203
+ isBot: isBot,
3204
+ });
3205
+ }
3206
+
3207
+ nextLink = jsonData["@odata.nextLink"] as string;
3208
+ pageCount++;
3209
+ } while (
3210
+ nextLink &&
3211
+ messages.length < maxMessages &&
3212
+ pageCount < maxPages
3213
+ );
3214
+
3215
+ logger.debug(
3216
+ `Retrieved ${messages.length} messages from Microsoft Teams channel ${params.channelId}`,
3217
+ );
3218
+
3219
+ // Sort by timestamp (oldest first)
3220
+ messages.sort(
3221
+ (a: WorkspaceChannelMessage, b: WorkspaceChannelMessage) => {
3222
+ return a.timestamp.getTime() - b.timestamp.getTime();
3223
+ },
3224
+ );
3225
+ } catch (error) {
3226
+ logger.error(`Error fetching Microsoft Teams channel messages: ${error}`);
3227
+ }
3228
+
3229
+ return messages;
3230
+ }
3065
3231
  }
@@ -1896,4 +1896,138 @@ export default class SlackUtil extends WorkspaceBase {
1896
1896
  public static convertMarkdownToSlackRichText(markdown: string): string {
1897
1897
  return SlackifyMarkdown(markdown);
1898
1898
  }
1899
+
1900
+ @CaptureSpan()
1901
+ public static async getChannelMessages(params: {
1902
+ channelId: string;
1903
+ authToken: string;
1904
+ limit?: number;
1905
+ oldestTimestamp?: Date;
1906
+ }): Promise<
1907
+ Array<{
1908
+ messageId: string;
1909
+ text: string;
1910
+ userId?: string;
1911
+ username?: string;
1912
+ timestamp: Date;
1913
+ isBot: boolean;
1914
+ }>
1915
+ > {
1916
+ const messages: Array<{
1917
+ messageId: string;
1918
+ text: string;
1919
+ userId?: string;
1920
+ username?: string;
1921
+ timestamp: Date;
1922
+ isBot: boolean;
1923
+ }> = [];
1924
+ let cursor: string | undefined = undefined;
1925
+ const maxMessages: number = params.limit || 1000;
1926
+ const maxPages: number = 10;
1927
+ let pageCount: number = 0;
1928
+
1929
+ do {
1930
+ const requestData: JSONObject = {
1931
+ channel: params.channelId,
1932
+ limit: Math.min(200, maxMessages - messages.length),
1933
+ };
1934
+
1935
+ if (cursor) {
1936
+ requestData["cursor"] = cursor;
1937
+ }
1938
+
1939
+ if (params.oldestTimestamp) {
1940
+ requestData["oldest"] = (
1941
+ params.oldestTimestamp.getTime() / 1000
1942
+ ).toString();
1943
+ }
1944
+
1945
+ const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
1946
+ await API.post<JSONObject>({
1947
+ url: URL.fromString("https://slack.com/api/conversations.history"),
1948
+ data: requestData,
1949
+ headers: {
1950
+ Authorization: `Bearer ${params.authToken}`,
1951
+ "Content-Type": "application/x-www-form-urlencoded",
1952
+ },
1953
+ options: {
1954
+ retries: 3,
1955
+ exponentialBackoff: true,
1956
+ },
1957
+ });
1958
+
1959
+ if (response instanceof HTTPErrorResponse) {
1960
+ logger.error("Error response from Slack API for channel history:");
1961
+ logger.error(response);
1962
+ break;
1963
+ }
1964
+
1965
+ const jsonData: JSONObject = response.jsonData as JSONObject;
1966
+
1967
+ if (jsonData["ok"] !== true) {
1968
+ logger.error("Invalid response from Slack API for channel history:");
1969
+ logger.error(jsonData);
1970
+ break;
1971
+ }
1972
+
1973
+ const slackMessages: Array<JSONObject> =
1974
+ (jsonData["messages"] as Array<JSONObject>) || [];
1975
+
1976
+ for (const msg of slackMessages) {
1977
+ // Skip bot messages if they're from the OneUptime bot (app messages)
1978
+ const isBot: boolean =
1979
+ Boolean(msg["bot_id"]) || msg["subtype"] === "bot_message";
1980
+
1981
+ // Extract text, handling attachments and blocks
1982
+ let text: string = (msg["text"] as string) || "";
1983
+
1984
+ // If there are attachments, append their text
1985
+ const attachments: Array<JSONObject> | undefined = msg[
1986
+ "attachments"
1987
+ ] as Array<JSONObject> | undefined;
1988
+ if (attachments && Array.isArray(attachments)) {
1989
+ for (const attachment of attachments) {
1990
+ if (attachment && attachment["text"]) {
1991
+ text += "\n" + (attachment["text"] as string);
1992
+ }
1993
+ if (attachment && attachment["fallback"]) {
1994
+ text += "\n" + (attachment["fallback"] as string);
1995
+ }
1996
+ }
1997
+ }
1998
+
1999
+ // Skip empty messages
2000
+ if (!text.trim()) {
2001
+ continue;
2002
+ }
2003
+
2004
+ const timestamp: Date = msg["ts"]
2005
+ ? new Date(parseFloat(msg["ts"] as string) * 1000)
2006
+ : new Date();
2007
+
2008
+ messages.push({
2009
+ messageId: msg["ts"] as string,
2010
+ text: text,
2011
+ userId: msg["user"] as string,
2012
+ username: msg["username"] as string,
2013
+ timestamp: timestamp,
2014
+ isBot: isBot,
2015
+ });
2016
+ }
2017
+
2018
+ cursor = (jsonData["response_metadata"] as JSONObject)?.[
2019
+ "next_cursor"
2020
+ ] as string;
2021
+ pageCount++;
2022
+ } while (cursor && messages.length < maxMessages && pageCount < maxPages);
2023
+
2024
+ logger.debug(
2025
+ `Retrieved ${messages.length} messages from Slack channel ${params.channelId}`,
2026
+ );
2027
+
2028
+ // Reverse to get chronological order (Slack returns newest first)
2029
+ messages.reverse();
2030
+
2031
+ return messages;
2032
+ }
1899
2033
  }