diff --git a/assets/controllers/nzine_magazine_publish_controller.js b/assets/controllers/nzine_magazine_publish_controller.js
new file mode 100644
index 0000000..434a236
--- /dev/null
+++ b/assets/controllers/nzine_magazine_publish_controller.js
@@ -0,0 +1,82 @@
+import { Controller } from '@hotwired/stimulus';
+
+export default class extends Controller {
+ static targets = ['status', 'publishButton'];
+ static values = {
+ categoryEvents: String,
+ magazineEvent: String,
+ publishUrl: String,
+ nzineSlug: String,
+ csrfToken: String
+ };
+
+ async publish(event) {
+ event.preventDefault();
+
+ if (!this.publishUrlValue || !this.csrfTokenValue || !this.nzineSlugValue) {
+ this.showError('Missing configuration');
+ return;
+ }
+
+ this.publishButtonTarget.disabled = true;
+
+ try {
+ const categoryEvents = JSON.parse(this.categoryEventsValue || '[]');
+ const magazineEvent = JSON.parse(this.magazineEventValue || '{}');
+
+ this.showStatus('Publishing magazine and categories...');
+
+ const response = await fetch(this.publishUrlValue, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRF-TOKEN': this.csrfTokenValue,
+ 'X-Requested-With': 'XMLHttpRequest'
+ },
+ body: JSON.stringify({
+ nzineSlug: this.nzineSlugValue,
+ categoryEvents: categoryEvents,
+ magazineEvent: magazineEvent
+ })
+ });
+
+ if (!response.ok) {
+ const data = await response.json().catch(() => ({}));
+ throw new Error(data.error || `HTTP ${response.status}`);
+ }
+
+ const result = await response.json();
+ this.showSuccess(result.message || 'Magazine published successfully!');
+
+ // Redirect to home or magazine page after a short delay
+ setTimeout(() => {
+ window.location.href = '/';
+ }, 2000);
+
+ } catch (e) {
+ console.error(e);
+ this.showError(e.message || 'Publish failed');
+ } finally {
+ this.publishButtonTarget.disabled = false;
+ }
+ }
+
+ showStatus(message) {
+ if (this.hasStatusTarget) {
+ this.statusTarget.innerHTML = `
${message}
`;
+ }
+ }
+
+ showSuccess(message) {
+ if (this.hasStatusTarget) {
+ this.statusTarget.innerHTML = `${message}
`;
+ }
+ }
+
+ showError(message) {
+ if (this.hasStatusTarget) {
+ this.statusTarget.innerHTML = `${message}
`;
+ }
+ }
+}
+
diff --git a/src/Command/RssFetchCommand.php b/src/Command/RssFetchCommand.php
index bee215e..d05a2e7 100644
--- a/src/Command/RssFetchCommand.php
+++ b/src/Command/RssFetchCommand.php
@@ -461,9 +461,17 @@ class RssFetchCommand extends Command
try {
$this->categoryIndexService->addArticleToCategoryIndex(
$categoryIndices[$categorySlug],
- $articleCoordinate
+ $articleCoordinate,
+ $nzine
);
+ // Flush to ensure the category index is saved to the database
$this->entityManager->flush();
+
+ $this->logger->debug('Added article to category index', [
+ 'article_slug' => $article->getSlug(),
+ 'category_slug' => $categorySlug,
+ 'coordinate' => $articleCoordinate,
+ ]);
} catch (\Exception $e) {
$this->logger->warning('Failed to add article to category index', [
'article_slug' => $article->getSlug(),
@@ -471,6 +479,11 @@ class RssFetchCommand extends Command
'error' => $e->getMessage(),
]);
}
+ } else {
+ $this->logger->warning('Category index not found for matched category', [
+ 'category_slug' => $categorySlug,
+ 'available_indices' => array_keys($categoryIndices),
+ ]);
}
}
diff --git a/src/Controller/DefaultController.php b/src/Controller/DefaultController.php
index e4851e8..0c5d046 100644
--- a/src/Controller/DefaultController.php
+++ b/src/Controller/DefaultController.php
@@ -60,10 +60,26 @@ class DefaultController extends AbstractController
* @throws InvalidArgumentException
*/
#[Route('/mag/{mag}', name: 'magazine-index')]
- public function magIndex(string $mag, RedisCacheService $redisCache) : Response
+ public function magIndex(string $mag, EntityManagerInterface $entityManager) : Response
{
- // redis cache lookup of magazine index by slug
- $magazine = $redisCache->getMagazineIndex($mag);
+ // Get latest magazine index by slug from database
+ $nzines = $entityManager->getRepository(Event::class)->findBy(['kind' => KindsEnum::PUBLICATION_INDEX]);
+
+ // Filter by slug
+ $nzines = array_filter($nzines, function ($index) use ($mag) {
+ return $index->getSlug() === $mag;
+ });
+
+ if (count($nzines) === 0) {
+ throw $this->createNotFoundException('Magazine not found');
+ }
+
+ // Sort by createdAt, keep newest
+ usort($nzines, function ($a, $b) {
+ return $b->getCreatedAt() <=> $a->getCreatedAt();
+ });
+
+ $magazine = array_shift($nzines);
return $this->render('magazine/magazine-front.html.twig', [
'magazine' => $magazine,
@@ -197,9 +213,15 @@ class DefaultController extends AbstractController
}
}
- // Create a simple stdClass object with the tags for template compatibility
- $catIndex = new \stdClass();
- $catIndex->tags = $tags;
+ // Create a proper Event object for template compatibility
+ $catIndex = new \swentel\nostr\Event\Event();
+ $catIndex->setId($eventData['id']);
+ $catIndex->setPublicKey($eventData['pubkey']);
+ $catIndex->setCreatedAt($eventData['created_at']);
+ $catIndex->setKind($eventData['kind']);
+ $catIndex->setTags($tags);
+ $catIndex->setContent($eventData['content']);
+ $catIndex->setSignature($eventData['sig']);
return $this->render('pages/category.html.twig', [
'mag' => $mag,
diff --git a/src/Controller/MagazineWizardController.php b/src/Controller/MagazineWizardController.php
index 35481d0..c2c34bc 100644
--- a/src/Controller/MagazineWizardController.php
+++ b/src/Controller/MagazineWizardController.php
@@ -6,13 +6,17 @@ namespace App\Controller;
use App\Dto\CategoryDraft;
use App\Dto\MagazineDraft;
+use App\Entity\Nzine;
use App\Enum\KindsEnum;
use App\Form\CategoryArticlesType;
use App\Form\MagazineSetupType;
+use App\Repository\NzineRepository;
+use App\Service\EncryptionService;
use App\Service\RedisCacheService;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Cache\CacheItemPoolInterface;
use swentel\nostr\Event\Event;
+use swentel\nostr\Sign\Sign;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
@@ -89,13 +93,23 @@ class MagazineWizardController extends AbstractController
}
#[Route('/magazine/wizard/review', name: 'mag_wizard_review')]
- public function review(Request $request): Response
+ public function review(Request $request, NzineRepository $nzineRepository): Response
{
$draft = $this->getDraft($request);
if (!$draft) {
return $this->redirectToRoute('mag_wizard_setup');
}
+ // Check if this slug belongs to an NZine (which has a bot)
+ $nzine = null;
+ $isNzineEdit = false;
+ if ($draft->slug) {
+ $nzine = $nzineRepository->findOneBy(['slug' => $draft->slug]);
+ if ($nzine && $nzine->getNzineBot()) {
+ $isNzineEdit = true;
+ }
+ }
+
// Build event skeletons (without pubkey/sig/id); created_at client can adjust
$categoryEvents = [];
foreach ($draft->categories as $cat) {
@@ -117,15 +131,25 @@ class MagazineWizardController extends AbstractController
}
// Determine current user's pubkey (hex) from their npub (user identifier)
+ // For NZine edits, use the NZine's npub instead
$pubkeyHex = null;
- $user = $this->getUser();
- if ($user && method_exists($user, 'getUserIdentifier')) {
+ if ($isNzineEdit && $nzine) {
try {
$key = new Key();
- $pubkeyHex = $key->convertToHex($user->getUserIdentifier());
+ $pubkeyHex = $key->convertToHex($nzine->getNpub());
} catch (\Throwable $e) {
$pubkeyHex = null;
}
+ } else {
+ $user = $this->getUser();
+ if ($user && method_exists($user, 'getUserIdentifier')) {
+ try {
+ $key = new Key();
+ $pubkeyHex = $key->convertToHex($user->getUserIdentifier());
+ } catch (\Throwable $e) {
+ $pubkeyHex = null;
+ }
+ }
}
$magTags = [];
@@ -158,6 +182,8 @@ class MagazineWizardController extends AbstractController
'categoryEventsJson' => json_encode($categoryEvents, JSON_UNESCAPED_SLASHES),
'magazineEventJson' => json_encode($magazineEvent, JSON_UNESCAPED_SLASHES),
'csrfToken' => $this->container->get('security.csrf.token_manager')->getToken('nostr_publish')->getValue(),
+ 'isNzineEdit' => $isNzineEdit,
+ 'nzineSlug' => $isNzineEdit ? $draft->slug : null,
]);
}
@@ -250,6 +276,153 @@ class MagazineWizardController extends AbstractController
return new JsonResponse(['ok' => true]);
}
+ #[Route('/api/nzine-index/publish', name: 'api-nzine-index-publish', methods: ['POST'])]
+ public function publishNzineIndexEvent(
+ Request $request,
+ CsrfTokenManagerInterface $csrfTokenManager,
+ NzineRepository $nzineRepository,
+ EncryptionService $encryptionService,
+ CacheItemPoolInterface $redisCache,
+ RedisClient $redis,
+ EntityManagerInterface $entityManager
+ ): JsonResponse {
+ // Verify CSRF token
+ $csrfToken = $request->headers->get('X-CSRF-TOKEN');
+ if (!$csrfTokenManager->isTokenValid(new CsrfToken('nostr_publish', $csrfToken))) {
+ return new JsonResponse(['error' => 'Invalid CSRF token'], 403);
+ }
+
+ $data = json_decode($request->getContent(), true);
+ if (!$data || !isset($data['nzineSlug']) || !isset($data['categoryEvents']) || !isset($data['magazineEvent'])) {
+ return new JsonResponse(['error' => 'Invalid request'], 400);
+ }
+
+ $nzineSlug = $data['nzineSlug'];
+ $categorySkeletons = $data['categoryEvents'];
+ $magazineSkeleton = $data['magazineEvent'];
+
+ // Load the NZine entity
+ $nzine = $nzineRepository->findOneBy(['slug' => $nzineSlug]);
+ if (!$nzine || !$nzine->getNzineBot()) {
+ return new JsonResponse(['error' => 'NZine not found or no bot configured'], 404);
+ }
+
+ // Get the bot's nsec for signing
+ $bot = $nzine->getNzineBot();
+ $bot->setEncryptionService($encryptionService);
+ $nsec = $bot->getNsec();
+ if (!$nsec) {
+ return new JsonResponse(['error' => 'Bot credentials not available'], 500);
+ }
+
+ $key = new Key();
+ $pubkeyHex = $key->getPublicKey($nsec);
+ $signer = new Sign();
+
+ $categoryCoordinates = [];
+
+ try {
+ // 1) Sign and publish each category event
+ foreach ($categorySkeletons as $catSkeleton) {
+ $catEvent = new Event();
+ $catEvent->setKind($catSkeleton['kind'] ?? 30040);
+ $catEvent->setCreatedAt($catSkeleton['created_at'] ?? time());
+ $catEvent->setTags($catSkeleton['tags'] ?? []);
+ $catEvent->setContent($catSkeleton['content'] ?? '');
+
+ // Sign with bot's nsec
+ $signer->signEvent($catEvent, $nsec);
+
+ // Extract slug from d tag
+ $slug = null;
+ foreach ($catEvent->getTags() as $tag) {
+ if (($tag[0] ?? null) === 'd' && isset($tag[1])) {
+ $slug = $tag[1];
+ break;
+ }
+ }
+ if (!$slug) {
+ return new JsonResponse(['error' => 'Category missing d tag'], 400);
+ }
+
+ // Save to Redis
+ $cacheKey = 'magazine-' . $slug;
+ $item = $redisCache->getItem($cacheKey);
+ $item->set($catEvent);
+ $redisCache->save($item);
+
+ // Save to database
+ $eventEntity = new \App\Entity\Event();
+ $eventEntity->setId($catEvent->getId());
+ $eventEntity->setPubkey($catEvent->getPublicKey());
+ $eventEntity->setCreatedAt($catEvent->getCreatedAt());
+ $eventEntity->setKind($catEvent->getKind());
+ $eventEntity->setTags($catEvent->getTags());
+ $eventEntity->setContent($catEvent->getContent());
+ $eventEntity->setSig($catEvent->getSignature());
+ $entityManager->persist($eventEntity);
+
+ // Build coordinate
+ $categoryCoordinates[] = sprintf('30040:%s:%s', $pubkeyHex, $slug);
+ }
+
+ // 2) Build and sign the magazine event with category references
+ $magEvent = new Event();
+ $magEvent->setKind($magazineSkeleton['kind'] ?? 30040);
+ $magEvent->setCreatedAt($magazineSkeleton['created_at'] ?? time());
+
+ // Remove any existing 'a' tags and add the new category coordinates
+ $magTags = array_filter($magazineSkeleton['tags'] ?? [], fn($t) => ($t[0] ?? null) !== 'a');
+ foreach ($categoryCoordinates as $coord) {
+ $magTags[] = ['a', $coord];
+ }
+ $magEvent->setTags($magTags);
+ $magEvent->setContent($magazineSkeleton['content'] ?? '');
+
+ // Sign with bot's nsec
+ $signer->signEvent($magEvent, $nsec);
+
+ // Extract magazine slug
+ $magSlug = null;
+ foreach ($magEvent->getTags() as $tag) {
+ if (($tag[0] ?? null) === 'd' && isset($tag[1])) {
+ $magSlug = $tag[1];
+ break;
+ }
+ }
+ if (!$magSlug) {
+ return new JsonResponse(['error' => 'Magazine missing d tag'], 400);
+ }
+
+ // Save magazine to Redis
+ $cacheKey = 'magazine-' . $magSlug;
+ $item = $redisCache->getItem($cacheKey);
+ $item->set($magEvent);
+ $redisCache->save($item);
+
+ // Save magazine to database
+ $magEventEntity = new \App\Entity\Event();
+ $magEventEntity->setId($magEvent->getId());
+ $magEventEntity->setPubkey($magEvent->getPublicKey());
+ $magEventEntity->setCreatedAt($magEvent->getCreatedAt());
+ $magEventEntity->setKind($magEvent->getKind());
+ $magEventEntity->setTags($magEvent->getTags());
+ $magEventEntity->setContent($magEvent->getContent());
+ $magEventEntity->setSig($magEvent->getSignature());
+ $entityManager->persist($magEventEntity);
+
+ // Record slug in Redis set for admin listing
+ $redis->sAdd('magazine_slugs', $magSlug);
+
+ $entityManager->flush();
+
+ return new JsonResponse(['ok' => true, 'message' => 'NZine magazine updated successfully']);
+
+ } catch (\Throwable $e) {
+ return new JsonResponse(['error' => $e->getMessage()], 500);
+ }
+ }
+
#[Route('/magazine/wizard/cancel', name: 'mag_wizard_cancel', methods: ['GET'])]
public function cancel(Request $request): Response
{
diff --git a/src/Controller/NzineController.php b/src/Controller/NzineController.php
index 18dc3af..92ac1c6 100644
--- a/src/Controller/NzineController.php
+++ b/src/Controller/NzineController.php
@@ -302,20 +302,20 @@ class NzineController extends AbstractController
}
- #[Route('/nzine/v/{npub}', name: 'nzine_view')]
- public function nzineView($npub, EntityManagerInterface $entityManager): Response
+ #[Route('/nzine/v/{pubkey}', name: 'nzine_view')]
+ public function nzineView($pubkey, EntityManagerInterface $entityManager): Response
{
- $nzine = $entityManager->getRepository(Nzine::class)->findOneBy(['npub' => $npub]);
+ $nzine = $entityManager->getRepository(Nzine::class)->findOneBy(['npub' => $pubkey]);
if (!$nzine) {
throw $this->createNotFoundException('N-Zine not found');
}
// Find all index events for this nzine
- $indices = $entityManager->getRepository(EventEntity::class)->findBy(['pubkey' => $npub, 'kind' => KindsEnum::PUBLICATION_INDEX]);
+ $indices = $entityManager->getRepository(EventEntity::class)->findBy(['pubkey' => $pubkey, 'kind' => KindsEnum::PUBLICATION_INDEX]);
$mainIndexCandidates = array_filter($indices, function ($index) use ($nzine) {
return $index->getSlug() == $nzine->getSlug();
});
- dump($indices);die();
+ dump($indices, $mainIndexCandidates);die();
$mainIndex = array_pop($mainIndexCandidates);
diff --git a/src/Service/NzineCategoryIndexService.php b/src/Service/NzineCategoryIndexService.php
new file mode 100644
index 0000000..5d684c7
--- /dev/null
+++ b/src/Service/NzineCategoryIndexService.php
@@ -0,0 +1,310 @@
+ EventEntity
+ * @throws \JsonException
+ */
+ public function ensureCategoryIndices(Nzine $nzine): array
+ {
+ $categories = $nzine->getMainCategories();
+ if (empty($categories)) {
+ return [];
+ }
+
+ $bot = $nzine->getNzineBot();
+ if (!$bot) {
+ $this->logger->warning('Cannot create category indices: nzine bot not found', [
+ 'nzine_id' => $nzine->getId(),
+ ]);
+ return [];
+ }
+
+ $bot->setEncryptionService($this->encryptionService);
+ $privateKey = $bot->getNsec();
+
+ if (!$privateKey) {
+ $this->logger->warning('Cannot create category indices: bot private key not found', [
+ 'nzine_id' => $nzine->getId(),
+ ]);
+ return [];
+ }
+
+ $slugger = new AsciiSlugger();
+ $categoryIndices = [];
+
+ // Load all existing category indices for this nzine at once
+ $existingIndices = $this->entityManager->getRepository(EventEntity::class)
+ ->findBy([
+ 'pubkey' => $nzine->getNpub(),
+ 'kind' => KindsEnum::PUBLICATION_INDEX->value,
+ ]);
+
+ // Index existing events by their d-tag (slug)
+ $existingBySlug = [];
+ foreach ($existingIndices as $existingIndex) {
+ $slug = $this->extractSlugFromTags($existingIndex->getTags());
+ if ($slug) {
+ $existingBySlug[$slug] = $existingIndex;
+ }
+ }
+
+ foreach ($categories as $category) {
+ if (empty($category['title'])) {
+ continue;
+ }
+
+ $title = $category['title'];
+ $slug = !empty($category['slug'])
+ ? $category['slug']
+ : $slugger->slug($title)->lower()->toString();
+
+ // Check if category index already exists
+ if (isset($existingBySlug[$slug])) {
+ $this->logger->debug('Using existing category index', [
+ 'category_slug' => $slug,
+ 'title' => $title,
+ ]);
+ continue;
+ }
+
+ // Create new category index event
+ $event = new Event();
+ $event->setKind(KindsEnum::PUBLICATION_INDEX->value);
+ $event->addTag(['d', $slug]);
+ $event->addTag(['title', $title]);
+ $event->addTag(['auto-update', 'yes']);
+ $event->addTag(['type', 'magazine']);
+
+ // Add tags for RSS matching
+ if (isset($category['tags']) && is_array($category['tags'])) {
+ foreach ($category['tags'] as $tag) {
+ $event->addTag(['t', $tag]);
+ }
+ }
+
+ $event->setPublicKey($nzine->getNpub());
+
+ // Sign the event
+ $signer = new Sign();
+ $signer->signEvent($event, $privateKey);
+
+ // Convert to EventEntity and save
+ $serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
+ $eventEntity = $serializer->deserialize($event->toJson(), EventEntity::class, 'json');
+
+ $this->entityManager->persist($eventEntity);
+ $categoryIndices[$slug] = $eventEntity;
+
+ $this->logger->info('Created category index event', [
+ 'nzine_id' => $nzine->getId(),
+ 'category_title' => $title,
+ 'category_slug' => $slug,
+ ]);
+ }
+
+ $this->entityManager->flush();
+
+ $this->logger->info('Category indices ready', [
+ 'nzine_id' => $nzine->getId(),
+ 'total_categories' => count($categories),
+ 'indexed_by_slug' => array_keys($categoryIndices),
+ ]);
+
+ return $categoryIndices;
+ }
+
+ /**
+ * Extract the slug (d-tag value) from event tags
+ */
+ private function extractSlugFromTags(array $tags): ?string
+ {
+ foreach ($tags as $tag) {
+ if (is_array($tag) && $tag[0] === 'd' && isset($tag[1])) {
+ return $tag[1];
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Add an article to a category index
+ *
+ * @param EventEntity $categoryIndex The category index event
+ * @param string $articleCoordinate The article coordinate (kind:pubkey:slug)
+ * @param Nzine $nzine The nzine entity (needed for signing)
+ */
+ public function addArticleToCategoryIndex(EventEntity $categoryIndex, string $articleCoordinate, Nzine $nzine): void
+ {
+ // Check if article already exists in the index
+ $tags = $categoryIndex->getTags();
+ foreach ($tags as $tag) {
+ if ($tag[0] === 'a' && isset($tag[1]) && $tag[1] === $articleCoordinate) {
+ // Article already in index
+ return;
+ }
+ }
+
+ // Get the bot and private key for signing
+ $bot = $nzine->getNzineBot();
+ if (!$bot) {
+ throw new \RuntimeException('Cannot sign category index: nzine bot not found');
+ }
+
+ $bot->setEncryptionService($this->encryptionService);
+ $privateKey = $bot->getNsec();
+
+ if (!$privateKey) {
+ throw new \RuntimeException('Cannot sign category index: bot private key not found');
+ }
+
+ // Add article coordinate to tags
+ $tags[] = ['a', $articleCoordinate];
+
+ // Create a new Event object with updated tags
+ $event = new Event();
+ $event->setKind($categoryIndex->getKind());
+ $event->setContent($categoryIndex->getContent() ?? '');
+ $event->setPublicKey($categoryIndex->getPubkey());
+
+ // Add all tags including the new article coordinate
+ foreach ($tags as $tag) {
+ $event->addTag($tag);
+ }
+
+ // Sign the event with current timestamp
+ $signer = new Sign();
+ $signer->signEvent($event, $privateKey);
+
+ // Convert to JSON and back to get all properties including sig
+ $eventJson = $event->toJson();
+ $eventData = json_decode($eventJson, true);
+
+ // Update the EventEntity with new tags, signature, ID, and timestamp
+ $categoryIndex->setTags($tags);
+ $categoryIndex->setSig($eventData['sig']);
+ $categoryIndex->setId($eventData['id']);
+ $categoryIndex->setEventId($eventData['id']);
+ $categoryIndex->setCreatedAt($eventData['created_at']);
+
+ $this->entityManager->persist($categoryIndex);
+
+ $this->logger->debug('Added article to category index and re-signed', [
+ 'category_slug' => $this->extractSlugFromTags($tags),
+ 'article_coordinate' => $articleCoordinate,
+ 'event_id' => $eventData['id'],
+ ]);
+ }
+
+ /**
+ * Re-sign and save category index events
+ * Should be called after all articles have been added to ensure valid signatures
+ *
+ * @param array $categoryIndices Map of category slug => EventEntity
+ * @param Nzine $nzine The nzine entity
+ */
+ public function resignCategoryIndices(array $categoryIndices, Nzine $nzine): void
+ {
+ if (empty($categoryIndices)) {
+ return;
+ }
+
+ $bot = $nzine->getNzineBot();
+ if (!$bot) {
+ $this->logger->warning('Cannot re-sign category indices: nzine bot not found', [
+ 'nzine_id' => $nzine->getId(),
+ ]);
+ return;
+ }
+
+ $bot->setEncryptionService($this->encryptionService);
+ $privateKey = $bot->getNsec();
+
+ if (!$privateKey) {
+ $this->logger->warning('Cannot re-sign category indices: bot private key not found', [
+ 'nzine_id' => $nzine->getId(),
+ ]);
+ return;
+ }
+
+ $signer = new Sign();
+ $serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
+
+ foreach ($categoryIndices as $slug => $categoryIndex) {
+ try {
+ // Create a new Event from the existing EventEntity
+ $event = new Event();
+ $event->setKind($categoryIndex->getKind());
+ $event->setContent($categoryIndex->getContent() ?? '');
+ $event->setPublicKey($categoryIndex->getPubkey());
+
+ // Add all tags from the category index
+ foreach ($categoryIndex->getTags() as $tag) {
+ $event->addTag($tag);
+ }
+
+ // Sign the event with current timestamp
+ $signer->signEvent($event, $privateKey);
+
+ // Convert to JSON and back to get all properties including sig
+ $eventJson = $event->toJson();
+ $eventData = json_decode($eventJson, true);
+
+ // Update the EventEntity with new signature and timestamp
+ $categoryIndex->setSig($eventData['sig']);
+ $categoryIndex->setId($eventData['id']);
+ $categoryIndex->setEventId($eventData['id']);
+ $categoryIndex->setCreatedAt($eventData['created_at']);
+
+ $this->entityManager->persist($categoryIndex);
+
+ $this->logger->info('Re-signed category index', [
+ 'category_slug' => $slug,
+ 'event_id' => $eventData['id'],
+ 'article_count' => count(array_filter($categoryIndex->getTags(), fn($tag) => $tag[0] === 'a')),
+ ]);
+ } catch (\Exception $e) {
+ $this->logger->error('Failed to re-sign category index', [
+ 'category_slug' => $slug,
+ 'error' => $e->getMessage(),
+ ]);
+ }
+ }
+
+ $this->entityManager->flush();
+
+ $this->logger->info('Category indices re-signed', [
+ 'nzine_id' => $nzine->getId(),
+ 'count' => count($categoryIndices),
+ ]);
+ }
+}
diff --git a/src/Twig/Components/Organisms/ZineList.php b/src/Twig/Components/Organisms/ZineList.php
index 6441ea2..6091b5d 100644
--- a/src/Twig/Components/Organisms/ZineList.php
+++ b/src/Twig/Components/Organisms/ZineList.php
@@ -3,7 +3,6 @@
namespace App\Twig\Components\Organisms;
use App\Entity\Event;
-use App\Entity\Nzine;
use App\Enum\KindsEnum;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
@@ -23,7 +22,7 @@ final class ZineList
$nzines = $this->entityManager->getRepository(Event::class)->findBy(['kind' => KindsEnum::PUBLICATION_INDEX]);
// filter, only keep type === magazine
- $this->nzines = array_filter($nzines, function ($index) {
+ $filtered = array_filter($nzines, function ($index) {
// look for tags
$tags = $index->getTags();
$isMagType = false;
@@ -46,6 +45,15 @@ final class ZineList
return $isMagType && $isTopLevel;
});
+ // Deduplicate by slug
+ $uniqueNzines = [];
+ foreach ($filtered as $nzine) {
+ $slug = $nzine->getSlug();
+ if ($slug !== null) {
+ $uniqueNzines[$slug] = $nzine;
+ }
+ }
+ $this->nzines = array_values($uniqueNzines);
}
}
diff --git a/templates/magazine/magazine_review.html.twig b/templates/magazine/magazine_review.html.twig
index 3abb835..1e616aa 100644
--- a/templates/magazine/magazine_review.html.twig
+++ b/templates/magazine/magazine_review.html.twig
@@ -3,9 +3,15 @@
{% block body %}
Review & Sign
+ {% if isNzineEdit %}
+
+
You are editing an NZine. Changes will be signed automatically using the NZine bot's credentials.
+
+ {% else %}
Review the details below. When ready, click Sign & Publish. Your NIP-07 extension will be used to sign events.
+ {% endif %}
Magazine
@@ -62,6 +68,24 @@
+ {% if isNzineEdit %}
+
+ {% else %}
+ {% endif %}
{% endblock %}
diff --git a/templates/nzine/list.html.twig b/templates/nzine/list.html.twig
index 094eaa4..9918c46 100644
--- a/templates/nzine/list.html.twig
+++ b/templates/nzine/list.html.twig
@@ -30,7 +30,7 @@
Edit
{% if nzine.hasMainIndex %}
-
View
+
View
{% else %}
View
{% endif %}