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.
327 lines
10 KiB
327 lines
10 KiB
import { Plugin, TFile, Notice, Menu } from "obsidian"; |
|
import { ScriptoriumSettings, EventKind, DEFAULT_SETTINGS } from "./types"; |
|
import { ScriptoriumSettingTab } from "./ui/settingsTab"; |
|
import { NewDocumentModal } from "./ui/newDocumentModal"; |
|
import { writeMetadata, createDefaultMetadata } from "./metadataManager"; |
|
import { safeConsoleError } from "./utils/security"; |
|
import { showErrorNotice } from "./utils/errorHandling"; |
|
import { log, logError } from "./utils/console"; |
|
import { getFolderNameForKind } from "./utils/eventKind"; |
|
import { |
|
getCurrentFile, |
|
ensureNostrNotesFolder, |
|
handleCreateEvents, |
|
handlePreviewStructure, |
|
handlePublishEvents, |
|
handleEditMetadata, |
|
} from "./commands/commandHandlers"; |
|
|
|
export default class ScriptoriumPlugin extends Plugin { |
|
settings!: ScriptoriumSettings; |
|
|
|
async onload() { |
|
log("Plugin loading..."); |
|
|
|
try { |
|
await this.loadSettings(); |
|
await this.loadPrivateKey(); |
|
|
|
// Note: We don't register file extensions for .adoc or .asciidoc files |
|
// Users should install the obsidian-asciidoc plugin for .adoc file support |
|
log("Plugin loaded - file extensions not registered"); |
|
log("Install obsidian-asciidoc plugin for .adoc file editing support"); |
|
|
|
// 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(), |
|
}); |
|
|
|
this.addCommand({ |
|
id: "new-nostr-document", |
|
name: "New Nostr Document", |
|
callback: () => this.handleNewDocument(), |
|
}); |
|
|
|
// Add ribbon icon with menu for Nostr actions |
|
const ribbonIcon = this.addRibbonIcon("zap", "Nostr", () => { |
|
// Create and show menu |
|
const menu = new Menu(); |
|
menu.addItem((item) => { |
|
item.setTitle("Write Nostr note") |
|
.setIcon("file-plus") |
|
.onClick(() => this.handleNewDocument()); |
|
}); |
|
menu.addItem((item) => { |
|
item.setTitle("Create Nostr events") |
|
.setIcon("file-check") |
|
.onClick(() => this.handleCreateEvents()); |
|
}); |
|
menu.addItem((item) => { |
|
item.setTitle("Publish events to relays") |
|
.setIcon("upload") |
|
.onClick(() => this.handlePublishEvents()); |
|
}); |
|
|
|
// Show menu at the ribbon icon position |
|
if (ribbonIcon) { |
|
const rect = ribbonIcon.getBoundingClientRect(); |
|
menu.showAtPosition({ x: rect.left, y: rect.bottom + 5 }); |
|
} |
|
}); |
|
|
|
// Status bar |
|
this.addStatusBarItem().setText("Scriptorium"); |
|
|
|
log("Plugin loaded successfully"); |
|
} catch (error: any) { |
|
logError("Error loading plugin", error); |
|
safeConsoleError("Error loading plugin:", error); |
|
} |
|
} |
|
|
|
onunload() {} |
|
|
|
async loadSettings() { |
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); |
|
} |
|
|
|
async saveSettings() { |
|
await this.saveData(this.settings); |
|
} |
|
|
|
async loadPrivateKey(): Promise<boolean> { |
|
// Load private key from environment variable only |
|
try { |
|
// @ts-ignore - process.env may not be typed in Obsidian context |
|
if (typeof process !== "undefined" && process.env?.SCRIPTORIUM_OBSIDIAN_KEY) { |
|
const envKey = process.env.SCRIPTORIUM_OBSIDIAN_KEY.trim(); |
|
if (envKey) { |
|
this.settings.privateKey = envKey; |
|
await this.saveSettings(); |
|
return true; |
|
} |
|
} |
|
} catch (error) { |
|
// Environment variable access not available |
|
} |
|
|
|
return false; |
|
} |
|
|
|
|
|
private async handleCreateEvents() { |
|
const file = await getCurrentFile(this.app); |
|
if (!file) return; |
|
await handleCreateEvents(this.app, file, this.settings); |
|
} |
|
|
|
private async handlePreviewStructure() { |
|
const file = await getCurrentFile(this.app); |
|
if (!file) return; |
|
await handlePreviewStructure(this.app, file); |
|
} |
|
|
|
private async handlePublishEvents() { |
|
const file = await getCurrentFile(this.app); |
|
if (!file) return; |
|
await handlePublishEvents(this.app, file, this.settings); |
|
} |
|
|
|
private async handleEditMetadata() { |
|
const file = await getCurrentFile(this.app); |
|
if (!file) return; |
|
await handleEditMetadata(this.app, file, this.settings.defaultEventKind); |
|
} |
|
|
|
private async handleNewDocument() { |
|
new NewDocumentModal(this.app, async (kind: EventKind, title: string) => { |
|
try { |
|
log(`Creating new document: kind=${kind}, title=${title}`); |
|
|
|
// Ensure folder structure exists |
|
const folderPath = await ensureNostrNotesFolder(this.app, kind); |
|
|
|
// Sanitize filename from title |
|
const sanitizedTitle = this.sanitizeFilename(title); |
|
|
|
// Determine file extension based on kind |
|
let extension = "md"; |
|
if (kind === 30040 || kind === 30041 || kind === 30818) { |
|
extension = "adoc"; |
|
} |
|
|
|
// Create file path in the appropriate folder |
|
const filename = `${sanitizedTitle}.${extension}`; |
|
const filePath = `${folderPath}/${filename}`; |
|
|
|
// Check if file already exists |
|
const existingFile = this.app.vault.getAbstractFileByPath(filePath); |
|
if (existingFile) { |
|
new Notice(`File ${filename} already exists`); |
|
return; |
|
} |
|
|
|
// Create default content based on kind |
|
// Note: This content will be replaced by writeMetadata() which formats |
|
// the file properly with metadata. We just need minimal content here. |
|
let content = ""; |
|
if (kind === 30040 || kind === 30041 || kind === 30818) { |
|
// AsciiDoc files - minimal content, writeMetadata will format properly |
|
content = `= ${title}\n\n`; |
|
} else if (kind === 30023 || kind === 30817) { |
|
// Markdown files - add title as heading |
|
content = `# ${title}\n\n`; |
|
} else if (kind === 1 || kind === 11) { |
|
// For kind 1 and 11, add a simple placeholder |
|
content = `\n`; |
|
} |
|
|
|
// Create the file - ensure we have at least a newline for empty files |
|
if (!content) { |
|
content = "\n"; |
|
} |
|
|
|
let file: TFile; |
|
try { |
|
log(`Creating file: ${filePath}`); |
|
file = await this.app.vault.create(filePath, content); |
|
log(`File created successfully: ${file.path}`); |
|
|
|
// Verify file was actually created |
|
const verifyFile = this.app.vault.getAbstractFileByPath(filePath); |
|
if (!verifyFile || !(verifyFile instanceof TFile)) { |
|
const msg = `Error: File ${filename} was not created properly`; |
|
log(msg); |
|
new Notice(msg); |
|
logError("File creation verification failed", { filePath }); |
|
return; |
|
} |
|
} catch (error: any) { |
|
logError("Error creating file", error); |
|
showErrorNotice("Error creating file", error); |
|
return; |
|
} |
|
|
|
// Create metadata with title preset from the filename |
|
const metadata = createDefaultMetadata(kind); |
|
// Set title if provided (skip for kind 1 which doesn't have title) |
|
if (kind !== 1 && title && title.trim()) { |
|
(metadata as any).title = title.trim(); |
|
} |
|
|
|
try { |
|
// Write metadata with all placeholders (title will be included if set) |
|
await writeMetadata(file, metadata, this.app); |
|
} catch (error: any) { |
|
showErrorNotice("Error creating metadata", error); |
|
safeConsoleError("Error creating metadata:", error); |
|
// Continue anyway - file was created |
|
} |
|
|
|
// For .adoc files, try to auto-open after a longer delay to allow obsidian-asciidoc plugin to initialize |
|
if (file.extension === "adoc" || file.extension === "asciidoc") { |
|
log(`AsciiDoc file created: ${file.path}`); |
|
|
|
// Wait longer for obsidian-asciidoc plugin to be ready |
|
await new Promise(resolve => setTimeout(resolve, 500)); |
|
|
|
try { |
|
log(`Attempting to open AsciiDoc file: ${file.path}`); |
|
const leaf = this.app.workspace.getMostRecentLeaf(); |
|
if (leaf && leaf.view) { |
|
await leaf.openFile(file, { active: true }); |
|
log("AsciiDoc file opened successfully"); |
|
new Notice(`Created and opened ${filename}`); |
|
} else { |
|
const newLeaf = this.app.workspace.getLeaf("tab"); |
|
await newLeaf.openFile(file, { active: true }); |
|
log("AsciiDoc file opened in new leaf"); |
|
new Notice(`Created and opened ${filename}`); |
|
} |
|
} catch (error: any) { |
|
logError("Error opening AsciiDoc file", error); |
|
// Don't show error to user - file was created successfully |
|
new Notice(`Created ${filename} in ${folderPath}. You may need to open it manually.`); |
|
} |
|
} else { |
|
// Open the new file in Obsidian workspace (use active leaf or create new) |
|
// Use a small delay to ensure file is fully created before opening |
|
log("Waiting before opening file..."); |
|
await new Promise(resolve => setTimeout(resolve, 200)); |
|
|
|
try { |
|
log(`Attempting to open file: ${file.path} (extension: ${file.extension})`); |
|
|
|
const leaf = this.app.workspace.getMostRecentLeaf(); |
|
if (leaf && leaf.view) { |
|
await leaf.openFile(file, { active: true }); |
|
log("File opened successfully in existing leaf"); |
|
} else { |
|
// Fallback: open in new leaf |
|
const newLeaf = this.app.workspace.getLeaf("tab"); |
|
await newLeaf.openFile(file, { active: true }); |
|
log("File opened successfully in new leaf"); |
|
} |
|
} catch (error: any) { |
|
logError("Error opening file", error); |
|
safeConsoleError("Error opening file:", error); |
|
// File was created, just couldn't open it - show a notice |
|
new Notice(`File created but couldn't open: ${file.name}`); |
|
} |
|
} |
|
|
|
new Notice(`Created ${filename} in ${folderPath}`); |
|
} catch (error: any) { |
|
showErrorNotice("Error creating document", error); |
|
safeConsoleError("Error creating document:", error); |
|
} |
|
}).open(); |
|
} |
|
|
|
/** |
|
* Sanitize a string to be used as a filename |
|
*/ |
|
private sanitizeFilename(title: string): string { |
|
// Remove or replace invalid filename characters |
|
let sanitized = title |
|
.replace(/[<>:"/\\|?*]/g, "-") // Replace invalid chars with dash |
|
.replace(/\s+/g, "-") // Replace spaces with dash |
|
.replace(/-+/g, "-") // Replace multiple dashes with single |
|
.replace(/^-+|-+$/g, ""); // Remove leading/trailing dashes |
|
|
|
// Limit length |
|
if (sanitized.length > 100) { |
|
sanitized = sanitized.substring(0, 100); |
|
} |
|
|
|
// Ensure it's not empty |
|
if (!sanitized) { |
|
sanitized = "untitled"; |
|
} |
|
|
|
return sanitized; |
|
} |
|
}
|
|
|