2014-11-12 21:28:49 +08:00
|
|
|
_ = require("lodash")
|
2014-11-13 02:35:25 +08:00
|
|
|
async = require("async")
|
|
|
|
redis = require("redis")
|
2014-11-12 21:28:49 +08:00
|
|
|
|
2014-11-13 02:35:25 +08:00
|
|
|
config = require("./config")
|
2014-11-11 05:12:37 +08:00
|
|
|
CallbackEmitter = require("./callback_emitter")
|
2014-11-13 22:15:20 +08:00
|
|
|
IDMapping = require("./id_mapping")
|
2014-11-15 00:12:37 +08:00
|
|
|
Logger = require("./logger")
|
2014-11-11 03:32:51 +08:00
|
|
|
|
2014-11-12 01:48:36 +08:00
|
|
|
# The database of hooks.
|
2014-11-13 22:03:42 +08:00
|
|
|
# Used always from memory, but saved to redis for persistence.
|
|
|
|
#
|
|
|
|
# Format:
|
|
|
|
# { id: Hook }
|
|
|
|
# Format on redis:
|
|
|
|
# * a SET "...:hooks" with all ids
|
|
|
|
# * a HASH "...:hook:<id>" for each hook with some of its attributes
|
2014-11-12 01:48:36 +08:00
|
|
|
db = {}
|
2014-11-13 02:35:25 +08:00
|
|
|
nextID = 1
|
2014-11-12 01:48:36 +08:00
|
|
|
|
|
|
|
# The representation of a hook and its properties. Stored in memory and persisted
|
|
|
|
# to redis.
|
2014-11-12 20:53:49 +08:00
|
|
|
# Hooks can be global, receiving callback calls for events from all meetings on the
|
|
|
|
# server, or for a specific meeting. If an `externalMeetingID` is set in the hook,
|
|
|
|
# it will only receive calls related to this meeting, otherwise it will be global.
|
2014-11-15 02:53:46 +08:00
|
|
|
# Events are kept in a queue to be sent in the order they are received.
|
|
|
|
# TODO: The queue should be cleared at some point. The hook is destroyed if too many
|
|
|
|
# callback attempts fail, after ~5min. So the queue is already protected in this case.
|
|
|
|
# But if the requests are going by but taking too long, the queue might be increasing
|
|
|
|
# faster than the callbacks are made.
|
2014-11-11 21:58:28 +08:00
|
|
|
module.exports = class Hook
|
2014-11-11 03:32:51 +08:00
|
|
|
|
2014-11-11 05:12:37 +08:00
|
|
|
constructor: ->
|
2014-11-12 01:48:36 +08:00
|
|
|
@id = null
|
2014-11-12 02:43:41 +08:00
|
|
|
@callbackURL = null
|
2014-11-12 01:48:36 +08:00
|
|
|
@externalMeetingID = null
|
2014-11-11 05:12:37 +08:00
|
|
|
@queue = []
|
|
|
|
@emitter = null
|
2014-11-13 02:35:25 +08:00
|
|
|
@redisClient = redis.createClient()
|
|
|
|
|
|
|
|
save: (callback) ->
|
|
|
|
@redisClient.hmset config.redis.keys.hook(@id), @toRedis(), (error, reply) =>
|
2014-11-15 00:12:37 +08:00
|
|
|
Logger.error "Hook: error saving hook to redis!", error, reply if error?
|
2014-11-13 02:35:25 +08:00
|
|
|
@redisClient.sadd config.redis.keys.hooks, @id, (error, reply) =>
|
2014-11-15 00:12:37 +08:00
|
|
|
Logger.error "Hook: error saving hookID to the list of hooks!", error, reply if error?
|
2014-11-13 02:35:25 +08:00
|
|
|
|
|
|
|
db[@id] = this
|
|
|
|
callback?(error, db[@id])
|
2014-11-11 03:32:51 +08:00
|
|
|
|
2014-11-13 02:35:25 +08:00
|
|
|
destroy: (callback) ->
|
|
|
|
@redisClient.srem config.redis.keys.hooks, @id, (error, reply) =>
|
2014-11-15 00:12:37 +08:00
|
|
|
Logger.error "Hook: error removing hookID from the list of hooks!", error, reply if error?
|
2014-11-13 02:35:25 +08:00
|
|
|
@redisClient.del config.redis.keys.hook(@id), (error) =>
|
2014-11-15 00:12:37 +08:00
|
|
|
Logger.error "Hook: error removing hook from redis!", error if error?
|
2014-11-12 01:48:36 +08:00
|
|
|
|
2014-11-13 02:35:25 +08:00
|
|
|
if db[@id]
|
|
|
|
delete db[@id]
|
|
|
|
callback?(error, true)
|
|
|
|
else
|
|
|
|
callback?(error, false)
|
2014-11-12 01:48:36 +08:00
|
|
|
|
2014-11-12 20:53:49 +08:00
|
|
|
# Is this a global hook?
|
|
|
|
isGlobal: ->
|
|
|
|
not @externalMeetingID?
|
|
|
|
|
|
|
|
# The meeting from which this hook should receive events.
|
|
|
|
targetMeetingID: ->
|
|
|
|
@externalMeetingID
|
|
|
|
|
2014-11-11 05:12:37 +08:00
|
|
|
# Puts a new message in the queue. Will also trigger a processing in the queue so this
|
|
|
|
# message might be processed instantly.
|
2014-11-11 03:32:51 +08:00
|
|
|
enqueue: (message) ->
|
2014-11-15 00:12:37 +08:00
|
|
|
Logger.info "Hook: enqueueing message", JSON.stringify(message)
|
2014-11-11 05:12:37 +08:00
|
|
|
@queue.push message
|
|
|
|
@_processQueue()
|
|
|
|
|
2014-11-13 02:35:25 +08:00
|
|
|
toRedis: ->
|
2014-11-13 04:07:29 +08:00
|
|
|
r =
|
2014-11-13 02:35:25 +08:00
|
|
|
"hookID": @id,
|
|
|
|
"callbackURL": @callbackURL
|
2014-11-13 04:07:29 +08:00
|
|
|
r.externalMeetingID = @externalMeetingID if @externalMeetingID?
|
|
|
|
r
|
2014-11-13 02:35:25 +08:00
|
|
|
|
|
|
|
fromRedis: (redisData) ->
|
2014-11-13 04:07:29 +08:00
|
|
|
@id = parseInt(redisData.hookID)
|
|
|
|
@callbackURL = redisData.callbackURL
|
|
|
|
if redisData.externalMeetingID?
|
|
|
|
@externalMeetingID = redisData.externalMeetingID
|
|
|
|
else
|
|
|
|
@externalMeetingID = null
|
2014-11-13 02:35:25 +08:00
|
|
|
|
2014-11-11 05:12:37 +08:00
|
|
|
# Gets the first message in the queue and start an emitter to send it. Will only do it
|
|
|
|
# if there is no emitter running already and if there is a message in the queue.
|
|
|
|
_processQueue: ->
|
|
|
|
message = @queue[0]
|
2014-11-12 03:54:00 +08:00
|
|
|
return if not message? or @emitter?
|
2014-11-11 05:12:37 +08:00
|
|
|
|
2014-11-12 02:43:41 +08:00
|
|
|
@emitter = new CallbackEmitter(@callbackURL, message)
|
2014-11-11 05:12:37 +08:00
|
|
|
@emitter.start()
|
|
|
|
|
|
|
|
@emitter.on "success", =>
|
|
|
|
delete @emitter
|
|
|
|
@queue.shift() # pop the first message just sent
|
|
|
|
@_processQueue() # go to the next message
|
|
|
|
|
2014-11-13 04:07:29 +08:00
|
|
|
# gave up trying to perform the callback, remove the hook forever
|
|
|
|
@emitter.on "stopped", (error) =>
|
2015-11-13 02:43:11 +08:00
|
|
|
Logger.warn "Hook: too many failed attempts to perform a callback call, removing the hook for", @callbackURL
|
2014-11-13 04:07:29 +08:00
|
|
|
@destroy()
|
2014-11-12 01:48:36 +08:00
|
|
|
|
2014-11-12 02:43:41 +08:00
|
|
|
@addSubscription = (callbackURL, meetingID=null, callback) ->
|
2014-11-12 02:56:15 +08:00
|
|
|
hook = Hook.findByCallbackURLSync(callbackURL)
|
|
|
|
if hook?
|
|
|
|
callback?(new Error("There is already a subscription for this callback URL"), hook)
|
|
|
|
else
|
2014-11-12 23:07:54 +08:00
|
|
|
msg = "Hook: adding a hook with callback URL [#{callbackURL}]"
|
2014-11-12 20:53:49 +08:00
|
|
|
msg += " for the meeting [#{meetingID}]" if meetingID?
|
2014-11-15 00:12:37 +08:00
|
|
|
Logger.info msg
|
2014-11-12 20:53:49 +08:00
|
|
|
|
2014-11-12 02:56:15 +08:00
|
|
|
hook = new Hook()
|
2014-11-13 02:35:25 +08:00
|
|
|
hook.id = nextID++
|
2014-11-12 02:56:15 +08:00
|
|
|
hook.callbackURL = callbackURL
|
|
|
|
hook.externalMeetingID = meetingID
|
2014-11-13 02:35:25 +08:00
|
|
|
hook.save (error, hook) -> callback?(error, hook)
|
2014-11-12 01:48:36 +08:00
|
|
|
|
2014-11-12 21:28:49 +08:00
|
|
|
@removeSubscription = (hookID, callback) ->
|
|
|
|
hook = Hook.getSync(hookID)
|
2014-11-12 01:48:36 +08:00
|
|
|
if hook?
|
2014-11-12 20:53:49 +08:00
|
|
|
msg = "Hook: removing the hook with callback URL [#{hook.callbackURL}]"
|
|
|
|
msg += " for the meeting [#{hook.externalMeetingID}]" if hook.externalMeetingID?
|
2014-11-15 00:12:37 +08:00
|
|
|
Logger.info msg
|
2014-11-12 20:53:49 +08:00
|
|
|
|
2014-11-13 02:35:25 +08:00
|
|
|
hook.destroy (error, removed) -> callback?(error, removed)
|
2014-11-12 01:48:36 +08:00
|
|
|
else
|
|
|
|
callback?(null, false)
|
|
|
|
|
|
|
|
@countSync = ->
|
|
|
|
Object.keys(db).length
|
|
|
|
|
|
|
|
@getSync = (id) ->
|
|
|
|
db[id]
|
|
|
|
|
|
|
|
@firstSync = ->
|
|
|
|
keys = Object.keys(db)
|
|
|
|
if keys.length > 0
|
|
|
|
db[keys[0]]
|
|
|
|
else
|
|
|
|
null
|
|
|
|
|
2014-11-12 23:07:54 +08:00
|
|
|
@findByExternalMeetingIDSync = (externalMeetingID) ->
|
2014-11-12 21:28:49 +08:00
|
|
|
hooks = Hook.allSync()
|
|
|
|
_.filter(hooks, (hook) ->
|
2014-11-12 23:07:54 +08:00
|
|
|
(externalMeetingID? and externalMeetingID is hook.externalMeetingID)
|
2014-11-12 21:28:49 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
@allGlobalSync = ->
|
|
|
|
hooks = Hook.allSync()
|
|
|
|
_.filter(hooks, (hook) -> hook.isGlobal())
|
|
|
|
|
2014-11-12 01:48:36 +08:00
|
|
|
@allSync = ->
|
|
|
|
arr = Object.keys(db).reduce((arr, id) ->
|
|
|
|
arr.push db[id]
|
|
|
|
arr
|
|
|
|
, [])
|
|
|
|
arr
|
|
|
|
|
|
|
|
@clearSync = ->
|
|
|
|
for id of db
|
|
|
|
delete db[id]
|
|
|
|
db = {}
|
2014-11-12 02:56:15 +08:00
|
|
|
|
|
|
|
@findByCallbackURLSync = (callbackURL) ->
|
|
|
|
for id of db
|
|
|
|
if db[id].callbackURL is callbackURL
|
|
|
|
return db[id]
|
2014-11-13 02:35:25 +08:00
|
|
|
|
|
|
|
@initialize = (callback) ->
|
|
|
|
Hook.resync(callback)
|
|
|
|
|
|
|
|
# Gets all hooks from redis to populate the local database.
|
|
|
|
# Calls `callback()` when done.
|
|
|
|
@resync = (callback) ->
|
|
|
|
client = redis.createClient()
|
|
|
|
tasks = []
|
|
|
|
|
|
|
|
client.smembers config.redis.keys.hooks, (error, hooks) =>
|
2014-11-15 00:12:37 +08:00
|
|
|
Logger.error "Hook: error getting list of hooks from redis", error if error?
|
2014-11-13 02:35:25 +08:00
|
|
|
|
|
|
|
hooks.forEach (id) =>
|
|
|
|
tasks.push (done) =>
|
|
|
|
client.hgetall config.redis.keys.hook(id), (error, hookData) ->
|
2014-11-15 00:12:37 +08:00
|
|
|
Logger.error "Hook: error getting information for a hook from redis", error if error?
|
2014-11-13 02:35:25 +08:00
|
|
|
|
|
|
|
if hookData?
|
|
|
|
hook = new Hook()
|
2014-11-13 22:03:42 +08:00
|
|
|
hook.fromRedis(hookData)
|
2014-11-13 02:35:25 +08:00
|
|
|
hook.save (error, hook) ->
|
|
|
|
nextID = hook.id + 1 if hook.id >= nextID
|
|
|
|
done(null, hook)
|
|
|
|
else
|
|
|
|
done(null, null)
|
|
|
|
|
|
|
|
async.series tasks, (errors, result) ->
|
|
|
|
hooks = _.map(Hook.allSync(), (hook) -> "[#{hook.id}] #{hook.callbackURL}")
|
2014-11-15 00:12:37 +08:00
|
|
|
Logger.info "Hook: finished resync, hooks registered:", hooks
|
2014-11-13 02:35:25 +08:00
|
|
|
callback?()
|