Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: automatic no show #16727

Merged
merged 53 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
08e5ff9
chore: save progress
Udit-takkar Sep 19, 2024
529c8ef
feat: add webhook and trigger
Udit-takkar Sep 20, 2024
e4ab4e0
feat: add logic to trigger no show
Udit-takkar Sep 23, 2024
7e35fca
fix: update code
Udit-takkar Sep 24, 2024
32f3a5e
chore: rename and type error
Udit-takkar Sep 24, 2024
2f68105
chore: err
Udit-takkar Sep 24, 2024
03c9c8c
chore: type err
Udit-takkar Sep 24, 2024
b8d0015
fix: type errors
Udit-takkar Sep 24, 2024
83c4490
fix: types
Udit-takkar Sep 25, 2024
099650e
chore: use number type
Udit-takkar Sep 25, 2024
0093e11
test: add units for triggerHostNoShow
Udit-takkar Sep 25, 2024
2ccc2f3
refactor: improve code
Udit-takkar Sep 25, 2024
5073ff7
fix: update test after refactor
Udit-takkar Sep 25, 2024
5f2d5a8
Merge branch 'main' into feat/automatic-no-show
Udit-takkar Sep 27, 2024
e684fd8
chore: feedback
Udit-takkar Sep 27, 2024
9f36c08
refactor: use scheduled webhook trigger events
Udit-takkar Sep 27, 2024
6fb3e10
fix: type check
Udit-takkar Sep 27, 2024
c0210f1
chore: update test
Udit-takkar Sep 27, 2024
6ecafb6
chore: update tests
Udit-takkar Sep 27, 2024
9bc36c9
fix: type err
Udit-takkar Sep 27, 2024
1e8ffb9
fix: missing prop
Udit-takkar Sep 27, 2024
0b26b11
chore: missing user prop
Udit-takkar Sep 27, 2024
9e72d6d
chore
Udit-takkar Sep 27, 2024
7c0800e
Merge branch 'main' into feat/automatic-no-show
Udit-takkar Sep 30, 2024
c49b147
refactor: also update booking
Udit-takkar Sep 30, 2024
14a9d5d
Merge branch 'main' into feat/automatic-no-show
emrysal Oct 8, 2024
8d09596
Remove eventType.id from getBooking (we already have eventTypeId)
emrysal Oct 8, 2024
7c9391b
Update packages/features/tasker/tasks/triggerNoShow/common.ts
PeerRich Oct 8, 2024
cb320f1
fix setting time and timeUnit value
Oct 8, 2024
f660095
Merge branch 'main' into feat/automatic-no-show
CarinaWolli Oct 8, 2024
25db972
Adding back ' ' to remove change
emrysal Oct 8, 2024
cb0f050
delete webhookScheduledTriggers after webhook triggered
Oct 8, 2024
a2bd361
Merge branch 'main' into feat/automatic-no-show
zomars Oct 8, 2024
58e9f56
Update packages/features/ee/workflows/components/WorkflowStepContaine…
zomars Oct 8, 2024
61412ed
fix zod error
Oct 8, 2024
1aed734
fix deleting webhookScheduledTriggers
Oct 8, 2024
ae2ed1e
Merge branch 'main' into feat/automatic-no-show
emrysal Oct 9, 2024
f1384e5
perf: Query optimisation + add index on 'Webhook.active'
emrysal Oct 9, 2024
4a9dd58
Merge branch 'main' into feat/automatic-no-show
zomars Oct 9, 2024
a321105
Update WorkflowStepContainer.tsx
zomars Oct 9, 2024
7e1e89a
tasker improvements
zomars Oct 10, 2024
db38447
Cleanup
zomars Oct 10, 2024
dd43a52
Discard changes to packages/embeds/embed-core/src/embed.test.ts
zomars Oct 10, 2024
6c1d987
Update triggerHostNoShow.test.ts
zomars Oct 10, 2024
eeb59be
Discard changes to packages/features/webhooks/lib/handleWebhookSchedu…
zomars Oct 10, 2024
40c31e8
Update sendPayload.ts
zomars Oct 10, 2024
8e5eb7d
Update getWebhooks.ts
zomars Oct 10, 2024
ccfae87
Update getWebhooks.ts
zomars Oct 10, 2024
1d1577b
test fixes
zomars Oct 10, 2024
56fdc97
Merge branch 'main' into feat/automatic-no-show
Udit-takkar Oct 10, 2024
c7dc695
fix: add missing migration
Udit-takkar Oct 10, 2024
e74154f
fix: user id
Udit-takkar Oct 10, 2024
b9f3774
Merge branch 'main' into feat/automatic-no-show
zomars Oct 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions apps/web/cron-tester.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { CronJob } from "cron";

async function fetchCron(endpoint: string) {
const apiKey = process.env.CRON_API_KEY;

const res = await fetch(`http://localhost:3000/api${endpoint}?${apiKey}`, {
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${process.env.CRON_SECRET}`,
},
});
const json = await res.json();
console.log(endpoint, json);
}

try {
console.log("⏳ Running cron endpoints");
new CronJob(
// Each 5 seconds
"*/5 * * * * *",
async function () {
await Promise.allSettled([
fetchCron("/tasks/cron"),
// fetchCron("/cron/calVideoNoShowWebhookTriggers"),
//
// fetchCron("/tasks/cleanup"),
]);
},
null,
true,
"America/Los_Angeles"
);
} catch (_err) {
console.error("❌ ❌ ❌ Something went wrong ❌ ❌ ❌");
process.exit(1);
}
12 changes: 7 additions & 5 deletions apps/web/lib/video/[uid]/getServerSideProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { GetServerSidePropsContext } from "next";

import {
generateGuestMeetingTokenFromOwnerMeetingToken,
setEnableRecordingUIForOrganizer,
setEnableRecordingUIAndUserIdForOrganizer,
} from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getCalVideoReference } from "@calcom/features/get-cal-video-reference";
Expand Down Expand Up @@ -81,18 +81,20 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
// set meetingPassword for guests
if (session?.user.id !== bookingObj.user?.id) {
const guestMeetingPassword = await generateGuestMeetingTokenFromOwnerMeetingToken(
oldVideoReference.meetingPassword
oldVideoReference.meetingPassword,
session?.user.id
);

bookingObj.references.forEach((bookRef) => {
bookRef.meetingPassword = guestMeetingPassword;
});
}
// Only for backward compatibility for organizer
// Only for backward compatibility and setting user id in particpants for organizer
else {
const meetingPassword = await setEnableRecordingUIForOrganizer(
const meetingPassword = await setEnableRecordingUIAndUserIdForOrganizer(
oldVideoReference.id,
oldVideoReference.meetingPassword
oldVideoReference.meetingPassword,
session?.user.id
);
if (!!meetingPassword) {
bookingObj.references.forEach((bookRef) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ export default function EditOAuthClientWebhooks() {
value: WebhookTriggerEvents.RECORDING_TRANSCRIPTION_GENERATED,
label: "recording_transcription_generated",
},
{
value: WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
label: "after_hosts_cal_video_no_show",
},
{
value: WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
label: "after_guests_cal_video_no_show",
},
]}
onSubmit={async (data) => {
try {
Expand Down
2 changes: 2 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"analyze:browser": "BUNDLE_ANALYZE=browser next build",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
"dev": "next dev",
"dev:cron": "ts-node cron-tester.ts",
"dev-https": "NODE_TLS_REJECT_UNAUTHORIZED=0 next dev --experimental-https",
"dx": "yarn dev",
"test-codegen": "yarn playwright codegen http://localhost:3000",
Expand Down Expand Up @@ -173,6 +174,7 @@
"@types/uuid": "8.3.1",
"autoprefixer": "^10.4.12",
"copy-webpack-plugin": "^11.0.0",
"cron": "^3.1.7",
"deasync": "^0.1.30",
"detect-port": "^1.3.0",
"env-cmd": "^10.1.0",
Expand Down
5 changes: 5 additions & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,8 @@
"number_provided": "Phone number will be provided",
"before_event_trigger": "before event starts",
"event_cancelled_trigger": "when event is canceled",
"after_hosts_cal_video_no_show": "After hosts don't join cal video",
"after_guests_cal_video_no_show": "After guests don't join cal video",
"new_event_trigger": "when new event is booked",
"email_host_action": "send email to host",
"email_attendee_action": "send email to attendees",
Expand Down Expand Up @@ -1643,6 +1645,9 @@
"email_address_action": "send email to a specific email address",
"after_event_trigger": "after event ends",
"how_long_after": "How long after event ends?",
"how_long_after_hosts_no_show": "How long after hosts don't show up on cal video meeting?",
"how_long_after_guests_no_show": "How long after guests don't show up on cal video meeting?",
"how_long_after_user_no_show_minutes": "How long after the users don't show up on cal video meeting?",
"no_available_slots": "No Available slots",
"time_available": "Time available",
"cant_find_the_right_conferencing_app_visit_our_app_store": "Can't find the right conferencing app? Visit our <1>App Store</1>.",
Expand Down
12 changes: 12 additions & 0 deletions apps/web/test/utils/bookingScenario/bookingScenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ type WhiteListedBookingProps = {
// TODO: Make sure that all references start providing credentialId and then remove this intersection of optional credentialId
credentialId?: number | null;
})[];
user?: { id: number };
bookingSeat?: Prisma.BookingSeatCreateInput[];
createdAt?: string;
};
Expand Down Expand Up @@ -400,6 +401,7 @@ async function addBookingsToDb(
bookings: (Prisma.BookingCreateInput & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
references: any[];
user?: { id: number };
})[]
) {
log.silly("TestData: Creating Bookings", JSON.stringify(bookings));
Expand Down Expand Up @@ -490,6 +492,16 @@ export async function addBookings(bookings: InputBooking[]) {
};
}

if (booking?.user?.id) {
bookingCreate.user = {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
connect: {
id: booking.user.id,
},
};
}

return bookingCreate;
})
);
Expand Down
31 changes: 12 additions & 19 deletions packages/app-store/dailyvideo/lib/VideoApiAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { z } from "zod";

import { handleErrorsJson } from "@calcom/lib/errors";
import { getDailyAppKeys } from "@calcom/app-store/dailyvideo/lib/getDailyAppKeys";
import { fetcher } from "@calcom/lib/dailyApiFetcher";
import { prisma } from "@calcom/prisma";
import type { GetRecordingsResponseSchema, GetAccessLinkResponseSchema } from "@calcom/prisma/zod-utils";
import {
Expand All @@ -15,7 +16,6 @@ import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapt

import { ZSubmitBatchProcessorJobRes, ZGetTranscriptAccessLink } from "../zod";
import type { TSubmitBatchProcessorJobRes, TGetTranscriptAccessLink, batchProcessorBody } from "../zod";
import { getDailyAppKeys } from "./getDailyAppKeys";
import {
dailyReturnTypeSchema,
getTranscripts,
Expand Down Expand Up @@ -54,19 +54,6 @@ export const FAKE_DAILY_CREDENTIAL: CredentialPayload & { invalid: boolean } = {
teamId: null,
};

export const fetcher = async (endpoint: string, init?: RequestInit | undefined) => {
const { api_key } = await getDailyAppKeys();
return fetch(`https://api.daily.co/v1${endpoint}`, {
method: "GET",
headers: {
Authorization: `Bearer ${api_key}`,
"Content-Type": "application/json",
...init?.headers,
},
...init,
}).then(handleErrorsJson);
};

function postToDailyAPI(endpoint: string, body: Record<string, unknown>) {
return fetcher(endpoint, {
method: "POST",
Expand Down Expand Up @@ -111,7 +98,10 @@ async function processTranscriptsInBatches(transcriptIds: Array<string>) {
return allTranscriptsAccessLinks;
}

export const generateGuestMeetingTokenFromOwnerMeetingToken = async (meetingToken: string | null) => {
export const generateGuestMeetingTokenFromOwnerMeetingToken = async (
meetingToken: string | null,
userId?: number
) => {
if (!meetingToken) return null;

const token = await fetcher(`/meeting-tokens/${meetingToken}`).then(ZGetMeetingTokenResponseSchema.parse);
Expand All @@ -120,28 +110,31 @@ export const generateGuestMeetingTokenFromOwnerMeetingToken = async (meetingToke
room_name: token.room_name,
exp: token.exp,
enable_recording_ui: false,
user_id: userId,
},
}).then(meetingTokenSchema.parse);

return guestMeetingToken.token;
};

// Only for backward compatibility
export const setEnableRecordingUIForOrganizer = async (
export const setEnableRecordingUIAndUserIdForOrganizer = async (
bookingReferenceId: number,
meetingToken: string | null
meetingToken: string | null,
userId?: number
) => {
if (!meetingToken) return null;

const token = await fetcher(`/meeting-tokens/${meetingToken}`).then(ZGetMeetingTokenResponseSchema.parse);
if (token.enable_recording_ui === false) return null;
if (token.enable_recording_ui === false && !!token.user_id) return null;

const organizerMeetingToken = await postToDailyAPI("/meeting-tokens", {
properties: {
room_name: token.room_name,
exp: token.exp,
enable_recording_ui: false,
is_owner: true,
user_id: userId,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To assign participant user_id from our db.

},
}).then(meetingTokenSchema.parse);

Expand Down
9 changes: 6 additions & 3 deletions packages/app-store/dailyvideo/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,17 @@ export const getRooms = z
})
.passthrough();

export const meetingTokenSchema = z.object({
token: z.string(),
});
export const meetingTokenSchema = z
.object({
token: z.string(),
})
.passthrough();

export const ZGetMeetingTokenResponseSchema = z
.object({
room_name: z.string(),
exp: z.number(),
enable_recording_ui: z.boolean().optional(),
user_id: z.number().optional(),
})
.passthrough();
17 changes: 17 additions & 0 deletions packages/features/bookings/lib/handleNewBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
OrganizerDefaultConferencingAppType,
getLocationValueForDB,
} from "@calcom/app-store/locations";
import { DailyLocationType } from "@calcom/app-store/locations";
import { getAppFromSlug } from "@calcom/app-store/utils";
import EventManager from "@calcom/core/EventManager";
import { getEventName } from "@calcom/core/event";
Expand Down Expand Up @@ -88,6 +89,7 @@ import { getSeatedBooking } from "./handleNewBooking/getSeatedBooking";
import { getVideoCallDetails } from "./handleNewBooking/getVideoCallDetails";
import { handleAppsStatus } from "./handleNewBooking/handleAppsStatus";
import { loadAndValidateUsers } from "./handleNewBooking/loadAndValidateUsers";
import { scheduleNoShowTriggers } from "./handleNewBooking/scheduleNoShowTriggers";
import type {
Invitee,
IEventTypePaymentCredentialType,
Expand Down Expand Up @@ -1654,6 +1656,21 @@ async function handler(
loggerWithEventDetails.error("Error while scheduling workflow reminders", JSON.stringify({ error }));
}

try {
if (isConfirmedByDefault && (booking.location === DailyLocationType || booking.location?.trim() === "")) {
await scheduleNoShowTriggers({
booking: { startTime: booking.startTime, id: booking.id },
triggerForUser,
organizerUser: { id: organizerUser.id },
eventTypeId,
teamId,
orgId,
});
}
} catch (error) {
loggerWithEventDetails.error("Error while scheduling no show triggers", JSON.stringify({ error }));
}

// booking successful
req.statusCode = 201;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import dayjs from "@calcom/dayjs";
import tasker from "@calcom/features/tasker";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import { WebhookTriggerEvents } from "@calcom/prisma/enums";

type ScheduleNoShowTriggersArgs = {
booking: {
startTime: Date;
id: number;
};
triggerForUser: number | true | null;
organizerUser: { id: number };
eventTypeId: number;
teamId?: number | null;
orgId?: number | null;
};

export const scheduleNoShowTriggers = async (args: ScheduleNoShowTriggersArgs) => {
const { booking, triggerForUser, organizerUser, eventTypeId, teamId, orgId } = args;
// Add task for automatic no show in cal video
const noShowPromises: Promise<any>[] = [];

const subscribersHostsNoShowStarted = await getWebhooks({
userId: triggerForUser ? organizerUser.id : null,
eventTypeId,
triggerEvent: WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
teamId,
orgId,
});

noShowPromises.push(
...subscribersHostsNoShowStarted.map((webhook) => {
if (booking?.startTime && webhook.time && webhook.timeUnit) {
const scheduledAt = dayjs(booking.startTime)
.add(webhook.time, webhook.timeUnit.toLowerCase() as dayjs.ManipulateType)
.toDate();
return tasker.create(
"triggerHostNoShowWebhook",
{
triggerEvent: WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
bookingId: booking.id,
// Prevents null values from being serialized
webhook: { ...webhook, time: webhook.time, timeUnit: webhook.timeUnit },
},
{ scheduledAt }
);
}
return Promise.resolve();
})
);

const subscribersGuestsNoShowStarted = await getWebhooks({
userId: triggerForUser ? organizerUser.id : null,
eventTypeId,
triggerEvent: WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
teamId,
orgId,
});

noShowPromises.push(
...subscribersGuestsNoShowStarted.map((webhook) => {
if (booking?.startTime && webhook.time && webhook.timeUnit) {
const scheduledAt = dayjs(booking.startTime)
.add(webhook.time, webhook.timeUnit.toLowerCase() as dayjs.ManipulateType)
.toDate();

return tasker.create(
"triggerGuestNoShowWebhook",
{
triggerEvent: WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
bookingId: booking.id,
// Prevents null values from being serialized
webhook: { ...webhook, time: webhook.time, timeUnit: webhook.timeUnit },
},
{ scheduledAt }
);
}

return Promise.resolve();
})
);

await Promise.all(noShowPromises);

// TODO: Support no show workflows
// const workflowHostsNoShow = workflows.filter(
// (workflow) => workflow.trigger === WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW
// );
// const workflowGuestsNoShow = workflows.filter(
// (workflow) => workflow.trigger === WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW
// );
};
Loading
Loading