222 lines
5.9 KiB
JavaScript
222 lines
5.9 KiB
JavaScript
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 = /<img[^>]+src="?([^"\s]+)"?[^>]*\/>/g
|
|
const urls = []
|
|
let m
|
|
|
|
while (m = re.exec(html)) {
|
|
urls.push(m[1])
|
|
}
|
|
|
|
return urls
|
|
}
|
|
|
|
const putImgPlaceholders = html => {
|
|
const re = /<img[^>]+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
|
|
}
|
|
|