1
0
Fork 0
forked from devs/v5shoutbox

service worker setup

This commit is contained in:
Lephenixnoir 2023-07-11 23:47:26 +02:00
parent ca3730b64e
commit f8e765018e
Signed by untrusted user: Lephenixnoir
GPG key ID: 1BBA026E13FC0495
3 changed files with 155 additions and 63 deletions

View file

@ -1,69 +1,95 @@
/*
# Planète Casio v5 shoutbox
This script contains all of the client-side logic for the Planète Casio
shoutbox. Unlike the legacy v4 shoutbox which was a poll-based HTTP service,
this shoutbox is run by an IRC server and this is mostly an IRC client.
This script contains all of the interface logic for the Planète Casio shoutbox.
It mostly sets up a service worker to use the IRC client and then manages the
DOM.
Support for IRC features is fairly limited as the shoutbox has never been a
very fancy chat. For full functionality, use a standard IRC client.
The shoutbox as a whole is divided into three modules:
- v5shoutbox.js (this file), handling the interface and coordinating pieces.
- v5shoutbox_irc.js, which implements the IRC client.
- v5shoutbox_worker.js, which is the service worker running the IRC client.
The one unusual feature in this program is the interaction with the Planète
Casio website. This solves an issue of permissions that is difficult to manage
with pure IRC. The expectation for the shoutbox is that:
- Anyone can read but only community members can write;
- It should load immediately when embedded into website pages and allow logged
in users to read and write messages with no further interactions.
## The service worker
In the v4's cursed design, the shoutbox was part of the website API, so users
logged into their Planète Casio account would be recognized by the shoutbox
server (which is just the website) and authenticated by their session cookie.
This doesn't work with the IRC server, since the connections are very different
and cookies are not a reliable method of authentication. With a standard IRC
client, users would need to type in their password to connect to the chat
despite already being logged in to the website.
One key aspect of the shoutbox is that it tries to run the IRC client in a
_service worker_, which is a web worker than runs in the browser's background
and can interact with multiple pages on the same site [1]. The purpose of the
worker is to share a single connection to the Planète Casio IRC server between
all open tabs, avoiding the redundancy of updating each shoutbox separately via
different connections.
The chosen solution is as follows:
- Every embedded shoutbox connects to the IRC server as a read-only guest;
- Sending a message is done via a website request. The website identifies
logged in users by their session cookies and forwards the messages to the
IRC server locally;
- Messages from the website to the IRC server are attributed to their original
authors by whichever nick/plugin trick works best.
Because service workers remain active in the background and can be used by
multiple pages at once, loading and updating them takes a bit of effort. This
results in the complex but well-designed _service worker life cycle_ which is
explained very nicely in [2]. Here, we skip the waiting phase by having all
versions of the worker claim active shoutboxes because interactions between the
shoutbox and the worker are limited in scope and it's reasonably easy to handle
the switch.
Hence, this client supports different connection modes depending on the
authentication setup: normal IRC (with password), PCv4, PCv5.
(WIP at time of writing)
[1] https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers
[2] https://web.dev/service-worker-lifecycle/
*/
"use strict";
import * as IRC from "./v5shoutbox_irc.js";
const registerServiceWorker = async () => {
if(!("serviceWorker" in navigator)) {
// TODO: Use previous, local IRC client
console.error("Service workers not supported, shoutbox not available.");
return;
/* IRC-backed chat running in a shared context (service worker) if possible,
with a default to a local client. */
class SharedChat {
/* Registers the service worker that runs the IRC client. Waits for
registration to complete and returns true on success, false on error.
If successful, sets up the onControllerChanged callback. */
async registerServiceWorker() {
if(!("serviceWorker" in navigator)) {
console.warn("No service workers, shoutbox will use per-tab IRC client");
return false;
}
/* Call the update method once if the worker was already there */
if(navigator.serviceWorker.controller !== null)
this.onControllerChanged();
/* Then call it after first install and every update */
navigator.serviceWorker.addEventListener("controllerchange", () => {
this.onControllerChanged();
});
return navigator.serviceWorker.register(
"v5shoutbox_worker.js", { scope: "./" })
.then((registration) => {
console.log(registration);
return true;
})
.catch((error) => {
console.error(`Service worker registration failed with ${error}`);
return false;
});
};
/* Handler called when the page is taken over by a service worker. Usually
the worker manages the page immediately when it starts loading, but this
also happens after the first install and during live worker updates. */
onControllerChanged() {
console.log("onControllerChanged!");
console.log(navigator.serviceWorker);
}
try {
const registration = await navigator.serviceWorker.register(
'v5shoutbox_worker.js', { scope: "./" });
async init() {
const ok = await this.registerServiceWorker(() => {
});
console.log("SharedChat service worker registration:", ok);
if (registration.installing)
console.log('Service worker installing');
else if (registration.waiting)
console.log('Service worker installed');
else if (registration.active)
console.log('Service worker active');
console.log(registration);
} catch (error) {
console.error(`Registration failed with ${error}`);
// if(ok)
// this.irc = null;
// else
// this.irc = new IRC.Client("wss://irc.planet-casio.com:443", undefined);
}
};
}
registerServiceWorker();
let sc = new SharedChat();
sc.init();
let irc = new IRC.Client("wss://irc.planet-casio.com:443", undefined);

View file

@ -1,6 +1,53 @@
/* WebSocket IRC with HTTP-based website interface. */
/*
# WebSocket IRC with HTTP-based website interface
class Message {
This script is the v5 shoutbox IRC client. Unlike the legacy v4 shoutbox which
was a poll-based HTTP service, this shoutbox is run by an IRC server, hence the
shoutbox is powered by this IRC client.
Support for IRC features is fairly limited as the shoutbox has never been a
very fancy chat. For full functionality, use a standard IRC client.
## Posting via HTTP to use the Planète Casio session cookie
The one unusual feature in this program is the interaction with the Planète
Casio website. This solves an issue of permissions that is difficult to manage
with pure IRC. The expectation for the shoutbox is that:
- Anyone can read but only community members can write;
- It should load immediately when embedded into website pages and allow logged
in users to read and write messages with no further interactions.
In the v4's cursed design, the shoutbox was part of the website API, so users
logged into their Planète Casio account would be recognized by the shoutbox
server (which is just the website) and authenticated by their session cookie.
This doesn't work with the IRC server, since the connections are very different
and cookies are not a reliable method of authentication. With a standard IRC
client, users would need to type in their password to connect to the chat
despite already being logged in to the website.
The chosen solution is as follows:
- Every embedded shoutbox connects to the IRC server as a read-only guest;
- Sending a message is done via a website request. The website identifies
logged in users by their session cookies and forwards the messages to the
IRC server locally;
- Messages from the website to the IRC server are attributed to their original
authors by whichever nick/plugin trick works best.
(WIP at time of writing)
## Message protocol
This IRC client usually runs inside of a service worker to that it can be
shared between any number of open Planète Casio tabs. Thus, communication
between this client and the user interface uses a message-based interface,
which is intended to work like RPC.
*/
"use strict";
class IRCMessage {
/* Regex for parsing messages */
static #rMessage = (function(){
const rTags = /(?:@([^ ]+) +)?/;
@ -13,7 +60,7 @@ class Message {
})();
constructor(text) {
const matches = Message.#rMessage.exec(text);
const matches = IRCMessage.#rMessage.exec(text);
if(matches === undefined)
return;
@ -142,12 +189,12 @@ export class Client {
ws.onmessage = function(net) {
this.log("[<] " + net.data);
let msg = new Message(net.data);
let msg = new IRCMessage(net.data);
if(msg.command === undefined) {
this.log("[v5shoutbox] invalid message");
return;
}
this.processMessage(msg);
this.processIRCMessage(msg);
}.bind(this);
ws.onclose = function() {
@ -180,7 +227,7 @@ export class Client {
this.send("USER", [this.username, "*", "0", this.username]);
}
processMessage(msg) {
processIRCMessage(msg) {
if(msg.command === "PING") {
this.send("PONG", msg.args);
return;

View file

@ -1,15 +1,34 @@
self.addEventListener("activate", (e) => {
console.log("[worker] activating!");
"use strict";
function log(...args) {
const time = new Date().toISOString();
console.log(`[${time}]`, ...args);
}
/* Activate eagerly as soon as the worker gets updated. Normally, when a worker
is updated, the previous version still gets to control new pages until all
of its controlled pages are closed simultaneously. We skip this, so new
pages will get the latest shoutbox service worker as soon as possible. */
self.addEventListener("install", (e) => {
log("installing!");
/* This promise is guarantee OK to ignore by API */
self.skipWaiting();
});
self.addEventListener("install", (e) => {
console.log("[worker] installing!");
/* As soon as the worker activates, we claim existing clients. This means that
not only new pages will use the updated worker, but running ones will also
transition to it, giving us transparent background updates. We handle
protocol differences manually; if the main script cannot handle the new
shoutbox version it will simply ask for a reload. Doing this allows us to
take control of the shoutbox without a page refresh when it is loaded for
the first time. */
self.addEventListener("activate", (e) => {
log("activating!");
e.waitUntil(self.clients.claim());
});
self.addEventListener("message", (e) => {
console.log("[worker] message!");
console.log(e);
console.log(self);
// if(e.data[0] == "!")
// postMessage(e.data.substring(1));
log(e);
if(e.data[0] == "!")
postMessage(e.data.substring(1));
});