@oneuptime/common 11.7.1 → 11.7.2

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 (237) hide show
  1. package/Models/DatabaseModels/DashboardDomain.ts +1 -0
  2. package/Models/DatabaseModels/Incident.ts +2 -0
  3. package/Models/DatabaseModels/IncidentEpisode.ts +1 -0
  4. package/Models/DatabaseModels/IncidentEpisodePublicNote.ts +1 -0
  5. package/Models/DatabaseModels/IncidentEpisodeStateTimeline.ts +1 -0
  6. package/Models/DatabaseModels/IncidentPublicNote.ts +1 -0
  7. package/Models/DatabaseModels/IncidentStateTimeline.ts +1 -0
  8. package/Models/DatabaseModels/Monitor.ts +1 -0
  9. package/Models/DatabaseModels/MonitorStatusTimeline.ts +1 -0
  10. package/Models/DatabaseModels/NetworkSite.ts +1 -0
  11. package/Models/DatabaseModels/NetworkSiteStatusTimeline.ts +1 -0
  12. package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +1 -0
  13. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +2 -0
  14. package/Models/DatabaseModels/Probe.ts +13 -13
  15. package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -0
  16. package/Models/DatabaseModels/ScheduledMaintenancePublicNote.ts +1 -0
  17. package/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.ts +1 -0
  18. package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -0
  19. package/Models/DatabaseModels/StatusPage.ts +1 -0
  20. package/Models/DatabaseModels/StatusPageAnnouncement.ts +1 -0
  21. package/Models/DatabaseModels/StatusPageDomain.ts +1 -0
  22. package/Models/DatabaseModels/WorkspaceNotificationSummary.ts +1 -0
  23. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +11 -0
  24. package/Server/API/BaseAPI.ts +17 -1
  25. package/Server/API/CommonAPI.ts +32 -0
  26. package/Server/API/MicrosoftTeamsAPI.ts +99 -2
  27. package/Server/API/ProbeAPI.ts +2 -0
  28. package/Server/API/SlackAPI.ts +62 -0
  29. package/Server/API/StatusPageAPI.ts +157 -192
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.ts +89 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.ts +61 -0
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.ts +280 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  34. package/Server/Services/DatabaseService.ts +11 -3
  35. package/Server/Services/MonitorGroupService.ts +145 -0
  36. package/Server/Services/MonitorService.ts +231 -30
  37. package/Server/Services/ProbeService.ts +319 -16
  38. package/Server/Services/StatusPageService.ts +72 -42
  39. package/Server/Services/WorkspaceNotificationRuleService.ts +232 -2
  40. package/Server/Utils/Database/PostgresErrorTranslator.ts +131 -0
  41. package/Server/Utils/Monitor/MonitorResource.ts +88 -35
  42. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +21 -1
  43. package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +158 -0
  44. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +706 -6
  45. package/Tests/Models/DatabaseModels/ProbeColumnAccessControl.test.ts +103 -0
  46. package/Tests/Server/API/BaseAPIUpdateNoOp.test.ts +135 -0
  47. package/Tests/Server/API/CommonAPIAuthGuard.test.ts +317 -0
  48. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +228 -0
  49. package/Tests/Server/API/ProbeAPI.test.ts +6 -0
  50. package/Tests/Server/API/ZZVerifyChatsAuth.test.ts +167 -0
  51. package/Tests/Server/Services/AddHotQueryIndexesMigration.test.ts +255 -0
  52. package/Tests/Server/Services/AddHotQueryIndexesSecondPassMigration.test.ts +258 -0
  53. package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +1 -1
  54. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +121 -0
  55. package/Tests/Server/Services/HeartbeatWriteFastPathSafety.test.ts +200 -0
  56. package/Tests/Server/Services/HookFreeWriteSafetySecondPass.test.ts +181 -0
  57. package/Tests/Server/Services/MonitorGroupServiceBatchedStatuses.test.ts +513 -0
  58. package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +492 -0
  59. package/Tests/Server/Services/MonitorServiceProbeSelection.test.ts +401 -0
  60. package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +453 -0
  61. package/Tests/Server/Services/ProbeAuthKeyCacheAndHeartbeat.test.ts +679 -0
  62. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +3 -3
  63. package/Tests/Server/Services/StatusPageDomainResolutionCache.test.ts +245 -0
  64. package/Tests/Server/Services/StatusPageOverviewTimelineWindow.test.ts +283 -0
  65. package/Tests/Server/Services/StatusPageServiceMcp.test.ts +17 -1
  66. package/Tests/Server/Services/StatusPageServiceMonitorGroupBatching.test.ts +331 -0
  67. package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +12 -12
  68. package/Tests/Server/Services/WorkspaceNotificationRuleChats.test.ts +1390 -0
  69. package/Tests/Server/Utils/AI/Insights/InsightFixRouting.test.ts +4 -4
  70. package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +2 -2
  71. package/Tests/Server/Utils/AI/Insights/InsightStore.test.ts +1 -3
  72. package/Tests/Server/Utils/AI/Insights/InsightStoreHardening.test.ts +1 -3
  73. package/Tests/Server/Utils/AI/Insights/InsightTriage.test.ts +1 -1
  74. package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +5 -5
  75. package/Tests/Server/Utils/Alert/AlertPrivacyFilter.test.ts +187 -0
  76. package/Tests/Server/Utils/AlertEpisode/AlertEpisodePrivacyFilter.test.ts +170 -0
  77. package/Tests/Server/Utils/Database/PostgresErrorTranslator.test.ts +125 -0
  78. package/Tests/Server/Utils/Incident/IncidentPrivacyFilter.test.ts +264 -0
  79. package/Tests/Server/Utils/IncidentEpisode/IncidentEpisodePrivacyFilter.test.ts +175 -0
  80. package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +181 -0
  81. package/Tests/Server/Utils/Monitor/NetworkDeviceWalkUtil.test.ts +1 -1
  82. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +2 -2
  83. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelsList.test.ts +341 -0
  84. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +2175 -0
  85. package/Tests/Server/Utils/Workspace/SlackChannelsList.test.ts +585 -0
  86. package/Tests/Types/Billing/MeteredPlan.test.ts +46 -0
  87. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +219 -1
  88. package/Tests/UI/Components/BulkUpdateForm.test.tsx +174 -0
  89. package/Tests/UI/Components/CardModelDetailEdit.test.tsx +315 -0
  90. package/Tests/UI/Components/ModelDetailSelect.test.tsx +229 -0
  91. package/Tests/UI/Components/ModelTable/BaseModelTableBulkSelectAll.test.tsx +1258 -0
  92. package/Tests/UI/Components/ModelTableExportFromColumns.test.ts +153 -0
  93. package/Tests/UI/Components/MonacoLoader.test.ts +67 -0
  94. package/Tests/UI/Components/MonacoRuntime.test.ts +121 -0
  95. package/Tests/UI/Components/TableBulkCsvExport.test.tsx +212 -3
  96. package/Tests/UI/Utils/Project.test.ts +809 -0
  97. package/Tests/UI/Utils/TableColumnsToCsv.test.ts +360 -0
  98. package/Tests/Utils/Alerts/AlertMetricType.test.ts +143 -0
  99. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +219 -0
  100. package/Tests/Utils/Incident/IncidentMetricType.test.ts +174 -0
  101. package/Tests/Utils/Monitor/MonitorProbeSelectionUtil.test.ts +145 -0
  102. package/Tests/Utils/StatusPage/ResourceUptime.test.ts +263 -0
  103. package/Types/Workspace/NotificationRules/BaseNotificationRule.ts +4 -0
  104. package/Types/Workspace/NotificationRules/NotificationRuleCondition.ts +25 -10
  105. package/Types/Workspace/WorkspaceMessagePayload.ts +1 -0
  106. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +63 -8
  107. package/UI/Components/CodeEditor/CodeEditor.tsx +3 -0
  108. package/UI/Components/CodeEditor/MonacoLoader.ts +36 -0
  109. package/UI/Components/Forms/ModelForm.tsx +41 -18
  110. package/UI/Components/ModelDetail/CardModelDetail.tsx +23 -1
  111. package/UI/Components/ModelDetail/ModelDetail.tsx +46 -1
  112. package/UI/Components/ModelTable/BaseModelTable.tsx +300 -30
  113. package/UI/Components/ModelTable/Column.ts +12 -0
  114. package/UI/Components/ModelTable/ExportFromColumns.ts +57 -0
  115. package/UI/Components/Navbar/NavBar.tsx +66 -1
  116. package/UI/Components/Table/Table.tsx +52 -13
  117. package/UI/Components/Table/Types/Column.ts +18 -0
  118. package/UI/Utils/Project.ts +214 -18
  119. package/UI/Utils/TableColumnsToCsv.ts +86 -6
  120. package/UI/esbuild-config.js +34 -0
  121. package/Utils/Monitor/MonitorProbeSelectionUtil.ts +72 -0
  122. package/build/dist/Models/DatabaseModels/DashboardDomain.js +1 -0
  123. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  124. package/build/dist/Models/DatabaseModels/Incident.js +2 -0
  125. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  126. package/build/dist/Models/DatabaseModels/IncidentEpisode.js +1 -0
  127. package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
  128. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js +1 -0
  129. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js.map +1 -1
  130. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js +1 -0
  131. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js.map +1 -1
  132. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +1 -0
  133. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
  134. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +1 -0
  135. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
  136. package/build/dist/Models/DatabaseModels/Monitor.js +1 -0
  137. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  138. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +1 -0
  139. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
  140. package/build/dist/Models/DatabaseModels/NetworkSite.js +1 -0
  141. package/build/dist/Models/DatabaseModels/NetworkSite.js.map +1 -1
  142. package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js +1 -0
  143. package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js.map +1 -1
  144. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +1 -0
  145. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
  146. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +2 -0
  147. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  148. package/build/dist/Models/DatabaseModels/Probe.js +13 -13
  149. package/build/dist/Models/DatabaseModels/Probe.js.map +1 -1
  150. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -0
  151. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  152. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js +1 -0
  153. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js.map +1 -1
  154. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js +1 -0
  155. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js.map +1 -1
  156. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -0
  157. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
  158. package/build/dist/Models/DatabaseModels/StatusPage.js +1 -0
  159. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  160. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +1 -0
  161. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
  162. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +1 -0
  163. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  164. package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js +1 -0
  165. package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js.map +1 -1
  166. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  167. package/build/dist/Server/API/BaseAPI.js +12 -1
  168. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  169. package/build/dist/Server/API/CommonAPI.js +21 -0
  170. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  171. package/build/dist/Server/API/MicrosoftTeamsAPI.js +72 -2
  172. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  173. package/build/dist/Server/API/ProbeAPI.js +2 -0
  174. package/build/dist/Server/API/ProbeAPI.js.map +1 -1
  175. package/build/dist/Server/API/SlackAPI.js +44 -0
  176. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  177. package/build/dist/Server/API/StatusPageAPI.js +98 -152
  178. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  179. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js +36 -0
  180. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js.map +1 -0
  181. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js +26 -0
  182. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js.map +1 -0
  183. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js +254 -0
  184. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js.map +1 -0
  185. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  186. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  187. package/build/dist/Server/Services/DatabaseService.js +10 -2
  188. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  189. package/build/dist/Server/Services/MonitorGroupService.js +118 -0
  190. package/build/dist/Server/Services/MonitorGroupService.js.map +1 -1
  191. package/build/dist/Server/Services/MonitorService.js +198 -24
  192. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  193. package/build/dist/Server/Services/ProbeService.js +238 -9
  194. package/build/dist/Server/Services/ProbeService.js.map +1 -1
  195. package/build/dist/Server/Services/StatusPageService.js +46 -39
  196. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  197. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +178 -3
  198. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  199. package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js +80 -0
  200. package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js.map +1 -0
  201. package/build/dist/Server/Utils/Monitor/MonitorResource.js +67 -29
  202. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  203. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +31 -14
  204. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  205. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +117 -0
  206. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -0
  207. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +549 -7
  208. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  209. package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js +17 -7
  210. package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js.map +1 -1
  211. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +37 -11
  212. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  213. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +2 -0
  214. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  215. package/build/dist/UI/Components/CodeEditor/MonacoLoader.js +34 -0
  216. package/build/dist/UI/Components/CodeEditor/MonacoLoader.js.map +1 -0
  217. package/build/dist/UI/Components/Forms/ModelForm.js +37 -17
  218. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  219. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +22 -1
  220. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  221. package/build/dist/UI/Components/ModelDetail/ModelDetail.js +31 -1
  222. package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
  223. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +234 -21
  224. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  225. package/build/dist/UI/Components/ModelTable/ExportFromColumns.js +30 -0
  226. package/build/dist/UI/Components/ModelTable/ExportFromColumns.js.map +1 -0
  227. package/build/dist/UI/Components/Navbar/NavBar.js +39 -1
  228. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  229. package/build/dist/UI/Components/Table/Table.js +31 -11
  230. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  231. package/build/dist/UI/Utils/Project.js +161 -15
  232. package/build/dist/UI/Utils/Project.js.map +1 -1
  233. package/build/dist/UI/Utils/TableColumnsToCsv.js +67 -6
  234. package/build/dist/UI/Utils/TableColumnsToCsv.js.map +1 -1
  235. package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js +52 -0
  236. package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js.map +1 -0
  237. package/package.json +4 -3
@@ -31,6 +31,8 @@ import BadDataException from "../../../../Types/Exception/BadDataException";
31
31
  import ObjectID from "../../../../Types/ObjectID";
32
32
  import WorkspaceProjectAuthTokenService from "../../../Services/WorkspaceProjectAuthTokenService";
33
33
  import WorkspaceProjectAuthToken, {
34
+ MicrosoftTeamsChat,
35
+ MicrosoftTeamsChatType,
34
36
  MicrosoftTeamsMiscData,
35
37
  } from "../../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
36
38
  import Incident from "../../../../Models/DatabaseModels/Incident";
@@ -60,12 +62,16 @@ import UserService from "../../../Services/UserService";
60
62
  // Import database utilities
61
63
  import QueryHelper from "../../../Types/Database/QueryHelper";
62
64
  import SortOrder from "../../../../Types/BaseDatabase/SortOrder";
65
+ import LIMIT_MAX from "../../../../Types/Database/LimitMax";
63
66
 
64
67
  // Bot Framework SDK imports
65
68
  import {
66
69
  CloudAdapter,
67
70
  ConfigurationBotFrameworkAuthentication,
68
71
  TeamsActivityHandler,
72
+ TeamsInfo,
73
+ TeamsChannelAccount,
74
+ TeamsPagedMembersResult,
69
75
  TurnContext,
70
76
  ConversationReference,
71
77
  MessageFactory,
@@ -387,9 +393,30 @@ export default class MicrosoftTeamsUtil extends WorkspaceBase {
387
393
  `Token expiry calculated: ${OneUptimeDate.toString(expiryDate)}`,
388
394
  );
389
395
 
390
- // Update the miscData with new token and expiry
396
+ /*
397
+ * Merge the token fields into a FRESH read of miscData instead of the
398
+ * snapshot taken before the OAuth round-trip. The snapshot can be
399
+ * seconds old, and writing it back verbatim would erase concurrent
400
+ * miscData updates — in particular chats captured into availableChats
401
+ * by bot install events, which cannot be re-derived from Graph.
402
+ */
403
+ let latestMiscData: MicrosoftTeamsMiscData = data.miscData;
404
+ try {
405
+ const latestProjectAuth: WorkspaceProjectAuthToken | null =
406
+ await WorkspaceProjectAuthTokenService.getProjectAuth({
407
+ projectId: data.projectId,
408
+ workspaceType: WorkspaceType.MicrosoftTeams,
409
+ });
410
+ if (latestProjectAuth?.miscData) {
411
+ latestMiscData = latestProjectAuth.miscData as MicrosoftTeamsMiscData;
412
+ }
413
+ } catch (err) {
414
+ logger.debug("Could not re-read miscData before token refresh write");
415
+ logger.debug(err);
416
+ }
417
+
391
418
  const updatedMiscData: MicrosoftTeamsMiscData = {
392
- ...data.miscData,
419
+ ...latestMiscData,
393
420
  appAccessToken: newAccessToken,
394
421
  appAccessTokenExpiresAt: OneUptimeDate.toString(expiryDate),
395
422
  lastAppTokenIssuedAt: OneUptimeDate.toString(now),
@@ -1223,6 +1250,55 @@ export default class MicrosoftTeamsUtil extends WorkspaceBase {
1223
1250
  }
1224
1251
  }
1225
1252
 
1253
+ // Send to Teams chats (group / personal chats the OneUptime app was added to).
1254
+ const chatIdsToPostTo: Array<string> =
1255
+ data.workspaceMessagePayload.chatIds || [];
1256
+
1257
+ if (chatIdsToPostTo.length > 0) {
1258
+ logger.debug(`Processing ${chatIdsToPostTo.length} chat ids`);
1259
+
1260
+ const availableChats: Record<string, MicrosoftTeamsChat> =
1261
+ await this.getChatsForProject({
1262
+ projectId: data.projectId,
1263
+ });
1264
+
1265
+ for (const chatId of chatIdsToPostTo) {
1266
+ const chatAsChannel: WorkspaceChannel = {
1267
+ id: chatId,
1268
+ name: availableChats[chatId]?.name || chatId,
1269
+ workspaceType: WorkspaceType.MicrosoftTeams,
1270
+ };
1271
+
1272
+ try {
1273
+ let lastThread: WorkspaceThread | undefined;
1274
+ for (const adaptiveCard of adaptiveCards) {
1275
+ lastThread = await this.sendAdaptiveCardToChat({
1276
+ chatId: chatId,
1277
+ adaptiveCard: adaptiveCard,
1278
+ projectId: data.projectId,
1279
+ });
1280
+ }
1281
+
1282
+ if (lastThread) {
1283
+ logger.debug(
1284
+ `Message sent successfully to chat ${chatAsChannel.name}, thread: ${JSON.stringify(lastThread)}`,
1285
+ );
1286
+ workspaceMessageResponse.threads.push(lastThread);
1287
+ }
1288
+ } catch (e) {
1289
+ logger.error(`Error sending message to chat ID ${chatId}:`, {
1290
+ projectId: data.projectId.toString(),
1291
+ chatId: chatId,
1292
+ });
1293
+ logger.error(e);
1294
+ workspaceMessageResponse.errors!.push({
1295
+ channel: chatAsChannel,
1296
+ error: e instanceof Error ? e.message : String(e),
1297
+ });
1298
+ }
1299
+ }
1300
+ }
1301
+
1226
1302
  logger.debug("=== Message sending completed ===");
1227
1303
  logger.debug(
1228
1304
  `Final thread count: ${workspaceMessageResponse.threads.length}`,
@@ -1388,6 +1464,137 @@ export default class MicrosoftTeamsUtil extends WorkspaceBase {
1388
1464
  }
1389
1465
  }
1390
1466
 
1467
+ /*
1468
+ * Sends an adaptive card to a Teams group chat or personal (1:1) chat via a
1469
+ * proactive Bot Framework message. The OneUptime app must have been added to
1470
+ * the chat — Graph has no app-only permission for posting to chats, so the
1471
+ * bot conversation is the only supported transport.
1472
+ */
1473
+ @CaptureSpan()
1474
+ public static async sendAdaptiveCardToChat(data: {
1475
+ chatId: string;
1476
+ projectId: ObjectID;
1477
+ adaptiveCard: JSONObject;
1478
+ }): Promise<WorkspaceThread> {
1479
+ logger.debug("sendAdaptiveCardToChat called with data:");
1480
+ logger.debug(`Chat ID: ${data.chatId}`);
1481
+ logger.debug(`Project ID: ${data.projectId.toString()}`);
1482
+
1483
+ try {
1484
+ const projectAuth: WorkspaceProjectAuthToken | null =
1485
+ await WorkspaceProjectAuthTokenService.getProjectAuth({
1486
+ projectId: data.projectId,
1487
+ workspaceType: WorkspaceType.MicrosoftTeams,
1488
+ });
1489
+
1490
+ if (!projectAuth || !projectAuth.miscData) {
1491
+ throw new BadDataException(
1492
+ "Microsoft Teams integration not found for this project",
1493
+ );
1494
+ }
1495
+
1496
+ const miscData: MicrosoftTeamsMiscData =
1497
+ projectAuth.miscData as MicrosoftTeamsMiscData;
1498
+ if (!miscData.botId) {
1499
+ throw new BadDataException(
1500
+ "Bot ID not found in Microsoft Teams integration",
1501
+ );
1502
+ }
1503
+
1504
+ const tenantId: string | undefined = projectAuth.workspaceProjectId;
1505
+
1506
+ if (!tenantId) {
1507
+ throw new BadDataException(
1508
+ "Tenant ID not found in Microsoft Teams integration",
1509
+ );
1510
+ }
1511
+
1512
+ if (!MicrosoftTeamsAppClientId) {
1513
+ throw new BadDataException(
1514
+ "Microsoft Teams App Client ID not configured",
1515
+ );
1516
+ }
1517
+
1518
+ const chat: MicrosoftTeamsChat | undefined =
1519
+ miscData.availableChats?.[data.chatId];
1520
+
1521
+ if (!chat) {
1522
+ throw new BadDataException(
1523
+ "This chat is not connected to OneUptime. Please add the OneUptime app to the chat in Microsoft Teams and try again.",
1524
+ );
1525
+ }
1526
+
1527
+ const adapter: CloudAdapter = this.getBotAdapter();
1528
+
1529
+ const conversationReference: ConversationReference = {
1530
+ bot: {
1531
+ id: MicrosoftTeamsAppClientId,
1532
+ name: "OneUptime Bot",
1533
+ },
1534
+ conversation: {
1535
+ id: chat.id,
1536
+ name: chat.name,
1537
+ isGroup: chat.chatType === "groupChat",
1538
+ conversationType: chat.chatType,
1539
+ tenantId: tenantId,
1540
+ },
1541
+ channelId: "msteams",
1542
+ /*
1543
+ * Fallback is the commercial-cloud global endpoint; the serviceUrl
1544
+ * captured from bot activities is preferred (required for GCC/DoD).
1545
+ */
1546
+ serviceUrl: chat.serviceUrl || "https://smba.trafficmanager.net/teams/",
1547
+ };
1548
+
1549
+ logger.debug(
1550
+ `Chat conversation reference: ${JSON.stringify(conversationReference)}`,
1551
+ );
1552
+
1553
+ let messageId: string = "";
1554
+
1555
+ await adapter.continueConversationAsync(
1556
+ MicrosoftTeamsAppClientId,
1557
+ conversationReference,
1558
+ async (context: TurnContext) => {
1559
+ logger.debug("Sending adaptive card to chat as proactive message");
1560
+
1561
+ const message: Partial<Activity> = MessageFactory.attachment({
1562
+ contentType: "application/vnd.microsoft.card.adaptive",
1563
+ content: data.adaptiveCard,
1564
+ });
1565
+
1566
+ const response: ResourceResponse | undefined =
1567
+ await context.sendActivity(message);
1568
+
1569
+ messageId = response?.id || "";
1570
+
1571
+ logger.debug(`Chat message sent with ID: ${messageId}`);
1572
+ },
1573
+ );
1574
+
1575
+ const thread: WorkspaceThread = {
1576
+ channel: {
1577
+ id: chat.id,
1578
+ name: chat.name,
1579
+ workspaceType: WorkspaceType.MicrosoftTeams,
1580
+ },
1581
+ threadId: messageId,
1582
+ };
1583
+
1584
+ logger.debug(
1585
+ `Sent message to chat via Bot Framework: ${JSON.stringify(thread)}`,
1586
+ );
1587
+ return thread;
1588
+ } catch (error) {
1589
+ logger.error("Error sending adaptive card to chat via Bot Framework:", {
1590
+ projectId: data.projectId.toString(),
1591
+ chatId: data.chatId,
1592
+ });
1593
+ logger.error(error);
1594
+ throw error;
1595
+ }
1596
+ }
1597
+
1391
1598
  @CaptureSpan()
1392
1599
  public static override async getWorkspaceChannelFromChannelId(data: {
1393
1600
  authToken: string;
@@ -1921,6 +2128,29 @@ export default class MicrosoftTeamsUtil extends WorkspaceBase {
1921
2128
  return;
1922
2129
  }
1923
2130
 
2131
+ /*
2132
+ * Backfill chat capture from inbound messages. Chats where the app was
2133
+ * installed before chat capture shipped never fire install events — per
2134
+ * Microsoft docs, app upgrades only send installationUpdate when the
2135
+ * manifest's bot is added or removed, so a version bump alone re-fires
2136
+ * nothing. Messaging the bot in a chat is the documented recovery path,
2137
+ * and this also keeps the stored serviceUrl fresh (docs: verify the
2138
+ * stored serviceUrl when a new message arrives). Cheap when the chat is
2139
+ * already captured (single read, no roster fetch, no write).
2140
+ */
2141
+ const messageConversationType: string =
2142
+ (conversation["conversationType"] as string) || "";
2143
+ if (
2144
+ messageConversationType === "personal" ||
2145
+ messageConversationType === "groupChat"
2146
+ ) {
2147
+ await this.captureChatFromBotActivity({
2148
+ activity: data.activity,
2149
+ turnContext: data.turnContext,
2150
+ onlyIfMissingOrStale: true,
2151
+ });
2152
+ }
2153
+
1924
2154
  // If this is actually an Adaptive Card submit wrapped as a message, route to invoke handler
1925
2155
  if (
1926
2156
  (possibleActionValue["action"] as string) ||
@@ -3239,10 +3469,25 @@ All monitoring checks are passing normally.`;
3239
3469
  return member["id"] === recipientId;
3240
3470
  });
3241
3471
 
3472
+ const botWasRemoved: boolean = membersRemoved.some((member: JSONObject) => {
3473
+ return member["id"] === recipientId;
3474
+ });
3475
+
3242
3476
  if (botWasAdded) {
3243
3477
  logger.debug("OneUptime bot was added to a Teams conversation");
3478
+ await this.captureChatFromBotActivity({
3479
+ activity: data.activity,
3480
+ turnContext: data.turnContext,
3481
+ });
3244
3482
  await this.sendWelcomeAdaptiveCard(data.turnContext);
3245
3483
  }
3484
+
3485
+ if (botWasRemoved) {
3486
+ logger.debug("OneUptime bot was removed from a Teams conversation");
3487
+ await this.removeChatFromBotActivity({
3488
+ activity: data.activity,
3489
+ });
3490
+ }
3246
3491
  }
3247
3492
 
3248
3493
  @CaptureSpan()
@@ -3258,13 +3503,421 @@ All monitoring checks are passing normally.`;
3258
3503
  logger.debug(`Installation update - Action: ${action}`);
3259
3504
  logger.debug(`Conversation: ${JSON.stringify(conversation)}`);
3260
3505
 
3261
- if (action === "add") {
3506
+ /*
3507
+ * Teams sends "add-upgrade" / "remove-upgrade" when an app upgrade adds
3508
+ * or removes the bot in the manifest (per Microsoft docs, a plain
3509
+ * version bump fires no installationUpdate at all). Treat them the same
3510
+ * as add/remove. Chats installed before chat capture shipped are
3511
+ * backfilled from inbound messages in handleBotMessageActivity instead.
3512
+ */
3513
+ if (action === "add" || action === "add-upgrade") {
3262
3514
  logger.debug("OneUptime bot was installed");
3263
- } else if (action === "remove") {
3515
+ await this.captureChatFromBotActivity({
3516
+ activity: data.activity,
3517
+ turnContext: data.turnContext,
3518
+ });
3519
+ } else if (action === "remove" || action === "remove-upgrade") {
3264
3520
  logger.debug("OneUptime bot was uninstalled");
3521
+ await this.removeChatFromBotActivity({
3522
+ activity: data.activity,
3523
+ });
3524
+ }
3525
+ }
3526
+
3527
+ /*
3528
+ * Chats (group chats and personal 1:1 chats) cannot be listed with the
3529
+ * app-only Graph token, so the only way OneUptime learns about them is by
3530
+ * capturing the conversation details when the OneUptime app is added to a
3531
+ * chat. These captured chats power the "post to chat" notification rules.
3532
+ */
3533
+
3534
+ private static getChatTypeFromConversation(
3535
+ conversation: JSONObject,
3536
+ ): MicrosoftTeamsChatType | null {
3537
+ const conversationType: string =
3538
+ (conversation["conversationType"] as string) || "";
3539
+
3540
+ if (conversationType === "personal" || conversationType === "groupChat") {
3541
+ return conversationType;
3542
+ }
3543
+
3544
+ return null; // channels and meetings are not chats.
3545
+ }
3546
+
3547
+ public static getChatDisplayName(data: {
3548
+ chatType: MicrosoftTeamsChatType;
3549
+ topic?: string | undefined;
3550
+ memberNames: Array<string>;
3551
+ }): string {
3552
+ /*
3553
+ * Keep well under the 100-char ShortText columns that store the chat
3554
+ * name in notification logs — Teams chat topics can be much longer.
3555
+ */
3556
+ const maxNameLength: number = 80;
3557
+
3558
+ const truncate: (name: string) => string = (name: string): string => {
3559
+ return name.length > maxNameLength
3560
+ ? `${name.substring(0, maxNameLength - 1)}…`
3561
+ : name;
3562
+ };
3563
+
3564
+ if (data.topic && data.topic.trim()) {
3565
+ return truncate(data.topic.trim());
3566
+ }
3567
+
3568
+ const memberNames: Array<string> = data.memberNames.filter(
3569
+ (name: string) => {
3570
+ return Boolean(name && name.trim());
3571
+ },
3572
+ );
3573
+
3574
+ if (data.chatType === "personal") {
3575
+ return memberNames[0] ? truncate(`${memberNames[0]}`) : "Personal chat";
3576
+ }
3577
+
3578
+ if (memberNames.length === 0) {
3579
+ return "Group chat";
3580
+ }
3581
+
3582
+ const maxNamesToShow: number = 3;
3583
+ const shownNames: Array<string> = memberNames.slice(0, maxNamesToShow);
3584
+ const remainingCount: number = memberNames.length - shownNames.length;
3585
+
3586
+ if (remainingCount > 0) {
3587
+ return truncate(`${shownNames.join(", ")} + ${remainingCount} more`);
3588
+ }
3589
+
3590
+ return truncate(shownNames.join(", "));
3591
+ }
3592
+
3593
+ /*
3594
+ * Returns true when every connected project of this tenant already has
3595
+ * this chat stored with the same service URL — i.e. there is nothing to
3596
+ * capture or refresh.
3597
+ */
3598
+ private static async isChatCapturedForTenant(data: {
3599
+ tenantId: string;
3600
+ chatId: string;
3601
+ serviceUrl?: string | undefined;
3602
+ }): Promise<boolean> {
3603
+ const projectAuths: Array<WorkspaceProjectAuthToken> =
3604
+ await WorkspaceProjectAuthTokenService.findBy({
3605
+ query: {
3606
+ workspaceType: WorkspaceType.MicrosoftTeams,
3607
+ workspaceProjectId: data.tenantId,
3608
+ },
3609
+ select: {
3610
+ _id: true,
3611
+ miscData: true,
3612
+ },
3613
+ limit: LIMIT_MAX,
3614
+ skip: 0,
3615
+ props: {
3616
+ isRoot: true,
3617
+ },
3618
+ });
3619
+
3620
+ if (projectAuths.length === 0) {
3621
+ return true; // no connected projects — nothing to capture into.
3622
+ }
3623
+
3624
+ for (const projectAuth of projectAuths) {
3625
+ const chat: MicrosoftTeamsChat | undefined = (
3626
+ projectAuth.miscData as MicrosoftTeamsMiscData
3627
+ )?.availableChats?.[data.chatId];
3628
+
3629
+ if (!chat) {
3630
+ return false;
3631
+ }
3632
+
3633
+ if (data.serviceUrl && chat.serviceUrl !== data.serviceUrl) {
3634
+ return false; // stored serviceUrl is stale.
3635
+ }
3636
+ }
3637
+
3638
+ return true;
3639
+ }
3640
+
3641
+ @CaptureSpan()
3642
+ private static async captureChatFromBotActivity(data: {
3643
+ activity: JSONObject;
3644
+ turnContext: TurnContext;
3645
+ onlyIfMissingOrStale?: boolean | undefined;
3646
+ }): Promise<void> {
3647
+ try {
3648
+ const conversation: JSONObject =
3649
+ (data.activity["conversation"] as JSONObject) || {};
3650
+
3651
+ const chatType: MicrosoftTeamsChatType | null =
3652
+ this.getChatTypeFromConversation(conversation);
3653
+
3654
+ if (!chatType) {
3655
+ return; // bot was added to a team channel, not a chat.
3656
+ }
3657
+
3658
+ const chatId: string = (conversation["id"] as string) || "";
3659
+
3660
+ if (!chatId) {
3661
+ logger.debug("No conversation id found on chat activity. Skipping.");
3662
+ return;
3663
+ }
3664
+
3665
+ const channelData: JSONObject =
3666
+ (data.activity["channelData"] as JSONObject) || {};
3667
+ const tenantId: string =
3668
+ ((channelData["tenant"] as JSONObject)?.["id"] as string) ||
3669
+ (conversation["tenantId"] as string) ||
3670
+ "";
3671
+
3672
+ if (!tenantId) {
3673
+ logger.debug("No tenant id found on chat activity. Skipping.");
3674
+ return;
3675
+ }
3676
+
3677
+ const activityServiceUrl: string | undefined =
3678
+ (data.activity["serviceUrl"] as string) ||
3679
+ data.turnContext.activity.serviceUrl ||
3680
+ undefined;
3681
+
3682
+ /*
3683
+ * On the message-backfill path, skip the roster fetch and DB writes
3684
+ * when the chat is already captured with a fresh serviceUrl.
3685
+ */
3686
+ if (data.onlyIfMissingOrStale) {
3687
+ const alreadyCaptured: boolean = await this.isChatCapturedForTenant({
3688
+ tenantId: tenantId,
3689
+ chatId: chatId,
3690
+ serviceUrl: activityServiceUrl,
3691
+ });
3692
+
3693
+ if (alreadyCaptured) {
3694
+ return;
3695
+ }
3696
+ }
3697
+
3698
+ // Resolve a human friendly name for the chat.
3699
+ const topic: string | undefined = conversation["name"] as
3700
+ | string
3701
+ | undefined;
3702
+
3703
+ let memberNames: Array<string> = [];
3704
+
3705
+ try {
3706
+ const botId: string | undefined =
3707
+ data.turnContext.activity.recipient?.id;
3708
+
3709
+ /*
3710
+ * TeamsInfo.getMembers is deprecated — page through the roster
3711
+ * instead. Chats return the full roster in one page in practice.
3712
+ */
3713
+ const members: Array<TeamsChannelAccount> = [];
3714
+ let continuationToken: string | undefined = undefined;
3715
+ do {
3716
+ const page: TeamsPagedMembersResult = await TeamsInfo.getPagedMembers(
3717
+ data.turnContext,
3718
+ 500,
3719
+ continuationToken,
3720
+ );
3721
+ members.push(...(page.members || []));
3722
+ continuationToken = page.continuationToken;
3723
+ } while (continuationToken);
3724
+
3725
+ memberNames = members
3726
+ .filter((member: TeamsChannelAccount) => {
3727
+ return member.id !== botId;
3728
+ })
3729
+ .map((member: TeamsChannelAccount) => {
3730
+ return member.name || "";
3731
+ });
3732
+ } catch (err) {
3733
+ logger.debug("Could not fetch chat members for chat name resolution");
3734
+ logger.debug(err);
3735
+ }
3736
+
3737
+ const chat: MicrosoftTeamsChat = {
3738
+ id: chatId,
3739
+ name: this.getChatDisplayName({
3740
+ chatType: chatType,
3741
+ topic: topic,
3742
+ memberNames: memberNames,
3743
+ }),
3744
+ chatType: chatType,
3745
+ serviceUrl: activityServiceUrl,
3746
+ addedAt: OneUptimeDate.getCurrentDate().toISOString(),
3747
+ };
3748
+
3749
+ await this.saveChatToProjectAuthTokens({
3750
+ tenantId: tenantId,
3751
+ chat: chat,
3752
+ });
3753
+
3754
+ logger.debug(
3755
+ `Captured Microsoft Teams chat ${chatId} (${chat.name}) for tenant ${tenantId}`,
3756
+ );
3757
+ } catch (err) {
3758
+ logger.error("Error capturing Microsoft Teams chat from bot activity:");
3759
+ logger.error(err);
3265
3760
  }
3266
3761
  }
3267
3762
 
3763
+ @CaptureSpan()
3764
+ private static async removeChatFromBotActivity(data: {
3765
+ activity: JSONObject;
3766
+ }): Promise<void> {
3767
+ try {
3768
+ const conversation: JSONObject =
3769
+ (data.activity["conversation"] as JSONObject) || {};
3770
+
3771
+ const chatType: MicrosoftTeamsChatType | null =
3772
+ this.getChatTypeFromConversation(conversation);
3773
+
3774
+ if (!chatType) {
3775
+ return;
3776
+ }
3777
+
3778
+ const chatId: string = (conversation["id"] as string) || "";
3779
+ const channelData: JSONObject =
3780
+ (data.activity["channelData"] as JSONObject) || {};
3781
+ const tenantId: string =
3782
+ ((channelData["tenant"] as JSONObject)?.["id"] as string) ||
3783
+ (conversation["tenantId"] as string) ||
3784
+ "";
3785
+
3786
+ if (!chatId || !tenantId) {
3787
+ return;
3788
+ }
3789
+
3790
+ await this.removeChatFromProjectAuthTokens({
3791
+ tenantId: tenantId,
3792
+ chatId: chatId,
3793
+ });
3794
+
3795
+ logger.debug(
3796
+ `Removed Microsoft Teams chat ${chatId} for tenant ${tenantId}`,
3797
+ );
3798
+ } catch (err) {
3799
+ logger.error("Error removing Microsoft Teams chat from bot activity:");
3800
+ logger.error(err);
3801
+ }
3802
+ }
3803
+
3804
+ @CaptureSpan()
3805
+ public static async saveChatToProjectAuthTokens(data: {
3806
+ tenantId: string;
3807
+ chat: MicrosoftTeamsChat;
3808
+ }): Promise<void> {
3809
+ /*
3810
+ * A tenant can be connected to more than one OneUptime project, and the
3811
+ * install event does not say which project it belongs to — so save the
3812
+ * chat on every project connected to this tenant.
3813
+ */
3814
+ const projectAuths: Array<WorkspaceProjectAuthToken> =
3815
+ await WorkspaceProjectAuthTokenService.findBy({
3816
+ query: {
3817
+ workspaceType: WorkspaceType.MicrosoftTeams,
3818
+ workspaceProjectId: data.tenantId,
3819
+ },
3820
+ select: {
3821
+ _id: true,
3822
+ miscData: true,
3823
+ },
3824
+ limit: LIMIT_MAX,
3825
+ skip: 0,
3826
+ props: {
3827
+ isRoot: true,
3828
+ },
3829
+ });
3830
+
3831
+ for (const projectAuth of projectAuths) {
3832
+ const miscData: MicrosoftTeamsMiscData = {
3833
+ ...((projectAuth.miscData as MicrosoftTeamsMiscData) || {}),
3834
+ } as MicrosoftTeamsMiscData;
3835
+
3836
+ miscData.availableChats = {
3837
+ ...(miscData.availableChats || {}),
3838
+ [data.chat.id]: data.chat,
3839
+ };
3840
+
3841
+ await WorkspaceProjectAuthTokenService.updateOneById({
3842
+ id: projectAuth.id!,
3843
+ data: {
3844
+ miscData: miscData,
3845
+ },
3846
+ props: {
3847
+ isRoot: true,
3848
+ },
3849
+ });
3850
+ }
3851
+ }
3852
+
3853
+ @CaptureSpan()
3854
+ public static async removeChatFromProjectAuthTokens(data: {
3855
+ tenantId: string;
3856
+ chatId: string;
3857
+ }): Promise<void> {
3858
+ const projectAuths: Array<WorkspaceProjectAuthToken> =
3859
+ await WorkspaceProjectAuthTokenService.findBy({
3860
+ query: {
3861
+ workspaceType: WorkspaceType.MicrosoftTeams,
3862
+ workspaceProjectId: data.tenantId,
3863
+ },
3864
+ select: {
3865
+ _id: true,
3866
+ miscData: true,
3867
+ },
3868
+ limit: LIMIT_MAX,
3869
+ skip: 0,
3870
+ props: {
3871
+ isRoot: true,
3872
+ },
3873
+ });
3874
+
3875
+ for (const projectAuth of projectAuths) {
3876
+ const miscData: MicrosoftTeamsMiscData = {
3877
+ ...((projectAuth.miscData as MicrosoftTeamsMiscData) || {}),
3878
+ } as MicrosoftTeamsMiscData;
3879
+
3880
+ if (!miscData.availableChats || !miscData.availableChats[data.chatId]) {
3881
+ continue;
3882
+ }
3883
+
3884
+ const availableChats: Record<string, MicrosoftTeamsChat> = {
3885
+ ...miscData.availableChats,
3886
+ };
3887
+ delete availableChats[data.chatId];
3888
+ miscData.availableChats = availableChats;
3889
+
3890
+ await WorkspaceProjectAuthTokenService.updateOneById({
3891
+ id: projectAuth.id!,
3892
+ data: {
3893
+ miscData: miscData,
3894
+ },
3895
+ props: {
3896
+ isRoot: true,
3897
+ },
3898
+ });
3899
+ }
3900
+ }
3901
+
3902
+ @CaptureSpan()
3903
+ public static async getChatsForProject(data: {
3904
+ projectId: ObjectID;
3905
+ }): Promise<Record<string, MicrosoftTeamsChat>> {
3906
+ const projectAuth: WorkspaceProjectAuthToken | null =
3907
+ await WorkspaceProjectAuthTokenService.getProjectAuth({
3908
+ projectId: data.projectId,
3909
+ workspaceType: WorkspaceType.MicrosoftTeams,
3910
+ });
3911
+
3912
+ if (!projectAuth || !projectAuth.miscData) {
3913
+ return {};
3914
+ }
3915
+
3916
+ return (
3917
+ (projectAuth.miscData as MicrosoftTeamsMiscData).availableChats || {}
3918
+ );
3919
+ }
3920
+
3268
3921
  /**
3269
3922
  * Process Bot Framework activity using the botbuilder SDK adapter.processActivity
3270
3923
  * This replaces the manual JWT validation and activity handling with proper SDK methods
@@ -3331,6 +3984,20 @@ All monitoring checks are passing normally.`;
3331
3984
  },
3332
3985
  );
3333
3986
 
3987
+ this.onMembersRemoved(
3988
+ async (context: TurnContext, next: () => Promise<void>) => {
3989
+ logger.debug(
3990
+ "Handling members removed activity: " +
3991
+ JSON.stringify(context.activity),
3992
+ );
3993
+ await MicrosoftTeamsUtil.handleConversationUpdateActivity({
3994
+ activity: context.activity as unknown as JSONObject,
3995
+ turnContext: context,
3996
+ });
3997
+ await next();
3998
+ },
3999
+ );
4000
+
3334
4001
  this.onInstallationUpdateAdd(
3335
4002
  async (context: TurnContext, next: () => Promise<void>) => {
3336
4003
  logger.debug(
@@ -3344,6 +4011,20 @@ All monitoring checks are passing normally.`;
3344
4011
  await next();
3345
4012
  },
3346
4013
  );
4014
+
4015
+ this.onInstallationUpdateRemove(
4016
+ async (context: TurnContext, next: () => Promise<void>) => {
4017
+ logger.debug(
4018
+ "Handling installation update remove activity: " +
4019
+ JSON.stringify(context.activity),
4020
+ );
4021
+ await MicrosoftTeamsUtil.handleInstallationUpdateActivity({
4022
+ activity: context.activity as unknown as JSONObject,
4023
+ turnContext: context,
4024
+ });
4025
+ await next();
4026
+ },
4027
+ );
3347
4028
  }
3348
4029
 
3349
4030
  protected override async onInvokeActivity(
@@ -3679,9 +4360,28 @@ All monitoring checks are passing normally.`;
3679
4360
 
3680
4361
  logger.debug(`Processed ${Object.keys(availableTeams).length} teams`);
3681
4362
 
3682
- // Update project auth token with new teams
3683
- const miscData: MicrosoftTeamsMiscData =
4363
+ /*
4364
+ * Update project auth token with new teams. Re-read miscData first
4365
+ * the snapshot from the start of this method is stale by the length
4366
+ * of the paginated Graph fetch, and writing it back verbatim would
4367
+ * erase concurrent updates (e.g. chats captured into availableChats
4368
+ * by bot install events, which cannot be re-derived).
4369
+ */
4370
+ let miscData: MicrosoftTeamsMiscData =
3684
4371
  (projectAuth.miscData as MicrosoftTeamsMiscData) || {};
4372
+ try {
4373
+ const latestProjectAuth: WorkspaceProjectAuthToken | null =
4374
+ await WorkspaceProjectAuthTokenService.getProjectAuth({
4375
+ projectId: data.projectId,
4376
+ workspaceType: WorkspaceType.MicrosoftTeams,
4377
+ });
4378
+ if (latestProjectAuth?.miscData) {
4379
+ miscData = latestProjectAuth.miscData as MicrosoftTeamsMiscData;
4380
+ }
4381
+ } catch (err) {
4382
+ logger.debug("Could not re-read miscData before teams refresh write");
4383
+ logger.debug(err);
4384
+ }
3685
4385
  miscData.availableTeams = availableTeams;
3686
4386
  miscData.tenantId = tenantId;
3687
4387