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,201 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../shared/relay/relay.dart';
|
||||
import '../channels/channel_management_provider.dart';
|
||||
import '../profile/user_cache_provider.dart';
|
||||
import '../profile/user_profile.dart';
|
||||
import 'channel_messages_provider.dart';
|
||||
|
||||
/// Sends messages by signing an event with the user's nsec and publishing it
|
||||
/// over the relay's NIP-42-authenticated WebSocket session.
|
||||
class SendMessage {
|
||||
final SignedEventRelay _signedEventRelay;
|
||||
final Future<List<ChannelMember>> Function(String channelId) _fetchMembers;
|
||||
final Map<String, UserProfile> Function() _readUserCache;
|
||||
final void Function(String channelId, NostrEvent event) _addLocalMessage;
|
||||
final void Function(String channelId, String eventId) _completeLocalMessage;
|
||||
final void Function(String channelId, String eventId) _removeLocalMessage;
|
||||
final bool Function()? _isDeliveryValid;
|
||||
|
||||
SendMessage({
|
||||
required SignedEventRelay signedEventRelay,
|
||||
required Future<List<ChannelMember>> Function(String channelId)
|
||||
fetchMembers,
|
||||
required Map<String, UserProfile> Function() readUserCache,
|
||||
required void Function(String channelId, NostrEvent event) addLocalMessage,
|
||||
required void Function(String channelId, String eventId)
|
||||
completeLocalMessage,
|
||||
required void Function(String channelId, String eventId) removeLocalMessage,
|
||||
bool Function()? isDeliveryValid,
|
||||
}) : _signedEventRelay = signedEventRelay,
|
||||
_fetchMembers = fetchMembers,
|
||||
_readUserCache = readUserCache,
|
||||
_addLocalMessage = addLocalMessage,
|
||||
_completeLocalMessage = completeLocalMessage,
|
||||
_removeLocalMessage = removeLocalMessage,
|
||||
_isDeliveryValid = isDeliveryValid;
|
||||
|
||||
/// Send a text message to a channel.
|
||||
///
|
||||
/// For thread replies, pass [parentEventId] and optionally [rootEventId].
|
||||
/// If [rootEventId] is null it defaults to [parentEventId] (direct reply to
|
||||
/// thread head). Tags are built to match the desktop's `buildReplyTags`
|
||||
/// convention with `root` / `reply` markers. Pass [mediaTags] to append
|
||||
/// relay-validated `imeta` tags and NIP-30 `emoji` tags.
|
||||
Future<void> call({
|
||||
required String channelId,
|
||||
required String content,
|
||||
String? parentEventId,
|
||||
String? rootEventId,
|
||||
List<String>? mentionPubkeys,
|
||||
List<List<String>> mediaTags = const [],
|
||||
}) async {
|
||||
_ensureDeliveryValid();
|
||||
// Use explicitly passed pubkeys, or resolve @mentions against
|
||||
// channel members to avoid matching the wrong user.
|
||||
final resolvedMentions =
|
||||
mentionPubkeys ?? await _resolveMentions(content, channelId);
|
||||
final authorPubkey = _signedEventRelay.pubkey;
|
||||
|
||||
// Normalize mentions: lowercase, deduplicate, exclude self (matching
|
||||
// the desktop's normalizeMentionPubkeys).
|
||||
final selfLower = authorPubkey?.toLowerCase();
|
||||
final seenMentions = <String>{?selfLower};
|
||||
final normalizedMentions = <String>[
|
||||
for (final pk in resolvedMentions)
|
||||
if (seenMentions.add(pk.toLowerCase())) pk,
|
||||
];
|
||||
|
||||
final tags = <List<String>>[
|
||||
['h', channelId],
|
||||
if (parentEventId != null) ..._buildReplyTags(parentEventId, rootEventId),
|
||||
for (final pk in normalizedMentions) ['p', pk],
|
||||
...mediaTags,
|
||||
];
|
||||
|
||||
_ensureDeliveryValid();
|
||||
NostrEvent? localMessage;
|
||||
try {
|
||||
await _signedEventRelay.submit(
|
||||
kind: EventKind.streamMessage,
|
||||
content: content,
|
||||
tags: tags,
|
||||
onSigned: (event) {
|
||||
localMessage = event;
|
||||
_addLocalMessage(channelId, event);
|
||||
},
|
||||
);
|
||||
final event = localMessage;
|
||||
if (event != null) _completeLocalMessage(channelId, event.id);
|
||||
} catch (_) {
|
||||
final event = localMessage;
|
||||
if (event != null) _removeLocalMessage(channelId, event.id);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void _ensureDeliveryValid() {
|
||||
if (_isDeliveryValid?.call() == false) {
|
||||
throw StateError(
|
||||
'Message delivery cancelled because the active community changed',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve @mentions to pubkeys, scoped to channel members.
|
||||
///
|
||||
/// Fetches channel members from the relay and matches @names only
|
||||
/// against members of that channel. Falls back to the full user cache
|
||||
/// if the member fetch fails.
|
||||
Future<List<String>> _resolveMentions(
|
||||
String content,
|
||||
String channelId,
|
||||
) async {
|
||||
final mentionPattern = RegExp(r'@(\w+)');
|
||||
final matches = mentionPattern.allMatches(content);
|
||||
if (matches.isEmpty) return const [];
|
||||
|
||||
// Try to get channel member pubkeys for scoped resolution.
|
||||
Set<String>? memberPubkeys;
|
||||
try {
|
||||
final members = await _fetchMembers(channelId);
|
||||
memberPubkeys = {for (final m in members) m.pubkey.toLowerCase()};
|
||||
} catch (_) {
|
||||
// Non-fatal — fall through to unscoped cache lookup.
|
||||
}
|
||||
|
||||
final cache = _readUserCache();
|
||||
final pubkeys = <String>{};
|
||||
|
||||
for (final match in matches) {
|
||||
final name = match.group(1)?.toLowerCase();
|
||||
if (name == null || name.isEmpty) continue;
|
||||
|
||||
for (final profile in cache.values) {
|
||||
final displayName = profile.displayName?.toLowerCase();
|
||||
if (displayName == null) continue;
|
||||
|
||||
// Match against full display name or first word.
|
||||
final firstName = displayName.split(RegExp(r'\s+')).first;
|
||||
if (displayName != name && firstName != name) continue;
|
||||
|
||||
// If we have channel members, only match members of this channel.
|
||||
if (memberPubkeys != null &&
|
||||
!memberPubkeys.contains(profile.pubkey.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pubkeys.add(profile.pubkey);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return pubkeys.toList();
|
||||
}
|
||||
|
||||
/// Build `e`-tags for a thread reply, matching the desktop convention:
|
||||
/// - Direct reply to thread head: `["e", id, "", "reply"]`
|
||||
/// - Nested reply: `["e", rootId, "", "root"]` + `["e", parentId, "", "reply"]`
|
||||
static List<List<String>> _buildReplyTags(
|
||||
String parentEventId,
|
||||
String? rootEventId,
|
||||
) {
|
||||
final root = rootEventId ?? parentEventId;
|
||||
if (parentEventId == root) {
|
||||
return [
|
||||
['e', root, '', 'reply'],
|
||||
];
|
||||
}
|
||||
return [
|
||||
['e', root, '', 'root'],
|
||||
['e', parentEventId, '', 'reply'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
final sendMessageProvider = Provider<SendMessage>((ref) {
|
||||
final config = ref.watch(relayConfigProvider);
|
||||
return SendMessage(
|
||||
signedEventRelay: SignedEventRelay(
|
||||
session: ref.read(relaySessionProvider.notifier),
|
||||
nsec: config.nsec,
|
||||
),
|
||||
fetchMembers: (channelId) =>
|
||||
ref.read(channelMembersProvider(channelId).future),
|
||||
readUserCache: () => ref.read(userCacheProvider),
|
||||
addLocalMessage: (channelId, event) => ref
|
||||
.read(channelMessagesProvider(channelId).notifier)
|
||||
.addLocalMessage(event),
|
||||
completeLocalMessage: (channelId, eventId) => ref
|
||||
.read(channelMessagesProvider(channelId).notifier)
|
||||
.completeLocalMessage(eventId),
|
||||
removeLocalMessage: (channelId, eventId) => ref
|
||||
.read(channelMessagesProvider(channelId).notifier)
|
||||
.removeLocalMessage(eventId),
|
||||
isDeliveryValid: () {
|
||||
final currentConfig = ref.read(relayConfigProvider);
|
||||
return currentConfig.baseUrl == config.baseUrl &&
|
||||
currentConfig.nsec == config.nsec;
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user