vigil/src/lib/backend.ts

143 lines
3.2 KiB
TypeScript
Raw Normal View History

2025-09-12 19:20:30 +02:00
export type UserEntry = {
id: string,
username: string,
display_name?: string,
biography?: string,
joined_at?: string,
age: number, // actually not just that
badges?: string[]
type?: 'user',
karma?: number
};
export type GuildEntry = {
type?: 'guild',
id: string,
name: string,
display_name?: string,
description?: string,
2025-10-07 09:36:42 +02:00
created_at?: string,
subscriber_count?: number,
post_count?: number
2025-09-12 19:20:30 +02:00
};
export type PostEntry = {
id: string,
url: string,
title: string,
created_at: string,
author?: UserEntry | null,
content?: string | null,
to: UserEntry | GuildEntry
};
export type ServerHealth = {
version: string,
name: string,
post_count: number,
user_count: number,
me: string | null,
2025-10-18 11:28:20 +02:00
csrf_token?: string,
color_theme?: number
2025-09-12 19:20:30 +02:00
};
2025-10-23 03:09:31 +02:00
// caches an oath (CSRF token)
class OathKeeper {
lastOath : number;
csrfToken: string | null;
constructor () {
this.lastOath = 0;
this.csrfToken = null;
}
set (tok: string) {
this.lastOath = +new Date;
this.csrfToken = tok;
}
get () {
if (this.lastOath === 0 || +new Date() - this.lastOath > 1800000) {
// 30 minutes decay
return null;
}
return this.csrfToken;
}
}
const oathKeeper = new OathKeeper();
2025-09-12 19:20:30 +02:00
export class Backend {
static ENDPOINT_BASE = '/v1';
constructor () {
}
async fetch(url: string, options?: RequestInit) {
2025-09-17 14:00:59 +02:00
return await fetch(`${Backend.ENDPOINT_BASE}/${url.replace(/^\/+/, '')}`, options);
2025-09-12 19:20:30 +02:00
}
withEvent(event: any) {
return new EventBackend(event);
}
}
class EventBackend extends Backend {
2025-10-23 03:09:31 +02:00
event: any | null;
2025-09-12 19:20:30 +02:00
2025-10-23 03:09:31 +02:00
constructor (event: any | null) {
2025-09-12 19:20:30 +02:00
super();
this.event = event;
}
async oath (): Promise<CsrfEventBackend> {
2025-10-23 03:09:31 +02:00
let csrfToken = oathKeeper.get();
if (!csrfToken) {
const resp = await this.fetch("oath");
if (resp.status !== 200) {
throw new Error();
}
const respJ = await resp.json();
csrfToken = respJ.csrf_token;
}
if (!csrfToken) {
console.warn("csrf_token is null");
}
else {
oathKeeper.set(csrfToken);
2025-09-12 19:20:30 +02:00
}
return new CsrfEventBackend(this.event, csrfToken);
}
async fetch(url: string, options?: RequestInit): Promise<Response> {
2025-09-17 14:00:59 +02:00
console.debug(`fetch ${Backend.ENDPOINT_BASE}/${url.replace(/^\/+/, '')}`);
2025-10-23 03:09:31 +02:00
return await (this.event?.fetch||fetch)(`${Backend.ENDPOINT_BASE}/${url.replace(/^\/+/, '')}`, options);
2025-09-12 19:20:30 +02:00
}
}
class CsrfEventBackend extends EventBackend {
2025-10-23 03:09:31 +02:00
csrfToken: string | null;
2025-09-12 19:20:30 +02:00
2025-10-23 03:09:31 +02:00
constructor(event: EventBackend | null, csrfToken: string | null) {
2025-09-12 19:20:30 +02:00
super(event);
this.csrfToken = csrfToken;
}
async submitJson(url: string, data: object) {
return await this.fetch(url, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'content-type': 'application/json',
2025-10-23 03:09:31 +02:00
'x-csrftoken': this.csrfToken || ""
2025-09-12 19:20:30 +02:00
}
});
}
}
export const backend = new Backend();