atagen

malice

public
Star0
Watch0
d67da9aec471fb9247bdddcbce01c951d6ae9854
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/src/lib/link-previews.ts

ecb8598b38afatagen2026-10-22 09:19 UTC1
import { get, writable } from 'svelte/store';
ecb8598b38afatagen2026-10-22 09:19 UTC2
import { ApiError, getAccountData, getUrlPreview, setAccountData } from './api';
ecb8598b38afatagen2026-10-22 09:19 UTC3
import { parseLinkPreview } from './types';
ecb8598b38afatagen2026-10-22 09:19 UTC4
import type { LinkPreview } from './types';
ecb8598b38afatagen2026-10-22 09:19 UTC5
ecb8598b38afatagen2026-10-22 09:19 UTC6
const EVENT_TYPE = 'dev.malice.url_previews';
ecb8598b38afatagen2026-10-22 09:19 UTC7
const BACKOFF_MS = 30_000;
85334efe0e3aatagen2026-10-27 05:55 UTC8
const BATCH_MS = 150;
ecb8598b38afatagen2026-10-22 09:19 UTC9
ecb8598b38afatagen2026-10-22 09:19 UTC10
export interface UrlPreviewSettings {
ecb8598b38afatagen2026-10-22 09:19 UTC11
  encrypted_rooms: boolean;
ecb8598b38afatagen2026-10-22 09:19 UTC12
}
ecb8598b38afatagen2026-10-22 09:19 UTC13
ecb8598b38afatagen2026-10-22 09:19 UTC14
const DEFAULT_SETTINGS: UrlPreviewSettings = { encrypted_rooms: false };
ecb8598b38afatagen2026-10-22 09:19 UTC15
ecb8598b38afatagen2026-10-22 09:19 UTC16
export const linkPreviews = writable<Record<string, LinkPreview | null>>({});
ecb8598b38afatagen2026-10-22 09:19 UTC17
export const urlPreviewSettings = writable<UrlPreviewSettings>(DEFAULT_SETTINGS);
ecb8598b38afatagen2026-10-22 09:19 UTC18
ecb8598b38afatagen2026-10-22 09:19 UTC19
const inFlight = new Set<string>();
85334efe0e3aatagen2026-10-27 05:55 UTC20
const landed: Record<string, LinkPreview | null> = {};
85334efe0e3aatagen2026-10-27 05:55 UTC21
let flushTimer: ReturnType<typeof setTimeout> | null = null;
85334efe0e3aatagen2026-10-27 05:55 UTC22
85334efe0e3aatagen2026-10-27 05:55 UTC23
/** Previews resolve one by one after a room opens, and each one landing on its own shifted the timeline again. */
85334efe0e3aatagen2026-10-27 05:55 UTC24
function land(url: string, preview: LinkPreview | null): void {
85334efe0e3aatagen2026-10-27 05:55 UTC25
  landed[url] = preview;
85334efe0e3aatagen2026-10-27 05:55 UTC26
  flushTimer ??= setTimeout(() => {
85334efe0e3aatagen2026-10-27 05:55 UTC27
    flushTimer = null;
85334efe0e3aatagen2026-10-27 05:55 UTC28
    const batch = { ...landed };
85334efe0e3aatagen2026-10-27 05:55 UTC29
    for (const key of Object.keys(landed)) delete landed[key];
85334efe0e3aatagen2026-10-27 05:55 UTC30
    linkPreviews.update((all) => ({ ...all, ...batch }));
85334efe0e3aatagen2026-10-27 05:55 UTC31
  }, BATCH_MS);
85334efe0e3aatagen2026-10-27 05:55 UTC32
}
ecb8598b38afatagen2026-10-22 09:19 UTC33
let backoffUntil = 0;
719f7cdd974eatagen2026-10-26 10:15 UTC34
let refusalLogged = false;
ecb8598b38afatagen2026-10-22 09:19 UTC35
ecb8598b38afatagen2026-10-22 09:19 UTC36
export function requestLinkPreview(roomId: string, url: string): void {
85334efe0e3aatagen2026-10-27 05:55 UTC37
  if (inFlight.has(url) || url in landed || url in get(linkPreviews) || Date.now() < backoffUntil) return;
ecb8598b38afatagen2026-10-22 09:19 UTC38
  inFlight.add(url);
ecb8598b38afatagen2026-10-22 09:19 UTC39
  getUrlPreview(roomId, url)
85334efe0e3aatagen2026-10-27 05:55 UTC40
    .then((raw) => land(url, parseLinkPreview(raw)))
ecb8598b38afatagen2026-10-22 09:19 UTC41
    .catch((e) => {
ecb8598b38afatagen2026-10-22 09:19 UTC42
      if (e instanceof ApiError && e.status === 429) backoffUntil = Date.now() + BACKOFF_MS;
719f7cdd974eatagen2026-10-26 10:15 UTC43
      else if (e instanceof ApiError && e.status === 403) {
85334efe0e3aatagen2026-10-27 05:55 UTC44
        land(url, null);
719f7cdd974eatagen2026-10-26 10:15 UTC45
        if (!refusalLogged) console.warn('The homeserver refused a link preview', e.message);
719f7cdd974eatagen2026-10-26 10:15 UTC46
        refusalLogged = true;
719f7cdd974eatagen2026-10-26 10:15 UTC47
      }
ecb8598b38afatagen2026-10-22 09:19 UTC48
    })
ecb8598b38afatagen2026-10-22 09:19 UTC49
    .finally(() => inFlight.delete(url));
ecb8598b38afatagen2026-10-22 09:19 UTC50
}
ecb8598b38afatagen2026-10-22 09:19 UTC51
ecb8598b38afatagen2026-10-22 09:19 UTC52
function parseSettings(raw: unknown): UrlPreviewSettings {
ecb8598b38afatagen2026-10-22 09:19 UTC53
  if (typeof raw !== 'object' || raw === null) return DEFAULT_SETTINGS;
ecb8598b38afatagen2026-10-22 09:19 UTC54
  return { encrypted_rooms: (raw as Record<string, unknown>)['encrypted_rooms'] === true };
ecb8598b38afatagen2026-10-22 09:19 UTC55
}
ecb8598b38afatagen2026-10-22 09:19 UTC56
ecb8598b38afatagen2026-10-22 09:19 UTC57
export async function loadUrlPreviewSettings(): Promise<void> {
ecb8598b38afatagen2026-10-22 09:19 UTC58
  try {
ecb8598b38afatagen2026-10-22 09:19 UTC59
    urlPreviewSettings.set(parseSettings(await getAccountData(EVENT_TYPE)));
ecb8598b38afatagen2026-10-22 09:19 UTC60
  } catch {
ecb8598b38afatagen2026-10-22 09:19 UTC61
    urlPreviewSettings.set(DEFAULT_SETTINGS);
ecb8598b38afatagen2026-10-22 09:19 UTC62
  }
ecb8598b38afatagen2026-10-22 09:19 UTC63
}
ecb8598b38afatagen2026-10-22 09:19 UTC64
ecb8598b38afatagen2026-10-22 09:19 UTC65
export async function saveUrlPreviewSettings(settings: UrlPreviewSettings): Promise<void> {
ecb8598b38afatagen2026-10-22 09:19 UTC66
  await setAccountData(EVENT_TYPE, { encrypted_rooms: settings.encrypted_rooms });
ecb8598b38afatagen2026-10-22 09:19 UTC67
  urlPreviewSettings.set(settings);
ecb8598b38afatagen2026-10-22 09:19 UTC68
}
ecb8598b38afatagen2026-10-22 09:19 UTC69
ecb8598b38afatagen2026-10-22 09:19 UTC70
export function resetLinkPreviews(): void {
ecb8598b38afatagen2026-10-22 09:19 UTC71
  linkPreviews.set({});
ecb8598b38afatagen2026-10-22 09:19 UTC72
  urlPreviewSettings.set(DEFAULT_SETTINGS);
ecb8598b38afatagen2026-10-22 09:19 UTC73
  inFlight.clear();
85334efe0e3aatagen2026-10-27 05:55 UTC74
  if (flushTimer) clearTimeout(flushTimer);
85334efe0e3aatagen2026-10-27 05:55 UTC75
  flushTimer = null;
85334efe0e3aatagen2026-10-27 05:55 UTC76
  for (const key of Object.keys(landed)) delete landed[key];
ecb8598b38afatagen2026-10-22 09:19 UTC77
  backoffUntil = 0;
ecb8598b38afatagen2026-10-22 09:19 UTC78
}