atagen

malice

public
Star0
Watch0
d9be72fdb39386b8e5863ac7f41631066c55cb22
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
web/src/lib/mentions-model.tsTypescript75 lines5.9 KB
1
import { parseEventContext, parseTimelineItem } from './types';
2
import type { EventContext, TimelineItem } from './types';
3
4
export interface MentionKey { room_id: string; event_id: string }
5
export type MentionReason = 'Direct' | 'Reply' | 'Room' | 'Keyword';
6
export type MentionCaptureState =
7
  | { kind: 'Pending' | 'Partial' | 'Ready' | 'Unavailable' }
8
  | { kind: 'Failed'; message: string; retry_at_ms: number };
9
export interface MentionEntry {
10
  key: MentionKey; timestamp_ms: number; discovered_at_ms: number; discovery_revision: number; revision: number;
11
  event: TimelineItem; reasons: MentionReason[]; read: boolean; capture: MentionCaptureState;
12
  room_name: string | null; notification_suppressed: boolean;
13
}
14
export interface MentionCoverage {
15
  earliest_timestamp_ms: number | null; historical_supported: boolean | null; recovering: boolean;
16
  gaps: MentionGap[]; truncated: boolean; unresolved_candidates: number;
17
}
18
export interface MentionGap { room_id: string | null; reason: string; token: string | null }
19
export interface MentionsBoundary { account: string; revision: number }
20
export interface MentionsPage {
21
  entries: MentionEntry[]; next_cursor: string | null; end_reached: boolean; coverage: MentionCoverage;
22
  unread_count: number; revision: number; boundary: MentionsBoundary;
23
}
24
export interface SavedMentionContext { key: MentionKey; state: MentionCaptureState; context: EventContext | null; revision: number }
25
export interface MentionsUpdate { snapshot: MentionsPage; upserts: MentionEntry[]; removed: MentionKey[] }
26
27
type Obj = Record<string, unknown>;
28
const obj = (v: unknown): v is Obj => typeof v === 'object' && v !== null && !Array.isArray(v);
29
const str = (v: unknown): v is string => typeof v === 'string';
30
const bool = (v: unknown): v is boolean => typeof v === 'boolean';
31
const nat = (v: unknown): v is number => typeof v === 'number' && Number.isSafeInteger(v) && v >= 0;
32
function key(v: unknown): MentionKey | null {
33
  if (!obj(v) || !str(v.room_id) || !v.room_id || !str(v.event_id) || !v.event_id) return null;
34
  return { room_id: v.room_id, event_id: v.event_id };
35
}
36
function capture(v: unknown): MentionCaptureState | null {
37
  if (str(v) && ['Pending', 'Partial', 'Ready', 'Unavailable'].includes(v)) return { kind: v as 'Pending' | 'Partial' | 'Ready' | 'Unavailable' };
38
  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 };
39
  return null;
40
}
41
function entry(v: unknown): MentionEntry | null {
42
  if (!obj(v)) return null;
43
  const k = key(v.key), ev = parseTimelineItem(v.event), c = capture(v.capture);
44
  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;
45
  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 };
46
}
47
function gap(v: unknown): MentionGap | null {
48
  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;
49
  return { room_id:v.room_id, reason:v.reason, token:v.token };
50
}
51
function page(v: unknown): MentionsPage | null {
52
  if (!obj(v) || !obj(v.coverage) || !Array.isArray(v.entries) || !Array.isArray(v.coverage['gaps'])) return null;
53
  const entries=v.entries.map(entry), cov=v.coverage as Obj;
54
  const gaps=(cov['gaps'] as unknown[]).map(gap);
55
  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;
56
  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']} };
57
}
58
export function parseMentionsPage(raw: unknown): MentionsPage | null { try { return page(raw); } catch { return null; } }
59
export function parseSavedMentionContext(raw: unknown): SavedMentionContext | null {
60
  try {
61
    if (!obj(raw)) return null;
62
    const k=key(raw.key), state=capture(raw.state), context=raw.context === null ? null : parseEventContext(raw.context);
63
    if (!k || !state || (raw.context !== null && !context) || !nat(raw.revision)) return null;
64
    return {key:k,state,context,revision:raw.revision};
65
  } catch { return null; }
66
}
67
export function parseMentionsUpdate(raw: unknown): MentionsUpdate | null {
68
  try {
69
    if (!obj(raw) || !Array.isArray(raw.upserts) || !Array.isArray(raw.removed)) return null;
70
    const snapshot=page(raw.snapshot), upserts=raw.upserts.map(entry), removed=raw.removed.map(key);
71
    if (!snapshot || upserts.some(x=>!x) || removed.some(x=>!x)) return null;
72
    return {snapshot,upserts:upserts as MentionEntry[],removed:removed as MentionKey[]};
73
  } catch { return null; }
74
}
75
export function mentionKey(key: MentionKey): string { return JSON.stringify([key.room_id,key.event_id]); }