clone of github.com/decent-newsroom/newsroom
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.
 
 
 
 
 
 

166 lines
5.0 KiB

import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['status', 'publishButton', 'computedPreview'];
static values = {
categoryEvents: String,
magazineEvent: String,
publishUrl: String,
csrfToken: String
};
async connect() {
try {
console.debug('[nostr-index-sign] connected');
await this.preparePreview();
} catch (_) {}
}
async preparePreview() {
try {
const catSkeletons = JSON.parse(this.categoryEventsValue || '[]');
const magSkeleton = JSON.parse(this.magazineEventValue || '{}');
let pubkey = '<pubkey>';
if (window.nostr && typeof window.nostr.getPublicKey === 'function') {
try { pubkey = await window.nostr.getPublicKey(); } catch (_) {}
}
const categoryCoordinates = [];
for (let i = 0; i < catSkeletons.length; i++) {
const evt = catSkeletons[i];
const slug = this.extractSlug(evt.tags);
if (slug) {
categoryCoordinates.push(`30040:${pubkey}:${slug}`);
}
}
const previewMag = JSON.parse(JSON.stringify(magSkeleton));
previewMag.tags = (previewMag.tags || []).filter(t => t[0] !== 'a');
categoryCoordinates.forEach(c => previewMag.tags.push(['a', c]));
previewMag.pubkey = pubkey;
if (this.hasComputedPreviewTarget) {
this.computedPreviewTarget.textContent = JSON.stringify(previewMag, null, 2);
}
} catch (e) {
// no-op preview errors
}
}
async signAndPublish(event) {
event.preventDefault();
if (!window.nostr) {
this.showError('Nostr extension not found');
return;
}
if (!this.publishUrlValue || !this.csrfTokenValue) {
this.showError('Missing config');
return;
}
this.publishButtonTarget.disabled = true;
try {
const pubkey = await window.nostr.getPublicKey();
const catSkeletons = JSON.parse(this.categoryEventsValue || '[]');
const magSkeleton = JSON.parse(this.magazineEventValue || '{}');
const categoryCoordinates = [];
// 1) Publish each category index
for (let i = 0; i < catSkeletons.length; i++) {
const evt = catSkeletons[i];
this.ensureCreatedAt(evt);
this.ensureContent(evt);
evt.pubkey = pubkey;
const slug = this.extractSlug(evt.tags);
if (!slug) throw new Error('Category missing slug (d tag)');
this.showStatus(`Signing category ${i + 1}/${catSkeletons.length}`);
const signed = await window.nostr.signEvent(evt);
this.showStatus(`Publishing category ${i + 1}/${catSkeletons.length}`);
await this.publishSigned(signed);
// Coordinate for the category index (kind:pubkey:slug)
const coord = `30040:${pubkey}:${slug}`;
categoryCoordinates.push(coord);
}
// 2) Build magazine event with 'a' tags referencing cats
this.showStatus('Preparing magazine index…');
this.ensureCreatedAt(magSkeleton);
this.ensureContent(magSkeleton);
magSkeleton.pubkey = pubkey;
// Remove any pre-existing 'a' to avoid duplicates, then add new ones
magSkeleton.tags = (magSkeleton.tags || []).filter(t => t[0] !== 'a');
categoryCoordinates.forEach(c => magSkeleton.tags.push(['a', c]));
// 3) Sign and publish magazine
this.showStatus('Signing magazine index…');
const signedMag = await window.nostr.signEvent(magSkeleton);
this.showStatus('Publishing magazine index…');
await this.publishSigned(signedMag);
this.showSuccess('Published magazine and categories successfully');
} catch (e) {
console.error(e);
this.showError(e.message || 'Publish failed');
} finally {
this.publishButtonTarget.disabled = false;
}
}
async publishSigned(signedEvent) {
const res = await fetch(this.publishUrlValue, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': this.csrfTokenValue,
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ event: signedEvent })
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || `HTTP ${res.status}`);
}
return res.json();
}
extractSlug(tags) {
if (!Array.isArray(tags)) return null;
for (const t of tags) {
if (Array.isArray(t) && t[0] === 'd' && t[1]) return t[1];
}
return null;
}
ensureCreatedAt(evt) {
if (!evt.created_at) evt.created_at = Math.floor(Date.now() / 1000);
}
ensureContent(evt) {
if (typeof evt.content !== 'string') evt.content = '';
}
showStatus(message) {
if (this.hasStatusTarget) {
this.statusTarget.innerHTML = `<div class="alert alert-info">${message}</div>`;
}
}
showSuccess(message) {
if (this.hasStatusTarget) {
this.statusTarget.innerHTML = `<div class="alert alert-success">${message}</div>`;
}
}
showError(message) {
if (this.hasStatusTarget) {
this.statusTarget.innerHTML = `<div class="alert alert-danger">${message}</div>`;
}
}
}