From 308708307950cddb60a6c23ee6299e8c1c517b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nu=C5=A1a=20Puk=C5=A1i=C4=8D?= Date: Sat, 29 Mar 2025 15:22:27 +0100 Subject: [PATCH] Add naddr support and static pages, refactor login --- src/Controller/ArticleController.php | 56 ++++++++++++- src/Controller/AuthorController.php | 34 +++++--- src/Controller/StaticController.php | 36 +++++++++ src/Security/NostrAuthenticator.php | 13 ++-- src/Service/NostrClient.php | 35 +++++++++ .../Components/Molecules/UserFromNpub.php | 12 ++- templates/base.html.twig | 2 +- templates/components/Footer.html.twig | 9 ++- templates/components/Molecules/Card.html.twig | 8 +- .../components/SearchComponent.html.twig | 6 +- templates/home.html.twig | 2 +- templates/pages/article.html.twig | 2 +- templates/pages/author.html.twig | 8 +- templates/pages/category.html.twig | 2 +- templates/static/about.html.twig | 34 ++++++++ templates/static/pricing.html.twig | 43 ++++++++++ templates/static/roadmap.html.twig | 60 ++++++++++++++ templates/static/tos.html.twig | 78 +++++++++++++++++++ 18 files changed, 404 insertions(+), 36 deletions(-) create mode 100644 src/Controller/StaticController.php create mode 100644 templates/static/about.html.twig create mode 100644 templates/static/pricing.html.twig create mode 100644 templates/static/roadmap.html.twig create mode 100644 templates/static/tos.html.twig diff --git a/src/Controller/ArticleController.php b/src/Controller/ArticleController.php index 7d7f8ed..0463920 100644 --- a/src/Controller/ArticleController.php +++ b/src/Controller/ArticleController.php @@ -6,6 +6,7 @@ use App\Entity\Article; use App\Enum\KindsEnum; use App\Form\EditorType; use App\Service\NostrClient; +use App\Util\Bech32\Bech32Decoder; use App\Util\CommonMark\Converter; use Doctrine\ORM\EntityManagerInterface; use League\CommonMark\Exception\CommonMarkException; @@ -19,6 +20,59 @@ use Symfony\Component\Workflow\WorkflowInterface; class ArticleController extends AbstractController { + /** + * @throws InvalidArgumentException|CommonMarkException + * @throws \Exception + */ + #[Route('/article/{naddr}', name: 'article-naddr')] + public function naddr(NostrClient $nostrClient, Bech32Decoder $bech32Decoder, $naddr) + { + // decode naddr + list($hrp, $tlv) = $bech32Decoder->decodeAndParseNostrBech32($naddr); + if ($hrp !== 'naddr') { + throw new \Exception('Invalid naddr'); + } + foreach ($tlv as $item) { + // d tag + if ($item['type'] === 0) { + $slug = implode('', array_map('chr', $item['value'])); + } + + // relays + if ($item['type'] === 1) { + $relays[] = implode('', array_map('chr', $item['value'])); + } + // author + if ($item['type'] === 2) { + $str = ''; + foreach ($item['value'] as $byte) { + $str .= str_pad(dechex($byte), 2, '0', STR_PAD_LEFT); + } + $author = $str; + } + if ($item['type'] === 3) { + // big-endian integer + $intValue = 0; + foreach ($item['value'] as $byte) { + $intValue = ($intValue << 8) | $byte; + } + $kind = $intValue; + } + } + + if ($kind !== KindsEnum::LONGFORM->value) { + throw new \Exception('Not a long form article'); + } + + $nostrClient->getLongFormFromNaddr($slug, $relays, $author, $kind); + + if ($slug) { + return $this->redirectToRoute('article-slug', ['slug' => $slug]); + } + + throw new \Exception('No article.'); + } + /** * @throws InvalidArgumentException|CommonMarkException */ @@ -30,7 +84,7 @@ class ArticleController extends AbstractController // check if an item with same eventId already exists in the db $repository = $entityManager->getRepository(Article::class); $articles = $repository->findBy(['slug' => $slug]); - $revisions = count($repository->findBy(['slug' => $slug])); + $revisions = count($articles); if ($revisions > 1) { // sort articles by created at date diff --git a/src/Controller/AuthorController.php b/src/Controller/AuthorController.php index 0730ef1..2bd1dd0 100644 --- a/src/Controller/AuthorController.php +++ b/src/Controller/AuthorController.php @@ -6,11 +6,11 @@ namespace App\Controller; use App\Entity\Article; use App\Entity\Event; -use App\Entity\Nzine; use App\Enum\KindsEnum; use App\Service\NostrClient; use Doctrine\ORM\EntityManagerInterface; use Psr\Cache\InvalidArgumentException; +use swentel\nostr\Key\Key; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -21,18 +21,19 @@ class AuthorController extends AbstractController * @throws \Exception * @throws InvalidArgumentException */ - #[Route('/p/{npub}', name: 'author-profile')] + #[Route('/p/{npub}', name: 'author-profile', requirements: ['npub' => '^npub1.*'])] public function index($npub, EntityManagerInterface $entityManager, NostrClient $client): Response { - - + $keys = new Key(); $meta = $client->getNpubMetadata($npub); $author = (array) json_decode($meta->content ?? '{}'); // $client->getNpubLongForm($npub); - $list = $entityManager->getRepository(Article::class)->findBy(['pubkey' => $npub, 'kind' => KindsEnum::LONGFORM], ['createdAt' => 'DESC']); + $pubkey = $keys->convertToHex($npub); + + $list = $entityManager->getRepository(Article::class)->findBy(['pubkey' => $pubkey, 'kind' => KindsEnum::LONGFORM], ['createdAt' => 'DESC']); // deduplicate by slugs $articles = []; @@ -42,19 +43,32 @@ class AuthorController extends AbstractController } } - $indices = $entityManager->getRepository(Event::class)->findBy(['pubkey' => $npub, 'kind' => KindsEnum::PUBLICATION_INDEX]); + $indices = $entityManager->getRepository(Event::class)->findBy(['pubkey' => $pubkey, 'kind' => KindsEnum::PUBLICATION_INDEX]); - $nzines = $entityManager->getRepository(Nzine::class)->findBy(['editor' => $npub]); + // $nzines = $entityManager->getRepository(Nzine::class)->findBy(['editor' => $pubkey]); - $nzine = $entityManager->getRepository(Nzine::class)->findBy(['npub' => $npub]); + // $nzine = $entityManager->getRepository(Nzine::class)->findBy(['npub' => $npub]); return $this->render('Pages/author.html.twig', [ 'author' => $author, 'npub' => $npub, 'articles' => $articles, - 'nzine' => $nzine, - 'nzines' => $nzines, + 'nzine' => null, + 'nzines' => null, 'idx' => $indices ]); } + + /** + * @throws \Exception + */ + #[Route('/p/{pubkey}', name: 'author-redirect')] + public function authorRedirect($pubkey): Response + { + $keys = new Key(); + + $npub = $keys->convertPublicKeyToBech32($pubkey); + + return $this->redirectToRoute('author-profile', ['npub' => $npub]); + } } diff --git a/src/Controller/StaticController.php b/src/Controller/StaticController.php new file mode 100644 index 0000000..b838648 --- /dev/null +++ b/src/Controller/StaticController.php @@ -0,0 +1,36 @@ +render('static/about.html.twig'); + } + + #[Route('/roadmap')] + public function roadmap(): Response + { + return $this->render('static/roadmap.html.twig'); + } + + #[Route('/pricing')] + public function pricing(): Response + { + return $this->render('static/pricing.html.twig'); + } + + #[Route('/tos')] + public function tos(): Response + { + return $this->render('static/tos.html.twig'); + } +} diff --git a/src/Security/NostrAuthenticator.php b/src/Security/NostrAuthenticator.php index b67b797..b472228 100644 --- a/src/Security/NostrAuthenticator.php +++ b/src/Security/NostrAuthenticator.php @@ -4,6 +4,7 @@ namespace App\Security; use App\Entity\Event; use Mdanter\Ecc\Crypto\Signature\SchnorrSignature; +use swentel\nostr\Key\Key; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; @@ -44,13 +45,15 @@ class NostrAuthenticator extends AbstractAuthenticator implements InteractiveAut if (time() > $event->getCreatedAt() + 60) { throw new AuthenticationException('Expired'); } - // $validity = (new SchnorrSignature())->verify($event->getPubkey(), $event->getSig(), $event->getId()); -// if (!$validity) { -// throw new AuthenticationException('Invalid Authorization header'); -// } + $validity = (new SchnorrSignature())->verify($event->getPubkey(), $event->getSig(), $event->getId()); + if (!$validity) { + throw new AuthenticationException('Invalid Authorization header'); + } + + $key = new Key(); return new SelfValidatingPassport( - new UserBadge($event->getPubkey()) + new UserBadge($key->convertPublicKeyToBech32($event->getPubkey())) ); } diff --git a/src/Service/NostrClient.php b/src/Service/NostrClient.php index 4a55b97..5badb68 100644 --- a/src/Service/NostrClient.php +++ b/src/Service/NostrClient.php @@ -206,6 +206,41 @@ class NostrClient + public function getLongFormFromNaddr($slug, $relayList, $author, $kind): void + { + $subscription = new Subscription(); + $subscriptionId = $subscription->setId(); + $filter = new Filter(); + $filter->setKinds([$kind]); + $filter->setAuthors([$author]); + $filter->setTag('#d', [$slug]); + + $requestMessage = new RequestMessage($subscriptionId, [$filter]); + + if (empty($relayList)) { + $relays = $this->defaultRelaySet; + } else { + $relays = new RelaySet(); + $relays->addRelay(new Relay($relayList[0])); + } + + $request = new Request($relays, $requestMessage); + + $response = $request->send(); + // response is an n-dimensional array, where n is the number of relays in the set + // check that response has events in the results + foreach ($response as $relayRes) { + $filtered = array_filter($relayRes, function ($item) { + return $item->type === 'EVENT'; + }); + if (count($filtered) > 0) { + $this->saveLongFormContent($filtered); + } + } + // TODO handle relays that require auth + } + + /** * User metadata * NIP-01 diff --git a/src/Twig/Components/Molecules/UserFromNpub.php b/src/Twig/Components/Molecules/UserFromNpub.php index 6433ea3..7ef3cd3 100644 --- a/src/Twig/Components/Molecules/UserFromNpub.php +++ b/src/Twig/Components/Molecules/UserFromNpub.php @@ -4,12 +4,14 @@ namespace App\Twig\Components\Molecules; use App\Service\NostrClient; use Psr\Cache\InvalidArgumentException; +use swentel\nostr\Key\Key; use Symfony\Contracts\Cache\CacheInterface; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; #[AsTwigComponent] final class UserFromNpub { + public string $pubkey; public string $npub; public ?array $user = null; @@ -17,14 +19,16 @@ final class UserFromNpub { } - public function mount(string $npub): void + public function mount(string $pubkey): void { - $this->npub = $npub; + $keys = new Key(); + $this->pubkey = $pubkey; + $this->npub = $keys->convertPublicKeyToBech32($pubkey); try { - $this->user = $this->redisCache->get('user_' . $npub, function () use ($npub) { + $this->user = $this->redisCache->get('user_' . $this->npub, function () { try { - $meta = $this->nostrClient->getNpubMetadata($npub); + $meta = $this->nostrClient->getNpubMetadata($this->npub); return (array) json_decode($meta->content); } catch (InvalidArgumentException|\Exception) { return null; diff --git a/templates/base.html.twig b/templates/base.html.twig index 49d8a7e..9228111 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -1,5 +1,5 @@ - + diff --git a/templates/components/Footer.html.twig b/templates/components/Footer.html.twig index 64ae958..2270cdd 100644 --- a/templates/components/Footer.html.twig +++ b/templates/components/Footer.html.twig @@ -1 +1,8 @@ -

{{ "now"|date("Y") }} Newsroom

+ +

{{ "now"|date("Y") }} Decent Newsroom - Preprint

diff --git a/templates/components/Molecules/Card.html.twig b/templates/components/Molecules/Card.html.twig index 22775f4..9eab0dd 100644 --- a/templates/components/Molecules/Card.html.twig +++ b/templates/components/Molecules/Card.html.twig @@ -7,16 +7,16 @@ {% endif %}
- {{ article.createdAt|date('F j') }} +{# {{ article.createdAt|date('F j') }}#}

{{ article.title }}

{{ article.summary }}

- +{##} {% endif %} {% if user is defined %} <{{ tag }} {{ attributes }}> diff --git a/templates/components/SearchComponent.html.twig b/templates/components/SearchComponent.html.twig index d5da0c2..008ab6c 100644 --- a/templates/components/SearchComponent.html.twig +++ b/templates/components/SearchComponent.html.twig @@ -7,10 +7,10 @@ /> - +
@@ -20,7 +20,7 @@ {% if this.results is not empty %} - + {% elseif this.query is not empty %}

{{ 'text.noResults'|trans }}

{% endif %} diff --git a/templates/home.html.twig b/templates/home.html.twig index 4c95422..9cb524a 100644 --- a/templates/home.html.twig +++ b/templates/home.html.twig @@ -7,7 +7,7 @@ {# content #} {# replace list with featured #} - + {% endblock %} {% block aside %} diff --git a/templates/pages/article.html.twig b/templates/pages/article.html.twig index 6f7b9d8..b5143ef 100644 --- a/templates/pages/article.html.twig +++ b/templates/pages/article.html.twig @@ -8,7 +8,7 @@ {% if author %}