code review

This commit is contained in:
randogoth 2026-06-10 11:20:20 +03:00
parent fd3dddf2ed
commit 719aaca022
10 changed files with 117 additions and 68 deletions

View file

@ -2,6 +2,8 @@ import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:actors/actors.dart'; import 'package:actors/actors.dart';
// =============== COMMANDS ===============
sealed class FileMessage { sealed class FileMessage {
const FileMessage(); const FileMessage();
} }
@ -28,6 +30,8 @@ final class SaveBinaryFile extends FileMessage {
const SaveBinaryFile(this.path, this.bytes); const SaveBinaryFile(this.path, this.bytes);
} }
// =============== RESULTS ===============
sealed class FileResult { sealed class FileResult {
const FileResult(); const FileResult();
} }
@ -46,15 +50,20 @@ final class BinaryFileLoaded extends FileResult {
const BinaryFileLoaded(this.bytes); const BinaryFileLoaded(this.bytes);
} }
// =============== ACTOR ===============
class FileActor with Handler<FileMessage, FileResult> { class FileActor with Handler<FileMessage, FileResult> {
@override @override
Future<FileResult> handle(FileMessage message) => switch (message) { Future<FileResult> handle(FileMessage message) => switch (message) {
LoadFile(:final path) => File(path).readAsString().then(FileLoaded.new), LoadFile(:final path) =>
File(path).readAsString().then(FileLoaded.new),
SaveFile(:final path, :final content) => SaveFile(:final path, :final content) =>
File(path).writeAsString(content).then((_) => const FileSaved()), File(path).writeAsString(content).then((_) =>
const FileSaved()),
LoadBinaryFile(:final path) => LoadBinaryFile(:final path) =>
File(path).readAsBytes().then(BinaryFileLoaded.new), File(path).readAsBytes().then(BinaryFileLoaded.new),
SaveBinaryFile(:final path, :final bytes) => SaveBinaryFile(:final path, :final bytes) =>
File(path).writeAsBytes(bytes).then((_) => const FileSaved()), File(path).writeAsBytes(bytes).then((_) =>
const FileSaved()),
}; };
} }

View file

@ -5,6 +5,8 @@ import 'package:http/http.dart' as http;
import 'file_actor.dart'; import 'file_actor.dart';
// =============== COMMANDS ===============
sealed class FileDownloadMessage { sealed class FileDownloadMessage {
const FileDownloadMessage(); const FileDownloadMessage();
} }
@ -15,6 +17,8 @@ final class DownloadFile extends FileDownloadMessage {
const DownloadFile({required this.url, required this.savePath}); const DownloadFile({required this.url, required this.savePath});
} }
// =============== RESULTS ===============
sealed class FileDownloadResult { sealed class FileDownloadResult {
const FileDownloadResult(); const FileDownloadResult();
} }
@ -24,6 +28,8 @@ final class FileDownloaded extends FileDownloadResult {
const FileDownloaded(this.savePath); const FileDownloaded(this.savePath);
} }
// =============== ACTOR ===============
class FileDownloadActor class FileDownloadActor
with Handler<FileDownloadMessage, FileDownloadResult> { with Handler<FileDownloadMessage, FileDownloadResult> {
final FileActor _fileActor; final FileActor _fileActor;

View file

@ -1,13 +1,12 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:actors/actors.dart'; import 'package:actors/actors.dart';
import 'package:bip340/bip340.dart' as bip340; import 'package:bip340/bip340.dart' as bip340;
import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'nip44.dart'; import 'nip44.dart';
import 'nostr_event.dart';
import 'relay_connection.dart'; import 'relay_connection.dart';
const _kServicePubkey = const _kServicePubkey =
@ -16,6 +15,8 @@ const _kDefaultApiBaseUrl = 'https://upload.otherwhere.app';
const _kDefaultRelayUrl = 'wss://relay.otherwhere.app/'; const _kDefaultRelayUrl = 'wss://relay.otherwhere.app/';
const _kMaxFileBytes = 10 * 1024 * 1024; // 10 MB server-side limit const _kMaxFileBytes = 10 * 1024 * 1024; // 10 MB server-side limit
// =============== COMMANDS ===============
sealed class FileUploadMessage { sealed class FileUploadMessage {
const FileUploadMessage(); const FileUploadMessage();
} }
@ -30,6 +31,8 @@ final class UploadFile extends FileUploadMessage {
const UploadFile({required this.bytes, required this.contentType}); const UploadFile({required this.bytes, required this.contentType});
} }
// =============== RESULTS ===============
sealed class FileUploadResult { sealed class FileUploadResult {
const FileUploadResult(); const FileUploadResult();
} }
@ -43,6 +46,8 @@ final class FileUploaded extends FileUploadResult {
const FileUploaded(this.cdnUrl); const FileUploaded(this.cdnUrl);
} }
// =============== ACTOR ===============
class FileUploadActor with Handler<FileUploadMessage, FileUploadResult> { class FileUploadActor with Handler<FileUploadMessage, FileUploadResult> {
final String _privateKey; final String _privateKey;
final String _publicKey; final String _publicKey;
@ -150,25 +155,12 @@ class FileUploadActor with Handler<FileUploadMessage, FileUploadResult> {
required int kind, required int kind,
required List<List<String>> tags, required List<List<String>> tags,
required String content, required String content,
}) { }) =>
final ts = DateTime.now().millisecondsSinceEpoch ~/ 1000; signNostrEvent(
final ser = jsonEncode([0, _publicKey, ts, kind, tags, content]); privateKey: _privateKey,
final idBytes = sha256.convert(utf8.encode(ser)).bytes; publicKey: _publicKey,
final id = kind: kind,
idBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); tags: tags,
final rng = Random.secure(); content: content,
final aux = List.generate(32, (_) => rng.nextInt(256)) );
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join();
final sig = bip340.sign(_privateKey, id, aux);
return jsonEncode({
'id': id,
'pubkey': _publicKey,
'created_at': ts,
'kind': kind,
'tags': tags,
'content': content,
'sig': sig,
});
}
} }

View file

@ -1,6 +1,8 @@
import 'package:actors/actors.dart'; import 'package:actors/actors.dart';
import 'package:dart_geohash/dart_geohash.dart'; import 'package:dart_geohash/dart_geohash.dart';
// =============== COMMANDS ===============
sealed class GeoMessage { sealed class GeoMessage {
const GeoMessage(); const GeoMessage();
} }
@ -26,6 +28,8 @@ final class NeighborsOf extends GeoMessage {
const NeighborsOf(this.geohash); const NeighborsOf(this.geohash);
} }
// =============== RESULTS ===============
sealed class GeoResult { sealed class GeoResult {
const GeoResult(); const GeoResult();
} }
@ -47,6 +51,8 @@ final class NeighborsResult extends GeoResult {
const NeighborsResult(this.neighbors); const NeighborsResult(this.neighbors);
} }
// =============== ACTOR ===============
class GeoActor with Handler<GeoMessage, GeoResult> { class GeoActor with Handler<GeoMessage, GeoResult> {
final _hasher = GeoHasher(); final _hasher = GeoHasher();

View file

@ -10,6 +10,8 @@ import 'nip44.dart';
import 'nostr_signer_actor.dart'; import 'nostr_signer_actor.dart';
import 'relay_connection.dart'; import 'relay_connection.dart';
// =============== ACTOR ===============
/// NIP-46 remote signer drop-in replacement for [NostrSignerActor]. /// NIP-46 remote signer drop-in replacement for [NostrSignerActor].
/// ///
/// Accepts a bunker URI (`bunker://<signerPubkey>?relay=<url>&secret=<secret>`), /// Accepts a bunker URI (`bunker://<signerPubkey>?relay=<url>&secret=<secret>`),
@ -94,7 +96,7 @@ class Nip46SignerActor with Handler<SignerMessage, SignerResult> {
_pending.clear(); _pending.clear();
} }
// --------------------------------------------------------------------------- // =============== METHODS ===============
Future<void> _ensureConnected() async { Future<void> _ensureConnected() async {
if (_connected) return; if (_connected) return;

View file

@ -10,6 +10,8 @@ import 'bip39.dart';
import 'nostr_fetch_actor.dart'; import 'nostr_fetch_actor.dart';
import 'nostr_publish_actor.dart'; import 'nostr_publish_actor.dart';
// =============== PROFILE OBJECT ===============
/// Nostr user profile (NIP-01 kind 0 content fields). /// Nostr user profile (NIP-01 kind 0 content fields).
class NostrProfile { class NostrProfile {
final String? name; final String? name;
@ -55,8 +57,7 @@ class NostrProfile {
}; };
} }
// --------------------------------------------------------------------------- // =============== COMMANDS ===============
// Messages
sealed class AccountMessage { sealed class AccountMessage {
const AccountMessage(); const AccountMessage();
@ -113,8 +114,7 @@ final class GetPublicData extends AccountMessage {
const GetPublicData(); const GetPublicData();
} }
// --------------------------------------------------------------------------- // =============== RESULTS ===============
// Results
sealed class AccountResult { sealed class AccountResult {
const AccountResult(); const AccountResult();
@ -161,8 +161,7 @@ final class PublicData extends AccountResult {
const PublicData(this.data); const PublicData(this.data);
} }
// --------------------------------------------------------------------------- // =============== ACTOR ===============
// Actor
/// Manages a Nostr user identity: key lifecycle, BIP-39 mnemonic support, and /// Manages a Nostr user identity: key lifecycle, BIP-39 mnemonic support, and
/// profile (kind 0) fetch / publish via injected helper actors. /// profile (kind 0) fetch / publish via injected helper actors.

36
lib/src/nostr_event.dart Normal file
View file

@ -0,0 +1,36 @@
import 'dart:convert';
import 'dart:math';
import 'package:bip340/bip340.dart' as bip340;
import 'package:crypto/crypto.dart';
String signNostrEvent({
required String privateKey,
required String publicKey,
required int kind,
required List<List<String>> tags,
required String content,
int? createdAt,
}) {
final ts = createdAt ?? DateTime.now().millisecondsSinceEpoch ~/ 1000;
final ser = jsonEncode([0, publicKey, ts, kind, tags, content]);
final idBytes = sha256.convert(utf8.encode(ser)).bytes;
final id = idBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
final rng = Random.secure();
final aux = List.generate(32, (_) => rng.nextInt(256))
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join();
final sig = bip340.sign(privateKey, id, aux);
return jsonEncode({
'id': id,
'pubkey': publicKey,
'created_at': ts,
'kind': kind,
'tags': tags,
'content': content,
'sig': sig,
});
}

View file

@ -4,6 +4,8 @@ import 'dart:math';
import 'package:actors/actors.dart'; import 'package:actors/actors.dart';
import 'relay_connection.dart'; import 'relay_connection.dart';
// =============== COMMANDS ===============
sealed class FetchMessage { sealed class FetchMessage {
const FetchMessage(); const FetchMessage();
} }
@ -13,6 +15,8 @@ final class FetchEvents extends FetchMessage {
const FetchEvents(this.filter); const FetchEvents(this.filter);
} }
// =============== RESULTS ===============
sealed class FetchResult { sealed class FetchResult {
const FetchResult(); const FetchResult();
} }
@ -22,6 +26,8 @@ final class EventsFetched extends FetchResult {
const EventsFetched(this.events); const EventsFetched(this.events);
} }
// =============== ACTOR ===============
class NostrFetchActor with Handler<FetchMessage, FetchResult> { class NostrFetchActor with Handler<FetchMessage, FetchResult> {
final List<String> _relayUrls; final List<String> _relayUrls;
final Duration _eoseDeadline; final Duration _eoseDeadline;

View file

@ -4,6 +4,8 @@ import 'package:actors/actors.dart';
import 'package:retry/retry.dart'; import 'package:retry/retry.dart';
import 'relay_connection.dart'; import 'relay_connection.dart';
// =============== HELPER OBJECTS ===============
final class RetryConfig { final class RetryConfig {
final int maxAttempts; final int maxAttempts;
final Duration baseDelay; final Duration baseDelay;
@ -17,6 +19,19 @@ final class RetryConfig {
}); });
} }
// =============== COMMANDS ===============
sealed class PublishMessage {
const PublishMessage();
}
final class PublishBatch extends PublishMessage {
final List<String> events;
const PublishBatch(this.events);
}
// =============== RESULTS ===============
sealed class RelayOutcome { sealed class RelayOutcome {
const RelayOutcome(); const RelayOutcome();
} }
@ -34,15 +49,6 @@ final class RelayFailed extends RelayOutcome {
const RelayFailed(); const RelayFailed();
} }
sealed class PublishMessage {
const PublishMessage();
}
final class PublishBatch extends PublishMessage {
final List<String> events;
const PublishBatch(this.events);
}
sealed class PublishResult { sealed class PublishResult {
const PublishResult(); const PublishResult();
} }
@ -57,6 +63,8 @@ final class _RelayRejectedException implements Exception {
const _RelayRejectedException(this.reason); const _RelayRejectedException(this.reason);
} }
// =============== ACTOR ===============
class NostrPublishActor with Handler<PublishMessage, PublishResult> { class NostrPublishActor with Handler<PublishMessage, PublishResult> {
final List<String> _relayUrls; final List<String> _relayUrls;
final RetryConfig _retryConfig; final RetryConfig _retryConfig;

View file

@ -1,9 +1,7 @@
import 'dart:convert';
import 'dart:math';
import 'package:actors/actors.dart'; import 'package:actors/actors.dart';
import 'package:bip340/bip340.dart' as bip340; import 'package:bip340/bip340.dart' as bip340;
import 'package:crypto/crypto.dart';
import 'nostr_event.dart';
sealed class SignerMessage { sealed class SignerMessage {
const SignerMessage(); const SignerMessage();
@ -60,26 +58,13 @@ class NostrSignerActor with Handler<SignerMessage, SignerResult> {
List<List<String>> tags, List<List<String>> tags,
String content, String content,
int? createdAt, int? createdAt,
) { ) =>
final ts = createdAt ?? DateTime.now().millisecondsSinceEpoch ~/ 1000; EventSigned(signNostrEvent(
final ser = jsonEncode([0, _publicKey, ts, kind, tags, content]); privateKey: _privateKey,
final idBytes = sha256.convert(utf8.encode(ser)).bytes; publicKey: _publicKey,
final id = idBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); kind: kind,
tags: tags,
final rng = Random.secure(); content: content,
final auxBytes = List.generate(32, (_) => rng.nextInt(256)); createdAt: createdAt,
final aux = auxBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); ));
final sig = bip340.sign(_privateKey, id, aux);
return EventSigned(jsonEncode({
'id': id,
'pubkey': _publicKey,
'created_at': ts,
'kind': kind,
'tags': tags,
'content': content,
'sig': sig,
}));
}
} }