diff --git a/src/Command/CacheLatestArticlesCommand.php b/src/Command/CacheLatestArticlesCommand.php
index 3983060..95ff542 100644
--- a/src/Command/CacheLatestArticlesCommand.php
+++ b/src/Command/CacheLatestArticlesCommand.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Command;
+use App\Service\RedisCacheService;
+use App\Util\NostrKeyUtil;
use FOS\ElasticaBundle\Finder\FinderInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
@@ -23,13 +25,20 @@ class CacheLatestArticlesCommand extends Command
private FinderInterface $finder;
private CacheItemPoolInterface $articlesCache;
private ParameterBagInterface $params;
+ private RedisCacheService $redisCacheService;
- public function __construct(FinderInterface $finder, CacheItemPoolInterface $articlesCache, ParameterBagInterface $params)
+ public function __construct(
+ FinderInterface $finder,
+ CacheItemPoolInterface $articlesCache,
+ ParameterBagInterface $params,
+ RedisCacheService $redisCacheService
+ )
{
parent::__construct();
$this->finder = $finder;
$this->articlesCache = $articlesCache;
$this->params = $params;
+ $this->redisCacheService = $redisCacheService;
}
protected function execute(InputInterface $input, OutputInterface $output): int
@@ -70,10 +79,34 @@ class CacheLatestArticlesCommand extends Command
$articles = $this->finder->find($query);
+ // Pre-fetch and cache author metadata for all articles
+ $authorPubkeys = [];
+ foreach ($articles as $article) {
+ // Debug: check what type of object we have
+ if ($article instanceof \App\Entity\Article) {
+ $pubkey = $article->getPubkey();
+ if ($pubkey && NostrKeyUtil::isHexPubkey($pubkey)) {
+ $authorPubkeys[] = $pubkey;
+ }
+ } elseif (is_object($article)) {
+ // Elastica result object
+ if (isset($article->pubkey) && NostrKeyUtil::isHexPubkey($article->pubkey)) {
+ $authorPubkeys[] = $article->pubkey;
+ } elseif (isset($article->npub) && NostrKeyUtil::isNpub($article->npub)) {
+ $authorPubkeys[] = NostrKeyUtil::npubToHex($article->npub);
+ }
+ }
+ }
+ $authorPubkeys = array_unique($authorPubkeys);
+
+ $output->writeln('Pre-fetching metadata for ' . count($authorPubkeys) . ' authors...');
+ $authorsMetadata = $this->redisCacheService->getMultipleMetadata($authorPubkeys);
+ $output->writeln('Fetched ' . count($authorsMetadata) . ' author profiles.');
+
$cacheItem->set($articles);
$cacheItem->expiresAfter(3600); // Cache for 1 hour
$this->articlesCache->save($cacheItem);
- $output->writeln('Cached ' . count($articles) . ' articles.');
+ $output->writeln('Cached ' . count($articles) . ' articles with author metadata.');
// } else {
// $output->writeln('Cache already exists for key: ' . $cacheKey . '');
// }
diff --git a/src/Command/NostrRelayPoolStatsCommand.php b/src/Command/NostrRelayPoolStatsCommand.php
index ad060a6..ba9d870 100644
--- a/src/Command/NostrRelayPoolStatsCommand.php
+++ b/src/Command/NostrRelayPoolStatsCommand.php
@@ -26,10 +26,35 @@ class NostrRelayPoolStatsCommand extends Command
$io = new SymfonyStyle($input, $output);
$stats = $this->relayPool->getStats();
+ $localRelay = $this->relayPool->getLocalRelay();
+ $defaultRelays = $this->relayPool->getDefaultRelays();
$io->title('Nostr Relay Pool Statistics');
- $io->section('Overview');
+ $io->section('Configuration');
+ $io->table(
+ ['Setting', 'Value'],
+ [
+ ['Local Relay', $localRelay ?: '(not configured)'],
+ ['Default Relays', count($defaultRelays)],
+ ]
+ );
+
+ if (!empty($defaultRelays)) {
+ $io->section('Default Relay List (Priority Order)');
+ $rows = [];
+ foreach ($defaultRelays as $index => $relay) {
+ $isLocal = $localRelay && $relay === $localRelay;
+ $rows[] = [
+ $index + 1,
+ $relay,
+ $isLocal ? '✓ Local' : 'Public'
+ ];
+ }
+ $io->table(['Priority', 'Relay URL', 'Type'], $rows);
+ }
+
+ $io->section('Connection Pool');
$io->table(
['Metric', 'Value'],
[
diff --git a/src/Controller/DefaultController.php b/src/Controller/DefaultController.php
index 7105450..9d93b49 100644
--- a/src/Controller/DefaultController.php
+++ b/src/Controller/DefaultController.php
@@ -61,28 +61,28 @@ class DefaultController extends AbstractController
CacheItemPoolInterface $articlesCache
): Response
{
- set_time_limit(300);
- ini_set('max_execution_time', '300');
-
$env = $this->getParameter('kernel.environment');
// Reuse previous latest list cache key to show same set as old 'latest'
$cacheKey = 'latest_articles_list_' . $env;
$cacheItem = $articlesCache->getItem($cacheKey);
- $key = new Key();
- $excludedPubkeys = [
- $key->convertToHex('npub1etsrcjz24fqewg4zmjze7t5q8c6rcwde5zdtdt4v3t3dz2navecscjjz94'),
- $key->convertToHex('npub1m7szwpud3jh2k3cqe73v0fd769uzsj6rzmddh4dw67y92sw22r3sk5m3ys'),
- $key->convertToHex('npub13wke9s6njrmugzpg6mqtvy2d49g4d6t390ng76dhxxgs9jn3f2jsmq82pk'),
- $key->convertToHex('npub10akm29ejpdns52ca082skmc3hr75wmv3ajv4987c9lgyrfynrmdqduqwlx'),
- $key->convertToHex('npub13uvnw9qehqkds68ds76c4nfcn3y99c2rl9z8tr0p34v7ntzsmmzspwhh99'),
- $key->convertToHex('npub1fls5au5fxj6qj0t36sage857cs4tgfpla0ll8prshlhstagejtkqc9s2yl'),
- $key->convertToHex('npub1t5d8kcn0hu8zmt6dpkgatd5hwhx76956g7qmdzwnca6fzgprzlhqnqks86'),
- $key->convertToHex('npub14l5xklll5vxzrf6hfkv8m6n2gqevythn5pqc6ezluespah0e8ars4279ss'),
- ];
-
if (!$cacheItem->isHit()) {
// Fallback: run query now if command hasn't populated cache yet
+ set_time_limit(300);
+ ini_set('max_execution_time', '300');
+
+ $key = new Key();
+ $excludedPubkeys = [
+ $key->convertToHex('npub1etsrcjz24fqewg4zmjze7t5q8c6rcwde5zdtdt4v3t3dz2navecscjjz94'),
+ $key->convertToHex('npub1m7szwpud3jh2k3cqe73v0fd769uzsj6rzmddh4dw67y92sw22r3sk5m3ys'),
+ $key->convertToHex('npub13wke9s6njrmugzpg6mqtvy2d49g4d6t390ng76dhxxgs9jn3f2jsmq82pk'),
+ $key->convertToHex('npub10akm29ejpdns52ca082skmc3hr75wmv3ajv4987c9lgyrfynrmdqduqwlx'),
+ $key->convertToHex('npub13uvnw9qehqkds68ds76c4nfcn3y99c2rl9z8tr0p34v7ntzsmmzspwhh99'),
+ $key->convertToHex('npub1fls5au5fxj6qj0t36sage857cs4tgfpla0ll8prshlhstagejtkqc9s2yl'),
+ $key->convertToHex('npub1t5d8kcn0hu8zmt6dpkgatd5hwhx76956g7qmdzwnca6fzgprzlhqnqks86'),
+ $key->convertToHex('npub14l5xklll5vxzrf6hfkv8m6n2gqevythn5pqc6ezluespah0e8ars4279ss'),
+ ];
+
$boolQuery = new BoolQuery();
$boolQuery->addMustNot(new Query\Terms('pubkey', $excludedPubkeys));
$query = new Query($boolQuery);
@@ -99,12 +99,22 @@ class DefaultController extends AbstractController
$articles = $cacheItem->get();
+ // Fetch author metadata - this is now much faster because
+ // metadata is pre-cached by the CacheLatestArticlesCommand
$authorPubkeys = [];
foreach ($articles as $article) {
- if (isset($article->pubkey) && NostrKeyUtil::isHexPubkey($article->pubkey)) {
- $authorPubkeys[] = $article->pubkey;
- } elseif (isset($article->npub) && NostrKeyUtil::isNpub($article->npub)) {
- $authorPubkeys[] = NostrKeyUtil::npubToHex($article->npub);
+ if ($article instanceof \App\Entity\Article) {
+ $pubkey = $article->getPubkey();
+ if ($pubkey && NostrKeyUtil::isHexPubkey($pubkey)) {
+ $authorPubkeys[] = $pubkey;
+ }
+ } elseif (is_object($article)) {
+ // Elastica result object fallback
+ if (isset($article->pubkey) && NostrKeyUtil::isHexPubkey($article->pubkey)) {
+ $authorPubkeys[] = $article->pubkey;
+ } elseif (isset($article->npub) && NostrKeyUtil::isNpub($article->npub)) {
+ $authorPubkeys[] = NostrKeyUtil::npubToHex($article->npub);
+ }
}
}
$authorPubkeys = array_unique($authorPubkeys);
diff --git a/src/Service/NostrClient.php b/src/Service/NostrClient.php
index dc3d944..ee0e0c3 100644
--- a/src/Service/NostrClient.php
+++ b/src/Service/NostrClient.php
@@ -129,13 +129,13 @@ class NostrClient
*/
public function getPubkeyMetadata($pubkey): \stdClass
{
- // Use relay pool for all relays including purplepag.es
- $relayUrls = [
+ // Use relay pool for all relays including purplepag.es, with local relay prioritized
+ $relayUrls = $this->relayPool->ensureLocalRelayInList([
'wss://theforest.nostr1.com',
'wss://nostr.land',
'wss://relay.primal.net',
'wss://purplepag.es'
- ];
+ ]);
$relaySet = $this->createRelaySet($relayUrls);
$this->logger->info('Getting metadata for pubkey ' . $pubkey );
@@ -168,6 +168,9 @@ class NostrClient
if (empty($relays)) {
$key = new Key();
$relays = $this->getTopReputableRelaysForAuthor($key->convertPublicKeyToBech32($event->getPublicKey()), 5);
+ } else {
+ // Ensure local relay is included when publishing
+ $relays = $this->relayPool->ensureLocalRelayInList($relays);
}
// Use relay pool instead of creating new Relay instances
@@ -206,8 +209,9 @@ class NostrClient
}
$requestMessage = new RequestMessage($subscriptionId, [$filter]);
- // Create relay set from all reputable relays on record
- $relaySet = $this->createRelaySet(self::REPUTABLE_RELAYS);
+ // Create relay set from all reputable relays on record, with local relay prioritized
+ $relayUrls = $this->relayPool->ensureLocalRelayInList(self::REPUTABLE_RELAYS);
+ $relaySet = $this->createRelaySet($relayUrls);
$request = new Request($relaySet, $requestMessage);
@@ -281,6 +285,9 @@ class NostrClient
{
$this->logger->info('Getting event by ID', ['event_id' => $eventId, 'relays' => $relays]);
+ // Ensure local relay is included in the relay list
+ $relays = $this->relayPool->ensureLocalRelayInList($relays);
+
// Merge relays with reputable ones
$allRelays = array_unique(array_merge($relays, self::REPUTABLE_RELAYS));
// Loop over reputable relays and bail as soon as you get a valid event back
@@ -341,6 +348,11 @@ class NostrClient
$this->logger->info('Getting events by IDs', ['event_ids' => $eventIds, 'relays' => $relays]);
+ // Ensure local relay is included
+ if (!empty($relays)) {
+ $relays = $this->relayPool->ensureLocalRelayInList($relays);
+ }
+
// Use provided relays or default if empty
$relaySet = empty($relays) ? $this->defaultRelaySet : $this->createRelaySet($relays);
@@ -578,7 +590,7 @@ class NostrClient
$parts = explode(':', $coordinate, 3);
$pubkey = $parts[1];
- // Get author's relays for better chances of finding zaps
+ // Get author's relays for better chances of finding zaps (includes local relay)
$authorRelays = $this->getTopReputableRelaysForAuthor($pubkey);
$relaySet = $this->createRelaySet($authorRelays);
diff --git a/src/Service/NostrRelayPool.php b/src/Service/NostrRelayPool.php
index 900e48e..de4559a 100644
--- a/src/Service/NostrRelayPool.php
+++ b/src/Service/NostrRelayPool.php
@@ -23,7 +23,10 @@ class NostrRelayPool
private array $lastConnected = [];
/** @var array */
- private array $defaultRelays = [
+ private array $defaultRelays = [];
+
+ /** @var array Default public relays to fall back to */
+ private const PUBLIC_RELAYS = [
'wss://theforest.nostr1.com',
'wss://nostr.land',
'wss://relay.primal.net',
@@ -39,11 +42,39 @@ class NostrRelayPool
private readonly string $nostrDefaultRelay,
array $defaultRelays = []
) {
- $relayList = $defaultRelays ?: $this->defaultRelays;
- if ($this->nostrDefaultRelay && !in_array($this->nostrDefaultRelay, $relayList, true)) {
- array_unshift($relayList, $this->nostrDefaultRelay);
+ // Build relay list: prioritize local relay, then custom relays, then public relays
+ $relayList = [];
+
+ // Add local relay first if configured
+ if ($this->nostrDefaultRelay) {
+ $relayList[] = $this->nostrDefaultRelay;
+ $this->logger->info('NostrRelayPool: Local relay configured as primary', [
+ 'relay' => $this->nostrDefaultRelay
+ ]);
+ }
+
+ // Add custom relays (excluding local relay if already added)
+ if (!empty($defaultRelays)) {
+ foreach ($defaultRelays as $relay) {
+ if (!in_array($relay, $relayList, true)) {
+ $relayList[] = $relay;
+ }
+ }
+ } else {
+ // Use public relays as fallback
+ foreach (self::PUBLIC_RELAYS as $relay) {
+ if (!in_array($relay, $relayList, true)) {
+ $relayList[] = $relay;
+ }
+ }
}
+
$this->defaultRelays = $relayList;
+
+ $this->logger->info('NostrRelayPool initialized with relay priority', [
+ 'relay_count' => count($this->defaultRelays),
+ 'relays' => $this->defaultRelays
+ ]);
}
/**
@@ -88,7 +119,7 @@ class NostrRelayPool
}
/**
- * Get multiple relay connections, prioritizing default relay
+ * Get multiple relay connections, prioritizing local relay (primary default relay)
*
* @param array $relayUrls
* @return array
@@ -96,26 +127,26 @@ class NostrRelayPool
public function getRelays(array $relayUrls): array
{
$relays = [];
- $defaultRelay = $this->defaultRelays[0] ?? null;
+ $localRelay = $this->nostrDefaultRelay ? $this->normalizeRelayUrl($this->nostrDefaultRelay) : null;
$relayUrlsNormalized = array_map([$this, 'normalizeRelayUrl'], $relayUrls);
- $defaultRelayNormalized = $defaultRelay ? $this->normalizeRelayUrl($defaultRelay) : null;
- // Try default relay first if present in requested URLs
- if ($defaultRelayNormalized && in_array($defaultRelayNormalized, $relayUrlsNormalized, true)) {
+ // Always try local relay first if configured, regardless of whether it's in the requested URLs
+ if ($localRelay) {
try {
- $relays[] = $this->getRelay($defaultRelayNormalized);
+ $relays[] = $this->getRelay($localRelay);
+ $this->logger->debug('Using local relay (priority)', ['relay' => $localRelay]);
} catch (\Throwable $e) {
- $this->logger->warning('Default relay unavailable, falling back to others', [
- 'relay' => $defaultRelayNormalized,
+ $this->logger->warning('Local relay unavailable, falling back to others', [
+ 'relay' => $localRelay,
'error' => $e->getMessage()
]);
}
}
- // Add other relays except the default
+ // Add other relays except the local relay
foreach ($relayUrlsNormalized as $url) {
- if ($url === $defaultRelayNormalized) {
- continue;
+ if ($url === $localRelay) {
+ continue; // Skip local relay as we already added it
}
try {
$relays[] = $this->getRelay($url);
@@ -126,6 +157,7 @@ class NostrRelayPool
]);
}
}
+
return $relays;
}
@@ -340,4 +372,37 @@ class NostrRelayPool
{
return $this->defaultRelays;
}
+
+ /**
+ * Get local relay URL if configured
+ */
+ public function getLocalRelay(): ?string
+ {
+ return $this->nostrDefaultRelay ?: null;
+ }
+
+ /**
+ * Ensure local relay is included in a list of relay URLs
+ * Adds local relay at the beginning if configured and not already in the list
+ */
+ public function ensureLocalRelayInList(array $relayUrls): array
+ {
+ if (!$this->nostrDefaultRelay) {
+ return $relayUrls;
+ }
+
+ $normalizedLocal = $this->normalizeRelayUrl($this->nostrDefaultRelay);
+ $normalizedUrls = array_map([$this, 'normalizeRelayUrl'], $relayUrls);
+
+ // If local relay not in list, add it at the beginning
+ if (!in_array($normalizedLocal, $normalizedUrls, true)) {
+ array_unshift($relayUrls, $this->nostrDefaultRelay);
+ $this->logger->debug('Added local relay to relay list', [
+ 'local_relay' => $this->nostrDefaultRelay,
+ 'total_relays' => count($relayUrls)
+ ]);
+ }
+
+ return $relayUrls;
+ }
}