Merge remote-tracking branch 'origin/main' into connections

This commit is contained in:
Alex Gleason 2024-05-28 18:28:50 -05:00
commit a617e32d65
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
3 changed files with 41 additions and 59 deletions

View File

@ -15,7 +15,7 @@ import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { hydrateEvents } from '@/storages/hydrate.ts'; import { hydrateEvents } from '@/storages/hydrate.ts';
import { createEvent, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts'; import { createEvent, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts';
import { getLnurl } from '@/utils/lnurl.ts'; import { getInvoice, getLnurl } from '@/utils/lnurl.ts';
import { lookupPubkey } from '@/utils/lookup.ts'; import { lookupPubkey } from '@/utils/lookup.ts';
import { addTag, deleteTag } from '@/utils/tags.ts'; import { addTag, deleteTag } from '@/utils/tags.ts';
import { asyncReplaceAll } from '@/utils/text.ts'; import { asyncReplaceAll } from '@/utils/text.ts';
@ -450,15 +450,16 @@ const zapController: AppController = async (c) => {
const author = target?.author; const author = target?.author;
const meta = n.json().pipe(n.metadata()).catch({}).parse(author?.content); const meta = n.json().pipe(n.metadata()).catch({}).parse(author?.content);
const lnurl = getLnurl(meta); const lnurl = getLnurl(meta);
const amount = params.data.amount;
if (target && lnurl) { if (target && lnurl) {
await createEvent({ const nostr = await createEvent({
kind: 9734, kind: 9734,
content: params.data.comment ?? '', content: params.data.comment ?? '',
tags: [ tags: [
['e', target.id], ['e', target.id],
['p', target.pubkey], ['p', target.pubkey],
['amount', params.data.amount.toString()], ['amount', amount.toString()],
['relays', Conf.relay], ['relays', Conf.relay],
['lnurl', lnurl], ['lnurl', lnurl],
], ],
@ -467,7 +468,11 @@ const zapController: AppController = async (c) => {
const status = await renderStatus(target, { viewerPubkey: await c.get('signer')?.getPublicKey() }); const status = await renderStatus(target, { viewerPubkey: await c.get('signer')?.getPublicKey() });
status.zapped = true; status.zapped = true;
return c.json(status); return c.json(status, {
headers: {
'Ln-Invoice': await getInvoice({ amount, nostr, lnurl }, signal),
},
});
} else { } else {
return c.json({ error: 'Event not found.' }, 404); return c.json({ error: 'Event not found.' }, 404);
} }

View File

@ -1,5 +1,4 @@
import { NKinds, NostrEvent, NPolicy, NSchema as n } from '@nostrify/nostrify'; import { NKinds, NostrEvent, NPolicy, NSchema as n } from '@nostrify/nostrify';
import { LNURL } from '@nostrify/nostrify/ln';
import { PipePolicy } from '@nostrify/nostrify/policies'; import { PipePolicy } from '@nostrify/nostrify/policies';
import Debug from '@soapbox/stickynotes/debug'; import Debug from '@soapbox/stickynotes/debug';
import { sql } from 'kysely'; import { sql } from 'kysely';
@ -12,15 +11,12 @@ import { DittoEvent } from '@/interfaces/DittoEvent.ts';
import { DVM } from '@/pipeline/DVM.ts'; import { DVM } from '@/pipeline/DVM.ts';
import { MuteListPolicy } from '@/policies/MuteListPolicy.ts'; import { MuteListPolicy } from '@/policies/MuteListPolicy.ts';
import { RelayError } from '@/RelayError.ts'; import { RelayError } from '@/RelayError.ts';
import { hydrateEvents, purifyEvent } from '@/storages/hydrate.ts'; import { hydrateEvents } from '@/storages/hydrate.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { eventAge, nostrDate, nostrNow, parseNip05, Time } from '@/utils.ts'; import { eventAge, nostrDate, parseNip05, Time } from '@/utils.ts';
import { fetchWorker } from '@/workers/fetch.ts';
import { policyWorker } from '@/workers/policy.ts'; import { policyWorker } from '@/workers/policy.ts';
import { TrendsWorker } from '@/workers/trends.ts'; import { TrendsWorker } from '@/workers/trends.ts';
import { verifyEventWorker } from '@/workers/verify.ts'; import { verifyEventWorker } from '@/workers/verify.ts';
import { AdminSigner } from '@/signers/AdminSigner.ts';
import { lnurlCache } from '@/utils/lnurl.ts';
import { nip05Cache } from '@/utils/nip05.ts'; import { nip05Cache } from '@/utils/nip05.ts';
import { updateStats } from '@/utils/stats.ts'; import { updateStats } from '@/utils/stats.ts';
import { getTagSet } from '@/utils/tags.ts'; import { getTagSet } from '@/utils/tags.ts';
@ -48,7 +44,6 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise<void
DVM.event(event), DVM.event(event),
trackHashtags(event), trackHashtags(event),
processMedia(event), processMedia(event),
payZap(event, signal),
streamOut(event), streamOut(event),
]); ]);
} }
@ -189,53 +184,6 @@ function processMedia({ tags, pubkey, user }: DittoEvent) {
} }
} }
/** Emit Nostr Wallet Connect event from zaps so users may pay. */
async function payZap(event: DittoEvent, signal: AbortSignal) {
if (event.kind !== 9734) return;
const lnurl = event.tags.find(([name]) => name === 'lnurl')?.[1];
const amount = Number(event.tags.find(([name]) => name === 'amount')?.[1]);
if (!lnurl || !amount) return;
try {
const details = await lnurlCache.fetch(lnurl, { signal });
if (details.tag !== 'payRequest' || !details.allowsNostr || !details.nostrPubkey) {
throw new Error('invalid lnurl');
}
if (amount > details.maxSendable || amount < details.minSendable) {
throw new Error('amount out of range');
}
const { pr } = await LNURL.callback(
details.callback,
{ amount, nostr: purifyEvent(event), lnurl },
{ fetch: fetchWorker, signal },
);
const signer = new AdminSigner();
const nwcRequestEvent = await signer.signEvent({
kind: 23194,
content: await signer.nip04.encrypt(
event.pubkey,
JSON.stringify({ method: 'pay_invoice', params: { invoice: pr } }),
),
created_at: nostrNow(),
tags: [
['p', event.pubkey],
['e', event.id],
],
});
await handleEvent(nwcRequestEvent, signal);
} catch (e) {
debug('lnurl error:', e);
}
}
/** Determine if the event is being received in a timely manner. */ /** Determine if the event is being received in a timely manner. */
function isFresh(event: NostrEvent): boolean { function isFresh(event: NostrEvent): boolean {
return eventAge(event) < Time.seconds(10); return eventAge(event) < Time.seconds(10);

View File

@ -4,6 +4,7 @@ import Debug from '@soapbox/stickynotes/debug';
import { SimpleLRU } from '@/utils/SimpleLRU.ts'; import { SimpleLRU } from '@/utils/SimpleLRU.ts';
import { Time } from '@/utils/time.ts'; import { Time } from '@/utils/time.ts';
import { fetchWorker } from '@/workers/fetch.ts'; import { fetchWorker } from '@/workers/fetch.ts';
import { NostrEvent } from '@nostrify/nostrify';
const debug = Debug('ditto:lnurl'); const debug = Debug('ditto:lnurl');
@ -38,4 +39,32 @@ function getLnurl({ lud06, lud16 }: { lud06?: string; lud16?: string }, limit?:
} }
} }
export { getLnurl, lnurlCache }; interface CallbackParams {
amount: number;
nostr: NostrEvent;
lnurl: string;
}
async function getInvoice(params: CallbackParams, signal?: AbortSignal): Promise<string> {
const { amount, lnurl } = params;
const details = await lnurlCache.fetch(lnurl, { signal });
if (details.tag !== 'payRequest' || !details.allowsNostr || !details.nostrPubkey) {
throw new Error('invalid lnurl');
}
if (amount > details.maxSendable || amount < details.minSendable) {
throw new Error('amount out of range');
}
const { pr } = await LNURL.callback(
details.callback,
params,
{ fetch: fetchWorker, signal },
);
return pr;
}
export { getInvoice, getLnurl, lnurlCache };