From be0b10973620c10c625652af821d5491d08b0f6d Mon Sep 17 00:00:00 2001 From: Skylar Ittner Date: Thu, 3 Feb 2022 18:10:21 -0700 Subject: [PATCH] Add outgoing MMS support --- config.example.json | 4 + main.js | 214 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 188 insertions(+), 30 deletions(-) diff --git a/config.example.json b/config.example.json index bc32770..9242425 100644 --- a/config.example.json +++ b/config.example.json @@ -1,5 +1,6 @@ { "homeserver": "https://matrix.org", + "mediaurlpath": "https://matrix.org/_matrix/media/r0/download/{{server-name}}/{{media-id}}", "matrixdomain": "matrix.org", "matrixuser": "smsbot", "matrixpass": "hunter2", @@ -7,6 +8,9 @@ "smsapikey": "Voxtelesys SMS API key goes here", "smsfrom": "Voxtelesys DID goes here", "smsonlyto": ["14061234567"], + "listenport": 8069, + "publicurl": "http://localhost:8069/", + "googleverifiedsms": false, "loglevel": "debug", "smsinterval": 15 } diff --git a/main.js b/main.js index 627f34e..bcccf05 100644 --- a/main.js +++ b/main.js @@ -14,12 +14,11 @@ 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 { fileURLToPath } from 'url'; +import { dirname } from 'path'; import request from 'request'; import FileType from 'file-type'; +import http from 'http'; // Load settings from config.json const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -293,12 +292,66 @@ function sendMatrixNotice(roomid, body, callback) { * @returns {undefined} */ function sendSMS(number, from, body, callback) { + if (settings.googleverifiedsms) { + // Use Google Verified SMS to add business branding to SMS message + } logger.info("Sending SMS to " + number + " from " + from); - const data = JSON.stringify({ + var data = { to: [number], from: from, body: body + }; + const jsondata = JSON.stringify(data); + 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(jsondata); + req.end(); + if (typeof callback == "function") { + callback(true); + } +} + +/** + * 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 sendMMS(number, from, mediauri, mimetype, callback) { + logger.info("Sending MMS to " + number + " from " + from); + var mediauriregex = /^mxc:\/\/([a-zA-Z0-9\-]+\.[a-zA-Z0-9]{2,})\/([a-z0-9]+)$/; + var matches = mediauriregex.exec(mediauri); + var httpmediaurl = settings.mediaurlpath; + + httpmediaurl = httpmediaurl.replace("{{server-name}}", matches[0]); + httpmediaurl = httpmediaurl.replace("{{media-id}}", matches[1]); + var data = { + to: [number], + from: from, + body: body, + media: [httpmediaurl] + }; + const jsondata = JSON.stringify(data); const options = { hostname: 'smsapi.voxtelesys.net', port: 443, @@ -320,13 +373,55 @@ function sendSMS(number, from, body, callback) { logger.error(error); callback(false); }); - req.write(data); + req.write(jsondata); req.end(); if (typeof callback == "function") { callback(true); } } +function handleHTTPRequest(request, response) { + if (request.url == "/webhook") { + try { + logger.debug("Got webhook: " + request.body); + var msg = JSON.parse(request.body); + + if (msg.type == "mo") { + 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) { + sendMatrix(roomid, msg.body, msg.media); + }); + response.status(204); + response.end(); + } else { + logger.info("Received SMS from " + msg.from + " for " + msg.to + ", ignoring based on smsonlyto list."); + response.status(403); + response.body("403 forbidden (this endpoint doesn't deliver SMS destined for that number)"); + response.end(); + } + } else { + response.status(403); + response.body("403 forbidden (webhook type not handled by this endpoint)"); + response.end(); + } + } catch (ex) { + logger.error("Decoding webhook body: " + ex); + response.status(500); + response.body("500 internal server error"); + response.end(); + } + } else { + try { + response.status(404); + response.body("404 not found"); + response.end(); + } catch (err) { + logger.error(err); + } + } +} + const client = sdk.createClient(settings.homeserver); client.login("m.login.password", {"user": settings.matrixuser, "password": settings.matrixpass}).then((response) => { client.startClient(); @@ -334,9 +429,14 @@ client.login("m.login.password", {"user": settings.matrixuser, "password": setti client.once('sync', function (state, prevState, res) { logger.debug("Initial sync complete (" + state + ")"); initialsynccomplete = true; + var httpserver = http.createServer(handleHTTPRequest); + httpserver.listen(settings.listenport); + logger.info("HTTP server started on port " + settings.listenport); logger.info("Up and running."); - setInterval(checkSMS, settings.smsinterval * 1000); - checkSMS(); + if (settings.smsinterval > 0) { + setInterval(checkSMS, settings.smsinterval * 1000); + checkSMS(); + } }); client.on("Room.timeline", function (event, room) { if (!initialsynccomplete) { @@ -348,7 +448,7 @@ client.login("m.login.password", {"user": settings.matrixuser, "password": setti if (client.getUserId() == event.getSender()) { return; // skip own messages to prevent loop } - if (event.getContent().body.startsWith("!sms")) { + if (event.getContent().body.toLowerCase().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) { @@ -373,7 +473,7 @@ client.login("m.login.password", {"user": settings.matrixuser, "password": setti } return; } - if (event.getContent().body.startsWith("!fixnumbers")) { + if (event.getContent().body.toLowerCase().startsWith("!fixnumbers")) { // capture command to start room for new number const matches = event.getContent().body.match(/([1-9]?[0-9]{10})/g); if (matches.length == 2) { @@ -403,25 +503,55 @@ client.login("m.login.password", {"user": settings.matrixuser, "password": setti var tel = response.tags["u.matrix-bridge-voxtelesys-sms"].tel; var from = response.tags["u.matrix-bridge-voxtelesys-sms"].ournumber; logger.info("Got message for " + tel + " from " + event.getSender() + ", relaying to " + from + "."); - sendSMS( - tel, - from, - event.getContent().body, - function () { - client.sendReadReceipt(event, {}); - }); + switch (event.getContent().msgtype) { + case "m.image": + sendMMS( + tel, + from, + event.getContent().url, + event.getContent().info.mimetype, + function () { + client.sendReadReceipt(event, {}); + }); + break; + case "m.text": + default: + sendSMS( + tel, + from, + event.getContent().body, + function () { + client.sendReadReceipt(event, {}); + }); + break; + } } else if (typeof response.tags["com.netsyms.matrix-bridge-voxtelesys.sms"] != "undefined") { var tel = response.tags["com.netsyms.matrix-bridge-voxtelesys.sms"].tel; var from = response.tags["com.netsyms.matrix-bridge-voxtelesys.sms"].ournumber; client.setRoomTag(event.getRoomId(), "u.matrix-bridge-voxtelesys-sms", {tel: tel, ournumber: from, order: 0.5}); logger.info("Got message for " + tel + " from " + event.getSender() + ", relaying to " + from + "."); - sendSMS( - tel, - from, - event.getContent().body, - function () { - client.sendReadReceipt(event, {}); - }); + switch (event.getContent().msgtype) { + case "m.image": + sendMMS( + tel, + from, + event.getContent().url, + event.getContent().info.mimetype, + function () { + client.sendReadReceipt(event, {}); + }); + break; + case "m.text": + default: + sendSMS( + tel, + from, + event.getContent().body, + function () { + client.sendReadReceipt(event, {}); + }); + break; + } } else { console.log(response.tags); sendMatrixNotice(room.roomId, "Error: couldn't determine correct number to send SMS from."); @@ -431,16 +561,40 @@ client.login("m.login.password", {"user": settings.matrixuser, "password": setti if (matches.length == 1) { var tel = matches[0]; logger.info("Got message for " + tel + " from " + event.getSender() + ", relaying."); - sendSMS(tel, settings.smsfrom, event.getContent().body, function () { - client.sendReadReceipt(event, {}); - }); + switch (event.getContent().msgtype) { + case "m.image": + sendMMS(tel, settings.smsfrom, event.getContent().url, + event.getContent().info.mimetype, + function () { + client.sendReadReceipt(event, {}); + }); + break; + case "m.text": + default: + sendSMS(tel, settings.smsfrom, event.getContent().body, function () { + client.sendReadReceipt(event, {}); + }); + break; + } } 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, event.getContent().body, function () { - client.sendReadReceipt(event, {}); - }); + switch (event.getContent().msgtype) { + case "m.image": + sendMMS(tel, from, event.getContent().url, + event.getContent().info.mimetype, + function () { + client.sendReadReceipt(event, {}); + }); + break; + case "m.text": + default: + sendSMS(tel, settings.smsfrom, event.getContent().body, function () { + client.sendReadReceipt(event, {}); + }); + break; + } } } });