import pino from "pino"; import WebSocket from "ws"; import { MicroBlogBackend } from "./pocketbase"; const logger = pino(); const pb = new MicroBlogBackend(logger); const fetcherNpub = process.env.NOSTR_FETCHER_NPUB; const myNpub = process.env.NOSTR_ID; if (!fetcherNpub) { throw new Error("NOSTR_FETCHER_NPUB is not set"); } if (!myNpub) { throw new Error("NOSTR_ID is not set"); } type NostrTag = ["t" | "r" | "imeta", string]; type NostrEvent = [ "EVENT" | "EOSE", string, { id: string; pubkey: string; created_at: number; kind: number; // 1 tags: NostrTag[]; content: string; sig: string; }, ]; (async () => { logger.info("Starting Nostr Fetcher"); // figure out when the last post of wasved const lastSavedPost = await pb.getLatestPostBySource("nostr"); if (!lastSavedPost) { throw new Error("No last saved nostr post found"); } let since: number | undefined; since = new Date(lastSavedPost.posted).getTime() / 1000; logger.info( { lastSavedPostId: lastSavedPost.id, since }, "lastSavedPost nostr post", ); // listen for new events for 30 seconds logger.info("trying to connecting to nostr relay"); const relay = process.env.NOSTR_RELAY; if (!relay) { throw new Error("No NOSTR_RELAY environment variable found"); } const events: NostrEvent[] = []; const ws = new WebSocket(relay); ws.on("error", logger.error); ws.on("message", function message(data: Buffer) { const decodedData = JSON.parse( Buffer.from(data).toString("utf8"), ) as NostrEvent; logger.info({ decodedData }, "recived a message from nostr relay"); if (decodedData[0] === "EVENT") { events.push(decodedData); } }); ws.on("open", function open() { logger.info("connection established"); ws.send( JSON.stringify([ "REQ", fetcherNpub, { kinds: [1], authors: [myNpub], ...(since ? { since } : {}) }, ]), ); }); await new Promise((resolve) => setTimeout(resolve, 30000)); logger.info("closing connection to nostr relay"); ws.close(); logger.info({ count: events.length }, "saving nostr posts"); for (const event of events) { const post = await pb.savePost({ remoteId: event[2].id, fullPost: event[2], posted: new Date(event[2].created_at * 1000).toISOString(), source: "nostr", authorId: event[1], }); for (const tag of event[2].tags) { if (tag[0] === "t") { await pb.setTag(tag[1], post.id); } else if (tag[0] === "imeta") { const value = tag[1]; // remove "url " from the start of the string const url = value.slice(4); await pb.saveAndSetImage( { remoteURL: url, }, post.id, ); } } } })();