614 lines
17 KiB
JavaScript
614 lines
17 KiB
JavaScript
#include eventtarget.js
|
|
#include websock.js
|
|
|
|
Umi.Protocol.SockChat.Protocol = function(pingDuration) {
|
|
if(typeof pingDuration !== 'number')
|
|
throw 'pingDuration must be a number';
|
|
|
|
const eventTarget = new MamiEventTarget('mami:proto');
|
|
|
|
const parseUserColour = str => {
|
|
// todo
|
|
return str;
|
|
};
|
|
|
|
let parseUserPermsSep;
|
|
const parseUserPerms = str => {
|
|
parseUserPermsSep ??= str.includes("\f") ? "\f" : ' ';
|
|
return str.split(parseUserPermsSep);
|
|
};
|
|
|
|
const parseMsgFlags = str => {
|
|
return {
|
|
nameBold: str[0] !== '0',
|
|
nameItalics: str[1] !== '0',
|
|
nameUnderline: str[2] !== '0',
|
|
showColon: str[3] !== '0',
|
|
isPM: str[4] !== '0',
|
|
isAction: str[1] !== '0' && str[3] === '0',
|
|
};
|
|
};
|
|
|
|
let wasConnected = false;
|
|
let wasKicked = false;
|
|
let isRestarting = false;
|
|
let dumpPackets = false;
|
|
|
|
let sock;
|
|
let selfUserId, selfChannelName, selfPseudoChannelName;
|
|
let lastPing, lastPong, pingTimer, pingWatcher;
|
|
let openResolve, openReject;
|
|
|
|
const handlers = {};
|
|
|
|
const stopPingWatcher = () => {
|
|
if(pingWatcher !== undefined) {
|
|
clearTimeout(pingWatcher);
|
|
pingWatcher = undefined;
|
|
}
|
|
};
|
|
const startPingWatcher = () => {
|
|
if(pingWatcher === undefined)
|
|
pingWatcher = setTimeout(() => {
|
|
stopPingWatcher();
|
|
|
|
if(lastPong === undefined)
|
|
eventTarget.dispatch('ping:long');
|
|
}, 2000);
|
|
};
|
|
|
|
const send = (...args) => {
|
|
if(args.length < 1)
|
|
throw 'you must specify at least one argument as an opcode';
|
|
|
|
const pack = args.join("\t");
|
|
|
|
if(dumpPackets)
|
|
console.log(pack);
|
|
|
|
sock?.send(pack);
|
|
};
|
|
|
|
const onSendPing = () => {
|
|
if(selfUserId === undefined)
|
|
return;
|
|
|
|
eventTarget.dispatch('ping:send');
|
|
startPingWatcher();
|
|
|
|
lastPong = undefined;
|
|
lastPing = Date.now();
|
|
};
|
|
const sendAuth = (...args) => {
|
|
if(selfUserId === undefined)
|
|
send('1', ...args);
|
|
};
|
|
const sendMessage = text => {
|
|
if(selfUserId === undefined)
|
|
return;
|
|
|
|
if(text.substring(0, 1) !== '/' && selfPseudoChannelName !== undefined)
|
|
text = `/msg ${selfPseudoChannelName} ${text}`;
|
|
|
|
send('2', selfUserId, text);
|
|
};
|
|
|
|
const startKeepAlive = () => sock?.sendInterval(`0\t${selfUserId}`, pingDuration);
|
|
const stopKeepAlive = () => sock?.clearIntervals();
|
|
|
|
const onOpen = ev => {
|
|
if(dumpPackets)
|
|
console.log(ev);
|
|
|
|
isRestarting = false;
|
|
|
|
if(typeof openResolve === 'function') {
|
|
openResolve();
|
|
openResolve = undefined;
|
|
}
|
|
};
|
|
|
|
const onClose = ev => {
|
|
if(dumpPackets)
|
|
console.log(ev);
|
|
|
|
selfUserId = undefined;
|
|
selfChannelName = undefined;
|
|
selfPseudoChannelName = undefined;
|
|
stopPingWatcher();
|
|
stopKeepAlive();
|
|
|
|
if(wasKicked)
|
|
return;
|
|
|
|
let code = ev.detail.code;
|
|
if(isRestarting && code === 1006) {
|
|
code = 1012;
|
|
} else if(code === 1012)
|
|
isRestarting = true;
|
|
|
|
if(typeof openReject === 'function') {
|
|
openReject({
|
|
code: code,
|
|
wasConnected: wasConnected,
|
|
isRestarting: isRestarting,
|
|
});
|
|
openReject = undefined;
|
|
}
|
|
|
|
eventTarget.dispatch('conn:lost', {
|
|
wasConnected: wasConnected,
|
|
isRestarting: isRestarting,
|
|
code: code,
|
|
});
|
|
};
|
|
|
|
const unfuckText = (text, isAction) => {
|
|
// P7.1 doesn't wrap in <i>, likely a bug in SharpChat
|
|
// check if this is the case with the PHPChat impl
|
|
if(isAction && text.startsWith('<i>'))
|
|
text = text.slice(3, -4);
|
|
|
|
return text.replace(/ <br\/> /g, "\n")
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
};
|
|
|
|
const onMessage = ev => {
|
|
const args = ev.detail.data.split("\t");
|
|
let handler = handlers;
|
|
|
|
if(dumpPackets)
|
|
console.log(args);
|
|
|
|
for(;;) {
|
|
handler = handler[args.shift()];
|
|
if(handler === undefined)
|
|
break;
|
|
|
|
if(typeof handler === 'function') {
|
|
handler(...args);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
// pong handler
|
|
handlers['0'] = () => {
|
|
lastPong = Date.now();
|
|
eventTarget.dispatch('ping:recv', {
|
|
ping: lastPing,
|
|
pong: lastPong,
|
|
diff: lastPong - lastPing,
|
|
});
|
|
};
|
|
|
|
// join/auth
|
|
handlers['1'] = (successOrTimeStamp, userIdOrReason, userNameOrExpiry, userColour, userPerms, chanNameOrMsgId, maxLength) => {
|
|
if(successOrTimeStamp === 'y') {
|
|
selfUserId = userIdOrReason;
|
|
selfChannelName = chanNameOrMsgId;
|
|
|
|
eventTarget.dispatch('session:start', {
|
|
wasConnected: wasConnected,
|
|
session: { success: true },
|
|
ctx: {
|
|
maxMsgLength: parseInt(maxLength),
|
|
},
|
|
user: {
|
|
id: selfUserId,
|
|
self: true,
|
|
name: userNameOrExpiry,
|
|
colour: parseUserColour(userColour),
|
|
perms: parseUserPerms(userPerms),
|
|
permsRaw: userPerms,
|
|
},
|
|
channel: {
|
|
name: selfChannelName,
|
|
},
|
|
});
|
|
|
|
startKeepAlive();
|
|
wasConnected = true;
|
|
return;
|
|
}
|
|
|
|
if(successOrTimeStamp === 'n') {
|
|
wasKicked = true;
|
|
|
|
const failInfo = {
|
|
session: {
|
|
success: false,
|
|
reason: userIdOrReason,
|
|
needsAuth: userIdOrReason === 'authfail',
|
|
},
|
|
};
|
|
if(userNameOrExpiry !== undefined)
|
|
failInfo.baka = {
|
|
type: 'join',
|
|
perma: userNameOrExpiry === '-1',
|
|
until: userNameOrExpiry === '-1' ? undefined : new Date(parseInt(userNameOrExpiry) * 1000),
|
|
};
|
|
|
|
eventTarget.dispatch('session:fail', failInfo);
|
|
return;
|
|
}
|
|
|
|
eventTarget.dispatch('user:add', {
|
|
msg: {
|
|
id: chanNameOrMsgId,
|
|
time: new Date(parseInt(successOrTimeStamp) * 1000),
|
|
channel: selfChannelName,
|
|
botInfo: {
|
|
type: 'join',
|
|
args: [userNameOrExpiry],
|
|
},
|
|
},
|
|
user: {
|
|
id: userIdOrReason,
|
|
self: userIdOrReason === selfUserId,
|
|
name: userNameOrExpiry,
|
|
colour: parseUserColour(userColour),
|
|
perms: parseUserPerms(userPerms),
|
|
permsRaw: userPerms,
|
|
},
|
|
});
|
|
};
|
|
|
|
// message add
|
|
handlers['2'] = (timeStamp, userId, msgText, msgId, msgFlags) => {
|
|
const mFlags = parseMsgFlags(msgFlags);
|
|
let mText = unfuckText(msgText, mFlags.isAction);
|
|
let mChannelName = selfChannelName;
|
|
|
|
if(msgFlags[4] !== '0') {
|
|
if(userId === selfUserId) {
|
|
const mTextParts = mText.split(' ');
|
|
mChannelName = `@${mTextParts.shift()}`;
|
|
mText = mTextParts.join(' ');
|
|
} else {
|
|
mChannelName = `@~${userId}`;
|
|
}
|
|
}
|
|
|
|
const msgInfo = {
|
|
msg: {
|
|
id: msgId,
|
|
time: new Date(parseInt(timeStamp) * 1000),
|
|
channel: mChannelName,
|
|
sender: {
|
|
id: userId,
|
|
self: userId === selfUserId,
|
|
},
|
|
flags: mFlags,
|
|
flagsRaw: msgFlags,
|
|
isBot: userId === '-1',
|
|
text: mText,
|
|
},
|
|
};
|
|
|
|
if(msgInfo.msg.isBot) {
|
|
const botParts = msgText.split("\f");
|
|
msgInfo.msg.botInfo = {
|
|
isError: botParts[0] === '1',
|
|
type: botParts[1],
|
|
args: botParts.slice(2),
|
|
};
|
|
}
|
|
|
|
eventTarget.dispatch('msg:add', msgInfo);
|
|
};
|
|
|
|
// user leave
|
|
handlers['3'] = (userId, userName, reason, timeStamp, msgId) => {
|
|
eventTarget.dispatch('user:remove', {
|
|
leave: { type: reason },
|
|
msg: {
|
|
id: msgId,
|
|
time: new Date(parseInt(timeStamp) * 1000),
|
|
channel: selfChannelName,
|
|
botInfo: {
|
|
type: reason,
|
|
args: [userName],
|
|
},
|
|
},
|
|
user: {
|
|
id: userId,
|
|
self: userId === selfUserId,
|
|
name: userName,
|
|
},
|
|
});
|
|
};
|
|
|
|
// channel add/upd/del
|
|
handlers['4'] = {};
|
|
|
|
// channel add
|
|
handlers['4']['0'] = (name, hasPass, isTemp) => {
|
|
eventTarget.dispatch('chan:add', {
|
|
channel: {
|
|
name: name,
|
|
hasPassword: hasPass !== '0',
|
|
isTemporary: isTemp !== '0',
|
|
},
|
|
});
|
|
};
|
|
|
|
// channel update
|
|
handlers['4']['1'] = (prevName, name, hasPass, isTemp) => {
|
|
eventTarget.dispatch('chan:update', {
|
|
channel: {
|
|
previousName: prevName,
|
|
name: name,
|
|
hasPassword: hasPass !== '0',
|
|
isTemporary: isTemp !== '0',
|
|
},
|
|
});
|
|
};
|
|
|
|
// channel remove
|
|
handlers['4']['2'] = name => {
|
|
eventTarget.dispatch('chan:remove', {
|
|
channel: { name: name },
|
|
});
|
|
};
|
|
|
|
// user channel move
|
|
handlers['5'] = {};
|
|
|
|
// user join channel
|
|
handlers['5']['0'] = (userId, userName, userColour, userPerms, msgId) => {
|
|
eventTarget.dispatch('chan:join', {
|
|
user: {
|
|
id: userId,
|
|
self: userId === selfUserId,
|
|
name: userName,
|
|
colour: parseUserColour(userColour),
|
|
perms: parseUserPerms(userPerms),
|
|
permsRaw: userPerms,
|
|
},
|
|
msg: {
|
|
id: msgId,
|
|
channel: selfChannelName,
|
|
botInfo: {
|
|
type: 'jchan',
|
|
args: [userName],
|
|
},
|
|
},
|
|
});
|
|
};
|
|
|
|
// user leave channel
|
|
handlers['5']['1'] = (userId, msgId) => {
|
|
eventTarget.dispatch('chan:leave', {
|
|
user: {
|
|
id: userId,
|
|
self: userId === selfUserId,
|
|
},
|
|
msg: {
|
|
id: msgId,
|
|
channel: selfChannelName,
|
|
botInfo: {
|
|
type: 'lchan',
|
|
args: [userId],
|
|
},
|
|
},
|
|
});
|
|
};
|
|
|
|
// user forced switch channel
|
|
handlers['5']['2'] = name => {
|
|
selfChannelName = name;
|
|
|
|
eventTarget.dispatch('chan:focus', {
|
|
channel: { name: selfChannelName },
|
|
});
|
|
};
|
|
|
|
// message delete
|
|
handlers['6'] = msgId => {
|
|
eventTarget.dispatch('msg:remove', {
|
|
msg: {
|
|
id: msgId,
|
|
channel: selfChannelName,
|
|
},
|
|
});
|
|
};
|
|
|
|
// context populate
|
|
handlers['7'] = {};
|
|
|
|
// existing users
|
|
handlers['7']['0'] = (count, ...args) => {
|
|
count = parseInt(count);
|
|
eventTarget.dispatch('user:clear');
|
|
|
|
for(let i = 0; i < count; ++i) {
|
|
const offset = 5 * i;
|
|
|
|
eventTarget.dispatch('user:add', {
|
|
user: {
|
|
id: args[offset],
|
|
self: args[offset] === selfUserId,
|
|
name: args[offset + 1],
|
|
colour: parseUserColour(args[offset + 2]),
|
|
perms: parseUserPerms(args[offset + 3]),
|
|
permsRaw: args[offset + 3],
|
|
hidden: args[offset + 4] !== '0',
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
// existing message
|
|
handlers['7']['1'] = (timeStamp, userId, userName, userColour, userPerms, msgText, msgId, msgNotify, msgFlags) => {
|
|
const mFlags = parseMsgFlags(msgFlags);
|
|
const info = {
|
|
msg: {
|
|
id: msgId,
|
|
time: new Date(parseInt(timeStamp) * 1000),
|
|
channel: selfChannelName,
|
|
sender: {
|
|
id: userId,
|
|
self: userId === selfUserId,
|
|
name: userName,
|
|
colour: parseUserColour(userColour),
|
|
perms: parseUserColour(userPerms),
|
|
permsRaw: userPerms,
|
|
},
|
|
isBot: userId === '-1',
|
|
silent: msgNotify === '0',
|
|
flags: mFlags,
|
|
flagsRaw: msgFlags,
|
|
text: unfuckText(msgText, mFlags.isAction),
|
|
},
|
|
};
|
|
|
|
const msgIdFirst = info.msg.id.charCodeAt(0);
|
|
if(msgIdFirst < 48 || msgIdFirst > 57)
|
|
info.msg.id = (Math.round(Number.MIN_SAFE_INTEGER * Math.random())).toString();
|
|
|
|
if(info.msg.isBot) {
|
|
const botParts = msgText.split("\f");
|
|
info.msg.botInfo = {
|
|
isError: botParts[0] === '1',
|
|
type: botParts[1],
|
|
args: botParts.slice(2),
|
|
};
|
|
|
|
// i think this is more Inaccurate Behaviour on the server side
|
|
if(info.msg.botInfo.type === 'say')
|
|
info.msg.botInfo.args[0] = unfuckText(info.msg.botInfo.args[0]);
|
|
}
|
|
|
|
eventTarget.dispatch('msg:add', info);
|
|
};
|
|
|
|
// existing channels
|
|
handlers['7']['2'] = (count, ...args) => {
|
|
count = parseInt(count);
|
|
eventTarget.dispatch('chan:clear');
|
|
|
|
for(let i = 0; i < count; ++i) {
|
|
const offset = 3 * i;
|
|
|
|
eventTarget.dispatch('chan:add', {
|
|
channel: {
|
|
name: args[offset],
|
|
hasPassword: args[offset + 1] !== '0',
|
|
isTemporary: args[offset + 2] !== '0',
|
|
isCurrent: args[offset] === selfChannelName,
|
|
},
|
|
});
|
|
}
|
|
|
|
eventTarget.dispatch('chan:focus', {
|
|
channel: { name: selfChannelName },
|
|
});
|
|
};
|
|
|
|
// context clear
|
|
handlers['8'] = mode => {
|
|
if(mode === '0' || mode === '3' || mode === '4')
|
|
eventTarget.dispatch('msg:clear');
|
|
|
|
if(mode === '1' || mode === '3' || mode === '4')
|
|
eventTarget.dispatch('user:clear');
|
|
|
|
if(mode === '2' || mode === '4')
|
|
eventTarget.dispatch('chan:clear');
|
|
};
|
|
|
|
// baka (ban/kick)
|
|
handlers['9'] = (type, expiry) => {
|
|
wasKicked = true;
|
|
|
|
const bakaInfo = {
|
|
session: { success: false },
|
|
baka: {
|
|
type: type === '0' ? 'kick' : 'ban',
|
|
},
|
|
};
|
|
|
|
if(bakaInfo.baka.type === 'ban') {
|
|
bakaInfo.baka.perma = expiry === '-1';
|
|
bakaInfo.baka.until = expiry === '-1' ? undefined : new Date(parseInt(expiry) * 1000);
|
|
}
|
|
|
|
eventTarget.dispatch('session:term', bakaInfo);
|
|
};
|
|
|
|
// user update
|
|
handlers['10'] = (userId, userName, userColour, userPerms) => {
|
|
eventTarget.dispatch('user:update', {
|
|
user: {
|
|
id: userId,
|
|
self: userId === selfUserId,
|
|
name: userName,
|
|
colour: parseUserColour(userColour),
|
|
perms: parseUserPerms(userPerms),
|
|
permsRaw: userPerms,
|
|
},
|
|
});
|
|
};
|
|
|
|
|
|
return {
|
|
sendAuth: sendAuth,
|
|
sendMessage: sendMessage,
|
|
open: url => {
|
|
return new Promise((resolve, reject) => {
|
|
if(typeof url !== 'string')
|
|
throw 'url must be a string';
|
|
if(url.startsWith('//'))
|
|
url = location.protocol.replace('http', 'ws') + url;
|
|
|
|
openResolve = resolve;
|
|
openReject = reject;
|
|
|
|
sock?.close();
|
|
|
|
sock = new UmiWebSocket(url);
|
|
sock.watch('open', onOpen);
|
|
sock.watch('close', onClose);
|
|
sock.watch('message', onMessage);
|
|
sock.watch('create_interval', ev => {
|
|
pingTimer = ev.detail.id;
|
|
});
|
|
sock.watch('call_interval', ev => {
|
|
if(ev.detail.id === pingTimer)
|
|
onSendPing();
|
|
});
|
|
sock.watch('create_intervals', ev => {
|
|
pingTimer = undefined;
|
|
});
|
|
});
|
|
},
|
|
close: () => {
|
|
sock?.close();
|
|
sock = undefined;
|
|
},
|
|
watch: eventTarget.watch,
|
|
unwatch: eventTarget.unwatch,
|
|
setDumpPackets: state => dumpPackets = !!state,
|
|
switchChannel: channelInfo => {
|
|
if(selfUserId === undefined)
|
|
return;
|
|
|
|
const name = channelInfo.getName();
|
|
|
|
if(channelInfo.isUserChannel()) {
|
|
selfPseudoChannelName = name.substring(1);
|
|
} else {
|
|
selfPseudoChannelName = undefined;
|
|
if(selfChannelName === name)
|
|
return;
|
|
|
|
selfChannelName = name;
|
|
sendMessage(`/join ${name}`);
|
|
}
|
|
},
|
|
};
|
|
};
|