code review
This commit is contained in:
parent
fd3dddf2ed
commit
719aaca022
10 changed files with 117 additions and 68 deletions
|
|
@ -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()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
36
lib/src/nostr_event.dart
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue