Skip to content
This repository has been archived by the owner on Dec 23, 2024. It is now read-only.

Feat 0.15.0 sort relays #120

Merged
merged 8 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions lib/api/get-relays-meta.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import 'dart:convert';

import 'package:collection/collection.dart';
import 'package:flutter_animate/flutter_animate.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<RelayMeta> 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:
// [
// [<connection speed>],
// [<read speed>],
// [<write speed>],
// ]
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<int> connectionLatencies;
final List<int> readLatencies;
final List<int> 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<String, dynamic> 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<dynamic>(content, "info.name") ?? relay,
contactInfo: adnk<dynamic>(content, "info.contact") ?? "",
description: adnk<dynamic>(content, "info.description") ?? "",
connectionLatencies: List<int?>.from(
adnk<dynamic>(content, "latency.$worldRegion.0") ?? [])
.where((value) => value != null)
.toList()
.cast<int>(),
readLatencies:
List<int?>.from(adnk<dynamic>(content, "latency.$worldRegion.1") ?? [])
.where((value) => value != null)
.toList()
.cast<int>(),
writeLatencies:
List<int?>.from(adnk<dynamic>(content, "latency.$worldRegion.2") ?? [])
.where((value) => value != null)
.toList()
.cast<int>(),
maxContentLength:
adnk<dynamic>(content, "info.limitations.max_content_length") ??
MIN_LENGTH,
maxMessageLength:
adnk<dynamic>(content, "info.limitations.max_message_length") ??
MIN_LENGTH,
requiresPayment:
adnk<dynamic>(content, "info.limitations.payment_required") ??
false,
minPowDifficulty:
adnk<dynamic>(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
// Lower score = better
return (connectionLatencies.average * 0.9 +
readLatencies.average * 0.5 +
writeLatencies.average) +
(maxContentLength - MIN_LENGTH) * 0.0001;
}
}

// Values taken from https://github.com/dskvr/nostr-watch/blob/develop/src/components/relays/jobs/LoadSeed.vue#L91
final REQUEST_DATA = NostrSocket.createNostrRequestData(
kinds: [30304],
limit: 1000,
from: DateTime.now().subtract(2.hours),
authors: ["b3b0d247f66bf40c4c9f4ce721abfe1fd3b7529fbc1ea5e64d5f0f8df3a4b6e6"],
);

Future<Map<String, List<RelayMeta>>> fetchRelaysMeta() async {
final fetcher = RelaysMetaFetcher(
relay: "wss://history.nostr.watch",
);
await fetcher.connect();
fetcher.addData(
Request(
generate64RandomHexChars(),
[
REQUEST_DATA,
],
).serialize(),
);
await fetcher.onComplete;

return {
"meta": fetcher.meta,
};
}
2 changes: 2 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@
}
},
"relaySelectSheet_selectRandomRelays": "Select {count} random Nostr Relays",
"relaySelectSheet_loadingRelaysMeta": "Loading Nostr Relays information...",
"relaySelectSheet_hint": "Relays are sorted from best to worst in ascending order. The best relays are at the top.",
"taskAction_start": "Start Task",
"taskAction_started_title": "Task started",
"taskAction_started_description": "Task started at {date}",
Expand Down
1 change: 1 addition & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions lib/screens/LocationsOverviewScreen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ class _ActiveSharesSheetState extends State<ActiveSharesSheet>
child: SizedBox.square(),
),
Expanded(
flex: 8,
flex: 6,
child: Center(
child: Text(
l10n.locationsOverview_activeShares_amount(
Expand Down
19 changes: 19 additions & 0 deletions lib/utils/access-deeply-nested-key.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
T? accessDeeplyNestedKey<T>(final Map<String, dynamic> obj, final String path) {
dynamic result = obj;

for (final subPath in path.split(".")) {
if (result is List) {
final index = int.tryParse(subPath)!;

result = result[index];
} else if (result.containsKey(subPath)) {
result = result[subPath];
} else {
return null;
}
}

return result as T;
}

const adnk = accessDeeplyNestedKey;
3 changes: 2 additions & 1 deletion lib/utils/nostr_fetcher/NostrSocket.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import 'dart:async';
import 'package:flutter_logs/flutter_logs.dart';
import 'package:locus/constants/values.dart';
import 'package:locus/services/location_point_service.dart';
import 'package:locus/services/task_service/index.dart';
import 'package:locus/services/task_service/mixins.dart';
import 'package:locus/utils/nostr_fetcher/BasicNostrFetchSocket.dart';
import 'package:locus/utils/nostr_fetcher/Socket.dart';
Expand Down Expand Up @@ -121,10 +120,12 @@ class NostrSocket extends BasicNostrFetchSocket {
final int? limit,
final DateTime? from,
final DateTime? until,
final List<String>? authors,
}) =>
Filter(
kinds: kinds,
limit: limit,
authors: authors ?? [],
since:
from == null ? null : (from.millisecondsSinceEpoch / 1000).floor(),
until: until == null
Expand Down
1 change: 1 addition & 0 deletions lib/utils/nostr_fetcher/Socket.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:flutter_logs/flutter_logs.dart';
Expand Down
Loading