diff --git a/syweb/webclient/app.js b/syweb/webclient/app.js index 35190a71f4..f9d5c18ec4 100644 --- a/syweb/webclient/app.js +++ b/syweb/webclient/app.js @@ -33,6 +33,7 @@ var matrixWebClient = angular.module('matrixWebClient', [ 'notificationService', 'recentsService', 'modelService', + 'commandsService', 'infinite-scroll', 'ui.bootstrap', 'monospaced.elastic' diff --git a/syweb/webclient/components/matrix/commands-service.js b/syweb/webclient/components/matrix/commands-service.js new file mode 100644 index 0000000000..3c516ad1e4 --- /dev/null +++ b/syweb/webclient/components/matrix/commands-service.js @@ -0,0 +1,164 @@ +/* +Copyright 2014 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +'use strict'; + +/* +This service contains logic for parsing and performing IRC style commands. +*/ +angular.module('commandsService', []) +.factory('commandsService', ['$q', '$location', 'matrixService', 'modelService', function($q, $location, matrixService, modelService) { + + // create a rejected promise with the given message + var reject = function(msg) { + var deferred = $q.defer(); + deferred.reject({ + data: { + error: msg + } + }); + return deferred.promise; + }; + + // Change your nickname + var doNick = function(room_id, args) { + if (args) { + return matrixService.setDisplayName(args); + } + return reject("Usage: /nick "); + }; + + // Join a room + var doJoin = function(room_id, args) { + if (args) { + var matches = args.match(/^(\S+)$/); + if (matches) { + var room_alias = matches[1]; + $location.url("room/" + room_alias); + // NB: We don't need to actually do the join, since that happens + // automatically if we are not joined onto a room already when + // the page loads. + return reject("Joining "+room_alias); + } + } + return reject("Usage: /join "); + }; + + // Kick a user from the room with an optional reason + var doKick = function(room_id, args) { + if (args) { + var matches = args.match(/^(\S+?)( +(.*))?$/); + if (matches) { + return matrixService.kick(room_id, matches[1], matches[3]); + } + } + return reject("Usage: /kick []"); + }; + + // Ban a user from the room with an optional reason + var doBan = function(room_id, args) { + if (args) { + var matches = args.match(/^(\S+?)( +(.*))?$/); + if (matches) { + return matrixService.ban(room_id, matches[1], matches[3]); + } + } + return reject("Usage: /ban []"); + }; + + // Unban a user from the room + var doUnban = function(room_id, args) { + if (args) { + var matches = args.match(/^(\S+)$/); + if (matches) { + // Reset the user membership to "leave" to unban him + return matrixService.unban(room_id, matches[1]); + } + } + return reject("Usage: /unban "); + }; + + // Define the power level of a user + var doOp = function(room_id, args) { + if (args) { + var matches = args.match(/^(\S+?)( +(\d+))?$/); + var powerLevel = 50; // default power level for op + if (matches) { + var user_id = matches[1]; + if (matches.length === 4 && undefined !== matches[3]) { + powerLevel = parseInt(matches[3]); + } + if (powerLevel !== NaN) { + var powerLevelEvent = modelService.getRoom(room_id).current_room_state.state("m.room.power_levels"); + return matrixService.setUserPowerLevel(room_id, user_id, powerLevel, powerLevelEvent); + } + } + } + return reject("Usage: /op []"); + }; + + // Reset the power level of a user + var doDeop = function(room_id, args) { + if (args) { + var matches = args.match(/^(\S+)$/); + if (matches) { + var powerLevelEvent = modelService.getRoom(room_id).current_room_state.state("m.room.power_levels"); + return matrixService.setUserPowerLevel(room_id, args, undefined, powerLevelEvent); + } + } + return reject("Usage: /deop "); + }; + + + var commands = { + "nick": doNick, + "join": doJoin, + "kick": doKick, + "ban": doBan, + "unban": doUnban, + "op": doOp, + "deop": doDeop + }; + + return { + + /** + * Process the given text for commands and perform them. + * @param {String} roomId The room in which the input was performed. + * @param {String} input The raw text input by the user. + * @return {Promise} A promise of the pending command, or null if the + * input is not a command. + */ + processInput: function(roomId, input) { + // trim any trailing whitespace, as it can confuse the parser for + // IRC-style commands + input = input.replace(/\s+$/, ""); + if (input[0] === "/" && input[1] !== "/") { + var bits = input.match(/^(\S+?)( +(.*))?$/); + var cmd = bits[1].substring(1); + var args = bits[3]; + if (commands[cmd]) { + return commands[cmd](roomId, args); + } + return reject("Unrecognised IRC-style command: " + cmd); + } + return null; // not a command + } + + }; + +}]); + diff --git a/syweb/webclient/index.html b/syweb/webclient/index.html index 4bca320e77..fb3c3f528c 100644 --- a/syweb/webclient/index.html +++ b/syweb/webclient/index.html @@ -45,6 +45,7 @@ + diff --git a/syweb/webclient/room/room-controller.js b/syweb/webclient/room/room-controller.js index 6670201707..bea0db5759 100644 --- a/syweb/webclient/room/room-controller.js +++ b/syweb/webclient/room/room-controller.js @@ -15,8 +15,8 @@ limitations under the License. */ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'angular-peity']) -.controller('RoomController', ['$modal', '$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall', 'notificationService', 'modelService', 'recentsService', - function($modal, $filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, mPresence, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall, notificationService, modelService, recentsService) { +.controller('RoomController', ['$modal', '$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall', 'notificationService', 'modelService', 'recentsService', 'commandsService', + function($modal, $filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, mPresence, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall, notificationService, modelService, recentsService, commandsService) { 'use strict'; var MESSAGES_PER_PAGINATION = 30; var THUMBNAIL_SIZE = 320; @@ -435,172 +435,18 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'a // Store the command in the history history.push(input); - var promise; - var cmd; - var args; + var promise = commandsService.processInput($scope.room_id, input); var echo = false; + var isEmote = input.indexOf("/me ") === 0; - // Check for IRC style commands first - // trim any trailing whitespace, as it can confuse the parser for IRC-style commands - input = input.replace(/\s+$/, ""); - - if (input[0] === "/" && input[1] !== "/") { - var bits = input.match(/^(\S+?)( +(.*))?$/); - cmd = bits[1]; - args = bits[3]; - - console.log("cmd: " + cmd + ", args: " + args); - - switch (cmd) { - case "/me": - promise = matrixService.sendEmoteMessage($scope.room_id, args); - echo = true; - break; - - case "/nick": - // Change user display name - if (args) { - promise = matrixService.setDisplayName(args); - } - else { - $scope.feedback = "Usage: /nick "; - } - break; - - case "/join": - // Join a room - if (args) { - var matches = args.match(/^(\S+)$/); - if (matches) { - var room_alias = matches[1]; - if (room_alias.indexOf(':') == -1) { - // FIXME: actually track the :domain style name of our homeserver - // with or without port as is appropriate and append it at this point - } - - var room_id = modelService.getAliasToRoomIdMapping(room_alias); - console.log("joining " + room_alias + " id=" + room_id); - if ($scope.room) { // TODO actually check that you = join - // don't send a join event for a room you're already in. - $location.url("room/" + room_alias); - } - else { - promise = matrixService.joinAlias(room_alias).then( - function(response) { - // TODO: factor out the common housekeeping whenever we try to join a room or alias - matrixService.roomState(response.room_id).then( - function(response) { - eventHandlerService.handleEvents(response.data, false, true); - }, - function(error) { - $scope.feedback = "Failed to get room state for: " + response.room_id; - } - ); - $location.url("room/" + room_alias); - }, - function(error) { - $scope.feedback = "Can't join room: " + JSON.stringify(error.data); - } - ); - } - } - } - else { - $scope.feedback = "Usage: /join "; - } - break; - - case "/kick": - // Kick a user from the room with an optional reason - if (args) { - var matches = args.match(/^(\S+?)( +(.*))?$/); - if (matches) { - promise = matrixService.kick($scope.room_id, matches[1], matches[3]); - } - } - - if (!promise) { - $scope.feedback = "Usage: /kick []"; - } - break; - - case "/ban": - // Ban a user from the room with an optional reason - if (args) { - var matches = args.match(/^(\S+?)( +(.*))?$/); - if (matches) { - promise = matrixService.ban($scope.room_id, matches[1], matches[3]); - } - } - - if (!promise) { - $scope.feedback = "Usage: /ban []"; - } - break; - - case "/unban": - // Unban a user from the room - if (args) { - var matches = args.match(/^(\S+)$/); - if (matches) { - // Reset the user membership to "leave" to unban him - promise = matrixService.unban($scope.room_id, matches[1]); - } - } - - if (!promise) { - $scope.feedback = "Usage: /unban "; - } - break; - - case "/op": - // Define the power level of a user - if (args) { - var matches = args.match(/^(\S+?)( +(\d+))?$/); - var powerLevel = 50; // default power level for op - if (matches) { - var user_id = matches[1]; - if (matches.length === 4 && undefined !== matches[3]) { - powerLevel = parseInt(matches[3]); - } - if (powerLevel !== NaN) { - var powerLevelEvent = $scope.room.current_room_state.state("m.room.power_levels"); - promise = matrixService.setUserPowerLevel($scope.room_id, user_id, powerLevel, powerLevelEvent); - } - } - } - - if (!promise) { - $scope.feedback = "Usage: /op []"; - } - break; - - case "/deop": - // Reset the power level of a user - if (args) { - var matches = args.match(/^(\S+)$/); - if (matches) { - var powerLevelEvent = $scope.room.current_room_state.state("m.room.power_levels"); - promise = matrixService.setUserPowerLevel($scope.room_id, args, undefined, powerLevelEvent); - } - } - - if (!promise) { - $scope.feedback = "Usage: /deop "; - } - break; - - default: - $scope.feedback = ("Unrecognised IRC-style command: " + cmd); - break; - } - } - - // By default send this as a message unless it's an IRC-style command - if (!promise && !cmd) { - // Make the request - promise = matrixService.sendTextMessage($scope.room_id, input); + if (!promise) { // not a non-echoable command echo = true; + if (isEmote) { + promise = matrixService.sendEmoteMessage($scope.room_id, input.substring(4)); + } + else { + promise = matrixService.sendTextMessage($scope.room_id, input); + } } if (echo) { @@ -608,8 +454,8 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'a // To do so, create a minimalist fake text message event and add it to the in-memory list of room messages var echoMessage = { content: { - body: (cmd === "/me" ? args : input), - msgtype: (cmd === "/me" ? "m.emote" : "m.text"), + body: (isEmote ? input.substring(4) : input), + msgtype: (isEmote ? "m.emote" : "m.text"), }, origin_server_ts: new Date().getTime(), // fake a timestamp room_id: $scope.room_id, @@ -642,7 +488,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'a } }, function(error) { - $scope.feedback = "Request failed: " + error.data.error; + $scope.feedback = error.data.error; if (echoMessage) { // Mark the message as unsent for the rest of the page life