Merge branch 'kind-20' into 'main'

Support kind 20 "Picture" events (NIP-68)

See merge request soapbox-pub/ditto!615
This commit is contained in:
Alex Gleason 2025-01-05 17:39:35 +00:00
commit 1e53457c9d
15 changed files with 38 additions and 34 deletions

View file

@ -47,7 +47,7 @@ const importUsers = async (
if (!profilesOnly) {
matched.push(
...await conn.query(
authors.map((author) => ({ kinds: [1], authors: [author], limit: 200 })),
authors.map((author) => ({ kinds: [1, 20], authors: [author], limit: 200 })),
),
);
}

View file

@ -252,7 +252,7 @@ class Conf {
}
/** Nostr event kinds of events to listen for on the firehose. */
static get firehoseKinds(): number[] {
return (Deno.env.get('FIREHOSE_KINDS') ?? '0, 1, 3, 5, 6, 7, 9735, 10002')
return (Deno.env.get('FIREHOSE_KINDS') ?? '0, 1, 3, 5, 6, 7, 20, 9735, 10002')
.split(/[, ]+/g)
.map(Number);
}

View file

@ -234,7 +234,7 @@ const accountStatusesController: AppController = async (c) => {
const filter: NostrFilter = {
authors: [pubkey],
kinds: [1, 6],
kinds: [1, 6, 20],
since,
until,
limit,
@ -473,7 +473,7 @@ const favouritesController: AppController = async (c) => {
.map((event) => event.tags.find((tag) => tag[0] === 'e')?.[1])
.filter((id): id is string => !!id);
const events1 = await store.query([{ kinds: [1], ids }], { signal })
const events1 = await store.query([{ kinds: [1, 20], ids }], { signal })
.then((events) => hydrateEvents({ events, store, signal }));
const viewerPubkey = await c.get('signer')?.getPublicKey();

View file

@ -260,7 +260,7 @@ export const statusZapSplitsController: AppController = async (c) => {
const id = c.req.param('id');
const { signal } = c.req.raw;
const [event] = await store.query([{ kinds: [1], ids: [id], limit: 1 }], { signal });
const [event] = await store.query([{ kinds: [1, 20], ids: [id], limit: 1 }], { signal });
if (!event) {
return c.json({ error: 'Event not found' }, 404);
}

View file

@ -20,7 +20,7 @@ const reactionController: AppController = async (c) => {
}
const store = await Storages.db();
const [event] = await store.query([{ kinds: [1], ids: [id], limit: 1 }]);
const [event] = await store.query([{ kinds: [1, 20], ids: [id], limit: 1 }]);
if (!event) {
return c.json({ error: 'Status not found' }, 404);
@ -56,7 +56,7 @@ const deleteReactionController: AppController = async (c) => {
}
const [event] = await store.query([
{ kinds: [1], ids: [id], limit: 1 },
{ kinds: [1, 20], ids: [id], limit: 1 },
]);
if (!event) {

View file

@ -166,7 +166,7 @@ async function getLookupFilters({ q, type, resolve }: SearchQuery, signal: Abort
if (n.id().safeParse(q).success) {
const filters: NostrFilter[] = [];
if (accounts) filters.push({ kinds: [0], authors: [q] });
if (statuses) filters.push({ kinds: [1], ids: [q] });
if (statuses) filters.push({ kinds: [1, 20], ids: [q] });
return filters;
}
@ -184,10 +184,10 @@ async function getLookupFilters({ q, type, resolve }: SearchQuery, signal: Abort
if (accounts) filters.push({ kinds: [0], authors: [result.data.pubkey] });
break;
case 'note':
if (statuses) filters.push({ kinds: [1], ids: [result.data] });
if (statuses) filters.push({ kinds: [1, 20], ids: [result.data] });
break;
case 'nevent':
if (statuses) filters.push({ kinds: [1], ids: [result.data.id] });
if (statuses) filters.push({ kinds: [1, 20], ids: [result.data.id] });
break;
}
return filters;

View file

@ -290,7 +290,7 @@ const deleteStatusController: AppController = async (c) => {
const contextController: AppController = async (c) => {
const id = c.req.param('id');
const store = c.get('store');
const event = await getEvent(id, { kind: 1, relations: ['author', 'event_stats', 'author_stats'] });
const [event] = await store.query([{ kinds: [1, 20], ids: [id] }]);
const viewerPubkey = await c.get('signer')?.getPublicKey();
async function renderStatuses(events: NostrEvent[]) {
@ -325,7 +325,8 @@ const contextController: AppController = async (c) => {
const favouriteController: AppController = async (c) => {
const id = c.req.param('id');
const target = await getEvent(id, { kind: 1, relations: ['author', 'event_stats', 'author_stats'] });
const store = await Storages.db();
const [target] = await store.query([{ ids: [id], kinds: [1, 20] }]);
if (target) {
await createEvent({
@ -337,6 +338,8 @@ const favouriteController: AppController = async (c) => {
],
}, c);
await hydrateEvents({ events: [target], store });
const status = await renderStatus(target, { viewerPubkey: await c.get('signer')?.getPublicKey() });
if (status) {
@ -397,7 +400,7 @@ const unreblogStatusController: AppController = async (c) => {
const pubkey = await c.get('signer')?.getPublicKey()!;
const store = await Storages.db();
const [event] = await store.query([{ ids: [eventId], kinds: [1] }]);
const [event] = await store.query([{ ids: [eventId], kinds: [1, 20] }]);
if (!event) {
return c.json({ error: 'Record not found' }, 404);
}
@ -429,13 +432,13 @@ const quotesController: AppController = async (c) => {
const params = c.get('pagination');
const store = await Storages.db();
const [event] = await store.query([{ ids: [id], kinds: [1] }]);
const [event] = await store.query([{ ids: [id], kinds: [1, 20] }]);
if (!event) {
return c.json({ error: 'Event not found.' }, 404);
}
const quotes = await store
.query([{ kinds: [1], '#q': [event.id], ...params }])
.query([{ kinds: [1, 20], '#q': [event.id], ...params }])
.then((events) => hydrateEvents({ events, store }));
const viewerPubkey = await c.get('signer')?.getPublicKey();

View file

@ -214,20 +214,20 @@ async function topicToFilter(
switch (topic) {
case 'public':
return { kinds: [1, 6] };
return { kinds: [1, 6, 20] };
case 'public:local':
return { kinds: [1, 6], search: `domain:${host}` };
return { kinds: [1, 6, 20], search: `domain:${host}` };
case 'hashtag':
if (query.tag) return { kinds: [1, 6], '#t': [query.tag] };
if (query.tag) return { kinds: [1, 6, 20], '#t': [query.tag] };
break;
case 'hashtag:local':
if (query.tag) return { kinds: [1, 6], '#t': [query.tag], search: `domain:${host}` };
if (query.tag) return { kinds: [1, 6, 20], '#t': [query.tag], search: `domain:${host}` };
break;
case 'user':
// HACK: this puts the user's entire contacts list into RAM,
// and then calls `matchFilters` over it. Refreshing the page
// is required after following a new user.
return pubkey ? { kinds: [1, 6], authors: [...await getFeedPubkeys(pubkey)] } : undefined;
return pubkey ? { kinds: [1, 6, 20], authors: [...await getFeedPubkeys(pubkey)] } : undefined;
}
}

View file

@ -14,7 +14,7 @@ const homeTimelineController: AppController = async (c) => {
const params = c.get('pagination');
const pubkey = await c.get('signer')?.getPublicKey()!;
const authors = [...await getFeedPubkeys(pubkey)];
return renderStatuses(c, [{ authors, kinds: [1, 6], ...params }]);
return renderStatuses(c, [{ authors, kinds: [1, 6, 20], ...params }]);
};
const publicQuerySchema = z.object({
@ -33,7 +33,7 @@ const publicTimelineController: AppController = (c) => {
const { local, instance, language } = result.data;
const filter: NostrFilter = { kinds: [1], ...params };
const filter: NostrFilter = { kinds: [1, 20], ...params };
const search: `${string}:${string}`[] = [];
@ -57,7 +57,7 @@ const publicTimelineController: AppController = (c) => {
const hashtagTimelineController: AppController = (c) => {
const hashtag = c.req.param('hashtag')!.toLowerCase();
const params = c.get('pagination');
return renderStatuses(c, [{ kinds: [1], '#t': [hashtag], ...params }]);
return renderStatuses(c, [{ kinds: [1, 20], '#t': [hashtag], ...params }]);
};
const suggestedTimelineController: AppController = async (c) => {
@ -70,7 +70,7 @@ const suggestedTimelineController: AppController = async (c) => {
const authors = [...getTagSet(follows?.tags ?? [], 'p')];
return renderStatuses(c, [{ authors, kinds: [1], ...params }]);
return renderStatuses(c, [{ authors, kinds: [1, 20], ...params }]);
};
/** Render statuses for timelines. */

View file

@ -134,7 +134,7 @@ const trendingStatusesController: AppController = async (c) => {
return c.json([]);
}
const results = await store.query([{ kinds: [1], ids }])
const results = await store.query([{ kinds: [1, 20], ids }])
.then((events) => hydrateEvents({ events, store }));
// Sort events in the order they appear in the label.

View file

@ -16,7 +16,7 @@ interface GetEventOpts {
signal?: AbortSignal;
/** Event kind. */
kind?: number;
/** Relations to include on the event. */
/** @deprecated Relations to include on the event. */
relations?: DittoRelation[];
}

View file

@ -283,6 +283,7 @@ class EventsDB extends NPostgres {
case 0:
return EventsDB.buildUserSearchContent(event);
case 1:
case 20:
return nip27.replaceAll(event.content, () => '');
case 30009:
return EventsDB.buildTagsSearchContent(event.tags.filter(([t]) => t !== 'alt'));

View file

@ -102,21 +102,21 @@ export function assembleEvents(
if (event.kind === 1) {
const id = findQuoteTag(event.tags)?.[1] || findQuoteInContent(event.content);
if (id) {
event.quote = b.find((e) => matchFilter({ kinds: [1], ids: [id] }, e));
event.quote = b.find((e) => matchFilter({ kinds: [1, 20], ids: [id] }, e));
}
}
if (event.kind === 6) {
const id = event.tags.find(([name]) => name === 'e')?.[1];
if (id) {
event.repost = b.find((e) => matchFilter({ kinds: [1], ids: [id] }, e));
event.repost = b.find((e) => matchFilter({ kinds: [1, 20], ids: [id] }, e));
}
}
if (event.kind === 7) {
const id = event.tags.findLast(([name]) => name === 'e')?.[1];
if (id) {
event.reacted = b.find((e) => matchFilter({ kinds: [1], ids: [id] }, e));
event.reacted = b.find((e) => matchFilter({ kinds: [1, 20], ids: [id] }, e));
}
}
@ -130,7 +130,7 @@ export function assembleEvents(
const ids = event.tags.filter(([name]) => name === 'e').map(([_name, value]) => value);
for (const id of ids) {
const reported = b.find((e) => matchFilter({ kinds: [1], ids: [id] }, e));
const reported = b.find((e) => matchFilter({ kinds: [1, 20], ids: [id] }, e));
if (reported) {
reportedEvents.push(reported);
}
@ -146,7 +146,7 @@ export function assembleEvents(
const id = event.tags.find(([name]) => name === 'e')?.[1];
if (id) {
event.zapped = b.find((e) => matchFilter({ kinds: [1], ids: [id] }, e));
event.zapped = b.find((e) => matchFilter({ kinds: [1, 20], ids: [id] }, e));
}
const zapRequestString = event?.tags?.find(([name]) => name === 'description')?.[1];
@ -313,7 +313,7 @@ function gatherReportedNotes({ events, store, signal }: HydrateOpts): Promise<Di
}
return store.query(
[{ kinds: [1], ids: [...ids], limit: ids.size }],
[{ kinds: [1, 20], ids: [...ids], limit: ids.size }],
{ signal },
);
}

View file

@ -273,7 +273,7 @@ export async function countAuthorStats(
): Promise<DittoTables['author_stats']> {
const [{ count: followers_count }, { count: notes_count }, [followList], [kind0]] = await Promise.all([
store.count([{ kinds: [3], '#p': [pubkey] }]),
store.count([{ kinds: [1], authors: [pubkey] }]),
store.count([{ kinds: [1, 20], authors: [pubkey] }]),
store.query([{ kinds: [3], authors: [pubkey], limit: 1 }]),
store.query([{ kinds: [0], authors: [pubkey], limit: 1 }]),
]);

View file

@ -75,7 +75,7 @@ async function renderStatuses(c: AppContext, ids: string[], signal = AbortSignal
const store = await Storages.db();
const { limit } = c.get('pagination');
const events = await store.query([{ kinds: [1], ids, limit }], { signal })
const events = await store.query([{ kinds: [1, 20], ids, limit }], { signal })
.then((events) => hydrateEvents({ events, store, signal }));
if (!events.length) {