import 'websocket-polyfill' import { SSMClient, GetParameterCommand, PutParameterCommand } from '@aws-sdk/client-ssm' import { extract } from '@extractus/feed-extractor' import NDK, { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk' import bech32 from 'bech32-buffer' import moment from 'moment' import FormData from 'form-data' const toHexString = nsec => { let buffer = bech32.decode(nsec).data return buffer.reduce((s, byte) => { let hex = byte.toString(16); if (hex.length === 1) hex = '0' + hex; return s + hex; }, ''); } const imgUrls = html => { const re = /]+src="?([^"\s]+)"?[^>]*\/>/g const urls = [] let m while (m = re.exec(html)) { urls.push(m[1]) } return urls } const putImgPlaceholders = html => { const re = /]+src="?([^"\s]+)"?[^>]*\/>/g const placeholders = [] let replaced = html const matches = html.matchAll(re) let img = 0 for(let match of matches) { replaced = replaced.replace(match[0], `%%${img}%%`) placeholders.push(`%%${img}%%`) img++ } return { replaced, placeholders } } const stripHtml = html => { return html.replaceAll(/<[^>]*>/g, "") } const replaceImgsWithNostrBuildUrls = async (storeUrl, html, npub) => { const urls = imgUrls(html) let { replaced, placeholders } = putImgPlaceholders(html) let count = 0 for (let placeholder of placeholders) { const nbUrl = await uploadImgStore(storeUrl, urls[count], npub) if (nbUrl) { replaced = replaced.replace(placeholder, nbUrl) } else { replaced = replaced.replace(placeholder, '') } count++ } return replaced } const uploadImgStore = async (hostUrl, imgUrl, npub) => { const response = await fetch(hostUrl, { method: 'POST', headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ "url": imgUrl, "npub": npub }) }) const json = await response.json() if (json.status !== "success") { return false } return json.data[0].url } export const handler = async (event) => { const region = process.env.AWS_REGION || "us-east-1" const feed = event.feedUrl const nostrNsecParam = event.nostrNsecParam const lastRunTimeParam = event.lastRunTimeParam const dryrun = event.dryRun if (dryrun) { console.log("Dry run. Simulating publishes to nostr.") } let response = { totalItems: 0, successes: 0, errors: [] } const ssmClient = new SSMClient({ region }) const ssmGetLastRunCommand = new GetParameterCommand({ Name: lastRunTimeParam }); let since = new Date() await ssmClient.send(ssmGetLastRunCommand) .then(data => { since = data.Parameter.Value }).catch(err => { console.dir(err) response.errors.push(err) return response }) const ssmGetNsecCommand = new GetParameterCommand({ Name: nostrNsecParam, WithDecryption: true }) let privkey = '' await ssmClient.send(ssmGetNsecCommand) .then(data => { privkey = toHexString(data.Parameter.Value) }).catch(err => { console.dir(err) response.errors.push(err) return response }) if (feed === "") { console.dir("feedUrl was not set.") return { statusCode: 400, body: "feedUrl must be provided in payload." } } const ndk = new NDK({ explicitRelayUrls: [ "wss://nostr.mom", "wss://nostr.snort.social", "wss://nostr.lol", "wss://nostr.oxtr.dev", "wss://relay.nostr.bg", "wss://nostr-pub.wellorder.net", "wss://relay.damus.io", "wss://nostr.milou.lol", "wss://nostr.bitcoiner.social" ] }) await ndk.connect(6000).catch(err => { console.dir(err) response.errors.push("failed to connect to relay", err) }) const signer = new NDKPrivateKeySigner(privkey) ndk.signer = signer let rss = await extract(feed, { descriptionMaxLen: 0, normalization: false }) .catch(err => { console.dir(err); return response }) let filtered = rss.item.filter(item => moment(item.pubDate).isAfter(since)) response.totalItems = filtered.length for (let item of filtered) { const user = await ndk.signer.user() let content = '' let replaced = await replaceImgsWithNostrBuildUrls( 'https://nostr.build/api/v2/upload/url', item.description, user.npub) if (replaced) { content = stripHtml(replaced) } let ndkEvent = new NDKEvent(ndk, { kind: 1, pubkey: user.hexpubkey, content, created_at: Math.floor(Date.now() / 1000), }) if (dryrun) { console.dir(content) response.successes++ continue } try { await ndkEvent.sign(ndk.signer) const published = await ndkEvent.publish() if (published) { response.successes++ const ssmPutCommand = new PutParameterCommand({ Name: lastRunTimeParam, Value: new Date().toISOString(), Type: "String", Overwrite: true }) await ssmClient.send(ssmPutCommand) continue } console.dir(`failed to publish note`) response.errors.push(`failed to publish note`) } catch(e) { console.dir(e) response.errors.push(e) return response } } return response }