From 5b5ca888a4108f9dede42b4fa4f73d3aa1f31485 Mon Sep 17 00:00:00 2001 From: flashwave Date: Sat, 24 Feb 2024 01:10:30 +0000 Subject: [PATCH] Moved all/most UI code out of the Sock Chat protocol handler. --- src/mami.js/colour.js | 12 +- src/mami.js/main.js | 393 ++++++++++++++++- src/mami.js/message.js | 7 +- src/mami.js/messages.js | 1 - src/mami.js/server.js | 25 +- src/mami.js/sockchat_old.js | 815 ++++++++++++++++++------------------ src/mami.js/ui/hooks.js | 1 + src/mami.js/ui/settings.jsx | 5 + src/mami.js/watcher.js | 24 +- src/mami.js/websock.js | 11 +- src/websock.js/main.js | 11 +- 11 files changed, 855 insertions(+), 450 deletions(-) diff --git a/src/mami.js/colour.js b/src/mami.js/colour.js index fb28b96..40fe0b8 100644 --- a/src/mami.js/colour.js +++ b/src/mami.js/colour.js @@ -1,8 +1,8 @@ const MamiColour = (() => { - const readThres = 168, - lumiRed = .299, - lumiGreen = .587, - lumiBlue = .114; + const readThres = 168; + const lumiRed = .299; + const lumiGreen = .587; + const lumiBlue = .114; const pub = {}; @@ -25,8 +25,8 @@ const MamiColour = (() => { }; const weighted = (raw1, raw2, weight) => { - const rgb1 = extractRGB(raw1), - rgb2 = extractRGB(raw2); + const rgb1 = extractRGB(raw1); + const rgb2 = extractRGB(raw2); return (weightedNumber(rgb1[0], rgb2[0], weight) << 16) | (weightedNumber(rgb1[1], rgb2[1], weight) << 8) | weightedNumber(rgb1[2], rgb2[2], weight); diff --git a/src/mami.js/main.js b/src/mami.js/main.js index f5d7c27..587a2d7 100644 --- a/src/mami.js/main.js +++ b/src/mami.js/main.js @@ -1,13 +1,19 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } }; #include animate.js +#include channel.js +#include channels.js #include common.js #include context.js #include emotes.js +#include message.js #include messages.js #include mszauth.js +#include parsing.js #include server.js #include txtrigs.js +#include user.js +#include users.js #include utility.js #include weeb.js #include audio/autoplay.js @@ -21,10 +27,12 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } }; #include sound/umisound.js #include ui/chat-layout.js #include ui/hooks.js +#include ui/emotes.js #include ui/input-menus.js #include ui/loading-overlay.jsx #include ui/markup.js #include ui/menus.js +#include ui/messages.jsx #include ui/view.js #include ui/settings.jsx #include ui/toggles.js @@ -109,6 +117,7 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } }; settings.define('osuKeysV2', ['no', 'yes', 'rng'], 'no'); settings.define('explosionRadius', 'number', 20); settings.define('dumpPackets', 'boolean', FUTAMI_DEBUG); + settings.define('dumpEvents', 'boolean', FUTAMI_DEBUG); settings.define('neverUseWorker', 'boolean', false, false, true); settings.define('forceUseWorker', 'boolean', false, false, true); settings.define('marqueeAllNames', 'boolean', false); @@ -244,7 +253,7 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } }; return 'minecraft:door:open'; if(v === 'old') return 'minecraft:door:open-old'; - return mami.sound.pack.getEventSound('join'); + return soundCtx.pack.getEventSound('join'); })()); }); @@ -445,7 +454,7 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } }; insertText = `[video]${fileInfo.url}[/video]`; uploadEntry.setThumbnail(fileInfo.thumb); } else - insertText = location.protocol + fileInfo.url; + insertText = location.Umi.Servercol + fileInfo.url; if(settings.get('eepromAutoInsert')) Umi.UI.Markup.InsertRaw(insertText, ''); @@ -521,8 +530,386 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } }; }); + // really not sure about all the watchers for the protocol just kinda being Listed here but we'll see i guess loadingOverlay.setMessage('Connecting...'); - Umi.Server.open(views, settings); + + const getLoadingOverlay = async (icon, header, message) => { + const currentView = views.current(); + + if('setIcon' in currentView) { + currentView.setIcon(icon); + currentView.setHeader(header); + currentView.setMessage(message); + return currentView; + } + + const loading = new Umi.UI.LoadingOverlay(icon, header, message); + await views.push(loading); + + return loading; + }; + + const wsCloseReasons = { + '_1000': 'The connection has been ended.', + '_1001': 'Something went wrong on the server side.', + '_1002': 'Your client sent broken data to the server.', + '_1003': 'Your client sent data to the server that it does not understand.', + '_1005': 'No additional information was provided.', + '_1006': 'You lost connection unexpectedly!', + '_1007': 'Your client sent broken data to the server.', + '_1008': 'Your client did something the server did not agree with.', + '_1009': 'Your client sent too much data to the server at once.', + '_1011': 'Something went wrong on the server side.', + '_1012': 'The server is restarting, reconnecting soon...', + '_1013': 'You cannot connect to the server right now, try again later.', + '_1015': 'Your client and the server could not establish a secure connection.', + }; + + let dumpEvents = false; + settings.watch('dumpEvents', value => dumpEvents = value); + settings.watch('dumpPackets', value => Umi.Server.setDumpPackets(value)); + + Umi.Server.watch('conn:init', init => { + if(dumpEvents) console.log('conn:init', init); + + let message = 'Connecting to server...'; + if(init.attempt > 2) + message += ` (Attempt ${connectAttempts})`; + + getLoadingOverlay('spinner', 'Loading...', message); + }); + Umi.Server.watch('conn:ready', ready => { + if(dumpEvents) console.log('conn:ready', ready); + + getLoadingOverlay('spinner', 'Loading...', 'Authenticating...'); + }); + Umi.Server.watch('conn:lost', lost => { + if(dumpEvents) console.log('conn:lost', lost); + + getLoadingOverlay( + 'unlink', 'Disconnected!', + wsCloseReasons[`_${lost.code}`] ?? `Something caused an unexpected connection loss. (${lost.code})` + ); + }); + Umi.Server.watch('conn:error', error => { + console.error('conn:error', error); + }); + + Umi.Server.watch('ping:send', send => { + if(dumpEvents) console.log('ping:send', send); + }); + Umi.Server.watch('ping:long', long => { + if(dumpEvents) console.log('ping:long', long); + }); + Umi.Server.watch('ping:recv', recv => { + if(dumpEvents) console.log('ping:recv', recv); + }); + + + const playBannedSfx = async () => { + await soundCtx.library.play('touhou:pichuun'); + }; + const playBannedBgm = async preload => { + const name = 'touhou:th10score'; + + if(preload) { + await soundCtx.library.loadBuffer(name); + return; + } + + const source = await soundCtx.library.loadSource(name); + source.setLoop(true, 10.512, 38.074); + await source.play(); + }; + const displayBanMessage = baka => { + let message; + if(baka.perma) + message = 'You have been banned till the end of time, please try again in a different dimension.'; + else if(baka.until) + message = `You were banned until ${baka.until.toLocaleString()}!`; + else + message = 'You were kicked, refresh to log back in!'; + + const icon = baka.type === 'kick' ? 'bomb' : 'hammer'; + const header = baka.type === 'kick' ? 'Kicked!' : 'Banned!'; + + const currentView = views.current(); + + if(currentView === undefined || 'setIcon' in currentView) { + playBannedBgm(); + getLoadingOverlay(icon, header, message); + } else { + const currentViewElem = views.currentElement(); + + MamiAnimate({ + duration: 550, + easing: 'outExpo', + start: () => { + playBannedBgm(true); + playBannedSfx(); + }, + update: t => { + currentViewElem.style.transform = `scale(${(1 - .5 * t)}, ${(1 - 1 * t)})`; + }, + end: () => { + getLoadingOverlay(icon, header, message).then(() => { + playBannedBgm(); + + // there's currently no way to reconnect after a kick/ban so just dispose of the ui entirely + if(views.count() > 1) + views.shift(); + }); + }, + }); + } + }; + + Umi.Server.watch('session:start', start => { + if(dumpEvents) console.log('session:start', start); + + const userInfo = new Umi.User(start.user.id, start.user.name, start.user.colour, start.user.permsRaw); + Umi.User.setCurrentUser(userInfo); + Umi.Users.Add(userInfo); + + Umi.UI.Markup.Reset(); + Umi.UI.Emoticons.Init(); + Umi.Parsing.Init(); + + views.pop(ctx => MamiAnimate({ + async: true, + duration: 120, + easing: 'inOutSine', + start: () => { + ctx.toElem.style.zIndex = '100'; + ctx.fromElem.style.pointerEvents = 'none'; + ctx.fromElem.style.zIndex = '200'; + }, + update: t => { + ctx.fromElem.style.transform = `scale(${1 + (.25 * t)})`; + ctx.fromElem.style.opacity = 1 - (1 * t).toString(); + }, + end: () => { + ctx.toElem.style.zIndex = null; + }, + })); + }); + Umi.Server.watch('session:fail', fail => { + if(dumpEvents) console.log('session:fail', fail); + + if(fail.baka !== undefined) { + displayBanMessage(fail.baka); + return; + } + + const message = (reason => { + if(reason === 'authfail') + return 'Authentication failed.'; + if(reason === 'sockfail') + return 'Too many active connections.'; + if(reason === 'userfail') + return 'Name in use.'; + if(reason === 'joinfail') + return 'You are banned.'; + return `Unknown reason: ${reason}`; + })(fail.session.reason); + + getLoadingOverlay('cross', 'Failed!', message); + + if(fail.session.needsAuth) + setTimeout(() => location.assign(futami.get('login')), 1000); + }); + Umi.Server.watch('session:term', term => { + if(dumpEvents) console.log('session:term', term); + + displayBanMessage(term.baka); + }); + + Umi.Server.watch('user:add', add => { + if(dumpEvents) console.log('user:add', add); + + if(add.user.self) + return; + + const userInfo = new Umi.User(add.user.id, add.user.name, add.user.colour, add.user.permsRaw); + Umi.Users.Add(userInfo); + + if(add.msg !== undefined) + Umi.Messages.Add(new Umi.Message( + add.msg.id, add.msg.time, undefined, '', add.msg.channel, false, + { + isError: false, + type: add.msg.botInfo.type, + args: add.msg.botInfo.args, + target: userInfo, + } + )); + }); + Umi.Server.watch('user:remove', remove => { + if(dumpEvents) console.log('user:remove', remove); + + const userInfo = Umi.Users.Get(remove.user.id); + if(userInfo === null) + return; + + if(remove.msg !== undefined) + Umi.Messages.Add(new Umi.Message( + remove.msg.id, + remove.msg.time, + undefined, + '', + remove.msg.channel, + false, + { + isError: false, + type: remove.msg.botInfo.type, + args: remove.msg.botInfo.args, + target: userInfo, + }, + )); + + Umi.Users.Remove(userInfo); + }); + Umi.Server.watch('user:update', update => { + if(dumpEvents) console.log('user:update', update); + + const userInfo = Umi.Users.Get(update.user.id); + userInfo.setName(update.user.name); + userInfo.setColour(update.user.colour); + userInfo.setPermissions(update.user.permsRaw); + Umi.Users.Update(userInfo.getId(), userInfo); + }); + Umi.Server.watch('user:clear', () => { + if(dumpEvents) console.log('user:clear'); + + const self = Umi.User.currentUser; + Umi.Users.Clear(); + if(self !== undefined) + Umi.Users.Add(self); + }); + + Umi.Server.watch('chan:add', add => { + if(dumpEvents) console.log('chan:add', add); + + Umi.Channels.Add(new Umi.Channel( + add.channel.name, + add.channel.hasPassword, + add.channel.isTemporary, + )); + }); + Umi.Server.watch('chan:remove', remove => { + if(dumpEvents) console.log('chan:remove', remove); + + Umi.Channels.Remove(Umi.Channels.Get(remove.channel.name)); + }); + Umi.Server.watch('chan:update', update => { + if(dumpEvents) console.log('chan:update', update); + + const chanInfo = Umi.Channels.Get(update.channel.previousName); + chanInfo.setName(update.channel.name); + chanInfo.setHasPassword(update.channel.hasPassword); + chanInfo.setTemporary(update.channel.isTemporary); + Umi.Channels.Update(update.channel.previousName, chanInfo); + }); + Umi.Server.watch('chan:clear', () => { + if(dumpEvents) console.log('chan:clear'); + + Umi.Channels.Clear(); + }); + Umi.Server.watch('chan:focus', focus => { + if(dumpEvents) console.log('chan:focus', focus); + + Umi.Channels.Switch(Umi.Channels.Get(focus.channel.name)); + }); + Umi.Server.watch('chan:join', join => { + if(dumpEvents) console.log('chan:join', join); + + const userInfo = new Umi.User(join.user.id, join.user.name, join.user.colour, join.user.permsRaw); + Umi.Users.Add(userInfo); + + if(join.msg !== undefined) + Umi.Messages.Add(new Umi.Message( + join.msg.id, null, undefined, '', join.msg.channel, false, + { + isError: false, + type: leave.msg.botInfo.type, + args: [ userInfo.getName() ], + target: userInfo, + }, + )); + }); + Umi.Server.watch('chan:leave', leave => { + if(dumpEvents) console.log('chan:leave', leave); + + if(leave.user.self) + return; + + const userInfo = Umi.Users.Get(leave.user.id); + if(userInfo === null) + return; + + if(leave.msg !== undefined) + Umi.Messages.Add(new Umi.Message( + leave.msg.id, null, undefined, '', leave.msg.channel, false, + { + isError: false, + type: leave.msg.botInfo.type, + args: [ userInfo.getName() ], + target: userInfo, + }, + )); + + Umi.Users.Remove(userInfo); + }); + + Umi.Server.watch('msg:add', add => { + if(dumpEvents) console.log('msg:add', add); + + const senderInfo = add.msg.sender; + const userInfo = senderInfo.name === undefined + ? Umi.Users.Get(senderInfo.id) + : new Umi.User(senderInfo.id, senderInfo.name, senderInfo.colour, senderInfo.permsRaw); + + // hack + let channelName = add.msg.channel; + if(channelName !== undefined && channelName.startsWith('@~')) { + const chanUserInfo = Umi.Users.Get(channelName.substring(2)); + if(chanUserInfo !== null) + channelName = `@${chanUserInfo.getName()}`; + } + + // also hack + if(add.msg.flags.isPM) { + if(Umi.Channels.Get(channelName) === null) + Umi.Channels.Add(new Umi.Channel(channelName, false, true, true)); + + // this should be raised for other channels too, but that is not possible yet + Umi.UI.Menus.Attention('channels'); + } + + Umi.Messages.Add(new Umi.Message( + add.msg.id, + add.msg.time, + userInfo, + add.msg.text, + channelName, + false, + add.msg.botInfo, + add.msg.flags.isAction, + add.msg.silent, + )); + }); + Umi.Server.watch('msg:remove', remove => { + if(dumpEvents) console.log('msg:remove', remove); + + Umi.Messages.Remove(Umi.Messages.Get(remove.msg.id)); + }); + Umi.Server.watch('msg:clear', () => { + if(dumpEvents) console.log('msg:clear'); + + Umi.UI.Messages.RemoveAll(); + }); + + Umi.Server.open(); if(window.dispatchEvent) window.dispatchEvent(new Event('umi:connect')); diff --git a/src/mami.js/message.js b/src/mami.js/message.js index 63fd743..0288956 100644 --- a/src/mami.js/message.js +++ b/src/mami.js/message.js @@ -5,7 +5,7 @@ Umi.Message = (() => { return function(msgId, time, user, text, channel, highlight, botInfo, isAction, isLog) { msgId = (msgId || '').toString(); - time = time === null ? new Date() : new Date(parseInt(time || 0) * 1000); + time = time === null ? new Date() : (typeof time === 'object' ? time : new Date(parseInt(time || 0) * 1000)); user = user !== null && typeof user === 'object' ? user : chatBot; text = (text || '').toString(); channel = (channel || '').toString(); @@ -18,7 +18,10 @@ Umi.Message = (() => { return { getId: () => msgId, - getIdInt: () => msgIdInt, + getIdInt: () => { + const num = parseInt(msgId); + return isNaN(num) ? (Math.round(Number.MIN_SAFE_INTEGER * Math.random())) : num; + }, getTime: () => time, getUser: () => user, getText: () => text, diff --git a/src/mami.js/messages.js b/src/mami.js/messages.js index 6fb5fbb..5c1a05a 100644 --- a/src/mami.js/messages.js +++ b/src/mami.js/messages.js @@ -1,5 +1,4 @@ #include channels.js -#include server.js #include ui/messages.jsx Umi.Messages = (function() { diff --git a/src/mami.js/server.js b/src/mami.js/server.js index 787cabb..da2a7fa 100644 --- a/src/mami.js/server.js +++ b/src/mami.js/server.js @@ -1,26 +1,3 @@ #include sockchat_old.js -Umi.Server = (function() { - let proto = null; - - return { - open: function(...args) { - proto = new Umi.Protocol.SockChat.Protocol(...args); - proto.open(); - }, - close: function() { - if(!proto) - return; - proto.close(); - proto = null; - }, - sendMessage: function(text) { - if(!proto) - return; - text = (text || '').toString(); - if(!text) - return; - proto.sendMessage(text); - }, - }; -})(); +Umi.Server = new Umi.Protocol.SockChat.Protocol; diff --git a/src/mami.js/sockchat_old.js b/src/mami.js/sockchat_old.js index 8c25618..5648686 100644 --- a/src/mami.js/sockchat_old.js +++ b/src/mami.js/sockchat_old.js @@ -1,149 +1,136 @@ -#include channel.js -#include channels.js #include common.js -#include message.js -#include messages.js #include mszauth.js -#include user.js -#include users.js -#include parsing.js #include servers.js +#include watcher.js #include websock.js -#include ui/emotes.js -#include ui/markup.js -#include ui/menus.js -#include ui/messages.jsx -#include ui/view.js -#include ui/loading-overlay.jsx -Umi.Protocol.SockChat.Protocol = function(views, settings) { - const pub = {}; +Umi.Protocol.SockChat.Protocol = function() { + const watchers = new MamiWatchers(false); + 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', + ]); - let noReconnect = false, - connectAttempts = 0, - wasKicked = false, - isRestarting = false; + const parseUserColour = str => { + // todo + return str; + }; - let userId = null, - channelName = null, - pmUserName = null; + let parseUserPermsSep; + const parseUserPerms = str => { + parseUserPermsSep ??= str.includes("\f") ? "\f" : ' '; + return str.split(parseUserPermsSep); + }; - let sock = null; + 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 noReconnect = false; + let connectAttempts = 0; + let wasKicked = false; + let isRestarting = false; + let dumpPackets = false; + + let sock; + let selfUserId, selfChannelName, selfPseudoChannelName; + let lastPing, lastPong, pingTimer, pingWatcher; + + const stopPingWatcher = () => { + if(pingWatcher !== undefined) { + clearTimeout(pingWatcher); + pingWatcher = undefined; + } + }; + const startPingWatcher = () => { + if(pingWatcher === undefined) + pingWatcher = setTimeout(() => { + stopPingWatcher(); + + if(lastPong === undefined) + watchers.call('ping:long'); + }, 2000); + }; + + const send = (opcode, data) => { + if(sock === undefined) + return; - const send = function(opcode, data) { - if(!sock) return; let msg = opcode; - if(data) msg += "\t" + data.join("\t"); - if(settings.get('dumpPackets')) + if(data) + msg += `\t${data.join("\t")}`; + + if(dumpPackets) console.log(msg); + sock.send(msg); }; - const sendPing = function() { - if(userId === null) + + const onSendPing = () => { + if(selfUserId === undefined) return; + + watchers.call('ping:send'); + startPingWatcher(); + + lastPong = undefined; lastPing = Date.now(); - send('0', [userId]); }; - const sendAuth = function(args) { - if(userId !== null) - return; - send('1', args); + const sendAuth = args => { + if(selfUserId === undefined) + send('1', args); }; - const sendMessage = function(text) { - if(userId === null) + const sendMessage = text => { + if(selfUserId === undefined) return; - if(text.substring(0, 1) !== '/' && pmUserName !== null) - text = '/msg ' + pmUserName + ' ' + text; + if(text.substring(0, 1) !== '/' && selfPseudoChannelName !== undefined) + text = `/msg ${selfPseudoChannelName} ${text}`; - send('2', [userId, text]); + send('2', [selfUserId, text]); }; - const switchChannel = function(name) { - if(channelName === name) - return; + const startKeepAlive = () => sock?.sendInterval(`0\t${selfUserId}`, futami.get('ping') * 1000); + const stopKeepAlive = () => sock?.clearIntervals(); - channelName = name; - sendMessage('/join ' + name); - }; - - const startKeepAlive = function() { - if(!sock) return; - sock.sendInterval("0\t" + userId, futami.get('ping') * 1000); - }; - const stopKeepAlive = function() { - if(!sock) return; - sock.clearIntervals(); - }; - - const getLoadingOverlay = async (icon, header, message) => { - const currentView = views.current(); - - if('setIcon' in currentView) { - currentView.setIcon(icon); - currentView.setHeader(header); - currentView.setMessage(message); - return currentView; - } - - const loading = new Umi.UI.LoadingOverlay(icon, header, message); - await views.push(loading); - - return loading; - }; - - const playBannedSfx = async () => { - await mami.sound.library.play('touhou:pichuun'); - }; - const playBannedBgm = async preload => { - const name = 'touhou:th10score'; - - if(preload) { - await mami.sound.library.loadBuffer(name); - return; - } - - const source = await mami.sound.library.loadSource(name); - source.setLoop(true, 10.512, 38.074); - await source.play(); - }; - - const onOpen = function(ev) { - if(settings.get('dumpPackets')) + const onOpen = ev => { + if(dumpPackets) console.log(ev); - wasKicked = false; isRestarting = false; - getLoadingOverlay('spinner', 'Loading...', 'Authenticating...'); + watchers.call('conn:ready', { + wasConnected: wasConnected, + }); + + // see if these are neccesary + watchers.call('user:clear'); + watchers.call('chan:clear'); const authInfo = MamiMisuzuAuth.getInfo(); sendAuth([authInfo.method, authInfo.token]); }; - const closeReasons = { - '_1000': 'The connection has been ended.', - '_1001': 'Something went wrong on the server side.', - '_1002': 'Your client sent broken data to the server.', - '_1003': 'Your client sent data to the server that it doesn\'t understand.', - '_1005': 'No additional information was provided.', - '_1006': 'You lost connection unexpectedly!', - '_1007': 'Your client sent broken data to the server.', - '_1008': 'Your client did something the server did not agree with.', - '_1009': 'Your client sent too much data to the server at once.', - '_1011': 'Something went wrong on the server side.', - '_1012': 'The server is restarting, reconnecting soon...', - '_1013': 'You cannot connect to the server right now, try again later.', - '_1015': 'Your client and the server could not establish a secure connection.', - }; - - const onClose = function(ev) { - if(settings.get('dumpPackets')) + const onClose = ev => { + if(dumpPackets) console.log(ev); - userId = null; - channelName = null; - pmUserName = null; + selfUserId = undefined; + selfChannelName = undefined; + selfPseudoChannelName = undefined; + stopPingWatcher(); stopKeepAlive(); if(wasKicked) @@ -155,189 +142,199 @@ Umi.Protocol.SockChat.Protocol = function(views, settings) { } else if(code === 1012) isRestarting = true; - const msg = closeReasons['_' + code.toString()] - || ('Something caused an unexpected connection loss, the error code was: ' + ev.code.toString() + '.'); - - getLoadingOverlay('unlink', 'Disconnected!', msg); - - Umi.Users.Clear(); + watchers.call('conn:lost', { + wasConnected: wasConnected, + isRestarting: isRestarting, + code: code, + }); connectAttempts = 0; - setTimeout(function() { - beginConnecting(); - }, 5000); + setTimeout(() => beginConnecting(), 5000); }; - const unfuckText = function(text) { + const onError = ex => { + watchers.call('conn:error', ex); + }; + + const unfuckText = text => { const elem = document.createElement('div'); elem.innerHTML = text.replace(/ /g, "\n"); text = elem.innerText; return text; }; - const onMessage = function(ev) { + const onMessage = ev => { const data = ev.data.split("\t"); - if(settings.get('dumpPackets')) + if(dumpPackets) console.log(data); switch(data[0]) { case '0': // ping - // nothing to do + lastPong = Date.now(); + watchers.call('ping:recv', { + ping: lastPing, + pong: lastPong, + diff: lastPong - lastPing, + }); break; case '1': // join - if(userId === null) { + if(data[1] !== 'y' && data[1] !== 'n') { + watchers.call('user:add', { + msg: { + id: data[6], + time: new Date(parseInt(data[1]) * 1000), + channel: selfChannelName, + botInfo: { + type: 'join', + args: [data[3]], + }, + }, + user: { + id: data[2], + self: data[2] === selfUserId, + name: data[3], + colour: parseUserColour(data[4]), + perms: parseUserPerms(data[5]), + permsRaw: data[5], + }, + }); + } else { if(data[1] == 'y') { - userId = data[2]; - channelName = data[6]; + selfUserId = data[2]; + selfChannelName = data[6]; - views.pop(ctx => MamiAnimate({ - async: true, - duration: 120, - easing: 'inOutSine', - start: () => { - ctx.toElem.style.zIndex = '100'; - ctx.fromElem.style.pointerEvents = 'none'; - ctx.fromElem.style.zIndex = '200'; + watchers.call('session:start', { + wasConnected: wasConnected, + session: { success: true }, + ctx: { + maxMsgLength: parseInt(data[7]), }, - update: t => { - ctx.fromElem.style.transform = `scale(${1 + (.25 * t)})`; - ctx.fromElem.style.opacity = 1 - (1 * t).toString(); + user: { + id: selfUserId, + self: true, + name: data[3], + colour: parseUserColour(data[4]), + perms: parseUserPerms(data[5]), + permsRaw: data[5], }, - end: () => { - ctx.toElem.style.zIndex = null; + channel: { + name: selfChannelName, }, - })); + }); startKeepAlive(); + wasConnected = true; } else { - switch (data[2]) { - case 'joinfail': - wasKicked = true; + wasKicked = true; - const jfuntil = new Date(parseInt(data[3]) * 1000); - let banmsg = 'You were banned until {0}!'.replace('{0}', jfuntil.toLocaleString()); + const failInfo = { + session: { + success: false, + reason: data[2], + needsAuth: data[2] === 'authfail', + }, + }; + if(data[3] !== undefined) + failInfo.baka = { + type: 'join', + perma: data[3] === '-1', + until: data[3] === '-1' ? undefined : new Date(parseInt(data[3]) * 1000), + }; - if(data[3] === '-1') - banmsg = 'You have been banned till the end of time, please try again in a different dimension.'; - - getLoadingOverlay('hammer', 'Banned!', banmsg); - playBannedBgm(); - break; - - case 'authfail': - let message = 'Authentication failed!'; - const afredir = futami.get('login'); - - if(afredir) { - message = 'Authentication failed, redirecting to login page...'; - setTimeout(function() { - location.assign(afredir); - }, 2000); - } - - getLoadingOverlay('cross', 'Failed!', message); - break; - - case 'sockfail': - getLoadingOverlay('cross', 'Failed!', 'Too many active connections.'); - break; - - default: - getLoadingOverlay('cross', 'Failed!', 'Connection failed!'); - break; - } - break; + watchers.call('session:fail', failInfo); } } - - const juser = new Umi.User(data[2], data[3], data[4], data[5]); - - if(userId === juser.getId()) - Umi.User.setCurrentUser(juser); - - Umi.Users.Add(juser); - - if(Umi.User.isCurrentUser(juser)) { - Umi.UI.Markup.Reset(); - Umi.UI.Emoticons.Init(); - Umi.Parsing.Init(); - break; - } - - Umi.Messages.Add(new Umi.Message( - data[6], data[1], undefined, '', channelName, false, - { type: 'join', isError: false, args: [juser.getName()], target: juser } - )); break; case '2': // message - let text = data[3]; - const muser = Umi.Users.Get(data[2]); - const textParts = text.split("\f"); - const isPM = data[5][4] !== '0'; - const isAction = data[5][1] !== '0' && data[5][3] === '0'; - const isBot = data[2] === '-1'; - const botInfo = {}; - let pmChannel = ''; + let mText = unfuckText(data[3]); + let mChannelName = selfChannelName; - text = unfuckText(text); - - if(isBot) { - botInfo.isError = textParts[0] !== '0'; - botInfo.type = textParts[1]; - botInfo.args = textParts.slice(2); + if(data[5][4] !== '0') { + if(data[2] === selfUserId) { + const mTextParts = mText.split(' '); + mChannelName = `@${mTextParts.shift()}`; + mText = mTextParts.join(' '); + } else { + mChannelName = `@~${data[2]}`; + } } - if(isPM) { - if(muser.getId() === userId) { - const tmpMsg = text.split(' '); - pmChannel = `@${tmpMsg.shift()}`; - text = tmpMsg.join(' '); - } else - pmChannel = `@${muser.getName()}`; + const msgInfo = { + msg: { + id: data[4], + time: new Date(parseInt(data[1]) * 1000), + channel: mChannelName, + sender: { id: data[2], }, + flags: parseMsgFlags(data[5]), + flagsRaw: data[5], + isBot: data[2] === '-1', + text: mText, + }, + }; - if(Umi.Channels.Get(pmChannel) === null) - Umi.Channels.Add(new Umi.Channel(pmChannel, false, true, true)); - - Umi.UI.Menus.Attention('channels'); + if(msgInfo.msg.isBot) { + const botParts = data[3].split("\f"); + msgInfo.msg.botInfo = { + isError: botParts[0] === '1', + type: botParts[1], + args: botParts.slice(2), + }; } - Umi.Messages.Add(new Umi.Message( - data[4], data[1], muser, text, - isPM ? pmChannel : channelName, - false, botInfo, isAction - )); + watchers.call('msg:add', msgInfo); break; case '3': // leave - const luser = Umi.Users.Get(data[1]); - - Umi.Messages.Add(new Umi.Message( - data[5], data[4], undefined, '', channelName, false, - { type: data[3], isError: false, args: [luser.getName()], target: luser } - )); - Umi.Users.Remove(luser); + watchers.call('user:remove', { + leave: { type: data[3] }, + msg: { + id: data[5], + time: new Date(parseInt(data[4]) * 1000), + channel: selfChannelName, + botInfo: { + type: data[3], + args: [data[2]], + }, + }, + user: { + id: data[1], + self: data[1] === selfUserId, + name: data[2], + }, + }); break; case '4': // channel switch(data[1]) { case '0': - Umi.Channels.Add(new Umi.Channel(data[2], data[3] !== '0', data[4] !== '0')); + watchers.call('chan:add', { + channel: { + name: data[2], + hasPassword: data[3] !== '0', + isTemporary: data[4] !== '0', + }, + }); break; case '1': - const uchannel = Umi.Channels.Get(data[2]); - uchannel.setName(data[3]); - uchannel.setHasPassword(data[4] !== '0'); - uchannel.setTemporary(data[5] !== '0'); - Umi.Channels.Update(data[2], uchannel); + watchers.call('chan:update', { + channel: { + previousName: data[2], + name: data[3], + hasPassword: data[4] !== '0', + isTemporary: data[5] !== '0', + }, + }); break; case '2': - Umi.Channels.Remove(Umi.Channels.Get(data[2])); + watchers.call('chan:remove', { + channel: { name: data[2] }, + }); break; } break; @@ -345,205 +342,202 @@ Umi.Protocol.SockChat.Protocol = function(views, settings) { case '5': // user move switch(data[1]) { case '0': - const umuser = new Umi.User(data[2], data[3], data[4], data[5]); - - Umi.Users.Add(umuser); - Umi.Messages.Add(new Umi.Message( - data[6], null, undefined, '', channelName, false, - { type: 'jchan', isError: false, args: [ umuser.getName() ], target: umuser } - )); + watchers.call('chan:join', { + user: { + id: data[2], + self: data[2] === selfUserId, + name: data[3], + colour: parseUserColour(data[4]), + perms: parseUserPerms(data[5]), + permsRaw: data[5], + }, + msg: { + id: data[6], + channel: selfChannelName, + botInfo: { + type: 'jchan', + args: [data[3]], + }, + }, + }); break; case '1': - if(data[2] === userId) - return; - - const mouser = Umi.Users.Get(+data[2]); - - Umi.Messages.Add(new Umi.Message( - data[3], null, undefined, '', channelName, false, - { type: 'lchan', isError: false, args: [ mouser.getName() ], target: mouser } - )); - Umi.Users.Remove(mouser); + watchers.call('chan:leave', { + user: { + id: data[2], + self: data[2] === selfUserId, + }, + msg: { + id: data[3], + channel: selfChannelName, + botInfo: { + type: 'lchan', + args: [data[2]], + }, + }, + }); break; case '2': - Umi.Channels.Switch(Umi.Channels.Get(data[2])); + selfChannelName = data[2]; + + watchers.call('chan:focus', { + channel: { name: selfChannelName }, + }); break; } break; case '6': // message delete - Umi.Messages.Remove(Umi.Messages.Get(data[1])); + watchers.call('msg:remove', { + msg: { + id: data[1], + channel: selfChannelName, + }, + }); break; case '7': // context populate switch(data[1]) { case '0': // users const cpuamount = parseInt(data[2]); - let cpustart = 3; - for(let i = 0; i < cpuamount; i++) { - const user = new Umi.User(data[cpustart], data[cpustart + 1], data[cpustart + 2], data[cpustart + 3]); - Umi.Users.Add(user); - cpustart += 5; + for(let i = 0; i < cpuamount; ++i) { + const cpuoffset = 3 + 5 * i; + + watchers.call('user:add', { + user: { + id: data[cpuoffset], + self: data[cpuoffset] === selfUserId, + name: data[cpuoffset + 1], + colour: parseUserColour(data[cpuoffset + 2]), + perms: parseUserPerms(data[cpuoffset + 3]), + permsRaw: data[cpuoffset + 3], + hidden: data[cpuoffset + 4] !== '0', + }, + }); } break; case '1': // message - let cmid = +data[8], - cmtext = data[7], - cmflags = data[10]; - const cmuser = new Umi.User(data[3], data[4], data[5], data[6]), - cmtextParts = cmtext.split("\f"), - cmbotInfo = {}; + const cmMsgInfo = { + msg: { + id: data[8], + time: new Date(parseInt(data[2]) * 1000), + channel: selfChannelName, + sender: { + id: data[3], + name: data[4], + colour: parseUserColour(data[5]), + perms: parseUserColour(data[6]), + permsRaw: data[6], + }, + isBot: data[3] === '-1', + silent: data[9] === '0', + flags: parseMsgFlags(data[10]), + flagsRaw: data[10], + text: unfuckText(data[7]), + }, + }; - if(isNaN(cmid)) - cmid = -Math.ceil(Math.random() * 10000000000); + const cmMsgIdFirst = cmMsgInfo.msg.id.charCodeAt(0); + if(cmMsgIdFirst < 48 || cmMsgIdFirst > 57) + cmMsgInfo.msg.id = (Math.round(Number.MIN_SAFE_INTEGER * Math.random())).toString(); - if(cmtextParts[1]) { - cmbotInfo.isError = cmtextParts[0] !== '0'; - cmbotInfo.type = cmtextParts[1]; - cmbotInfo.args = cmtextParts.slice(2); - cmtext = ''; - } else - cmtext = unfuckText(cmtext); + if(cmMsgInfo.msg.isBot) { + const cmBotParts = data[7].split("\f"); + cmMsgInfo.msg.botInfo = { + isError: cmBotParts[0] === '1', + type: cmBotParts[1], + args: cmBotParts.slice(2), + }; + } - Umi.Messages.Add(new Umi.Message( - cmid, data[2], cmuser, cmtext, channelName, false, cmbotInfo, - cmflags[1] !== '0' && cmflags[3] === '0', true - )); + watchers.call('msg:add', cmMsgInfo); break; case '2': // channels - const ecpamount = +data[2]; - let ecpstart = 3; + const ecpamount = parseInt(data[2]); - for(let i = 0; i < ecpamount; i++) { - const channel = new Umi.Channel( - data[ecpstart], - data[ecpstart + 1] !== '0', - data[ecpstart + 2] !== '0' - ); - Umi.Channels.Add(channel); + for(let i = 0; i < ecpamount; ++i) { + const ecpoffset = 3 + 3 * i; - if(channel.getName() === channelName) - Umi.Channels.Switch(channel); - - ecpstart = ecpstart + 3; + watchers.call('chan:add', { + channel: { + name: data[ecpoffset], + hasPassword: data[ecpoffset + 1] !== '0', + isTemporary: data[ecpoffset + 2] !== '0', + isCurrent: data[ecpoffset] === selfChannelName, + }, + }); } + + watchers.call('chan:focus', { + channel: { name: selfChannelName }, + }); break; } break; case '8': // context clear - const cckeep = Umi.Users.Get(userId); + if(data[1] === '0' || data[1] === '3' || data[1] === '4') + watchers.call('msg:clear'); - switch(data[1]) { - case '0': - Umi.UI.Messages.RemoveAll(); - break; + if(data[1] === '1' || data[1] === '3' || data[1] === '4') + watchers.call('user:clear'); - case '1': - Umi.Users.Clear(); - Umi.Users.Add(cckeep); - break; - - case '2': - Umi.Channels.Clear(); - break; - - case '3': - Umi.Messages.Clear(); - Umi.Users.Clear(); - Umi.Users.Add(cckeep); - break; - - case '4': - Umi.UI.Messages.RemoveAll(); - Umi.Users.Clear(); - Umi.Users.Add(cckeep); - Umi.Channels.Clear(); - break; - } + if(data[1] === '2' || data[1] === '4') + watchers.call('chan:clear'); break; case '9': // baka noReconnect = true; wasKicked = true; - const isBan = data[1] !== '0'; - - let message = 'You were kicked, refresh to log back in!'; - if(isBan) { - if(data[2] === '-1') { - message = 'You have been banned till the end of time, please try again in a different dimension.'; - } else { - const until = new Date(parseInt(data[2]) * 1000); - message = 'You were banned until {0}!'.replace('{0}', until.toLocaleString()); - } + const bakaInfo = { + session: { success: false }, + baka: { + type: data[1] === '0' ? 'kick' : 'ban', + }, + }; + if(bakaInfo.baka.type === 'ban') { + bakaInfo.baka.perma = data[2] === '-1'; + bakaInfo.baka.until = data[2] === '-1' ? undefined : new Date(parseInt(data[2]) * 1000); } - let icon, header; - if(isBan) { - icon = 'hammer'; - header = 'Banned!'; - } else { - icon = 'bomb'; - header = 'Kicked!'; - } - - const currentView = views.currentElement(); - - MamiAnimate({ - duration: 550, - easing: 'outExpo', - start: () => { - playBannedBgm(true); - playBannedSfx(); - }, - update: t => { - currentView.style.transform = `scale(${(1 - .5 * t)}, ${(1 - 1 * t)})`; - }, - end: () => { - getLoadingOverlay(icon, header, message).then(() => { - playBannedBgm(); - - // there's currently no way to reconnect after a kick/ban so just dispose of the ui entirely - if(views.count() > 1) - views.shift(); - }); - }, - }); + watchers.call('session:term', bakaInfo); break; case '10': // user update - const spuser = Umi.Users.Get(+data[1]); - spuser.setName(data[2]); - spuser.setColour(data[3]); - spuser.setPermissions(data[4]); - Umi.Users.Update(spuser.getId(), spuser); + watchers.call('user:update', { + user: { + id: data[1], + self: data[1] === selfUserId, + name: data[2], + colour: parseUserColour(data[3]), + perms: parseUserPerms(data[4]), + permsRaw: data[4], + }, + }); break; } }; - const beginConnecting = function() { + const beginConnecting = () => { + sock?.close(); + if(noReconnect) return; - ++connectAttempts; + UmiServers.getServer(server => { + watchers.call('conn:init', { + server: server, + wasConnected: wasConnected, + attempt: ++connectAttempts, + }); - let str = 'Connecting to server...'; - if(connectAttempts > 1) - str += ' (Attempt ' + connectAttempts.toString() + ')'; - - getLoadingOverlay('spinner', 'Loading...', str); - - UmiServers.getServer(function(server) { - if(settings.get('dumpPackets')) - console.log('Connecting to ' + server); - - sock = new UmiWebSocket(server, function(ev) { + sock = new UmiWebSocket(server, ev => { switch(ev.act) { case 'ws:open': onOpen(ev); @@ -554,6 +548,16 @@ Umi.Protocol.SockChat.Protocol = function(views, settings) { case 'ws:message': onMessage(ev); break; + case 'ws:create_interval': + pingTimer = ev.id; + break; + case 'ws:call_interval': + if(ev.id === pingTimer) + onSendPing(); + break; + case 'ws:clear_intervals': + pingTimer = undefined; + break; default: console.log(ev.data); break; @@ -562,26 +566,35 @@ Umi.Protocol.SockChat.Protocol = function(views, settings) { }); }; - Umi.Channels.OnSwitch.push(function(old, channel) { - if(channel.isUserChannel()) { - pmUserName = channel.getName().substring(1); - } else { - pmUserName = null; - switchChannel(channel.getName()); - } - }); + return { + sendMessage: sendMessage, + open: () => { + noReconnect = false; + beginConnecting(); + }, + close: () => { + noReconnect = true; + sock?.close(); + }, + watch: (name, handler) => watchers.watch(name, handler), + unwatch: (name, handler) => watchers.unwatch(name, handler), + setDumpPackets: state => dumpPackets = !!state, + switchChannel: channelInfo => { + if(selfUserId === undefined) + return; - pub.sendMessage = sendMessage; + const name = channelInfo.getName(); - pub.open = function() { - noReconnect = false; - beginConnecting(); + if(channelInfo.isUserChannel()) { + selfPseudoChannelName = name.substring(1); + } else { + selfPseudoChannelName = undefined; + if(selfChannelName === name) + return; + + selfChannelName = name; + sendMessage(`/join ${name}`); + } + }, }; - - pub.close = function() { - noReconnect = true; - sock.close(); - }; - - return pub; }; diff --git a/src/mami.js/ui/hooks.js b/src/mami.js/ui/hooks.js index 12e0330..7dab259 100644 --- a/src/mami.js/ui/hooks.js +++ b/src/mami.js/ui/hooks.js @@ -47,6 +47,7 @@ Umi.UI.Hooks = (function() { Umi.Channels.OnSwitch.push(function(name, channel) { Umi.UI.Channels.Reload(name === null); + Umi.Server.switchChannel(channel); }); diff --git a/src/mami.js/ui/settings.jsx b/src/mami.js/ui/settings.jsx index 6f936dd..1417ff0 100644 --- a/src/mami.js/ui/settings.jsx +++ b/src/mami.js/ui/settings.jsx @@ -358,6 +358,11 @@ Umi.UI.Settings = (function() { title: 'Dump packets to console', type: 'checkbox', }, + { + name: 'dumpEvents', + title: 'Dump events to console', + type: 'checkbox', + }, { name: 'neverUseWorker', title: 'Never use Worker for connection', diff --git a/src/mami.js/watcher.js b/src/mami.js/watcher.js index 1c52046..b859a0f 100644 --- a/src/mami.js/watcher.js +++ b/src/mami.js/watcher.js @@ -1,6 +1,9 @@ #include utility.js -const MamiWatcher = function() { +const MamiWatcher = function(initCall) { + if(typeof initCall !== 'boolean') + initCall = true; + const handlers = []; const watch = (handler, ...args) => { @@ -10,8 +13,11 @@ const MamiWatcher = function() { throw 'handler already registered'; handlers.push(handler); - args.push(true); - handler(...args); + + if(initCall) { + args.push(true); + handler(...args); + } }; const unwatch = handler => { @@ -22,7 +28,8 @@ const MamiWatcher = function() { watch: watch, unwatch: unwatch, call: (...args) => { - args.push(false); + if(initCall) + args.push(false); for(const handler of handlers) handler(...args); @@ -30,7 +37,10 @@ const MamiWatcher = function() { }; }; -const MamiWatchers = function() { +const MamiWatchers = function(initCall) { + if(typeof initCall !== 'boolean') + initCall = true; + const watchers = new Map; const getWatcher = name => { @@ -53,10 +63,10 @@ const MamiWatchers = function() { unwatch: unwatch, define: names => { if(typeof names === 'string') - watchers.set(names, new MamiWatcher); + watchers.set(names, new MamiWatcher(initCall)); else if(Array.isArray(names)) for(const name of names) - watchers.set(name, new MamiWatcher); + watchers.set(name, new MamiWatcher(initCall)); else throw 'names must be an array of names or a single name'; }, diff --git a/src/mami.js/websock.js b/src/mami.js/websock.js index 630cf31..e13424b 100644 --- a/src/mami.js/websock.js +++ b/src/mami.js/websock.js @@ -91,10 +91,15 @@ const UmiWebSocket = function(server, message, useWorker) { return intervals; }; sendInterval = function(text, interval) { - intervals.push(setInterval(function() { - if(websocket) + const intervalId = setInterval(function() { + if(websocket) { websocket.send(text); - }, interval)); + message({ act: 'ws:call_interval', id: intervalId }); + } + }, interval); + + intervals.push(intervalId); + message({ act: 'ws:create_interval', id: intervalId }); }; clearIntervals = function() { for(let i = 0; i < intervals.length; ++i) diff --git a/src/websock.js/main.js b/src/websock.js/main.js index 08669d7..ca142ef 100644 --- a/src/websock.js/main.js +++ b/src/websock.js/main.js @@ -75,10 +75,15 @@ addEventListener('message', function(ev) { case 'ws:send_interval': (function(interval, text) { - intervals.push(setInterval(function() { - if(websocket) + const intervalId = setInterval(function() { + if(websocket) { websocket.send(text); - }, interval)); + postMessage({ act: 'ws:call_interval', id: intervalId }); + } + }, interval); + + intervals.push(intervalId); + postMessage({ act: 'ws:create_interval', id: intervalId }); })(ev.data.interval, ev.data.text); break;