diff --git a/packages/rocketchat-api/server/v1/commands.js b/packages/rocketchat-api/server/v1/commands.js index 0c3e26d8fe40..21717555cd0b 100644 --- a/packages/rocketchat-api/server/v1/commands.js +++ b/packages/rocketchat-api/server/v1/commands.js @@ -59,7 +59,7 @@ RocketChat.API.v1.addRoute('commands.run', { authRequired: true }, { } if (typeof body.roomId !== 'string') { - return RocketChat.API.v1.failure('The room\'s id where to execute this command must provided and be a string.'); + return RocketChat.API.v1.failure('The room\'s id where to execute this command must be provided and be a string.'); } const cmd = body.command.toLowerCase(); @@ -84,3 +84,81 @@ RocketChat.API.v1.addRoute('commands.run', { authRequired: true }, { return RocketChat.API.v1.success({ result }); } }); + +RocketChat.API.v1.addRoute('commands.preview', { authRequired: true }, { + // Expects these query params: command: 'giphy', params: 'mine', roomId: 'value' + get() { + const query = this.queryParams; + const user = this.getLoggedInUser(); + + if (typeof query.command !== 'string') { + return RocketChat.API.v1.failure('You must provide a command to get the previews from.'); + } + + if (query.params && typeof query.params !== 'string') { + return RocketChat.API.v1.failure('The parameters for the command must be a single string.'); + } + + if (typeof query.roomId !== 'string') { + return RocketChat.API.v1.failure('The room\'s id where the previews are being displayed must be provided and be a string.'); + } + + const cmd = query.command.toLowerCase(); + if (!RocketChat.slashCommands.commands[cmd]) { + return RocketChat.API.v1.failure('The command provided does not exist (or is disabled).'); + } + + // This will throw an error if they can't or the room is invalid + Meteor.call('canAccessRoom', query.roomId, user._id); + + const params = query.params ? query.params : ''; + + let preview; + Meteor.runAsUser(user._id, () => { + preview = Meteor.call('getSlashCommandPreviews', { cmd, params, msg: { rid: query.roomId } }); + }); + + return RocketChat.API.v1.success({ preview }); + }, + // Expects a body format of: { command: 'giphy', params: 'mine', roomId: 'value', previewItem: { id: 'sadf8' type: 'image', value: 'https://dev.null/gif } } + post() { + const body = this.bodyParams; + const user = this.getLoggedInUser(); + + if (typeof body.command !== 'string') { + return RocketChat.API.v1.failure('You must provide a command to run the preview item on.'); + } + + if (body.params && typeof body.params !== 'string') { + return RocketChat.API.v1.failure('The parameters for the command must be a single string.'); + } + + if (typeof body.roomId !== 'string') { + return RocketChat.API.v1.failure('The room\'s id where the preview is being executed in must be provided and be a string.'); + } + + if (typeof body.previewItem === 'undefined') { + return RocketChat.API.v1.failure('The preview item being executed must be provided.'); + } + + if (!body.previewItem.id || !body.previewItem.type || typeof body.previewItem.value === 'undefined') { + return RocketChat.API.v1.failure('The preview item being executed is in the wrong format.'); + } + + const cmd = body.command.toLowerCase(); + if (!RocketChat.slashCommands.commands[cmd]) { + return RocketChat.API.v1.failure('The command provided does not exist (or is disabled).'); + } + + // This will throw an error if they can't or the room is invalid + Meteor.call('canAccessRoom', body.roomId, user._id); + + const params = body.params ? body.params : ''; + + Meteor.runAsUser(user._id, () => { + Meteor.call('executeSlashCommandPreview', { cmd, params, msg: { rid: body.roomId } }, body.previewItem); + }); + + return RocketChat.API.v1.success(); + } +}); diff --git a/packages/rocketchat-apps/client/admin/appInstall.js b/packages/rocketchat-apps/client/admin/appInstall.js index 0369f75254c4..e1e03a37991d 100644 --- a/packages/rocketchat-apps/client/admin/appInstall.js +++ b/packages/rocketchat-apps/client/admin/appInstall.js @@ -65,21 +65,26 @@ Template.appInstall.events({ if (url) { try { t.isInstalling.set(true); + const isUpdating = t.isUpdatingId.get(); let result; - if (t.isUpdatingId.get()) { + if (isUpdating) { result = await RocketChat.API.post(`apps/${ t.isUpdatingId.get() }`, { url }); } else { result = await RocketChat.API.post('apps', { url }); } - FlowRouter.go(`/admin/apps/${ result.app.id }`); + if (result.compilerErrors.length !== 0 || result.app.status === 'compiler_error') { + console.warn(`The App contains errors and could not be ${ isUpdating ? 'updated' : 'installed' }.`); + } else { + FlowRouter.go(`/admin/apps/${ result.app.id }`); + } } catch (err) { console.warn('err', err); - } finally { - t.isInstalling.set(false); } + t.isInstalling.set(false); + return; } @@ -103,19 +108,26 @@ Template.appInstall.events({ t.isInstalling.set(true); try { + const isUpdating = t.isUpdatingId.get(); let result; - if (t.isUpdatingId.get()) { + if (isUpdating) { result = await RocketChat.API.upload(`apps/${ t.isUpdatingId.get() }`, data); } else { result = await RocketChat.API.upload('apps', data); } - FlowRouter.go(`/admin/apps/${ result.app.id }`); + console.log('install result', result); + + if (result.compilerErrors.length !== 0 || result.app.status === 'compiler_error') { + console.warn(`The App contains errors and could not be ${ isUpdating ? 'updated' : 'installed' }.`); + } else { + FlowRouter.go(`/admin/apps/${ result.app.id }`); + } } catch (err) { console.warn('err', err); - } finally { - t.isInstalling.set(false); } + + t.isInstalling.set(false); } }); diff --git a/packages/rocketchat-apps/client/admin/appManage.js b/packages/rocketchat-apps/client/admin/appManage.js index b39022f97e13..108f426357ad 100644 --- a/packages/rocketchat-apps/client/admin/appManage.js +++ b/packages/rocketchat-apps/client/admin/appManage.js @@ -295,11 +295,14 @@ Template.appManage.events({ } const setting = t.settings.get()[this.id]; - setting.value = value; - if (setting.oldValue !== setting.value) { - t.settings.get()[this.id].hasChanged = true; - t.settings.set(t.settings.get()); + if (setting) { + setting.value = value; + + if (setting.oldValue !== setting.value) { + t.settings.get()[this.id].hasChanged = true; + t.settings.set(t.settings.get()); + } } }, 500) }); diff --git a/packages/rocketchat-apps/package.js b/packages/rocketchat-apps/package.js index d1f8b03fc37b..fed27b276893 100644 --- a/packages/rocketchat-apps/package.js +++ b/packages/rocketchat-apps/package.js @@ -87,6 +87,6 @@ Package.onUse(function(api) { Npm.depends({ 'busboy': '0.2.13', - '@rocket.chat/apps-engine': '0.5.11', - '@rocket.chat/apps-ts-definition': '0.9.8' + '@rocket.chat/apps-engine': '0.6.7', + '@rocket.chat/apps-ts-definition': '0.9.13' }); diff --git a/packages/rocketchat-apps/server/bridges/commands.js b/packages/rocketchat-apps/server/bridges/commands.js index adec9f715ca5..cdfffd0609da 100644 --- a/packages/rocketchat-apps/server/bridges/commands.js +++ b/packages/rocketchat-apps/server/bridges/commands.js @@ -73,13 +73,16 @@ export class AppCommandsBridge { item.params = command.paramsExample ? command.paramsExample : item.params; item.description = command.i18nDescription ? command.i18nDescription : item.params; item.callback = this._appCommandExecutor.bind(this); + item.providesPreview = command.providesPreview; + item.previewer = command.previewer ? this._appCommandPreviewer.bind(this) : item.previewer; + item.previewCallback = command.executePreviewItem ? this._appCommandPreviewExecutor.bind(this) : item.previewCallback; RocketChat.slashCommands.commands[cmd] = item; this.orch.getNotifier().commandUpdated(cmd); } registerCommand(command, appId) { - console.log(`The App ${ appId } is registering the command: "${ command.command }"`); + console.log(`The App ${ appId } is registerin the command: "${ command.command }"`); this._verifyCommand(command); @@ -87,7 +90,10 @@ export class AppCommandsBridge { command: command.command.toLowerCase(), params: command.paramsExample, description: command.i18nDescription, - callback: this._appCommandExecutor.bind(this) + callback: this._appCommandExecutor.bind(this), + providesPreview: command.providesPreview, + previewer: !command.previewer ? undefined : this._appCommandPreviewer.bind(this), + previewCallback: !command.executePreviewItem ? undefined : this._appCommandPreviewExecutor.bind(this) }; RocketChat.slashCommands.commands[command.command.toLowerCase()] = item; @@ -117,7 +123,7 @@ export class AppCommandsBridge { throw new Error('Invalid Slash Command parameter provided, it must be a valid ISlashCommand object.'); } - if (command.paramsExample && typeof command.paramsExample !== 'string') { + if (command.i18nParamsExample && typeof command.i18nParamsExample !== 'string') { throw new Error('Invalid Slash Command parameter provided, it must be a valid ISlashCommand object.'); } @@ -125,6 +131,10 @@ export class AppCommandsBridge { throw new Error('Invalid Slash Command parameter provided, it must be a valid ISlashCommand object.'); } + if (typeof command.providesPreview !== 'boolean') { + throw new Error('Invalid Slash Command parameter provided, it must be a valid ISlashCommand object.'); + } + if (typeof command.executor !== 'function') { throw new Error('Invalid Slash Command parameter provided, it must be a valid ISlashCommand object.'); } @@ -136,6 +146,24 @@ export class AppCommandsBridge { const params = parameters.length === 0 || parameters === ' ' ? [] : parameters.split(' '); const context = new SlashCommandContext(Object.freeze(user), Object.freeze(room), Object.freeze(params)); - this.orch.getManager().getCommandManager().executeCommand(command, context); + Promise.await(this.orch.getManager().getCommandManager().executeCommand(command, context)); + } + + _appCommandPreviewer(command, parameters, message) { + const user = this.orch.getConverters().get('users').convertById(Meteor.userId()); + const room = this.orch.getConverters().get('rooms').convertById(message.rid); + const params = parameters.length === 0 || parameters === ' ' ? [] : parameters.split(' '); + + const context = new SlashCommandContext(Object.freeze(user), Object.freeze(room), Object.freeze(params)); + return Promise.await(this.orch.getManager().getCommandManager().getPreviews(command, context)); + } + + _appCommandPreviewExecutor(command, parameters, message, preview) { + const user = this.orch.getConverters().get('users').convertById(Meteor.userId()); + const room = this.orch.getConverters().get('rooms').convertById(message.rid); + const params = parameters.length === 0 || parameters === ' ' ? [] : parameters.split(' '); + + const context = new SlashCommandContext(Object.freeze(user), Object.freeze(room), Object.freeze(params)); + Promise.await(this.orch.getManager().getCommandManager().executePreview(command, preview, context)); } } diff --git a/packages/rocketchat-apps/server/bridges/rooms.js b/packages/rocketchat-apps/server/bridges/rooms.js index 28782d432940..d0bece2d7e8e 100644 --- a/packages/rocketchat-apps/server/bridges/rooms.js +++ b/packages/rocketchat-apps/server/bridges/rooms.js @@ -43,6 +43,30 @@ export class AppRoomBridge { return this.orch.getConverters().get('rooms').convertByName(roomName); } + async getCreatorById(roomId, appId) { + console.log(`The App ${ appId } is getting the room's creator by id: "${ roomId }"`); + + const room = RocketChat.models.Rooms.findOneById(roomId); + + if (!room || !room.u || !room.u._id) { + return undefined; + } + + return this.orch.getConverters().get('users').convertById(room.u._id); + } + + async getCreatorByName(roomName, appId) { + console.log(`The App ${ appId } is getting the room's creator by name: "${ roomName }"`); + + const room = RocketChat.models.Rooms.findOneByName(roomName); + + if (!room || !room.u || !room.u._id) { + return undefined; + } + + return this.orch.getConverters().get('users').convertById(room.u._id); + } + async update(room, appId) { console.log(`The App ${ appId } is updating a room.`); diff --git a/packages/rocketchat-apps/server/communication/rest.js b/packages/rocketchat-apps/server/communication/rest.js index d5414b1676c2..2c4427447aa7 100644 --- a/packages/rocketchat-apps/server/communication/rest.js +++ b/packages/rocketchat-apps/server/communication/rest.js @@ -73,7 +73,13 @@ export class AppsRestApi { const aff = Promise.await(manager.add(buff.toString('base64'), false)); const info = aff.getAppInfo(); - info.status = aff.getApp().getStatus(); + + // If there are compiler errors, there won't be an App to get the status of + if (aff.getApp()) { + info.status = aff.getApp().getStatus(); + } else { + info.status = 'compiler_error'; + } return RocketChat.API.v1.success({ app: info, @@ -134,7 +140,13 @@ export class AppsRestApi { const aff = Promise.await(manager.update(buff.toString('base64'))); const info = aff.getAppInfo(); - info.status = aff.getApp().getStatus(); + + // Should the updated version have compiler errors, no App will be returned + if (aff.getApp()) { + info.status = aff.getApp().getStatus(); + } else { + info.status = 'compiler_error'; + } return RocketChat.API.v1.success({ app: info, diff --git a/packages/rocketchat-apps/server/storage/logs-storage.js b/packages/rocketchat-apps/server/storage/logs-storage.js index a184526883bc..d499b930cfcd 100644 --- a/packages/rocketchat-apps/server/storage/logs-storage.js +++ b/packages/rocketchat-apps/server/storage/logs-storage.js @@ -48,4 +48,16 @@ export class AppRealLogsStorage extends AppLogStorage { resolve(docs); }); } + + removeEntriesFor(appId) { + return new Promise((resolve, reject) => { + try { + this.db.remove({ appId }); + } catch (e) { + return reject(e); + } + + resolve(); + }); + } } diff --git a/packages/rocketchat-lib/lib/slashCommand.js b/packages/rocketchat-lib/lib/slashCommand.js index 69bb56b56b48..58fc0032d35e 100644 --- a/packages/rocketchat-lib/lib/slashCommand.js +++ b/packages/rocketchat-lib/lib/slashCommand.js @@ -2,7 +2,7 @@ RocketChat.slashCommands = { commands: {} }; -RocketChat.slashCommands.add = function(command, callback, options = {}, result) { +RocketChat.slashCommands.add = function _addingSlashCommand(command, callback, options = {}, result, providesPreview = false, previewer, previewCallback) { RocketChat.slashCommands.commands[command] = { command, callback, @@ -10,13 +10,57 @@ RocketChat.slashCommands.add = function(command, callback, options = {}, result) description: options.description, permission: options.permission, clientOnly: options.clientOnly || false, - result + result, + providesPreview, + previewer, + previewCallback }; }; -RocketChat.slashCommands.run = function(command, params, item) { - if (RocketChat.slashCommands.commands[command] && RocketChat.slashCommands.commands[command].callback) { - return RocketChat.slashCommands.commands[command].callback(command, params, item); +RocketChat.slashCommands.run = function _runningSlashCommand(command, params, message) { + if (RocketChat.slashCommands.commands[command] && typeof RocketChat.slashCommands.commands[command].callback === 'function') { + if (!message || !message.rid) { + throw new Meteor.Error('invalid-command-usage', 'Executing a command requires at least a message with a room id.'); + } + + return RocketChat.slashCommands.commands[command].callback(command, params, message); + } +}; + +RocketChat.slashCommands.getPreviews = function _gettingSlashCommandPreviews(command, params, message) { + if (RocketChat.slashCommands.commands[command] && typeof RocketChat.slashCommands.commands[command].previewer === 'function') { + if (!message || !message.rid) { + throw new Meteor.Error('invalid-command-usage', 'Executing a command requires at least a message with a room id.'); + } + + // { i18nTitle, items: [{ id, type, value }] } + const previewInfo = RocketChat.slashCommands.commands[command].previewer(command, params, message); + + if (typeof previewInfo !== 'object' || !Array.isArray(previewInfo.items) || previewInfo.items.length === 0) { + return; + } + + // A limit of ten results, to save time and bandwidth + if (previewInfo.items.length >= 10) { + previewInfo.items = previewInfo.items.slice(0, 10); + } + + return previewInfo; + } +}; + +RocketChat.slashCommands.executePreview = function _executeSlashCommandPreview(command, params, message, preview) { + if (RocketChat.slashCommands.commands[command] && typeof RocketChat.slashCommands.commands[command].previewCallback === 'function') { + if (!message || !message.rid) { + throw new Meteor.Error('invalid-command-usage', 'Executing a command requires at least a message with a room id.'); + } + + // { id, type, value } + if (!preview.id || !preview.type || !preview.value) { + throw new Meteor.Error('error-invalid-preview', 'Preview Item must have an id, type, and value.'); + } + + return RocketChat.slashCommands.commands[command].previewCallback(command, params, message, preview); } }; @@ -28,6 +72,12 @@ Meteor.methods({ }); } + if (!command || !command.cmd || !RocketChat.slashCommands.commands[command.cmd]) { + throw new Meteor.Error('error-invalid-command', 'Invalid Command Provided', { + method: 'executeSlashCommandPreview' + }); + } + return RocketChat.slashCommands.run(command.cmd, command.params, command.msg); } }); diff --git a/packages/rocketchat-lib/package.js b/packages/rocketchat-lib/package.js index cfc092292f67..f17aafd3b1e3 100644 --- a/packages/rocketchat-lib/package.js +++ b/packages/rocketchat-lib/package.js @@ -159,6 +159,7 @@ Package.onUse(function(api) { api.addFiles('server/methods/createPrivateGroup.js', 'server'); api.addFiles('server/methods/deleteMessage.js', 'server'); api.addFiles('server/methods/deleteUserOwnAccount.js', 'server'); + api.addFiles('server/methods/executeSlashCommandPreview.js', 'server'); api.addFiles('server/methods/filterBadWords.js', ['server']); api.addFiles('server/methods/filterATAllTag.js', 'server'); api.addFiles('server/methods/filterATHereTag.js', 'server'); @@ -168,6 +169,7 @@ Package.onUse(function(api) { api.addFiles('server/methods/getRoomRoles.js', 'server'); api.addFiles('server/methods/getServerInfo.js', 'server'); api.addFiles('server/methods/getSingleMessage.js', 'server'); + api.addFiles('server/methods/getSlashCommandPreviews.js', 'server'); api.addFiles('server/methods/getUserRoles.js', 'server'); api.addFiles('server/methods/insertOrUpdateUser.js', 'server'); api.addFiles('server/methods/joinDefaultChannels.js', 'server'); diff --git a/packages/rocketchat-lib/server/functions/deleteMessage.js b/packages/rocketchat-lib/server/functions/deleteMessage.js index eb7d85cac91d..080686c0ae1d 100644 --- a/packages/rocketchat-lib/server/functions/deleteMessage.js +++ b/packages/rocketchat-lib/server/functions/deleteMessage.js @@ -2,7 +2,14 @@ RocketChat.deleteMessage = function(message, user) { const keepHistory = RocketChat.settings.get('Message_KeepHistory'); const showDeletedStatus = RocketChat.settings.get('Message_ShowDeletedStatus'); - let deletedMsg; + const deletedMsg = RocketChat.models.Messages.findOneById(message._id); + + if (deletedMsg && Apps && Apps.isLoaded()) { + const prevent = Promise.await(Apps.getBridges().getListenerBridge().messageEvent('IPreMessageDeletePrevent', deletedMsg)); + if (prevent) { + throw new Meteor.Error('error-app-prevented-deleting', 'A Rocket.Chat App prevented the message deleting.'); + } + } if (keepHistory) { if (showDeletedStatus) { @@ -16,7 +23,6 @@ RocketChat.deleteMessage = function(message, user) { } } else { if (!showDeletedStatus) { - deletedMsg = RocketChat.models.Messages.findOneById(message._id); RocketChat.models.Messages.removeById(message._id); } @@ -26,7 +32,7 @@ RocketChat.deleteMessage = function(message, user) { } Meteor.defer(function() { - RocketChat.callbacks.run('afterDeleteMessage', deletedMsg || { _id: message._id }); + RocketChat.callbacks.run('afterDeleteMessage', deletedMsg); }); // update last message @@ -43,4 +49,8 @@ RocketChat.deleteMessage = function(message, user) { } else { RocketChat.Notifications.notifyRoom(message.rid, 'deleteMessage', { _id: message._id }); } + + if (Apps && Apps.isLoaded()) { + Apps.getBridges().getListenerBridge().messageEvent('IPostMessageDeleted', deletedMsg); + } }; diff --git a/packages/rocketchat-lib/server/methods/executeSlashCommandPreview.js b/packages/rocketchat-lib/server/methods/executeSlashCommandPreview.js new file mode 100644 index 000000000000..9fa9ec8ae792 --- /dev/null +++ b/packages/rocketchat-lib/server/methods/executeSlashCommandPreview.js @@ -0,0 +1,30 @@ +Meteor.methods({ + executeSlashCommandPreview(command, preview) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'getSlashCommandPreview' + }); + } + + if (!command || !command.cmd || !RocketChat.slashCommands.commands[command.cmd]) { + throw new Meteor.Error('error-invalid-command', 'Invalid Command Provided', { + method: 'executeSlashCommandPreview' + }); + } + + const theCmd = RocketChat.slashCommands.commands[command.cmd]; + if (!theCmd.providesPreview) { + throw new Meteor.Error('error-invalid-command', 'Command Does Not Provide Previews', { + method: 'executeSlashCommandPreview' + }); + } + + if (!preview) { + throw new Meteor.Error('error-invalid-command-preview', 'Invalid Preview Provided', { + method: 'executeSlashCommandPreview' + }); + } + + return RocketChat.slashCommands.executePreview(command.cmd, command.params, command.msg, preview); + } +}); diff --git a/packages/rocketchat-lib/server/methods/getSlashCommandPreviews.js b/packages/rocketchat-lib/server/methods/getSlashCommandPreviews.js new file mode 100644 index 000000000000..953eb46c74a3 --- /dev/null +++ b/packages/rocketchat-lib/server/methods/getSlashCommandPreviews.js @@ -0,0 +1,24 @@ +Meteor.methods({ + getSlashCommandPreviews(command) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'getSlashCommandPreview' + }); + } + + if (!command || !command.cmd || !RocketChat.slashCommands.commands[command.cmd]) { + throw new Meteor.Error('error-invalid-command', 'Invalid Command Provided', { + method: 'executeSlashCommandPreview' + }); + } + + const theCmd = RocketChat.slashCommands.commands[command.cmd]; + if (!theCmd.providesPreview) { + throw new Meteor.Error('error-invalid-command', 'Command Does Not Provide Previews', { + method: 'executeSlashCommandPreview' + }); + } + + return RocketChat.slashCommands.getPreviews(command.cmd, command.params, command.msg); + } +}); diff --git a/packages/rocketchat-ui-message/client/messageBox.html b/packages/rocketchat-ui-message/client/messageBox.html index 39bc62295fbf..5468e2be970e 100644 --- a/packages/rocketchat-ui-message/client/messageBox.html +++ b/packages/rocketchat-ui-message/client/messageBox.html @@ -21,6 +21,8 @@ {{/with}} {{#if allowedToSend}} {{> messagePopupConfig getPopupConfig}} + {{> messagePopupSlashCommandPreview getPopupConfig}} + {{#if dataReply}} {{#with dataReply}}
diff --git a/packages/rocketchat-ui-message/client/popup/messagePopupSlashCommandPreview.html b/packages/rocketchat-ui-message/client/popup/messagePopupSlashCommandPreview.html new file mode 100644 index 000000000000..8457fc8c93fe --- /dev/null +++ b/packages/rocketchat-ui-message/client/popup/messagePopupSlashCommandPreview.html @@ -0,0 +1,51 @@ + diff --git a/packages/rocketchat-ui-message/client/popup/messagePopupSlashCommandPreview.js b/packages/rocketchat-ui-message/client/popup/messagePopupSlashCommandPreview.js new file mode 100644 index 000000000000..0e61b538b6d1 --- /dev/null +++ b/packages/rocketchat-ui-message/client/popup/messagePopupSlashCommandPreview.js @@ -0,0 +1,256 @@ +import _ from 'underscore'; + +const keys = { + TAB: 9, + ENTER: 13, + ESC: 27, + ARROW_LEFT: 37, + ARROW_UP: 38, + ARROW_RIGHT: 39, + ARROW_DOWN: 40 +}; + +function getCursorPosition(input) { + if (!input) { + return; + } + + if (input.selectionStart) { + return input.selectionStart; + } else if (document.selection) { + input.focus(); + const sel = document.selection.createRange(); + const selLen = document.selection.createRange().text.length; + sel.moveStart('character', -input.value.length); + return sel.text.length - selLen; + } +} + +Template.messagePopupSlashCommandPreview.onCreated(function() { + this.open = new ReactiveVar(false); + this.isLoading = new ReactiveVar(true); + this.preview = new ReactiveVar(); + this.selectedItem = new ReactiveVar(); + this.commandName = new ReactiveVar(''); + this.commandArgs = new ReactiveVar(''); + + // regex ensures a command is entered into the input + // such as "/testing " before continuing + this.matchSelectorRegex = /(?:^)(\/[\w\d\S]+ )[^]*$/; + this.selectorRegex = /(\/[\w\d\S]+ )([^]*)$/; + this.replaceRegex = /(\/[\w\d\S]+ )[^]*$/; // WHAT'S THIS + + const template = this; + template.fetchPreviews = _.debounce(function _previewFetcher(cmd, args) { + const command = cmd; + const params = args; + Meteor.call('getSlashCommandPreviews', { cmd, params, msg: { rid: Session.get('openedRoom') } }, function(err, preview) { + if (err) { + return; + } + + if (!preview || !Array.isArray(preview.items) || preview.items.length === 0) { + // TODO: Display no results found + template.open.set(false); + return; + } + + template.preview.set(preview); + template.commandName.set(command); + template.commandArgs.set(params); + template.isLoading.set(false); + + Meteor.defer(function() { + template.verifySelection(); + }); + }); + }, 500); + + template.enterKeyAction = () => { + const current = template.find('.popup-item.selected'); + + if (!current) { + return; + } + + const selectedId = current.getAttribute('data-id'); + + if (!selectedId) { + return; + } + + const cmd = template.commandName.curValue; + const params = template.commandArgs.curValue; + + if (!cmd || !params) { + return; + } + + const item = template.preview.curValue.items.find((i) => i.id === selectedId); + + if (!item) { + return; + } + + Meteor.call('executeSlashCommandPreview', { cmd, params, msg: { rid: Session.get('openedRoom') } }, item, function(err) { + if (err) { + console.warn(err); + } + }); + + template.open.set(false); + template.inputBox.value = ''; + template.preview.set(); + template.commandName.set(''); + template.commandArgs.set(''); + }; + + template.verifySelection = () => { + const current = template.find('.popup-item.selected'); + + if (!current) { + const first = template.find('.popup-item'); + + if (first) { + first.className += ' selected sidebar-item__popup-active'; + } + } + }; + + // Typing data + template.onInputKeyup = (event) => { + if (template.open.curValue === true && event.which === keys.ESC) { + template.open.set(false); + $('.toolbar').css('display', 'none'); + event.preventDefault(); + event.stopPropagation(); + return; + } + + if (event.which === keys.ARROW_UP || event.which === keys.ARROW_DOWN) { + // Arrow up and down are for navigating the choices + return; + } + + const inputValueAtCursor = template.inputBox.value.substr(0, getCursorPosition(template.inputBox)); + + if (!template.matchSelectorRegex.test(inputValueAtCursor)) { + template.open.set(false); + return; + } + + const matches = inputValueAtCursor.match(template.selectorRegex); + const cmd = matches[1].replace('/', '').trim().toLowerCase(); + const command = RocketChat.slashCommands.commands[cmd]; + + // Ensure the command they're typing actually exists + // And it provides a command preview + // And if it provides a permission to check, they have permission to run the command + if (!command || !command.providesPreview || (command.permission && !RocketChat.authz.hasAtLeastOnePermission(command.permission, Session.get('openedRoom')))) { + template.open.set(false); + return; + } + + const args = matches[2]; + + // Check to verify there are no additional arguments passed, + // Because we we don't care about what it if there isn't + if (!args) { + template.open.set(false); + return; + } + + // If they haven't changed a thing, show what we already got + if (template.commandName.curValue === cmd && template.commandArgs.curValue === args && template.preview.curValue) { + template.isLoading.set(false); + template.open.set(true); + return; + } + + template.isLoading.set(true); + template.open.set(true); + + // Fetch and display them + template.fetchPreviews(cmd, args); + }; + + // Using the keyboard to navigate the options + template.onInputKeydown = (event) => { + if (!template.open.curValue || template.isLoading.curValue) { + return; + } + + if (event.which === keys.ENTER) { // || event.which === keys.TAB) { <-- does using tab to select make sense? + template.enterKeyAction(); + event.preventDefault(); + event.stopPropagation(); + return; + } + + if (event.which === keys.ARROW_UP) { + template.up(); + event.preventDefault(); + event.stopPropagation(); + return; + } + + if (event.which === keys.ARROW_DOWN) { + template.down(); + event.preventDefault(); + event.stopPropagation(); + } + }; + + template.up = () => { + const current = template.find('.popup-item.selected'); + const previous = $(current).prev('.popup-item')[0] || template.find('.popup-item:last-child'); + + if (previous != null) { + current.className = current.className.replace(/\sselected/, '').replace('sidebar-item__popup-active', ''); + previous.className += ' selected sidebar-item__popup-active'; + } + }; + + template.down = () => { + const current = template.find('.popup-item.selected'); + const next = $(current).next('.popup-item')[0] || template.find('.popup-item'); + + if (next && next.classList.contains('popup-item')) { + current.className = current.className.replace(/\sselected/, '').replace('sidebar-item__popup-active', ''); + next.className += ' selected sidebar-item__popup-active'; + } + }; +}); + +Template.messagePopupSlashCommandPreview.onRendered(function _messagePopupSlashCommandPreviewRendered() { + if (!this.data.getInput) { + throw Error('Somethign wrong happened.'); + } + + this.inputBox = this.data.getInput(); + $(this.inputBox).on('keyup', this.onInputKeyup.bind(this)); + $(this.inputBox).on('keydown', this.onInputKeydown.bind(this)); +}); + +Template.messagePopupSlashCommandPreview.onDestroyed(function() { + $(this.inputBox).off('keyup', this.onInputKeyup); + $(this.inputBox).off('keydown', this.onInputKeydown); +}); + +Template.messagePopupSlashCommandPreview.helpers({ + isOpen() { + return Template.instance().open.get(); // && ((Template.instance().hasData.get() || (Template.instance().data.emptyTemplate != null)) || !Template.instance().parentTemplate(1).subscriptionsReady()); + }, + getArgs() { + return Template.instance().commandArgs.get(); + }, + isLoading() { + return Template.instance().isLoading.get(); + }, + isType(actual, expected) { + return actual === expected; + }, + preview() { + return Template.instance().preview.get(); + } +}); diff --git a/packages/rocketchat-ui-message/package.js b/packages/rocketchat-ui-message/package.js index 1f4baec67459..769e66d373f5 100644 --- a/packages/rocketchat-ui-message/package.js +++ b/packages/rocketchat-ui-message/package.js @@ -29,6 +29,8 @@ Package.onUse(function(api) { api.addFiles('client/popup/messagePopupConfig.html', 'client'); api.addFiles('client/popup/messagePopupEmoji.html', 'client'); api.addFiles('client/popup/messagePopupSlashCommand.html', 'client'); + api.addFiles('client/popup/messagePopupSlashCommandPreview.html', 'client'); + api.addFiles('client/popup/messagePopupSlashCommandPreview.js', 'client'); api.addFiles('client/popup/messagePopupUser.html', 'client'); api.addFiles('client/message.js', 'client'); diff --git a/server/methods/eraseRoom.js b/server/methods/eraseRoom.js index a9ac97d94ed3..83a09918d3aa 100644 --- a/server/methods/eraseRoom.js +++ b/server/methods/eraseRoom.js @@ -18,10 +18,23 @@ Meteor.methods({ }); } + if (Apps && Apps.isLoaded()) { + const prevent = Promise.await(Apps.getBridges().getListenerBridge().roomEvent('IPreRoomDeletePrevent', room)); + if (prevent) { + throw new Meteor.Error('error-app-prevented-deleting', 'A Rocket.Chat App prevented the room erasing.'); + } + } + if (RocketChat.roomTypes.roomTypes[room.t].canBeDeleted(room)) { RocketChat.models.Messages.removeByRoomId(rid); RocketChat.models.Subscriptions.removeByRoomId(rid); - return RocketChat.models.Rooms.removeById(rid); + const result = RocketChat.models.Rooms.removeById(rid); + + if (Apps && Apps.isLoaded()) { + Apps.getBridges().getListenerBridge().roomEvent('IPostRoomDeleted', room); + } + + return result; } else { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'eraseRoom'