dev
dart 127 lines 3.81 KB
Raw
1 import 'dart:async';
2 import 'dart:convert';
3
4 import 'package:cake_wallet/core/address_resolver/nostr/nostr_user.dart';
5 import 'package:nostr_tools/nostr_tools.dart';
6 import 'dart:async' show Completer, TimeoutException, runZonedGuarded;
7
8 class NostrProfileHandler {
9 static final relayToDomainMap = {
10 'relay.snort.social': 'snort.social',
11 };
12
13 static final Nip05 _nip05 = Nip05();
14
15 static Future<ProfilePointer?> queryProfile(String nip05Address) async {
16 final profile = await _nip05.queryProfile(nip05Address);
17 if (profile?.pubkey != null && profile?.relays?.isNotEmpty == true) {
18 return profile;
19 }
20 return null;
21 }
22
23 static Future<UserMetadata?> processRelays(
24 ProfilePointer profile,
25 String nip05Address,
26 ) async {
27 final userDomain = _extractDomain(nip05Address);
28 const int metaKind = 0;
29
30 // Domain-matched relays first
31 for (final String relayUrl in profile.relays ?? []) {
32 final relayDomain =
33 relayToDomainMap[_getDomainFromRelayUrl(relayUrl)] ?? _getDomainFromRelayUrl(relayUrl);
34
35 if (relayDomain == userDomain) {
36 final data = await _fetchInfoFromRelay(relayUrl, profile.pubkey, [metaKind]);
37 if (data != null) return data;
38 }
39 }
40
41 // Then try every remaining relay
42 for (final String relayUrl in profile.relays ?? []) {
43 final data = await _fetchInfoFromRelay(relayUrl, profile.pubkey, [metaKind]);
44 if (data != null) return data;
45 }
46
47 // Nothing found
48 return null;
49 }
50
51 static const Duration _relayTimeout = Duration(seconds: 3);
52
53 static Future<UserMetadata?> _fetchInfoFromRelay(
54 String relayUrl, String userPubKey, List<int> kinds) async {
55 // sanitize so obvious junk (like '#') doesn't reach connect()
56 final clean = _sanitizeRelay(relayUrl);
57 if (clean.isEmpty) return null;
58
59 final result = Completer<UserMetadata?>();
60
61 runZonedGuarded(() async {
62 try {
63 final relay = RelayApi(relayUrl: clean);
64
65 final stream = await relay.connect().timeout(
66 _relayTimeout,
67 onTimeout: () {
68 relay.close();
69 throw TimeoutException('Relay connect timeout');
70 },
71 );
72
73 relay.sub([
74 Filter(kinds: kinds, authors: [userPubKey])
75 ]);
76
77 final sub = stream.listen((msg) {
78 if (msg.type == 'EVENT' && !result.isCompleted) {
79 try {
80 final event = msg.message as Event;
81 final jsonMap = json.decode(event.content) as Map<String, dynamic>;
82 result.complete(UserMetadata.fromJson(jsonMap));
83 } catch (_) {
84 if (!result.isCompleted) result.complete(null);
85 }
86 }
87 }, onError: (_) {
88 if (!result.isCompleted) result.complete(null);
89 }, onDone: () {
90 if (!result.isCompleted) result.complete(null);
91 });
92
93 final value = await result.future.timeout(_relayTimeout, onTimeout: () => null);
94 await sub.cancel();
95 relay.close();
96 if (!result.isCompleted) result.complete(value);
97 } catch (_) {
98 if (!result.isCompleted) result.complete(null);
99 }
100 }, (error, stack) {
101 // swallow ALL async errors from the websocket layer (including "was not upgraded to websocket")
102 if (!result.isCompleted) result.complete(null);
103 });
104
105 return result.future;
106 }
107
108 static String _sanitizeRelay(String url) {
109 url = url.replaceFirst(RegExp(r'^https?://'), 'wss://');
110 final uri = Uri.parse(url);
111 return Uri(
112 scheme: uri.scheme.isEmpty ? 'wss' : uri.scheme,
113 host: uri.host,
114 port: uri.hasPort ? uri.port : 443,
115 ).toString();
116 }
117
118 static String _extractDomain(String nip05) => nip05.split('@').last;
119
120 static String _getDomainFromRelayUrl(String url) {
121 try {
122 return Uri.parse(url).host;
123 } catch (_) {
124 return '';
125 }
126 }
127 }