@oneuptime/common 8.0.5237 → 8.0.5283

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 (371) hide show
  1. package/Models/DatabaseModels/Index.ts +4 -2
  2. package/Models/DatabaseModels/OnCallDutyPolicy.ts +7 -0
  3. package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +7 -0
  4. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +7 -0
  5. package/Models/DatabaseModels/OnCallDutyPolicyTimeLog.ts +7 -0
  6. package/Models/DatabaseModels/OnCallDutyPolicyUserOverride.ts +5 -3
  7. package/Models/DatabaseModels/Project.ts +4 -2
  8. package/Models/DatabaseModels/ProjectSmtpConfig.ts +4 -2
  9. package/Models/DatabaseModels/StatusPageDomain.ts +6 -4
  10. package/Models/DatabaseModels/User.ts +0 -46
  11. package/Models/DatabaseModels/{UserTwoFactorAuth.ts → UserTotpAuth.ts} +16 -16
  12. package/Models/DatabaseModels/UserWebAuthn.ts +244 -0
  13. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +21 -0
  14. package/Server/API/BaseAPI.ts +4 -2
  15. package/Server/API/GlobalConfigAPI.ts +16 -12
  16. package/Server/API/MicrosoftTeamsAPI.ts +1240 -0
  17. package/Server/API/ProjectAPI.ts +4 -2
  18. package/Server/API/ResellerPlanAPI.ts +4 -2
  19. package/Server/API/SlackAPI.ts +54 -48
  20. package/Server/API/StatusPageAPI.ts +5 -3
  21. package/Server/API/UserOnCallLogTimelineAPI.ts +5 -3
  22. package/Server/API/{UserTwoFactorAuthAPI.ts → UserTotpAuthAPI.ts} +20 -20
  23. package/Server/API/UserWebAuthnAPI.ts +103 -0
  24. package/Server/EnvironmentConfig.ts +6 -0
  25. package/Server/Images/MicrosoftTeams/color.png +0 -0
  26. package/Server/Images/MicrosoftTeams/outline.png +0 -0
  27. package/Server/Infrastructure/Postgres/SchemaMigrations/1753131488925-AddEnableCustomSubscriberEmailNotificationFooterText.ts +4 -2
  28. package/Server/Infrastructure/Postgres/SchemaMigrations/1759175457008-MigrationName.ts +27 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/1759232954703-MigrationName.ts +25 -0
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1759234532998-MigrationName.ts +15 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  32. package/Server/Infrastructure/Queue.ts +4 -2
  33. package/Server/Infrastructure/SocketIO.ts +4 -2
  34. package/Server/Middleware/ProjectAuthorization.ts +5 -3
  35. package/Server/Middleware/SlackAuthorization.ts +2 -2
  36. package/Server/Middleware/TelemetryIngest.ts +12 -6
  37. package/Server/Services/AlertStateTimelineService.ts +34 -18
  38. package/Server/Services/BillingInvoiceService.ts +8 -4
  39. package/Server/Services/BillingService.ts +13 -9
  40. package/Server/Services/DatabaseService.ts +42 -30
  41. package/Server/Services/IncidentService.ts +5 -3
  42. package/Server/Services/IncidentStateTimelineService.ts +34 -18
  43. package/Server/Services/Index.ts +4 -2
  44. package/Server/Services/MonitorStatusTimelineService.ts +34 -18
  45. package/Server/Services/OnCallDutyPolicyScheduleService.ts +4 -2
  46. package/Server/Services/ProjectService.ts +6 -4
  47. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +26 -14
  48. package/Server/Services/StatusPageService.ts +4 -2
  49. package/Server/Services/UserService.ts +21 -5
  50. package/Server/Services/{UserTwoFactorAuthService.ts → UserTotpAuthService.ts} +26 -7
  51. package/Server/Services/UserWebAuthnService.ts +419 -0
  52. package/Server/Services/WorkspaceNotificationRuleService.ts +257 -77
  53. package/Server/Services/WorkspaceProjectAuthTokenService.ts +2 -2
  54. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -5
  55. package/Server/Types/Database/Permissions/BasePermission.ts +4 -2
  56. package/Server/Types/Database/Permissions/TenantPermission.ts +5 -3
  57. package/Server/Types/Database/QueryHelper.ts +4 -2
  58. package/Server/Types/Markdown.ts +6 -4
  59. package/Server/Types/Workflow/ComponentCode.ts +4 -2
  60. package/Server/Types/Workflow/Components/Conditions/IfElse.ts +5 -3
  61. package/Server/Types/Workflow/Components/JavaScript.ts +5 -3
  62. package/Server/Types/Workflow/TriggerCode.ts +4 -2
  63. package/Server/Utils/AnalyticsDatabase/Statement.ts +4 -2
  64. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +21 -11
  65. package/Server/Utils/Browser.ts +6 -4
  66. package/Server/Utils/CodeRepository/GitHub/GitHub.ts +4 -2
  67. package/Server/Utils/LocalFile.ts +14 -0
  68. package/Server/Utils/Monitor/MonitorResource.ts +17 -9
  69. package/Server/Utils/Realtime.ts +4 -2
  70. package/Server/Utils/StartServer.ts +1 -1
  71. package/Server/Utils/Telemetry.ts +15 -9
  72. package/Server/Utils/{TwoFactorAuth.ts → TotpAuth.ts} +2 -2
  73. package/Server/Utils/Workspace/MicrosoftTeams/Actions/ActionTypes.ts +75 -16
  74. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +649 -0
  75. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +237 -0
  76. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +1321 -0
  77. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Monitor.ts +155 -0
  78. package/Server/Utils/Workspace/MicrosoftTeams/Actions/OnCallDutyPolicy.ts +119 -0
  79. package/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.ts +959 -0
  80. package/Server/Utils/Workspace/MicrosoftTeams/Messages/Alert.ts +16 -14
  81. package/Server/Utils/Workspace/MicrosoftTeams/Messages/Incident.ts +17 -14
  82. package/Server/Utils/Workspace/MicrosoftTeams/Messages/ScheduledMaintenance.ts +18 -13
  83. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +2547 -14
  84. package/Server/Utils/Workspace/Slack/Actions/Alert.ts +4 -2
  85. package/Server/Utils/Workspace/Slack/Actions/Auth.ts +4 -2
  86. package/Server/Utils/Workspace/Slack/Actions/Incident.ts +14 -10
  87. package/Server/Utils/Workspace/Slack/Actions/Monitor.ts +4 -2
  88. package/Server/Utils/Workspace/Slack/Actions/OnCallDutyPolicy.ts +4 -2
  89. package/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.ts +14 -10
  90. package/Server/Utils/Workspace/Slack/Messages/Alert.ts +9 -7
  91. package/Server/Utils/Workspace/Slack/Messages/Incident.ts +9 -7
  92. package/Server/Utils/Workspace/Slack/Messages/Monitor.ts +9 -7
  93. package/Server/Utils/Workspace/Slack/Messages/ScheduledMaintenance.ts +9 -7
  94. package/Server/Utils/Workspace/Slack/Slack.ts +6 -0
  95. package/Server/Utils/Workspace/Workspace.ts +13 -10
  96. package/Server/Utils/Workspace/WorkspaceBase.ts +9 -0
  97. package/Tests/Server/API/BaseAPI.test.ts +64 -52
  98. package/Tests/Server/Services/BillingService.test.ts +4 -4
  99. package/Tests/Server/Services/TeamMemberService.test.ts +20 -12
  100. package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +2 -2
  101. package/Tests/Types/OnCallDutyPolicy/LayerUtil.test.ts +8 -4
  102. package/Tests/UI/Components/DictionaryOfStrings.test.tsx +4 -2
  103. package/Tests/UI/Components/FilePicker.test.tsx +2 -2
  104. package/Tests/Utils/API.test.ts +9 -8
  105. package/Types/BaseDatabase/DatabaseCommonInteractionPropsUtil.ts +5 -3
  106. package/Types/Html.ts +5 -3
  107. package/Types/JSONFunctions.ts +5 -5
  108. package/Types/Metrics/MetricsQuery.ts +6 -4
  109. package/Types/Monitor/MonitorType.ts +8 -6
  110. package/Types/OnCallDutyPolicy/Layer.ts +29 -17
  111. package/Types/Phone.ts +5 -3
  112. package/Types/Workspace/NotificationRules/BaseNotificationRule.ts +1 -0
  113. package/Types/Workspace/NotificationRules/CreateChannelNotificationRule.ts +5 -2
  114. package/Types/Workspace/WorkspaceMessagePayload.ts +1 -0
  115. package/Types/Workspace/WorkspaceType.ts +13 -0
  116. package/UI/Components/Charts/Utils/DataPoint.ts +8 -6
  117. package/UI/Components/Detail/Detail.tsx +4 -1
  118. package/UI/Components/FilePicker/FilePicker.tsx +1 -1
  119. package/UI/Components/Forms/Types/Field.ts +4 -2
  120. package/UI/Components/Image/Image.tsx +1 -1
  121. package/UI/Components/JSONTable/JSONTable.tsx +4 -2
  122. package/UI/Components/ModelTable/BaseModelTable.tsx +5 -3
  123. package/UI/Components/SideMenu/SideMenu.tsx +4 -2
  124. package/UI/Components/SideMenu/SideMenuItem.tsx +69 -45
  125. package/UI/Config.ts +3 -0
  126. package/UI/Utils/API/API.ts +5 -3
  127. package/UI/Utils/Countries.ts +5 -3
  128. package/UI/Utils/Login.ts +6 -1
  129. package/Utils/Base64.ts +13 -0
  130. package/Utils/Schema/ModelSchema.ts +4 -2
  131. package/build/dist/Models/DatabaseModels/Index.js +4 -2
  132. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  133. package/build/dist/Models/DatabaseModels/OnCallDutyPolicy.js +7 -0
  134. package/build/dist/Models/DatabaseModels/OnCallDutyPolicy.js.map +1 -1
  135. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +7 -0
  136. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
  137. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +7 -0
  138. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  139. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js +7 -0
  140. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js.map +1 -1
  141. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js +5 -3
  142. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js.map +1 -1
  143. package/build/dist/Models/DatabaseModels/Project.js +4 -2
  144. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  145. package/build/dist/Models/DatabaseModels/ProjectSmtpConfig.js +4 -2
  146. package/build/dist/Models/DatabaseModels/ProjectSmtpConfig.js.map +1 -1
  147. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +6 -4
  148. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  149. package/build/dist/Models/DatabaseModels/User.js +0 -49
  150. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  151. package/build/dist/Models/DatabaseModels/{UserTwoFactorAuth.js → UserTotpAuth.js} +27 -27
  152. package/build/dist/Models/DatabaseModels/UserTotpAuth.js.map +1 -0
  153. package/build/dist/Models/DatabaseModels/UserWebAuthn.js +270 -0
  154. package/build/dist/Models/DatabaseModels/UserWebAuthn.js.map +1 -0
  155. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  156. package/build/dist/Server/API/BaseAPI.js +4 -2
  157. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  158. package/build/dist/Server/API/GlobalConfigAPI.js +16 -12
  159. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  160. package/build/dist/Server/API/MicrosoftTeamsAPI.js +771 -0
  161. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -0
  162. package/build/dist/Server/API/ProjectAPI.js +4 -2
  163. package/build/dist/Server/API/ProjectAPI.js.map +1 -1
  164. package/build/dist/Server/API/ResellerPlanAPI.js +4 -2
  165. package/build/dist/Server/API/ResellerPlanAPI.js.map +1 -1
  166. package/build/dist/Server/API/SlackAPI.js +53 -47
  167. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  168. package/build/dist/Server/API/StatusPageAPI.js +5 -3
  169. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  170. package/build/dist/Server/API/UserOnCallLogTimelineAPI.js +5 -3
  171. package/build/dist/Server/API/UserOnCallLogTimelineAPI.js.map +1 -1
  172. package/build/dist/Server/API/{UserTwoFactorAuthAPI.js → UserTotpAuthAPI.js} +16 -16
  173. package/build/dist/Server/API/UserTotpAuthAPI.js.map +1 -0
  174. package/build/dist/Server/API/UserWebAuthnAPI.js +65 -0
  175. package/build/dist/Server/API/UserWebAuthnAPI.js.map +1 -0
  176. package/build/dist/Server/EnvironmentConfig.js +3 -0
  177. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  178. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1753131488925-AddEnableCustomSubscriberEmailNotificationFooterText.js +4 -2
  179. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1753131488925-AddEnableCustomSubscriberEmailNotificationFooterText.js.map +1 -1
  180. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1759175457008-MigrationName.js +16 -0
  181. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1759175457008-MigrationName.js.map +1 -0
  182. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1759232954703-MigrationName.js +16 -0
  183. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1759232954703-MigrationName.js.map +1 -0
  184. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1759234532998-MigrationName.js +12 -0
  185. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1759234532998-MigrationName.js.map +1 -0
  186. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  187. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  188. package/build/dist/Server/Infrastructure/Queue.js +4 -2
  189. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  190. package/build/dist/Server/Infrastructure/SocketIO.js +4 -2
  191. package/build/dist/Server/Infrastructure/SocketIO.js.map +1 -1
  192. package/build/dist/Server/Middleware/ProjectAuthorization.js +5 -3
  193. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  194. package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
  195. package/build/dist/Server/Middleware/TelemetryIngest.js +12 -6
  196. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  197. package/build/dist/Server/Services/AlertStateTimelineService.js +34 -18
  198. package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
  199. package/build/dist/Server/Services/BillingInvoiceService.js +8 -4
  200. package/build/dist/Server/Services/BillingInvoiceService.js.map +1 -1
  201. package/build/dist/Server/Services/BillingService.js +13 -9
  202. package/build/dist/Server/Services/BillingService.js.map +1 -1
  203. package/build/dist/Server/Services/DatabaseService.js +40 -28
  204. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  205. package/build/dist/Server/Services/IncidentService.js +5 -3
  206. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  207. package/build/dist/Server/Services/IncidentStateTimelineService.js +34 -18
  208. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  209. package/build/dist/Server/Services/Index.js +4 -2
  210. package/build/dist/Server/Services/Index.js.map +1 -1
  211. package/build/dist/Server/Services/MonitorStatusTimelineService.js +34 -18
  212. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  213. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +4 -2
  214. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  215. package/build/dist/Server/Services/ProjectService.js +6 -4
  216. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  217. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +26 -14
  218. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  219. package/build/dist/Server/Services/StatusPageService.js +4 -2
  220. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  221. package/build/dist/Server/Services/UserService.js +16 -3
  222. package/build/dist/Server/Services/UserService.js.map +1 -1
  223. package/build/dist/Server/Services/{UserTwoFactorAuthService.js → UserTotpAuthService.js} +22 -8
  224. package/build/dist/Server/Services/UserTotpAuthService.js.map +1 -0
  225. package/build/dist/Server/Services/UserWebAuthnService.js +365 -0
  226. package/build/dist/Server/Services/UserWebAuthnService.js.map +1 -0
  227. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +142 -51
  228. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  229. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -5
  230. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  231. package/build/dist/Server/Types/Database/Permissions/BasePermission.js +4 -2
  232. package/build/dist/Server/Types/Database/Permissions/BasePermission.js.map +1 -1
  233. package/build/dist/Server/Types/Database/Permissions/TenantPermission.js +5 -3
  234. package/build/dist/Server/Types/Database/Permissions/TenantPermission.js.map +1 -1
  235. package/build/dist/Server/Types/Database/QueryHelper.js +4 -2
  236. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  237. package/build/dist/Server/Types/Markdown.js +6 -4
  238. package/build/dist/Server/Types/Markdown.js.map +1 -1
  239. package/build/dist/Server/Types/Workflow/ComponentCode.js +4 -2
  240. package/build/dist/Server/Types/Workflow/ComponentCode.js.map +1 -1
  241. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js +5 -3
  242. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js.map +1 -1
  243. package/build/dist/Server/Types/Workflow/Components/JavaScript.js +5 -3
  244. package/build/dist/Server/Types/Workflow/Components/JavaScript.js.map +1 -1
  245. package/build/dist/Server/Types/Workflow/TriggerCode.js.map +1 -1
  246. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +4 -2
  247. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  248. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +21 -11
  249. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  250. package/build/dist/Server/Utils/Browser.js +6 -4
  251. package/build/dist/Server/Utils/Browser.js.map +1 -1
  252. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +4 -2
  253. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
  254. package/build/dist/Server/Utils/LocalFile.js +16 -0
  255. package/build/dist/Server/Utils/LocalFile.js.map +1 -1
  256. package/build/dist/Server/Utils/Monitor/MonitorResource.js +17 -9
  257. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  258. package/build/dist/Server/Utils/Realtime.js +4 -2
  259. package/build/dist/Server/Utils/Realtime.js.map +1 -1
  260. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  261. package/build/dist/Server/Utils/Telemetry.js +6 -4
  262. package/build/dist/Server/Utils/Telemetry.js.map +1 -1
  263. package/build/dist/Server/Utils/{TwoFactorAuth.js → TotpAuth.js} +8 -8
  264. package/build/dist/Server/Utils/TotpAuth.js.map +1 -0
  265. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ActionTypes.js +86 -36
  266. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ActionTypes.js.map +1 -1
  267. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +531 -0
  268. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -0
  269. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +206 -0
  270. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -0
  271. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +1102 -0
  272. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -0
  273. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Monitor.js +136 -0
  274. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Monitor.js.map +1 -0
  275. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/OnCallDutyPolicy.js +107 -0
  276. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/OnCallDutyPolicy.js.map +1 -0
  277. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js +795 -0
  278. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js.map +1 -0
  279. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Messages/Alert.js +16 -14
  280. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Messages/Alert.js.map +1 -1
  281. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Messages/Incident.js +16 -14
  282. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Messages/Incident.js.map +1 -1
  283. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Messages/ScheduledMaintenance.js +15 -13
  284. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Messages/ScheduledMaintenance.js.map +1 -1
  285. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +1982 -13
  286. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  287. package/build/dist/Server/Utils/Workspace/Slack/Actions/Alert.js +4 -2
  288. package/build/dist/Server/Utils/Workspace/Slack/Actions/Alert.js.map +1 -1
  289. package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js +4 -2
  290. package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js.map +1 -1
  291. package/build/dist/Server/Utils/Workspace/Slack/Actions/Incident.js +14 -10
  292. package/build/dist/Server/Utils/Workspace/Slack/Actions/Incident.js.map +1 -1
  293. package/build/dist/Server/Utils/Workspace/Slack/Actions/Monitor.js +4 -2
  294. package/build/dist/Server/Utils/Workspace/Slack/Actions/Monitor.js.map +1 -1
  295. package/build/dist/Server/Utils/Workspace/Slack/Actions/OnCallDutyPolicy.js +4 -2
  296. package/build/dist/Server/Utils/Workspace/Slack/Actions/OnCallDutyPolicy.js.map +1 -1
  297. package/build/dist/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.js +14 -10
  298. package/build/dist/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.js.map +1 -1
  299. package/build/dist/Server/Utils/Workspace/Slack/Messages/Alert.js +9 -7
  300. package/build/dist/Server/Utils/Workspace/Slack/Messages/Alert.js.map +1 -1
  301. package/build/dist/Server/Utils/Workspace/Slack/Messages/Incident.js +9 -7
  302. package/build/dist/Server/Utils/Workspace/Slack/Messages/Incident.js.map +1 -1
  303. package/build/dist/Server/Utils/Workspace/Slack/Messages/Monitor.js +9 -7
  304. package/build/dist/Server/Utils/Workspace/Slack/Messages/Monitor.js.map +1 -1
  305. package/build/dist/Server/Utils/Workspace/Slack/Messages/ScheduledMaintenance.js +9 -7
  306. package/build/dist/Server/Utils/Workspace/Slack/Messages/ScheduledMaintenance.js.map +1 -1
  307. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +5 -0
  308. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  309. package/build/dist/Server/Utils/Workspace/Workspace.js +12 -10
  310. package/build/dist/Server/Utils/Workspace/Workspace.js.map +1 -1
  311. package/build/dist/Server/Utils/Workspace/WorkspaceBase.js.map +1 -1
  312. package/build/dist/Tests/Server/API/BaseAPI.test.js +59 -47
  313. package/build/dist/Tests/Server/API/BaseAPI.test.js.map +1 -1
  314. package/build/dist/Tests/Server/Services/BillingService.test.js +4 -4
  315. package/build/dist/Tests/Server/Services/BillingService.test.js.map +1 -1
  316. package/build/dist/Tests/Server/Services/TeamMemberService.test.js +20 -12
  317. package/build/dist/Tests/Server/Services/TeamMemberService.test.js.map +1 -1
  318. package/build/dist/Tests/Server/TestingUtils/Services/BillingServiceHelper.js +2 -2
  319. package/build/dist/Tests/Server/TestingUtils/Services/BillingServiceHelper.js.map +1 -1
  320. package/build/dist/Tests/Types/OnCallDutyPolicy/LayerUtil.test.js +8 -4
  321. package/build/dist/Tests/Types/OnCallDutyPolicy/LayerUtil.test.js.map +1 -1
  322. package/build/dist/Tests/UI/Components/DictionaryOfStrings.test.js +4 -2
  323. package/build/dist/Tests/UI/Components/DictionaryOfStrings.test.js.map +1 -1
  324. package/build/dist/Tests/UI/Components/FilePicker.test.js +2 -2
  325. package/build/dist/Tests/UI/Components/FilePicker.test.js.map +1 -1
  326. package/build/dist/Tests/Utils/API.test.js +8 -7
  327. package/build/dist/Tests/Utils/API.test.js.map +1 -1
  328. package/build/dist/Types/BaseDatabase/DatabaseCommonInteractionPropsUtil.js +5 -3
  329. package/build/dist/Types/BaseDatabase/DatabaseCommonInteractionPropsUtil.js.map +1 -1
  330. package/build/dist/Types/Html.js +5 -3
  331. package/build/dist/Types/Html.js.map +1 -1
  332. package/build/dist/Types/JSONFunctions.js +5 -5
  333. package/build/dist/Types/JSONFunctions.js.map +1 -1
  334. package/build/dist/Types/Monitor/MonitorType.js +8 -6
  335. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  336. package/build/dist/Types/OnCallDutyPolicy/Layer.js +29 -17
  337. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  338. package/build/dist/Types/Phone.js +5 -3
  339. package/build/dist/Types/Phone.js.map +1 -1
  340. package/build/dist/Types/Workspace/WorkspaceType.js +9 -0
  341. package/build/dist/Types/Workspace/WorkspaceType.js.map +1 -1
  342. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +8 -6
  343. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  344. package/build/dist/UI/Components/Detail/Detail.js +4 -1
  345. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  346. package/build/dist/UI/Components/FilePicker/FilePicker.js +1 -1
  347. package/build/dist/UI/Components/FilePicker/FilePicker.js.map +1 -1
  348. package/build/dist/UI/Components/Image/Image.js +1 -1
  349. package/build/dist/UI/Components/Image/Image.js.map +1 -1
  350. package/build/dist/UI/Components/JSONTable/JSONTable.js.map +1 -1
  351. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  352. package/build/dist/UI/Components/SideMenu/SideMenu.js +4 -2
  353. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  354. package/build/dist/UI/Components/SideMenu/SideMenuItem.js +62 -38
  355. package/build/dist/UI/Components/SideMenu/SideMenuItem.js.map +1 -1
  356. package/build/dist/UI/Config.js +1 -0
  357. package/build/dist/UI/Config.js.map +1 -1
  358. package/build/dist/UI/Utils/API/API.js +5 -3
  359. package/build/dist/UI/Utils/API/API.js.map +1 -1
  360. package/build/dist/UI/Utils/Countries.js.map +1 -1
  361. package/build/dist/UI/Utils/Login.js +6 -1
  362. package/build/dist/UI/Utils/Login.js.map +1 -1
  363. package/build/dist/Utils/Base64.js +12 -0
  364. package/build/dist/Utils/Base64.js.map +1 -0
  365. package/build/dist/Utils/Schema/ModelSchema.js +4 -2
  366. package/build/dist/Utils/Schema/ModelSchema.js.map +1 -1
  367. package/package.json +5 -1
  368. package/build/dist/Models/DatabaseModels/UserTwoFactorAuth.js.map +0 -1
  369. package/build/dist/Server/API/UserTwoFactorAuthAPI.js.map +0 -1
  370. package/build/dist/Server/Services/UserTwoFactorAuthService.js.map +0 -1
  371. package/build/dist/Server/Utils/TwoFactorAuth.js.map +0 -1
@@ -3,14 +3,385 @@ import HTTPResponse from "../../../../Types/API/HTTPResponse";
3
3
  import URL from "../../../../Types/API/URL";
4
4
  import { JSONObject } from "../../../../Types/JSON";
5
5
  import API from "../../../../Utils/API";
6
+ import WorkspaceMessagePayload, {
7
+ WorkspaceCheckboxBlock,
8
+ WorkspaceDateTimePickerBlock,
9
+ WorkspaceDropdownBlock,
10
+ WorkspaceMessageBlock,
11
+ WorkspaceMessagePayloadButton,
12
+ WorkspaceModalBlock,
13
+ WorkspacePayloadButtons,
14
+ WorkspacePayloadHeader,
15
+ WorkspacePayloadImage,
16
+ WorkspacePayloadMarkdown,
17
+ WorkspaceTextAreaBlock,
18
+ WorkspaceTextBoxBlock,
19
+ } from "../../../../Types/Workspace/WorkspaceMessagePayload";
6
20
  import logger from "../../Logger";
7
- import WorkspaceBase from "../WorkspaceBase";
21
+ import Dictionary from "../../../../Types/Dictionary";
22
+ import WorkspaceBase, {
23
+ WorkspaceChannel,
24
+ WorkspaceSendMessageResponse,
25
+ WorkspaceThread,
26
+ } from "../WorkspaceBase";
27
+ import WorkspaceType from "../../../../Types/Workspace/WorkspaceType";
8
28
  import CaptureSpan from "../../Telemetry/CaptureSpan";
29
+ import BadDataException from "../../../../Types/Exception/BadDataException";
30
+ import ObjectID from "../../../../Types/ObjectID";
31
+ import WorkspaceProjectAuthTokenService from "../../../Services/WorkspaceProjectAuthTokenService";
32
+ import WorkspaceProjectAuthToken, {
33
+ MicrosoftTeamsMiscData,
34
+ MicrosoftTeamsTeam,
35
+ } from "../../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
36
+ import Incident from "../../../../Models/DatabaseModels/Incident";
37
+ import IncidentState from "../../../../Models/DatabaseModels/IncidentState";
38
+ import Alert from "../../../../Models/DatabaseModels/Alert";
39
+ import AlertState from "../../../../Models/DatabaseModels/AlertState";
40
+ import ScheduledMaintenance from "../../../../Models/DatabaseModels/ScheduledMaintenance";
41
+ import Monitor from "../../../../Models/DatabaseModels/Monitor";
42
+ import OneUptimeDate from "../../../../Types/Date";
43
+ import {
44
+ MicrosoftTeamsAppClientId,
45
+ MicrosoftTeamsAppClientSecret,
46
+ } from "../../../EnvironmentConfig";
47
+
48
+ // Import services for bot commands
49
+ import IncidentService from "../../../Services/IncidentService";
50
+ import AlertService from "../../../Services/AlertService";
51
+ import ScheduledMaintenanceService from "../../../Services/ScheduledMaintenanceService";
52
+ import IncidentStateService from "../../../Services/IncidentStateService";
53
+ import AlertStateService from "../../../Services/AlertStateService";
54
+
55
+ // Import database utilities
56
+ import QueryHelper from "../../../Types/Database/QueryHelper";
57
+ import SortOrder from "../../../../Types/BaseDatabase/SortOrder";
58
+
59
+ // Microsoft Teams apps should always be single-tenant
60
+ const MICROSOFT_TEAMS_APP_TYPE: string = "SingleTenant";
61
+
62
+ // Bot Framework SDK imports
63
+ import {
64
+ CloudAdapter,
65
+ ConfigurationBotFrameworkAuthentication,
66
+ TeamsActivityHandler,
67
+ TurnContext,
68
+ ConversationReference,
69
+ MessageFactory,
70
+ ConfigurationBotFrameworkAuthenticationOptions,
71
+ Activity,
72
+ ResourceResponse,
73
+ } from "botbuilder";
74
+ import { ExpressRequest, ExpressResponse } from "../../Express";
75
+ // Teams action handlers and types
76
+ import MicrosoftTeamsAuthAction, {
77
+ MicrosoftTeamsRequest,
78
+ } from "./Actions/Auth";
79
+ import MicrosoftTeamsIncidentActions from "./Actions/Incident";
80
+ import {
81
+ MicrosoftTeamsActionType,
82
+ MicrosoftTeamsScheduledMaintenanceActionType,
83
+ MicrosoftTeamsOnCallDutyActionType,
84
+ } from "./Actions/ActionTypes";
85
+ import MicrosoftTeamsAlertActions from "./Actions/Alert";
86
+ import MicrosoftTeamsMonitorActions from "./Actions/Monitor";
87
+ import MicrosoftTeamsScheduledMaintenanceActions from "./Actions/ScheduledMaintenance";
88
+ import MicrosoftTeamsOnCallDutyActions from "./Actions/OnCallDutyPolicy";
89
+
90
+ export default class MicrosoftTeamsUtil extends WorkspaceBase {
91
+ // Get or create Bot Framework adapter for a specific tenant
92
+ private static getBotAdapter(microsoftAppTenantId: string): CloudAdapter {
93
+ if (!MicrosoftTeamsAppClientId || !MicrosoftTeamsAppClientSecret) {
94
+ throw new BadDataException(
95
+ "Microsoft Teams App credentials not configured",
96
+ );
97
+ }
98
+
99
+ logger.debug(
100
+ "Creating Bot Framework adapter with authentication configuration",
101
+ );
102
+ logger.debug(`App ID: ${MicrosoftTeamsAppClientId}`);
103
+ logger.debug(`App Type: ${MICROSOFT_TEAMS_APP_TYPE}`);
104
+ logger.debug(`Tenant ID: ${microsoftAppTenantId}`);
105
+
106
+ const authConfig: ConfigurationBotFrameworkAuthenticationOptions = {
107
+ MicrosoftAppId: MicrosoftTeamsAppClientId,
108
+ MicrosoftAppPassword: MicrosoftTeamsAppClientSecret,
109
+ MicrosoftAppType: MICROSOFT_TEAMS_APP_TYPE,
110
+ MicrosoftAppTenantId: microsoftAppTenantId,
111
+ };
112
+
113
+ const botFrameworkAuthentication: ConfigurationBotFrameworkAuthentication =
114
+ new ConfigurationBotFrameworkAuthentication(authConfig);
115
+ const adapter: CloudAdapter = new CloudAdapter(botFrameworkAuthentication);
116
+
117
+ logger.debug("Bot Framework adapter created successfully");
118
+ return adapter;
119
+ }
120
+ // Helper method to get a valid access token, refreshing if necessary
121
+ public static async getValidAccessToken(data: {
122
+ authToken: string;
123
+ projectId: ObjectID;
124
+ }): Promise<string> {
125
+ logger.debug("=== getValidAccessToken called ===");
126
+ logger.debug(`Project ID: ${data.projectId.toString()}`);
127
+ logger.debug(
128
+ `Auth token (first 20 chars): ${data.authToken?.substring(0, 20)}...`,
129
+ );
130
+
131
+ // Get project auth and check token expiration
132
+ const projectAuth: WorkspaceProjectAuthToken | null =
133
+ await WorkspaceProjectAuthTokenService.getProjectAuth({
134
+ projectId: data.projectId,
135
+ workspaceType: WorkspaceType.MicrosoftTeams,
136
+ });
137
+
138
+ logger.debug(`Project auth found: ${Boolean(projectAuth)}`);
139
+ if (projectAuth) {
140
+ logger.debug(
141
+ `Project auth has miscData: ${Boolean(projectAuth.miscData)}`,
142
+ );
143
+ }
144
+
145
+ if (!projectAuth || !projectAuth.miscData) {
146
+ logger.error(
147
+ "Microsoft Teams integration not found for this project - no project auth or miscData",
148
+ );
149
+ throw new BadDataException(
150
+ "Microsoft Teams integration not found for this project",
151
+ );
152
+ }
153
+
154
+ const miscData: MicrosoftTeamsMiscData =
155
+ projectAuth.miscData as MicrosoftTeamsMiscData;
156
+ logger.debug(
157
+ `MiscData appAccessToken exists: ${Boolean(miscData.appAccessToken)}`,
158
+ );
159
+ logger.debug(
160
+ `MiscData appAccessTokenExpiresAt: ${miscData.appAccessTokenExpiresAt}`,
161
+ );
162
+
163
+ // Check if token exists and is valid
164
+ if (miscData.appAccessToken && miscData.appAccessToken.includes(".")) {
165
+ logger.debug("Found app access token in miscData");
166
+ // Check if token is expired
167
+ if (miscData.appAccessTokenExpiresAt) {
168
+ const expiryDate: Date = OneUptimeDate.fromString(
169
+ miscData.appAccessTokenExpiresAt,
170
+ );
171
+ const now: Date = OneUptimeDate.getCurrentDate();
172
+ const isExpired: boolean = OneUptimeDate.isAfter(now, expiryDate);
173
+ const secondsToExpiry: number = OneUptimeDate.getSecondsTo(expiryDate);
174
+ logger.debug(`Token expires in ${secondsToExpiry} seconds`);
175
+ logger.debug(`Token is expired: ${isExpired}`);
176
+
177
+ // If token is already expired or expires within the next 5 minutes, refresh it
178
+ if (isExpired || secondsToExpiry <= 300) {
179
+ logger.debug(
180
+ "Access token is expired or expiring soon, attempting to refresh",
181
+ );
182
+ const newToken: string | null = await this.refreshAccessToken({
183
+ projectId: data.projectId,
184
+ miscData,
185
+ });
186
+ if (newToken) {
187
+ logger.debug("Successfully refreshed token");
188
+ return newToken;
189
+ }
190
+ logger.warn("Failed to refresh token, falling back to cached token");
191
+ } else {
192
+ logger.debug(
193
+ "Using cached appAccessToken from miscData for Microsoft Graph API call",
194
+ );
195
+ return miscData.appAccessToken;
196
+ }
197
+ } else {
198
+ // No expiry information, use the token but it might be expired
199
+ logger.debug(
200
+ "Using appAccessToken from miscData (no expiry info available)",
201
+ );
202
+ return miscData.appAccessToken;
203
+ }
204
+ }
205
+
206
+ // If we couldn't find a valid token, try to refresh
207
+ logger.debug("No valid app access token found, attempting to refresh");
208
+ const newToken: string | null = await this.refreshAccessToken({
209
+ projectId: data.projectId,
210
+ miscData,
211
+ });
212
+ if (newToken) {
213
+ logger.debug("Successfully refreshed token");
214
+ return newToken;
215
+ }
216
+
217
+ // If refresh failed, throw error
218
+ logger.error("Could not obtain valid access token for Microsoft Teams");
219
+ throw new BadDataException(
220
+ "Could not obtain valid access token for Microsoft Teams",
221
+ );
222
+ }
223
+
224
+ // Method to refresh the Microsoft Teams access token
225
+ private static async refreshAccessToken(data: {
226
+ projectId: ObjectID;
227
+ miscData: MicrosoftTeamsMiscData;
228
+ }): Promise<string | null> {
229
+ logger.debug("=== refreshAccessToken called ===");
230
+ logger.debug(`Project ID: ${data.projectId.toString()}`);
231
+ logger.debug(`Tenant ID: ${data.miscData.tenantId}`);
232
+
233
+ try {
234
+ // Check if we have the necessary client credentials
235
+ if (!MicrosoftTeamsAppClientId || !MicrosoftTeamsAppClientSecret) {
236
+ logger.error(
237
+ "Microsoft Teams app client credentials are not configured",
238
+ );
239
+ logger.error(
240
+ "Please set MICROSOFT_TEAMS_APP_CLIENT_ID and MICROSOFT_TEAMS_APP_CLIENT_SECRET environment variables",
241
+ );
242
+ return null;
243
+ }
244
+
245
+ logger.debug("Client credentials are configured");
246
+
247
+ if (!data.miscData.tenantId) {
248
+ logger.error("Tenant ID not found in miscData, cannot refresh token");
249
+ return null;
250
+ }
251
+
252
+ logger.debug(
253
+ `Attempting to refresh Microsoft Teams access token for project ${data.projectId.toString()}`,
254
+ );
255
+ logger.debug(`Using tenant ID: ${data.miscData.tenantId}`);
256
+
257
+ // Use OAuth 2.0 client credentials flow to get a new app access token
258
+ const tokenUrl: string = `https://login.microsoftonline.com/${data.miscData.tenantId}/oauth2/v2.0/token`;
259
+ logger.debug(`Token URL: ${tokenUrl}`);
260
+
261
+ const tokenRequestBody: JSONObject = {
262
+ client_id: MicrosoftTeamsAppClientId,
263
+ client_secret: MicrosoftTeamsAppClientSecret,
264
+ grant_type: "client_credentials",
265
+ scope: "https://graph.microsoft.com/.default",
266
+ };
267
+
268
+ logger.debug("Making token refresh request to Microsoft");
269
+ const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
270
+ await API.post({
271
+ url: URL.fromString(tokenUrl),
272
+ data: tokenRequestBody,
273
+ headers: {
274
+ "Content-Type": "application/x-www-form-urlencoded",
275
+ Accept: "application/json",
276
+ },
277
+ });
278
+
279
+ if (response instanceof HTTPErrorResponse) {
280
+ logger.error("Error refreshing Microsoft Teams access token:");
281
+ logger.error(response);
282
+ return null;
283
+ }
284
+
285
+ logger.debug("Token refresh response received successfully");
286
+ const tokenData: JSONObject = response.data;
287
+ const newAccessToken: string = tokenData["access_token"] as string;
288
+ const expiresIn: number = tokenData["expires_in"] as number; // seconds
289
+
290
+ logger.debug(`New access token received: ${Boolean(newAccessToken)}`);
291
+ logger.debug(`Token expires in: ${expiresIn} seconds`);
292
+
293
+ if (!newAccessToken) {
294
+ logger.error("No access token received in token refresh response");
295
+ return null;
296
+ }
297
+
298
+ // Calculate expiry time
299
+ const now: Date = OneUptimeDate.getCurrentDate();
300
+ const expiryDate: Date = OneUptimeDate.addRemoveSeconds(
301
+ now,
302
+ expiresIn - 300,
303
+ ); // Subtrutes buffer
304
+
305
+ logger.debug(
306
+ `Token expiry calculated: ${OneUptimeDate.toString(expiryDate)}`,
307
+ );
308
+
309
+ // Update the miscData with new token and expiry
310
+ const updatedMiscData: MicrosoftTeamsMiscData = {
311
+ ...data.miscData,
312
+ appAccessToken: newAccessToken,
313
+ appAccessTokenExpiresAt: OneUptimeDate.toString(expiryDate),
314
+ lastAppTokenIssuedAt: OneUptimeDate.toString(now),
315
+ };
316
+
317
+ logger.debug("Saving updated token to database");
318
+ // Save the updated token to the database
319
+ await WorkspaceProjectAuthTokenService.refreshAuthToken({
320
+ projectId: data.projectId,
321
+ workspaceType: WorkspaceType.MicrosoftTeams,
322
+ authToken: newAccessToken,
323
+ workspaceProjectId: data.miscData.tenantId,
324
+ miscData: updatedMiscData as any,
325
+ });
326
+
327
+ logger.debug("Microsoft Teams access token refreshed successfully");
328
+ logger.debug(
329
+ `New token expires at: ${updatedMiscData.appAccessTokenExpiresAt}`,
330
+ );
331
+
332
+ return newAccessToken;
333
+ } catch (error) {
334
+ logger.error("Error refreshing Microsoft Teams access token:");
335
+ logger.error(error);
336
+ return null;
337
+ }
338
+ }
339
+
340
+ // Extract action type and value from Teams Adaptive Card submit value
341
+ private static extractActionFromValue(value: JSONObject): {
342
+ actionType: MicrosoftTeamsActionType;
343
+ actionValue: string;
344
+ } {
345
+ /*
346
+ * Support multiple shapes that Teams may send for Adaptive Card submits
347
+ * 1) { action: "ack-incident", actionValue: "<id>" }
348
+ * 2) { data: { action: "ack-incident", actionValue: "<id>" } }
349
+ * 3) { action: { type: "Action.Submit", data: { action: "ack-incident", actionValue: "<id>" } } }
350
+ */
351
+ let actionType: string = (value["action"] as string) || "";
352
+ let actionValue: string = (value["actionValue"] as string) || "";
353
+
354
+ const valData: JSONObject | undefined =
355
+ (value["data"] as JSONObject) || undefined;
356
+ if ((!actionType || !actionValue) && valData) {
357
+ actionType = (valData["action"] as string) || actionType;
358
+ actionValue = (valData["actionValue"] as string) || actionValue;
359
+ }
360
+
361
+ const actionObj: JSONObject | undefined = value[
362
+ "action"
363
+ ] as unknown as JSONObject;
364
+ if (
365
+ (!actionType || !actionValue) &&
366
+ actionObj &&
367
+ typeof actionObj === "object"
368
+ ) {
369
+ const embeddedData: JSONObject | undefined =
370
+ (actionObj["data"] as JSONObject) || undefined;
371
+ if (embeddedData) {
372
+ actionType = (embeddedData["action"] as string) || actionType;
373
+ actionValue = (embeddedData["actionValue"] as string) || actionValue;
374
+ }
375
+ }
376
+
377
+ return { actionType: actionType as MicrosoftTeamsActionType, actionValue };
378
+ }
9
379
 
10
- export default class MicrosoftTeams extends WorkspaceBase {
11
380
  private static buildMessageCardFromMarkdown(markdown: string): JSONObject {
12
- // Teams MessageCard has limited markdown support. Headings like '##' are not supported
13
- // and single newlines can collapse. Convert common patterns to a structured card.
381
+ /*
382
+ * Teams MessageCard has limited markdown support. Headings like '##' are not supported
383
+ * and single newlines can collapse. Convert common patterns to a structured card.
384
+ */
14
385
  const lines: Array<string> = markdown
15
386
  .split("\n")
16
387
  .map((l: string) => {
@@ -32,13 +403,34 @@ export default class MicrosoftTeams extends WorkspaceBase {
32
403
  .replace(/^#+\s*/, "") // remove leading markdown headers like ##
33
404
  .replace(/^\*\*|\*\*$/g, "") // remove stray bold markers if any
34
405
  .trim();
406
+ // Remove markdown link syntax from title for cleaner rendering
407
+ const titleLinkRegex: RegExp = /\[([^\]]+)\]\(([^)]+)\)/g;
408
+ title = title.replace(titleLinkRegex, "$1");
409
+ // Sanitize unmatched bold markers if any remain
410
+ const boldCountTitle: number = (title.match(/\*\*/g) || []).length;
411
+ if (boldCountTitle % 2 !== 0) {
412
+ title = title.replace(/\*\*/g, "");
413
+ }
35
414
  lines.shift();
36
415
  }
37
416
 
38
417
  const linkRegex: RegExp = /\[([^\]]+)\]\(([^)]+)\)/g; // [text](url)
39
418
 
419
+ // Helper to clean up unmatched bold markers that can break rendering
420
+ const sanitizeMarkdownText: (text: string) => string = (
421
+ text: string,
422
+ ): string => {
423
+ const boldCount: number = (text.match(/\*\*/g) || []).length;
424
+ // If we have an odd number of **, remove them all to avoid raw markers showing
425
+ if (boldCount % 2 !== 0) {
426
+ text = text.replace(/\*\*/g, "");
427
+ }
428
+ // Collapse multiple spaces introduced by replacements
429
+ return text.replace(/\s{2,}/g, " ");
430
+ };
431
+
40
432
  for (const line of lines) {
41
- // Extract links to actions and strip them from text
433
+ // Extract links to actions and keep link display text in-place (without markdown)
42
434
  let lineWithoutLinks: string = line;
43
435
  let match: RegExpExecArray | null = null;
44
436
  while ((match = linkRegex.exec(line))) {
@@ -54,7 +446,8 @@ export default class MicrosoftTeams extends WorkspaceBase {
54
446
  },
55
447
  ],
56
448
  });
57
- lineWithoutLinks = lineWithoutLinks.replace(match[0], "").trim();
449
+ // Replace markdown link with just the display text to preserve sentence flow
450
+ lineWithoutLinks = lineWithoutLinks.replace(match[0], name).trim();
58
451
  }
59
452
 
60
453
  // Parse facts of the form **Label:** value
@@ -74,7 +467,7 @@ export default class MicrosoftTeams extends WorkspaceBase {
74
467
  facts.push({ name: name, value: value });
75
468
  }
76
469
  } else if (lineWithoutLinks) {
77
- bodyTextParts.push(lineWithoutLinks);
470
+ bodyTextParts.push(sanitizeMarkdownText(lineWithoutLinks));
78
471
  }
79
472
  }
80
473
 
@@ -85,16 +478,16 @@ export default class MicrosoftTeams extends WorkspaceBase {
85
478
  summary: title,
86
479
  };
87
480
 
481
+ // Build a single section so we can enable markdown explicitly
482
+ const section: JSONObject = { markdown: true } as any;
88
483
  if (bodyTextParts.length > 0) {
89
- payload["text"] = bodyTextParts.join("\n\n");
484
+ section["text"] = bodyTextParts.join("\n\n");
90
485
  }
91
-
92
486
  if (facts.length > 0) {
93
- payload["sections"] = [
94
- {
95
- facts: facts,
96
- },
97
- ];
487
+ section["facts"] = facts;
488
+ }
489
+ if (section["text"] || section["facts"]) {
490
+ payload["sections"] = [section];
98
491
  }
99
492
 
100
493
  if (actions.length > 0) {
@@ -156,4 +549,2144 @@ export default class MicrosoftTeams extends WorkspaceBase {
156
549
  urlString.includes("office.com")
157
550
  );
158
551
  }
552
+
553
+ @CaptureSpan()
554
+ public static override async getUsernameFromUserId(data: {
555
+ authToken: string;
556
+ userId: string;
557
+ projectId: ObjectID;
558
+ }): Promise<string | null> {
559
+ logger.debug("Getting username from user ID with data:");
560
+ logger.debug(data);
561
+
562
+ // Get valid access token
563
+ const accessToken: string = await this.getValidAccessToken({
564
+ authToken: data.authToken,
565
+ projectId: data.projectId,
566
+ });
567
+
568
+ const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
569
+ await API.get<JSONObject>({
570
+ url: URL.fromString(
571
+ `https://graph.microsoft.com/v1.0/users/${data.userId}`,
572
+ ),
573
+ headers: {
574
+ Authorization: `Bearer ${accessToken}`,
575
+ "Content-Type": "application/json",
576
+ },
577
+ });
578
+
579
+ logger.debug("Response from Microsoft Graph API for getting user info:");
580
+ logger.debug(response);
581
+
582
+ if (response instanceof HTTPErrorResponse) {
583
+ logger.error("Error response from Microsoft Graph API:");
584
+ logger.error(response);
585
+ throw response;
586
+ }
587
+
588
+ const userData: JSONObject = response.data;
589
+ const username: string =
590
+ (userData["displayName"] as string) ||
591
+ (userData["userPrincipalName"] as string);
592
+
593
+ logger.debug("Username obtained:");
594
+ logger.debug(username);
595
+ return username;
596
+ }
597
+
598
+ @CaptureSpan()
599
+ public static override async sendDirectMessageToUser(data: {
600
+ authToken: string;
601
+ workspaceUserId: string;
602
+ messageBlocks: Array<WorkspaceMessageBlock>;
603
+ }): Promise<void> {
604
+ // Send direct message to user via Microsoft Graph API
605
+ const adaptiveCard: JSONObject = this.buildAdaptiveCardFromMessageBlocks({
606
+ messageBlocks: data.messageBlocks,
607
+ });
608
+
609
+ const chatMessage: JSONObject = {
610
+ body: {
611
+ contentType: "html",
612
+ content: this.convertAdaptiveCardToHtml(adaptiveCard),
613
+ },
614
+ attachments: [
615
+ {
616
+ contentType: "application/vnd.microsoft.card.adaptive",
617
+ content: adaptiveCard,
618
+ },
619
+ ],
620
+ };
621
+
622
+ await API.post({
623
+ url: URL.fromString(
624
+ `https://graph.microsoft.com/v1.0/chats/${data.workspaceUserId}/messages`,
625
+ ),
626
+ data: chatMessage,
627
+ headers: {
628
+ Authorization: `Bearer ${data.authToken}`,
629
+ "Content-Type": "application/json",
630
+ },
631
+ });
632
+ }
633
+
634
+ @CaptureSpan()
635
+ public static override async createChannelsIfDoesNotExist(data: {
636
+ authToken: string;
637
+ channelNames: Array<string>;
638
+ projectId: ObjectID;
639
+ teamId: string; // Required team ID
640
+ }): Promise<Array<WorkspaceChannel>> {
641
+ logger.debug("Creating channels if they do not exist with data:");
642
+ logger.debug(data);
643
+
644
+ const workspaceChannels: Array<WorkspaceChannel> = [];
645
+
646
+ for (let channelName of data.channelNames) {
647
+ // Normalize channel name - Teams has different naming requirements
648
+ if (channelName && channelName.startsWith("#")) {
649
+ channelName = channelName.substring(1);
650
+ }
651
+ // Teams channels cannot have spaces in the name for some operations
652
+ const normalizedChannelName: string = channelName.replace(/\s+/g, "-");
653
+
654
+ // Check if channel exists
655
+ const existingChannel: WorkspaceChannel | null =
656
+ await this.getWorkspaceChannelByName({
657
+ authToken: data.authToken,
658
+ channelName: normalizedChannelName,
659
+ projectId: data.projectId,
660
+ teamId: data.teamId,
661
+ });
662
+
663
+ if (existingChannel) {
664
+ logger.debug(`Channel ${channelName} already exists.`);
665
+ workspaceChannels.push(existingChannel);
666
+ continue;
667
+ }
668
+
669
+ logger.debug(`Channel ${channelName} does not exist. Creating channel.`);
670
+ const createChannelData: {
671
+ authToken: string;
672
+ channelName: string;
673
+ projectId: ObjectID;
674
+ teamId: string;
675
+ } = {
676
+ authToken: data.authToken,
677
+ channelName: normalizedChannelName,
678
+ projectId: data.projectId,
679
+ teamId: data.teamId,
680
+ };
681
+
682
+ const channel: WorkspaceChannel =
683
+ await this.createChannel(createChannelData);
684
+
685
+ if (channel) {
686
+ logger.debug(`Channel ${channelName} created successfully.`);
687
+ workspaceChannels.push(channel);
688
+ }
689
+ }
690
+
691
+ logger.debug("Channels created or found:");
692
+ logger.debug(workspaceChannels);
693
+ return workspaceChannels;
694
+ }
695
+
696
+ @CaptureSpan()
697
+ public static override async createChannel(data: {
698
+ authToken: string;
699
+ channelName: string;
700
+ projectId: ObjectID;
701
+ teamId: string; // Required team ID
702
+ }): Promise<WorkspaceChannel> {
703
+ const teamId: string = data.teamId;
704
+
705
+ // Get valid access token
706
+ const accessToken: string = await this.getValidAccessToken({
707
+ authToken: data.authToken,
708
+ projectId: data.projectId,
709
+ });
710
+
711
+ const channelPayload: JSONObject = {
712
+ displayName: data.channelName,
713
+ description: `OneUptime notifications for ${data.channelName}`,
714
+ membershipType: "standard",
715
+ };
716
+
717
+ logger.debug("Creating Teams channel with payload:");
718
+ logger.debug(channelPayload);
719
+
720
+ const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
721
+ await API.post({
722
+ url: URL.fromString(
723
+ `https://graph.microsoft.com/v1.0/teams/${teamId}/channels`,
724
+ ),
725
+ data: channelPayload,
726
+ headers: {
727
+ Authorization: `Bearer ${accessToken}`,
728
+ "Content-Type": "application/json",
729
+ },
730
+ });
731
+
732
+ if (response instanceof HTTPErrorResponse) {
733
+ logger.error("Error response from Microsoft Graph API:");
734
+ logger.error(response);
735
+ throw response;
736
+ }
737
+
738
+ const channelData: JSONObject = response.data;
739
+ const channel: WorkspaceChannel = {
740
+ id: channelData["id"] as string,
741
+ name: channelData["displayName"] as string,
742
+ workspaceType: WorkspaceType.MicrosoftTeams,
743
+ teamId: data.teamId,
744
+ };
745
+
746
+ logger.debug("Channel created successfully:");
747
+ logger.debug(channel);
748
+
749
+ return channel;
750
+ }
751
+
752
+ @CaptureSpan()
753
+ public static override async getWorkspaceChannelFromChannelName(data: {
754
+ authToken: string;
755
+ channelName: string;
756
+ projectId: ObjectID;
757
+ teamId: string;
758
+ }): Promise<WorkspaceChannel> {
759
+ const channel: WorkspaceChannel | null =
760
+ await this.getWorkspaceChannelByName({
761
+ authToken: data.authToken,
762
+ channelName: data.channelName,
763
+ projectId: data.projectId,
764
+ teamId: data.teamId,
765
+ });
766
+
767
+ if (!channel) {
768
+ throw new BadDataException("Channel not found.");
769
+ }
770
+
771
+ return channel;
772
+ }
773
+
774
+ @CaptureSpan()
775
+ public static async getWorkspaceChannelByName(data: {
776
+ authToken: string;
777
+ channelName: string;
778
+ projectId: ObjectID;
779
+ teamId: string;
780
+ }): Promise<WorkspaceChannel | null> {
781
+ logger.debug(`Getting workspace channel by name: ${data.channelName}`);
782
+
783
+ // Get project auth to get available teams
784
+ const projectAuth: WorkspaceProjectAuthToken | null =
785
+ await WorkspaceProjectAuthTokenService.getProjectAuth({
786
+ projectId: data.projectId,
787
+ workspaceType: WorkspaceType.MicrosoftTeams,
788
+ });
789
+
790
+ if (!projectAuth?.miscData) {
791
+ logger.error("Microsoft Teams integration not found for this project");
792
+ throw new BadDataException(
793
+ "Microsoft Teams integration not found for this project",
794
+ );
795
+ }
796
+
797
+ // Get valid access token
798
+ const accessToken: string | null = await this.getValidAccessToken({
799
+ authToken: data.authToken,
800
+ projectId: data.projectId,
801
+ });
802
+
803
+ // Get channels for this team
804
+ const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
805
+ await API.get({
806
+ url: URL.fromString(
807
+ `https://graph.microsoft.com/v1.0/teams/${data.teamId}/channels`,
808
+ ),
809
+ headers: {
810
+ Authorization: `Bearer ${accessToken}`,
811
+ "Content-Type": "application/json",
812
+ },
813
+ });
814
+
815
+ if (response instanceof HTTPErrorResponse) {
816
+ logger.error("Error response from Microsoft Graph API:");
817
+ logger.error(response);
818
+ throw response;
819
+ }
820
+
821
+ const channelsData: JSONObject = response.data;
822
+ const channels: Array<JSONObject> =
823
+ (channelsData["value"] as Array<JSONObject>) || [];
824
+
825
+ logger.debug(`Found ${channels.length} channels from API`);
826
+
827
+ const channelName: string = data.channelName.toLowerCase();
828
+
829
+ for (const channelData of channels) {
830
+ const displayName: string | undefined = channelData[
831
+ "displayName"
832
+ ] as string;
833
+ if (!displayName) {
834
+ continue;
835
+ }
836
+ const apiChannelName: string = displayName.toLowerCase();
837
+ logger.debug(
838
+ `Comparing channel '${apiChannelName}' with requested '${channelName}'`,
839
+ );
840
+ if (apiChannelName === channelName) {
841
+ const foundChannel: WorkspaceChannel = {
842
+ id: `${channelData["id"]}`,
843
+ name: displayName,
844
+ workspaceType: WorkspaceType.MicrosoftTeams,
845
+ teamId: data.teamId,
846
+ };
847
+ logger.debug(`Channel match found: ${JSON.stringify(foundChannel)}`);
848
+ return foundChannel;
849
+ }
850
+ }
851
+
852
+ logger.debug(`No channel found with name: ${data.channelName}`);
853
+ return null;
854
+ }
855
+
856
+ @CaptureSpan()
857
+ public static override async sendMessage(data: {
858
+ workspaceMessagePayload: WorkspaceMessagePayload;
859
+ authToken: string;
860
+ userId: string;
861
+ projectId: ObjectID;
862
+ }): Promise<WorkspaceSendMessageResponse> {
863
+ logger.debug("=== MicrosoftTeamsUtil.sendMessage called ===");
864
+ logger.debug("Sending message to Microsoft Teams with data:");
865
+ logger.debug(data);
866
+
867
+ const adaptiveCard: JSONObject = this.buildAdaptiveCardFromMessageBlocks({
868
+ messageBlocks: data.workspaceMessagePayload.messageBlocks,
869
+ });
870
+
871
+ logger.debug("Adaptive card built successfully:");
872
+ logger.debug(JSON.stringify(adaptiveCard, null, 2));
873
+
874
+ const workspaceChannelsToPostTo: Array<WorkspaceChannel> = [];
875
+
876
+ logger.debug(
877
+ `Processing ${data.workspaceMessagePayload.channelNames.length} channel names`,
878
+ );
879
+ logger.debug(
880
+ `Channel names: ${JSON.stringify(data.workspaceMessagePayload.channelNames)}`,
881
+ );
882
+
883
+ // Resolve channel names
884
+ for (const channelName of data.workspaceMessagePayload.channelNames) {
885
+ logger.debug(`Attempting to resolve channel name: ${channelName}`);
886
+
887
+ if (!data.workspaceMessagePayload.teamId) {
888
+ throw new BadDataException(
889
+ "Team ID is required to resolve channel names.",
890
+ );
891
+ }
892
+
893
+ const channel: WorkspaceChannel | null =
894
+ await this.getWorkspaceChannelByName({
895
+ authToken: data.authToken,
896
+ channelName: channelName,
897
+ projectId: data.projectId,
898
+ teamId: data.workspaceMessagePayload.teamId,
899
+ });
900
+
901
+ if (channel) {
902
+ logger.debug(
903
+ `Channel resolved successfully: ${JSON.stringify(channel)}`,
904
+ );
905
+ workspaceChannelsToPostTo.push(channel);
906
+ } else {
907
+ logger.warn(`Channel not found: ${channelName}`);
908
+ }
909
+ }
910
+
911
+ logger.debug("=== Starting message sending loop ===");
912
+ logger.debug(
913
+ `Total channels to post to: ${workspaceChannelsToPostTo.length}`,
914
+ );
915
+ logger.debug(`Channels: ${JSON.stringify(workspaceChannelsToPostTo)}`);
916
+
917
+ // Add channels by ID
918
+ for (const channelId of data.workspaceMessagePayload.channelIds) {
919
+ if (!data.workspaceMessagePayload.teamId) {
920
+ throw new BadDataException(
921
+ "Team ID is required to resolve channel IDs.",
922
+ );
923
+ }
924
+
925
+ try {
926
+ logger.debug(`Getting channel info for channel ID: ${channelId}`);
927
+ const channel: WorkspaceChannel =
928
+ await this.getWorkspaceChannelFromChannelId({
929
+ authToken: data.authToken,
930
+ channelId: channelId,
931
+ projectId: data.projectId,
932
+ teamId: data.workspaceMessagePayload.teamId,
933
+ });
934
+ logger.debug(`Channel info obtained: ${JSON.stringify(channel)}`);
935
+ workspaceChannelsToPostTo.push(channel);
936
+ } catch (err) {
937
+ logger.error(`Error getting channel info for channel ID ${channelId}:`);
938
+ logger.error(err);
939
+ }
940
+ }
941
+
942
+ logger.debug("=== Starting message sending loop ===");
943
+ logger.debug(
944
+ `Total channels to post to: ${workspaceChannelsToPostTo.length}`,
945
+ );
946
+ logger.debug(`Channels: ${JSON.stringify(workspaceChannelsToPostTo)}`);
947
+
948
+ const workspaceMessageResponse: WorkspaceSendMessageResponse = {
949
+ threads: [],
950
+ workspaceType: WorkspaceType.MicrosoftTeams,
951
+ errors: [],
952
+ };
953
+
954
+ for (const channel of workspaceChannelsToPostTo) {
955
+ try {
956
+ logger.debug(
957
+ `Attempting to send message to channel: ${JSON.stringify(channel)}`,
958
+ );
959
+
960
+ if (!data.workspaceMessagePayload.teamId) {
961
+ throw new BadDataException(
962
+ "Team ID is required to send messages to channels.",
963
+ );
964
+ }
965
+
966
+ const thread: WorkspaceThread = await this.sendAdaptiveCardToChannel({
967
+ authToken: data.authToken,
968
+ teamId: data.workspaceMessagePayload.teamId!,
969
+ workspaceChannel: channel,
970
+ adaptiveCard: adaptiveCard,
971
+ projectId: data.projectId,
972
+ });
973
+
974
+ logger.debug(
975
+ `Message sent successfully to channel ${channel.name}, thread: ${JSON.stringify(thread)}`,
976
+ );
977
+ workspaceMessageResponse.threads.push(thread);
978
+ } catch (e) {
979
+ logger.error(`Error sending message to channel ID ${channel.id}:`);
980
+ logger.error(e);
981
+ workspaceMessageResponse.errors!.push({
982
+ channel: channel,
983
+ error: e instanceof Error ? e.message : String(e),
984
+ });
985
+ }
986
+ }
987
+
988
+ logger.debug("=== Message sending completed ===");
989
+ logger.debug(
990
+ `Final thread count: ${workspaceMessageResponse.threads.length}`,
991
+ );
992
+ logger.debug(`Final response: ${JSON.stringify(workspaceMessageResponse)}`);
993
+
994
+ return workspaceMessageResponse;
995
+ }
996
+
997
+ @CaptureSpan()
998
+ public static async sendAdaptiveCardToChannel(data: {
999
+ authToken: string;
1000
+ teamId: string;
1001
+ workspaceChannel: WorkspaceChannel;
1002
+ adaptiveCard: JSONObject;
1003
+ projectId: ObjectID;
1004
+ }): Promise<WorkspaceThread> {
1005
+ logger.debug(
1006
+ `Sending adaptive card to channel via Bot Framework: ${data.workspaceChannel.name} (${data.workspaceChannel.id})`,
1007
+ );
1008
+ logger.debug(`Team ID: ${data.teamId}`);
1009
+ logger.debug(`Adaptive card: ${JSON.stringify(data.adaptiveCard)}`);
1010
+
1011
+ try {
1012
+ // Get project auth to retrieve bot ID
1013
+ const projectAuth: WorkspaceProjectAuthToken | null =
1014
+ await WorkspaceProjectAuthTokenService.getProjectAuth({
1015
+ projectId: data.projectId,
1016
+ workspaceType: WorkspaceType.MicrosoftTeams,
1017
+ });
1018
+
1019
+ if (!projectAuth || !projectAuth.miscData) {
1020
+ throw new BadDataException(
1021
+ "Microsoft Teams integration not found for this project",
1022
+ );
1023
+ }
1024
+
1025
+ const miscData: MicrosoftTeamsMiscData =
1026
+ projectAuth.miscData as MicrosoftTeamsMiscData;
1027
+ if (!miscData.botId) {
1028
+ throw new BadDataException(
1029
+ "Bot ID not found in Microsoft Teams integration",
1030
+ );
1031
+ }
1032
+
1033
+ // Check if app client ID is configured
1034
+ if (!MicrosoftTeamsAppClientId) {
1035
+ throw new BadDataException(
1036
+ "Microsoft Teams App Client ID not configured",
1037
+ );
1038
+ }
1039
+
1040
+ logger.debug(`Using bot ID: ${miscData.botId}`);
1041
+
1042
+ // Get Bot Framework adapter
1043
+ const adapter: CloudAdapter = this.getBotAdapter(miscData.tenantId);
1044
+
1045
+ // Create conversation reference for the channel
1046
+ const conversationReference: ConversationReference = {
1047
+ bot: {
1048
+ id: MicrosoftTeamsAppClientId,
1049
+ name: "OneUptime Bot",
1050
+ },
1051
+ conversation: {
1052
+ id: data.workspaceChannel.id,
1053
+ name: data.workspaceChannel.name,
1054
+ isGroup: true,
1055
+ conversationType: "channel",
1056
+ tenantId: miscData.tenantId,
1057
+ },
1058
+ channelId: "msteams",
1059
+ serviceUrl: "https://smba.trafficmanager.net/teams/",
1060
+ };
1061
+
1062
+ logger.debug(
1063
+ `Conversation reference: ${JSON.stringify(conversationReference)}`,
1064
+ );
1065
+
1066
+ // Send proactive message using Bot Framework
1067
+ let messageId: string = "";
1068
+
1069
+ await adapter.continueConversationAsync(
1070
+ MicrosoftTeamsAppClientId,
1071
+ conversationReference,
1072
+ async (context: TurnContext) => {
1073
+ logger.debug("Sending adaptive card as proactive message");
1074
+
1075
+ // Create message with adaptive card attachment
1076
+ const message: Partial<Activity> = MessageFactory.attachment({
1077
+ contentType: "application/vnd.microsoft.card.adaptive",
1078
+ content: data.adaptiveCard,
1079
+ });
1080
+
1081
+ const response: ResourceResponse | undefined =
1082
+ await context.sendActivity(message);
1083
+
1084
+ messageId = response?.id || "";
1085
+
1086
+ logger.debug(`Message sent with ID: ${messageId}`);
1087
+ },
1088
+ );
1089
+
1090
+ const thread: WorkspaceThread = {
1091
+ channel: data.workspaceChannel,
1092
+ threadId: messageId,
1093
+ };
1094
+
1095
+ logger.debug(
1096
+ `Created thread via Bot Framework: ${JSON.stringify(thread)}`,
1097
+ );
1098
+ return thread;
1099
+ } catch (error) {
1100
+ logger.error("Error sending adaptive card via Bot Framework:");
1101
+ logger.error(error);
1102
+ throw error;
1103
+ }
1104
+ }
1105
+
1106
+ @CaptureSpan()
1107
+ public static override async getWorkspaceChannelFromChannelId(data: {
1108
+ authToken: string;
1109
+ channelId: string;
1110
+ teamId: string;
1111
+ projectId: ObjectID;
1112
+ }): Promise<WorkspaceChannel> {
1113
+ logger.debug("=== getWorkspaceChannelFromChannelId called ===");
1114
+ logger.debug(`Channel ID: ${data.channelId}`);
1115
+ logger.debug(`Team ID: ${data.teamId}`);
1116
+ logger.debug(`Project ID: ${data.projectId.toString()}`);
1117
+
1118
+ try {
1119
+ // Get valid access token
1120
+ const accessToken: string | null = await this.getValidAccessToken({
1121
+ authToken: data.authToken,
1122
+ projectId: data.projectId,
1123
+ });
1124
+
1125
+ logger.debug("Access token obtained for channel info retrieval");
1126
+
1127
+ // Fetch channel information from Microsoft Graph API
1128
+ const apiUrl: string = `https://graph.microsoft.com/v1.0/teams/${data.teamId}/channels/${data.channelId}`;
1129
+ logger.debug(`Making API call to: ${apiUrl}`);
1130
+
1131
+ const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
1132
+ await API.get({
1133
+ url: URL.fromString(apiUrl),
1134
+ headers: {
1135
+ Authorization: `Bearer ${accessToken}`,
1136
+ "Content-Type": "application/json",
1137
+ },
1138
+ });
1139
+
1140
+ if (response instanceof HTTPErrorResponse) {
1141
+ logger.error("Error getting channel info from Microsoft Graph API:");
1142
+ logger.error(response);
1143
+ // Fall back to basic channel object
1144
+ logger.debug("Falling back to basic channel object");
1145
+ return {
1146
+ id: data.channelId,
1147
+ name: data.channelId,
1148
+ workspaceType: WorkspaceType.MicrosoftTeams,
1149
+ teamId: data.teamId,
1150
+ };
1151
+ }
1152
+
1153
+ logger.debug("Channel info API call successful");
1154
+ const channelData: JSONObject = response.data;
1155
+
1156
+ const channel: WorkspaceChannel = {
1157
+ id: data.channelId,
1158
+ name: channelData["displayName"] as string,
1159
+ workspaceType: WorkspaceType.MicrosoftTeams,
1160
+ teamId: data.teamId,
1161
+ };
1162
+
1163
+ logger.debug(`Channel info retrieved: ${JSON.stringify(channel)}`);
1164
+ return channel;
1165
+ } catch (error) {
1166
+ logger.error("Error fetching channel information:");
1167
+ logger.error(error);
1168
+ throw error;
1169
+ }
1170
+ }
1171
+
1172
+ private static buildAdaptiveCardFromMessageBlocks(data: {
1173
+ messageBlocks: Array<WorkspaceMessageBlock>;
1174
+ }): JSONObject {
1175
+ logger.debug("=== buildAdaptiveCardFromMessageBlocks called ===");
1176
+ logger.debug(`Number of message blocks: ${data.messageBlocks.length}`);
1177
+
1178
+ const card: JSONObject = {
1179
+ type: "AdaptiveCard",
1180
+ $schema: "http://adaptivecards.io/schemas/adaptive-card.json",
1181
+ version: "1.5",
1182
+ body: [],
1183
+ actions: [],
1184
+ };
1185
+
1186
+ const body: Array<JSONObject> = [];
1187
+ const actions: Array<JSONObject> = [];
1188
+
1189
+ for (const block of data.messageBlocks) {
1190
+ logger.debug(`Processing message block of type: ${block._type}`);
1191
+
1192
+ if (block._type === "WorkspacePayloadMarkdown") {
1193
+ const markdownBlock: WorkspacePayloadMarkdown =
1194
+ block as WorkspacePayloadMarkdown;
1195
+ logger.debug(`Markdown text: ${markdownBlock.text}`);
1196
+ const markdownObj: JSONObject = this.getMarkdownBlock({
1197
+ payloadMarkdownBlock: markdownBlock,
1198
+ });
1199
+ body.push(markdownObj);
1200
+ } else if (block._type === "WorkspacePayloadHeader") {
1201
+ const headerBlock: WorkspacePayloadHeader =
1202
+ block as WorkspacePayloadHeader;
1203
+ logger.debug(`Header text: ${headerBlock.text}`);
1204
+ const headerObj: JSONObject = this.getHeaderBlock({
1205
+ payloadHeaderBlock: headerBlock,
1206
+ });
1207
+ body.push(headerObj);
1208
+ } else if (block._type === "WorkspacePayloadButtons") {
1209
+ const buttonsBlock: WorkspacePayloadButtons =
1210
+ block as WorkspacePayloadButtons;
1211
+ logger.debug(`Processing ${buttonsBlock.buttons.length} buttons`);
1212
+ for (const button of buttonsBlock.buttons) {
1213
+ logger.debug(
1214
+ `Button: ${button.title} -> ${button.url ? button.url.toString() : "invoke"}`,
1215
+ );
1216
+ const actionObj: JSONObject = this.getButtonBlock({
1217
+ payloadButtonBlock: button,
1218
+ });
1219
+ actions.push(actionObj);
1220
+ }
1221
+ }
1222
+ }
1223
+
1224
+ card["body"] = body;
1225
+ card["actions"] = actions;
1226
+
1227
+ logger.debug(
1228
+ `Built adaptive card with ${body.length} body elements and ${actions.length} actions`,
1229
+ );
1230
+ return card;
1231
+ }
1232
+
1233
+ private static convertAdaptiveCardToHtml(adaptiveCard: JSONObject): string {
1234
+ logger.debug("=== convertAdaptiveCardToHtml called ===");
1235
+
1236
+ // Convert adaptive card to basic HTML for fallback
1237
+ let html: string = "";
1238
+ const body: Array<JSONObject> =
1239
+ (adaptiveCard["body"] as Array<JSONObject>) || [];
1240
+
1241
+ logger.debug(`Converting ${body.length} body elements to HTML`);
1242
+
1243
+ for (const element of body) {
1244
+ if (element["type"] === "TextBlock") {
1245
+ const text: string = element["text"] as string;
1246
+ const size: string = element["size"] as string;
1247
+
1248
+ if (size === "Large") {
1249
+ html += `<h2>${text}</h2>`;
1250
+ logger.debug(`Added header: ${text}`);
1251
+ } else {
1252
+ html += `<p>${text}</p>`;
1253
+ logger.debug(`Added paragraph: ${text}`);
1254
+ }
1255
+ }
1256
+ }
1257
+
1258
+ const actions: Array<JSONObject> =
1259
+ (adaptiveCard["actions"] as Array<JSONObject>) || [];
1260
+ if (actions.length > 0) {
1261
+ logger.debug(`Converting ${actions.length} actions to HTML`);
1262
+ html += "<div>";
1263
+ for (const action of actions) {
1264
+ if (action["type"] === "Action.OpenUrl") {
1265
+ const title: string = action["title"] as string;
1266
+ const url: string = action["url"] as string;
1267
+ html += `<a href="${url}">${title}</a> `;
1268
+ logger.debug(`Added link: ${title} -> ${url}`);
1269
+ }
1270
+ }
1271
+ html += "</div>";
1272
+ }
1273
+
1274
+ logger.debug(`Generated HTML length: ${html.length} characters`);
1275
+ return html;
1276
+ }
1277
+
1278
+ // Placeholder implementations for abstract methods
1279
+ @CaptureSpan()
1280
+ public static override async showModalToUser(_data: {
1281
+ authToken: string;
1282
+ triggerId: string;
1283
+ modalBlock: WorkspaceModalBlock;
1284
+ }): Promise<void> {
1285
+ // Microsoft Teams doesn't support modals in the same way as Slack
1286
+ throw new Error("Modals are not supported in Microsoft Teams integration");
1287
+ }
1288
+
1289
+ @CaptureSpan()
1290
+ public static override async archiveChannels(_data: {
1291
+ userId: string;
1292
+ channelIds: Array<string>;
1293
+ authToken: string;
1294
+ sendMessageBeforeArchiving: WorkspacePayloadMarkdown;
1295
+ projectId: ObjectID;
1296
+ }): Promise<void> {
1297
+ // Microsoft Teams doesn't support archiving channels via API
1298
+ throw new Error(
1299
+ "Channel archiving is not supported in Microsoft Teams integration",
1300
+ );
1301
+ }
1302
+
1303
+ @CaptureSpan()
1304
+ public static override async joinChannel(_data: {
1305
+ authToken: string;
1306
+ channelId: string;
1307
+ }): Promise<void> {
1308
+ // Bot automatically has access to channels in Teams
1309
+ logger.debug("Bot automatically has access to Teams channels");
1310
+ }
1311
+
1312
+ @CaptureSpan()
1313
+ public static override async inviteUserToChannelByChannelId(_data: {
1314
+ authToken: string;
1315
+ channelId: string;
1316
+ workspaceUserId: string;
1317
+ }): Promise<void> {
1318
+ // Teams channel membership is managed differently
1319
+ logger.debug("Teams channel membership is managed at the team level");
1320
+ }
1321
+
1322
+ @CaptureSpan()
1323
+ public static override async inviteUserToChannelByChannelName(_data: {
1324
+ authToken: string;
1325
+ channelName: string;
1326
+ workspaceUserId: string;
1327
+ projectId: ObjectID;
1328
+ }): Promise<void> {
1329
+ // Teams channel membership is managed differently
1330
+ logger.debug("Teams channel membership is managed at the team level");
1331
+ }
1332
+
1333
+ @CaptureSpan()
1334
+ public static override async getAllWorkspaceChannels(data: {
1335
+ authToken: string;
1336
+ projectId: ObjectID;
1337
+ teamId: string;
1338
+ }): Promise<Dictionary<WorkspaceChannel>> {
1339
+ logger.debug("Getting all workspace channels for team ID: " + data.teamId);
1340
+
1341
+ // Get valid access token
1342
+ const accessToken: string | null = await this.getValidAccessToken({
1343
+ authToken: data.authToken,
1344
+ projectId: data.projectId,
1345
+ });
1346
+
1347
+ const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
1348
+ await API.get({
1349
+ url: URL.fromString(
1350
+ `https://graph.microsoft.com/v1.0/teams/${data.teamId}/channels`,
1351
+ ),
1352
+ headers: {
1353
+ Authorization: `Bearer ${accessToken}`,
1354
+ "Content-Type": "application/json",
1355
+ },
1356
+ });
1357
+
1358
+ if (response instanceof HTTPErrorResponse) {
1359
+ logger.error("Error response from Microsoft Graph API:");
1360
+ logger.error(response);
1361
+ throw response;
1362
+ }
1363
+
1364
+ const channelsData: JSONObject = response.data;
1365
+ const channelsArray: Array<JSONObject> =
1366
+ (channelsData["value"] as Array<JSONObject>) || [];
1367
+
1368
+ const channelsDict: Dictionary<WorkspaceChannel> = {};
1369
+
1370
+ for (const channelData of channelsArray) {
1371
+ const channel: WorkspaceChannel = {
1372
+ id: channelData["id"] as string,
1373
+ name: channelData["displayName"] as string,
1374
+ workspaceType: WorkspaceType.MicrosoftTeams,
1375
+ };
1376
+ channelsDict[channel.id] = channel;
1377
+ }
1378
+
1379
+ logger.debug(
1380
+ `Retrieved ${Object.keys(channelsDict).length} channels from API`,
1381
+ );
1382
+ return channelsDict;
1383
+ }
1384
+
1385
+ @CaptureSpan()
1386
+ public static override async doesChannelExist(data: {
1387
+ authToken: string;
1388
+ channelName: string;
1389
+ projectId: ObjectID;
1390
+ teamId?: string;
1391
+ }): Promise<boolean> {
1392
+ if (!data.teamId) {
1393
+ throw new BadDataException(
1394
+ "teamId is required for Microsoft Teams doesChannelExist",
1395
+ );
1396
+ }
1397
+ const channel: WorkspaceChannel | null =
1398
+ await this.getWorkspaceChannelByName({
1399
+ authToken: data.authToken,
1400
+ channelName: data.channelName,
1401
+ projectId: data.projectId,
1402
+ teamId: data.teamId,
1403
+ });
1404
+ return channel !== null;
1405
+ }
1406
+
1407
+ @CaptureSpan()
1408
+ public static override async isUserInDirectMessageChannel(_data: {
1409
+ authToken: string;
1410
+ userId: string;
1411
+ directMessageChannelId: string;
1412
+ }): Promise<boolean> {
1413
+ return false; // Placeholder
1414
+ }
1415
+
1416
+ @CaptureSpan()
1417
+ public static override async isUserInChannel(_data: {
1418
+ authToken: string;
1419
+ channelId: string;
1420
+ userId: string;
1421
+ }): Promise<boolean> {
1422
+ return false; // Placeholder
1423
+ }
1424
+
1425
+ // Block generation methods - these create adaptive card elements
1426
+ @CaptureSpan()
1427
+ public static override getDividerBlock(): JSONObject {
1428
+ return {
1429
+ type: "Container",
1430
+ separator: true,
1431
+ items: [],
1432
+ };
1433
+ }
1434
+
1435
+ @CaptureSpan()
1436
+ public static override getButtonsBlock(_data: {
1437
+ payloadButtonsBlock: WorkspacePayloadButtons;
1438
+ }): JSONObject {
1439
+ // Return adaptive card actions
1440
+ return {
1441
+ type: "ActionSet",
1442
+ actions: [],
1443
+ };
1444
+ }
1445
+
1446
+ @CaptureSpan()
1447
+ public static override getHeaderBlock(data: {
1448
+ payloadHeaderBlock: WorkspacePayloadHeader;
1449
+ }): JSONObject {
1450
+ return {
1451
+ type: "TextBlock",
1452
+ text: data.payloadHeaderBlock.text,
1453
+ size: "Large",
1454
+ weight: "Bolder",
1455
+ };
1456
+ }
1457
+
1458
+ @CaptureSpan()
1459
+ public static override getMarkdownBlock(data: {
1460
+ payloadMarkdownBlock: WorkspacePayloadMarkdown;
1461
+ }): JSONObject {
1462
+ return {
1463
+ type: "TextBlock",
1464
+ text: data.payloadMarkdownBlock.text,
1465
+ wrap: true,
1466
+ markdown: true,
1467
+ };
1468
+ }
1469
+
1470
+ @CaptureSpan()
1471
+ public static override getButtonBlock(data: {
1472
+ payloadButtonBlock: WorkspaceMessagePayloadButton;
1473
+ }): JSONObject {
1474
+ // If URL is present, render as link; otherwise use Action.Submit to post back action/value
1475
+ if (data.payloadButtonBlock.url) {
1476
+ return {
1477
+ type: "Action.OpenUrl",
1478
+ title: data.payloadButtonBlock.title,
1479
+ url: data.payloadButtonBlock.url.toString(),
1480
+ };
1481
+ }
1482
+
1483
+ return {
1484
+ type: "Action.Submit",
1485
+ title: data.payloadButtonBlock.title,
1486
+ data: {
1487
+ action: data.payloadButtonBlock.actionId,
1488
+ actionValue: data.payloadButtonBlock.value,
1489
+ },
1490
+ } as any;
1491
+ }
1492
+
1493
+ // Other block methods - placeholders for now
1494
+ @CaptureSpan()
1495
+ public static override getCheckboxBlock(_data: {
1496
+ payloadCheckboxBlock: WorkspaceCheckboxBlock;
1497
+ }): JSONObject {
1498
+ return { type: "Input.Toggle" };
1499
+ }
1500
+
1501
+ @CaptureSpan()
1502
+ public static override getDateTimePickerBlock(_data: {
1503
+ payloadDateTimePickerBlock: WorkspaceDateTimePickerBlock;
1504
+ }): JSONObject {
1505
+ return { type: "Input.Date" };
1506
+ }
1507
+
1508
+ @CaptureSpan()
1509
+ public static override getTextAreaBlock(_data: {
1510
+ payloadTextAreaBlock: WorkspaceTextAreaBlock;
1511
+ }): JSONObject {
1512
+ return { type: "Input.Text", isMultiline: true };
1513
+ }
1514
+
1515
+ @CaptureSpan()
1516
+ public static override getTextBoxBlock(_data: {
1517
+ payloadTextBoxBlock: WorkspaceTextBoxBlock;
1518
+ }): JSONObject {
1519
+ return { type: "Input.Text" };
1520
+ }
1521
+
1522
+ @CaptureSpan()
1523
+ public static override getImageBlock(_data: {
1524
+ payloadImageBlock: WorkspacePayloadImage;
1525
+ }): JSONObject {
1526
+ return { type: "Image" };
1527
+ }
1528
+
1529
+ @CaptureSpan()
1530
+ public static override getDropdownBlock(_data: {
1531
+ payloadDropdownBlock: WorkspaceDropdownBlock;
1532
+ }): JSONObject {
1533
+ return { type: "Input.ChoiceSet" };
1534
+ }
1535
+
1536
+ @CaptureSpan()
1537
+ public static override getModalBlock(_data: {
1538
+ payloadModalBlock: WorkspaceModalBlock;
1539
+ }): JSONObject {
1540
+ // Teams doesn't support modals like Slack
1541
+ return {};
1542
+ }
1543
+
1544
+ @CaptureSpan()
1545
+ public static override async sendPayloadBlocksToChannel(_data: {
1546
+ authToken: string;
1547
+ workspaceChannel: WorkspaceChannel;
1548
+ blocks: Array<JSONObject>;
1549
+ }): Promise<WorkspaceThread> {
1550
+ // This is handled by sendAdaptiveCardToChannel
1551
+ throw new Error("Use sendAdaptiveCardToChannel instead");
1552
+ }
1553
+
1554
+ @CaptureSpan()
1555
+ public static convertMarkdownToTeamsRichText(markdown: string): string {
1556
+ // Basic markdown to Teams format conversion
1557
+ return markdown
1558
+ .replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
1559
+ .replace(/\*(.*?)\*/g, "<em>$1</em>")
1560
+ .replace(/`(.*?)`/g, "<code>$1</code>");
1561
+ }
1562
+
1563
+ // Bot Framework specific methods
1564
+ @CaptureSpan()
1565
+ public static async handleBotMessageActivity(data: {
1566
+ activity: JSONObject;
1567
+ turnContext: TurnContext;
1568
+ }): Promise<void> {
1569
+ // Handle direct messages to bot or @mentions via Bot Framework
1570
+ const messageText: string = (data.activity["text"] as string) || "";
1571
+ const possibleActionValue: JSONObject =
1572
+ (data.activity["value"] as JSONObject) || {};
1573
+ const from: JSONObject = (data.activity["from"] as JSONObject) || {};
1574
+ const conversation: JSONObject =
1575
+ (data.activity["conversation"] as JSONObject) || {};
1576
+ const channelData: JSONObject =
1577
+ (data.activity["channelData"] as JSONObject) || {};
1578
+ const entities: Array<JSONObject> =
1579
+ (data.activity["entities"] as Array<JSONObject>) || [];
1580
+
1581
+ logger.debug(`Bot message from: ${JSON.stringify(from)}`);
1582
+ logger.debug(`Message text: ${messageText}`);
1583
+ logger.debug(`Conversation: ${JSON.stringify(conversation)}`);
1584
+ logger.debug(`Channel data: ${JSON.stringify(channelData)}`);
1585
+ logger.debug(`Entities: ${JSON.stringify(entities)}`);
1586
+
1587
+ // If this is actually an Adaptive Card submit wrapped as a message, route to invoke handler
1588
+ if (
1589
+ (possibleActionValue["action"] as string) ||
1590
+ (possibleActionValue["data"] as any)?.["action"]
1591
+ ) {
1592
+ logger.debug(
1593
+ "Message activity contains action payload; routing to invoke handler",
1594
+ );
1595
+ await this.handleBotInvokeActivity({
1596
+ activity: data.activity,
1597
+ turnContext: data.turnContext,
1598
+ });
1599
+ return;
1600
+ }
1601
+
1602
+ // Check if the bot was mentioned
1603
+ const recipientId: string = (data.activity["recipient"] as JSONObject)?.[
1604
+ "id"
1605
+ ] as string;
1606
+ const conversationType: string =
1607
+ (conversation["conversationType"] as string) || "";
1608
+ const isDirectMessage: boolean = conversationType === "personal";
1609
+ const isMentioned: boolean = entities.some((entity: JSONObject) => {
1610
+ return (
1611
+ entity["type"] === "mention" &&
1612
+ (entity["mentioned"] as JSONObject)?.["id"] === recipientId
1613
+ );
1614
+ });
1615
+
1616
+ // Only respond if it's a direct message or the bot was mentioned
1617
+ if (!isDirectMessage && !isMentioned) {
1618
+ logger.debug("Bot not mentioned in channel message, ignoring");
1619
+ return;
1620
+ }
1621
+
1622
+ // Extract tenant ID to get project ID
1623
+ const tenantId: string = (channelData["tenant"] as JSONObject)?.[
1624
+ "id"
1625
+ ] as string;
1626
+ if (!tenantId) {
1627
+ logger.error("Tenant ID not found in channelData");
1628
+ await data.turnContext.sendActivity(
1629
+ "Sorry, I couldn't identify your organization. Please try again later.",
1630
+ );
1631
+ return;
1632
+ }
1633
+
1634
+ // Get project auth by tenant ID
1635
+ const projectAuth: WorkspaceProjectAuthToken | null =
1636
+ await WorkspaceProjectAuthTokenService.findOneBy({
1637
+ query: {
1638
+ workspaceType: WorkspaceType.MicrosoftTeams,
1639
+ miscData: {
1640
+ tenantId: tenantId,
1641
+ } as any,
1642
+ },
1643
+ select: {
1644
+ projectId: true,
1645
+ miscData: true,
1646
+ },
1647
+ props: {
1648
+ isRoot: true,
1649
+ },
1650
+ });
1651
+
1652
+ if (!projectAuth || !projectAuth.projectId) {
1653
+ logger.error("Project auth not found for tenant ID: " + tenantId);
1654
+ await data.turnContext.sendActivity(
1655
+ "Sorry, I couldn't find your project configuration. Please try again later.",
1656
+ );
1657
+ return;
1658
+ }
1659
+
1660
+ const projectId: ObjectID = projectAuth.projectId;
1661
+ logger.debug(
1662
+ `Found project ID: ${projectId.toString()} for tenant ID: ${tenantId}`,
1663
+ );
1664
+
1665
+ // Clean the message text by removing bot mentions
1666
+ const cleanText: string = messageText
1667
+ .replace(/<at[^>]*>.*?<\/at>/g, "")
1668
+ .trim()
1669
+ .toLowerCase();
1670
+
1671
+ let responseText: string = "";
1672
+
1673
+ try {
1674
+ if (cleanText.includes("help") || cleanText === "") {
1675
+ responseText = this.getHelpMessage();
1676
+ } else if (
1677
+ cleanText === "/incident" ||
1678
+ cleanText.startsWith("/incident ")
1679
+ ) {
1680
+ // Handle /incident slash command
1681
+ logger.debug("Processing /incident command");
1682
+ const card: JSONObject =
1683
+ await MicrosoftTeamsIncidentActions.buildNewIncidentCard(projectId);
1684
+ await data.turnContext.sendActivity({
1685
+ attachments: [
1686
+ {
1687
+ contentType: "application/vnd.microsoft.card.adaptive",
1688
+ content: card,
1689
+ },
1690
+ ],
1691
+ });
1692
+ logger.debug("New incident card sent successfully");
1693
+ return;
1694
+ } else if (
1695
+ cleanText === "/maintenance" ||
1696
+ cleanText.startsWith("/maintenance ")
1697
+ ) {
1698
+ // Handle /maintenance slash command
1699
+ logger.debug("Processing /maintenance command");
1700
+ const card: JSONObject =
1701
+ await MicrosoftTeamsScheduledMaintenanceActions.buildNewScheduledMaintenanceCard(
1702
+ projectId,
1703
+ );
1704
+ await data.turnContext.sendActivity({
1705
+ attachments: [
1706
+ {
1707
+ contentType: "application/vnd.microsoft.card.adaptive",
1708
+ content: card,
1709
+ },
1710
+ ],
1711
+ });
1712
+ logger.debug("New scheduled maintenance card sent successfully");
1713
+ return;
1714
+ } else if (
1715
+ cleanText.includes("show active incidents") ||
1716
+ cleanText.includes("active incidents")
1717
+ ) {
1718
+ responseText = await this.getActiveIncidentsMessage(projectId);
1719
+ } else if (
1720
+ cleanText.includes("show scheduled maintenance") ||
1721
+ cleanText.includes("scheduled maintenance")
1722
+ ) {
1723
+ responseText = await this.getScheduledMaintenanceMessage(projectId);
1724
+ } else if (
1725
+ cleanText.includes("show ongoing maintenance") ||
1726
+ cleanText.includes("ongoing maintenance")
1727
+ ) {
1728
+ responseText = await this.getOngoingMaintenanceMessage(projectId);
1729
+ } else if (
1730
+ cleanText.includes("show active alerts") ||
1731
+ cleanText.includes("active alerts")
1732
+ ) {
1733
+ responseText = await this.getActiveAlertsMessage(projectId);
1734
+ } else {
1735
+ responseText = `I received your message: "${cleanText}". Type 'help' to see what I can do for you.`;
1736
+ }
1737
+
1738
+ // Send response directly using TurnContext - this is the recommended Bot Framework pattern
1739
+ await data.turnContext.sendActivity(responseText);
1740
+ logger.debug("Bot message sent successfully using TurnContext");
1741
+ } catch (error) {
1742
+ logger.error("Error sending bot message via TurnContext: " + error);
1743
+ await data.turnContext.sendActivity(
1744
+ "Sorry, I encountered an error processing your request. Please try again later.",
1745
+ );
1746
+ throw error;
1747
+ }
1748
+ }
1749
+
1750
+ // Helper methods for bot commands
1751
+ private static getHelpMessage(): string {
1752
+ return `Hello! I'm the OneUptime bot. I can help you with the following commands:
1753
+
1754
+ **Available Commands:**
1755
+ - **help** — Show this help message
1756
+ - **/incident** — Create a new incident
1757
+ - **/maintenance** — Create a new scheduled maintenance event
1758
+ - **show active incidents** — Display all currently active incidents
1759
+ - **show scheduled maintenance** — Show upcoming scheduled maintenance events
1760
+ - **show ongoing maintenance** — Display currently ongoing maintenance events
1761
+ - **show active alerts** — Display all active alerts
1762
+
1763
+ Just type any of these commands to get the information you need!`;
1764
+ }
1765
+
1766
+ private static async getActiveIncidentsMessage(
1767
+ projectId: ObjectID,
1768
+ ): Promise<string> {
1769
+ try {
1770
+ logger.debug(
1771
+ "Getting active incidents for project: " + projectId.toString(),
1772
+ );
1773
+
1774
+ // Get unresolved incident states
1775
+ const unresolvedIncidentStates: Array<IncidentState> =
1776
+ await IncidentStateService.getUnresolvedIncidentStates(projectId, {
1777
+ isRoot: true,
1778
+ });
1779
+
1780
+ const unresolvedIncidentStateIds: Array<ObjectID> =
1781
+ unresolvedIncidentStates.map((state: IncidentState) => {
1782
+ return state.id!;
1783
+ });
1784
+
1785
+ // Find active incidents
1786
+ const activeIncidents: Array<Incident> = await IncidentService.findBy({
1787
+ query: {
1788
+ projectId: projectId,
1789
+ currentIncidentStateId: QueryHelper.any(unresolvedIncidentStateIds),
1790
+ },
1791
+ select: {
1792
+ _id: true,
1793
+ incidentNumber: true,
1794
+ title: true,
1795
+ description: true,
1796
+ currentIncidentState: {
1797
+ name: true,
1798
+ color: true,
1799
+ },
1800
+ incidentSeverity: {
1801
+ name: true,
1802
+ color: true,
1803
+ },
1804
+ createdAt: true,
1805
+ monitors: {
1806
+ name: true,
1807
+ },
1808
+ },
1809
+ sort: {
1810
+ createdAt: SortOrder.Descending,
1811
+ },
1812
+ limit: 10,
1813
+ skip: 0,
1814
+ props: {
1815
+ isRoot: true,
1816
+ },
1817
+ });
1818
+
1819
+ if (activeIncidents.length === 0) {
1820
+ return `**Active Incidents**
1821
+
1822
+ Currently, there are no active incidents in the system. All services are operating normally.
1823
+
1824
+ If you need to report an incident or check historical incidents, please visit the OneUptime dashboard.`;
1825
+ }
1826
+
1827
+ let message: string = `**Active Incidents** (${activeIncidents.length})
1828
+
1829
+ `;
1830
+
1831
+ for (const incident of activeIncidents) {
1832
+ const severity: string = incident.incidentSeverity?.name || "Unknown";
1833
+ const state: string = incident.currentIncidentState?.name || "Unknown";
1834
+ const createdAt: string = incident.createdAt
1835
+ ? OneUptimeDate.getDateAsFormattedString(incident.createdAt)
1836
+ : "Unknown";
1837
+
1838
+ const severityIcon: string = ["Critical", "Major"].includes(severity)
1839
+ ? "🔴"
1840
+ : severity === "Minor"
1841
+ ? "🟠"
1842
+ : "🟡";
1843
+
1844
+ const incidentUrl: URL =
1845
+ await IncidentService.getIncidentLinkInDashboard(
1846
+ projectId,
1847
+ incident.id!,
1848
+ );
1849
+
1850
+ message += `${severityIcon} **[Incident #${incident.incidentNumber}: ${incident.title}](${incidentUrl.toString()})**
1851
+ • **Severity:** ${severity}
1852
+ • **Status:** ${state}
1853
+ • **Created:** ${createdAt}
1854
+ `;
1855
+
1856
+ if (incident.monitors && incident.monitors.length > 0) {
1857
+ message += `• **Affected Services:** ${incident.monitors
1858
+ .map((m: Monitor) => {
1859
+ return m.name;
1860
+ })
1861
+ .join(", ")}\n`;
1862
+ }
1863
+
1864
+ if (incident.description) {
1865
+ const desc: string = incident.description.replace(/\s+/g, " ");
1866
+ message += `• **Description:** ${desc.substring(0, 180)}${desc.length > 180 ? "..." : ""}\n`;
1867
+ }
1868
+
1869
+ message += `• [Open in Dashboard](${incidentUrl.toString()})\n\n`;
1870
+ }
1871
+
1872
+ return message;
1873
+ } catch (error) {
1874
+ logger.error("Error getting active incidents: " + error);
1875
+ return "Sorry, I couldn't retrieve active incidents information at the moment. Please try again later.";
1876
+ }
1877
+ }
1878
+
1879
+ private static async getScheduledMaintenanceMessage(
1880
+ projectId: ObjectID,
1881
+ ): Promise<string> {
1882
+ try {
1883
+ logger.debug(
1884
+ "Getting scheduled maintenance events for project: " +
1885
+ projectId.toString(),
1886
+ );
1887
+
1888
+ // Get scheduled maintenance events
1889
+ const scheduledEvents: Array<ScheduledMaintenance> =
1890
+ await ScheduledMaintenanceService.findBy({
1891
+ query: {
1892
+ projectId: projectId,
1893
+ currentScheduledMaintenanceState: {
1894
+ isScheduledState: true,
1895
+ } as any,
1896
+ isVisibleOnStatusPage: true, // Only show events visible on status page
1897
+ },
1898
+ select: {
1899
+ _id: true,
1900
+ title: true,
1901
+ description: true,
1902
+ startsAt: true,
1903
+ endsAt: true,
1904
+ currentScheduledMaintenanceState: {
1905
+ name: true,
1906
+ },
1907
+ monitors: {
1908
+ name: true,
1909
+ },
1910
+ scheduledMaintenanceNumber: true,
1911
+ },
1912
+ sort: {
1913
+ startsAt: SortOrder.Ascending,
1914
+ },
1915
+ limit: 10,
1916
+ skip: 0,
1917
+ props: {
1918
+ isRoot: true,
1919
+ },
1920
+ });
1921
+
1922
+ if (scheduledEvents.length === 0) {
1923
+ return `**Scheduled Maintenance Events**
1924
+
1925
+ There are currently no scheduled maintenance events.
1926
+
1927
+ When maintenance is scheduled, you'll see details here including:
1928
+ • Event title and description
1929
+ • Scheduled start and end times
1930
+ • Affected services
1931
+ • Status updates
1932
+
1933
+ Check back later for upcoming maintenance windows.`;
1934
+ }
1935
+
1936
+ let message: string = `**Scheduled Maintenance Events** (${scheduledEvents.length})
1937
+
1938
+ `;
1939
+
1940
+ for (const event of scheduledEvents) {
1941
+ const state: string =
1942
+ event.currentScheduledMaintenanceState?.name || "Scheduled";
1943
+ const startTime: string = event.startsAt
1944
+ ? OneUptimeDate.getDateAsFormattedString(event.startsAt)
1945
+ : "TBD";
1946
+ const endTime: string = event.endsAt
1947
+ ? OneUptimeDate.getDateAsFormattedString(event.endsAt)
1948
+ : "TBD";
1949
+
1950
+ const eventUrl: URL =
1951
+ await ScheduledMaintenanceService.getScheduledMaintenanceLinkInDashboard(
1952
+ projectId,
1953
+ event.id!,
1954
+ );
1955
+
1956
+ message += `🛠️ **[Scheduled Maintenance #${event.scheduledMaintenanceNumber}: ${event.title}](${eventUrl.toString()})**
1957
+ • **Status:** ${state}
1958
+ • **Starts:** ${startTime}
1959
+ • **Ends:** ${endTime}
1960
+ `;
1961
+
1962
+ if (event.monitors && event.monitors.length > 0) {
1963
+ message += `• **Affected Services:** ${event.monitors
1964
+ .map((m: Monitor) => {
1965
+ return m.name;
1966
+ })
1967
+ .join(", ")}\n`;
1968
+ }
1969
+
1970
+ if (event.description) {
1971
+ const desc: string = event.description.replace(/\s+/g, " ");
1972
+ message += `• **Description:** ${desc.substring(0, 180)}${desc.length > 180 ? "..." : ""}\n`;
1973
+ }
1974
+
1975
+ message += `• [View Event](${eventUrl.toString()})\n\n`;
1976
+ }
1977
+
1978
+ return message;
1979
+ } catch (error) {
1980
+ logger.error("Error getting scheduled maintenance: " + error);
1981
+ return "Sorry, I couldn't retrieve scheduled maintenance information at the moment. Please try again later.";
1982
+ }
1983
+ }
1984
+
1985
+ private static async getOngoingMaintenanceMessage(
1986
+ projectId: ObjectID,
1987
+ ): Promise<string> {
1988
+ try {
1989
+ logger.debug(
1990
+ "Getting ongoing maintenance events for project: " +
1991
+ projectId.toString(),
1992
+ );
1993
+
1994
+ // Get ongoing maintenance events
1995
+ const ongoingEvents: Array<ScheduledMaintenance> =
1996
+ await ScheduledMaintenanceService.findBy({
1997
+ query: {
1998
+ projectId: projectId,
1999
+ currentScheduledMaintenanceState: {
2000
+ isOngoingState: true,
2001
+ } as any,
2002
+ },
2003
+ select: {
2004
+ _id: true,
2005
+ title: true,
2006
+ description: true,
2007
+ startsAt: true,
2008
+ endsAt: true,
2009
+ currentScheduledMaintenanceState: {
2010
+ name: true,
2011
+ },
2012
+ monitors: {
2013
+ name: true,
2014
+ },
2015
+ scheduledMaintenanceNumber: true,
2016
+ },
2017
+ sort: {
2018
+ startsAt: SortOrder.Descending,
2019
+ },
2020
+ limit: 10,
2021
+ skip: 0,
2022
+ props: {
2023
+ isRoot: true,
2024
+ },
2025
+ });
2026
+
2027
+ if (ongoingEvents.length === 0) {
2028
+ return `**Ongoing Maintenance Events**
2029
+
2030
+ There are currently no ongoing maintenance events.
2031
+
2032
+ When maintenance is in progress, you'll see details here including:
2033
+ • Event title and description
2034
+ • Current status and progress
2035
+ • Affected services
2036
+ • Expected completion time
2037
+
2038
+ All systems are currently operating normally.`;
2039
+ }
2040
+
2041
+ let message: string = `**Ongoing Maintenance Events** (${ongoingEvents.length})
2042
+
2043
+ `;
2044
+
2045
+ for (const event of ongoingEvents) {
2046
+ const state: string =
2047
+ event.currentScheduledMaintenanceState?.name || "Ongoing";
2048
+ const startTime: string = event.startsAt
2049
+ ? OneUptimeDate.getDateAsFormattedString(event.startsAt)
2050
+ : "Unknown";
2051
+ const endTime: string = event.endsAt
2052
+ ? OneUptimeDate.getDateAsFormattedString(event.endsAt)
2053
+ : "TBD";
2054
+
2055
+ const eventUrl: URL =
2056
+ await ScheduledMaintenanceService.getScheduledMaintenanceLinkInDashboard(
2057
+ projectId,
2058
+ event.id!,
2059
+ );
2060
+
2061
+ message += `🔧 **[Scheduled Maintenance #${event.scheduledMaintenanceNumber}: ${event.title}](${eventUrl.toString()})**
2062
+ • **Status:** ${state}
2063
+ • **Started:** ${startTime}
2064
+ • **Expected End:** ${endTime}
2065
+ `;
2066
+
2067
+ if (event.monitors && event.monitors.length > 0) {
2068
+ message += `• **Affected Services:** ${event.monitors
2069
+ .map((m: Monitor) => {
2070
+ return m.name;
2071
+ })
2072
+ .join(", ")}\n`;
2073
+ }
2074
+
2075
+ if (event.description) {
2076
+ const desc: string = event.description.replace(/\s+/g, " ");
2077
+ message += `• **Description:** ${desc.substring(0, 180)}${desc.length > 180 ? "..." : ""}\n`;
2078
+ }
2079
+
2080
+ message += `• [View Event](${eventUrl.toString()})\n\n`;
2081
+ }
2082
+
2083
+ return message;
2084
+ } catch (error) {
2085
+ logger.error("Error getting ongoing maintenance: " + error);
2086
+ return "Sorry, I couldn't retrieve ongoing maintenance information at the moment. Please try again later.";
2087
+ }
2088
+ }
2089
+
2090
+ private static async getActiveAlertsMessage(
2091
+ projectId: ObjectID,
2092
+ ): Promise<string> {
2093
+ try {
2094
+ logger.debug(
2095
+ "Getting active alerts for project: " + projectId.toString(),
2096
+ );
2097
+
2098
+ // Get unresolved alert states
2099
+ const unresolvedAlertStates: Array<AlertState> =
2100
+ await AlertStateService.getUnresolvedAlertStates(projectId, {
2101
+ isRoot: true,
2102
+ });
2103
+
2104
+ const unresolvedAlertStateIds: Array<ObjectID> =
2105
+ unresolvedAlertStates.map((state: AlertState) => {
2106
+ return state.id!;
2107
+ });
2108
+
2109
+ // Find active alerts
2110
+ const activeAlerts: Array<Alert> = await AlertService.findBy({
2111
+ query: {
2112
+ projectId: projectId,
2113
+ currentAlertStateId: QueryHelper.any(unresolvedAlertStateIds),
2114
+ },
2115
+ select: {
2116
+ _id: true,
2117
+ alertNumber: true,
2118
+ title: true,
2119
+ description: true,
2120
+ currentAlertState: {
2121
+ name: true,
2122
+ color: true,
2123
+ },
2124
+ alertSeverity: {
2125
+ name: true,
2126
+ color: true,
2127
+ },
2128
+ createdAt: true,
2129
+ monitor: {
2130
+ name: true,
2131
+ },
2132
+ },
2133
+ sort: {
2134
+ createdAt: SortOrder.Descending,
2135
+ },
2136
+ limit: 10,
2137
+ skip: 0,
2138
+ props: {
2139
+ isRoot: true,
2140
+ },
2141
+ });
2142
+
2143
+ if (activeAlerts.length === 0) {
2144
+ return `**Active Alerts**
2145
+
2146
+ Currently, there are no active alerts in the system.
2147
+
2148
+ When alerts are triggered, you'll see details here including:
2149
+ • Alert title and description
2150
+ • Severity level
2151
+ • Affected services or monitors
2152
+ • Time triggered
2153
+ • Current status
2154
+
2155
+ All monitoring checks are passing normally.`;
2156
+ }
2157
+
2158
+ let message: string = `**Active Alerts** (${activeAlerts.length})
2159
+
2160
+ `;
2161
+
2162
+ for (const alert of activeAlerts) {
2163
+ const severity: string = alert.alertSeverity?.name || "Unknown";
2164
+ const state: string = alert.currentAlertState?.name || "Unknown";
2165
+ const createdAt: string = alert.createdAt
2166
+ ? OneUptimeDate.getDateAsFormattedString(alert.createdAt)
2167
+ : "Unknown";
2168
+
2169
+ const alertUrl: URL = await AlertService.getAlertLinkInDashboard(
2170
+ projectId,
2171
+ alert.id!,
2172
+ );
2173
+
2174
+ message += `⚠️ **[Alert #${alert.alertNumber}: ${alert.title}](${alertUrl.toString()})**
2175
+ • **Severity:** ${severity}
2176
+ • **Status:** ${state}
2177
+ • **Triggered:** ${createdAt}
2178
+ `;
2179
+
2180
+ if (alert.monitor?.name) {
2181
+ message += `• **Monitor:** ${alert.monitor.name}\n`;
2182
+ }
2183
+
2184
+ if (alert.description) {
2185
+ const desc: string = alert.description.replace(/\s+/g, " ");
2186
+ message += `• **Description:** ${desc.substring(0, 180)}${desc.length > 180 ? "..." : ""}\n`;
2187
+ }
2188
+
2189
+ message += `• [Open in Dashboard](${alertUrl.toString()})\n\n`;
2190
+ }
2191
+
2192
+ return message;
2193
+ } catch (error) {
2194
+ logger.error("Error getting active alerts: " + error);
2195
+ return "Sorry, I couldn't retrieve active alerts information at the moment. Please try again later.";
2196
+ }
2197
+ }
2198
+
2199
+ @CaptureSpan()
2200
+ public static async handleBotInvokeActivity(data: {
2201
+ activity: JSONObject;
2202
+ turnContext: TurnContext;
2203
+ }): Promise<void> {
2204
+ // Handle adaptive card button clicks via Bot Framework
2205
+ const value: JSONObject = (data.activity["value"] as JSONObject) || {};
2206
+
2207
+ // Extract action type and value from the value object
2208
+ const { actionType, actionValue } = this.extractActionFromValue(value);
2209
+
2210
+ logger.debug(`Bot invoke activity - Action type: ${actionType}`);
2211
+ logger.debug(`Bot invoke value: ${JSON.stringify(value)}`);
2212
+
2213
+ try {
2214
+ // Resolve project and user context from activity
2215
+ const channelData: JSONObject =
2216
+ (data.activity["channelData"] as JSONObject) || {};
2217
+ const tenantId: string = ((channelData["tenant"] as JSONObject) || {})[
2218
+ "id"
2219
+ ] as string;
2220
+ if (!tenantId) {
2221
+ logger.error("Tenant ID not found in invoke activity");
2222
+ await data.turnContext.sendActivity(
2223
+ "Sorry, I couldn't identify your organization. Please try again later.",
2224
+ );
2225
+ return;
2226
+ }
2227
+
2228
+ const projectAuth: WorkspaceProjectAuthToken | null =
2229
+ await WorkspaceProjectAuthTokenService.findOneBy({
2230
+ query: {
2231
+ workspaceType: WorkspaceType.MicrosoftTeams,
2232
+ miscData: { tenantId: tenantId } as any,
2233
+ },
2234
+ select: { projectId: true, authToken: true },
2235
+ props: { isRoot: true },
2236
+ });
2237
+
2238
+ if (!projectAuth || !projectAuth.projectId) {
2239
+ logger.error(
2240
+ "Project auth not found for invoke activity tenant: " + tenantId,
2241
+ );
2242
+ await data.turnContext.sendActivity(
2243
+ "Sorry, I couldn't find your project configuration.",
2244
+ );
2245
+ return;
2246
+ }
2247
+
2248
+ const projectId: ObjectID = projectAuth.projectId;
2249
+ const fromObj: JSONObject = ((data.activity["from"] as JSONObject) ||
2250
+ {}) as JSONObject;
2251
+ const teamsUserId: string | undefined =
2252
+ (fromObj["aadObjectId"] as string) || undefined;
2253
+
2254
+ if (!teamsUserId) {
2255
+ logger.error(
2256
+ "AAD Object ID (teamsUserId) not found in invoke activity from object",
2257
+ );
2258
+ await data.turnContext.sendActivity(
2259
+ "Sorry, I couldn't identify you. Please try again later.",
2260
+ );
2261
+ return;
2262
+ }
2263
+
2264
+ const userLookupParamsRes: {
2265
+ teamsUserId: string;
2266
+ projectId: ObjectID;
2267
+ aadObjectId?: string;
2268
+ } = {
2269
+ teamsUserId: teamsUserId,
2270
+ projectId: projectId,
2271
+ };
2272
+
2273
+ const oneUptimeUserId: ObjectID =
2274
+ await MicrosoftTeamsAuthAction.getOneUptimeUserIdFromTeamsUserId(
2275
+ userLookupParamsRes,
2276
+ );
2277
+
2278
+ // Handle incident actions
2279
+ if (MicrosoftTeamsIncidentActions.isIncidentAction({ actionType })) {
2280
+ await MicrosoftTeamsIncidentActions.handleBotIncidentAction({
2281
+ actionType,
2282
+ actionValue,
2283
+ value,
2284
+ projectId,
2285
+ oneUptimeUserId,
2286
+ turnContext: data.turnContext,
2287
+ });
2288
+ return;
2289
+ }
2290
+
2291
+ // Handle alert actions
2292
+ if (MicrosoftTeamsAlertActions.isAlertAction({ actionType })) {
2293
+ await MicrosoftTeamsAlertActions.handleBotAlertAction({
2294
+ actionType,
2295
+ actionValue,
2296
+ value,
2297
+ projectId,
2298
+ oneUptimeUserId,
2299
+ turnContext: data.turnContext,
2300
+ });
2301
+ return;
2302
+ }
2303
+
2304
+ // Handle monitor actions
2305
+ if (MicrosoftTeamsMonitorActions.isMonitorAction({ actionType })) {
2306
+ await MicrosoftTeamsMonitorActions.handleBotMonitorAction({
2307
+ actionType,
2308
+ actionValue,
2309
+ value,
2310
+ projectId,
2311
+ oneUptimeUserId,
2312
+ turnContext: data.turnContext,
2313
+ });
2314
+ return;
2315
+ }
2316
+
2317
+ // Handle scheduled maintenance actions
2318
+ if (
2319
+ MicrosoftTeamsScheduledMaintenanceActions.isScheduledMaintenanceAction({
2320
+ actionType,
2321
+ })
2322
+ ) {
2323
+ await MicrosoftTeamsScheduledMaintenanceActions.handleBotScheduledMaintenanceAction(
2324
+ actionType as MicrosoftTeamsScheduledMaintenanceActionType,
2325
+ data.turnContext,
2326
+ value,
2327
+ {
2328
+ userId: oneUptimeUserId.toString(),
2329
+ projectId,
2330
+ isAuthorized: true,
2331
+ authToken: "",
2332
+ payloadType: "invoke",
2333
+ } as MicrosoftTeamsRequest,
2334
+ );
2335
+ return;
2336
+ }
2337
+
2338
+ // Handle on-call duty actions
2339
+ if (MicrosoftTeamsOnCallDutyActions.isOnCallDutyAction({ actionType })) {
2340
+ await MicrosoftTeamsOnCallDutyActions.handleBotOnCallDutyAction(
2341
+ actionType as MicrosoftTeamsOnCallDutyActionType,
2342
+ data.turnContext,
2343
+ value,
2344
+ );
2345
+ return;
2346
+ }
2347
+ } catch (error) {
2348
+ logger.error("Error handling bot invoke activity:");
2349
+ logger.error(error);
2350
+ await data.turnContext.sendActivity(
2351
+ "Sorry, that action failed. Please try again later.",
2352
+ );
2353
+ }
2354
+ }
2355
+
2356
+ @CaptureSpan()
2357
+ public static async handleConversationUpdateActivity(data: {
2358
+ activity: JSONObject;
2359
+ turnContext: TurnContext;
2360
+ }): Promise<void> {
2361
+ // Handle bot added to team/channel or members added/removed
2362
+ const membersAdded: Array<JSONObject> =
2363
+ (data.activity["membersAdded"] as Array<JSONObject>) || [];
2364
+ const membersRemoved: Array<JSONObject> =
2365
+ (data.activity["membersRemoved"] as Array<JSONObject>) || [];
2366
+ const conversation: JSONObject =
2367
+ (data.activity["conversation"] as JSONObject) || {};
2368
+ const channelData: JSONObject =
2369
+ (data.activity["channelData"] as JSONObject) || {};
2370
+
2371
+ logger.debug(
2372
+ `Conversation update - Members added: ${JSON.stringify(membersAdded)}`,
2373
+ );
2374
+ logger.debug(
2375
+ `Conversation update - Members removed: ${JSON.stringify(membersRemoved)}`,
2376
+ );
2377
+ logger.debug(`Conversation: ${JSON.stringify(conversation)}`);
2378
+ logger.debug(`Channel data: ${JSON.stringify(channelData)}`);
2379
+
2380
+ // Check if the bot was added
2381
+ const botWasAdded: boolean = membersAdded.some((member: JSONObject) => {
2382
+ return member["id"] === MicrosoftTeamsAppClientId;
2383
+ });
2384
+
2385
+ if (botWasAdded) {
2386
+ logger.debug("OneUptime bot was added to a Teams conversation");
2387
+
2388
+ const welcomeText: string =
2389
+ "🎉 Welcome to OneUptime!\n\nI'm your monitoring and alerting assistant. I'll help you stay on top of your system's health and notify you about any incidents.\n\nType 'help' to see what I can do for you.";
2390
+
2391
+ try {
2392
+ // Send welcome message directly using TurnContext
2393
+ await data.turnContext.sendActivity(welcomeText);
2394
+ logger.debug("Welcome message sent successfully using TurnContext");
2395
+ } catch (error) {
2396
+ logger.error("Error sending welcome message via TurnContext: " + error);
2397
+ }
2398
+ }
2399
+ }
2400
+
2401
+ @CaptureSpan()
2402
+ public static async handleInstallationUpdateActivity(data: {
2403
+ activity: JSONObject;
2404
+ }): Promise<void> {
2405
+ // Handle bot installation/uninstallation
2406
+ const action: string = (data.activity["action"] as string) || "";
2407
+ const conversation: JSONObject =
2408
+ (data.activity["conversation"] as JSONObject) || {};
2409
+
2410
+ logger.debug(`Installation update - Action: ${action}`);
2411
+ logger.debug(`Conversation: ${JSON.stringify(conversation)}`);
2412
+
2413
+ if (action === "add") {
2414
+ logger.debug("OneUptime bot was installed");
2415
+ } else if (action === "remove") {
2416
+ logger.debug("OneUptime bot was uninstalled");
2417
+ }
2418
+ }
2419
+
2420
+ /**
2421
+ * Process Bot Framework activity using the botbuilder SDK adapter.processActivity
2422
+ * This replaces the manual JWT validation and activity handling with proper SDK methods
2423
+ */
2424
+ @CaptureSpan()
2425
+ public static async processBotActivity(
2426
+ req: ExpressRequest,
2427
+ res: ExpressResponse,
2428
+ ): Promise<void> {
2429
+ logger.debug(
2430
+ "Processing Bot Framework activity using adapter.processActivity",
2431
+ );
2432
+ logger.debug("Request body: " + JSON.stringify(req.body, null, 2));
2433
+
2434
+ try {
2435
+ if (!MicrosoftTeamsAppClientId || !MicrosoftTeamsAppClientSecret) {
2436
+ logger.error("Microsoft Teams App credentials not configured");
2437
+ res.status(500).json({ error: "Bot credentials not configured" });
2438
+ return;
2439
+ }
2440
+
2441
+ // Extract tenant ID from the activity
2442
+ const tenantId: string = req.body?.channelData?.tenant?.id;
2443
+ if (!tenantId) {
2444
+ logger.error("Tenant ID not found in activity channelData");
2445
+ res.status(400).json({ error: "Invalid activity: missing tenant ID" });
2446
+ return;
2447
+ }
2448
+
2449
+ // Get Bot Framework adapter
2450
+ const adapter: CloudAdapter = this.getBotAdapter(tenantId);
2451
+
2452
+ // Create custom activity handler class that extends TeamsActivityHandler
2453
+ class OneUptimeTeamsActivityHandler extends TeamsActivityHandler {
2454
+ public constructor() {
2455
+ super();
2456
+
2457
+ // Set up message handlers using the proper API
2458
+ this.onMessage(
2459
+ async (context: TurnContext, next: () => Promise<void>) => {
2460
+ logger.debug(
2461
+ "Handling message activity: " +
2462
+ JSON.stringify(context.activity),
2463
+ );
2464
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
2465
+ activity: context.activity as unknown as JSONObject,
2466
+ turnContext: context,
2467
+ });
2468
+ await next();
2469
+ },
2470
+ );
2471
+
2472
+ this.onMembersAdded(
2473
+ async (context: TurnContext, next: () => Promise<void>) => {
2474
+ logger.debug(
2475
+ "Handling members added activity: " +
2476
+ JSON.stringify(context.activity),
2477
+ );
2478
+ await MicrosoftTeamsUtil.handleConversationUpdateActivity({
2479
+ activity: context.activity as unknown as JSONObject,
2480
+ turnContext: context,
2481
+ });
2482
+ await next();
2483
+ },
2484
+ );
2485
+ }
2486
+
2487
+ protected override async onInvokeActivity(
2488
+ context: TurnContext,
2489
+ ): Promise<any> {
2490
+ logger.debug(
2491
+ "Handling invoke activity: " + JSON.stringify(context.activity),
2492
+ );
2493
+ await MicrosoftTeamsUtil.handleBotInvokeActivity({
2494
+ activity: context.activity as unknown as JSONObject,
2495
+ turnContext: context,
2496
+ });
2497
+ // Return empty response for invoke activities
2498
+ return { status: 200 };
2499
+ }
2500
+ }
2501
+
2502
+ // Create activity handler instance
2503
+ const activityHandler: TeamsActivityHandler =
2504
+ new OneUptimeTeamsActivityHandler();
2505
+
2506
+ // Use the adapter's process method with Express-style req/res
2507
+ await adapter.process(req, res, async (context: TurnContext) => {
2508
+ logger.debug(
2509
+ "Processing activity with TurnContext: " +
2510
+ JSON.stringify({
2511
+ activityType: context.activity.type,
2512
+ activityId: context.activity.id,
2513
+ from: context.activity.from?.name,
2514
+ conversationId: context.activity.conversation?.id,
2515
+ }),
2516
+ );
2517
+
2518
+ // Run the activity through our activity handler
2519
+ await activityHandler.run(context);
2520
+ });
2521
+
2522
+ logger.debug("Bot Framework activity processed successfully");
2523
+ } catch (error) {
2524
+ logger.error("Error processing Bot Framework activity: " + error);
2525
+ if (!res.headersSent) {
2526
+ res.status(500).json({ error: "Failed to process bot activity" });
2527
+ }
2528
+ }
2529
+ }
2530
+
2531
+ // Method to refresh teams list for a user
2532
+ @CaptureSpan()
2533
+ public static async refreshTeams(data: {
2534
+ projectId: ObjectID;
2535
+ }): Promise<Record<string, { id: string; name: string }>> {
2536
+ logger.debug("=== refreshTeams called ===");
2537
+ logger.debug(`Project ID: ${data.projectId.toString()}`);
2538
+
2539
+ try {
2540
+ // Get project auth to get app access token
2541
+ const projectAuth: WorkspaceProjectAuthToken | null =
2542
+ await WorkspaceProjectAuthTokenService.getProjectAuth({
2543
+ projectId: data.projectId,
2544
+ workspaceType: WorkspaceType.MicrosoftTeams,
2545
+ });
2546
+
2547
+ if (!projectAuth || !projectAuth.miscData) {
2548
+ throw new BadDataException(
2549
+ "Microsoft Teams integration not found for this project",
2550
+ );
2551
+ }
2552
+
2553
+ // Get a valid app access token
2554
+ const accessToken: string | null = await this.refreshAccessToken({
2555
+ projectId: data.projectId,
2556
+ miscData: projectAuth.miscData as MicrosoftTeamsMiscData,
2557
+ });
2558
+
2559
+ if (!accessToken) {
2560
+ throw new BadDataException(
2561
+ "Could not obtain valid access token for Microsoft Teams",
2562
+ );
2563
+ }
2564
+
2565
+ // Fetch all teams from Microsoft Graph API using app permissions
2566
+ const teamsResponse: HTTPErrorResponse | HTTPResponse<JSONObject> =
2567
+ await API.get<JSONObject>({
2568
+ url: URL.fromString("https://graph.microsoft.com/v1.0/teams"),
2569
+ headers: {
2570
+ Authorization: `Bearer ${accessToken}`,
2571
+ },
2572
+ });
2573
+
2574
+ if (teamsResponse instanceof HTTPErrorResponse) {
2575
+ logger.error("Error fetching teams from Microsoft Teams:");
2576
+ logger.error(teamsResponse);
2577
+ throw new BadDataException(
2578
+ "Failed to fetch teams from Microsoft Teams",
2579
+ );
2580
+ }
2581
+
2582
+ const teams: Array<JSONObject> =
2583
+ (teamsResponse.data as any)["value"] || [];
2584
+
2585
+ if (teams.length === 0) {
2586
+ logger.debug("No teams found in organization");
2587
+ return {};
2588
+ }
2589
+
2590
+ // Process teams
2591
+ const availableTeams: Record<string, { id: string; name: string }> =
2592
+ teams.reduce(
2593
+ (
2594
+ acc: Record<string, { id: string; name: string }>,
2595
+ t: JSONObject,
2596
+ ) => {
2597
+ const team: { id: string; name: string } = {
2598
+ id: t["id"] as string,
2599
+ name: (t["displayName"] as string) || "Unnamed Team",
2600
+ };
2601
+ acc[team.name] = team;
2602
+ return acc;
2603
+ },
2604
+ {} as Record<string, { id: string; name: string }>,
2605
+ );
2606
+
2607
+ logger.debug(`Fetched ${Object.keys(availableTeams).length} teams`);
2608
+
2609
+ // Update project auth token with new teams
2610
+ const miscData: MicrosoftTeamsMiscData =
2611
+ (projectAuth.miscData as MicrosoftTeamsMiscData) || {};
2612
+ miscData.availableTeams = availableTeams;
2613
+
2614
+ await WorkspaceProjectAuthTokenService.updateOneById({
2615
+ id: projectAuth.id!,
2616
+ data: {
2617
+ miscData: miscData,
2618
+ },
2619
+ props: {
2620
+ isRoot: true,
2621
+ },
2622
+ });
2623
+
2624
+ logger.debug("Updated project auth token with refreshed teams");
2625
+
2626
+ return availableTeams;
2627
+ } catch (error) {
2628
+ logger.error("Error refreshing teams:");
2629
+ logger.error(error);
2630
+ throw error;
2631
+ }
2632
+ }
2633
+
2634
+ // Method to get user's joined teams using user access token
2635
+ @CaptureSpan()
2636
+ public static async getUserJoinedTeams(
2637
+ accessToken: string,
2638
+ ): Promise<Record<string, { id: string; name: string }>> {
2639
+ logger.debug("=== getUserJoinedTeams called ===");
2640
+
2641
+ try {
2642
+ // Get user's teams
2643
+ const teamsResponse: HTTPErrorResponse | HTTPResponse<JSONObject> =
2644
+ await API.get<JSONObject>({
2645
+ url: URL.fromString(
2646
+ "https://graph.microsoft.com/v1.0/me/joinedTeams",
2647
+ ),
2648
+ headers: {
2649
+ Authorization: `Bearer ${accessToken}`,
2650
+ },
2651
+ });
2652
+
2653
+ if (teamsResponse instanceof HTTPErrorResponse) {
2654
+ logger.error("Error getting teams:");
2655
+ logger.error(teamsResponse);
2656
+ throw teamsResponse;
2657
+ }
2658
+
2659
+ const teamsData: JSONObject = teamsResponse.data;
2660
+ const teams: Array<JSONObject> =
2661
+ (teamsData["value"] as Array<JSONObject>) || [];
2662
+
2663
+ if (teams.length === 0) {
2664
+ logger.debug("No joined teams found for user");
2665
+ return {};
2666
+ }
2667
+
2668
+ // Process teams
2669
+ const availableTeams: Record<string, MicrosoftTeamsTeam> = teams.reduce(
2670
+ (acc: Record<string, MicrosoftTeamsTeam>, t: JSONObject) => {
2671
+ const team: { id: string; name: string } = {
2672
+ id: t["id"] as string,
2673
+ name: (t["displayName"] as string) || "Unnamed Team",
2674
+ };
2675
+ acc[team.name] = team;
2676
+ return acc;
2677
+ },
2678
+ {} as Record<string, MicrosoftTeamsTeam>,
2679
+ );
2680
+
2681
+ logger.debug(
2682
+ `Fetched ${Object.keys(availableTeams).length} joined teams`,
2683
+ );
2684
+
2685
+ return availableTeams;
2686
+ } catch (error) {
2687
+ logger.error("Error getting user joined teams:");
2688
+ logger.error(error);
2689
+ throw error;
2690
+ }
2691
+ }
159
2692
  }