Skip to content

Commit

Permalink
identify calls
Browse files Browse the repository at this point in the history
  • Loading branch information
KevLehman committed Dec 11, 2024
1 parent d5aa94b commit d468365
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 30 deletions.
104 changes: 81 additions & 23 deletions apps/meteor/ee/server/local-services/voip-freeswitch/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import type {
IFreeSwitchEventCaller,
IFreeSwitchEvent,
FreeSwitchExtension,
IFreeSwitchChannelUser,
IUser,
IFreeSwitchCall,
IFreeSwitchCallEventType,
IFreeSwitchCallEvent,
Expand Down Expand Up @@ -248,25 +246,6 @@ export class VoipFreeSwitchService extends ServiceClassInternal implements IVoip
}) as InsertionModel<WithoutId<IFreeSwitchEvent>>;
}

private async getUserFromFreeSwitchIdentifier(identifier: string): Promise<IFreeSwitchChannelUser | null> {
const strippedValue = identifier.replace(/\@.*/, '');

const user = await Users.findOneByFreeSwitchExtension<Pick<Required<IUser>, '_id' | 'username' | 'name' | 'avatarETag'>>(
strippedValue,
{ projection: { _id: 1, name: 1, username: 1, avatarETag: 1 } },
);

if (!user) {
return null;
}

return {
user,
extension: strippedValue,
identifier,
};
}

private parseTimestamp(timestamp: string | undefined): Date | undefined {
if (!timestamp || timestamp === '0') {
return undefined;
Expand Down Expand Up @@ -320,11 +299,41 @@ export class VoipFreeSwitchService extends ServiceClassInternal implements IVoip
}
}

console.log(modifiedEventName, state, callState);

return 'OTHER';
}

private identifyCallerFromEvent(event: IFreeSwitchEvent): string {
if (event.call?.from?.user) {
return event.call.from.user;
}

if (event.caller?.username) {
return event.caller.username;
}

if (event.caller?.number) {
return event.caller.number;
}

if (event.caller?.ani) {
return event.caller.ani;
}

return '';
}

private identifyCalleeFromEvent(event: IFreeSwitchEvent): string {
if (event.call?.to?.dialedExtension) {
return event.call.to.dialedExtension;
}

if (event.call?.to?.dialedUser) {
return event.call.to.dialedUser;
}

return '';
}

private async computeCall(callUUID: string): Promise<void> {
const allEvents = await FreeSwitchEvent.findAllByCallUUID(callUUID).toArray();
const call: InsertionModel<IFreeSwitchCall> = {
Expand All @@ -350,12 +359,16 @@ export class VoipFreeSwitchService extends ServiceClassInternal implements IVoip
return (event1.firedAt?.valueOf() || 0) - (event2.firedAt?.valueOf() || 0);
});

const fromUser = new Set<string>();
const toUser = new Set<string>();
for (const event of sortedEvents) {
if (event.channelUniqueId && !call.channels.includes(event.channelUniqueId)) {
call.channels.push(event.channelUniqueId);
}

const eventType = this.getEventType(event);
fromUser.add(this.identifyCallerFromEvent(event));
toUser.add(this.identifyCalleeFromEvent(event));

const hasUsefulCallData = Object.keys(event.eventData).some((key) => key.startsWith('variable_'));

Expand Down Expand Up @@ -418,6 +431,51 @@ export class VoipFreeSwitchService extends ServiceClassInternal implements IVoip
});
}

// A call has 2 channels at max
// If it has 3 channels, it's a forwarded call
if (call.channels.length === 3) {
const originalCalls = await FreeSwitchCall.findAllByChannelUniqueIds(call.channels, { projection: { events: 0 } }).toArray();
if (originalCalls.length) {
call.forwardedFrom = originalCalls;
}
}

if (fromUser.size) {
const callerIds = [...fromUser].filter((e) => !!e);
const user = await Users.findOneByFreeSwitchExtensions(callerIds);

if (user) {
call.from = {
_id: user._id,
username: user.username,
name: user.name,
avatarETag: user.avatarETag,
};
}
}

if (toUser.size) {
const calleeIds = [...toUser].filter((e) => !!e);
const user = await Users.findOneByFreeSwitchExtensions(calleeIds);
if (user) {
call.to = {
_id: user._id,
username: user.username,
name: user.name,
avatarETag: user.avatarETag,
};
}
}

if (!call.from || !call.to) {
console.log({
msg: 'Ignoring call not originated by a Rocket.Chat user',
call: JSON.stringify(call),
fromUser,
toUser,
});
return;
}
await FreeSwitchCall.registerCall(call);
}

Expand Down
6 changes: 5 additions & 1 deletion apps/meteor/server/models/raw/FreeSwitchEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ export class FreeSwitchEventRaw extends BaseRaw<IFreeSwitchEvent> implements IFr
}

protected modelIndexes(): IndexDescription[] {
return [{ key: { channelUniqueId: 1, sequence: 1 }, unique: false }];
return [
{ key: { channelUniqueId: 1, sequence: 1 }, unique: false },
// Allow 15 days of events to be saved
{ key: { updatedAt: 1 }, expireAfterSeconds: 30 * 24 * 60 * 15 },
];
}

public async registerEvent(event: WithoutId<InsertionModel<IFreeSwitchEvent>>): Promise<void> {
Expand Down
9 changes: 9 additions & 0 deletions apps/meteor/server/models/raw/Users.js
Original file line number Diff line number Diff line change
Expand Up @@ -2485,6 +2485,15 @@ export class UsersRaw extends BaseRaw {
);
}

findOneByFreeSwitchExtensions(freeSwitchExtensions, options = {}) {
return this.findOne(
{
freeSwitchExtension: { $in: freeSwitchExtensions },
},
options,
);
}

findAssignedFreeSwitchExtensions() {
return this.findUsersWithAssignedFreeSwitchExtensions({
projection: {
Expand Down
6 changes: 3 additions & 3 deletions apps/meteor/server/services/video-conference/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
isLivechatVideoConference,
} from '@rocket.chat/core-typings';
import { Logger } from '@rocket.chat/logger';
import type { InsertionModel } from '@rocket.chat/model-typings';
import { Users, VideoConference as VideoConferenceModel, Rooms, Messages, Subscriptions } from '@rocket.chat/models';
import { Random } from '@rocket.chat/random';
import type { PaginatedResult } from '@rocket.chat/rest-typings';
Expand Down Expand Up @@ -58,7 +59,6 @@ import { isRoomCompatibleWithVideoConfRinging } from '../../lib/isRoomCompatible
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
import { videoConfProviders } from '../../lib/videoConfProviders';
import { videoConfTypes } from '../../lib/videoConfTypes';
import { InsertionModel } from '@rocket.chat/model-typings';

const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo;

Expand Down Expand Up @@ -461,8 +461,8 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf
return true;
}

public async createVoIP(data: InsertionModel<IVoIPVideoConference>): Promise<IVoIPVideoConference['_id']> {
return wrapExceptions(async () => VideoConferenceModel.createVoIP(data)).catch((e) => {
public async createVoIP(data: InsertionModel<IVoIPVideoConference>): Promise<IVoIPVideoConference['_id'] | undefined> {
return wrapExceptions<string | undefined>(async () => VideoConferenceModel.createVoIP(data)).catch((e) => {
logger.error({
name: 'Error on VideoConf.createVoIP',
error: e,
Expand Down
3 changes: 1 addition & 2 deletions apps/meteor/tests/unit/server/lib/freeswitch.tests.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { expect } from 'chai';
import { describe } from 'mocha';

import { settings } from '../../../../app/settings/server/cached';
import { VoipFreeSwitchService } from '../../../../ee/server/local-services/voip-freeswitch/service';

const VoipFreeSwitch = new VoipFreeSwitchService((id) => settings.get(id));
const VoipFreeSwitch = new VoipFreeSwitchService();

// Those tests still need a proper freeswitch environment configured in order to run
// So for now they are being deliberately skipped on CI
Expand Down
2 changes: 1 addition & 1 deletion packages/core-services/src/types/IVideoConfService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,5 @@ export interface IVideoConfService {
params: { callId: VideoConference['_id']; uid: IUser['_id']; rid: IRoom['_id'] },
): Promise<boolean>;
assignDiscussionToConference(callId: VideoConference['_id'], rid: IRoom['_id'] | undefined): Promise<void>;
createVoIP(data: InsertionModel<IVoIPVideoConference>): Promise<IVoIPVideoConference['_id']>;
createVoIP(data: InsertionModel<IVoIPVideoConference>): Promise<IVoIPVideoConference['_id'] | undefined>;
}
4 changes: 4 additions & 0 deletions packages/core-typings/src/voip/IFreeSwitchCall.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import type { IRocketChatRecord } from '../IRocketChatRecord';
import type { IUser } from '../IUser';
import type { IFreeSwitchEventCall, IFreeSwitchEventCaller } from './IFreeSwitchEvent';

export interface IFreeSwitchCall extends IRocketChatRecord {
UUID: string;
channels: string[];
events: IFreeSwitchCallEvent[];
from?: Pick<IUser, '_id' | 'username' | 'name' | 'avatarETag'>;
to?: Pick<IUser, '_id' | 'username' | 'name' | 'avatarETag'>;
forwardedFrom?: Omit<IFreeSwitchCall, 'events'>[];
}

const knownEventTypes = [
Expand Down
1 change: 1 addition & 0 deletions packages/model-typings/src/models/IUsersModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ export interface IUsersModel extends IBaseModel<IUser> {
findAgentsAvailableWithoutBusinessHours(userIds: string[] | null): FindCursor<Pick<ILivechatAgent, '_id' | 'openBusinessHours'>>;
updateLivechatStatusByAgentIds(userIds: string[], status: ILivechatAgentStatus): Promise<UpdateResult>;
findOneByFreeSwitchExtension<T = IUser>(extension: string, options?: FindOptions<IUser>): Promise<T | null>;
findOneByFreeSwitchExtensions<T = IUser>(extensions: string[], options?: FindOptions<IUser>): Promise<T | null>;
setFreeSwitchExtension(userId: string, extension: string | undefined): Promise<UpdateResult>;
findAssignedFreeSwitchExtensions(): FindCursor<string>;
findUsersWithAssignedFreeSwitchExtensions<T = IUser>(options?: FindOptions<IUser>): FindCursor<T>;
Expand Down

0 comments on commit d468365

Please sign in to comment.