├── .gitignore ├── LICENSE ├── README.md ├── config.json.dist ├── index.js ├── package.json ├── src ├── MatrixServer.js ├── Server.js └── XmppServer.js └── test ├── MatrixServer.test.js └── XmppServer.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | #IDE 2 | .idea 3 | 4 | #dependencies 5 | node_modules 6 | 7 | #config 8 | config.json 9 | 10 | #misc 11 | *.swp 12 | todo 13 | *.db 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mero 2 | 3 | NodeJS based XMPP facade bridge for matrix.org 4 | 5 | ## About 6 | 7 | This Matrix Application Server will pretend to be a XMPP (jabber) server and mirror all actions to and from Matrix. 8 | 9 | The current state is *very* experimental. **Use this at your own risk. Mero might eat your cat.** 10 | 11 | ## Features 12 | 13 | * 1:1 Chat (XMPP <-> Matrix) 14 | * Presence (XMPP -> Matrix) 15 | * Typing notification (XMPP -> Matrix) 16 | * Subscription adding (XMPP -> Matrix) 17 | 18 | ## Todo 19 | 20 | * Migration script 21 | * Full presence sync 22 | * Full typing notification sync 23 | * Complete subscription handling (unsubscribe, subscription from matrix) 24 | * XMPP profile info 25 | * Avatar sync 26 | * Multi user chat 27 | -------------------------------------------------------------------------------- /config.json.dist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkaveRat/mero/54aaacd75f8d70f7d7e9b792885dea8f8d92640e/config.json.dist -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Server = require('./src/Server'); 3 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; 4 | var s = new Server(); 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mero", 3 | "version": "1.0.0", 4 | "description": "Matrix XMPP bridge", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/SkaveRat/mero.git" 12 | }, 13 | "keywords": [ 14 | "xmpp", 15 | "matrix" 16 | ], 17 | "author": "SkaveRat", 18 | "license": "Apache-2.0", 19 | "bugs": { 20 | "url": "https://github.com/SkaveRat/mero/issues" 21 | }, 22 | "homepage": "https://github.com/SkaveRat/mero#readme", 23 | "dependencies": { 24 | "debug": "^2.2.0", 25 | "matrix-js-sdk": "^0.3.0", 26 | "nedb": "^1.5.0", 27 | "node-xmpp-core": "^4.2.0", 28 | "node-xmpp-server": "^2.1.2", 29 | "pem": "^1.8.1", 30 | "q": "^1.4.1", 31 | "request": "^2.67.0", 32 | "restify": "^4.0.3" 33 | }, 34 | "devDependencies": { 35 | "mocha": "^2.3.4", 36 | "proxyquire": "^1.7.3", 37 | "unit.js": "^2.0.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/MatrixServer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var EventEmitter = require('events').EventEmitter 3 | , restify = require('restify') 4 | , config = require('../config.json') 5 | , debug = require('debug')('mero:MatrixServer') 6 | , Matrix = require("matrix-js-sdk") 7 | , _ = require('util') 8 | , Q = require('q') 9 | , request = require('request') 10 | ; 11 | 12 | 13 | class MatrixServer extends EventEmitter { 14 | constructor() { 15 | super(); 16 | var server = restify.createServer(); 17 | server.use(restify.bodyParser({})); 18 | server.put('/transactions/:transaction', this.handleIncoming.bind(this)); 19 | server.listen(config.mx.app_port,'0.0.0.0', () => { 20 | debug("Matrix Appserver listening on " + config.mx.app_port) 21 | }); 22 | } 23 | 24 | registerUser(jid) { 25 | let deferred = Q.defer(); 26 | 27 | request.post({ 28 | url: 'https://' + config.mx.host + ':' + config.mx.port + '/_matrix/client/api/v1/register', 29 | body: { 30 | user: 'mero_' + jid, 31 | type: 'm.login.application_service' 32 | }, 33 | json: true, 34 | qs: { 35 | access_token: config.mx.access_token 36 | } 37 | }, function (err, res, body) { 38 | if(body.access_token) { 39 | matrixClient = MatrixServer._getMatrixConnection(jid); 40 | matrixClient.setDisplayName(jid) 41 | .catch((err) => { 42 | debug(err); 43 | }); 44 | deferred.resolve(); 45 | }else if(body.errcode == 'M_USER_IN_USE') { 46 | deferred.resolve(); 47 | }else{ 48 | deferred.reject(body.errcode); 49 | } 50 | }); 51 | 52 | return deferred.promise; 53 | } 54 | 55 | createRoom(invitedby, invite) { 56 | let usernameParts = invite.split('@'); 57 | let matrixClient = MatrixServer._getMatrixConnection(invitedby); 58 | 59 | return this.registerUser(invitedby) 60 | .then(() => { 61 | return matrixClient.createRoom({ 62 | visibility: "private", 63 | invite: [_.format("@%s:%s", usernameParts[0], config.mx.host)] //@local:remote.tld 64 | }) 65 | .then((roomdata) => { 66 | matrixClient.setRoomName(roomdata.room_id, _.format("%s (XMPP)", invitedby)) 67 | .catch((err) => { 68 | debug(err); 69 | }); 70 | 71 | return roomdata; 72 | }) 73 | .catch((err) => { 74 | debug(err); 75 | }); 76 | }) 77 | .catch((err) => { 78 | console.log("User create fail: " + err); 79 | }); 80 | 81 | } 82 | 83 | /** 84 | * 85 | * @param from sender JID 86 | * @param to matrix room ID 87 | * @param isTyping 88 | */ 89 | setTypingNotification(from, to, isTyping) { 90 | let matrixClient = MatrixServer._getMatrixConnection(from); 91 | 92 | matrixClient.sendTyping(to, isTyping, 10000) 93 | .then(function (foo) { 94 | debug(foo); 95 | }) 96 | .catch(function (foo) { 97 | debug(foo); 98 | }); 99 | } 100 | 101 | /** 102 | * send a message from xmpp to matrix 103 | * @param from sender JID 104 | * @param to matrix room_id 105 | * @param message 106 | */ 107 | sendMessage(from, to, message) { 108 | let matrixClient = MatrixServer._getMatrixConnection(from); 109 | 110 | matrixClient.sendTextMessage(to, message) 111 | .then(function (foo) { 112 | debug(foo); 113 | }) 114 | .catch(function (foo) { 115 | debug(foo); 116 | }); 117 | } 118 | 119 | /** 120 | * 121 | * @param user JID 122 | * @param name new displayname 123 | */ 124 | setDisplayname(user, name) { 125 | let matrixClient = MatrixServer._getMatrixConnection(user); 126 | 127 | matrixClient.setDisplayName(name) 128 | .then(() => { 129 | debug("displayname success!"); 130 | }) 131 | .catch((e) => { 132 | debug("displayname fail!"); 133 | debug(e) 134 | }); 135 | } 136 | 137 | setPresence(user, status) { 138 | let matrixClient = MatrixServer._getMatrixConnection(user); 139 | 140 | let presence = 'online'; 141 | switch (status) { 142 | case 'xa': 143 | case 'away': 144 | case 'dnd': 145 | presence = 'unavailable'; 146 | break; 147 | case 'online': 148 | case 'chat': 149 | presence = 'online'; 150 | break; 151 | case 'offline': 152 | presence = 'offline'; 153 | break; 154 | } 155 | 156 | matrixClient.setPresence(presence) 157 | .then(() => { 158 | debug("presence success!"); 159 | }) 160 | .catch((e) => { 161 | debug("presence fail!"); 162 | debug(e) 163 | }); 164 | } 165 | 166 | setPowerlevel(from, to, level) { 167 | let matrixClient = MatrixServer._getMatrixConnection(from); 168 | 169 | matrixClient.setPowerLevel() 170 | .then(() => { 171 | debug("presence success!"); 172 | }) 173 | .catch((e) => { 174 | debug("presence fail!"); 175 | debug(e) 176 | }); 177 | } 178 | 179 | handleIncoming(req, res, next) { 180 | debug("incoming"); 181 | debug(req.body.events); 182 | 183 | req.body.events.forEach((event) => { 184 | switch (event.type) { 185 | case 'm.room.member': 186 | if(event.content.membership == "join"){ 187 | this.emit("matrix.room.join", event.room_id); 188 | } 189 | break; 190 | case 'm.room.message': 191 | let from = event.user_id; 192 | let room_id = event.room_id; 193 | let message = event.content.body; 194 | this.emit("matrix.room.message", from, room_id, message); 195 | break; 196 | default: 197 | debug("Unhandled event type '%s'", event.type); 198 | } 199 | 200 | 201 | }); 202 | 203 | res.send("[]"); 204 | next(); 205 | } 206 | 207 | /** 208 | * @param jid Sender JID 209 | * @returns MatrixClient 210 | * @private 211 | */ 212 | static _getMatrixConnection(jid) { 213 | let jidParts = jid.split('/'); 214 | 215 | return Matrix.createClient({ 216 | baseUrl: 'https://' + config.mx.host + ':' + config.mx.port, 217 | queryParams: { 218 | user_id: '@mero_' + jidParts[0] + ':' + config.mx.host 219 | }, 220 | accessToken: config.mx.access_token, 221 | userId: '@mero_' + jidParts[0] + ':' + config.mx.host 222 | }); 223 | 224 | } 225 | } 226 | 227 | module.exports = MatrixServer; -------------------------------------------------------------------------------- /src/Server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var ltx = require('node-xmpp-core').ltx 3 | , debug = require('debug')('mero:Server') 4 | , _ = require('util') 5 | , Datastore = require('nedb') 6 | , Q = require('q') 7 | ; 8 | 9 | var MatrixServer = require('./MatrixServer') 10 | , XmppServer = require('./XmppServer') 11 | ; 12 | 13 | class Server { 14 | constructor() { 15 | this.matrixServer = new MatrixServer(); 16 | this.xmppServer = new XmppServer(); 17 | this.database = new Datastore({filename: 'mero.db', autoload: true}); 18 | 19 | this.matrixServer.on('matrix.room.join', this.handleMatrixRoomJoin.bind(this)); 20 | this.matrixServer.on('matrix.room.message', this.handleMatrixRoomMessage.bind(this)); 21 | 22 | this.xmppServer.on('xmpp.message', this.handleXmppMessage.bind(this)); 23 | this.xmppServer.on('xmpp.message.typing', this.handleXmppMessageTyping.bind(this)); 24 | 25 | 26 | this.xmppServer.on('xmpp.presence.subscribe', this.handleXmppPresenceSubscribe.bind(this)); 27 | this.xmppServer.on('xmpp.presence.status', this.handleXmppPresenceStatus.bind(this)); 28 | } 29 | 30 | handleMatrixRoomMessage(from, room_id, message) { 31 | if (!from.startsWith('@mero_')) { 32 | this._findRoomDataByRoomId(room_id) 33 | .then((data) => { 34 | data = data[0]; 35 | this.xmppServer.sendMessage(data.xmpp_internal, data.xmpp_external, message); 36 | }); 37 | } 38 | } 39 | 40 | handleMatrixRoomJoin(room_id) { 41 | debug("Handling Room join!"); 42 | this._findRoomDataByRoomId(room_id) 43 | .then((data) => { 44 | if (data.length > 0) { 45 | let roomData = data[0]; 46 | this.xmppServer.acceptSubscription(roomData.xmpp_external, roomData.xmpp_internal); 47 | } else { 48 | debug("Room join event without corresponding room data!") 49 | } 50 | }); 51 | } 52 | 53 | handleXmppPresenceStatus(from, status) { 54 | debug("status from %s: %s", from, status); 55 | this.matrixServer.setPresence(from, status); 56 | //this.matrixServer.setUsername(from, status); 57 | } 58 | 59 | handleXmppPresenceSubscribe(from, to) { 60 | debug("Incoming sub request: %s -> %s", from, to); 61 | 62 | this._findRoomData(from, to) 63 | .then((data) => { 64 | //we maybealready created a room for this contact combination 65 | // only create new one, if not existant 66 | if (data.length == 0) { 67 | this.matrixServer.createRoom(from, to) 68 | .then((result) => { 69 | this.database.insert({ 70 | xmpp_external: from, 71 | xmpp_internal: to, 72 | matrix: result.room_id 73 | }); //TODO error handling insert 74 | }) 75 | .catch((err) => { 76 | debug(err); 77 | }); 78 | } else { 79 | debug("Auth request duplicate"); 80 | // TODO poke user on re-request 81 | } 82 | }); 83 | 84 | } 85 | 86 | handleXmppMessageTyping(from, to, isTyping) { 87 | from = from.split('/')[0]; //todo use xmpp core JID class? 88 | to = to.split('/')[0]; 89 | 90 | this._findRoomData(from, to) 91 | .then((data) => { 92 | if (data.length > 0) { 93 | let roomData = data[0]; 94 | this.matrixServer.setTypingNotification(roomData.xmpp_external, roomData.matrix, isTyping); 95 | } else { 96 | debug("Incoming message for unsubscribed user"); //TODO proper xmpp response? Resending after subscription? 97 | } 98 | }) 99 | .catch((err) => { 100 | debug(err); 101 | }); 102 | } 103 | 104 | handleXmppMessage(from, to, message) { 105 | from = from.split('/')[0]; //todo use xmpp core JID class? 106 | to = to.split('/')[0]; 107 | 108 | this._findRoomData(from, to) 109 | .then((data) => { 110 | if (data.length > 0) { 111 | let roomData = data[0]; 112 | this.matrixServer.sendMessage(roomData.xmpp_external, roomData.matrix, message); 113 | } else { 114 | debug("Incoming message for unsubscribed user"); //TODO proper xmpp response? Resending after subscription? 115 | } 116 | }) 117 | .catch((err) => { 118 | debug(err); 119 | }); 120 | } 121 | 122 | _findRoomDataByRoomId(room_id) { 123 | let deferred = Q.defer(); 124 | 125 | this.database.find({matrix: room_id}, (err, data) => { 126 | if (err) { 127 | deferred.reject(new Error(err)) 128 | } else { 129 | deferred.resolve(data); 130 | } 131 | }); 132 | 133 | return deferred.promise; 134 | } 135 | 136 | _findRoomData(from, to) { 137 | let deferred = Q.defer(); 138 | 139 | this.database.find({xmpp_external: from, xmpp_internal: to}, (err, data) => { 140 | if (err) { 141 | deferred.reject(new Error(err)) 142 | } else { 143 | deferred.resolve(data); 144 | } 145 | }); 146 | 147 | return deferred.promise; 148 | } 149 | 150 | } 151 | 152 | module.exports = Server; -------------------------------------------------------------------------------- /src/XmppServer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var xmpp = require('node-xmpp-server') 4 | , debug = require('debug')('mero:XmppServer') 5 | , _ = require('util') 6 | , ltx = require('node-xmpp-core').ltx 7 | , EventEmitter = require('events').EventEmitter 8 | ; 9 | 10 | 11 | class XmppServer extends EventEmitter { 12 | constructor() { 13 | super(); 14 | 15 | this.stanzas = { 16 | presence: "" 17 | } 18 | 19 | this.xmppServer = new xmpp.Router(5269, '0.0.0.0'); 20 | this.xmppServer.register("sipgoat.de", (stanza) => this.handleStanza(stanza)); 21 | } 22 | 23 | 24 | handleStanza(stanza) { 25 | debug(stanza.toString()); 26 | switch (stanza.getName()) { 27 | case "presence": 28 | this.handlePresenceStanza(stanza); 29 | break; 30 | 31 | case "message": 32 | this.handleMessageStanza(stanza); 33 | break; 34 | default: 35 | debug("Unknown stanza:"); 36 | debug(stanza.toString()); 37 | } 38 | } 39 | 40 | handlePresenceStanza(stanza) { 41 | const from = stanza.attrs.from; 42 | const to = stanza.attrs.to; 43 | switch (stanza.attrs.type) { 44 | case "subscribe": 45 | this.emit('xmpp.presence.subscribe', from, to); 46 | break; 47 | case "probe": 48 | this.xmppServer.send(ltx.parse(_.format(this.stanzas.presence, to, from, ''))); 49 | break; 50 | default: 51 | const status = stanza.getChild('show'); 52 | if (stanza.attrs.type == 'unavailable') { 53 | this.emit('xmpp.presence.status', from, 'offline'); 54 | } else if (status) { 55 | this.emit('xmpp.presence.status', from, status.getText()); 56 | } else { 57 | this.emit('xmpp.presence.status', from, 'online'); 58 | } 59 | break; 60 | } 61 | } 62 | 63 | handleMessageStanza(stanza) { 64 | stanza.children.forEach((element) => { 65 | const from = stanza.attrs['from']; 66 | const to = stanza.attrs['to']; 67 | switch (element.getName()) { 68 | case 'body': 69 | this.emit('xmpp.message', from, to, element.getText().trim()); 70 | break; 71 | case 'composing': 72 | this.emit('xmpp.message.typing', from, to, true); 73 | break; 74 | case 'paused': 75 | this.emit('xmpp.message.typing', from, to, false); 76 | break; 77 | case 'active': 78 | this.emit('xmpp.message.typing', from, to, false); 79 | break; 80 | } 81 | }); 82 | } 83 | 84 | sendMessage(from, to, message) { 85 | let stanzaTemplate = "%s"; 86 | 87 | 88 | let hrTime = process.hrtime(); 89 | let stanza = _.format(stanzaTemplate, from, to, (hrTime[0] * 1000000 + hrTime[1]), message); 90 | debug(stanza); 91 | this.xmppServer.send(ltx.parse(stanza)); 92 | } 93 | 94 | /** 95 | * @param from The requester 96 | * @param to the matrix user 97 | */ 98 | acceptSubscription(from, to) { 99 | var resFormatted = _.format(this.stanzas.presence, to, from, "subscribed"); 100 | var reqFormatted = _.format(this.stanzas.presence, to, from, "subscribe"); 101 | this.xmppServer.send(ltx.parse(resFormatted)); 102 | this.xmppServer.send(ltx.parse(reqFormatted)); 103 | } 104 | 105 | } 106 | 107 | module.exports = XmppServer; -------------------------------------------------------------------------------- /test/MatrixServer.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('unit.js'); 4 | var proxyquire = require('proxyquire'); 5 | 6 | 7 | describe('Matrix Server', function () { 8 | 9 | let MatrixServer, mockConfig; 10 | beforeEach(function () { 11 | mockConfig = { 12 | "mx": { 13 | "app_port": 42, 14 | "access_token": 'myToken', 15 | "host": 'example.com', 16 | "port": 23 17 | } 18 | }; 19 | 20 | MatrixServer = proxyquire('../src/MatrixServer', { 21 | 'restify': {}, 22 | '../config.json': mockConfig 23 | }); 24 | }); 25 | 26 | describe('#_getMatrixConnection', function () { 27 | it('loads baseUrl from config', function () { 28 | mockConfig.mx.host = 'foo.com'; 29 | mockConfig.mx.port = 423; 30 | 31 | var result = MatrixServer._getMatrixConnection('foo@example.com'); 32 | 33 | test.assert.equal(result.baseUrl, 'https://foo.com:423'); 34 | }); 35 | 36 | it('generates a correct virtual user id for given JID', function() { 37 | const jid = 'foo@example.com'; 38 | var result = MatrixServer._getMatrixConnection(jid); 39 | 40 | test.assert.equal(result.credentials.userId, '@mero_' + jid + ':example.com'); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/XmppServer.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('unit.js'); 4 | var ltx = require('node-xmpp-core').ltx; 5 | var proxyquire = require('proxyquire'); 6 | var _ = require('util'); 7 | 8 | 9 | describe('Xmpp Server', function () { 10 | 11 | let XmppServer; 12 | beforeEach(function () { 13 | XmppServer = proxyquire('../src/XmppServer', { 14 | 'node-xmpp-server': {} 15 | }); 16 | }); 17 | 18 | describe('#handleStanza', function () { 19 | it('does not emit event on unknown stanza', function () { 20 | var unknownStanza = ltx.parse(''); 21 | var server = new XmppServer(); 22 | 23 | var eventStub = test.stub(server, 'emit'); 24 | server.handleStanza(unknownStanza); 25 | test.assert(!eventStub.called); 26 | eventStub.restore(); 27 | }); 28 | 29 | it('emit message event on message stanza', function () { 30 | const xmppFrom = 'foobar@example.com'; 31 | const xmppTo = 'barbaz@example.com'; 32 | const xmppMessage = 'foobar'; 33 | 34 | var messageStanza = ltx.parse(_.format('%s', xmppFrom, xmppTo, xmppMessage)); 35 | var server = new XmppServer(); 36 | 37 | var eventStub = test.stub(server, 'emit'); 38 | server.handleStanza(messageStanza); 39 | test.assert(eventStub.calledWithExactly('xmpp.message', xmppFrom, xmppTo, xmppMessage)); 40 | eventStub.restore(); 41 | }); 42 | 43 | it('trims incoming messages', function () { 44 | const xmppFrom = 'foobar@example.com'; 45 | const xmppTo = 'barbaz@example.com'; 46 | const xmppMessage = ' foobar with space '; 47 | 48 | var messageStanza = ltx.parse(_.format('%s', xmppFrom, xmppTo, xmppMessage)); 49 | var server = new XmppServer(); 50 | 51 | var eventStub = test.stub(server, 'emit'); 52 | server.handleStanza(messageStanza); 53 | test.assert(eventStub.calledWithExactly('xmpp.message', xmppFrom, xmppTo, 'foobar with space')); 54 | eventStub.restore(); 55 | }); 56 | 57 | it('notifies about typing notifications', function () { 58 | const xmppFrom = 'foobar@example.com'; 59 | const xmppTo = 'barbaz@example.com'; 60 | const isTyping = true; 61 | var typingStanza = ltx.parse(_.format('', xmppFrom, xmppTo)); 62 | 63 | var server = new XmppServer(); 64 | 65 | var eventStub = test.stub(server, 'emit'); 66 | server.handleStanza(typingStanza); 67 | test.assert(eventStub.calledWithExactly('xmpp.message.typing', xmppFrom, xmppTo, isTyping)); 68 | eventStub.restore(); 69 | }); 70 | 71 | it('notifies about typing notifications paused', function () { 72 | const xmppFrom = 'foobar@example.com'; 73 | const xmppTo = 'barbaz@example.com'; 74 | const isTyping = false; 75 | var typingStanza = ltx.parse(_.format('', xmppFrom, xmppTo)); 76 | 77 | var server = new XmppServer(); 78 | 79 | var eventStub = test.stub(server, 'emit'); 80 | server.handleStanza(typingStanza); 81 | test.assert(eventStub.calledWithExactly('xmpp.message.typing', xmppFrom, xmppTo, isTyping)); 82 | eventStub.restore(); 83 | }); 84 | 85 | it('notifies about typing notifications stopped', function () { 86 | const xmppFrom = 'foobar@example.com'; 87 | const xmppTo = 'barbaz@example.com'; 88 | const isTyping = false; 89 | var typingStanza = ltx.parse(_.format('', xmppFrom, xmppTo)); 90 | 91 | var server = new XmppServer(); 92 | 93 | var eventStub = test.stub(server, 'emit'); 94 | server.handleStanza(typingStanza); 95 | test.assert(eventStub.calledWithExactly('xmpp.message.typing', xmppFrom, xmppTo, isTyping)); 96 | eventStub.restore(); 97 | }); 98 | }); 99 | }); 100 | --------------------------------------------------------------------------------