@twentyhq/call-recorder 1.0.0
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.
- package/.env.example +5 -0
- package/.nvmrc +1 -0
- package/.oxlintrc.json +20 -0
- package/.yarnrc.yml +1 -0
- package/AGENTS.md +67 -0
- package/CLAUDE.md +67 -0
- package/README.md +24 -0
- package/SETUP.md +95 -0
- package/package.json +42 -0
- package/public/gallery/call-recorder-cover.png +0 -0
- package/public/logo.svg +5 -0
- package/src/__tests__/global-setup.ts +100 -0
- package/src/__tests__/schema.integration-test.ts +104 -0
- package/src/application-config.ts +96 -0
- package/src/constants/__tests__/call-recording-field-universal-identifiers.test.ts +19 -0
- package/src/constants/app-description.ts +2 -0
- package/src/constants/app-display-name.ts +1 -0
- package/src/constants/application-universal-identifier.ts +2 -0
- package/src/constants/calendar-event-reconciliation-logic-function-universal-identifier.ts +2 -0
- package/src/constants/calendar-event-record-page-layout-universal-identifier.ts +2 -0
- package/src/constants/calendar-event-recording-front-component-universal-identifier.ts +2 -0
- package/src/constants/calendar-event-recording-page-layout-tab-universal-identifier.ts +2 -0
- package/src/constants/calendar-event-recording-page-layout-widget-universal-identifier.ts +2 -0
- package/src/constants/call-recorder-everyone-left-timeout-seconds-app-variable-universal-identifier.ts +2 -0
- package/src/constants/call-recorder-failure-reason-on-call-recording-field-universal-identifier.ts +2 -0
- package/src/constants/call-recorder-join-early-minutes-app-variable-universal-identifier.ts +2 -0
- package/src/constants/call-recorder-name-app-variable-universal-identifier.ts +2 -0
- package/src/constants/call-recorder-noone-joined-timeout-seconds-app-variable-universal-identifier.ts +2 -0
- package/src/constants/call-recorder-preference-off-option-id.ts +2 -0
- package/src/constants/call-recorder-preference-on-calendar-event-field-universal-identifier.ts +2 -0
- package/src/constants/call-recorder-preference-on-calendar-event-view-field-universal-identifier.ts +2 -0
- package/src/constants/call-recorder-preference-on-option-id.ts +2 -0
- package/src/constants/call-recorder-preference.ts +4 -0
- package/src/constants/call-recorder-waiting-room-timeout-seconds-app-variable-universal-identifier.ts +2 -0
- package/src/constants/call-recording-audio-field-universal-identifier.ts +2 -0
- package/src/constants/call-recording-video-field-universal-identifier.ts +2 -0
- package/src/constants/default-role-universal-identifier.ts +2 -0
- package/src/constants/process-recall-webhook-logic-function-universal-identifier.ts +2 -0
- package/src/constants/recall-webhook-logic-function-universal-identifier.ts +2 -0
- package/src/constants/stale-bot-state-logic-function-universal-identifier.ts +2 -0
- package/src/default-role.ts +69 -0
- package/src/fields/call-recorder-failure-reason-on-call-recording.field.ts +22 -0
- package/src/fields/call-recorder-preference-on-calendar-event.field.ts +41 -0
- package/src/front-components/calendar-event-recording.front-component.tsx +13 -0
- package/src/front-components/components/CalendarEventRecording.tsx +39 -0
- package/src/front-components/components/CalendarEventRecordingBody.tsx +96 -0
- package/src/front-components/components/CalendarEventRecordingContent.tsx +111 -0
- package/src/front-components/components/RecordingTranscript.tsx +92 -0
- package/src/front-components/components/RecordingVideoPlayer.tsx +52 -0
- package/src/front-components/components/TranscriptEntryList.tsx +61 -0
- package/src/front-components/components/TranscriptEntryListItem.tsx +115 -0
- package/src/front-components/components/TranscriptErrorBox.tsx +48 -0
- package/src/front-components/components/TranscriptSpeakerAvatar.tsx +141 -0
- package/src/front-components/components/TranscriptSpeakerChip.tsx +51 -0
- package/src/front-components/constants/recording-theme-css-variables.ts +40 -0
- package/src/front-components/hooks/use-calendar-event-participants.ts +172 -0
- package/src/front-components/hooks/use-calendar-event-recording.ts +155 -0
- package/src/front-components/types/calendar-event-participant-by-speaker-name.type.ts +6 -0
- package/src/front-components/types/calendar-event-recording-participant.type.ts +7 -0
- package/src/front-components/types/transcript-entry.type.ts +13 -0
- package/src/front-components/utils/__tests__/find-active-transcript-entry-index.test.ts +66 -0
- package/src/front-components/utils/__tests__/format-transcript-timestamp.test.ts +29 -0
- package/src/front-components/utils/__tests__/get-speaker-name-match-keys.test.ts +22 -0
- package/src/front-components/utils/__tests__/parse-transcript-entries.test.ts +162 -0
- package/src/front-components/utils/build-calendar-event-participant-by-speaker-name.util.ts +45 -0
- package/src/front-components/utils/find-active-transcript-entry-index.util.ts +77 -0
- package/src/front-components/utils/format-transcript-timestamp.util.ts +16 -0
- package/src/front-components/utils/get-absolute-avatar-url.util.ts +48 -0
- package/src/front-components/utils/get-calendar-event-participant-for-speaker-name.util.ts +24 -0
- package/src/front-components/utils/get-speaker-name-match-keys.util.ts +64 -0
- package/src/front-components/utils/get-video-file-extension.util.ts +23 -0
- package/src/front-components/utils/parse-transcript-entries.util.ts +85 -0
- package/src/logic-functions/__tests__/process-recall-webhook.test.ts +62 -0
- package/src/logic-functions/__tests__/recall-webhook.test.ts +180 -0
- package/src/logic-functions/constants/call-recorder-everyone-left-timeout-seconds-env-var-name.ts +2 -0
- package/src/logic-functions/constants/call-recorder-everyone-left-timeout-seconds.ts +1 -0
- package/src/logic-functions/constants/call-recorder-join-early-minutes-env-var-name.ts +2 -0
- package/src/logic-functions/constants/call-recorder-name-env-var-name.ts +1 -0
- package/src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds-env-var-name.ts +2 -0
- package/src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds.ts +1 -0
- package/src/logic-functions/constants/call-recorder-recording-retention-hours-env-var-name.ts +2 -0
- package/src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds-env-var-name.ts +2 -0
- package/src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds.ts +1 -0
- package/src/logic-functions/constants/call-recording-micro-credits-per-hour.ts +1 -0
- package/src/logic-functions/constants/call-recording-request-status.ts +5 -0
- package/src/logic-functions/constants/call-recording-status.ts +9 -0
- package/src/logic-functions/constants/default-call-recorder-join-early-minutes.ts +1 -0
- package/src/logic-functions/constants/default-call-recorder-name.ts +1 -0
- package/src/logic-functions/constants/default-call-recorder-recording-retention-hours.ts +2 -0
- package/src/logic-functions/constants/default-recall-region.ts +1 -0
- package/src/logic-functions/constants/milliseconds-per-minute.ts +1 -0
- package/src/logic-functions/constants/non-terminal-call-recording-statuses.ts +8 -0
- package/src/logic-functions/constants/recall-api-key-env-var-name.ts +1 -0
- package/src/logic-functions/constants/recall-api-max-attempts.ts +1 -0
- package/src/logic-functions/constants/recall-api-retry-delay-ms.ts +1 -0
- package/src/logic-functions/constants/recall-bot-automatic-leave.ts +74 -0
- package/src/logic-functions/constants/recall-bot-everyone-left-min-activate-after-seconds.ts +1 -0
- package/src/logic-functions/constants/recall-bot-recording-config.ts +34 -0
- package/src/logic-functions/constants/recall-region-env-var-name.ts +1 -0
- package/src/logic-functions/constants/recall-webhook-secret-env-var-name.ts +1 -0
- package/src/logic-functions/constants/restricted-field-placeholder.ts +3 -0
- package/src/logic-functions/constants/stale-bot-state-cron-pattern.ts +1 -0
- package/src/logic-functions/constants/twenty-page-size.ts +1 -0
- package/src/logic-functions/data/__tests__/complete-call-recording-ingestion.test.ts +55 -0
- package/src/logic-functions/data/__tests__/fetch-all-nodes.test.ts +43 -0
- package/src/logic-functions/data/__tests__/get-current-workspace-id.test.ts +38 -0
- package/src/logic-functions/data/__tests__/strip-restricted-field-value.test.ts +22 -0
- package/src/logic-functions/data/complete-call-recording-ingestion.util.ts +24 -0
- package/src/logic-functions/data/create-call-recording.util.ts +41 -0
- package/src/logic-functions/data/fetch-all-nodes.util.ts +44 -0
- package/src/logic-functions/data/fetch-calendar-events-by-filter.util.ts +80 -0
- package/src/logic-functions/data/fetch-calendar-events-by-ids.util.ts +20 -0
- package/src/logic-functions/data/fetch-calendar-events-by-starts-at-values.util.ts +19 -0
- package/src/logic-functions/data/find-call-recordings-by-calendar-event-ids.util.ts +17 -0
- package/src/logic-functions/data/find-call-recordings-by-filter.util.ts +102 -0
- package/src/logic-functions/data/find-call-recordings-by-ids.util.ts +17 -0
- package/src/logic-functions/data/find-open-scheduled-call-recordings.util.ts +14 -0
- package/src/logic-functions/data/get-current-workspace-id.util.ts +36 -0
- package/src/logic-functions/data/strip-restricted-field-value.util.ts +6 -0
- package/src/logic-functions/data/update-call-recording.util.ts +24 -0
- package/src/logic-functions/domain/__tests__/build-call-recorder-policy-result.test.ts +47 -0
- package/src/logic-functions/domain/__tests__/compute-call-recording-charge.test.ts +71 -0
- package/src/logic-functions/domain/__tests__/compute-call-recording-id-for-meeting.test.ts +37 -0
- package/src/logic-functions/domain/__tests__/compute-real-meeting-key.test.ts +88 -0
- package/src/logic-functions/domain/__tests__/is-call-recording-ingestion-complete.test.ts +59 -0
- package/src/logic-functions/domain/__tests__/is-call-recording-status-downgrade.test.ts +37 -0
- package/src/logic-functions/domain/__tests__/resolve-call-recorder-policy-result.test.ts +120 -0
- package/src/logic-functions/domain/__tests__/should-complete-call-recording-ingestion.test.ts +102 -0
- package/src/logic-functions/domain/aggregate-call-recorder-policy-results-by-meeting.util.ts +42 -0
- package/src/logic-functions/domain/build-call-recorder-policy-result.util.ts +53 -0
- package/src/logic-functions/domain/build-failed-transcript-marker.util.ts +13 -0
- package/src/logic-functions/domain/build-pending-transcript-marker.util.ts +13 -0
- package/src/logic-functions/domain/build-recall-routing-metadata.util.ts +12 -0
- package/src/logic-functions/domain/build-transcript-failure-reason.util.ts +7 -0
- package/src/logic-functions/domain/compute-call-recording-charge.util.ts +41 -0
- package/src/logic-functions/domain/compute-call-recording-id-for-meeting.util.ts +16 -0
- package/src/logic-functions/domain/compute-real-meeting-key.util.ts +48 -0
- package/src/logic-functions/domain/compute-recall-bot-join-at.util.ts +34 -0
- package/src/logic-functions/domain/is-call-recording-ingestion-complete.util.ts +19 -0
- package/src/logic-functions/domain/is-call-recording-status-downgrade.util.ts +37 -0
- package/src/logic-functions/domain/is-recall-recording-done-signal.util.ts +13 -0
- package/src/logic-functions/domain/map-recall-status-code-to-call-recording-status.util.ts +26 -0
- package/src/logic-functions/domain/parse-transcript-marker.util.ts +29 -0
- package/src/logic-functions/domain/resolve-call-recorder-policy-result.util.ts +72 -0
- package/src/logic-functions/domain/should-complete-call-recording-ingestion.util.ts +32 -0
- package/src/logic-functions/flows/__tests__/charge-completed-call-recording.test.ts +45 -0
- package/src/logic-functions/flows/__tests__/complete-and-charge-call-recording.test.ts +61 -0
- package/src/logic-functions/flows/__tests__/converge-diverged-call-recordings.test.ts +727 -0
- package/src/logic-functions/flows/__tests__/download-transcript.test.ts +74 -0
- package/src/logic-functions/flows/__tests__/handle-recall-webhook.test.ts +1301 -0
- package/src/logic-functions/flows/__tests__/heal-call-recordings-missing-bot.test.ts +225 -0
- package/src/logic-functions/flows/__tests__/ingest-call-recording-media.test.ts +153 -0
- package/src/logic-functions/flows/__tests__/reap-orphaned-call-recorders.test.ts +425 -0
- package/src/logic-functions/flows/__tests__/reconcile-call-recorder.test.ts +1007 -0
- package/src/logic-functions/flows/cancel-call-recording-request.util.ts +46 -0
- package/src/logic-functions/flows/charge-completed-call-recording.util.ts +31 -0
- package/src/logic-functions/flows/complete-and-charge-call-recording.util.ts +29 -0
- package/src/logic-functions/flows/converge-diverged-call-recordings-result.type.ts +8 -0
- package/src/logic-functions/flows/converge-diverged-call-recordings.util.ts +447 -0
- package/src/logic-functions/flows/download-transcript.util.ts +67 -0
- package/src/logic-functions/flows/ensure-call-recorder.util.ts +73 -0
- package/src/logic-functions/flows/handle-recall-webhook.util.ts +672 -0
- package/src/logic-functions/flows/heal-call-recordings-missing-bot.util.ts +82 -0
- package/src/logic-functions/flows/ingest-call-recording-media.util.ts +128 -0
- package/src/logic-functions/flows/persist-call-recording-progress.util.ts +58 -0
- package/src/logic-functions/flows/reap-orphaned-call-recorders.util.ts +183 -0
- package/src/logic-functions/flows/reconcile-call-recorder.util.ts +495 -0
- package/src/logic-functions/flows/reconcile-call-recording-transcript-artifact-result.type.ts +11 -0
- package/src/logic-functions/flows/reconcile-call-recording-transcript-artifact.util.ts +182 -0
- package/src/logic-functions/flows/reschedule-call-recording-bot.util.ts +69 -0
- package/src/logic-functions/process-recall-webhook.ts +23 -0
- package/src/logic-functions/recall-api/__tests__/extract-recall-bot-convergence.test.ts +153 -0
- package/src/logic-functions/recall-api/__tests__/extract-recall-media-urls.test.ts +67 -0
- package/src/logic-functions/recall-api/__tests__/recall-bot-api.test.ts +744 -0
- package/src/logic-functions/recall-api/__tests__/verify-recall-webhook-signature.test.ts +122 -0
- package/src/logic-functions/recall-api/cancel-recall-bot.util.ts +28 -0
- package/src/logic-functions/recall-api/create-async-recall-transcript.util.ts +47 -0
- package/src/logic-functions/recall-api/eject-recall-bot.util.ts +28 -0
- package/src/logic-functions/recall-api/extract-recall-bot-convergence.util.ts +149 -0
- package/src/logic-functions/recall-api/extract-recall-bot-id.util.ts +10 -0
- package/src/logic-functions/recall-api/extract-recall-media-urls.util.ts +30 -0
- package/src/logic-functions/recall-api/extract-twenty-workspace-id-from-recall-webhook.util.ts +8 -0
- package/src/logic-functions/recall-api/get-recall-api-config.util.ts +59 -0
- package/src/logic-functions/recall-api/get-recall-bot.util.ts +42 -0
- package/src/logic-functions/recall-api/get-recall-recording.util.ts +31 -0
- package/src/logic-functions/recall-api/get-recall-webhook-bot-metadata.util.ts +18 -0
- package/src/logic-functions/recall-api/list-recall-transcripts.util.ts +141 -0
- package/src/logic-functions/recall-api/list-scheduled-recall-bots.util.ts +106 -0
- package/src/logic-functions/recall-api/normalize-recall-timestamp.util.ts +14 -0
- package/src/logic-functions/recall-api/parse-recall-webhook-event.util.ts +88 -0
- package/src/logic-functions/recall-api/recall-bot-api-request.util.ts +165 -0
- package/src/logic-functions/recall-api/recall-transcript-summary.type.ts +5 -0
- package/src/logic-functions/recall-api/reschedule-recall-bot.util.ts +56 -0
- package/src/logic-functions/recall-api/retrieve-recall-transcript.util.ts +71 -0
- package/src/logic-functions/recall-api/schedule-recall-bot.util.ts +68 -0
- package/src/logic-functions/recall-api/verify-recall-webhook-signature.util.ts +109 -0
- package/src/logic-functions/recall-webhook.ts +90 -0
- package/src/logic-functions/reconcile-call-recorder-calendar-event.ts +178 -0
- package/src/logic-functions/reconcile-stale-bot-state.ts +106 -0
- package/src/logic-functions/types/calendar-event-record.type.ts +5 -0
- package/src/logic-functions/types/call-recorder-policy-calendar-event-input.type.ts +10 -0
- package/src/logic-functions/types/call-recorder-policy-input.type.ts +9 -0
- package/src/logic-functions/types/call-recorder-policy-not-required-reason.type.ts +5 -0
- package/src/logic-functions/types/call-recorder-policy-required-reason.type.ts +1 -0
- package/src/logic-functions/types/call-recorder-policy-result-for-calendar-event.type.ts +9 -0
- package/src/logic-functions/types/call-recorder-policy-result-for-meeting.type.ts +6 -0
- package/src/logic-functions/types/call-recorder-policy-result.type.ts +12 -0
- package/src/logic-functions/types/call-recorder-reconciliation-result.type.ts +16 -0
- package/src/logic-functions/types/call-recording-media-file.type.ts +1 -0
- package/src/logic-functions/types/call-recording-record.type.ts +15 -0
- package/src/logic-functions/types/call-recording-update-fields.type.ts +20 -0
- package/src/logic-functions/types/files-field-value.type.ts +1 -0
- package/src/logic-functions/types/meeting-recording.type.ts +7 -0
- package/src/logic-functions/types/recall-bot-operation-result.type.ts +19 -0
- package/src/logic-functions/types/recall-routing-metadata.type.ts +4 -0
- package/src/logic-functions/types/removed-call-recorder-occurrence.type.ts +6 -0
- package/src/logic-functions/types/transcript-marker.type.ts +6 -0
- package/src/logic-functions/utils/as-record.util.ts +6 -0
- package/src/logic-functions/utils/get-application-variable-value.util.ts +3 -0
- package/src/logic-functions/utils/get-record-at-path.util.ts +10 -0
- package/src/logic-functions/utils/get-string.util.ts +4 -0
- package/src/logic-functions/utils/get-unique-sorted-ids.util.ts +8 -0
- package/src/logic-functions/utils/is-non-empty-string.util.ts +5 -0
- package/src/page-layouts/calendar-event-recording-tab.ts +33 -0
- package/src/view-fields/call-recorder-preference-on-calendar-event.view-field.ts +27 -0
- package/tsconfig.json +42 -0
- package/tsconfig.spec.json +9 -0
- package/vitest.config.ts +31 -0
- package/vitest.unit.config.ts +14 -0
package/.env.example
ADDED
package/.nvmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
24.5.0
|
package/.oxlintrc.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
|
3
|
+
"extends": ["../../.oxlintrc.base.json"],
|
|
4
|
+
"plugins": ["typescript"],
|
|
5
|
+
"categories": {
|
|
6
|
+
"correctness": "off"
|
|
7
|
+
},
|
|
8
|
+
"ignorePatterns": ["node_modules", "dist"],
|
|
9
|
+
"rules": {
|
|
10
|
+
"no-unused-vars": "off",
|
|
11
|
+
|
|
12
|
+
"typescript/no-unused-vars": [
|
|
13
|
+
"warn",
|
|
14
|
+
{
|
|
15
|
+
"argsIgnorePattern": "^_"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"typescript/no-explicit-any": "off"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/.yarnrc.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nodeLinker: node-modules
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
## Base documentation
|
|
2
|
+
|
|
3
|
+
- Getting started:
|
|
4
|
+
- https://docs.twenty.com/developers/extend/apps/getting-started/quick-start.md
|
|
5
|
+
- https://docs.twenty.com/developers/extend/apps/getting-started/concepts.md
|
|
6
|
+
- https://docs.twenty.com/developers/extend/apps/getting-started/project-structure.md
|
|
7
|
+
- https://docs.twenty.com/developers/extend/apps/getting-started/local-server.md
|
|
8
|
+
- https://docs.twenty.com/developers/extend/apps/getting-started/scaffolding.md
|
|
9
|
+
- https://docs.twenty.com/developers/extend/apps/getting-started/troubleshooting.md
|
|
10
|
+
- Config:
|
|
11
|
+
- https://docs.twenty.com/developers/extend/apps/config/overview.md
|
|
12
|
+
- https://docs.twenty.com/developers/extend/apps/config/application.md
|
|
13
|
+
- https://docs.twenty.com/developers/extend/apps/config/roles.md
|
|
14
|
+
- https://docs.twenty.com/developers/extend/apps/config/install-hooks.md
|
|
15
|
+
- https://docs.twenty.com/developers/extend/apps/config/public-assets.md
|
|
16
|
+
- Data:
|
|
17
|
+
- https://docs.twenty.com/developers/extend/apps/data/overview.md
|
|
18
|
+
- https://docs.twenty.com/developers/extend/apps/data/objects.md
|
|
19
|
+
- https://docs.twenty.com/developers/extend/apps/data/extending-objects.md
|
|
20
|
+
- https://docs.twenty.com/developers/extend/apps/data/relations.md
|
|
21
|
+
- Logic:
|
|
22
|
+
- https://docs.twenty.com/developers/extend/apps/logic/overview.md
|
|
23
|
+
- https://docs.twenty.com/developers/extend/apps/logic/logic-functions.md
|
|
24
|
+
- https://docs.twenty.com/developers/extend/apps/logic/skills-and-agents.md
|
|
25
|
+
- https://docs.twenty.com/developers/extend/apps/logic/connections.md
|
|
26
|
+
- Layout:
|
|
27
|
+
- https://docs.twenty.com/developers/extend/apps/layout/overview.md
|
|
28
|
+
- https://docs.twenty.com/developers/extend/apps/layout/views.md
|
|
29
|
+
- https://docs.twenty.com/developers/extend/apps/layout/navigation-menu-items.md
|
|
30
|
+
- https://docs.twenty.com/developers/extend/apps/layout/page-layouts.md
|
|
31
|
+
- https://docs.twenty.com/developers/extend/apps/layout/front-components.md
|
|
32
|
+
- https://docs.twenty.com/developers/extend/apps/layout/command-menu-items.md
|
|
33
|
+
- Operations:
|
|
34
|
+
- https://docs.twenty.com/developers/extend/apps/operations/overview.md
|
|
35
|
+
- https://docs.twenty.com/developers/extend/apps/operations/cli.md
|
|
36
|
+
- https://docs.twenty.com/developers/extend/apps/operations/testing.md
|
|
37
|
+
- https://docs.twenty.com/developers/extend/apps/operations/publishing.md
|
|
38
|
+
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
|
|
39
|
+
|
|
40
|
+
## UUID requirement
|
|
41
|
+
|
|
42
|
+
- All generated UUIDs must be valid UUID v4.
|
|
43
|
+
|
|
44
|
+
## Common Pitfalls
|
|
45
|
+
|
|
46
|
+
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
|
|
47
|
+
- Creating a view without a navigationMenuItem associated. This will make the view unavailable on the left sidebar.
|
|
48
|
+
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
|
|
49
|
+
|
|
50
|
+
## Best practice
|
|
51
|
+
|
|
52
|
+
It's highly recommended to create new app entities using `yarn twenty dev:add`. These are the options:
|
|
53
|
+
|
|
54
|
+
| Entity type | Command | Generated file |
|
|
55
|
+
| -------------------- | ---------------------------------------- | ------------------------------------- |
|
|
56
|
+
| Object | `yarn twenty dev:add object` | `src/objects/<name>.ts` |
|
|
57
|
+
| Field | `yarn twenty dev:add field` | `src/fields/<name>.ts` |
|
|
58
|
+
| Logic function | `yarn twenty dev:add logicFunction` | `src/logic-functions/<name>.ts` |
|
|
59
|
+
| Front component | `yarn twenty dev:add frontComponent` | `src/front-components/<name>.tsx` |
|
|
60
|
+
| Role | `yarn twenty dev:add role` | `src/roles/<name>.ts` |
|
|
61
|
+
| Skill | `yarn twenty dev:add skill` | `src/skills/<name>.ts` |
|
|
62
|
+
| Agent | `yarn twenty dev:add agent` | `src/agents/<name>.ts` |
|
|
63
|
+
| View | `yarn twenty dev:add view` | `src/views/<name>.ts` |
|
|
64
|
+
| Navigation menu item | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/<name>.ts` |
|
|
65
|
+
| Page layout | `yarn twenty dev:add pageLayout` | `src/page-layouts/<name>.ts` |
|
|
66
|
+
|
|
67
|
+
This helps automatically generate required IDs etc.
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
## Base documentation
|
|
2
|
+
|
|
3
|
+
- Getting started:
|
|
4
|
+
- https://docs.twenty.com/developers/extend/apps/getting-started/quick-start.md
|
|
5
|
+
- https://docs.twenty.com/developers/extend/apps/getting-started/concepts.md
|
|
6
|
+
- https://docs.twenty.com/developers/extend/apps/getting-started/project-structure.md
|
|
7
|
+
- https://docs.twenty.com/developers/extend/apps/getting-started/local-server.md
|
|
8
|
+
- https://docs.twenty.com/developers/extend/apps/getting-started/scaffolding.md
|
|
9
|
+
- https://docs.twenty.com/developers/extend/apps/getting-started/troubleshooting.md
|
|
10
|
+
- Config:
|
|
11
|
+
- https://docs.twenty.com/developers/extend/apps/config/overview.md
|
|
12
|
+
- https://docs.twenty.com/developers/extend/apps/config/application.md
|
|
13
|
+
- https://docs.twenty.com/developers/extend/apps/config/roles.md
|
|
14
|
+
- https://docs.twenty.com/developers/extend/apps/config/install-hooks.md
|
|
15
|
+
- https://docs.twenty.com/developers/extend/apps/config/public-assets.md
|
|
16
|
+
- Data:
|
|
17
|
+
- https://docs.twenty.com/developers/extend/apps/data/overview.md
|
|
18
|
+
- https://docs.twenty.com/developers/extend/apps/data/objects.md
|
|
19
|
+
- https://docs.twenty.com/developers/extend/apps/data/extending-objects.md
|
|
20
|
+
- https://docs.twenty.com/developers/extend/apps/data/relations.md
|
|
21
|
+
- Logic:
|
|
22
|
+
- https://docs.twenty.com/developers/extend/apps/logic/overview.md
|
|
23
|
+
- https://docs.twenty.com/developers/extend/apps/logic/logic-functions.md
|
|
24
|
+
- https://docs.twenty.com/developers/extend/apps/logic/skills-and-agents.md
|
|
25
|
+
- https://docs.twenty.com/developers/extend/apps/logic/connections.md
|
|
26
|
+
- Layout:
|
|
27
|
+
- https://docs.twenty.com/developers/extend/apps/layout/overview.md
|
|
28
|
+
- https://docs.twenty.com/developers/extend/apps/layout/views.md
|
|
29
|
+
- https://docs.twenty.com/developers/extend/apps/layout/navigation-menu-items.md
|
|
30
|
+
- https://docs.twenty.com/developers/extend/apps/layout/page-layouts.md
|
|
31
|
+
- https://docs.twenty.com/developers/extend/apps/layout/front-components.md
|
|
32
|
+
- https://docs.twenty.com/developers/extend/apps/layout/command-menu-items.md
|
|
33
|
+
- Operations:
|
|
34
|
+
- https://docs.twenty.com/developers/extend/apps/operations/overview.md
|
|
35
|
+
- https://docs.twenty.com/developers/extend/apps/operations/cli.md
|
|
36
|
+
- https://docs.twenty.com/developers/extend/apps/operations/testing.md
|
|
37
|
+
- https://docs.twenty.com/developers/extend/apps/operations/publishing.md
|
|
38
|
+
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
|
|
39
|
+
|
|
40
|
+
## UUID requirement
|
|
41
|
+
|
|
42
|
+
- All generated UUIDs must be valid UUID v4.
|
|
43
|
+
|
|
44
|
+
## Common Pitfalls
|
|
45
|
+
|
|
46
|
+
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
|
|
47
|
+
- Creating a view without a navigationMenuItem associated. This will make the view unavailable on the left sidebar.
|
|
48
|
+
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
|
|
49
|
+
|
|
50
|
+
## Best practice
|
|
51
|
+
|
|
52
|
+
It's highly recommended to create new app entities using `yarn twenty dev:add`. These are the options:
|
|
53
|
+
|
|
54
|
+
| Entity type | Command | Generated file |
|
|
55
|
+
| -------------------- | ---------------------------------------- | ------------------------------------- |
|
|
56
|
+
| Object | `yarn twenty dev:add object` | `src/objects/<name>.ts` |
|
|
57
|
+
| Field | `yarn twenty dev:add field` | `src/fields/<name>.ts` |
|
|
58
|
+
| Logic function | `yarn twenty dev:add logicFunction` | `src/logic-functions/<name>.ts` |
|
|
59
|
+
| Front component | `yarn twenty dev:add frontComponent` | `src/front-components/<name>.tsx` |
|
|
60
|
+
| Role | `yarn twenty dev:add role` | `src/roles/<name>.ts` |
|
|
61
|
+
| Skill | `yarn twenty dev:add skill` | `src/skills/<name>.ts` |
|
|
62
|
+
| Agent | `yarn twenty dev:add agent` | `src/agents/<name>.ts` |
|
|
63
|
+
| View | `yarn twenty dev:add view` | `src/views/<name>.ts` |
|
|
64
|
+
| Navigation menu item | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/<name>.ts` |
|
|
65
|
+
| Page layout | `yarn twenty dev:add pageLayout` | `src/page-layouts/<name>.ts` |
|
|
66
|
+
|
|
67
|
+
This helps automatically generate required IDs etc.
|
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Call Recorder
|
|
2
|
+
|
|
3
|
+
**Record, transcribe, and save every meeting — right inside your CRM.**
|
|
4
|
+
|
|
5
|
+
## ✨ What you get
|
|
6
|
+
|
|
7
|
+
- **Recordings on every meeting**
|
|
8
|
+
- **A Call Recording tab**
|
|
9
|
+
- **A per-meeting on/off switch**
|
|
10
|
+
- **Built for AI & automation**
|
|
11
|
+
|
|
12
|
+
## 💳 Billing
|
|
13
|
+
|
|
14
|
+
Metered in Twenty credits based on the bot's actual recording time, prorated by
|
|
15
|
+
duration — **$1.00 per recording-hour** (1 credit). No recording — opted out,
|
|
16
|
+
canceled, or no-show — means no charge.
|
|
17
|
+
|
|
18
|
+
## 📌 Heads up
|
|
19
|
+
|
|
20
|
+
- **Needs a synced calendar + video link** — ad-hoc calls that were never on
|
|
21
|
+
your Google, Outlook, or CalDAV calendar aren't recorded.
|
|
22
|
+
- **Your copy is yours** — Twenty stores its own video, audio, and transcript,
|
|
23
|
+
so they stay available after the source media expires.
|
|
24
|
+
|
package/SETUP.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Call Recorder — Self-hosting setup
|
|
2
|
+
|
|
3
|
+
This guide is for Twenty **server admins**. It covers connecting Call Recorder
|
|
4
|
+
to the recording service (Recall.ai), the environment variables it reads, and
|
|
5
|
+
how to wire up the webhook.
|
|
6
|
+
|
|
7
|
+
If you're on **Twenty Cloud**, these credentials may already be configured —
|
|
8
|
+
check with your workspace before setting them.
|
|
9
|
+
|
|
10
|
+
## What you need to wire up
|
|
11
|
+
|
|
12
|
+
Call Recorder talks to [Recall.ai](https://recall.ai) to send bots to meetings
|
|
13
|
+
and to receive recordings back. Two things must be configured:
|
|
14
|
+
|
|
15
|
+
1. A **Recall.ai API key** (and optionally a region), so the app can schedule,
|
|
16
|
+
update, and cancel bots.
|
|
17
|
+
2. A **webhook** from Recall.ai back to your deployment, so the app learns when
|
|
18
|
+
a recording is ready and can ingest it.
|
|
19
|
+
|
|
20
|
+
## Server variables
|
|
21
|
+
|
|
22
|
+
Set these on the application registration after installing
|
|
23
|
+
(**Settings → Applications → Call Recorder**):
|
|
24
|
+
|
|
25
|
+
| Server variable | Required | Purpose |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `RECALL_API_KEY` | Yes | Recall.ai API key for the configured region; used to schedule, update, and cancel bots. |
|
|
28
|
+
| `RECALL_REGION` | No | Recall.ai region for API requests. Defaults to `eu-central-1` (Europe / Frankfurt). |
|
|
29
|
+
| `CALL_RECORDER_RECORDING_RETENTION_HOURS` | No | How long Recall.ai retains the source media after processing. Defaults to `166` hours (6 days 22 hours), just under Recall's 168-hour free-storage window. Values above `168` may incur Recall storage charges. Twenty's ingested copy is unaffected. |
|
|
30
|
+
| `RECALL_WEBHOOK_SECRET` | Yes | Svix signing secret (`whsec_…`) used to verify incoming Recall webhooks. |
|
|
31
|
+
|
|
32
|
+
> **Bot behavior settings** (display name, join timing, lobby and leave
|
|
33
|
+
> timeouts) are **application variables** that a workspace admin tunes inside the
|
|
34
|
+
> app — not server variables. See **Customize the bot** in the
|
|
35
|
+
> [README](./README.md).
|
|
36
|
+
|
|
37
|
+
## Configuring the Recall webhook
|
|
38
|
+
|
|
39
|
+
The app exposes a server webhook route that verifies the Recall/Svix signature,
|
|
40
|
+
advances the matching recording's lifecycle status (`JOINING` → `RECORDING` →
|
|
41
|
+
`PROCESSING`, or `FAILED`), and — once the recording finishes — ingests the
|
|
42
|
+
audio, video, and transcript. It never moves a status backward, so out-of-order
|
|
43
|
+
or duplicate deliveries are safe, and it returns a non-2xx response on signature
|
|
44
|
+
failures so Recall retries.
|
|
45
|
+
|
|
46
|
+
Use this URL on your deployment, replacing only the host:
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
https://<your-twenty-host>/webhooks/server/9215afe6-1497-4149-a49d-e608e239bbaf
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The ID is the **Recall webhook logic function**.
|
|
53
|
+
|
|
54
|
+
1. In the Recall.ai dashboard, create a webhook endpoint pointing at your
|
|
55
|
+
deployment's webhook URL, subscribed to the **bot status-change**,
|
|
56
|
+
**recording**, and **transcript** events (`bot.status_change`,
|
|
57
|
+
`recording.done`, `recording.failed`, `transcript.done`,
|
|
58
|
+
`transcript.failed`). Status-change drives the lifecycle; the recording and
|
|
59
|
+
transcript events trigger media and transcript ingestion. Subscribing to
|
|
60
|
+
status changes alone leaves ingestion to the reconciliation backstop.
|
|
61
|
+
2. Copy the endpoint's signing secret — it starts with `whsec_`.
|
|
62
|
+
3. Set it as the `RECALL_WEBHOOK_SECRET` server variable on the **Call Recorder**
|
|
63
|
+
application registration.
|
|
64
|
+
4. Set `RECALL_API_KEY` (and optionally `RECALL_REGION`) the same way.
|
|
65
|
+
|
|
66
|
+
## Recording lifecycle
|
|
67
|
+
|
|
68
|
+
Each recording is stored as a **CallRecording** record and moves through a
|
|
69
|
+
lifecycle status: `SCHEDULED` → `JOINING` → `RECORDING` → `PROCESSING` →
|
|
70
|
+
`COMPLETED`, or `FAILED` (with a failure reason when one is available).
|
|
71
|
+
|
|
72
|
+
A recording reaches `COMPLETED` only once **both** its audio and video have been
|
|
73
|
+
ingested. Recall produces only the artifacts requested at bot creation (mixed
|
|
74
|
+
MP3 + MP4); if processing fails, the recording is marked `FAILED`.
|
|
75
|
+
|
|
76
|
+
Recall.ai retains the source media for a limited window (about seven days by
|
|
77
|
+
default) to stay inside its free-storage window. Twenty ingests and stores the
|
|
78
|
+
video, audio, and transcript in its own storage, so they remain available after
|
|
79
|
+
Recall's media expires.
|
|
80
|
+
|
|
81
|
+
A periodic reconciliation job runs as a safety net, pulling the latest status
|
|
82
|
+
from Recall and keeping recordings and bots in sync even when a real-time
|
|
83
|
+
webhook update is missed.
|
|
84
|
+
|
|
85
|
+
## Troubleshooting
|
|
86
|
+
|
|
87
|
+
| Symptom | Likely cause | Fix |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| No bot joined a meeting | **Recording** was off, the event had no conference link, it wasn't synced from a connected calendar, or `RECALL_API_KEY` isn't set | Confirm the event is on, upcoming, has a video link, and came from a synced calendar; confirm `RECALL_API_KEY` is set |
|
|
90
|
+
| Recording never reaches `COMPLETED` | A Recall webhook was missed, or only one of audio/video was produced | The reconciliation job pulls the latest status from Recall within a few minutes; if it is marked `FAILED`, inspect the bot in the Recall dashboard |
|
|
91
|
+
| Transcript empty, or marked pending/failed | Recall hasn't finished async transcription yet, or transcription failed for that call | Wait for the reconciliation job to ingest the transcript; a persistent failure leaves a marker in the transcript |
|
|
92
|
+
| Webhook rejected with `500` (`Invalid webhook signature`, Recall keeps retrying) | `RECALL_WEBHOOK_SECRET` doesn't match the Recall endpoint's signing secret | Re-copy the `whsec_…` secret from the Recall webhook endpoint into the `RECALL_WEBHOOK_SECRET` server variable |
|
|
93
|
+
| Webhook rejected with `500` (`RECALL_WEBHOOK_SECRET … not set`) | `RECALL_WEBHOOK_SECRET` is not set | Set it on the application registration |
|
|
94
|
+
| Bot left almost immediately | No one was admitted before the lobby / empty-meeting timeout, or everyone left | Adjust the lobby / empty-meeting timeouts in the app settings (see **Customize the bot** in the README) if they're too aggressive |
|
|
95
|
+
| Bot joined a meeting you didn't want recorded | Recording is on by default | Set the event's **Recording** field to Off; the scheduled bot is canceled |
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@twentyhq/call-recorder",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": "^24.5.0",
|
|
7
|
+
"npm": "please-use-yarn",
|
|
8
|
+
"yarn": ">=4.0.2"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"twenty-app"
|
|
12
|
+
],
|
|
13
|
+
"packageManager": "yarn@4.9.2",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"twenty": "twenty",
|
|
16
|
+
"lint": "oxlint -c .oxlintrc.json .",
|
|
17
|
+
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
|
|
18
|
+
"typecheck": "tsgo --noEmit -p tsconfig.spec.json",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:unit": "vitest run --config vitest.unit.config.ts",
|
|
21
|
+
"test:unit:watch": "vitest --config vitest.unit.config.ts",
|
|
22
|
+
"test:watch": "vitest"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@sniptt/guards": "^0.2.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@emotion/react": "^11.14.0",
|
|
29
|
+
"@emotion/styled": "^11.14.0",
|
|
30
|
+
"@types/node": "^24.7.2",
|
|
31
|
+
"@types/react": "^19.0.0",
|
|
32
|
+
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
|
|
33
|
+
"oxlint": "^0.16.0",
|
|
34
|
+
"react": "^19.0.0",
|
|
35
|
+
"react-dom": "^19.0.0",
|
|
36
|
+
"twenty-client-sdk": "2.16.0",
|
|
37
|
+
"twenty-sdk": "2.16.0",
|
|
38
|
+
"typescript": "^5.9.3",
|
|
39
|
+
"vite-tsconfig-paths": "^4.2.1",
|
|
40
|
+
"vitest": "^4.1.9"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
Binary file
|
package/public/logo.svg
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg width="136" height="136" viewBox="0 0 136 136" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<rect width="136" height="136" fill="#D9F3EE"/>
|
|
3
|
+
<path d="M75.9072 46.0928C76.7667 46.9522 77.2499 48.1176 77.25 49.333V86.667C77.2499 87.8824 76.7667 89.0478 75.9072 89.9072C75.0478 90.7667 73.8824 91.2499 72.667 91.25H35.333C34.1176 91.2499 32.9522 90.7667 32.0928 89.9072C31.2333 89.0478 30.7501 87.8825 30.75 86.667V50.2044C30.75 49.6487 30.5188 49.1181 30.1119 48.7397L22.9309 42.0629C22.2912 41.4681 21.25 41.9218 21.25 42.7953V86.667C21.2501 90.402 22.7339 93.9839 25.375 96.625C28.0161 99.2661 31.598 100.75 35.333 100.75H72.667C76.402 100.75 79.9839 99.2661 82.625 96.625C85.2661 93.9839 86.7499 90.402 86.75 86.667V49.333C86.7499 45.598 85.2661 42.0161 82.625 39.375C79.9839 36.7339 76.402 35.2501 72.667 35.25H27.157C26.2342 35.25 25.8038 36.3934 26.4977 37.0018L34.7671 44.2537C35.1319 44.5736 35.6005 44.75 36.0857 44.75H72.667C73.8825 44.7501 75.0478 45.2333 75.9072 46.0928Z" fill="#12A594"/>
|
|
4
|
+
<path d="M104.757 42.8125C106.259 42.8802 107.723 43.3068 109.024 44.0547L109.283 44.208L109.536 44.3701C110.704 45.1443 111.685 46.1699 112.406 47.3711L112.557 47.6309L112.699 47.8955C113.389 49.2291 113.749 50.7108 113.75 52.2149V83.7852C113.749 85.3894 113.338 86.9672 112.557 88.3682C111.775 89.7692 110.648 90.9477 109.283 91.791C107.918 92.6344 106.36 93.1143 104.757 93.1865C103.154 93.2587 101.559 92.9203 100.124 92.2031L92.1058 88.1953C91.4281 87.8566 91 87.164 91 86.4063V78.6384C91 77.8951 91.7823 77.4116 92.4472 77.744L104.25 83.6445V52.3535L92.4471 58.2532C91.7822 58.5856 91 58.1021 91 57.3588V49.5926C91 48.835 91.4281 48.1425 92.1057 47.8037L100.123 43.7959C101.558 43.0788 103.154 42.7404 104.757 42.8125Z" fill="#12A594"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
import { appDevOnce, appUninstall } from 'twenty-sdk/cli';
|
|
6
|
+
|
|
7
|
+
const APP_PATH = process.cwd();
|
|
8
|
+
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
|
|
9
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.test.json');
|
|
10
|
+
|
|
11
|
+
function validateEnv(): { apiUrl: string; apiKey: string } {
|
|
12
|
+
const apiUrl = process.env.TWENTY_API_URL;
|
|
13
|
+
const apiKey = process.env.TWENTY_API_KEY;
|
|
14
|
+
|
|
15
|
+
if (!apiUrl || !apiKey) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
|
|
18
|
+
'Start a local server: yarn twenty docker:start\n' +
|
|
19
|
+
'Or set them in vitest env config.',
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return { apiUrl, apiKey };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function checkServer(apiUrl: string) {
|
|
27
|
+
let response: Response;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
response = await fetch(`${apiUrl}/healthz`);
|
|
31
|
+
} catch {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Twenty server is not reachable at ${apiUrl}. ` +
|
|
34
|
+
'Make sure the server is running before executing integration tests.',
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error(`Server at ${apiUrl} returned ${response.status}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeConfig(apiUrl: string, apiKey: string) {
|
|
44
|
+
const payload = JSON.stringify(
|
|
45
|
+
{
|
|
46
|
+
remotes: {
|
|
47
|
+
local: { apiUrl, apiKey, accessToken: apiKey },
|
|
48
|
+
},
|
|
49
|
+
defaultRemote: 'local',
|
|
50
|
+
},
|
|
51
|
+
null,
|
|
52
|
+
2,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
56
|
+
fs.writeFileSync(CONFIG_PATH, payload);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function removeConfig() {
|
|
60
|
+
fs.rmSync(CONFIG_PATH, { force: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function setup() {
|
|
64
|
+
const { apiUrl, apiKey } = validateEnv();
|
|
65
|
+
|
|
66
|
+
await checkServer(apiUrl);
|
|
67
|
+
|
|
68
|
+
writeConfig(apiUrl, apiKey);
|
|
69
|
+
|
|
70
|
+
await appUninstall({ appPath: APP_PATH }).catch(() => {});
|
|
71
|
+
|
|
72
|
+
const result = await appDevOnce({
|
|
73
|
+
appPath: APP_PATH,
|
|
74
|
+
onProgress: (message: string) => console.log(`[dev] ${message}`),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!result.success) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Dev sync failed: ${result.error?.message ?? 'Unknown error'}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function teardown() {
|
|
85
|
+
try {
|
|
86
|
+
const uninstallResult = await appUninstall({ appPath: APP_PATH });
|
|
87
|
+
|
|
88
|
+
if (!uninstallResult.success) {
|
|
89
|
+
console.warn(
|
|
90
|
+
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.warn(
|
|
95
|
+
`App uninstall failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
96
|
+
);
|
|
97
|
+
} finally {
|
|
98
|
+
removeConfig();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { CoreApiClient } from 'twenty-client-sdk/core';
|
|
2
|
+
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/application-universal-identifier';
|
|
6
|
+
import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status';
|
|
7
|
+
import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status';
|
|
8
|
+
|
|
9
|
+
describe('App installation', () => {
|
|
10
|
+
it('should find the installed app in the applications list', async () => {
|
|
11
|
+
const client = new MetadataApiClient();
|
|
12
|
+
|
|
13
|
+
const result = await client.query({
|
|
14
|
+
findManyApplications: {
|
|
15
|
+
id: true,
|
|
16
|
+
name: true,
|
|
17
|
+
universalIdentifier: true,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const app = result.findManyApplications.find(
|
|
22
|
+
(a: { universalIdentifier: string }) =>
|
|
23
|
+
a.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
expect(app).toBeDefined();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('CallRecording status contract', () => {
|
|
31
|
+
it('accepts every app status supported by the current server and every request status value the app mirrors', async () => {
|
|
32
|
+
const client = new CoreApiClient();
|
|
33
|
+
const serverCallRecordingStatuses = await getServerCallRecordingStatuses();
|
|
34
|
+
|
|
35
|
+
const created = await client.mutation({
|
|
36
|
+
createCallRecording: {
|
|
37
|
+
__args: {
|
|
38
|
+
data: {
|
|
39
|
+
title: 'Integration test recording',
|
|
40
|
+
status: CallRecordingStatus.SCHEDULED,
|
|
41
|
+
recordingRequestStatus: CallRecordingRequestStatus.REQUESTED,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
id: true,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const callRecordingId = created.createCallRecording?.id;
|
|
49
|
+
|
|
50
|
+
expect(callRecordingId).toBeDefined();
|
|
51
|
+
|
|
52
|
+
if (callRecordingId === undefined) {
|
|
53
|
+
throw new Error('Expected call recording creation to return an id');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
expect(serverCallRecordingStatuses).toEqual(
|
|
57
|
+
expect.arrayContaining(Object.values(CallRecordingStatus)),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
for (const status of Object.values(CallRecordingStatus)) {
|
|
61
|
+
const updated = await client.mutation({
|
|
62
|
+
updateCallRecording: {
|
|
63
|
+
__args: { id: callRecordingId, data: { status } },
|
|
64
|
+
status: true,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(updated.updateCallRecording?.status).toBe(status);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const recordingRequestStatus of Object.values(
|
|
72
|
+
CallRecordingRequestStatus,
|
|
73
|
+
)) {
|
|
74
|
+
const updated = await client.mutation({
|
|
75
|
+
updateCallRecording: {
|
|
76
|
+
__args: { id: callRecordingId, data: { recordingRequestStatus } },
|
|
77
|
+
recordingRequestStatus: true,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(updated.updateCallRecording?.recordingRequestStatus).toBe(
|
|
82
|
+
recordingRequestStatus,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await client.mutation({
|
|
87
|
+
destroyCallRecording: {
|
|
88
|
+
__args: { id: callRecordingId },
|
|
89
|
+
id: true,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
type GeneratedCoreSchemaRuntime = {
|
|
96
|
+
enumCallRecordingStatusEnum: Record<string, string>;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const getServerCallRecordingStatuses = async (): Promise<string[]> => {
|
|
100
|
+
const generatedCoreSchema =
|
|
101
|
+
(await import('twenty-client-sdk/core')) as unknown as GeneratedCoreSchemaRuntime;
|
|
102
|
+
|
|
103
|
+
return Object.values(generatedCoreSchema.enumCallRecordingStatusEnum);
|
|
104
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { defineApplication } from 'twenty-sdk/define';
|
|
2
|
+
|
|
3
|
+
import { APP_DESCRIPTION } from 'src/constants/app-description';
|
|
4
|
+
import { APP_DISPLAY_NAME } from 'src/constants/app-display-name';
|
|
5
|
+
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/application-universal-identifier';
|
|
6
|
+
import { CALL_RECORDER_EVERYONE_LEFT_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/call-recorder-everyone-left-timeout-seconds-app-variable-universal-identifier';
|
|
7
|
+
import { CALL_RECORDER_JOIN_EARLY_MINUTES_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/call-recorder-join-early-minutes-app-variable-universal-identifier';
|
|
8
|
+
import { CALL_RECORDER_NAME_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/call-recorder-name-app-variable-universal-identifier';
|
|
9
|
+
import { CALL_RECORDER_NOONE_JOINED_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/call-recorder-noone-joined-timeout-seconds-app-variable-universal-identifier';
|
|
10
|
+
import { CALL_RECORDER_WAITING_ROOM_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/call-recorder-waiting-room-timeout-seconds-app-variable-universal-identifier';
|
|
11
|
+
import { CALL_RECORDER_EVERYONE_LEFT_TIMEOUT_SECONDS } from 'src/logic-functions/constants/call-recorder-everyone-left-timeout-seconds';
|
|
12
|
+
import { CALL_RECORDER_EVERYONE_LEFT_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-everyone-left-timeout-seconds-env-var-name';
|
|
13
|
+
import { CALL_RECORDER_JOIN_EARLY_MINUTES_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-join-early-minutes-env-var-name';
|
|
14
|
+
import { CALL_RECORDER_NAME_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-name-env-var-name';
|
|
15
|
+
import { CALL_RECORDER_NOONE_JOINED_TIMEOUT_SECONDS } from 'src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds';
|
|
16
|
+
import { CALL_RECORDER_NOONE_JOINED_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds-env-var-name';
|
|
17
|
+
import { CALL_RECORDER_RECORDING_RETENTION_HOURS_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-recording-retention-hours-env-var-name';
|
|
18
|
+
import { CALL_RECORDER_WAITING_ROOM_TIMEOUT_SECONDS } from 'src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds';
|
|
19
|
+
import { CALL_RECORDER_WAITING_ROOM_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds-env-var-name';
|
|
20
|
+
import { DEFAULT_CALL_RECORDER_JOIN_EARLY_MINUTES } from 'src/logic-functions/constants/default-call-recorder-join-early-minutes';
|
|
21
|
+
import { DEFAULT_CALL_RECORDER_NAME } from 'src/logic-functions/constants/default-call-recorder-name';
|
|
22
|
+
import { DEFAULT_CALL_RECORDER_RECORDING_RETENTION_HOURS } from 'src/logic-functions/constants/default-call-recorder-recording-retention-hours';
|
|
23
|
+
import { DEFAULT_RECALL_REGION } from 'src/logic-functions/constants/default-recall-region';
|
|
24
|
+
import { RECALL_API_KEY_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-api-key-env-var-name';
|
|
25
|
+
import { RECALL_REGION_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-region-env-var-name';
|
|
26
|
+
import { RECALL_WEBHOOK_SECRET_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-webhook-secret-env-var-name';
|
|
27
|
+
|
|
28
|
+
export default defineApplication({
|
|
29
|
+
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
|
30
|
+
displayName: APP_DISPLAY_NAME,
|
|
31
|
+
description: APP_DESCRIPTION,
|
|
32
|
+
logoUrl: 'public/logo.svg',
|
|
33
|
+
screenshots: ['public/gallery/call-recorder-cover.png'],
|
|
34
|
+
applicationVariables: {
|
|
35
|
+
[CALL_RECORDER_NAME_ENV_VAR_NAME]: {
|
|
36
|
+
universalIdentifier: CALL_RECORDER_NAME_APP_VARIABLE_UNIVERSAL_IDENTIFIER,
|
|
37
|
+
description: 'Display name the call recorder uses when it joins a call.',
|
|
38
|
+
isSecret: false,
|
|
39
|
+
value: DEFAULT_CALL_RECORDER_NAME,
|
|
40
|
+
},
|
|
41
|
+
[CALL_RECORDER_JOIN_EARLY_MINUTES_ENV_VAR_NAME]: {
|
|
42
|
+
universalIdentifier:
|
|
43
|
+
CALL_RECORDER_JOIN_EARLY_MINUTES_APP_VARIABLE_UNIVERSAL_IDENTIFIER,
|
|
44
|
+
description:
|
|
45
|
+
'How many minutes before the meeting start time the bot should join. Set to 0 to join at the scheduled start time.',
|
|
46
|
+
isSecret: false,
|
|
47
|
+
value: String(DEFAULT_CALL_RECORDER_JOIN_EARLY_MINUTES),
|
|
48
|
+
},
|
|
49
|
+
[CALL_RECORDER_WAITING_ROOM_TIMEOUT_SECONDS_ENV_VAR_NAME]: {
|
|
50
|
+
universalIdentifier:
|
|
51
|
+
CALL_RECORDER_WAITING_ROOM_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER,
|
|
52
|
+
description:
|
|
53
|
+
'How many seconds the bot waits in a meeting lobby before giving up and leaving.',
|
|
54
|
+
isSecret: false,
|
|
55
|
+
value: String(CALL_RECORDER_WAITING_ROOM_TIMEOUT_SECONDS),
|
|
56
|
+
},
|
|
57
|
+
[CALL_RECORDER_NOONE_JOINED_TIMEOUT_SECONDS_ENV_VAR_NAME]: {
|
|
58
|
+
universalIdentifier:
|
|
59
|
+
CALL_RECORDER_NOONE_JOINED_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER,
|
|
60
|
+
description:
|
|
61
|
+
'How many seconds the bot stays in an empty meeting when no one else ever joins.',
|
|
62
|
+
isSecret: false,
|
|
63
|
+
value: String(CALL_RECORDER_NOONE_JOINED_TIMEOUT_SECONDS),
|
|
64
|
+
},
|
|
65
|
+
[CALL_RECORDER_EVERYONE_LEFT_TIMEOUT_SECONDS_ENV_VAR_NAME]: {
|
|
66
|
+
universalIdentifier:
|
|
67
|
+
CALL_RECORDER_EVERYONE_LEFT_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER,
|
|
68
|
+
description:
|
|
69
|
+
'How many seconds the bot keeps recording after everyone else leaves the meeting.',
|
|
70
|
+
isSecret: false,
|
|
71
|
+
value: String(CALL_RECORDER_EVERYONE_LEFT_TIMEOUT_SECONDS),
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
serverVariables: {
|
|
75
|
+
[RECALL_API_KEY_ENV_VAR_NAME]: {
|
|
76
|
+
description:
|
|
77
|
+
'Recall.ai API key for the configured region. Set by the server admin on this registration after installation; used to create, update, and cancel scheduled recording bots.',
|
|
78
|
+
isSecret: true,
|
|
79
|
+
isRequired: true,
|
|
80
|
+
},
|
|
81
|
+
[RECALL_REGION_ENV_VAR_NAME]: {
|
|
82
|
+
description: `Recall.ai region used for API requests. Defaults to ${DEFAULT_RECALL_REGION} when unset. Europe Frankfurt is eu-central-1.`,
|
|
83
|
+
isSecret: false,
|
|
84
|
+
},
|
|
85
|
+
[CALL_RECORDER_RECORDING_RETENTION_HOURS_ENV_VAR_NAME]: {
|
|
86
|
+
description: `How many hours Recall.ai retains recording media after processing. Defaults to ${DEFAULT_CALL_RECORDER_RECORDING_RETENTION_HOURS} hours (6 days and 22 hours) to stay below Recall.ai's 7-day free storage window. Values above 168 hours may incur Recall.ai storage charges.`,
|
|
87
|
+
isSecret: false,
|
|
88
|
+
},
|
|
89
|
+
[RECALL_WEBHOOK_SECRET_ENV_VAR_NAME]: {
|
|
90
|
+
description:
|
|
91
|
+
'Recall.ai webhook signing secret (whsec_...). Set by the server admin from the Recall webhook endpoint settings; used to verify the Svix signature of incoming Recall webhook deliveries.',
|
|
92
|
+
isSecret: true,
|
|
93
|
+
isRequired: true,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|