Browse Source

bug-fixes

gitcitadel
Silberengel 2 weeks ago
parent
commit
c967cf1df5
  1. 3
      config/services.yaml
  2. 11
      src/Command/PrewarmCommand.php
  3. 31
      src/Repository/EventRepository.php
  4. 13
      src/Service/ArticleBodyHighlightInjector.php
  5. 158
      src/Service/CacheService.php
  6. 14
      src/Service/FeaturedAuthorListedRows.php
  7. 5
      src/Service/HighlightAuthorMetadataProvider.php
  8. 32
      src/Service/NostrClient.php
  9. 22
      src/Service/NostrWireEventMerge.php
  10. 42
      src/Twig/ArticleCardCoverExtension.php
  11. 1
      templates/components/Organisms/FeaturedList.html.twig
  12. 1
      templates/components/Organisms/FeaturedWall.html.twig
  13. 1
      templates/components/Organisms/HomeMagazineArticleStrip.html.twig
  14. 1
      tests/Service/ArticleBodyHighlightInjectorTest.php
  15. 1
      tests/Service/ArticleHighlightCommonMarkPipelineTest.php
  16. 24
      tests/Service/NostrWireEventMergeTest.php

3
config/services.yaml

@ -77,6 +77,9 @@ services: @@ -77,6 +77,9 @@ services:
App\Service\Nip05VerificationService:
arguments:
$appCache: '@cache.app'
App\Service\CacheService:
tags:
- { name: kernel.reset, method: reset }
when@test:
services:

11
src/Command/PrewarmCommand.php

@ -379,6 +379,17 @@ final class PrewarmCommand extends Command @@ -379,6 +379,17 @@ final class PrewarmCommand extends Command
$this->waitForSiteWellKnownBeforeVerification($io, $domain);
}
$io->writeln('Verifying <comment>NIP-05</comment> (HTTPS <comment>/.well-known/nostr.json</comment>, per identifier)…');
$npubsForVerify = [];
foreach ($toWarm as $hex) {
if (64 !== \strlen($hex) || !ctype_xdigit($hex)) {
continue;
}
try {
$npubsForVerify[] = $this->nostrKeyHelper->convertPublicKeyToBech32(strtolower($hex));
} catch (\Throwable) {
}
}
$this->cacheService->prefetchMetadataForNpubs($npubsForVerify);
$nt = 0;
$nv = 0;
foreach ($toWarm as $hex) {

31
src/Repository/EventRepository.php

@ -22,4 +22,35 @@ class EventRepository extends ServiceEntityRepository @@ -22,4 +22,35 @@ class EventRepository extends ServiceEntityRepository
{
return $this->findOneBy(['coreRowKey' => $key]);
}
/**
* @param list<string> $keys
*
* @return array<string, Event> keyed by coreRowKey
*/
public function findByCoreRowKeys(array $keys): array
{
$keys = array_values(array_unique(array_filter(
$keys,
static fn (mixed $k): bool => \is_string($k) && $k !== '',
)));
if ($keys === []) {
return [];
}
/** @var list<Event> $rows */
$rows = $this->createQueryBuilder('e')
->andWhere('e.coreRowKey IN (:keys)')
->setParameter('keys', $keys)
->getQuery()
->getResult();
$out = [];
foreach ($rows as $row) {
$k = $row->getCoreRowKey();
if ($k !== null && $k !== '') {
$out[$k] = $row;
}
}
return $out;
}
}

13
src/Service/ArticleBodyHighlightInjector.php

@ -338,6 +338,19 @@ final class ArticleBodyHighlightInjector @@ -338,6 +338,19 @@ final class ArticleBodyHighlightInjector
*/
private function buildHighlightAuthorsJson(array $group): string
{
$npubsForPrefetch = [];
foreach ($group as $h) {
$pk = $h->getAuthorPubkey();
if (64 !== \strlen($pk) || !ctype_xdigit($pk)) {
continue;
}
try {
$npubsForPrefetch[] = $this->nostrKeyHelper->convertPublicKeyToBech32($pk);
} catch (\Throwable) {
}
}
$this->highlightAuthorMetadata->prefetchMetadataForNpubs($npubsForPrefetch);
$byNpub = [];
foreach ($group as $h) {
$eidH = $h->getEventId();

158
src/Service/CacheService.php

@ -9,9 +9,20 @@ use App\Nostr\MagazineEventKeys; @@ -9,9 +9,20 @@ use App\Nostr\MagazineEventKeys;
use App\Repository\EventRepository;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\Service\ResetInterface;
readonly class CacheService implements HighlightAuthorMetadataProvider
final class CacheService implements HighlightAuthorMetadataProvider, ResetInterface
{
/**
* @var array<string, array{content: \stdClass, kind0_tags: list<list<string>>, nip30_custom_emojis: list<array{shortcode: string, url: string, set?: string}>}>
*/
private array $requestBundlesByHex = [];
/** @var array<string, string> lowercase hex pubkey => npub */
private array $pendingHexToNpub = [];
private int $metadataBatchDepth = 0;
public function __construct(
private NostrClient $nostrClient,
private EntityManagerInterface $entityManager,
@ -23,6 +34,13 @@ readonly class CacheService implements HighlightAuthorMetadataProvider @@ -23,6 +34,13 @@ readonly class CacheService implements HighlightAuthorMetadataProvider
) {
}
public function reset(): void
{
$this->requestBundlesByHex = [];
$this->pendingHexToNpub = [];
$this->metadataBatchDepth = 0;
}
public function getMetadata(string $npub): \stdClass
{
return $this->getMetadataBundle($npub)['content'];
@ -37,41 +55,129 @@ readonly class CacheService implements HighlightAuthorMetadataProvider @@ -37,41 +55,129 @@ readonly class CacheService implements HighlightAuthorMetadataProvider
if ($authorHex === null) {
return $this->placeholderMetadataBundle($npub);
}
if (isset($this->requestBundlesByHex[$authorHex])) {
return $this->requestBundlesByHex[$authorHex];
}
$row = $this->eventRepository->findOneByCoreRowKey(MagazineEventKeys::profileKind0($authorHex));
if ($row !== null) {
return $this->bundleFromKind0EventRow($row, $npub);
$bundle = $this->bundleFromKind0EventRow($row, $npub);
$this->requestBundlesByHex[$authorHex] = $bundle;
return $bundle;
}
try {
$ev = $this->nostrClient->getNpubMetadata($npub);
if (!\is_object($ev)) {
return $this->placeholderMetadataBundle($npub);
$this->pendingHexToNpub[$authorHex] = $npub;
$this->runPendingMetadataBatch();
return $this->requestBundlesByHex[$authorHex] ?? $this->placeholderMetadataBundle($npub);
}
/**
* @param list<string> $npubs
*/
public function prefetchMetadataForNpubs(array $npubs): void
{
foreach ($npubs as $npub) {
if (!\is_string($npub) || $npub === '') {
continue;
}
$nip30 = $this->nip30EmojiCatalogBuilder->buildMergedCatalog($ev, null, []);
$this->replaceByCoreKey(
MagazineEventKeys::profileKind0($authorHex),
Event::STORAGE_PROFILE_KIND0,
$ev,
$nip30,
);
$tags = self::normalizeEventTagsList($ev->tags ?? null);
$content = $this->decodeKind0ContentObject($ev);
if ($this->isPlaceholderContent($content, $npub)) {
$content = $this->namePlaceholderNpubObject($npub);
$authorHex = $this->npubToAuthorHex64($npub);
if ($authorHex === null || isset($this->requestBundlesByHex[$authorHex])) {
continue;
}
$this->pendingHexToNpub[$authorHex] = $npub;
}
$this->runPendingMetadataBatch();
}
/**
* @param list<string> $pubkeyHex 64-char hex pubkeys (any case)
*/
public function prefetchMetadataForPubkeyHexes(array $pubkeyHex): void
{
$npubs = [];
foreach ($pubkeyHex as $hex) {
if (!\is_string($hex) || 64 !== \strlen($hex) || !ctype_xdigit($hex)) {
continue;
}
try {
$npubs[] = $this->nostrKeyHelper->convertPublicKeyToBech32(strtolower($hex));
} catch (\Throwable) {
}
}
$this->prefetchMetadataForNpubs($npubs);
}
private function runPendingMetadataBatch(): void
{
if ($this->pendingHexToNpub === []) {
return;
}
++$this->metadataBatchDepth;
try {
do {
if ($this->pendingHexToNpub === []) {
break;
}
$this->flushPendingMetadataFetches();
} while ($this->pendingHexToNpub !== []);
} finally {
--$this->metadataBatchDepth;
}
}
private function flushPendingMetadataFetches(): void
{
if ($this->pendingHexToNpub === []) {
return;
}
$pending = $this->pendingHexToNpub;
$this->pendingHexToNpub = [];
return [
'content' => $content,
'kind0_tags' => $tags,
'nip30_custom_emojis' => $nip30,
];
} catch (\Exception $e) {
$keys = [];
foreach (array_keys($pending) as $hex) {
$keys[] = MagazineEventKeys::profileKind0($hex);
}
$rowsByKey = $this->eventRepository->findByCoreRowKeys($keys);
$relayHex = [];
foreach ($pending as $hex => $npub) {
$key = MagazineEventKeys::profileKind0($hex);
if (isset($rowsByKey[$key])) {
$this->requestBundlesByHex[$hex] = $this->bundleFromKind0EventRow($rowsByKey[$key], $npub);
continue;
}
$relayHex[] = $hex;
}
if ($relayHex === []) {
return;
}
try {
$fetched = $this->nostrClient->fetchProfilePrewarmWireBundlesForAuthors($relayHex);
$this->putPrewarmMetadataBatch($relayHex, $fetched);
} catch (\Throwable $e) {
$this->logger->warning('Profile metadata batch fetch failed.', [
'authors' => \count($relayHex),
'exception' => $e,
]);
}
$rowsAfterRelay = $this->eventRepository->findByCoreRowKeys(array_map(
static fn (string $hex): string => MagazineEventKeys::profileKind0($hex),
$relayHex,
));
foreach ($relayHex as $hex) {
$npub = $pending[$hex];
$key = MagazineEventKeys::profileKind0($hex);
if (isset($rowsAfterRelay[$key])) {
$this->requestBundlesByHex[$hex] = $this->bundleFromKind0EventRow($rowsAfterRelay[$key], $npub);
continue;
}
$this->logger->warning('Profile metadata fetch failed; using npub placeholder.', [
'npub' => $npub,
'exception' => $e->getPrevious() ?? $e,
]);
$this->requestBundlesByHex[$hex] = $this->placeholderMetadataBundle($npub);
}
return $this->placeholderMetadataBundle($npub);
}
/**

14
src/Service/FeaturedAuthorListedRows.php

@ -33,9 +33,11 @@ final class FeaturedAuthorListedRows @@ -33,9 +33,11 @@ final class FeaturedAuthorListedRows
return $fromDb;
}
$hexes = \array_slice($this->magazineContent->getAllDistinctCategoryAuthorPubkeyHexes(), 0, $limit);
$this->cacheService->prefetchMetadataForPubkeyHexes($hexes);
$authors = [];
$hexes = $this->magazineContent->getAllDistinctCategoryAuthorPubkeyHexes();
foreach (\array_slice($hexes, 0, $limit) as $hex) {
foreach ($hexes as $hex) {
try {
$npub = $this->nostrKeyHelper->convertPublicKeyToBech32($hex);
} catch (\Throwable) {
@ -52,8 +54,14 @@ final class FeaturedAuthorListedRows @@ -52,8 +54,14 @@ final class FeaturedAuthorListedRows
*/
public function buildListedByLocalPartPage(int $limit, int $offset = 0): array
{
$listed = $this->featuredAuthorRepository->findListedOrderByLocalPartPaginated($limit, $offset);
$this->cacheService->prefetchMetadataForPubkeyHexes(array_map(
static fn ($fa) => $fa->getPubkeyHex(),
$listed,
));
$authors = [];
foreach ($this->featuredAuthorRepository->findListedOrderByLocalPartPaginated($limit, $offset) as $fa) {
foreach ($listed as $fa) {
try {
$npub = $this->nostrKeyHelper->convertPublicKeyToBech32($fa->getPubkeyHex());
} catch (\Throwable) {

5
src/Service/HighlightAuthorMetadataProvider.php

@ -10,4 +10,9 @@ namespace App\Service; @@ -10,4 +10,9 @@ namespace App\Service;
interface HighlightAuthorMetadataProvider
{
public function getMetadata(string $npub): \stdClass;
/**
* @param list<string> $npubs
*/
public function prefetchMetadataForNpubs(array $npubs): void;
}

32
src/Service/NostrClient.php

@ -386,6 +386,10 @@ class NostrClient @@ -386,6 +386,10 @@ class NostrClient
*/
public function getNpubMetadata($npub): \stdClass
{
$authorHex = $this->wireMerge->authorIdentToHexLower($npub);
if ($authorHex === null) {
throw new \Exception('Invalid npub for metadata: '.$npub);
}
$relaysTried = $this->relayListFactory->capSequentialRelaysForProfileFetches($this->relayListFactory->getProfileMetadataQueryRelayUrlList());
$relaysTriedStr = implode(', ', array_map(NostrRelayQuery::relayLogLabel(...), $relaysTried));
$relaySet = $this->relayListFactory->relaySetFromDistinctUrlList($relaysTried);
@ -393,7 +397,7 @@ class NostrClient @@ -393,7 +397,7 @@ class NostrClient
$request = $this->nostrRelayQuery->createNostrRequest(
defaultRelaySet: $this->defaultRelaySet,
kinds: [KindsEnum::METADATA],
filters: ['authors' => [$npub]],
filters: ['authors' => [$authorHex]],
relaySet: $relaySet
);
@ -410,10 +414,6 @@ class NostrClient @@ -410,10 +414,6 @@ class NostrClient
throw new \Exception('No metadata for npub '.$npub.' (relays: '.$relaysTriedStr.')');
}
$byAddr = $this->wireMerge->mergeKind0EventsByReplaceableAddress($events);
$authorHex = $this->wireMerge->npubToHexPubkey($npub);
if ($authorHex === null) {
throw new \Exception('Invalid npub for metadata: '.$npub);
}
$key = '0:'.$authorHex;
if (!isset($byAddr[$key])) {
throw new \Exception('No kind-0 metadata for npub '.$npub.' (relays: '.$relaysTriedStr.')');
@ -431,6 +431,10 @@ class NostrClient @@ -431,6 +431,10 @@ class NostrClient
*/
public function getKind10133PaymentTargetEventsForNpub(string $npub, int $limit = 20): array
{
$authorHex = $this->wireMerge->authorIdentToHexLower($npub);
if ($authorHex === null) {
return [];
}
$relaysTried = $this->relayListFactory->capSequentialRelaysForProfileFetches($this->relayListFactory->getProfileMetadataQueryRelayUrlList());
$relaysTriedStr = implode(', ', array_map(NostrRelayQuery::relayLogLabel(...), $relaysTried));
$relaySet = $this->relayListFactory->relaySetFromDistinctUrlList($relaysTried);
@ -438,7 +442,7 @@ class NostrClient @@ -438,7 +442,7 @@ class NostrClient
$request = $this->nostrRelayQuery->createNostrRequest(
defaultRelaySet: $this->defaultRelaySet,
kinds: [KindsEnum::PAYMENT_TARGETS],
filters: ['authors' => [$npub], 'limit' => max(1, min(50, $limit))],
filters: ['authors' => [$authorHex], 'limit' => max(1, min(50, $limit))],
relaySet: $relaySet
);
$events = $this->nostrRelayQuery->processResponse(
@ -463,11 +467,17 @@ class NostrClient @@ -463,11 +467,17 @@ class NostrClient
public function getNpubLongForm($npub): void
{
$authorHex = $this->wireMerge->authorIdentToHexLower($npub);
if ($authorHex === null) {
$this->logger->warning('nostr.longform_by_author.invalid_npub', ['npub' => $npub]);
return;
}
$subscription = new Subscription();
$subscriptionId = $subscription->setId();
$filter = new Filter();
$filter->setKinds([KindsEnum::LONGFORM]);
$filter->setAuthors([$npub]);
$filter->setAuthors([$authorHex]);
$filter->setSince(strtotime('-6 months')); // too much?
$requestMessage = new RequestMessage($subscriptionId, [$filter]);
@ -843,10 +853,14 @@ class NostrClient @@ -843,10 +853,14 @@ class NostrClient
*/
public function getNpubRelayList10002Wire($npub): ?object
{
$authorHex = $this->wireMerge->authorIdentToHexLower($npub);
if ($authorHex === null) {
return null;
}
$request = $this->nostrRelayQuery->createNostrRequest(
defaultRelaySet: $this->defaultRelaySet,
kinds: [KindsEnum::RELAY_LIST],
filters: ['authors' => [$npub]],
filters: ['authors' => [$authorHex]],
relaySet: $this->defaultRelaySet
);
$response = $this->nostrRelayQuery->processResponse($request->send(), function ($received) {
@ -1713,7 +1727,7 @@ class NostrClient @@ -1713,7 +1727,7 @@ class NostrClient
defaultRelaySet: $this->defaultRelaySet,
relaySet: $relaySet,
kinds: [KindsEnum::PUBLICATION_INDEX],
filters: ['authors' => [(string) $npub], 'tag' => ['#d', [(string) $dTag]]],
filters: ['authors' => [$authorHex], 'tag' => ['#d', [(string) $dTag]]],
);
$this->logger->info(sprintf('Magazine index query (relays: %s)', $relaysForLog), [
'npub' => $npub,

22
src/Service/NostrWireEventMerge.php

@ -375,16 +375,18 @@ final readonly class NostrWireEventMerge @@ -375,16 +375,18 @@ final readonly class NostrWireEventMerge
if ($raw instanceof PublicationEventEntity) {
return $raw;
}
if (!\is_object($raw)) {
return null;
}
try {
$data = json_decode(json_encode($raw, \JSON_THROW_ON_ERROR), true, 512, \JSON_THROW_ON_ERROR);
} catch (\JsonException) {
return null;
}
if (!\is_array($data)) {
if (\is_array($raw)) {
$data = $raw;
} elseif (\is_object($raw)) {
try {
$data = json_decode(json_encode($raw, \JSON_THROW_ON_ERROR), true, 512, \JSON_THROW_ON_ERROR);
} catch (\JsonException) {
return null;
}
if (!\is_array($data)) {
return null;
}
} else {
return null;
}
$entity = new PublicationEventEntity();

42
src/Twig/ArticleCardCoverExtension.php

@ -42,11 +42,34 @@ final class ArticleCardCoverExtension extends AbstractExtension @@ -42,11 +42,34 @@ final class ArticleCardCoverExtension extends AbstractExtension
{
return [
new TwigFunction('article_card_cover', $this->articleCardCover(...)),
new TwigFunction('prefetch_article_card_covers', $this->prefetchArticleCardCovers(...)),
new TwigFunction('article_og_image', $this->articleOgImage(...)),
new TwigFunction('site_og_image', $this->siteOgImage(...)),
];
}
/**
* Batch kind-0 profile lookups before a list of cards (one relay REQ per chunk, not per tile).
*
* @param iterable<mixed> $items Rows with optional `pubkey` (64-char hex)
*/
public function prefetchArticleCardCovers(iterable $items): void
{
$hexes = [];
foreach ($items as $item) {
if (\is_object($item) && isset($item->article)) {
$item = $item->article;
} elseif (\is_array($item) && isset($item['article'])) {
$item = $item['article'];
}
$hex = $this->pubkeyHexFromItem($item);
if ($hex !== null) {
$hexes[] = $hex;
}
}
$this->cacheService->prefetchMetadataForPubkeyHexes($hexes);
}
/**
* Branded site Open Graph image (home, category lists, base layout default): not tied to any article or author.
*
@ -167,4 +190,23 @@ final class ArticleCardCoverExtension extends AbstractExtension @@ -167,4 +190,23 @@ final class ArticleCardCoverExtension extends AbstractExtension
{
return $this->packages->getUrl(self::DEFAULT_PACKAGE_IMAGE);
}
private function pubkeyHexFromItem(mixed $item): ?string
{
$raw = null;
if (\is_object($item) && isset($item->pubkey)) {
$raw = $item->pubkey;
} elseif (\is_array($item) && isset($item['pubkey'])) {
$raw = $item['pubkey'];
}
if (!\is_string($raw)) {
return null;
}
$hex = strtolower(trim($raw));
if (64 !== \strlen($hex) || !ctype_xdigit($hex)) {
return null;
}
return $hex;
}
}

1
templates/components/Organisms/FeaturedList.html.twig

@ -6,6 +6,7 @@ @@ -6,6 +6,7 @@
role="region"
aria-label="{{ title|e('html_attr') }}"
>
{% do prefetch_article_card_covers(list) %}
{% for item in list %}
<article class="featured-tile" style="--tile-hue: {{ _hue }};">
<a

1
templates/components/Organisms/FeaturedWall.html.twig

@ -7,6 +7,7 @@ @@ -7,6 +7,7 @@
role="region"
aria-label="{{ (region_aria_label|default(website_name ~ ' — featured articles'))|e('html_attr') }}"
>
{% do prefetch_article_card_covers(tiles) %}
{% for tile in tiles %}
{% set _hue = (tile.categoryTitle|default('x')|length * 47) % 360 %}
{% set item = tile.article %}

1
templates/components/Organisms/HomeMagazineArticleStrip.html.twig

@ -8,6 +8,7 @@ @@ -8,6 +8,7 @@
aria-label="{{ (website_name ~ ' — featured articles')|e('html_attr') }}"
>
<div class="home-curation-landmark__articles">
{% do prefetch_article_card_covers(tiles) %}
{% for tile in tiles %}
{% set item = tile.article %}
{% set article_href = (item.pubkey and npub_from_hex(item.pubkey) != '') ? path('article', { npub: npub_from_hex(item.pubkey), slug: item.slug }) : path('article-legacy-redirect', { slug: item.slug }) %}

1
tests/Service/ArticleBodyHighlightInjectorTest.php

@ -134,6 +134,7 @@ final class ArticleBodyHighlightInjectorTest extends TestCase @@ -134,6 +134,7 @@ final class ArticleBodyHighlightInjectorTest extends TestCase
private function createInjector(): ArticleBodyHighlightInjector
{
$meta = $this->createMock(HighlightAuthorMetadataProvider::class);
$meta->method('prefetchMetadataForNpubs');
$meta->method('getMetadata')->willReturn(
(object) [
'display_name' => 'Test',

1
tests/Service/ArticleHighlightCommonMarkPipelineTest.php

@ -104,6 +104,7 @@ final class ArticleHighlightCommonMarkPipelineTest extends KernelTestCase @@ -104,6 +104,7 @@ final class ArticleHighlightCommonMarkPipelineTest extends KernelTestCase
private function createInjector(): ArticleBodyHighlightInjector
{
$meta = $this->createMock(HighlightAuthorMetadataProvider::class);
$meta->method('prefetchMetadataForNpubs');
$meta->method('getMetadata')->willReturn(
(object) ['display_name' => 'Test', 'name' => 'Test', 'picture' => ''],
);

24
tests/Service/NostrWireEventMergeTest.php

@ -94,4 +94,28 @@ final class NostrWireEventMergeTest extends TestCase @@ -94,4 +94,28 @@ final class NostrWireEventMergeTest extends TestCase
$this->assertFalse($this->m->isNip33ParameterizedKind(29_999));
$this->assertFalse($this->m->isNip33ParameterizedKind(40_000));
}
public function testMagazineEventToPublicationEntityAcceptsDecodedWireArray(): void
{
$pk = str_repeat('e', 64);
$id = str_repeat('f', 64);
$data = [
'kind' => 30_040,
'id' => $id,
'pubkey' => $pk,
'content' => 'summary',
'created_at' => 1_700_000_000,
'tags' => [['d', 'newsroom-magazine-on-imwald-by-laeserin-category-bitcoin']],
'sig' => str_repeat('a', 128),
];
$entity = $this->m->magazineEventToPublicationEntity($data);
$this->assertNotNull($entity);
$this->assertSame($id, $entity->getId());
$this->assertSame(30_040, $entity->getKind());
$this->assertSame($pk, $entity->getPubkey());
$this->assertSame('summary', $entity->getContent());
$this->assertSame(1_700_000_000, $entity->getCreatedAt());
$this->assertSame($data['tags'], $entity->getTags());
}
}

Loading…
Cancel
Save