From 278e13a6ef28cb51c2c8d3a92f4870022be53958 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 28 Mar 2025 14:53:24 -0500 Subject: [PATCH 01/11] Add .goosehints file --- .goosehints | 43 +++++++++++++++++++++++++++++++++++++++++++ .vscode/settings.json | 5 ++++- 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 .goosehints diff --git a/.goosehints b/.goosehints new file mode 100644 index 00000000..4e17068c --- /dev/null +++ b/.goosehints @@ -0,0 +1,43 @@ +# Ditto + +This project is called Ditto, a self-hosted social media server written in TypeScript with Deno. It implements the [Nostr Protocol](https://raw.githubusercontent.com/nostr-protocol/nips/refs/heads/master/README.md), and parts of the [Mastodon API](https://docs.joinmastodon.org/methods/) and [Pleroma API](https://git.pleroma.social/pleroma/pleroma/-/raw/develop/docs/development/API/pleroma_api.md). + +## Project Structure + +Ditto is a monorepo with a `packages` directory. The main package is `packages/ditto`, and the main API definition is in `packages/ditto/app.ts`. + +## Deno, npm, and jsr + +Ditto uses Deno 2.x + +Dependencies are managed in `deno.json`, which are added with the `deno add` command. This command also updates the `deno.lock` file. npm packages can be added by using `deno add` and prefixing the package name with an `npm:` protocol. For example, `deno add npm:kysely` would add the `kysely` package from npm. + +[jsr](https://jsr.io/) is a modern alternative to npm. It's a completely different registry with different packages available. jsr packages can be added by using `deno add` and prefixing the package name with a `jsr:` protocol. For example, `deno add jsr:@std/assert` would add the `@std/assert` package from jsr. + +## Nostr + +Nostr is a decentralized social media protocol involving clients, relays, keys, and a unified Nostr event format. + +Specifications on Nostr are called "NIPs". NIP stands for "Nostr Implementation Possibilities". NIPs are numbered like `NIP-XX` where `XX` are two capitalized hexadecimal digits, eg `NIP-01` and `NIP-C7`. + +To learn about Nostr, use the fetch tool to read [NIP-01](https://raw.githubusercontent.com/nostr-protocol/nips/refs/heads/master/01.md). + +To read a specific NIP, construct the NIP URL following this template: `https://raw.githubusercontent.com/nostr-protocol/nips/refs/heads/master/{nip}.md` (replace `{nip}` in the URL template with the relevant NIP name, eg `07` for NIP-07, or `C7` for NIP-C7). Then use the fetch tool to read the URL. + +To discover the full list of NIPs, use the fetch tool to read the [NIPs README](https://raw.githubusercontent.com/nostr-protocol/nips/refs/heads/master/README.md). + +It's important that Ditto conforms to Nostr standards. Please read as much of the NIPs as you need to have a full understanding before adding or modifying Nostr events and filters. It is possible to add new ideas to Nostr that don't exist yet in the NIPs, but only after other options have been explored. Care must be taken when adding new Nostr ideas, to ensure they fit seamlessly within the existing Nostr ecosystem. + +## How Ditto uses Nostr and Mastodon API + +Ditto implements a full Nostr relay, available at `/relay` of the Ditto server. + +Mastodon API functionality, available at `/api/*, is built around the Nostr relay's storage implementation. + +Ditto's goal is to enable Mastodon API clients to interact directly with Nostr. It achieves this by implementing most of Mastodon's API, and "pretending" to be a Mastodon server to client applications, while in actuality it uses Nostr as its decentralized protocol layer. + +## Testing Changes + +After making changes, please run `deno task check` to check for type errors. If there are any type errors, please try to fix them. + +Afterwards, run `deno fmt` to format the code, and then you are done. Please do not try to run the server, or run any other tests. \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index ba5b1bb4..8a761ba9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,8 @@ "deno.enable": true, "deno.lint": true, "editor.defaultFormatter": "denoland.vscode-deno", - "path-intellisense.extensionOnImport": true + "path-intellisense.extensionOnImport": true, + "files.associations": { + ".goosehints": "markdown" + } } From 34a29c8f4e6454d428d6546a5b060a02fc1c4d6d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 28 Mar 2025 16:15:20 -0500 Subject: [PATCH 02/11] NIP-11 improvements --- packages/ditto/controllers/nostr/relay-info.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ditto/controllers/nostr/relay-info.ts b/packages/ditto/controllers/nostr/relay-info.ts index 945e311b..f9df43d0 100644 --- a/packages/ditto/controllers/nostr/relay-info.ts +++ b/packages/ditto/controllers/nostr/relay-info.ts @@ -9,6 +9,8 @@ const relayInfoController: AppController = async (c) => { const meta = await getInstanceMetadata(c.var); c.res.headers.set('access-control-allow-origin', '*'); + c.res.headers.set('access-control-allow-headers', '*'); + c.res.headers.set('access-control-allow-methods', 'GET, POST, OPTIONS'); return c.json({ name: meta.name, @@ -16,7 +18,7 @@ const relayInfoController: AppController = async (c) => { pubkey: await conf.signer.getPublicKey(), contact: meta.email, supported_nips: [1, 5, 9, 11, 16, 45, 50, 46, 98], - software: 'Ditto', + software: 'https://gitlab.com/soapbox-pub/ditto', version: denoJson.version, limitation: { auth_required: false, From 1a6c114a57b76ebd14615b08b6fad63ec7f30730 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 28 Mar 2025 16:22:03 -0500 Subject: [PATCH 03/11] Fix inline codeblock in .goosehints --- .goosehints | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.goosehints b/.goosehints index 4e17068c..f22691c7 100644 --- a/.goosehints +++ b/.goosehints @@ -32,7 +32,7 @@ It's important that Ditto conforms to Nostr standards. Please read as much of th Ditto implements a full Nostr relay, available at `/relay` of the Ditto server. -Mastodon API functionality, available at `/api/*, is built around the Nostr relay's storage implementation. +Mastodon API functionality, available at `/api/*`, is built around the Nostr relay's storage implementation. Ditto's goal is to enable Mastodon API clients to interact directly with Nostr. It achieves this by implementing most of Mastodon's API, and "pretending" to be a Mastodon server to client applications, while in actuality it uses Nostr as its decentralized protocol layer. From 8dc9ea98e2811f51efb474716da9b981f2f50c27 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 28 Mar 2025 17:50:51 -0500 Subject: [PATCH 04/11] Fix relay always sending a CLOSED message after the client sends CLOSE --- packages/ditto/controllers/nostr/relay.ts | 4 +++- packages/ditto/storages/DittoPgStore.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/ditto/controllers/nostr/relay.ts b/packages/ditto/controllers/nostr/relay.ts index bbec9b5c..af233c86 100644 --- a/packages/ditto/controllers/nostr/relay.ts +++ b/packages/ditto/controllers/nostr/relay.ts @@ -165,6 +165,8 @@ function connectStream(socket: WebSocket, ip: string | undefined, opts: ConnectS try { for await (const msg of relay.req(filters, { limit: 100, signal, timeout: conf.db.timeouts.relay })) { + if (!controllers.has(subId)) break; + if (msg[0] === 'EVENT') { const [, , event] = msg; send(['EVENT', subId, purifyEvent(event)]); @@ -186,8 +188,8 @@ function connectStream(socket: WebSocket, ip: string | undefined, opts: ConnectS send(['CLOSED', subId, 'error: something went wrong']); } } finally { - controllers.get(subId)?.abort(); controllers.delete(subId); + controller.abort(); } } diff --git a/packages/ditto/storages/DittoPgStore.ts b/packages/ditto/storages/DittoPgStore.ts index e8a969be..a1499f1d 100644 --- a/packages/ditto/storages/DittoPgStore.ts +++ b/packages/ditto/storages/DittoPgStore.ts @@ -324,7 +324,7 @@ export class DittoPgStore extends NPostgres { machina.push(['EOSE', subId]); }).catch((error) => { - if (error instanceof Error && error.message.includes('timeout')) { + if (error instanceof Error && (error.name === 'TimeoutError' || error.message.includes('timeout'))) { machina.push(['CLOSED', subId, 'error: the relay could not respond fast enough']); } else { machina.push(['CLOSED', subId, 'error: something went wrong']); @@ -361,7 +361,7 @@ export class DittoPgStore extends NPostgres { yield msg; } } catch (e) { - if (e instanceof Error && e.name === 'AbortError') { + if (e instanceof Error && (e.name === 'TimeoutError' || e.message.includes('timeout'))) { yield ['CLOSED', subId, 'error: the relay could not respond fast enough']; } else { yield ['CLOSED', subId, 'error: something went wrong']; From c8b1c2050bf9f2c00d6b21af6c46e92f717c406e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 1 Apr 2025 18:52:36 -0500 Subject: [PATCH 05/11] Publish nip89 application handler on startup --- .goosehints | 2 ++ packages/ditto/app.ts | 2 ++ packages/ditto/utils/instance.ts | 1 + packages/ditto/utils/nip89.ts | 34 ++++++++++++++++++++++++++++++++ 4 files changed, 39 insertions(+) create mode 100644 packages/ditto/utils/nip89.ts diff --git a/.goosehints b/.goosehints index f22691c7..2d76b934 100644 --- a/.goosehints +++ b/.goosehints @@ -24,6 +24,8 @@ To learn about Nostr, use the fetch tool to read [NIP-01](https://raw.githubuser To read a specific NIP, construct the NIP URL following this template: `https://raw.githubusercontent.com/nostr-protocol/nips/refs/heads/master/{nip}.md` (replace `{nip}` in the URL template with the relevant NIP name, eg `07` for NIP-07, or `C7` for NIP-C7). Then use the fetch tool to read the URL. +To read the definition of a specific kind, construct a URL following this template: `https://nostrbook.dev/kinds/{kind}.md` (replace `{kind}` in the template with the kind number, eg `https://nostrbook.dev/kinds/0.md` for kind 0). + To discover the full list of NIPs, use the fetch tool to read the [NIPs README](https://raw.githubusercontent.com/nostr-protocol/nips/refs/heads/master/README.md). It's important that Ditto conforms to Nostr standards. Please read as much of the NIPs as you need to have a full understanding before adding or modifying Nostr events and filters. It is possible to add new ideas to Nostr that don't exist yet in the NIPs, but only after other options have been explored. Care must be taken when adding new Nostr ideas, to ensure they fit seamlessly within the existing Nostr ecosystem. diff --git a/packages/ditto/app.ts b/packages/ditto/app.ts index bd86ac51..b4ecbaec 100644 --- a/packages/ditto/app.ts +++ b/packages/ditto/app.ts @@ -16,6 +16,7 @@ import { startSentry } from '@/sentry.ts'; import { DittoAPIStore } from '@/storages/DittoAPIStore.ts'; import { DittoPgStore } from '@/storages/DittoPgStore.ts'; import { DittoPool } from '@/storages/DittoPool.ts'; +import { createNip89 } from '@/utils/nip89.ts'; import { Time } from '@/utils/time.ts'; import { seedZapSplits } from '@/utils/zap-split.ts'; @@ -198,6 +199,7 @@ const pgstore = new DittoPgStore({ const pool = new DittoPool({ conf, relay: pgstore }); const relay = new DittoRelayStore({ db, conf, pool, relay: pgstore }); +await createNip89({ conf, relay }); await seedZapSplits({ conf, relay }); if (conf.firehoseEnabled) { diff --git a/packages/ditto/utils/instance.ts b/packages/ditto/utils/instance.ts index 52fe7358..df021478 100644 --- a/packages/ditto/utils/instance.ts +++ b/packages/ditto/utils/instance.ts @@ -44,6 +44,7 @@ export async function getInstanceMetadata(opts: GetInstanceMetadataOpts): Promis tagline: meta.tagline ?? meta.about ?? 'Nostr community server', email: meta.email ?? `postmaster@${conf.url.host}`, picture: meta.picture ?? conf.local('/images/thumbnail.png'), + website: meta.website ?? conf.localDomain, event, screenshots: meta.screenshots ?? [], }; diff --git a/packages/ditto/utils/nip89.ts b/packages/ditto/utils/nip89.ts new file mode 100644 index 00000000..2e0cd0d1 --- /dev/null +++ b/packages/ditto/utils/nip89.ts @@ -0,0 +1,34 @@ +import { DittoConf } from '@ditto/conf'; + +import { getInstanceMetadata } from '@/utils/instance.ts'; + +import type { NStore } from '@nostrify/nostrify'; + +interface CreateNip89Opts { + conf: DittoConf; + relay: NStore; + signal?: AbortSignal; +} + +/** + * Creates a NIP-89 application handler event (kind 31990) + * This identifies Ditto as a client that can handle various kinds of events + */ +export async function createNip89(opts: CreateNip89Opts): Promise { + const { conf, relay, signal } = opts; + + const { event: _, ...metadata } = await getInstanceMetadata(opts); + + const event = await conf.signer.signEvent({ + kind: 31990, + tags: [ + ['d', 'ditto'], + ['k', '1'], + ['web', conf.local('/'), 'web'], + ], + content: JSON.stringify(metadata), + created_at: Math.floor(Date.now() / 1000), + }); + + await relay.event(event, { signal }); +} From caf59f4078f3033341c542b6ea9078ed1be195c4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 1 Apr 2025 19:49:31 -0500 Subject: [PATCH 06/11] Insert NIP-89 "client" tags when users post statuses --- packages/ditto/controllers/api/statuses.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/ditto/controllers/api/statuses.ts b/packages/ditto/controllers/api/statuses.ts index e80441b6..9d44ac56 100644 --- a/packages/ditto/controllers/api/statuses.ts +++ b/packages/ditto/controllers/api/statuses.ts @@ -17,6 +17,7 @@ import { languageSchema } from '@/schema.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { assertAuthenticated, createEvent, parseBody, updateListEvent } from '@/utils/api.ts'; import { getCustomEmojis } from '@/utils/custom-emoji.ts'; +import { getInstanceMetadata } from '@/utils/instance.ts'; import { getInvoice, getLnurl } from '@/utils/lnurl.ts'; import { purifyEvent } from '@/utils/purify.ts'; import { getZapSplits } from '@/utils/zap-split.ts'; @@ -25,6 +26,7 @@ import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; const createStatusSchema = z.object({ + disclose_client: z.boolean().nullish(), in_reply_to_id: n.id().nullish(), language: languageSchema.nullish(), media_ids: z.string().array().nullish(), @@ -265,6 +267,11 @@ const createStatusController: AppController = async (c) => { content += mediaUrls.join('\n'); } + if (data.disclose_client) { + const { name } = await getInstanceMetadata(c.var); + tags.push(['client', name, `31990:${await conf.signer.getPublicKey()}:ditto`, conf.relay]); + } + const event = await createEvent({ kind: 1, content, From 23eb5313057b7ff6359736267204df74e61e559f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 1 Apr 2025 20:08:13 -0500 Subject: [PATCH 07/11] Render client tags --- packages/ditto/interfaces/DittoEvent.ts | 1 + packages/ditto/storages/hydrate.ts | 38 ++++++++++++++++++++++- packages/ditto/views/mastodon/statuses.ts | 19 ++++++++++-- packages/mastoapi/types/MastodonStatus.ts | 5 ++- 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/packages/ditto/interfaces/DittoEvent.ts b/packages/ditto/interfaces/DittoEvent.ts index cdd4343d..62f6c626 100644 --- a/packages/ditto/interfaces/DittoEvent.ts +++ b/packages/ditto/interfaces/DittoEvent.ts @@ -56,4 +56,5 @@ export interface DittoEvent extends NostrEvent { zap_message?: string; /** Language of the event (kind 1s are more accurate). */ language?: LanguageCode; + client?: DittoEvent; } diff --git a/packages/ditto/storages/hydrate.ts b/packages/ditto/storages/hydrate.ts index d2c64e90..7568a2b6 100644 --- a/packages/ditto/storages/hydrate.ts +++ b/packages/ditto/storages/hydrate.ts @@ -1,6 +1,6 @@ import { DittoDB, DittoTables } from '@ditto/db'; import { DittoConf } from '@ditto/conf'; -import { NStore } from '@nostrify/nostrify'; +import { type NostrFilter, NStore } from '@nostrify/nostrify'; import { Kysely } from 'kysely'; import { matchFilter } from 'nostr-tools'; import { NSchema as n } from '@nostrify/nostrify'; @@ -50,6 +50,10 @@ async function hydrateEvents(opts: HydrateOpts): Promise { cache.push(event); } + for (const event of await gatherClients({ ...opts, events: cache })) { + cache.push(event); + } + const authorStats = await gatherAuthorStats(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.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) { const id = findQuoteTag(event.tags)?.[1] || findQuoteInContent(event.content); if (id) { @@ -353,6 +367,28 @@ async function gatherInfo({ conf, events, relay, signal }: HydrateOpts): Promise ); } +function gatherClients({ events, relay, signal }: HydrateOpts): Promise { + 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. */ async function gatherAuthorStats( events: DittoEvent[], diff --git a/packages/ditto/views/mastodon/statuses.ts b/packages/ditto/views/mastodon/statuses.ts index ba2e8d86..579aea2b 100644 --- a/packages/ditto/views/mastodon/statuses.ts +++ b/packages/ditto/views/mastodon/statuses.ts @@ -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 { Conf } from '@/config.ts'; @@ -118,11 +118,27 @@ async function renderStatus( return acc; }, [] 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); return { id: event.id, account, + application, card: event.event_stats?.link_preview ?? null, content: compatMentions + html, created_at: nostrDate(event.created_at).toISOString(), @@ -142,7 +158,6 @@ async function renderStatus( bookmarked: Boolean(bookmarkEvent), pinned: Boolean(pinEvent), reblog: null, - application: null, media_attachments: media .map((m) => renderAttachment({ tags: m })) .filter((m): m is MastodonAttachment => Boolean(m)), diff --git a/packages/mastoapi/types/MastodonStatus.ts b/packages/mastoapi/types/MastodonStatus.ts index 019e5a7b..db99d847 100644 --- a/packages/mastoapi/types/MastodonStatus.ts +++ b/packages/mastoapi/types/MastodonStatus.ts @@ -5,6 +5,10 @@ import type { MastodonPreviewCard } from './MastodonPreviewCard.ts'; export interface MastodonStatus { id: string; account: MastodonAccount; + application?: { + name: string; + website: string | null; + }; card: MastodonPreviewCard | null; content: string; created_at: string; @@ -24,7 +28,6 @@ export interface MastodonStatus { bookmarked: boolean; pinned: boolean; reblog: MastodonStatus | null; - application: unknown; media_attachments: MastodonAttachment[]; mentions: unknown[]; tags: unknown[]; From c425a9d39a34d6a9d0124b7a8c29640e55ead0db Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 3 Apr 2025 12:29:45 -0500 Subject: [PATCH 08/11] DittoPgStore: index "client" tag --- packages/ditto/storages/DittoPgStore.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ditto/storages/DittoPgStore.ts b/packages/ditto/storages/DittoPgStore.ts index a1499f1d..63d32d56 100644 --- a/packages/ditto/storages/DittoPgStore.ts +++ b/packages/ditto/storages/DittoPgStore.ts @@ -83,6 +83,7 @@ export class DittoPgStore extends NPostgres { /** Conditions for when to index certain tags. */ static tagConditions: Record = { 'a': ({ count }) => count < 15, + 'client': ({ count, value }) => count === 0 && value.length < 50, 'd': ({ event, count }) => count === 0 && NKinds.parameterizedReplaceable(event.kind), 'e': DittoPgStore.eTagCondition, 'k': ({ count }) => count < 3, From 0abee76e38539a9540339c0111fd463d1eac249c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 3 Apr 2025 12:29:58 -0500 Subject: [PATCH 09/11] nip89: add ["t", "ditto"] tag to application handler event --- packages/ditto/utils/nip89.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ditto/utils/nip89.ts b/packages/ditto/utils/nip89.ts index 2e0cd0d1..b0d7ba4b 100644 --- a/packages/ditto/utils/nip89.ts +++ b/packages/ditto/utils/nip89.ts @@ -24,6 +24,7 @@ export async function createNip89(opts: CreateNip89Opts): Promise { tags: [ ['d', 'ditto'], ['k', '1'], + ['t', 'ditto'], ['web', conf.local('/'), 'web'], ], content: JSON.stringify(metadata), From 621d92dcf63af8f7dda8173e8dd8d44a3c64f3dd Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 3 Apr 2025 12:31:10 -0500 Subject: [PATCH 10/11] Render "client" tags in statuses even if they don't have an application handler event --- packages/ditto/views/mastodon/statuses.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/ditto/views/mastodon/statuses.ts b/packages/ditto/views/mastodon/statuses.ts index 579aea2b..58c9653e 100644 --- a/packages/ditto/views/mastodon/statuses.ts +++ b/packages/ditto/views/mastodon/statuses.ts @@ -123,7 +123,7 @@ async function renderStatus( 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]; + const name = result.data.name ?? event.tags.find(([name]) => name === 'client')?.[1]; if (name) { application = { name, @@ -131,6 +131,14 @@ async function renderStatus( }; } } + } else { + const name = event.tags.find(([name]) => name === 'client')?.[1]; + if (name) { + application = { + name, + website: null, + }; + } } const expiresAt = new Date(Number(event.tags.find(([name]) => name === 'expiration')?.[1]) * 1000); From 1c2d7a67931476461e12af7e25cca2e010516292 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 3 Apr 2025 12:38:39 -0500 Subject: [PATCH 11/11] Index client addr in search extensions --- packages/ditto/storages/DittoPgStore.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/ditto/storages/DittoPgStore.ts b/packages/ditto/storages/DittoPgStore.ts index 63d32d56..5152ec09 100644 --- a/packages/ditto/storages/DittoPgStore.ts +++ b/packages/ditto/storages/DittoPgStore.ts @@ -522,6 +522,12 @@ export class DittoPgStore extends NPostgres { } } + const client = event.tags.find(([name]) => name === 'client')?.[2]; + + if (client && /^31990:([0-9a-f]{64}):(.+)$/.test(client)) { + ext.client = client; + } + ext.protocol = event.tags.find(([name]) => name === 'proxy')?.[2] ?? 'nostr'; return ext;