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, created_at?: string, subscriber_count?: number, post_count?: number }; 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, csrf_token?: string, color_theme?: number }; // 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(); export class Backend { static ENDPOINT_BASE = '/v1'; constructor () { } async fetch(url: string, options?: RequestInit) { return await fetch(`${Backend.ENDPOINT_BASE}/${url.replace(/^\/+/, '')}`, options); } withEvent(event: any) { return new EventBackend(event); } } class EventBackend extends Backend { event: any | null; constructor (event: any | null) { super(); this.event = event; } async oath (): Promise { 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); } return new CsrfEventBackend(this.event, csrfToken); } async fetch(url: string, options?: RequestInit): Promise { console.debug(`fetch ${Backend.ENDPOINT_BASE}/${url.replace(/^\/+/, '')}`); return await (this.event?.fetch||fetch)(`${Backend.ENDPOINT_BASE}/${url.replace(/^\/+/, '')}`, options); } } class CsrfEventBackend extends EventBackend { csrfToken: string | null; constructor(event: EventBackend | null, csrfToken: string | null) { 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', 'x-csrftoken': this.csrfToken || "" } }); } } export const backend = new Backend();