mami/src/mami.js/sockchat_old.js

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(/&lt;/g, '<')
.replace(/&gt;/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}`);
}
},
};
};