Skip to content

Instantly share code, notes, and snippets.

@danshev
Last active August 29, 2015 14:19
Show Gist options
  • Save danshev/4227f07e5859a30e6304 to your computer and use it in GitHub Desktop.
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 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