Configure Recall bot server variables (#21774)
## What changed - Added server variables for Recall bot leave behavior - Added `RECALL_BOT_JOIN_EARLY_MINUTES` so the bot can join slightly before meeting start - Defaults stay aligned with Recall where applicable - Kept descriptions more human-friendly in app config + README <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21774?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
@@ -17,7 +17,9 @@ Run `yarn twenty help` to list all available commands.
|
||||
|
||||
## Recall.ai configuration
|
||||
|
||||
This app schedules Recall.ai meeting bots and ingests their lifecycle events. A server admin configures it through server variables on the application registration (Settings → Applications → Twenty Meeting Bot):
|
||||
This app schedules Recall.ai meeting bots and ingests their lifecycle events.
|
||||
|
||||
A server admin configures Recall credentials through server variables on the application registration (Settings → Applications → Twenty Meeting Bot):
|
||||
|
||||
| Server variable | Required | Purpose |
|
||||
| --- | --- | --- |
|
||||
@@ -25,6 +27,16 @@ This app schedules Recall.ai meeting bots and ingests their lifecycle events. A
|
||||
| `RECALL_REGION` | No | Recall.ai region for API requests. Defaults to `eu-central-1`. |
|
||||
| `RECALL_WEBHOOK_SECRET` | Yes | Svix signing secret (`whsec_…`) used to verify incoming Recall webhooks. |
|
||||
|
||||
A workspace admin can adjust bot behavior through application variables:
|
||||
|
||||
| Application variable | Default | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `RECALL_BOT_NAME` | `Twenty Meeting Bot` | Display name used when scheduling Recall.ai meeting bots. |
|
||||
| `RECALL_BOT_JOIN_EARLY_MINUTES` | `1` | How many minutes before the meeting start time the bot should join. Set to `0` to join at the scheduled start time. |
|
||||
| `RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS` | `1200` | How many seconds the bot waits in a meeting lobby before giving up and leaving. |
|
||||
| `RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS` | `1200` | How many seconds the bot stays in an empty meeting when no one else ever joins. |
|
||||
| `RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS` | `2` | How many seconds the bot keeps recording after everyone else leaves the meeting. |
|
||||
|
||||
### Configuring the webhook
|
||||
|
||||
The app exposes an unauthenticated route, `POST /webhook/recall`, that verifies the Recall/Svix signature and updates the matching `CallRecording`'s lifecycle status (`JOINING` → `RECORDING` → `PROCESSING`, or `FAILED_UNKNOWN`).
|
||||
|
||||
@@ -3,11 +3,23 @@ import { defineApplication } from 'twenty-sdk/define';
|
||||
import { APP_DESCRIPTION } from 'src/constants/app-description';
|
||||
import { APP_DISPLAY_NAME } from 'src/constants/app-display-name';
|
||||
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/application-universal-identifier';
|
||||
import { RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/recall-bot-everyone-left-timeout-seconds-app-variable-universal-identifier';
|
||||
import { RECALL_BOT_JOIN_EARLY_MINUTES_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/recall-bot-join-early-minutes-app-variable-universal-identifier';
|
||||
import { RECALL_BOT_NAME_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/recall-bot-name-app-variable-universal-identifier';
|
||||
import { RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/recall-bot-noone-joined-timeout-seconds-app-variable-universal-identifier';
|
||||
import { RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/recall-bot-waiting-room-timeout-seconds-app-variable-universal-identifier';
|
||||
import { DEFAULT_RECALL_BOT_JOIN_EARLY_MINUTES } from 'src/logic-functions/constants/default-recall-bot-join-early-minutes';
|
||||
import { DEFAULT_RECALL_BOT_NAME } from 'src/logic-functions/constants/default-recall-bot-name';
|
||||
import { DEFAULT_RECALL_REGION } from 'src/logic-functions/constants/default-recall-region';
|
||||
import { RECALL_API_KEY_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-api-key-env-var-name';
|
||||
import { RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS } from 'src/logic-functions/constants/recall-bot-everyone-left-timeout-seconds';
|
||||
import { RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-bot-everyone-left-timeout-seconds-env-var-name';
|
||||
import { RECALL_BOT_JOIN_EARLY_MINUTES_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-bot-join-early-minutes-env-var-name';
|
||||
import { RECALL_BOT_NAME_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-bot-name-env-var-name';
|
||||
import { RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS } from 'src/logic-functions/constants/recall-bot-noone-joined-timeout-seconds';
|
||||
import { RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-bot-noone-joined-timeout-seconds-env-var-name';
|
||||
import { RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS } from 'src/logic-functions/constants/recall-bot-waiting-room-timeout-seconds';
|
||||
import { RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-bot-waiting-room-timeout-seconds-env-var-name';
|
||||
import { RECALL_REGION_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-region-env-var-name';
|
||||
import { RECALL_WEBHOOK_SECRET_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-webhook-secret-env-var-name';
|
||||
|
||||
@@ -23,6 +35,38 @@ export default defineApplication({
|
||||
isSecret: false,
|
||||
value: DEFAULT_RECALL_BOT_NAME,
|
||||
},
|
||||
[RECALL_BOT_JOIN_EARLY_MINUTES_ENV_VAR_NAME]: {
|
||||
universalIdentifier:
|
||||
RECALL_BOT_JOIN_EARLY_MINUTES_APP_VARIABLE_UNIVERSAL_IDENTIFIER,
|
||||
description:
|
||||
'How many minutes before the meeting start time the bot should join. Set to 0 to join at the scheduled start time.',
|
||||
isSecret: false,
|
||||
value: String(DEFAULT_RECALL_BOT_JOIN_EARLY_MINUTES),
|
||||
},
|
||||
[RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS_ENV_VAR_NAME]: {
|
||||
universalIdentifier:
|
||||
RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER,
|
||||
description:
|
||||
'How many seconds the bot waits in a meeting lobby before giving up and leaving.',
|
||||
isSecret: false,
|
||||
value: String(RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS),
|
||||
},
|
||||
[RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS_ENV_VAR_NAME]: {
|
||||
universalIdentifier:
|
||||
RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER,
|
||||
description:
|
||||
'How many seconds the bot stays in an empty meeting when no one else ever joins.',
|
||||
isSecret: false,
|
||||
value: String(RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS),
|
||||
},
|
||||
[RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_ENV_VAR_NAME]: {
|
||||
universalIdentifier:
|
||||
RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER,
|
||||
description:
|
||||
'How many seconds the bot keeps recording after everyone else leaves the meeting.',
|
||||
isSecret: false,
|
||||
value: String(RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS),
|
||||
},
|
||||
},
|
||||
serverVariables: {
|
||||
[RECALL_API_KEY_ENV_VAR_NAME]: {
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER =
|
||||
'c866ddd4-fb7b-4cb4-8ad1-5599755e495c';
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const RECALL_BOT_JOIN_EARLY_MINUTES_APP_VARIABLE_UNIVERSAL_IDENTIFIER =
|
||||
'0568ebb2-3f64-47de-8c0d-d367dfbb7462';
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER =
|
||||
'241180e8-d864-4160-ad02-db44a9e8d395';
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER =
|
||||
'12e4e14d-d539-4d07-b477-4773539dd20b';
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const DEFAULT_RECALL_BOT_JOIN_EARLY_MINUTES = 1;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const MILLISECONDS_PER_MINUTE = 60_000;
|
||||
+71
-5
@@ -1,7 +1,73 @@
|
||||
import { RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS } from 'src/logic-functions/constants/recall-bot-noone-joined-timeout-seconds';
|
||||
import { RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS } from 'src/logic-functions/constants/recall-bot-waiting-room-timeout-seconds';
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
|
||||
export const RECALL_BOT_AUTOMATIC_LEAVE = {
|
||||
waiting_room_timeout: RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS,
|
||||
noone_joined_timeout: RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS,
|
||||
import { RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-bot-everyone-left-timeout-seconds-env-var-name';
|
||||
import { RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-bot-noone-joined-timeout-seconds-env-var-name';
|
||||
import { RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-bot-waiting-room-timeout-seconds-env-var-name';
|
||||
import { getApplicationVariableValue } from 'src/logic-functions/utils/get-application-variable-value.util';
|
||||
import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util';
|
||||
|
||||
type RecallBotAutomaticLeave = {
|
||||
waiting_room_timeout?: number;
|
||||
noone_joined_timeout?: number;
|
||||
everyone_left_timeout?: {
|
||||
timeout: number;
|
||||
activate_after: number;
|
||||
};
|
||||
};
|
||||
|
||||
export const getRecallBotAutomaticLeave = ():
|
||||
| RecallBotAutomaticLeave
|
||||
| undefined => {
|
||||
const waitingRoomTimeoutSeconds = getOptionalPositiveIntegerVariable(
|
||||
RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS_ENV_VAR_NAME,
|
||||
);
|
||||
const nooneJoinedTimeoutSeconds = getOptionalPositiveIntegerVariable(
|
||||
RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS_ENV_VAR_NAME,
|
||||
);
|
||||
const everyoneLeftTimeoutSeconds = getOptionalPositiveIntegerVariable(
|
||||
RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_ENV_VAR_NAME,
|
||||
);
|
||||
|
||||
const automaticLeave: RecallBotAutomaticLeave = {};
|
||||
|
||||
if (!isUndefined(waitingRoomTimeoutSeconds)) {
|
||||
automaticLeave.waiting_room_timeout = waitingRoomTimeoutSeconds;
|
||||
}
|
||||
|
||||
if (!isUndefined(nooneJoinedTimeoutSeconds)) {
|
||||
automaticLeave.noone_joined_timeout = nooneJoinedTimeoutSeconds;
|
||||
}
|
||||
|
||||
if (!isUndefined(everyoneLeftTimeoutSeconds)) {
|
||||
automaticLeave.everyone_left_timeout = {
|
||||
timeout: everyoneLeftTimeoutSeconds,
|
||||
activate_after: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return Object.keys(automaticLeave).length === 0 ? undefined : automaticLeave;
|
||||
};
|
||||
|
||||
const getOptionalPositiveIntegerVariable = (
|
||||
variableName: string,
|
||||
): number | undefined => {
|
||||
const rawValue = normalizeOptionalString(
|
||||
getApplicationVariableValue(variableName),
|
||||
);
|
||||
|
||||
if (isUndefined(rawValue)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const timeoutSeconds = Number(rawValue);
|
||||
|
||||
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return timeoutSeconds;
|
||||
};
|
||||
|
||||
const normalizeOptionalString = (
|
||||
value: string | undefined,
|
||||
): string | undefined => (isNonEmptyString(value) ? value.trim() : undefined);
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_ENV_VAR_NAME =
|
||||
'RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS';
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS = 2;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const RECALL_BOT_JOIN_EARLY_MINUTES_ENV_VAR_NAME =
|
||||
'RECALL_BOT_JOIN_EARLY_MINUTES';
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS_ENV_VAR_NAME =
|
||||
'RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS';
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS_ENV_VAR_NAME =
|
||||
'RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS';
|
||||
+1
-1
@@ -1 +1 @@
|
||||
export const STALE_BOT_STATE_CRON_PATTERN = '*/15 * * * *';
|
||||
export const STALE_BOT_STATE_CRON_PATTERN = '*/5 * * * *';
|
||||
|
||||
+1
-1
@@ -1,9 +1,9 @@
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
|
||||
import { CALL_RECORDING_MICRO_CREDITS_PER_HOUR } from 'src/logic-functions/constants/call-recording-micro-credits-per-hour';
|
||||
import { MILLISECONDS_PER_MINUTE } from 'src/logic-functions/constants/milliseconds-per-minute';
|
||||
|
||||
const MILLISECONDS_PER_HOUR = 3_600_000;
|
||||
const MILLISECONDS_PER_MINUTE = 60_000;
|
||||
|
||||
export type CallRecordingCharge = {
|
||||
creditsUsedMicro: number;
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { DEFAULT_RECALL_BOT_JOIN_EARLY_MINUTES } from 'src/logic-functions/constants/default-recall-bot-join-early-minutes';
|
||||
import { MILLISECONDS_PER_MINUTE } from 'src/logic-functions/constants/milliseconds-per-minute';
|
||||
import { RECALL_BOT_JOIN_EARLY_MINUTES_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-bot-join-early-minutes-env-var-name';
|
||||
import { getApplicationVariableValue } from 'src/logic-functions/utils/get-application-variable-value.util';
|
||||
import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util';
|
||||
|
||||
export const computeRecallBotJoinAt = (meetingStartsAt: string): string => {
|
||||
const meetingStartTimeInMilliseconds = new Date(meetingStartsAt).getTime();
|
||||
|
||||
if (Number.isNaN(meetingStartTimeInMilliseconds)) {
|
||||
return meetingStartsAt;
|
||||
}
|
||||
|
||||
return new Date(
|
||||
meetingStartTimeInMilliseconds -
|
||||
getRecallBotJoinEarlyMinutes() * MILLISECONDS_PER_MINUTE,
|
||||
).toISOString();
|
||||
};
|
||||
|
||||
const getRecallBotJoinEarlyMinutes = (): number => {
|
||||
const rawValue = getApplicationVariableValue(
|
||||
RECALL_BOT_JOIN_EARLY_MINUTES_ENV_VAR_NAME,
|
||||
);
|
||||
|
||||
if (!isNonEmptyString(rawValue)) {
|
||||
return DEFAULT_RECALL_BOT_JOIN_EARLY_MINUTES;
|
||||
}
|
||||
|
||||
const joinEarlyMinutes = Number(rawValue.trim());
|
||||
|
||||
return Number.isInteger(joinEarlyMinutes) && joinEarlyMinutes >= 0
|
||||
? joinEarlyMinutes
|
||||
: DEFAULT_RECALL_BOT_JOIN_EARLY_MINUTES;
|
||||
};
|
||||
+5
-3
@@ -22,6 +22,7 @@ vi.mock('src/logic-functions/recall-api/cancel-recall-bot.util', () => ({
|
||||
|
||||
const NOW = new Date('2026-01-01T12:00:00.000Z');
|
||||
const FUTURE_STARTS_AT = '2026-01-01T13:00:00.000Z';
|
||||
const FUTURE_RECALL_BOT_JOIN_AT = '2026-01-01T12:59:00.000Z';
|
||||
const FUTURE_ENDS_AT = '2026-01-01T14:00:00.000Z';
|
||||
|
||||
const buildCustomerSyncCallRecordingId = (
|
||||
@@ -252,7 +253,7 @@ describe('reconcileMeetingBotForCalendarEventIds', () => {
|
||||
]);
|
||||
expect(scheduleRecallBotMock).toHaveBeenCalledWith({
|
||||
meetingUrl: 'https://meet.example.com/customer-sync',
|
||||
joinAt: FUTURE_STARTS_AT,
|
||||
joinAt: FUTURE_RECALL_BOT_JOIN_AT,
|
||||
metadata: {
|
||||
twentyCallRecordingId: buildCustomerSyncCallRecordingId(),
|
||||
twentyCalendarEventId: 'calendar-event-1',
|
||||
@@ -402,7 +403,7 @@ describe('reconcileMeetingBotForCalendarEventIds', () => {
|
||||
expect(rescheduleRecallBotMock).toHaveBeenCalledWith({
|
||||
externalBotId: 'recall-bot-1',
|
||||
meetingUrl: 'https://meet.example.com/customer-sync',
|
||||
joinAt: FUTURE_STARTS_AT,
|
||||
joinAt: FUTURE_RECALL_BOT_JOIN_AT,
|
||||
metadata: {
|
||||
twentyCallRecordingId: buildCustomerSyncCallRecordingId(),
|
||||
twentyCalendarEventId: 'calendar-event-1',
|
||||
@@ -657,6 +658,7 @@ describe('reconcileMeetingBotForCalendarEventIds', () => {
|
||||
|
||||
it('cancels the old occurrence and creates a fresh recording when the meeting moves to a new time', async () => {
|
||||
const NEW_STARTS_AT = '2026-01-02T13:00:00.000Z';
|
||||
const NEW_RECALL_BOT_JOIN_AT = '2026-01-02T12:59:00.000Z';
|
||||
const NEW_ENDS_AT = '2026-01-02T14:00:00.000Z';
|
||||
const client = buildFakeCoreApiClient({
|
||||
calendarEvents: [
|
||||
@@ -706,7 +708,7 @@ describe('reconcileMeetingBotForCalendarEventIds', () => {
|
||||
externalBotId: 'recall-bot-old',
|
||||
});
|
||||
expect(scheduleRecallBotMock).toHaveBeenCalledExactlyOnceWith(
|
||||
expect.objectContaining({ joinAt: NEW_STARTS_AT }),
|
||||
expect.objectContaining({ joinAt: NEW_RECALL_BOT_JOIN_AT }),
|
||||
);
|
||||
expect(client.callRecordings).toEqual([
|
||||
expect.objectContaining({
|
||||
|
||||
+5
-2
@@ -4,6 +4,7 @@ import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status';
|
||||
import { type MeetingRecording } from 'src/logic-functions/types/meeting-recording.type';
|
||||
import { buildRecallBotMetadata } from 'src/logic-functions/domain/build-recall-bot-metadata.util';
|
||||
import { computeRecallBotJoinAt } from 'src/logic-functions/domain/compute-recall-bot-join-at.util';
|
||||
import { findCallRecordingsByIds } from 'src/logic-functions/data/find-call-recordings-by-ids.util';
|
||||
import { scheduleRecallBot } from 'src/logic-functions/recall-api/schedule-recall-bot.util';
|
||||
import { updateCallRecording } from 'src/logic-functions/data/update-call-recording.util';
|
||||
@@ -14,12 +15,14 @@ export const ensureMeetingBot = async (
|
||||
{ callRecording, calendarEvent }: MeetingRecording,
|
||||
): Promise<boolean> => {
|
||||
const meetingUrl = calendarEvent.conferenceLinkUrl;
|
||||
const joinAt = calendarEvent.startsAt;
|
||||
const meetingStartsAt = calendarEvent.startsAt;
|
||||
|
||||
if (isUndefined(meetingUrl) || isUndefined(joinAt)) {
|
||||
if (isUndefined(meetingUrl) || isUndefined(meetingStartsAt)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const joinAt = computeRecallBotJoinAt(meetingStartsAt);
|
||||
|
||||
const freshCallRecording = (
|
||||
await findCallRecordingsByIds(client, [callRecording.id])
|
||||
)[0];
|
||||
|
||||
+5
-2
@@ -3,6 +3,7 @@ import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
import { type MeetingRecording } from 'src/logic-functions/types/meeting-recording.type';
|
||||
import { buildRecallBotMetadata } from 'src/logic-functions/domain/build-recall-bot-metadata.util';
|
||||
import { computeRecallBotJoinAt } from 'src/logic-functions/domain/compute-recall-bot-join-at.util';
|
||||
import { rescheduleRecallBot } from 'src/logic-functions/recall-api/reschedule-recall-bot.util';
|
||||
import { updateCallRecording } from 'src/logic-functions/data/update-call-recording.util';
|
||||
|
||||
@@ -19,12 +20,14 @@ export const rescheduleCallRecordingBot = async (
|
||||
}
|
||||
|
||||
const meetingUrl = calendarEvent.conferenceLinkUrl;
|
||||
const joinAt = calendarEvent.startsAt;
|
||||
const meetingStartsAt = calendarEvent.startsAt;
|
||||
|
||||
if (isUndefined(meetingUrl) || isUndefined(joinAt)) {
|
||||
if (isUndefined(meetingUrl) || isUndefined(meetingStartsAt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const joinAt = computeRecallBotJoinAt(meetingStartsAt);
|
||||
|
||||
const rescheduleResult = await rescheduleRecallBot({
|
||||
externalBotId,
|
||||
meetingUrl,
|
||||
|
||||
-31
@@ -63,10 +63,6 @@ describe('recall bot api', () => {
|
||||
meeting_url: 'https://meet.google.com/abc-defg-hij',
|
||||
join_at: '2026-01-01T13:00:00.000Z',
|
||||
bot_name: 'Twenty Meeting Bot',
|
||||
automatic_leave: {
|
||||
waiting_room_timeout: 1200,
|
||||
noone_joined_timeout: 1200,
|
||||
},
|
||||
recording_config: {
|
||||
video_mixed_mp4: {},
|
||||
audio_mixed_mp3: {},
|
||||
@@ -79,33 +75,6 @@ describe('recall bot api', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('carries the automatic leave config when rescheduling a bot', async () => {
|
||||
const result = await rescheduleRecallBot({
|
||||
externalBotId: 'recall-bot-id',
|
||||
meetingUrl: 'https://meet.google.com/abc-defg-hij',
|
||||
joinAt: '2026-01-02T13:00:00.000Z',
|
||||
metadata: {
|
||||
twentyCallRecordingId: 'call-recording-id',
|
||||
twentyCalendarEventId: 'calendar-event-id',
|
||||
twentyRealMeetingKey: 'meeting-key',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ok: true, externalBotId: 'recall-bot-id' });
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://ap-northeast-1.recall.ai/api/v1/bot/recall-bot-id/',
|
||||
expect.objectContaining({ method: 'PATCH' }),
|
||||
);
|
||||
expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual(
|
||||
expect.objectContaining({
|
||||
automatic_leave: {
|
||||
waiting_room_timeout: 1200,
|
||||
noone_joined_timeout: 1200,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('fails when the create response does not include a bot id', async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
|
||||
+6
-2
@@ -1,4 +1,6 @@
|
||||
import { RECALL_BOT_AUTOMATIC_LEAVE } from 'src/logic-functions/constants/recall-bot-automatic-leave';
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
|
||||
import { getRecallBotAutomaticLeave } from 'src/logic-functions/constants/recall-bot-automatic-leave';
|
||||
import { RECALL_BOT_RECORDING_CONFIG } from 'src/logic-functions/constants/recall-bot-recording-config';
|
||||
import { type RecallBotScheduleResult } from 'src/logic-functions/types/recall-bot-operation-result.type';
|
||||
import {
|
||||
@@ -25,6 +27,8 @@ export const rescheduleRecallBot = async ({
|
||||
return { ok: false, status: null, errorMessage: configResult.error };
|
||||
}
|
||||
|
||||
const automaticLeave = getRecallBotAutomaticLeave();
|
||||
|
||||
const result = await recallBotApiRequest<RecallBotResponse>({
|
||||
config: configResult.config,
|
||||
path: `/bot/${externalBotId}/`,
|
||||
@@ -33,7 +37,7 @@ export const rescheduleRecallBot = async ({
|
||||
meeting_url: meetingUrl,
|
||||
join_at: joinAt,
|
||||
bot_name: configResult.config.botName,
|
||||
automatic_leave: RECALL_BOT_AUTOMATIC_LEAVE,
|
||||
...(isUndefined(automaticLeave) ? {} : { automatic_leave: automaticLeave }),
|
||||
recording_config: RECALL_BOT_RECORDING_CONFIG,
|
||||
metadata,
|
||||
},
|
||||
|
||||
+4
-2
@@ -1,6 +1,6 @@
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
|
||||
import { RECALL_BOT_AUTOMATIC_LEAVE } from 'src/logic-functions/constants/recall-bot-automatic-leave';
|
||||
import { getRecallBotAutomaticLeave } from 'src/logic-functions/constants/recall-bot-automatic-leave';
|
||||
import { RECALL_BOT_RECORDING_CONFIG } from 'src/logic-functions/constants/recall-bot-recording-config';
|
||||
import { type RecallBotMetadata } from 'src/logic-functions/types/recall-bot-metadata.type';
|
||||
import { type RecallBotScheduleResult } from 'src/logic-functions/types/recall-bot-operation-result.type';
|
||||
@@ -28,6 +28,8 @@ export const scheduleRecallBot = async ({
|
||||
return { ok: false, status: null, errorMessage: configResult.error };
|
||||
}
|
||||
|
||||
const automaticLeave = getRecallBotAutomaticLeave();
|
||||
|
||||
const result = await recallBotApiRequest<RecallBotResponse>({
|
||||
config: configResult.config,
|
||||
path: '/bot/',
|
||||
@@ -36,7 +38,7 @@ export const scheduleRecallBot = async ({
|
||||
meeting_url: meetingUrl,
|
||||
join_at: joinAt,
|
||||
bot_name: configResult.config.botName,
|
||||
automatic_leave: RECALL_BOT_AUTOMATIC_LEAVE,
|
||||
...(isUndefined(automaticLeave) ? {} : { automatic_leave: automaticLeave }),
|
||||
recording_config: RECALL_BOT_RECORDING_CONFIG,
|
||||
metadata,
|
||||
},
|
||||
|
||||
+1
-1
@@ -122,7 +122,7 @@ export default defineLogicFunction({
|
||||
name: 'reconcile-stale-bot-state',
|
||||
description:
|
||||
'Converges call recordings with Recall on a schedule: pulls stale bot statuses and overdue transcripts, finishes failed cancellations, schedules bots for recordings still missing one, and reaps unclaimed bots. Reads calendar events only to heal already-decided recordings, never to discover meetings.',
|
||||
timeoutSeconds: 300,
|
||||
timeoutSeconds: 250,
|
||||
handler: reconcileStaleBotStateHandler,
|
||||
cronTriggerSettings: {
|
||||
pattern: STALE_BOT_STATE_CRON_PATTERN,
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
// Application variables are injected into process.env on every execution.
|
||||
// Application and server variables are injected into process.env on every execution.
|
||||
export const getApplicationVariableValue = (key: string): string | undefined =>
|
||||
process.env[key];
|
||||
|
||||
Reference in New Issue
Block a user