Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changeset/quick-impalas-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/apps-engine': patch
'@rocket.chat/meteor': patch
---

Fixes the issue of the lacking MessageUpdater not being available to apps during runtime
3 changes: 1 addition & 2 deletions apps/meteor/app/reactions/server/setReaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,7 @@ export async function executeSetReaction(
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setReaction' });
}

const userAlreadyReacted =
message.reactions && Boolean(message.reactions[reaction]) && message.reactions[reaction].usernames.includes(user.username as string);
const userAlreadyReacted = Boolean(message.reactions?.[reaction]?.usernames?.includes(user.username as string));

// When shouldReact was not informed, toggle the reaction.
if (shouldReact === undefined) {
Expand Down
44 changes: 44 additions & 0 deletions apps/meteor/tests/data/apps/app-packages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,3 +440,47 @@ export class UpdateStatusTextEndpoint extends ApiEndpoint {
```

</details>

#### Message Reaction Test

File name: `message-updater-test_0.0.1.zip`

An app used to test the message reaction updates. Provides a `/msg-update` slashcommand that takes an action of `'add' | 'remove'` and a message id, then adds or removes a reaction in the target message accordingly using the App's bot user.

<details>
<summary>App source code</summary>

```typescript
export class MessageUpdaterTestApp extends App {
protected async extendConfiguration(configuration: IConfigurationExtend, _environmentRead: IEnvironmentRead): Promise<void> {
await configuration.slashCommands.provideSlashCommand(new class UpdateCommand implements ISlashCommand {
command = 'msg-update';
i18nDescription = 'msg-update';
i18nParamsExample = 'msg-update';
providesPreview = false;

constructor(private readonly app: App) { }

public async executor(context: SlashCommandContext, read: IRead, modify: IModify, _http: IHttp, _persis: IPersistence) {
const [action, msgId] = context.getArguments() as ['add' | 'remove', string];

const user = await read.getUserReader().getAppUser();

if (!user) {
this.app.getLogger().error(`Couldn't find app user`);
return;
}

if (action === 'add') {
await modify.getUpdater().getMessageUpdater().addReaction(msgId, user.id, ':+1:');
this.app.getLogger().debug(`Added reaction 👍 to message ${msgId}`);
} else {
await modify.getUpdater().getMessageUpdater().removeReaction(msgId, user.id, ':+1:');
this.app.getLogger().debug(`Removed reaction 👍 from message ${msgId}`);
}
}
}(this));
}
}
```
</details>
2 changes: 2 additions & 0 deletions apps/meteor/tests/data/apps/app-packages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export const appCausingNestedRequests = path.resolve(__dirname, './nested-reques
export const appUpdateStatusTest = path.resolve(__dirname, './update-status-test_0.0.1.zip');

export const appExternalIdTest = path.resolve(__dirname, './external-id-test_0.0.1.zip');

export const messageReactionTest = path.resolve(__dirname, './message-updater-test_0.0.1.zip');
Binary file not shown.
14 changes: 12 additions & 2 deletions apps/meteor/tests/end-to-end/api/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ import { sleep } from '../../../lib/utils/sleep';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { sendSimpleMessage, deleteMessage } from '../../data/chat.helper';
import { imgURL } from '../../data/interactions';
import { getSettingValueById, updateEEPermission, updatePermission, updateSetting } from '../../data/permissions.helper';
import {
getSettingValueById,
restorePermissionToRoles,
updateEEPermission,
updatePermission,
updateSetting,
} from '../../data/permissions.helper';
import { assignRoleToUser, createCustomRole, deleteCustomRole } from '../../data/roles.helper';
import { createRoom, deleteRoom } from '../../data/rooms.helper';
import { createTeam, deleteTeam } from '../../data/teams.helper';
Expand Down Expand Up @@ -3939,7 +3945,11 @@ describe('[Rooms]', () => {
deleteRoom({ type: 'c', roomId: publicChannelInPrivateTeam._id }),
]);

await Promise.all([deleteTeam(credentials, publicTeam.name), deleteTeam(credentials, privateTeam.name)]);
await Promise.all([
deleteTeam(credentials, publicTeam.name),
deleteTeam(credentials, privateTeam.name),
restorePermissionToRoles('view-c-room'),
]);

await Promise.all([deleteUser(outsiderUser), deleteUser(insideUser), deleteUser(nonTeamUser)]);
});
Expand Down
49 changes: 49 additions & 0 deletions apps/meteor/tests/end-to-end/apps/app-message-reactions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';

import { getCredentials } from '../../data/api-data';
import { messageReactionTest } from '../../data/apps/app-packages';
import { cleanupApps, installLocalTestPackage } from '../../data/apps/helper';
import { sendSimpleMessage, getMessageById } from '../../data/chat.helper';
import { createRoom, deleteRoom } from '../../data/rooms.helper';
import { executeAppSlashCommand } from '../../data/slashcommands.helpers';
import { IS_EE } from '../../e2e/config/constants';

(IS_EE ? describe : describe.skip)('Apps - Message Reactions', () => {
let roomId: string;

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

before(async () => {
await cleanupApps();
await installLocalTestPackage(messageReactionTest);

const res = await createRoom({ type: 'c', name: `apps-reactions-test-${Date.now()}` });
roomId = res.body.channel._id;
});

after(async () => {
await deleteRoom({ type: 'c', roomId });
await cleanupApps();
});

it('should add and remove a thumbs-up reaction to a message', async () => {
const sendRes = await sendSimpleMessage({ roomId, text: 'reaction test message' });
const messageId = sendRes.body.message._id;

const slashRes = await executeAppSlashCommand('msg-update', roomId, `add ${messageId}`);
expect(slashRes.status, 'Slash command to add reaction failed').to.equal(200);

let message = await getMessageById({ msgId: messageId });
expect(message.reactions, 'Message reactions should exist after adding').to.exist;
expect(message.reactions, 'Message should have a thumbs-up reaction').to.have.property(':+1:');

// Now remove the reaction
const removeRes = await executeAppSlashCommand('msg-update', roomId, `remove ${messageId}`);
expect(removeRes.status, 'Slash command to remove reaction failed').to.equal(200);

message = await getMessageById({ msgId: messageId });
const hasThumbsUp = Boolean(message.reactions && ':+1:' in message.reactions);
expect(hasThumbsUp, 'Thumbs-up reaction should have been removed').to.be.false;
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { IModifyUpdater } from '@rocket.chat/apps-engine/definition/accessors/IModifyUpdater.ts';
import type { ILivechatUpdater } from '@rocket.chat/apps-engine/definition/accessors/ILivechatUpdater.ts';
import type { IUserUpdater } from '@rocket.chat/apps-engine/definition/accessors/IUserUpdater.ts';
import type { IMessageUpdater } from '@rocket.chat/apps-engine/definition/accessors/IMessageUpdater.ts';
import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder.ts';
import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder.ts';
import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts';
Expand All @@ -27,48 +28,47 @@ const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definit
};

export class ModifyUpdater implements IModifyUpdater {
constructor(private readonly senderFn: typeof Messenger.sendRequest) {}
private readonly livechatUpdater: ILivechatUpdater;
private readonly userUpdater: IUserUpdater;
private readonly messageUpdater: IMessageUpdater;

constructor(private readonly senderFn: typeof Messenger.sendRequest) {
this.livechatUpdater = this.proxify('getLivechatUpdater');
this.userUpdater = this.proxify('getUserUpdater');
this.messageUpdater = this.proxify('getMessageUpdater');
}

public getLivechatUpdater(): ILivechatUpdater {
private proxify<T extends ILivechatUpdater | IUserUpdater | IMessageUpdater>(target: 'getLivechatUpdater' | 'getUserUpdater' | 'getMessageUpdater'): T {
return new Proxy(
{ __kind: 'getLivechatUpdater' },
{ __kind: target },
{
get:
(_target: unknown, prop: string) =>
(...params: unknown[]) =>
prop === 'toJSON'
? {}
: this.senderFn({
method: `accessor:getModifier:getUpdater:getLivechatUpdater:${prop}`,
method: `accessor:getModifier:getUpdater:${target}:${prop}`,
params,
})
.then((response) => response.result)
.catch((err) => {
throw formatErrorResponse(err);
}),
},
) as ILivechatUpdater;
) as T;
}

public getLivechatUpdater(): ILivechatUpdater {
return this.livechatUpdater;
}

public getUserUpdater(): IUserUpdater {
return new Proxy(
{ __kind: 'getUserUpdater' },
{
get:
(_target: unknown, prop: string) =>
(...params: unknown[]) =>
prop === 'toJSON'
? {}
: this.senderFn({
method: `accessor:getModifier:getUpdater:getUserUpdater:${prop}`,
params,
})
.then((response) => response.result)
.catch((err) => {
throw formatErrorResponse(err);
}),
},
) as IUserUpdater;
return this.userUpdater;
}

public getMessageUpdater(): IMessageUpdater {
return this.messageUpdater;
}

public async message(messageId: string, editor: IUser): Promise<IMessageBuilder> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,15 @@ describe('ModifyUpdater', () => {
});
});

it('correctly formats requests to MessageUpdater methods', async () => {
const result = (await modifyUpdater.getMessageUpdater().addReaction('message-id', 'user-id', ':smile:')) as any;

assertEquals(result, {
method: 'accessor:getModifier:getUpdater:getMessageUpdater:addReaction',
params: ['message-id', 'user-id', ':smile:'],
});
});

describe('Error Handling', () => {
describe('message', () => {
it('throws an instance of Error when senderFn throws an error', async () => {
Expand Down
18 changes: 18 additions & 0 deletions packages/apps-engine/src/server/accessors/MessageUpdater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Reaction } from '../../definition/messages';
import type { IMessageUpdater } from '../../definition/accessors/IMessageUpdater';
import type { AppBridges } from '../bridges';

export class MessageUpdater implements IMessageUpdater {
constructor(
private readonly bridges: AppBridges,
private readonly appId: string,
) {}

public async addReaction(messageId: string, userId: string, reaction: Reaction): Promise<void> {
return this.bridges.getMessageBridge().doAddReaction(messageId, userId, reaction, this.appId);
}

public async removeReaction(messageId: string, userId: string, reaction: Reaction): Promise<void> {
return this.bridges.getMessageBridge().doRemoveReaction(messageId, userId, reaction, this.appId);
}
}
2 changes: 2 additions & 0 deletions packages/apps-engine/src/server/accessors/ModifyUpdater.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { LivechatUpdater } from './LivechatUpdater';
import { MessageBuilder } from './MessageBuilder';
import { MessageUpdater } from './MessageUpdater';
import { RoomBuilder } from './RoomBuilder';
import { UserUpdater } from './UserUpdater';
import type { ILivechatUpdater, IMessageBuilder, IMessageUpdater, IModifyUpdater, IRoomBuilder } from '../../definition/accessors';
Expand All @@ -23,6 +24,7 @@ export class ModifyUpdater implements IModifyUpdater {
) {
this.livechatUpdater = new LivechatUpdater(this.bridges, this.appId);
this.userUpdater = new UserUpdater(this.bridges, this.appId);
this.messageUpdater = new MessageUpdater(this.bridges, this.appId);
}

public getLivechatUpdater(): ILivechatUpdater {
Expand Down
Loading