9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
150 lines
4.4 KiB
Dart
150 lines
4.4 KiB
Dart
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:nostr/nostr.dart' as nostr;
|
|
|
|
import '../crypto/ecdh.dart';
|
|
import '../crypto/nip44.dart';
|
|
import '../relay/relay.dart';
|
|
|
|
/// The message a reminder points back to. Mirrors the desktop
|
|
/// `ReminderTarget` shape (`desktop/src/features/reminders/lib/reminderTypes.ts`)
|
|
/// so reminders created on mobile are readable by desktop and vice versa.
|
|
class ReminderTarget {
|
|
/// Event ID of the target message.
|
|
final String eventId;
|
|
|
|
/// Channel ID where the message lives.
|
|
final String channelId;
|
|
|
|
/// Preview text of the target message (truncated).
|
|
final String preview;
|
|
|
|
/// Author pubkey of the target message.
|
|
final String authorPubkey;
|
|
|
|
const ReminderTarget({
|
|
required this.eventId,
|
|
required this.channelId,
|
|
required this.preview,
|
|
required this.authorPubkey,
|
|
});
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'eventId': eventId,
|
|
'channelId': channelId,
|
|
'preview': preview,
|
|
'authorPubkey': authorPubkey,
|
|
};
|
|
}
|
|
|
|
/// Build the NIP-ER reminder plaintext for a new pending reminder.
|
|
///
|
|
/// Matches the JSON desktop writes in `reminderService.ts#createReminder`:
|
|
/// `{"target": {...}, "note": <optional>, "status": "pending"}`. `note` is
|
|
/// omitted entirely when absent so both clients parse each other's payloads.
|
|
String buildReminderPlaintext({required ReminderTarget target, String? note}) {
|
|
return jsonEncode({
|
|
'target': target.toJson(),
|
|
if (note != null && note.isNotEmpty) 'note': note,
|
|
'status': 'pending',
|
|
});
|
|
}
|
|
|
|
/// Generate a reminder `d`-tag with 128 bits of entropy (NIP-ER MUST).
|
|
String randomReminderDTag() {
|
|
final bytes = secureRandomBytes(16);
|
|
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
|
}
|
|
|
|
/// Tags for a new pending reminder event: `d` plus strict-decimal
|
|
/// `not_before`, exactly as the relay's NIP-ER validator expects.
|
|
List<List<String>> buildReminderTags({
|
|
required String dTag,
|
|
required int notBefore,
|
|
}) {
|
|
if (notBefore < 0) {
|
|
throw ArgumentError('notBefore must be a non-negative Unix timestamp');
|
|
}
|
|
return [
|
|
['d', dTag],
|
|
['not_before', '$notBefore'],
|
|
];
|
|
}
|
|
|
|
/// NIP-44 self-encryption for author-private reminder payloads. Derives the
|
|
/// conversation key to the author's own pubkey, matching the desktop's
|
|
/// `nip44_encrypt_to_self` Tauri command.
|
|
class ReminderCrypto {
|
|
final Uint8List _conversationKey;
|
|
|
|
ReminderCrypto(String nsec, String pubkey)
|
|
: _conversationKey = _deriveKey(nsec, pubkey);
|
|
|
|
static Uint8List _deriveKey(String nsec, String pubkey) {
|
|
final privkeyHex = nostr.Nip19.decode(payload: nsec).data;
|
|
return getConversationKey(privkeyHex, pubkey);
|
|
}
|
|
|
|
String encrypt(String plaintext) => nip44Encrypt(_conversationKey, plaintext);
|
|
|
|
String decrypt(String ciphertext) =>
|
|
nip44Decrypt(_conversationKey, ciphertext);
|
|
}
|
|
|
|
/// Creates author-private kind-30300 reminders on the relay.
|
|
class ReminderService {
|
|
final SignedEventRelay _signedEventRelay;
|
|
final ReminderCrypto _crypto;
|
|
|
|
ReminderService({
|
|
required SignedEventRelay signedEventRelay,
|
|
required ReminderCrypto crypto,
|
|
}) : _signedEventRelay = signedEventRelay,
|
|
_crypto = crypto;
|
|
|
|
/// Encrypt, sign, and publish a new pending reminder. [notBefore] is a Unix
|
|
/// timestamp in seconds after which clients surface the reminder.
|
|
Future<void> createReminder({
|
|
required ReminderTarget target,
|
|
required int notBefore,
|
|
String? note,
|
|
}) async {
|
|
final plaintext = buildReminderPlaintext(target: target, note: note);
|
|
final ciphertext = _crypto.encrypt(plaintext);
|
|
await _signedEventRelay.submit(
|
|
kind: EventKind.eventReminder,
|
|
content: ciphertext,
|
|
tags: buildReminderTags(dTag: randomReminderDTag(), notBefore: notBefore),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Provides a [ReminderService], or null when no signing identity is
|
|
/// available (signed out).
|
|
final reminderServiceProvider = Provider<ReminderService?>((ref) {
|
|
final relayConfig = ref.watch(relayConfigProvider);
|
|
final pubkey = ref.watch(myPubkeyProvider);
|
|
|
|
final nsec = relayConfig.nsec?.trim();
|
|
if (nsec == null || nsec.isEmpty || pubkey == null || pubkey.isEmpty) {
|
|
return null;
|
|
}
|
|
|
|
final ReminderCrypto crypto;
|
|
try {
|
|
crypto = ReminderCrypto(nsec, pubkey);
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
|
|
return ReminderService(
|
|
signedEventRelay: SignedEventRelay(
|
|
session: ref.read(relaySessionProvider.notifier),
|
|
nsec: nsec,
|
|
),
|
|
crypto: crypto,
|
|
);
|
|
});
|