flash
cf71bab92d
This has been in the works for over a month and might break things because it's a very radical change. If it causes you to be unable to join chat, report it on the forum or try joining using the legacy chat on https://sockchat.flashii.net.
249 lines
7.6 KiB
JavaScript
249 lines
7.6 KiB
JavaScript
#include timedp.js
|
|
#include sockchat/authed.js
|
|
#include sockchat/ctx.js
|
|
#include sockchat/unauthed.js
|
|
|
|
const SockChatProtocol = function(dispatch, options) {
|
|
if(typeof dispatch !== 'function')
|
|
throw 'dispatch must be a function';
|
|
if(typeof options !== 'object' || options === null)
|
|
throw 'options must be an object';
|
|
if(typeof options.ping !== 'number')
|
|
throw 'options.ping must be a number';
|
|
|
|
let ctx, sock;
|
|
let dumpPackets = false;
|
|
|
|
const handlers = {
|
|
unauthed: {
|
|
'1': {
|
|
'y': SockChatS2CAuthSuccess,
|
|
'n': SockChatS2CAuthFail,
|
|
},
|
|
'7': {
|
|
// MOTD gets sent before auth success :D
|
|
'1': SockChatS2CMessagePopulate,
|
|
},
|
|
},
|
|
authed: {
|
|
'0': SockChatS2CPong,
|
|
'1': SockChatS2CUserAdd,
|
|
'2': SockChatS2CMessageAdd,
|
|
'3': SockChatS2CUserRemove,
|
|
'4': {
|
|
'0': SockChatS2CChannelAdd,
|
|
'1': SockChatS2CChannelUpdate,
|
|
'2': SockChatS2CChannelRemove,
|
|
},
|
|
'5': {
|
|
'0': SockChatS2CUserChannelJoin,
|
|
'1': SockChatS2CUserChannelLeave,
|
|
'2': SockChatS2CUserChannelFocus,
|
|
},
|
|
'6': SockChatS2CMessageRemove,
|
|
'7': {
|
|
'0': SockChatS2CUserPopulate,
|
|
'1': SockChatS2CMessagePopulate,
|
|
'2': SockChatS2CChannelPopulate,
|
|
},
|
|
'8': SockChatS2CContextClear,
|
|
'9': SockChatS2CBanKick,
|
|
'10': SockChatS2CUserUpdate,
|
|
},
|
|
};
|
|
|
|
const handleOpen = () => {
|
|
if(dumpPackets)
|
|
console.log('[sockchat:open]');
|
|
|
|
ctx.isRestarting = false;
|
|
ctx.keepAlive.start();
|
|
|
|
const detail = {
|
|
opitons: options,
|
|
wasConnected: ctx.wasConnected,
|
|
wasKicked: ctx.wasKicked,
|
|
};
|
|
|
|
dispatch('conn:open', detail);
|
|
|
|
// event (and maybe also resolve?) should be delayed until after capability negotiation
|
|
dispatch('conn:ready');
|
|
|
|
ctx.openPromise?.resolve(detail);
|
|
};
|
|
|
|
const handleClose = ev => {
|
|
if(dumpPackets)
|
|
console.log('[sockchat:close]', ev.code, ev.reason, ev.wasClean);
|
|
|
|
ctx.keepAlive.stop();
|
|
ctx.userId = undefined;
|
|
ctx.channelName = undefined;
|
|
ctx.pseudoChannelName = undefined;
|
|
|
|
if(ev.code === 1012)
|
|
ctx.isRestarting = true;
|
|
|
|
const detail = {
|
|
code: ev.code,
|
|
wasConnected: ctx.wasConnected,
|
|
isRestarting: ctx.isRestarting,
|
|
wasKicked: ctx.wasKicked,
|
|
};
|
|
|
|
dispatch('conn:lost', detail);
|
|
ctx.openPromise?.reject(detail);
|
|
};
|
|
|
|
const handleMessage = ev => {
|
|
const args = ev.data.split("\t");
|
|
if(dumpPackets)
|
|
console.log('[sockchat:incoming]', args);
|
|
|
|
let handler = handlers[ctx.isAuthed ? 'authed' : 'unauthed'];
|
|
|
|
for(;;) {
|
|
handler = handler[args.shift()];
|
|
if(handler === undefined)
|
|
break;
|
|
|
|
if(typeof handler === 'function') {
|
|
handler(ctx, ...args);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
const send = (...args) => {
|
|
if(args.length < 1)
|
|
throw 'you must specify at least one argument as an opcode';
|
|
|
|
if(ctx?.dumpPackets)
|
|
console.log('[sockchat:outgoing]', args);
|
|
|
|
sock?.send(args.join("\t"));
|
|
};
|
|
|
|
const sendPing = () => {
|
|
return new Promise((resolve, reject) => {
|
|
if(ctx === undefined)
|
|
throw 'no connection opened';
|
|
if(!ctx.isAuthed)
|
|
throw 'must be authenticated';
|
|
if(ctx.pingPromise !== undefined)
|
|
throw 'already sending a ping';
|
|
|
|
ctx.lastPing = Date.now();
|
|
ctx.pingPromise = new TimedPromise(resolve, reject, () => ctx.pingPromise = undefined, 2000);
|
|
|
|
send('0', ctx.userId);
|
|
});
|
|
};
|
|
|
|
const sendAuth = (...args) => {
|
|
return new Promise((resolve, reject) => {
|
|
if(ctx === undefined)
|
|
throw 'no connection opened';
|
|
if(ctx.isAuthed)
|
|
throw 'already authenticated';
|
|
if(ctx.authPromise !== undefined)
|
|
throw 'already authenticating';
|
|
|
|
// HttpClient in C# has its Moments, so lets give this way too long to do its thing
|
|
ctx.authPromise = new TimedPromise(resolve, reject, () => ctx.authPromise = undefined, 10000);
|
|
|
|
send('1', ...args);
|
|
});
|
|
};
|
|
|
|
const sendMessage = text => {
|
|
return new Promise((resolve, reject) => {
|
|
if(typeof text !== 'string')
|
|
throw 'text must be a string';
|
|
if(ctx === undefined)
|
|
throw 'no connection opened';
|
|
if(!ctx.isAuthed)
|
|
throw 'must be authenticated';
|
|
|
|
// there's actually a pretty big bug here lol
|
|
// any unsupported command is gonna fall through to the actual channel you're in
|
|
if(!text.startsWith('/') && ctx.pseudoChannelName !== undefined)
|
|
text = `/msg ${ctx.pseudoChannelName} ${text}`;
|
|
|
|
send('2', ctx.userId, text);
|
|
|
|
// server doesn't send a direct ACK and we can't tell what message
|
|
// which response messages is actually associated with this
|
|
// an ACK or request/response extension to the protocol will be required
|
|
if(typeof resolve === 'function')
|
|
resolve();
|
|
});
|
|
};
|
|
|
|
const createContext = () => {
|
|
ctx = new SockChatContext(dispatch, sendPing, options.ping);
|
|
};
|
|
|
|
const closeWebSocket = () => {
|
|
try {
|
|
const localSock = sock;
|
|
sock = undefined;
|
|
|
|
if(localSock !== undefined) {
|
|
localSock.removeEventListener('message', handleMessage);
|
|
localSock.removeEventListener('close', handleClose);
|
|
localSock.removeEventListener('open', handleOpen);
|
|
localSock.close();
|
|
}
|
|
} finally {
|
|
ctx?.dispose();
|
|
}
|
|
};
|
|
|
|
return {
|
|
setDumpPackets: state => {
|
|
dumpPackets = !!state;
|
|
},
|
|
open: url => {
|
|
return new Promise((resolve, reject) => {
|
|
if(typeof url !== 'string')
|
|
throw 'url must be a string';
|
|
if(ctx?.openPromise !== undefined)
|
|
throw 'already opening a connection';
|
|
|
|
closeWebSocket();
|
|
createContext();
|
|
|
|
ctx.openPromise = new TimedPromise(resolve, reject, () => ctx.openPromise = undefined, 5000);
|
|
|
|
sock = new WebSocket(url);
|
|
sock.addEventListener('open', handleOpen);
|
|
sock.addEventListener('close', handleClose);
|
|
sock.addEventListener('message', handleMessage);
|
|
});
|
|
},
|
|
close: () => { closeWebSocket(); },
|
|
sendPing: sendPing,
|
|
sendAuth: sendAuth,
|
|
sendMessage: sendMessage,
|
|
switchChannel: async info => {
|
|
if(!ctx.isAuthed)
|
|
return;
|
|
|
|
const name = info.name;
|
|
|
|
if(info.isUserChannel) {
|
|
selfPseudoChannelName = name.substring(1);
|
|
} else {
|
|
ctx.pseudoChannelName = undefined;
|
|
if(ctx.channelName === name)
|
|
return;
|
|
|
|
ctx.channelName = name;
|
|
await sendMessage(`/join ${name}`);
|
|
}
|
|
},
|
|
};
|
|
};
|