gamja/state.js

691 lines
17 KiB
JavaScript
Raw Normal View History

2021-06-04 12:03:03 -04:00
import * as irc from "./lib/irc.js";
import Client from "./lib/client.js";
import { createContext } from "./lib/index.js";
2021-01-22 12:29:22 -05:00
2020-07-13 11:22:24 -04:00
export const SERVER_BUFFER = "*";
2020-06-26 04:35:38 -04:00
export const BufferType = {
SERVER: "server",
CHANNEL: "channel",
NICK: "nick",
};
2020-06-24 10:56:28 -04:00
export const ServerStatus = Client.Status;
2020-06-24 10:56:28 -04:00
export const Unread = {
NONE: "",
MESSAGE: "message",
2020-06-29 05:08:47 -04:00
HIGHLIGHT: "highlight",
2020-06-24 10:56:28 -04:00
2021-05-31 12:26:04 -04:00
compare(a, b) {
2020-06-24 10:56:28 -04:00
const priority = {
2021-05-27 16:35:41 -04:00
[Unread.NONE]: 0,
2020-06-24 10:56:28 -04:00
[Unread.MESSAGE]: 1,
2020-06-29 05:08:47 -04:00
[Unread.HIGHLIGHT]: 2,
2020-06-24 10:56:28 -04:00
};
2021-05-31 12:26:04 -04:00
return priority[a] - priority[b];
},
union(a, b) {
return (Unread.compare(a, b) > 0) ? a : b;
2020-06-24 10:56:28 -04:00
},
};
2020-07-15 12:47:33 -04:00
2020-07-23 03:58:05 -04:00
export const ReceiptType = {
DELIVERED: "delivered",
READ: "read",
};
export const BufferEventsDisplayMode = {
FOLD: "fold",
EXPAND: "expand",
HIDE: "hide",
};
export const SettingsContext = createContext("settings");
2020-07-15 12:47:33 -04:00
export function getBufferURL(buf) {
switch (buf.type) {
case BufferType.SERVER:
2023-04-19 05:43:45 -04:00
return irc.formatURL();
2020-07-15 12:47:33 -04:00
case BufferType.CHANNEL:
2023-04-19 05:43:45 -04:00
return irc.formatURL({ entity: buf.name });
2020-07-15 12:47:33 -04:00
case BufferType.NICK:
2023-04-19 05:43:45 -04:00
return irc.formatURL({ entity: buf.name, enttype: "user" });
2020-07-15 12:47:33 -04:00
}
throw new Error("Unknown buffer type: " + buf.type);
}
export function getMessageURL(buf, msg) {
2021-06-10 12:11:11 -04:00
let bufURL = getBufferURL(buf);
2020-07-21 08:48:04 -04:00
if (msg.tags.msgid) {
return bufURL + "?msgid=" + encodeURIComponent(msg.tags.msgid);
2020-07-21 08:48:04 -04:00
} else {
return bufURL + "?timestamp=" + encodeURIComponent(msg.tags.time);
2020-07-21 08:48:04 -04:00
}
2020-07-15 12:47:33 -04:00
}
export function getServerName(server, bouncerNetwork) {
let netName = server.name;
if (bouncerNetwork && bouncerNetwork.name && bouncerNetwork.name !== bouncerNetwork.host) {
// User has picked a custom name for the network, use that
return bouncerNetwork.name;
}
if (netName) {
// Server has specified a name
return netName;
}
if (bouncerNetwork) {
return bouncerNetwork.name || bouncerNetwork.host || "server";
} else if (server.isBouncer) {
return "bouncer";
} else {
return "server";
}
}
2021-06-04 12:03:03 -04:00
export function receiptFromMessage(msg) {
// At this point all messages are supposed to have a time tag.
// App.addMessage ensures this is the case even if the server doesn't
// support server-time.
if (!msg.tags.time) {
throw new Error("Missing time message tag");
}
return { time: msg.tags.time };
}
2022-02-12 04:21:11 -05:00
export function isReceiptBefore(a, b) {
if (!b) {
return false;
}
if (!a) {
return true;
}
if (!a.time || !b.time) {
throw new Error("Missing receipt time");
}
return a.time <= b.time;
}
2022-02-11 10:37:58 -05:00
export function isMessageBeforeReceipt(msg, receipt) {
if (!receipt) {
return false;
}
if (!msg.tags.time) {
throw new Error("Missing time message tag");
}
if (!receipt.time) {
throw new Error("Missing receipt time");
}
return msg.tags.time <= receipt.time;
}
2021-06-04 12:03:03 -04:00
function updateState(state, updater) {
2021-06-10 12:11:11 -04:00
let updated;
2021-06-04 12:03:03 -04:00
if (typeof updater === "function") {
updated = updater(state, state);
} else {
updated = updater;
}
if (state === updated || !updated) {
return;
}
return { ...state, ...updated };
}
2021-06-04 12:37:34 -04:00
function isServerBuffer(buf) {
2024-10-13 18:56:18 -04:00
return buf.type === BufferType.SERVER;
2021-06-04 12:37:34 -04:00
}
function isChannelBuffer(buf) {
return buf.type === BufferType.CHANNEL;
}
function trimStartCharacter(s, c) {
let i = 0;
for (; i < s.length; ++i) {
if (s[i] !== c) {
break;
}
}
return s.substring(i);
}
2021-06-04 12:37:34 -04:00
/* Returns 1 if a should appear after b, -1 if a should appear before b, or
* 0 otherwise. */
function compareBuffers(a, b) {
2024-10-13 18:56:18 -04:00
if (a.server !== b.server) {
2021-06-04 12:37:34 -04:00
return a.server > b.server ? 1 : -1;
}
2024-10-13 18:56:18 -04:00
if (isServerBuffer(a) !== isServerBuffer(b)) {
2021-06-04 12:37:34 -04:00
return isServerBuffer(b) ? 1 : -1;
}
if (isChannelBuffer(a) && isChannelBuffer(b)) {
const strippedA = trimStartCharacter(a.name, a.name[0]);
const strippedB = trimStartCharacter(b.name, b.name[0]);
const cmp = strippedA.localeCompare(strippedB);
if (cmp !== 0) {
return cmp;
}
// if they are the same when stripped, fallthough to default logic
2021-06-04 12:37:34 -04:00
}
return a.name.localeCompare(b.name);
2021-06-04 12:37:34 -04:00
}
2021-06-11 06:44:14 -04:00
function updateMembership(membership, letter, add, client) {
let prefix = client.isupport.prefix();
2021-06-11 06:44:14 -04:00
let prefixPrivs = new Map(irc.parseMembershipModes(prefix).map((membership, i) => {
return [membership.prefix, i];
}));
if (add) {
let i = membership.indexOf(letter);
if (i < 0) {
membership += letter;
membership = Array.from(membership).sort((a, b) => {
return prefixPrivs.get(a) - prefixPrivs.get(b);
}).join("");
}
} else {
membership = membership.replace(letter, "");
}
return membership;
}
2021-06-04 13:07:14 -04:00
/* Insert a message in an immutable list of sorted messages. */
function insertMessage(list, msg) {
2024-10-13 18:56:18 -04:00
if (list.length === 0) {
2021-06-04 13:07:14 -04:00
return [msg];
} else if (!irc.findBatchByType(msg, "chathistory") || list[list.length - 1].tags.time <= msg.tags.time) {
2021-06-04 13:07:14 -04:00
return list.concat(msg);
}
2021-06-10 12:11:11 -04:00
let insertBefore = -1;
for (let i = 0; i < list.length; i++) {
let other = list[i];
2021-06-04 13:07:14 -04:00
if (msg.tags.time < other.tags.time) {
insertBefore = i;
break;
}
}
console.assert(insertBefore >= 0, "");
list = [ ...list ];
list.splice(insertBefore, 0, msg);
return list;
}
2021-06-10 12:11:11 -04:00
let lastServerID = 0;
let lastBufferID = 0;
let lastMessageKey = 0;
2021-06-04 12:37:34 -04:00
2021-06-04 12:03:03 -04:00
export const State = {
2021-09-21 06:33:22 -04:00
create() {
return {
servers: new Map(),
buffers: new Map(),
activeBuffer: null,
2022-02-04 08:22:50 -05:00
bouncerNetworks: new Map(),
settings: {
secondsInTimestamps: true,
bufferEvents: BufferEventsDisplayMode.FOLD,
},
2021-09-21 06:33:22 -04:00
};
},
2021-06-04 12:03:03 -04:00
updateServer(state, id, updater) {
2021-06-10 12:11:11 -04:00
let server = state.servers.get(id);
2021-06-04 12:03:03 -04:00
if (!server) {
return;
}
2021-06-10 12:11:11 -04:00
let updated = updateState(server, updater);
2021-06-04 12:03:03 -04:00
if (!updated) {
return;
}
2021-06-10 12:11:11 -04:00
let servers = new Map(state.servers);
2021-06-04 12:03:03 -04:00
servers.set(id, updated);
return { servers };
},
updateBuffer(state, id, updater) {
2021-06-10 12:11:11 -04:00
let buf = State.getBuffer(state, id);
2021-06-04 12:03:03 -04:00
if (!buf) {
return;
}
2021-06-10 12:11:11 -04:00
let updated = updateState(buf, updater);
2021-06-04 12:03:03 -04:00
if (!updated) {
return;
}
2021-06-10 12:11:11 -04:00
let buffers = new Map(state.buffers);
2021-06-04 12:03:03 -04:00
buffers.set(buf.id, updated);
return { buffers };
},
getActiveServerID(state) {
2021-06-10 12:11:11 -04:00
let buf = state.buffers.get(state.activeBuffer);
2021-06-04 12:03:03 -04:00
if (!buf) {
return null;
}
return buf.server;
},
getBuffer(state, id) {
switch (typeof id) {
case "number":
return state.buffers.get(id);
case "object":
if (id.id) {
return state.buffers.get(id.id);
}
2021-06-10 12:11:11 -04:00
let serverID = id.server, name = id.name;
2021-06-04 12:03:03 -04:00
if (!serverID) {
serverID = State.getActiveServerID(state);
}
if (!name) {
name = SERVER_BUFFER;
}
2021-06-04 12:03:03 -04:00
2021-06-10 12:11:11 -04:00
let cm = irc.CaseMapping.RFC1459;
let server = state.servers.get(serverID);
2021-06-04 12:03:03 -04:00
if (server) {
cm = server.cm;
2021-06-04 12:03:03 -04:00
}
2021-06-10 12:11:11 -04:00
let nameCM = cm(name);
for (let buf of state.buffers.values()) {
2021-06-04 12:03:03 -04:00
if (buf.server === serverID && cm(buf.name) === nameCM) {
return buf;
}
}
return null;
default:
throw new Error("Invalid buffer ID type: " + (typeof id));
}
},
2021-06-10 04:54:33 -04:00
createServer(state) {
lastServerID++;
2021-06-10 12:11:11 -04:00
let id = lastServerID;
2021-06-10 04:54:33 -04:00
2021-06-10 12:11:11 -04:00
let servers = new Map(state.servers);
2021-06-10 04:54:33 -04:00
servers.set(id, {
id,
name: null, // from ISUPPORT NETWORK
2021-06-10 04:54:33 -04:00
status: ServerStatus.DISCONNECTED,
cm: irc.CaseMapping.RFC1459,
users: new irc.CaseMapMap(null, irc.CaseMapping.RFC1459),
2021-11-21 06:13:44 -05:00
account: null,
supportsSASLPlain: false,
supportsAccountRegistration: false,
reliableUserAccounts: false,
statusMsg: null, // from ISUPPORT STATUSMSG
isBouncer: false,
bouncerNetID: null,
2021-06-10 04:54:33 -04:00
});
return [id, { servers }];
},
2021-06-04 12:37:34 -04:00
createBuffer(state, name, serverID, client) {
2021-06-10 12:11:11 -04:00
let buf = State.getBuffer(state, { server: serverID, name });
2021-06-04 12:37:34 -04:00
if (buf) {
return [buf.id, null];
}
lastBufferID++;
2021-06-10 12:11:11 -04:00
let id = lastBufferID;
2021-06-04 12:37:34 -04:00
2021-06-10 12:11:11 -04:00
let type;
2024-10-13 18:56:18 -04:00
if (name === SERVER_BUFFER) {
2021-06-04 12:37:34 -04:00
type = BufferType.SERVER;
} else if (client.isChannel(name)) {
type = BufferType.CHANNEL;
} else {
type = BufferType.NICK;
}
2021-06-10 12:11:11 -04:00
let bufferList = Array.from(state.buffers.values());
2021-06-04 12:37:34 -04:00
bufferList.push({
id,
name,
type,
server: serverID,
serverInfo: null, // if server
joined: false, // if channel
2021-06-04 12:37:34 -04:00
topic: null, // if channel
hasInitialWho: false, // if channel
2021-06-04 12:37:34 -04:00
members: new irc.CaseMapMap(null, client.cm), // if channel
messages: [],
unread: Unread.NONE,
prevReadReceipt: null,
2021-06-04 12:37:34 -04:00
});
bufferList = bufferList.sort(compareBuffers);
2021-06-10 12:11:11 -04:00
let buffers = new Map(bufferList.map((buf) => [buf.id, buf]));
2021-06-04 12:37:34 -04:00
return [id, { buffers }];
},
2022-02-04 08:22:50 -05:00
storeBouncerNetwork(state, id, attrs) {
let bouncerNetworks = new Map(state.bouncerNetworks);
bouncerNetworks.set(id, {
...bouncerNetworks.get(id),
...attrs,
});
return { bouncerNetworks };
},
deleteBouncerNetwork(state, id) {
let bouncerNetworks = new Map(state.bouncerNetworks);
bouncerNetworks.delete(id);
return { bouncerNetworks };
},
handleMessage(state, msg, serverID, client) {
function updateServer(updater) {
return State.updateServer(state, serverID, updater);
}
function updateBuffer(name, updater) {
return State.updateBuffer(state, { server: serverID, name }, updater);
}
function updateUser(name, updater) {
return updateServer((server) => {
let users = new irc.CaseMapMap(server.users);
let updated = updateState(users.get(name), updater);
if (!updated) {
return;
}
users.set(name, updated);
return { users };
});
}
2021-06-04 13:45:51 -04:00
// Don't update our internal state if it's a chat history message
if (irc.findBatchByType(msg, "chathistory")) {
return;
}
let target, channel, topic, targets, who, update, buffers;
switch (msg.command) {
case irc.RPL_MYINFO:
// TODO: parse available modes
2021-06-10 12:11:11 -04:00
let serverInfo = {
name: msg.params[1],
version: msg.params[2],
};
return updateBuffer(SERVER_BUFFER, { serverInfo });
case irc.RPL_ISUPPORT:
buffers = new Map(state.buffers);
state.buffers.forEach((buf) => {
2024-10-13 18:56:18 -04:00
if (buf.server !== serverID) {
return;
}
2021-06-10 12:11:11 -04:00
let members = new irc.CaseMapMap(buf.members, client.cm);
buffers.set(buf.id, { ...buf, members });
});
return {
buffers,
...updateServer((server) => {
return {
name: client.isupport.network(),
cm: client.cm,
users: new irc.CaseMapMap(server.users, client.cm),
reliableUserAccounts: client.isupport.monitor() > 0 && client.isupport.whox(),
statusMsg: client.isupport.statusMsg(),
2021-12-07 07:37:14 -05:00
bouncerNetID: client.isupport.bouncerNetID(),
};
}),
};
case "CAP":
return updateServer({
supportsSASLPlain: client.supportsSASL("PLAIN"),
2021-12-10 09:34:51 -05:00
supportsAccountRegistration: client.caps.enabled.has("draft/account-registration"),
isBouncer: client.caps.enabled.has("soju.im/bouncer-networks"),
});
2021-11-21 06:13:44 -05:00
case irc.RPL_LOGGEDIN:
return updateServer({ account: msg.params[2] });
case irc.RPL_LOGGEDOUT:
return updateServer({ account: null });
case "REGISTER":
case "VERIFY":
if (msg.params[0] === "SUCCESS") {
return updateServer({ account: msg.params[1] });
}
break;
case irc.RPL_NOTOPIC:
2021-06-10 12:11:11 -04:00
channel = msg.params[1];
return updateBuffer(channel, { topic: null });
case irc.RPL_TOPIC:
2021-06-10 12:11:11 -04:00
channel = msg.params[1];
topic = msg.params[2];
return updateBuffer(channel, { topic });
case irc.RPL_TOPICWHOTIME:
// Ignore
break;
case irc.RPL_ENDOFNAMES:
channel = msg.params[1];
return updateBuffer(channel, (buf) => {
let members = new irc.CaseMapMap(null, buf.members.caseMap);
msg.list.forEach((namreply) => {
let membersList = namreply.params[3].split(" ");
membersList.forEach((s) => {
let member = irc.parseTargetPrefix(s);
members.set(member.name, member.prefix);
});
});
return { members };
});
case irc.RPL_ENDOFWHO:
2021-06-11 06:44:14 -04:00
target = msg.params[1];
2024-10-13 18:56:18 -04:00
if (msg.list.length === 0 && !client.isChannel(target) && target.indexOf("*") < 0) {
// Not a channel nor a mask, likely a nick
return updateUser(target, (user) => {
return { offline: true };
});
2023-04-19 07:04:58 -04:00
} else {
return updateServer((server) => {
let users = new irc.CaseMapMap(server.users);
for (let reply of msg.list) {
let who = client.parseWhoReply(reply);
if (who.flags !== undefined) {
who.away = who.flags.indexOf("G") >= 0; // H for here, G for gone
who.operator = who.flags.indexOf("*") >= 0;
let botFlag = client.isupport.bot();
if (botFlag) {
who.bot = who.flags.indexOf(botFlag) >= 0;
}
delete who.flags;
}
who.offline = false;
users.set(who.nick, who);
}
return { users };
});
}
case "JOIN":
2021-06-10 12:11:11 -04:00
channel = msg.params[0];
if (client.isMyNick(msg.prefix.name)) {
let [_id, update] = State.createBuffer(state, channel, serverID, client);
state = { ...state, ...update };
}
update = updateBuffer(channel, (buf) => {
2021-06-10 12:11:11 -04:00
let members = new irc.CaseMapMap(buf.members);
members.set(msg.prefix.name, "");
let joined = buf.joined || client.isMyNick(msg.prefix.name);
return { members, joined };
});
2021-09-21 08:12:07 -04:00
state = { ...state, ...update };
who = { nick: msg.prefix.name, offline: false };
if (msg.prefix.user) {
who.username = msg.prefix.user;
}
if (msg.prefix.host) {
who.hostname = msg.prefix.host;
}
2021-09-21 08:29:31 -04:00
if (msg.params.length > 2) {
who.account = msg.params[1];
if (who.account === "*") {
who.account = null;
}
who.realname = msg.params[2];
}
2021-09-21 08:12:07 -04:00
update = updateUser(msg.prefix.name, who);
state = { ...state, ...update };
return state;
case "PART":
2021-06-10 12:11:11 -04:00
channel = msg.params[0];
return updateBuffer(channel, (buf) => {
2021-06-10 12:11:11 -04:00
let members = new irc.CaseMapMap(buf.members);
members.delete(msg.prefix.name);
let joined = buf.joined && !client.isMyNick(msg.prefix.name);
return { members, joined };
});
case "KICK":
2021-06-10 12:11:11 -04:00
channel = msg.params[0];
let nick = msg.params[1];
return updateBuffer(channel, (buf) => {
2021-06-10 12:11:11 -04:00
let members = new irc.CaseMapMap(buf.members);
members.delete(nick);
2021-12-04 10:52:38 -05:00
let joined = buf.joined && !client.isMyNick(nick);
return { members, joined };
});
2021-09-21 08:00:52 -04:00
case "QUIT":
buffers = new Map(state.buffers);
state.buffers.forEach((buf) => {
2024-10-13 18:56:18 -04:00
if (buf.server !== serverID) {
return;
}
if (!buf.members.has(msg.prefix.name)) {
return;
}
let members = new irc.CaseMapMap(buf.members);
members.delete(msg.prefix.name);
buffers.set(buf.id, { ...buf, members });
});
state = { ...state, buffers };
update = updateUser(msg.prefix.name, (user) => {
2021-09-21 08:00:52 -04:00
if (!user) {
return;
}
return { offline: true };
});
state = { ...state, ...update };
return state;
2021-09-21 08:00:52 -04:00
case "NICK":
let newNick = msg.params[0];
buffers = new Map(state.buffers);
state.buffers.forEach((buf) => {
2024-10-13 18:56:18 -04:00
if (buf.server !== serverID) {
return;
}
if (!buf.members.has(msg.prefix.name)) {
return;
}
let members = new irc.CaseMapMap(buf.members);
members.set(newNick, members.get(msg.prefix.name));
members.delete(msg.prefix.name);
buffers.set(buf.id, { ...buf, members });
});
state = { ...state, buffers };
update = updateServer((server) => {
2021-09-21 08:00:52 -04:00
let users = new irc.CaseMapMap(server.users);
let user = users.get(msg.prefix.name);
if (!user) {
return;
}
users.set(newNick, user);
users.delete(msg.prefix.name);
return { users };
});
state = { ...state, ...update };
return state;
case "SETNAME":
return updateUser(msg.prefix.name, { realname: msg.params[0] });
case "CHGHOST":
return updateUser(msg.prefix.name, {
username: msg.params[0],
hostname: msg.params[1],
});
2021-09-21 08:49:32 -04:00
case "ACCOUNT":
let account = msg.params[0];
if (account === "*") {
account = null;
}
return updateUser(msg.prefix.name, { account });
case "AWAY":
2021-06-10 12:11:11 -04:00
let awayMessage = msg.params[0];
2024-11-16 06:17:23 -05:00
return updateUser(msg.prefix.name, { away: Boolean(awayMessage) });
2021-06-04 12:57:02 -04:00
case "TOPIC":
2021-06-10 12:11:11 -04:00
channel = msg.params[0];
topic = msg.params[1];
2021-06-04 12:57:02 -04:00
return updateBuffer(channel, { topic });
2021-06-11 06:44:14 -04:00
case "MODE":
target = msg.params[0];
if (!client.isChannel(target)) {
return; // TODO: handle user mode changes too
}
let prefix = client.isupport.prefix();
2021-06-11 06:44:14 -04:00
let prefixByMode = new Map(irc.parseMembershipModes(prefix).map((membership) => {
return [membership.mode, membership.prefix];
}));
return updateBuffer(target, (buf) => {
let members = new irc.CaseMapMap(buf.members);
irc.forEachChannelModeUpdate(msg, client.isupport, (mode, add, arg) => {
2021-06-11 06:44:14 -04:00
if (prefixByMode.has(mode)) {
let nick = arg;
let membership = members.get(nick);
let letter = prefixByMode.get(mode);
members.set(nick, updateMembership(membership, letter, add, client));
}
});
2021-06-11 06:44:14 -04:00
return { members };
});
2021-08-24 06:53:46 -04:00
case irc.RPL_MONONLINE:
case irc.RPL_MONOFFLINE:
targets = msg.params[1].split(",");
for (let target of targets) {
let prefix = irc.parsePrefix(target);
2024-10-13 18:56:18 -04:00
let update = updateUser(prefix.name, { offline: msg.command === irc.RPL_MONOFFLINE });
2021-08-24 06:53:46 -04:00
state = { ...state, ...update };
}
return state;
}
},
2021-06-04 13:07:14 -04:00
addMessage(state, msg, bufID) {
lastMessageKey++;
msg.key = lastMessageKey;
2021-06-04 13:07:14 -04:00
return State.updateBuffer(state, bufID, (buf) => {
2021-06-10 12:11:11 -04:00
let messages = insertMessage(buf.messages, msg);
2021-06-04 13:07:14 -04:00
return { messages };
});
},
2021-06-04 12:03:03 -04:00
};