717aeed4ff
A stranger could pick the display name of an existing contact ("Mom")
and appear as that contact. Messages from a key that is not in the
contact list now open a message request labelled "(unknown)": no
replies, read receipts, typing indicators, calls or name in
notifications until the user accepts it and names the contact
themselves. Names that look like an existing contact (case, diacritics,
Cyrillic/Greek homoglyphs, invisible characters) also show a warning in
the real contact's chat.
When the relay account of a known contact shows up with a different
key, the chat shows "safety number changed", verification is cleared
and the session that now belongs to the other key is dropped before
sending, so a reply never goes to the new key.
114 lines
5.0 KiB
Dart
114 lines
5.0 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:sepot/core/calls/call_controller.dart';
|
|
import 'package:sepot/core/network/relay_client.dart';
|
|
import 'package:sepot/core/push/push_notifier.dart';
|
|
import 'package:sepot/core/services/messaging_service.dart';
|
|
import 'package:sepot/providers/contacts_provider.dart';
|
|
import 'package:sepot/providers/conversations_provider.dart';
|
|
import 'package:sepot/providers/groups_provider.dart';
|
|
import 'package:sepot/providers/identity_provider.dart';
|
|
import 'package:sepot/providers/storage_provider.dart';
|
|
|
|
/// Jediná instance messaging služby (krypto + relay + storage).
|
|
final messagingServiceProvider = Provider<MessagingService>((ref) {
|
|
final service = MessagingService(storage: ref.read(storageProvider));
|
|
ref.onDispose(service.stop);
|
|
return service;
|
|
});
|
|
|
|
/// Kdo právě píše: konverzace → otisky pisatelů. Prázdné, když je
|
|
/// „píše…“ vypnuté (služba pak nic nepřijímá).
|
|
final typingProvider = StreamProvider<Map<String, Set<String>>>((ref) async* {
|
|
final tracker = ref.watch(messagingServiceProvider).typing;
|
|
yield tracker.current;
|
|
yield* tracker.changes;
|
|
});
|
|
|
|
/// Profilová fotka člověka podle otisku (null = iniciály). Přepočítá se,
|
|
/// když se změní kontakty — tam dorazí nový odkaz i stažení fotky.
|
|
final avatarProvider =
|
|
FutureProvider.family<Uint8List?, String>((ref, fingerprint) async {
|
|
final service = ref.watch(messagingServiceProvider);
|
|
final me = ref.watch(identityProvider).valueOrNull;
|
|
final contacts = ref.watch(contactsProvider).valueOrNull ?? const [];
|
|
if (!service.isStarted) return null;
|
|
final contact = contacts.where((c) => c.id == fingerprint).firstOrNull;
|
|
// Fotka neznámého odesílatele může být fotka mámy z Facebooku — do
|
|
// přijetí jen iniciály na tónu z klíče, ne jeho obrázek.
|
|
final avatar = fingerprint == me?.id
|
|
? service.myAvatar
|
|
: contact != null && contact.accepted
|
|
? contact.avatar
|
|
: null;
|
|
return avatar == null ? null : service.loadAvatar(avatar);
|
|
});
|
|
|
|
/// Nastartuje službu, jakmile existuje identita. Watchovat z první
|
|
/// obrazovky po odemčení (chat list) — do té doby se nic nespojuje.
|
|
/// Předpokládá odemčený storage — na /chats se jde vždy až po unlocku.
|
|
final messagingStartProvider = FutureProvider<void>((ref) async {
|
|
final identity = await ref.watch(identityProvider.future);
|
|
if (identity == null) return;
|
|
|
|
// Po auto-locku se tenhle provider eager-rebuildne (drží ho pushInitProvider)
|
|
// ještě NAD zamčeným storage → service.start() by spadl na „Storage is
|
|
// locked" a provider by uvázl v AsyncError (žádný příjem/odeslání až do
|
|
// killu procesu). Nad zamčeným storage tiše nic nestartuj; po odemčení
|
|
// se provider invaliduje (viz unlock cesty) a spustí start znovu.
|
|
final storage = ref.read(storageProvider);
|
|
if (!storage.isUnlocked) return;
|
|
// Nátlaková relace: žádná síť. Messaging by se registroval skutečným
|
|
// identity klíčem (409 každých 30 s = důkaz pro pozorovatele sítě).
|
|
if (storage.isDecoyMode) return;
|
|
|
|
final service = ref.read(messagingServiceProvider);
|
|
if (service.isStarted) return;
|
|
|
|
service.onUpdate = () {
|
|
ref.invalidate(conversationsProvider);
|
|
// celá family — jinak se otevřená konverzace nedozví o nové zprávě
|
|
// ani o změně stavu přílohy
|
|
ref.invalidate(conversationProvider);
|
|
ref.invalidate(contactsProvider);
|
|
// příchozí ginv/gleave/gmeta mění skupiny
|
|
ref.invalidate(groupsProvider);
|
|
};
|
|
|
|
// Appka běží, ale není v popředí (jiná appka / zhasnutý displej se
|
|
// živým procesem) → systémová notifikace. Jen jméno, žádný obsah.
|
|
// Ztlumená konverzace notifikaci potlačí (unread badge běží dál),
|
|
// zmínka mě ve skupině projde i tak.
|
|
service.onMessageReceived = (contact, message, convId) async {
|
|
final visible = WidgetsBinding.instance.lifecycleState ==
|
|
AppLifecycleState.resumed;
|
|
if (visible) return;
|
|
final conv = await ref.read(storageProvider).loadConversation(convId);
|
|
final mentionedMe = message.mentions.contains(identity.id);
|
|
if (conv?.isMuted == true && !mentionedMe) return;
|
|
// Jméno, které si neznámý zvolil sám, nepatří do notifikace —
|
|
// „Nová zpráva od Máma“ by byl phishing ještě před otevřením appky.
|
|
if (contact.accepted) {
|
|
PushNotifier.showMessageNotification(contact.displayName);
|
|
} else if (convId != contact.id) {
|
|
PushNotifier.showWakeNotification();
|
|
} else {
|
|
PushNotifier.showMessageRequestNotification();
|
|
}
|
|
};
|
|
|
|
service.onCallSignal = (contact, signal) =>
|
|
ref.read(callProvider.notifier).onSignal(contact, signal);
|
|
|
|
await service.start(identity);
|
|
});
|
|
|
|
/// Stav připojení k relayi pro UI indikátor.
|
|
final relayConnectionProvider = StreamProvider<RelayConnectionState>((ref) {
|
|
ref.watch(messagingStartProvider);
|
|
final service = ref.read(messagingServiceProvider);
|
|
return service.connectionStream ?? const Stream.empty();
|
|
});
|