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>
209 lines
5.5 KiB
Dart
209 lines
5.5 KiB
Dart
import 'dart:convert';
|
|
import 'dart:math';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
import 'read_state_format.dart';
|
|
import 'read_state_time.dart';
|
|
|
|
const _clientIdKeyPrefix = 'buzz.nip-rs.client-id';
|
|
const _slotIdKeyPrefix = 'buzz.nip-rs.slot-id';
|
|
const _uuid = Uuid();
|
|
|
|
String localReadStateKey(String pubkey) => 'buzz.channel-read-state.v2:$pubkey';
|
|
|
|
String localPublishableContextKey(String pubkey) =>
|
|
'buzz.channel-read-state.publishable.v1:$pubkey';
|
|
|
|
String localSourceCreatedAtKey(String pubkey) =>
|
|
'buzz.channel-read-state.source-created-at.v1:$pubkey';
|
|
|
|
String clientIdKey(String pubkey) => '$_clientIdKeyPrefix:$pubkey';
|
|
|
|
String slotIdKey(String pubkey) => '$_slotIdKeyPrefix:$pubkey';
|
|
|
|
class StoredReadState {
|
|
final Map<String, int> contexts;
|
|
final Set<String> publishableContextIds;
|
|
final Map<String, int> sourceCreatedAt;
|
|
|
|
StoredReadState({
|
|
required Map<String, int> contexts,
|
|
required Set<String> publishableContextIds,
|
|
required Map<String, int> sourceCreatedAt,
|
|
}) : contexts = Map.unmodifiable(contexts),
|
|
publishableContextIds = Set.unmodifiable(publishableContextIds),
|
|
sourceCreatedAt = Map.unmodifiable(sourceCreatedAt);
|
|
}
|
|
|
|
class ReadStateStorage {
|
|
final SharedPreferences _prefs;
|
|
|
|
ReadStateStorage(this._prefs);
|
|
|
|
String getOrCreateClientId(String pubkey) {
|
|
final key = clientIdKey(pubkey);
|
|
final stored = _prefs.getString(key);
|
|
if (_isValidClientId(stored)) {
|
|
return stored!;
|
|
}
|
|
|
|
final generated = _uuid.v4();
|
|
_prefs.setString(key, generated);
|
|
return generated;
|
|
}
|
|
|
|
String getOrCreateSlotId(String pubkey) {
|
|
final key = slotIdKey(pubkey);
|
|
final stored = _prefs.getString(key);
|
|
if (_isValidSlotId(stored)) {
|
|
return stored!;
|
|
}
|
|
|
|
final generated = generateReadStateSlotId();
|
|
_prefs.setString(key, generated);
|
|
return generated;
|
|
}
|
|
|
|
void writeSlotId(String pubkey, String slotId) {
|
|
_prefs.setString(slotIdKey(pubkey), slotId);
|
|
}
|
|
|
|
StoredReadState read(String pubkey) {
|
|
return StoredReadState(
|
|
contexts: _readContexts(pubkey),
|
|
publishableContextIds: _readPublishableContextIds(pubkey),
|
|
sourceCreatedAt: _readSourceCreatedAt(pubkey),
|
|
);
|
|
}
|
|
|
|
void write(
|
|
String pubkey,
|
|
Map<String, int> contexts,
|
|
Set<String> publishableContextIds,
|
|
Map<String, int> sourceCreatedAt,
|
|
) {
|
|
final state = <String, String>{};
|
|
for (final entry in contexts.entries) {
|
|
state[entry.key] = unixSecondsToDateTime(entry.value).toIso8601String();
|
|
}
|
|
|
|
_prefs.setString(localReadStateKey(pubkey), jsonEncode(state));
|
|
_prefs.setString(
|
|
localPublishableContextKey(pubkey),
|
|
jsonEncode(publishableContextIds.toList()),
|
|
);
|
|
_prefs.setString(
|
|
localSourceCreatedAtKey(pubkey),
|
|
jsonEncode(sourceCreatedAt.map((k, v) => MapEntry(k, v.toString()))),
|
|
);
|
|
}
|
|
|
|
Map<String, int> _readContexts(String pubkey) {
|
|
final raw = _prefs.getString(localReadStateKey(pubkey));
|
|
if (raw == null || raw.isEmpty) {
|
|
return {};
|
|
}
|
|
|
|
final Object? parsed;
|
|
try {
|
|
parsed = jsonDecode(raw);
|
|
} catch (e) {
|
|
debugPrint('[ReadStateManager] storage: contexts JSON corrupt: $e');
|
|
return {};
|
|
}
|
|
|
|
final record = asStringObjectMap(parsed);
|
|
if (record == null) {
|
|
return {};
|
|
}
|
|
|
|
final contexts = <String, int>{};
|
|
for (final entry in record.entries) {
|
|
final timestamp = isoToUnixSeconds(entry.value);
|
|
if (timestamp == null) continue;
|
|
|
|
final current = contexts[entry.key] ?? 0;
|
|
if (timestamp > current) {
|
|
contexts[entry.key] = timestamp;
|
|
}
|
|
}
|
|
return contexts;
|
|
}
|
|
|
|
Set<String> _readPublishableContextIds(String pubkey) {
|
|
final raw = _prefs.getString(localPublishableContextKey(pubkey));
|
|
if (raw == null || raw.isEmpty) {
|
|
return {};
|
|
}
|
|
|
|
final Object? parsed;
|
|
try {
|
|
parsed = jsonDecode(raw);
|
|
} catch (e) {
|
|
debugPrint(
|
|
'[ReadStateManager] storage: publishableContextIds JSON corrupt: $e',
|
|
);
|
|
return {};
|
|
}
|
|
|
|
if (parsed is! List) {
|
|
return {};
|
|
}
|
|
|
|
return {
|
|
for (final value in parsed)
|
|
if (value is String) value,
|
|
};
|
|
}
|
|
|
|
Map<String, int> _readSourceCreatedAt(String pubkey) {
|
|
final raw = _prefs.getString(localSourceCreatedAtKey(pubkey));
|
|
if (raw == null || raw.isEmpty) return {};
|
|
|
|
final Object? parsed;
|
|
try {
|
|
parsed = jsonDecode(raw);
|
|
} catch (e) {
|
|
debugPrint(
|
|
'[ReadStateManager] storage: sourceCreatedAt JSON corrupt: $e',
|
|
);
|
|
return {};
|
|
}
|
|
|
|
final record = asStringObjectMap(parsed);
|
|
if (record == null) return {};
|
|
|
|
final result = <String, int>{};
|
|
for (final entry in record.entries) {
|
|
final value = entry.value;
|
|
if (value is int) {
|
|
result[entry.key] = value;
|
|
} else if (value is String) {
|
|
final parsed = int.tryParse(value);
|
|
if (parsed != null) result[entry.key] = parsed;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
String generateReadStateSlotId() {
|
|
final random = Random.secure();
|
|
final bytes = List<int>.generate(16, (_) => random.nextInt(256));
|
|
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
|
|
}
|
|
|
|
bool _isValidClientId(String? value) {
|
|
return value != null && value.isNotEmpty && value.runes.length <= 64;
|
|
}
|
|
|
|
bool _isValidSlotId(String? value) {
|
|
if (value == null || value.isEmpty || value.length > 64) {
|
|
return false;
|
|
}
|
|
return isValidReadStateDTag('$readStateDTagPrefix$value');
|
|
}
|