Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c7bef7a
refactor: use chat.update endpoint instead of updateMessage method
cardoso Oct 3, 2025
7eb54bb
fix: support e2ee messages
cardoso Oct 3, 2025
03ffb04
fix: schema
cardoso Oct 3, 2025
30947f1
fix: e2ee message editing
cardoso Oct 4, 2025
e4c2c30
chore: reduce branch
cardoso Oct 4, 2025
9e285be
chore: improve readability
cardoso Oct 4, 2025
35c0df3
fix: message edit tests
cardoso Oct 4, 2025
50e314c
fix: e2ee mentions
cardoso Oct 4, 2025
18c90f0
chore: undo unneeded changes
cardoso Oct 5, 2025
f3f17e3
Merge branch 'develop' into refactor/chat.update-ESH-39
cardoso Oct 6, 2025
1de66bb
Merge branch 'develop' into refactor/chat.update-ESH-39
cardoso Oct 6, 2025
b863031
fix: schema in rest-typings
cardoso Oct 6, 2025
a184e69
fix: IChatUpdateText customFields type
cardoso Oct 6, 2025
e1f0430
Merge branch 'develop' into refactor/chat.update-ESH-39
cardoso Oct 6, 2025
ffa2b69
Merge branch 'develop' into refactor/chat.update-ESH-39
cardoso Oct 7, 2025
20ba0ad
Merge branch 'develop' into refactor/chat.update-ESH-39
cardoso Oct 7, 2025
82477c9
Merge branch 'develop' into refactor/chat.update-ESH-39
cardoso Oct 8, 2025
700b159
Merge branch 'develop' into refactor/chat.update-ESH-39
cardoso Oct 8, 2025
60fda4e
chore: use isEncryptedMessageContent
cardoso Oct 8, 2025
b6bbbd3
fix: reject content update for unencrypted messages
cardoso Oct 8, 2025
151581c
fix: async handling in test
cardoso Oct 8, 2025
0748842
Merge branch 'develop' into refactor/chat.update-ESH-39
cardoso Oct 10, 2025
13160a5
Merge branch 'develop' into refactor/chat.update-ESH-39
cardoso Oct 10, 2025
ff1783a
Merge branch 'develop' into refactor/chat.update-ESH-39
cardoso Oct 10, 2025
2d027d4
Merge branch 'develop' into refactor/chat.update-ESH-39
kodiakhq[bot] Oct 10, 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
113 changes: 71 additions & 42 deletions apps/meteor/app/api/server/v1/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,77 @@ const chatEndpoints = API.v1

return API.v1.success();
},
)
.post(
'chat.update',
{
authRequired: true,
body: isChatUpdateProps,
response: {
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
200: ajv.compile<{ message: IMessage }>({
type: 'object',
properties: {
message: { $ref: '#/components/schemas/IMessage' },
success: {
type: 'boolean',
enum: [true],
},
},
required: ['message', 'success'],
additionalProperties: false,
}),
},
},
async function action() {
const { bodyParams } = this;

const msg = await Messages.findOneById(bodyParams.msgId);

// Ensure the message exists
if (!msg) {
return API.v1.failure(`No message found with the id of "${bodyParams.msgId}".`);
}

if (bodyParams.roomId !== msg.rid) {
return API.v1.failure('The room id provided does not match where the message is from.');
}

const hasContent = 'content' in bodyParams;

if (hasContent && msg.t !== 'e2e') {
return API.v1.failure('Only encrypted messages can have content updated.');
}

const updateData: Parameters<typeof executeUpdateMessage> = [
this.userId,
hasContent
? {
_id: msg._id,
rid: msg.rid,
content: bodyParams.content,
...(bodyParams.e2eMentions && { e2eMentions: bodyParams.e2eMentions }),
}
: {
_id: msg._id,
rid: msg.rid,
msg: bodyParams.text,
...(bodyParams.customFields && { customFields: bodyParams.customFields }),
},
'previewUrls' in bodyParams ? bodyParams.previewUrls : undefined,
];

// Permission checks are already done in the updateMessage method, so no need to duplicate them
await applyAirGappedRestrictionsValidation(() => executeUpdateMessage(...updateData));

const updatedMessage = await Messages.findOneById(msg._id);
const [message] = await normalizeMessagesForUser(updatedMessage ? [updatedMessage] : [], this.userId);

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

API.v1.addRoute(
Expand Down Expand Up @@ -421,48 +492,6 @@ API.v1.addRoute(
},
);

API.v1.addRoute(
'chat.update',
{ authRequired: true, validateParams: isChatUpdateProps },
{
async post() {
const msg = await Messages.findOneById(this.bodyParams.msgId);

// Ensure the message exists
if (!msg) {
return API.v1.failure(`No message found with the id of "${this.bodyParams.msgId}".`);
}

if (this.bodyParams.roomId !== msg.rid) {
return API.v1.failure('The room id provided does not match where the message is from.');
}

const msgFromBody = this.bodyParams.text;

// Permission checks are already done in the updateMessage method, so no need to duplicate them
await applyAirGappedRestrictionsValidation(() =>
executeUpdateMessage(
this.userId,
{
_id: msg._id,
msg: msgFromBody,
rid: msg.rid,
...(this.bodyParams.customFields && { customFields: this.bodyParams.customFields }),
},
this.bodyParams.previewUrls,
),
);

const updatedMessage = await Messages.findOneById(msg._id);
const [message] = await normalizeMessagesForUser(updatedMessage ? [updatedMessage] : [], this.userId);

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

API.v1.addRoute(
'chat.react',
{ authRequired: true, validateParams: isChatReactProps },
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/lib/server/functions/updateMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { notifyOnRoomChangedById, notifyOnMessageChange } from '../lib/notifyLis
import { validateCustomMessageFields } from '../lib/validateCustomMessageFields';

export const updateMessage = async function (
message: AtLeast<IMessage, '_id' | 'rid' | 'msg' | 'customFields'>,
message: AtLeast<IMessage, '_id' | 'rid' | 'msg' | 'customFields'> | AtLeast<IMessage, '_id' | 'rid' | 'content'>,
user: IUser,
originalMsg?: IMessage,
previewUrls?: string[],
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/lib/server/methods/updateMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const allowedEditedFields = ['tshow', 'alias', 'attachments', 'avatar', 'emoji',

export async function executeUpdateMessage(
uid: IUser['_id'],
message: AtLeast<IMessage, '_id' | 'rid' | 'msg' | 'customFields'>,
message: AtLeast<IMessage, '_id' | 'rid' | 'msg' | 'customFields'> | AtLeast<IMessage, '_id' | 'rid' | 'content'>,
previewUrls?: string[],
) {
const originalMessage = await Messages.findOneById(message._id);
Expand Down
21 changes: 19 additions & 2 deletions apps/meteor/client/lib/chats/data.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
isEncryptedMessageContent,
isOTRAckMessage,
isOTRMessage,
type IEditedMessage,
Expand Down Expand Up @@ -176,8 +177,24 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage
Messages.state.store({ ...message, rid, ...(tmid && { tmid }) });
};

const updateMessage = async (message: IEditedMessage, previewUrls?: string[]): Promise<void> =>
sdk.call('updateMessage', message, previewUrls);
const updateMessage = async (message: IEditedMessage, previewUrls?: string[]): Promise<void> => {
const params = isEncryptedMessageContent(message)
? {
msgId: message._id,
roomId: message.rid,
content: message.content,
e2eMentions: message.e2eMentions,
}
: {
previewUrls,
msgId: message._id,
roomId: message.rid,
customFields: message.customFields,
text: message.msg,
};

await sdk.rest.post('/v1/chat.update', params);
};

const canDeleteMessage = async (message: IMessage): Promise<boolean> => {
const uid = getUserId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,4 +319,64 @@ test.describe('E2EE Encrypted Channels', () => {
await lastStarredMessage.locator('role=button[name="More"]').click();
await expect(page.locator('role=menuitem[name="Copy link"]')).toHaveClass(/disabled/);
});

test('expect to edit encrypted message', async ({ page }) => {
const channelName = faker.string.uuid();
const originalMessage = 'This is the original encrypted message';
const editedMessage = 'This is the edited encrypted message';

await poHomeChannel.sidenav.createEncryptedChannel(channelName);
await expect(page).toHaveURL(`/group/${channelName}`);
await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible();

await poHomeChannel.content.sendMessage(originalMessage);

await expect(poHomeChannel.content.lastUserMessageBody).toHaveText(originalMessage);
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();

await poHomeChannel.content.openLastMessageMenu();
await poHomeChannel.content.btnOptionEditMessage.click();
await poHomeChannel.content.inputMessage.fill(editedMessage);

await page.keyboard.press('Enter');

await expect(poHomeChannel.content.lastUserMessageBody).toHaveText(editedMessage);
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
});

test('expect to edit encrypted message to include mention', async ({ page }) => {
const channelName = faker.string.uuid();
const originalMessage = 'This is the original encrypted message';
const editedMessage = 'This is the edited encrypted message with a mention to @user1 and #general';
const displayedMessage = 'This is the edited encrypted message with a mention to user1 and general';

await poHomeChannel.sidenav.createEncryptedChannel(channelName);
await expect(page).toHaveURL(`/group/${channelName}`);
await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible();

await poHomeChannel.content.sendMessage(originalMessage);
await expect(poHomeChannel.content.lastUserMessageBody).toHaveText(originalMessage);
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();

await poHomeChannel.content.openLastMessageMenu();
await poHomeChannel.content.btnOptionEditMessage.click();
await poHomeChannel.content.inputMessage.fill(editedMessage);

await page.keyboard.press('Enter');

await expect(poHomeChannel.content.lastUserMessageBody).toHaveText(displayedMessage);
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();

const userMention = page.getByRole('button', {
name: 'user1',
});

await expect(userMention).toBeVisible();

const channelMention = page.getByRole('button', {
name: 'general',
});

await expect(channelMention).toBeVisible();
});
});
10 changes: 2 additions & 8 deletions apps/meteor/tests/e2e/messaging.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,7 @@ test.describe('Messaging', () => {

await test.step('send edited message', async () => {
const editPromise = page.waitForResponse(
(response) =>
/api\/v1\/method.call\/updateMessage/.test(response.url()) &&
response.status() === 200 &&
response.request().method() === 'POST',
(response) => /api\/v1\/chat.update/.test(response.url()) && response.status() === 200 && response.request().method() === 'POST',
);

await poHomeChannel.content.sendMessage('edited msg2', false);
Expand All @@ -187,10 +184,7 @@ test.describe('Messaging', () => {

await test.step('stress test on message editions', async () => {
const editPromise = page.waitForResponse(
(response) =>
/api\/v1\/method.call\/updateMessage/.test(response.url()) &&
response.status() === 200 &&
response.request().method() === 'POST',
(response) => /api\/v1\/chat.update/.test(response.url()) && response.status() === 200 && response.request().method() === 'POST',
);

for (const element of ['edited msg2 a', 'edited msg2 b', 'edited msg2 c', 'edited msg2 d', 'edited msg2 e']) {
Expand Down
15 changes: 11 additions & 4 deletions apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import fs from 'fs/promises';
import { resolve, join, relative } from 'node:path';

import type { Locator, Page } from '@playwright/test';

import { expect } from '../../utils/test';

const FIXTURES_PATH = relative(process.cwd(), resolve(__dirname, '../../fixtures/files'));

export function getFilePath(fileName: string): string {
return join(FIXTURES_PATH, fileName);
}

export class HomeContent {
protected readonly page: Page;

Expand Down Expand Up @@ -379,7 +386,7 @@ export class HomeContent {
}

async dragAndDropTxtFile(): Promise<void> {
const contract = await fs.readFile('./tests/e2e/fixtures/files/any_file.txt', 'utf-8');
const contract = await fs.readFile(getFilePath('any_file.txt'), 'utf-8');
const dataTransfer = await this.page.evaluateHandle((contract) => {
const data = new DataTransfer();
const file = new File([`${contract}`], 'any_file.txt', {
Expand All @@ -395,7 +402,7 @@ export class HomeContent {
}

async dragAndDropLstFile(): Promise<void> {
const contract = await fs.readFile('./tests/e2e/fixtures/files/lst-test.lst', 'utf-8');
const contract = await fs.readFile(getFilePath('lst-test.lst'), 'utf-8');
const dataTransfer = await this.page.evaluateHandle((contract) => {
const data = new DataTransfer();
const file = new File([`${contract}`], 'lst-test.lst', {
Expand All @@ -411,7 +418,7 @@ export class HomeContent {
}

async dragAndDropTxtFileToThread(): Promise<void> {
const contract = await fs.readFile('./tests/e2e/fixtures/files/any_file.txt', 'utf-8');
const contract = await fs.readFile(getFilePath('any_file.txt'), 'utf-8');
const dataTransfer = await this.page.evaluateHandle((contract) => {
const data = new DataTransfer();
const file = new File([`${contract}`], 'any_file.txt', {
Expand All @@ -427,7 +434,7 @@ export class HomeContent {
}

async sendFileMessage(fileName: string): Promise<void> {
await this.page.locator('input[type=file]').setInputFiles(`./tests/e2e/fixtures/files/${fileName}`);
await this.page.locator('input[type=file]').setInputFiles(getFilePath(fileName));
}

async openLastMessageMenu(): Promise<void> {
Expand Down
21 changes: 21 additions & 0 deletions apps/meteor/tests/end-to-end/api/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1734,6 +1734,27 @@ describe('[Chat]', () => {
});
});

it('should fail updating a message with "content" if it is not encrypted', (done) => {
void request
.post(api('chat.update'))
.set(credentials)
.send({
roomId: testChannel._id,
msgId: message._id,
content: {
algorithm: 'rc.v1.aes-sha2',
ciphertext: 'U2FsdGVkX1+u3j0u2+oXg4o3kw5y4t7D9sdfsdff==',
},
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'Only encrypted messages can have content updated.');
})
.end(done);
});

it('should update a message successfully', (done) => {
void request
.post(api('chat.update'))
Expand Down
Loading
Loading