diff --git a/assets/controllers/tabular_publish_controller.js b/assets/controllers/tabular_publish_controller.js new file mode 100644 index 0000000..c9a9c6c --- /dev/null +++ b/assets/controllers/tabular_publish_controller.js @@ -0,0 +1,102 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + static targets = ['publishButton', 'status']; + static values = { + publishUrl: String, + csrfToken: String, + eventData: Object + }; + + connect() { + console.log('Tabular data publish controller connected'); + } + + async publish(event) { + event.preventDefault(); + + if (!this.publishUrlValue) { + this.showError('Publish URL is not configured'); + return; + } + if (!this.csrfTokenValue) { + this.showError('Missing CSRF token'); + return; + } + + if (!window.nostr) { + this.showError('Nostr extension not found'); + return; + } + + this.publishButtonTarget.disabled = true; + this.showStatus('Requesting signature from Nostr extension...'); + + try { + // Prepare the event data + const eventData = this.eventDataValue; + delete eventData.sig; // Remove sig if present + delete eventData.id; // Remove id if present + + // Sign the event with Nostr extension + const signedEvent = await window.nostr.signEvent(eventData); + + this.showStatus('Publishing tabular data...'); + + // Send to backend + await this.sendToBackend(signedEvent); + + this.showSuccess('Tabular data published successfully!'); + + // Redirect to the event page + setTimeout(() => { + window.location.href = `/e/${signedEvent.id}`; + }, 2000); + + } catch (error) { + console.error('Publishing error:', error); + this.showError(`Publishing failed: ${error.message}`); + } finally { + this.publishButtonTarget.disabled = false; + } + } + + async sendToBackend(signedEvent) { + const response = await fetch(this.publishUrlValue, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRF-TOKEN': this.csrfTokenValue + }, + body: JSON.stringify({ + event: signedEvent + }) + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`); + } + + return await response.json(); + } + + 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/Controller/TabularDataController.php b/src/Controller/TabularDataController.php new file mode 100644 index 0000000..a527b98 --- /dev/null +++ b/src/Controller/TabularDataController.php @@ -0,0 +1,96 @@ +getUser(); + if (!$user) { + return $this->redirectToRoute('login'); + } + + $form = $this->createForm(TabularDataType::class); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $data = $form->getData(); + + // Create the event + $event = new Event(); + $event->setKind(KindsEnum::TABULAR_DATA->value); + $event->setContent($data['csvContent']); + + // Add tags + $tags = [ + ['title', $data['title']], + ['m', 'text/csv'], + ['M', 'text/csv; charset=utf-8'], + ]; + + if (!empty($data['license'])) { + $tags[] = ['license', $data['license']]; + } + + if (!empty($data['units'])) { + // Parse units, e.g., "col=2,EH/s" -> ['unit', 'col=2', 'EH/s'] + $unitParts = explode(',', $data['units'], 2); + if (count($unitParts) == 2) { + $tags[] = ['unit', trim($unitParts[0]), trim($unitParts[1])]; + } + } + + foreach ($tags as $tag) { + $event->addTag($tag); + } + + // For now, just render the event JSON + return $this->render('tabular_data/preview.html.twig', [ + 'event' => $event, + 'data' => $data, + ]); + } + + return $this->render('tabular_data/publish.html.twig', [ + 'form' => $form->createView(), + ]); + } + + #[Route('/tabular-data/publish', name: 'tabular_data_publish_event', methods: ['POST'])] + public function publishEvent(Request $request, NostrClient $nostrClient): Response + { + $data = json_decode($request->getContent(), true); + if (!$data || !isset($data['event'])) { + return $this->json(['error' => 'Invalid data'], 400); + } + + $signedEvent = $data['event']; + + // Validate the event + if ($signedEvent['kind'] !== KindsEnum::TABULAR_DATA->value) { + return $this->json(['error' => 'Invalid event kind'], 400); + } + + // Publish the event + try { + $nostrClient->publishEvent($signedEvent, ['wss://relay.damus.io', 'wss://nos.lol']); + return $this->json(['success' => true, 'eventId' => $signedEvent['id']]); + } catch (\Exception $e) { + return $this->json(['error' => 'Publishing failed: ' . $e->getMessage()], 500); + } + } +} diff --git a/src/Enum/KindsEnum.php b/src/Enum/KindsEnum.php index 75cceda..d60b234 100644 --- a/src/Enum/KindsEnum.php +++ b/src/Enum/KindsEnum.php @@ -21,4 +21,5 @@ enum KindsEnum: int case HIGHLIGHTS = 9802; case RELAY_LIST = 10002; // NIP-65, Relay list metadata case APP_DATA = 30078; // NIP-78, Arbitrary custom app data + case TABULAR_DATA = 1450; // NIP-XX, Tabular Data (CSV) } diff --git a/src/Form/TabularDataType.php b/src/Form/TabularDataType.php new file mode 100644 index 0000000..abc3ade --- /dev/null +++ b/src/Form/TabularDataType.php @@ -0,0 +1,43 @@ +add('title', TextType::class, [ + 'label' => 'Title', + 'required' => true, + ]) + ->add('csvContent', TextareaType::class, [ + 'label' => 'CSV Content', + 'required' => true, + 'attr' => ['rows' => 10, 'placeholder' => 'date,hashrate\n2025-10-01,795\n2025-10-02,802'], + ]) + ->add('license', TextType::class, [ + 'label' => 'License (optional)', + 'required' => false, + ]) + ->add('units', TextType::class, [ + 'label' => 'Units (optional, e.g., col=2,EH/s)', + 'required' => false, + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + // Configure your form options here + ]); + } +} diff --git a/templates/event/_kind1450_tabular.html.twig b/templates/event/_kind1450_tabular.html.twig new file mode 100644 index 0000000..e69de29 diff --git a/templates/event/index.html.twig b/templates/event/index.html.twig index 6302514..038e17d 100644 --- a/templates/event/index.html.twig +++ b/templates/event/index.html.twig @@ -113,6 +113,8 @@ {# NIP-71 Video Events (kind 21 and 22) #} {% elseif event.kind == 21 or event.kind == 22 %} {% include 'event/_kind22_video.html.twig' %} + {% elseif event.kind == 1450 %} + {% include 'event/_kind1450_tabular.html.twig' %} {% else %} {# Regular event content for non-picture and non-video events #}
diff --git a/templates/tabular_data/preview.html.twig b/templates/tabular_data/preview.html.twig new file mode 100644 index 0000000..030d41e --- /dev/null +++ b/templates/tabular_data/preview.html.twig @@ -0,0 +1,24 @@ +{% extends 'layout.html.twig' %} + +{% block title %}Preview Tabular Data Event{% endblock %} + +{% block body %} +
+
+
+

Event Preview

+
+
+

Here is the generated event. Click "Sign and Publish" to sign with your Nostr key and publish to relays.

+
{{ event|json_encode(constant('JSON_PRETTY_PRINT')) }}
+
+ + Back +
+
+
+{% endblock %} diff --git a/templates/tabular_data/publish.html.twig b/templates/tabular_data/publish.html.twig new file mode 100644 index 0000000..e69de29