Skip to content

Commit

Permalink
Migrate from simple-xmpp to xmpp/client
Browse files Browse the repository at this point in the history
  • Loading branch information
nioc committed Nov 21, 2019
1 parent 36d3c30 commit 48046db
Show file tree
Hide file tree
Showing 13 changed files with 586 additions and 225 deletions.
13 changes: 3 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ User ⇄ XMPP client ⇄ XMPP Server ⇄ **XMPP Bot** ⇄ REST A
## Key features

- Call outgoing webhook on XMPP incoming messages from user chat or group chat (Multi-user chat "MUC"),

- Send message templates (with values to apply to variables in that template) to user or room (MUC) on incoming authorized (basic or bearer) webhook.

## Installation
Expand Down Expand Up @@ -86,29 +85,24 @@ User ⇄ XMPP client ⇄ XMPP Server ⇄ **XMPP Bot** ⇄ REST A
### Logger

- `level` log4js level (all < trace < debug < info < warn < error < fatal < mark < off)

- `file`, `console` and `stdout` define log appenders (see [log4js doc](https://log4js-node.github.io/log4js-node/appenders.html))

### Webhooks listener

- `path` and `port` define the listening endpoint

- `ssl` define key and certificat location and port used for exposing in https, make sure that user of the process is allowed to read cert

- `users` is an array of user/password for basic authentication

- `accessLog` define the listener logger

### XMPP Server

- `host` and `port` define XMPP server
- `jid` and `password` define XMPP "bot" user credentials
- `service` and `domain` define XMPP server
- `username` and `password` define XMPP "bot" user credentials
- `rooms` list rooms (and optionnal password) where bot will listen

### Incoming webhooks (list)

- `path` is the webhook key:a POST request on this path will trigger corresponding `action`

- `action` among enumeration:
- `send_xmpp_message` will send message (`message` in request body) to `destination` (from request body) ; if `destination` is found in `config.xmppServer.rooms` array, message will send as a groupchat). Request exemple:

Expand All @@ -125,12 +119,11 @@ User &rlarr; XMPP client &rlarr; XMPP Server &rlarr; **XMPP Bot** &rlarr; REST A
}
```

- `send_xmpp_template` will send template with merged variables (using JMESPath) to `destination` (user or room if `sendToGroup` set to true)
- `send_xmpp_template` will send template with merged variables (using JMESPath) to `destination` (user or room if `type` set to `chat` or `groupchat`)

### XMPP hooks (list)

- `room` is the XMPP hook key: an incoming groupchat (or chat) from this room (or this user) will trigger corresponding `action`

- `action` among enumeration:
- `outgoing_webhook` will execute a request to corresponding webhook with `args` as webhook code

Expand Down
12 changes: 6 additions & 6 deletions lib/config/config.json.dist
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@
}
},
"xmppServer": {
"host": "domain-xmpp.ltd",
"port": 5222,
"jid": "[email protected]",
"service": "xmpps:https://domain-xmpp.ltd:5223",
"domain": "domain-xmpp.ltd",
"username": "[email protected]",
"password": "botPass",
"rooms": [
{
Expand All @@ -62,14 +62,14 @@
"action": "send_xmpp_template",
"args": {
"destination": "[email protected]",
"sendToGroup": true
"type": "groupchat"
},
"template": "${title}\r\n${message}\r\n${evalMatches[].metric}: ${evalMatches[].value}\r\n${imageUrl}"
}
],
"xmppHooks": [
{
"room": "bot",
"room": "bot@domain-xmpp.ltd",
"action": "outgoing_webhook",
"args": ["w1"]
},
Expand All @@ -91,4 +91,4 @@
"bearer": null
}
]
}
}
6 changes: 5 additions & 1 deletion lib/error/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ module.exports = (logger, xmpp) => {
nodeCleanup(function (exitCode, signal) {
logger.warn(`Received ${exitCode}/${signal} (application is closing), disconnect from XMPP server`)
try {
xmpp.disconnect()
xmpp.close()
.then(logger.debug('Connection successfully closed'))
.catch((error) => {
logger.error('Error during XMPP disconnection', error)
})
} catch (error) {
logger.error('Error during XMPP disconnection: ' + error.message)
}
Expand Down
4 changes: 2 additions & 2 deletions lib/outgoing/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* @license AGPL-3.0+
*/

module.exports = (logger, config, xmpp, user, destination, message, sendToGroup, code, callback = () => {}) => {
module.exports = (logger, config, xmpp, user, destination, message, type, code, callback = () => {}) => {
let webhook = config.getOutgoingWebhook(code)
if (!webhook) {
logger.warn(`There is no webhook with code "${code}"`)
Expand Down Expand Up @@ -79,7 +79,7 @@ module.exports = (logger, config, xmpp, user, destination, message, sendToGroup,
logger.trace('Response:', body)
if (body && typeof (body) === 'object' && 'reply' in body === true) {
logger.debug(`There is a reply to send back in chat ${destination}: ${body.reply}`)
xmpp.send(destination, body.reply, sendToGroup)
xmpp.send(destination, body.reply, type)
callback(null, `Message sent. There is a reply to send back in chat ${destination}: ${body.reply}`, null)
return
}
Expand Down
10 changes: 5 additions & 5 deletions lib/webhook/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,11 @@ module.exports = (logger, config, xmpp) => {
let message = req.body.message

// check if destination is a group chat
const sendToGroup = config.xmpp.rooms.some((room) => room.id === destination)
const type = config.xmpp.rooms.some((room) => room.id === destination) ? 'groupchat' : 'chat'

// send message
logger.trace(`Send to ${destination} (group:${sendToGroup}) following message :\r\n${message}`)
xmpp.send(destination, message, sendToGroup)
logger.trace(`Send to ${destination} (group:${type}) following message :\r\n${message}`)
xmpp.send(destination, message, type)
return res.status(200).send({ 'status': 'ok', destination })

case 'send_xmpp_template':
Expand All @@ -99,8 +99,8 @@ module.exports = (logger, config, xmpp) => {
logger.trace(`Arguments: ${webhook.args}`)

// send message
logger.trace(`Send to ${webhook.args.destination} (group:${webhook.args.sendToGroup}) following message :\r\n${msg}`)
xmpp.send(webhook.args.destination, msg, webhook.args.sendToGroup)
logger.trace(`Send to ${webhook.args.destination} (group:${webhook.args.type}) following message :\r\n${msg}`)
xmpp.send(webhook.args.destination, msg, webhook.args.type)
return res.status(200).send('ok')

default:
Expand Down
157 changes: 108 additions & 49 deletions lib/xmpp/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,81 +6,140 @@
* @exports xmpp
* @file This files defines the XMPP bot
* @author nioc
* @since 1.0.0
* @since 2.0.0
* @license AGPL-3.0+
*/

module.exports = (logger, config) => {
const xmpp = require('simple-xmpp')
const { client, xml, jid } = require('@xmpp/client')
const outgoing = require('../outgoing')
this.jid = null

// declare send chat/groupchat function
this.send = async (to, message, type) => {
logger.debug(`Send ${type} message to ${to}: '${message}'`)
const stanza = xml(
'message', {
to,
type
},
xml(
'body', {
},
message)
)
await xmppClient.send(stanza)
}

// declare close function
this.close = async () => {
await xmppClient.stop()
}

// create XMPP client
const xmppClient = client(config.xmpp)

// handle connection
xmpp.on('online', function (data) {
logger.info(`XMPP connected on ${config.xmpp.host}:${config.xmpp.port} with JID: ${data.jid.user}`)
this.jid = data.jid.user
xmppClient.on('online', (address) => {
logger.info(`XMPP connected on ${config.xmpp.service} with JID: ${address.toString()}`)
this.jid = address
// send presence
xmppClient.send(xml('presence'))
.then(logger.debug('presence sent'))
.catch((error) => {
logger.warn('presence returned following error:', error)
})
// join rooms
config.xmpp.rooms.forEach(function (room) {
logger.debug(`Join room: ${room.id} ('${room.id}/${data.jid.user}')`)
xmpp.join(room.id + '/' + data.jid.user, room.password)
let occupantJid = room.id + '/' + address.local
logger.debug(`Join room: ${room.id} ('${occupantJid}')`)
const stanza = xml(
'presence', {
to: occupantJid
},
xml(
'x', {
xmlns: 'https://jabber.org/protocol/muc'
}
)
)
xmppClient.send(stanza)
logger.info(`Joined room: ${room.id}`)
})
})

// handle direct message
xmpp.on('chat', function (from, message) {
logger.info(`Incoming chat message from ${from}`)
logger.debug(`Message: "${message}"`)
let xmppHook = config.getXmppHookAction(this.jid)
if (!xmppHook) {
logger.error(`There is no action for incoming chat message to ${this.jid}`)
// handle stanzas
xmppClient.on('stanza', stanza => {
if (!stanza.is('message')) {
// not a message, do nothing
return
}
switch (xmppHook.action) {
case 'outgoing_webhook':
logger.debug(`Call outgoing webhook: ${xmppHook.args[0]}`)
outgoing(logger, config, xmpp, from, from, message, false, xmppHook.args[0])
break
default:
let type = stanza.attrs.type
switch (type) {
case 'chat':
case 'groupchat':
let body = stanza.getChild('body')
if (!body) {
// empty body, do nothing
return
}
let fromJid = jid(stanza.attrs.from)
// for chat, "to" and "replyTo" must be something like "[email protected]", "from" is local part "user"
let to = this.jid.bare()
let from = fromJid.local
let replyTo = fromJid.bare()
if (type === 'groupchat') {
// for groupchat, "to" and "replyTo" is conference name, "from" is nickname
to = fromJid.bare()
from = fromJid.getResource()
replyTo = to
if (from === this.jid.local || stanza.getChild('delay')) {
// message from bot or old message, do nothing
return
}
}
let message = body.text()
logger.info(`Incoming ${type} message from ${from} (${fromJid.toString()}) to ${to}`)
logger.debug(`Message: "${message}"`)
let xmppHook = config.getXmppHookAction(to.toString())
if (!xmppHook) {
logger.error(`There is no action for incoming ${type} message to: "${to}"`)
return
}
switch (xmppHook.action) {
case 'outgoing_webhook':
logger.debug(`Call outgoing webhook: ${xmppHook.args[0]}`)
outgoing(logger, config, this, from.toString(), replyTo.toString(), message, type, xmppHook.args[0])
break
default:
break
}
break
}
})

// handle group message
xmpp.on('groupchat', function (conference, from, message, stamp, delay) {
// logger.trace(`Get following group message: "${message}" in ${conference} from ${from}. stamp = ${stamp} - delay = `, delay)
if (from === this.jid || stamp !== null) {
// message from bot, do nothing
return
}
logger.info(`Incoming groupchat message from ${from} in ${conference}`)
logger.debug(`Message: "${message}"`)
let xmppHook = config.getXmppHookAction(conference)
if (!xmppHook) {
logger.error(`There is no action for incoming groupchat message from conference: "${conference}"`)
return
}
switch (xmppHook.action) {
case 'outgoing_webhook':
logger.debug(`Call outgoing webhook: ${xmppHook.args[0]}`)
outgoing(logger, config, xmpp, from, conference, message, true, xmppHook.args[0])
break
default:
break
}
// handle status
xmppClient.on('status', (status) => {
logger.trace(`Status changed to ${status}`)
})

// trace input/output
// xmppClient.on('input', (input) => {
// logger.trace('<<<<', input)
// })
// xmppClient.on('output', (output) => {
// logger.trace('>>>', output)
// })

// handle error
xmpp.on('error', function (err) {
logger.error(err)
xmppClient.on('error', (err) => {
logger.error('XMPP client encountered following error:', err.message)
process.exit(99)
})

// connect
xmpp.connect(config.xmpp)

// get roster
xmpp.getRoster()
xmppClient.start()
.catch(logger.error)

return xmpp
return this
}
Loading

0 comments on commit 48046db

Please sign in to comment.