Skip to content

Instantly share code, notes, and snippets.

@Zirak
Last active September 16, 2015 03:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Zirak/015917a951f4083d6f48 to your computer and use it in GitHub Desktop.
Save Zirak/015917a951f4083d6f48 to your computer and use it in GitHub Desktop.
/*global Promise, Rx*/
/*global DOMParser, WebSocket, URL, fetch*/
/*global fkey*/
function fetchJson (url, options) {
// fetch closed up sending an object as a form. fak.
if (options.body === Object(options.body)) {
options.body = URL.stringify(options.body);
options.headers = options.headers || {};
options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
return fetch(url, options).then(function (resp) {
return resp.json();
});
}
function decodeHTML (html) {
var parser = new DOMParser(),
doc = parser.parseFromString(html, 'text/html');
return doc.body.textContent;
}
if (!Object.assign) {
Object.assign = function (target /*, ...sources*/) {
Array.from(arguments).slice(1).forEach(mergeObject);
return target;
function mergeObject (source) {
Object.keys(source).forEach(assignKey);
function assignKey (key) {
target[key] = source[key];
}
}
};
}
if (!Array.from) {
Array.from = function (arg) {
var ret = [];
for (var i = 0; i < arg.length; i += 1) {
ret[i] = arg[i];
}
return ret;
};
}
// this should probably be in the spec
URL.stringify = function (obj) {
return Object.keys(obj).map(stringifyPair).join('&');
function stringifyPair (key) {
var value = obj[key];
return encodeURIComponent(key) + '=' + encodeURIComponent(value);
}
};
String.prototype.capitalize = function () {
return this[0].toUpperCase() + this.slice(1);
};
String.prototype.supplant = function (arg) {
var params;
if (arguments.length > 1 || Object(params) !== params) {
params = arguments;
}
else {
params = arg;
}
return this.replace(/\{(.+?)\}/g, supplant);
function supplant (match, key) {
return params.hasOwnProperty(key) ?
params[key] :
match;
}
};
if (![].find) {
Object.defineProperty(Array.prototype, 'find', {
value : function (predicate, thisArg) {
var ret = undefined;
this.some(function (item, idx, arr) {
if (predicate.call(this, item, idx, arr)) {
ret = item;
return ret;
}
return false;
}, thisArg);
return ret;
},
writable : true,
configurable : true
});
}
var io = {
rooms : {}
};
// "constructors"
io.Room = function (roomid) {
var ret = {
id : roomid,
messages : [],
responses : {},
users : {}
};
ret.send = ret.reply;
return ret;
};
io.ChatMessage = function (context) {
// message is a simple { event, room } object. we want to turn it into
//something fun to use.
var event = context.event,
room = context.room;
var ret = {
event : event,
room : room,
user : io.User(room.users[event.user_id]),
text : decodeHTML(event.content),
reply : function (text) {
console.log(
'(%s) replying to message %d: %s',
room.id, event.message_id, text
);
var out = ':{0} {1}'.supplant(event.message_id, text);
io.output.add(out, this);
}
};
return ret;
};
io.User = function (user) {
// meh
user.isOwner = user.is_owner || user.is_mod;
// double meh
return user;
};
io.User.request = function (ids, roomId) {
if (!Array.isArray(ids)) {
ids = [ids];
}
console.log('requesting', ids);
return fetchJson('/user/info', {
method : 'POST',
body : {
ids : ids,
roomId : roomId
}
});
};
io.addUsers = function (ids, room) {
return io.User.request(ids, room.id)
.then(addUsers)
.catch(function (err) {
console.error('user request error', err);
});
function addUsers (resp) {
/*
{
users : [{
email_hash : "gravatarHash | !imageurl",
id : userIdInt,
is_moderator : bool,
is_owner : bool,
last_post : someInt,
last_seen : someOtherInt,
name : "user name",
reputation : guessWhat
}, ...]
}
*/
resp.users.forEach(function (user) {
console.debug('((', user.name, 'loaded', user, '))');
room.users[user.id] = user;
});
}
};
io.input = {
events : {
newMessage : 1,
editMessage : 2,
userJoin : 3,
userLeave : 4,
topicChange : 5,
// star event also means unstar and star cancellation. if the
//event hass a message_stars property, then that's the newest
//star count; otherwise, it's been unstarred.
star : 6,
ping : 8,
// flag event can both mean flag and un-flag.
flag : 9,
delete : 10,
// several admin/room-owner stuff.
// kickmute has content of "priv 106 created"
admin : 15,
messagePing : 18,
// message moving. 19 when a message was moved from this room to another
movedAway : 19,
// 20 when a message was moved to our room.
movedInto : 20
// we can't know from the event which room it was moved to.
// 34? it has something to do with Feeds, broadcasted to all rooms,
//couldn't discern much more.
},
socketMessageListener : function (evt) {
var data = JSON.parse(evt.data);
console.log(data);
Object.keys(data).forEach(function (strungRoomid) {
var roomid = strungRoomid.replace('r', '');
if (!io.rooms[roomid]) {
io.rooms[roomid] = io.Room(roomid);
}
this.handleRoomData(data[strungRoomid], io.rooms[roomid]);
}, this);
},
handleRoomData : function (data, room) {
// data is an object which could be several things:
/*
{}
the empty object. nothing happened.
*/
/*
{ "t" : someHugeInt, "d" : someSmallInt }
an object containing just "t" and "d". means something happened in
another room.
*/
/*
{ "e" : [...], "t" : someHugeInt, "d" someSmallInt }
the "e" property is our array of events. means something happened
in our room!
*/
// we only really care about the last one.
if (data.t) {
room.t = data.t;
}
if (!data.e) {
return;
}
data.e.forEach(function (chatEvent) {
this.handleEvent(chatEvent, room);
}, this);
},
handleEvent : function (chatEvent, room) {
var events = this.events;
var eventName = Object.keys(events).find(function (evt) {
return events[evt] === chatEvent.event_type;
});
var message = { event : chatEvent, room : room };
console.info(eventName);
if (eventName) {
this['on' + eventName.capitalize()].onNext(message);
}
else {
console.error(chatEvent, eventName);
}
},
handleNewMessage : function (message) {
var msgEvent = message.event,
room = message.room;
console.log(
'%c(%s) %s: %s',
'color:darkgreen',
msgEvent.room_name, msgEvent.user_name, msgEvent.content
);
room.messages.push(msgEvent);
// TODO de-magic 100
while (room.messages.length > 100) {
room.messages.shift();
}
},
handleEditMessage : function (message) {
var msgEvent = message.event,
room = message.room;
var eventIndex = -1;
room.messages.some(function (event, index) {
if (event.message_id === msgEvent.message_id) {
eventIndex = index;
return true;
}
return false;
});
if (eventIndex > -1) {
room.messages[eventIndex] = msgEvent;
}
console.info(
'%c(%s) %s: %s',
'color:green',
msgEvent.room_name, msgEvent.user_name, msgEvent.content
);
},
handleUserJoin : function (message) {
var userEvent = message.event,
room = message.room;
console.info(userEvent.user_name, 'joined room', userEvent.room_name);
io.addUsers(userEvent.user_id, room);
},
handleUserLeave : function (message) {
var userEvent = message.event,
room = message.room;
delete room.users[userEvent.user_id];
console.info(
'%s left room %s',
userEvent.user_name, userEvent.room_name
);
},
handleStar : function (message) {
var starEvent = message.event;
var logFormat;
if (starEvent.message_stars) {
logFormat = '%s (un?)starred: %s';
}
else {
logFormat = '%s cancelled stars of: %s';
}
console.info(
'%c' + logFormat,
'color : yellow',
starEvent.user_name, starEvent.content
);
},
handleTopicChange : function (message) {
var event = message.event;
console.info(
'(%s) Room topic changed to %s',
event.room_name,
event.content
);
},
handleDelete : function (message) {
var event = message.event;
console.info(
'(%s) %s deleted message %d',
event.room_name, event.user_name, event.message_id
);
}
};
io.output = {
// TODO should this be here? maybe move this to io.ChatMessage?
add : function (text, context) {
var msgid = context.event.message_id,
room = context.room,
msgPromise;
if (room.responses[msgid]) {
return this.edit(msgid, text, room.id);
}
return this.send(text, room.id).then(function (resp) {
room.responses[msgid] = resp.responseId;
return resp;
});
},
send : function (text, roomid) {
return fetchJson('/chats/{0}/messages/new'.supplant(roomid), {
method : 'POST',
body : {
fkey : fkey().fkey,
text : text
}
});
},
edit : function (msgid, text) {
return fetchJson('/messages/' + msgid, {
method : 'POST',
body : {
fkey : fkey().fkey,
text : text
}
});
},
delete : function (messageid) {
return fetchJson('/messages/{0}/delete'.supplant(messageid), {
method : 'POST',
body : fkey()
});
}
};
function requestRoomSocket (roomid) {
return fetchJson('/ws-auth', {
method : 'POST',
body : {
roomid : roomid,
fkey : fkey().fkey
}
}).then(function (resp) {
console.info(resp);
return new WebSocket(resp.url + '?l=99999999999999');
});
}
// meh
function loadScripts (urls) {
// can only use some basic Promise stuff here.
var urlPromises = urls.map(importScript);
return Promise.all(urlPromises);
}
function importScript (src) {
if (document.querySelector('script[src="' + src + '"]')) {
return Promise.resolve();
}
return new Promise(function (resolve, reject) {
var script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = function (e) { reject(e); };
document.head.appendChild(script);
});
}
var bootstrap = loadScripts([
'https://rawgit.com/github/fetch/master/fetch.js',
'https://rawgit.com/petkaantonov/bluebird/master/js/browser/bluebird.js',
'https://rawgit.com/Reactive-Extensions/RxJS/master/dist/rx.lite.min.js'
]).then(function () {
Promise.longStackTraces();
Object.keys(io.input.events).forEach(function (evt) {
var eventName = 'on' + evt.capitalize(),
handlerName = 'handle' + evt.capitalize();
var subject = io.input[eventName] = new Rx.Subject();
if (io.input[handlerName]) {
console.info(handlerName);
subject.subscribe(io.input[handlerName].bind(io.input));
}
});
console.log('whoop de doop');
io.input.messageStream = Rx.Observable.merge(
io.input.onNewMessage,
io.input.onEditMessage
).map(io.ChatMessage);
io.input.userStream = io.input.onUserJoin.map(io.User);
console.log('wtf mark');
var defaultRoomId = 17,
defaultRoom = io.rooms[defaultRoomId] = io.Room(defaultRoomId);
console.log('CHEEESE');
var roomPromise = requestRoomSocket(defaultRoomId).then(function (socket) {
io.socket = socket;
socket.addEventListener(
'message',
io.input.socketMessageListener.bind(io.input)
);
});
console.log('hai my name is bob');
// I dunno how it'll be done once we move server-side, but anyway...
var userIds = CHAT.RoomUsers.allPresent().toArray().map(function (u) { return u.id; });
var loadUsersPromise = io.addUsers(userIds, defaultRoom);
console.log('hai bob im alice');
return Promise.join(roomPromise, loadUsersPromise);
});
// io ends, bot begins!
var bot = {
pattern : 'bot!',
commands : {},
handleMessage : function (message) {
var messageParts = message.text.replace(bot.pattern, '').split(' '),
commandName = messageParts.shift();
message.text = messageParts.join(' ');
if (!bot.commands.hasOwnProperty(commandName)) {
message.reply('I dunno what {0} is'.supplant(commandName));
return;
}
var command = bot.commands[commandName];
// TODO command permissions
command.onNext(message);
},
isMessageAcceptable : function (message) {
// TODO:
// 1. don't talk to ourselves
// 2. ignore mindjailed users
// 3. do some spam prevention
// but in the meantime...
return true;
},
isAddressedToUs : function (message) {
return message.text.indexOf(bot.pattern) === 0;
}
};
bot.Command = function (descriptor) {
// teehee
var ret = new Rx.Subject();
Object.assign(ret, descriptor);
return ret;
};
bot.addCommand = function (name, observable) {
bot.commands[name] = observable;
};
bootstrap.then(function () {
io.input.messageStream.subscribe(function (message) {
if (/^ping$/.test(message.text)) {
message.reply('pong');
}
});
bot.io = io;
bot.messageStream = io.input.messageStream.filter(bot.isMessageAcceptable);
bot.invokeStream = bot.messageStream.filter(bot.isAddressedToUs);
// it's always fun when an indescernible line does everything important
bot.invokeStream.subscribe(bot.handleMessage);
}).then(function addShittyCommands () {
// listcommands
var listcommands = bot.Command({});
listcommands.subscribe(function (message) {
// meh
message.reply(Object.keys(bot.commands).join(', '));
});
bot.addCommand('listcommands', listcommands);
// I dunno
var wat = bot.Command({});
wat.subscribe(function (message) {
message.reply('wat?');
});
bot.addCommand('wat', wat);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment