Skip to content

Commit

Permalink
Merge pull request #120 from Myzel394/feat-0.15.0-sort-relays
Browse files Browse the repository at this point in the history
Feat 0.15.0 sort relays
  • Loading branch information
Myzel394 committed Sep 28, 2023
2 parents b38b59c + 1c6a2a9 commit 9a5c31b
Show file tree
Hide file tree
Showing 9 changed files with 342 additions and 36 deletions.
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

0 comments on commit 9a5c31b

Please sign in to comment.