pubkey ?? '')); if (64 !== \strlen($pubkeyHex) || !ctype_xdigit($pubkeyHex)) { return null; } $key = new Key(); $npub = $key->convertPublicKeyToBech32($pubkeyHex); $kind = (int) ($event->kind ?? 0); $d = self::dTagFromWireEvent($event); $eventIdHex = strtolower((string) ($event->id ?? '')); if (Nip19Addressable::isParameterizedReplaceableKind($kind) && $d !== null) { $naddr = Nip19Addressable::naddrBech32($kind, $pubkeyHex, $d, $relayHints); $neventForRev = (64 === \strlen($eventIdHex) && ctype_xdigit($eventIdHex)) ? $this->nip19->encodeNevent($eventIdHex, $relayHints, $pubkeyHex, $kind) : null; return new NostrShareMenuContext( $npub, $neventForRev, $naddr, $this->feedJumble($naddr), ); } if (64 === \strlen($eventIdHex) && ctype_xdigit($eventIdHex)) { $rebuilt = $this->nip19->encodeNevent($eventIdHex, $relayHints, $pubkeyHex, $kind); return new NostrShareMenuContext( $npub, $rebuilt, null, $this->feedJumble($rebuilt), ); } return new NostrShareMenuContext( $npub, null, null, $this->profileJumbleUrl($npub), ); } public function shareContextForArticle(Article $article): NostrShareMenuContext { return $this->fromArticle($article); } public function applyWireEventToRequest(Request $request, object $event, array $relayHints = []): void { $ctx = $this->shareContextFromWireEvent($event, $relayHints); if (null === $ctx || null === $ctx->npub) { return; } $request->attributes->set(self::ATTR_NPUB, $ctx->npub); if ($ctx->naddrBech32 !== null && $ctx->naddrBech32 !== '') { $request->attributes->set(self::ATTR_NADDR_BECH32, $ctx->naddrBech32); } if ($ctx->neventBech32 !== null && $ctx->neventBech32 !== '') { $request->attributes->set(self::ATTR_NEVENT_BECH32, $ctx->neventBech32); } } /** * @param list|\ArrayObject $event->tags */ private static function dTagFromWireEvent(object $event): ?string { if (!isset($event->tags)) { return null; } $rows = $event->tags; if ($rows instanceof \ArrayObject) { $rows = $rows->getArrayCopy(); } if (!\is_array($rows)) { return null; } $norm = array_values( array_map( static function ($r) { if (!\is_array($r) && !\is_object($r)) { return $r; } if (\is_object($r)) { $r = (array) $r; } return $r; }, $rows ) ); return Nip19Addressable::dTagFromTagRows($norm); } public function __construct( private readonly MagazineIndexStore $magazineIndexStore, private readonly ArticleRepository $articleRepository, private readonly Nip19Codec $nip19, #[Autowire('%npub%')] private readonly string $siteNpub, #[Autowire('%d_tag%')] private readonly string $rootDTag, #[Autowire('%jumble_profile_users_base%')] private readonly string $jumbleProfileUsersBase, #[Autowire('%jumble_feed_notes_base%')] private readonly string $jumbleFeedNotesBase, ) { } private function nostrKey(): Key { return new Key(); } /** * Context for the header Nostr menu. Always returns a context on real HTTP requests (never null). * Templates that do not include the header never call this; no need to suppress on XHR / fragments. */ public function buildForRequest(Request $request): NostrShareMenuContext { $route = (string) $request->attributes->get('_route', ''); if ('' === $route) { return $this->siteWithRootMenu(); } return match ($route) { 'home' => $this->siteWithRootMenu(), 'article' => $this->forArticleNpubD( (string) $request->attributes->get('npub', ''), (string) $request->attributes->get('slug', ''), ), 'author-profile' => $this->forAuthorProfile($request->attributes->get('npub', '')), 'nevent' => $this->forNevent($request, (string) $request->attributes->get('nevent', '')), 'magazine-category' => $this->forCategory($request->attributes->get('slug', '')), 'articles', 'featured_authors', 'search', 'article-preview', 'article-preview-event', 'editor-create', 'editor-edit' => $this->siteWithRootMenu(), default => $this->siteWithRootMenu(), }; } private function forArticleNpubD(string $npub, string $slug): NostrShareMenuContext { if ($npub === '' || $slug === '' || !str_starts_with($npub, 'npub1')) { return $this->siteWithRootMenu(); } $list = $this->articleRepository->findBy(['slug' => $slug], ['createdAt' => 'DESC'], 1); $article = $list[0] ?? null; if ($article === null) { return $this->siteWithRootMenu(); } if ($this->nostrKey()->convertToHex($npub) !== strtolower((string) $article->getPubkey())) { return $this->siteWithRootMenu(); } return $this->fromArticle($article); } private function fromArticle(Article $article): NostrShareMenuContext { $npub = $this->nostrKey()->convertPublicKeyToBech32((string) $article->getPubkey()); $kind = (int) ($article->getKind()?->value ?? 30023); $d = (string) ($article->getSlug() ?? ''); if ($d === '') { return new NostrShareMenuContext( $npub, null, null, $this->profileJumbleUrl($npub), ); } $pk = strtolower((string) $article->getPubkey()); $naddr = Nip19Addressable::naddrBech32($kind, $pk, $d, []); $eid = strtolower((string) ($article->getEventId() ?? '')); $nevent = (64 === \strlen($eid) && ctype_xdigit($eid)) ? $this->nip19->encodeNevent($eid, [], $pk, $kind) : null; return new NostrShareMenuContext( $npub, $nevent, $naddr, $this->feedJumble($naddr), ); } private function forAuthorProfile(mixed $npubParam): NostrShareMenuContext { $npub = (string) $npubParam; if ($npub === '' || !str_starts_with($npub, 'npub1')) { return $this->siteWithRootMenu(); } return new NostrShareMenuContext( $npub, null, null, $this->profileJumbleUrl($npub), ); } private function forNevent(Request $request, string $neventFromRoute): NostrShareMenuContext { if ($request->attributes->has(self::ATTR_NPUB) && ($request->attributes->has(self::ATTR_NADDR_BECH32) || $request->attributes->has(self::ATTR_NEVENT_BECH32))) { $np = (string) $request->attributes->get(self::ATTR_NPUB); $naddrRaw = $request->attributes->get(self::ATTR_NADDR_BECH32); $naddr = \is_string($naddrRaw) && $naddrRaw !== '' ? $naddrRaw : null; $neventRaw = $request->attributes->get(self::ATTR_NEVENT_BECH32); $nb = \is_string($neventRaw) && $neventRaw !== '' ? $neventRaw : null; if (null !== $naddr || null !== $nb) { $jumble = $this->feedJumble($naddr ?? $nb); return new NostrShareMenuContext( $np, $nb, $naddr, $jumble, ); } } $nevent = $neventFromRoute; if ($nevent === '' || !str_starts_with($nevent, 'nevent1')) { return $this->siteWithRootMenu(); } try { $decoded = $this->nip19->decode($nevent); } catch (\Throwable) { return $this->siteWithRootMenu(); } if ($decoded->type !== 'nevent' || !isset($decoded->data->id)) { return $this->siteWithRootMenu(); } $eventId = strtolower((string) $decoded->data->id); if (64 !== \strlen($eventId) || !ctype_xdigit($eventId)) { return $this->siteWithRootMenu(); } $authorHex = $decoded->data->author ?? null; if (\is_string($authorHex) && 64 === \strlen($authorHex) && ctype_xdigit($authorHex)) { $authorHex = strtolower($authorHex); } else { $authorHex = null; } $kind = isset($decoded->data->kind) ? (int) $decoded->data->kind : 1; $relays = $decoded->data->relays ?? []; $relays = \is_array($relays) ? $relays : []; if ($authorHex !== null) { $rebuilt = $this->nip19->encodeNevent($eventId, $relays, $authorHex, $kind); return new NostrShareMenuContext( $this->nostrKey()->convertPublicKeyToBech32($authorHex), $rebuilt, null, $this->feedJumble($rebuilt), ); } return new NostrShareMenuContext( null, $nevent, null, $this->feedJumble($nevent), ); } private function forCategory(string $slug): NostrShareMenuContext { if ($slug === '') { return $this->siteWithRootMenu(); } $cat = $this->magazineIndexStore->getCategory($slug); if ($cat === null) { return $this->siteWithRootMenu(); } return $this->fromNostrEvent($cat) ?? $this->siteWithRootMenu(); } private function fromNostrEvent(Event $e): ?NostrShareMenuContext { $id = strtolower($e->getId()); if (64 !== \strlen($id) || !ctype_xdigit($id)) { return null; } $pk = strtolower($e->getPubkey()); if (64 !== \strlen($pk) || !ctype_xdigit($pk)) { return null; } $kind = (int) $e->getKind(); $d = Nip19Addressable::dTagFromEventEntity($e); $npub = $this->nostrKey()->convertPublicKeyToBech32($pk); if (Nip19Addressable::isParameterizedReplaceableKind($kind) && $d !== null) { $naddr = Nip19Addressable::naddrBech32($kind, $pk, $d, []); $neventForRev = $this->nip19->encodeNevent($id, [], $pk, $kind); return new NostrShareMenuContext( $npub, $neventForRev, $naddr, $this->feedJumble($naddr), ); } $nevent = $this->nip19->encodeNevent($id, [], $pk, $kind); return new NostrShareMenuContext( $npub, $nevent, null, $this->feedJumble($nevent), ); } private function siteWithRootMenu(): NostrShareMenuContext { $root = $this->magazineIndexStore->getRoot($this->siteNpub, $this->rootDTag); if (null === $fromRoot = $root ? $this->fromNostrEvent($root) : null) { return new NostrShareMenuContext( $this->siteNpub, null, null, $this->profileJumbleUrl($this->siteNpub), ); } return $fromRoot; } private function profileJumbleUrl(string $npub): string { $b = rtrim($this->jumbleProfileUsersBase, '/'); return $b === '' ? '#' : $b.'/'.$npub; } private function feedJumble(string $naddrOrNeventOrNoteBech32): string { $b = rtrim($this->jumbleFeedNotesBase, '/'); return $b === '' ? $naddrOrNeventOrNoteBech32 : $b.'/'.$naddrOrNeventOrNoteBech32; } }