diff --git a/src/SlashCommands.js b/src/SlashCommands.js new file mode 100644 index 00000000..a3426347 --- /dev/null +++ b/src/SlashCommands.js @@ -0,0 +1,227 @@ +/* +Copyright 2015 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'; + +var MatrixClientPeg = require("./MatrixClientPeg"); +var dis = require("./dispatcher"); + +var reject = function(msg) { + return { + error: msg + }; +}; + +var success = function(promise) { + return { + promise: promise + }; +}; + +var commands = { + // Change your nickname + nick: function(room_id, args) { + if (args) { + return success( + MatrixClientPeg.get().setDisplayName(args) + ); + } + return reject("Usage: /nick "); + }, + + // Invite a user + invite: function(room_id, args) { + if (args) { + var matches = args.match(/^(\S+)$/); + if (matches) { + return success( + MatrixClientPeg.get().invite(room_id, matches[1]) + ); + } + } + return reject("Usage: /invite "); + }, + + // Join a room + join: function(room_id, args) { + if (args) { + var matches = args.match(/^(\S+)$/); + if (matches) { + var room_alias = matches[1]; + // Try to find a room with this alias + var rooms = MatrixClientPeg.get().getRooms(); + var roomId; + for (var i = 0; i < rooms.length; i++) { + var aliasEvents = rooms[i].currentState.getStateEvents( + "m.room.aliases" + ); + for (var j = 0; j < aliasEvents.length; j++) { + var aliases = aliasEvents[j].getContent().aliases || []; + for (var k = 0; k < aliases.length; k++) { + if (aliases[k] === room_alias) { + roomId = rooms[i].roomId; + break; + } + } + if (roomId) { break; } + } + if (roomId) { break; } + } + if (roomId) { // we've already joined this room, view it. + dis.dispatch({ + action: 'view_room', + room_id: roomId + }); + return success(); + } + else { + // attempt to join this alias. + return success( + MatrixClientPeg.get().joinRoom(room_alias).done( + function(room) { + dis.dispatch({ + action: 'view_room', + room_id: room.roomId + }); + }, function(err) { + console.error( + "Failed to join room: %s", JSON.stringify(err) + ); + }) + ); + } + } + } + return reject("Usage: /join [NOT IMPLEMENTED]"); + }, + + // Kick a user from the room with an optional reason + kick: function(room_id, args) { + if (args) { + var matches = args.match(/^(\S+?)( +(.*))?$/); + if (matches) { + return success( + MatrixClientPeg.get().kick(room_id, matches[1], matches[3]) + ); + } + } + return reject("Usage: /kick []"); + }, + + // Ban a user from the room with an optional reason + ban: function(room_id, args) { + if (args) { + var matches = args.match(/^(\S+?)( +(.*))?$/); + if (matches) { + return success( + MatrixClientPeg.get().ban(room_id, matches[1], matches[3]) + ); + } + } + return reject("Usage: /ban []"); + }, + + // Unban a user from the room + unban: function(room_id, args) { + if (args) { + var matches = args.match(/^(\S+)$/); + if (matches) { + // Reset the user membership to "leave" to unban him + return success( + MatrixClientPeg.get().unban(room_id, matches[1]) + ); + } + } + return reject("Usage: /unban "); + }, + + // Define the power level of a user + op: 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 room = MatrixClientPeg.get().getRoom(room_id); + if (!room) { + return reject("Bad room ID: " + room_id); + } + var powerLevelEvent = room.currentState.getStateEvents( + "m.room.power_levels", "" + ); + return success( + MatrixClientPeg.get().setPowerLevel( + room_id, user_id, powerLevel, powerLevelEvent + ) + ); + } + } + } + return reject("Usage: /op []"); + }, + + // Reset the power level of a user + deop: function(room_id, args) { + if (args) { + var matches = args.match(/^(\S+)$/); + if (matches) { + var room = MatrixClientPeg.get().getRoom(room_id); + if (!room) { + return reject("Bad room ID: " + room_id); + } + + var powerLevelEvent = room.currentState.getStateEvents( + "m.room.power_levels", "" + ); + return success( + MatrixClientPeg.get().setPowerLevel( + room_id, args, undefined, powerLevelEvent + ) + ); + } + } + return reject("Usage: /deop "); + } +}; + +module.exports = { + /** + * Process the given text for /commands and perform them. + * @param {string} roomId The room in which the command was performed. + * @param {string} input The raw text input by the user. + * @return {Object|null} An object with the property 'error' if there was an error + * processing the command, or 'promise' if a request was sent out. + * Returns null if the input didn't match 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).toLowerCase(); + var args = bits[3]; + if (commands[cmd]) { + return commands[cmd](roomId, args); + } + } + return null; // not a command + } +}; \ No newline at end of file diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 4788c9e8..4a6d6dc3 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -3,9 +3,14 @@ function textForMemberEvent(ev) { // XXX: SYJS-16 var senderName = ev.sender ? ev.sender.name : ev.getSender(); var targetName = ev.target ? ev.target.name : ev.getContent().target; + var reason = ev.getContent().reason ? ( + " Reason: " + ev.getContent().reason + ) : ""; switch (ev.getContent().membership) { case 'invite': return senderName + " invited " + targetName + "."; + case 'ban': + return senderName + " banned " + targetName + "." + reason; case 'join': if (ev.getPrevContent() && ev.getPrevContent().membership == 'join') { if (ev.getPrevContent().displayname && ev.getContent().displayname) { @@ -21,7 +26,18 @@ function textForMemberEvent(ev) { return targetName + " joined the room."; } case 'leave': - return targetName + " left the room."; + if (ev.getSender() === ev.getStateKey()) { + return targetName + " left the room."; + } + else if (ev.getPrevContent().membership === "ban") { + return senderName + " unbanned " + targetName + "."; + } + else if (ev.getPrevContent().membership === "join") { + return senderName + " kicked " + targetName + "." + reason; + } + else { + return targetName + " left the room."; + } } }; diff --git a/src/controllers/molecules/MessageComposer.js b/src/controllers/molecules/MessageComposer.js index 38d4a4f4..34183083 100644 --- a/src/controllers/molecules/MessageComposer.js +++ b/src/controllers/molecules/MessageComposer.js @@ -17,6 +17,7 @@ limitations under the License. 'use strict'; var MatrixClientPeg = require("../../MatrixClientPeg"); +var SlashCommands = require("../../SlashCommands"); var dis = require("../../dispatcher"); var KeyCode = { @@ -171,6 +172,26 @@ module.exports = { onEnter: function(ev) { var contentText = this.refs.textarea.getDOMNode().value; + + var cmd = SlashCommands.processInput(this.props.room.roomId, contentText); + if (cmd) { + ev.preventDefault(); + if (!cmd.error) { + this.refs.textarea.getDOMNode().value = ''; + } + if (cmd.promise) { + cmd.promise.done(function() { + console.log("Command success."); + }, function(err) { + console.error("Command failure: %s", err); + }); + } + else if (cmd.error) { + console.error(cmd.error); + } + return; + } + var content = null; if (/^\/me /i.test(contentText)) { content = {