mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Fix duplicate self messages
This commit is contained in:
@@ -292,6 +292,16 @@ class AppProvider with ChangeNotifier {
|
|||||||
String? _resolveContactNameForNotification(Uint8List? publicKey) {
|
String? _resolveContactNameForNotification(Uint8List? publicKey) {
|
||||||
if (publicKey == null || publicKey.isEmpty) return null;
|
if (publicKey == null || publicKey.isEmpty) return null;
|
||||||
|
|
||||||
|
final ownPublicKey = connectionProvider.deviceInfo.publicKey;
|
||||||
|
final ownName =
|
||||||
|
connectionProvider.deviceInfo.deviceName ??
|
||||||
|
connectionProvider.deviceInfo.selfName;
|
||||||
|
if (_matchesPublicKeyPrefix(publicKey, ownPublicKey) &&
|
||||||
|
ownName != null &&
|
||||||
|
ownName.trim().isNotEmpty) {
|
||||||
|
return ownName;
|
||||||
|
}
|
||||||
|
|
||||||
Contact? contact;
|
Contact? contact;
|
||||||
if (publicKey.length >= 32) {
|
if (publicKey.length >= 32) {
|
||||||
contact = contactsProvider.findContactByKey(publicKey);
|
contact = contactsProvider.findContactByKey(publicKey);
|
||||||
@@ -304,6 +314,60 @@ class AppProvider with ChangeNotifier {
|
|||||||
return contact?.advName;
|
return contact?.advName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool shouldIgnoreSelfReplay({
|
||||||
|
required Message message,
|
||||||
|
required Uint8List? ownPublicKey,
|
||||||
|
required String? ownName,
|
||||||
|
}) {
|
||||||
|
if ((!message.isContactMessage && !message.isChannelMessage) ||
|
||||||
|
message.pathLen > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_matchesPublicKeyPrefix(message.senderPublicKeyPrefix, ownPublicKey)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final trimmedSenderName = message.senderName?.trim();
|
||||||
|
final trimmedOwnName = ownName?.trim();
|
||||||
|
if (trimmedSenderName == null ||
|
||||||
|
trimmedSenderName.isEmpty ||
|
||||||
|
trimmedOwnName == null ||
|
||||||
|
trimmedOwnName.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmedSenderName == trimmedOwnName;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool _matchesPublicKeyPrefix(
|
||||||
|
Uint8List? candidateKey,
|
||||||
|
Uint8List? ownPublicKey,
|
||||||
|
) {
|
||||||
|
if (candidateKey == null ||
|
||||||
|
candidateKey.isEmpty ||
|
||||||
|
ownPublicKey == null ||
|
||||||
|
ownPublicKey.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final compareLength = candidateKey.length >= 6 && ownPublicKey.length >= 6
|
||||||
|
? 6
|
||||||
|
: (candidateKey.length < ownPublicKey.length
|
||||||
|
? candidateKey.length
|
||||||
|
: ownPublicKey.length);
|
||||||
|
if (compareLength <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var index = 0; index < compareLength; index++) {
|
||||||
|
if (candidateKey[index] != ownPublicKey[index]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// Load map enabled setting from shared preferences
|
/// Load map enabled setting from shared preferences
|
||||||
Future<void> _loadMapEnabled() async {
|
Future<void> _loadMapEnabled() async {
|
||||||
try {
|
try {
|
||||||
@@ -850,6 +914,17 @@ class AppProvider with ChangeNotifier {
|
|||||||
|
|
||||||
// When a message is received
|
// When a message is received
|
||||||
connectionProvider.onMessageReceived = (message) {
|
connectionProvider.onMessageReceived = (message) {
|
||||||
|
if (AppProvider.shouldIgnoreSelfReplay(
|
||||||
|
message: message,
|
||||||
|
ownPublicKey: connectionProvider.deviceInfo.publicKey,
|
||||||
|
ownName:
|
||||||
|
connectionProvider.deviceInfo.deviceName ??
|
||||||
|
connectionProvider.deviceInfo.selfName,
|
||||||
|
)) {
|
||||||
|
debugPrint('⏭️ [AppProvider] Ignoring self replay: ${message.id}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Enrich message with sender name from contacts first
|
// Enrich message with sender name from contacts first
|
||||||
Message enrichedMessage = message;
|
Message enrichedMessage = message;
|
||||||
Contact? senderContact;
|
Contact? senderContact;
|
||||||
|
|||||||
@@ -505,6 +505,7 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
enhancedMessage = _resolveSenderNameIfNeeded(enhancedMessage);
|
||||||
|
|
||||||
// For channel messages with sender name, try to link with contact
|
// For channel messages with sender name, try to link with contact
|
||||||
Message finalMessage = enhancedMessage;
|
Message finalMessage = enhancedMessage;
|
||||||
@@ -634,17 +635,36 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
if (existing.channelIdx != message.channelIdx) {
|
if (existing.channelIdx != message.channelIdx) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final existingSender = existing.senderKeyShort ?? existing.senderName;
|
final existingSenderKey = existing.senderKeyShort;
|
||||||
final incomingSender = message.senderKeyShort ?? message.senderName;
|
final incomingSenderKey = message.senderKeyShort;
|
||||||
if (existingSender == incomingSender) {
|
if (existingSenderKey != null &&
|
||||||
|
incomingSenderKey != null &&
|
||||||
|
existingSenderKey == incomingSenderKey) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final existingSenderName = _normalizeSenderName(existing.senderName);
|
||||||
|
final incomingSenderName = _normalizeSenderName(message.senderName);
|
||||||
|
if (existingSenderName != null &&
|
||||||
|
incomingSenderName != null &&
|
||||||
|
existingSenderName == incomingSenderName) {
|
||||||
|
if (!existing.isSentMessage && !message.isSentMessage) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When we send to a channel we add a local "sent" bubble immediately,
|
// When we send to a channel we add a local "sent" bubble immediately,
|
||||||
// then firmware may later sync back the same message as a received
|
// then firmware may later sync back the same message as a received
|
||||||
// channel item with only the public sender handle. Only fold that replay
|
// channel item under our public handle. Only fold that replay into the
|
||||||
// into the original bubble after LOG_RX_DATA has already confirmed it as
|
// original bubble after LOG_RX_DATA has already confirmed it as our
|
||||||
// our own transmitted packet.
|
// transmitted packet.
|
||||||
|
if (existing.isSentMessage &&
|
||||||
|
existing.echoCount > 0 &&
|
||||||
|
!message.isSentMessage &&
|
||||||
|
(existing.senderTimestamp - message.senderTimestamp).abs() <= 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (existing.isSentMessage &&
|
if (existing.isSentMessage &&
|
||||||
existing.echoCount > 0 &&
|
existing.echoCount > 0 &&
|
||||||
!message.isSentMessage &&
|
!message.isSentMessage &&
|
||||||
@@ -694,6 +714,29 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? _normalizeSenderName(String? value) {
|
||||||
|
final trimmed = value?.trim();
|
||||||
|
if (trimmed == null || trimmed.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return trimmed.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
Message _resolveSenderNameIfNeeded(Message message) {
|
||||||
|
if (message.senderName != null || message.senderPublicKeyPrefix == null) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
final resolvedSenderName = resolveContactNameCallback?.call(
|
||||||
|
message.senderPublicKeyPrefix,
|
||||||
|
);
|
||||||
|
if (resolvedSenderName == null || resolvedSenderName.trim().isEmpty) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return message.copyWith(senderName: resolvedSenderName.trim());
|
||||||
|
}
|
||||||
|
|
||||||
/// Trigger urgent notification for SAR marker
|
/// Trigger urgent notification for SAR marker
|
||||||
Future<void> _triggerSarNotification(
|
Future<void> _triggerSarNotification(
|
||||||
Message message,
|
Message message,
|
||||||
@@ -1251,6 +1294,7 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
enhancedMessage = _resolveSenderNameIfNeeded(enhancedMessage);
|
||||||
|
|
||||||
// Check for duplicates (shouldn't happen for sent messages, but be safe)
|
// Check for duplicates (shouldn't happen for sent messages, but be safe)
|
||||||
if (_findDuplicateMessageIndex(enhancedMessage) != -1) {
|
if (_findDuplicateMessageIndex(enhancedMessage) != -1) {
|
||||||
|
|||||||
@@ -1,30 +1,137 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:meshcore_sar_app/models/message.dart';
|
||||||
import 'package:meshcore_sar_app/providers/app_provider.dart';
|
import 'package:meshcore_sar_app/providers/app_provider.dart';
|
||||||
|
|
||||||
|
Uint8List _ownPublicKey() =>
|
||||||
|
Uint8List.fromList([1, 2, 3, 4, 5, 6, ...List<int>.filled(26, 0)]);
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('AppProvider channel info handling', () {
|
group('AppProvider channel info handling', () {
|
||||||
test('treats zeroed unnamed non-public channel as deleted', () {
|
test('treats zeroed unnamed non-public channel as deleted', () {
|
||||||
|
expect(AppProvider.isDeletedChannelInfo(2, '', Uint8List(16)), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps unnamed non-public channel when secret is configured', () {
|
||||||
|
final secret = Uint8List.fromList([1, ...List<int>.filled(15, 0)]);
|
||||||
|
|
||||||
|
expect(AppProvider.isDeletedChannelInfo(2, '', secret), isFalse);
|
||||||
|
expect(AppProvider.channelContactName(2, ''), 'Channel 2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('AppProvider self replay handling', () {
|
||||||
|
test('ignores direct self replay without hops', () {
|
||||||
|
final message = Message(
|
||||||
|
id: 'dm-self',
|
||||||
|
messageType: MessageType.contact,
|
||||||
|
senderPublicKeyPrefix: Uint8List.fromList([1, 2, 3, 4, 5, 6]),
|
||||||
|
pathLen: 0,
|
||||||
|
textType: MessageTextType.plain,
|
||||||
|
senderTimestamp: 1700000000,
|
||||||
|
text: 'hello',
|
||||||
|
receivedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
AppProvider.isDeletedChannelInfo(2, '', Uint8List(16)),
|
AppProvider.shouldIgnoreSelfReplay(
|
||||||
|
message: message,
|
||||||
|
ownPublicKey: _ownPublicKey(),
|
||||||
|
ownName: 'dz0ny (SI)',
|
||||||
|
),
|
||||||
isTrue,
|
isTrue,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('keeps unnamed non-public channel when secret is configured', () {
|
test('ignores room self replay without hops', () {
|
||||||
final secret = Uint8List.fromList([
|
final message = Message(
|
||||||
1,
|
id: 'room-self',
|
||||||
...List<int>.filled(15, 0),
|
messageType: MessageType.contact,
|
||||||
]);
|
senderPublicKeyPrefix: Uint8List.fromList([1, 2, 3, 4, 5, 6]),
|
||||||
|
recipientPublicKey: Uint8List.fromList([9, 9, 9, 9, 9, 9]),
|
||||||
|
pathLen: 0,
|
||||||
|
textType: MessageTextType.plain,
|
||||||
|
senderTimestamp: 1700000000,
|
||||||
|
text: 'hello room',
|
||||||
|
receivedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
AppProvider.isDeletedChannelInfo(2, '', secret),
|
AppProvider.shouldIgnoreSelfReplay(
|
||||||
|
message: message,
|
||||||
|
ownPublicKey: _ownPublicKey(),
|
||||||
|
ownName: 'dz0ny (SI)',
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps direct self replay when it traversed hops', () {
|
||||||
|
final message = Message(
|
||||||
|
id: 'dm-self-routed',
|
||||||
|
messageType: MessageType.contact,
|
||||||
|
senderPublicKeyPrefix: Uint8List.fromList([1, 2, 3, 4, 5, 6]),
|
||||||
|
pathLen: 1,
|
||||||
|
textType: MessageTextType.plain,
|
||||||
|
senderTimestamp: 1700000000,
|
||||||
|
text: 'hello',
|
||||||
|
receivedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
AppProvider.shouldIgnoreSelfReplay(
|
||||||
|
message: message,
|
||||||
|
ownPublicKey: _ownPublicKey(),
|
||||||
|
ownName: 'dz0ny (SI)',
|
||||||
|
),
|
||||||
isFalse,
|
isFalse,
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ignores channel self replay by self name without hops', () {
|
||||||
|
final message = Message(
|
||||||
|
id: 'channel-self',
|
||||||
|
messageType: MessageType.channel,
|
||||||
|
senderName: 'dz0ny (SI)',
|
||||||
|
channelIdx: 0,
|
||||||
|
pathLen: 0,
|
||||||
|
textType: MessageTextType.plain,
|
||||||
|
senderTimestamp: 1700000000,
|
||||||
|
text: 'hello public',
|
||||||
|
receivedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
AppProvider.channelContactName(2, ''),
|
AppProvider.shouldIgnoreSelfReplay(
|
||||||
'Channel 2',
|
message: message,
|
||||||
|
ownPublicKey: _ownPublicKey(),
|
||||||
|
ownName: 'dz0ny (SI)',
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps channel self replay when it traversed hops', () {
|
||||||
|
final message = Message(
|
||||||
|
id: 'channel-self-routed',
|
||||||
|
messageType: MessageType.channel,
|
||||||
|
senderName: 'dz0ny (SI)',
|
||||||
|
channelIdx: 0,
|
||||||
|
pathLen: 1,
|
||||||
|
textType: MessageTextType.plain,
|
||||||
|
senderTimestamp: 1700000000,
|
||||||
|
text: 'hello public',
|
||||||
|
receivedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
AppProvider.shouldIgnoreSelfReplay(
|
||||||
|
message: message,
|
||||||
|
ownPublicKey: _ownPublicKey(),
|
||||||
|
ownName: 'dz0ny (SI)',
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ void main() {
|
|||||||
|
|
||||||
test('channel messages are marked sent immediately', () {
|
test('channel messages are marked sent immediately', () {
|
||||||
final provider = MessagesProvider();
|
final provider = MessagesProvider();
|
||||||
|
provider.resolveContactNameCallback = (_) => 'dz0ny (SI)';
|
||||||
provider.addSentMessage(
|
provider.addSentMessage(
|
||||||
_buildSentChannelMessage(id: 'c1', senderTimestamp: 1700000000),
|
_buildSentChannelMessage(id: 'c1', senderTimestamp: 1700000000),
|
||||||
);
|
);
|
||||||
@@ -179,6 +180,7 @@ void main() {
|
|||||||
'channel replay is deduped after raw echo detection confirms our send',
|
'channel replay is deduped after raw echo detection confirms our send',
|
||||||
() {
|
() {
|
||||||
final provider = MessagesProvider();
|
final provider = MessagesProvider();
|
||||||
|
provider.resolveContactNameCallback = (_) => 'dz0ny (SI)';
|
||||||
provider.addSentMessage(
|
provider.addSentMessage(
|
||||||
_buildSentChannelMessage(id: 'c-echo', senderTimestamp: 1700000100),
|
_buildSentChannelMessage(id: 'c-echo', senderTimestamp: 1700000100),
|
||||||
);
|
);
|
||||||
@@ -188,12 +190,14 @@ void main() {
|
|||||||
provider.addMessage(
|
provider.addMessage(
|
||||||
_buildReceivedChannelReplay(
|
_buildReceivedChannelReplay(
|
||||||
id: 'c-echo-incoming',
|
id: 'c-echo-incoming',
|
||||||
senderTimestamp: 1700000100,
|
senderTimestamp: 1700000101,
|
||||||
|
senderName: 'dz0ny (SI)',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(provider.messages, hasLength(1));
|
expect(provider.messages, hasLength(1));
|
||||||
expect(provider.messages.single.id, equals('c-echo'));
|
expect(provider.messages.single.id, equals('c-echo'));
|
||||||
|
expect(provider.messages.single.senderName, equals('dz0ny (SI)'));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -201,6 +205,7 @@ void main() {
|
|||||||
'channel replay is not deduped before raw echo detection confirms it',
|
'channel replay is not deduped before raw echo detection confirms it',
|
||||||
() {
|
() {
|
||||||
final provider = MessagesProvider();
|
final provider = MessagesProvider();
|
||||||
|
provider.resolveContactNameCallback = (_) => 'dz0ny (SI)';
|
||||||
provider.addSentMessage(
|
provider.addSentMessage(
|
||||||
_buildSentChannelMessage(
|
_buildSentChannelMessage(
|
||||||
id: 'c-no-echo',
|
id: 'c-no-echo',
|
||||||
@@ -212,7 +217,8 @@ void main() {
|
|||||||
provider.addMessage(
|
provider.addMessage(
|
||||||
_buildReceivedChannelReplay(
|
_buildReceivedChannelReplay(
|
||||||
id: 'c-no-echo-incoming',
|
id: 'c-no-echo-incoming',
|
||||||
senderTimestamp: 1700000200,
|
senderTimestamp: 1700000201,
|
||||||
|
senderName: 'dz0ny (SI)',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user