141 lines
4.1 KiB
TypeScript
141 lines
4.1 KiB
TypeScript
import { useQuery } from '@tanstack/react-query';
|
|
import { useNostr } from '@nostrify/react';
|
|
import { useCurrentUser } from './useCurrentUser';
|
|
import { onlyVerified } from '@/lib/verifyEvents';
|
|
|
|
/**
|
|
* Hook to calculate trust score for a Nostr pubkey using Web of Trust (WoT).
|
|
*
|
|
* Trust scores are calculated using social graph analysis:
|
|
* - 100: Self (viewing your own content)
|
|
* - 90: Direct follow (you follow them)
|
|
* - 60: Friend-of-friend (you follow someone who follows them)
|
|
* - 30: Has NIP-05 verification
|
|
* - 0: No connection or verification
|
|
*
|
|
* @param targetPubkey - The pubkey to calculate trust score for
|
|
* @returns Trust score (0-100) or undefined if not available
|
|
*/
|
|
export function useTrustScore(targetPubkey: string | undefined) {
|
|
const { nostr } = useNostr();
|
|
const { user } = useCurrentUser();
|
|
|
|
return useQuery({
|
|
queryKey: ['trust-score', targetPubkey, user?.pubkey],
|
|
queryFn: async (c) => {
|
|
if (!targetPubkey) return undefined;
|
|
|
|
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(5000)]);
|
|
|
|
try {
|
|
// Self score = 100
|
|
if (user?.pubkey && targetPubkey === user.pubkey) {
|
|
return 100;
|
|
}
|
|
|
|
// Get user's follow list (kind 3)
|
|
const sourcePubkey = user?.pubkey || '20e7c953439e66f3882ab96fe0faa8ba246d6fb12c03b0cb4e2dc4623b8bcb21';
|
|
|
|
// Signature-verify follow lists before deriving any trust from them —
|
|
// an unverified kind-3 from a malicious relay could inflate a score.
|
|
const followListEvents = onlyVerified(await nostr.query(
|
|
[{ kinds: [3], authors: [sourcePubkey], limit: 1 }],
|
|
{ signal }
|
|
));
|
|
|
|
const followList = followListEvents[0];
|
|
if (!followList) {
|
|
return 0;
|
|
}
|
|
|
|
// Extract followed pubkeys from tags
|
|
const following = new Set(
|
|
followList.tags
|
|
.filter(([tag]) => tag === 'p')
|
|
.map(([, pubkey]) => pubkey)
|
|
);
|
|
|
|
// Direct follow = 90
|
|
if (following.has(targetPubkey)) {
|
|
return 90;
|
|
}
|
|
|
|
// Check friend-of-friend (2 hops)
|
|
const friendsFollowLists = onlyVerified(await nostr.query(
|
|
[{ kinds: [3], authors: Array.from(following), limit: 100 }],
|
|
{ signal }
|
|
));
|
|
|
|
for (const friendList of friendsFollowLists) {
|
|
const friendFollowing = friendList.tags
|
|
.filter(([tag]) => tag === 'p')
|
|
.map(([, pubkey]) => pubkey);
|
|
|
|
if (friendFollowing.includes(targetPubkey)) {
|
|
return 60;
|
|
}
|
|
}
|
|
|
|
// Check if they have NIP-05 verification (basic credibility)
|
|
const profileEvents = onlyVerified(await nostr.query(
|
|
[{ kinds: [0], authors: [targetPubkey], limit: 1 }],
|
|
{ signal }
|
|
));
|
|
|
|
if (profileEvents[0]) {
|
|
try {
|
|
const metadata = JSON.parse(profileEvents[0].content);
|
|
if (metadata.nip05) {
|
|
return 30;
|
|
}
|
|
} catch {
|
|
// Ignore JSON parse errors
|
|
}
|
|
}
|
|
|
|
// No trust signals found
|
|
return 0;
|
|
} catch (error) {
|
|
console.warn('[useTrustScore] Error calculating trust score:', error);
|
|
return undefined;
|
|
}
|
|
},
|
|
enabled: !!targetPubkey,
|
|
staleTime: 3600000, // Cache for 1 hour (trust scores change slowly)
|
|
gcTime: 86400000, // Keep in cache for 24 hours
|
|
retry: 1, // Only retry once on failure
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get trust badge configuration based on trust score
|
|
*/
|
|
export function getTrustBadgeConfig(score: number | undefined) {
|
|
if (score === undefined || score < 50) {
|
|
return {
|
|
show: false,
|
|
variant: 'outline' as const,
|
|
className: 'border-gray-500 text-gray-500',
|
|
label: 'UNVERIFIED',
|
|
icon: null,
|
|
};
|
|
}
|
|
|
|
if (score < 80) {
|
|
return {
|
|
show: true,
|
|
variant: 'outline' as const,
|
|
className: 'border-yellow-500 text-yellow-500',
|
|
label: 'TRUSTED',
|
|
icon: 'shield',
|
|
};
|
|
}
|
|
|
|
return {
|
|
show: true,
|
|
variant: 'default' as const,
|
|
className: 'bg-green-500 text-white',
|
|
label: 'VERIFIED',
|
|
icon: 'check',
|
|
};
|
|
}
|