Last active
September 16, 2015 03:51
-
-
Save Zirak/015917a951f4083d6f48 to your computer and use it in GitHub Desktop.
This file contains 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
/*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