aboutsummaryrefslogtreecommitdiffstats
path: root/lib/mls.ts
blob: 9e3d5d3553982e0d3fb1cec5acd29f376a3ff719 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import config from "$config";
import { db } from "$lib/db";
import { l, warn } from "$lib/logging";
import { getProfile } from "$lib/nostr";
import { Relay } from "nostr-tools/relay";
import type { Event } from "nostr-tools";

const MLS_KEY = "mls:users";
const RELAYS = config.relays.filter((r) => r.startsWith("wss://"));

interface MlsUser {
  pubkey: string;
  name?: string;
  picture?: string;
  nip05?: string;
}

const knownPubkeys = new Set<string>();

async function resolveProfile(pubkey: string): Promise<MlsUser> {
  try {
    const profile = await getProfile(pubkey);
    return {
      pubkey,
      name: profile.display_name || profile.name || undefined,
      picture: profile.picture || undefined,
      nip05: profile.nip05 || undefined,
    };
  } catch {
    return { pubkey };
  }
}

async function addUser(pubkey: string): Promise<void> {
  if (knownPubkeys.has(pubkey)) return;
  knownPubkeys.add(pubkey);
  const user = await resolveProfile(pubkey);
  await db.hSet(MLS_KEY, pubkey, JSON.stringify(user));
}

async function backfill(): Promise<void> {
  // Paginate back 3 weeks
  const threeWeeksAgo = Math.floor(Date.now() / 1000) - 21 * 24 * 60 * 60;
  let until: number | undefined;

  for (let pass = 0; pass < 50; pass++) {
    const filter: any = { kinds: [443], limit: 500 };
    if (until) filter.until = until;

    let events: Event[] = [];
    try {
      events = (
        await Promise.all(
          RELAYS.map(
            (url) =>
              new Promise<Event[]>((resolve) => {
                Relay.connect(url)
                  .then((r) => {
                    const found: Event[] = [];
                    r.subscribe([filter], {
                      onevent(e) { found.push(e); },
                      oneose() { r.close(); resolve(found); },
                    });
                  })
                  .catch(() => resolve([]));
              }),
          ),
        )
      ).flat();
    } catch {
      break;
    }

    if (events.length === 0) break;

    const newPubkeys = [...new Set(events.map((e) => e.pubkey))].filter(
      (pk) => !knownPubkeys.has(pk),
    );

    for (const pk of newPubkeys) {
      await addUser(pk);
    }

    const oldest = Math.min(...events.map((e) => e.created_at));
    if (oldest <= threeWeeksAgo) break;
    until = oldest;
  }

  l(`mls backfill complete: ${knownPubkeys.size} users`);
}

function subscribe(): void {
  for (const url of RELAYS) {
    Relay.connect(url)
      .then((r) => {
        r.subscribe([{ kinds: [443], since: Math.floor(Date.now() / 1000) }], {
          onevent(e) {
            addUser(e.pubkey).catch(() => {});
          },
        });
      })
      .catch((e) => warn(`mls subscribe failed: ${url}`, e.message));
  }
}

export async function initMlsIndex(): Promise<void> {
  // Load existing index into memory
  try {
    const existing = await db.hGetAll(MLS_KEY);
    for (const pk of Object.keys(existing)) knownPubkeys.add(pk);
  } catch {}

  // Subscribe for new key packages
  subscribe();

  // Backfill in background
  backfill().catch((e) => warn("mls backfill error", e.message));
}

export async function getMlsUsers(): Promise<MlsUser[]> {
  const data = await db.hGetAll(MLS_KEY);
  return Object.values(data).map((v) => JSON.parse(v));
}