gamja/assets/client.js

646 lines
15 KiB
JavaScript
Raw Normal View History

2020-04-24 13:01:02 -04:00
var server = {
2020-06-07 07:07:43 -04:00
name: "server",
2020-06-05 17:35:33 -04:00
username: null,
realname: null,
nick: null,
pass: null,
2020-06-12 12:17:49 -04:00
saslPlain: null,
2020-06-07 06:46:38 -04:00
autojoin: [],
2020-04-24 13:01:02 -04:00
};
2020-06-12 13:12:17 -04:00
// Static list of capabilities that are always requested when supported by the
// server
const permanentCaps = ["message-tags", "server-time"];
2020-06-05 17:35:33 -04:00
var ws = null;
2020-06-12 12:17:49 -04:00
var registered = false;
var availableCaps = {};
2020-06-12 12:17:49 -04:00
var enabledCaps = {};
2020-06-05 17:35:33 -04:00
2020-04-24 13:01:02 -04:00
var buffers = {};
var activeBuffer = null;
2020-06-05 17:35:33 -04:00
var serverBuffer = null;
2020-04-24 13:01:02 -04:00
var bufferListElt = document.querySelector("#buffer-list");
2020-04-25 06:51:35 -04:00
var bufferElt = document.querySelector("#buffer");
var composerElt = document.querySelector("#composer");
var composerInputElt = document.querySelector("#composer input");
2020-06-05 17:35:33 -04:00
var connectElt = document.querySelector("#connect");
var connectFormElt = document.querySelector("#connect form");
2020-04-24 13:01:02 -04:00
2020-04-25 04:28:23 -04:00
function djb2(s) {
var hash = 5381;
for (var i = 0; i < s.length; i++) {
hash = (hash << 5) + hash + s.charCodeAt(i);
hash = hash >>> 0; // convert to uint32
}
return hash;
}
2020-04-25 17:00:49 -04:00
function createNickElement(name) {
var nick = document.createElement("a");
nick.href = "#";
nick.className = "nick nick-" + (djb2(name) % 16 + 1);
nick.innerText = name;
nick.onclick = function(event) {
event.preventDefault();
switchBuffer(createBuffer(name));
};
return nick;
}
2020-04-24 13:01:02 -04:00
function createMessageElement(msg) {
var date = new Date();
2020-06-12 13:12:17 -04:00
if (msg.tags["time"]) {
date = new Date(msg.tags["time"]);
}
2020-04-24 13:01:02 -04:00
var line = document.createElement("div");
line.className = "logline";
var timestamp = document.createElement("a");
timestamp.href = "#";
timestamp.className = "timestamp";
timestamp.innerText = date.toLocaleTimeString(undefined, {
timeStyle: "short",
hour12: false,
});
timestamp.onclick = function(event) {
event.preventDefault();
};
line.appendChild(timestamp);
2020-04-25 14:20:39 -04:00
line.appendChild(document.createTextNode(" "));
2020-04-24 13:01:02 -04:00
switch (msg.command) {
case "NOTICE":
case "PRIVMSG":
var text = msg.params[1];
2020-04-25 14:20:39 -04:00
var actionPrefix = "\001ACTION ";
if (text.startsWith(actionPrefix) && text.endsWith("\001")) {
var action = text.slice(actionPrefix.length, -1);
line.className += " me-tell";
line.appendChild(document.createTextNode("* "));
2020-04-25 17:00:49 -04:00
line.appendChild(createNickElement(msg.prefix.name));
2020-04-25 14:20:39 -04:00
line.appendChild(document.createTextNode(" " + action));
} else {
line.className += " talk";
line.appendChild(document.createTextNode("<"));
2020-04-25 17:00:49 -04:00
line.appendChild(createNickElement(msg.prefix.name));
2020-04-25 14:20:39 -04:00
line.appendChild(document.createTextNode("> "));
line.appendChild(document.createTextNode(text));
}
2020-04-24 13:01:02 -04:00
break;
2020-04-25 17:00:49 -04:00
case "JOIN":
line.appendChild(createNickElement(msg.prefix.name));
line.appendChild(document.createTextNode(" has joined"));
break;
case "PART":
line.appendChild(createNickElement(msg.prefix.name));
line.appendChild(document.createTextNode(" has left"));
break;
case "NICK":
var newNick = msg.params[0];
line.appendChild(createNickElement(msg.prefix.name));
line.appendChild(document.createTextNode(" is now known as "));
line.appendChild(createNickElement(newNick));
break;
2020-06-10 13:24:03 -04:00
case "TOPIC":
line.appendChild(createNickElement(msg.prefix.name));
line.appendChild(document.createTextNode(" changed the topic to: " + msg.params[1]));
break;
2020-04-24 13:01:02 -04:00
default:
line.appendChild(document.createTextNode(" " + msg.command + " " + msg.params.join(" ")));
}
return line;
}
function createBuffer(name) {
if (buffers[name]) {
return buffers[name];
}
var a = document.createElement("a");
a.href = "#";
a.onclick = function(event) {
event.preventDefault();
switchBuffer(name);
};
a.innerText = name;
var li = document.createElement("li");
li.appendChild(a);
var buf = {
2020-04-24 13:01:02 -04:00
name: name,
li: li,
readOnly: false,
2020-06-10 13:24:03 -04:00
topic: null,
members: {},
2020-06-10 13:24:03 -04:00
messages: [],
2020-04-24 13:01:02 -04:00
addMessage: function(msg) {
buf.messages.push(msg);
if (activeBuffer === buf) {
2020-04-25 06:51:35 -04:00
bufferElt.appendChild(createMessageElement(msg));
2020-04-24 13:01:02 -04:00
}
},
};
buffers[name] = buf;
bufferListElt.appendChild(li);
return buf;
}
function switchBuffer(buf) {
if (typeof buf == "string") {
buf = buffers[buf];
}
if (activeBuffer && buf === activeBuffer) {
return;
}
if (activeBuffer) {
activeBuffer.li.classList.remove("active");
}
activeBuffer = buf;
if (!buf) {
return;
}
buf.li.classList.add("active");
2020-04-25 06:51:35 -04:00
bufferElt.innerHTML = "";
2020-04-24 13:01:02 -04:00
for (var msg of buf.messages) {
2020-04-25 06:51:35 -04:00
bufferElt.appendChild(createMessageElement(msg));
2020-04-24 13:01:02 -04:00
}
composerElt.classList.toggle("read-only", buf.readOnly);
if (!buf.readOnly) {
composerInputElt.focus();
}
}
2020-06-05 17:35:33 -04:00
function showConnectForm() {
setConnectFormDisabled(false);
connectElt.style.display = "block";
}
2020-04-24 13:01:02 -04:00
function addAvailableCaps(s) {
var l = s.split(" ");
l.forEach(function(s) {
var parts = s.split("=");
var k = parts[0];
2020-06-12 12:17:49 -04:00
var v = "";
if (parts.length > 1) {
v = parts[1];
}
availableCaps[k] = v;
});
}
function handleCap(msg) {
var subCmd = msg.params[1];
var args = msg.params.slice(2);
switch (subCmd) {
case "LS":
addAvailableCaps(args[args.length - 1]);
if (args[0] != "*") {
console.log("Available server caps:", availableCaps);
2020-06-12 12:17:49 -04:00
var reqCaps = [];
var saslCap = availableCaps["sasl"];
var supportsSaslPlain = (saslCap !== undefined);
if (saslCap.length > 0) {
supportsSaslPlain = saslCap.split(",").includes("PLAIN");
}
var capEnd = true;
if (server.saslPlain && supportsSaslPlain) {
// CAP END is deferred after authentication finishes
reqCaps.push("sasl");
capEnd = false;
}
2020-06-12 13:12:17 -04:00
permanentCaps.forEach(function(cap) {
if (availableCaps[cap] !== undefined) {
reqCaps.push(cap);
}
});
2020-06-12 12:56:08 -04:00
2020-06-12 12:17:49 -04:00
if (reqCaps.length > 0) {
2020-06-12 12:56:08 -04:00
sendMessage({ command: "CAP", params: ["REQ", reqCaps.join(" ")] });
2020-06-12 12:17:49 -04:00
}
if (!registered && capEnd) {
sendMessage({ command: "CAP", params: ["END"] });
}
}
break;
case "NEW":
addAvailableCaps(args[0]);
console.log("Server added available caps:", args[0]);
break;
case "DEL":
2020-06-12 12:17:49 -04:00
args[0].split(" ").forEach(function(cap) {
delete availableCaps[cap];
delete enabledCaps[cap];
});
console.log("Server removed available caps:", args[0]);
break;
2020-06-12 12:17:49 -04:00
case "ACK":
console.log("Server ack'ed caps:", args[0]);
args[0].split(" ").forEach(function(cap) {
enabledCaps[cap] = true;
if (cap == "sasl" && server.saslPlain) {
console.log("Starting SASL PLAIN authentication");
sendMessage({ command: "AUTHENTICATE", params: ["PLAIN"] });
}
});
break;
case "NAK":
console.log("Server nak'ed caps:", args[0]);
if (!registered) {
sendMessage({ command: "CAP", params: ["END"] });
}
break;
}
}
2020-06-12 12:17:49 -04:00
function handleAuthenticate(msg) {
var challengeStr = msg.params[0];
// For now only PLAIN is supported
if (challengeStr != "+") {
console.error("Expected an empty challenge, got:", challengeStr);
sendMessage({ command: "AUTHENTICATE", params: ["*"] });
return;
}
var respStr = btoa("\0" + server.saslPlain.username + "\0" + server.saslPlain.password);
sendMessage({ command: "AUTHENTICATE", params: [respStr] });
}
2020-06-05 17:35:33 -04:00
function connect() {
try {
ws = new WebSocket(server.url);
} catch (err) {
console.error(err);
showConnectForm();
return;
2020-04-24 13:01:02 -04:00
}
2020-06-05 17:35:33 -04:00
ws.onopen = function() {
console.log("Connection opened");
2020-04-24 13:01:02 -04:00
sendMessage({ command: "CAP", params: ["LS", "302"] });
2020-06-05 17:35:33 -04:00
if (server.pass) {
2020-06-07 06:46:38 -04:00
sendMessage({ command: "PASS", params: [server.pass] });
2020-04-25 08:59:20 -04:00
}
2020-06-07 06:46:38 -04:00
sendMessage({ command: "NICK", params: [server.nick] });
sendMessage({
2020-06-05 17:35:33 -04:00
command: "USER",
params: [server.username, "0", "*", server.realname],
2020-06-07 06:46:38 -04:00
});
2020-06-05 17:35:33 -04:00
};
ws.onmessage = function(event) {
var msg = parseMessage(event.data);
2020-06-07 06:48:48 -04:00
console.log("Received:", msg);
2020-06-05 17:35:33 -04:00
switch (msg.command) {
2020-06-06 04:06:07 -04:00
case RPL_WELCOME:
2020-06-12 12:17:49 -04:00
if (server.saslPlain && availableCaps["sasl"] === undefined) {
console.error("Server doesn't support SASL PLAIN");
disconnect();
return;
}
2020-06-06 04:06:07 -04:00
console.log("Registration complete");
2020-06-12 12:17:49 -04:00
registered = true;
2020-06-06 04:06:07 -04:00
connectElt.style.display = "none";
2020-06-07 06:46:38 -04:00
if (server.autojoin.length > 0) {
sendMessage({
command: "JOIN",
params: [server.autojoin.join(",")],
});
}
2020-06-06 04:06:07 -04:00
break;
2020-06-10 13:24:03 -04:00
case RPL_TOPIC:
var channel = msg.params[1];
var topic = msg.params[2];
var buf = buffers[channel];
if (!buf) {
break;
}
buf.topic = topic;
break;
case RPL_NAMREPLY:
var channel = msg.params[2];
var members = msg.params.slice(3);
var buf = buffers[channel];
if (!buf) {
break;
}
members.forEach(function(s) {
var member = parseMembership(s);
buf.members[member.nick] = member.prefix;
});
break;
case RPL_ENDOFNAMES:
break;
2020-06-06 04:43:28 -04:00
case ERR_PASSWDMISMATCH:
console.error("Password mismatch");
disconnect();
break;
case "CAP":
handleCap(msg);
break;
2020-06-12 12:17:49 -04:00
case "AUTHENTICATE":
handleAuthenticate(msg);
break;
case RPL_LOGGEDIN:
console.log("Logged in");
break;
case RPL_LOGGEDOUT:
console.log("Logged out");
break;
case RPL_SASLSUCCESS:
console.log("SASL authentication success");
if (!registered) {
sendMessage({ command: "CAP", params: ["END"] });
}
break;
case ERR_NICKLOCKED:
case ERR_SASLFAIL:
case ERR_SASLTOOLONG:
case ERR_SASLABORTED:
case ERR_SASLALREADY:
console.error("SASL error:", msg);
disconnect();
break;
2020-06-05 17:35:33 -04:00
case "NOTICE":
case "PRIVMSG":
var target = msg.params[0];
if (target == server.nick) {
target = msg.prefix.name;
}
var buf;
if (target == "*") {
buf = serverBuffer;
} else {
buf = createBuffer(target);
}
buf.addMessage(msg);
break;
case "JOIN":
var channel = msg.params[0];
2020-06-07 06:46:38 -04:00
var buf = createBuffer(channel);
buf.members[msg.prefix.name] = null;
2020-06-07 06:46:38 -04:00
if (msg.prefix.name != server.nick) {
buf.addMessage(msg);
}
if (channel == server.autojoin[0]) {
// TODO: only switch once right after connect
switchBuffer(buf);
2020-06-05 17:35:33 -04:00
}
break;
case "PART":
var channel = msg.params[0];
var buf = createBuffer(channel);
delete buf.members[msg.prefix.name];
buf.addMessage(msg);
2020-06-05 17:35:33 -04:00
break;
2020-06-06 04:02:22 -04:00
case "NICK":
var newNick = msg.params[0];
if (msg.prefix.name == server.nick) {
server.nick = newNick;
}
for (var name in buffers) {
var buf = buffers[name];
if (buf.members[msg.prefix.name] !== undefined) {
buf.members[newNick] = buf.members[msg.prefix.name];
delete buf.members[msg.prefix.name];
buf.addMessage(msg);
}
}
2020-06-06 04:02:22 -04:00
break;
2020-06-10 13:24:03 -04:00
case "TOPIC":
var channel = msg.params[0];
var topic = msg.params[1];
var buf = buffers[channel];
if (!buf) {
break;
}
buf.topic = topic;
buf.addMessage(msg);
break;
2020-06-05 17:35:33 -04:00
default:
serverBuffer.addMessage(msg);
2020-04-24 13:01:02 -04:00
}
2020-06-05 17:35:33 -04:00
};
ws.onclose = function() {
console.log("Connection closed");
showConnectForm();
};
ws.onerror = function() {
console.error("Connection error");
};
serverBuffer = createBuffer(server.name);
serverBuffer.readOnly = true;
switchBuffer(serverBuffer);
}
2020-06-06 04:43:28 -04:00
function disconnect() {
ws.close(1000);
2020-06-12 12:17:49 -04:00
registered = false;
2020-06-06 04:43:28 -04:00
}
2020-06-05 17:35:33 -04:00
function sendMessage(msg) {
ws.send(formatMessage(msg));
2020-06-07 06:48:48 -04:00
console.log("Sent:", msg);
2020-06-05 17:35:33 -04:00
}
function executeCommand(s) {
var parts = s.split(" ");
var cmd = parts[0].toLowerCase().slice(1);
var args = parts.slice(1);
switch (cmd) {
2020-06-06 04:43:39 -04:00
case "quit":
2020-06-06 04:58:32 -04:00
if (localStorage) {
localStorage.removeItem("server");
}
2020-06-06 04:43:39 -04:00
disconnect();
break;
2020-06-05 17:35:33 -04:00
case "join":
var channel = args[0];
2020-06-06 05:09:41 -04:00
if (!channel) {
console.error("Missing channel name");
return;
}
sendMessage({ command: "JOIN", params: [channel] });
break;
case "part":
// TODO: part reason
if (!activeBuffer || activeBuffer.readOnly) {
console.error("Not in a channel");
return;
}
var channel = activeBuffer.name;
sendMessage({ command: "PART", params: [channel] });
2020-04-25 17:00:49 -04:00
break;
2020-06-10 12:46:19 -04:00
case "msg":
var target = args[0];
var text = args.slice(1).join(" ");
sendMessage({ command: "PRIVMSG", params: [target, text] });
break;
case "nick":
var newNick = args[0];
sendMessage({ command: "NICK", params: [newNick] });
break;
2020-04-24 13:01:02 -04:00
default:
2020-06-05 17:35:33 -04:00
console.error("Unknwon command '" + cmd + "'");
2020-04-24 13:01:02 -04:00
}
2020-06-05 17:35:33 -04:00
}
2020-04-24 13:01:02 -04:00
composerElt.onsubmit = function(event) {
event.preventDefault();
2020-06-05 17:35:33 -04:00
2020-04-24 13:01:02 -04:00
var text = composerInputElt.value;
2020-06-05 17:35:33 -04:00
composerInputElt.value = "";
2020-04-25 04:33:52 -04:00
if (!text) {
return;
}
2020-06-05 17:35:33 -04:00
if (text.startsWith("//")) {
text = text.slice(1);
} else if (text.startsWith("/")) {
executeCommand(text);
return;
}
if (!activeBuffer || activeBuffer.readOnly) {
return;
}
var target = activeBuffer.name;
2020-04-24 13:01:02 -04:00
var msg = { command: "PRIVMSG", params: [target, text] };
2020-06-05 17:35:33 -04:00
sendMessage(msg);
2020-04-24 13:01:02 -04:00
msg.prefix = { name: server.nick };
activeBuffer.addMessage(msg);
2020-06-05 17:35:33 -04:00
};
function setConnectFormDisabled(disabled) {
connectElt.querySelectorAll("input, button").forEach(function(elt) {
elt.disabled = disabled;
});
}
2020-06-07 06:31:01 -04:00
function parseQueryString() {
var query = window.location.search.substring(1);
var params = {};
query.split('&').forEach(function(s) {
if (!s) {
return;
}
var pair = s.split('=');
params[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || "");
});
return params;
}
2020-06-05 17:35:33 -04:00
connectFormElt.onsubmit = function(event) {
event.preventDefault();
setConnectFormDisabled(true);
server.url = connectFormElt.elements.url.value;
server.nick = connectFormElt.elements.nick.value;
server.username = connectFormElt.elements.username.value || server.nick;
server.realname = connectFormElt.elements.realname.value || server.nick;
2020-06-12 12:19:59 -04:00
server.pass = connectFormElt.elements.pass.value;
2020-06-05 17:35:33 -04:00
2020-06-12 12:17:49 -04:00
server.saslPlain = null;
if (connectFormElt.elements.password.value) {
server.saslPlain = {
username: server.username,
password: connectFormElt.elements.password.value,
};
}
2020-06-07 06:46:38 -04:00
server.autojoin = [];
connectFormElt.elements.autojoin.value.split(",").forEach(function(ch) {
ch = ch.trim();
if (!ch) {
return;
}
server.autojoin.push(ch);
});
if (localStorage) {
if (connectFormElt.elements["remember-me"].checked) {
localStorage.setItem("server", JSON.stringify(server));
} else {
localStorage.removeItem("server");
}
}
2020-06-05 17:35:33 -04:00
connect();
};
window.onkeydown = function(event) {
if (activeBuffer && activeBuffer.readOnly && event.key == "/" && document.activeElement != composerInputElt) {
// Allow typing commands even in read-only buffers
composerElt.classList.remove("read-only");
composerInputElt.focus();
composerInputElt.value = "";
}
2020-04-24 13:01:02 -04:00
};
if (localStorage && localStorage.getItem("server")) {
server = JSON.parse(localStorage.getItem("server"));
connectFormElt.elements.url.value = server.url;
connectFormElt.elements.nick.value = server.nick;
if (server.username != server.nick) {
connectFormElt.elements.username.value = server.username;
}
if (server.realname != server.nick) {
connectFormElt.elements.realname.value = server.realname;
}
connectFormElt.elements["remember-me"].checked = true;
setConnectFormDisabled(true);
connect();
2020-06-07 06:31:01 -04:00
} else {
var params = parseQueryString();
2020-06-07 07:46:46 -04:00
2020-06-07 06:31:01 -04:00
if (params.server) {
2020-06-07 07:46:46 -04:00
connectFormElt.elements.url.value = params.server;
} else if (!connectFormElt.elements.url.value) {
var host = window.location.host || "localhost:8080";
var proto = "wss:";
if (window.location.protocol != "https:") {
proto = "ws:";
}
connectFormElt.elements.url.value = proto + "//" + host + "/socket";
2020-06-07 06:31:01 -04:00
}
2020-06-07 07:46:46 -04:00
2020-06-07 06:46:38 -04:00
if (params.channels) {
connectFormElt.elements.autojoin.value = params.channels;
}
}