You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

241 lines
8.8 KiB

<?php
namespace App\Controller;
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;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Cache\InvalidArgumentException;
use swentel\nostr\Key\Key;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\String\Slugger\AsciiSlugger;
use Symfony\Component\Workflow\WorkflowInterface;
class ArticleController extends AbstractController
{
/**
* @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');
}
if (empty($relays ?? [])) {
// get author npub relays from their config
$relays = $nostrClient->getNpubRelays($author);
}
$nostrClient->getLongFormFromNaddr($slug, $relays ?? null, $author, $kind);
if ($slug) {
return $this->redirectToRoute('article-slug', ['slug' => $slug]);
}
throw new \Exception('No article.');
}
/**
* @throws InvalidArgumentException|CommonMarkException
*/
#[Route('/article/d/{slug}', name: 'article-slug')]
public function article(EntityManagerInterface $entityManager, CacheItemPoolInterface $articlesCache,
NostrClient $nostrClient, Converter $converter, $slug): Response
{
$article = null;
// check if an item with same eventId already exists in the db
$repository = $entityManager->getRepository(Article::class);
$articles = $repository->findBy(['slug' => $slug]);
$revisions = count($articles);
if ($revisions === 0) {
throw $this->createNotFoundException('The article could not be found');
}
if ($revisions > 1) {
// sort articles by created at date
usort($articles, function ($a, $b) {
return $b->getCreatedAt() <=> $a->getCreatedAt();
});
// get the last article
$article = end($articles);
} else {
$article = $articles[0];
}
$cacheKey = 'article_' . $article->getId();
$cacheItem = $articlesCache->getItem($cacheKey);
if (!$cacheItem->isHit()) {
$cacheItem->set($converter->convertToHtml($article->getContent()));
$articlesCache->save($cacheItem);
}
// // suggestions
// $suggestions = $repository->findBy(['pubkey' => $article->getPubkey()], ['createdAt' => 'DESC'], 3);
// // skip current, if listed in suggestions
// $suggestions = array_filter($suggestions, function ($s) use ($article) {
// return $s->getId() !== $article->getId();
// });
// $suggestions = array_merge($suggestions, $repository->findBy([], ['createdAt' => 'DESC'], 6 - count($suggestions)));
// // sort by date
// usort($suggestions, function ($a, $b) {
// return $b->getCreatedAt() <=> $a->getCreatedAt();
// });
try {
$meta = $nostrClient->getNpubMetadata($article->getPubkey());
if ($meta?->content) {
$author = (array) json_decode($meta->content);
} else {
$author = [
'name' => '<anonymous>'
];
}
} catch (\Exception $e) {
// Whatever?
}
return $this->render('Pages/article.html.twig', [
'article' => $article,
'author' => $author ?? null,
'content' => $cacheItem->get(),
//'suggestions' => $suggestions
]);
}
/**
* Create new article
* @throws InvalidArgumentException
* @throws \Exception
*/
#[Route('/article-editor/create', name: 'editor-create')]
#[Route('/article-editor/edit/{id}', name: 'editor-edit')]
public function newArticle(Request $request, EntityManagerInterface $entityManager, CacheItemPoolInterface $articlesCache,
WorkflowInterface $articlePublishingWorkflow, Article $article = null): Response
{
if (!$article) {
$article = new Article();
$article->setKind(KindsEnum::LONGFORM);
$article->setCreatedAt(new \DateTimeImmutable());
$formAction = $this->generateUrl('editor-create');
} else {
$formAction = $this->generateUrl('editor-edit', ['id' => $article->getId()]);
}
$form = $this->createForm(EditorType::class, $article, ['action' => $formAction]);
$form->handleRequest($request);
// Step 3: Check if the form is submitted and valid
if ($form->isSubmitted() && $form->isValid()) {
$user = $this->getUser();
$key = new Key();
$currentPubkey = $key->convertToHex($user->getUserIdentifier());
if ($article->getPubkey() === null) {
$article->setPubkey($currentPubkey);
}
// Check which button was clicked
if ($form->getClickedButton() === $form->get('actions')->get('submit')) {
// Save button was clicked, handle the "Publish" action
$this->addFlash('success', 'Product published!');
} elseif ($form->getClickedButton() === $form->get('actions')->get('draft')) {
// Save and Publish button was clicked, handle the "Draft" action
$this->addFlash('success', 'Product saved as draft!');
} elseif ($form->getClickedButton() === $form->get('actions')->get('preview')) {
// Preview button was clicked, handle the "Preview" action
// construct slug from title and save to tags
$slugger = new AsciiSlugger();
$slug = $slugger->slug($article->getTitle())->lower();
$article->setSig(''); // clear the sig
$article->setSlug($slug);
$cacheKey = 'article_' . $currentPubkey . '_' . $article->getSlug();
$cacheItem = $articlesCache->getItem($cacheKey);
$cacheItem->set($article);
$articlesCache->save($cacheItem);
return $this->redirectToRoute('article-preview', ['d' => $article->getSlug()]);
}
}
// load template with content editor
return $this->render('pages/editor.html.twig', [
'article' => $article,
'form' => $this->createForm(EditorType::class, $article)->createView(),
]);
}
/**
* Preview article
* @throws InvalidArgumentException
* @throws CommonMarkException
* @throws \Exception
*/
#[Route('/article-preview/{d}', name: 'article-preview')]
public function preview($d, Converter $converter,
CacheItemPoolInterface $articlesCache): Response
{
$user = $this->getUser();
$key = new Key();
$currentPubkey = $key->convertToHex($user->getUserIdentifier());
$cacheKey = 'article_' . $currentPubkey . '_' . $d;
$cacheItem = $articlesCache->getItem($cacheKey);
$article = $cacheItem->get();
$content = $converter->convertToHtml($article->getContent());
return $this->render('pages/article.html.twig', [
'article' => $article,
'content' => $content,
'author' => $user->getMetadata(),
]);
}
}