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.
 
 
 

268 lines
8.0 KiB

import { Plugin, TFile, Notice } from "obsidian";
import { ScriptoriumSettings, EventKind, EventMetadata, DEFAULT_SETTINGS } from "./types";
import { ScriptoriumSettingTab } from "./ui/settingsTab";
import { MetadataModal } from "./ui/metadataModal";
import { StructurePreviewModal } from "./ui/structurePreviewModal";
import { readMetadata, writeMetadata, createDefaultMetadata, validateMetadata, mergeWithHeaderTitle } from "./metadataManager";
import { buildEvents } from "./eventManager";
import { saveEvents, loadEvents, eventsFileExists } from "./eventStorage";
import { publishEventsWithRetry } from "./nostr/relayClient";
import { getWriteRelays } from "./relayManager";
import { parseAsciiDocStructure, isAsciiDocDocument } from "./asciidocParser";
import { normalizeSecretKey, getPubkeyFromPrivkey } from "./nostr/eventBuilder";
export default class ScriptoriumPlugin extends Plugin {
settings: ScriptoriumSettings;
async onload() {
await this.loadSettings();
await this.loadPrivateKey();
// Add settings tab
this.addSettingTab(new ScriptoriumSettingTab(this.app, this));
// Register commands
this.addCommand({
id: "create-nostr-events",
name: "Create Nostr Events",
callback: () => this.handleCreateEvents(),
});
this.addCommand({
id: "preview-structure",
name: "Preview Document Structure",
callback: () => this.handlePreviewStructure(),
});
this.addCommand({
id: "publish-events",
name: "Publish Events to Relays",
callback: () => this.handlePublishEvents(),
});
this.addCommand({
id: "edit-metadata",
name: "Edit Metadata",
callback: () => this.handleEditMetadata(),
});
// Status bar
this.addStatusBarItem().setText("Scriptorium");
}
onunload() {}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
async loadPrivateKey() {
// Try to load from environment variable
// Note: In Obsidian, process.env may not be available
// Users should set the key manually in settings or via system environment
try {
// @ts-ignore - process.env may not be typed in Obsidian context
const envKey = typeof process !== "undefined" && process.env?.SCRIPTORIUM_OBSIDIAN_KEY;
if (envKey) {
this.settings.privateKey = envKey;
await this.saveSettings();
}
} catch (error) {
// Environment variable access not available, user must set manually
console.log("Environment variable access not available, use settings to set private key");
}
}
private async getCurrentFile(): Promise<TFile | null> {
const activeFile = this.app.workspace.getActiveFile();
if (!activeFile) {
new Notice("No active file");
return null;
}
return activeFile;
}
private async handleCreateEvents() {
const file = await this.getCurrentFile();
if (!file) return;
if (!this.settings.privateKey) {
new Notice("Please set your private key in settings");
return;
}
try {
const content = await this.app.vault.read(file);
let metadata = await readMetadata(file, this.app);
// Determine event kind from file extension or metadata
let eventKind: EventKind = this.settings.defaultEventKind;
if (file.extension === "adoc" || file.extension === "asciidoc") {
if (isAsciiDocDocument(content)) {
eventKind = 30040;
} else {
eventKind = 30818;
}
} else if (file.extension === "md") {
eventKind = metadata?.kind || this.settings.defaultEventKind;
}
// Create default metadata if none exists
if (!metadata) {
metadata = createDefaultMetadata(eventKind);
}
// Merge with header title for 30040
if (eventKind === 30040 && isAsciiDocDocument(content)) {
const headerTitle = content.split("\n")[0]?.replace(/^=+\s*/, "").trim() || "";
metadata = mergeWithHeaderTitle(metadata, headerTitle);
}
// Validate metadata
const validation = validateMetadata(metadata, eventKind);
if (!validation.valid) {
new Notice(`Metadata validation failed: ${validation.errors.join(", ")}`);
return;
}
// Build events
const result = await buildEvents(file, content, metadata, this.settings.privateKey, this.app);
if (result.errors.length > 0) {
new Notice(`Errors: ${result.errors.join(", ")}`);
return;
}
// Show preview for structured documents
if (result.structure.length > 0) {
new StructurePreviewModal(this.app, result.structure, async () => {
await saveEvents(file, result.events, this.app);
new Notice(`Created ${result.events.length} event(s) and saved to ${file.basename}_events.jsonl`);
}).open();
} else {
await saveEvents(file, result.events, this.app);
new Notice(`Created ${result.events.length} event(s) and saved to ${file.basename}_events.jsonl`);
}
} catch (error: any) {
new Notice(`Error creating events: ${error.message}`);
console.error(error);
}
}
private async handlePreviewStructure() {
const file = await this.getCurrentFile();
if (!file) return;
try {
const content = await this.app.vault.read(file);
if (!isAsciiDocDocument(content)) {
new Notice("This file is not an AsciiDoc document with structure");
return;
}
let metadata = await readMetadata(file, this.app);
if (!metadata || metadata.kind !== 30040) {
metadata = createDefaultMetadata(30040);
}
const headerTitle = content.split("\n")[0]?.replace(/^=+\s*/, "").trim() || "";
metadata = mergeWithHeaderTitle(metadata, headerTitle);
const structure = parseAsciiDocStructure(content, metadata as any);
new StructurePreviewModal(this.app, structure, () => {}).open();
} catch (error: any) {
new Notice(`Error previewing structure: ${error.message}`);
console.error(error);
}
}
private async handlePublishEvents() {
const file = await this.getCurrentFile();
if (!file) return;
if (!this.settings.privateKey) {
new Notice("Please set your private key in settings");
return;
}
const exists = await eventsFileExists(file, this.app);
if (!exists) {
new Notice("No events file found. Please create events first.");
return;
}
try {
const events = await loadEvents(file, this.app);
if (events.length === 0) {
new Notice("No events to publish");
return;
}
const writeRelays = getWriteRelays(this.settings.relayList);
if (writeRelays.length === 0) {
new Notice("No write relays configured. Please fetch relay list in settings.");
return;
}
new Notice(`Publishing ${events.length} event(s) to ${writeRelays.length} relay(s)...`);
const results = await publishEventsWithRetry(writeRelays, events, this.settings.privateKey);
// Count successes
let successCount = 0;
let failureCount = 0;
results.forEach((relayResults) => {
relayResults.forEach((result) => {
if (result.success) {
successCount++;
} else {
failureCount++;
}
});
});
if (failureCount === 0) {
new Notice(`Successfully published all ${successCount} event(s)`);
} else {
new Notice(`Published ${successCount} event(s), ${failureCount} failed`);
}
} catch (error: any) {
new Notice(`Error publishing events: ${error.message}`);
console.error(error);
}
}
private async handleEditMetadata() {
const file = await this.getCurrentFile();
if (!file) return;
try {
let metadata = await readMetadata(file, this.app);
if (!metadata) {
// Determine kind from file extension
let eventKind: EventKind = this.settings.defaultEventKind;
if (file.extension === "adoc" || file.extension === "asciidoc") {
const content = await this.app.vault.read(file);
if (isAsciiDocDocument(content)) {
eventKind = 30040;
} else {
eventKind = 30818;
}
}
metadata = createDefaultMetadata(eventKind);
}
new MetadataModal(this.app, metadata, async (updatedMetadata) => {
await writeMetadata(file, updatedMetadata, this.app);
new Notice("Metadata saved");
}).open();
} catch (error: any) {
new Notice(`Error editing metadata: ${error.message}`);
console.error(error);
}
}
}