From 1b8670d8110d7f1fa2ae2964539bf7e181f1d009 Mon Sep 17 00:00:00 2001 From: Artur Date: Sun, 26 Apr 2026 00:07:35 +0500 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=9F=D1=83=D1=88=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 + android/app/build.gradle.kts | 16 +- android/app/src/main/AndroidManifest.xml | 13 + .../chepuhagram/app}/MainActivity.kt | 2 +- android/build.gradle.kts | 4 + lib/domain/services/api_service.dart | 26 ++ lib/main.dart | 233 ++++++++++++++++- lib/presentation/screens/chat_screen.dart | 72 ++++-- lib/presentation/screens/contacts_screen.dart | 235 +++++++++++++++++- .../screens/key_recovery_screen.dart | 6 +- lib/presentation/screens/splash_screen.dart | 103 +++++++- macos/Flutter/GeneratedPluginRegistrant.swift | 10 + pubspec.lock | 208 ++++++++++++++++ pubspec.yaml | 5 + srv/alembic/versions/b577fae9f973_.py | 36 +++ srv/app/api/endpoints/auth.py | 11 +- srv/app/api/endpoints/messages.py | 2 +- srv/app/db/models.py | 1 + srv/app/websocket/connection_manager.py | 49 +++- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 21 files changed, 1004 insertions(+), 37 deletions(-) rename android/app/src/main/kotlin/{com/example/chepuhagram => ru/chepuhagram/app}/MainActivity.kt (74%) create mode 100644 srv/alembic/versions/b577fae9f973_.py diff --git a/.gitignore b/.gitignore index 22891bc..ac9dacf 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,11 @@ migrate_working_dir/ *.db .env venv/ +.venv/ +chepuhagram-6ca5d-firebase-adminsdk-fbsvc-cf8a5ad2f3.json +.firebaserc +firebase-tools-instant-win.exe +google-services.json # IntelliJ related *.iml diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 4ae0b5b..4df7405 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -3,14 +3,16 @@ plugins { id("kotlin-android") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") + id("com.google.gms.google-services") } android { - namespace = "com.example.chepuhagram" - compileSdk = flutter.compileSdkVersion + namespace = "ru.chepuhagram.app" + compileSdk = 36 ndkVersion = flutter.ndkVersion compileOptions { + isCoreLibraryDesugaringEnabled = true sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } @@ -21,11 +23,11 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.example.chepuhagram" + applicationId = "ru.chepuhagram.app" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion - targetSdk = flutter.targetSdkVersion + targetSdk = 36 versionCode = flutter.versionCode versionName = flutter.versionName } @@ -42,3 +44,9 @@ android { flutter { source = "../.." } + +dependencies { + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") + implementation(platform("com.google.firebase:firebase-bom:34.12.0")) + implementation("com.google.firebase:firebase-messaging") +} \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d2d92cf..8849fc9 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,7 @@ + + + + + + + + + + + ("clean") { delete(rootProject.layout.buildDirectory) } + +plugins { + id("com.google.gms.google-services") version "4.4.4" apply false +} \ No newline at end of file diff --git a/lib/domain/services/api_service.dart b/lib/domain/services/api_service.dart index 660554c..63d5443 100644 --- a/lib/domain/services/api_service.dart +++ b/lib/domain/services/api_service.dart @@ -64,6 +64,32 @@ class ApiService extends ChangeNotifier { return token; } + Future updateFcmToken(String fcmtoken) async { + notifyListeners(); + + try { + final token = await getAccessToken(); + final response = await _client.post( + Uri.http(AppConstants.baseUrl, 'auth/update-fcm', {'token': fcmtoken}), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + ); + + if (response.statusCode == 200) { + return true; + } else { + print("Ошибка установки ключа: ${response.statusCode}"); + return false; + } + } catch (e) { + rethrow; + } finally { + notifyListeners(); + } + } + Future setPublicKey(String publicKey) async { notifyListeners(); diff --git a/lib/main.dart b/lib/main.dart index 4b6ef79..f377d29 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,13 +1,174 @@ -import 'package:chepuhagram/presentation/screens/splash_screen.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'dart:io'; +import 'package:path_provider/path_provider.dart'; import 'data/datasources/ws_client.dart'; import 'logic/auth_provider.dart'; import 'logic/contact_provider.dart'; import 'core/theme_manager.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:chepuhagram/domain/services/crypto_service.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'dart:convert'; +import 'package:chepuhagram/presentation/screens/chat_screen.dart'; +import 'package:chepuhagram/presentation/screens/contacts_screen.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'presentation/screens/splash_screen.dart'; -void main() { +final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); +final GlobalKey navigatorKey = GlobalKey(); + +// Глобальная переменная для отслеживания текущего активного контакта в чате +int? currentActiveChatContactId; + +// Глобальная переменная для хранения начального сообщения (при запуске из уведомления) +RemoteMessage? initialMessage; + +// Ключ для SharedPreferences +const String _notificationLaunchKey = 'notification_launch_data'; + +Future _onSelectNotification(NotificationResponse notificationResponse) async { + final payload = notificationResponse.payload; + if (payload != null) { + try { + final data = jsonDecode(payload); + final senderId = int.tryParse(data['sender_id']?.toString() ?? ''); + if (senderId != null) { + print('Notification selected, payload sender_id=$senderId'); + + final context = navigatorKey.currentContext; + final prefs = await SharedPreferences.getInstance(); + + // Важно: не сохраняем payload в SharedPreferences, если можем сразу перейти в чат. + // Иначе при следующем обычном запуске (по иконке) останется "хвост" и приложение + // будет снова автопереходить в чат. + if (context == null) { + await prefs.setString(_notificationLaunchKey, jsonEncode(data)); + print('Navigator context is null, saved notification payload to SharedPreferences'); + } else { + await prefs.remove(_notificationLaunchKey); + } + + // Navigate to chat with this contact (if context is ready) + _navigateToChat(senderId); + } else { + print('Notification payload has invalid sender_id: ${data['sender_id']}'); + } + } catch (e) { + print('Error parsing notification payload: $e'); + } + } +} + +void _navigateToChat(int senderId) { + print('Navigating to chat with senderId: $senderId'); + final context = navigatorKey.currentContext; + if (context != null) { + final contactProvider = Provider.of(context, listen: false); + + // Check if contacts are loaded + if (contactProvider.contacts.isEmpty) { + print('Contacts not loaded yet, navigating to contacts screen first'); + // Navigate to contacts screen and pass the senderId to navigate to chat later + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ContactsScreen(targetChatId: senderId), + ), + ); + return; + } + + try { + final contact = contactProvider.contacts.firstWhere( + (c) => c.id == senderId, + orElse: () => throw Exception('Contact not found'), + ); + print('Found contact: ${contact.username}, navigating to chat'); + currentActiveChatContactId = senderId; // Устанавливаем активный чат + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ChatScreen(contact: contact), + ), + ); + } catch (e) { + print('Contact with id $senderId not found, navigating to contacts screen'); + // Contact not found, go to contacts screen + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const ContactsScreen(), + ), + ); + } + } else { + print('Navigator context is null'); + } +} + +void main() async { WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(); + + // Проверяем, было ли приложение запущено из уведомления + // Добавляем небольшую задержку, чтобы Firebase полностью инициализировался + await Future.delayed(const Duration(milliseconds: 500)); + initialMessage = await FirebaseMessaging.instance.getInitialMessage(); + print('Initial message from main() after delay: $initialMessage'); + // Сохраняем информацию в SharedPreferences для надежности + final prefs = await SharedPreferences.getInstance(); + if (initialMessage != null) { + print('App launched from notification: ${initialMessage!.data}'); + print('Message type: ${initialMessage!.data['type']}'); + print('Sender ID: ${initialMessage!.data['sender_id']}'); + + // Сохраняем данные уведомления + await prefs.setString(_notificationLaunchKey, jsonEncode(initialMessage!.data)); + print('Saved notification data to SharedPreferences'); + } else { + print('No initial message - app launched normally'); + // Очищаем сохраненные данные, если приложение запущено нормально + await prefs.remove(_notificationLaunchKey); + } + + // Initialize local notifications + const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher'); + final InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid); + await flutterLocalNotificationsPlugin.initialize( + initializationSettings, + onDidReceiveNotificationResponse: _onSelectNotification, + ); + + // Если приложение было запущено из локального уведомления, сохраним payload + final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); + if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) { + final payload = notificationAppLaunchDetails?.notificationResponse?.payload; + print('App launched from local notification, payload: $payload'); + if (payload != null && payload.isNotEmpty) { + try { + final data = jsonDecode(payload); + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_notificationLaunchKey, jsonEncode(data)); + print('Saved local notification launch payload to SharedPreferences'); + } catch (e) { + print('Failed to save notification launch payload: $e'); + } + } + } + + // Create notification channel for Android 8+ + const AndroidNotificationChannel channel = AndroidNotificationChannel( + 'chat_id', // id + 'Messages', // title + description: 'Chat messages notifications', // description + importance: Importance.high, + ); + + await flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation()?.createNotificationChannel(channel); + + FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); runApp( MultiProvider( @@ -22,6 +183,69 @@ void main() { ); } +@pragma('vm:entry-point') +Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { + print("Фоновый пуш получен: ${message.data}"); + if (message.data['type'] == 'enc_message') { + try { + // Initialize notifications for background + const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher'); + const InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid); + await flutterLocalNotificationsPlugin.initialize(initializationSettings); + + // Create notification channel + const AndroidNotificationChannel channel = AndroidNotificationChannel( + 'chat_id', + 'Messages', + description: 'Chat messages notifications', + importance: Importance.high, + ); + + await flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation()?.createNotificationChannel(channel); + + // Try to decrypt + String notificationText = 'New encrypted message'; + try { + // 1. Инициализируем крипто-сервис + final crypto = CryptoService(); + + // 2. Достаем ключи (они должны быть в SecureStorage) + final myPrivKey = await crypto.getPrivateKey(); + print('Private key retrieved: ${myPrivKey != null}'); + if (myPrivKey == null) { + print('Private key not found, showing encrypted message'); + notificationText = 'Encrypted message: ${message.data['content']?.substring(0, 50) ?? 'N/A'}...'; + } else { + // 3. Расшифровываем + final sharedSecret = await crypto.deriveSharedSecret(myPrivKey, message.data['public_key']); + final decryptedText = await crypto.decryptMessage(message.data['content'], sharedSecret); + notificationText = decryptedText; + } + } catch (e) { + print('Decryption failed: $e'); + notificationText = 'Failed to decrypt: ${e.toString()}'; + } + + // 4. Показываем локальное уведомление + await flutterLocalNotificationsPlugin.show( + message.hashCode, + message.data['username'] ?? 'Unknown', + notificationText, + const NotificationDetails(android: AndroidNotificationDetails('chat_id', 'Messages')), + payload: jsonEncode({ + 'type': 'enc_message', + 'sender_id': message.data['sender_id'], + }), + ); + print('Notification shown successfully'); + } catch (e) { + print('Error processing background message: $e'); + } + } else { + print('Message type is not enc_message: ${message.data['type']}'); + } +} + class MyApp extends StatelessWidget { const MyApp({super.key}); @@ -35,6 +259,7 @@ class MyApp extends StatelessWidget { themeAnimationCurve: Curves.easeInOut, theme: themeProvider.themeData, themeMode: themeProvider.themeMode, + navigatorKey: navigatorKey, // Начальный экран home: const SplashScreen(), diff --git a/lib/presentation/screens/chat_screen.dart b/lib/presentation/screens/chat_screen.dart index fa99e89..0ef4277 100644 --- a/lib/presentation/screens/chat_screen.dart +++ b/lib/presentation/screens/chat_screen.dart @@ -1,3 +1,5 @@ +import 'dart:async'; +import 'dart:convert'; import 'package:flutter/material.dart'; import '/data/models/message_model.dart'; import '/data/models/contact_model.dart'; @@ -5,11 +7,13 @@ import 'package:chepuhagram/presentation/widgets/message_bubble.dart'; import 'package:chepuhagram/data/repositories/contact_repository.dart'; import 'package:chepuhagram/domain/services/crypto_service.dart'; import 'package:chepuhagram/data/datasources/ws_client.dart'; -import 'dart:convert'; import 'package:provider/provider.dart'; import '/logic/contact_provider.dart'; import '../../domain/services/api_service.dart'; import 'package:chepuhagram/data/datasources/local_db_service.dart'; +import 'package:chepuhagram/main.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'contacts_screen.dart'; class ChatScreen extends StatefulWidget { final Contact contact; @@ -21,6 +25,7 @@ class ChatScreen extends StatefulWidget { } class _ChatScreenState extends State { + static const String _notificationLaunchKey = 'notification_launch_data'; int myId = 0; late Contact _currentContact; bool _isKeyLoading = false; @@ -29,32 +34,41 @@ class _ChatScreenState extends State { final apiService = ApiService(); final CryptoService _cryptoService = CryptoService(); List messages = []; + StreamSubscription? _socketSubscription; @override void initState() { super.initState(); _currentContact = widget.contact; + + currentActiveChatContactId = _currentContact.id; // Устанавливаем активный чат final contactProvider = context.read(); myId = contactProvider.getCurrentUserId() ?? 0; - _loadHistory(); // Если ключа нет, загружаем его при входе if (_currentContact.publicKey == null) { _loadContactKey(); } + _loadHistory(); + + final socketService = Provider.of(context, listen: false); + _socketSubscription = socketService.messages.listen(_handleIncomingMessage); } Future _loadContactKey() async { + if (!mounted) return; setState(() => _isKeyLoading = true); try { final updatedContact = await _contactRepository.fetchContactById( _currentContact.id, ); + if (!mounted) return; setState(() { _currentContact = updatedContact; _isKeyLoading = false; }); print(updatedContact.publicKey); } catch (e) { + if (!mounted) return; setState(() => _isKeyLoading = false); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -66,6 +80,8 @@ class _ChatScreenState extends State { @override void dispose() { + currentActiveChatContactId = null; // Сбрасываем активный чат + _socketSubscription?.cancel(); _controller.dispose(); super.dispose(); } @@ -73,7 +89,21 @@ class _ChatScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text(_currentContact.name)), + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + if (Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } else { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const ContactsScreen()), + ); + } + }, + ), + title: Text(_currentContact.name), + ), body: Column( children: [ Expanded( @@ -146,11 +176,17 @@ class _ChatScreenState extends State { sharedSecret, ); + final encryptedText50 = await _cryptoService.encryptMessage( + rawText.length > 50 ? rawText.substring(0, 50) : rawText, + sharedSecret, + ); + // Формируем payload для сервера final payload = { "type": "private_message", "receiver_id": _currentContact.id, "content": encryptedText, + "content50": encryptedText50, }; // Отправляем @@ -180,16 +216,6 @@ class _ChatScreenState extends State { } } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - // Подписываемся на поток сообщений из сокета - final socketService = Provider.of(context, listen: false); - - socketService.messages.listen((rawData) { - _handleIncomingMessage(rawData); - }); - } void _handleIncomingMessage(Map data) async { if (data['type'] == 'private_message') { @@ -214,6 +240,7 @@ class _ChatScreenState extends State { // 4. Добавляем в список и обновляем экран await LocalDbService().saveMessages([data]); + if (!mounted) return; setState(() { messages.add( MessageModel( @@ -238,6 +265,11 @@ class _ChatScreenState extends State { } Future _loadHistory() async { + + initialMessage = null; // Сбрасываем данные уведомления при загрузке ключа + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_notificationLaunchKey); + await prefs.setString(_notificationLaunchKey, ''); // Очищаем данные уведомления при загрузке ключа try { final myPrivKey = await _cryptoService.getPrivateKey(); final sharedSecret = await _cryptoService.deriveSharedSecret( @@ -265,6 +297,7 @@ class _ChatScreenState extends State { ); } if (cached.isNotEmpty) { + if (!mounted) return; setState(() { messages = loadedLocalMessages; _isKeyLoading = false; @@ -275,14 +308,15 @@ class _ChatScreenState extends State { } final history = await apiService.getChatHistory(widget.contact.id); - + print(history); List loadedMessages = []; for (var msg in history) { final decrypted = await _cryptoService.decryptMessage( msg['content'], sharedSecret, ); - loadedMessages.add( + loadedMessages.insert( + 0, MessageModel( text: decrypted, isMe: msg['sender_id'] == myId, @@ -292,14 +326,20 @@ class _ChatScreenState extends State { ), ); } - await localDb.saveMessages(history); + try { + await localDb.saveMessages(history); + } catch (e) { + print("Ошибка сохранения истории в локальную базу: $e"); + } + if (!mounted) return; setState(() { messages = loadedMessages; _isKeyLoading = false; }); } catch (e) { print("Ошибка загрузки истории: $e"); + if (!mounted) return; setState(() => _isKeyLoading = false); } } diff --git a/lib/presentation/screens/contacts_screen.dart b/lib/presentation/screens/contacts_screen.dart index 30eae83..57b12e0 100644 --- a/lib/presentation/screens/contacts_screen.dart +++ b/lib/presentation/screens/contacts_screen.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; +import 'package:chepuhagram/domain/services/aPI_service.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../widgets/contact_tile.dart'; @@ -6,28 +8,249 @@ import '../screens/new_chat_screen.dart'; import '../screens/chat_screen.dart'; import '/logic/contact_provider.dart'; import '/logic/auth_provider.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:chepuhagram/domain/services/crypto_service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:chepuhagram/main.dart'; class ContactsScreen extends StatefulWidget { - const ContactsScreen({super.key}); + final int? targetChatId; + + const ContactsScreen({super.key, this.targetChatId}); @override State createState() => _ContactsScreenState(); } class _ContactsScreenState extends State { + static const String _notificationLaunchKey = 'notification_launch_data'; + @override void initState() { super.initState(); + print( + 'ContactsScreen initState, targetChatId: ${widget.targetChatId}', + ); + _setupPushNotifications(); WidgetsBinding.instance.addPostFrameCallback((_) { final authProvider = context.read(); final contactProvider = context.read(); - + // Установить текущего пользователя и загрузить контакты с сообщениями contactProvider.setCurrentUserId(authProvider.currentUserId); - contactProvider.loadContacts(); + contactProvider.loadContacts().then((_) { + print( + 'Contacts loaded, checking targetChatId: ${widget.targetChatId}', + ); + // После загрузки контактов проверить, нужно ли перейти к чату + if (widget.targetChatId != null) { + _navigateToTargetChat(); + } else { + _checkSavedNotificationTarget(); + } + }); }); } + Future _checkSavedNotificationTarget() async { + final prefs = await SharedPreferences.getInstance(); + final savedData = prefs.getString(_notificationLaunchKey); + if (savedData == null) { + print('No saved notification data found in SharedPreferences'); + return; + } + + try { + final data = jsonDecode(savedData) as Map; + print('Recovered saved notification data: $data'); + final senderId = int.tryParse(data['sender_id']?.toString() ?? ''); + final type = data['type']?.toString(); + + // Поддерживаем старый payload (только sender_id) и новый (type+sender_id) + if (senderId != null && (type == null || type == 'enc_message')) { + print('Recovered targetChatId from saved data: $senderId'); + + await prefs.remove(_notificationLaunchKey); + _navigateToTargetChatWithId(senderId); + return; + } + + print('Saved notification data is not a valid payload: $data'); + await prefs.remove(_notificationLaunchKey); + } catch (e) { + print('Error parsing saved notification data: $e'); + await prefs.remove(_notificationLaunchKey); + } + } + + void _navigateToTargetChat() { + if (widget.targetChatId == null) return; + + _navigateToTargetChatWithId(widget.targetChatId!); + } + + void _navigateToTargetChatWithId(int targetChatId) { + print( + '_navigateToTargetChat called with targetChatId: $targetChatId', + ); + final contactProvider = context.read(); + try { + final contact = contactProvider.contacts.firstWhere( + (c) => c.id == targetChatId, + ); + print( + 'Auto-navigating to chat with contact: ${contact.username}', + ); + currentActiveChatContactId = targetChatId; // Устанавливаем активный чат + Navigator.push( + context, + MaterialPageRoute(builder: (_) => ChatScreen(contact: contact)), + ); + } catch (e) { + print('Target contact with id $targetChatId not found: $e'); + } + } + + Future _setupPushNotifications() async { + // Request permissions + await FirebaseMessaging.instance.requestPermission(); + + String? token = await FirebaseMessaging.instance.getToken(); + if (token != null) { + ApiService apiService = ApiService(); + print(token); + await apiService.updateFcmToken(token); + } + + // Listen for token refresh + FirebaseMessaging.instance.onTokenRefresh.listen((newToken) { + ApiService apiService = ApiService(); + apiService.updateFcmToken(newToken); + print('FCM Token refreshed: $newToken'); + }); + + // Listen for foreground messages + FirebaseMessaging.onMessage.listen((RemoteMessage message) { + print('Foreground message received: ${message.data}'); + if (message.data['type'] == 'enc_message') { + _handleIncomingMessage(message); + } + }); + + // Handle notification tap when app was terminated/backgrounded + FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { + print('Notification tapped, app opened: ${message.data}'); + if (message.data['type'] == 'enc_message') { + final senderId = int.tryParse( + message.data['sender_id']?.toString() ?? '', + ); + if (senderId != null) { + _navigateToChatFromNotification(senderId); + } else { + print( + 'Notification tap contains invalid sender_id: ${message.data['sender_id']}', + ); + } + } + }); + } + + void _navigateToChatFromNotification(int senderId) { + final contactProvider = context.read(); + print( + 'Navigate to chat from notification with senderId: $senderId', + ); + + // Если контакты еще не загружены, ждем их загрузки + if (contactProvider.contacts.isEmpty) { + print('Contacts not loaded yet, waiting...'); + // Ждем немного и пробуем снова + Future.delayed(const Duration(milliseconds: 500), () { + if (mounted) { + _navigateToChatFromNotification(senderId); + } + }); + return; + } + + try { + final contact = contactProvider.contacts.firstWhere( + (c) => c.id == senderId, + ); + print( + 'Navigating to chat from notification: ${contact.username}', + ); + currentActiveChatContactId = senderId; // Устанавливаем активный чат + Navigator.push( + context, + MaterialPageRoute(builder: (_) => ChatScreen(contact: contact)), + ); + } catch (e) { + // Contact not found, stay on contacts screen + print('Contact not found for notification: $senderId'); + } + } + + Future _handleIncomingMessage(RemoteMessage message) async { + try { + // Проверяем, не находимся ли мы уже в чате с отправителем + final senderId = int.tryParse( + message.data['sender_id']?.toString() ?? '', + ); + if (senderId != null && currentActiveChatContactId == senderId) { + print('Already in chat with sender $senderId, skipping notification'); + return; + } + + // Ensure notification channel exists + const AndroidNotificationChannel channel = AndroidNotificationChannel( + 'chat_id', + 'Messages', + description: 'Chat messages notifications', + importance: Importance.high, + ); + + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin + >() + ?.createNotificationChannel(channel); + + final crypto = CryptoService(); + final myPrivKey = await crypto.getPrivateKey(); + if (myPrivKey == null) { + print('Private key not found, cannot decrypt message'); + return; + } + + final sharedSecret = await crypto.deriveSharedSecret( + myPrivKey, + message.data['public_key'], + ); + final decryptedText = await crypto.decryptMessage( + message.data['content'], + sharedSecret, + ); + + // Show local notification + await flutterLocalNotificationsPlugin.show( + message.hashCode, + message.data['username'] ?? 'Unknown', + decryptedText, + const NotificationDetails( + android: AndroidNotificationDetails('chat_id', 'Messages'), + ), + payload: jsonEncode({ + 'type': 'enc_message', + 'sender_id': message.data['sender_id'], + }), + ); + } catch (e) { + print('Error processing foreground message: $e'); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -91,7 +314,11 @@ class _ContactsScreenState extends State { accountEmail: Text("@ArturKarasevich"), currentAccountPicture: CircleAvatar( backgroundColor: Theme.of(context).colorScheme.onSurface, - child: Icon(Icons.person, size: 40, color: Theme.of(context).colorScheme.primaryContainer,), + child: Icon( + Icons.person, + size: 40, + color: Theme.of(context).colorScheme.primaryContainer, + ), ), decoration: BoxDecoration( color: Theme.of(context).colorScheme.primaryContainer, diff --git a/lib/presentation/screens/key_recovery_screen.dart b/lib/presentation/screens/key_recovery_screen.dart index 83d83a9..581a9f8 100644 --- a/lib/presentation/screens/key_recovery_screen.dart +++ b/lib/presentation/screens/key_recovery_screen.dart @@ -1,3 +1,4 @@ +import 'package:chepuhagram/presentation/screens/contacts_screen.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../logic/auth_provider.dart'; @@ -98,7 +99,10 @@ class _KeyRecoveryScreenState extends State { if (mounted) { // Возвращаемся на главный экран - Navigator.of(context).popUntil((route) => route.isFirst); + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const ContactsScreen()), + ); } } catch (e) { if (mounted) { diff --git a/lib/presentation/screens/splash_screen.dart b/lib/presentation/screens/splash_screen.dart index 9d534f1..8f2b0ff 100644 --- a/lib/presentation/screens/splash_screen.dart +++ b/lib/presentation/screens/splash_screen.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../logic/auth_provider.dart'; +import '../../logic/contact_provider.dart'; import 'login_screen.dart'; import 'contacts_screen.dart'; import 'account_setup_screen.dart'; import 'key_recovery_screen.dart'; +import 'chat_screen.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:chepuhagram/main.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:convert'; class SplashScreen extends StatefulWidget { const SplashScreen({super.key}); @@ -14,12 +20,36 @@ class SplashScreen extends StatefulWidget { } class _SplashScreenState extends State { + int? _targetChatId; + + // Ключ для SharedPreferences + static const String _notificationLaunchKey = 'notification_launch_data'; + @override void initState() { super.initState(); + print('SplashScreen initState'); + _setupNotificationHandler(); _initializeApp(); } + void _setupNotificationHandler() { + print('Setting up notification handler'); + // Обработка открытия приложения из уведомления + FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { + print('App opened from notification: ${message.data}'); + if (message.data['type'] == 'enc_message') { + final senderId = int.tryParse(message.data['sender_id']?.toString() ?? ''); + if (senderId != null) { + setState(() { + _targetChatId = senderId; + }); + print('Set target chat from opened app: $senderId'); + } + } + }); + } + Future _initializeApp() async { // 1. Искусственная задержка в 2 секунды для демонстрации splash await Future.delayed(const Duration(seconds: 2)); @@ -51,9 +81,78 @@ class _SplashScreenState extends State { ); } else { // Путь Б: Нормальный вход в контакты + // Проверяем, было ли приложение запущено из уведомления + int? targetChatId = _targetChatId; // Сначала проверяем из onMessageOpenedApp + + // Если не установлено, проверяем SharedPreferences + if (targetChatId == null) { + final prefs = await SharedPreferences.getInstance(); + final savedData = prefs.getString(_notificationLaunchKey); + + if (savedData != null) { + try { + final data = jsonDecode(savedData) as Map; + print('Found saved notification data: $data'); + final senderId = int.tryParse(data['sender_id']?.toString() ?? ''); + final type = data['type']?.toString(); + + // Поддерживаем старый payload (только sender_id) и новый (type+sender_id) + if (senderId != null && (type == null || type == 'enc_message')) { + targetChatId = senderId; + print('App launched from saved notification, target chat: $targetChatId'); + } + + // Очищаем сохраненные данные после использования + await prefs.remove(_notificationLaunchKey); + } catch (e) { + print('Error parsing saved notification data: $e'); + await prefs.remove(_notificationLaunchKey); + } + } + + // Также проверяем initialMessage как fallback + if (targetChatId == null) { + print('Checking initialMessage: $initialMessage'); + if (initialMessage != null) { + print('Initial message data: ${initialMessage!.data}'); + if (initialMessage!.data['type'] == 'enc_message') { + targetChatId = int.tryParse(initialMessage!.data['sender_id']?.toString() ?? ''); + print('Set target chat from initialMessage: $targetChatId'); + } else { + print('Initial message type is not enc_message: ${initialMessage!.data['type']}'); + } + } else { + print('No initial message found'); + } + } + } else { + print('Using targetChatId from onMessageOpenedApp: $targetChatId'); + } + + if (targetChatId != null) { + print('Notification targetChatId resolved: $targetChatId, trying to open chat directly'); + try { + final contactProvider = context.read(); + contactProvider.setCurrentUserId(authProvider.currentUserId); + await contactProvider.loadContacts(); + + final contact = contactProvider.contacts.firstWhere((c) => c.id == targetChatId); + currentActiveChatContactId = targetChatId; + print('Directly navigating to ChatScreen for contact: ${contact.username}'); + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => ChatScreen(contact: contact)), + ); + return; + } catch (e) { + print('Failed to open chat directly, falling back to ContactsScreen: $e'); + } + } + + print('Navigating to ContactsScreen, targetChatId: $targetChatId'); Navigator.pushReplacement( context, - MaterialPageRoute(builder: (_) => const ContactsScreen()), + MaterialPageRoute(builder: (_) => ContactsScreen(targetChatId: targetChatId)), ); } } else { @@ -102,7 +201,7 @@ class _SplashScreenState extends State { fontSize: 12, ), ), - const SizedBox(height: 80), + const SizedBox(height: 40), ], ), ), diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d378756..9493ab3 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,12 +5,22 @@ import FlutterMacOS import Foundation +import firebase_analytics +import firebase_core +import firebase_messaging +import flutter_local_notifications import flutter_secure_storage_darwin import path_provider_foundation +import shared_preferences_foundation import sqflite_darwin func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 0efd1b5..2f379e8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "37a42d06068e2fe3deddb2da079a8c4d105f241225ba27b7122b37e9865fd8f7" + url: "https://pub.dev" + source: hosted + version: "1.3.35" args: dependency: transitive description: @@ -73,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.9" + dbus: + dependency: transitive + description: + name: dbus + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + url: "https://pub.dev" + source: hosted + version: "0.7.12" fake_async: dependency: transitive description: @@ -89,6 +105,86 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + firebase_analytics: + dependency: "direct main" + description: + name: firebase_analytics + sha256: dbf1e7ab22cfb1f4a4adb103b46a26276b4edc593d4a78ef6fb942bafc92e035 + url: "https://pub.dev" + source: hosted + version: "10.10.7" + firebase_analytics_platform_interface: + dependency: transitive + description: + name: firebase_analytics_platform_interface + sha256: "3729b74f8cf1d974a27ba70332ecb55ff5ff560edc8164a6469f4a055b429c37" + url: "https://pub.dev" + source: hosted + version: "3.10.8" + firebase_analytics_web: + dependency: transitive + description: + name: firebase_analytics_web + sha256: "019cd7eee74254d33fbd2e29229367ce33063516bf6b3258a341d89e3b0f1655" + url: "https://pub.dev" + source: hosted + version: "0.5.7+7" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "26de145bb9688a90962faec6f838247377b0b0d32cc0abecd9a4e43525fc856c" + url: "https://pub.dev" + source: hosted + version: "2.32.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: "8bcfad6d7033f5ea951d15b867622a824b13812178bfec0c779b9d81de011bbb" + url: "https://pub.dev" + source: hosted + version: "5.4.2" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: eb3afccfc452b2b2075acbe0c4b27de62dd596802b4e5e19869c1e926cbb20b3 + url: "https://pub.dev" + source: hosted + version: "2.24.0" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + sha256: "980259425fa5e2afc03e533f33723335731d21a56fd255611083bceebf4373a8" + url: "https://pub.dev" + source: hosted + version: "14.7.10" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + sha256: "87c4a922cb6f811cfb7a889bdbb3622702443c52a0271636cbc90d813ceac147" + url: "https://pub.dev" + source: hosted + version: "4.5.37" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + sha256: "90dc7ed885e90a24bb0e56d661d4d2b5f84429697fd2cbb9e5890a0ca370e6f4" + url: "https://pub.dev" + source: hosted + version: "3.5.18" flutter: dependency: "direct main" description: flutter @@ -102,6 +198,30 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "674173fd3c9eda9d4c8528da2ce0ea69f161577495a9cc835a2a4ecd7eadeb35" + url: "https://pub.dev" + source: hosted + version: "17.2.4" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af + url: "https://pub.dev" + source: hosted + version: "4.0.1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" + url: "https://pub.dev" + source: hosted + version: "7.2.0" flutter_secure_storage: dependency: "direct main" description: @@ -192,6 +312,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" jwt_decoder: dependency: "direct main" description: @@ -328,6 +456,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" platform: dependency: transitive description: @@ -352,6 +488,62 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5+1" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + url: "https://pub.dev" + source: hosted + version: "2.5.5" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 + url: "https://pub.dev" + source: hosted + version: "2.4.23" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -453,6 +645,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.7" + timezone: + dependency: transitive + description: + name: timezone + sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" + url: "https://pub.dev" + source: hosted + version: "0.9.4" typed_data: dependency: transitive description: @@ -517,6 +717,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" sdks: dart: ">=3.10.0 <4.0.0" flutter: ">=3.38.0" diff --git a/pubspec.yaml b/pubspec.yaml index 7390ecd..a9e56fc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,11 @@ dependencies: cryptography: ^2.5.0 sqflite: ^2.3.0 path: ^1.9.0 + firebase_core: ^2.24.2 + firebase_messaging: ^14.7.10 + flutter_local_notifications: ^17.2.2 + firebase_analytics: ^10.10.7 + shared_preferences: ^2.5.5 dev_dependencies: flutter_test: diff --git a/srv/alembic/versions/b577fae9f973_.py b/srv/alembic/versions/b577fae9f973_.py new file mode 100644 index 0000000..02e718f --- /dev/null +++ b/srv/alembic/versions/b577fae9f973_.py @@ -0,0 +1,36 @@ +"""empty message + +Revision ID: b577fae9f973 +Revises: 4e1aa78f81c6 +Create Date: 2026-04-24 23:01:14.120776 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'b577fae9f973' +down_revision: Union[str, Sequence[str], None] = '4e1aa78f81c6' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('users', schema=None) as batch_op: + batch_op.add_column(sa.Column('fcm_token', sa.String(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('users', schema=None) as batch_op: + batch_op.drop_column('fcm_token') + + # ### end Alembic commands ### diff --git a/srv/app/api/endpoints/auth.py b/srv/app/api/endpoints/auth.py index 5369be4..38db9ec 100644 --- a/srv/app/api/endpoints/auth.py +++ b/srv/app/api/endpoints/auth.py @@ -6,7 +6,6 @@ from app.api import schemas from app.db import models from jose import JWTError, jwt from app.core.security import get_current_user - # бд @@ -105,4 +104,12 @@ async def setup_account(data: schemas.SetupAccount, current_user: models.User = user_to_update.encrypted_private_key = data.encrypted_private_key db.commit() db.refresh(user_to_update) - return {"status": "ok", "message": "Account setup completed"} \ No newline at end of file + return {"status": "ok", "message": "Account setup completed"} + +@authRouter.post("/update-fcm") +async def update_fcm(token: str, current_user: models.User = Depends(get_current_user), db: Session = Depends(get_db)): + user_to_update = db.merge(current_user) + user_to_update.fcm_token = token + db.commit() + db.refresh(user_to_update) + return {"status": "ok"} \ No newline at end of file diff --git a/srv/app/api/endpoints/messages.py b/srv/app/api/endpoints/messages.py index 3e46fdd..f917389 100644 --- a/srv/app/api/endpoints/messages.py +++ b/srv/app/api/endpoints/messages.py @@ -29,7 +29,7 @@ async def get_chat_history( messages = db.query(models.Message).filter( (models.Message.sender_id == current_user.id) & (models.Message.receiver_id == contact_id) | (models.Message.sender_id == contact_id) & (models.Message.receiver_id == current_user.id) - ).order_by(models.Message.timestamp.asc()).limit(limit).all() + ).order_by(models.Message.timestamp.desc()).limit(limit).all() return messages diff --git a/srv/app/db/models.py b/srv/app/db/models.py index 0c21fc5..f0ae5cd 100644 --- a/srv/app/db/models.py +++ b/srv/app/db/models.py @@ -22,6 +22,7 @@ class User(Base): hashed_password = Column(String) public_key = Column(String, nullable=True) encrypted_private_key = Column(String, nullable=True) + fcm_token = Column(String, nullable=True) class Message(Base): __tablename__ = "messages" diff --git a/srv/app/websocket/connection_manager.py b/srv/app/websocket/connection_manager.py index 1879d20..0533946 100644 --- a/srv/app/websocket/connection_manager.py +++ b/srv/app/websocket/connection_manager.py @@ -5,9 +5,15 @@ from datetime import datetime import json from sqlalchemy.orm import Session from app.db import models +from firebase_admin import messaging, credentials, exceptions +import firebase_admin +cred = credentials.Certificate("chepuhagram-6ca5d-firebase-adminsdk-fbsvc-cf8a5ad2f3.json") +firebase_admin.initialize_app(cred) # бд + + def get_db(): db = models.SessionLocal() try: @@ -40,10 +46,13 @@ async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), db: data = await websocket.receive_text() message_data = json.loads(data) print(f"DEBUG: Получены данные: {message_data}") - + if message_data.get("type") == "private_message": + + user = db.query(models.User).filter(models.User.id == user_id).first() receiver_id = message_data.get("receiver_id") content = message_data.get("content") + content50 = message_data.get("content50") new_msg = models.Message( sender_id=user_id, receiver_id=receiver_id, @@ -52,11 +61,24 @@ async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), db: db.add(new_msg) db.commit() db.refresh(new_msg) + + if receiver_id not in manager.active_connections and user.public_key != '': + receiver = db.query(models.User).filter( + models.User.id == receiver_id).first() + if receiver.fcm_token: + send_fcm_notification( + receiver.fcm_token, + user_id, + user.first_name, + user.public_key, + content50 if content50 else content, + datetime.now(), + ) # Формируем пакет для получателя outgoing_message = { "id": new_msg.id, "type": "private_message", - "sender_id": user_id, + "sender_id": user_id, "reciever_id": receiver_id, "content": message_data.get("content"), "timestamp": datetime.now().isoformat() @@ -70,6 +92,29 @@ async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), db: manager.disconnect(user_id) +def send_fcm_notification(token, user_id, username, public_key, encrypted_text, timestamp): + print(f"DEBUG: Отправляем FCM уведомление пользователю {user_id} с токеном {token}") + message = messaging.Message( + data={ + "type": "enc_message", + "sender_id": str(user_id), + "username": username, + "public_key": public_key, + "content": encrypted_text, # Зашифрованный текст + "timestamp": timestamp.isoformat(), + }, + android=messaging.AndroidConfig( + priority='high', + ), + token=token, + ) + try: + response = messaging.send(message) + print('Successfully sent message:', response) + except Exception as e: + print('Unexpected error sending push:', e) + + class ConnectionManager: def __init__(self): # Храним активные соединения: {user_id: websocket} diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 0c50753..39cedd3 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,12 @@ #include "generated_plugin_registrant.h" +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index d0b33f8..b1ad9e1 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + firebase_core flutter_secure_storage_windows )