mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-14 09:26:52 +02:00
chore: remove unused NIP-C7 adapter
The NIP-C7 adapter was already disabled/commented out everywhere. Removing the file to reduce dead code. https://claude.ai/code/session_01Jy51Ayk57fzaFuuFFm1j1K
This commit is contained in:
@@ -1,437 +0,0 @@
|
||||
import { Observable, firstValueFrom } from "rxjs";
|
||||
import { map, first } from "rxjs/operators";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import type { Filter } from "nostr-tools";
|
||||
import type { EventPointer, AddressPointer } from "nostr-tools/nip19";
|
||||
import { ChatProtocolAdapter, type SendMessageOptions } from "./base-adapter";
|
||||
import type {
|
||||
Conversation,
|
||||
Message,
|
||||
ProtocolIdentifier,
|
||||
ChatCapabilities,
|
||||
LoadMessagesOptions,
|
||||
} from "@/types/chat";
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
import eventStore from "@/services/event-store";
|
||||
import pool from "@/services/relay-pool";
|
||||
import { publishEvent } from "@/services/hub";
|
||||
import accountManager from "@/services/accounts";
|
||||
import { isNip05, resolveNip05 } from "@/lib/nip05";
|
||||
import { getDisplayName, getQuotePointer } from "@/lib/nostr-utils";
|
||||
import { isValidHexPubkey } from "@/lib/nostr-validation";
|
||||
import { getProfileContent } from "applesauce-core/helpers";
|
||||
import { getSeenRelays } from "applesauce-core/helpers/relays";
|
||||
import { EventFactory } from "applesauce-core/event-factory";
|
||||
import { ReactionBlueprint } from "applesauce-common/blueprints";
|
||||
|
||||
/**
|
||||
* NIP-C7 Adapter - Simple Chat (Kind 9)
|
||||
*
|
||||
* Features:
|
||||
* - Direct messaging between users
|
||||
* - Quote-based threading (q-tag)
|
||||
* - No encryption
|
||||
* - Uses outbox relays
|
||||
*/
|
||||
export class NipC7Adapter extends ChatProtocolAdapter {
|
||||
readonly protocol = "nip-c7" as const;
|
||||
readonly type = "dm" as const;
|
||||
|
||||
/**
|
||||
* Parse identifier - accepts npub, nprofile, hex pubkey, or NIP-05
|
||||
*/
|
||||
parseIdentifier(input: string): ProtocolIdentifier | null {
|
||||
// Try bech32 decoding (npub/nprofile)
|
||||
try {
|
||||
const decoded = nip19.decode(input);
|
||||
if (decoded.type === "npub") {
|
||||
return {
|
||||
type: "chat-partner",
|
||||
value: decoded.data,
|
||||
};
|
||||
}
|
||||
if (decoded.type === "nprofile") {
|
||||
return {
|
||||
type: "chat-partner",
|
||||
value: decoded.data.pubkey,
|
||||
relays: decoded.data.relays,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Not bech32, try other formats
|
||||
}
|
||||
|
||||
// Try hex pubkey
|
||||
if (isValidHexPubkey(input)) {
|
||||
return {
|
||||
type: "chat-partner",
|
||||
value: input,
|
||||
};
|
||||
}
|
||||
|
||||
// Try NIP-05
|
||||
if (isNip05(input)) {
|
||||
return {
|
||||
type: "chat-partner-nip05",
|
||||
value: input,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve conversation from identifier
|
||||
*/
|
||||
async resolveConversation(
|
||||
identifier: ProtocolIdentifier,
|
||||
): Promise<Conversation> {
|
||||
let pubkey: string;
|
||||
|
||||
// Resolve NIP-05 if needed
|
||||
if (identifier.type === "chat-partner-nip05") {
|
||||
const resolved = await resolveNip05(identifier.value);
|
||||
if (!resolved) {
|
||||
throw new Error(`Failed to resolve NIP-05: ${identifier.value}`);
|
||||
}
|
||||
pubkey = resolved;
|
||||
} else if (
|
||||
identifier.type === "chat-partner" ||
|
||||
identifier.type === "dm-recipient"
|
||||
) {
|
||||
pubkey = identifier.value;
|
||||
} else {
|
||||
throw new Error(
|
||||
`NIP-C7 adapter cannot handle identifier type: ${identifier.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
const activePubkey = accountManager.active$.value?.pubkey;
|
||||
if (!activePubkey) {
|
||||
throw new Error("No active account");
|
||||
}
|
||||
|
||||
// Get display name for partner
|
||||
const metadataEvent = await this.getMetadata(pubkey);
|
||||
const metadata = metadataEvent
|
||||
? getProfileContent(metadataEvent)
|
||||
: undefined;
|
||||
const title = getDisplayName(pubkey, metadata);
|
||||
|
||||
return {
|
||||
id: `nip-c7:${pubkey}`,
|
||||
type: "dm",
|
||||
protocol: "nip-c7",
|
||||
title,
|
||||
participants: [
|
||||
{ pubkey: activePubkey, role: "member" },
|
||||
{ pubkey, role: "member" },
|
||||
],
|
||||
unreadCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load messages between active user and conversation partner
|
||||
*/
|
||||
loadMessages(
|
||||
conversation: Conversation,
|
||||
options?: LoadMessagesOptions,
|
||||
): Observable<Message[]> {
|
||||
const activePubkey = accountManager.active$.value?.pubkey;
|
||||
if (!activePubkey) {
|
||||
throw new Error("No active account");
|
||||
}
|
||||
|
||||
const partner = conversation.participants.find(
|
||||
(p) => p.pubkey !== activePubkey,
|
||||
);
|
||||
if (!partner) {
|
||||
throw new Error("No conversation partner found");
|
||||
}
|
||||
|
||||
// Subscribe to kind 9 messages between users
|
||||
const filter: Filter = {
|
||||
kinds: [9],
|
||||
authors: [activePubkey, partner.pubkey],
|
||||
"#p": [activePubkey, partner.pubkey],
|
||||
limit: options?.limit || 50,
|
||||
};
|
||||
|
||||
if (options?.before) {
|
||||
filter.until = options.before;
|
||||
}
|
||||
if (options?.after) {
|
||||
filter.since = options.after;
|
||||
}
|
||||
|
||||
// Start subscription to populate EventStore
|
||||
pool
|
||||
.subscription([], [filter], {
|
||||
eventStore, // Automatically add to store
|
||||
})
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
if (typeof response === "string") {
|
||||
// EOSE received
|
||||
console.log("[NIP-C7] EOSE received for messages");
|
||||
} else {
|
||||
// Event received
|
||||
console.log(
|
||||
`[NIP-C7] Received message: ${response.id.slice(0, 8)}...`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Return observable from EventStore which will update automatically
|
||||
return eventStore.timeline(filter).pipe(
|
||||
map((events) => {
|
||||
console.log(`[NIP-C7] Timeline has ${events.length} messages`);
|
||||
return events
|
||||
.map((event) => this.eventToMessage(event, conversation.id))
|
||||
.sort((a, b) => a.timestamp - b.timestamp);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load more historical messages (pagination)
|
||||
*/
|
||||
async loadMoreMessages(
|
||||
_conversation: Conversation,
|
||||
_before: number,
|
||||
): Promise<Message[]> {
|
||||
// For now, return empty - pagination to be implemented in Phase 6
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message
|
||||
*/
|
||||
async sendMessage(
|
||||
conversation: Conversation,
|
||||
content: string,
|
||||
options?: SendMessageOptions,
|
||||
): Promise<void> {
|
||||
const activePubkey = accountManager.active$.value?.pubkey;
|
||||
const activeSigner = accountManager.active$.value?.signer;
|
||||
|
||||
if (!activePubkey || !activeSigner) {
|
||||
throw new Error("No active account or signer");
|
||||
}
|
||||
|
||||
const partner = conversation.participants.find(
|
||||
(p) => p.pubkey !== activePubkey,
|
||||
);
|
||||
if (!partner) {
|
||||
throw new Error("No conversation partner found");
|
||||
}
|
||||
|
||||
// Create event factory and sign event
|
||||
const factory = new EventFactory();
|
||||
factory.setSigner(activeSigner);
|
||||
|
||||
const tags: string[][] = [["p", partner.pubkey]];
|
||||
|
||||
// Add q-tag for replies (per NIP-C7 quote tag format)
|
||||
// Format: ["q", eventId, relayUrl, pubkey]
|
||||
if (options?.replyTo) {
|
||||
const replyEvent = eventStore.getEvent(options.replyTo);
|
||||
if (replyEvent) {
|
||||
// Get a relay hint from where we've seen this event
|
||||
const seenRelays = getSeenRelays(replyEvent);
|
||||
const relayHint =
|
||||
seenRelays && seenRelays.size > 0 ? [...seenRelays][0] : "";
|
||||
// Full q-tag with relay hint and author pubkey
|
||||
tags.push(["q", options.replyTo, relayHint, replyEvent.pubkey]);
|
||||
} else {
|
||||
// Fallback: just the event ID
|
||||
tags.push(["q", options.replyTo]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add NIP-30 emoji tags
|
||||
if (options?.emojiTags) {
|
||||
for (const emoji of options.emojiTags) {
|
||||
tags.push(["emoji", emoji.shortcode, emoji.url]);
|
||||
}
|
||||
}
|
||||
|
||||
const draft = await factory.build({ kind: 9, content, tags });
|
||||
const event = await factory.sign(draft);
|
||||
await publishEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a reaction (kind 7) to a message
|
||||
*/
|
||||
async sendReaction(
|
||||
conversation: Conversation,
|
||||
messageId: string,
|
||||
emoji: string,
|
||||
customEmoji?: { shortcode: string; url: string },
|
||||
): Promise<void> {
|
||||
const activePubkey = accountManager.active$.value?.pubkey;
|
||||
const activeSigner = accountManager.active$.value?.signer;
|
||||
|
||||
if (!activePubkey || !activeSigner) {
|
||||
throw new Error("No active account or signer");
|
||||
}
|
||||
|
||||
const partner = conversation.participants.find(
|
||||
(p) => p.pubkey !== activePubkey,
|
||||
);
|
||||
if (!partner) {
|
||||
throw new Error("No conversation partner found");
|
||||
}
|
||||
|
||||
// Fetch the message being reacted to
|
||||
const messageEvent = await firstValueFrom(eventStore.event(messageId), {
|
||||
defaultValue: undefined,
|
||||
});
|
||||
|
||||
if (!messageEvent) {
|
||||
throw new Error("Message event not found");
|
||||
}
|
||||
|
||||
// Create event factory
|
||||
const factory = new EventFactory();
|
||||
factory.setSigner(activeSigner);
|
||||
|
||||
// Use ReactionBlueprint - auto-handles e-tag, k-tag, p-tag, custom emoji
|
||||
const emojiArg = customEmoji
|
||||
? { shortcode: customEmoji.shortcode, url: customEmoji.url }
|
||||
: emoji;
|
||||
|
||||
const draft = await factory.create(
|
||||
ReactionBlueprint,
|
||||
messageEvent,
|
||||
emojiArg,
|
||||
);
|
||||
|
||||
// Note: ReactionBlueprint already adds p-tag for message author
|
||||
// For NIP-C7, we might want to ensure partner is tagged if different from author
|
||||
// but the blueprint should handle this correctly
|
||||
|
||||
// Sign the event
|
||||
const event = await factory.sign(draft);
|
||||
await publishEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get protocol capabilities
|
||||
*/
|
||||
getCapabilities(): ChatCapabilities {
|
||||
return {
|
||||
supportsEncryption: false,
|
||||
supportsThreading: true, // q-tag quotes
|
||||
supportsModeration: false,
|
||||
supportsRoles: false,
|
||||
supportsGroupManagement: false,
|
||||
canCreateConversations: true,
|
||||
requiresRelay: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a replied-to message
|
||||
* First checks EventStore, then fetches from relays if needed
|
||||
*/
|
||||
async loadReplyMessage(
|
||||
_conversation: Conversation,
|
||||
pointer: EventPointer | AddressPointer,
|
||||
): Promise<NostrEvent | null> {
|
||||
// Extract event ID from pointer (EventPointer has 'id', AddressPointer doesn't)
|
||||
const eventId = "id" in pointer ? pointer.id : null;
|
||||
|
||||
if (!eventId) {
|
||||
console.warn(
|
||||
"[NIP-C7] AddressPointer not supported for loadReplyMessage",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// First check EventStore - might already be loaded
|
||||
const cachedEvent = await eventStore
|
||||
.event(eventId)
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
if (cachedEvent) {
|
||||
return cachedEvent;
|
||||
}
|
||||
|
||||
// Not in store, fetch from relay pool (use pointer relays if available)
|
||||
const relays = pointer.relays || [];
|
||||
console.log(
|
||||
`[NIP-C7] Fetching reply message ${eventId.slice(0, 8)}... from ${relays.length > 0 ? relays.join(", ") : "global pool"}`,
|
||||
);
|
||||
|
||||
const filter: Filter = {
|
||||
ids: [eventId],
|
||||
limit: 1,
|
||||
};
|
||||
|
||||
const events: NostrEvent[] = [];
|
||||
const obs = pool.subscription(relays, [filter], { eventStore });
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
console.log(
|
||||
`[NIP-C7] Reply message fetch timeout for ${eventId.slice(0, 8)}...`,
|
||||
);
|
||||
resolve();
|
||||
}, 3000);
|
||||
|
||||
const sub = obs.subscribe({
|
||||
next: (response) => {
|
||||
if (typeof response === "string") {
|
||||
// EOSE received
|
||||
clearTimeout(timeout);
|
||||
sub.unsubscribe();
|
||||
resolve();
|
||||
} else {
|
||||
// Event received
|
||||
events.push(response);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
clearTimeout(timeout);
|
||||
console.error(`[NIP-C7] Reply message fetch error:`, err);
|
||||
sub.unsubscribe();
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return events[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Convert Nostr event to Message
|
||||
*/
|
||||
private eventToMessage(event: NostrEvent, conversationId: string): Message {
|
||||
// Use getQuotePointer to extract full EventPointer with relay hints
|
||||
const replyTo = getQuotePointer(event);
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
conversationId,
|
||||
author: event.pubkey,
|
||||
content: event.content,
|
||||
timestamp: event.created_at,
|
||||
replyTo: replyTo || undefined,
|
||||
protocol: "nip-c7",
|
||||
event,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Get user metadata
|
||||
*/
|
||||
private async getMetadata(pubkey: string): Promise<NostrEvent | undefined> {
|
||||
return firstValueFrom(eventStore.replaceable(0, pubkey), {
|
||||
defaultValue: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user