/* * 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; 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, function (roomid) { 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 {function} callback function with the room ID as an argument. * @returns {undefined} */ function createOrJoinSMSRoom(tel, callback) { logger.debug("Checking if room #SMS_" + tel + ":" + settings.matrixdomain + " exists."); client.getRoomIdForAlias("#SMS_" + tel + ":" + settings.matrixdomain).then((res) => { logger.debug("Room #SMS_" + tel + ":" + settings.matrixdomain + " 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 #SMS_" + tel + ":" + settings.matrixdomain + " already joined."); callback(res.room_id); } else { // not in the room, join it client.joinRoom(res.room_id).then((room) => { logger.debug("Room #SMS_" + tel + ":" + settings.matrixdomain + " joined."); callback(room.room_id); }); } }).catch((err) => { // room doesn't exist, create it //logger.debug(err); logger.debug("Room #SMS_" + tel + ":" + settings.matrixdomain + " does not exist. Creating it now."); client.createRoom({ room_alias_name: "SMS_" + tel, visibility: "private", invite: settings.inviteusers, name: "SMS: " + tel, topic: "SMS conversation with " + tel }).then((room) => { logger.debug("Room #SMS_" + tel + ":" + settings.matrixdomain + " 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, 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, body, callback) { logger.info("Sending SMS to " + number); const data = JSON.stringify({ to: [number], from: settings.smsfrom, 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) { var tel = matches[0]; if (tel.length == 10) { // make it the full number tel = "1" + tel; } logger.info("Got request to start new SMS conversation with " + tel + " from " + event.getSender() + "."); sendMatrixNotice(event.getRoomId(), "Starting new conversation with " + tel); createOrJoinSMSRoom(tel, function () { return; }); } return; } //sendMatrix(room.roomId, "echo " + event.event.content.body); const matches = room.name.match(/([1-9][0-9]+)/g); if (matches.length == 1) { var tel = matches[0]; logger.info("Got message for " + tel + " from " + event.getSender() + ", relaying."); sendSMS(tel, event.getContent().body, function () { client.sendReadReceipt(event, {}); }); } }); });