add details to DNS identity

This commit is contained in:
hzrd149 2024-05-05 09:26:11 -05:00
parent 08b5503815
commit 4e3c9070ed
9 changed files with 113 additions and 71 deletions

View File

@ -9,7 +9,7 @@ import { Kind0ParsedContent, getDisplayName } from "../../helpers/nostr/user-met
import useAppSettings from "../../hooks/use-app-settings";
import useCurrentAccount from "../../hooks/use-current-account";
import { buildImageProxyURL } from "../../helpers/image";
import UserDnsIdentityIcon, { useDnsIdentityColor } from "./user-dns-identity-icon";
import UserDnsIdentityIcon from "./user-dns-identity-icon";
import styled from "@emotion/styled";
export const UserIdenticon = memo(({ pubkey }: { pubkey: string }) => {

View File

@ -1,41 +1,19 @@
import { forwardRef } from "react";
import { IconProps, useColorMode } from "@chakra-ui/react";
import { IconProps } from "@chakra-ui/react";
import useDnsIdentity from "../../hooks/use-dns-identity";
import useUserMetadata from "../../hooks/use-user-metadata";
import { VerificationFailed, VerificationMissing, VerifiedIcon } from "../icons";
export function useDnsIdentityColor(pubkey: string) {
const metadata = useUserMetadata(pubkey);
const identity = useDnsIdentity(metadata?.nip05);
const { colorMode } = useColorMode();
if (!metadata?.nip05) {
return colorMode === "light" ? "gray.200" : "gray.800";
}
if (identity === undefined) {
return "yellow.500";
} else if (identity === null) {
return "red.500";
} else if (pubkey === identity.pubkey) {
return "purple.500";
} else {
return "red.500";
}
}
const UserDnsIdentityIcon = forwardRef<SVGSVGElement, { pubkey: string } & IconProps>(({ pubkey, ...props }, ref) => {
const metadata = useUserMetadata(pubkey);
const identity = useDnsIdentity(metadata?.nip05);
if (!metadata?.nip05) {
return null;
}
if (!metadata?.nip05) return null;
if (identity === undefined) {
return <VerificationFailed color="yellow.500" {...props} ref={ref} />;
} else if (identity === null) {
} else if (identity.exists === false || identity.pubkey === undefined) {
return <VerificationMissing color="red.500" {...props} ref={ref} />;
} else if (pubkey === identity.pubkey) {
return <VerifiedIcon color="purple.500" {...props} ref={ref} />;

View File

@ -12,54 +12,63 @@ export function parseAddress(address: string): { name?: string; domain?: string
}
type IdentityJson = {
names: Record<string, string | undefined>;
names?: Record<string, string | undefined>;
relays?: Record<string, string[]>;
nip46?: Record<string, string[]>;
};
export type DnsIdentity = {
name: string;
domain: string;
pubkey: string;
relays: string[];
/** If the nostr.json file exists */
exists: boolean;
/** pubkey found for name */
pubkey?: string;
/** relays found for name */
relays?: string[];
hasNip46?: boolean;
nip46Relays?: string[];
};
function getIdentityFromJson(name: string, domain: string, json: IdentityJson): DnsIdentity | undefined {
function getIdentityFromJson(name: string, domain: string, json: IdentityJson): DnsIdentity {
if (!json.names) return { name, domain, exists: true };
const pubkey = json.names[name];
if (!pubkey) return;
if (!pubkey) return { name, domain, exists: true };
const relays: string[] = json.relays?.[pubkey] ?? [];
const hasNip46 = !!json.nip46;
const nip46Relays = json.nip46?.[pubkey];
return { name, domain, pubkey, relays, nip46Relays, hasNip46 };
return { name, domain, pubkey, relays, nip46Relays, hasNip46, exists: true };
}
class DnsIdentityService {
identities = new SuperMap<string, Subject<DnsIdentity | null>>(() => new Subject());
// undefined === loading
identities = new SuperMap<string, Subject<DnsIdentity | undefined>>(() => new Subject());
async fetchIdentity(address: string) {
async fetchIdentity(address: string): Promise<DnsIdentity> {
const { name, domain } = parseAddress(address);
if (!name || !domain) throw new Error("invalid address " + address);
const json = await fetchWithProxy(`https://${domain}/.well-known/nostr.json?name=${name}`)
.then((res) => res.json() as Promise<IdentityJson>)
.then((json) => {
// convert all keys in names, and relays to lower case
if (json.names) {
for (const [name, pubkey] of Object.entries(json.names)) {
delete json.names[name];
json.names[name.toLowerCase()] = pubkey;
}
const res = await fetchWithProxy(`https://${domain}/.well-known/nostr.json?name=${name}`);
// if request was rejected consider identity invalid
if (res.status >= 400 && res.status < 500) return { name, domain, exists: false };
const json = await (res.json() as Promise<IdentityJson>).then((json) => {
// convert all keys in names, and relays to lower case
if (json.names) {
for (const [name, pubkey] of Object.entries(json.names)) {
delete json.names[name];
json.names[name.toLowerCase()] = pubkey;
}
if (json.relays) {
for (const [name, pubkey] of Object.entries(json.relays)) {
delete json.relays[name];
json.relays[name.toLowerCase()] = pubkey;
}
}
if (json.relays) {
for (const [name, pubkey] of Object.entries(json.relays)) {
delete json.relays[name];
json.relays[name.toLowerCase()] = pubkey;
}
return json;
});
}
return json;
});
await this.addToCache(domain, json);
@ -67,12 +76,14 @@ class DnsIdentityService {
}
async addToCache(domain: string, json: IdentityJson) {
if (!json.names) return;
const now = dayjs().unix();
const transaction = db.transaction("dnsIdentifiers", "readwrite");
for (const name of Object.keys(json.names)) {
const identity = getIdentityFromJson(name, domain, json);
if (identity) {
if (identity && identity.exists && identity.pubkey) {
const address = `${name}@${domain}`;
// add to memory cache
@ -80,7 +91,16 @@ class DnsIdentityService {
// ad to db cache
if (transaction.store.put) {
await transaction.store.put({ ...identity, updated: now }, address);
await transaction.store.put(
{
name: identity.name,
domain: identity.domain,
pubkey: identity.pubkey,
relays: identity.relays ?? [],
updated: now,
},
address,
);
}
}
}
@ -95,14 +115,14 @@ class DnsIdentityService {
this.loading.add(address);
db.get("dnsIdentifiers", address).then((fromDb) => {
if (fromDb) sub.next(fromDb);
if (fromDb) sub.next({ exists: true, ...fromDb });
this.loading.delete(address);
});
if (!sub.value || alwaysFetch) {
this.fetchIdentity(address)
.then((identity) => {
sub.next(identity ?? null);
sub.next(identity);
})
.finally(() => {
this.loading.delete(address);

View File

@ -13,7 +13,6 @@ import RelaySet from "../../../classes/relay-set";
import { useReadRelays, useWriteRelays } from "../../../hooks/use-client-relays";
import useCurrentAccount from "../../../hooks/use-current-account";
import RelayControl from "./relay-control";
import SelectRelaySet from "./select-relay-set";
import useUserMailboxes from "../../../hooks/use-user-mailboxes";
import { getRelaysFromExt } from "../../../helpers/nip07";
import { useUserDNSIdentity } from "../../../hooks/use-user-dns-identity";

View File

@ -69,7 +69,7 @@ export default function RelaysView() {
</Button>
</>
)}
{nip05 && (
{nip05?.exists && (
<Button
variant="outline"
as={RouterLink}

View File

@ -42,7 +42,7 @@ export default function NIP05RelaysView() {
</Link>
</Text>
{nip05?.relays.map((url) => <RelayItem key={url} url={url} />)}
{nip05?.relays?.map((url) => <RelayItem key={url} url={url} />)}
</Flex>
);
}

View File

@ -92,7 +92,7 @@ export default function LoginNostrAddressCreate() {
if (!nip05 || nip05.pubkey !== selected.pubkey) throw new Error("Invalid provider");
if (nip05.name !== "_") throw new Error("Provider does not own the domain");
if (!nip05.hasNip46) throw new Error("Provider does not support NIP-46");
const relays = safeRelayUrls(nip05.nip46Relays || nip05.relays);
const relays = safeRelayUrls(nip05.nip46Relays || nip05.relays || []);
if (relays.length === 0) throw new Error("Cant find providers relays");
const client = nostrConnectService.createClient("", relays, undefined, nip05.pubkey);

View File

@ -39,24 +39,26 @@ export default function LoginNostrAddressView() {
if (!nip05) return;
try {
if (nip05.hasNip46) {
if (nip05.hasNip46 && nip05.pubkey) {
setLoading("Connecting...");
const relays = safeRelayUrls(nip05.nip46Relays || rootNip05?.nip46Relays || rootNip05?.relays || nip05.relays);
const relays = safeRelayUrls(
nip05.nip46Relays || rootNip05?.nip46Relays || rootNip05?.relays || nip05.relays || [],
);
const client = nostrConnectService.fromHostedBunker(nip05.pubkey, relays);
await client.connect();
nostrConnectService.saveClient(client);
accountService.addFromNostrConnect(client);
accountService.switchAccount(client.pubkey!);
} else {
} else if (nip05.pubkey) {
accountService.addAccount({
type: "pubkey",
pubkey: nip05.pubkey,
relays: [...nip05.relays, COMMON_CONTACT_RELAY],
relays: nip05.relays ? [...nip05.relays, COMMON_CONTACT_RELAY] : [COMMON_CONTACT_RELAY],
readonly: true,
});
accountService.switchAccount(nip05.pubkey);
}
} else throw Error("Cant find address");
} catch (e) {
if (e instanceof Error) toast({ status: "error", description: e.message });
}

View File

@ -50,6 +50,7 @@ import UserStatsAccordion from "./user-stats-accordion";
import UserJoinedChanneled from "./user-joined-channels";
import { getTextColor } from "../../../helpers/color";
import UserName from "../../../components/user/user-name";
import { useUserDNSIdentity } from "../../../hooks/use-user-dns-identity";
function buildDescriptionContent(description: string) {
let content: EmbedableContent = [description.trim()];
@ -60,6 +61,42 @@ function buildDescriptionContent(description: string) {
return content;
}
function DNSIdentityWarning({ pubkey }: { pubkey: string }) {
const metadata = useUserMetadata(pubkey);
const dnsIdentity = useUserDNSIdentity(pubkey);
const parsedNip05 = metadata?.nip05 ? parseAddress(metadata.nip05) : undefined;
const nip05URL = parsedNip05
? `https://${parsedNip05.domain}/.well-known/nostr.json?name=${parsedNip05.name}`
: undefined;
if (dnsIdentity === undefined)
return (
<Text color="yellow.500">
Unable to check DNS identity due to CORS error{" "}
{nip05URL && (
<Link
color="blue.500"
href={`https://cors-test.codehappy.dev/?url=${encodeURIComponent(nip05URL)}&method=get`}
isExternal
>
Test
<ExternalLinkIcon ml="1" />
</Link>
)}
</Text>
);
else if (dnsIdentity.exists === false) return <Text color="red.500">Unable to find nostr.json file</Text>;
else if (dnsIdentity.pubkey === undefined)
return <Text color="red.500">Unable to find DNS Identity in nostr.json file</Text>;
else if (dnsIdentity.pubkey === pubkey) return null;
else
return (
<Text color="red.500" fontWeight="bold">
Invalid DNS Identity!
</Text>
);
}
export default function UserAboutTab() {
const expanded = useDisclosure();
const { pubkey } = useOutletContext() as { pubkey: string };
@ -69,10 +106,13 @@ export default function UserAboutTab() {
const metadata = useUserMetadata(pubkey, contextRelays);
const npub = nip19.npubEncode(pubkey);
const nprofile = useSharableProfileId(pubkey);
const pubkeyColor = "#" + pubkey.slice(0, 6);
const aboutContent = metadata?.about && buildDescriptionContent(metadata?.about);
const parsedNip05 = metadata?.nip05 ? parseAddress(metadata.nip05) : undefined;
const pubkeyColor = "#" + pubkey.slice(0, 6);
const nip05URL = parsedNip05
? `https://${parsedNip05.domain}/.well-known/nostr.json?name=${parsedNip05.name}`
: undefined;
return (
<Flex
@ -162,13 +202,16 @@ export default function UserAboutTab() {
</Link>
</Flex>
)}
{parsedNip05 && (
<Flex gap="2">
<AtIcon />
<Link href={`//${parsedNip05.domain}/.well-known/nostr.json?name=${parsedNip05.name}`} isExternal>
<UserDnsIdentity pubkey={pubkey} />
</Link>
</Flex>
{nip05URL && (
<Box>
<Flex gap="2">
<AtIcon />
<Link href={nip05URL} isExternal>
<UserDnsIdentity pubkey={pubkey} />
</Link>
</Flex>
<DNSIdentityWarning pubkey={pubkey} />
</Box>
)}
{metadata?.website && (
<Flex gap="2">