aboutsummaryrefslogtreecommitdiffstats
path: root/views/sessions/new.html
diff options
context:
space:
mode:
Diffstat (limited to 'views/sessions/new.html')
0 files changed, 0 insertions, 0 deletions
a> 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 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
import config from "$config";
import { db, g } from "$lib/db";
import { l, warn } from "$lib/logging";
import { getUser } from "$lib/utils";
import { Relay } from "nostr-tools/relay";
import webpush from "web-push";

if (config.vapid) {
  webpush.setVapidDetails(`mailto:${config.support}`, config.vapid.pk, config.vapid.sk);
}

// ---------------------------------------------------------------------------
// Push notification delivery
// ---------------------------------------------------------------------------

const sendPush = async (pubkey: string, url = "/messages", event?: any) => {
  try {
    const user = await getUser(pubkey);
    if (!user?.id) return;

    const subscriptions = await db.sMembers(`${user.id}:subscriptions`);
    if (!subscriptions?.length) return;

    l(`push to ${user.username} (${subscriptions.length} subs)`);

    const payloadObj: any = {
      title: "New message",
      body: "You have a new encrypted message",
      url,
    };

    // Include the raw event for push-as-data-transport (size guard for ~4KB push limit)
    if (event) {
      const eventStr = JSON.stringify(event);
      if (eventStr.length < 3000) payloadObj.event = eventStr;
    }

    const payload = JSON.stringify(payloadObj);

    for (const s of subscriptions as any) {
      webpush
        .sendNotification(JSON.parse(s as string), payload)
        .catch((e) => {
          warn("push failed", e.message);
          db.sRem(`${user.id}:subscriptions`, s);
        });
    }
  } catch {
    // User not found — not a coinos user
  }
};

// ---------------------------------------------------------------------------
// Kind 1059 (gift wrap / invite) — notifies recipient via p tag
// ---------------------------------------------------------------------------

const handleInviteEvent = async (event: any) => {
  const pTag = event.tags.find((t: any) => t[0] === "p");
  if (!pTag) return;
  await sendPush(pTag[1]);
};

// ---------------------------------------------------------------------------
// Kind 445 (MLS group message) — notifies all group members except sender
// ---------------------------------------------------------------------------

// In-memory group registry: nostrGroupId → { members, relays }
interface GroupEntry {
  members: string[];
  relays: string[];
}

const groupRegistry = new Map<string, GroupEntry>();

// Track active relay subscriptions for kind 445
const activeGroupRelays = new Set<string>();
const groupRelaySubs: (() => void)[] = [];

const handleGroupMessage = async (event: any) => {
  const hTag = event.tags.find((t: any) => t[0] === "h");
  if (!hTag) return;

  const nostrGroupId = hTag[1];
  const group = groupRegistry.get(nostrGroupId);
  if (!group) return;

  const sender = event.pubkey;
  for (const member of group.members) {
    if (member !== sender) sendPush(member, "/messages", event);
  }
};

const subscribeGroupRelay = (url: string) => {
  if (activeGroupRelays.has(url)) return;
  activeGroupRelays.add(url);

  Relay.connect(url)
    .then((relay) => {
      const sub = relay.subscribe(
        [{ kinds: [445], since: Math.floor(Date.now() / 1000) }],
        { onevent: handleGroupMessage },
      );
      groupRelaySubs.push(() => sub.close());
      // l(`listening for group messages on ${url}`);
    })
    .catch((e) => {
      warn(`group listener failed ${url}`, e.message);
      activeGroupRelays.delete(url);
      setTimeout(() => subscribeGroupRelay(url), 5000);
    });
};

/** Called when a client syncs its groups. Updates registry and subscribes to new relays. */
export const syncGroups = async (groups: { nostrGroupId: string; members: string[]; relays: string[] }[]) => {
  for (const g of groups) {
    groupRegistry.set(g.nostrGroupId, { members: g.members, relays: g.relays });
    for (const url of g.relays) subscribeGroupRelay(url);
  }

  l(`group registry: ${groupRegistry.size} groups, ${activeGroupRelays.size} relays`);
};

// ---------------------------------------------------------------------------
// Kind 1059 relay subscriptions (invites / NIP-17 DMs)
// ---------------------------------------------------------------------------

const subscribeInviteRelay = (url: string) => {
  Relay.connect(url)
    .then((relay) => {
      relay.subscribe([{ kinds: [1059], since: Math.floor(Date.now() / 1000) }], {
        onevent: handleInviteEvent,
      });
      // l(`listening for invites on ${url}`);
    })
    .catch((e) => {
      warn(`invite listener failed ${url}`, e.message);
      setTimeout(() => subscribeInviteRelay(url), 5000);
    });
};

// ---------------------------------------------------------------------------
// Boot: restore group registry from Redis, start listeners
// ---------------------------------------------------------------------------

const REGISTRY_KEY = "mls:groupRegistry";

const saveRegistry = async () => {
  const obj: Record<string, GroupEntry> = {};
  for (const [id, entry] of groupRegistry) obj[id] = entry;
  await db.set(REGISTRY_KEY, JSON.stringify(obj));
};

const loadRegistry = async () => {
  try {
    const raw = await db.get(REGISTRY_KEY);
    if (!raw) return;
    const obj = JSON.parse(raw as string);
    for (const [id, entry] of Object.entries(obj) as [string, GroupEntry][]) {
      groupRegistry.set(id, entry);
      for (const url of entry.relays) subscribeGroupRelay(url);
    }
    l(`restored ${groupRegistry.size} groups from registry`);
  } catch {}
};

/** Called from syncGroups to persist after updates */
export const syncGroupsAndSave = async (groups: { nostrGroupId: string; members: string[]; relays: string[] }[]) => {
  await syncGroups(groups);
  await saveRegistry();
};

export const listenForDMs = () => {
  // Subscribe to kind 1059 on configured relays
  const relays = new Set([config.nostr, ...config.relays.filter((r: string) => r.startsWith("wss://"))]);
  for (const url of relays) subscribeInviteRelay(url);

  // Restore persisted group registry and subscribe to kind 445
  loadRegistry();
};