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 'package:actors/actors.dart';
// =============== COMMANDS ===============
sealed class FileMessage {
const FileMessage();
}
@ -28,6 +30,8 @@ final class SaveBinaryFile extends FileMessage {
const SaveBinaryFile(this.path, this.bytes);
}
// =============== RESULTS ===============
sealed class FileResult {
const FileResult();
}
@ -46,15 +50,20 @@ final class BinaryFileLoaded extends FileResult {
const BinaryFileLoaded(this.bytes);
}
// =============== ACTOR ===============
class FileActor with Handler<FileMessage, FileResult> {
@override
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) =>
File(path).writeAsString(content).then((_) => const FileSaved()),
File(path).writeAsString(content).then((_) =>
const FileSaved()),
LoadBinaryFile(:final path) =>
File(path).readAsBytes().then(BinaryFileLoaded.new),
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';
// =============== COMMANDS ===============
sealed class FileDownloadMessage {
const FileDownloadMessage();
}
@ -15,6 +17,8 @@ final class DownloadFile extends FileDownloadMessage {
const DownloadFile({required this.url, required this.savePath});
}
// =============== RESULTS ===============
sealed class FileDownloadResult {
const FileDownloadResult();
}
@ -24,6 +28,8 @@ final class FileDownloaded extends FileDownloadResult {
const FileDownloaded(this.savePath);
}
// =============== ACTOR ===============
class FileDownloadActor
with Handler<FileDownloadMessage, FileDownloadResult> {
final FileActor _fileActor;

View file

@ -1,13 +1,12 @@
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:actors/actors.dart';
import 'package:bip340/bip340.dart' as bip340;
import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http;
import 'nip44.dart';
import 'nostr_event.dart';
import 'relay_connection.dart';
const _kServicePubkey =
@ -16,6 +15,8 @@ const _kDefaultApiBaseUrl = 'https://upload.otherwhere.app';
const _kDefaultRelayUrl = 'wss://relay.otherwhere.app/';
const _kMaxFileBytes = 10 * 1024 * 1024; // 10 MB server-side limit
// =============== COMMANDS ===============
sealed class FileUploadMessage {
const FileUploadMessage();
}
@ -30,6 +31,8 @@ final class UploadFile extends FileUploadMessage {
const UploadFile({required this.bytes, required this.contentType});
}
// =============== RESULTS ===============
sealed class FileUploadResult {
const FileUploadResult();
}
@ -43,6 +46,8 @@ final class FileUploaded extends FileUploadResult {
const FileUploaded(this.cdnUrl);
}
// =============== ACTOR ===============
class FileUploadActor with Handler<FileUploadMessage, FileUploadResult> {
final String _privateKey;
final String _publicKey;
@ -150,25 +155,12 @@ class FileUploadActor with Handler<FileUploadMessage, FileUploadResult> {
required int kind,
required List<List<String>> tags,
required String content,
}) {
final ts = 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,
});
}
}) =>
signNostrEvent(
privateKey: _privateKey,
publicKey: _publicKey,
kind: kind,
tags: tags,
content: content,
);
}

View file

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

View file

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

View file

@ -10,6 +10,8 @@ import 'bip39.dart';
import 'nostr_fetch_actor.dart';
import 'nostr_publish_actor.dart';
// =============== PROFILE OBJECT ===============
/// Nostr user profile (NIP-01 kind 0 content fields).
class NostrProfile {
final String? name;
@ -55,8 +57,7 @@ class NostrProfile {
};
}
// ---------------------------------------------------------------------------
// Messages
// =============== COMMANDS ===============
sealed class AccountMessage {
const AccountMessage();
@ -113,8 +114,7 @@ final class GetPublicData extends AccountMessage {
const GetPublicData();
}
// ---------------------------------------------------------------------------
// Results
// =============== RESULTS ===============
sealed class AccountResult {
const AccountResult();
@ -161,8 +161,7 @@ final class PublicData extends AccountResult {
const PublicData(this.data);
}
// ---------------------------------------------------------------------------
// Actor
// =============== ACTOR ===============
/// Manages a Nostr user identity: key lifecycle, BIP-39 mnemonic support, and
/// 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 'relay_connection.dart';
// =============== COMMANDS ===============
sealed class FetchMessage {
const FetchMessage();
}
@ -13,6 +15,8 @@ final class FetchEvents extends FetchMessage {
const FetchEvents(this.filter);
}
// =============== RESULTS ===============
sealed class FetchResult {
const FetchResult();
}
@ -22,6 +26,8 @@ final class EventsFetched extends FetchResult {
const EventsFetched(this.events);
}
// =============== ACTOR ===============
class NostrFetchActor with Handler<FetchMessage, FetchResult> {
final List<String> _relayUrls;
final Duration _eoseDeadline;

View file

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

View file

@ -1,9 +1,7 @@
import 'dart:convert';
import 'dart:math';
import 'package:actors/actors.dart';
import 'package:bip340/bip340.dart' as bip340;
import 'package:crypto/crypto.dart';
import 'nostr_event.dart';
sealed class SignerMessage {
const SignerMessage();
@ -60,26 +58,13 @@ class NostrSignerActor with Handler<SignerMessage, SignerResult> {
List<List<String>> tags,
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 auxBytes = List.generate(32, (_) => rng.nextInt(256));
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,
}));
}
) =>
EventSigned(signNostrEvent(
privateKey: _privateKey,
publicKey: _publicKey,
kind: kind,
tags: tags,
content: content,
createdAt: createdAt,
));
}