Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1a7bc7e
chore: adds new validations to push notifications
lucas-a-pelegrino Dec 17, 2025
07e8540
chore: removes unnecessary comments
lucas-a-pelegrino Dec 17, 2025
dfc26c4
docs: adds .changeset
lucas-a-pelegrino Dec 17, 2025
c27908f
tests: adds unit testing for PushClass.send function
lucas-a-pelegrino Dec 17, 2025
0b3b24d
fix: proxyquire import path
lucas-a-pelegrino Dec 17, 2025
f525c11
chore: adds minor improvement to push text size limit
lucas-a-pelegrino Dec 17, 2025
f82b3bf
fix: PushClass.send failing tests
lucas-a-pelegrino Dec 17, 2025
412a75f
tests: adds improvements to error assertion
lucas-a-pelegrino Dec 17, 2025
5a206b8
chore: removes .only
lucas-a-pelegrino Dec 17, 2025
857629d
chore: adds minor improvement from code review
lucas-a-pelegrino Dec 17, 2025
999ae4e
chore: removes unused sinon.useFakeTimers
lucas-a-pelegrino Dec 18, 2025
d1ece1e
chores: adds some more improvements to push logic and new helper func…
lucas-a-pelegrino Dec 18, 2025
776e2f4
chore: adds improvements to truncateString
lucas-a-pelegrino Dec 18, 2025
f0aba03
chore: adds improvements from code review
lucas-a-pelegrino Dec 18, 2025
fe1f84f
docs: updates changeset
lucas-a-pelegrino Dec 18, 2025
947a0dd
chore: increases body message and title limits
lucas-a-pelegrino Dec 19, 2025
d1b3abd
Merge branch 'develop' into bugfix/SUP-951
lucas-a-pelegrino Dec 19, 2025
f1d05c7
Merge branch 'develop' into bugfix/SUP-951
kodiakhq[bot] Dec 19, 2025
b2f2d0e
Merge branch 'develop' into bugfix/SUP-951
kodiakhq[bot] Dec 19, 2025
371cc37
Merge branch 'develop' into bugfix/SUP-951
kodiakhq[bot] Dec 20, 2025
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/rotten-pugs-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/tools": patch
---

Adds improvements to the push notifications logic; the logic now truncates messages and titles larger than 240, and 65 characters respectively.
9 changes: 7 additions & 2 deletions apps/meteor/app/push/server/push.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { IAppsTokens, RequiredField, Optional, IPushNotificationConfig } from '@rocket.chat/core-typings';
import { AppsTokens } from '@rocket.chat/models';
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
import { pick } from '@rocket.chat/tools';
import { pick, truncateString } from '@rocket.chat/tools';
import Ajv from 'ajv';
import { JWT } from 'google-auth-library';
import { Match, check } from 'meteor/check';
Expand All @@ -15,6 +15,9 @@ import { settings } from '../../settings/server';

export const _matchToken = Match.OneOf({ apn: String }, { gcm: String });

const PUSH_TITLE_LIMIT = 65;
const PUSH_MESSAGE_BODY_LIMIT = 240;

const ajv = new Ajv({
coerceTypes: true,
});
Expand Down Expand Up @@ -459,8 +462,10 @@ class PushClass {
createdBy: '<SERVER>',
sent: false,
sending: 0,
title: truncateString(options.title, PUSH_TITLE_LIMIT),
text: truncateString(options.text, PUSH_MESSAGE_BODY_LIMIT),

...pick(options, 'from', 'title', 'text', 'userId', 'payload', 'badge', 'sound', 'notId', 'priority'),
...pick(options, 'from', 'userId', 'payload', 'badge', 'sound', 'notId', 'priority'),

...(this.hasApnOptions(options)
? {
Expand Down
112 changes: 112 additions & 0 deletions apps/meteor/tests/unit/app/push/push.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { IPushNotificationConfig } from '@rocket.chat/core-typings/src/IPushNotificationConfig';
import { pick, truncateString } from '@rocket.chat/tools';
import { expect } from 'chai';
import proxyquire from 'proxyquire';
import sinon from 'sinon';

const loggerStub = { debug: sinon.stub(), warn: sinon.stub(), error: sinon.stub(), info: sinon.stub(), log: sinon.stub() };
const settingsStub = { get: sinon.stub().returns('') };

const { Push } = proxyquire.noCallThru().load('../../../../app/push/server/push', {
'./logger': { logger: loggerStub },
'../../settings/server': { settings: settingsStub },
'@rocket.chat/tools': { pick, truncateString },
'meteor/check': {
check: sinon.stub(),
Match: {
Optional: () => sinon.stub(),
Integer: Number,
OneOf: () => sinon.stub(),
test: sinon.stub().returns(true),
},
},
'meteor/meteor': {
Meteor: {
absoluteUrl: sinon.stub().returns('http://localhost'),
},
},
});

describe('Push Notifications [PushClass]', () => {
afterEach(() => {
sinon.restore();
});

describe('send()', () => {
let sendNotificationStub: sinon.SinonStub;
beforeEach(() => {
sendNotificationStub = sinon.stub(Push, 'sendNotification').resolves({ apn: [], gcm: [] });
});

it('should call sendNotification with required fields', async () => {
const options: IPushNotificationConfig = {
from: 'test',
title: 'title',
text: 'body',
userId: 'user1',
apn: { category: 'MESSAGE' },
gcm: { style: 'inbox', image: 'url' },
};

await Push.send(options);

expect(sendNotificationStub.calledOnce).to.be.true;

const notification = sendNotificationStub.firstCall.args[0];
expect(notification.from).to.equal('test');
expect(notification.title).to.equal('title');
expect(notification.text).to.equal('body');
expect(notification.userId).to.equal('user1');
});

it('should truncate text if longer than 240 chars', async () => {
const longText = 'a'.repeat(300);
const options: IPushNotificationConfig = {
from: 'test',
title: 'title',
text: longText,
userId: 'user1',
apn: { category: 'MESSAGE' },
gcm: { style: 'inbox', image: 'url' },
};

await Push.send(options);

const notification = sendNotificationStub.firstCall.args[0];

expect(notification.text.length).to.equal(240);
});

it('should truncate title if longer than 65 chars', async () => {
const longTitle = 'a'.repeat(100);
const options: IPushNotificationConfig = {
from: 'test',
title: longTitle,
text: 'bpdu',
userId: 'user1',
apn: { category: 'MESSAGE' },
gcm: { style: 'inbox', image: 'url' },
};

await Push.send(options);

const notification = sendNotificationStub.firstCall.args[0];

expect(notification.title.length).to.equal(65);
});

it('should throw if userId is missing', async () => {
const options = {
from: 'test',
title: 'title',
text: 'body',
apn: { category: 'MESSAGE' },
gcm: { style: 'inbox', image: 'url' },
};

await expect(Push.send(options)).to.be.rejectedWith('No userId found');

expect(sendNotificationStub.called).to.be.false;
});
});
});
1 change: 1 addition & 0 deletions packages/tools/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export * from './removeEmpty';
export * from './isObject';
export * from './isRecord';
export * from './validateEmail';
export * from './truncateString';
23 changes: 23 additions & 0 deletions packages/tools/src/truncateString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Truncates a string to a specified maximum length, optionally adding ellipses.
* @param str
* @param maxLength
* @param shouldAddEllipses
* @return {string}
*/
export function truncateString(str: string, maxLength: number, shouldAddEllipses = true): string {
const ellipsis = '...';
if (str.length <= maxLength) {
return str;
}

if (shouldAddEllipses && str.length > maxLength) {
if (maxLength <= ellipsis.length) {
return str.slice(0, maxLength);
}

return `${str.slice(0, maxLength - ellipsis.length)}${ellipsis}`;
}

return str.slice(0, maxLength);
}
Loading