Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
96051a1
feat: allow clients to accept calls before the session is established
pierre-lehnen-rc Feb 12, 2026
a19f3c5
Merge branch 'develop' into feat/accept-media-call-via-rest
pierre-lehnen-rc Feb 13, 2026
7efd36a
cleanup
pierre-lehnen-rc Feb 16, 2026
c5d46ba
chore: Allow clients to register a session after accepting a call
pierre-lehnen-rc Feb 16, 2026
7f15e37
use query to validate callee
pierre-lehnen-rc Feb 26, 2026
57d5163
changeset
pierre-lehnen-rc Feb 26, 2026
a9fcf32
chore: accept additional push tokens intended for voip to iOS devices
pierre-lehnen-rc Feb 27, 2026
8f27671
configure apn and voip tokens on the same request
pierre-lehnen-rc Mar 5, 2026
8a525cb
use API.v1.failure
pierre-lehnen-rc Mar 5, 2026
453508e
this was no longer needed
pierre-lehnen-rc Mar 5, 2026
0eaeee8
feat: send push notifications for voice calls
pierre-lehnen-rc Mar 5, 2026
43b8434
state change notifications
pierre-lehnen-rc Mar 6, 2026
380f0e7
shrink changes
pierre-lehnen-rc Mar 6, 2026
8a14109
use UUIDs
pierre-lehnen-rc Mar 6, 2026
04a504c
Merge branch 'chore/voip-register-after-accept' into mobile-voip-test…
pierre-lehnen-rc Mar 6, 2026
3d14ca6
Merge branch 'feat/accept-media-call-via-rest' into mobile-voip-test-…
pierre-lehnen-rc Mar 6, 2026
d990aca
use UUIDs
pierre-lehnen-rc Mar 6, 2026
27af03b
Merge branch 'feat/voip-send-push' into mobile-voip-test-branch
pierre-lehnen-rc Mar 9, 2026
424406a
add createdAt to voip push notification payload
pierre-lehnen-rc Mar 9, 2026
d689df5
Merge branch 'feat/voip-send-push' into mobile-voip-test-branch
pierre-lehnen-rc Mar 9, 2026
b066854
Merge branch 'develop' into feat/voip-send-push
pierre-lehnen-rc Mar 9, 2026
cb4338f
code review
pierre-lehnen-rc Mar 9, 2026
7096fdf
more changes from code review
pierre-lehnen-rc Mar 9, 2026
021bb35
adjust maxRetries
pierre-lehnen-rc Mar 9, 2026
36adc1f
import meteor explicitly
pierre-lehnen-rc Mar 9, 2026
395f8ad
LSP crashed once again
pierre-lehnen-rc Mar 9, 2026
65c07d6
add caller username to push payload
pierre-lehnen-rc Mar 10, 2026
feffaf7
Merge branch 'feat/voip-send-push' into mobile-voip-test-branch
pierre-lehnen-rc Mar 10, 2026
1109b45
Merge branch 'develop' into feat/voip-send-push
pierre-lehnen-rc Mar 10, 2026
533d805
add call kind to the notification payload
pierre-lehnen-rc Mar 10, 2026
a976955
Update apps/meteor/server/services/media-call/logger.ts
pierre-lehnen-rc Mar 11, 2026
6c3ae5d
handle ack invalid state
pierre-lehnen-rc Mar 16, 2026
2d7482b
Merge branch 'feat/accept-media-call-via-rest' into mobile-voip-test-…
pierre-lehnen-rc Mar 16, 2026
bbd36e2
handle reject validations
pierre-lehnen-rc Mar 16, 2026
98ab09f
Merge branch 'feat/accept-media-call-via-rest' into mobile-voip-test-…
pierre-lehnen-rc Mar 16, 2026
bb0aed3
Merge branch 'develop' into feat/accept-media-call-via-rest
pierre-lehnen-rc Mar 16, 2026
c0f5184
Merge branch 'feat/accept-media-call-via-rest' into mobile-voip-test-…
pierre-lehnen-rc Mar 16, 2026
9ba2421
fix: blank notifications when a voice call ends
pierre-lehnen-rc Mar 19, 2026
ea7c0b4
Merge branch 'develop' into feat/voip-send-push
pierre-lehnen-rc Mar 23, 2026
38d455b
changes to signaling lib to support accepting a call received via push
pierre-lehnen-rc Mar 23, 2026
923b9b9
Merge branch 'develop' into mobile-voip-test-branch
pierre-lehnen-rc Mar 23, 2026
51e4c44
Merge branch 'feat/voip-send-push' into mobile-voip-test-branch
pierre-lehnen-rc Mar 23, 2026
aa49ca4
merge error
pierre-lehnen-rc Mar 23, 2026
62c9d81
Merge branch 'feat/voip-send-push' into mobile-voip-test-branch
pierre-lehnen-rc Mar 23, 2026
a484534
use an agent event for the trying signal
pierre-lehnen-rc Mar 25, 2026
89670cf
add expirationSeconds param to apn notifications
pierre-lehnen-rc Mar 25, 2026
0efacde
Merge branch 'feat/voip-send-push' into mobile-voip-test-branch
pierre-lehnen-rc Mar 25, 2026
c8c1704
Merge branch 'fix-voice-blank-notification' into mobile-voip-test-branch
pierre-lehnen-rc Mar 25, 2026
8301f27
filter report of active calls to list only calls signed to that session
pierre-lehnen-rc Mar 25, 2026
2116ed0
Merge branch 'feat/voip-send-push' into mobile-voip-test-branch
pierre-lehnen-rc Mar 25, 2026
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
6 changes: 6 additions & 0 deletions .changeset/eighty-experts-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/ui-voip': patch
'@rocket.chat/meteor': patch
---

Fixes empty notifications sent when a voice call ends
10 changes: 10 additions & 0 deletions .changeset/fair-lions-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@rocket.chat/media-calls': minor
'@rocket.chat/core-services': minor
'@rocket.chat/model-typings': minor
'@rocket.chat/models': minor
'@rocket.chat/meteor': minor
'@rocket.chat/media-signaling': minor
---

Adds a new REST endpoint to accept or reject media calls without an active media session
1 change: 1 addition & 0 deletions apps/meteor/app/api/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import './v1/integrations';
import './v1/invites';
import './v1/import';
import './v1/ldap';
import './v1/media-calls';
import './v1/misc';
import './v1/permissions';
import './v1/presence';
Expand Down
98 changes: 98 additions & 0 deletions apps/meteor/app/api/server/v1/media-calls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { MediaCall } from '@rocket.chat/core-services';
import type { IMediaCall } from '@rocket.chat/core-typings';
import type { CallAnswer, CallFeature } from '@rocket.chat/media-signaling';
import { callFeatureList, callAnswerList } from '@rocket.chat/media-signaling';
import {
ajv,
validateNotFoundErrorResponse,
validateBadRequestErrorResponse,
validateUnauthorizedErrorResponse,
validateForbiddenErrorResponse,
} from '@rocket.chat/rest-typings';
import type { JSONSchemaType } from 'ajv';

import type { ExtractRoutesFromAPI } from '../ApiClass';
import { API } from '../api';

type MediaCallsAnswer = {
callId: string;
contractId: string;

answer: CallAnswer;

supportedFeatures?: CallFeature[];
};

const MediaCallsAnswerSchema: JSONSchemaType<MediaCallsAnswer> = {
type: 'object',
properties: {
callId: {
type: 'string',
},
contractId: {
type: 'string',
},
answer: {
type: 'string',
enum: callAnswerList,
},
supportedFeatures: {
type: 'array',
items: {
type: 'string',
enum: callFeatureList,
},
nullable: true,
},
},
required: ['callId', 'contractId', 'answer'],
additionalProperties: false,
};

export const isMediaCallsAnswerProps = ajv.compile<MediaCallsAnswer>(MediaCallsAnswerSchema);

const mediaCallsAnswerEndpoints = API.v1.post(
'media-calls.answer',
{
response: {
200: ajv.compile<{
call: IMediaCall;
}>({
additionalProperties: false,
type: 'object',
properties: {
call: {
type: 'object',
$ref: '#/components/schemas/IMediaCall',
description: 'The updated call information.',
},
success: {
type: 'boolean',
description: 'Indicates if the request was successful.',
},
},
required: ['call', 'success'],
}),
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
404: validateNotFoundErrorResponse,
},
body: isMediaCallsAnswerProps,
authRequired: true,
},
async function action() {
const call = await MediaCall.answerCall(this.userId, this.bodyParams);

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

type MediaCallsAnswerEndpoints = ExtractRoutesFromAPI<typeof mediaCallsAnswerEndpoints>;

declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
interface Endpoints extends MediaCallsAnswerEndpoints {}
}
4 changes: 3 additions & 1 deletion apps/meteor/app/api/server/v1/push.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Push } from '@rocket.chat/core-services';
import { pushTokenTypes } from '@rocket.chat/core-typings';
import type { IPushToken, IPushTokenTypes } from '@rocket.chat/core-typings';
import { Messages, PushToken, Users, Rooms, Settings } from '@rocket.chat/models';
import {
Expand Down Expand Up @@ -38,7 +39,7 @@ const PushTokenPOSTSchema: JSONSchemaType<PushTokenPOST> = {
},
type: {
type: 'string',
enum: ['apn', 'gcm'],
enum: pushTokenTypes,
},
value: {
type: 'string',
Expand Down Expand Up @@ -148,6 +149,7 @@ const pushTokenEndpoints = API.v1
},
voipToken: {
type: 'string',
nullable: true,
},
},
additionalProperties: false,
Expand Down
5 changes: 3 additions & 2 deletions apps/meteor/app/lib/server/functions/sendMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import { afterSaveMessage } from '../lib/afterSaveMessage';
import { notifyOnRoomChangedById } from '../lib/notifyListener';
import { validateCustomMessageFields } from '../lib/validateCustomMessageFields';

type SendMessageOptions = {
export type SendMessageOptions = {
upsert?: boolean;
previewUrls?: string[];
skipNotifications?: boolean;
};

// TODO: most of the types here are wrong, but I don't want to change them now
Expand Down Expand Up @@ -289,7 +290,7 @@ export const sendMessage = async function (user: any, message: any, room: any, o
void Apps.self?.triggerEvent(messageEvent, message);
}

await afterSaveMessage(message, room, user);
await afterSaveMessage(message, room, user, { options });

void notifyOnRoomChangedById(message.rid);

Expand Down
41 changes: 36 additions & 5 deletions apps/meteor/app/lib/server/lib/afterSaveMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,28 @@ import type { Updater } from '@rocket.chat/models';
import { Rooms } from '@rocket.chat/models';

import { callbacks } from '../../../../server/lib/callbacks';

export async function afterSaveMessage(message: IMessage, room: IRoom, user: IUser, roomUpdater?: Updater<IRoom>): Promise<IMessage> {
import type { SendMessageOptions } from '../functions/sendMessage';

export async function afterSaveMessage(
message: IMessage,
room: IRoom,
user: IUser,
{
roomUpdater,
options,
}: {
roomUpdater?: Updater<IRoom>;
options?: SendMessageOptions;
} = {},
): Promise<IMessage> {
const updater = roomUpdater ?? Rooms.getUpdater();
const data: IMessage = (await callbacks.run('afterSaveMessage', message, { room, user, roomUpdater: updater })) as unknown as IMessage;

const data: IMessage = (await callbacks.run('afterSaveMessage', message, {
room,
user,
roomUpdater: updater,
options,
})) as unknown as IMessage;

if (!roomUpdater && updater.hasChanges()) {
await Rooms.updateFromUpdater({ _id: room._id }, updater);
Expand All @@ -19,8 +37,21 @@ export async function afterSaveMessage(message: IMessage, room: IRoom, user: IUs
return data;
}

export function afterSaveMessageAsync(message: IMessage, room: IRoom, user: IUser, roomUpdater: Updater<IRoom> = Rooms.getUpdater()): void {
callbacks.runAsync('afterSaveMessage', message, { room, user, roomUpdater });
export function afterSaveMessageAsync(
message: IMessage,
room: IRoom,
user: IUser,
{
roomUpdater: updater,
options,
}: {
roomUpdater?: Updater<IRoom>;
options?: SendMessageOptions;
} = {},
): void {
const roomUpdater = updater ?? Rooms.getUpdater();

callbacks.runAsync('afterSaveMessage', message, { room, user, roomUpdater, options });

if (roomUpdater.hasChanges()) {
void Rooms.updateFromUpdater({ _id: room._id }, roomUpdater);
Expand Down
8 changes: 7 additions & 1 deletion apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,13 @@ settings.watch('Troubleshoot_Disable_Notifications', (value) => {

callbacks.add(
'afterSaveMessage',
(message, { room }) => sendAllNotifications(message, room),
(message, { room, options }) => {
if (options?.skipNotifications) {
return message;
}

return sendAllNotifications(message, room);
},
callbacks.priority.LOW,
'sendNotificationsOnMessage',
);
Expand Down
34 changes: 24 additions & 10 deletions apps/meteor/app/push/server/apn.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import apn from '@parse/node-apn';
import type { IPushToken, RequiredField } from '@rocket.chat/core-typings';
import type { RequiredField } from '@rocket.chat/core-typings';
import EJSON from 'ejson';

import type { PushOptions, PendingPushNotification } from './definition';
Expand All @@ -24,7 +24,7 @@ export const sendAPN = ({
}: {
userToken: string;
notification: PendingPushNotification & { topic: string };
_removeToken: (token: IPushToken['token']) => void;
_removeToken: (token: string) => void;
}) => {
if (!apnConnection) {
throw new Error('Apn Connection not initialized.');
Expand All @@ -34,7 +34,15 @@ export const sendAPN = ({

const note = new apn.Notification();

note.expiry = Math.floor(Date.now() / 1000) + 3600; // Expires 1 hour from now.
// Expires 1 hour from now, unless configured otherwise.
const expirationSeconds = notification.apn?.expirationSeconds ?? 3600;

if (notification.useVoipToken) {
note.pushType = 'voip';
}

note.expiry = Math.floor(Date.now() / 1000) + expirationSeconds;

if (notification.badge !== undefined) {
note.badge = notification.badge;
}
Expand All @@ -50,10 +58,16 @@ export const sendAPN = ({
// adds category support for iOS8 custom actions as described here:
// https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/
// RemoteNotificationsPG/Chapters/IPhoneOSClientImp.html#//apple_ref/doc/uid/TP40008194-CH103-SW36
note.category = notification.apn?.category;
if (notification.apn?.category) {
note.category = notification.apn.category;
}

note.body = notification.text;
note.title = notification.title;
if (notification.text) {
note.body = notification.text;
}
if (notification.title) {
note.title = notification.title;
}

if (notification.notId != null) {
note.threadId = String(notification.notId);
Expand All @@ -62,7 +76,9 @@ export const sendAPN = ({
// Allow the user to set payload data
note.payload = notification.payload ? { ejson: EJSON.stringify(notification.payload) } : {};

note.payload.messageFrom = notification.from;
if (notification.from) {
note.payload.messageFrom = notification.from;
}
note.priority = priority;

note.topic = notification.topic;
Expand All @@ -81,9 +97,7 @@ export const sendAPN = ({
msg: 'Removing APN token',
token: userToken,
});
_removeToken({
apn: userToken,
});
_removeToken(userToken);
}
});
});
Expand Down
8 changes: 5 additions & 3 deletions apps/meteor/app/push/server/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ export type PushOptions = {
};

export type PendingPushNotification = {
from: string;
title: string;
text: string;
from?: string;
title?: string;
text?: string;
badge?: number;
sound?: string;
notId?: number;
apn?: {
category?: string;
expirationSeconds?: number;
};
gcm?: {
style?: string;
Expand All @@ -42,4 +43,5 @@ export type PendingPushNotification = {
priority?: number;

contentAvailable?: 1 | 0;
useVoipToken?: boolean;
};
12 changes: 6 additions & 6 deletions apps/meteor/app/push/server/fcm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import type { NativeNotificationParameters } from './push';
type FCMDataField = Record<string, any>;

type FCMNotificationField = {
title: string;
body: string;
title?: string;
body?: string;
image?: string;
};

Expand Down Expand Up @@ -140,13 +140,13 @@ function getFCMMessagesFromPushData(userTokens: string[], notification: PendingP

// then we will create the notification field
const notificationField: FCMNotificationField = {
title: notification.title,
body: notification.text,
...(notification.title && { title: notification.title }),
...(notification.text && { body: notification.text }),
};

// then we will create the message
const message: FCMMessage = {
notification: notificationField,
...(Object.keys(notificationField).length && { notification: notificationField }),
data,
android: {
priority: 'HIGH',
Expand Down Expand Up @@ -185,7 +185,7 @@ export const sendFCM = function ({ userTokens, notification, _removeToken, optio

const removeToken = () => {
const { token } = fcmRequest.message;
token && _removeToken({ gcm: token });
token && _removeToken(token);
};

const response = fetchWithRetry(url, removeToken, {
Expand Down
Loading
Loading