Skip to content
Open
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
9 changes: 9 additions & 0 deletions .changeset/real-teeth-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@rocket.chat/media-calls': minor
'@rocket.chat/model-typings': minor
'@rocket.chat/core-typings': minor
'@rocket.chat/models': minor
'@rocket.chat/meteor': minor
---

Added an endpoint to save SIP metadata on the media calls model
65 changes: 65 additions & 0 deletions apps/meteor/app/api/server/v1/media-calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,68 @@ declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
interface Endpoints extends MediaCallsStateEndpoints {}
}

type MediaCallsInfoParams = {
callId: string;
};

const MediaCallsInfoSchema: JSONSchemaType<MediaCallsInfoParams> = {
type: 'object',
properties: {
callId: {
type: 'string',
},
},
required: ['callId'],
additionalProperties: false,
};

export const isMediaCallsInfoProps = ajv.compile<MediaCallsInfoParams>(MediaCallsInfoSchema);

const mediaCallsInfoEndpoints = API.v1.get(
'media-calls.info',
{
response: {
200: ajv.compile<{
call: IMediaCall;
}>({
additionalProperties: false,
type: 'object',
properties: {
call: {
type: 'object',
$ref: '#/components/schemas/IMediaCall',
},
success: {
type: 'boolean',
description: 'Indicates the request was successful.',
},
},
required: ['call', 'success'],
}),
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
404: validateNotFoundErrorResponse,
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
},
query: isMediaCallsInfoProps,
authRequired: true,
},
async function action() {
const call = await MediaCalls.findOneById(this.queryParams.callId);

if (!call?.uids.includes(this.userId)) {
return API.v1.notFound();
}

return API.v1.success({
call,
});
},
);

type MediaCallsInfoEndpoints = ExtractRoutesFromAPI<typeof mediaCallsInfoEndpoints>;

declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
interface Endpoints extends MediaCallsInfoEndpoints {}
}
2 changes: 2 additions & 0 deletions apps/meteor/server/startup/callHistoryTestData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ export async function addCallHistoryTestData(uid: string, extraUid: string): Pro
activatedAt: new Date(),
uids: [uid],
features: ['audio'],
sipCallId: 'sipCallId3',
},
{
_id: callId4,
Expand Down Expand Up @@ -233,6 +234,7 @@ export async function addCallHistoryTestData(uid: string, extraUid: string): Pro
activatedAt: new Date(),
uids: [uid],
features: ['audio'],
sipCallId: 'sipCallId4',
},
]);
}
114 changes: 114 additions & 0 deletions apps/meteor/tests/end-to-end/api/media-calls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import type { Response } from 'supertest';

import { getCredentials, api, request, credentials } from '../../data/api-data';
import { password } from '../../data/user';
import { createUser, deleteUser, login } from '../../data/users.helper';

describe('[Media Calls]', () => {
let user2: IUser;
let userCredentials: Credentials;

before((done) => getCredentials(done));

before(async () => {
user2 = await createUser();
userCredentials = await login(user2.username, password);
});

after(() => deleteUser(user2));

describe('[/media-calls.info]', () => {
it('should return valid internal call information', async () => {
await request
.get(api('media-calls.info'))
.set(credentials)
.query({
callId: 'rocketchat.internal.call.test',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('call').that.is.an('object');

const { call } = res.body;
expect(call).to.have.property('_id', 'rocketchat.internal.call.test');
expect(call).to.have.property('service', 'webrtc');
expect(call).to.have.property('kind', 'direct');
expect(call).to.have.property('state', 'hangup');
expect(call).to.have.property('ended', true);
expect(call).to.have.property('hangupReason', 'normal');
expect(call).to.have.property('createdBy').that.is.an('object');
expect(call.createdBy).to.have.property('type', 'user');
expect(call).to.have.property('caller').that.is.an('object');
expect(call.caller).to.have.property('type', 'user');
expect(call).to.have.property('callee').that.is.an('object');
expect(call.callee).to.have.property('type', 'user');
expect(call).to.have.property('endedBy').that.is.an('object');
expect(call.endedBy).to.have.property('type', 'user');
expect(call).to.have.property('uids').that.is.an('array');
expect(call.uids).to.have.lengthOf(2);
});
});

it('should return valid external call information', async () => {
await request
.get(api('media-calls.info'))
.set(credentials)
.query({
callId: 'rocketchat.external.call.test.outbound',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('call').that.is.an('object');

const { call } = res.body;
expect(call).to.have.property('_id', 'rocketchat.external.call.test.outbound');
expect(call).to.have.property('service', 'webrtc');
expect(call).to.have.property('kind', 'direct');
expect(call).to.have.property('state', 'hangup');
expect(call).to.have.property('ended', true);
expect(call).to.have.property('hangupReason', 'normal');
expect(call).to.have.property('createdBy').that.is.an('object');
expect(call).to.have.property('sipCallId', 'sipCallId3');
expect(call.createdBy).to.have.property('type', 'user');
expect(call).to.have.property('caller').that.is.an('object');
expect(call.caller).to.have.property('type', 'user');
expect(call).to.have.property('callee').that.is.an('object');
expect(call.callee).to.have.property('type', 'sip');
expect(call).to.have.property('endedBy').that.is.an('object');
expect(call.endedBy).to.have.property('type', 'user');
expect(call).to.have.property('uids').that.is.an('array');
expect(call.uids).to.have.lengthOf(1);
});
});

it('should not return invalid calls', async () => {
await request
.get(api('media-calls.info'))
.set(credentials)
.query({
callId: 'invalid.call',
})
.expect('Content-Type', 'application/json')
.expect(404);
});

it('should not return calls from other users', async () => {
await request
.get(api('media-calls.info'))
.set(userCredentials)
.query({
callId: 'rocketchat.internal.call.test',
})
.expect('Content-Type', 'application/json')
.expect(404);
});
});
});
9 changes: 8 additions & 1 deletion ee/packages/media-calls/src/server/CallDirector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const EXPIRATION_CHECK_TIMEOUT = EXPIRATION_TIME + 1000;
export type CreateCallParams = InternalCallParams & {
callerAgent: IMediaCallAgent;
calleeAgent: IMediaCallAgent;
sipCallId?: string;
};

// expiration checks by call id
Expand Down Expand Up @@ -61,7 +62,12 @@ class MediaCallDirector {
public async acceptCall(
call: MediaCallHeader,
calleeAgent: IMediaCallAgent,
data: { calleeContractId: string; webrtcAnswer?: RTCSessionDescriptionInit; supportedFeatures: CallFeature[] },
data: {
calleeContractId: string;
webrtcAnswer?: RTCSessionDescriptionInit;
supportedFeatures: CallFeature[];
sipCallId?: string;
},
): Promise<boolean> {
logger.debug({ msg: 'MediaCallDirector.acceptCall' });

Expand Down Expand Up @@ -233,6 +239,7 @@ class MediaCallDirector {
...(parentCallId && { parentCallId }),

features: allowedFeatures,
...(params.sipCallId && { sipCallId: params.sipCallId }),
};

logger.debug({ msg: 'creating call', call });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export class IncomingSipCall extends BaseSipCall {
callerAgent,
calleeAgent,
features: SIP_CALL_FEATURES,
sipCallId: req.get('Call-ID'),
});

const negotiationId = await mediaCallDirector.startNewNegotiation(call, 'caller', webrtcOffer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ export class OutgoingSipCall extends BaseSipCall {
calleeContractId: this.session.sessionId,
webrtcAnswer: { type: 'answer', sdp: this.sipDialog.remote.sdp },
supportedFeatures: SIP_CALL_FEATURES,
sipCallId: this.sipDialog.sip?.callId,
});
}

Expand Down
2 changes: 2 additions & 0 deletions packages/core-typings/src/mediaCalls/IMediaCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,6 @@ export interface IMediaCall extends IRocketChatRecord {

/** The list of features that may be used in this call. Values are final once the call is accepted. */
features: string[];

sipCallId?: string;
}
6 changes: 5 additions & 1 deletion packages/model-typings/src/models/IMediaCallsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ export interface IMediaCallsModel extends IBaseModel<IMediaCall> {
options?: FindOptions<T>,
): Promise<T | null>;
startRingingById(callId: string, expiresAt: Date): Promise<UpdateResult>;
acceptCallById(callId: string, data: { calleeContractId: string; supportedFeatures: string[] }, expiresAt: Date): Promise<UpdateResult>;
acceptCallById(
callId: string,
data: { calleeContractId: string; supportedFeatures: string[]; sipCallId?: string },
Comment thread
d-gubert marked this conversation as resolved.
expiresAt: Date,
): Promise<UpdateResult>;
activateCallById(callId: string, expiresAt: Date): Promise<UpdateResult>;
setExpiresAtById(callId: string, expiresAt: Date): Promise<UpdateResult>;
hangupCallById(callId: string, params: { endedBy?: IMediaCall['endedBy']; reason?: string } | undefined): Promise<UpdateResult>;
Expand Down
5 changes: 3 additions & 2 deletions packages/models/src/models/MediaCalls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@ export class MediaCallsRaw extends BaseRaw<IMediaCall> implements IMediaCallsMod

public async acceptCallById(
callId: string,
data: { calleeContractId: string; supportedFeatures: string[] },
data: { calleeContractId: string; supportedFeatures: string[]; sipCallId?: string },
expiresAt: Date,
): Promise<UpdateResult> {
const { calleeContractId } = data;
const { calleeContractId, sipCallId } = data;

return this.updateOne(
{
Expand All @@ -105,6 +105,7 @@ export class MediaCallsRaw extends BaseRaw<IMediaCall> implements IMediaCallsMod
'callee.contractId': calleeContractId,
'acceptedAt': new Date(),
expiresAt,
...(sipCallId && { sipCallId }),
},
$pull: {
features: {
Expand Down
Loading