Moved all/most UI code out of the Sock Chat protocol handler.

This commit is contained in:
flash 2024-02-24 01:10:30 +00:00
parent ec7ca22811
commit 5b5ca888a4
11 changed files with 855 additions and 450 deletions

View file

@ -1,8 +1,8 @@
const MamiColour = (() => { const MamiColour = (() => {
const readThres = 168, const readThres = 168;
lumiRed = .299, const lumiRed = .299;
lumiGreen = .587, const lumiGreen = .587;
lumiBlue = .114; const lumiBlue = .114;
const pub = {}; const pub = {};
@ -25,8 +25,8 @@ const MamiColour = (() => {
}; };
const weighted = (raw1, raw2, weight) => { const weighted = (raw1, raw2, weight) => {
const rgb1 = extractRGB(raw1), const rgb1 = extractRGB(raw1);
rgb2 = extractRGB(raw2); const rgb2 = extractRGB(raw2);
return (weightedNumber(rgb1[0], rgb2[0], weight) << 16) return (weightedNumber(rgb1[0], rgb2[0], weight) << 16)
| (weightedNumber(rgb1[1], rgb2[1], weight) << 8) | (weightedNumber(rgb1[1], rgb2[1], weight) << 8)
| weightedNumber(rgb1[2], rgb2[2], weight); | weightedNumber(rgb1[2], rgb2[2], weight);

View file

@ -1,13 +1,19 @@
window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } }; window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
#include animate.js #include animate.js
#include channel.js
#include channels.js
#include common.js #include common.js
#include context.js #include context.js
#include emotes.js #include emotes.js
#include message.js
#include messages.js #include messages.js
#include mszauth.js #include mszauth.js
#include parsing.js
#include server.js #include server.js
#include txtrigs.js #include txtrigs.js
#include user.js
#include users.js
#include utility.js #include utility.js
#include weeb.js #include weeb.js
#include audio/autoplay.js #include audio/autoplay.js
@ -21,10 +27,12 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
#include sound/umisound.js #include sound/umisound.js
#include ui/chat-layout.js #include ui/chat-layout.js
#include ui/hooks.js #include ui/hooks.js
#include ui/emotes.js
#include ui/input-menus.js #include ui/input-menus.js
#include ui/loading-overlay.jsx #include ui/loading-overlay.jsx
#include ui/markup.js #include ui/markup.js
#include ui/menus.js #include ui/menus.js
#include ui/messages.jsx
#include ui/view.js #include ui/view.js
#include ui/settings.jsx #include ui/settings.jsx
#include ui/toggles.js #include ui/toggles.js
@ -109,6 +117,7 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
settings.define('osuKeysV2', ['no', 'yes', 'rng'], 'no'); settings.define('osuKeysV2', ['no', 'yes', 'rng'], 'no');
settings.define('explosionRadius', 'number', 20); settings.define('explosionRadius', 'number', 20);
settings.define('dumpPackets', 'boolean', FUTAMI_DEBUG); settings.define('dumpPackets', 'boolean', FUTAMI_DEBUG);
settings.define('dumpEvents', 'boolean', FUTAMI_DEBUG);
settings.define('neverUseWorker', 'boolean', false, false, true); settings.define('neverUseWorker', 'boolean', false, false, true);
settings.define('forceUseWorker', 'boolean', false, false, true); settings.define('forceUseWorker', 'boolean', false, false, true);
settings.define('marqueeAllNames', 'boolean', false); settings.define('marqueeAllNames', 'boolean', false);
@ -244,7 +253,7 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
return 'minecraft:door:open'; return 'minecraft:door:open';
if(v === 'old') if(v === 'old')
return 'minecraft:door:open-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]`; insertText = `[video]${fileInfo.url}[/video]`;
uploadEntry.setThumbnail(fileInfo.thumb); uploadEntry.setThumbnail(fileInfo.thumb);
} else } else
insertText = location.protocol + fileInfo.url; insertText = location.Umi.Servercol + fileInfo.url;
if(settings.get('eepromAutoInsert')) if(settings.get('eepromAutoInsert'))
Umi.UI.Markup.InsertRaw(insertText, ''); 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...'); 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) if(window.dispatchEvent)
window.dispatchEvent(new Event('umi:connect')); window.dispatchEvent(new Event('umi:connect'));

View file

@ -5,7 +5,7 @@ Umi.Message = (() => {
return function(msgId, time, user, text, channel, highlight, botInfo, isAction, isLog) { return function(msgId, time, user, text, channel, highlight, botInfo, isAction, isLog) {
msgId = (msgId || '').toString(); 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; user = user !== null && typeof user === 'object' ? user : chatBot;
text = (text || '').toString(); text = (text || '').toString();
channel = (channel || '').toString(); channel = (channel || '').toString();
@ -18,7 +18,10 @@ Umi.Message = (() => {
return { return {
getId: () => msgId, getId: () => msgId,
getIdInt: () => msgIdInt, getIdInt: () => {
const num = parseInt(msgId);
return isNaN(num) ? (Math.round(Number.MIN_SAFE_INTEGER * Math.random())) : num;
},
getTime: () => time, getTime: () => time,
getUser: () => user, getUser: () => user,
getText: () => text, getText: () => text,

View file

@ -1,5 +1,4 @@
#include channels.js #include channels.js
#include server.js
#include ui/messages.jsx #include ui/messages.jsx
Umi.Messages = (function() { Umi.Messages = (function() {

View file

@ -1,26 +1,3 @@
#include sockchat_old.js #include sockchat_old.js
Umi.Server = (function() { Umi.Server = new Umi.Protocol.SockChat.Protocol;
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);
},
};
})();

View file

@ -1,149 +1,136 @@
#include channel.js
#include channels.js
#include common.js #include common.js
#include message.js
#include messages.js
#include mszauth.js #include mszauth.js
#include user.js
#include users.js
#include parsing.js
#include servers.js #include servers.js
#include watcher.js
#include websock.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) { Umi.Protocol.SockChat.Protocol = function() {
const pub = {}; 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, const parseUserColour = str => {
connectAttempts = 0, // todo
wasKicked = false, return str;
isRestarting = false; };
let userId = null, let parseUserPermsSep;
channelName = null, const parseUserPerms = str => {
pmUserName = null; 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; let msg = opcode;
if(data) msg += "\t" + data.join("\t"); if(data)
if(settings.get('dumpPackets')) msg += `\t${data.join("\t")}`;
if(dumpPackets)
console.log(msg); console.log(msg);
sock.send(msg); sock.send(msg);
}; };
const sendPing = function() {
if(userId === null) const onSendPing = () => {
if(selfUserId === undefined)
return; return;
watchers.call('ping:send');
startPingWatcher();
lastPong = undefined;
lastPing = Date.now(); lastPing = Date.now();
send('0', [userId]);
}; };
const sendAuth = function(args) { const sendAuth = args => {
if(userId !== null) if(selfUserId === undefined)
return;
send('1', args); send('1', args);
}; };
const sendMessage = function(text) { const sendMessage = text => {
if(userId === null) if(selfUserId === undefined)
return; return;
if(text.substring(0, 1) !== '/' && pmUserName !== null) if(text.substring(0, 1) !== '/' && selfPseudoChannelName !== undefined)
text = '/msg ' + pmUserName + ' ' + text; text = `/msg ${selfPseudoChannelName} ${text}`;
send('2', [userId, text]); send('2', [selfUserId, text]);
}; };
const switchChannel = function(name) { const startKeepAlive = () => sock?.sendInterval(`0\t${selfUserId}`, futami.get('ping') * 1000);
if(channelName === name) const stopKeepAlive = () => sock?.clearIntervals();
return;
channelName = name; const onOpen = ev => {
sendMessage('/join ' + name); if(dumpPackets)
};
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'))
console.log(ev); console.log(ev);
wasKicked = false;
isRestarting = 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(); const authInfo = MamiMisuzuAuth.getInfo();
sendAuth([authInfo.method, authInfo.token]); sendAuth([authInfo.method, authInfo.token]);
}; };
const closeReasons = { const onClose = ev => {
'_1000': 'The connection has been ended.', if(dumpPackets)
'_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'))
console.log(ev); console.log(ev);
userId = null; selfUserId = undefined;
channelName = null; selfChannelName = undefined;
pmUserName = null; selfPseudoChannelName = undefined;
stopPingWatcher();
stopKeepAlive(); stopKeepAlive();
if(wasKicked) if(wasKicked)
@ -155,189 +142,199 @@ Umi.Protocol.SockChat.Protocol = function(views, settings) {
} else if(code === 1012) } else if(code === 1012)
isRestarting = true; isRestarting = true;
const msg = closeReasons['_' + code.toString()] watchers.call('conn:lost', {
|| ('Something caused an unexpected connection loss, the error code was: ' + ev.code.toString() + '.'); wasConnected: wasConnected,
isRestarting: isRestarting,
getLoadingOverlay('unlink', 'Disconnected!', msg); code: code,
});
Umi.Users.Clear();
connectAttempts = 0; connectAttempts = 0;
setTimeout(function() { setTimeout(() => beginConnecting(), 5000);
beginConnecting();
}, 5000);
}; };
const unfuckText = function(text) { const onError = ex => {
watchers.call('conn:error', ex);
};
const unfuckText = text => {
const elem = document.createElement('div'); const elem = document.createElement('div');
elem.innerHTML = text.replace(/ <br\/> /g, "\n"); elem.innerHTML = text.replace(/ <br\/> /g, "\n");
text = elem.innerText; text = elem.innerText;
return text; return text;
}; };
const onMessage = function(ev) { const onMessage = ev => {
const data = ev.data.split("\t"); const data = ev.data.split("\t");
if(settings.get('dumpPackets')) if(dumpPackets)
console.log(data); console.log(data);
switch(data[0]) { switch(data[0]) {
case '0': // ping case '0': // ping
// nothing to do lastPong = Date.now();
watchers.call('ping:recv', {
ping: lastPing,
pong: lastPong,
diff: lastPong - lastPing,
});
break; break;
case '1': // join 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') { if(data[1] == 'y') {
userId = data[2]; selfUserId = data[2];
channelName = data[6]; selfChannelName = data[6];
views.pop(ctx => MamiAnimate({ watchers.call('session:start', {
async: true, wasConnected: wasConnected,
duration: 120, session: { success: true },
easing: 'inOutSine', ctx: {
start: () => { maxMsgLength: parseInt(data[7]),
ctx.toElem.style.zIndex = '100';
ctx.fromElem.style.pointerEvents = 'none';
ctx.fromElem.style.zIndex = '200';
}, },
update: t => { user: {
ctx.fromElem.style.transform = `scale(${1 + (.25 * t)})`; id: selfUserId,
ctx.fromElem.style.opacity = 1 - (1 * t).toString(); self: true,
name: data[3],
colour: parseUserColour(data[4]),
perms: parseUserPerms(data[5]),
permsRaw: data[5],
}, },
end: () => { channel: {
ctx.toElem.style.zIndex = null; name: selfChannelName,
}, },
})); });
startKeepAlive(); startKeepAlive();
wasConnected = true;
} else { } else {
switch (data[2]) {
case 'joinfail':
wasKicked = true; wasKicked = true;
const jfuntil = new Date(parseInt(data[3]) * 1000); const failInfo = {
let banmsg = 'You were banned until {0}!'.replace('{0}', jfuntil.toLocaleString()); 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') watchers.call('session:fail', failInfo);
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;
} }
} }
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; break;
case '2': // message case '2': // message
let text = data[3]; let mText = unfuckText(data[3]);
const muser = Umi.Users.Get(data[2]); let mChannelName = selfChannelName;
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 = '';
text = unfuckText(text); if(data[5][4] !== '0') {
if(data[2] === selfUserId) {
if(isBot) { const mTextParts = mText.split(' ');
botInfo.isError = textParts[0] !== '0'; mChannelName = `@${mTextParts.shift()}`;
botInfo.type = textParts[1]; mText = mTextParts.join(' ');
botInfo.args = textParts.slice(2); } else {
mChannelName = `@~${data[2]}`;
}
} }
if(isPM) { const msgInfo = {
if(muser.getId() === userId) { msg: {
const tmpMsg = text.split(' '); id: data[4],
pmChannel = `@${tmpMsg.shift()}`; time: new Date(parseInt(data[1]) * 1000),
text = tmpMsg.join(' '); channel: mChannelName,
} else sender: { id: data[2], },
pmChannel = `@${muser.getName()}`; flags: parseMsgFlags(data[5]),
flagsRaw: data[5],
isBot: data[2] === '-1',
text: mText,
},
};
if(Umi.Channels.Get(pmChannel) === null) if(msgInfo.msg.isBot) {
Umi.Channels.Add(new Umi.Channel(pmChannel, false, true, true)); const botParts = data[3].split("\f");
msgInfo.msg.botInfo = {
Umi.UI.Menus.Attention('channels'); isError: botParts[0] === '1',
type: botParts[1],
args: botParts.slice(2),
};
} }
Umi.Messages.Add(new Umi.Message( watchers.call('msg:add', msgInfo);
data[4], data[1], muser, text,
isPM ? pmChannel : channelName,
false, botInfo, isAction
));
break; break;
case '3': // leave case '3': // leave
const luser = Umi.Users.Get(data[1]); watchers.call('user:remove', {
leave: { type: data[3] },
Umi.Messages.Add(new Umi.Message( msg: {
data[5], data[4], undefined, '', channelName, false, id: data[5],
{ type: data[3], isError: false, args: [luser.getName()], target: luser } time: new Date(parseInt(data[4]) * 1000),
)); channel: selfChannelName,
Umi.Users.Remove(luser); botInfo: {
type: data[3],
args: [data[2]],
},
},
user: {
id: data[1],
self: data[1] === selfUserId,
name: data[2],
},
});
break; break;
case '4': // channel case '4': // channel
switch(data[1]) { switch(data[1]) {
case '0': 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; break;
case '1': case '1':
const uchannel = Umi.Channels.Get(data[2]); watchers.call('chan:update', {
uchannel.setName(data[3]); channel: {
uchannel.setHasPassword(data[4] !== '0'); previousName: data[2],
uchannel.setTemporary(data[5] !== '0'); name: data[3],
Umi.Channels.Update(data[2], uchannel); hasPassword: data[4] !== '0',
isTemporary: data[5] !== '0',
},
});
break; break;
case '2': case '2':
Umi.Channels.Remove(Umi.Channels.Get(data[2])); watchers.call('chan:remove', {
channel: { name: data[2] },
});
break; break;
} }
break; break;
@ -345,205 +342,202 @@ Umi.Protocol.SockChat.Protocol = function(views, settings) {
case '5': // user move case '5': // user move
switch(data[1]) { switch(data[1]) {
case '0': case '0':
const umuser = new Umi.User(data[2], data[3], data[4], data[5]); watchers.call('chan:join', {
user: {
Umi.Users.Add(umuser); id: data[2],
Umi.Messages.Add(new Umi.Message( self: data[2] === selfUserId,
data[6], null, undefined, '', channelName, false, name: data[3],
{ type: 'jchan', isError: false, args: [ umuser.getName() ], target: umuser } colour: parseUserColour(data[4]),
)); perms: parseUserPerms(data[5]),
permsRaw: data[5],
},
msg: {
id: data[6],
channel: selfChannelName,
botInfo: {
type: 'jchan',
args: [data[3]],
},
},
});
break; break;
case '1': case '1':
if(data[2] === userId) watchers.call('chan:leave', {
return; user: {
id: data[2],
const mouser = Umi.Users.Get(+data[2]); self: data[2] === selfUserId,
},
Umi.Messages.Add(new Umi.Message( msg: {
data[3], null, undefined, '', channelName, false, id: data[3],
{ type: 'lchan', isError: false, args: [ mouser.getName() ], target: mouser } channel: selfChannelName,
)); botInfo: {
Umi.Users.Remove(mouser); type: 'lchan',
args: [data[2]],
},
},
});
break; break;
case '2': case '2':
Umi.Channels.Switch(Umi.Channels.Get(data[2])); selfChannelName = data[2];
watchers.call('chan:focus', {
channel: { name: selfChannelName },
});
break; break;
} }
break; break;
case '6': // message delete case '6': // message delete
Umi.Messages.Remove(Umi.Messages.Get(data[1])); watchers.call('msg:remove', {
msg: {
id: data[1],
channel: selfChannelName,
},
});
break; break;
case '7': // context populate case '7': // context populate
switch(data[1]) { switch(data[1]) {
case '0': // users case '0': // users
const cpuamount = parseInt(data[2]); const cpuamount = parseInt(data[2]);
let cpustart = 3;
for(let i = 0; i < cpuamount; i++) { for(let i = 0; i < cpuamount; ++i) {
const user = new Umi.User(data[cpustart], data[cpustart + 1], data[cpustart + 2], data[cpustart + 3]); const cpuoffset = 3 + 5 * i;
Umi.Users.Add(user);
cpustart += 5; 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; break;
case '1': // message case '1': // message
let cmid = +data[8], const cmMsgInfo = {
cmtext = data[7], msg: {
cmflags = data[10]; id: data[8],
const cmuser = new Umi.User(data[3], data[4], data[5], data[6]), time: new Date(parseInt(data[2]) * 1000),
cmtextParts = cmtext.split("\f"), channel: selfChannelName,
cmbotInfo = {}; 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)) const cmMsgIdFirst = cmMsgInfo.msg.id.charCodeAt(0);
cmid = -Math.ceil(Math.random() * 10000000000); if(cmMsgIdFirst < 48 || cmMsgIdFirst > 57)
cmMsgInfo.msg.id = (Math.round(Number.MIN_SAFE_INTEGER * Math.random())).toString();
if(cmtextParts[1]) { if(cmMsgInfo.msg.isBot) {
cmbotInfo.isError = cmtextParts[0] !== '0'; const cmBotParts = data[7].split("\f");
cmbotInfo.type = cmtextParts[1]; cmMsgInfo.msg.botInfo = {
cmbotInfo.args = cmtextParts.slice(2); isError: cmBotParts[0] === '1',
cmtext = ''; type: cmBotParts[1],
} else args: cmBotParts.slice(2),
cmtext = unfuckText(cmtext); };
}
Umi.Messages.Add(new Umi.Message( watchers.call('msg:add', cmMsgInfo);
cmid, data[2], cmuser, cmtext, channelName, false, cmbotInfo,
cmflags[1] !== '0' && cmflags[3] === '0', true
));
break; break;
case '2': // channels case '2': // channels
const ecpamount = +data[2]; const ecpamount = parseInt(data[2]);
let ecpstart = 3;
for(let i = 0; i < ecpamount; i++) { for(let i = 0; i < ecpamount; ++i) {
const channel = new Umi.Channel( const ecpoffset = 3 + 3 * i;
data[ecpstart],
data[ecpstart + 1] !== '0',
data[ecpstart + 2] !== '0'
);
Umi.Channels.Add(channel);
if(channel.getName() === channelName) watchers.call('chan:add', {
Umi.Channels.Switch(channel); channel: {
name: data[ecpoffset],
ecpstart = ecpstart + 3; hasPassword: data[ecpoffset + 1] !== '0',
isTemporary: data[ecpoffset + 2] !== '0',
isCurrent: data[ecpoffset] === selfChannelName,
},
});
} }
watchers.call('chan:focus', {
channel: { name: selfChannelName },
});
break; break;
} }
break; break;
case '8': // context clear 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]) { if(data[1] === '1' || data[1] === '3' || data[1] === '4')
case '0': watchers.call('user:clear');
Umi.UI.Messages.RemoveAll();
break;
case '1': if(data[1] === '2' || data[1] === '4')
Umi.Users.Clear(); watchers.call('chan: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;
}
break; break;
case '9': // baka case '9': // baka
noReconnect = true; noReconnect = true;
wasKicked = true; wasKicked = true;
const isBan = data[1] !== '0'; const bakaInfo = {
session: { success: false },
let message = 'You were kicked, refresh to log back in!'; baka: {
if(isBan) { type: data[1] === '0' ? 'kick' : 'ban',
if(data[2] === '-1') { },
message = 'You have been banned till the end of time, please try again in a different dimension.'; };
} else { if(bakaInfo.baka.type === 'ban') {
const until = new Date(parseInt(data[2]) * 1000); bakaInfo.baka.perma = data[2] === '-1';
message = 'You were banned until {0}!'.replace('{0}', until.toLocaleString()); bakaInfo.baka.until = data[2] === '-1' ? undefined : new Date(parseInt(data[2]) * 1000);
}
} }
let icon, header; watchers.call('session:term', bakaInfo);
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();
});
},
});
break; break;
case '10': // user update case '10': // user update
const spuser = Umi.Users.Get(+data[1]); watchers.call('user:update', {
spuser.setName(data[2]); user: {
spuser.setColour(data[3]); id: data[1],
spuser.setPermissions(data[4]); self: data[1] === selfUserId,
Umi.Users.Update(spuser.getId(), spuser); name: data[2],
colour: parseUserColour(data[3]),
perms: parseUserPerms(data[4]),
permsRaw: data[4],
},
});
break; break;
} }
}; };
const beginConnecting = function() { const beginConnecting = () => {
sock?.close();
if(noReconnect) if(noReconnect)
return; return;
++connectAttempts; UmiServers.getServer(server => {
watchers.call('conn:init', {
server: server,
wasConnected: wasConnected,
attempt: ++connectAttempts,
});
let str = 'Connecting to server...'; sock = new UmiWebSocket(server, ev => {
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) {
switch(ev.act) { switch(ev.act) {
case 'ws:open': case 'ws:open':
onOpen(ev); onOpen(ev);
@ -554,6 +548,16 @@ Umi.Protocol.SockChat.Protocol = function(views, settings) {
case 'ws:message': case 'ws:message':
onMessage(ev); onMessage(ev);
break; 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: default:
console.log(ev.data); console.log(ev.data);
break; break;
@ -562,26 +566,35 @@ Umi.Protocol.SockChat.Protocol = function(views, settings) {
}); });
}; };
Umi.Channels.OnSwitch.push(function(old, channel) { return {
if(channel.isUserChannel()) { sendMessage: sendMessage,
pmUserName = channel.getName().substring(1); open: () => {
} else {
pmUserName = null;
switchChannel(channel.getName());
}
});
pub.sendMessage = sendMessage;
pub.open = function() {
noReconnect = false; noReconnect = false;
beginConnecting(); beginConnecting();
}; },
close: () => {
pub.close = function() {
noReconnect = true; noReconnect = true;
sock.close(); 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;
return pub; const name = channelInfo.getName();
if(channelInfo.isUserChannel()) {
selfPseudoChannelName = name.substring(1);
} else {
selfPseudoChannelName = undefined;
if(selfChannelName === name)
return;
selfChannelName = name;
sendMessage(`/join ${name}`);
}
},
};
}; };

View file

@ -47,6 +47,7 @@ Umi.UI.Hooks = (function() {
Umi.Channels.OnSwitch.push(function(name, channel) { Umi.Channels.OnSwitch.push(function(name, channel) {
Umi.UI.Channels.Reload(name === null); Umi.UI.Channels.Reload(name === null);
Umi.Server.switchChannel(channel);
}); });

View file

@ -358,6 +358,11 @@ Umi.UI.Settings = (function() {
title: 'Dump packets to console', title: 'Dump packets to console',
type: 'checkbox', type: 'checkbox',
}, },
{
name: 'dumpEvents',
title: 'Dump events to console',
type: 'checkbox',
},
{ {
name: 'neverUseWorker', name: 'neverUseWorker',
title: 'Never use Worker for connection', title: 'Never use Worker for connection',

View file

@ -1,6 +1,9 @@
#include utility.js #include utility.js
const MamiWatcher = function() { const MamiWatcher = function(initCall) {
if(typeof initCall !== 'boolean')
initCall = true;
const handlers = []; const handlers = [];
const watch = (handler, ...args) => { const watch = (handler, ...args) => {
@ -10,8 +13,11 @@ const MamiWatcher = function() {
throw 'handler already registered'; throw 'handler already registered';
handlers.push(handler); handlers.push(handler);
if(initCall) {
args.push(true); args.push(true);
handler(...args); handler(...args);
}
}; };
const unwatch = handler => { const unwatch = handler => {
@ -22,6 +28,7 @@ const MamiWatcher = function() {
watch: watch, watch: watch,
unwatch: unwatch, unwatch: unwatch,
call: (...args) => { call: (...args) => {
if(initCall)
args.push(false); args.push(false);
for(const handler of handlers) for(const handler of handlers)
@ -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 watchers = new Map;
const getWatcher = name => { const getWatcher = name => {
@ -53,10 +63,10 @@ const MamiWatchers = function() {
unwatch: unwatch, unwatch: unwatch,
define: names => { define: names => {
if(typeof names === 'string') if(typeof names === 'string')
watchers.set(names, new MamiWatcher); watchers.set(names, new MamiWatcher(initCall));
else if(Array.isArray(names)) else if(Array.isArray(names))
for(const name of names) for(const name of names)
watchers.set(name, new MamiWatcher); watchers.set(name, new MamiWatcher(initCall));
else else
throw 'names must be an array of names or a single name'; throw 'names must be an array of names or a single name';
}, },

View file

@ -91,10 +91,15 @@ const UmiWebSocket = function(server, message, useWorker) {
return intervals; return intervals;
}; };
sendInterval = function(text, interval) { sendInterval = function(text, interval) {
intervals.push(setInterval(function() { const intervalId = setInterval(function() {
if(websocket) if(websocket) {
websocket.send(text); websocket.send(text);
}, interval)); message({ act: 'ws:call_interval', id: intervalId });
}
}, interval);
intervals.push(intervalId);
message({ act: 'ws:create_interval', id: intervalId });
}; };
clearIntervals = function() { clearIntervals = function() {
for(let i = 0; i < intervals.length; ++i) for(let i = 0; i < intervals.length; ++i)

View file

@ -75,10 +75,15 @@ addEventListener('message', function(ev) {
case 'ws:send_interval': case 'ws:send_interval':
(function(interval, text) { (function(interval, text) {
intervals.push(setInterval(function() { const intervalId = setInterval(function() {
if(websocket) if(websocket) {
websocket.send(text); 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); })(ev.data.interval, ev.data.text);
break; break;