Skip to content

Commit

Permalink
Merge pull request #55 from idobata/eventd
Browse files Browse the repository at this point in the history
Server-Sent Events based event streaming (aka eventd)
  • Loading branch information
hibariya authored Jan 30, 2017
2 parents 65c484b + 7df84af commit 55b9e48
Show file tree
Hide file tree
Showing 5 changed files with 1,703 additions and 134 deletions.
14 changes: 8 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,20 @@
},
"homepage": "https://github.com/idobata/hubot-idobata",
"dependencies": {
"request": "~2.75.0",
"pusher-client": "~1.1.0"
"eventsource": "badunk/eventsource-node#bugfix/reconnect-multiply-tests",
"parent-require": "^1.0.0",
"request": "~2.75.0"
},
"devDependencies": {
"chai": "~3.5.0",
"coffee-script": "~1.11.0",
"espower-coffee": "~1.0.0",
"hubot": ">= 2.7",
"memory-streams": "^0.1.0",
"mocha": "~3.1.1",
"chai": "~3.5.0",
"sinon": "~1.17.2",
"nock": "~8.0.0",
"hubot": ">= 2.7",
"power-assert": "~1.3.1",
"espower-coffee": "~1.0.0"
"sinon": "~1.17.2"
},
"directories": {
"test": "test/"
Expand Down
72 changes: 36 additions & 36 deletions src/idobata.coffee
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
Url = require('url')
Util = require('util')

Request = require('request')
Pusher = require('pusher-client')
Hubot = require('hubot')
Package = require('../package')
Request = require('request')
EventSource = require('eventsource')
Package = require('../package')

IDOBATA_URL = process.env.HUBOT_IDOBATA_URL || 'https://idobata.io/'
PUSHER_KEY = process.env.HUBOT_IDOBATA_PUSHER_KEY || '44ffe67af1c7035be764'
API_TOKEN = process.env.HUBOT_IDOBATA_API_TOKEN
try
{Adapter, TextMessage} = require('hubot')
catch
{Adapter, TextMessage} = require('parent-require')('hubot')

class Idobata extends Hubot.Adapter
IDOBATA_URL = process.env.HUBOT_IDOBATA_URL || 'https://idobata.io/'
IDOBATA_EVENTD_URL = process.env.HUBOT_IDOBATA_EVENTD_URL || IDOBATA_URL
API_TOKEN = process.env.HUBOT_IDOBATA_API_TOKEN

class Idobata extends Adapter
send: (envelope, strings...) ->
@_postMessage {source: string, room_id: @_extractRoomId(envelope)} for string in strings

Expand All @@ -22,39 +26,35 @@ class Idobata extends Hubot.Adapter
unless API_TOKEN
@emit 'error', new Error(`'The environment variable \`\033[31mHUBOT_IDOBATA_API_TOKEN\033[39m\` is required.'`)

options =
url: Url.resolve(IDOBATA_URL, '/api/seed')
headers: @_http_headers
endpoint = Url.resolve(IDOBATA_EVENTD_URL, "/api/stream")
stream = new EventSource("#{endpoint}?access_token=#{API_TOKEN}", headers: @_http_headers)

Request options, (error, response, body) =>
unless response.statusCode == 200
console.error `'Idobata returns (status=' + response.statusCode + '). Please check your \`\033[31mHUBOT_IDOBATA_API_TOKEN\033[39m\`.'`
stream.on 'open', =>
@robot.logger.info 'hubot-idobata: Established streaming connection'

@emit 'error', error
stream.on 'seed', (e) =>
@robot.logger.info 'hubot-idobata: Received seed'

seed = JSON.parse(body)
seed = JSON.parse(e.data)
_bot = seed.records.bot
bot = @robot.brain.userForId("bot:#{_bot.id}", _bot)

Util._extend bot, _bot

if bot.name != @robot.name
console.warn """
@robot.logger.warning """
Your bot on Idobata is named as '#{bot.name}'.
But this hubot is named as '#{@robot.name}'.
To respond to mention correctly, it is recommended that #{`'\033[33mHUBOT_NAME='`}#{bot.name}#{`'\033[39m'`} is configured.
"""

pusher = new Pusher(PUSHER_KEY,
encrypted: /^https/.test(IDOBATA_URL)
authEndpoint: Url.resolve(IDOBATA_URL, '/pusher/auth')
auth:
headers: @_http_headers
)
stream.on 'event', (e) =>
{type, data} = JSON.parse(e.data)

return unless type == 'message:created'

channel = pusher.subscribe(bot.channel_name)
{message} = data

channel.bind 'message:created', ({message}) =>
identifier = "#{message.sender_type.toLowerCase()}:#{message.sender_id}"
_user = {name: message.sender_name}
user = @robot.brain.userForId(identifier, _user)
Expand All @@ -63,19 +63,21 @@ class Idobata extends Hubot.Adapter

return if "bot:#{bot.id}" == identifier

textMessage = new Hubot.TextMessage(user, message.body_plain, message.id)
textMessage = new TextMessage(user, message.body_plain, message.id)
textMessage.data = message

@receive textMessage

pusher.connection.bind 'disconnected', =>
# When `pusher.connect` is failed, `disconnected` event is fired. So `setInterval` is not needed.
setTimeout ->
do pusher.connect
, @_reconnectInterval

@emit 'connected'

stream.on 'error', (e) =>
if e.status
@robot.logger.error `'hubot-idobata: Idobata returns (status=' + e.status + '). Please check your \`\033[31mHUBOT_IDOBATA_API_TOKEN\033[39m\`.'`

@emit 'error', e
else
@robot.logger.info 'hubot-idobata: The connection seems to have been temporarily lost. Reconnecting...'

sendHTML: (envelope, htmls...) ->
for html in htmls
@_postMessage
Expand Down Expand Up @@ -104,10 +106,8 @@ class Idobata extends Hubot.Adapter
Request.post(options)

_http_headers:
'X-API-Token': API_TOKEN
'User-Agent': "hubot-idobata / v#{Package.version}"

_reconnectInterval: 5 * 1000 # 5s
'Authorization': "Bearer #{API_TOKEN}"
'User-Agent': "hubot-idobata / v#{Package.version}"

exports.use = (robot) ->
new Idobata(robot)
103 changes: 47 additions & 56 deletions test/idobata.test.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,57 @@ process.env.HUBOT_IDOBATA_API_TOKEN = 'MY API TOKEN'

querystring = require('querystring')

assert = require('power-assert')
sinon = require('sinon')
nock = require('nock')

Pusher = require('pusher-client')
assert = require('power-assert')
sinon = require('sinon')
nock = require('nock')
streams = require('memory-streams')

MockRobot = require('./mock/robot')
MockPusher = require('./mock/pusher')
MessageData = require('./mock/message')

Adapter = require('../')

triggerEvent = (stream, type, data) ->
stream.append """
event: event
data: #{JSON.stringify(type: type, data: data)}
"""

describe 'hubot-idobata', ->
robot = null
adapter = null
pusher = null
stream = null

beforeEach ->
nock('https://idobata.io')
.matchHeader('X-API-Token', 'MY API TOKEN')
.get('/api/seed')
.reply 200,
version: 1
records:
bot:
id: 99
name: 'Hubot'
icon_url: 'http://www.gravatar.com/avatar/9fef32520aa08836d774873cb8b7df28.png'
token: 'API TOKEN'
status: 'online'
channel_name: 'presence-guy_99'

sinon.stub Pusher.prototype, 'initialize', ->
pusher = new MockPusher(arguments...)
seed =
version: 1
last_event_id: 42
records:
bot:
id: 99
name: 'Hubot'
icon_url: 'http://www.gravatar.com/avatar/9fef32520aa08836d774873cb8b7df28.png'
token: 'API TOKEN'
status: 'online'
channel_name: 'presence-guy_99'

stream = new streams.ReadableStream """
event: seed
data: #{JSON.stringify(seed)}
"""

nock('https://idobata.io').get('/api/stream').query(access_token: 'MY API TOKEN').reply 200, -> stream

robot = new MockRobot
adapter = robot.adapter

afterEach ->
do nock.cleanAll
do Pusher::initialize.restore

describe '#run', (done) ->
# TODO Test error thrown
Expand All @@ -54,12 +64,6 @@ describe 'hubot-idobata', ->
it 'should receive connected event', (done) ->
adapter.on 'connected', done

it 'should subscribe own channel', (done) ->
adapter.on 'connected', ->
assert pusher.channels['presence-guy_99'].length == 1

do done

context 'After connected', ->
beforeEach (done) ->
do robot.run
Expand All @@ -73,9 +77,8 @@ describe 'hubot-idobata', ->
msg.send msg.match[1]

it 'should send message', (done) ->
nock('https://idobata.io')
.matchHeader('X-API-Token', 'MY API TOKEN')
.post('/api/messages')
nock('https://idobata.io').post('/api/messages')
.matchHeader('Authorization', 'Bearer MY API TOKEN')
.reply 201, (uri, body) ->
request = querystring.parse(body)

Expand All @@ -84,12 +87,11 @@ describe 'hubot-idobata', ->

do done

pusher.channels['presence-guy_99'][0].trigger 'message:created', MessageData
triggerEvent stream, 'message:created', MessageData

it 'should respond with Robot#messageRoom', (done) ->
nock('https://idobata.io')
.matchHeader('X-API-Token', 'MY API TOKEN')
.post('/api/messages')
nock('https://idobata.io').post('/api/messages')
.matchHeader('Authorization', 'Bearer MY API TOKEN')
.reply 201, (uri, body) ->
request = querystring.parse(body)

Expand All @@ -107,9 +109,8 @@ describe 'hubot-idobata', ->
msg.reply msg.match[1]

it 'should reply mesasge to sender', (done) ->
nock('https://idobata.io')
.matchHeader('X-API-Token', 'MY API TOKEN')
.post('/api/messages')
nock('https://idobata.io').post('/api/messages')
.matchHeader('Authorization', 'Bearer MY API TOKEN')
.reply 201, (uri, body) ->
request = querystring.parse(body)

Expand All @@ -118,7 +119,7 @@ describe 'hubot-idobata', ->

do done

pusher.channels['presence-guy_99'][0].trigger 'message:created', MessageData
triggerEvent stream, 'message:created', MessageData

describe '#sendHTML', ->
beforeEach ->
Expand All @@ -128,9 +129,8 @@ describe 'hubot-idobata', ->
adapter.sendHTML envelope, '<h1>hi</h1>'

it 'should send message with HTML format', (done) ->
nock('https://idobata.io')
.matchHeader('X-API-Token', 'MY API TOKEN')
.post('/api/messages')
nock('https://idobata.io').post('/api/messages')
.matchHeader('Authorization', 'Bearer MY API TOKEN')
.reply 201, (uri, body) ->
request = querystring.parse(body)

Expand All @@ -140,33 +140,24 @@ describe 'hubot-idobata', ->

do done

pusher.channels['presence-guy_99'][0].trigger 'message:created', MessageData
triggerEvent stream, 'message:created', MessageData

describe 'User data', ->
it 'should updated in automatically', ->
assert robot.brain.userForName('hi') == null

pusher.channels['presence-guy_99'][0].trigger 'message:created',
triggerEvent stream, 'message:created',
message:
sender_id: 43
sender_type: 'User'
sender_name: 'hi'

assert robot.brain.userForId('user:43').name == 'hi'

pusher.channels['presence-guy_99'][0].trigger 'message:created',
triggerEvent stream, 'message:created',
message:
sender_id: 43
sender_type: 'User'
sender_name: 'hihi' # name is updated
sender_name: 'hihi'

assert robot.brain.userForId('user:43').name == 'hihi'

context 'when connection is disconnected', ->
beforeEach ->
adapter._reconnectInterval = 10

do pusher.disconnect

it 'should reconnect automatically', (done) ->
pusher.connection.bind 'connected', done
36 changes: 0 additions & 36 deletions test/mock/pusher.coffee

This file was deleted.

Loading

0 comments on commit 55b9e48

Please sign in to comment.