UNPKG

@babblevoice/babble-drachtio-presence

Version:

A presence agent which will work with other modules in this family.

353 lines (314 loc) 10.3 kB
const { XMLParser, XMLBuilder } = require( "fast-xml-parser" ) /** * * @param { object } obj * @returns { string | false } */ function objtoxml( obj ) { try { const builder = new XMLBuilder( { ignoreAttributes : false, attributeNamePrefix: "_" } ) return builder.build( obj ) } catch( error ) { console.error( "Cannot create XML", error ) return false } } /** * * @param { string } xml * @returns { object | false } */ function xmltoobj( xml ) { try { const xmlparser = new XMLParser( { ignoreAttributes : false, attributeNamePrefix: "_" } ) return xmlparser.parse( xml ) } catch( error ) { console.error( "Bad XML", error ) return false } } /** * * @param { object } xpidobj * @returns { object } */ function parseperson( xpidobj ) { const busy = !( "rpid:activities" in xpidobj.presence[ "dm:person" ] ) || "rpid:permanent-absence" in xpidobj.presence[ "dm:person" ][ "rpid:activities" ] || "rpid:busy" in xpidobj.presence[ "dm:person" ][ "rpid:activities" ] let onthephone = false let note = xpidobj.presence.tuple.note===undefined?"":xpidobj.presence.tuple.note if( xpidobj.presence[ "dm:person" ][ "rpid:activities" ] !== undefined ) { onthephone = "rpid:on-the-phone" in xpidobj.presence[ "dm:person" ][ "rpid:activities" ] if( "dm:note" in xpidobj.presence[ "dm:person" ][ "rpid:activities" ] ) { note = xpidobj.presence[ "dm:person" ][ "rpid:activities" ][ "dm:note" ] } } return { "status": busy?"closed":"open", "note": note, "dnd": busy, "onthephone": onthephone } } function parsexmlpdif( xpidobj ) { /* Favur RFC 8440 */ if( "dm:person" in xpidobj.presence ) { return parseperson( xpidobj ) } /* RFC 3863 states we SHOULD NOT (but not MUST not) use the note to determin status, but in some cases this is the only way. Phones which have supported RFC 8440 should really think about it! */ const lownote = xpidobj.presence.tuple.note.toLowerCase() return { "status": xpidobj.presence.tuple.status.basic, "note": xpidobj.presence.tuple.note, "dnd": "closed" == xpidobj.presence.tuple.status.basic || "dnd" == lownote || "busy" == lownote, "onthephone": "away" == lownote || "on the phone" == lownote } } /* # parsepidfxml Util function to look at pidf+xml docs and pull out information into status and note. contenttype ENUM{ "application/pidf+xml", "application/xpidf+xml" } Then the xml is parsed for the informating in the relavent place. "application/xpidf+xml" is specific to Polycom. Return { status, note, dnd } on success or false on failure. status and not we pull out of the xml from the most relavent place. dnd we attempt to get the most relavent place. dnd = true will instruct our callmanager (or other lib) should not attempt to place a call to this client. According to RFC 3863 status (in tuple.status) should be used to indicated if the UA can receive instant message. It is not clear if an INVITE is an instant message and so far different clients I have seen interprit this differently. Note, watch out for the difference between on-the-phone and busy. Busy is the same as DND, and wilst the user may be busy with a phone call we can place further phone calls to that UA. Althoughe almost will certainly derive on the phone from our own dialog info. */ module.exports.parsepidfxml = ( contenttype, xml ) => { const xpidobj = xmltoobj( xml ) if( "object" === typeof xpidobj && null !== xpidobj ) { if( "application/pidf+xml" === contenttype ) { return parsexmlpdif( xpidobj ) } else if ( "application/xpidf+xml" === contenttype ) { return { "status": xpidobj.presence.atom.address.status._status, "note": xpidobj.presence.atom.address.msnsubstatus._substatus, "dnd": "busy" == xpidobj.presence.atom.address.msnsubstatus._substatus.toLowerCase(), "onthephone": "inuse" === xpidobj.presence.atom.address.status._status } } } return false } /* `<?xml version="1.0" encoding="ISO-8859-1"?> <presence xmlns='urn:ietf:params:xml:ns:pidf' xmlns:dm='urn:ietf:params:xml:ns:pidf:data-model' xmlns:rpid='urn:ietf:params:xml:ns:pidf:rpid' xmlns:c='urn:ietf:params:xml:ns:pidf:cipid' entity='sip:1000@bling.babblevoice.com'> <tuple id='t6a5ed77e'> <status> <basic>open</basic> </status> </tuple> <dm:person id='p06360c4a'> <dm:note>Available</dm:note> </dm:person> </presence>` With pottential: <rpid:activities> <rpid:%s/> </rpid:activities> activity is one of (taken from RFC 4480) on-the-phone busy permanent-absence (Not there are other activities - but these appear the most appropriate and suitable for us.) */ /** * * @param { string } entity * @param { "open" | "closed" } status * @param { string } note - an extended note, i.e. Talk 077660000111 * @param { "on-the-phone"|"busy"|"permanent-absence"|string } activity * @returns */ module.exports.genpidfxml = ( entity, status = "open", note = "", activity = "" ) => { const presdoc = { "presence": { "_xmlns": "urn:ietf:params:xml:ns:pidf", "_xmlns:dm": "urn:ietf:params:xml:ns:pidf:data-model", "_xmlns:rpid": "urn:ietf:params:xml:ns:pidf:rpid", "_xmlns:c": "urn:ietf:params:xml:ns:pidf:cipid", "_entity": "sip:" + entity, "tuple": { "_id": "t6a5ed77e", "status": { "basic": { "#text": status } } }, "dm:person": { "_id": "p06360c4a" } } } if( 0 < note.length ) { presdoc.presence.tuple.status.note = note } if( 0 < activity.length ) { const rpidactivity = {} rpidactivity[ "rpid:" + activity ] = null presdoc.presence[ "dm:person" ][ "rpid:activities" ] = rpidactivity presdoc.presence[ "dm:person" ][ "rpid:activities" ][ "dm:note" ] = note } return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + objtoxml( presdoc ) } /** * * @param { string } entity * @param { "Available" | "Busy" | "Away" | "Offline" | "Do Not Disturb" | "In a Meeting" | "Out of Office" } note * @param { "open" | "closed" } status * @param { "onthephone" | "online" | "busy" | "away" } substatus * @returns */ module.exports.genxpidfxml = ( entity, status = "closed" , note = "Offline", substatus = "away" ) => { const presdoc = { "presence": { "status": { "note": note }, "presentity": { "_uri": "sip:" + entity + ";method=SUBSCRIBE" }, "atom": { "_id": entity, "address": { "_uri": entity + ";user=ip", "_priority": "0.800000", "status": { "_status": status, }, "msnsubstatus": { "_substatus": substatus } } } } } return "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE presence PUBLIC \"-//IETF//DTD RFCxxxx XPIDF 1.0//EN\" \"xpidf.dtd\">" + objtoxml( presdoc ) + "\n" } /* This function is our creator function from RFC 4235. <dialog-info xmlns="urn:ietf:params:xml:ns:dialog-info" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:ietf:params:xml:ns:dialog-info" version="1" state="full"> </dialog-info> A dialog element is <dialog id="123456"> <state>confirmed</state> <duration>274</duration> <local> <identity display="Alice">sip:alice@example.com</identity> <target uri="sip:alice@pc33.example.com"></target> </local> <remote> <identity display="Bob">sip:bob@example.org</identity> <target uri="sip:bobster@phone21.example.org"/> </remote> </dialog> <dialog id="123"> <state>confirmed</state> <duration>274</duration> <local> <identity display="Alice">sip:alice@example.com</identity> <target uri="sip:bobster@phone21.example.org"></target> </local> <remote> <identity display="Bob">sip:bob@example.org</identity> <target uri="ip:bobster@phone21.example.org"></target> </remote> </dialog> */ /** * * @param { number } version * @param { "full" | "partial" } state * @param { string } entity * @param { string } display * @param { object } [ call ] * @param { object } call.sip * @param { string } call.sip.callid * @param { string } call.direction * @param { string } call.state * @param { number } call.duration * @param { object } call.remote * @param { string } call.remote.display * @param { string } call.remote.uri * @param { boolean } call.hasmedia * @returns */ module.exports.createdialoginfoxml = ( version, state, entity, display, call ) => { if( undefined === call ) { return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + objtoxml( { "dialog-info": { "_xmlns": "urn:ietf:params:xml:ns:dialog-info", //"_xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", //"_xsi:schemaLocation": "urn:ietf:params:xml:ns:dialog-info", "_version": ""+version, "_state": state, "_entity": "sip:" + entity } } ) } return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + objtoxml( { "dialog-info": { "_xmlns": "urn:ietf:params:xml:ns:dialog-info", //"_xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", //"_xsi:schemaLocation": "urn:ietf:params:xml:ns:dialog-info", "_version": ""+version, "_state": state, "_entity": "sip:" + entity, "dialog": { "_id": call.sip.callid, "_direction": call.direction, "state": call.state, "duration": call.duration, "local": { "identity": { "_display": display, "#text": "sip:" + entity }, "target": { "_uri": "sip:" + entity, "param": { "pname": "sip.rendering", "pval": call.hasmedia?"yes":"no" /* used for hold as well as to inidicate media transmitting */ } } }, "remote": { "identity": { "_display": call.remote.display, "#text": "sip:" + call.remote.uri }, "target": { "_uri": "sip:**" + entity } } } } } ) }