feat: import Chinese-localized Buzz source snapshot
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
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>
This commit is contained in:
@@ -0,0 +1,407 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Nostr event kind constants.
|
||||
///
|
||||
/// Keep in sync with `desktop/src/shared/constants/kinds.ts`.
|
||||
abstract final class EventKind {
|
||||
static const note = 1;
|
||||
static const contactList = 3;
|
||||
static const deletion = 5;
|
||||
static const reaction = 7;
|
||||
static const streamMessage = 9;
|
||||
static const nip29DeleteEvent = 9005;
|
||||
static const presenceUpdate = 20001;
|
||||
static const typingIndicator = 20002;
|
||||
static const auth = 22242;
|
||||
static const agentObserverFrame = 24200;
|
||||
static const huddleReaction = 24810;
|
||||
static const readState = 30078;
|
||||
static const eventReminder = 30300;
|
||||
static const userStatus = 30315;
|
||||
static const dmVisibility = 30622;
|
||||
static const streamMessageV2 = 40002;
|
||||
static const channelThreadSummary = 39005;
|
||||
static const channelWindowBounds = 39006;
|
||||
static const streamMessageEdit = 40003;
|
||||
static const streamMessageDiff = 40008;
|
||||
static const systemMessage = 40099;
|
||||
static const jobRequest = 43001;
|
||||
static const jobAccepted = 43002;
|
||||
static const jobProgress = 43003;
|
||||
static const jobResult = 43004;
|
||||
static const jobCancel = 43005;
|
||||
static const jobError = 43006;
|
||||
static const forumPost = 45001;
|
||||
static const forumComment = 45003;
|
||||
static const huddleStarted = 48100;
|
||||
static const huddleParticipantJoined = 48101;
|
||||
static const huddleParticipantLeft = 48102;
|
||||
static const huddleEnded = 48103;
|
||||
|
||||
/// Event kinds that represent user-visible channel messages.
|
||||
static const channelMessageEventKinds = [
|
||||
streamMessage, // 9
|
||||
streamMessageV2, // 40002
|
||||
forumPost, // 45001
|
||||
forumComment, // 45003
|
||||
];
|
||||
|
||||
/// Event kinds that represent channel activity (messages, edits, reactions,
|
||||
/// deletions, system events). Matches the desktop's `CHANNEL_EVENT_KINDS`.
|
||||
static const channelEventKinds = [
|
||||
deletion, // 5
|
||||
reaction, // 7
|
||||
nip29DeleteEvent, // 9005 — Buzz-native deletion
|
||||
...channelMessageEventKinds,
|
||||
40001, // legacy pre-migration stream messages
|
||||
streamMessageEdit, // 40003
|
||||
streamMessageDiff, // 40008
|
||||
systemMessage, // 40099
|
||||
huddleStarted, // 48100 — visible huddle session row
|
||||
huddleParticipantJoined, // 48101 — huddle lifecycle metadata
|
||||
huddleParticipantLeft, // 48102 — huddle lifecycle metadata
|
||||
huddleEnded, // 48103 — visible huddle ended row
|
||||
];
|
||||
|
||||
/// Auxiliary timeline kinds that overlay or hide existing rows.
|
||||
static const channelAuxEventKinds = [
|
||||
deletion,
|
||||
reaction,
|
||||
nip29DeleteEvent,
|
||||
streamMessageEdit,
|
||||
];
|
||||
|
||||
/// Visible content kinds requested by the NIP-CW channel-window path.
|
||||
static const channelTimelineContentKinds = [
|
||||
streamMessage,
|
||||
streamMessageV2,
|
||||
streamMessageDiff,
|
||||
systemMessage,
|
||||
jobRequest,
|
||||
jobAccepted,
|
||||
jobProgress,
|
||||
jobResult,
|
||||
jobCancel,
|
||||
jobError,
|
||||
huddleStarted,
|
||||
];
|
||||
}
|
||||
|
||||
/// A Nostr event as defined by NIP-01.
|
||||
@immutable
|
||||
class NostrEvent {
|
||||
final String id;
|
||||
final String pubkey;
|
||||
final int createdAt;
|
||||
final int kind;
|
||||
final List<List<String>> tags;
|
||||
final String content;
|
||||
final String sig;
|
||||
|
||||
const NostrEvent({
|
||||
required this.id,
|
||||
required this.pubkey,
|
||||
required this.createdAt,
|
||||
required this.kind,
|
||||
required this.tags,
|
||||
required this.content,
|
||||
required this.sig,
|
||||
});
|
||||
|
||||
factory NostrEvent.fromJson(Map<String, dynamic> json) {
|
||||
return NostrEvent(
|
||||
id: json['id'] as String,
|
||||
pubkey: json['pubkey'] as String,
|
||||
createdAt: json['created_at'] as int,
|
||||
kind: json['kind'] as int,
|
||||
tags: (json['tags'] as List<dynamic>)
|
||||
.map((t) => (t as List<dynamic>).map((e) => e as String).toList())
|
||||
.toList(),
|
||||
content: json['content'] as String,
|
||||
sig: json['sig'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'pubkey': pubkey,
|
||||
'created_at': createdAt,
|
||||
'kind': kind,
|
||||
'tags': tags,
|
||||
'content': content,
|
||||
'sig': sig,
|
||||
};
|
||||
|
||||
/// Get the first value for a given tag key.
|
||||
String? getTagValue(String key) {
|
||||
for (final tag in tags) {
|
||||
if (tag.isNotEmpty && tag[0] == key && tag.length > 1) {
|
||||
return tag[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// The channel/group ID from the `h` tag (NIP-29).
|
||||
String? get channelId => getTagValue('h');
|
||||
|
||||
/// Extract thread parent and root IDs from `e` tags.
|
||||
///
|
||||
/// Matches the desktop's `getThreadReference` logic:
|
||||
/// - Tags with marker `"reply"` identify the direct parent.
|
||||
/// - Tags with marker `"root"` identify the thread root.
|
||||
/// - If no markers are present, falls back to null (top-level message).
|
||||
({String? parentId, String? rootId}) get threadReference {
|
||||
final eTags = [
|
||||
for (final tag in tags)
|
||||
if (tag.length >= 2 && tag[0] == 'e') tag,
|
||||
];
|
||||
|
||||
if (eTags.isEmpty) return (parentId: null, rootId: null);
|
||||
|
||||
// Find tagged root and reply markers (desktop convention).
|
||||
List<String>? rootTag;
|
||||
List<String>? replyTag;
|
||||
for (final tag in eTags) {
|
||||
if (tag.length >= 4) {
|
||||
if (tag[3] == 'root') rootTag = tag;
|
||||
if (tag[3] == 'reply') replyTag = tag;
|
||||
}
|
||||
}
|
||||
|
||||
if (replyTag == null) return (parentId: null, rootId: null);
|
||||
|
||||
final parentId = replyTag[1];
|
||||
final rootId = rootTag?[1] ?? parentId;
|
||||
return (parentId: parentId, rootId: rootId);
|
||||
}
|
||||
|
||||
/// The parent event ID from the `e` tag.
|
||||
String? get parentEventId => threadReference.parentId;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) || other is NostrEvent && id == other.id;
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
}
|
||||
|
||||
/// A NIP-01 subscription filter.
|
||||
@immutable
|
||||
class NostrFilter {
|
||||
final List<int> kinds;
|
||||
final List<String>? authors;
|
||||
|
||||
/// Specific event IDs (NIP-01 single-event lookup).
|
||||
final List<String>? ids;
|
||||
final int limit;
|
||||
final int? since;
|
||||
final int? until;
|
||||
|
||||
/// NIP-50 full-text search query.
|
||||
final String? search;
|
||||
|
||||
/// Tag filters, e.g. `{'#h': ['channel-id']}`.
|
||||
final Map<String, List<String>> tags;
|
||||
|
||||
/// Buzz relay bridge filter extensions (for example NIP-CW `top_level`).
|
||||
final Map<String, Object?> extensions;
|
||||
|
||||
const NostrFilter({
|
||||
required this.kinds,
|
||||
this.authors,
|
||||
this.ids,
|
||||
this.limit = 100,
|
||||
this.since,
|
||||
this.until,
|
||||
this.search,
|
||||
this.tags = const {},
|
||||
this.extensions = const {},
|
||||
});
|
||||
|
||||
/// Return a copy with an updated `since` value.
|
||||
NostrFilter copyWithSince(int since) => NostrFilter(
|
||||
kinds: kinds,
|
||||
authors: authors,
|
||||
ids: ids,
|
||||
limit: limit,
|
||||
since: since,
|
||||
until: until,
|
||||
search: search,
|
||||
tags: tags,
|
||||
extensions: extensions,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{'kinds': kinds, 'limit': limit};
|
||||
if (authors != null) json['authors'] = authors;
|
||||
if (ids != null) json['ids'] = ids;
|
||||
if (since != null) json['since'] = since;
|
||||
if (until != null) json['until'] = until;
|
||||
if (search != null) json['search'] = search;
|
||||
for (final entry in tags.entries) {
|
||||
json[entry.key] = entry.value;
|
||||
}
|
||||
for (final entry in extensions.entries) {
|
||||
json[entry.key] = entry.value;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed kind:0 user profile metadata.
|
||||
@immutable
|
||||
class ProfileData {
|
||||
final String pubkey;
|
||||
final String? displayName;
|
||||
final String? avatarUrl;
|
||||
final String? about;
|
||||
final String? nip05;
|
||||
|
||||
const ProfileData({
|
||||
required this.pubkey,
|
||||
this.displayName,
|
||||
this.avatarUrl,
|
||||
this.about,
|
||||
this.nip05,
|
||||
});
|
||||
|
||||
factory ProfileData.fromEvent(NostrEvent event) {
|
||||
Map<String, dynamic> meta = {};
|
||||
try {
|
||||
final decoded = jsonDecode(event.content);
|
||||
if (decoded is Map<String, dynamic>) meta = decoded;
|
||||
} catch (_) {}
|
||||
return ProfileData(
|
||||
pubkey: event.pubkey,
|
||||
displayName:
|
||||
(meta['display_name'] as String?) ?? (meta['name'] as String?),
|
||||
avatarUrl: meta['picture'] as String?,
|
||||
about: meta['about'] as String?,
|
||||
nip05: meta['nip05'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed kind:39000 channel metadata.
|
||||
@immutable
|
||||
class ChannelData {
|
||||
final String id;
|
||||
final String name;
|
||||
final String channelType;
|
||||
final String visibility;
|
||||
final String description;
|
||||
final String? topic;
|
||||
final List<String> participantPubkeys;
|
||||
final int? ttlSeconds;
|
||||
final DateTime? ttlDeadline;
|
||||
final bool isArchived;
|
||||
|
||||
const ChannelData({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.channelType,
|
||||
required this.visibility,
|
||||
required this.description,
|
||||
this.topic,
|
||||
this.participantPubkeys = const [],
|
||||
this.ttlSeconds,
|
||||
this.ttlDeadline,
|
||||
this.isArchived = false,
|
||||
});
|
||||
|
||||
factory ChannelData.fromEvent(NostrEvent event) {
|
||||
final id = event.getTagValue('d') ?? '';
|
||||
final name = event.getTagValue('name') ?? '';
|
||||
// Prefer explicit ["t", type]; fall back to ["hidden"] => dm, else "stream".
|
||||
// The fallback exists for relays that haven't been upgraded to emit the
|
||||
// explicit type tag yet.
|
||||
final explicitType = event.getTagValue('t');
|
||||
final hasHidden = event.tags.any((t) => t.isNotEmpty && t[0] == 'hidden');
|
||||
final channelType = explicitType ?? (hasHidden ? 'dm' : 'stream');
|
||||
// Prefer explicit ["public"]; fall back to NIP-29 absence-of-"private".
|
||||
final hasPublic = event.tags.any((t) => t.isNotEmpty && t[0] == 'public');
|
||||
final hasPrivate = event.tags.any((t) => t.isNotEmpty && t[0] == 'private');
|
||||
final visibility = hasPublic
|
||||
? 'open'
|
||||
: hasPrivate
|
||||
? 'private'
|
||||
: 'open';
|
||||
final description = event.getTagValue('about') ?? '';
|
||||
final topic = event.getTagValue('topic');
|
||||
final participants = [
|
||||
for (final t in event.tags)
|
||||
if (t.length >= 2 && t[0] == 'p') t[1],
|
||||
];
|
||||
final ttlRaw = event.getTagValue('ttl');
|
||||
final ttlSeconds = ttlRaw != null ? int.tryParse(ttlRaw) : null;
|
||||
final ttlDeadlineRaw = event.getTagValue('ttl_deadline');
|
||||
final ttlDeadline = ttlDeadlineRaw != null
|
||||
? DateTime.tryParse(ttlDeadlineRaw)
|
||||
: null;
|
||||
// Relay republishes kind:39000 with `["archived", "true"]` when a channel
|
||||
// is archived (including the auto-archive emitted by the TTL reaper). The
|
||||
// tag value "false" is also accepted server-side, so only treat "true" as
|
||||
// archived — anything else (missing tag, "false", unexpected value) means
|
||||
// active.
|
||||
final isArchived = event.getTagValue('archived') == 'true';
|
||||
return ChannelData(
|
||||
id: id,
|
||||
name: name,
|
||||
channelType: channelType,
|
||||
visibility: visibility,
|
||||
description: description,
|
||||
topic: topic,
|
||||
participantPubkeys: participants,
|
||||
ttlSeconds: ttlSeconds,
|
||||
ttlDeadline: ttlDeadline,
|
||||
isArchived: isArchived,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A single member entry parsed from a kind:39002 members event.
|
||||
@immutable
|
||||
class MemberEntry {
|
||||
final String pubkey;
|
||||
final String role;
|
||||
|
||||
const MemberEntry({required this.pubkey, required this.role});
|
||||
}
|
||||
|
||||
/// Parse a kind:39002 members event into the list of `(pubkey, role)` entries.
|
||||
///
|
||||
/// NIP-29 members tags follow the shape `["p", <pubkey>, <relay>, <role>]`.
|
||||
List<MemberEntry> membersFromEvent(NostrEvent event) {
|
||||
return [
|
||||
for (final t in event.tags)
|
||||
if (t.length >= 2 && t[0] == 'p')
|
||||
MemberEntry(pubkey: t[1], role: t.length >= 4 ? t[3] : 'member'),
|
||||
];
|
||||
}
|
||||
|
||||
/// Parse a Buzz command response from the relay's OK message content.
|
||||
///
|
||||
/// Command kinds (e.g. 41010, 30620, 46020) return `"response:{...}"` in the
|
||||
/// OK message. Returns `null` if the message is not a command response or the
|
||||
/// JSON is invalid.
|
||||
Map<String, dynamic>? parseCommandResponse(String message) {
|
||||
// Try the spec format first: "response:{...}".
|
||||
const prefix = 'response:';
|
||||
if (message.startsWith(prefix)) {
|
||||
try {
|
||||
final decoded = jsonDecode(message.substring(prefix.length));
|
||||
if (decoded is Map<String, dynamic>) return decoded;
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
// Fallback: raw JSON object (older relays, backward compat).
|
||||
try {
|
||||
final decoded = jsonDecode(message);
|
||||
if (decoded is Map<String, dynamic>) return decoded;
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user