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-07 06:46:38 -04:00
|
|
|
autojoin: [],
|
2020-04-24 13:01:02 -04:00
|
|
|
};
|
|
|
|
|
2020-06-05 17:35:33 -04:00
|
|
|
var ws = null;
|
|
|
|
|
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
|
|
|
|
2020-04-25 06:48:50 -04:00
|
|
|
var bufferListElt = document.querySelector("#buffer-list");
|
2020-04-25 06:51:35 -04:00
|
|
|
var bufferElt = document.querySelector("#buffer");
|
2020-04-25 06:48:50 -04:00
|
|
|
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();
|
|
|
|
|
|
|
|
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;
|
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);
|
|
|
|
|
2020-04-25 04:32:29 -04:00
|
|
|
var buf = {
|
2020-04-24 13:01:02 -04:00
|
|
|
name: name,
|
|
|
|
li: li,
|
|
|
|
messages: [],
|
|
|
|
readOnly: false,
|
|
|
|
|
|
|
|
addMessage: function(msg) {
|
|
|
|
buf.messages.push(msg);
|
|
|
|
|
2020-04-25 04:32:29 -04:00
|
|
|
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
|
|
|
|
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
|
|
|
|
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:
|
|
|
|
console.log("Registration complete");
|
|
|
|
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-06 04:43:28 -04:00
|
|
|
case ERR_PASSWDMISMATCH:
|
|
|
|
console.error("Password mismatch");
|
|
|
|
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);
|
|
|
|
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];
|
2020-04-25 17:00:49 -04:00
|
|
|
createBuffer(channel).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;
|
|
|
|
}
|
|
|
|
// TODO: append message to all buffers the user is a member of
|
|
|
|
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-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-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.pass = connectFormElt.elements.password.value;
|
|
|
|
server.username = connectFormElt.elements.username.value || server.nick;
|
|
|
|
server.realname = connectFormElt.elements.realname.value || server.nick;
|
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
2020-06-06 04:19:44 -04:00
|
|
|
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
|
|
|
};
|
2020-06-06 04:19:44 -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;
|
|
|
|
connectFormElt.elements.password.value = server.pass;
|
|
|
|
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();
|
|
|
|
if (params.server) {
|
|
|
|
connectFormElt.elements.url.value = params.server;
|
|
|
|
document.querySelector("#connect-url-container").style.display = "none";
|
|
|
|
}
|
2020-06-07 06:46:38 -04:00
|
|
|
if (params.channels) {
|
|
|
|
connectFormElt.elements.autojoin.value = params.channels;
|
|
|
|
}
|
2020-06-06 04:19:44 -04:00
|
|
|
}
|