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;
}