mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Limit direct payload hops
This commit is contained in:
@@ -5,22 +5,22 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000228">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000191">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.45931">
|
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.404227">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: build_app" time="102.765046">
|
<testcase classname="fastlane.lanes" name="2: build_app" time="99.61383">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="41.615863">
|
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="229.815554">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import '../utils/image_message_parser.dart';
|
|||||||
|
|
||||||
/// Main App Provider - coordinates all other providers
|
/// Main App Provider - coordinates all other providers
|
||||||
class AppProvider with ChangeNotifier {
|
class AppProvider with ChangeNotifier {
|
||||||
|
static const int _maxDirectPayloadHops = 3;
|
||||||
final ConnectionProvider connectionProvider;
|
final ConnectionProvider connectionProvider;
|
||||||
final ContactsProvider contactsProvider;
|
final ContactsProvider contactsProvider;
|
||||||
final MessagesProvider messagesProvider;
|
final MessagesProvider messagesProvider;
|
||||||
@@ -565,6 +566,22 @@ class AppProvider with ChangeNotifier {
|
|||||||
debugPrint(
|
debugPrint(
|
||||||
'⚠️ [AppProvider] Voice fetch requester contact not found',
|
'⚠️ [AppProvider] Voice fetch requester contact not found',
|
||||||
);
|
);
|
||||||
|
messagesProvider.logSystemMessage(
|
||||||
|
text:
|
||||||
|
'Cannot fetch voice: requester contact is unknown. Add/sync contacts first.',
|
||||||
|
level: 'warning',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (requester.outPathLen > _maxDirectPayloadHops) {
|
||||||
|
debugPrint(
|
||||||
|
'⚠️ [AppProvider] Voice fetch requester too far: ${requester.outPathLen} hops',
|
||||||
|
);
|
||||||
|
messagesProvider.logSystemMessage(
|
||||||
|
text:
|
||||||
|
'Cannot fetch voice for ${requester.advName}: message is too far (${requester.outPathLen} hops, max $_maxDirectPayloadHops).',
|
||||||
|
level: 'warning',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
unawaited(
|
unawaited(
|
||||||
@@ -665,6 +682,17 @@ class AppProvider with ChangeNotifier {
|
|||||||
senderPrefix,
|
senderPrefix,
|
||||||
);
|
);
|
||||||
if (requester != null) {
|
if (requester != null) {
|
||||||
|
if (requester.outPathLen > _maxDirectPayloadHops) {
|
||||||
|
debugPrint(
|
||||||
|
'⚠️ [AppProvider] Image fetch requester too far: ${requester.outPathLen} hops',
|
||||||
|
);
|
||||||
|
messagesProvider.logSystemMessage(
|
||||||
|
text:
|
||||||
|
'Cannot fetch image for ${requester.advName}: message is too far (${requester.outPathLen} hops, max $_maxDirectPayloadHops).',
|
||||||
|
level: 'warning',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
unawaited(
|
unawaited(
|
||||||
imageProvider.serveSessionTo(
|
imageProvider.serveSessionTo(
|
||||||
sessionId: imageFetchRequest.sessionId,
|
sessionId: imageFetchRequest.sessionId,
|
||||||
@@ -674,6 +702,15 @@ class AppProvider with ChangeNotifier {
|
|||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
debugPrint(
|
||||||
|
'⚠️ [AppProvider] Image fetch requester contact not found',
|
||||||
|
);
|
||||||
|
messagesProvider.logSystemMessage(
|
||||||
|
text:
|
||||||
|
'Cannot fetch image: requester contact is unknown. Add/sync contacts first.',
|
||||||
|
level: 'warning',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class ImageSession {
|
|||||||
class ImageProvider with ChangeNotifier {
|
class ImageProvider with ChangeNotifier {
|
||||||
static const String _storageKey = 'stored_image_sessions_v1';
|
static const String _storageKey = 'stored_image_sessions_v1';
|
||||||
static const Duration _outgoingTtl = Duration(minutes: 15);
|
static const Duration _outgoingTtl = Duration(minutes: 15);
|
||||||
|
static const int maxDirectPayloadHops = 3;
|
||||||
|
|
||||||
/// Incoming sessions keyed by sessionId.
|
/// Incoming sessions keyed by sessionId.
|
||||||
final Map<String, ImageSession> _sessions = {};
|
final Map<String, ImageSession> _sessions = {};
|
||||||
@@ -190,6 +191,12 @@ class ImageProvider with ChangeNotifier {
|
|||||||
debugPrint('⚠️ [ImageProvider] ${requester.advName} has no direct path');
|
debugPrint('⚠️ [ImageProvider] ${requester.advName} has no direct path');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (requester.outPathLen > maxDirectPayloadHops) {
|
||||||
|
debugPrint(
|
||||||
|
'⚠️ [ImageProvider] ${requester.advName} is too far: ${requester.outPathLen} hops (max $maxDirectPayloadHops)',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
for (final fragment in cached.fragments) {
|
for (final fragment in cached.fragments) {
|
||||||
if (requestedIndices != null &&
|
if (requestedIndices != null &&
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class VoiceSession {
|
|||||||
/// Manages incoming voice packet sessions and coordinates playback.
|
/// Manages incoming voice packet sessions and coordinates playback.
|
||||||
class VoiceProvider with ChangeNotifier {
|
class VoiceProvider with ChangeNotifier {
|
||||||
static const String _voiceSessionsStorageKey = 'stored_voice_sessions_v1';
|
static const String _voiceSessionsStorageKey = 'stored_voice_sessions_v1';
|
||||||
|
static const int maxDirectPayloadHops = 3;
|
||||||
final VoiceCodecService _codec;
|
final VoiceCodecService _codec;
|
||||||
final VoicePlayerService _player;
|
final VoicePlayerService _player;
|
||||||
late final StreamSubscription<void> _playerEventsSub;
|
late final StreamSubscription<void> _playerEventsSub;
|
||||||
@@ -161,6 +162,12 @@ class VoiceProvider with ChangeNotifier {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (requester.outPathLen > maxDirectPayloadHops) {
|
||||||
|
debugPrint(
|
||||||
|
'⚠️ [VoiceProvider] Requester ${requester.advName} is too far: ${requester.outPathLen} hops (max $maxDirectPayloadHops)',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
for (final packet in cached.packets) {
|
for (final packet in cached.packets) {
|
||||||
if (requestedIndices != null &&
|
if (requestedIndices != null &&
|
||||||
|
|||||||
@@ -434,6 +434,8 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
ImageSource source = ImageSource.gallery,
|
ImageSource source = ImageSource.gallery,
|
||||||
}) async {
|
}) async {
|
||||||
if (_isSendingImage) return;
|
if (_isSendingImage) return;
|
||||||
|
final shouldContinue = await _confirmPublicChannelMediaSend('image');
|
||||||
|
if (!shouldContinue) return;
|
||||||
final connectionProvider = context.read<ConnectionProvider>();
|
final connectionProvider = context.read<ConnectionProvider>();
|
||||||
if (!connectionProvider.deviceInfo.isConnected) {
|
if (!connectionProvider.deviceInfo.isConnected) {
|
||||||
ToastLogger.error(context, 'Not connected to device');
|
ToastLogger.error(context, 'Not connected to device');
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class ImageMessageBubble extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ImageMessageBubbleState extends State<ImageMessageBubble> {
|
class _ImageMessageBubbleState extends State<ImageMessageBubble> {
|
||||||
|
static const int _maxFetchHops = 3;
|
||||||
bool _isRequesting = false;
|
bool _isRequesting = false;
|
||||||
String? _errorText;
|
String? _errorText;
|
||||||
Timer? _requestTimeoutTimer;
|
Timer? _requestTimeoutTimer;
|
||||||
@@ -208,15 +209,41 @@ class _ImageMessageBubbleState extends State<ImageMessageBubble> {
|
|||||||
sender = _resolveSender(envelope);
|
sender = _resolveSender(envelope);
|
||||||
}
|
}
|
||||||
if (sender == null) {
|
if (sender == null) {
|
||||||
setState(() => _errorText = 'Sender not reachable');
|
await _showBlockingAlert(
|
||||||
|
'Cannot fetch image',
|
||||||
|
'Sender contact is unknown. Sync contacts first.',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (sender.outPathLen < 0) {
|
||||||
|
await _showBlockingAlert(
|
||||||
|
'Cannot fetch image',
|
||||||
|
'Sender route is unknown. Sync contacts/path first.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sender.outPathLen > _maxFetchHops) {
|
||||||
|
await _showBlockingAlert(
|
||||||
|
'Cannot fetch image',
|
||||||
|
'Message is too far (${sender.outPathLen} hops, max $_maxFetchHops).',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sender.outPathLen >= 2) {
|
||||||
|
_showToast(
|
||||||
|
'Image fetch over ${sender.outPathLen} hops may take a while.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _errorText = null);
|
||||||
final conn = context.read<ConnectionProvider>();
|
final conn = context.read<ConnectionProvider>();
|
||||||
final imageProvider = context.read<ip.ImageProvider>();
|
final imageProvider = context.read<ip.ImageProvider>();
|
||||||
final deviceKey = conn.deviceInfo.publicKey;
|
final deviceKey = conn.deviceInfo.publicKey;
|
||||||
if (deviceKey == null || deviceKey.length < 6) {
|
if (deviceKey == null || deviceKey.length < 6) {
|
||||||
setState(() => _errorText = 'Device key unavailable');
|
await _showBlockingAlert(
|
||||||
|
'Cannot fetch image',
|
||||||
|
'Device key is unavailable.',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,8 +254,8 @@ class _ImageMessageBubbleState extends State<ImageMessageBubble> {
|
|||||||
|
|
||||||
// If we already have some fragments, request only what's missing.
|
// If we already have some fragments, request only what's missing.
|
||||||
final missing = imageProvider.missingFragmentIndices(envelope.sessionId);
|
final missing = imageProvider.missingFragmentIndices(envelope.sessionId);
|
||||||
final isPartialResume = missing.isNotEmpty &&
|
final isPartialResume =
|
||||||
missing.length < envelope.total;
|
missing.isNotEmpty && missing.length < envelope.total;
|
||||||
final request = isPartialResume
|
final request = isPartialResume
|
||||||
? ImageFetchRequest(
|
? ImageFetchRequest(
|
||||||
sessionId: envelope.sessionId,
|
sessionId: envelope.sessionId,
|
||||||
@@ -276,7 +303,9 @@ class _ImageMessageBubbleState extends State<ImageMessageBubble> {
|
|||||||
_requestTimeoutTimer = TransferTimeout.start(
|
_requestTimeoutTimer = TransferTimeout.start(
|
||||||
txEstimate: txEstimate,
|
txEstimate: txEstimate,
|
||||||
onTimeout: () {
|
onTimeout: () {
|
||||||
if (mounted && _isRequesting && !imageProvider.isComplete(envelope.sessionId)) {
|
if (mounted &&
|
||||||
|
_isRequesting &&
|
||||||
|
!imageProvider.isComplete(envelope.sessionId)) {
|
||||||
setState(() => _isRequesting = false);
|
setState(() => _isRequesting = false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -309,6 +338,30 @@ class _ImageMessageBubbleState extends State<ImageMessageBubble> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showToast(String message) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(message), duration: const Duration(seconds: 3)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showBlockingAlert(String title, String message) async {
|
||||||
|
if (!mounted) return;
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: Text(message),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
|
child: const Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static String _statusText({
|
static String _statusText({
|
||||||
required bool isComplete,
|
required bool isComplete,
|
||||||
required bool isRequesting,
|
required bool isRequesting,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class VoiceMessageBubble extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
|
class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
|
||||||
|
static const int _maxFetchHops = 3;
|
||||||
bool _isRequesting = false;
|
bool _isRequesting = false;
|
||||||
bool _autoPlayWhenReady = false;
|
bool _autoPlayWhenReady = false;
|
||||||
String? _errorText;
|
String? _errorText;
|
||||||
@@ -207,14 +208,44 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
|
|||||||
sender = _resolveSenderContact();
|
sender = _resolveSenderContact();
|
||||||
}
|
}
|
||||||
if (sender == null) {
|
if (sender == null) {
|
||||||
_setUnavailable();
|
await _showBlockingAlert(
|
||||||
|
'Cannot fetch voice',
|
||||||
|
'Sender contact is unknown. Sync contacts first.',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (sender.outPathLen < 0) {
|
||||||
|
await _showBlockingAlert(
|
||||||
|
'Cannot fetch voice',
|
||||||
|
'Sender route is unknown. Sync contacts/path first.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sender.outPathLen > _maxFetchHops) {
|
||||||
|
await _showBlockingAlert(
|
||||||
|
'Cannot fetch voice',
|
||||||
|
'Message is too far (${sender.outPathLen} hops, max $_maxFetchHops).',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sender.outPathLen >= 2) {
|
||||||
|
_showToast(
|
||||||
|
'Voice fetch over ${sender.outPathLen} hops may take a while.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_errorText = null;
|
||||||
|
});
|
||||||
|
|
||||||
final connectionProvider = context.read<ConnectionProvider>();
|
final connectionProvider = context.read<ConnectionProvider>();
|
||||||
final deviceKey = connectionProvider.deviceInfo.publicKey;
|
final deviceKey = connectionProvider.deviceInfo.publicKey;
|
||||||
if (deviceKey == null || deviceKey.length < 6) {
|
if (deviceKey == null || deviceKey.length < 6) {
|
||||||
_setUnavailable();
|
await _showBlockingAlert(
|
||||||
|
'Cannot fetch voice',
|
||||||
|
'Device key is unavailable.',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,6 +338,30 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showToast(String message) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(message), duration: const Duration(seconds: 3)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showBlockingAlert(String title, String message) async {
|
||||||
|
if (!mounted) return;
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: Text(message),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
|
child: const Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static String _formatDuration(double seconds) {
|
static String _formatDuration(double seconds) {
|
||||||
final s = seconds.round();
|
final s = seconds.round();
|
||||||
if (s < 60) return '${s}s';
|
if (s < 60) return '${s}s';
|
||||||
|
|||||||
Reference in New Issue
Block a user