diff --git a/importmap.php b/importmap.php index 93f5f2d..713b8ac 100644 --- a/importmap.php +++ b/importmap.php @@ -59,6 +59,10 @@ return [ '@noble/hashes' => [ 'version' => '1.3.1', ], + // Required by nostr-tools/nip19 (bytesToHex / hexToBytes / concatBytes); bare @noble/hashes is not enough. + '@noble/hashes/utils' => [ + 'version' => '1.3.1', + ], '@scure/base' => [ 'version' => '1.1.1', ], diff --git a/src/Controller/AuthorController.php b/src/Controller/AuthorController.php index 791dfbd..4086b41 100644 --- a/src/Controller/AuthorController.php +++ b/src/Controller/AuthorController.php @@ -33,6 +33,11 @@ class AuthorController extends AbstractController ProfilePaymentLinksBuilder $profilePaymentLinks, ProfileIdentityLinksBuilder $profileIdentityLinks, ): Response { + // Profile pages chain several sequential Nostr REQ runs; match article pages so a slow relay + // set does not hit PHP’s default 30s max_execution_time during Twig render. + @set_time_limit(300); + @ini_set('max_execution_time', '300'); + $keys = new Key(); $pubkey = $keys->convertToHex($npub); diff --git a/src/Service/ArticleCommentThreadLoader.php b/src/Service/ArticleCommentThreadLoader.php index 3e169a7..6592d6b 100644 --- a/src/Service/ArticleCommentThreadLoader.php +++ b/src/Service/ArticleCommentThreadLoader.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Service; +use App\Enum\KindsEnum; use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\InvalidArgumentException; use Psr\Log\LoggerInterface; @@ -162,6 +163,7 @@ final readonly class ArticleCommentThreadLoader ]); $this->enrichThreadListForDisplay($list, $articleEventHexId); + $this->stripRepostEventBodies($list, $quotes); $commentLinks = []; $quoteLinks = []; @@ -194,6 +196,39 @@ final readonly class ArticleCommentThreadLoader ]; } + /** + * NIP-18 reposts (kinds 6 and 16) carry a JSON-wrapped copy of the original; we only show who reposted, not the body. + * + * @param array $list + * @param array $quotes + */ + private function stripRepostEventBodies(array $list, array $quotes): void + { + $strip = static function (object $ev): void { + $k = (int) ($ev->kind ?? 0); + if ($k !== KindsEnum::REPOST->value && $k !== KindsEnum::GENERIC_REPOST->value) { + return; + } + $ev->content = ''; + if (isset($ev->unfold_reply_blurb)) { + $ev->unfold_reply_blurb = null; + } + if (isset($ev->unfold_body)) { + $ev->unfold_body = ''; + } + }; + foreach ($list as $ev) { + if (\is_object($ev)) { + $strip($ev); + } + } + foreach ($quotes as $ev) { + if (\is_object($ev)) { + $strip($ev); + } + } + } + /** * @param array> $linkBucket * @param array $processedContent diff --git a/src/Service/NostrClient.php b/src/Service/NostrClient.php index 229b366..971a000 100644 --- a/src/Service/NostrClient.php +++ b/src/Service/NostrClient.php @@ -39,6 +39,12 @@ class NostrClient */ private const MAX_DISCUSSION_RELAY_URLS = 10; + /** + * {@see Request::send()} hits relays sequentially; profile pages (metadata, long-form list, 10133) used + * the full default+article+profile list (~8–9 wss) → 2 slow relays can exceed PHP’s 30s default max_execution_time. + */ + private const MAX_PROFILE_SEQUENTIAL_RELAY_URLS = 3; + /** * {@see sendArticleDiscussionToRelaysSequential} visits relays one after another (~RELAY_REQUEST_TIMEOUT_SEC * each). Keep this low so HTTP /fragment/comments and browsers do not hit 60–90s proxy cuts. @@ -327,6 +333,25 @@ class NostrClient return $relaySet; } + /** + * @param list $urls + * + * @return list + */ + private function capSequentialRelaysForProfileFetches(array $urls): array + { + if (\count($urls) <= self::MAX_PROFILE_SEQUENTIAL_RELAY_URLS) { + return $urls; + } + $this->logger->notice('nostr.relay_list_capped', [ + 'context' => 'profile_sequential', + 'max' => self::MAX_PROFILE_SEQUENTIAL_RELAY_URLS, + 'had' => \count($urls), + ]); + + return \array_values(\array_slice($urls, 0, self::MAX_PROFILE_SEQUENTIAL_RELAY_URLS)); + } + /** * Full NIP-65 (kind-10002) wss:// list for a hex pubkey, cached. Used for comment fetches; prefer * {@see getTopReputableRelaysForAuthor} when you only need a few relays. @@ -589,9 +614,9 @@ class NostrClient */ public function getNpubMetadata($npub): \stdClass { - $relaysTried = $this->profileMetadataQueryRelayUrlList(); + $relaysTried = $this->capSequentialRelaysForProfileFetches($this->profileMetadataQueryRelayUrlList()); $relaysTriedStr = implode(', ', array_map(self::relayLogLabel(...), $relaysTried)); - $relaySet = $this->relaySetForProfileMetadataFetch(); + $relaySet = $this->relaySetFromDistinctUrlList($relaysTried); $this->logger->info(sprintf('Getting metadata for npub (relays: %s)', $relaysTriedStr), ['npub' => $npub, 'relays' => $relaysTried]); $request = $this->createNostrRequest( kinds: [KindsEnum::METADATA], @@ -624,9 +649,9 @@ class NostrClient */ public function getKind10133PaymentTargetEventsForNpub(string $npub, int $limit = 20): array { - $relaysTried = $this->profileMetadataQueryRelayUrlList(); + $relaysTried = $this->capSequentialRelaysForProfileFetches($this->profileMetadataQueryRelayUrlList()); $relaysTriedStr = implode(', ', array_map(self::relayLogLabel(...), $relaysTried)); - $relaySet = $this->relaySetForProfileMetadataFetch(); + $relaySet = $this->relaySetFromDistinctUrlList($relaysTried); try { $request = $this->createNostrRequest( kinds: [KindsEnum::PAYMENT_TARGETS], @@ -1527,13 +1552,20 @@ class NostrClient */ public function getLongFormContentForPubkey(string $ident): array { - // Add user relays to the default set $authorRelays = $this->getTopReputableRelaysForAuthor($ident); - // Create a RelaySet from the author's relays - $relaySet = $this->defaultRelaySet; - if (!empty($authorRelays)) { - $relaySet = $this->createRelaySet($authorRelays); + $base = $this->configuredArticleRelayUrlList(); + $merged = $authorRelays !== [] ? array_merge($base, $authorRelays) : $base; + $seen = []; + $deduped = []; + foreach ($merged as $url) { + if (!\is_string($url) || $url === '' || isset($seen[$url])) { + continue; + } + $seen[$url] = true; + $deduped[] = $url; } + $capped = $this->capSequentialRelaysForProfileFetches($deduped); + $relaySet = $this->relaySetFromDistinctUrlList($capped); // Create request using the helper method $request = $this->createNostrRequest( diff --git a/templates/components/Organisms/Comments.html.twig b/templates/components/Organisms/Comments.html.twig index cd956cb..98aa00d 100644 --- a/templates/components/Organisms/Comments.html.twig +++ b/templates/components/Organisms/Comments.html.twig @@ -63,6 +63,7 @@ {% set cpk = item.pubkey|default('') %} {% set cts = item.created_at|default(null) %} {% set cdepth = item.unfold_depth|default(0) %} + {% set is_nip18_repost = item.kind is defined and (item.kind == 6 or item.kind == 16) %}
- {% if item.unfold_reply_blurb|default('')|trim != '' %} + {% if not is_nip18_repost and item.unfold_reply_blurb|default('')|trim != '' %}
{% endif %}
- + {% if is_nip18_repost %} +

Repost

+ {% else %} + + {% endif %}
- {% if cid != '' and commentLinks[cid] is defined and commentLinks[cid]|length > 0 %} + {% if not is_nip18_repost and cid != '' and commentLinks[cid] is defined and commentLinks[cid]|length > 0 %}