Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
121 changes: 119 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,120 @@ 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(() => 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,
}),
CallHistory.insertOne({
...sharedData,
uid: call.callee.id,
direction: 'inbound',
contactId: call.caller.id,
}),
]);

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

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

const hasError = call.hangupReason?.includes('error');
if (!call.acceptedAt) {
if (hasError) {
return 'failed';
}

return 'not-answered';
}

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

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);
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
// TODO: IVideoConfHistoryItem

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