diff --git a/Dockerfile b/Dockerfile
index fbdc52d..bd32add 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -23,6 +23,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
gettext \
git \
libnss3-tools \
+ cron \
&& rm -rf /var/lib/apt/lists/*
RUN set -eux; \
diff --git a/assets/controllers/form-collection_controller.js b/assets/controllers/form-collection_controller.js
new file mode 100644
index 0000000..6b67190
--- /dev/null
+++ b/assets/controllers/form-collection_controller.js
@@ -0,0 +1,18 @@
+import { Controller } from '@hotwired/stimulus';
+
+export default class extends Controller {
+ static targets = ["collectionContainer"]
+
+ static values = {
+ index : Number,
+ prototype: String,
+ }
+
+ addCollectionElement(event)
+ {
+ const item = document.createElement('li');
+ item.innerHTML = this.prototypeValue.replace(/__name__/g, this.indexValue);
+ this.collectionContainerTarget.appendChild(item);
+ this.indexValue++;
+ }
+}
diff --git a/assets/styles/app.css b/assets/styles/app.css
index 9c4ddcf..29a08f0 100644
--- a/assets/styles/app.css
+++ b/assets/styles/app.css
@@ -22,6 +22,7 @@ h1, h2, h3, h4, h5, h6 {
h1 {
font-size: 3.2rem;
margin-top: 0.25em;
+ font-weight: 600;
}
h2 {
@@ -319,8 +320,6 @@ footer p {
}
.alert {
- background-color: var(--color-bg-light);
- color: var(--color-text-mid);
padding: 10px 20px; /* Padding around the text */
border-radius: 5px; /* Rounded corners */
margin: 20px 0; /* Spacing around the alert */
@@ -328,4 +327,40 @@ footer p {
.alert.alert-success {
background-color: var(--color-secondary);
+ color: var(--color-text-contrast);
+}
+
+/* Tabs Container */
+.nav-tabs {
+ display: flex; /* Arrange items in a row */
+ justify-content: center;
+ padding: 0; /* Remove padding */
+ margin: 0; /* Remove margin */
+ list-style: none; /* Remove list item styling */
+}
+
+/* Individual Tab Item */
+.nav-tabs .nav-item {
+ margin: 0; /* No margin around list items */
+}
+
+/* NON-Active Tab */
+.nav-tabs .nav-link {
+ color: var(--color-text);
+ background-color: transparent;
+ border: none;
+}
+
+
+/* Active Tab */
+.nav-tabs .nav-link.active {
+ color: var(--color-text-contrast);
+ background-color: var(--color-primary);
+ font-weight: bold;
+}
+
+/* Content Container */
+.tab-content {
+ padding: 15px; /* Spacing inside the content */
+ border-top: none; /* Remove border overlap with active tab */
}
diff --git a/compose.yaml b/compose.yaml
index 1b9d4d7..897d403 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -5,6 +5,7 @@ services:
context: .
dockerfile: Dockerfile
environment:
+ APP_ENCRYPTION_KEY: '%env(APP_ENCRYPTION_KEY)%'
SERVER_NAME: ${SERVER_NAME:-localhost}, php:80
MERCURE_PUBLISHER_JWT_KEY: ${MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
MERCURE_SUBSCRIBER_JWT_KEY: ${MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
diff --git a/composer.json b/composer.json
index a3a1710..1432a51 100644
--- a/composer.json
+++ b/composer.json
@@ -40,7 +40,8 @@
"symfony/ux-live-component": "^2.21",
"symfony/yaml": "7.1.*",
"twig/extra-bundle": "^2.12|^3.0",
- "twig/twig": "^3.15"
+ "twig/twig": "^3.15",
+ "ext-openssl": "*"
},
"config": {
"allow-plugins": {
diff --git a/config/packages/security.yaml b/config/packages/security.yaml
index bcf5396..58c2449 100644
--- a/config/packages/security.yaml
+++ b/config/packages/security.yaml
@@ -29,6 +29,7 @@ security:
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/admin, roles: ROLE_USER }
+ - { path: ^/nzine, roles: ROLE_USER }
# - { path: ^/profile, roles: ROLE_USER }
when@test:
diff --git a/config/services.yaml b/config/services.yaml
index fcab339..97fe0f0 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -4,6 +4,7 @@
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
+ encryption_key: '%env(APP_ENCRYPTION_KEY)%'
services:
# default configuration for services in *this* file
diff --git a/migrations/Version20241211164528.php b/migrations/Version20241211164528.php
new file mode 100644
index 0000000..a6cc0b2
--- /dev/null
+++ b/migrations/Version20241211164528.php
@@ -0,0 +1,44 @@
+addSql('CREATE TABLE nzine (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, npub VARCHAR(255) NOT NULL, main_categories JSON NOT NULL, lists JSON DEFAULT NULL, editor VARCHAR(255) DEFAULT NULL, nzine_bot_id INT DEFAULT NULL, PRIMARY KEY(id))');
+ $this->addSql('CREATE UNIQUE INDEX UNIQ_65025D9871FD5427 ON nzine (nzine_bot_id)');
+ $this->addSql('CREATE TABLE nzine_bot (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, nsec VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
+ $this->addSql('ALTER TABLE nzine ADD CONSTRAINT FK_65025D9871FD5427 FOREIGN KEY (nzine_bot_id) REFERENCES nzine_bot (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
+ $this->addSql('DROP TABLE journal');
+ $this->addSql('ALTER TABLE app_user ADD nzine_bot_id INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE app_user ADD CONSTRAINT FK_88BDF3E971FD5427 FOREIGN KEY (nzine_bot_id) REFERENCES nzine_bot (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
+ $this->addSql('CREATE UNIQUE INDEX UNIQ_88BDF3E971FD5427 ON app_user (nzine_bot_id)');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('CREATE TABLE journal (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, npub VARCHAR(255) NOT NULL, main_categories JSON NOT NULL, lists JSON DEFAULT NULL, editor VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
+ $this->addSql('ALTER TABLE nzine DROP CONSTRAINT FK_65025D9871FD5427');
+ $this->addSql('DROP TABLE nzine');
+ $this->addSql('DROP TABLE nzine_bot');
+ $this->addSql('ALTER TABLE app_user DROP CONSTRAINT FK_88BDF3E971FD5427');
+ $this->addSql('DROP INDEX UNIQ_88BDF3E971FD5427');
+ $this->addSql('ALTER TABLE app_user DROP nzine_bot_id');
+ }
+}
diff --git a/migrations/Version20241211174012.php b/migrations/Version20241211174012.php
new file mode 100644
index 0000000..14837b7
--- /dev/null
+++ b/migrations/Version20241211174012.php
@@ -0,0 +1,35 @@
+addSql('ALTER TABLE app_user DROP CONSTRAINT fk_88bdf3e971fd5427');
+ $this->addSql('DROP INDEX uniq_88bdf3e971fd5427');
+ $this->addSql('ALTER TABLE app_user DROP nzine_bot_id');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('ALTER TABLE app_user ADD nzine_bot_id INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE app_user ADD CONSTRAINT fk_88bdf3e971fd5427 FOREIGN KEY (nzine_bot_id) REFERENCES nzine_bot (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
+ $this->addSql('CREATE UNIQUE INDEX uniq_88bdf3e971fd5427 ON app_user (nzine_bot_id)');
+ }
+}
diff --git a/migrations/Version20241211175518.php b/migrations/Version20241211175518.php
new file mode 100644
index 0000000..020b00a
--- /dev/null
+++ b/migrations/Version20241211175518.php
@@ -0,0 +1,31 @@
+addSql('ALTER TABLE nzine_bot RENAME COLUMN nsec TO encrypted_nsec');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('ALTER TABLE nzine_bot RENAME COLUMN encrypted_nsec TO nsec');
+ }
+}
diff --git a/migrations/Version20241217153251.php b/migrations/Version20241217153251.php
new file mode 100644
index 0000000..eb1cc9c
--- /dev/null
+++ b/migrations/Version20241217153251.php
@@ -0,0 +1,31 @@
+addSql('CREATE TABLE event (id VARCHAR(225) NOT NULL, kind INT NOT NULL, pubkey VARCHAR(255) NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, tags JSON NOT NULL, sig VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('DROP TABLE event');
+ }
+}
diff --git a/migrations/Version20241217154700.php b/migrations/Version20241217154700.php
new file mode 100644
index 0000000..2498b3d
--- /dev/null
+++ b/migrations/Version20241217154700.php
@@ -0,0 +1,31 @@
+addSql('ALTER TABLE nzine ADD slug TEXT DEFAULT NULL');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('ALTER TABLE nzine DROP slug');
+ }
+}
diff --git a/migrations/Version20241217154841.php b/migrations/Version20241217154841.php
new file mode 100644
index 0000000..1dc3433
--- /dev/null
+++ b/migrations/Version20241217154841.php
@@ -0,0 +1,31 @@
+addSql('ALTER TABLE nzine ADD slug TEXT DEFAULT NULL');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('ALTER TABLE nzine DROP slug');
+ }
+}
diff --git a/migrations/Version20241217170102.php b/migrations/Version20241217170102.php
new file mode 100644
index 0000000..b3ac9bc
--- /dev/null
+++ b/migrations/Version20241217170102.php
@@ -0,0 +1,31 @@
+addSql('ALTER TABLE event ALTER created_at TYPE BIGINT USING EXTRACT(EPOCH FROM created_at)::BIGINT');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('ALTER TABLE event ALTER created_at TYPE TIMESTAMP(0) WITHOUT TIME ZONE');
+ }
+}
diff --git a/migrations/Version20241231150438.php b/migrations/Version20241231150438.php
new file mode 100644
index 0000000..b29ab08
--- /dev/null
+++ b/migrations/Version20241231150438.php
@@ -0,0 +1,31 @@
+addSql('ALTER TABLE event ADD slug TEXT DEFAULT NULL');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('ALTER TABLE event DROP slug');
+ }
+}
diff --git a/migrations/Version20241231153428.php b/migrations/Version20241231153428.php
new file mode 100644
index 0000000..ad81158
--- /dev/null
+++ b/migrations/Version20241231153428.php
@@ -0,0 +1,31 @@
+addSql('ALTER TABLE event DROP slug');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('ALTER TABLE event ADD slug TEXT DEFAULT NULL');
+ }
+}
diff --git a/src/Controller/NzineController.php b/src/Controller/NzineController.php
new file mode 100644
index 0000000..ff9eeae
--- /dev/null
+++ b/src/Controller/NzineController.php
@@ -0,0 +1,284 @@
+createForm(NzineBotType::class);
+ $form->handleRequest($request);
+ $user = $this->getUser();
+
+ // TODO change into a workflow
+ if ($form->isSubmitted() && $form->isValid()) {
+ $data = $form->getData();
+ // create NZine bot
+ $key = new Key();
+ $private_key = '8c55771e896581fffea62c6440e306d502630e9dbd067e484bf6fc9c83ede28c';
+ // $private_key = $key->generatePrivateKey();
+ // $bot = new NzineBot($encryptionService);
+ // $bot->setNsec($private_key);
+ $bot = $entityManager->getRepository(NzineBot::class)->find(1);
+ //$entityManager->persist($bot);
+ //$entityManager->flush();
+ $profileContent = [
+ 'name' => $data['name'],
+ 'about' => $data['about'],
+ 'bot' => true
+ ];
+
+ // publish bot profile
+ $profileEvent = new Event();
+ $profileEvent->setKind(0);
+ $profileEvent->setContent(json_encode($profileContent));
+ $signer = new Sign();
+ $signer->signEvent($profileEvent, $private_key);
+ $eventMessage = new EventMessage($profileEvent);
+ $relayUrl = 'wss://purplepag.es';
+ $relay = new Relay($relayUrl);
+ $relay->setMessage($eventMessage);
+ // $result = $relay->send();
+
+ // create NZine entity
+ $nzine = new Nzine();
+ $public_key = $key->getPublicKey($private_key);
+ $nzine->setNpub($public_key);
+ $nzine->setNzineBot($bot);
+ $nzine->setEditor($user->getUserIdentifier());
+ // $entityManager->persist($nzine);
+ // $entityManager->flush();
+
+ // TODO add EDITOR role to the user
+ $role = RolesEnum::EDITOR->value;
+ $user = $entityManager->getRepository(User::class)->findOneBy(['npub' => $user->getUserIdentifier()]);
+ $user->addRole($role);
+ // $entityManager->persist($user);
+ // $entityManager->flush();
+
+
+ $slugger = new AsciiSlugger();
+ $title = $profileContent['name'];
+ $slug = 'nzine-'.$slugger->slug($title)->lower().'-'.rand(10000,99999);
+ // create NZine main index
+ $index = new Event();
+ $index->setKind(KindsEnum::PUBLICATION_INDEX->value);
+
+ $index->addTag(['d' => $slug]);
+ $index->addTag(['title' => $title]);
+ $index->addTag(['summary' => $profileContent['about']]);
+ $index->addTag(['auto-update' => 'yes']);
+ $index->addTag(['type' => 'magazine']);
+ $signer = new Sign();
+ $signer->signEvent($index, $private_key);
+ // save to persistence, first map to EventEntity
+ $serializer = new Serializer([new ObjectNormalizer()],[new JsonEncoder()]);
+ $i = $serializer->deserialize($index->toJson(), EventEntity::class, 'json');
+ // don't save any more for now
+ $entityManager->persist($i);
+ // $entityManager->flush();
+ // TODO publish index to relays
+
+ // TODO remove this, this is temporary, to not create a host of nzines
+ $nzine = $entityManager->getRepository(Nzine::class)->findOneBy(['npub' => $nzine->getNpub()]);
+ $nzine->setSlug($slug);
+ $entityManager->persist($nzine);
+ $entityManager->flush();
+
+ return $this->redirectToRoute('nzine_edit', ['npub' => $public_key ]);
+ }
+ // on submit, create a key pair and save it securely
+ // create a new NZine entity and link it to the key pair
+ // then redirect to edit
+
+ return $this->render('pages/nzine-editor.html.twig', [
+ 'form' => $form
+ ]);
+ }
+
+ #[Route('/nzine/{npub}', name: 'nzine_edit')]
+ public function edit(Request $request, $npub, EntityManagerInterface $entityManager,
+ ManagerRegistry $managerRegistry, NostrClient $nostrClient): Response
+ {
+ $nzine = $entityManager->getRepository(Nzine::class)->findOneBy(['npub' => $npub]);
+ if (!$nzine) {
+ throw $this->createNotFoundException('N-Zine not found');
+ }
+ try {
+ $bot = $entityManager->getRepository(User::class)->findOneBy(['npub' => $npub]);
+ } catch (\Exception $e) {
+ // sth went wrong, but whatever
+ $managerRegistry->resetManager();
+ }
+
+ $catForm = $this->createForm(NzineType::class, ['categories' => $nzine->getMainCategories()]);
+ $catForm->handleRequest($request);
+ if ($catForm->isSubmitted() && $catForm->isValid()) {
+ // Process and normalize the 'tags' field
+ $data = $catForm->get('categories')->getData();
+
+ $nzine->setMainCategories($data);
+
+// try {
+// $entityManager->beginTransaction();
+// $entityManager->persist($nzine);
+// $entityManager->flush();
+// $entityManager->commit();
+// } catch (Exception $e) {
+// $entityManager->rollback();
+// $managerRegistry->resetManager();
+// }
+
+ // existing indices
+ $indices = $entityManager->getRepository(EventEntity::class)->findBy(['pubkey' => $npub, 'kind' => KindsEnum::PUBLICATION_INDEX]);
+ // get oldest, treat it as root
+ $mainIndex = $indices[0];
+ // TODO create and update indices
+ foreach ($data as $cat) {
+ // find or create new index
+ $slugger = new AsciiSlugger();
+ $title = $cat['title'];
+ $slug = $mainIndex->getSlug().'-'.$slugger->slug($title)->lower();
+ // create category index
+ $index = new Event();
+ $index->setKind(KindsEnum::PUBLICATION_INDEX->value);
+
+ $index->addTag(['d' => $slug]);
+ $index->addTag(['title' => $title]);
+ $index->addTag(['auto-update' => 'yes']);
+ $index->addTag(['type' => 'magazine']);
+ // TODO add indexed items that fall into the category
+
+ $signer = new Sign();
+ // TODO get key
+ // $signer->signEvent($index, $private_key);
+ // save to persistence, first map to EventEntity
+ $serializer = new Serializer([new ObjectNormalizer()],[new JsonEncoder()]);
+ $i = $serializer->deserialize($index->toJson(), EventEntity::class, 'json');
+ // don't save any more for now
+ $entityManager->persist($i);
+ // $entityManager->flush();
+ // TODO publish index to relays
+ }
+
+ // TODO add the new and updated indices to the main index
+
+ // redirect to route nzine_view
+ return $this->redirectToRoute('nzine_view', [
+ 'npub' => $nzine->getNpub(),
+ ]);
+ }
+
+
+ return $this->render('pages/nzine-editor.html.twig', [
+ 'nzine' => $nzine,
+ 'bot' => $bot,
+ 'catForm' => $catForm
+ ]);
+ }
+
+ /**
+ * Update and (re)publish indices,
+ * when you want to look for new articles or
+ * when categories have changed
+ * @return void
+ */
+ #[Route('/nzine/{npub}', name: 'nzine_update')]
+ public function nzineUpdate()
+ {
+ // TODO make this a separate step and create all the indices and populate with articles all at once
+
+ }
+
+
+ #[Route('/nzine/v/{npub}', name: 'nzine_view')]
+ public function nzineView($npub, EntityManagerInterface $entityManager) {
+ $nzine = $entityManager->getRepository(Nzine::class)->findOneBy(['npub' => $npub]);
+ 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]);
+ // TODO Filter out the main index by the d-tag saved to entity or something
+ $main = $indices[0];
+ // let's pretend we have some nested indices in this zine
+ $main->setTags(['a', '30040:'.$npub.':1'.$nzine->getSlug()]);
+ $main->setTags(['a', '30040:'.$npub.':2'.$nzine->getSlug()]);
+
+
+ return $this->render('pages/nzine.html.twig', [
+ 'nzine' => $nzine,
+ 'index' => $main
+ ]);
+ }
+
+ #[Route('/nzine/v/{npub}/{cat}', name: 'nzine_category')]
+ public function nzineCategory($npub, $cat, EntityManagerInterface $entityManager) {
+ $nzine = $entityManager->getRepository(Nzine::class)->findOneBy(['npub' => $npub]);
+ if (!$nzine) {
+ throw $this->createNotFoundException('N-Zine not found');
+ }
+ $bot = $entityManager->getRepository(User::class)->findOneBy(['npub' => $npub]);
+
+ $tags = [];
+ foreach ($nzine->getMainCategories() as $category) {
+ if (isset($category['title']) && $category['title'] === $cat) {
+ $tags = $category['tags'] ?? [];
+ }
+ }
+
+ $all = $entityManager->getRepository(Article::class)->findAll();
+ $list = array_slice($all, 0, 100);
+
+ $filtered = [];
+ foreach ($tags as $tag) {
+ $partial = array_filter($list, function($v) use ($tag) {
+ /* @var Article $v */
+ return in_array($tag, $v->getTopics() ?? []);
+ });
+ $filtered = array_merge($filtered, $partial);
+ }
+
+
+ return $this->render('pages/nzine.html.twig', [
+ 'nzine' => $nzine,
+ 'bot' => $bot,
+ 'list' => $filtered
+ ]);
+ }
+
+}
diff --git a/src/Entity/Article.php b/src/Entity/Article.php
index 841b226..475eb25 100644
--- a/src/Entity/Article.php
+++ b/src/Entity/Article.php
@@ -5,14 +5,18 @@ namespace App\Entity;
use App\Enum\EventStatusEnum;
use App\Enum\IndexStatusEnum;
use App\Enum\KindsEnum;
+use App\Repository\ArticleRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
/**
* Entity storing long-form articles
+ * Needed beyond the Event entity, because of the local functionalities built on top of the original events
+ * - editor
+ * - indexing and search
* NIP-23, kinds 30023, 30024
*/
-#[ORM\Entity]
+#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article
{
#[ORM\Id]
diff --git a/src/Entity/Event.php b/src/Entity/Event.php
index 06bc1f4..aee6777 100644
--- a/src/Entity/Event.php
+++ b/src/Entity/Event.php
@@ -2,14 +2,29 @@
namespace App\Entity;
+use Doctrine\DBAL\Types\Types;
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * Nostr events
+ */
+#[ORM\Entity]
class Event
{
+ #[ORM\Id]
+ #[ORM\Column(length: 225)]
private string $id;
+ #[ORM\Column(type: Types::INTEGER)]
private int $kind = 0;
+ #[ORM\Column(length: 255)]
private string $pubkey = '';
+ #[ORM\Column(type: Types::TEXT)]
private string $content = '';
+ #[ORM\Column(type: Types::BIGINT)]
private int $created_at = 0;
+ #[ORM\Column(type: Types::JSON)]
private array $tags = [];
+ #[ORM\Column(length: 255)]
private string $sig = '';
public function getId(): string
@@ -83,4 +98,24 @@ class Event
}
+ public function getTitle(): ?string
+ {
+ foreach ($this->getTags() as $tag) {
+ if (array_key_first($tag) === 'title') {
+ return $tag['title'];
+ }
+ }
+ return null;
+ }
+
+ public function getSlug(): ?string
+ {
+ foreach ($this->getTags() as $tag) {
+ if (array_key_first($tag) === 'd') {
+ return $tag['d'];
+ }
+ }
+
+ return null;
+ }
}
diff --git a/src/Entity/MainCategory.php b/src/Entity/MainCategory.php
new file mode 100644
index 0000000..f3e7b36
--- /dev/null
+++ b/src/Entity/MainCategory.php
@@ -0,0 +1,30 @@
+title;
+ }
+
+ public function setTitle(string $title): void
+ {
+ $this->title = $title;
+ }
+
+ public function getTags(): array
+ {
+ return $this->tags;
+ }
+
+ public function setTags(array $tags): void
+ {
+ $this->tags = $tags;
+ }
+
+
+}
diff --git a/src/Entity/Journal.php b/src/Entity/Nzine.php
similarity index 50%
rename from src/Entity/Journal.php
rename to src/Entity/Nzine.php
index 7edfd5a..ca4be60 100644
--- a/src/Entity/Journal.php
+++ b/src/Entity/Nzine.php
@@ -2,11 +2,13 @@
namespace App\Entity;
-use App\Repository\JournalRepository;
+use App\Repository\NzineRepository;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
-#[ORM\Entity(repositoryClass: JournalRepository::class)]
-class Journal
+#[ORM\Entity(repositoryClass: NzineRepository::class)]
+class Nzine
{
#[ORM\Id]
#[ORM\GeneratedValue]
@@ -16,8 +18,12 @@ class Journal
#[ORM\Column(length: 255)]
private ?string $npub = null;
- #[ORM\Column]
- private array $mainCategories = [];
+ #[ORM\OneToOne(targetEntity: NzineBot::class)]
+ #[ORM\JoinColumn(nullable: true)]
+ private ?NzineBot $nzineBot = null;
+
+ #[ORM\Column(type: Types::JSON)]
+ private array|ArrayCollection $mainCategories;
#[ORM\Column(nullable: true)]
private ?array $lists = null;
@@ -25,6 +31,18 @@ class Journal
#[ORM\Column(length: 255, nullable: true)]
private ?string $editor = null;
+ /**
+ * Slug (d-tag) of the main index event that contains all the main category indices
+ * @var string|null
+ */
+ #[ORM\Column(type: Types::TEXT, nullable: true)]
+ private ?string $slug = null;
+
+ public function __construct()
+ {
+ $this->mainCategories = new ArrayCollection();
+ }
+
public function getId(): ?int
{
return $this->id;
@@ -42,6 +60,16 @@ class Journal
return $this;
}
+ public function getNsec(): ?string
+ {
+ return $this->nsec;
+ }
+
+ public function setNsec(?string $nsec): void
+ {
+ $this->nsec = $nsec;
+ }
+
public function getMainCategories(): array
{
return $this->mainCategories;
@@ -77,4 +105,24 @@ class Journal
return $this;
}
+
+ public function getNzineBot(): ?NzineBot
+ {
+ return $this->nzineBot;
+ }
+
+ public function setNzineBot(?NzineBot $nzineBot): void
+ {
+ $this->nzineBot = $nzineBot;
+ }
+
+ public function getSlug(): ?string
+ {
+ return $this->slug;
+ }
+
+ public function setSlug(?string $slug): void
+ {
+ $this->slug = $slug;
+ }
}
diff --git a/src/Entity/NzineBot.php b/src/Entity/NzineBot.php
new file mode 100644
index 0000000..79c4b9c
--- /dev/null
+++ b/src/Entity/NzineBot.php
@@ -0,0 +1,45 @@
+id;
+ }
+
+ public function getNsec(): ?string
+ {
+ if (null === $this->nsec && null !== $this->encryptedNsec) {
+ $this->nsec = $this->encryptionService->decrypt($this->encryptedNsec);
+ }
+ return $this->nsec;
+ }
+
+ public function setNsec(?string $nsec): self
+ {
+ $this->nsec = $nsec;
+ $this->encryptedNsec = $this->encryptionService->encrypt($nsec);
+ return $this;
+ }
+}
diff --git a/src/Form/DataTransformer/CommaSeparatedToArrayTransformer.php b/src/Form/DataTransformer/CommaSeparatedToArrayTransformer.php
new file mode 100644
index 0000000..321c738
--- /dev/null
+++ b/src/Form/DataTransformer/CommaSeparatedToArrayTransformer.php
@@ -0,0 +1,52 @@
+add('title', TextType::class, [
+ 'label' => 'Title',
+ ])
+ ->add('tags', TextType::class, [
+ 'label' => 'Tags (comma-separated, no #s)',
+ ]);
+
+ $builder->get('tags')->addModelTransformer($this->transformer);
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'data_class' => null, // This form maps directly to the object array
+ ]);
+ }
+}
diff --git a/src/Form/NzineBotType.php b/src/Form/NzineBotType.php
new file mode 100644
index 0000000..f9dc068
--- /dev/null
+++ b/src/Form/NzineBotType.php
@@ -0,0 +1,32 @@
+add('name', TextType::class, [
+ 'required' => true
+ ])
+ ->add('about', TextareaType::class, [
+ 'required' => false
+ ])
+ ->add('submit', SubmitType::class)
+ ;
+ }
+
+ public function configureOptions(OptionsResolver $resolver)
+ {
+ }
+}
diff --git a/src/Form/NzineType.php b/src/Form/NzineType.php
new file mode 100644
index 0000000..86d707e
--- /dev/null
+++ b/src/Form/NzineType.php
@@ -0,0 +1,30 @@
+add('categories', CollectionType::class, [
+ 'entry_type' => MainCategoryType::class,
+ 'allow_add' => true,
+ 'allow_delete' => true,
+ 'by_reference' => false,
+ 'prototype' => true, // Enables the JavaScript prototype feature
+ 'label' => false,
+ ]);
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ }
+}
diff --git a/src/Repository/ArticleRepository.php b/src/Repository/ArticleRepository.php
new file mode 100644
index 0000000..161f307
--- /dev/null
+++ b/src/Repository/ArticleRepository.php
@@ -0,0 +1,17 @@
+
+ * @extends ServiceEntityRepository
{{ article.summary }}
{{ user.about }}{{ article.title }}
+ {{ article.title }}
{{ user.name }}
+
+
+ An N-Zine is a digital magazine definition for + collecting long form articles from the nostr ecosystem according to specified filters. + The N-Zine can then be read and browsed as a traditional digital magazine made available on this platform. + Additionally, it can be subscribed to using the nostr bot which will be generated during the setup process. + Your currently logged-in npub will be assigned to the N-Zine as an editor, so you can come back later and tweak the filters. +
+ +
+ Choose a title and write a description for your N-Zine.
+ A profile for your N-Zine bot will also be created.
+ The bot will publish an update when a new article is found that matches N-Zine's filters.
+
+ We know it's lame, but right now we cannot automatically update your follows to include the N-Zine bot.
+
{{ bot.about }}
+ ++ Create and edit categories. You can have as many as you like. Aim at up to 9 for the sake of your readers. +
+ + {{ form_start(catForm) }} + + + +{{ index.slug }}
+