Skip to content

Commit

Permalink
[NEW] [Apps-Engine] New Room events (#17487)
Browse files Browse the repository at this point in the history
* Prevent apps from reading all usernames

* Add usernames being added to the room object passed to apps

* Add call to pre-room apps-engine events on DM creation

* Refactor Apps-Engine event triggering

* Add trigger to IPreRoomUserJoined event

* Call IPostRoomCreate event on DM creation

* Add trigger for IPostRoomUserJoined event

* Improve error handling

* Update Apps-Engine version
  • Loading branch information
d-gubert committed May 19, 2020
1 parent 2589cd8 commit 7aabfa4
Show file tree
Hide file tree
Showing 12 changed files with 173 additions and 66 deletions.
4 changes: 4 additions & 0 deletions app/apps/server/bridges/internal.js
Expand Up @@ -6,6 +6,10 @@ export class AppInternalBridge {
}

getUsernamesOfRoomById(roomId) {
if (!roomId) {
return [];
}

const records = Subscriptions.findByRoomIdWhenUsernameExists(roomId, {
fields: {
'u.username': 1,
Expand Down
99 changes: 66 additions & 33 deletions app/apps/server/bridges/listeners.js
@@ -1,10 +1,53 @@
import { AppInterface } from '@rocket.chat/apps-engine/server/compiler';
import { AppInterface } from '@rocket.chat/apps-engine/definition/metadata';

export class AppListenerBridge {
constructor(orch) {
this.orch = orch;
}

async handleEvent(event, ...payload) {
const method = (() => {
switch (event) {
case AppInterface.IPreMessageSentPrevent:
case AppInterface.IPreMessageSentExtend:
case AppInterface.IPreMessageSentModify:
case AppInterface.IPostMessageSent:
case AppInterface.IPreMessageDeletePrevent:
case AppInterface.IPostMessageDeleted:
case AppInterface.IPreMessageUpdatedPrevent:
case AppInterface.IPreMessageUpdatedExtend:
case AppInterface.IPreMessageUpdatedModify:
case AppInterface.IPostMessageUpdated:
return 'messageEvent';
case AppInterface.IPreRoomCreatePrevent:
case AppInterface.IPreRoomCreateExtend:
case AppInterface.IPreRoomCreateModify:
case AppInterface.IPostRoomCreate:
case AppInterface.IPreRoomDeletePrevent:
case AppInterface.IPostRoomDeleted:
case AppInterface.IPreRoomUserJoined:
case AppInterface.IPostRoomUserJoined:
return 'roomEvent';
case AppInterface.IPostExternalComponentOpened:
case AppInterface.IPostExternalComponentClosed:
return 'externalComponentEvent';
/**
* @deprecated please prefer the AppInterface.IPostLivechatRoomClosed event
*/
case AppInterface.ILivechatRoomClosedHandler:
case AppInterface.IPostLivechatRoomStarted:
case AppInterface.IPostLivechatRoomClosed:
case AppInterface.IPostLivechatAgentAssigned:
case AppInterface.IPostLivechatAgentUnassigned:
return 'livechatEvent';
case AppInterface.IUIKitInteractionHandler:
return 'uiKitInteractionEvent';
}
})();

return this[method](event, ...payload);
}

async messageEvent(inte, message) {
const msg = this.orch.getConverters().get('messages').convertMessage(message);
const result = await this.orch.getManager().getListenerManager().executeListener(inte, msg);
Expand All @@ -13,64 +56,54 @@ export class AppListenerBridge {
return result;
}
return this.orch.getConverters().get('messages').convertAppMessage(result);

// try {

// } catch (e) {
// this.orch.debugLog(`${ e.name }: ${ e.message }`);
// this.orch.debugLog(e.stack);
// }
}

async roomEvent(inte, room) {
async roomEvent(inte, room, ...payload) {
const rm = this.orch.getConverters().get('rooms').convertRoom(room);
const result = await this.orch.getManager().getListenerManager().executeListener(inte, rm);

const params = (() => {
switch (inte) {
case AppInterface.IPreRoomUserJoined:
case AppInterface.IPostRoomUserJoined:
const [joiningUser, invitingUser] = payload;
return {
room: rm,
joiningUser: this.orch.getConverters().get('users').convertToApp(joiningUser),
invitingUser: this.orch.getConverters().get('users').convertToApp(invitingUser),
};
default:
return rm;
}
})();

const result = await this.orch.getManager().getListenerManager().executeListener(inte, params);

if (typeof result === 'boolean') {
return result;
}
return this.orch.getConverters().get('rooms').convertAppRoom(result);

// try {

// } catch (e) {
// this.orch.debugLog(`${ e.name }: ${ e.message }`);
// this.orch.debugLog(e.stack);
// }
}

async externalComponentEvent(inte, externalComponent) {
const result = await this.orch.getManager().getListenerManager().executeListener(inte, externalComponent);

return result;
return this.orch.getManager().getListenerManager().executeListener(inte, externalComponent);
}

async uiKitInteractionEvent(inte, action) {
return this.orch.getManager().getListenerManager().executeListener(inte, action);

// try {

// } catch (e) {
// this.orch.debugLog(`${ e.name }: ${ e.message }`);
// this.orch.debugLog(e.stack);
// }
}

async livechatEvent(inte, data) {
switch (inte) {
case AppInterface.IPostLivechatRoomStarted:
case AppInterface.IPostLivechatRoomClosed:
const room = this.orch.getConverters().get('rooms').convertRoom(data);

return this.orch.getManager().getListenerManager().executeListener(inte, room);
case AppInterface.IPostLivechatAgentAssigned:
case AppInterface.IPostLivechatAgentUnassigned:
return this.orch.getManager().getListenerManager().executeListener(inte, {
room: this.orch.getConverters().get('rooms').convertRoom(data.room),
agent: this.orch.getConverters().get('users').convertToApp(data.user),
});
default:
break;
const room = this.orch.getConverters().get('rooms').convertRoom(data);

return this.orch.getManager().getListenerManager().executeListener(inte, room);
}
}
}
1 change: 1 addition & 0 deletions app/apps/server/converters/rooms.js
Expand Up @@ -116,6 +116,7 @@ export class AppRoomsConverter {
customFields: 'customFields',
isWaitingResponse: 'waitingResponse',
isOpen: 'open',
_USERNAMES: '_USERNAMES',
isDefault: (room) => {
const result = !!room.default;
delete room.default;
Expand Down
2 changes: 1 addition & 1 deletion app/apps/server/index.js
@@ -1,3 +1,3 @@
import './cron';

export { Apps } from './orchestrator';
export { Apps, AppEvents } from './orchestrator';
20 changes: 18 additions & 2 deletions app/apps/server/orchestrator.js
@@ -1,5 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { EssentialAppDisabledException } from '@rocket.chat/apps-engine/definition/exceptions';
import { AppInterface } from '@rocket.chat/apps-engine/definition/metadata';
import { AppManager } from '@rocket.chat/apps-engine/server/AppManager';
import { Meteor } from 'meteor/meteor';

import { Logger } from '../../logger';
import { AppsLogsModel, AppsModel, AppsPersistenceModel, Permissions } from '../../models';
Expand All @@ -16,7 +18,6 @@ function isTesting() {
return process.env.TEST_MODE === 'true';
}


class AppServerOrchestrator {
constructor() {
this._isInitialized = false;
Expand Down Expand Up @@ -155,8 +156,23 @@ class AppServerOrchestrator {
return this._manager.updateAppsMarketplaceInfo(apps)
.then(() => this._manager.get());
}

async triggerEvent(event, ...payload) {
if (!this.isLoaded()) {
return;
}

return this.getBridges().getListenerBridge().handleEvent(event, ...payload).catch((error) => {
if (error instanceof EssentialAppDisabledException) {
throw new Meteor.Error('error-essential-app-disabled');
}

throw error;
});
}
}

export const AppEvents = AppInterface;
export const Apps = new AppServerOrchestrator();

settings.addGroup('General', function() {
Expand Down
16 changes: 14 additions & 2 deletions app/lib/server/functions/addUserToRoom.js
@@ -1,8 +1,10 @@
import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions';
import { Meteor } from 'meteor/meteor';

import { Rooms, Subscriptions, Messages } from '../../../models';
import { AppEvents, Apps } from '../../../apps/server';
import { callbacks } from '../../../callbacks';
import { roomTypes, RoomMemberActions } from '../../../utils/server';
import { Messages, Rooms, Subscriptions } from '../../../models';
import { RoomMemberActions, roomTypes } from '../../../utils/server';

export const addUserToRoom = function(rid, user, inviter, silenced) {
const now = new Date();
Expand All @@ -27,6 +29,14 @@ export const addUserToRoom = function(rid, user, inviter, silenced) {
callbacks.run('beforeJoinRoom', user, room);
}

Promise.await(Apps.triggerEvent(AppEvents.IPreRoomUserJoined, room, user, inviter).catch((error) => {
if (error instanceof AppsEngineException) {
throw new Meteor.Error('error-app-prevented', error.message);
}

throw error;
}));

Subscriptions.createWithRoomAndUser(room, user, {
ts: now,
open: true,
Expand Down Expand Up @@ -59,6 +69,8 @@ export const addUserToRoom = function(rid, user, inviter, silenced) {

// Keep the current event
callbacks.run('afterJoinRoom', user, room);

Apps.triggerEvent(AppEvents.IPostRoomUserJoined, room, user, inviter);
});
}

Expand Down
40 changes: 37 additions & 3 deletions app/lib/server/functions/createDirectRoom.js
@@ -1,7 +1,12 @@
import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions';
import { Meteor } from 'meteor/meteor';

import { Apps } from '../../../apps/server';
import { callbacks } from '../../../callbacks/server';
import { Rooms, Subscriptions } from '../../../models/server';
import { settings } from '../../../settings/server';
import { getDefaultSubscriptionPref } from '../../../utils/server';
import { callbacks } from '../../../callbacks/server';


const generateSubscription = (fname, name, user, extra) => ({
alert: false,
Expand Down Expand Up @@ -40,7 +45,7 @@ export const createDirectRoom = function(members, roomExtraData = {}, options =

const isNewRoom = !room;

const rid = room?._id || Rooms.insert({
const roomInfo = {
...uids.length === 2 && { _id: uids.join('') }, // Deprecated: using users' _id to compose the room _id is deprecated
t: 'd',
usernames,
Expand All @@ -49,7 +54,34 @@ export const createDirectRoom = function(members, roomExtraData = {}, options =
ts: new Date(),
uids,
...roomExtraData,
});
};

if (isNewRoom) {
roomInfo._USERNAMES = usernames;

const prevent = Promise.await(Apps.triggerEvent('IPreRoomCreatePrevent', roomInfo).catch((error) => {
if (error instanceof AppsEngineException) {
throw new Meteor.Error('error-app-prevented', error.message);
}

throw error;
}));
if (prevent) {
throw new Meteor.Error('error-app-prevented', 'A Rocket.Chat App prevented the room creation.');
}

let result;
result = Promise.await(Apps.triggerEvent('IPreRoomCreateExtend', roomInfo));
result = Promise.await(Apps.triggerEvent('IPreRoomCreateModify', result));

if (typeof result === 'object') {
Object.assign(roomInfo, result);
}

delete roomInfo._USERNAMES;
}

const rid = room?._id || Rooms.insert(roomInfo);

if (members.length === 1) { // dm to yourself
Subscriptions.upsert({ rid, 'u._id': members[0]._id }, {
Expand Down Expand Up @@ -80,6 +112,8 @@ export const createDirectRoom = function(members, roomExtraData = {}, options =
const insertedRoom = Rooms.findOneById(rid);

callbacks.run('afterCreateDirectRoom', insertedRoom, { members });

Apps.triggerEvent('IPostRoomCreate', insertedRoom);
}

return {
Expand Down
43 changes: 25 additions & 18 deletions app/lib/server/functions/createRoom.js
@@ -1,14 +1,16 @@
import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions';
import { Meteor } from 'meteor/meteor';
import _ from 'underscore';
import s from 'underscore.string';

import { Users, Rooms, Subscriptions } from '../../../models';
import { callbacks } from '../../../callbacks';
import { Apps } from '../../../apps/server';
import { addUserRoles } from '../../../authorization';
import { callbacks } from '../../../callbacks';
import { Rooms, Subscriptions, Users } from '../../../models';
import { getValidRoomName } from '../../../utils';
import { Apps } from '../../../apps/server';
import { createDirectRoom } from './createDirectRoom';


export const createRoom = function(type, name, owner, members = [], readOnly, extraData = {}, options = {}) {
callbacks.run('beforeCreateRoom', { type, name, owner, members, readOnly, extraData, options });

Expand Down Expand Up @@ -62,21 +64,30 @@ export const createRoom = function(type, name, owner, members = [], readOnly, ex
ro: readOnly === true,
};

if (Apps && Apps.isLoaded()) {
const prevent = Promise.await(Apps.getBridges().getListenerBridge().roomEvent('IPreRoomCreatePrevent', room));
if (prevent) {
throw new Meteor.Error('error-app-prevented-creation', 'A Rocket.Chat App prevented the room creation.');
room._USERNAMES = members;

const prevent = Promise.await(Apps.triggerEvent('IPreRoomCreatePrevent', room).catch((error) => {
if (error instanceof AppsEngineException) {
throw new Meteor.Error('error-app-prevented', error.message);
}

let result;
result = Promise.await(Apps.getBridges().getListenerBridge().roomEvent('IPreRoomCreateExtend', room));
result = Promise.await(Apps.getBridges().getListenerBridge().roomEvent('IPreRoomCreateModify', result));
throw error;
}));

if (typeof result === 'object') {
room = Object.assign(room, result);
}
if (prevent) {
throw new Meteor.Error('error-app-prevented', 'A Rocket.Chat App prevented the room creation.');
}

let result;
result = Promise.await(Apps.triggerEvent('IPreRoomCreateExtend', room));
result = Promise.await(Apps.triggerEvent('IPreRoomCreateModify', result));

if (typeof result === 'object') {
Object.assign(room, result);
}

delete room._USERNAMES;

if (type === 'c') {
callbacks.run('beforeCreateChannel', owner, room);
}
Expand Down Expand Up @@ -119,11 +130,7 @@ export const createRoom = function(type, name, owner, members = [], readOnly, ex
callbacks.run('afterCreateRoom', owner, room);
});

if (Apps && Apps.isLoaded()) {
// This returns a promise, but it won't mutate anything about the message
// so, we don't really care if it is successful or fails
Apps.getBridges().getListenerBridge().roomEvent('IPostRoomCreate', room);
}
Apps.triggerEvent('IPostRoomCreate', room);

return {
rid: room._id, // backwards compatible
Expand Down

0 comments on commit 7aabfa4

Please sign in to comment.