ami/src/ami.js/sockchat.js

523 lines
14 KiB
JavaScript

#include servers.js
#include watcher.js
var AmiSockChat = function(auth) {
var pub = {},
watchers = new AmiWatcherCollection;
var sock = undefined,
handlers = {},
dumpPackets = false;
var selfUserId = undefined,
selfChannelName = undefined;
var wasConnected = false,
wasKicked = false,
isClosing = false;
var lastPing = undefined,
lastPong = undefined,
pingTimer = undefined,
pingWatcher = undefined;
watchers.proxy(pub);
watchers.define([
'conn:init', 'conn:ready', 'conn:lost', 'conn:error',
'ping:send', 'ping:long', 'ping:recv',
'session:start', 'session:fail', 'session:term',
'user:add', 'user:remove', 'user:update', 'user:clear',
'chan:add', 'chan:remove', 'chan:update', 'chan:clear', 'chan:focus', 'chan:join', 'chan:leave',
'msg:add', 'msg:remove', 'msg:clear',
]);
pub.setPacketDump = function(value) { dumpPackets = !!value; };
var parseUserColour = function(str) {
return str;
};
var parseUserPermsSep = undefined;
var parseUserPerms = function(str) {
if(parseUserPermsSep === undefined)
parseUserPermsSep = str.indexOf("\f") > -1 ? "\f" : ' ';
return str;
};
var parseMsgFlags = function(str) {
return {
isNameBold: str[0] !== '0',
isNameItalic: str[1] !== '0',
isNameUnderline: str[2] !== '0',
isNameSeparate: str[3] !== '0',
isPrivate: str[4] !== '0',
};
};
var stopPingWatcher = function() {
if(pingWatcher === undefined)
return;
clearTimeout(pingWatcher);
pingWatcher = undefined;
};
var startPingWatcher = function() {
if(pingWatcher !== undefined)
return;
pingWatcher = setTimeout(function() {
stopPingWatcher();
if(lastPong !== undefined)
return;
watchers.call('ping:long', pub);
}, 2000);
};
var send = function(opcode, data) {
if(!sock) return;
var msg = opcode;
if(data) msg += "\t" + data.join("\t");
if(dumpPackets) console.log(msg);
sock.send(msg);
};
var sendPing = function() {
if(!UserContext.self)
return;
watchers.call('ping:send', pub);
startPingWatcher();
lastPong = undefined;
lastPing = Date.now();
send('0', [UserContext.self.id]);
};
var sendAuth = function(args) {
send('1', args);
};
var sendMessage = function(text) {
if(!UserContext.self)
return;
send('2', [UserContext.self.id, text]);
};
var startKeepAlive = function() {
if(pingTimer !== undefined)
return;
pingTimer = setInterval(sendPing, futami.get('ping') * 1000);
};
var stopKeepAlive = function() {
if(pingTimer === undefined)
return;
clearInterval(pingTimer);
pingTimer = undefined;
};
var connect = function() {
watchers.call('conn:init', pub, [{
wasConnected: wasConnected,
}]);
ami.servers.getServer(function(server) {
sock = new WebSocket(server);
sock.addEventListener('open', onOpen);
sock.addEventListener('close', onClose);
sock.addEventListener('error', onError);
sock.addEventListener('message', onMessage);
}.bind(this));
};
var formatBotMsg = function(type, args, isError) {
var str = (isError ? '1' : '0') + "\f" + type;
if(args && args.length)
str += "\f" + args.join("\f");
return str;
};
// pong handler
handlers['0'] = function(args) {
lastPong = Date.now();
watchers.call('ping:recv', pub, [{
ping: lastPing,
pong: lastPong,
diff: lastPong - lastPing,
}]);
};
// join/auth
handlers['1'] = function(args) {
if(args[0] !== 'y' && args[0] !== 'n') {
watchers.call('user:add', pub, [{
msg: {
id: args[5],
time: new Date(parseInt(args[0]) * 1000),
text: formatBotMsg('join', [args[2]]),
},
user: {
id: args[1],
name: args[2],
colour: parseUserColour(args[3]),
perms: parseUserPerms(args[4]),
permsRaw: args[4],
},
}]);
} else {
if(args[0] === 'y') {
var userId = args[1],
channelName = args[5],
maxMsgLength = parseInt(args[6]);
selfUserId = userId;
selfChannelName = channelName;
watchers.call('session:start', pub, [{
wasConnected: wasConnected,
session: { success: true },
ctx: {
maxMsgLength: maxMsgLength,
},
user: {
id: userId,
name: args[2],
colour: parseUserColour(args[3]),
perms: parseUserPerms(args[4]),
permsRaw: args[4],
},
channel: {
name: channelName,
},
}]);
wasConnected = true;
} else {
wasKicked = true;
var info = {
session: {
success: false,
reason: args[1],
},
};
if(args[2] !== undefined)
info.baka = {
type: 'join',
perma: args[2] === '-1',
until: new Date(parseInt(args[2]) * 1000),
};
watchers.call('session:fail', pub, [info]);
}
}
};
// message add
handlers['2'] = function(args) {
var info = {
msg: {
id: args[3],
time: new Date(parseInt(args[0]) * 1000),
sender: { id: args[1] },
flags: parseMsgFlags(args[4]),
flagsRaw: args[4],
isBot: args[1] === '-1',
text: args[2],
},
};
if(info.msg.isBot) {
var botParts = args[2].split("\f");
info.msg.botInfo = {
isError: botParts[0] === '1',
type: botParts[1],
args: botParts.slice(2),
};
}
watchers.call('msg:add', pub, [info]);
};
// user leave
handlers['3'] = function(args) {
watchers.call('user:remove', pub, [{
leave: { type: args[2] },
msg: {
id: args[4],
time: new Date(parseInt(args[3]) * 1000),
text: formatBotMsg(args[2], [args[1]]),
},
user: {
id: args[0],
name: args[1],
},
}]);
};
// channel add/upd/del
handlers['4'] = {};
// channel add
handlers['4']['0'] = function(args) {
watchers.call('chan:add', pub, [{
channel: {
name: args[0],
hasPassword: args[1] !== '0',
isTemporary: args[2] !== '0',
},
}]);
};
// channel update
handlers['4']['1'] = function(args) {
watchers.call('chan:update', pub, [{
channel: {
previousName: args[0],
name: args[1],
hasPassword: args[2] !== '0',
isTemporary: args[3] !== '0',
},
}]);
};
// channel nuke
handlers['4']['2'] = function(args) {
watchers.call('chan:remove', pub, [{
channel: { name: args[0] },
}]);
};
// user channel move
handlers['5'] = {};
// user join channel
handlers['5']['0'] = function(args) {
watchers.call('chan:join', pub, [{
user: {
id: args[0],
name: args[1],
colour: parseUserColour(args[2]),
perms: parseUserPerms(args[3]),
permsRaw: args[3],
},
msg: {
id: args[4],
text: formatBotMsg('jchan', [args[1]]),
},
}]);
};
// user leave channel
handlers['5']['1'] = function(args) {
watchers.call('chan:leave', pub, [{
user: { id: args[0] },
msg: {
id: args[1],
text: formatBotMsg('lchan', [UserContext.users[args[0]].username]),
},
}]);
};
// user forced channel switch
handlers['5']['2'] = function(args) {
selfChannelName = args[0];
watchers.call('chan:focus', pub, [{
channel: { name: selfChannelName },
}]);
};
// message delete
handlers['6'] = function(args) {
watchers.call('msg:remove', pub, [{
msg: { id: args[0] },
}]);
};
// context populate
handlers['7'] = {};
// existing users
handlers['7']['0'] = function(args) {
var count = parseInt(args[0]);
for(var i = 0; i < count; i++) {
var offset = 1 + 5 * i,
userId = args[offset];
if(selfUserId === userId)
continue;
watchers.call('user:add', pub, [{
user: {
id: userId,
name: args[offset + 1],
colour: parseUserColour(args[offset + 2]),
perms: parseUserPerms(args[offset + 3]),
permsRaw: args[offset + 3],
hidden: args[offset + 4] !== '0',
},
}]);
}
};
// existing messages
handlers['7']['1'] = function(args) {
var info = {
msg: {
id: args[6],
time: new Date(parseInt(args[0]) * 1000),
sender: {
id: args[1],
name: args[2],
colour: parseUserColour(args[3]),
perms: parseUserPerms(args[4]),
permsRaw: args[4],
},
isBot: args[1] === '-1',
silent: args[7] === '0',
flags: parseMsgFlags(args[8]),
flagsRaw: args[8],
text: args[5],
},
};
if(info.msg.isBot) {
var botParts = args[5].split("\f");
info.msg.botInfo = {
isError: botParts[0] === '1',
type: botParts[1],
args: botParts.slice(2),
};
}
watchers.call('msg:add', pub, [info]);
};
// existing channels
handlers['7']['2'] = function(args) {
var count = parseInt(args[0]);
for(var i = 0; i < count; i++) {
var offset = 1 + 3 * i,
chanName = args[offset];
watchers.call('chan:add', pub, [{
channel: {
name: chanName,
hasPassword: args[offset + 1] !== '0',
isTemporary: args[offset + 2] !== '0',
isCurrent: chanName === selfChannelName,
},
}]);
}
watchers.call('chan:focus', pub, [{
channel: { name: selfChannelName },
}]);
};
// context clear
handlers['8'] = function(args) {
if(args[0] === '0' || args[0] === '3' || args[0] === '4')
watchers.call('msg:clear', pub);
if(args[0] === '1' || args[0] === '3' || args[0] === '4')
watchers.call('user:clear', pub);
if(args[0] === '2' || args[0] === '4')
watchers.call('chan:clear', pub);
};
// baka (ban/kick)
handlers['9'] = function(args) {
var bakaType = args[0],
bakaTime = parseInt(args[1]);
wasKicked = true;
var info = {
session: { success: false },
baka: {
type: bakaType === '0' ? 'kick' : 'ban',
},
};
if(info.baka.type === 'ban')
info.baka.until = new Date(bakaTime * 1000);
watchers.call('session:term', pub, [info]);
};
// user update
handlers['10'] = function(args) {
watchers.call('user:update', pub, [{
user: {
id: args[0],
name: args[1],
colour: parseUserColour(args[2]),
perms: parseUserPerms(args[3]),
permsRaw: args[3],
},
}]);
};
var onOpen = function() {
watchers.call('conn:ready', pub, [{
wasConnected: wasConnected,
}]);
startKeepAlive();
watchers.call('user:clear', pub);
watchers.call('msg:clear', pub);
watchers.call('chan:clear', pub);
var authInfo = auth.getInfo();
sendAuth([authInfo.method, authInfo.token]);
};
var onClose = function(ev) {
stopPingWatcher();
stopKeepAlive();
if(isClosing || wasKicked)
return;
watchers.call('conn:lost', pub, [{
wasConnected: wasConnected,
code: ev.code,
msg: formatBotMsg('reconnect', [], true),
}]);
};
var onError = function(ex) {
watchers.call('conn:error', pub, [ex]);
};
var onMessage = function(ev) {
var args = ev.data.split("\t"),
handler = handlers;
if(dumpPackets)
console.log(args);
for(;;) {
handler = handler[args.shift()];
if(!handler)
break;
if(typeof handler === 'function') {
handler.call(pub, args);
break;
}
}
};
pub.sendMessage = sendMessage;
pub.open = function() {
connect();
};
pub.close = function() {
isClosing = true;
};
return pub;
};