Last active
August 29, 2015 14:19
-
-
Save danshev/4227f07e5859a30e6304 to your computer and use it in GitHub Desktop.
Dispatch() function meant to be used as the callback for .sendMessage() of Firebase's Firechat.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is client-side code meant for the callback of Firebase's Firechat .sendMessage(), it | |
// does two things: | |
// | |
// 1. It determines if there are any @mentions in the message. If there are, | |
// then it determines if the mentioned user(s) are in the chatroom. | |
// | |
// a. For each user mentioned who is NOT in the chatroom, a message to them | |
// is immediately added to the `dispatch-queue` node. | |
// | |
// b. For each user mentioned who *is* in the chatroom, then the user's | |
// online status is determined. If the user is not online, then a message | |
// to them is immediately added to the `dispatch-queue` node. If the user | |
// *is* online, then (since they're both in the chatroom and online) we | |
// assume they received the message. | |
// | |
// 2. If there aren't any @mentions in the message, then essentially the same | |
// process is performed, just using a list of *all* members of the chatroom. | |
// | |
// The intention is to use this code in conjunction with a 3rd-party server that | |
// monitors the `dispatch-queue` node. When a message is added, the server will | |
// detect the change and dispatch a message to the target user via SMS/Push | |
// (using data from the server-side database) and then remove the message from | |
// the `dispatch-queue` node. | |
Firechat.prototype.dispatch = function(roomId, messageContent) { | |
// abort here if not auth'd | |
if (!self._user) { | |
self._onAuthRequired(); | |
if (cb) { | |
cb(new Error('Not authenticated or user not set!')); | |
} | |
return; | |
}; | |
// REGEX for @userName mentions, usernames objects | |
var regex = /(^|[^@\w])@(\w{1,15})\b/g, | |
userIds = {}, | |
roomUsers = {}; | |
// get a list of the room's current users | |
query = self._firebase.child('room-users').child(roomId); | |
query.once('value', function(snapshot) { | |
userIds = snapshot.val() || {}; | |
Object.keys(userIds).forEach(function(userId) { | |
for (var sessionId in userIds[userId]) { | |
var fullName = userIds[userId].name; | |
var userId = userId.toLowerCase(); | |
roomUsers[userId] = { name: fullName }; | |
break; | |
}; | |
}); | |
// check if the message @mentions anyone ... an @, unlike a "call out", can be directed at ANY user | |
// of the Organization. "Callouts" only affect people in the room and occurs when one's first name is | |
// used. For example: (Assuming "David Sch" is in a room) "I was going to talk to David about that" | |
// would qualify as a callout. | |
if (regex.test(messageContent)){ | |
var usersToCheck = []; | |
// before we proceed, first check if any of the room's participants were called out by first name | |
// without an @ sign. | |
var usersCalledout = self.checkCallouts(roomUsers, messageContent); | |
// append the usersCalledout to usersToCheck | |
usersToCheck.push.apply(usersToCheck, usersCalledout); | |
// get an array of all users @mentioned | |
// *note* each element still has an '@' sign | |
var mentions = messageContent.match(regex); | |
// Style points ... if there's only one mention and it's at the beginning | |
// of the message, then remove it (cleaner message to the recipient). | |
// * Don't do this if anyone was called out (hence the first check of length == 0), because | |
// that person might also be offline and need a SMS/Push message. If so, then shaving off | |
// the principle recipient would remove important context. | |
// | |
// Example: "@thatGuy were you going to talk about that with David?" | |
// ==> we wouldn't want David -- called out -- to receive just | |
// "were you going to talk about that with David?" | |
if (usersToCheck.length == 0) { | |
if (mentions.length == 1) { | |
if (messageContent.indexOf(mentions[0].trim()) == 0) { | |
var amountToSlice = mentions[0].trim().length; | |
messageContent = messageContent.slice(amountToSlice).replace(/(\s*[\,-:])/g, '').trim(); | |
}; | |
}; | |
}; | |
// loop through the @mentions | |
for (var userIdMentioned in mentions){ | |
// trim any white space and slice off the '@' sign ... also lowercase() the mentioned userId | |
var recipientUserId = userIdMentioned.trim().slice(1).toLowerCase(); | |
// determine if the mentioned user is a participant in the room | |
if (!(recipientUserId in roomUsers)){ | |
// the mentioned user is not in the room ==> dispatch message | |
self.dispatchMessage(recipientUserId, roomId, messageContent); | |
} | |
else { | |
// mentioned user *is* in the room | |
// if they're not already in the `usersToCheck` array (due to being "called out"), then | |
// add them to list of users for whom we will now determine if are online | |
if usersToCheck.indexOf(recipientUserId) < 0 { | |
usersToCheck.append(recipientUserId); | |
}; | |
}; | |
}; | |
// if there are users who were mentioned for whom we need to determine status ... | |
if (usersToCheck.length > 0){ | |
// ... check! | |
self.checkOnline(usersToCheck, roomId, messageContent); | |
}; | |
} | |
// no one was mentioned, so it's essentially addressed to all users in the room | |
else { | |
self.checkOnline(roomUsers, roomId, messageContent); | |
}; | |
}); | |
}; | |
// takes an object of { userId: "Full Name" } -- meant to be those in the chatroom -- and the messageContent. | |
// returns an array of userId's of users who were "called out" (mentioned without an @ sign). | |
function checkCallouts(roomUsers, messageContent){ | |
var usersToCheck = []; | |
// *** requires modifying usernamesUnique from [] ==> {} | |
Object.keys(roomUsers).forEach(function (userId) { | |
var lowercaseFirstName = roomUsers[userId].name.split(" ")[0].toLowerCase(); | |
var lowercaseMessage = messageContent.toLowerCase(); | |
if (lowercaseMessage.indexOf(lowercaseFirstName) >= 0) { | |
usersToCheck.append(userId); | |
}; | |
}); | |
return usersToCheck; | |
}; | |
function checkOnline(roomUsers, roomId, messageContent){ | |
// get a list of the users online | |
self._usersOnlineRef.once('value', function(usersOnline) { | |
// check if the user is in the list of online users | |
Object.keys(roomUsers).forEach(function (recipientId) { | |
if (!usersOnline.hasChild(recipientId)) { | |
// the user is not online ==> dispatch a message | |
self.dispatchMessage(recipientId, roomId, messageContent); | |
}; | |
}); | |
}); | |
}; | |
function dispatchMessage(recipientId, roomId, message){ | |
// build the message | |
var self = this, | |
message = { | |
senderId: self._userId, | |
recipientId: recipientId, | |
roomId: roomId, | |
message: message | |
}, | |
newDispatchRef; | |
// add message to the dispatch-queue node | |
newDispatchRef = self._dispatchRef.push(); | |
newDispatchRef.setWithPriority(message, Firebase.ServerValue.TIMESTAMP); | |
}; | |
this._dispatchRef = this._firebase.child('dispatch-queue'); | |
this._usersOnlineRef = this._firebase.child('users-online'); // *** change to the default | |
// Create and automatically enter a new chat room. | |
// *** changed to | |
Firechat.prototype.createRoom = function(roomName, roomType, callback) { | |
var self = this, | |
newRoomRef = this._roomRef.push(); | |
// format the name based on | |
switch(roomType) { | |
case 'private': | |
code block | |
break; | |
case 'public': | |
code block | |
break; | |
default: | |
default code block | |
} | |
var newRoom = { | |
id: newRoomRef.key(), | |
name: roomName, | |
type: roomType || 'public', | |
createdByUserId: this._userId, | |
createdAt: Firebase.ServerValue.TIMESTAMP | |
}; | |
if (roomType === 'private') { | |
newRoom.authorizedUsers = {}; | |
newRoom.authorizedUsers[this._userId] = true; | |
} | |
newRoomRef.set(newRoom, function(error) { | |
if (!error) { | |
self.enterRoom(newRoomRef.key()); | |
} | |
if (callback) { | |
callback(newRoomRef.key()); | |
} | |
}); | |
}; | |
function Firechat(firebaseRef, options) { | |
// Instantiate a new connection to Firebase. | |
this._firebase = firebaseRef; | |
... | |
// build various user groups | |
// `allUsers` will provide the main dataset to search when the User types @{{ anyone's name }} | |
// `producerUsers` and `reporterUsers` will be used if the special @producers or @reporters tags are used. | |
// In these last two cases, the sets will be used by the dispatch() function in determining | |
// which userIds need to be checked for in-room? and/or online? status. | |
this._allUsers = {}; | |
this._producerUsers = {}; | |
this._reporterUsers = {}; | |
var userGroups = {}; | |
userGroups["all"] = "_allUsers"; | |
userGroups["producers"] = "_producerUsers"; | |
userGroups["reporters"] = "_reporterUsers"; | |
// roll through and query the users associated with each group, store in respective this._ object | |
Object.keys(userGroups).forEach(function (userGroup) { | |
var firebaseChild = userGroups[userGroup]; | |
query = self._firebase.child('users').child(firebaseChild); | |
query.once('value', function(snapshot) { | |
snapshot.forEach(function(childSnapshot) { | |
var userId = snapshot.key().toLowerCase(); | |
var userName = snapshot.val(); | |
this[firebaseChild][userId] = userName; | |
}); | |
}); | |
}); | |
// Used when opening a [private] chat window with another User via GUI. This | |
// will either create and enter a new room -or- return the ID of an existing. | |
// | |
// User argument must be in the format: { id: 'UserID', name: 'User Name' } | |
Firechat.prototype.getPrivateRoomId = function(user) { | |
var self = this; | |
// get all rooms | |
query = this._roomRef; | |
query.once('value', function(snapshotAllRooms) { | |
// get the "target" user's rooms | |
query = this._firebase.child('users').child(user.id).child('rooms'); | |
query.once('value', function(snapshotTheirRooms) { | |
var existingRoomId; | |
// for each of the current user's rooms ... | |
Object.keys(this._rooms).forEach(function (roomId) { | |
// essentially break out of the forEach once a value for existingRoomId is set | |
if (!existingRoomId){ | |
// ensure the room exists "globally" (this should always be true) | |
if (snapshotAllRooms.hasChild(roomId)){ | |
var room = snapshotAllRooms.child(roomId).val(); | |
// if the room is private ... | |
if (room.type == 'private'){ | |
// ... determine if the "target" user is in it | |
if (snapshotTheirRooms.hasChild(roomId) { | |
existingRoomId = roomId; | |
}; | |
}; | |
}; | |
}; | |
}); | |
// now that we've evaluted all our rooms, enter or create | |
if (existingRoomId) { | |
// based on the automatic `invite` -- `accept invite`, if there's an existing room, | |
// then both users should already be in it, so this is more of a just-in-case. | |
self.enterRoom(existingRoomId); | |
return existingRoomId; | |
} | |
else { | |
self.createRoom("Private Chat", "private", function(newRoomId){ | |
self.inviteAddUser(user.id, newRoomId, "Private Chat"); | |
return newRoomId; | |
}); | |
}; | |
}); | |
}); | |
}; | |
// Top level function to add a user to a room. | |
// If a room is private, the function will perform the necessary to check | |
// to determine whether the user should be added to the private room (and the room | |
// converted to a group room) -or- whether an entirely new group room should be spawned. | |
// | |
// User argument must be in the format: { id: 'UserID', name: 'User Name' } | |
Firechat.prototype.addUser = function(user, roomId) { | |
var self = this; | |
query = this._firebase.child('users').child(user.id).child('rooms'); | |
query.once('value', function(snapshotTheirRooms) { | |
// verify that the to-add User isn't already in the room | |
if (!snapshotTheirRooms.hasChild(roomId)) { | |
// get the room's metadata | |
roomRef = this._roomRef.child(roomId); | |
roomRef.once('value', function(roomSnapshot) { | |
var room = roomSnapshot.val(); | |
// there's a special case with private rooms | |
if (room.type == 'private') { | |
// determine how many messages are associated with the current room | |
query = this._messageRef.child(roomId); | |
query.once('value', function(messagesSnapshot) { | |
if (messagesSnapshot.numChildren() < 10){ | |
// fresh room ==> convert room to type == 'group' & invite the user. | |
room.type = 'group'; | |
roomRef.update(room, function(){ | |
self.inviteAddUser(user.id, room.id, room.name); | |
}); | |
} | |
else { | |
// get the users from the last room | |
self.getUsersByRoom(roomId, function(users){ | |
// add the new user to the list of previous users | |
users[user.id] = user; | |
// remove the current user from the list of previous users | |
delete users[self._userId]; | |
// note: createRoom() adds the current user to the room anyways | |
self.createRoom(roomName, 'group', function(newRoomId){ | |
// now that the room is created, invite all the other users | |
Object.keys(users).forEach(function(userId) { | |
self.inviteAddUser(userId, newRoomId, roomName); | |
}); | |
}); | |
}); | |
}; | |
}); | |
} | |
else { | |
self.inviteAddUser(user.id, room.id, room.name); | |
}; | |
}); | |
}; | |
}); | |
}; | |
// A wrapper for the invite function. | |
// Even though we've moded _onFirechatInvite() to auto-accept Invitations, it still lives on | |
// *THE CLIENT-SIDE*, so certain actions will not occur if the "target" user isn't connected. | |
// As such, the extra set() calls are used in order to add a few nodes necessary to support | |
// dispatching a message to the "target" User, even if they're initially offline. | |
Firechat.prototype.inviteAddUser(userId, roomId, roomName){ | |
// invite (which will be auto-accepted) in order to trigger listeners if the user is connected | |
self.inviteUser(userId, roomId); | |
// add the room to room-users in order to ensure the user is detected by the dispatch() function | |
self._firebase.child('room-users').child(roomId).set({ | |
id: roomId, | |
name: roomName, | |
active: true | |
}); | |
// add to the room the user's rooms node in order to ensure they listen to it upon connection | |
self._firebase.child('users').child(userId).child('rooms').child(roomId).set({ | |
id: roomId, | |
name: roomName, | |
active: true | |
}); | |
}; | |
// *** remove: | |
// this._privateRoomRef = this._firebase.child('room-private-metadata'); // Firebase doesn't reference it anywhere else | |
// *** modify: | |
// Events to monitor chat invitations and invitation replies. | |
_onFirechatInvite: function(snapshot) { | |
var self = this, | |
invite = snapshot.val(); | |
// Skip invites we've already responded to. | |
if (invite.status) { | |
return; | |
} | |
invite.id = invite.id || snapshot.key(); | |
self.acceptInvite(invite.id); // <<------ this is the addition | |
/* | |
self.getRoom(invite.roomId, function(room) { | |
invite.toRoomName = room.name; | |
self._invokeEventCallbacks('room-invite', invite); | |
}); | |
*/ | |
}, | |
// Invite a user to a specific chat room. | |
Firechat.prototype.inviteUser = function(userId, roomId) { | |
var self = this, | |
sendInvite = function() { | |
var inviteRef = self._firebase.child('users').child(userId).child('invites').push(); | |
inviteRef.set({ | |
id: inviteRef.key(), | |
fromUserId: self._userId, | |
fromUserName: self._userName, | |
roomId: roomId | |
}); | |
// Handle listen unauth / failure in case we're kicked. | |
inviteRef.on('value', self._onFirechatInviteResponse, function(){}, self); | |
}; | |
if (!self._user) { | |
self._onAuthRequired(); | |
return; | |
} | |
sendInvite(); // <<------- moved up, removed the auth stuff | |
/* | |
self.getRoom(roomId, function(room) { | |
if (room.type === 'private') { | |
var authorizedUserRef = self._roomRef.child(roomId).child('authorizedUsers'); | |
authorizedUserRef.child(userId).set(true, function(error) { | |
if (!error) { | |
sendInvite(); | |
} | |
}); | |
} else { | |
sendInvite(); | |
} | |
}); | |
*/ | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment