Šepot — end-to-end encrypted messenger

Initial public release. Anonymous on-device identity (no phone number,
e-mail, or password), Signal-protocol cryptography (X3DH + PQXDH, Double
Ratchet, sealed sender) via libsignal, a metadata-minimizing relay,
group chats, disappearing messages, encrypted local storage, optional
Tor transport, and a UI localized into Czech, English, German, Russian,
and Italian.

Monorepo: Flutter client (app/), Rust relay + wire types (rust/),
protobuf definitions (proto/), self-host deploy (deploy/).

Licensed under AGPL-3.0 with an app-store distribution exception (§7).
This commit is contained in:
Julius Joska
2026-07-11 17:06:39 +02:00
commit 8081d98c67
309 changed files with 44102 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.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;
});
/// 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;
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).
service.onMessageReceived = (contact, message) async {
final visible = WidgetsBinding.instance.lifecycleState ==
AppLifecycleState.resumed;
if (visible) return;
final conv = await ref.read(storageProvider).loadConversation(contact.id);
if (conv?.isMuted == true) return;
PushNotifier.showMessageNotification(contact.displayName);
};
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();
});