Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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 @@ -293,3 +293,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 @@ -7,3 +7,5 @@ export const appAPIParameterTest = path.resolve(__dirname, './api-parameter-test
export const appCausingNestedRequests = path.resolve(__dirname, './nested-requests_0.0.1.zip');

export const appUpdateStatusTest = path.resolve(__dirname, './update-status-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