@twentyhq/call-recorder 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/manifest.json +297 -0
  2. package/package.json +2 -2
  3. package/src/front-components/calendar-event-recording.front-component.mjs +269 -0
  4. package/src/front-components/calendar-event-recording.front-component.mjs.map +7 -0
  5. package/src/logic-functions/process-recall-webhook.mjs +1744 -0
  6. package/src/logic-functions/process-recall-webhook.mjs.map +7 -0
  7. package/src/logic-functions/recall-webhook.mjs +391 -0
  8. package/src/logic-functions/recall-webhook.mjs.map +7 -0
  9. package/src/logic-functions/reconcile-call-recorder-calendar-event.mjs +1602 -0
  10. package/src/logic-functions/reconcile-call-recorder-calendar-event.mjs.map +7 -0
  11. package/src/logic-functions/reconcile-stale-bot-state.mjs +2268 -0
  12. package/src/logic-functions/reconcile-stale-bot-state.mjs.map +7 -0
  13. package/.env.example +0 -5
  14. package/.nvmrc +0 -1
  15. package/.oxlintrc.json +0 -20
  16. package/AGENTS.md +0 -67
  17. package/CLAUDE.md +0 -67
  18. package/README.md +0 -24
  19. package/SETUP.md +0 -95
  20. package/src/__tests__/global-setup.ts +0 -100
  21. package/src/__tests__/schema.integration-test.ts +0 -104
  22. package/src/application-config.ts +0 -96
  23. package/src/constants/__tests__/call-recording-field-universal-identifiers.test.ts +0 -19
  24. package/src/constants/app-description.ts +0 -2
  25. package/src/constants/app-display-name.ts +0 -1
  26. package/src/constants/application-universal-identifier.ts +0 -2
  27. package/src/constants/calendar-event-reconciliation-logic-function-universal-identifier.ts +0 -2
  28. package/src/constants/calendar-event-record-page-layout-universal-identifier.ts +0 -2
  29. package/src/constants/calendar-event-recording-front-component-universal-identifier.ts +0 -2
  30. package/src/constants/calendar-event-recording-page-layout-tab-universal-identifier.ts +0 -2
  31. package/src/constants/calendar-event-recording-page-layout-widget-universal-identifier.ts +0 -2
  32. package/src/constants/call-recorder-everyone-left-timeout-seconds-app-variable-universal-identifier.ts +0 -2
  33. package/src/constants/call-recorder-failure-reason-on-call-recording-field-universal-identifier.ts +0 -2
  34. package/src/constants/call-recorder-join-early-minutes-app-variable-universal-identifier.ts +0 -2
  35. package/src/constants/call-recorder-name-app-variable-universal-identifier.ts +0 -2
  36. package/src/constants/call-recorder-noone-joined-timeout-seconds-app-variable-universal-identifier.ts +0 -2
  37. package/src/constants/call-recorder-preference-off-option-id.ts +0 -2
  38. package/src/constants/call-recorder-preference-on-calendar-event-field-universal-identifier.ts +0 -2
  39. package/src/constants/call-recorder-preference-on-calendar-event-view-field-universal-identifier.ts +0 -2
  40. package/src/constants/call-recorder-preference-on-option-id.ts +0 -2
  41. package/src/constants/call-recorder-preference.ts +0 -4
  42. package/src/constants/call-recorder-waiting-room-timeout-seconds-app-variable-universal-identifier.ts +0 -2
  43. package/src/constants/call-recording-audio-field-universal-identifier.ts +0 -2
  44. package/src/constants/call-recording-video-field-universal-identifier.ts +0 -2
  45. package/src/constants/default-role-universal-identifier.ts +0 -2
  46. package/src/constants/process-recall-webhook-logic-function-universal-identifier.ts +0 -2
  47. package/src/constants/recall-webhook-logic-function-universal-identifier.ts +0 -2
  48. package/src/constants/stale-bot-state-logic-function-universal-identifier.ts +0 -2
  49. package/src/default-role.ts +0 -69
  50. package/src/fields/call-recorder-failure-reason-on-call-recording.field.ts +0 -22
  51. package/src/fields/call-recorder-preference-on-calendar-event.field.ts +0 -41
  52. package/src/front-components/calendar-event-recording.front-component.tsx +0 -13
  53. package/src/front-components/components/CalendarEventRecording.tsx +0 -39
  54. package/src/front-components/components/CalendarEventRecordingBody.tsx +0 -96
  55. package/src/front-components/components/CalendarEventRecordingContent.tsx +0 -111
  56. package/src/front-components/components/RecordingTranscript.tsx +0 -92
  57. package/src/front-components/components/RecordingVideoPlayer.tsx +0 -52
  58. package/src/front-components/components/TranscriptEntryList.tsx +0 -61
  59. package/src/front-components/components/TranscriptEntryListItem.tsx +0 -115
  60. package/src/front-components/components/TranscriptErrorBox.tsx +0 -48
  61. package/src/front-components/components/TranscriptSpeakerAvatar.tsx +0 -141
  62. package/src/front-components/components/TranscriptSpeakerChip.tsx +0 -51
  63. package/src/front-components/constants/recording-theme-css-variables.ts +0 -40
  64. package/src/front-components/hooks/use-calendar-event-participants.ts +0 -172
  65. package/src/front-components/hooks/use-calendar-event-recording.ts +0 -155
  66. package/src/front-components/types/calendar-event-participant-by-speaker-name.type.ts +0 -6
  67. package/src/front-components/types/calendar-event-recording-participant.type.ts +0 -7
  68. package/src/front-components/types/transcript-entry.type.ts +0 -13
  69. package/src/front-components/utils/__tests__/find-active-transcript-entry-index.test.ts +0 -66
  70. package/src/front-components/utils/__tests__/format-transcript-timestamp.test.ts +0 -29
  71. package/src/front-components/utils/__tests__/get-speaker-name-match-keys.test.ts +0 -22
  72. package/src/front-components/utils/__tests__/parse-transcript-entries.test.ts +0 -162
  73. package/src/front-components/utils/build-calendar-event-participant-by-speaker-name.util.ts +0 -45
  74. package/src/front-components/utils/find-active-transcript-entry-index.util.ts +0 -77
  75. package/src/front-components/utils/format-transcript-timestamp.util.ts +0 -16
  76. package/src/front-components/utils/get-absolute-avatar-url.util.ts +0 -48
  77. package/src/front-components/utils/get-calendar-event-participant-for-speaker-name.util.ts +0 -24
  78. package/src/front-components/utils/get-speaker-name-match-keys.util.ts +0 -64
  79. package/src/front-components/utils/get-video-file-extension.util.ts +0 -23
  80. package/src/front-components/utils/parse-transcript-entries.util.ts +0 -85
  81. package/src/logic-functions/__tests__/process-recall-webhook.test.ts +0 -62
  82. package/src/logic-functions/__tests__/recall-webhook.test.ts +0 -180
  83. package/src/logic-functions/constants/call-recorder-everyone-left-timeout-seconds-env-var-name.ts +0 -2
  84. package/src/logic-functions/constants/call-recorder-everyone-left-timeout-seconds.ts +0 -1
  85. package/src/logic-functions/constants/call-recorder-join-early-minutes-env-var-name.ts +0 -2
  86. package/src/logic-functions/constants/call-recorder-name-env-var-name.ts +0 -1
  87. package/src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds-env-var-name.ts +0 -2
  88. package/src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds.ts +0 -1
  89. package/src/logic-functions/constants/call-recorder-recording-retention-hours-env-var-name.ts +0 -2
  90. package/src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds-env-var-name.ts +0 -2
  91. package/src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds.ts +0 -1
  92. package/src/logic-functions/constants/call-recording-micro-credits-per-hour.ts +0 -1
  93. package/src/logic-functions/constants/call-recording-request-status.ts +0 -5
  94. package/src/logic-functions/constants/call-recording-status.ts +0 -9
  95. package/src/logic-functions/constants/default-call-recorder-join-early-minutes.ts +0 -1
  96. package/src/logic-functions/constants/default-call-recorder-name.ts +0 -1
  97. package/src/logic-functions/constants/default-call-recorder-recording-retention-hours.ts +0 -2
  98. package/src/logic-functions/constants/default-recall-region.ts +0 -1
  99. package/src/logic-functions/constants/milliseconds-per-minute.ts +0 -1
  100. package/src/logic-functions/constants/non-terminal-call-recording-statuses.ts +0 -8
  101. package/src/logic-functions/constants/recall-api-key-env-var-name.ts +0 -1
  102. package/src/logic-functions/constants/recall-api-max-attempts.ts +0 -1
  103. package/src/logic-functions/constants/recall-api-retry-delay-ms.ts +0 -1
  104. package/src/logic-functions/constants/recall-bot-automatic-leave.ts +0 -74
  105. package/src/logic-functions/constants/recall-bot-everyone-left-min-activate-after-seconds.ts +0 -1
  106. package/src/logic-functions/constants/recall-bot-recording-config.ts +0 -34
  107. package/src/logic-functions/constants/recall-region-env-var-name.ts +0 -1
  108. package/src/logic-functions/constants/recall-webhook-secret-env-var-name.ts +0 -1
  109. package/src/logic-functions/constants/restricted-field-placeholder.ts +0 -3
  110. package/src/logic-functions/constants/stale-bot-state-cron-pattern.ts +0 -1
  111. package/src/logic-functions/constants/twenty-page-size.ts +0 -1
  112. package/src/logic-functions/data/__tests__/complete-call-recording-ingestion.test.ts +0 -55
  113. package/src/logic-functions/data/__tests__/fetch-all-nodes.test.ts +0 -43
  114. package/src/logic-functions/data/__tests__/get-current-workspace-id.test.ts +0 -38
  115. package/src/logic-functions/data/__tests__/strip-restricted-field-value.test.ts +0 -22
  116. package/src/logic-functions/data/complete-call-recording-ingestion.util.ts +0 -24
  117. package/src/logic-functions/data/create-call-recording.util.ts +0 -41
  118. package/src/logic-functions/data/fetch-all-nodes.util.ts +0 -44
  119. package/src/logic-functions/data/fetch-calendar-events-by-filter.util.ts +0 -80
  120. package/src/logic-functions/data/fetch-calendar-events-by-ids.util.ts +0 -20
  121. package/src/logic-functions/data/fetch-calendar-events-by-starts-at-values.util.ts +0 -19
  122. package/src/logic-functions/data/find-call-recordings-by-calendar-event-ids.util.ts +0 -17
  123. package/src/logic-functions/data/find-call-recordings-by-filter.util.ts +0 -102
  124. package/src/logic-functions/data/find-call-recordings-by-ids.util.ts +0 -17
  125. package/src/logic-functions/data/find-open-scheduled-call-recordings.util.ts +0 -14
  126. package/src/logic-functions/data/get-current-workspace-id.util.ts +0 -36
  127. package/src/logic-functions/data/strip-restricted-field-value.util.ts +0 -6
  128. package/src/logic-functions/data/update-call-recording.util.ts +0 -24
  129. package/src/logic-functions/domain/__tests__/build-call-recorder-policy-result.test.ts +0 -47
  130. package/src/logic-functions/domain/__tests__/compute-call-recording-charge.test.ts +0 -71
  131. package/src/logic-functions/domain/__tests__/compute-call-recording-id-for-meeting.test.ts +0 -37
  132. package/src/logic-functions/domain/__tests__/compute-real-meeting-key.test.ts +0 -88
  133. package/src/logic-functions/domain/__tests__/is-call-recording-ingestion-complete.test.ts +0 -59
  134. package/src/logic-functions/domain/__tests__/is-call-recording-status-downgrade.test.ts +0 -37
  135. package/src/logic-functions/domain/__tests__/resolve-call-recorder-policy-result.test.ts +0 -120
  136. package/src/logic-functions/domain/__tests__/should-complete-call-recording-ingestion.test.ts +0 -102
  137. package/src/logic-functions/domain/aggregate-call-recorder-policy-results-by-meeting.util.ts +0 -42
  138. package/src/logic-functions/domain/build-call-recorder-policy-result.util.ts +0 -53
  139. package/src/logic-functions/domain/build-failed-transcript-marker.util.ts +0 -13
  140. package/src/logic-functions/domain/build-pending-transcript-marker.util.ts +0 -13
  141. package/src/logic-functions/domain/build-recall-routing-metadata.util.ts +0 -12
  142. package/src/logic-functions/domain/build-transcript-failure-reason.util.ts +0 -7
  143. package/src/logic-functions/domain/compute-call-recording-charge.util.ts +0 -41
  144. package/src/logic-functions/domain/compute-call-recording-id-for-meeting.util.ts +0 -16
  145. package/src/logic-functions/domain/compute-real-meeting-key.util.ts +0 -48
  146. package/src/logic-functions/domain/compute-recall-bot-join-at.util.ts +0 -34
  147. package/src/logic-functions/domain/is-call-recording-ingestion-complete.util.ts +0 -19
  148. package/src/logic-functions/domain/is-call-recording-status-downgrade.util.ts +0 -37
  149. package/src/logic-functions/domain/is-recall-recording-done-signal.util.ts +0 -13
  150. package/src/logic-functions/domain/map-recall-status-code-to-call-recording-status.util.ts +0 -26
  151. package/src/logic-functions/domain/parse-transcript-marker.util.ts +0 -29
  152. package/src/logic-functions/domain/resolve-call-recorder-policy-result.util.ts +0 -72
  153. package/src/logic-functions/domain/should-complete-call-recording-ingestion.util.ts +0 -32
  154. package/src/logic-functions/flows/__tests__/charge-completed-call-recording.test.ts +0 -45
  155. package/src/logic-functions/flows/__tests__/complete-and-charge-call-recording.test.ts +0 -61
  156. package/src/logic-functions/flows/__tests__/converge-diverged-call-recordings.test.ts +0 -727
  157. package/src/logic-functions/flows/__tests__/download-transcript.test.ts +0 -74
  158. package/src/logic-functions/flows/__tests__/handle-recall-webhook.test.ts +0 -1301
  159. package/src/logic-functions/flows/__tests__/heal-call-recordings-missing-bot.test.ts +0 -225
  160. package/src/logic-functions/flows/__tests__/ingest-call-recording-media.test.ts +0 -153
  161. package/src/logic-functions/flows/__tests__/reap-orphaned-call-recorders.test.ts +0 -425
  162. package/src/logic-functions/flows/__tests__/reconcile-call-recorder.test.ts +0 -1007
  163. package/src/logic-functions/flows/cancel-call-recording-request.util.ts +0 -46
  164. package/src/logic-functions/flows/charge-completed-call-recording.util.ts +0 -31
  165. package/src/logic-functions/flows/complete-and-charge-call-recording.util.ts +0 -29
  166. package/src/logic-functions/flows/converge-diverged-call-recordings-result.type.ts +0 -8
  167. package/src/logic-functions/flows/converge-diverged-call-recordings.util.ts +0 -447
  168. package/src/logic-functions/flows/download-transcript.util.ts +0 -67
  169. package/src/logic-functions/flows/ensure-call-recorder.util.ts +0 -73
  170. package/src/logic-functions/flows/handle-recall-webhook.util.ts +0 -672
  171. package/src/logic-functions/flows/heal-call-recordings-missing-bot.util.ts +0 -82
  172. package/src/logic-functions/flows/ingest-call-recording-media.util.ts +0 -128
  173. package/src/logic-functions/flows/persist-call-recording-progress.util.ts +0 -58
  174. package/src/logic-functions/flows/reap-orphaned-call-recorders.util.ts +0 -183
  175. package/src/logic-functions/flows/reconcile-call-recorder.util.ts +0 -495
  176. package/src/logic-functions/flows/reconcile-call-recording-transcript-artifact-result.type.ts +0 -11
  177. package/src/logic-functions/flows/reconcile-call-recording-transcript-artifact.util.ts +0 -182
  178. package/src/logic-functions/flows/reschedule-call-recording-bot.util.ts +0 -69
  179. package/src/logic-functions/process-recall-webhook.ts +0 -23
  180. package/src/logic-functions/recall-api/__tests__/extract-recall-bot-convergence.test.ts +0 -153
  181. package/src/logic-functions/recall-api/__tests__/extract-recall-media-urls.test.ts +0 -67
  182. package/src/logic-functions/recall-api/__tests__/recall-bot-api.test.ts +0 -744
  183. package/src/logic-functions/recall-api/__tests__/verify-recall-webhook-signature.test.ts +0 -122
  184. package/src/logic-functions/recall-api/cancel-recall-bot.util.ts +0 -28
  185. package/src/logic-functions/recall-api/create-async-recall-transcript.util.ts +0 -47
  186. package/src/logic-functions/recall-api/eject-recall-bot.util.ts +0 -28
  187. package/src/logic-functions/recall-api/extract-recall-bot-convergence.util.ts +0 -149
  188. package/src/logic-functions/recall-api/extract-recall-bot-id.util.ts +0 -10
  189. package/src/logic-functions/recall-api/extract-recall-media-urls.util.ts +0 -30
  190. package/src/logic-functions/recall-api/extract-twenty-workspace-id-from-recall-webhook.util.ts +0 -8
  191. package/src/logic-functions/recall-api/get-recall-api-config.util.ts +0 -59
  192. package/src/logic-functions/recall-api/get-recall-bot.util.ts +0 -42
  193. package/src/logic-functions/recall-api/get-recall-recording.util.ts +0 -31
  194. package/src/logic-functions/recall-api/get-recall-webhook-bot-metadata.util.ts +0 -18
  195. package/src/logic-functions/recall-api/list-recall-transcripts.util.ts +0 -141
  196. package/src/logic-functions/recall-api/list-scheduled-recall-bots.util.ts +0 -106
  197. package/src/logic-functions/recall-api/normalize-recall-timestamp.util.ts +0 -14
  198. package/src/logic-functions/recall-api/parse-recall-webhook-event.util.ts +0 -88
  199. package/src/logic-functions/recall-api/recall-bot-api-request.util.ts +0 -165
  200. package/src/logic-functions/recall-api/recall-transcript-summary.type.ts +0 -5
  201. package/src/logic-functions/recall-api/reschedule-recall-bot.util.ts +0 -56
  202. package/src/logic-functions/recall-api/retrieve-recall-transcript.util.ts +0 -71
  203. package/src/logic-functions/recall-api/schedule-recall-bot.util.ts +0 -68
  204. package/src/logic-functions/recall-api/verify-recall-webhook-signature.util.ts +0 -109
  205. package/src/logic-functions/recall-webhook.ts +0 -90
  206. package/src/logic-functions/reconcile-call-recorder-calendar-event.ts +0 -178
  207. package/src/logic-functions/reconcile-stale-bot-state.ts +0 -106
  208. package/src/logic-functions/types/calendar-event-record.type.ts +0 -5
  209. package/src/logic-functions/types/call-recorder-policy-calendar-event-input.type.ts +0 -10
  210. package/src/logic-functions/types/call-recorder-policy-input.type.ts +0 -9
  211. package/src/logic-functions/types/call-recorder-policy-not-required-reason.type.ts +0 -5
  212. package/src/logic-functions/types/call-recorder-policy-required-reason.type.ts +0 -1
  213. package/src/logic-functions/types/call-recorder-policy-result-for-calendar-event.type.ts +0 -9
  214. package/src/logic-functions/types/call-recorder-policy-result-for-meeting.type.ts +0 -6
  215. package/src/logic-functions/types/call-recorder-policy-result.type.ts +0 -12
  216. package/src/logic-functions/types/call-recorder-reconciliation-result.type.ts +0 -16
  217. package/src/logic-functions/types/call-recording-media-file.type.ts +0 -1
  218. package/src/logic-functions/types/call-recording-record.type.ts +0 -15
  219. package/src/logic-functions/types/call-recording-update-fields.type.ts +0 -20
  220. package/src/logic-functions/types/files-field-value.type.ts +0 -1
  221. package/src/logic-functions/types/meeting-recording.type.ts +0 -7
  222. package/src/logic-functions/types/recall-bot-operation-result.type.ts +0 -19
  223. package/src/logic-functions/types/recall-routing-metadata.type.ts +0 -4
  224. package/src/logic-functions/types/removed-call-recorder-occurrence.type.ts +0 -6
  225. package/src/logic-functions/types/transcript-marker.type.ts +0 -6
  226. package/src/logic-functions/utils/as-record.util.ts +0 -6
  227. package/src/logic-functions/utils/get-application-variable-value.util.ts +0 -3
  228. package/src/logic-functions/utils/get-record-at-path.util.ts +0 -10
  229. package/src/logic-functions/utils/get-string.util.ts +0 -4
  230. package/src/logic-functions/utils/get-unique-sorted-ids.util.ts +0 -8
  231. package/src/logic-functions/utils/is-non-empty-string.util.ts +0 -5
  232. package/src/page-layouts/calendar-event-recording-tab.ts +0 -33
  233. package/src/view-fields/call-recorder-preference-on-calendar-event.view-field.ts +0 -27
  234. package/tsconfig.json +0 -42
  235. package/tsconfig.spec.json +0 -9
  236. package/vitest.config.ts +0 -31
  237. package/vitest.unit.config.ts +0 -14
@@ -1,46 +0,0 @@
1
- import { isUndefined } from '@sniptt/guards';
2
- import { type CoreApiClient } from 'twenty-client-sdk/core';
3
-
4
- import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status';
5
- import { type CallRecordingRecord } from 'src/logic-functions/types/call-recording-record.type';
6
- import { cancelRecallBot } from 'src/logic-functions/recall-api/cancel-recall-bot.util';
7
- import { updateCallRecording } from 'src/logic-functions/data/update-call-recording.util';
8
-
9
- // Intent-first: the stale-state cron finishes the Recall half when this call fails.
10
- export const cancelCallRecordingRequest = async ({
11
- client,
12
- callRecording,
13
- }: {
14
- client: CoreApiClient;
15
- callRecording: CallRecordingRecord;
16
- }): Promise<void> => {
17
- await updateCallRecording(client, {
18
- id: callRecording.id,
19
- data: {
20
- recordingRequestStatus: CallRecordingRequestStatus.CANCELED,
21
- },
22
- });
23
-
24
- if (isUndefined(callRecording.externalBotId)) {
25
- return;
26
- }
27
-
28
- const cancelResult = await cancelRecallBot({
29
- externalBotId: callRecording.externalBotId,
30
- });
31
-
32
- if (!cancelResult.ok) {
33
- console.warn(
34
- `[call-recorder] failed to cancel Recall bot for callRecording ${callRecording.id}, leaving it for the stale-state cron: ${cancelResult.errorMessage}`,
35
- );
36
-
37
- return;
38
- }
39
-
40
- await updateCallRecording(client, {
41
- id: callRecording.id,
42
- data: {
43
- externalBotId: null,
44
- },
45
- });
46
- };
@@ -1,31 +0,0 @@
1
- import { isUndefined } from '@sniptt/guards';
2
- import { chargeCredits } from 'twenty-sdk/billing';
3
-
4
- import { computeCallRecordingCharge } from 'src/logic-functions/domain/compute-call-recording-charge.util';
5
-
6
- export const chargeCompletedCallRecording = async ({
7
- callRecordingId,
8
- startedAt,
9
- endedAt,
10
- }: {
11
- callRecordingId: string;
12
- startedAt: string | undefined;
13
- endedAt: string | undefined;
14
- }): Promise<void> => {
15
- const charge = computeCallRecordingCharge({ startedAt, endedAt });
16
-
17
- if (isUndefined(charge)) {
18
- console.warn(
19
- `[call-recorder] call recording ${callRecordingId} completed without usable recording timestamps; it will not be billed`,
20
- );
21
-
22
- return;
23
- }
24
-
25
- await chargeCredits({
26
- creditsUsedMicro: charge.creditsUsedMicro,
27
- quantity: charge.quantityMinutes,
28
- operationType: 'CALL_RECORDING',
29
- resourceContext: 'recall',
30
- });
31
- };
@@ -1,29 +0,0 @@
1
- import { type CoreApiClient } from 'twenty-client-sdk/core';
2
-
3
- import { completeCallRecordingIngestion } from 'src/logic-functions/data/complete-call-recording-ingestion.util';
4
- import { chargeCompletedCallRecording } from 'src/logic-functions/flows/charge-completed-call-recording.util';
5
-
6
- export const completeAndChargeCallRecording = async (
7
- client: CoreApiClient,
8
- {
9
- id,
10
- startedAt,
11
- endedAt,
12
- }: {
13
- id: string;
14
- startedAt: string | undefined;
15
- endedAt: string | undefined;
16
- },
17
- ): Promise<boolean> => {
18
- const claimed = await completeCallRecordingIngestion(client, { id });
19
-
20
- if (claimed) {
21
- await chargeCompletedCallRecording({
22
- callRecordingId: id,
23
- startedAt,
24
- endedAt,
25
- });
26
- }
27
-
28
- return claimed;
29
- };
@@ -1,8 +0,0 @@
1
- export type ConvergeDivergedCallRecordingsResult = {
2
- candidateCount: number;
3
- updatedCallRecordingIds: string[];
4
- markedFailedCallRecordingIds: string[];
5
- requestedTranscriptCallRecordingIds: string[];
6
- unconvergeableCallRecordingIds: string[];
7
- skippedNotStartedCallRecordingIds: string[];
8
- };
@@ -1,447 +0,0 @@
1
- import { isNonEmptyArray, isUndefined } from '@sniptt/guards';
2
- import { type CoreApiClient } from 'twenty-client-sdk/core';
3
-
4
- import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status';
5
- import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status';
6
- import { NON_TERMINAL_CALL_RECORDING_STATUSES } from 'src/logic-functions/constants/non-terminal-call-recording-statuses';
7
- import { TWENTY_PAGE_SIZE } from 'src/logic-functions/constants/twenty-page-size';
8
- import { type FilesFieldValue } from 'src/logic-functions/types/files-field-value.type';
9
- import {
10
- extractRecallBotConvergence,
11
- type RecallBotConvergence,
12
- } from 'src/logic-functions/recall-api/extract-recall-bot-convergence.util';
13
- import {
14
- fetchAllNodes,
15
- type ConnectionPage,
16
- } from 'src/logic-functions/data/fetch-all-nodes.util';
17
- import { getRecallBot } from 'src/logic-functions/recall-api/get-recall-bot.util';
18
- import { ingestCallRecordingMedia } from 'src/logic-functions/flows/ingest-call-recording-media.util';
19
- import { isCallRecordingStatusDowngrade } from 'src/logic-functions/domain/is-call-recording-status-downgrade.util';
20
- import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util';
21
- import { parseTranscriptMarker } from 'src/logic-functions/domain/parse-transcript-marker.util';
22
- import { persistCallRecordingProgress } from 'src/logic-functions/flows/persist-call-recording-progress.util';
23
- import { reconcileCallRecordingTranscriptArtifact } from 'src/logic-functions/flows/reconcile-call-recording-transcript-artifact.util';
24
- import { type ConvergeDivergedCallRecordingsResult } from 'src/logic-functions/flows/converge-diverged-call-recordings-result.type';
25
- import { shouldCompleteCallRecordingIngestion } from 'src/logic-functions/domain/should-complete-call-recording-ingestion.util';
26
- import { updateCallRecording } from 'src/logic-functions/data/update-call-recording.util';
27
- import { type CallRecordingUpdateFields } from 'src/logic-functions/types/call-recording-update-fields.type';
28
-
29
- const CONVERGENCE_LOOKBACK_DAYS = 7;
30
-
31
- type DivergedCallRecordingCandidate = {
32
- id: string;
33
- status: string | undefined;
34
- startedAt: string | undefined;
35
- endedAt: string | undefined;
36
- externalBotId: string | undefined;
37
- externalRecordingId: string | undefined;
38
- transcript: unknown;
39
- audio: FilesFieldValue | undefined;
40
- video: FilesFieldValue | undefined;
41
- createdAt: string | undefined;
42
- calendarEventStartsAt: string | undefined;
43
- calendarEventEndsAt: string | undefined;
44
- };
45
-
46
- type DivergedCallRecordingNode = {
47
- id: string;
48
- status?: string | null;
49
- startedAt?: string | null;
50
- endedAt?: string | null;
51
- externalBotId?: string | null;
52
- externalRecordingId?: string | null;
53
- transcript?: unknown;
54
- audio?: FilesFieldValue | null;
55
- video?: FilesFieldValue | null;
56
- createdAt?: string | null;
57
- calendarEvent?: { startsAt?: string | null; endsAt?: string | null } | null;
58
- };
59
-
60
- // Webhook deliveries get lost; this pull pass re-derives state from Recall.
61
- export const convergeDivergedCallRecordings = async ({
62
- client,
63
- now,
64
- }: {
65
- client: CoreApiClient;
66
- now: Date;
67
- }): Promise<ConvergeDivergedCallRecordingsResult> => {
68
- const candidates = await fetchDivergedCallRecordingCandidates(client);
69
- const convergenceLowerBound = new Date(
70
- now.getTime() - CONVERGENCE_LOOKBACK_DAYS * 24 * 60 * 60 * 1000,
71
- );
72
- const result: ConvergeDivergedCallRecordingsResult = {
73
- candidateCount: candidates.length,
74
- updatedCallRecordingIds: [],
75
- markedFailedCallRecordingIds: [],
76
- requestedTranscriptCallRecordingIds: [],
77
- unconvergeableCallRecordingIds: [],
78
- skippedNotStartedCallRecordingIds: [],
79
- };
80
-
81
- for (const candidate of candidates) {
82
- if (isOutsideConvergenceBound(candidate, convergenceLowerBound)) {
83
- console.warn(
84
- `[call-recorder] call recording ${candidate.id} diverged but its meeting ended more than ${CONVERGENCE_LOOKBACK_DAYS} days ago; it will not converge automatically`,
85
- );
86
- result.unconvergeableCallRecordingIds.push(candidate.id);
87
- continue;
88
- }
89
-
90
- if (isUndefined(candidate.externalBotId)) {
91
- console.warn(
92
- `[call-recorder] call recording ${candidate.id} diverged but has no Recall bot id; it will not converge automatically`,
93
- );
94
- result.unconvergeableCallRecordingIds.push(candidate.id);
95
- continue;
96
- }
97
-
98
- if (isBeforeMeetingStart(candidate, now)) {
99
- result.skippedNotStartedCallRecordingIds.push(candidate.id);
100
- continue;
101
- }
102
-
103
- await convergeCallRecording({
104
- client,
105
- candidate,
106
- externalBotId: candidate.externalBotId,
107
- now,
108
- result,
109
- });
110
- }
111
-
112
- return result;
113
- };
114
-
115
- const fetchDivergedCallRecordingCandidates = async (
116
- client: CoreApiClient,
117
- ): Promise<DivergedCallRecordingCandidate[]> => {
118
- // No createdAt bound: older-than-lookback candidates must surface in logs.
119
- const filter: Record<string, unknown> = {
120
- or: [
121
- {
122
- recordingRequestStatus: { eq: CallRecordingRequestStatus.REQUESTED },
123
- status: { in: NON_TERMINAL_CALL_RECORDING_STATUSES },
124
- externalBotId: { is: 'NOT_NULL' },
125
- },
126
- {
127
- status: { eq: CallRecordingStatus.COMPLETED },
128
- or: [{ startedAt: { is: 'NULL' } }, { endedAt: { is: 'NULL' } }],
129
- },
130
- ],
131
- };
132
- const candidateNodes = await fetchAllNodes<DivergedCallRecordingNode>(
133
- async (afterCursor) => {
134
- const queryResult = await client.query({
135
- callRecordings: {
136
- __args: {
137
- filter,
138
- first: TWENTY_PAGE_SIZE,
139
- ...(isUndefined(afterCursor) ? {} : { after: afterCursor }),
140
- },
141
- pageInfo: {
142
- hasNextPage: true,
143
- endCursor: true,
144
- },
145
- edges: {
146
- node: {
147
- id: true,
148
- status: true,
149
- startedAt: true,
150
- endedAt: true,
151
- externalBotId: true,
152
- externalRecordingId: true,
153
- transcript: true,
154
- audio: { fileId: true },
155
- video: { fileId: true },
156
- createdAt: true,
157
- calendarEvent: {
158
- startsAt: true,
159
- endsAt: true,
160
- },
161
- },
162
- },
163
- },
164
- });
165
-
166
- return (queryResult.callRecordings ?? undefined) as
167
- | ConnectionPage<DivergedCallRecordingNode>
168
- | undefined;
169
- },
170
- );
171
-
172
- return candidateNodes.map((node) => ({
173
- id: node.id,
174
- status: node.status ?? undefined,
175
- startedAt: node.startedAt ?? undefined,
176
- endedAt: node.endedAt ?? undefined,
177
- externalBotId: isNonEmptyString(node.externalBotId)
178
- ? node.externalBotId
179
- : undefined,
180
- externalRecordingId: isNonEmptyString(node.externalRecordingId)
181
- ? node.externalRecordingId
182
- : undefined,
183
- transcript: node.transcript ?? undefined,
184
- audio: node.audio ?? undefined,
185
- video: node.video ?? undefined,
186
- createdAt: node.createdAt ?? undefined,
187
- calendarEventStartsAt: node.calendarEvent?.startsAt ?? undefined,
188
- calendarEventEndsAt: node.calendarEvent?.endsAt ?? undefined,
189
- }));
190
- };
191
-
192
- // Anchored to meeting end: createdAt is scheduling time and can predate the meeting by weeks.
193
- const isOutsideConvergenceBound = (
194
- candidate: DivergedCallRecordingCandidate,
195
- convergenceLowerBound: Date,
196
- ): boolean => {
197
- const meetingEndReference =
198
- candidate.calendarEventEndsAt ?? candidate.createdAt;
199
-
200
- return (
201
- !isUndefined(meetingEndReference) &&
202
- new Date(meetingEndReference).getTime() < convergenceLowerBound.getTime()
203
- );
204
- };
205
-
206
- // Until the meeting starts the bot has recorded nothing, so there is nothing to pull yet.
207
- const isBeforeMeetingStart = (
208
- candidate: DivergedCallRecordingCandidate,
209
- now: Date,
210
- ): boolean =>
211
- !isUndefined(candidate.calendarEventStartsAt) &&
212
- new Date(candidate.calendarEventStartsAt).getTime() > now.getTime();
213
-
214
- const convergeCallRecording = async ({
215
- client,
216
- candidate,
217
- externalBotId,
218
- now,
219
- result,
220
- }: {
221
- client: CoreApiClient;
222
- candidate: DivergedCallRecordingCandidate;
223
- externalBotId: string;
224
- now: Date;
225
- result: ConvergeDivergedCallRecordingsResult;
226
- }): Promise<void> => {
227
- const botResult = await getRecallBot({ externalBotId });
228
-
229
- if (!botResult.ok) {
230
- if (botResult.status === 404) {
231
- await markCallRecordingFailedAfterBotLoss({
232
- client,
233
- candidate,
234
- externalBotId,
235
- result,
236
- });
237
-
238
- return;
239
- }
240
-
241
- console.warn(
242
- `[call-recorder] failed to fetch Recall bot ${externalBotId} for call recording ${candidate.id}: ${botResult.errorMessage}`,
243
- );
244
-
245
- return;
246
- }
247
-
248
- const convergence = extractRecallBotConvergence(botResult.bot);
249
- const updateData = buildConvergenceFieldUpdates({ candidate, convergence });
250
-
251
- const externalRecordingId =
252
- candidate.externalRecordingId ?? convergence.externalRecordingId;
253
-
254
- if (convergence.isRecallRecordingDone && !isUndefined(externalRecordingId)) {
255
- const transcriptArtifactResult =
256
- await reconcileCallRecordingTranscriptArtifact({
257
- callRecordingId: candidate.id,
258
- currentStatus: candidate.status,
259
- externalRecordingId,
260
- requestedAt: now.toISOString(),
261
- transcript: candidate.transcript,
262
- });
263
-
264
- Object.assign(updateData, transcriptArtifactResult.updateData);
265
-
266
- if (transcriptArtifactResult.requestedTranscript) {
267
- result.requestedTranscriptCallRecordingIds.push(candidate.id);
268
- }
269
-
270
- Object.assign(
271
- updateData,
272
- await ingestCallRecordingMedia({
273
- callRecordingId: candidate.id,
274
- externalRecordingId,
275
- hasAudio: isNonEmptyArray(candidate.audio),
276
- hasVideo: isNonEmptyArray(candidate.video),
277
- }),
278
- );
279
- }
280
-
281
- const terminalArtifactGateFailureUpdate =
282
- buildTerminalArtifactGateFailureUpdate({
283
- candidate,
284
- convergence,
285
- externalRecordingId,
286
- updateData,
287
- });
288
-
289
- if (!isUndefined(terminalArtifactGateFailureUpdate)) {
290
- Object.assign(updateData, terminalArtifactGateFailureUpdate);
291
- }
292
-
293
- const completesIngestion = shouldCompleteCallRecordingIngestion({
294
- current: candidate,
295
- updateData,
296
- });
297
-
298
- if (Object.keys(updateData).length === 0 && !completesIngestion) {
299
- return;
300
- }
301
-
302
- await persistCallRecordingProgress(client, {
303
- id: candidate.id,
304
- current: candidate,
305
- updateData,
306
- });
307
-
308
- result.updatedCallRecordingIds.push(candidate.id);
309
- };
310
-
311
- // Pure merge: fill only unset candidate fields and never downgrade status.
312
- const buildConvergenceFieldUpdates = ({
313
- candidate,
314
- convergence,
315
- }: {
316
- candidate: DivergedCallRecordingCandidate;
317
- convergence: RecallBotConvergence;
318
- }): CallRecordingUpdateFields => {
319
- const updateData: CallRecordingUpdateFields = {};
320
-
321
- if (
322
- !isUndefined(convergence.status) &&
323
- convergence.status !== candidate.status &&
324
- !isCallRecordingStatusDowngrade({
325
- fromStatus: candidate.status,
326
- toStatus: convergence.status,
327
- })
328
- ) {
329
- updateData.status = convergence.status;
330
-
331
- if (convergence.status === CallRecordingStatus.FAILED) {
332
- updateData.callRecorderFailureReason =
333
- convergence.failureReason ?? 'recall_bot_failed';
334
- }
335
- }
336
-
337
- if (isUndefined(candidate.startedAt) && !isUndefined(convergence.startedAt)) {
338
- updateData.startedAt = convergence.startedAt;
339
- }
340
-
341
- if (isUndefined(candidate.endedAt) && !isUndefined(convergence.endedAt)) {
342
- updateData.endedAt = convergence.endedAt;
343
- }
344
-
345
- if (
346
- isUndefined(candidate.externalRecordingId) &&
347
- !isUndefined(convergence.externalRecordingId)
348
- ) {
349
- updateData.externalRecordingId = convergence.externalRecordingId;
350
- }
351
-
352
- return updateData;
353
- };
354
-
355
- type TerminalArtifactGateFailureUpdate = {
356
- status: CallRecordingStatus.FAILED;
357
- callRecorderFailureReason: string;
358
- };
359
-
360
- const buildTerminalArtifactGateFailureUpdate = ({
361
- candidate,
362
- convergence,
363
- externalRecordingId,
364
- updateData,
365
- }: {
366
- candidate: DivergedCallRecordingCandidate;
367
- convergence: RecallBotConvergence;
368
- externalRecordingId: string | undefined;
369
- updateData: CallRecordingUpdateFields;
370
- }): TerminalArtifactGateFailureUpdate | undefined => {
371
- if (
372
- candidate.status === CallRecordingStatus.COMPLETED ||
373
- updateData.status === CallRecordingStatus.FAILED ||
374
- !convergence.isRecallRecordingDone ||
375
- !isUndefined(externalRecordingId) ||
376
- hasRecordingArtifactPath({ candidate, updateData })
377
- ) {
378
- return undefined;
379
- }
380
-
381
- return {
382
- status: CallRecordingStatus.FAILED,
383
- callRecorderFailureReason:
384
- convergence.failureReason ?? 'recording_artifacts_unavailable',
385
- };
386
- };
387
-
388
- const hasRecordingArtifactPath = ({
389
- candidate,
390
- updateData,
391
- }: {
392
- candidate: DivergedCallRecordingCandidate;
393
- updateData: CallRecordingUpdateFields;
394
- }): boolean => {
395
- return (
396
- isNonEmptyArray(updateData.audio ?? candidate.audio) ||
397
- isNonEmptyArray(updateData.video ?? candidate.video) ||
398
- hasReachableTranscript(updateData.transcript ?? candidate.transcript)
399
- );
400
- };
401
-
402
- const hasReachableTranscript = (transcript: unknown): boolean => {
403
- if (isUndefined(transcript)) {
404
- return false;
405
- }
406
-
407
- const marker = parseTranscriptMarker(transcript);
408
-
409
- return isUndefined(marker) || marker.status === 'PENDING';
410
- };
411
-
412
- const markCallRecordingFailedAfterBotLoss = async ({
413
- client,
414
- candidate,
415
- externalBotId,
416
- result,
417
- }: {
418
- client: CoreApiClient;
419
- candidate: DivergedCallRecordingCandidate;
420
- externalBotId: string;
421
- result: ConvergeDivergedCallRecordingsResult;
422
- }): Promise<void> => {
423
- // externalBotId is kept for audit even though the bot is gone at Recall.
424
- console.warn(
425
- `[call-recorder] Recall bot ${externalBotId} for call recording ${candidate.id} no longer exists; it will not converge automatically`,
426
- );
427
-
428
- if (
429
- isCallRecordingStatusDowngrade({
430
- fromStatus: candidate.status,
431
- toStatus: CallRecordingStatus.FAILED,
432
- })
433
- ) {
434
- result.unconvergeableCallRecordingIds.push(candidate.id);
435
-
436
- return;
437
- }
438
-
439
- await updateCallRecording(client, {
440
- id: candidate.id,
441
- data: {
442
- status: CallRecordingStatus.FAILED,
443
- callRecorderFailureReason: 'recall_bot_not_found',
444
- },
445
- });
446
- result.markedFailedCallRecordingIds.push(candidate.id);
447
- };
@@ -1,67 +0,0 @@
1
- import { isUndefined } from '@sniptt/guards';
2
-
3
- import { retrieveRecallTranscript } from 'src/logic-functions/recall-api/retrieve-recall-transcript.util';
4
-
5
- const TRANSCRIPT_DOWNLOAD_TIMEOUT_MS = 20_000;
6
-
7
- export type DownloadTranscriptResult =
8
- | { outcome: 'filled'; content: unknown }
9
- | { outcome: 'failed'; subCode: string | null }
10
- | { outcome: 'pending' }
11
- | { outcome: 'error'; errorMessage: string };
12
-
13
- export const downloadTranscript = async ({
14
- transcriptId,
15
- }: {
16
- transcriptId: string;
17
- }): Promise<DownloadTranscriptResult> => {
18
- const retrieveResult = await retrieveRecallTranscript({ transcriptId });
19
-
20
- if (!retrieveResult.ok) {
21
- return { outcome: 'error', errorMessage: retrieveResult.errorMessage };
22
- }
23
-
24
- const { downloadUrl, statusCode, statusSubCode } = retrieveResult.transcript;
25
-
26
- if (!isUndefined(downloadUrl)) {
27
- return downloadTranscriptContent(downloadUrl);
28
- }
29
-
30
- if (statusCode === 'error' || statusCode === 'failed') {
31
- return { outcome: 'failed', subCode: statusSubCode ?? null };
32
- }
33
-
34
- return { outcome: 'pending' };
35
- };
36
-
37
- const downloadTranscriptContent = async (
38
- downloadUrl: string,
39
- ): Promise<DownloadTranscriptResult> => {
40
- try {
41
- const response = await fetch(downloadUrl, {
42
- signal: AbortSignal.timeout(TRANSCRIPT_DOWNLOAD_TIMEOUT_MS),
43
- });
44
-
45
- if (!response.ok) {
46
- console.warn(
47
- `[call-recorder] transcript download responded with HTTP ${response.status}`,
48
- );
49
-
50
- return {
51
- outcome: 'error',
52
- errorMessage: 'transcript download failed',
53
- };
54
- }
55
-
56
- return { outcome: 'filled', content: await response.json() };
57
- } catch (error) {
58
- console.warn(
59
- `[call-recorder] transcript download failed: ${error instanceof Error ? error.message : String(error)}`,
60
- );
61
-
62
- return {
63
- outcome: 'error',
64
- errorMessage: 'transcript download failed',
65
- };
66
- }
67
- };
@@ -1,73 +0,0 @@
1
- import { isUndefined } from '@sniptt/guards';
2
- import { type CoreApiClient } from 'twenty-client-sdk/core';
3
-
4
- import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status';
5
- import { type MeetingRecording } from 'src/logic-functions/types/meeting-recording.type';
6
- import { buildRecallRoutingMetadata } from 'src/logic-functions/domain/build-recall-routing-metadata.util';
7
- import { computeRecallBotJoinAt } from 'src/logic-functions/domain/compute-recall-bot-join-at.util';
8
- import { findCallRecordingsByIds } from 'src/logic-functions/data/find-call-recordings-by-ids.util';
9
- import { getCurrentWorkspaceId } from 'src/logic-functions/data/get-current-workspace-id.util';
10
- import { scheduleRecallBot } from 'src/logic-functions/recall-api/schedule-recall-bot.util';
11
- import { updateCallRecording } from 'src/logic-functions/data/update-call-recording.util';
12
-
13
- // The sole place a Recall bot is created. Only the deterministic-create winner and the stale-state cron call it, so one writer per meeting POSTs exactly one bot.
14
- export const ensureCallRecorder = async (
15
- client: CoreApiClient,
16
- { callRecording, calendarEvent }: MeetingRecording,
17
- ): Promise<boolean> => {
18
- const meetingUrl = calendarEvent.conferenceLinkUrl;
19
- const meetingStartsAt = calendarEvent.startsAt;
20
-
21
- if (isUndefined(meetingUrl) || isUndefined(meetingStartsAt)) {
22
- return false;
23
- }
24
-
25
- const joinAt = computeRecallBotJoinAt(meetingStartsAt);
26
-
27
- const freshCallRecording = (
28
- await findCallRecordingsByIds(client, [callRecording.id])
29
- )[0];
30
-
31
- if (
32
- isUndefined(freshCallRecording) ||
33
- freshCallRecording.recordingRequestStatus !==
34
- CallRecordingRequestStatus.REQUESTED ||
35
- !isUndefined(freshCallRecording.externalBotId)
36
- ) {
37
- return false;
38
- }
39
-
40
- const workspaceId = getCurrentWorkspaceId();
41
-
42
- if (isUndefined(workspaceId)) {
43
- console.error(
44
- `[call-recorder] cannot schedule Recall bot for callRecording ${callRecording.id}: workspace id unavailable, the shared webhook could not be routed back`,
45
- );
46
-
47
- return false;
48
- }
49
-
50
- const scheduleResult = await scheduleRecallBot({
51
- meetingUrl,
52
- joinAt,
53
- metadata: buildRecallRoutingMetadata({
54
- callRecordingId: callRecording.id,
55
- workspaceId,
56
- }),
57
- });
58
-
59
- if (!scheduleResult.ok) {
60
- console.warn(
61
- `[call-recorder] failed to schedule Recall bot for callRecording ${callRecording.id}: ${scheduleResult.errorMessage}`,
62
- );
63
-
64
- return false;
65
- }
66
-
67
- await updateCallRecording(client, {
68
- id: callRecording.id,
69
- data: { externalBotId: scheduleResult.externalBotId },
70
- });
71
-
72
- return true;
73
- };