Browse Source

make npub display more complete

master
Silberengel 1 week ago
parent
commit
02b7530e3b
  1. 43
      README.md
  2. 56
      src/main.ts
  3. 65
      src/nostr/profileFetcher.ts
  4. 103
      src/ui/settingsTab.ts

43
README.md

@ -33,11 +33,44 @@ An Obsidian plugin for creating, editing, and publishing Nostr document events d
## Setup ## Setup
1. Set your Nostr private key in the environment variable `SCRIPTORIUM_OBSIDIAN_KEY`: ### Private Key Configuration
- Format: `nsec1...` (bech32) or 64-character hex string
2. Open Obsidian settings → Scriptorium Nostr You have three options to set your private key:
3. Click "Refresh from Env" to load your private key
4. Click "Fetch" to get your relay list from Nostr relays **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 ## Usage

56
src/main.ts

@ -61,21 +61,57 @@ export default class ScriptoriumPlugin extends Plugin {
await this.saveData(this.settings); await this.saveData(this.settings);
} }
async loadPrivateKey() { async loadPrivateKey(): Promise<boolean> {
// Try to load from environment variable // Try multiple methods to load the private key
// Note: In Obsidian, process.env may not be available
// Users should set the key manually in settings or via system environment // Method 1: Try environment variable (may not work in Obsidian's sandbox)
try { try {
// @ts-ignore - process.env may not be typed in Obsidian context // @ts-ignore - process.env may not be typed in Obsidian context
const envKey = typeof process !== "undefined" && process.env?.SCRIPTORIUM_OBSIDIAN_KEY; if (typeof process !== "undefined" && process.env?.SCRIPTORIUM_OBSIDIAN_KEY) {
if (envKey) { const envKey = process.env.SCRIPTORIUM_OBSIDIAN_KEY.trim();
this.settings.privateKey = envKey; if (envKey) {
await this.saveSettings(); 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) { } catch (error) {
// Environment variable access not available, user must set manually // File doesn't exist or can't be read
safeConsoleLog("Environment variable access not available, use settings to set private key");
} }
// 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) {
// File doesn't exist or can't be read
}
return false;
} }
private async getCurrentFile(): Promise<TFile | null> { private async getCurrentFile(): Promise<TFile | null> {

65
src/nostr/profileFetcher.ts

@ -7,6 +7,8 @@ import { safeConsoleError } from "../utils/security";
export interface UserProfile { export interface UserProfile {
name?: string; name?: string;
display_name?: string; display_name?: string;
username?: string;
nip05?: string; // NIP-05 identifier (handle)
about?: string; about?: string;
picture?: string; picture?: string;
} }
@ -17,19 +19,29 @@ export interface UserProfile {
export async function fetchUserProfile( export async function fetchUserProfile(
pubkey: string, pubkey: string,
relayUrls: string[], relayUrls: string[],
timeout: number = 5000 timeout: number = 10000
): Promise<UserProfile | null> { ): Promise<UserProfile | null> {
for (const relayUrl of relayUrls) { if (relayUrls.length === 0) {
try { return null;
const profile = await fetchProfileFromRelay(relayUrl, pubkey, timeout); }
if (profile) {
return profile; // Try all relays in parallel for faster response
} const promises = relayUrls.map(relayUrl =>
} catch (error) { fetchProfileFromRelay(relayUrl, pubkey, timeout).catch(error => {
safeConsoleError(`Error fetching profile from ${relayUrl}:`, 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; return null;
} }
@ -54,39 +66,56 @@ async function fetchProfileFromRelay(
relay = new Relay(relayUrl); relay = new Relay(relayUrl);
await relay.connect(); await relay.connect();
let profileReceived = false;
const sub = relay.subscribe( const sub = relay.subscribe(
[ [
{ {
kinds: [0], kinds: [0],
authors: [pubkey], authors: [pubkey],
limit: 1,
}, },
], ],
{ {
onevent: (event) => { onevent: (event) => {
if (profileReceived) return; // Only process first event
profileReceived = true;
clearTimeout(timer); clearTimeout(timer);
sub.close();
relay?.close(); relay?.close();
try { try {
const profile = JSON.parse(event.content) as UserProfile; 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) { } catch (error) {
resolve(null); resolve(null);
} }
}, },
oneose: () => { oneose: () => {
clearTimeout(timer); if (!profileReceived) {
relay?.close(); clearTimeout(timer);
resolve(null); sub.close();
relay?.close();
resolve(null);
}
}, },
} }
); );
// Wait for response // Wait for response with timeout
setTimeout(() => { setTimeout(() => {
sub.close(); if (!profileReceived) {
if (relay) { sub.close();
relay.close(); if (relay) {
relay.close();
}
resolve(null);
} }
}, timeout - 100); }, timeout);
} catch (error) { } catch (error) {
clearTimeout(timer); clearTimeout(timer);
if (relay) { if (relay) {

103
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 ScriptoriumPlugin from "../main";
import { EventKind } from "../types"; import { EventKind } from "../types";
import { fetchRelayList, addTheCitadelIfMissing, includesTheCitadel, getReadRelays } from "../relayManager"; import { fetchRelayList, addTheCitadelIfMissing, includesTheCitadel, getReadRelays } from "../relayManager";
@ -23,42 +23,94 @@ export class ScriptoriumSettingTab extends PluginSettingTab {
containerEl.createEl("h2", { text: "Scriptorium Nostr Settings" }); 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) { if (this.plugin.settings.privateKey) {
try { try {
const npub = getNpubFromPrivkey(this.plugin.settings.privateKey); const npub = getNpubFromPrivkey(this.plugin.settings.privateKey);
const pubkey = getPubkeyFromPrivkey(this.plugin.settings.privateKey); const pubkey = getPubkeyFromPrivkey(this.plugin.settings.privateKey);
// Fetch profile to get handle/name // 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); const readRelays = getReadRelays(this.plugin.settings.relayList);
if (readRelays.length > 0) { if (readRelays.length > 0) {
profile = await fetchUserProfile(pubkey, readRelays); 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) new Setting(containerEl)
.setName("Your Identity") .setName("Your Identity")
.setDesc("Your Nostr public identity (loaded from SCRIPTORIUM_OBSIDIAN_KEY)") .setDesc(identityDesc)
.addText((text) => { .addText((text) => {
text.setValue(`${displayName} (${npub})`) text.setValue(`${displayName} (${npub})`)
.setDisabled(true); .setDisabled(true);
}) })
.addButton((button) => { .addButton((button) => {
button.setButtonText("Refresh from Env") button.setButtonText("Refresh")
.setCta() .setCta()
.onClick(async () => { .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(); 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) { } catch (error: any) {
new Setting(containerEl) new Setting(containerEl)
.setName("Private Key Status") .setName("Private Key Status")
.setDesc(`Error: ${error.message}`) .setDesc(`Error: ${error.message}`)
.addButton((button) => { .addButton((button) => {
button.setButtonText("Refresh from Env") button.setButtonText("Refresh")
.setCta() .setCta()
.onClick(async () => { .onClick(async () => {
await this.plugin.loadPrivateKey(); await this.plugin.loadPrivateKey();
@ -69,13 +121,40 @@ export class ScriptoriumSettingTab extends PluginSettingTab {
} else { } else {
new Setting(containerEl) new Setting(containerEl)
.setName("Private Key") .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) => { .addButton((button) => {
button.setButtonText("Refresh from Env") button.setButtonText("Set Key")
.setCta() .setCta()
.onClick(async () => { .onClick(async () => {
await this.plugin.loadPrivateKey(); const input = containerEl.querySelector("input[type='password']") as HTMLInputElement;
await this.display(); 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.");
}
}); });
}); });
} }

Loading…
Cancel
Save