// Google Meet REST API Service
// API v2: https://meet.googleapis.com/v2
import type { OAuth2Client } from 'google-auth-library';
// ─────────────────────────────────────────────────────────────────────────────
// TYPES
// ─────────────────────────────────────────────────────────────────────────────
// Access type for meeting spaces
export type AccessType = 'ACCESS_TYPE_UNSPECIFIED' | 'OPEN' | 'TRUSTED' | 'RESTRICTED';
// Entry point access configuration
export type EntryPointAccess = 'ENTRY_POINT_ACCESS_UNSPECIFIED' | 'ALL' | 'CREATOR_APP_ONLY';
// Moderation mode
export type ModerationMode = 'MODERATION_UNSPECIFIED' | 'OFF' | 'ON';
// Restriction type for moderation
export type RestrictionType = 'RESTRICTION_TYPE_UNSPECIFIED' | 'HOSTS_ONLY' | 'NO_RESTRICTION';
// Default join as viewer type
export type DefaultJoinAsViewerType = 'DEFAULT_JOIN_AS_VIEWER_TYPE_UNSPECIFIED' | 'ON' | 'OFF';
// Auto generation type for artifacts
export type AutoGenerationType = 'AUTO_GENERATION_TYPE_UNSPECIFIED' | 'ON' | 'OFF';
// Recording/Transcript state
export type ArtifactState = 'STATE_UNSPECIFIED' | 'STARTED' | 'ENDED' | 'FILE_GENERATED';
// Member role
export type MemberRole = 'ROLE_UNSPECIFIED' | 'COHOST';
// ─────────────────────────────────────────────────────────────────────────────
// INTERFACES
// ─────────────────────────────────────────────────────────────────────────────
export interface ModerationRestrictions {
chatRestriction?: RestrictionType;
reactionRestriction?: RestrictionType;
presentRestriction?: RestrictionType;
defaultJoinAsViewerType?: DefaultJoinAsViewerType;
}
export interface RecordingConfig {
autoGenerationType?: AutoGenerationType;
}
export interface TranscriptionConfig {
autoGenerationType?: AutoGenerationType;
}
export interface SmartNotesConfig {
autoGenerationType?: AutoGenerationType;
}
export interface ArtifactConfig {
recordingConfig?: RecordingConfig;
transcriptionConfig?: TranscriptionConfig;
smartNotesConfig?: SmartNotesConfig;
}
export interface SpaceConfig {
accessType?: AccessType;
entryPointAccess?: EntryPointAccess;
moderation?: ModerationMode;
moderationRestrictions?: ModerationRestrictions;
attendanceReportGenerationType?: 'ATTENDANCE_REPORT_GENERATION_TYPE_UNSPECIFIED' | 'GENERATE_REPORT' | 'DO_NOT_GENERATE';
artifactConfig?: ArtifactConfig;
}
export interface ActiveConference {
conferenceRecord?: string;
}
export interface Space {
name?: string;
meetingUri?: string;
meetingCode?: string;
config?: SpaceConfig;
activeConference?: ActiveConference;
}
export interface Member {
name?: string;
email?: string;
role?: MemberRole;
user?: string;
}
export interface ConferenceRecord {
name?: string;
startTime?: string;
endTime?: string;
expireTime?: string;
space?: string;
}
export interface SignedinUser {
user?: string;
displayName?: string;
}
export interface AnonymousUser {
displayName?: string;
}
export interface PhoneUser {
displayName?: string;
}
export interface Participant {
name?: string;
earliestStartTime?: string;
latestEndTime?: string;
signedinUser?: SignedinUser;
anonymousUser?: AnonymousUser;
phoneUser?: PhoneUser;
}
export interface ParticipantSession {
name?: string;
startTime?: string;
endTime?: string;
}
export interface DriveDestination {
file?: string;
exportUri?: string;
}
export interface Recording {
name?: string;
state?: ArtifactState;
startTime?: string;
endTime?: string;
driveDestination?: DriveDestination;
}
export interface DocsDestination {
document?: string;
exportUri?: string;
}
export interface Transcript {
name?: string;
state?: ArtifactState;
startTime?: string;
endTime?: string;
docsDestination?: DocsDestination;
}
export interface TranscriptEntry {
name?: string;
participant?: string;
text?: string;
languageCode?: string;
startTime?: string;
endTime?: string;
}
// ─────────────────────────────────────────────────────────────────────────────
// PARAMS INTERFACES
// ─────────────────────────────────────────────────────────────────────────────
export interface CreateSpaceParams {
accessType?: AccessType;
entryPointAccess?: EntryPointAccess;
moderation?: ModerationMode;
moderationRestrictions?: ModerationRestrictions;
autoRecording?: boolean;
autoTranscription?: boolean;
autoSmartNotes?: boolean;
generateAttendanceReport?: boolean;
}
export interface UpdateSpaceParams {
spaceId: string;
accessType?: AccessType;
entryPointAccess?: EntryPointAccess;
moderation?: ModerationMode;
moderationRestrictions?: ModerationRestrictions;
autoRecording?: boolean;
autoTranscription?: boolean;
autoSmartNotes?: boolean;
generateAttendanceReport?: boolean;
}
export interface AddMemberParams {
spaceId: string;
email: string;
role?: MemberRole;
}
export interface ListParams {
pageSize?: number;
pageToken?: string;
filter?: string;
}
// ─────────────────────────────────────────────────────────────────────────────
// SERVICE CLASS
// ─────────────────────────────────────────────────────────────────────────────
const MEET_API_BASE = 'https://meet.googleapis.com/v2';
const MEET_API_BETA = 'https://meet.googleapis.com/v2beta';
export class MeetService {
private auth: OAuth2Client;
constructor(authClient: OAuth2Client) {
this.auth = authClient;
}
// ─────────────────────────────────────────────────────────────────────────
// HELPER METHODS
// ─────────────────────────────────────────────────────────────────────────
private async request<T>(
method: string,
path: string,
body?: unknown,
useBeta = false
): Promise<T> {
const baseUrl = useBeta ? MEET_API_BETA : MEET_API_BASE;
const url = `${baseUrl}${path}`;
const accessToken = await this.auth.getAccessToken();
if (!accessToken.token) {
throw new Error('Failed to get access token');
}
const headers: Record<string, string> = {
'Authorization': `Bearer ${accessToken.token}`,
'Content-Type': 'application/json',
};
const options: RequestInit = {
method,
headers,
};
if (body && (method === 'POST' || method === 'PATCH' || method === 'PUT')) {
options.body = JSON.stringify(body);
}
const response = await fetch(url, options);
if (!response.ok) {
const errorText = await response.text();
let errorMessage = `Meet API error: ${response.status} ${response.statusText}`;
try {
const errorJson = JSON.parse(errorText);
if (errorJson.error?.message) {
errorMessage = errorJson.error.message;
}
} catch {
if (errorText) {
errorMessage += ` - ${errorText}`;
}
}
throw new Error(errorMessage);
}
// Handle empty responses (204 No Content)
const text = await response.text();
if (!text) {
return {} as T;
}
return JSON.parse(text) as T;
}
private buildSpaceConfig(params: CreateSpaceParams | UpdateSpaceParams): SpaceConfig {
const config: SpaceConfig = {};
if (params.accessType) {
config.accessType = params.accessType;
}
if (params.entryPointAccess) {
config.entryPointAccess = params.entryPointAccess;
}
if (params.moderation) {
config.moderation = params.moderation;
}
if (params.moderationRestrictions) {
config.moderationRestrictions = params.moderationRestrictions;
}
if (params.generateAttendanceReport !== undefined) {
config.attendanceReportGenerationType = params.generateAttendanceReport
? 'GENERATE_REPORT'
: 'DO_NOT_GENERATE';
}
// Artifact config
if (params.autoRecording !== undefined ||
params.autoTranscription !== undefined ||
params.autoSmartNotes !== undefined) {
config.artifactConfig = {};
if (params.autoRecording !== undefined) {
config.artifactConfig.recordingConfig = {
autoGenerationType: params.autoRecording ? 'ON' : 'OFF'
};
}
if (params.autoTranscription !== undefined) {
config.artifactConfig.transcriptionConfig = {
autoGenerationType: params.autoTranscription ? 'ON' : 'OFF'
};
}
if (params.autoSmartNotes !== undefined) {
config.artifactConfig.smartNotesConfig = {
autoGenerationType: params.autoSmartNotes ? 'ON' : 'OFF'
};
}
}
return config;
}
private normalizeSpaceId(spaceId: string): string {
// Accept either "spaces/xxx" or just "xxx" or meeting code like "abc-defg-hij"
if (spaceId.startsWith('spaces/')) {
return spaceId;
}
return `spaces/${spaceId}`;
}
private normalizeConferenceId(conferenceId: string): string {
if (conferenceId.startsWith('conferenceRecords/')) {
return conferenceId;
}
return `conferenceRecords/${conferenceId}`;
}
// ─────────────────────────────────────────────────────────────────────────
// SPACES OPERATIONS
// ─────────────────────────────────────────────────────────────────────────
/**
* Create a new meeting space
*/
async createSpace(params: CreateSpaceParams = {}): Promise<Space> {
const config = this.buildSpaceConfig(params);
const body: { config?: SpaceConfig } = {};
if (Object.keys(config).length > 0) {
body.config = config;
}
return this.request<Space>('POST', '/spaces', body);
}
/**
* Get meeting space details
*/
async getSpace(spaceId: string): Promise<Space> {
const name = this.normalizeSpaceId(spaceId);
return this.request<Space>('GET', `/${name}`);
}
/**
* Update meeting space configuration
*/
async updateSpace(params: UpdateSpaceParams): Promise<Space> {
const name = this.normalizeSpaceId(params.spaceId);
const config = this.buildSpaceConfig(params);
const updateMask: string[] = [];
if (params.accessType) updateMask.push('config.accessType');
if (params.entryPointAccess) updateMask.push('config.entryPointAccess');
if (params.moderation) updateMask.push('config.moderation');
if (params.moderationRestrictions) updateMask.push('config.moderationRestrictions');
if (params.generateAttendanceReport !== undefined) updateMask.push('config.attendanceReportGenerationType');
if (params.autoRecording !== undefined) updateMask.push('config.artifactConfig.recordingConfig');
if (params.autoTranscription !== undefined) updateMask.push('config.artifactConfig.transcriptionConfig');
if (params.autoSmartNotes !== undefined) updateMask.push('config.artifactConfig.smartNotesConfig');
const queryParams = updateMask.length > 0 ? `?updateMask=${updateMask.join(',')}` : '';
return this.request<Space>('PATCH', `/${name}${queryParams}`, {
name,
config
});
}
/**
* End the active conference in a space
*/
async endActiveConference(spaceId: string): Promise<void> {
const name = this.normalizeSpaceId(spaceId);
await this.request<unknown>('POST', `/${name}:endActiveConference`, {});
}
// ─────────────────────────────────────────────────────────────────────────
// MEMBERS OPERATIONS (v2beta)
// ─────────────────────────────────────────────────────────────────────────
/**
* Add a member to a meeting space (e.g., co-host)
*/
async addMember(params: AddMemberParams): Promise<Member> {
const spaceName = this.normalizeSpaceId(params.spaceId);
const body = {
email: params.email,
role: params.role || 'COHOST'
};
return this.request<Member>('POST', `/${spaceName}/members`, body, true);
}
/**
* List members of a meeting space
*/
async listMembers(
spaceId: string,
params: ListParams = {}
): Promise<{ members: Member[]; nextPageToken?: string }> {
const spaceName = this.normalizeSpaceId(spaceId);
const queryParams = new URLSearchParams();
if (params.pageSize) queryParams.set('pageSize', params.pageSize.toString());
if (params.pageToken) queryParams.set('pageToken', params.pageToken);
const query = queryParams.toString();
const path = `/${spaceName}/members${query ? `?${query}` : ''}`;
const response = await this.request<{ members?: Member[]; nextPageToken?: string }>('GET', path, undefined, true);
return {
members: response.members || [],
nextPageToken: response.nextPageToken
};
}
/**
* Get a specific member
*/
async getMember(spaceId: string, memberId: string): Promise<Member> {
const spaceName = this.normalizeSpaceId(spaceId);
return this.request<Member>('GET', `/${spaceName}/members/${memberId}`, undefined, true);
}
/**
* Remove a member from a meeting space
*/
async deleteMember(spaceId: string, memberId: string): Promise<void> {
const spaceName = this.normalizeSpaceId(spaceId);
await this.request<unknown>('DELETE', `/${spaceName}/members/${memberId}`, undefined, true);
}
// ─────────────────────────────────────────────────────────────────────────
// CONFERENCE RECORDS OPERATIONS
// ─────────────────────────────────────────────────────────────────────────
/**
* List all conference records
*/
async listConferenceRecords(
params: ListParams = {}
): Promise<{ conferenceRecords: ConferenceRecord[]; nextPageToken?: string }> {
const queryParams = new URLSearchParams();
if (params.pageSize) queryParams.set('pageSize', params.pageSize.toString());
if (params.pageToken) queryParams.set('pageToken', params.pageToken);
if (params.filter) queryParams.set('filter', params.filter);
const query = queryParams.toString();
const path = `/conferenceRecords${query ? `?${query}` : ''}`;
const response = await this.request<{ conferenceRecords?: ConferenceRecord[]; nextPageToken?: string }>('GET', path);
return {
conferenceRecords: response.conferenceRecords || [],
nextPageToken: response.nextPageToken
};
}
/**
* Get a specific conference record
*/
async getConferenceRecord(conferenceId: string): Promise<ConferenceRecord> {
const name = this.normalizeConferenceId(conferenceId);
return this.request<ConferenceRecord>('GET', `/${name}`);
}
// ─────────────────────────────────────────────────────────────────────────
// PARTICIPANTS OPERATIONS
// ─────────────────────────────────────────────────────────────────────────
/**
* List participants in a conference
*/
async listParticipants(
conferenceId: string,
params: ListParams = {}
): Promise<{ participants: Participant[]; nextPageToken?: string }> {
const conferenceName = this.normalizeConferenceId(conferenceId);
const queryParams = new URLSearchParams();
if (params.pageSize) queryParams.set('pageSize', params.pageSize.toString());
if (params.pageToken) queryParams.set('pageToken', params.pageToken);
if (params.filter) queryParams.set('filter', params.filter);
const query = queryParams.toString();
const path = `/${conferenceName}/participants${query ? `?${query}` : ''}`;
const response = await this.request<{ participants?: Participant[]; nextPageToken?: string }>('GET', path);
return {
participants: response.participants || [],
nextPageToken: response.nextPageToken
};
}
/**
* Get a specific participant
*/
async getParticipant(conferenceId: string, participantId: string): Promise<Participant> {
const conferenceName = this.normalizeConferenceId(conferenceId);
return this.request<Participant>('GET', `/${conferenceName}/participants/${participantId}`);
}
/**
* List participant sessions
*/
async listParticipantSessions(
conferenceId: string,
participantId: string,
params: ListParams = {}
): Promise<{ participantSessions: ParticipantSession[]; nextPageToken?: string }> {
const conferenceName = this.normalizeConferenceId(conferenceId);
const queryParams = new URLSearchParams();
if (params.pageSize) queryParams.set('pageSize', params.pageSize.toString());
if (params.pageToken) queryParams.set('pageToken', params.pageToken);
const query = queryParams.toString();
const path = `/${conferenceName}/participants/${participantId}/participantSessions${query ? `?${query}` : ''}`;
const response = await this.request<{ participantSessions?: ParticipantSession[]; nextPageToken?: string }>('GET', path);
return {
participantSessions: response.participantSessions || [],
nextPageToken: response.nextPageToken
};
}
// ─────────────────────────────────────────────────────────────────────────
// RECORDINGS OPERATIONS
// ─────────────────────────────────────────────────────────────────────────
/**
* List recordings from a conference
*/
async listRecordings(
conferenceId: string,
params: ListParams = {}
): Promise<{ recordings: Recording[]; nextPageToken?: string }> {
const conferenceName = this.normalizeConferenceId(conferenceId);
const queryParams = new URLSearchParams();
if (params.pageSize) queryParams.set('pageSize', params.pageSize.toString());
if (params.pageToken) queryParams.set('pageToken', params.pageToken);
const query = queryParams.toString();
const path = `/${conferenceName}/recordings${query ? `?${query}` : ''}`;
const response = await this.request<{ recordings?: Recording[]; nextPageToken?: string }>('GET', path);
return {
recordings: response.recordings || [],
nextPageToken: response.nextPageToken
};
}
/**
* Get a specific recording
*/
async getRecording(conferenceId: string, recordingId: string): Promise<Recording> {
const conferenceName = this.normalizeConferenceId(conferenceId);
return this.request<Recording>('GET', `/${conferenceName}/recordings/${recordingId}`);
}
// ─────────────────────────────────────────────────────────────────────────
// TRANSCRIPTS OPERATIONS
// ─────────────────────────────────────────────────────────────────────────
/**
* List transcripts from a conference
*/
async listTranscripts(
conferenceId: string,
params: ListParams = {}
): Promise<{ transcripts: Transcript[]; nextPageToken?: string }> {
const conferenceName = this.normalizeConferenceId(conferenceId);
const queryParams = new URLSearchParams();
if (params.pageSize) queryParams.set('pageSize', params.pageSize.toString());
if (params.pageToken) queryParams.set('pageToken', params.pageToken);
const query = queryParams.toString();
const path = `/${conferenceName}/transcripts${query ? `?${query}` : ''}`;
const response = await this.request<{ transcripts?: Transcript[]; nextPageToken?: string }>('GET', path);
return {
transcripts: response.transcripts || [],
nextPageToken: response.nextPageToken
};
}
/**
* Get a specific transcript
*/
async getTranscript(conferenceId: string, transcriptId: string): Promise<Transcript> {
const conferenceName = this.normalizeConferenceId(conferenceId);
return this.request<Transcript>('GET', `/${conferenceName}/transcripts/${transcriptId}`);
}
/**
* List transcript entries
*/
async listTranscriptEntries(
conferenceId: string,
transcriptId: string,
params: ListParams = {}
): Promise<{ transcriptEntries: TranscriptEntry[]; nextPageToken?: string }> {
const conferenceName = this.normalizeConferenceId(conferenceId);
const queryParams = new URLSearchParams();
if (params.pageSize) queryParams.set('pageSize', params.pageSize.toString());
if (params.pageToken) queryParams.set('pageToken', params.pageToken);
const query = queryParams.toString();
const path = `/${conferenceName}/transcripts/${transcriptId}/entries${query ? `?${query}` : ''}`;
const response = await this.request<{ transcriptEntries?: TranscriptEntry[]; nextPageToken?: string }>('GET', path);
return {
transcriptEntries: response.transcriptEntries || [],
nextPageToken: response.nextPageToken
};
}
/**
* Get a specific transcript entry
*/
async getTranscriptEntry(
conferenceId: string,
transcriptId: string,
entryId: string
): Promise<TranscriptEntry> {
const conferenceName = this.normalizeConferenceId(conferenceId);
return this.request<TranscriptEntry>(
'GET',
`/${conferenceName}/transcripts/${transcriptId}/entries/${entryId}`
);
}
}