1
0
mirror of https://github.com/node-red/node-red-nodes.git synced 2023-10-10 13:36:58 +02:00

Let xmpp input node join "ALL_ROOMS" automatically (if asked)

This commit is contained in:
Dave Conway-Jones 2021-02-23 09:04:12 +00:00
parent b1a2741fa0
commit 3ad829d75a
No known key found for this signature in database
GPG Key ID: 88BA2B8A411BE9FF
3 changed files with 115 additions and 58 deletions

View File

@ -17,6 +17,9 @@
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label> <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name"> <input type="text" id="node-input-name" placeholder="Name">
</div> </div>
<div class="form-tips"><b>Note:</b> By setting Buddy to "ALL_ROOMS" and ticking "Is a chat room",
the node will try to listen to all the rooms the user has access to.
</div>
</script> </script>
<script type="text/html" data-help-name="xmpp in"> <script type="text/html" data-help-name="xmpp in">
@ -79,7 +82,6 @@
<script type="text/html" data-help-name="xmpp out"> <script type="text/html" data-help-name="xmpp out">
<p>Connects to an XMPP server to send messages.</p> <p>Connects to an XMPP server to send messages.</p>
<p>The <b>To</b> field is optional. If not set the <code>msg.topic</code> property of the message is used.</p> <p>The <b>To</b> field is optional. If not set the <code>msg.topic</code> property of the message is used.</p>
<p>If you are joining a room then the <b>To</b> field must be supplied.</p>
<p>You may also send a msg with <code>msg.presence</code> to set your presence to one of <i>chat, away, dnd</i> or <i>xa</i>. <p>You may also send a msg with <code>msg.presence</code> to set your presence to one of <i>chat, away, dnd</i> or <i>xa</i>.
If you do so then the <code>msg.payload</code> will set your status message.</p> If you do so then the <code>msg.payload</code> will set your status message.</p>
</script> </script>

View File

@ -15,13 +15,13 @@ module.exports = function(RED) {
if ("undefined" === typeof n.server || n.server === "") { if ("undefined" === typeof n.server || n.server === "") {
this.server = n.user.split('@')[1]; this.server = n.user.split('@')[1];
} }
else{ else {
this.server = n.server; this.server = n.server;
} }
if ("undefined" === typeof n.port || n.port === "") { if ("undefined" === typeof n.port || n.port === "") {
this.port = 5222; this.port = 5222;
} }
else{ else {
this.port = parseInt(n.port); this.port = parseInt(n.port);
} }
@ -50,7 +50,7 @@ module.exports = function(RED) {
this.connected = false; this.connected = false;
// store the nodes that have us as config so we know when to tear it all down. // store the nodes that have us as config so we know when to tear it all down.
this.users = {}; this.users = {};
// Store the chatrooms (MUC) that we've joined (sent "presence" XML to) already // store the chatrooms (MUC) that we've joined (sent "presence" XML to) already
this.MUCs = {}; this.MUCs = {};
// helper variable, because "this" changes definition inside a callback // helper variable, because "this" changes definition inside a callback
var that = this; var that = this;
@ -75,7 +75,8 @@ module.exports = function(RED) {
if (Object.keys(that.users).length === 0) { if (Object.keys(that.users).length === 0) {
if (that.client && that.client.connected) { if (that.client && that.client.connected) {
return that.client.stop(done); return that.client.stop(done);
} else { }
else {
return done(); return done();
} }
} }
@ -84,6 +85,7 @@ module.exports = function(RED) {
// store the last node to use us, in case we get an error back // store the last node to use us, in case we get an error back
this.lastUsed = undefined; this.lastUsed = undefined;
// function for a node to tell us it has just sent a message to our server // 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 // so we know which node to blame if it all goes Pete Tong
this.used = function(xmppThat) { this.used = function(xmppThat) {
@ -91,11 +93,10 @@ module.exports = function(RED) {
that.lastUsed = xmppThat; that.lastUsed = xmppThat;
} }
// Some errors come back as a message :-( // Some errors come back as a message :-(
// this means we need to figure out which node might have sent it // this means we need to figure out which node might have sent it
// we also deal with subscriptions (i.e. presence information) here // we also deal with subscriptions (i.e. presence information) here
this.client.on('stanza', async (stanza) =>{ this.client.on('stanza', async (stanza) => {
if (stanza.is('message')) { if (stanza.is('message')) {
if (stanza.attrs.type == 'error') { if (stanza.attrs.type == 'error') {
if (RED.settings.verbose || LOGITALL) { if (RED.settings.verbose || LOGITALL) {
@ -106,17 +107,17 @@ module.exports = function(RED) {
if (err) { if (err) {
var textObj = err.getChild('text'); var textObj = err.getChild('text');
var text = "node-red:common.status.error"; var text = "node-red:common.status.error";
if ("undefined" !== typeof textObj) { if (typeof textObj !== "undefined") {
text = textObj.getText(); text = textObj.getText();
} }
else{ else {
textObj = err.getChild('code'); textObj = err.getChild('code');
if ("undefined" !== typeof textObj) { if (typeof textObj !== "undefined") {
text = textObj.getText(); text = textObj.getText();
} }
} }
if (RED.settings.verbose || LOGITALL) {that.log("Culprit: "+that.lastUsed.id); } if (RED.settings.verbose || LOGITALL) {that.log("Culprit: "+that.lastUsed.id); }
if ("undefined" !== typeof that.lastUsed) { if (typeof that.lastUsed !== "undefined") {
that.lastUsed.status({fill:"red",shape:"ring",text:text}); that.lastUsed.status({fill:"red",shape:"ring",text:text});
that.lastUsed.warn(text); that.lastUsed.warn(text);
if (that.lastUsed.join) { if (that.lastUsed.join) {
@ -165,12 +166,10 @@ module.exports = function(RED) {
var query = stanza.getChild('query'); var query = stanza.getChild('query');
if (RED.settings.verbose || LOGITALL) {that.log("result!"); } if (RED.settings.verbose || LOGITALL) {that.log("result!"); }
if (RED.settings.verbose || LOGITALL) {that.log(query); } if (RED.settings.verbose || LOGITALL) {that.log(query); }
} }
} }
}); });
// We shouldn't have any errors here that the input/output nodes can't handle // 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 // if you need to see everything though; uncomment this block
// this.client.on('error', err => { // this.client.on('error', err => {
@ -217,6 +216,24 @@ module.exports = function(RED) {
} }
}); });
function getItems(thing,id,xmpp) {
// Now try to get a list of all items/conference rooms available on this server
var stanza = xml('iq',
{type:'get', id:id, to:thing},
xml('query', 'http://jabber.org/protocol/disco#items')
);
xmpp.send(stanza);
}
function getInfo(thing,id,xmpp) {
// Now try to get a list of all info about a thing
var stanza = xml('iq',
{type:'get', id:id, to:thing},
xml('query', 'http://jabber.org/protocol/disco#info')
);
xmpp.send(stanza);
}
function joinMUC(node, xmpp, name) { function joinMUC(node, xmpp, name) {
// the presence with the muc x element signifies we want to join the muc // 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 // if we want to support passwords, we need to add that as a child of the x element
@ -231,13 +248,15 @@ module.exports = function(RED) {
} }
else { else {
var stanza = xml('presence', var stanza = xml('presence',
{"to": name}, {"to":name},
xml("x",'http://jabber.org/protocol/muc'), xml("x",'http://jabber.org/protocol/muc',
{ maxstanzas:0, seconds:1 } xml("history", {maxstanzas:0, seconds:1}) // We don't want any history
)
); );
node.serverConfig.used(node); node.serverConfig.used(node);
node.serverConfig.MUCs[name] = "joined"; node.serverConfig.MUCs[name] = "joined";
xmpp.send(stanza); xmpp.send(stanza);
if (RED.settings.verbose || LOGITALL) { node.log("JOINED",name); }
} }
} }
@ -248,6 +267,7 @@ module.exports = function(RED) {
} }
config.MUCs = {}; config.MUCs = {};
} }
// separated out since we want the same functionality from both in and out nodes // separated out since we want the same functionality from both in and out nodes
function errorHandler(node, err){ function errorHandler(node, err){
if (!node.quiet) { if (!node.quiet) {
@ -276,7 +296,6 @@ module.exports = function(RED) {
node.error("Authorization error! "+err.condition,err); node.error("Authorization error! "+err.condition,err);
node.status({fill:"red",shape:"ring",text:"XMPP authorization failure"}); node.status({fill:"red",shape:"ring",text:"XMPP authorization failure"});
} }
// or it might have the errno set. // or it might have the errno set.
else if (err.errno === "ETIMEDOUT") { else if (err.errno === "ETIMEDOUT") {
node.error("Timeout connecting to server",err); node.error("Timeout connecting to server",err);
@ -294,6 +313,7 @@ module.exports = function(RED) {
} }
} }
function XmppInNode(n) { function XmppInNode(n) {
RED.nodes.createNode(this,n); RED.nodes.createNode(this,n);
this.server = n.server; this.server = n.server;
@ -306,6 +326,8 @@ module.exports = function(RED) {
this.quiet = false; this.quiet = false;
// MUC == Multi-User-Chat == chatroom // MUC == Multi-User-Chat == chatroom
this.muc = this.join && (this.from !== "") this.muc = this.join && (this.from !== "")
// list of possible rooms - queried from server
this.roomsFound = {};
var node = this; var node = this;
var xmpp = this.serverConfig.client; var xmpp = this.serverConfig.client;
@ -324,24 +346,30 @@ module.exports = function(RED) {
*/ */
// if we're already connected, then do the actions now, otherwise register a callback // if we're already connected, then do the actions now, otherwise register a callback
if(xmpp.status === "online") { // if (xmpp.status === "online") {
node.status({fill:"green",shape:"dot",text:"node-red:common.status.connected"}); // node.status({fill:"green",shape:"dot",text:"node-red:common.status.connected"});
if(node.muc) { // if (node.muc) {
joinMUC(node, xmpp, node.from+'/'+node.nick); // joinMUC(node, xmpp, node.from+'/'+node.nick);
} // }
} // }
// sod it, register it anyway, that way things will work better on a reconnect: // sod it, register it anyway, that way things will work better on a reconnect:
xmpp.on('online', async address => { xmpp.on('online', async address => {
node.quiet = false; node.quiet = false;
node.status({fill:"green",shape:"dot",text:"node-red:common.status.connected"}); node.status({fill:"green",shape:"dot",text:"node-red:common.status.connected"});
if (node.muc) { if (node.muc) {
if (node.from.toUpperCase() === "ALL_ROOMS") {
// try to get list of all rooms and join them all.
getItems(this.serverConfig.server,this.serverConfig.id,xmpp);
}
else {
// if we want to use a chatroom, we need to tell the server we want to join it // if we want to use a chatroom, we need to tell the server we want to join it
joinMUC(node, xmpp, node.from+'/'+node.nick); joinMUC(node, xmpp, node.from+'/'+node.nick);
} }
}
}); });
xmpp.on('connecting', async address => { xmpp.on('connecting', async address => {
if(!node.quiet) { if (!node.quiet) {
node.status({fill:"grey",shape:"dot",text:"node-red:common.status.connecting"}); node.status({fill:"grey",shape:"dot",text:"node-red:common.status.connecting"});
} }
}); });
@ -372,8 +400,7 @@ module.exports = function(RED) {
}); });
// Meat of it, a stanza object contains chat messages (and other things) // Meat of it, a stanza object contains chat messages (and other things)
xmpp.on('stanza', async (stanza) =>{ xmpp.on('stanza', async (stanza) => {
// node.log("Received stanza");
if (RED.settings.verbose || LOGITALL) {node.log(stanza); } if (RED.settings.verbose || LOGITALL) {node.log(stanza); }
if (stanza.is('message')) { if (stanza.is('message')) {
if (stanza.attrs.type == 'chat') { if (stanza.attrs.type == 'chat') {
@ -397,12 +424,12 @@ module.exports = function(RED) {
var from = parts[1]; var from = parts[1];
var body = stanza.getChild('body'); var body = stanza.getChild('body');
var payload = ""; var payload = "";
if ("undefined" !== typeof body) { if (typeof body !== "undefined") {
payload = body.getText(); payload = body.getText();
} }
var msg = { topic:from, payload:payload, room:conference }; var msg = { topic:from, payload:payload, room:conference };
if (stanza.attrs.from != node.nick) { if (from && stanza.attrs.from != node.nick && from != node.nick) {
if ((node.join) && (node.from === conference)) { if (node.from.toUpperCase() === "ALL_ROOMS" || node.from === conference) {
node.send([msg,null]); node.send([msg,null]);
} }
} }
@ -411,26 +438,28 @@ module.exports = function(RED) {
else if (stanza.is('presence')) { else if (stanza.is('presence')) {
if (['subscribe','subscribed','unsubscribe','unsubscribed'].indexOf(stanza.attrs.type) > -1) { if (['subscribe','subscribed','unsubscribe','unsubscribed'].indexOf(stanza.attrs.type) > -1) {
// this isn't for us, let the config node deal with it. // this isn't for us, let the config node deal with it.
} }
else{ else {
var state = stanza.getChild('show');
if (state) { state = state.getText(); }
else { state = "available"; }
var statusText=""; var statusText="";
if (stanza.attrs.type === 'unavailable') { if (stanza.attrs.type === 'unavailable') {
// the user might not exist, but the server doesn't tell us that! // the user might not exist, but the server doesn't tell us that!
statusText = "offline"; statusText = "offline";
state = "offline";
} }
var status = stanza.getChild('status'); var status = stanza.getChild('status');
if ("undefined" !== typeof status) { if (typeof status !== "undefined") {
statusText = status.getText(); statusText = status.getText();
} }
// right, do we care if there's no status? // right, do we care if there's no status?
if (statusText !== "") { if (statusText !== "") {
var from = stanza.attrs.from; var from = stanza.attrs.from;
var state = stanza.attrs.show;
var msg = {topic:from, payload: {presence:state, status:statusText} }; var msg = {topic:from, payload: {presence:state, status:statusText} };
node.send([null,msg]); node.send([null,msg]);
} }
else{ else {
if (RED.settings.verbose || LOGITALL) { if (RED.settings.verbose || LOGITALL) {
node.log("not propagating blank status"); node.log("not propagating blank status");
node.log(stanza); node.log(stanza);
@ -438,6 +467,32 @@ module.exports = function(RED) {
} }
} }
} }
else if (stanza.attrs.type === 'result') {
// AM To-Do check for 'bind' result with our current jid
var query = stanza.getChild('query');
if (RED.settings.verbose || LOGITALL) {this.log("result!"); }
if (RED.settings.verbose || LOGITALL) {this.log(query); }
// handle query for list of rooms available
if (query && query.attrs.hasOwnProperty("xmlns") && query.attrs["xmlns"] === "http://jabber.org/protocol/disco#items") {
var _items = stanza.getChild('query').getChildren('item');
for (var i = 0; i<_items.length; i++) {
if ( _items[i].attrs.jid.indexOf('@') === -1 ) {
// if no @ in jid then it's probably the root or the room server so ask again
getItems(_items[i].attrs.jid,this.serverConfig.jid,xmpp);
}
else {
node.roomsFound[_items[i].attrs.name] = _items[i].attrs.jid;
var name = _items[i].attrs.jid+'/'+node.serverConfig.username;
if (!(name in node.serverConfig.MUCs)) {
if (RED.settings.verbose || LOGITALL) {this.log("Need to Join room:"+name); }
joinMUC(node, xmpp, name)
}
}
}
if (RED.settings.verbose || LOGITALL) {this.log("ROOMS:"+this.server+this.roomsFound); }
}
}
}); });
// xmpp.on('subscribe', from => { // xmpp.on('subscribe', from => {
@ -451,7 +506,7 @@ module.exports = function(RED) {
if (xmpp.status === "online") { if (xmpp.status === "online") {
node.status({fill:"green",shape:"dot",text:"node-red:common.status.connected"}); node.status({fill:"green",shape:"dot",text:"node-red:common.status.connected"});
} }
else{ else {
node.status({fill:"grey",shape:"dot",text:"node-red:common.status.connecting"}); node.status({fill:"grey",shape:"dot",text:"node-red:common.status.connecting"});
if (xmpp.status === "offline") { if (xmpp.status === "offline") {
if (RED.settings.verbose || LOGITALL) { if (RED.settings.verbose || LOGITALL) {
@ -504,23 +559,25 @@ module.exports = function(RED) {
*/ */
// if we're already connected, then do the actions now, otherwise register a callback // if we're already connected, then do the actions now, otherwise register a callback
if(xmpp.status === "online") { // if (xmpp.status === "online") {
node.status({fill:"green",shape:"dot",text:"node-red:common.status.connected"}); // node.status({fill:"green",shape:"dot",text:"node-red:common.status.connected"});
if(node.muc){ // if (node.muc){
joinMUC(node, xmpp, node.to+'/'+node.nick); // // if we want to use a chatroom, we need to tell the server we want to join it
} // joinMUC(node, xmpp, node.from+'/'+node.nick);
} // }
// }
// sod it, register it anyway, that way things will work better on a reconnect: // sod it, register it anyway, that way things will work better on a reconnect:
xmpp.on('online', function(data) { xmpp.on('online', function(data) {
node.quiet = false; node.quiet = false;
node.status({fill:"green",shape:"dot",text:"node-red:common.status.connected"}); node.status({fill:"green",shape:"dot",text:"node-red:common.status.connected"});
if (node.muc) { if (node.muc) {
joinMUC(node, xmpp,node.to+"/"+node.nick); // if we want to use a chatroom, we need to tell the server we want to join it
joinMUC(node, xmpp, node.from+'/'+node.nick);
} }
}); });
xmpp.on('connecting', async address => { xmpp.on('connecting', async address => {
if(!node.quiet) { if (!node.quiet) {
node.status({fill:"grey",shape:"dot",text:"node-red:common.status.connecting"}); node.status({fill:"grey",shape:"dot",text:"node-red:common.status.connecting"});
} }
}); });
@ -555,7 +612,7 @@ module.exports = function(RED) {
if (xmpp.status === "online") { if (xmpp.status === "online") {
node.status({fill:"green",shape:"dot",text:"online"}); node.status({fill:"green",shape:"dot",text:"online"});
} }
else{ else {
node.status({fill:"grey",shape:"dot",text:"node-red:common.status.connecting"}); node.status({fill:"grey",shape:"dot",text:"node-red:common.status.connecting"});
if (xmpp.status === "offline") { if (xmpp.status === "offline") {
xmpp.start().catch(error => { xmpp.start().catch(error => {
@ -571,9 +628,7 @@ module.exports = function(RED) {
node.on("input", function(msg) { node.on("input", function(msg) {
if (msg.presence) { if (msg.presence) {
if (['away', 'dnd', 'xa', 'chat'].indexOf(msg.presence) > -1 ) { if (['away', 'dnd', 'xa', 'chat'].indexOf(msg.presence) > -1 ) {
var stanza = xml('presence', var stanza = xml('presence', {"show":msg.presence}, xml('status', {}, msg.payload));
{"show":msg.presence},
xml('status',{},msg.payload));
node.serverConfig.used(node); node.serverConfig.used(node);
xmpp.send(stanza); xmpp.send(stanza);
} }
@ -581,22 +636,22 @@ module.exports = function(RED) {
} }
else if (msg.command) { else if (msg.command) {
if (msg.command === "subscribe") { if (msg.command === "subscribe") {
var stanza = xml('presence', var stanza = xml('presence', {type:'subscribe', to:msg.payload});
{type:'subscribe', to: msg.payload});
node.serverConfig.used(node); node.serverConfig.used(node);
xmpp.send(stanza); xmpp.send(stanza);
} }
else if (msg.command === "get") { else if (msg.command === "get") {
var to = node.to || msg.topic || ""; var to = node.to || msg.topic || "";
var stanza = xml('iq', var stanza = xml('iq',
{type:'get', id:node.id, to: to}, {type:'get', id:node.id, to:to},
xml('query', 'http://jabber.org/protocol/muc#admin', xml('query', 'http://jabber.org/protocol/muc#admin',
xml('item',{affiliation:msg.payload}))); xml('item', {affiliation:msg.payload})
)
);
node.serverConfig.used(node); node.serverConfig.used(node);
if (RED.settings.verbose || LOGITALL) {node.log("sending stanza "+stanza.toString()); } if (RED.settings.verbose || LOGITALL) {node.log("sending stanza "+stanza.toString()); }
xmpp.send(stanza); xmpp.send(stanza);
} }
} }
else { else {
var to = node.to || msg.topic || ""; var to = node.to || msg.topic || "";

View File

@ -1,6 +1,6 @@
{ {
"name": "node-red-node-xmpp", "name": "node-red-node-xmpp",
"version": "0.3.4", "version": "0.4.0",
"description": "A Node-RED node to talk to an XMPP server", "description": "A Node-RED node to talk to an XMPP server",
"dependencies": { "dependencies": {
"@xmpp/client": "^0.12.0" "@xmpp/client": "^0.12.0"