node-red-nodes/social/xmpp/92-xmpp.js

543 lines
23 KiB
JavaScript
Raw Normal View History

2013-10-30 21:38:23 +01:00
module.exports = function(RED) {
"use strict";
const {client, xml, jid} = require('@xmpp/client')
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
const LOGITALL=false;
function XMPPServerNode(n) {
RED.nodes.createNode(this,n);
this.nickname = n.nickname;
this.jid = n.user;
this.username = n.user.split('@')[0];
// The user may elect to just specify the jid in the settings,
// in which case extract the server from the jid and default the port
if("undefined" === typeof n.server || n.server === ""){
this.server = n.user.split('@')[1];
}
else{
this.server = n.server;
}
if("undefined" === typeof n.port || n.port === ""){
this.port = 5222;
}
else{
this.port = n.port;
}
// The password is obfuscated and stored in a separate location
var credentials = this.credentials;
if (credentials) {
this.password = credentials.password;
}
// The basic xmpp client object, this will be referred to as "xmpp" in the nodes.
// note we're not actually connecting here.
this.client = client({
service: 'xmpp://' + this.server + ':' + this.port,
username: this.username,
password: this.password
});
// helper variable for checking against later, maybe we should be using the client
// object directly...
this.connected = false;
// store the nodes that have us as config so we know when to tear it all down.
this.users = {};
// helper variable, because "this" changes definition inside a callback
var that = this;
// function for a node to tell us it has us as config
this.register = function(xmppThat) {
if (RED.settings.verbose || LOGITALL) {that.log("registering "+xmppThat.id);}
that.users[xmppThat.id] = xmppThat;
// So we could start the connection here, but we already have the logic in the thats.
// if (Object.keys(that.users).length === 1) {
// this.client.start();
// }
};
// function for a node to tell us it's not using us anymore
this.deregister = function(xmppThat,done) {
if (RED.settings.verbose || LOGITALL) {that.log("deregistering "+xmppThat.id);}
delete that.users[xmppThat.id];
if (that.closing) {
return done();
}
if (Object.keys(that.users).length === 0) {
if (that.client && that.client.connected) {
return that.client.stop(done);
} else {
that.client.stop();
return done();
}
}
done();
};
// store the last node to use us, in case we get an error back
this.lastUsed = undefined;
// function for a node to tell us it has just sent a message to our server
// so we know which node to blame if it all goes Pete Tong
this.used = function(xmppThat){
if (RED.settings.verbose || LOGITALL) {that.log(xmppThat.id+" sent a message to the xmpp server");}
that.lastUsed = xmppThat;
}
// Some errors come back as a message :-(
// this means we need to figure out which node might have sent it
// we also deal with subscriptions (i.e. presence information) here
this.client.on('stanza', async (stanza) =>{
if (stanza.is('message')) {
if (stanza.attrs.type == 'error') {
if (RED.settings.verbose || LOGITALL) {
that.log("Received error");
that.log(stanza);
}
var err = stanza.getChild('error');
if(err){
var textObj = err.getChild('text');
var text = "node-red:common.status.error";
if("undefined" !== typeof textObj){
text = textObj.getText();
}
if (RED.settings.verbose || LOGITALL) {that.log("Culprit: "+that.lastUsed);}
if("undefined" !== typeof that.lastUsed){
that.lastUsed.status({fill:"red",shape:"ring",text:text});
that.lastUsed.warn(text);
}
// maybe throw the message or summit
//that.error(text);
}
}
}
else if(stanza.is('presence')){
if(['subscribe','subscribed','unsubscribe','unsubscribed'].indexOf(stanza.attrs.type) > -1){
if (RED.settings.verbose || LOGITALL) {that.log("got a subscription based message");}
switch(stanza.attrs.type){
case 'subscribe':
// they're asking for permission let's just say yes
var response = xml('presence',
{type:'subscribed', to:stanza.attrs.from});
// if an error comes back we can't really blame anyone else
that.used(that);
that.client.send(response);
break;
default:
that.log("Was told we've "+stanza.attrs.type+" from "+stanza.attrs.from+" but we don't really care");
}
}
}
});
// We shouldn't have any errors here that the input/output nodes can't handle
// if you need to see everything though; uncomment this block
// this.client.on('error', err => {
// that.warn(err);
// that.warn(err.stack);
// });
// this gets called when we've completed the connection
this.client.on('online', async address => {
// provide some presence so people can see we're online
that.connected = true;
await that.client.send(xml('presence'));
// await that.client.send(xml('presence', {type: 'available'},xml('status', {}, 'available')));
if (RED.settings.verbose || LOGITALL) {that.log('connected as '+that.username+' to ' +that.server+':'+that.port);}
});
// if the connection has gone away, not sure why!
this.client.on('offline', () => {
that.connected = false;
if (RED.settings.verbose || LOGITALL) {that.log('connection closed');}
});
// gets called when the node is destroyed, e.g. if N-R is being stopped.
this.on("close", async done => {
if(that.connected){
await that.client.send(xml('presence', {type: 'unavailable'}));
}
that.client.stop().catch(that.error);
done();
});
}
RED.nodes.registerType("xmpp-server",XMPPServerNode,{
credentials: {
password: {type: "password"}
}
});
function XmppInNode(n) {
RED.nodes.createNode(this,n);
this.server = n.server;
this.serverConfig = RED.nodes.getNode(this.server);
this.nick = this.serverConfig.nickname || this.serverConfig.username.split("@")[0];
this.join = n.join || false;
this.sendAll = n.sendObject;
this.from = n.to || "";
var node = this;
var xmpp = this.serverConfig.client;
/* connection states
online: We are connected
offline: disconnected and will not autoretry
connecting: Socket is connecting
connect: Socket is connected
opening: Stream is opening
open: Stream is open
closing: Stream is closing
close: Stream is closed
disconnecting: Socket is disconnecting
disconnect: Socket is disconnected
*/
xmpp.on('online', async address => {
node.status({fill:"green",shape:"dot",text:"node-red:common.status.connected"});
if ((node.join) && (node.from !== "")) {
2020-07-10 13:30:44 +02:00
var to = node.from+'/'+node.nick;
// the presence with the muc x element signifies we want to join the muc
// if we want to support passwords, we need to add that as a child of the x element
// (third argument to the x/muc/children )
// We also turn off chat history (maxstanzas 0) because that's not what this node is about.
var stanza = xml('presence',
{"to": to},
xml("x",'http://jabber.org/protocol/muc'),
{ maxstanzas:0, seconds:1 }
);
node.serverConfig.used(node);
xmpp.send(stanza);
}
});
xmpp.on('connecting', async address => {
node.status({fill:"grey",shape:"dot",text:"node-red:common.status.connecting"});
});
xmpp.on('connect', async address => {
node.status({fill:"grey",shape:"dot",text:"node-red:common.status.connected"});
});
xmpp.on('opening', async address => {
node.status({fill:"grey",shape:"dot",text:"opening"});
});
xmpp.on('open', async address => {
node.status({fill:"grey",shape:"dot",text:"open"});
});
xmpp.on('closing', async address => {
node.status({fill:"grey",shape:"dot",text:"closing"});
});
xmpp.on('close', async address => {
node.status({fill:"grey",shape:"dot",text:"closed"});
});
xmpp.on('disconnecting', async address => {
node.status({fill:"grey",shape:"dot",text:"disconnecting"});
});
// we'll not add a offline catcher, as the error catcher should populate the status for us
// Should we listen on other's status (chatstate) or a chatroom state (groupbuddy)?
xmpp.on('error', err => {
if (RED.settings.verbose || LOGITALL) { node.log(err); }
if (err.hasOwnProperty("stanza")) {
if (err.stanza.name === 'stream:error') { node.error("stream:error - bad login id/pwd ?",err); }
else { node.error(err.stanza.name,err); }
node.status({fill:"red",shape:"ring",text:"bad login"});
}
else {
if (err.errno === "ETIMEDOUT") {
node.error("Timeout connecting to server",err);
node.status({fill:"red",shape:"ring",text:"timeout"});
}
if (err.errno === "ENOTFOUND") {
node.error("Server doesn't exist "+xmpp.options.service,err);
node.status({fill:"red",shape:"ring",text:"bad address"});
}
else if (err === "XMPP authentication failure") {
node.error(err,err);
node.status({fill:"red",shape:"ring",text:"XMPP authentication failure"});
}
else {
node.error(err.errno,err);
node.status({fill:"red",shape:"ring",text:"node-red:common.status.error"});
}
}
});
// Meat of it, a stanza object contains chat messages (and other things)
xmpp.on('stanza', async (stanza) =>{
// node.log("Received stanza");
if (RED.settings.verbose || LOGITALL) {node.log(stanza);}
if (stanza.is('message')) {
if (stanza.attrs.type == 'chat') {
var body = stanza.getChild('body');
if (body) {
var msg = { payload:body.getText() };
var ids = stanza.attrs.from.split('/');
if (ids[1].length !== 36) {
msg.topic = stanza.attrs.from
}
else { msg.topic = ids[0]; }
2019-03-06 18:46:28 +01:00
if (!node.join && ((node.from === "") || (node.from === stanza.attrs.from))) {
node.send([msg,null]);
}
}
}
else if(stanza.attrs.type == 'groupchat'){
const parts = stanza.attrs.from.split("/");
var conference = parts[0];
var from = parts[1];
var body = stanza.getChild('body');
var payload = "";
if("undefined" !== typeof body){
payload = body.getText();
}
var msg = { topic:from, payload:payload, room:conference };
if (stanza.attrs.from != node.nick) {
if ((node.join) && (node.from === conference)) {
node.send([msg,null]);
}
}
}
}
else if(stanza.is('presence')){
if(['subscribe','subscribed','unsubscribe','unsubscribed'].indexOf(stanza.attrs.type) > -1){
// this isn't for us, let the config node deal with it.
}
else{
var statusText="";
if(stanza.attrs.type === 'unavailable'){
// the user might not exist, but the server doesn't tell us that!
statusText = "offline";
}
var status = stanza.getChild('status');
if("undefined" !== typeof status){
statusText = status.getText();
}
// right, do we care if there's no status?
if(statusText !== ""){
var from = stanza.attrs.from;
var state = stanza.attrs.show;
var msg = {topic:from, payload: {presence:state, status:statusText} };
node.send([null,msg]);
}
else{
if (RED.settings.verbose || LOGITALL) {
node.log("not propagating blank status");
node.log(stanza);
}
}
}
}
});
// xmpp.on('subscribe', from => {
// xmpp.acceptSubscription(from);
// });
//register with config
this.serverConfig.register(this);
// Now actually make the connection
try {
if(xmpp.status === "online"){
node.status({fill:"green",shape:"dot",text:"node-red:common.status.connected"});
}
else{
node.status({fill:"grey",shape:"dot",text:"node-red:common.status.connecting"});
if(xmpp.status === "offline"){
xmpp.start();
}
}
2017-01-29 18:45:44 +01:00
}
catch(e) {
node.error("Bad xmpp configuration; service: "+xmpp.options.service+" jid: "+node.serverConfig.jid);
node.warn(e);
node.warn(e.stack);
node.status({fill:"red",shape:"ring",text:"node-red:common.status.disconnected"});
}
node.on("close", function(removed, done) {
node.status({fill:"red",shape:"ring",text:"node-red:common.status.disconnected"});
node.serverConfig.deregister(node, done);
});
}
RED.nodes.registerType("xmpp in",XmppInNode);
function XmppOutNode(n) {
RED.nodes.createNode(this,n);
this.server = n.server;
this.serverConfig = RED.nodes.getNode(this.server);
this.nick = this.serverConfig.nickname || this.serverConfig.username.split("@")[0];
this.join = n.join || false;
this.sendAll = n.sendObject;
this.to = n.to || "";
var node = this;
var xmpp = this.serverConfig.client;
/* connection states
online: We are connected
offline: disconnected and will not autoretry
connecting: Socket is connecting
connect: Socket is connected
opening: Stream is opening
open: Stream is open
closing: Stream is closing
close: Stream is closed
disconnecting: Socket is disconnecting
disconnect: Socket is disconnected
*/
xmpp.on('online', function(data) {
node.status({fill:"green",shape:"dot",text:"node-red:common.status.connected"});
if ((node.join) && (node.from !== "")) {
// disable chat history
var to = node.to+'/'+node.nick;
// the presence with the muc x element signifies we want to join the muc
// if we want to support passwords, we need to add that as a child of the x element
// (third argument to the x/muc/children )
// We also turn off chat history (maxstanzas 0) because that's not what this node is about.
var stanza = xml('presence',
{"to": to},
xml("x",'http://jabber.org/protocol/muc'),
{ maxstanzas:0, seconds:1 }
);
node.serverConfig.used(node);
xmpp.send(stanza);
}
});
xmpp.on('connecting', async address => {
node.status({fill:"grey",shape:"dot",text:"node-red:common.status.connecting"});
});
xmpp.on('connect', async address => {
node.status({fill:"grey",shape:"dot",text:"node-red:common.status.connected"});
});
xmpp.on('opening', async address => {
node.status({fill:"grey",shape:"dot",text:"opening"});
});
xmpp.on('open', async address => {
node.status({fill:"grey",shape:"dot",text:"open"});
});
xmpp.on('closing', async address => {
node.status({fill:"grey",shape:"dot",text:"closing"});
});
xmpp.on('close', async address => {
node.status({fill:"grey",shape:"dot",text:"closed"});
});
xmpp.on('disconnecting', async address => {
node.status({fill:"grey",shape:"dot",text:"disconnecting"});
});
// we'll not add a offline catcher, as the error catcher should populate the status for us
xmpp.on('error', function(err) {
if (RED.settings.verbose || LOGITALL) { node.log(err); }
if (err.hasOwnProperty("stanza")) {
if (err.stanza.name === 'stream:error') { node.error("stream:error - bad login id/pwd ?",err); }
else { node.error(err.stanza.name,err); }
node.status({fill:"red",shape:"ring",text:"bad login"});
}
else {
if (err.errno === "ETIMEDOUT") {
node.error("Timeout connecting to server",err);
node.status({fill:"red",shape:"ring",text:"timeout"});
}
if (err.errno === "ENOTFOUND") {
node.error("Server doesn't exist "+xmpp.options.service,err);
node.status({fill:"red",shape:"ring",text:"bad address"});
}
else if (err === "XMPP authentication failure") {
node.error(err,err);
node.status({fill:"red",shape:"ring",text:"XMPP authentication failure"});
}
else {
node.error(err.errno,err);
node.status({fill:"red",shape:"ring",text:"node-red:common.status.error"});
}
}
});
//register with config
this.serverConfig.register(this);
// Now actually make the connection
if(xmpp.status === "online"){
node.status({fill:"green",shape:"dot",text:"online"});
2017-01-29 18:45:44 +01:00
}
else{
node.status({fill:"grey",shape:"dot",text:"node-red:common.status.connecting"});
if(xmpp.status === "offline"){
xmpp.start().catch(error => {
node.error("Bad xmpp configuration; service: "+xmpp.options.service+" jid: "+node.serverConfig.jid);
node.warn(error);
node.warn(error.stack);
node.status({fill:"red",shape:"ring",text:"node-red:common.status.error"});
});
}
}
// Let's get down to business and actually send a message
node.on("input", function(msg) {
if (msg.presence) {
if (['away', 'dnd', 'xa', 'chat'].indexOf(msg.presence) > -1 ) {
var stanza = xml('presence',
{"show":msg.presence},
xml('status',{},msg.payload));
node.serverConfig.used(node);
xmpp.send(stanza);
}
else { node.warn("Can't set presence - invalid value: "+msg.presence); }
}
else if(msg.command){
if(msg.command === "subscribe"){
var stanza = xml('presence',
{type:'subscribe', to: msg.payload});
node.serverConfig.used(node);
xmpp.send(stanza);
}
}
else {
var to = node.to || msg.topic || "";
if (to !== "") {
var message;
var type = node.join? "groupchat":"chat";
if (node.sendAll) {
message = xml(
"message",
{ type: type, to: to },
xml("body", {}, JSON.stringify(msg))
);
2017-01-29 18:45:44 +01:00
}
else if (msg.payload) {
if (typeof(msg.payload) === "object") {
message = xml(
"message",
{ type: type, to: to },
xml("body", {}, JSON.stringify(msg.payload))
);
}
else {
message = xml(
"message",
{ type: type, to: to },
xml("body", {}, msg.payload.toString())
);
}
}
node.serverConfig.used(node);
xmpp.send(message);
}
}
});
node.on("close", function(removed, done) {
if (RED.settings.verbose || LOGITALL) {node.log("Closing");}
node.status({fill:"red",shape:"ring",text:"node-red:common.status.disconnected"});
node.serverConfig.deregister(node, done);
});
}
RED.nodes.registerType("xmpp out",XmppOutNode);
}