/* * Copyright 2020 Netsyms Technologies. * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ console.log("Starting up..."); import sdk from 'matrix-js-sdk'; import fs from 'fs'; import log4js from 'log4js'; import https from 'https'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; import request from 'request'; import FileType from 'file-type'; // Load settings from config.json const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); let rawdata = fs.readFileSync(__dirname + '/config.json'); let settings = JSON.parse(rawdata); console.log(__dirname + "/config.json loaded."); var logger = log4js.getLogger(); logger.level = settings.loglevel; logger.info("Log initialized."); var initialsynccomplete = false; /** * From https://github.com/stevekinney/node-phone-formatter * @param {string} phoneNumber * @param {string} formatString * @returns {string} */ function formatPhoneNumber(phoneNumber, formatString) { phoneNumber = phoneNumber.replace( /^[\+\d{1,3}\-\s]*\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/, "$1$2$3" ); for (var i = 0, l = phoneNumber.length; i < l; i++) { formatString = formatString.replace("N", phoneNumber[i]); } return formatString; } function checkSMS() { logger.debug("Checking SMS"); const options = { hostname: 'smsapi.voxtelesys.net', port: 443, path: '/api/v1/msgs/inbound?unread_only=true', method: 'GET', headers: { "Authorization": "Bearer " + settings.smsapikey } }; const req = https.request(options, res => { res.on('data', d => { try { var json = JSON.parse(d); var messages = json.results; if (messages.length == 0) { logger.debug("No new SMS messages."); } for (var i = 0; i < messages.length; i++) { var msg = messages[i]; if (settings.smsonlyto.length > 0 && settings.smsonlyto.indexOf(msg.to) != -1) { logger.info("Received SMS from " + msg.from + " for " + msg.to + ": " + msg.body); createOrJoinSMSRoom(msg.from, msg.to, function (roomid) { client.setRoomTag(roomid, "com.netsyms.matrix-bridge-voxtelesys.sms", {tel: msg.from, ournumber: msg.to}); sendMatrix(roomid, msg.body, msg.media); }); } else { logger.info("Received SMS from " + msg.from + " for " + msg.to + ", ignoring based on smsonlyto list."); } } } catch (ex) { logger.error(ex); } }); }); req.on('error', error => { logger.warning(error); }); req.end(); } /** * Join or create+join a room with alias #SMS_{tel}. If already joined, do nothing. * @param {string} tel * @param {string} ournumber the phone number Matrix messages will be sent from for this room * @param {function} callback function with the room ID as an argument. * @returns {undefined} */ function createOrJoinSMSRoom(tel, ournumber, callback) { var roomName = "#SMS_" + tel + "_" + ournumber + ":" + settings.matrixdomain; logger.debug("Checking if room " + roomName + " exists."); client.getRoomIdForAlias(roomName).then((res) => { logger.debug("Room " + roomName + " exists!"); var inRoom = false; var rooms = client.getRooms(); for (var i = 0; i < rooms.length; i++) { if (rooms[i].roomId == res.room_id) { inRoom = true; break; } } if (inRoom) { // we're already in the room, do nothing logger.debug("Room " + roomName + " already joined."); client.setRoomTag(roomid, "com.netsyms.matrix-bridge-voxtelesys.sms", {tel: tel, ournumber: ournumber}); callback(res.room_id); } else { // not in the room, join it client.joinRoom(res.room_id).then((room) => { logger.debug("Room " + roomName + " joined."); client.setRoomTag(roomid, "com.netsyms.matrix-bridge-voxtelesys.sms", {tel: tel, ournumber: ournumber}); callback(room.room_id); }); } }).catch((err) => { // room doesn't exist, create it //logger.debug(err); logger.debug("Room " + roomName + " does not exist. Creating it now."); var userPowerLevels = {}; for (var i = 0; i < settings.inviteusers.length; i++) { userPowerLevels[settings.inviteusers[i]] = 50; } userPowerLevels["@" + settings["matrixuser"] + ":" + settings["matrixdomain"]] = 100; client.createRoom({ room_alias_name: "SMS_" + tel + "_" + ournumber, preset: "trusted_private_chat", visibility: "private", invite: settings.inviteusers, power_level_content_override: { "events": { "m.room.name": 50, "m.room.power_levels": 50, "m.room.canonical_alias": 100 }, "events_default": 0, "invite": 50, "kick": 50, "notifications": { "room": 50 }, "redact": 50, "state_default": 50, "users": userPowerLevels, "users_default": 50 }, is_direct: true, name: formatPhoneNumber(tel, "(NNN) NNN-NNNN"), topic: "SMS conversation with " + formatPhoneNumber(tel, "(NNN) NNN-NNNN") + " (using " + formatPhoneNumber(ournumber, "(NNN) NNN-NNNN") + ")" }).then((room) => { logger.debug("Room" + roomName + " created with ID " + room.room_id); // The first message or two we send doesn't go through unless we do this. // It just spits out "Error sending event M_FORBIDDEN: Unknown room" instead. createOrJoinSMSRoom(tel, ournumber, callback); }); }); } function getAndUploadFile(url, callback) { logger.info("Downloading MMS media " + url); // download request({url, encoding: null}, (err, resp, buffer) => { FileType.fromBuffer(buffer).then(function (mimeobj) { logger.debug(mimeobj); // upload logger.info("Uploading MMS media to Matrix " + url); client.uploadContent(buffer, { onlyContentUri: true, rawResponse: false, type: mimeobj.mime }).then((res) => { if (typeof callback == "function") { callback(res, mimeobj.mime); logger.info("Media URI: " + res); } }).catch((err) => { if (typeof callback == "function") { callback(false); } if (err.data.error == "Unknown room") { return; } logger.error(err); }); }); }); } /** * Send a message to a Matrix room. * @param {string} roomid the room to post the message in. * @param {string} body message content. * @param {array} media Array of media URLs to download via HTTP(s) and send to Matrix. * @param {function|undefined} callback passes true when successful, false on failure. * @returns {undefined} */ function sendMatrix(roomid, body, media, callback) { if (Array.isArray(media)) { for (var i = 0; i < media.length; i++) { getAndUploadFile(media[i], function (uri, mimetype) { if (mimetype == "image/jpg" || mimetype == "image/jpeg" || mimetype == "image/png" || mimetype == "image/gif") { var content = { body: "Image", msgtype: "m.image", url: uri, info: { mimetype: mimetype } }; } else { var content = { body: "File", msgtype: "m.file", url: uri, info: { mimetype: mimetype } }; } client.sendEvent(roomid, "m.room.message", content, "").then((res) => { }).catch((err) => { if (err.data.error == "Unknown room") { return; } logger.error(err); }); }); } } if (body != "") { var content = { body: body, msgtype: "m.text" }; client.sendEvent(roomid, "m.room.message", content, "").then((res) => { if (typeof callback == "function") { callback(true); } }).catch((err) => { if (typeof callback == "function") { callback(false); } if (err.data.error == "Unknown room") { return; } logger.error(err); }); } else { callback(true); } } function sendMatrixNotice(roomid, body, callback) { var content = { body: body, msgtype: "m.notice" }; client.sendEvent(roomid, "m.room.message", content, "").then((res) => { if (typeof callback == "function") { callback(true); } }).catch((err) => { if (typeof callback == "function") { callback(false); } if (err.data.error == "Unknown room") { return; } logger.error(err); }); } /** * Send a SMS to a phone number. * @param {string} number * @param {string} body message content. * @param {function|undefined} callback passes true when successful, false on failure. * @returns {undefined} */ function sendSMS(number, from, body, callback) { logger.info("Sending SMS to " + number + " from " + from); const data = JSON.stringify({ to: [number], from: from, body: body }); const options = { hostname: 'smsapi.voxtelesys.net', port: 443, path: '/api/v1/sms', method: 'POST', headers: { "Authorization": "Bearer " + settings.smsapikey, "Content-Type": "application/json", "Accept": "application/json" } } const req = https.request(options, res => { res.on('data', d => { logger.debug(d.toString('utf8')); }); }); req.on('error', error => { logger.error(error); callback(false); }); req.write(data); req.end(); if (typeof callback == "function") { callback(true); } } const client = sdk.createClient(settings.homeserver); client.login("m.login.password", {"user": settings.matrixuser, "password": settings.matrixpass}).then((response) => { client.startClient(); logger.info("Plugged into the matrix."); client.once('sync', function (state, prevState, res) { logger.debug("Initial sync complete (" + state + ")"); initialsynccomplete = true; logger.info("Up and running."); setInterval(checkSMS, settings.smsinterval * 1000); checkSMS(); }); client.on("Room.timeline", function (event, room) { if (!initialsynccomplete) { return; // ignore anything while we were offline } if (event.getType() !== "m.room.message") { return; // only use messages } if (client.getUserId() == event.getSender()) { return; // skip own messages to prevent loop } if (event.getContent().body.startsWith("!sms")) { // capture command to start room for new number const matches = event.getContent().body.match(/([1-9]?[0-9]{10})/g); if (matches.length == 1 || matches.length == 2) { var tel = matches[0]; var ournumber = settings.smsfrom; if (tel.length == 10) { // make it the full number tel = "1" + tel; } if (matches.length == 2) { ournumber = matches[1]; if (ournumber.length == 10) { // make it the full number ournumber = "1" + ournumber; } } logger.info("Got request to start new SMS conversation with " + tel + " using " + ournumber + " from " + event.getSender() + "."); sendMatrixNotice(event.getRoomId(), "Starting conversation with " + tel); createOrJoinSMSRoom(tel, ournumber, function (roomid) { //client.setRoomTag(roomid, "com.netsyms.matrix-bridge-voxtelesys.sms", {tel: tel, ournumber: ournumber}); }); } return; } var matches = room.name.match(/SMS_([1-9][0-9]+)(?:_([1-9][0-9]+))?/g); console.log(room); if (matches == null || (matches.length != 1 && matches.length != 2)) { client.getRoomTags(event.getRoomId()).then((response) => { if (typeof response.tags["com.netsyms.matrix-bridge-voxtelesys.sms"] != "undefined") { sendSMS( response.tags["com.netsyms.matrix-bridge-voxtelesys.sms"].tel, response.tags["com.netsyms.matrix-bridge-voxtelesys.sms"].ournumber, event.getContent().body, function () { client.sendReadReceipt(event, {}); }); } else { console.log(response.tags); sendMatrixNotice(room.roomId, "Error: couldn't determine correct number to send SMS from."); } }); } else { if (matches.length == 1) { var tel = matches[0]; logger.info("Got message for " + tel + " from " + event.getSender() + ", relaying."); sendSMS(tel, settings.smsfrom, sevent.getContent().body, function () { client.sendReadReceipt(event, {}); }); } else if (matches.length == 2) { var tel = matches[0]; var from = matches[1]; logger.info("Got message for " + tel + " from " + event.getSender() + ", relaying to " + from + "."); sendSMS(tel, from, sevent.getContent().body, function () { client.sendReadReceipt(event, {}); }); } } }); });