Add status zap endpoint, publish zap request to pipeline
This commit is contained in:
parent
786d9914af
commit
826a244f47
|
@ -64,6 +64,7 @@ import {
|
||||||
statusController,
|
statusController,
|
||||||
unbookmarkController,
|
unbookmarkController,
|
||||||
unpinController,
|
unpinController,
|
||||||
|
zapController,
|
||||||
} from './controllers/api/statuses.ts';
|
} from './controllers/api/statuses.ts';
|
||||||
import { streamingController } from './controllers/api/streaming.ts';
|
import { streamingController } from './controllers/api/streaming.ts';
|
||||||
import {
|
import {
|
||||||
|
@ -168,6 +169,7 @@ app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/bookmark', requirePubkey, bookmarkC
|
||||||
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unbookmark', requirePubkey, unbookmarkController);
|
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unbookmark', requirePubkey, unbookmarkController);
|
||||||
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/pin', requirePubkey, pinController);
|
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/pin', requirePubkey, pinController);
|
||||||
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unpin', requirePubkey, unpinController);
|
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unpin', requirePubkey, unpinController);
|
||||||
|
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/zap', requirePubkey, zapController);
|
||||||
app.post('/api/v1/statuses', requirePubkey, createStatusController);
|
app.post('/api/v1/statuses', requirePubkey, createStatusController);
|
||||||
|
|
||||||
app.post('/api/v1/media', requireRole('user', { validatePayload: false }), mediaController);
|
app.post('/api/v1/media', requireRole('user', { validatePayload: false }), mediaController);
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import { type AppController } from '@/app.ts';
|
import { type AppController } from '@/app.ts';
|
||||||
|
import { Conf } from '@/config.ts';
|
||||||
import { getUnattachedMediaByIds } from '@/db/unattached-media.ts';
|
import { getUnattachedMediaByIds } from '@/db/unattached-media.ts';
|
||||||
import { type Event, ISO6391, z } from '@/deps.ts';
|
import { type Event, ISO6391, z } from '@/deps.ts';
|
||||||
import { getAncestors, getAuthor, getDescendants, getEvent } from '@/queries.ts';
|
import { getAncestors, getAuthor, getDescendants, getEvent } from '@/queries.ts';
|
||||||
|
import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
|
||||||
import { addTag, deleteTag } from '@/tags.ts';
|
import { addTag, deleteTag } from '@/tags.ts';
|
||||||
import { createEvent, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts';
|
import { createEvent, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts';
|
||||||
import { renderEventAccounts } from '@/views.ts';
|
import { renderEventAccounts } from '@/views.ts';
|
||||||
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
||||||
|
import { getLnurl } from '@/utils/lnurl.ts';
|
||||||
|
|
||||||
const createStatusSchema = z.object({
|
const createStatusSchema = z.object({
|
||||||
in_reply_to_id: z.string().regex(/[0-9a-f]{64}/).nullish(),
|
in_reply_to_id: z.string().regex(/[0-9a-f]{64}/).nullish(),
|
||||||
|
@ -261,6 +264,47 @@ const unpinController: AppController = async (c) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const zapSchema = z.object({
|
||||||
|
amount: z.number().int().positive(),
|
||||||
|
comment: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const zapController: AppController = async (c) => {
|
||||||
|
const id = c.req.param('id');
|
||||||
|
const body = await parseBody(c.req.raw);
|
||||||
|
const params = zapSchema.safeParse(body);
|
||||||
|
|
||||||
|
if (!params.success) {
|
||||||
|
return c.json({ error: 'Bad request', schema: params.error }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = await getEvent(id, { kind: 1, relations: ['author', 'event_stats', 'author_stats'] });
|
||||||
|
const author = target?.author;
|
||||||
|
const meta = jsonMetaContentSchema.parse(author?.content);
|
||||||
|
const lnurl = getLnurl(meta);
|
||||||
|
|
||||||
|
if (target && lnurl) {
|
||||||
|
await createEvent({
|
||||||
|
kind: 9734,
|
||||||
|
content: params.data.comment ?? '',
|
||||||
|
tags: [
|
||||||
|
['e', target.id],
|
||||||
|
['p', target.pubkey],
|
||||||
|
['amount', params.data.amount.toString()],
|
||||||
|
['relays', Conf.relay],
|
||||||
|
['lnurl', lnurl],
|
||||||
|
],
|
||||||
|
}, c);
|
||||||
|
|
||||||
|
const status = await renderStatus(target, c.get('pubkey'));
|
||||||
|
status.zapped = true;
|
||||||
|
|
||||||
|
return c.json(status);
|
||||||
|
} else {
|
||||||
|
return c.json({ error: 'Event not found.' }, 404);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
bookmarkController,
|
bookmarkController,
|
||||||
contextController,
|
contextController,
|
||||||
|
@ -272,4 +316,5 @@ export {
|
||||||
statusController,
|
statusController,
|
||||||
unbookmarkController,
|
unbookmarkController,
|
||||||
unpinController,
|
unpinController,
|
||||||
|
zapController,
|
||||||
};
|
};
|
||||||
|
|
|
@ -86,5 +86,6 @@ export { EventEmitter } from 'npm:tseep@^1.1.3';
|
||||||
export { default as stringifyStable } from 'npm:fast-stable-stringify@^1.0.0';
|
export { default as stringifyStable } from 'npm:fast-stable-stringify@^1.0.0';
|
||||||
// @deno-types="npm:@types/debug@^4.1.12"
|
// @deno-types="npm:@types/debug@^4.1.12"
|
||||||
export { default as Debug } from 'npm:debug@^4.3.4';
|
export { default as Debug } from 'npm:debug@^4.3.4';
|
||||||
|
export { bech32 } from 'npm:@scure/base@^1.1.1';
|
||||||
|
|
||||||
export type * as TypeFest from 'npm:type-fest@^4.3.0';
|
export type * as TypeFest from 'npm:type-fest@^4.3.0';
|
||||||
|
|
|
@ -19,7 +19,7 @@ interface GetEventOpts<K extends number> {
|
||||||
const getEvent = async <K extends number = number>(
|
const getEvent = async <K extends number = number>(
|
||||||
id: string,
|
id: string,
|
||||||
opts: GetEventOpts<K> = {},
|
opts: GetEventOpts<K> = {},
|
||||||
): Promise<Event<K> | undefined> => {
|
): Promise<DittoEvent<K> | undefined> => {
|
||||||
debug(`getEvent: ${id}`);
|
debug(`getEvent: ${id}`);
|
||||||
const { kind, relations, signal = AbortSignal.timeout(1000) } = opts;
|
const { kind, relations, signal = AbortSignal.timeout(1000) } = opts;
|
||||||
const microfilter: IdMicrofilter = { ids: [id] };
|
const microfilter: IdMicrofilter = { ids: [id] };
|
||||||
|
|
|
@ -70,6 +70,7 @@ const metaContentSchema = z.object({
|
||||||
picture: z.string().optional().catch(undefined),
|
picture: z.string().optional().catch(undefined),
|
||||||
banner: z.string().optional().catch(undefined),
|
banner: z.string().optional().catch(undefined),
|
||||||
nip05: z.string().optional().catch(undefined),
|
nip05: z.string().optional().catch(undefined),
|
||||||
|
lud06: z.string().optional().catch(undefined),
|
||||||
lud16: z.string().optional().catch(undefined),
|
lud16: z.string().optional().catch(undefined),
|
||||||
}).partial().passthrough();
|
}).partial().passthrough();
|
||||||
|
|
||||||
|
|
|
@ -15,4 +15,16 @@ function lnurlDecode(lnurl: string): string {
|
||||||
return new TextDecoder().decode(data);
|
return new TextDecoder().decode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { lnurlDecode, lnurlEncode };
|
/** Get an LNURL from a lud06 or lud16. */
|
||||||
|
function getLnurl({ lud06, lud16 }: { lud06?: string; lud16?: string }): string | undefined {
|
||||||
|
if (lud06) return lud06;
|
||||||
|
if (lud16) {
|
||||||
|
const [name, host] = lud16.split('@');
|
||||||
|
if (name && host) {
|
||||||
|
const url = new URL(`/.well-known/lnurlp/${name}`, `https://${host}`).toString();
|
||||||
|
return lnurlEncode(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getLnurl, lnurlDecode, lnurlEncode };
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { findUser } from '@/db/users.ts';
|
||||||
import { lodash, nip19, type UnsignedEvent } from '@/deps.ts';
|
import { lodash, nip19, type UnsignedEvent } from '@/deps.ts';
|
||||||
import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
|
import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
|
||||||
import { type DittoEvent } from '@/storages/types.ts';
|
import { type DittoEvent } from '@/storages/types.ts';
|
||||||
|
import { getLnurl } from '@/utils/lnurl.ts';
|
||||||
import { verifyNip05Cached } from '@/utils/nip05.ts';
|
import { verifyNip05Cached } from '@/utils/nip05.ts';
|
||||||
import { Nip05, nostrDate, nostrNow, parseNip05 } from '@/utils.ts';
|
import { Nip05, nostrDate, nostrNow, parseNip05 } from '@/utils.ts';
|
||||||
import { renderEmojis } from '@/views/mastodon/emojis.ts';
|
import { renderEmojis } from '@/views/mastodon/emojis.ts';
|
||||||
|
@ -24,6 +25,8 @@ async function renderAccount(
|
||||||
picture = Conf.local('/images/avi.png'),
|
picture = Conf.local('/images/avi.png'),
|
||||||
banner = Conf.local('/images/banner.png'),
|
banner = Conf.local('/images/banner.png'),
|
||||||
about,
|
about,
|
||||||
|
lud06,
|
||||||
|
lud16,
|
||||||
} = jsonMetaContentSchema.parse(event.content);
|
} = jsonMetaContentSchema.parse(event.content);
|
||||||
|
|
||||||
const npub = nip19.npubEncode(pubkey);
|
const npub = nip19.npubEncode(pubkey);
|
||||||
|
@ -67,6 +70,9 @@ async function renderAccount(
|
||||||
statuses_count: event.author_stats?.notes_count ?? 0,
|
statuses_count: event.author_stats?.notes_count ?? 0,
|
||||||
url: Conf.local(`/users/${pubkey}`),
|
url: Conf.local(`/users/${pubkey}`),
|
||||||
username: parsed05?.nickname || npub.substring(0, 8),
|
username: parsed05?.nickname || npub.substring(0, 8),
|
||||||
|
ditto: {
|
||||||
|
accepts_zaps: Boolean(getLnurl({ lud06, lud16 })),
|
||||||
|
},
|
||||||
pleroma: {
|
pleroma: {
|
||||||
is_admin: user?.admin || false,
|
is_admin: user?.admin || false,
|
||||||
is_moderator: user?.admin || false,
|
is_moderator: user?.admin || false,
|
||||||
|
|
|
@ -38,6 +38,7 @@ async function renderStatus(event: DittoEvent<1>, viewerPubkey?: string) {
|
||||||
? await eventsDB.filter([
|
? await eventsDB.filter([
|
||||||
{ kinds: [6], '#e': [event.id], authors: [viewerPubkey], limit: 1 },
|
{ kinds: [6], '#e': [event.id], authors: [viewerPubkey], limit: 1 },
|
||||||
{ kinds: [7], '#e': [event.id], authors: [viewerPubkey], limit: 1 },
|
{ kinds: [7], '#e': [event.id], authors: [viewerPubkey], limit: 1 },
|
||||||
|
{ kinds: [9734], '#e': [event.id], authors: [viewerPubkey], limit: 1 },
|
||||||
{ kinds: [10001], '#e': [event.id], authors: [viewerPubkey], limit: 1 },
|
{ kinds: [10001], '#e': [event.id], authors: [viewerPubkey], limit: 1 },
|
||||||
{ kinds: [10003], '#e': [event.id], authors: [viewerPubkey], limit: 1 },
|
{ kinds: [10003], '#e': [event.id], authors: [viewerPubkey], limit: 1 },
|
||||||
])
|
])
|
||||||
|
@ -48,6 +49,7 @@ async function renderStatus(event: DittoEvent<1>, viewerPubkey?: string) {
|
||||||
const repostEvent = relatedEvents.find((event) => event.kind === 6);
|
const repostEvent = relatedEvents.find((event) => event.kind === 6);
|
||||||
const pinEvent = relatedEvents.find((event) => event.kind === 10001);
|
const pinEvent = relatedEvents.find((event) => event.kind === 10001);
|
||||||
const bookmarkEvent = relatedEvents.find((event) => event.kind === 10003);
|
const bookmarkEvent = relatedEvents.find((event) => event.kind === 10003);
|
||||||
|
const zapEvent = relatedEvents.find((event) => event.kind === 9734);
|
||||||
|
|
||||||
const content = buildInlineRecipients(mentions) + html;
|
const content = buildInlineRecipients(mentions) + html;
|
||||||
|
|
||||||
|
@ -91,6 +93,7 @@ async function renderStatus(event: DittoEvent<1>, viewerPubkey?: string) {
|
||||||
poll: null,
|
poll: null,
|
||||||
uri: Conf.local(`/posts/${event.id}`),
|
uri: Conf.local(`/posts/${event.id}`),
|
||||||
url: Conf.local(`/posts/${event.id}`),
|
url: Conf.local(`/posts/${event.id}`),
|
||||||
|
zapped: Boolean(zapEvent),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue