meeting-display.ts3.2 KBView on GitHub /**
* Meeting display utilities
* Shared helpers for rendering meeting event data in the UI
*/
/** Gong call URL format - fallback when recordingUrl/meetingUrl not provided */
const GONG_CALL_BASE = 'https://us-4796.app.gong.io/call';
/** Where Circleback serves a meeting page; `app.circleback.ai` 307s here. */
const CIRCLEBACK_PAGE_BASE = 'https://circleback.ai';
export interface MeetingDisplayUrlInput {
recordingUrl?: string | null;
meetingUrl?: string | null;
meetingProvider?: string | null;
externalId?: string | null;
}
export interface MeetingDisplayUrlResult {
url: string;
label: 'Recording' | 'Meeting Link';
}
/**
* True when a stored URL is the MEDIA FILE rather than a page about the meeting.
*
* Circleback stores a signed Google Cloud Storage object whose signature dies 24h after
* ingest; the bare object answers 403 forever after. Offering it as a link gives the
* reader an XML error page that says nothing about the meeting, so a media URL is never
* what "open at the provider" means — see `circlebackMeetingPageUrl` on the server, which
* builds the real one.
*/
function isRawRecordingFile(url: string): boolean {
return /storage\.googleapis\.com|googleusercontent\.com/i.test(url) || /\.mp4(\?|$)/i.test(url);
}
/**
* The provider's own page for a meeting, or null when it has none Cedar can address.
*
* Every recorder spells this differently, and two of them cannot use the stored value at
* all: Circleback's is a media file (above), and Gong never stored a page — its call URL is
* built from the call id. Fathom's stored share page IS the page, so it passes through.
*
* Webhook-era Circleback rows kept the provider's NUMERIC meeting id, which its page route
* does not take, so those get null rather than a link to a page that cannot resolve.
*/
export function getMeetingProviderPageUrl(meeting: MeetingDisplayUrlInput): string | null {
const provider = meeting.meetingProvider?.trim().toLowerCase();
if (provider === 'circleback') {
const id = meeting.externalId?.trim();
return id && !/^\d+$/.test(id) ? `${CIRCLEBACK_PAGE_BASE}/meetings/${encodeURIComponent(id)}` : null;
}
// For Gong: prefer the constructed Gong URL over meetingUrl (a Google Meet join link).
// Gong URL is where you watch the recording; meetingUrl is the live join link.
if (provider === 'gong') {
if (meeting.recordingUrl) return meeting.recordingUrl;
return meeting.externalId ? `${GONG_CALL_BASE}?id=${meeting.externalId}` : null;
}
if (meeting.recordingUrl && !isRawRecordingFile(meeting.recordingUrl)) {
return meeting.recordingUrl;
}
return null;
}
/**
* Returns the URL to display for a meeting event: the provider's page where there is one,
* otherwise the meeting's own join link. Never a raw media file — that link 403s the day
* after ingest, and the player mints a fresh one for playback rather than linking to it.
*/
export function getMeetingDisplayUrl(
meeting: MeetingDisplayUrlInput,
): MeetingDisplayUrlResult | null {
const providerPage = getMeetingProviderPageUrl(meeting);
if (providerPage) {
return { url: providerPage, label: 'Recording' };
}
if (meeting.meetingUrl) {
return { url: meeting.meetingUrl, label: 'Meeting Link' };
}
return null;
}