Files
vnctalk-muc-rest/app/routes/index.js
T

572 lines
21 KiB
JavaScript

var express = require("express");
var router = express.Router();
var moment = require('moment');
var request = require('@cypress/request');
var env = process.env.NODE_ENV || 'development';
var config = require('../../config/prosody-muc-rest.js')[env];
var Pool = require('pg-pool');
var createTelnetPool = require('../telnet-pool');
function asNumber(value, fallback) {
var n = typeof value === 'number' ? value : parseInt(value, 10);
return Number.isFinite(n) ? n : fallback;
}
var telnetParams = {
host: config.telnet.host,
port: asNumber(config.telnet.port, 5582),
negotiationMandatory: config.telnet.negotiationMandatory,
timeout: asNumber(config.telnet.timeout, 5000),
debug: false
}
var telnetPool = createTelnetPool(telnetParams, asNumber(config.telnet.poolSize, 4));
async function doesMucExist(jid) {
return new Promise((resolve, reject) => {
var queryparams = [];
queryparams[0] = jid.split("@")[0];
queryparams[1] = jid.split("@")[1];
queryparams[2] = "jid";
queryparams[3] = "_jid";
query = "select prosody.value from prosody where prosody.host=$2 and prosody.user=$1 and (prosody.key=$3 or prosody.key=$4)";
dbpool.query(query, queryparams, async function (derr, dres) {
if (derr == null) {
console.log("doesMucExist dres.rows: ", dres.rows);
if (dres.rowCount > 0) {
resolve(1);
} else {
resolve(0);
}
} else {
console.log("db err: ", derr);
resolve(-1);
}
});
});
}
function setMucData(actor, muc, data) {
return new Promise((resolve, reject) => {
var query = "select value::jsonb as config from prosody where prosody.user=$1 and prosody.host=$2 and prosody.key=$3;"
var queryparams = [];
queryparams = muc.split("@");
queryparams[2] = "_data";
dbpool.query(query, queryparams, function (derr, dres) {
if (derr != null) {
console.log("[setMucData] db error for actor:", actor, "target:", muc, derr);
return reject(derr);
}
if (dres.rowCount === 0) {
let err = new Error("setMucData: no config row for " + muc);
console.log("[setMucData]", err.message);
return reject(err);
}
console.log("got data: ", dres.rows[0]);
let e2e = "0";
if (dres.rows[0].config.e2e) {
e2e = "1";
}
let persistentroom = "0";
if (dres.rows[0].config.persistent) {
persistentroom = "1";
}
let changesubject = "0";
if (dres.rows[0].config.changesubject) {
changesubject = "1";
}
let publicroom = "0";
if (dres.rows[0].config.publicroom) {
publicroom = "1";
}
if (!!data.hidden) {
publicroom = "0";
} else {
publicroom = "1";
}
let membersonly = "0";
if (dres.rows[0].config.members_only) {
membersonly = "1";
}
if (!!data.members_only) {
membersonly = "1";
}
if (data.members_only === false) {
membersonly = "0";
}
let roomname = "";
if (dres.rows[0].config.roomname) {
roomname = dres.rows[0].config.roomname;
}
let moderatedroom = "0";
let whois = "anyone";
if (dres.rows[0].config.whois) {
whois = dres.rows[0].config.whois;
}
if (!!data.whois && ((data.whois === "anyone") || (data.whois === "moderators"))) {
whois = data.whois;
}
let historylength = 5;
if (dres.rows[0].config.historylength) {
historylength = dres.rows[0].config.historylength;
}
let vdata = JSON.stringify(data);
vdata = vdata.replace(/&/g, '&');
vdata = vdata.replace(/"/g, '"');
vdata = vdata.replace(/</g, '&lt;');
vdata = vdata.replace(/>/g, '&gt;');
var messageStanza = "";
var msg_id = "";
var dictionary = "abcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < 12; i++) {
msg_id += dictionary.charAt(Math.floor(Math.random() * 12));
}
messageStanza = "<iq xmlns=\"jabber:client\" from=\"" + actor + "\" to=\"" + muc +"\" type=\"set\" id=\"" + msg_id + "\">";
messageStanza += "<query xmlns=\"http://jabber.org/protocol/muc#owner\"><x xmlns=\"jabber:x:data\" type=\"submit\"><field var=\"FORM_TYPE\"><value>http://jabber.org/protocol/muc#roomconfig</value></field> ";
messageStanza += "<field var=\"muc#roomconfig_persistentroom\"><value>" + persistentroom + "</value></field>";
messageStanza += "<field var=\"muc#roomconfig_changesubject\"><value>" + changesubject + "</value></field>";
messageStanza += "<field var=\"muc#roomconfig_publicroom\"><value>" + publicroom + "</value></field>";
messageStanza += "<field var=\"muc#roomconfig_roomname\"><value>" + roomname + "</value></field>";
messageStanza += "<field var=\"muc#roomconfig_moderatedroom\"><value>0</value></field>";
messageStanza += "<field var=\"muc#roomconfig_membersonly\"><value>" + membersonly + "</value></field>";
messageStanza += "<field var=\"muc#roomconfig_whois\"><value>" + whois + "</value></field>";
messageStanza += "<field var=\"muc#roomconfig_historylength\"><value>" + historylength.toString() + "</value></field>";
messageStanza += "<field var=\"muc#roomconfig_e2e\"><value>" + e2e + "</value></field>";
messageStanza += "<field var=\"muc#roomconfig_vdata\"><value>" + vdata + "</value></field></x></query></iq>";
var authHeader = "";
if (config.prosodyRESTuser && config.prosodyRESTuser !="") {
authHeader = "Basic " + Buffer.from(config.prosodyRESTuser + ":" + config.prosodyRESTsecret).toString("base64");
} else {
authHeader = "Basic " + Buffer.from("prosody:" + config.prosodyRESTsecret).toString("base64");
}
console.log(moment().format("LTS") + " [xmpp-rest] messageStanza", messageStanza);
request.post(
{
url: config.prosodyRESTUrl,
body: messageStanza,
headers: {
"Authorization" : authHeader,
"Host" : config.mucDomain.split("conference.")[1],
"Content-Type": "text/xml"
}
},
function (error, response, body) {
if (!error && (response.statusCode == 200 || response.statusCode == 201)) {
console.log(moment().format("LTS") + " [xmpp-rest succes for] ", actor);
return resolve(true);
} else {
console.log(moment().format("LTS") + " [xmpp-rest error] ", body, error);
return reject(new Error("setMucData: xmpp-rest failed for " + actor + " -> " + muc + " status=" + (response && response.statusCode) + " error=" + error));
}
}
);
});
});
}
let dbPoolOpts = {
database: config.database.name,
user: config.database.user,
password: config.database.pass,
host: config.database.host,
port: config.database.port,
max: 20, // set pool max size to 20
min: 4, // set min pool size to 4
idleTimeoutMillis: config.database.idleTimeoutMillis, // close idle clients after 1 second
connectionTimeoutMillis: config.database.connectionTimeoutMillis, // return an error after 1 second if connection could not be established
};
if (!config.disableDBtls) {
dbPoolOpts['ssl'] = { rejectUnauthorized: false };
}
var dbpool = new Pool(dbPoolOpts);
/**
* Get Group chat list
*/
router.get('/groupchats', async function (req, res) {
var queryparams = [];
var sqlquery="select room, created, updated from group_owners where room LIKE '%' || $1";
queryparams.push(config.mucDomain);
if (req.query.created) {
var created = parseInt(req.query.created, 10);
if (!isNaN(created)) {
queryparams.push(created);
sqlquery += " AND created > $" + queryparams.length;
}
}
if (req.query.updated) {
var updated = parseInt(req.query.updated, 10);
if (!isNaN(updated)) {
queryparams.push(updated);
sqlquery += " AND updated > $" + queryparams.length;
}
}
dbpool.query(sqlquery, queryparams, function (derr, dres) {
if (derr == null) {
if (dres.rowCount > 0) {
let mucresults = [];
for (var i = 0; i < dres.rowCount; i++) {
mucresults.push({ room: dres.rows[i].room, created: dres.rows[i].created, updated: dres.rows[i].updated });
}
res.send(mucresults);
} else {
res.status(404).json([]);
}
} else {
res.status(500).json(derr);
}
});
});
/**
* Get Group chat info
*/
router.get('/groupchats/:target', async function (req, res) {
var target = req.params.target;
if (target == null || target == "") {
res.status(500).json({message: '500 empty groupchats target'});
} else {
var queryparams = [];
queryparams[0] = target.split("@")[0];
queryparams[1] = target.split("@")[1];
var sqlquery="select key, value from prosody where prosody.store='config' and prosody.user=$1 and prosody.host=$2";
dbpool.query(sqlquery, queryparams, function (derr, dres) {
if (derr == null) {
if (dres.rowCount > 0) {
let result = dres.rows.filter(row => row.key === "_data").map(row => JSON.parse(row.value))[0];
let owners = dres.rows.filter(row => row.value === "owner").map(row => { return { jid: row.key } });
let members = dres.rows.filter(row => row.value === "member").map(row => { return { jid: row.key } });
result.affiliations_owner = owners;
result.affiliations_member = members;
if (!result.subject) {
result.subject = queryparams[0].split(".").map(v => v.replace(/^\w/, c => c.toUpperCase())).join(" ");
}
res.send(result);
} else {
res.status(410).json({status:"groupchat not found - probably deleted"});
}
} else {
console.log("db err: ", derr);
res.status(500).json(derr);
}
});
}
});
/**
* Rename existing group subject
*/
router.put('/groupchats/:target', async function (req, res) {
var target = req.params.target;
if (target == null || target == "") {
res.status(500).json({message: '500 empty groupchats target'});
} else {
let user = req.body.user;
let subject = req.body.subject;
let valid = false;
let data;
try {
data = JSON.stringify(req.body.data);
console.log("received data: ", data);
valid = true;
} catch (e) {
console.log("error parsing data: ", e);
valid = false;
}
let has_subject = true;
let has_data = true;
if (subject == null || subject == "") {
has_subject = false;
};
if (data == null) {
has_data = false;
}
if (has_data || has_subject) {
if (has_subject) {
try {
let response = await telnetPool.send('muc:room(\"'+ target +'\"):set_subject(\"'+ user +'\", \"'+ subject +'\");\n');
let rawresults = response.split("\n");
let mucresults = [];
for (var i = 0; i < rawresults.length - 1 ; i++) {
if ((rawresults[i].startsWith("\u0000| ") || rawresults[i].startsWith("\| ")) && !rawresults[i].startsWith("| OK") && !rawresults[i].startsWith("\u0000| Result") ) {
console.log("adding: ", rawresults[i].split("| ")[1].split("\r")[0]);
mucresults.push(rawresults[i].split("| ")[1].split("\r")[0]);
}
}
console.log('async rawresult:', rawresults);
console.log('async result:', mucresults);
res.status(200).json(mucresults);
} catch (error) {
console.log("telnet send error: ", error);
res.status(500).json({message: 'telnet connection is not established.'});
}
} else {
if (has_data) {
var queryparams = [];
queryparams[0] = target.split("@")[0];
queryparams[1] = target.split("@")[1];
queryparams[2] = 'owner';
var actor = "";
var query = "select prosody.key as actor from prosody where prosody.user=$1 and prosody.host=$2 and prosody.value=$3 limit 1";
console.log("creating xmpp data from: ", req.body.data);
dbpool.query(query, queryparams, async function (derr, dres) {
if (derr == null) {
if (dres.rowCount > 0) {
actor = dres.rows[0].actor;
console.log("actor1: ", actor);
console.log("actor1: ", Object.keys(nAct));
try {
await setMucData(actor, req.params.target, req.body.data);
res.status(200).json(null);
} catch (e) {
console.log("[setMucData] failed:", e.message);
res.status(500).json({message: "failed to update muc data", error: e.message});
}
} else {
res.status(410).json({err: "target does not exist - probably deleted"});
}
} else {
res.status(500).json(derr);
}
});
} else {
res.status(500).json({message: '500 insufficient data'});
}
}
} else {
res.status(500).json({message: '500 insufficient data'});
}
}
});
/**
* Get affiliations info of group
*/
router.get('/affiliations/:target', async function (req, res) {
var target = req.params.target;
if (target == null || target == "") {
res.status(500).json({message: '500 empty affiliations target'});
} else {
var queryparams = [];
queryparams[0] = target.split("@")[0];
queryparams[1] = target.split("@")[1];
queryparams[2] = 'member';
queryparams[3] = 'owner';
var sqlquery="select key, value from prosody where prosody.store='config' and prosody.user=$1 and prosody.host=$2 and prosody.value=any(array[$3, $4])";
dbpool.query(sqlquery, queryparams, function (derr, dres) {
if (derr == null) {
if (dres.rowCount > 0) {
let result = {};
for (var i = 0; i < dres.rowCount; i++) {
let affiliation = dres.rows[i].value;
let key = dres.rows[i].key;
result[key] = affiliation;
}
res.send(result);
} else {
res.status(410).json({status:"groupchat not found - probably deleted"});
}
} else {
res.status(500).json(derr);
}
});
}
});
/*
* Get Online users detail
*/
router.get('/occupants/:target', async function (req, res) {
var target = req.params.target;
if (target == null || target == "") {
res.status(500).json({message: '500 empty affiliations target'});
} else {
var queryparams = [];
queryparams[0] = target.split("@")[0];
queryparams[1] = target.split("@")[1];
var sqlquery="select value from prosody where prosody.store='config' and prosody.key='_occupants' and prosody.user=$1 and prosody.host=$2";
dbpool.query(sqlquery, queryparams, function (derr, dres) {
if (derr == null) {
if (dres.rowCount > 0) {
res.send(dres.rows[0].value);
} else {
res.status(404).json(null);
}
} else {
res.status(500).json(derr);
}
});
}
});
/**
* add affiliation for member into group
*/
router.post('/affiliations/:target', async function (req, res) {
var target = req.params.target;
if (target == null || target == "") {
res.status(500).json({message: '500 empty affiliations target'});
} else {
let user = req.body.user;
let affiliation = req.body.affiliation ? req.body.affiliation : "member";
if (user == null || user == "") {
res.status(500).json({message: '500 empty user'});
} else {
var queryparams = [];
queryparams[0] = target.split("@")[0];
queryparams[1] = target.split("@")[1];
var sqlquery="select value from prosody where prosody.store='config' and prosody.key='_jid' and prosody.user=$1 and prosody.host=$2";
dbpool.query(sqlquery, queryparams, async function (derr, dres) {
if (derr == null) {
if (dres.rowCount > 0) {
// groupchat is found and exists
try {
let response = await telnetPool.send('muc:room(\"'+ target +'\"):set_affiliation(true, \"'+ user +'\", \"' + affiliation +'\");\n');
if ((response.indexOf("nil value") > -1) || (response.indexOf("Fatal ") > -1)) {
res.status(500).json(response);
} else {
let rawresults = response.split("\n");
let mucresults = [];
for (var i = 0; i < rawresults.length - 1 ; i++) {
if ((rawresults[i].startsWith("\u0000| ") || rawresults[i].startsWith("\| ")) && !rawresults[i].startsWith("| OK") && !rawresults[i].startsWith("\u0000| Result") ) {
console.log("adding: ", rawresults[i].split("| ")[1].split("\r")[0]);
mucresults.push(rawresults[i].split("| ")[1].split("\r")[0]);
}
}
console.log('async rawresult:', rawresults);
console.log('async result:', mucresults);
res.status(200).json(mucresults);
}
} catch (error) {
console.log("telnet send error: ", error);
res.status(500).json({message: 'telnet connection is not established.'});
}
} else {
// no groupchat found for requested operation - send 410 gone
res.status(410).json({status: "requested groupchat not found, probably deleted"});
}
} else {
res.status(500).json(derr);
}
});
}
}
});
/**
* create new groupchat
* payload:
* { jid: newgroupjid,
* owner: owner of new group,
* members: [],
* subject: subject of the group
* }
*/
router.post('/creategroup', async function (req, res) {
console.log("[creategroup] ", req.body);
let mucJidValid = true;
if (req.body.jid && req.body.jid != "") {
if (!req.body.jid.toLowerCase().endsWith("@" + config.mucDomain.toLowerCase())) {
mucJidValid = false;
}
}
if (mucJidValid && req.body && req.body.jid && req.body.jid != "" && req.body.subject && req.body.subject != "" && req.body.owner && req.body.owner != "") {
let alreadyExists = await doesMucExist(req.body.jid);
console.log("existenceCheck ", alreadyExists);
if (alreadyExists == 1) {
res.status(409).json({message: "muc already exists"});
} else {
try {
console.log("creating room...");
let cmd = 'muc:create("' + req.body.jid.toLowerCase() + '", {';
cmd += 'subject="' + req.body.subject + '", history_length = 5, persistent = true });';
let response = await telnetPool.send(cmd);
console.log(response);
if (response.indexOf("Result: MUC room (" + req.body.jid.toLowerCase()) > -1 ) {
// all good
console.log("setting owner to " + req.body.owner);
cmd = 'muc:room("' + req.body.jid + '"):set_affiliation(true, "' + req.body.owner + '", "owner");';
if (req.body.members && req.body.members.length > 0) {
for (var i = 0; i < req.body.members.length; i++) {
console.log("adding member " + req.body.members[i]);
cmd += 'muc:room("' + req.body.jid + '"):set_affiliation(true, "' + req.body.members[i] + '", "member");';
}
}
cmd += 'muc:room("' + req.body.jid + '"):save(true);';
response = await telnetPool.send(cmd);
console.log(response);
res.json({status: "ok"});
} else {
res.status(500).json({error: response});
}
} catch (error) {
console.log("telnet send error: ", error);
res.status(500).json({message: "connection not writable"});
}
}
} else {
res.status(500).json({message: "insufficient params"});
}
});
router.get('/healthcheck', async function (req, res) {
try {
let response = await telnetPool.send('c2s:show();\n');
console.log(response);
let rawresults;
let resArr = [];
if (response.indexOf("https://prosody.im/doc/console\r\n") > -1) {
rawresults = response.split("https://prosody.im/doc/console\r\n")[1];
let rawDataArr = rawresults.split(" ");
for (var i = 0; i < rawDataArr.length - 1 ; i++) {
if (rawDataArr[i].indexOf("@") > -1) {
let jid = rawDataArr[i].split("/")[0];
resArr.push(jid);
}
}
console.log("processed: ", resArr);
} else {
rawresults = response;
let rawDataArr = rawresults.split(" ");
for (var i = 0; i < rawDataArr.length - 1 ; i++) {
if (rawDataArr[i].indexOf("@") > -1) {
let jid = rawDataArr[i].split("/")[0];
resArr.push(jid);
}
}
console.log("processed: ", resArr);
}
res.json({res: resArr});
} catch (error) {
console.log("telnet send error: ", error);
res.json({status: "ok"});
}
});
router.get('/health', async function (req, res) {
res.json({status: "ok"});
});
module.exports = router;