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
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(), |
|
]); |
|
} |
|
|
|
}
|
|
|