572 lines
21 KiB
JavaScript
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, '<');
|
|
vdata = vdata.replace(/>/g, '>');
|
|
|
|
|
|
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;
|