|
|
|
@ -35,8 +35,8 @@ class ArticleController extends AbstractController |
|
|
|
{ |
|
|
|
{ |
|
|
|
// {@see NostrClient::getArticleDiscussion} runs per-relay work in parallel CLI workers; allow headroom |
|
|
|
// {@see NostrClient::getArticleDiscussion} runs per-relay work in parallel CLI workers; allow headroom |
|
|
|
// for all processes + Symfony (45s was too low and caused an uncatchable max-execution fatal → HTTP 500). |
|
|
|
// for all processes + Symfony (45s was too low and caused an uncatchable max-execution fatal → HTTP 500). |
|
|
|
@set_time_limit(120); |
|
|
|
@set_time_limit(300); |
|
|
|
@ini_set('max_execution_time', '120'); |
|
|
|
@ini_set('max_execution_time', '300'); |
|
|
|
|
|
|
|
|
|
|
|
$t0 = microtime(true); |
|
|
|
$t0 = microtime(true); |
|
|
|
$coordinate = $request->query->getString('coordinate'); |
|
|
|
$coordinate = $request->query->getString('coordinate'); |
|
|
|
@ -56,6 +56,85 @@ class ArticleController extends AbstractController |
|
|
|
if (strlen($articleTitle) > 200) { |
|
|
|
if (strlen($articleTitle) > 200) { |
|
|
|
$articleTitle = substr($articleTitle, 0, 200); |
|
|
|
$articleTitle = substr($articleTitle, 0, 200); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$logger->info('http.fragment.comments_start', [ |
|
|
|
|
|
|
|
'coordinate' => $coordinate, |
|
|
|
|
|
|
|
'article_event_hex' => $articleEventId, |
|
|
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$headers = [ |
|
|
|
|
|
|
|
'Content-Type' => 'text/html; charset=UTF-8', |
|
|
|
|
|
|
|
'Cache-Control' => 'private, max-age=60', |
|
|
|
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
$data = $loader->load($coordinate, $articleEventId); |
|
|
|
|
|
|
|
$data = $this->enrichCommentDataWithReplyContext( |
|
|
|
|
|
|
|
$data, |
|
|
|
|
|
|
|
$coordinate, |
|
|
|
|
|
|
|
$articleEventId, |
|
|
|
|
|
|
|
$articleTitle |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
$logger->info('http.fragment.comments_after_load', [ |
|
|
|
|
|
|
|
'elapsed_ms' => (int) round((microtime(true) - $t0) * 1000), |
|
|
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$tRender = microtime(true); |
|
|
|
|
|
|
|
$response = $this->render('components/Organisms/Comments.html.twig', $data, new Response( |
|
|
|
|
|
|
|
'', |
|
|
|
|
|
|
|
Response::HTTP_OK, |
|
|
|
|
|
|
|
$headers |
|
|
|
|
|
|
|
)); |
|
|
|
|
|
|
|
$logger->info('http.fragment.comments_response', [ |
|
|
|
|
|
|
|
'total_elapsed_ms' => (int) round((microtime(true) - $t0) * 1000), |
|
|
|
|
|
|
|
'render_elapsed_ms' => (int) round((microtime(true) - $tRender) * 1000), |
|
|
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return $response; |
|
|
|
|
|
|
|
} catch (\Throwable $e) { |
|
|
|
|
|
|
|
$logger->error('http.fragment.comments_exception', [ |
|
|
|
|
|
|
|
'message' => $e->getMessage(), |
|
|
|
|
|
|
|
'exception_class' => \get_class($e), |
|
|
|
|
|
|
|
'elapsed_ms' => (int) round((microtime(true) - $t0) * 1000), |
|
|
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return new Response('<div class="comments"></div>', Response::HTTP_OK, $headers); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Adds `comment_reply_context` for the reply composer (same data as the HTML fragment, used for full-page SSR when cache hits). |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param array{ |
|
|
|
|
|
|
|
* list: array<int, object>, |
|
|
|
|
|
|
|
* quotes: array<int, object>, |
|
|
|
|
|
|
|
* commentLinks: array<string, array<int, mixed>>, |
|
|
|
|
|
|
|
* quoteLinks: array<string, array<int, mixed>>, |
|
|
|
|
|
|
|
* processedContent: array<string, string> |
|
|
|
|
|
|
|
* } $data |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @return array{ |
|
|
|
|
|
|
|
* list: array<int, object>, |
|
|
|
|
|
|
|
* quotes: array<int, object>, |
|
|
|
|
|
|
|
* commentLinks: array<string, array<int, mixed>>, |
|
|
|
|
|
|
|
* quoteLinks: array<string, array<int, mixed>>, |
|
|
|
|
|
|
|
* processedContent: array<string, string>, |
|
|
|
|
|
|
|
* comment_reply_context: array{ |
|
|
|
|
|
|
|
* can_publish: bool, |
|
|
|
|
|
|
|
* coordinate: string, |
|
|
|
|
|
|
|
* article_event_id: ?string, |
|
|
|
|
|
|
|
* parent_kind: int, |
|
|
|
|
|
|
|
* rows: array<int, array<string, mixed>>, |
|
|
|
|
|
|
|
* fragment_url: string |
|
|
|
|
|
|
|
* } |
|
|
|
|
|
|
|
* } |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
private function enrichCommentDataWithReplyContext( |
|
|
|
|
|
|
|
array $data, |
|
|
|
|
|
|
|
string $coordinate, |
|
|
|
|
|
|
|
?string $articleEventId, |
|
|
|
|
|
|
|
string $articleTitle |
|
|
|
|
|
|
|
): array { |
|
|
|
$coordparts = explode(':', $coordinate, 3); |
|
|
|
$coordparts = explode(':', $coordinate, 3); |
|
|
|
$articleKind = isset($coordparts[0]) && ctype_digit($coordparts[0]) ? (int) $coordparts[0] : 30023; |
|
|
|
$articleKind = isset($coordparts[0]) && ctype_digit($coordparts[0]) ? (int) $coordparts[0] : 30023; |
|
|
|
$articleAuthorPubkey = $coordparts[1] ?? ''; |
|
|
|
$articleAuthorPubkey = $coordparts[1] ?? ''; |
|
|
|
@ -66,7 +145,6 @@ class ArticleController extends AbstractController |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
$parentIdForNaddr = str_repeat('0', 64); |
|
|
|
$parentIdForNaddr = str_repeat('0', 64); |
|
|
|
$articleParentId = $articleEventId ?? $parentIdForNaddr; |
|
|
|
|
|
|
|
if ($articleEventId !== null && 64 === \strlen($articleEventId) && ctype_xdigit($articleEventId)) { |
|
|
|
if ($articleEventId !== null && 64 === \strlen($articleEventId) && ctype_xdigit($articleEventId)) { |
|
|
|
$articleParentId = $articleEventId; |
|
|
|
$articleParentId = $articleEventId; |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
@ -84,21 +162,6 @@ class ArticleController extends AbstractController |
|
|
|
'authorPubkey' => $articleAuthorPubkey, |
|
|
|
'authorPubkey' => $articleAuthorPubkey, |
|
|
|
'expectedTags' => $articleReplyTags, |
|
|
|
'expectedTags' => $articleReplyTags, |
|
|
|
]; |
|
|
|
]; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$logger->info('http.fragment.comments_start', [ |
|
|
|
|
|
|
|
'coordinate' => $coordinate, |
|
|
|
|
|
|
|
'article_event_hex' => $articleEventId, |
|
|
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$headers = [ |
|
|
|
|
|
|
|
'Content-Type' => 'text/html; charset=UTF-8', |
|
|
|
|
|
|
|
'Cache-Control' => 'private, max-age=60', |
|
|
|
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
$data = $loader->load($coordinate, $articleEventId); |
|
|
|
|
|
|
|
if ($userMayReply && $articleReplyTags !== null) { |
|
|
|
|
|
|
|
/** @var array<int, object> $list */ |
|
|
|
/** @var array<int, object> $list */ |
|
|
|
$list = $data['list'] ?? []; |
|
|
|
$list = $data['list'] ?? []; |
|
|
|
foreach ($list as $row) { |
|
|
|
foreach ($list as $row) { |
|
|
|
@ -143,11 +206,7 @@ class ArticleController extends AbstractController |
|
|
|
]; |
|
|
|
]; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
$logger->info('http.fragment.comments_after_load', [ |
|
|
|
|
|
|
|
'elapsed_ms' => (int) round((microtime(true) - $t0) * 1000), |
|
|
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$tRender = microtime(true); |
|
|
|
|
|
|
|
$fragmentQuery = ['coordinate' => $coordinate, 'title' => $articleTitle]; |
|
|
|
$fragmentQuery = ['coordinate' => $coordinate, 'title' => $articleTitle]; |
|
|
|
if ($articleEventId !== null) { |
|
|
|
if ($articleEventId !== null) { |
|
|
|
$fragmentQuery['e'] = $articleEventId; |
|
|
|
$fragmentQuery['e'] = $articleEventId; |
|
|
|
@ -161,26 +220,7 @@ class ArticleController extends AbstractController |
|
|
|
'fragment_url' => $this->generateUrl('article_comments_fragment', $fragmentQuery), |
|
|
|
'fragment_url' => $this->generateUrl('article_comments_fragment', $fragmentQuery), |
|
|
|
]; |
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
$response = $this->render('components/Organisms/Comments.html.twig', $data, new Response( |
|
|
|
return $data; |
|
|
|
'', |
|
|
|
|
|
|
|
Response::HTTP_OK, |
|
|
|
|
|
|
|
$headers |
|
|
|
|
|
|
|
)); |
|
|
|
|
|
|
|
$logger->info('http.fragment.comments_response', [ |
|
|
|
|
|
|
|
'total_elapsed_ms' => (int) round((microtime(true) - $t0) * 1000), |
|
|
|
|
|
|
|
'render_elapsed_ms' => (int) round((microtime(true) - $tRender) * 1000), |
|
|
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return $response; |
|
|
|
|
|
|
|
} catch (\Throwable $e) { |
|
|
|
|
|
|
|
$logger->error('http.fragment.comments_exception', [ |
|
|
|
|
|
|
|
'message' => $e->getMessage(), |
|
|
|
|
|
|
|
'exception_class' => \get_class($e), |
|
|
|
|
|
|
|
'elapsed_ms' => (int) round((microtime(true) - $t0) * 1000), |
|
|
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return new Response('<div class="comments"></div>', Response::HTTP_OK, $headers); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private static function isValidNostrCoordinate(string $coordinate): bool |
|
|
|
private static function isValidNostrCoordinate(string $coordinate): bool |
|
|
|
@ -243,7 +283,8 @@ class ArticleController extends AbstractController |
|
|
|
EntityManagerInterface $entityManager, |
|
|
|
EntityManagerInterface $entityManager, |
|
|
|
CacheService $cacheService, |
|
|
|
CacheService $cacheService, |
|
|
|
CacheItemPoolInterface $articlesCache, |
|
|
|
CacheItemPoolInterface $articlesCache, |
|
|
|
Converter $converter |
|
|
|
Converter $converter, |
|
|
|
|
|
|
|
ArticleCommentThreadLoader $commentThreadLoader |
|
|
|
): Response |
|
|
|
): Response |
|
|
|
{ |
|
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
@ -282,12 +323,34 @@ class ArticleController extends AbstractController |
|
|
|
$npub = $key->convertPublicKeyToBech32($article->getPubkey()); |
|
|
|
$npub = $key->convertPublicKeyToBech32($article->getPubkey()); |
|
|
|
$author = $cacheService->getMetadata($npub); |
|
|
|
$author = $cacheService->getMetadata($npub); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$kind = $article->getKind()?->value ?? 30023; |
|
|
|
|
|
|
|
$pubkey = (string) $article->getPubkey(); |
|
|
|
|
|
|
|
$articleSlug = (string) ($article->getSlug() ?? $slug); |
|
|
|
|
|
|
|
$coordinate = $kind.':'.$pubkey.':'.$articleSlug; |
|
|
|
|
|
|
|
$eid = $article->getEventId(); |
|
|
|
|
|
|
|
$eid = ($eid !== null && $eid !== '' && self::isValidHexEventId($eid)) ? $eid : null; |
|
|
|
|
|
|
|
$articleTitle = (string) ($article->getTitle() ?? ''); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$commentsData = null; |
|
|
|
|
|
|
|
$commentsPreloaded = false; |
|
|
|
|
|
|
|
$cached = $commentThreadLoader->tryLoadFromCacheOnly($coordinate, $eid); |
|
|
|
|
|
|
|
if (null !== $cached) { |
|
|
|
|
|
|
|
$commentsData = $this->enrichCommentDataWithReplyContext( |
|
|
|
|
|
|
|
$cached, |
|
|
|
|
|
|
|
$coordinate, |
|
|
|
|
|
|
|
$eid, |
|
|
|
|
|
|
|
$articleTitle |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
$commentsPreloaded = true; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return $this->render('pages/article.html.twig', [ |
|
|
|
return $this->render('pages/article.html.twig', [ |
|
|
|
'article' => $article, |
|
|
|
'article' => $article, |
|
|
|
'author' => $author, |
|
|
|
'author' => $author, |
|
|
|
'npub' => $npub, |
|
|
|
'npub' => $npub, |
|
|
|
'content' => $cacheItem->get(), |
|
|
|
'content' => $cacheItem->get(), |
|
|
|
|
|
|
|
'comments_data' => $commentsData, |
|
|
|
|
|
|
|
'comments_preloaded' => $commentsPreloaded, |
|
|
|
]); |
|
|
|
]); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -450,6 +513,7 @@ class ArticleController extends AbstractController |
|
|
|
'article' => $article, |
|
|
|
'article' => $article, |
|
|
|
'content' => $content, |
|
|
|
'content' => $content, |
|
|
|
'author' => $user->getMetadata(), |
|
|
|
'author' => $user->getMetadata(), |
|
|
|
|
|
|
|
'comments_preloaded' => false, |
|
|
|
]); |
|
|
|
]); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|