diff --git a/README.md b/README.md index b0bdde4..1a57b13 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,44 @@ An Obsidian plugin for creating, editing, and publishing Nostr document events d ## Setup -1. Set your Nostr private key in the environment variable `SCRIPTORIUM_OBSIDIAN_KEY`: - - Format: `nsec1...` (bech32) or 64-character hex string -2. Open Obsidian settings → Scriptorium Nostr -3. Click "Refresh from Env" to load your private key -4. Click "Fetch" to get your relay list from Nostr relays +### Private Key Configuration + +You have three options to set your private key: + +**Option 1: Manual Entry (Easiest)** +1. Open Obsidian settings → Scriptorium Nostr +2. Enter your private key in the password field +3. Click "Set Key" + +**Option 2: File in Vault (Most Reliable)** +1. Create a file named `.scriptorium_key` in your vault root +2. Put your private key on a single line (nsec1... or 64-char hex) +3. Open plugin settings and click "Refresh" + +**Option 3: Environment Variable** +1. Set `SCRIPTORIUM_OBSIDIAN_KEY` in your terminal: + ```bash + export SCRIPTORIUM_OBSIDIAN_KEY="nsec1..." + ``` +2. **Important:** Launch Obsidian from the same terminal: + ```bash + obsidian + ``` + (If Obsidian is already running, close it and restart from the terminal) +3. Open plugin settings → Scriptorium Nostr +4. Click "Refresh" to load your private key + +**Note:** Environment variables are only available to processes launched from the terminal where they were set. If you launch Obsidian from a desktop shortcut or application menu, it won't have access to the environment variable. You must launch Obsidian from the terminal where you set the variable. + +See [ENV_SETUP.md](ENV_SETUP.md) for detailed instructions on setting environment variables. + +**Key Format:** `nsec1...` (bech32) or 64-character hex string + +### Relay Configuration + +1. Open Obsidian settings → Scriptorium Nostr +2. Click "Fetch" to get your relay list from Nostr relays +3. The plugin will automatically fetch from `wss://profiles.nostr1.com`, `wss://relay.damus.io`, and `wss://thecitadel.nostr1.com` ## Usage diff --git a/src/main.ts b/src/main.ts index d50fb99..1e7f8ff 100644 --- a/src/main.ts +++ b/src/main.ts @@ -61,21 +61,57 @@ export default class ScriptoriumPlugin extends Plugin { 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 + async loadPrivateKey(): Promise { + // Try multiple methods to load the private key + + // Method 1: Try environment variable (may not work in Obsidian's sandbox) 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(); + 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 + } + + // Method 2: Try reading from a file in the vault (.scriptorium_key) + try { + const keyFile = this.app.vault.getAbstractFileByPath(".scriptorium_key"); + if (keyFile && keyFile instanceof TFile) { + const keyContent = await this.app.vault.read(keyFile); + const key = keyContent.trim(); + if (key && (key.startsWith("nsec1") || /^[0-9a-f]{64}$/i.test(key))) { + this.settings.privateKey = key; + await this.saveSettings(); + return true; + } + } + } catch (error) { + // File doesn't exist or can't be read + } + + // Method 3: Try reading from .obsidian/scriptorium_key (hidden file) + try { + const hiddenKeyFile = this.app.vault.getAbstractFileByPath(".obsidian/scriptorium_key"); + if (hiddenKeyFile && hiddenKeyFile instanceof TFile) { + const keyContent = await this.app.vault.read(hiddenKeyFile); + const key = keyContent.trim(); + if (key && (key.startsWith("nsec1") || /^[0-9a-f]{64}$/i.test(key))) { + this.settings.privateKey = key; + await this.saveSettings(); + return true; + } } } catch (error) { - // Environment variable access not available, user must set manually - safeConsoleLog("Environment variable access not available, use settings to set private key"); + // File doesn't exist or can't be read } + + return false; } private async getCurrentFile(): Promise { diff --git a/src/nostr/profileFetcher.ts b/src/nostr/profileFetcher.ts index e7513f5..f58a064 100644 --- a/src/nostr/profileFetcher.ts +++ b/src/nostr/profileFetcher.ts @@ -7,6 +7,8 @@ import { safeConsoleError } from "../utils/security"; export interface UserProfile { name?: string; display_name?: string; + username?: string; + nip05?: string; // NIP-05 identifier (handle) about?: string; picture?: string; } @@ -17,19 +19,29 @@ export interface UserProfile { export async function fetchUserProfile( pubkey: string, relayUrls: string[], - timeout: number = 5000 + timeout: number = 10000 ): Promise { - for (const relayUrl of relayUrls) { - try { - const profile = await fetchProfileFromRelay(relayUrl, pubkey, timeout); - if (profile) { - return profile; - } - } catch (error) { + if (relayUrls.length === 0) { + return null; + } + + // Try all relays in parallel for faster response + const promises = relayUrls.map(relayUrl => + fetchProfileFromRelay(relayUrl, pubkey, timeout).catch(error => { safeConsoleError(`Error fetching profile from ${relayUrl}:`, error); - continue; + return null; + }) + ); + + const results = await Promise.all(promises); + + // Return first successful result + for (const profile of results) { + if (profile) { + return profile; } } + return null; } @@ -54,39 +66,56 @@ async function fetchProfileFromRelay( relay = new Relay(relayUrl); await relay.connect(); + let profileReceived = false; + const sub = relay.subscribe( [ { kinds: [0], authors: [pubkey], + limit: 1, }, ], { onevent: (event) => { + if (profileReceived) return; // Only process first event + profileReceived = true; clearTimeout(timer); + sub.close(); relay?.close(); try { const profile = JSON.parse(event.content) as UserProfile; - resolve(profile); + // Validate that we got some profile data + if (profile && (profile.name || profile.display_name || profile.nip05 || profile.username)) { + resolve(profile); + } else { + resolve(null); + } } catch (error) { resolve(null); } }, oneose: () => { - clearTimeout(timer); - relay?.close(); - resolve(null); + if (!profileReceived) { + clearTimeout(timer); + sub.close(); + relay?.close(); + resolve(null); + } }, } ); - // Wait for response + // Wait for response with timeout setTimeout(() => { - sub.close(); - if (relay) { - relay.close(); + if (!profileReceived) { + sub.close(); + if (relay) { + relay.close(); + } + resolve(null); } - }, timeout - 100); + }, timeout); } catch (error) { clearTimeout(timer); if (relay) { diff --git a/src/ui/settingsTab.ts b/src/ui/settingsTab.ts index dde5561..8468787 100644 --- a/src/ui/settingsTab.ts +++ b/src/ui/settingsTab.ts @@ -1,4 +1,4 @@ -import { App, PluginSettingTab, Setting } from "obsidian"; +import { App, PluginSettingTab, Setting, Notice } from "obsidian"; import ScriptoriumPlugin from "../main"; import { EventKind } from "../types"; import { fetchRelayList, addTheCitadelIfMissing, includesTheCitadel, getReadRelays } from "../relayManager"; @@ -23,42 +23,94 @@ export class ScriptoriumSettingTab extends PluginSettingTab { containerEl.createEl("h2", { text: "Scriptorium Nostr Settings" }); - // User Identity (npub and handle) + // User Identity (npub and handle) or Private Key Input if (this.plugin.settings.privateKey) { try { const npub = getNpubFromPrivkey(this.plugin.settings.privateKey); const pubkey = getPubkeyFromPrivkey(this.plugin.settings.privateKey); // Fetch profile to get handle/name - let profile: { name?: string; display_name?: string } | null = null; + let profile: { name?: string; display_name?: string; username?: string; nip05?: string } | null = null; const readRelays = getReadRelays(this.plugin.settings.relayList); if (readRelays.length > 0) { profile = await fetchUserProfile(pubkey, readRelays); } - const displayName = profile?.display_name || profile?.name || "Unknown"; + // Priority: nip05 (handle) > display_name > name > username > "Unknown" + const displayName = profile?.nip05 || + profile?.display_name || + profile?.name || + profile?.username || + "Unknown"; + + // Build description with what we found + let identityDesc = "Your Nostr public identity"; + if (profile) { + const parts: string[] = []; + if (profile.nip05) parts.push(`NIP-05: ${profile.nip05}`); + if (profile.display_name) parts.push(`Display: ${profile.display_name}`); + if (profile.name) parts.push(`Name: ${profile.name}`); + if (parts.length > 0) { + identityDesc += `\n${parts.join(" | ")}`; + } + } else if (readRelays.length > 0) { + identityDesc += "\n(Profile not found on relays - may need to publish kind 0 event)"; + } else { + identityDesc += "\n(No read relays configured - fetch relay list first)"; + } new Setting(containerEl) .setName("Your Identity") - .setDesc("Your Nostr public identity (loaded from SCRIPTORIUM_OBSIDIAN_KEY)") + .setDesc(identityDesc) .addText((text) => { text.setValue(`${displayName} (${npub})`) .setDisabled(true); }) .addButton((button) => { - button.setButtonText("Refresh from Env") + button.setButtonText("Refresh") .setCta() .onClick(async () => { - await this.plugin.loadPrivateKey(); + const loaded = await this.plugin.loadPrivateKey(); + if (!loaded && !this.plugin.settings.privateKey) { + new Notice("Could not load private key. Please enter it manually below."); + } await this.display(); }); }); + + // Allow manual update of private key + new Setting(containerEl) + .setName("Update Private Key") + .setDesc("Manually enter or update your private key (nsec1... or 64-char hex). Leave empty to keep current.") + .addText((text) => { + text.setPlaceholder("nsec1... or hex key") + .setValue("") + .inputEl.type = "password"; + }) + .addButton((button) => { + button.setButtonText("Update") + .onClick(async () => { + const input = containerEl.querySelector("input[type='password']") as HTMLInputElement; + if (input && input.value.trim()) { + const key = input.value.trim(); + if (key.startsWith("nsec1") || /^[0-9a-f]{64}$/i.test(key)) { + this.plugin.settings.privateKey = key; + await this.plugin.saveSettings(); + input.value = ""; + new Notice("Private key updated successfully"); + await this.display(); + } else { + new Notice("Invalid key format. Expected nsec1... or 64-char hex string."); + } + } + }); + }); } catch (error: any) { new Setting(containerEl) .setName("Private Key Status") .setDesc(`Error: ${error.message}`) .addButton((button) => { - button.setButtonText("Refresh from Env") + button.setButtonText("Refresh") .setCta() .onClick(async () => { await this.plugin.loadPrivateKey(); @@ -69,13 +121,40 @@ export class ScriptoriumSettingTab extends PluginSettingTab { } else { new Setting(containerEl) .setName("Private Key") - .setDesc("No private key found. Set SCRIPTORIUM_OBSIDIAN_KEY environment variable.") + .setDesc("Enter your private key manually, or set SCRIPTORIUM_OBSIDIAN_KEY environment variable, or create .scriptorium_key file in vault root.") + .addText((text) => { + text.setPlaceholder("nsec1... or 64-char hex key") + .inputEl.type = "password"; + }) .addButton((button) => { - button.setButtonText("Refresh from Env") + button.setButtonText("Set Key") .setCta() .onClick(async () => { - await this.plugin.loadPrivateKey(); - await this.display(); + const input = containerEl.querySelector("input[type='password']") as HTMLInputElement; + if (input && input.value.trim()) { + const key = input.value.trim(); + if (key.startsWith("nsec1") || /^[0-9a-f]{64}$/i.test(key)) { + this.plugin.settings.privateKey = key; + await this.plugin.saveSettings(); + input.value = ""; + new Notice("Private key saved successfully"); + await this.display(); + } else { + new Notice("Invalid key format. Expected nsec1... or 64-char hex string."); + } + } + }); + }) + .addButton((button) => { + button.setButtonText("Refresh") + .onClick(async () => { + const loaded = await this.plugin.loadPrivateKey(); + if (loaded) { + new Notice("Private key loaded successfully"); + await this.display(); + } else { + new Notice("Could not load private key. Please enter it manually above."); + } }); }); }