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,221 @@
|
||||
/// Parsing for `buzz://` deep links.
|
||||
///
|
||||
/// Mirrors the desktop handler in `desktop/src-tauri/src/deep_link.rs`:
|
||||
/// `buzz://message?channel=<uuid>&id=<hex>[&thread=<hex>]` references a
|
||||
/// message (optionally inside a thread) in a channel. Required params that
|
||||
/// are missing or empty make the link invalid — the caller never sees a
|
||||
/// half-formed target.
|
||||
library;
|
||||
|
||||
import '../relay/relay_validation.dart';
|
||||
|
||||
/// A parsed deep link supported by the app.
|
||||
sealed class BuzzDeepLink {
|
||||
const BuzzDeepLink();
|
||||
}
|
||||
|
||||
/// A parsed relay invite link.
|
||||
///
|
||||
/// Canonical share links are `https://<relay>/invite/<code>`. The custom
|
||||
/// `buzz://join?relay=<ws(s)://relay>&code=<code>` form is only an installed-app
|
||||
/// handoff from the web landing page.
|
||||
class InviteDeepLink extends BuzzDeepLink {
|
||||
/// Relay URL normalized to the websocket scheme used by the app.
|
||||
final String relayUrl;
|
||||
|
||||
/// Invite code from the link.
|
||||
final String code;
|
||||
|
||||
/// Optional receipt proving acceptance of the relay's current join policy.
|
||||
final String? policyReceipt;
|
||||
|
||||
const InviteDeepLink({
|
||||
required this.relayUrl,
|
||||
required this.code,
|
||||
this.policyReceipt,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is InviteDeepLink &&
|
||||
other.relayUrl == relayUrl &&
|
||||
other.code == code &&
|
||||
other.policyReceipt == policyReceipt;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(relayUrl, code, policyReceipt);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'InviteDeepLink(relay: $relayUrl, code: $code, policyReceipt: $policyReceipt)';
|
||||
}
|
||||
|
||||
/// A parsed `buzz://message` deep link.
|
||||
class MessageDeepLink extends BuzzDeepLink {
|
||||
/// Channel UUID from the `channel` query param.
|
||||
final String channelId;
|
||||
|
||||
/// Event ID (hex) from the `id` query param.
|
||||
final String messageId;
|
||||
|
||||
/// Optional thread root event ID from the `thread` query param.
|
||||
final String? threadRootId;
|
||||
|
||||
const MessageDeepLink({
|
||||
required this.channelId,
|
||||
required this.messageId,
|
||||
this.threadRootId,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is MessageDeepLink &&
|
||||
other.channelId == channelId &&
|
||||
other.messageId == messageId &&
|
||||
other.threadRootId == threadRootId;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(channelId, messageId, threadRootId);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'MessageDeepLink(channel: $channelId, id: $messageId, '
|
||||
'thread: $threadRootId)';
|
||||
}
|
||||
|
||||
/// Build a canonical `buzz://message` link for a channel message.
|
||||
///
|
||||
/// Mirrors `desktop/src/features/messages/lib/messageLink.ts` so links copied
|
||||
/// or shared from mobile round-trip through every client's parser:
|
||||
/// `buzz://message?channel=<uuid>&id=<eventId>[&thread=<rootId>]`.
|
||||
///
|
||||
/// An empty [threadRootId] is treated as "no thread" so callers can pass
|
||||
/// through a nullable thread reference without extra checks.
|
||||
String buildMessageLink({
|
||||
required String channelId,
|
||||
required String messageId,
|
||||
String? threadRootId,
|
||||
}) {
|
||||
if (channelId.isEmpty) {
|
||||
throw ArgumentError('buildMessageLink: channelId is required');
|
||||
}
|
||||
if (messageId.isEmpty) {
|
||||
throw ArgumentError('buildMessageLink: messageId is required');
|
||||
}
|
||||
|
||||
final params = <String, String>{
|
||||
'channel': channelId,
|
||||
'id': messageId,
|
||||
if (threadRootId != null && threadRootId.isNotEmpty) 'thread': threadRootId,
|
||||
};
|
||||
return Uri(
|
||||
scheme: 'buzz',
|
||||
host: 'message',
|
||||
queryParameters: params,
|
||||
).toString();
|
||||
}
|
||||
|
||||
/// Parse a `buzz://message?…` URI into a [MessageDeepLink].
|
||||
///
|
||||
/// Returns `null` for non-`buzz` schemes, non-`message` hosts (e.g.
|
||||
/// `buzz://connect` which is desktop-only), or links missing a non-empty
|
||||
/// `channel` or `id` param.
|
||||
MessageDeepLink? parseMessageDeepLink(Uri uri) {
|
||||
if (uri.scheme != 'buzz' || uri.host != 'message') return null;
|
||||
|
||||
final channel = uri.queryParameters['channel'];
|
||||
final id = uri.queryParameters['id'];
|
||||
if (channel == null || channel.isEmpty || id == null || id.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final thread = uri.queryParameters['thread'];
|
||||
return MessageDeepLink(
|
||||
channelId: channel,
|
||||
messageId: id,
|
||||
threadRootId: (thread == null || thread.isEmpty) ? null : thread,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parse canonical HTTPS invite links and `buzz://join` app handoffs.
|
||||
///
|
||||
/// Accepted forms:
|
||||
/// - `https://<relay>/invite/<code>` -> `wss://<relay>` + code
|
||||
/// - `http://localhost/invite/<code>` -> `ws://localhost` + code in debug builds
|
||||
/// - `buzz://join?relay=<wss://relay>&code=<code>` -> relay + code
|
||||
/// - `buzz://join?relay=<ws://localhost>&code=<code>` -> local relay in debug
|
||||
///
|
||||
/// Rejects credentials, fragments, missing params, nested relay credentials, and
|
||||
/// non-invite paths so scanners do not accidentally treat arbitrary URLs as
|
||||
/// community admission links.
|
||||
InviteDeepLink? parseInviteDeepLink(Uri uri) {
|
||||
if (uri.hasFragment || uri.userInfo.isNotEmpty) return null;
|
||||
|
||||
if (uri.scheme == 'buzz') {
|
||||
if (uri.host != 'join') return null;
|
||||
final relay = uri.queryParameters['relay'];
|
||||
final code = uri.queryParameters['code'];
|
||||
if (relay == null || relay.isEmpty || code == null || code.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final relayUri = Uri.tryParse(relay);
|
||||
if (relayUri == null ||
|
||||
(relayUri.scheme != 'ws' && relayUri.scheme != 'wss') ||
|
||||
relayUri.host.isEmpty ||
|
||||
relayUri.userInfo.isNotEmpty ||
|
||||
relayUri.hasFragment) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
validateInviteRelayUri(relayUri);
|
||||
} on FormatException {
|
||||
return null;
|
||||
}
|
||||
final normalizedRelay = Uri(
|
||||
scheme: relayUri.scheme,
|
||||
host: relayUri.host,
|
||||
port: relayUri.hasPort ? relayUri.port : null,
|
||||
).toString();
|
||||
final policyReceipt = uri.queryParameters['policy_receipt'];
|
||||
return InviteDeepLink(
|
||||
relayUrl: normalizedRelay,
|
||||
code: code,
|
||||
policyReceipt: policyReceipt == null || policyReceipt.isEmpty
|
||||
? null
|
||||
: policyReceipt,
|
||||
);
|
||||
}
|
||||
|
||||
if (uri.scheme == 'https' || uri.scheme == 'http') {
|
||||
if (uri.host.isEmpty) return null;
|
||||
final segments = uri.pathSegments;
|
||||
if (segments.length != 2 ||
|
||||
segments[0] != 'invite' ||
|
||||
segments[1].isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final relayScheme = uri.scheme == 'https' ? 'wss' : 'ws';
|
||||
final relayUri = Uri(
|
||||
scheme: relayScheme,
|
||||
host: uri.host,
|
||||
port: uri.hasPort ? uri.port : null,
|
||||
);
|
||||
try {
|
||||
validateInviteRelayUri(relayUri);
|
||||
} on FormatException {
|
||||
return null;
|
||||
}
|
||||
final relay = Uri(
|
||||
scheme: relayScheme,
|
||||
host: uri.host,
|
||||
port: uri.hasPort ? uri.port : null,
|
||||
).toString();
|
||||
return InviteDeepLink(relayUrl: relay, code: segments[1]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Parse any supported Buzz deep link.
|
||||
BuzzDeepLink? parseBuzzDeepLink(Uri uri) =>
|
||||
parseInviteDeepLink(uri) ?? parseMessageDeepLink(uri);
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import 'deep_link.dart';
|
||||
|
||||
/// Holds the most recent supported deep link that has not been
|
||||
/// dispatched yet.
|
||||
///
|
||||
/// Listens to [AppLinks.uriLinkStream], which delivers both the cold-start
|
||||
/// link (the URL that launched the app) and links received while running.
|
||||
/// Navigation cannot always happen the moment a link arrives — the user may
|
||||
/// not be authenticated yet, or channels may still be loading — so the parsed
|
||||
/// link is parked here and consumed by the dispatcher once the app is ready.
|
||||
class PendingDeepLinkNotifier extends Notifier<BuzzDeepLink?> {
|
||||
@visibleForTesting
|
||||
static Stream<Uri>? debugUriStreamOverride;
|
||||
|
||||
StreamSubscription<Uri>? _subscription;
|
||||
|
||||
@override
|
||||
BuzzDeepLink? build() {
|
||||
final stream = debugUriStreamOverride ?? AppLinks().uriLinkStream;
|
||||
_subscription = stream.listen(handleUri);
|
||||
ref.onDispose(() {
|
||||
_subscription?.cancel();
|
||||
_subscription = null;
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Parse and park an incoming URI. Unsupported links are ignored loudly.
|
||||
@visibleForTesting
|
||||
void handleUri(Uri uri) {
|
||||
final link = parseBuzzDeepLink(uri);
|
||||
if (link == null) {
|
||||
debugPrint('deep-link: ignoring unsupported link: $uri');
|
||||
return;
|
||||
}
|
||||
state = link;
|
||||
}
|
||||
|
||||
/// Clear the pending link after it has been dispatched (or dropped).
|
||||
void consume() => state = null;
|
||||
}
|
||||
|
||||
final pendingDeepLinkProvider =
|
||||
NotifierProvider<PendingDeepLinkNotifier, BuzzDeepLink?>(
|
||||
PendingDeepLinkNotifier.new,
|
||||
);
|
||||
Reference in New Issue
Block a user