|
|
|
@ -4,18 +4,29 @@ declare(strict_types=1); |
|
|
|
|
|
|
|
|
|
|
|
namespace App\Controller; |
|
|
|
namespace App\Controller; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
use App\Entity\Article; |
|
|
|
|
|
|
|
use App\Message\FetchAuthorArticlesMessage; |
|
|
|
|
|
|
|
use App\Repository\ArticleRepository; |
|
|
|
use App\Service\NostrClient; |
|
|
|
use App\Service\NostrClient; |
|
|
|
use App\Service\RedisCacheService; |
|
|
|
use App\Service\RedisCacheService; |
|
|
|
use App\Util\NostrKeyUtil; |
|
|
|
use App\Util\NostrKeyUtil; |
|
|
|
|
|
|
|
use Doctrine\ORM\EntityManagerInterface; |
|
|
|
|
|
|
|
use Elastica\Query\BoolQuery; |
|
|
|
|
|
|
|
use Elastica\Collapse; |
|
|
|
|
|
|
|
use Elastica\Query\Term; |
|
|
|
use Elastica\Query\Terms; |
|
|
|
use Elastica\Query\Terms; |
|
|
|
use Exception; |
|
|
|
use Exception; |
|
|
|
use FOS\ElasticaBundle\Finder\FinderInterface; |
|
|
|
use FOS\ElasticaBundle\Finder\FinderInterface; |
|
|
|
|
|
|
|
use Psr\Cache\InvalidArgumentException; |
|
|
|
use swentel\nostr\Key\Key; |
|
|
|
use swentel\nostr\Key\Key; |
|
|
|
use swentel\nostr\Nip19\Nip19Helper; |
|
|
|
use swentel\nostr\Nip19\Nip19Helper; |
|
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; |
|
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; |
|
|
|
use Symfony\Component\HttpFoundation\Request; |
|
|
|
use Symfony\Component\HttpFoundation\Request; |
|
|
|
use Symfony\Component\HttpFoundation\Response; |
|
|
|
use Symfony\Component\HttpFoundation\Response; |
|
|
|
|
|
|
|
use Symfony\Component\Messenger\Exception\ExceptionInterface; |
|
|
|
use Symfony\Component\Routing\Attribute\Route; |
|
|
|
use Symfony\Component\Routing\Attribute\Route; |
|
|
|
|
|
|
|
use Symfony\Component\Messenger\MessageBusInterface; |
|
|
|
|
|
|
|
use Symfony\Component\Serializer\SerializerInterface; |
|
|
|
|
|
|
|
|
|
|
|
class AuthorController extends AbstractController |
|
|
|
class AuthorController extends AbstractController |
|
|
|
{ |
|
|
|
{ |
|
|
|
@ -108,40 +119,43 @@ class AuthorController extends AbstractController |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* @throws Exception |
|
|
|
* @throws Exception |
|
|
|
|
|
|
|
* @throws ExceptionInterface |
|
|
|
|
|
|
|
* @throws InvalidArgumentException |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
#[Route('/p/{npub}', name: 'author-profile', requirements: ['npub' => '^npub1.*'])] |
|
|
|
#[Route('/p/{npub}', name: 'author-profile', requirements: ['npub' => '^npub1.*'])] |
|
|
|
public function index($npub, NostrClient $nostrClient, RedisCacheService $redisCacheService, FinderInterface $finder): Response |
|
|
|
public function index($npub, RedisCacheService $redisCacheService, FinderInterface $finder, |
|
|
|
|
|
|
|
MessageBusInterface $messageBus): Response |
|
|
|
{ |
|
|
|
{ |
|
|
|
$keys = new Key(); |
|
|
|
$keys = new Key(); |
|
|
|
$pubkey = $keys->convertToHex($npub); |
|
|
|
$pubkey = $keys->convertToHex($npub); |
|
|
|
|
|
|
|
|
|
|
|
$author = $redisCacheService->getMetadata($pubkey); |
|
|
|
$author = $redisCacheService->getMetadata($pubkey); |
|
|
|
// Retrieve long-form content for the author |
|
|
|
|
|
|
|
try { |
|
|
|
// Get articles using Elasticsearch with collapse on slug |
|
|
|
$list = $nostrClient->getLongFormContentForPubkey($npub); |
|
|
|
$boolQuery = new BoolQuery(); |
|
|
|
} catch (Exception $e) { |
|
|
|
$boolQuery->addMust(new Term(['pubkey' => $pubkey])); |
|
|
|
$list = []; |
|
|
|
$query = new \Elastica\Query($boolQuery); |
|
|
|
} |
|
|
|
$query->setSort(['createdAt' => ['order' => 'desc']]); |
|
|
|
// Also look for articles in the Elastica index |
|
|
|
$collapse = new Collapse(); |
|
|
|
$query = new Terms('pubkey', [$pubkey]); |
|
|
|
$collapse->setFieldname('slug'); |
|
|
|
$list = array_merge($list, $finder->find($query, 25)); |
|
|
|
$query->setCollapse($collapse); |
|
|
|
|
|
|
|
$articles = $finder->find($query); |
|
|
|
// Sort articles by date |
|
|
|
|
|
|
|
usort($list, function ($a, $b) { |
|
|
|
// Get latest createdAt for dispatching fetch message |
|
|
|
return $b->getCreatedAt() <=> $a->getCreatedAt(); |
|
|
|
if (!empty($articles)) { |
|
|
|
}); |
|
|
|
$latest = $articles[0]->getCreatedAt()->getTimestamp(); |
|
|
|
|
|
|
|
// Dispatch async message to fetch new articles since latest + 1 |
|
|
|
$articles = []; |
|
|
|
$messageBus->dispatch(new FetchAuthorArticlesMessage($pubkey, $latest + 1)); |
|
|
|
// Deduplicate by slugs |
|
|
|
} else { |
|
|
|
foreach ($list as $item) { |
|
|
|
// No articles, fetch all |
|
|
|
if (!key_exists((string) $item->getSlug(), $articles)) { |
|
|
|
$messageBus->dispatch(new FetchAuthorArticlesMessage($pubkey, 0)); |
|
|
|
$articles[(string) $item->getSlug()] = $item; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return $this->render('pages/author.html.twig', [ |
|
|
|
return $this->render('pages/author.html.twig', [ |
|
|
|
'author' => $author, |
|
|
|
'author' => $author, |
|
|
|
'npub' => $npub, |
|
|
|
'npub' => $npub, |
|
|
|
|
|
|
|
'pubkey' => $pubkey, |
|
|
|
'articles' => $articles, |
|
|
|
'articles' => $articles, |
|
|
|
'is_author_profile' => true, |
|
|
|
'is_author_profile' => true, |
|
|
|
]); |
|
|
|
]); |
|
|
|
@ -157,4 +171,18 @@ class AuthorController extends AbstractController |
|
|
|
$npub = $keys->convertPublicKeyToBech32($pubkey); |
|
|
|
$npub = $keys->convertPublicKeyToBech32($pubkey); |
|
|
|
return $this->redirectToRoute('author-profile', ['npub' => $npub]); |
|
|
|
return $this->redirectToRoute('author-profile', ['npub' => $npub]); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[Route('/articles/render', name: 'render_articles', methods: ['POST'], options: ['csrf_protection' => false])] |
|
|
|
|
|
|
|
public function renderArticles(Request $request, SerializerInterface $serializer): Response |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$data = json_decode($request->getContent(), true); |
|
|
|
|
|
|
|
$articlesJson = json_encode($data['articles'] ?? []); |
|
|
|
|
|
|
|
$articles = $serializer->deserialize($articlesJson, Article::class.'[]', 'json'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Render the articles using the template |
|
|
|
|
|
|
|
return $this->render('articles.html.twig', [ |
|
|
|
|
|
|
|
'articles' => $articles |
|
|
|
|
|
|
|
]); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|