gamja/lib/client.js

230 lines
5.4 KiB
JavaScript
Raw Normal View History

2020-06-14 08:50:59 -04:00
import * as irc from "./irc.js";
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", "multi-prefix"];
2020-06-12 13:12:17 -04:00
2020-06-23 14:00:49 -04:00
export default class Client extends EventTarget {
ws = null;
nick = null;
params = {
url: null,
2020-06-23 14:00:49 -04:00
username: null,
realname: null,
nick: null,
pass: null,
saslPlain: null,
2020-04-24 13:01:02 -04:00
};
2020-06-23 14:00:49 -04:00
registered = false;
availableCaps = {};
enabledCaps = {};
2020-04-24 13:01:02 -04:00
2020-06-23 14:00:49 -04:00
constructor(params) {
super();
2020-04-24 13:01:02 -04:00
2020-06-23 14:00:49 -04:00
this.params = Object.assign(this.params, params);
2020-04-24 13:01:02 -04:00
2020-06-23 14:00:49 -04:00
try {
this.ws = new WebSocket(params.url);
} catch (err) {
console.error("Failed to create connection:", err);
setTimeout(() => this.dispatchEvent(new CustomEvent("close")), 0);
return;
}
2020-06-12 12:17:49 -04:00
2020-06-23 14:00:49 -04:00
this.ws.addEventListener("open", this.handleOpen.bind(this));
this.ws.addEventListener("message", this.handleMessage.bind(this));
2020-06-12 12:17:49 -04:00
2020-06-23 14:00:49 -04:00
this.ws.addEventListener("close", () => {
console.log("Connection closed");
this.dispatchEvent(new CustomEvent("close"));
});
2020-06-12 12:17:49 -04:00
2020-06-23 14:00:49 -04:00
this.ws.addEventListener("error", () => {
console.error("Connection error");
2020-06-12 12:17:49 -04:00
});
}
2020-06-23 14:00:49 -04:00
handleOpen() {
2020-06-05 17:35:33 -04:00
console.log("Connection opened");
2020-04-24 13:01:02 -04:00
2020-06-23 14:00:49 -04:00
this.nick = this.params.nick;
this.send({ command: "CAP", params: ["LS", "302"] });
if (this.params.pass) {
this.send({ command: "PASS", params: [this.params.pass] });
2020-04-25 08:59:20 -04:00
}
2020-06-23 14:00:49 -04:00
this.send({ command: "NICK", params: [this.nick] });
this.send({
2020-06-05 17:35:33 -04:00
command: "USER",
2020-06-23 14:00:49 -04:00
params: [this.params.username, "0", "*", this.params.realname],
2020-06-07 06:46:38 -04:00
});
2020-06-23 14:00:49 -04:00
}
2020-06-05 17:35:33 -04:00
2020-06-23 14:00:49 -04:00
handleMessage(event) {
2020-06-14 08:50:59 -04:00
var msg = irc.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-14 08:50:59 -04:00
case irc.RPL_WELCOME:
2020-06-23 14:00:49 -04:00
if (this.params.saslPlain && this.availableCaps["sasl"] === undefined) {
2020-06-12 12:17:49 -04:00
console.error("Server doesn't support SASL PLAIN");
2020-06-23 14:00:49 -04:00
this.close();
2020-06-12 12:17:49 -04:00
return;
}
2020-06-06 04:06:07 -04:00
console.log("Registration complete");
2020-06-23 14:00:49 -04:00
this.registered = true;
break;
2020-06-14 08:50:59 -04:00
case irc.ERR_PASSWDMISMATCH:
2020-06-06 04:43:28 -04:00
console.error("Password mismatch");
2020-06-23 14:00:49 -04:00
this.close();
2020-06-06 04:43:28 -04:00
break;
case "CAP":
2020-06-23 14:00:49 -04:00
this.handleCap(msg);
break;
2020-06-12 12:17:49 -04:00
case "AUTHENTICATE":
2020-06-23 14:00:49 -04:00
this.handleAuthenticate(msg);
2020-06-12 12:17:49 -04:00
break;
2020-06-14 08:50:59 -04:00
case irc.RPL_LOGGEDIN:
2020-06-12 12:17:49 -04:00
console.log("Logged in");
break;
2020-06-14 08:50:59 -04:00
case irc.RPL_LOGGEDOUT:
2020-06-12 12:17:49 -04:00
console.log("Logged out");
break;
2020-06-14 08:50:59 -04:00
case irc.RPL_SASLSUCCESS:
2020-06-12 12:17:49 -04:00
console.log("SASL authentication success");
2020-06-23 14:00:49 -04:00
if (!this.registered) {
this.send({ command: "CAP", params: ["END"] });
2020-06-12 12:17:49 -04:00
}
break;
2020-06-14 08:50:59 -04:00
case irc.ERR_NICKLOCKED:
case irc.ERR_SASLFAIL:
case irc.ERR_SASLTOOLONG:
case irc.ERR_SASLABORTED:
case irc.ERR_SASLALREADY:
2020-06-12 12:17:49 -04:00
console.error("SASL error:", msg);
2020-06-23 14:00:49 -04:00
this.close();
2020-06-05 17:35:33 -04:00
break;
2020-06-06 04:02:22 -04:00
case "NICK":
var newNick = msg.params[0];
2020-06-23 14:00:49 -04:00
if (msg.prefix.name == this.nick) {
this.nick = newNick;
2020-06-10 13:24:03 -04:00
}
break;
2020-06-06 04:58:32 -04:00
}
2020-06-05 17:35:33 -04:00
2020-06-23 14:00:49 -04:00
this.dispatchEvent(new CustomEvent("message", {
detail: { message: msg },
}));
2020-04-25 04:33:52 -04:00
}
2020-06-05 17:35:33 -04:00
2020-06-23 14:00:49 -04:00
addAvailableCaps(s) {
var l = s.split(" ");
l.forEach((s) => {
var parts = s.split("=");
var k = parts[0];
var v = "";
if (parts.length > 1) {
v = parts[1];
}
this.availableCaps[k] = v;
});
2020-06-05 17:35:33 -04:00
}
2020-06-23 14:00:49 -04:00
handleCap(msg) {
var subCmd = msg.params[1];
var args = msg.params.slice(2);
switch (subCmd) {
case "LS":
this.addAvailableCaps(args[args.length - 1]);
if (args[0] != "*") {
console.log("Available server caps:", this.availableCaps);
2020-06-05 17:35:33 -04:00
2020-06-23 14:00:49 -04:00
var reqCaps = [];
2020-06-05 17:35:33 -04:00
2020-06-23 14:00:49 -04:00
var saslCap = this.availableCaps["sasl"];
var supportsSaslPlain = (saslCap !== undefined);
if (saslCap.length > 0) {
supportsSaslPlain = saslCap.split(",").includes("PLAIN");
}
2020-06-07 06:31:01 -04:00
2020-06-23 14:00:49 -04:00
var capEnd = true;
if (this.params.saslPlain && supportsSaslPlain) {
// CAP END is deferred after authentication finishes
reqCaps.push("sasl");
capEnd = false;
}
2020-06-05 17:35:33 -04:00
2020-06-23 14:00:49 -04:00
permanentCaps.forEach((cap) => {
if (this.availableCaps[cap] !== undefined) {
reqCaps.push(cap);
}
});
2020-06-05 17:35:33 -04:00
2020-06-23 14:00:49 -04:00
if (reqCaps.length > 0) {
this.send({ command: "CAP", params: ["REQ", reqCaps.join(" ")] });
}
2020-06-12 12:17:49 -04:00
2020-06-23 14:00:49 -04:00
if (!this.registered && capEnd) {
this.send({ command: "CAP", params: ["END"] });
}
}
break;
case "NEW":
this.addAvailableCaps(args[0]);
console.log("Server added available caps:", args[0]);
// TODO: request caps
break;
case "DEL":
args[0].split(" ").forEach((cap) => {
delete this.availableCaps[cap];
delete this.enabledCaps[cap];
});
console.log("Server removed available caps:", args[0]);
break;
case "ACK":
console.log("Server ack'ed caps:", args[0]);
args[0].split(" ").forEach((cap) => {
this.enabledCaps[cap] = true;
2020-06-07 06:46:38 -04:00
2020-06-23 14:00:49 -04:00
if (cap == "sasl" && this.params.saslPlain) {
console.log("Starting SASL PLAIN authentication");
this.send({ command: "AUTHENTICATE", params: ["PLAIN"] });
}
});
break;
case "NAK":
console.log("Server nak'ed caps:", args[0]);
if (!this.registered) {
this.send({ command: "CAP", params: ["END"] });
}
break;
}
}
2020-06-23 14:00:49 -04:00
handleAuthenticate(msg) {
var challengeStr = msg.params[0];
2020-06-05 17:35:33 -04:00
2020-06-23 14:00:49 -04:00
// For now only PLAIN is supported
if (challengeStr != "+") {
console.error("Expected an empty challenge, got:", challengeStr);
this.send({ command: "AUTHENTICATE", params: ["*"] });
return;
}
2020-06-23 14:00:49 -04:00
var respStr = btoa("\0" + this.params.saslPlain.username + "\0" + this.params.saslPlain.password);
this.send({ command: "AUTHENTICATE", params: [respStr] });
}
2020-06-07 07:46:46 -04:00
2020-06-23 14:00:49 -04:00
send(msg) {
this.ws.send(irc.formatMessage(msg));
console.log("Sent:", msg);
2020-06-07 06:31:01 -04:00
}
2020-06-07 07:46:46 -04:00
2020-06-23 14:00:49 -04:00
close() {
this.ws.close(1000);
this.registered = false;
2020-06-07 06:46:38 -04:00
}
}