Render client tags

This commit is contained in:
Alex Gleason 2025-04-01 20:08:13 -05:00
parent caf59f4078
commit 23eb531305
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
4 changed files with 59 additions and 4 deletions

View file

@ -56,4 +56,5 @@ export interface DittoEvent extends NostrEvent {
zap_message?: string; zap_message?: string;
/** Language of the event (kind 1s are more accurate). */ /** Language of the event (kind 1s are more accurate). */
language?: LanguageCode; language?: LanguageCode;
client?: DittoEvent;
} }

View file

@ -1,6 +1,6 @@
import { DittoDB, DittoTables } from '@ditto/db'; import { DittoDB, DittoTables } from '@ditto/db';
import { DittoConf } from '@ditto/conf'; import { DittoConf } from '@ditto/conf';
import { NStore } from '@nostrify/nostrify'; import { type NostrFilter, NStore } from '@nostrify/nostrify';
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import { matchFilter } from 'nostr-tools'; import { matchFilter } from 'nostr-tools';
import { NSchema as n } from '@nostrify/nostrify'; import { NSchema as n } from '@nostrify/nostrify';
@ -50,6 +50,10 @@ async function hydrateEvents(opts: HydrateOpts): Promise<DittoEvent[]> {
cache.push(event); cache.push(event);
} }
for (const event of await gatherClients({ ...opts, events: cache })) {
cache.push(event);
}
const authorStats = await gatherAuthorStats(cache, db.kysely); const authorStats = await gatherAuthorStats(cache, db.kysely);
const eventStats = await gatherEventStats(cache, db.kysely); const eventStats = await gatherEventStats(cache, db.kysely);
@ -128,6 +132,16 @@ export function assembleEvents(
event.user = b.find((e) => matchFilter({ kinds: [30382], authors: [admin], '#d': [event.pubkey] }, e)); event.user = b.find((e) => matchFilter({ kinds: [30382], authors: [admin], '#d': [event.pubkey] }, e));
event.info = b.find((e) => matchFilter({ kinds: [30383], authors: [admin], '#d': [event.id] }, e)); event.info = b.find((e) => matchFilter({ kinds: [30383], authors: [admin], '#d': [event.id] }, e));
for (const [name, _value, addr] of event.tags) {
if (name === 'client' && addr) {
const match = addr.match(/^31990:([0-9a-f]{64}):(.+)$/);
if (match) {
const [, pubkey, d] = match;
event.client = b.find((e) => matchFilter({ kinds: [31990], authors: [pubkey], '#d': [d] }, e));
}
}
}
if (event.kind === 1) { if (event.kind === 1) {
const id = findQuoteTag(event.tags)?.[1] || findQuoteInContent(event.content); const id = findQuoteTag(event.tags)?.[1] || findQuoteInContent(event.content);
if (id) { if (id) {
@ -353,6 +367,28 @@ async function gatherInfo({ conf, events, relay, signal }: HydrateOpts): Promise
); );
} }
function gatherClients({ events, relay, signal }: HydrateOpts): Promise<DittoEvent[]> {
const filters: NostrFilter[] = [];
for (const event of events) {
for (const [name, _value, addr] of event.tags) {
if (name === 'client' && addr) {
const match = addr.match(/^31990:([0-9a-f]{64}):(.+)$/);
if (match) {
const [, pubkey, d] = match;
filters.push({ kinds: [31990], authors: [pubkey], '#d': [d], limit: 1 });
}
}
}
}
if (!filters.length) {
return Promise.resolve([]);
}
return relay.query(filters, { signal });
}
/** Collect author stats from the events. */ /** Collect author stats from the events. */
async function gatherAuthorStats( async function gatherAuthorStats(
events: DittoEvent[], events: DittoEvent[],

View file

@ -1,4 +1,4 @@
import { NostrEvent, NStore } from '@nostrify/nostrify'; import { NostrEvent, NSchema as n, NStore } from '@nostrify/nostrify';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
@ -118,11 +118,27 @@ async function renderStatus(
return acc; return acc;
}, [] as { name: string; count: number; me: boolean; url?: string }[]); }, [] as { name: string; count: number; me: boolean; url?: string }[]);
let application: MastodonStatus['application'] = undefined;
if (event.client) {
const result = n.json().pipe(n.metadata()).safeParse(event.client.content);
if (result.success) {
const name = result.data.name ?? result.data.display_name ?? event.tags.find(([name]) => name === 'client')?.[1];
if (name) {
application = {
name,
website: result.data.website ?? null,
};
}
}
}
const expiresAt = new Date(Number(event.tags.find(([name]) => name === 'expiration')?.[1]) * 1000); const expiresAt = new Date(Number(event.tags.find(([name]) => name === 'expiration')?.[1]) * 1000);
return { return {
id: event.id, id: event.id,
account, account,
application,
card: event.event_stats?.link_preview ?? null, card: event.event_stats?.link_preview ?? null,
content: compatMentions + html, content: compatMentions + html,
created_at: nostrDate(event.created_at).toISOString(), created_at: nostrDate(event.created_at).toISOString(),
@ -142,7 +158,6 @@ async function renderStatus(
bookmarked: Boolean(bookmarkEvent), bookmarked: Boolean(bookmarkEvent),
pinned: Boolean(pinEvent), pinned: Boolean(pinEvent),
reblog: null, reblog: null,
application: null,
media_attachments: media media_attachments: media
.map((m) => renderAttachment({ tags: m })) .map((m) => renderAttachment({ tags: m }))
.filter((m): m is MastodonAttachment => Boolean(m)), .filter((m): m is MastodonAttachment => Boolean(m)),

View file

@ -5,6 +5,10 @@ import type { MastodonPreviewCard } from './MastodonPreviewCard.ts';
export interface MastodonStatus { export interface MastodonStatus {
id: string; id: string;
account: MastodonAccount; account: MastodonAccount;
application?: {
name: string;
website: string | null;
};
card: MastodonPreviewCard | null; card: MastodonPreviewCard | null;
content: string; content: string;
created_at: string; created_at: string;
@ -24,7 +28,6 @@ export interface MastodonStatus {
bookmarked: boolean; bookmarked: boolean;
pinned: boolean; pinned: boolean;
reblog: MastodonStatus | null; reblog: MastodonStatus | null;
application: unknown;
media_attachments: MastodonAttachment[]; media_attachments: MastodonAttachment[];
mentions: unknown[]; mentions: unknown[];
tags: unknown[]; tags: unknown[];