diff --git a/src/lib/chat/adapters/nip-29-adapter.test.ts b/src/lib/chat/adapters/nip-29-adapter.test.ts index 335246d..5e2464f 100644 --- a/src/lib/chat/adapters/nip-29-adapter.test.ts +++ b/src/lib/chat/adapters/nip-29-adapter.test.ts @@ -180,4 +180,77 @@ describe("Nip29Adapter", () => { expect(capabilities.requiresRelay).toBe(true); }); }); + + describe("profile fallback for pubkey group IDs", () => { + it("should parse valid pubkey as group ID", () => { + const validPubkey = + "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"; + const result = adapter.parseIdentifier( + `wss://relay.example.com'${validPubkey}`, + ); + + expect(result).toEqual({ + type: "group", + value: validPubkey, + relays: ["wss://relay.example.com"], + }); + }); + + it("should parse uppercase pubkey as group ID", () => { + const validPubkey = + "3BF0C63FCB93463407AF97A5E5EE64FA883D107EF9E558472C4EB9AAAEFA459D"; + const result = adapter.parseIdentifier( + `wss://relay.example.com'${validPubkey}`, + ); + + expect(result).toEqual({ + type: "group", + value: validPubkey, + relays: ["wss://relay.example.com"], + }); + }); + + it("should parse mixed case pubkey as group ID", () => { + const validPubkey = + "3bF0c63Fcb93463407aF97a5e5Ee64fA883d107eF9e558472c4eB9aaaEfa459D"; + const result = adapter.parseIdentifier( + `wss://relay.example.com'${validPubkey}`, + ); + + expect(result).toEqual({ + type: "group", + value: validPubkey, + relays: ["wss://relay.example.com"], + }); + }); + + it("should not treat short hex strings as valid pubkeys", () => { + // Less than 64 characters should be treated as normal group IDs + const shortHex = "3bf0c63f"; + const result = adapter.parseIdentifier( + `wss://relay.example.com'${shortHex}`, + ); + + expect(result).toEqual({ + type: "group", + value: shortHex, + relays: ["wss://relay.example.com"], + }); + }); + + it("should not treat non-hex strings as valid pubkeys", () => { + // 64 characters but contains non-hex characters + const nonHex = + "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"; + const result = adapter.parseIdentifier( + `wss://relay.example.com'${nonHex}`, + ); + + expect(result).toEqual({ + type: "group", + value: nonHex, + relays: ["wss://relay.example.com"], + }); + }); + }); }); diff --git a/src/lib/chat/adapters/nip-29-adapter.ts b/src/lib/chat/adapters/nip-29-adapter.ts index fd6b826..d326586 100644 --- a/src/lib/chat/adapters/nip-29-adapter.ts +++ b/src/lib/chat/adapters/nip-29-adapter.ts @@ -1,7 +1,7 @@ import { Observable, firstValueFrom } from "rxjs"; import { map, first, toArray } from "rxjs/operators"; import type { Filter } from "nostr-tools"; -import { nip19 } from "nostr-tools"; +import { nip19, kinds } from "nostr-tools"; import { ChatProtocolAdapter, type SendMessageOptions } from "./base-adapter"; import type { Conversation, @@ -25,6 +25,15 @@ import { GroupMessageBlueprint, ReactionBlueprint, } from "applesauce-common/blueprints"; +import { profileLoader } from "@/services/loaders"; +import { getProfileContent } from "applesauce-core/helpers"; + +/** + * Check if a string is a valid nostr pubkey (64 character hex string) + */ +function isValidPubkey(str: string): boolean { + return /^[0-9a-f]{64}$/i.test(str); +} /** * NIP-29 Adapter - Relay-Based Groups @@ -181,16 +190,53 @@ export class Nip29Adapter extends ChatProtocolAdapter { } // Extract group info from metadata event - const title = metadataEvent + let title = metadataEvent ? getTagValues(metadataEvent, "name")[0] || groupId : groupId; - const description = metadataEvent + let description = metadataEvent ? getTagValues(metadataEvent, "about")[0] : undefined; - const icon = metadataEvent + let icon = metadataEvent ? getTagValues(metadataEvent, "picture")[0] : undefined; + // Fallback: If no metadata found and groupId is a valid pubkey, use profile metadata + if (!metadataEvent && isValidPubkey(groupId)) { + console.log( + `[NIP-29] No group metadata found, groupId is valid pubkey. Fetching profile for ${groupId.slice(0, 8)}...`, + ); + + try { + // Fetch profile metadata (kind 0) for the pubkey + const profileEvent = await firstValueFrom( + profileLoader({ + kind: kinds.Metadata, + pubkey: groupId, + relays: [relayUrl], // Try the group relay first + }), + { defaultValue: undefined }, + ); + + if (profileEvent) { + const profileContent = getProfileContent(profileEvent); + if (profileContent) { + console.log( + `[NIP-29] Using profile metadata as fallback for group ${groupId.slice(0, 8)}...`, + ); + title = profileContent.display_name || profileContent.name || title; + description = profileContent.about || description; + icon = profileContent.picture || icon; + } + } + } catch (error) { + console.warn( + `[NIP-29] Failed to fetch profile fallback for ${groupId.slice(0, 8)}:`, + error, + ); + // Continue with default title (groupId) if profile fetch fails + } + } + console.log(`[NIP-29] Group title: ${title}`); // Fetch admins (kind 39001) and members (kind 39002) in parallel