bigbluebutton-Github/labs/bbb-html5-client/lib/redis_action.coffee

552 lines
26 KiB
CoffeeScript
Raw Normal View History

_ = require("lodash")
rack = require("hat").rack()
config = require("../config")
Logger = require("./logger")
RedisKeys = require("./redis_keys")
Utils = require("./utils")
moduleDeps = ["RedisStore"]
# Includes methods to fetch and set values related to meetings in redis. Acts as a bridge
# between the application and redis.
#
# All callbacks are in the format `method(error, response)`, where `response` is usually the
# response returned by the method called on redis.
#
# @TODO some callbacks are not always called
# @TODO use standard success/error responses (e.g. sometimes failure returns `null`, sometimes `false`)
module.exports = class RedisAction
constructor: ->
config.modules.wait moduleDeps, =>
@redisStore = config.modules.get("RedisStore")
# Set the public and session ID to match one another for lookup later
#
# @param meetingID [string] the ID of the meeting
# @param sessionID [string] the sessionID (unique ID) of the user
# @param publicID [string] the unique public ID of the user
# @param callback [Function(err, reply)] callback function
setIDs: (meetingID, sessionID, publicID, callback) ->
@redisStore.set RedisKeys.getSessionIDString(meetingID, sessionID), publicID, (err, reply) =>
registerResponse "setIDs", err, reply
if err?
callback?(err, reply)
else
@redisStore.set RedisKeys.getPublicIDString(meetingID, publicID), sessionID, (err, reply) ->
callback?(err, reply)
# Given a meetingID, sessionID and username a meeting will be created and a user with the
# given username will be joined. The callback indicates either true or false depending on whether
# the meeting was created successfully or not.
#
# @param meetingID [string] the meeting ID of the meeting we are creating and/or connecting to
# @param sessionID [string] the session ID of the user that is connecting to the meeting
# @param username [string] username of the users that that is connecting to the meeting
# @param callback [Function(succeeded)] the callback function returns true if meeting successfully started and joined, false otherwise
makeMeeting: (meetingID, sessionID, username, callback) ->
failed = false
publicID = new Date().getTime()
@_isMeetingRunning meetingID, (err, isRunning) =>
# TODO: Currently the meeting is always created in the flash client. To allow the HTML5 to create
# a meeting this block has to be implemented
# unless isRunning
# @redisStore.sadd RedisKeys.getMeetingsString(), meetingID, (err, reply) =>
# @setCurrentTool meetingID, "line"
# @setPresenter meetingID, sessionID, publicID
@createUser meetingID, sessionID, (err, reply) =>
failed or= err?
@setIDs meetingID, sessionID, publicID, (err, reply) =>
failed or= err?
# TODO: comment what these parameters are used for
properties = ["username", username, "meetingID", meetingID, "refreshing", false, "dupSess", false, "sockets", 0, "pubID", publicID]
@updateUserProperties meetingID, sessionID, properties, (err, reply) ->
failed or= err?
callback?(!failed)
# Create a reference to a user on redis.
# The user is identified by his sessionID and will be grouped inside the users key for
# the meeting `meetingID`.
#
# @example Example values for the key created
# meeting-183f0bf3a0982a127bdb8161e0c44eb696b3e75c-1382970693956-users qT6PfopPUcRsRbtH4T9A8o2R.y7bD/JtU6MFbBn9g1lhKAYmNkD/rzZRS5AYl7TWw/9A
#
# @param meetingID [string] the ID of the meeting
# @param userID [string] the unique session ID of the user
# @param callback [Function(err, reply)] callback function
createUser: (meetingID, userID, callback) ->
@redisStore.sadd RedisKeys.getUsersString(meetingID), userID, (err, reply) ->
registerResponse "createUser", err, reply
callback?(err, reply)
# Set user properties on redis
#
# @param meetingID [string] the ID of the meeting
# @param userID [string] the unique session ID of the user
# @param properties [Object] a hash of properties to set as the users properties
# @param callback [Function(err, properties)] the callback function to be called when finished
updateUserProperties: (meetingID, userID, properties, callback) ->
properties.unshift RedisKeys.getUserString(meetingID, userID)
# push the callback as the last parameter in the array
properties.push (err, reply) ->
registerResponse "updateUserProperties", err, reply
callback?(err, reply)
@redisStore.hmset.apply @redisStore, properties
# Sets the current URL for the current presentation page.
#
# @param url [string] the URL for the page being set as current
# @param callback [Function(err, reply)] called when the URL is set, with the error and reply from redis
setCurrentUrl: (url, callback) ->
# TODO: "currentUrl" is a bad name for a key, there's probably already a key with the current page
@redisStore.set "currentUrl", url, (err, reply) ->
registerResponse "setCurrentUrl", err, reply, "setting current page url to #{url}"
callback?(err, reply)
# Called when the application receives a `changeslide` from the server.
#
# @param url [string] the URL for the slide that will be shown
# @param callback [Function(err, reply)] called when the URL is set, with the error and reply from redis
onChangeSlide: (url, callback) ->
@setCurrentUrl url, callback
# Called when the application receives an `undo` from the server.
#
# @param meetingID [string] the ID of the meeting the undo was called on
# @param callback [Function(err, reply)] called when done, with the error and reply from redis
onUndo: (meetingID, callback) ->
@getCurrentPresentationID meetingID, (err, presentationID) =>
@getCurrentPageID meetingID, presentationID, (err, pageID) =>
@redisStore.rpop RedisKeys.getCurrentShapesString(meetingID, presentationID, pageID), (err, reply) =>
callback?(err, reply)
# Called when the application receives a `clrPaper` from the server.
#
# @param meetingID [string] the ID of the meeting the method was called on
# @param callback [Function(err, reply)] called when done, with the error and reply from redis
onClearPaper: (meetingID, callback) ->
@getCurrentPresentationID meetingID, (err, presentationID) =>
@getCurrentPageID meetingID, presentationID, (err, pageID) =>
@_getItemIDs meetingID, presentationID, pageID, "currentshapes", (err, itemIDs) =>
@_deleteItemList meetingID, presentationID, pageID, "currentshapes", itemIDs, (err, reply) ->
emit()
# Get the session ID (private) of the current presenter.
#
# @param meetingID [string] the ID of the meeting
# @param callback [Function(error, presenterID)] callback function
getPresenterSessionID: (meetingID, callback) ->
@redisStore.hget RedisKeys.getPresenterString(meetingID), "sessionID", (err, reply) ->
registerResponse "getPresenterSessionID", err, reply
callback?(err, reply)
# Get the public ID of the presenter.
#
# @param meetingID [string] the ID of the meeting
# @param callback [Function(err, id)] callback function
getPresenterPublicID: (meetingID, callback) ->
@redisStore.hget RedisKeys.getPresenterString(meetingID), "publicID", (err, id) ->
registerResponse "getPresenterPublicID", err, id
callback?(err, id)
# Get the current tool of the meeting.
#
# @param meetingID [string] the ID of the meeting
# @param callback [Function(err, tool)] callback function
getCurrentTool: (meetingID, callback) ->
@redisStore.get RedisKeys.getCurrentToolString(meetingID), (err, reply) ->
registerResponse "getCurrentTool", err, reply
callback?(err, reply)
# Get a image size from the image on the page
#
# @param meetingID [string] the ID of the meeting
# @param presentationID [string] the unique ID of the presentation in the meeting
# @param pageID [string] the unique ID of the page in the presentation
# @param callback [Function(err, width, height)] the callback function to be called when finished
getImageSize: (meetingID, presentationID, pageID, callback) ->
@redisStore.get RedisKeys.getPageWidthString(meetingID, presentationID, pageID), (err, width) =>
registerResponse "getImageSize", err, width, "getting width of #{meetingID}, #{presentationID}, #{pageID}"
@redisStore.get RedisKeys.getPageHeightString(meetingID, presentationID, pageID), (err, height) =>
registerResponse "getImageSize", err, height, "getting height of #{meetingID}, #{presentationID}, #{pageID}"
callback?(err, width, height)
# Get the page IDs for a presentation
#
# @param meetingID [string] the ID of the meeting
# @param presentationID [string] the unique ID of the presentation in the meeting
# @param callback [Function(err, ids)] the callback function to be called when finished
getPageIDs: (meetingID, presentationID, callback) ->
@redisStore.lrange RedisKeys.getPagesString(meetingID, presentationID), 0, -1, (err, pageIDs) ->
registerResponse "getPageIDs", err, pageIDs, "getting page IDs for #{meetingID}, #{presentationID}"
callback?(err, pageIDs)
# Checks the redis datastore whether the session is valid
#
# @param meetingID [string] the ID of the meeting
# @param sessionID [string] the sessionID (unique ID) of the user
# @param callback [Function(err, isValid)] callback function returns true if valid session
isValidSession: (meetingID, sessionID, callback) ->
@redisStore.sismember RedisKeys.getUsersString(meetingID), sessionID, (err, isValid) ->
registerResponse "isValidSession", err, isValid, "checking session #{meetingID}, #{sessionID}"
callback?(err, isValid)
# Delete the user from redis
#
# @param meetingID [string] the ID of the meeting
# @param sessionID [string] the sessionID (unique ID) of the user
# @param callback [Function(err, isValid)] callback function called when finished
deleteUser: (meetingID, sessionID, callback) ->
@redisStore.srem RedisKeys.getUsersString(meetingID), sessionID, (err, reply) =>
registerResponse "deleteUser", err, reply, "deleting user #{meetingID}, #{sessionID}"
if err?
callback?(err, reply)
else
@redisStore.del RedisKeys.getUserString(meetingID, sessionID), (err, reply) =>
registerResponse "deleteUser", err, reply, "deleting user string #{meetingID}, #{sessionID}"
callback?(err, reply)
# Gets all the properties associated with a specific user (sessionID)
#
# @param meetingID [string] the ID of the meeting
# @param sessionID [string] the sessionID (unique ID) of the user
# @param callback [Function(err, properties)] callback function
getUserProperties: (meetingID, sessionID, callback) ->
@redisStore.hgetall RedisKeys.getUserString(meetingID, sessionID), (err, properties) ->
registerResponse "getUserProperties", err, properties, "getting user properties #{meetingID}, #{sessionID}"
callback?(err, properties)
# Get all users and their data in an array
#
# @param meetingID [string] the ID of the meeting
# @param callback [Function(err, users)] callback function
#
# @todo callback is not always called, can use async for this
getUsers: (meetingID, callback) ->
users = []
usercount = 0
usersdone = 0
@redisStore.smembers RedisKeys.getUsersString(meetingID), (err, userids) =>
registerResponse "getUsers", err, userids, "getting users string for #{meetingID}"
userids.forEach (userid) =>
@redisStore.hgetall RedisKeys.getUserString(meetingID, userid), (err, props) =>
registerResponse "getUsers", err, props, "getting user string for #{meetingID}, #{userid}"
users.push props
usersdone++
if userids.length is usersdone
callback?(err, users)
# Get array of items by item name and meeting id
#
# @param meetingID [string] the ID of the meeting
# @param presentationID [string] the unique ID of the presentation in the meeting
# @param pageID [string] the unique ID of the page in the presentation
# @param item [string] the name of the type of item
# @param callback [Function(err, items)] the callback function to be called when finished
#
# @todo callback is not always called, can use async for this
getItems: (meetingID, presentationID, pageID, item, callback) ->
items = []
itemCount = 0
itemsDone = 0
itemsGetFunction = undefined
itemGetFunction = undefined
itemGetFunction = @_getItemStringFunction(item)
itemsGetFunction = @_getItemsStringFunction(item)
@redisStore.lrange itemsGetFunction(meetingID, presentationID, pageID), 0, -1, (err, itemIDs) =>
registerResponse "getItems", err, itemIDs, "getting items for #{meetingID}, #{presentationID}, #{pageID}"
callback?([]) if itemCount is 0
itemIDs.forEach (itemID) =>
@redisStore.hgetall itemGetFunction(meetingID, presentationID, pageID, itemID), (err, itemHash) ->
registerResponse "getItems", err, itemHash, "getting item #{meetingID}, #{presentationID}, #{pageID}, #{itemID}"
items.push itemHash
itemsDone++
if itemIDs.length is itemsDone
callback?(err, items)
# Get the image filename of a presentation page
#
# @param meetingID [string] the ID of the meeting
# @param presentationID [string] the unique ID of the presentation in the meeting
# @param pageID [string] the unique ID of the page in the presentation
# @param callback [Function(err, filename)] the callback function to be called when finished
getPageImage: (meetingID, presentationID, pageID, callback) ->
@redisStore.get RedisKeys.getPageImageString(meetingID, presentationID, pageID), (err, filename) ->
registerResponse "getPageImage", err, filename, "getting page image #{meetingID}, #{presentationID}, #{pageID}"
callback?(err, filename)
# Get the current presentation ID for the meeting
#
# @param meetingID [string] the ID of the meeting
# @param callback [Function(err, id)] callback function
getCurrentPresentationID: (meetingID, callback) ->
@redisStore.get RedisKeys.getCurrentPresentationString(meetingID), (err, id) ->
registerResponse "getCurrentPresentationID", err, id, "getting current presentation id for #{meetingID}"
callback?(err, id)
# Get the current page of the presentation
#
# @param meetingID [string] the ID of the meeting
# @param presentationID [string] the unique ID of the presentation in the meeting
# @param callback [Function(err, id)] the callback function to be called when finished
getCurrentPageID: (meetingID, presentationID, callback) ->
# the first element in the pages is always the current page
@redisStore.lindex RedisKeys.getPagesString(meetingID, presentationID), 0, (err, id) ->
registerResponse "getCurrentPageID", err, id, "getting current page id for #{meetingID}, #{presentationID}"
callback?(err, id)
# Set the value of the current viewbox for the meeting
#
# @param meetingID [string] the ID of the meeting
# @param viewbox [string] the string representing the viewbox value
# @param callback [Function(err, reply)] the callback function to be called when finished
# @todo sets values on redis, should be on bbb-apps
setViewBox: (meetingID, viewbox, callback) ->
@redisStore.set RedisKeys.getCurrentViewBoxString(meetingID), viewbox, (err, reply) ->
registerResponse "setViewBox", err, reply, "setting view box for #{meetingID}, #{viewbox}"
callback?(err, reply)
# Get the current viewbox of the meeting
#
# @param meetingID [string] the ID of the meeting
# @param callback [Function(err, viewbox)] the callback function to be called when finished
getViewBox: (meetingID, callback) ->
@redisStore.get RedisKeys.getCurrentViewBoxString(meetingID), (err, reply) ->
registerResponse "getViewBox", err, reply, "getting view box for #{meetingID}"
callback?(err, JSON.parse(reply))
# Get the list of meetings from redis
#
# @param callback [Function(err, meetings)] the callback function to be called when finished
#
# @example Returned meeting array:
# [ { meetingID: '183f0bf3a0982a127bdb8161e0c44eb696b3e75c-1383072476983',
# externalID: 'Demo Meeting',
# meetingName: 'Demo Meeting' } ]
getMeetings: (callback) ->
@redisStore.smembers "meetings", (err, meetingids) =>
registerResponse "getMeetings", err, meetingids
if meetingids
index = 0
resp = []
f = (err, properties) =>
if index is meetingids.length
callback?(err, resp)
else
r =
meetingID: meetingids[index]
externalID: properties["externalID"]
meetingName: properties["name"]
resp.push r
index += 1
@redisStore.hgetall "meeting-" + meetingids[index], f
@redisStore.hgetall "meeting-" + meetingids[index], f
else
callback?(null, [])
#
# # Private methods
#
# Returns the function for getting the string of a specific item given the name of
# the item type in redis.
#
# @param itemString [string] the name of the item
# @return [Function] the function used to get the key for a specific item from redis
# @private
_getItemStringFunction: (itemString) ->
functions =
messages: RedisKeys.getMessageString
shapes: RedisKeys.getShapeString
currentshapes: RedisKeys.getShapeString
functions[itemString]
# Returns the function for getting the string of all the items given the name of the items
# in redis
#
# @param itemString [string] the name of the item
# @return [Function] the function used to get the key for the list of specific items in redis
# @private
_getItemsStringFunction: (itemString) ->
functions =
messages: RedisKeys.getMessagesString
shapes: RedisKeys.getCurrentShapesString
currentshapes: RedisKeys.getCurrentShapesString
functions[itemString]
# Get the session ID from the public ID of a user
#
# @param meetingID [string] the ID of the meeting
# @param publicID [string] the unique public ID of the user
# @param callback(sessionID) [Function] callback function
# @private
_getSessionIDFromPublicID: (meetingID, publicID, callback) ->
@redisStore.get RedisKeys.getPublicIDString(meetingID, publicID), (err, sessionID) ->
registerError("_getSessionIDFromPublicID", err)
callback?(sessionID)
# Set the image for a particular page ID
#
# @param meetingID [string] the ID of the meeting
# @param presentationID [string] the unique ID of the presentation in the meeting
# @param pageID [string] the unique ID of the page in the presentation
# @param imageName [string] the file name of the image
# @private
_setPageImage: (meetingID, presentationID, pageID, imageName, callback) ->
@redisStore.set RedisKeys.getPageImageString(meetingID, presentationID, pageID), imageName, (err, reply) ->
registerSuccess("_setPageImage", "set page #{pageID} image to #{imageName}") if reply
registerError("_setPageImage", err, "couldn't set page #{pageID} image to #{imageName}") if err?
callback?()
# Delete a list of items from redis
#
# @param meetingID [string] the ID of the meeting
# @param presentationID [string] the unique ID of the presentation in the meeting
# @param pageID [string] the unique ID of the page in the presentation
# @param itemName [string] the type of items being deleted
# @param callback [Function(err, reply)] the callback function to be called when finished
# @private
_deleteItemList: (meetingID, presentationID, pageID, itemName, callback) ->
@redisStore.del @_getItemsStringFunction(itemName)(meetingID, presentationID, pageID), (err, reply) ->
registerResponse "_deleteItemList", err, reply, "deleting the list of items: #{itemName}"
callback?(err, reply)
# Deletes the items by itemName and an array of itemIDs (use helper).
#
# @param meetingID [string] the ID of the meeting
# @param presentationID [string] the unique ID of the presentation in the meeting
# @param pageID [pageID] the unique ID of the page in the presentation
# @param itemName [string] the name of the item
# @param itemIDs [string] an array of itemIDs to delete
# @param callback [Function(err, reply)] the callback function to be called when finished
# @private
#
# @todo review if the loop is ok
# @todo callback should be called after all items are deleted, use a lib like 'async' or other similar
# @todo add examples of what types of items can be removed
_deleteItems: (meetingID, presentationID, pageID, itemName, itemIDs, callback) ->
getString = @_getItemStringFunction(itemName)
itemIDs.forEach (item) =>
@redisStore.del getString(meetingID, presentationID, pageID, item), (err, reply) ->
registerResponse "_deleteItems", err, reply, "deleting the item: #{itemName}"
# Delete a meeting from redis.
#
# @param meetingID [string] the ID of the meeting
# @param callback [Function(err, reply)] the callback function to be called when finished
# @private
_deleteMeeting: (meetingID, callback) ->
@redisStore.srem RedisKeys.getMeetingsString(), meetingID, (err, reply) ->
registerResponse "_deleteMeeting", err, reply, "deleting the meeting from the list: #{meetingID}"
callback?(err, reply)
# Get the item IDs from the item name
#
# @param meetingID [string] the ID of the meeting
# @param presentationID [string] the unique ID of the presentation in the meeting
# @param pageID [string] the unique ID of the page in the presentation
# @param itemName [string] the name of the type of items getting the IDs of
# @param callback [Function(err, reply)] the callback function to be called when finished
# @private
_getItemIDs: (meetingID, presentationID, pageID, itemName, callback) ->
method = @_getItemsStringFunction(itemName)
@redisStore.lrange method(meetingID, presentationID, pageID), 0, -1, (err, itemIDs) ->
registerResponse "_getItemIDs", err, itemIDs
callback?(err, itemIDs)
# Get the list of presentation IDs for a meeting
#
# @param meetingID [string] the ID of the meeting
# @param callback [Function(err, ids)] callback called when finished
# @private
_getPresentationIDs: (meetingID, callback) ->
@redisStore.smembers RedisKeys.getPresentationsString(meetingID), (err, presIDs) ->
registerResponse "_getPresentationIDs", err, presIDs
callback?(err, presIDs)
# Checks whether the meeting is running or not
#
# @param meetingID [string] the ID of the meeting
# @param callback [Function(err, isRunning)] callback function returns true if meeting is running
# @private
_isMeetingRunning: (meetingID, callback) ->
@redisStore.sismember RedisKeys.getMeetingsString(), meetingID, (err, reply) ->
registerResponse "isMeetingRunning", err, reply, "checking if meeting is running #{meetingID}"
callback?(err, (reply is 1))
# Delete all page references from a presentation.
#
# @param meetingID [string] the ID of the meeting
# @param presentationID [string] the unique ID of the presentation in the meeting
# @param callback [Function(err, reply)] the callback function to be called when finished
# @private
#
# @todo why `while`s? and not `for`s?
# @todo review
_deletePages: (meetingID, presentationID, callback) ->
# delete each page image
@getPageIDs meetingID, presentationID, (err, pageIDs) =>
pageIDs.forEach (pageID) =>
# @todo has a callback
@_deletePageImage meetingID, presentationID, pageID
# delete list of pages
@redisStore.del RedisKeys.getPagesString(meetingID, presentationID), (err, reply) =>
registerResponse "_deletePages", err, reply, "deleting pages from #{meetingID}, #{presentationID}"
# delete currentpage
@redisStore.del RedisKeys.getCurrentPageString(meetingID, presentationID), (err, reply) ->
registerResponse "_deletePages", err, reply, "deleting current page from #{meetingID}, #{presentationID}"
callback?(err, reply)
# Delete the reference to the image from a particular presentation page in a meeting
#
# @param meetingID [string] the ID of the meeting
# @param presentationID [string] the unique ID of the presentation in the meeting
# @param pageID [string] the unique ID of the page in the presentation
# @param callback [Function(err, reply)] the callback function to be called when finished
# @private
_deletePageImage: (meetingID, presentationID, pageID, callback) ->
@redisStore.del RedisKeys.getPageImageString(meetingID, presentationID, pageID), (err, reply) ->
registerResponse "_deletePageImage", err, reply, "deleting page image for #{meetingID}, #{presentationID}, #{pageID}"
callback?(err, reply)
# Delete all presentation references from the meeting
#
# @param meetingID [string] the ID of the meeting
# @param callback [Function(err, reply)] callback function
# @private
_deletePresentations: (meetingID, callback) ->
@redisStore.del RedisKeys.getPresentationsString(meetingID), (err, reply) =>
registerResponse "_deletePresentations", err, reply, "deleting presentatations for #{meetingID}"
if err?
callback?(err, reply)
else
@redisStore.del RedisKeys.getCurrentPresentationString(meetingID), (err, reply) ->
registerResponse "_deletePresentations", err, reply, "deleting current presentatation for #{meetingID}"
callback?(err, reply)
#
# # Local methods
#
registerError = (method, err, message="") ->
Logger.error "error on RedisAction##{method}:", message, err if err?
registerSuccess = (method, message="") ->
Logger.info "success on RedisAction##{method}:", message
registerResponse = (method, err, reply, message="") ->
Utils.registerResponse "RedisAction##{method}", err, reply, message