-
Notifications
You must be signed in to change notification settings - Fork 98
Expand file tree
/
Copy pathxmpp.coffee
More file actions
407 lines (327 loc) · 13.5 KB
/
xmpp.coffee
File metadata and controls
407 lines (327 loc) · 13.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
{Adapter,Robot,TextMessage,EnterMessage,LeaveMessage} = require 'hubot'
XmppClient = require 'node-xmpp-client'
JID = require('node-xmpp-core').JID
ltx = require 'ltx'
util = require 'util'
class XmppBot extends Adapter
reconnectTryCount: 0
constructor: ( robot ) ->
@robot = robot
# Flag to log a warning message about group chat configuration only once
@anonymousGroupChatWarningLogged = false
# Store the room JID to private JID map.
# Key is the room JID, value is the private JID
@roomToPrivateJID = {}
run: ->
options =
username: process.env.HUBOT_XMPP_USERNAME
password: '********'
host: process.env.HUBOT_XMPP_HOST
port: process.env.HUBOT_XMPP_PORT
rooms: @parseRooms process.env.HUBOT_XMPP_ROOMS.split(',')
# ms interval to send whitespace to xmpp server
keepaliveInterval: 30000
legacySSL: process.env.HUBOT_XMPP_LEGACYSSL
preferredSaslMechanism: process.env.HUBOT_XMPP_PREFERRED_SASL_MECHANISM
disallowTLS: process.env.HUBOT_XMPP_DISALLOW_TLS
@robot.logger.info util.inspect(options)
options.password = process.env.HUBOT_XMPP_PASSWORD
@options = options
@connected = false
@makeClient()
# Only try to reconnect 5 times
reconnect: () ->
@reconnectTryCount += 1
if @reconnectTryCount > 5
@robot.logger.error 'Unable to reconnect to jabber server dying.'
process.exit 1
@client.removeListener 'error', @.error
@client.removeListener 'online', @.online
@client.removeListener 'offline', @.offline
@client.removeListener 'stanza', @.read
setTimeout () =>
@makeClient()
, 5000
makeClient: () ->
options = @options
@client = new XmppClient
reconnect: true
jid: options.username
password: options.password
host: options.host
port: options.port
legacySSL: options.legacySSL
preferredSaslMechanism: options.preferredSaslMechanism
disallowTLS: options.disallowTLS
@configClient(options)
configClient: (options) ->
@client.connection.socket.setTimeout 0
@client.connection.socket.setKeepAlive true, options.keepaliveInterval
@client.on 'error', @.error
@client.on 'online', @.online
@client.on 'offline', @.offline
@client.on 'stanza', @.read
@client.on 'end', () =>
@robot.logger.info 'Connection closed, attempting to reconnect'
@reconnect()
error: (error) =>
@robot.logger.error "Received error #{error.toString()}"
online: =>
@robot.logger.info 'Hubot XMPP client online'
# Setup keepalive
@client.connection.socket.setTimeout 0
@client.connection.socket.setKeepAlive true, @options.keepaliveInterval
presence = new ltx.Element 'presence'
presence.c('nick', xmlns: 'http://jabber.org/protocol/nick').t(@robot.name)
@client.send presence
@robot.logger.info 'Hubot XMPP sent initial presence'
@joinRoom room for room in @options.rooms
@emit if @connected then 'reconnected' else 'connected'
@connected = true
@reconnectTryCount = 0
parseRooms: (items) ->
rooms = []
for room in items
index = room.indexOf(':')
rooms.push
jid: room.slice(0, if index > 0 then index else room.length)
password: if index > 0 then room.slice(index+1) else false
return rooms
# XMPP Joining a room - http://xmpp.org/extensions/xep-0045.html#enter-muc
joinRoom: (room) ->
@client.send do =>
@robot.logger.debug "Joining #{room.jid}/#{@robot.name}"
# prevent the server from confusing us with old messages
# and it seems that servers don't reliably support maxchars
# or zero values
el = new ltx.Element('presence', to: "#{room.jid}/#{@robot.name}")
x = el.c('x', xmlns: 'http://jabber.org/protocol/muc')
x.c('history', seconds: 1 )
if (room.password)
x.c('password').t(room.password)
return x
# XMPP Leaving a room - http://xmpp.org/extensions/xep-0045.html#exit
leaveRoom: (room) ->
# messageFromRoom check for joined rooms so remvove it from the list
for joined, index in @options.rooms
if joined.jid == room.jid
@options.rooms.splice index, 1
@client.send do =>
@robot.logger.debug "Leaving #{room.jid}/#{@robot.name}"
return new ltx.Element('presence',
to: "#{room.jid}/#{@robot.name}",
type: 'unavailable')
# XMPP invite to a room, directly - http://xmpp.org/extensions/xep-0249.html
sendInvite: (room, invitee, message) ->
@client.send do =>
@robot.logger.debug "Inviting #{invitee} to #{room.jid}"
mes = new ltx.Element('message',
to : invitee)
mes.c('x',
xmlns : 'jabber:x:conference',
jid: room.jid,
reason: message)
return mes
# XMPP Create a room - http://xmpp.org/extensions/xep-0045.html#createroom-instant
createRoom: (room) ->
@client.send do =>
@robot.logger.debug "Sending presence for creation of room: #{room.jid}"
pres = new ltx.Element('presence',
from: @robot.name
to: "#{room.jid}/#{@robot.name}")
pres.c('x', {'xmlns': 'http://jabber.org/protocol/muc'})
.c('history', seconds: 1 )
return pres
@client.send do =>
@robot.logger.debug "Sending iq for creation of room: #{room.jid}"
iq = new ltx.Element('iq',
to: "#{room.jid}"
type: 'set')
.c('query', { 'xmlns': 'http://jabber.org/protocol/muc#owner' })
.c('x', { 'xmlns': 'jabber:x:data', type: 'submit'})
.c('history', seconds: 1 )
return iq
read: (stanza) =>
if stanza.attrs.type is 'error'
@robot.logger.error '[xmpp error]' + stanza
return
switch stanza.name
when 'message'
@readMessage stanza
when 'presence'
@readPresence stanza
when 'iq'
@readIq stanza
readIq: (stanza) =>
@robot.logger.debug "[received iq] #{stanza}"
# Some servers use iq pings to make sure the client is still functional.
# We need to reply or we'll get kicked out of rooms we've joined.
if (stanza.attrs.type == 'get' && stanza.children[0].name == 'ping')
pong = new ltx.Element('iq',
to: stanza.attrs.from
from: stanza.attrs.to
type: 'result'
id: stanza.attrs.id)
@robot.logger.debug "[sending pong] #{pong}"
@client.send pong
readMessage: (stanza) =>
# ignore non-messages
return if stanza.attrs.type not in ['groupchat', 'direct', 'chat']
return if stanza.attrs.from is undefined
# ignore empty bodies (i.e., topic changes -- maybe watch these someday)
body = stanza.getChild 'body'
return unless body
from = stanza.attrs.from
message = body.getText()
if stanza.attrs.type == 'groupchat'
# Everything before the / is the room name in groupchat JID
[room, user] = from.split '/'
# ignore our own messages in rooms or messaged without user part
return if user is undefined or user == "" or user == @robot.name
# Convert the room JID to private JID if we have one
privateChatJID = @roomToPrivateJID[from]
else
# Not sure how to get the user's alias. Use the username.
# The resource is not the user's alias but the unique client
# ID which is often the machine name
[user] = from.split '@'
# Not from a room
room = undefined
# Also store the private JID so we can use it in the send method
privateChatJID = from
# note that 'user' isn't a full JID in case of group chat,
# just the local user part
# FIXME Not sure it's a good idea to use the groupchat JID resource part
# as two users could have the same resource in two different rooms.
# I leave it as-is for backward compatiblity. A better idea would
# be to use the full groupchat JID.
user = @robot.brain.userForId user
user.type = stanza.attrs.type
user.room = room
user.privateChatJID = privateChatJID if privateChatJID
@robot.logger.debug "Received message: #{message} in room: #{user.room}, from: #{user.name}. Private chat JID is #{user.privateChatJID}"
@receive new TextMessage(user, message)
readPresence: (stanza) =>
fromJID = new JID(stanza.attrs.from)
# xmpp doesn't add types for standard available mesages
# note that upon joining a room, server will send available
# presences for all members
# http://xmpp.org/rfcs/rfc3921.html#rfc.section.2.2.1
stanza.attrs.type ?= 'available'
switch stanza.attrs.type
when 'subscribe'
@robot.logger.debug "#{stanza.attrs.from} subscribed to me"
@client.send new ltx.Element('presence',
from: stanza.attrs.to
to: stanza.attrs.from
id: stanza.attrs.id
type: 'subscribed'
)
when 'probe'
@robot.logger.debug "#{stanza.attrs.from} probed me"
@client.send new ltx.Element('presence',
from: stanza.attrs.to
to: stanza.attrs.from
id: stanza.attrs.id
)
when 'available'
# If the presence is from us, track that.
if fromJID.resource == @robot.name
@heardOwnPresence = true
return
# ignore presence messages that sometimes get broadcast
# Group chat jid are of the form
# room_name@conference.hostname/Room specific id
room = fromJID.bare().toString()
return if not @messageFromRoom room
# Try to resolve the private JID
privateChatJID = @resolvePrivateJID(stanza)
# Keep the room JID to private JID map in this class as there
# is an initialization race condition between the presence messages
# and the brain initial load.
# See https://github.com/github/hubot/issues/619
@roomToPrivateJID[fromJID.toString()] = privateChatJID?.toString()
@robot.logger.debug "Available received from #{fromJID.toString()} in room #{room} and private chat jid is #{privateChatJID?.toString()}"
# Use the resource part from the room jid as this
# is most likely the user's name
user = @robot.brain.userForId(fromJID.resource,
room: room,
jid: fromJID.toString(),
privateChatJID: privateChatJID?.toString())
# Xmpp sends presence for every person in a room, when join it
# Only after we've heard our own presence should we respond to
# presence messages.
@receive new EnterMessage user unless not @heardOwnPresence
when 'unavailable'
[room, user] = stanza.attrs.from.split '/'
# ignore presence messages that sometimes get broadcast
return if not @messageFromRoom room
# ignore our own messages in rooms
return if user == @options.username
@robot.logger.debug "Unavailable received from #{user} in room #{room}"
user = @robot.brain.userForId user, room: room
@receive new LeaveMessage(user)
# Accept a stanza from a group chat
# return privateJID (instanceof JID) or the
# http://jabber.org/protocol/muc#user extension was not provided
resolvePrivateJID: ( stanza ) ->
jid = new JID(stanza.attrs.from)
# room presence in group chat uses a jid which is not the real user jid
# To send private message to a user seen in a groupchat,
# you need to get the real jid. If the groupchat is configured to do so,
# the real jid is also sent as an extension
# http://xmpp.org/extensions/xep-0045.html#enter-nonanon
privateJID = stanza.getChild('x', 'http://jabber.org/protocol/muc#user')?.getChild?('item')?.attrs?.jid
unless privateJID
unless @anonymousGroupChatWarningLogged
@robot.logger.warning "Could not get private JID from group chat. Make sure the server is configured to broadcast real jid for groupchat (see http://xmpp.org/extensions/xep-0045.html#enter-nonanon)"
@anonymousGroupChatWarningLogged = true
return null
return new JID(privateJID)
# Checks that the room parameter is a room the bot is in.
messageFromRoom: (room) ->
for joined in @options.rooms
return true if joined.jid.toUpperCase() == room.toUpperCase()
return false
send: (envelope, messages...) ->
for msg in messages
@robot.logger.debug "Sending to #{envelope.room}: #{msg}"
to = envelope.room
if envelope.user?.type in ['direct', 'chat']
to = envelope.user.privateChatJID ? "#{envelope.room}/#{envelope.user.name}"
params =
# Send a real private chat if we know the real private JID,
# else, send to the groupchat JID but in private mode
# Note that if the original message was not a group chat
# message, envelope.user.privateChatJID will be
# set to the JID from that private message
to: to
type: envelope.user?.type or 'groupchat'
# ltx.Element type
if msg.attrs?
message = msg.root()
message.attrs.to ?= params.to
message.attrs.type ?= params.type
else
message = new ltx.Element('message', params).
c('body').t(msg)
@client.send message
reply: (envelope, messages...) ->
for msg in messages
# ltx.Element?
if msg.attrs?
@send envelope, msg
else
@send envelope, "#{envelope.user.name}: #{msg}"
topic: (envelope, strings...) ->
string = strings.join "\n"
message = new ltx.Element('message',
to: envelope.room
type: envelope.user.type
).
c('subject').t(string)
@client.send message
offline: =>
@robot.logger.debug "Received offline event"
exports.use = (robot) ->
new XmppBot robot