From eb728721f7a96f72f79d4925098a643f83e7180c Mon Sep 17 00:00:00 2001 From: Anup Dhamala Date: Fri, 25 Sep 2015 12:02:58 -0400 Subject: [PATCH 1/5] Add functionality to send room invites --- src/xmpp.coffee | 12 ++++++++++++ test/adapter-test.coffee | 25 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/xmpp.coffee b/src/xmpp.coffee index f21ac69..157ffb8 100644 --- a/src/xmpp.coffee +++ b/src/xmpp.coffee @@ -150,6 +150,18 @@ class XmppBot extends Adapter to: "#{room.jid}/#{@robot.name}", type: 'unavailable') + # XMPP invite to a room, directly - http://xmpp.org/extensions/xep-0249.html + sendInvite: (room, invitee, reason) -> + @client.send do => + @robot.logger.debug "Inviting #{invitee} to #{room.jid}" + message = new ltx.Element('message', + to : invitee) + message.c('x', + xmlns : 'jabber:x:conference', + jid: room.jid, + reason: reason) + return message + read: (stanza) => if stanza.attrs.type is 'error' @robot.logger.error '[xmpp error]' + stanza diff --git a/test/adapter-test.coffee b/test/adapter-test.coffee index 542b42c..144e7ce 100644 --- a/test/adapter-test.coffee +++ b/test/adapter-test.coffee @@ -337,6 +337,31 @@ describe 'XmppBot', -> done() bot.topic envelope, 'one', 'two' + describe '#sendInvite()', -> + bot = Bot.use() + + bot.client = + stub: 'xmpp client' + + bot.robot = + name: 'bot' + logger: + debug: () -> + + room = + jid: 'test@example.com' + password: false + invitee = 'anup@example.com' + reason = 'Inviting to test' + + it 'should call @client.send()', (done) -> + bot.client.send = (message) -> + assert.equal message.attrs.to, invitee + assert.equal message.children[0].attrs.jid, room.jid + assert.equal message.children[0].attrs.reason, reason + done() + bot.sendInvite room, invitee, reason + describe '#error()', -> bot = Bot.use() bot.robot = From 38168baa110226fa9b1560317ae126213c91e880 Mon Sep 17 00:00:00 2001 From: Anup Dhamala Date: Sun, 27 Sep 2015 07:49:20 -0400 Subject: [PATCH 2/5] Add functionality to get list of users in a room --- src/xmpp.coffee | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/xmpp.coffee b/src/xmpp.coffee index 157ffb8..e318203 100644 --- a/src/xmpp.coffee +++ b/src/xmpp.coffee @@ -150,6 +150,22 @@ class XmppBot extends Adapter to: "#{room.jid}/#{@robot.name}", type: 'unavailable') + # This just sends the request for users in a room and the server response is + # read in readIq(), after which the event 'receivedUsersForRoom' is emitted. + # Use it to actually get the users. + # http://xmpp.org/extensions/xep-0045.html#disco-roomitems + getUsersInRoom: (room) -> + @client.send do => + @robot.logger.debug "Fetching users in the room #{room.jid}" + message = new ltx.Element('iq', + from : @options.username, + id: 'get_users_in_room' + to : room.jid, + type: 'get') + message.c('query', + xmlns : 'http://jabber.org/protocol/disco#items') + return message + # XMPP invite to a room, directly - http://xmpp.org/extensions/xep-0249.html sendInvite: (room, invitee, reason) -> @client.send do => @@ -189,6 +205,15 @@ class XmppBot extends Adapter @robot.logger.debug "[sending pong] #{pong}" @client.send pong + else if (stanza.attrs.id == 'get_users_in_room' && stanza.children[0].children) + roomJID = stanza.attrs.from + userItems = stanza.children[0].children + + # Note that this contains usernames and NOT the full user JID. + usersInRoom = (item.attrs.name for item in userItems) + @robot.logger.debug "[users in room] #{roomJID} has #{usersInRoom}" + + @emit 'receivedUsersForRoom', roomJID, usersInRoom readMessage: (stanza) => # ignore non-messages From 1adef105a9888c4cb2e24a064f23aa9f2163b591 Mon Sep 17 00:00:00 2001 From: Anup Dhamala Date: Sun, 27 Sep 2015 08:33:43 -0400 Subject: [PATCH 3/5] Tests for listing users in a room --- test/adapter-test.coffee | 43 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/test/adapter-test.coffee b/test/adapter-test.coffee index 144e7ce..7eaf6b2 100644 --- a/test/adapter-test.coffee +++ b/test/adapter-test.coffee @@ -173,6 +173,20 @@ describe 'XmppBot', -> done() bot.readIq stanza + it 'should parse room query iqs for users in the room', (done) -> + stanza.attrs.id = 'get_users_in_room' + stanza.attrs.from = 'test@example.com' + userItems = [ + { attrs: {name: 'mark'} }, + { attrs: {name: 'anup'} } + ] + stanza.children = [ {children: userItems} ] + bot.on 'receivedUsersForRoom', (roomJID, usersInRoom) -> + assert.equal roomJID, stanza.attrs.from + assert.deepEqual usersInRoom, (item.attrs.name for item in userItems) + done() + bot.readIq stanza + describe '#readMessage()', -> stanza = '' bot = Bot.use() @@ -337,6 +351,35 @@ describe 'XmppBot', -> done() bot.topic envelope, 'one', 'two' + describe '#getUsersInRoom()', -> + bot = Bot.use() + + bot.client = + stub: 'xmpp client' + + bot.robot = + name: 'bot' + logger: + debug: () -> + + bot.options = + username: 'bot@example.com' + + room = + jid: 'test@example.com' + password: false + + it 'should call @client.send()', (done) -> + bot.client.send = (message) -> + assert.equal message.attrs.from, bot.options.username + assert.equal message.attrs.id, 'get_users_in_room' + assert.equal message.attrs.to, room.jid + assert.equal message.attrs.type, 'get' + assert.equal message.children[0].name, 'query' + assert.equal message.children[0].attrs.xmlns, 'http://jabber.org/protocol/disco#items' + done() + bot.getUsersInRoom room + describe '#sendInvite()', -> bot = Bot.use() From 6ca0d43170e0a6c4babb3b398439a52dd09edb1d Mon Sep 17 00:00:00 2001 From: Anup Dhamala Date: Mon, 28 Sep 2015 13:21:54 -0400 Subject: [PATCH 4/5] Use callback after getting user data --- src/xmpp.coffee | 15 +++++++++------ test/adapter-test.coffee | 13 ++++++++++--- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/xmpp.coffee b/src/xmpp.coffee index e318203..ad49c34 100644 --- a/src/xmpp.coffee +++ b/src/xmpp.coffee @@ -150,11 +150,12 @@ class XmppBot extends Adapter to: "#{room.jid}/#{@robot.name}", type: 'unavailable') - # This just sends the request for users in a room and the server response is - # read in readIq(), after which the event 'receivedUsersForRoom' is emitted. - # Use it to actually get the users. - # http://xmpp.org/extensions/xep-0045.html#disco-roomitems - getUsersInRoom: (room) -> + # Send query for users in the room and once the server response is parsed, + # apply the callback against the retrieved data. + # callback should be of the form `(usersInRoom) -> console.log usersInRoom` + # where usersInRoom is an array of username strings. + getUsersInRoom: (room, callback) -> + # http://xmpp.org/extensions/xep-0045.html#disco-roomitems @client.send do => @robot.logger.debug "Fetching users in the room #{room.jid}" message = new ltx.Element('iq', @@ -166,6 +167,8 @@ class XmppBot extends Adapter xmlns : 'http://jabber.org/protocol/disco#items') return message + @once 'receivedUsersInRoom', callback + # XMPP invite to a room, directly - http://xmpp.org/extensions/xep-0249.html sendInvite: (room, invitee, reason) -> @client.send do => @@ -213,7 +216,7 @@ class XmppBot extends Adapter usersInRoom = (item.attrs.name for item in userItems) @robot.logger.debug "[users in room] #{roomJID} has #{usersInRoom}" - @emit 'receivedUsersForRoom', roomJID, usersInRoom + @emit 'receivedUsersInRoom', usersInRoom readMessage: (stanza) => # ignore non-messages diff --git a/test/adapter-test.coffee b/test/adapter-test.coffee index 7eaf6b2..63d710d 100644 --- a/test/adapter-test.coffee +++ b/test/adapter-test.coffee @@ -181,8 +181,7 @@ describe 'XmppBot', -> { attrs: {name: 'anup'} } ] stanza.children = [ {children: userItems} ] - bot.on 'receivedUsersForRoom', (roomJID, usersInRoom) -> - assert.equal roomJID, stanza.attrs.from + bot.on 'receivedUsersInRoom', (usersInRoom) -> assert.deepEqual usersInRoom, (item.attrs.name for item in userItems) done() bot.readIq stanza @@ -378,7 +377,15 @@ describe 'XmppBot', -> assert.equal message.children[0].name, 'query' assert.equal message.children[0].attrs.xmlns, 'http://jabber.org/protocol/disco#items' done() - bot.getUsersInRoom room + bot.getUsersInRoom room, () -> + + it 'should call callback on receiving users', (done) -> + users = ['mark', 'anup'] + bot.client.send = () -> + bot.getUsersInRoom room, (usersInRoom) -> + assert.deepEqual usersInRoom, users + done() + bot.emit 'receivedUsersInRoom', users describe '#sendInvite()', -> bot = Bot.use() From 509531f60f070cb7b4a36b8b24ff8ee1573ab14c Mon Sep 17 00:00:00 2001 From: Anup Dhamala Date: Mon, 28 Sep 2015 15:25:13 -0400 Subject: [PATCH 5/5] Use unique IDs to keep track of concurrent calls --- src/xmpp.coffee | 20 +++++++++++++++----- test/adapter-test.coffee | 12 +++++++----- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/xmpp.coffee b/src/xmpp.coffee index ad49c34..498ba39 100644 --- a/src/xmpp.coffee +++ b/src/xmpp.coffee @@ -19,6 +19,9 @@ class XmppBot extends Adapter # Key is the room JID, value is the private JID @roomToPrivateJID = {} + # http://stackoverflow.com/a/646643 + String::startsWith ?= (s) -> @slice(0, s.length) == s + run: -> options = username: process.env.HUBOT_XMPP_USERNAME @@ -154,20 +157,27 @@ class XmppBot extends Adapter # apply the callback against the retrieved data. # callback should be of the form `(usersInRoom) -> console.log usersInRoom` # where usersInRoom is an array of username strings. - getUsersInRoom: (room, callback) -> + # For normal use, no need to pass requestId: it's there for testing purposes. + getUsersInRoom: (room, callback, requestId) -> + # (pseudo) random string to keep track of the current request + # Useful in case of concurrent requests + unless requestId + requestId = 'get_users_in_room_' + Date.now() + Math.random().toString(36).slice(2) + # http://xmpp.org/extensions/xep-0045.html#disco-roomitems @client.send do => @robot.logger.debug "Fetching users in the room #{room.jid}" message = new ltx.Element('iq', from : @options.username, - id: 'get_users_in_room' + id: requestId, to : room.jid, type: 'get') message.c('query', xmlns : 'http://jabber.org/protocol/disco#items') return message - @once 'receivedUsersInRoom', callback + # Listen to the event with the current request id, one time only + @once "completedRequest#{requestId}", callback # XMPP invite to a room, directly - http://xmpp.org/extensions/xep-0249.html sendInvite: (room, invitee, reason) -> @@ -208,7 +218,7 @@ class XmppBot extends Adapter @robot.logger.debug "[sending pong] #{pong}" @client.send pong - else if (stanza.attrs.id == 'get_users_in_room' && stanza.children[0].children) + else if ((stanza.attrs.id.startsWith 'get_users_in_room') && stanza.children[0].children) roomJID = stanza.attrs.from userItems = stanza.children[0].children @@ -216,7 +226,7 @@ class XmppBot extends Adapter usersInRoom = (item.attrs.name for item in userItems) @robot.logger.debug "[users in room] #{roomJID} has #{usersInRoom}" - @emit 'receivedUsersInRoom', usersInRoom + @emit "completedRequest#{stanza.attrs.id}", usersInRoom readMessage: (stanza) => # ignore non-messages diff --git a/test/adapter-test.coffee b/test/adapter-test.coffee index 63d710d..51f837d 100644 --- a/test/adapter-test.coffee +++ b/test/adapter-test.coffee @@ -174,14 +174,14 @@ describe 'XmppBot', -> bot.readIq stanza it 'should parse room query iqs for users in the room', (done) -> - stanza.attrs.id = 'get_users_in_room' + stanza.attrs.id = 'get_users_in_room_8139nj32ma' stanza.attrs.from = 'test@example.com' userItems = [ { attrs: {name: 'mark'} }, { attrs: {name: 'anup'} } ] stanza.children = [ {children: userItems} ] - bot.on 'receivedUsersInRoom', (usersInRoom) -> + bot.on "completedRequest#{stanza.attrs.id}", (usersInRoom) -> assert.deepEqual usersInRoom, (item.attrs.name for item in userItems) done() bot.readIq stanza @@ -371,7 +371,7 @@ describe 'XmppBot', -> it 'should call @client.send()', (done) -> bot.client.send = (message) -> assert.equal message.attrs.from, bot.options.username - assert.equal message.attrs.id, 'get_users_in_room' + assert.equal (message.attrs.id.startsWith 'get_users_in_room'), true assert.equal message.attrs.to, room.jid assert.equal message.attrs.type, 'get' assert.equal message.children[0].name, 'query' @@ -381,11 +381,13 @@ describe 'XmppBot', -> it 'should call callback on receiving users', (done) -> users = ['mark', 'anup'] + requestId = 'get_users_in_room_8139nj32ma' bot.client.send = () -> - bot.getUsersInRoom room, (usersInRoom) -> + callback = (usersInRoom) -> assert.deepEqual usersInRoom, users done() - bot.emit 'receivedUsersInRoom', users + bot.getUsersInRoom room, callback, requestId + bot.emit "completedRequest#{requestId}", users describe '#sendInvite()', -> bot = Bot.use()