Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions apps/meteor/server/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
BannersDismissRaw,
BannersRaw,
CalendarEventRaw,
CallHistoryRaw,
CredentialTokensRaw,
CronHistoryRaw,
CustomSoundsRaw,
Expand Down Expand Up @@ -95,6 +96,7 @@ registerModel('IAvatarsModel', new AvatarsRaw(db));
registerModel('IBannersDismissModel', new BannersDismissRaw(db));
registerModel('IBannersModel', new BannersRaw(db));
registerModel('ICalendarEventModel', new CalendarEventRaw(db));
registerModel('ICallHistoryModel', new CallHistoryRaw(db));
registerModel('ICredentialTokensModel', new CredentialTokensRaw(db));
registerModel('ICronHistoryModel', new CronHistoryRaw(db));
registerModel('ICustomSoundsModel', new CustomSoundsRaw(db));
Expand Down
127 changes: 125 additions & 2 deletions apps/meteor/server/services/media-call/service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { api, ServiceClassInternal, type IMediaCallService, Authorization } from '@rocket.chat/core-services';
import type { IUser } from '@rocket.chat/core-typings';
import type { IMediaCall, IUser, IRoom, IInternalMediaCallHistoryItem, CallHistoryItemState } from '@rocket.chat/core-typings';
import { Logger } from '@rocket.chat/logger';
import { callServer, type IMediaCallServerSettings } from '@rocket.chat/media-calls';
import { isClientMediaSignal, type ClientMediaSignal, type ServerMediaSignal } from '@rocket.chat/media-signaling';
import { MediaCalls } from '@rocket.chat/models';
import type { InsertionModel } from '@rocket.chat/model-typings';
import { CallHistory, MediaCalls, Rooms, Users } from '@rocket.chat/models';

import { settings } from '../../../app/settings/server';
import { createDirectMessage } from '../../methods/createDirectMessage';

const logger = new Logger('media-call service');

Expand All @@ -16,6 +18,7 @@ export class MediaCallService extends ServiceClassInternal implements IMediaCall
super();
callServer.emitter.on('signalRequest', ({ toUid, signal }) => this.sendSignal(toUid, signal));
callServer.emitter.on('callUpdated', (params) => api.broadcast('media-call.updated', params));
callServer.emitter.on('historyUpdate', ({ callId }) => setImmediate(() => this.saveCallToHistory(callId)));
this.onEvent('media-call.updated', (params) => callServer.receiveCallUpdate(params));

this.onEvent('watch.settings', async ({ setting }): Promise<void> => {
Expand Down Expand Up @@ -62,6 +65,126 @@ export class MediaCallService extends ServiceClassInternal implements IMediaCall
}
}

private async saveCallToHistory(callId: IMediaCall['_id']): Promise<void> {
logger.info({ msg: 'saving media call to history', callId });

const call = await MediaCalls.findOneById(callId);
if (!call) {
logger.warn({ msg: 'Attempt to save an invalid call to history', callId });
return;
}
if (!call.ended) {
logger.warn({ msg: 'Attempt to save a pending call to history', callId });
return;
}

// TODO: save external media calls to history
if (call.uids.length !== 2) {
return;
}

return this.saveInternalCallToHistory(call);
}

private async saveInternalCallToHistory(call: IMediaCall): Promise<void> {
if (call.caller.type !== 'user' || call.callee.type !== 'user') {
logger.warn({ msg: 'Attempt to save an internal call history with a call that is not internal', callId: call._id });
return;
}

const rid = await this.getRoomIdForInternalCall(call).catch((error) => {
logger.error({ msg: 'Failed to determine room id for Internal Call', error });
return undefined;
});
const state = this.getCallHistoryItemState(call);
const duration = this.getCallDuration(call);

const sharedData: Omit<InsertionModel<IInternalMediaCallHistoryItem>, 'uid' | 'direction' | 'contactId'> = {
ts: call.createdAt,
callId: call._id,
state,
type: 'media-call',
duration,
endedAt: call.endedAt || new Date(),
external: false,
...(rid && { rid }),
};

await Promise.allSettled([
CallHistory.insertOne({
...sharedData,
uid: call.caller.id,
direction: 'outbound',
contactId: call.callee.id,
}).catch((error: unknown) => logger.error({ msg: 'Failed to insert item into Call History', error })),
CallHistory.insertOne({
...sharedData,
uid: call.callee.id,
direction: 'inbound',
contactId: call.caller.id,
}).catch((error: unknown) => logger.error({ msg: 'Failed to insert item into Call History', error })),
]);

// TODO: If there's a `rid`, send a message in that room - planned for 7.13
}

private getCallDuration(call: IMediaCall): number {
const { activatedAt, endedAt = new Date() } = call;
if (!activatedAt) {
return 0;
}

const diff = endedAt.valueOf() - activatedAt.valueOf();
return Math.floor(diff / 1000);
}

private getCallHistoryItemState(call: IMediaCall): CallHistoryItemState {
if (call.transferredBy) {
return 'transferred';
}

if (call.hangupReason?.includes('error')) {
if (!call.activatedAt) {
return 'failed';
}

return 'error';
}

if (!call.acceptedAt) {
return 'not-answered';
}

if (!call.activatedAt) {
return 'failed';
}

return 'ended';
}

private async getRoomIdForInternalCall(call: IMediaCall): Promise<IRoom['_id']> {
const room = await Rooms.findOneDirectRoomContainingAllUserIDs(call.uids, { projection: { _id: 1 } });
if (room) {
return room._id;
}

const requesterId = call.createdBy.type === 'user' && call.createdBy.id;
const callerId = call.caller.type === 'user' && call.caller.id;

const dmCreatorId = requesterId || callerId || call.uids[0];

const usernames = (await Users.findByIds(call.uids, { projection: { username: 1 } }).toArray())
.map(({ username }) => username)
.filter((username) => username);

if (usernames.length !== 2) {
throw new Error('Invalid usernames for DM.');
}

const newRoom = await createDirectMessage(usernames, dmCreatorId, true);
return newRoom.rid;
}

private async sendSignal(toUid: IUser['_id'], signal: ServerMediaSignal): Promise<void> {
void api.broadcast('user.media-signal', { userId: toUid, signal });
}
Expand Down
2 changes: 2 additions & 0 deletions ee/packages/media-calls/src/definition/IMediaCallServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { InternalCallParams } from './common';
export type MediaCallServerEvents = {
callUpdated: { callId: string; dtmf?: ClientMediaSignalBody<'dtmf'> };
signalRequest: { toUid: IUser['_id']; signal: ServerMediaSignal };
historyUpdate: { callId: string };
};

export interface IMediaCallServerSettings {
Expand Down Expand Up @@ -39,6 +40,7 @@ export interface IMediaCallServer {
// functions that trigger events
sendSignal(toUid: IUser['_id'], signal: ServerMediaSignal): void;
reportCallUpdate(params: { callId: string; dtmf?: ClientMediaSignalBody<'dtmf'> }): void;
updateCallHistory(params: { callId: string }): void;

// functions that are run on events
receiveSignal(fromUid: IUser['_id'], signal: ClientMediaSignal): void;
Expand Down
10 changes: 8 additions & 2 deletions ee/packages/media-calls/src/server/CallDirector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { CallHangupReason, CallRole } from '@rocket.chat/media-signaling';
import type { InsertionModel } from '@rocket.chat/model-typings';
import { MediaCallNegotiations, MediaCalls } from '@rocket.chat/models';

import { getCastDirector } from './injection';
import { getCastDirector, getMediaCallServer } from './injection';
import type { IMediaCallAgent } from '../definition/IMediaCallAgent';
import type { IMediaCallCastDirector } from '../definition/IMediaCallCastDirector';
import type { InternalCallParams, MediaCallHeader } from '../definition/common';
Expand Down Expand Up @@ -368,7 +368,13 @@ class MediaCallDirector {
});
throw error;
});
return Boolean(result.modifiedCount);

const ended = Boolean(result.modifiedCount);
if (ended) {
getMediaCallServer().updateCallHistory({ callId });
}

return ended;
}

public async hangupCallByIdAndNotifyAgents(
Expand Down
6 changes: 6 additions & 0 deletions ee/packages/media-calls/src/server/MediaCallServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ export class MediaCallServer implements IMediaCallServer {
this.emitter.emit('callUpdated', params);
}

public updateCallHistory(params: { callId: string }): void {
logger.debug({ msg: 'MediaCallServer.updateCallHistory', params });

this.emitter.emit('historyUpdate', params);
}

public async requestCall(params: InternalCallParams): Promise<void> {
try {
const fullParams = await this.parseCallContacts(params);
Expand Down
46 changes: 46 additions & 0 deletions packages/core-typings/src/ICallHistoryItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { IRocketChatRecord } from './IRocketChatRecord';
import type { IRoom } from './IRoom';
import type { IUser } from './IUser';

export type CallHistoryItemState =
/** One of the users ended the call */
| 'ended'
/** Call was not answered */
| 'not-answered'
/** The call could not be established */
| 'failed'
/** The call was established, but it ended due to an error */
| 'error'
/** The call ended due to a transfer */
| 'transferred';

interface ICallHistoryItem extends IRocketChatRecord {
uid: IUser['_id'];
ts: Date;

callId: string;

direction: 'inbound' | 'outbound';
state: CallHistoryItemState;
}

interface IMediaCallHistoryItem extends ICallHistoryItem {
type: 'media-call';
external: boolean;

/* The call's duration, in seconds */
duration: number;
endedAt: Date;
}

export interface IInternalMediaCallHistoryItem extends IMediaCallHistoryItem {
external: false;
contactId: IUser['_id'];

rid?: IRoom['_id'];
}

// TODO: IExternalMediaCallHistoryItem, planned for 8.0
// TODO: IVideoConfHistoryItem, expected in the future but not yet on the roadmap

export type CallHistoryItem = IInternalMediaCallHistoryItem;
1 change: 1 addition & 0 deletions packages/core-typings/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,6 @@ export * from './RoomRouteData';
export * as Cloud from './cloud';
export * from './themes';
export * from './mediaCalls';
export * from './ICallHistoryItem';

export { schemas } from './Ajv';
1 change: 1 addition & 0 deletions packages/model-typings/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,4 @@ export * from './models/IMediaCallChannelsModel';
export * from './models/IMediaCallNegotiationsModel';
export * from './updater';
export * from './models/IWorkspaceCredentialsModel';
export * from './models/ICallHistoryModel';
5 changes: 5 additions & 0 deletions packages/model-typings/src/models/ICallHistoryModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { CallHistoryItem } from '@rocket.chat/core-typings';

import type { IBaseModel } from './IBaseModel';

export type ICallHistoryModel = IBaseModel<CallHistoryItem>;
2 changes: 2 additions & 0 deletions packages/models/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import type {
IMediaCallsModel,
IMediaCallChannelsModel,
IMediaCallNegotiationsModel,
ICallHistoryModel,
} from '@rocket.chat/model-typings';
import type { Collection, Db } from 'mongodb';

Expand Down Expand Up @@ -145,6 +146,7 @@ export const Analytics = proxify<IAnalyticsModel>('IAnalyticsModel');
export const Avatars = proxify<IAvatarsModel>('IAvatarsModel');
export const BannersDismiss = proxify<IBannersDismissModel>('IBannersDismissModel');
export const Banners = proxify<IBannersModel>('IBannersModel');
export const CallHistory = proxify<ICallHistoryModel>('ICallHistoryModel');
export const CannedResponse = proxify<ICannedResponseModel>('ICannedResponseModel');
export const CredentialTokens = proxify<ICredentialTokensModel>('ICredentialTokensModel');
export const CustomSounds = proxify<ICustomSoundsModel>('ICustomSoundsModel');
Expand Down
1 change: 1 addition & 0 deletions packages/models/src/modelClasses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,4 @@ export * from './models/MediaCallChannels';
export * from './models/MediaCallNegotiations';
export * from './models/WorkspaceCredentials';
export * from './models/Trash';
export * from './models/CallHistory';
18 changes: 18 additions & 0 deletions packages/models/src/models/CallHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { CallHistoryItem } from '@rocket.chat/core-typings';
import type { ICallHistoryModel } from '@rocket.chat/model-typings';
import type { Db, IndexDescription } from 'mongodb';

import { BaseRaw } from './BaseRaw';

export class CallHistoryRaw extends BaseRaw<CallHistoryItem> implements ICallHistoryModel {
constructor(db: Db) {
super(db, 'call_history');
}

protected modelIndexes(): IndexDescription[] {
return [
{ key: { uid: 1, callId: 1 }, unique: true },
{ key: { uid: 1, ts: -1 }, unique: false },
];
}
}
Loading