diff --git a/lib/api/get-relays-meta.dart b/lib/api/get-relays-meta.dart new file mode 100644 index 0000000..27cdf3d --- /dev/null +++ b/lib/api/get-relays-meta.dart @@ -0,0 +1,175 @@ +import 'dart:convert'; + +import 'package:collection/collection.dart'; +import 'package:locus/utils/access-deeply-nested-key.dart'; +import 'package:locus/utils/nostr_fetcher/BasicNostrFetchSocket.dart'; +import 'package:locus/utils/nostr_fetcher/NostrSocket.dart'; +import 'package:nostr/nostr.dart'; + +const MIN_LENGTH = 5000; + +class RelaysMetaFetcher extends BasicNostrFetchSocket { + List meta = []; + + RelaysMetaFetcher({ + required super.relay, + super.timeout, + }); + + @override + void onEndOfStream() { + closeConnection(); + } + + @override + void onNostrEvent(final Message message) { + // Relay URL, canWrite and canRead are in message.tags + // Latencies are saved in content, separated per region + // with the following schema: + // [ + // [], + // [], + // [], + // ] + final event = message.message as Event; + + final relayMeta = RelayMeta.fromFetchedContent( + canWrite: event.tags[1][1] == "true", + canRead: event.tags[2][1] == "true", + relay: event.tags[0][1], + content: jsonDecode(event.content), + worldRegion: "eu-west", + ); + + meta.add(relayMeta); + } + + @override + void onError(error) { + closeConnection(); + } +} + +class RelayMeta { + final String relay; + final bool canWrite; + final bool canRead; + final String contactInfo; + final String description; + final String name; + + final List connectionLatencies; + final List readLatencies; + final List writeLatencies; + + final int maxMessageLength; + final int maxContentLength; + + final int minPowDifficulty; + final bool requiresPayment; + + const RelayMeta({ + required this.relay, + required this.canWrite, + required this.canRead, + required this.contactInfo, + required this.description, + required this.name, + required this.connectionLatencies, + required this.readLatencies, + required this.writeLatencies, + required this.maxMessageLength, + required this.maxContentLength, + required this.minPowDifficulty, + required this.requiresPayment, + }); + + factory RelayMeta.fromFetchedContent({ + required final Map content, + required final String relay, + required final bool canRead, + required final bool canWrite, + required final String worldRegion, + }) => + RelayMeta( + relay: relay, + canRead: canRead, + canWrite: canWrite, + name: adnk(content, "info.name") ?? relay, + contactInfo: adnk(content, "info.contact") ?? "", + description: adnk(content, "info.description") ?? "", + connectionLatencies: + List.from(adnk(content, "latency$worldRegion.0") ?? []) + .where((value) => value != null) + .toList() + .cast(), + readLatencies: + List.from(adnk(content, "latency$worldRegion.1") ?? []) + .where((value) => value != null) + .toList() + .cast(), + writeLatencies: + List.from(adnk(content, "latency$worldRegion.2") ?? []) + .where((value) => value != null) + .toList() + .cast(), + maxContentLength: + adnk(content, "info.limitations.max_content_length") ?? 0, + maxMessageLength: + adnk(content, "info.limitations.max_message_length") ?? 0, + requiresPayment: + adnk(content, "info.limitations.payment_required") ?? false, + minPowDifficulty: + adnk(content, "info.limitations.min_pow_difficulty") ?? 0); + + bool get isSuitable => + canWrite && + canRead && + !requiresPayment && + minPowDifficulty == 0 && + maxContentLength >= MIN_LENGTH; + + // Calculate average latency, we use the average as we want extreme highs + // to be taken into account. + double get score { + if (connectionLatencies.isEmpty || + readLatencies.isEmpty || + writeLatencies.isEmpty) { + // If there is no data available, we don't know if the relay is fully intact + return double.infinity; + } + + // Each latency has it's own factor to give each of them a different weight + // Lower latency = better - Because of this + // a factor closer to 0 resembles a HIGHER weight + // We prioritize read latency as we want to be able to provide a fast app + return (connectionLatencies.average * 0.9 + + readLatencies.average * 0.5 + + writeLatencies.average) + + (maxContentLength - MIN_LENGTH) * 0.0001; + } +} + +final REQUEST_DATA = NostrSocket.createNostrRequestData( + kinds: [30304], + limit: 10, + authors: ["b3b0d247f66bf40c4c9f4ce721abfe1fd3b7529fbc1ea5e64d5f0f8df3a4b6e6"], +); + +Future fetchRelaysMeta() async { + final fetcher = RelaysMetaFetcher( + relay: "wss://history.nostr.watch", + ); + await fetcher.connect(); + fetcher.addData( + Request( + generate64RandomHexChars(), + [ + REQUEST_DATA, + ], + ).serialize(), + ); + await fetcher.onComplete; + + print(fetcher.meta); +} diff --git a/lib/main.dart b/lib/main.dart index cd77f55..cb00750 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_logs/flutter_logs.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:locus/App.dart'; +import 'package:locus/api/get-relays-meta.dart'; import 'package:locus/screens/locations_overview_screen_widgets/LocationFetchers.dart'; import 'package:locus/services/app_update_service.dart'; import 'package:locus/services/current_location_service.dart'; diff --git a/lib/screens/LocationsOverviewScreen.dart b/lib/screens/LocationsOverviewScreen.dart index 48b07cf..0380515 100644 --- a/lib/screens/LocationsOverviewScreen.dart +++ b/lib/screens/LocationsOverviewScreen.dart @@ -19,6 +19,7 @@ import 'package:flutter_map_marker_popup/flutter_map_marker_popup.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:geolocator/geolocator.dart'; import 'package:latlong2/latlong.dart'; +import 'package:locus/api/get-relays-meta.dart'; import 'package:locus/constants/spacing.dart'; import 'package:locus/screens/ImportTaskSheet.dart'; import 'package:locus/screens/SettingsScreen.dart'; @@ -194,6 +195,8 @@ class _LocationsOverviewScreenState extends State flutterMapPopupController = PopupController(); } + + fetchRelaysMeta(); } @override diff --git a/lib/utils/access-deeply-nested-key.dart b/lib/utils/access-deeply-nested-key.dart new file mode 100644 index 0000000..981e883 --- /dev/null +++ b/lib/utils/access-deeply-nested-key.dart @@ -0,0 +1,15 @@ +T? accessDeeplyNestedKey(final Map obj, final String path) { + dynamic result = obj; + + for (final subPath in path.split(".")) { + if (result.containsKey(subPath)) { + result = result[subPath]; + } else { + return null; + } + } + + return result as T; +} + +const adnk = accessDeeplyNestedKey; diff --git a/lib/utils/nostr_fetcher/Socket.dart b/lib/utils/nostr_fetcher/Socket.dart index 78c4f99..b767499 100644 --- a/lib/utils/nostr_fetcher/Socket.dart +++ b/lib/utils/nostr_fetcher/Socket.dart @@ -76,11 +76,6 @@ abstract class Socket { return; } - // Prettify event.content - print( - jsonDecode(event)[2], - ); - _resetTimer(); onEvent(event); diff --git a/lib/widgets/RelaySelectSheet.dart b/lib/widgets/RelaySelectSheet.dart index f4bfebd..82c3f79 100644 --- a/lib/widgets/RelaySelectSheet.dart +++ b/lib/widgets/RelaySelectSheet.dart @@ -139,29 +139,6 @@ class _RelaySelectSheetState extends State { _closeSheet(); } }); - - final socket = NostrSocket( - relay: "wss://history.nostr.watch", - decryptMessage: (_) async { - return LocationPointService.dummyFromLatLng(LatLng(0, 0)); - }, - ); - socket.connect().then((_) { - socket.addData( - Request( - generate64RandomHexChars(), - [ - NostrSocket.createNostrRequestData( - kinds: [30304], - limit: 10, - authors: [ - "b3b0d247f66bf40c4c9f4ce721abfe1fd3b7529fbc1ea5e64d5f0f8df3a4b6e6" - ], - ), - ], - ).serialize(), - ); - }); } _closeSheet() {