atagen

malice

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

Blame: web/src/lib/mentions-model.ts

668ab5c30531atagen2026-10-25 03:13 UTC1
import { parseEventContext, parseTimelineItem } from './types';
668ab5c30531atagen2026-10-25 03:13 UTC2
import type { EventContext, TimelineItem } from './types';
668ab5c30531atagen2026-10-25 03:13 UTC3
668ab5c30531atagen2026-10-25 03:13 UTC4
export interface MentionKey { room_id: string; event_id: string }
a4555a7eae2datagen2026-10-25 09:18 UTC5
export type MentionReason = 'Direct' | 'Reply' | 'Room' | 'Keyword';
668ab5c30531atagen2026-10-25 03:13 UTC6
export type MentionCaptureState =
668ab5c30531atagen2026-10-25 03:13 UTC7
  | { kind: 'Pending' | 'Partial' | 'Ready' | 'Unavailable' }
668ab5c30531atagen2026-10-25 03:13 UTC8
  | { kind: 'Failed'; message: string; retry_at_ms: number };
668ab5c30531atagen2026-10-25 03:13 UTC9
export interface MentionEntry {
668ab5c30531atagen2026-10-25 03:13 UTC10
  key: MentionKey; timestamp_ms: number; discovered_at_ms: number; discovery_revision: number; revision: number;
668ab5c30531atagen2026-10-25 03:13 UTC11
  event: TimelineItem; reasons: MentionReason[]; read: boolean; capture: MentionCaptureState;
668ab5c30531atagen2026-10-25 03:13 UTC12
  room_name: string | null; notification_suppressed: boolean;
668ab5c30531atagen2026-10-25 03:13 UTC13
}
668ab5c30531atagen2026-10-25 03:13 UTC14
export interface MentionCoverage {
668ab5c30531atagen2026-10-25 03:13 UTC15
  earliest_timestamp_ms: number | null; historical_supported: boolean | null; recovering: boolean;
249e439c27e5atagen2026-10-25 13:26 UTC16
  gaps: MentionGap[]; truncated: boolean; unresolved_candidates: number;
668ab5c30531atagen2026-10-25 03:13 UTC17
}
668ab5c30531atagen2026-10-25 03:13 UTC18
export interface MentionGap { room_id: string | null; reason: string; token: string | null }
668ab5c30531atagen2026-10-25 03:13 UTC19
export interface MentionsBoundary { account: string; revision: number }
668ab5c30531atagen2026-10-25 03:13 UTC20
export interface MentionsPage {
668ab5c30531atagen2026-10-25 03:13 UTC21
  entries: MentionEntry[]; next_cursor: string | null; end_reached: boolean; coverage: MentionCoverage;
668ab5c30531atagen2026-10-25 03:13 UTC22
  unread_count: number; revision: number; boundary: MentionsBoundary;
668ab5c30531atagen2026-10-25 03:13 UTC23
}
668ab5c30531atagen2026-10-25 03:13 UTC24
export interface SavedMentionContext { key: MentionKey; state: MentionCaptureState; context: EventContext | null; revision: number }
668ab5c30531atagen2026-10-25 03:13 UTC25
export interface MentionsUpdate { snapshot: MentionsPage; upserts: MentionEntry[]; removed: MentionKey[] }
668ab5c30531atagen2026-10-25 03:13 UTC26
668ab5c30531atagen2026-10-25 03:13 UTC27
type Obj = Record<string, unknown>;
668ab5c30531atagen2026-10-25 03:13 UTC28
const obj = (v: unknown): v is Obj => typeof v === 'object' && v !== null && !Array.isArray(v);
668ab5c30531atagen2026-10-25 03:13 UTC29
const str = (v: unknown): v is string => typeof v === 'string';
668ab5c30531atagen2026-10-25 03:13 UTC30
const bool = (v: unknown): v is boolean => typeof v === 'boolean';
668ab5c30531atagen2026-10-25 03:13 UTC31
const nat = (v: unknown): v is number => typeof v === 'number' && Number.isSafeInteger(v) && v >= 0;
668ab5c30531atagen2026-10-25 03:13 UTC32
function key(v: unknown): MentionKey | null {
668ab5c30531atagen2026-10-25 03:13 UTC33
  if (!obj(v) || !str(v.room_id) || !v.room_id || !str(v.event_id) || !v.event_id) return null;
668ab5c30531atagen2026-10-25 03:13 UTC34
  return { room_id: v.room_id, event_id: v.event_id };
668ab5c30531atagen2026-10-25 03:13 UTC35
}
668ab5c30531atagen2026-10-25 03:13 UTC36
function capture(v: unknown): MentionCaptureState | null {
668ab5c30531atagen2026-10-25 03:13 UTC37
  if (str(v) && ['Pending', 'Partial', 'Ready', 'Unavailable'].includes(v)) return { kind: v as 'Pending' | 'Partial' | 'Ready' | 'Unavailable' };
668ab5c30531atagen2026-10-25 03:13 UTC38
  if (obj(v) && obj(v.Failed) && str(v.Failed.message) && nat(v.Failed.retry_at_ms)) return { kind: 'Failed', message: v.Failed.message, retry_at_ms: v.Failed.retry_at_ms };
668ab5c30531atagen2026-10-25 03:13 UTC39
  return null;
668ab5c30531atagen2026-10-25 03:13 UTC40
}
668ab5c30531atagen2026-10-25 03:13 UTC41
function entry(v: unknown): MentionEntry | null {
668ab5c30531atagen2026-10-25 03:13 UTC42
  if (!obj(v)) return null;
668ab5c30531atagen2026-10-25 03:13 UTC43
  const k = key(v.key), ev = parseTimelineItem(v.event), c = capture(v.capture);
a4555a7eae2datagen2026-10-25 09:18 UTC44
  if (!k || !ev || !c || !nat(v.timestamp_ms) || !nat(v.discovered_at_ms) || !nat(v.discovery_revision) || !nat(v.revision) || !Array.isArray(v.reasons) || !v.reasons.every(x => str(x) && ['Direct','Reply','Room','Keyword'].includes(x)) || !bool(v.read) || !(v.room_name === null || str(v.room_name)) || !bool(v.notification_suppressed)) return null;
668ab5c30531atagen2026-10-25 03:13 UTC45
  return { key:k, timestamp_ms:v.timestamp_ms, discovered_at_ms:v.discovered_at_ms, discovery_revision:v.discovery_revision, revision:v.revision, event:ev, reasons:v.reasons as MentionReason[], read:v.read, capture:c, room_name:v.room_name, notification_suppressed:v.notification_suppressed };
668ab5c30531atagen2026-10-25 03:13 UTC46
}
668ab5c30531atagen2026-10-25 03:13 UTC47
function gap(v: unknown): MentionGap | null {
668ab5c30531atagen2026-10-25 03:13 UTC48
  if (!obj(v) || !(v.room_id === null || (str(v.room_id) && v.room_id)) || !str(v.reason) || !(v.token === null || str(v.token))) return null;
668ab5c30531atagen2026-10-25 03:13 UTC49
  return { room_id:v.room_id, reason:v.reason, token:v.token };
668ab5c30531atagen2026-10-25 03:13 UTC50
}
668ab5c30531atagen2026-10-25 03:13 UTC51
function page(v: unknown): MentionsPage | null {
668ab5c30531atagen2026-10-25 03:13 UTC52
  if (!obj(v) || !obj(v.coverage) || !Array.isArray(v.entries) || !Array.isArray(v.coverage['gaps'])) return null;
668ab5c30531atagen2026-10-25 03:13 UTC53
  const entries=v.entries.map(entry), cov=v.coverage as Obj;
668ab5c30531atagen2026-10-25 03:13 UTC54
  const gaps=(cov['gaps'] as unknown[]).map(gap);
249e439c27e5atagen2026-10-25 13:26 UTC55
  if (entries.some(x=>!x) || gaps.some(x=>!x) || !(v.next_cursor === null || str(v.next_cursor)) || !bool(v.end_reached) || !(cov['earliest_timestamp_ms'] === null || nat(cov['earliest_timestamp_ms'])) || !(cov['historical_supported'] === null || bool(cov['historical_supported'])) || !bool(cov['recovering']) || !bool(cov['truncated']) || !nat(cov['unresolved_candidates']) || !nat(v.unread_count) || !nat(v.revision) || !obj(v.boundary) || !str(v.boundary['account']) || !v.boundary['account'] || !nat(v.boundary['revision'])) return null;
249e439c27e5atagen2026-10-25 13:26 UTC56
  return { entries:entries as MentionEntry[], next_cursor:v.next_cursor as string|null, end_reached:v.end_reached, coverage:{earliest_timestamp_ms:cov['earliest_timestamp_ms'] as number|null,historical_supported:cov['historical_supported'] as boolean|null,recovering:cov['recovering'],gaps:gaps as MentionGap[],truncated:cov['truncated'],unresolved_candidates:cov['unresolved_candidates']}, unread_count:v.unread_count, revision:v.revision, boundary:{account:v.boundary['account'],revision:v.boundary['revision']} };
668ab5c30531atagen2026-10-25 03:13 UTC57
}
668ab5c30531atagen2026-10-25 03:13 UTC58
export function parseMentionsPage(raw: unknown): MentionsPage | null { try { return page(raw); } catch { return null; } }
668ab5c30531atagen2026-10-25 03:13 UTC59
export function parseSavedMentionContext(raw: unknown): SavedMentionContext | null {
668ab5c30531atagen2026-10-25 03:13 UTC60
  try {
668ab5c30531atagen2026-10-25 03:13 UTC61
    if (!obj(raw)) return null;
668ab5c30531atagen2026-10-25 03:13 UTC62
    const k=key(raw.key), state=capture(raw.state), context=raw.context === null ? null : parseEventContext(raw.context);
668ab5c30531atagen2026-10-25 03:13 UTC63
    if (!k || !state || (raw.context !== null && !context) || !nat(raw.revision)) return null;
668ab5c30531atagen2026-10-25 03:13 UTC64
    return {key:k,state,context,revision:raw.revision};
668ab5c30531atagen2026-10-25 03:13 UTC65
  } catch { return null; }
668ab5c30531atagen2026-10-25 03:13 UTC66
}
668ab5c30531atagen2026-10-25 03:13 UTC67
export function parseMentionsUpdate(raw: unknown): MentionsUpdate | null {
668ab5c30531atagen2026-10-25 03:13 UTC68
  try {
668ab5c30531atagen2026-10-25 03:13 UTC69
    if (!obj(raw) || !Array.isArray(raw.upserts) || !Array.isArray(raw.removed)) return null;
668ab5c30531atagen2026-10-25 03:13 UTC70
    const snapshot=page(raw.snapshot), upserts=raw.upserts.map(entry), removed=raw.removed.map(key);
668ab5c30531atagen2026-10-25 03:13 UTC71
    if (!snapshot || upserts.some(x=>!x) || removed.some(x=>!x)) return null;
668ab5c30531atagen2026-10-25 03:13 UTC72
    return {snapshot,upserts:upserts as MentionEntry[],removed:removed as MentionKey[]};
668ab5c30531atagen2026-10-25 03:13 UTC73
  } catch { return null; }
668ab5c30531atagen2026-10-25 03:13 UTC74
}
668ab5c30531atagen2026-10-25 03:13 UTC75
export function mentionKey(key: MentionKey): string { return JSON.stringify([key.room_id,key.event_id]); }