Сообщения работают
This commit is contained in:
parent
8a0a237e18
commit
40d715539e
|
|
@ -1,6 +1,6 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<application
|
<application
|
||||||
android:label="chepuhagram"
|
android:label="Chepuhagram"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
import 'package:chepuhagram/data/models/message_model.dart';
|
||||||
|
|
||||||
|
class LocalDbService {
|
||||||
|
static final LocalDbService _instance = LocalDbService._internal();
|
||||||
|
static Database? _database;
|
||||||
|
|
||||||
|
factory LocalDbService() => _instance;
|
||||||
|
LocalDbService._internal();
|
||||||
|
|
||||||
|
Future<Database> get database async {
|
||||||
|
if (_database != null) return _database!;
|
||||||
|
_database = await _initDb();
|
||||||
|
return _database!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Database> _initDb() async {
|
||||||
|
String path = join(await getDatabasesPath(), 'chat_app.db');
|
||||||
|
return await openDatabase(
|
||||||
|
path,
|
||||||
|
version: 1,
|
||||||
|
onCreate: (db, version) async {
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE messages(
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
sender_id INTEGER,
|
||||||
|
receiver_id INTEGER,
|
||||||
|
content TEXT,
|
||||||
|
timestamp TEXT
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохранение списка сообщений (из истории)
|
||||||
|
Future<void> saveMessages(List<dynamic> messages) async {
|
||||||
|
final db = await database;
|
||||||
|
Batch batch = db.batch();
|
||||||
|
for (var msg in messages) {
|
||||||
|
if (msg is MessageModel) {
|
||||||
|
batch.insert('messages', {
|
||||||
|
'id': msg.id,
|
||||||
|
'sender_id': msg.senderId,
|
||||||
|
'receiver_id': msg.receiverId,
|
||||||
|
'content': msg.text, // ВАЖНО: сохраняй зашифрованный текст!
|
||||||
|
'timestamp': msg.createdAt.toIso8601String(),
|
||||||
|
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||||
|
} else {
|
||||||
|
// Если это Map из API
|
||||||
|
batch.insert('messages', {
|
||||||
|
'id': msg['id'],
|
||||||
|
'sender_id': msg['sender_id'],
|
||||||
|
'receiver_id': msg['receiver_id'], // Убедись, что ключ совпадает с API
|
||||||
|
'content': msg['content'],
|
||||||
|
'timestamp': msg['timestamp'],
|
||||||
|
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await batch.commit(noResult: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение сообщений конкретного чата
|
||||||
|
Future<List<Map<String, dynamic>>> getChatHistory(
|
||||||
|
int contactId,
|
||||||
|
int myId,
|
||||||
|
) async {
|
||||||
|
final db = await database;
|
||||||
|
return await db.query(
|
||||||
|
'messages',
|
||||||
|
where:
|
||||||
|
'(sender_id = ? AND receiver_id = ?) OR (sender_id = ? AND receiver_id = ?)',
|
||||||
|
whereArgs: [contactId, myId, myId, contactId],
|
||||||
|
orderBy: 'timestamp ASC',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,13 @@ import 'package:web_socket_channel/status.dart' as status;
|
||||||
import 'package:chepuhagram/core/constants.dart';
|
import 'package:chepuhagram/core/constants.dart';
|
||||||
|
|
||||||
class SocketService {
|
class SocketService {
|
||||||
|
static final SocketService _instance = SocketService._internal();
|
||||||
|
|
||||||
|
factory SocketService() {
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
SocketService._internal();
|
||||||
|
|
||||||
WebSocketChannel? _channel;
|
WebSocketChannel? _channel;
|
||||||
final StreamController<Map<String, dynamic>> _messageController =
|
final StreamController<Map<String, dynamic>> _messageController =
|
||||||
StreamController<Map<String, dynamic>>.broadcast();
|
StreamController<Map<String, dynamic>>.broadcast();
|
||||||
|
|
@ -38,7 +45,21 @@ class SocketService {
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendMessage(Map<String, dynamic> data) {
|
void sendMessage(Map<String, dynamic> data) {
|
||||||
_channel?.sink.add(jsonEncode(data));
|
if (_channel == null) {
|
||||||
|
print("❌ ОШИБКА: Попытка отправить сообщение через NULL канал.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final encodedData = jsonEncode(data);
|
||||||
|
|
||||||
|
// 1. Проверяем, не закрыт ли sink (у некоторых провайдеров это доступно)
|
||||||
|
_channel!.sink.add(encodedData);
|
||||||
|
|
||||||
|
// 2. Добавляем принт подтверждения
|
||||||
|
print("🚀 СООБЩЕНИЕ ОТПРАВЛЕНО В SINK: $encodedData");
|
||||||
|
} catch (e) {
|
||||||
|
print("❌ КРИТИЧЕСКАЯ ОШИБКА ПРИ ОТПРАВКЕ: $e");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void disconnect() {
|
void disconnect() {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ class Contact {
|
||||||
final DateTime? lastMessageTime;
|
final DateTime? lastMessageTime;
|
||||||
final bool isOnline;
|
final bool isOnline;
|
||||||
final int unreadCount;
|
final int unreadCount;
|
||||||
|
final String? publicKey;
|
||||||
|
|
||||||
Contact({
|
Contact({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
|
@ -19,6 +20,7 @@ class Contact {
|
||||||
this.lastMessageTime,
|
this.lastMessageTime,
|
||||||
this.isOnline = false,
|
this.isOnline = false,
|
||||||
this.unreadCount = 0,
|
this.unreadCount = 0,
|
||||||
|
this.publicKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Contact.fromJson(Map<String, dynamic> json) {
|
factory Contact.fromJson(Map<String, dynamic> json) {
|
||||||
|
|
@ -27,7 +29,7 @@ class Contact {
|
||||||
username: json['username'] ?? 'Unknown',
|
username: json['username'] ?? 'Unknown',
|
||||||
name: json['name'] ?? 'Unknown',
|
name: json['name'] ?? 'Unknown',
|
||||||
surname: json['surname'] ?? 'Unknown',
|
surname: json['surname'] ?? 'Unknown',
|
||||||
// Другие поля можно добавить позже
|
publicKey: json['public_key'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
class MessageModel {
|
class MessageModel {
|
||||||
final int? id; // ID из базы данных (null, если сообщение еще не отправлено)
|
final int? id; // ID из базы данных (null, если сообщение еще не отправлено)
|
||||||
final int senderId; // ID отправителя
|
final int senderId; // ID отправителя
|
||||||
|
final int receiverId; // ID отправителя
|
||||||
final String text; // Текст сообщения
|
final String text; // Текст сообщения
|
||||||
final DateTime createdAt; // Время отправки
|
final DateTime createdAt; // Время отправки
|
||||||
final bool isMe; // Локальный флаг для UI (мое/чужое)
|
final bool isMe; // Локальный флаг для UI (мое/чужое)
|
||||||
|
|
@ -8,6 +9,7 @@ class MessageModel {
|
||||||
MessageModel({
|
MessageModel({
|
||||||
this.id,
|
this.id,
|
||||||
required this.senderId,
|
required this.senderId,
|
||||||
|
required this.receiverId,
|
||||||
required this.text,
|
required this.text,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
this.isMe = false,
|
this.isMe = false,
|
||||||
|
|
@ -18,6 +20,7 @@ class MessageModel {
|
||||||
return MessageModel(
|
return MessageModel(
|
||||||
id: json['id'],
|
id: json['id'],
|
||||||
senderId: json['sender_id'],
|
senderId: json['sender_id'],
|
||||||
|
receiverId: json['receiverId'],
|
||||||
text: json['text'] ?? '',
|
text: json['text'] ?? '',
|
||||||
// Парсим дату из ISO строки или временной метки
|
// Парсим дату из ISO строки или временной метки
|
||||||
createdAt: DateTime.parse(json['created_at']),
|
createdAt: DateTime.parse(json['created_at']),
|
||||||
|
|
|
||||||
|
|
@ -29,4 +29,22 @@ class ContactRepository {
|
||||||
throw Exception('Failed to load contacts');
|
throw Exception('Failed to load contacts');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Contact> fetchContactById(int userId) async {
|
||||||
|
final token = await _apiService.getAccessToken();
|
||||||
|
final response = await _client.get(
|
||||||
|
Uri.http(AppConstants.baseUrl, 'users/$userId'),
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = jsonDecode(utf8.decode(response.bodyBytes));
|
||||||
|
return Contact.fromJson(data);
|
||||||
|
} else {
|
||||||
|
throw Exception('Не удалось загрузить данные контакта');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -90,4 +90,19 @@ class ApiService extends ChangeNotifier {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<dynamic>> getChatHistory(int contactId) async {
|
||||||
|
final token = await getAccessToken();
|
||||||
|
final response = await http.get(
|
||||||
|
Uri.http(
|
||||||
|
AppConstants.baseUrl,
|
||||||
|
'messages/history/${contactId.toString()}',
|
||||||
|
),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
"Authorization": "Bearer $token",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return jsonDecode(response.body) as List<dynamic>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ class ContactProvider extends ChangeNotifier {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int? getCurrentUserId() {
|
||||||
|
return _currentUserId;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> loadContacts() async {
|
Future<void> loadContacts() async {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_error = null;
|
_error = null;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:chepuhagram/presentation/screens/splash_screen.dart';
|
import 'package:chepuhagram/presentation/screens/splash_screen.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'data/datasources/ws_client.dart';
|
||||||
import 'logic/auth_provider.dart';
|
import 'logic/auth_provider.dart';
|
||||||
import 'logic/contact_provider.dart';
|
import 'logic/contact_provider.dart';
|
||||||
import 'core/theme_manager.dart';
|
import 'core/theme_manager.dart';
|
||||||
|
|
@ -13,7 +13,10 @@ void main() {
|
||||||
MultiProvider(
|
MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider(create: (_) => AuthProvider()),
|
ChangeNotifierProvider(create: (_) => AuthProvider()),
|
||||||
ChangeNotifierProvider(create: (_) => ThemeProvider()), ChangeNotifierProvider(create: (_) => ContactProvider()), ],
|
ChangeNotifierProvider(create: (_) => ThemeProvider()),
|
||||||
|
ChangeNotifierProvider(create: (_) => ContactProvider()),
|
||||||
|
Provider(create: (_) => SocketService()),
|
||||||
|
],
|
||||||
child: const MyApp(),
|
child: const MyApp(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,14 @@ import 'package:flutter/material.dart';
|
||||||
import '/data/models/message_model.dart';
|
import '/data/models/message_model.dart';
|
||||||
import '/data/models/contact_model.dart';
|
import '/data/models/contact_model.dart';
|
||||||
import 'package:chepuhagram/presentation/widgets/message_bubble.dart';
|
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';
|
||||||
|
|
||||||
class ChatScreen extends StatefulWidget {
|
class ChatScreen extends StatefulWidget {
|
||||||
final Contact contact;
|
final Contact contact;
|
||||||
|
|
@ -13,8 +21,48 @@ class ChatScreen extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChatScreenState extends State<ChatScreen> {
|
class _ChatScreenState extends State<ChatScreen> {
|
||||||
|
int myId = 0;
|
||||||
|
late Contact _currentContact;
|
||||||
|
bool _isKeyLoading = false;
|
||||||
final TextEditingController _controller = TextEditingController();
|
final TextEditingController _controller = TextEditingController();
|
||||||
final List<MessageModel> messages = [];
|
final ContactRepository _contactRepository = ContactRepository();
|
||||||
|
final apiService = ApiService();
|
||||||
|
final CryptoService _cryptoService = CryptoService();
|
||||||
|
List<MessageModel> messages = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_currentContact = widget.contact;
|
||||||
|
final contactProvider = context.read<ContactProvider>();
|
||||||
|
myId = contactProvider.getCurrentUserId() ?? 0;
|
||||||
|
_loadHistory();
|
||||||
|
// Если ключа нет, загружаем его при входе
|
||||||
|
if (_currentContact.publicKey == null) {
|
||||||
|
_loadContactKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadContactKey() async {
|
||||||
|
setState(() => _isKeyLoading = true);
|
||||||
|
try {
|
||||||
|
final updatedContact = await _contactRepository.fetchContactById(
|
||||||
|
_currentContact.id,
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_currentContact = updatedContact;
|
||||||
|
_isKeyLoading = false;
|
||||||
|
});
|
||||||
|
print(updatedContact.publicKey);
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => _isKeyLoading = false);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text("Не удалось получить ключ шифрования собеседника"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
|
@ -25,7 +73,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text(widget.contact.name)),
|
appBar: AppBar(title: Text(_currentContact.name)),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|
@ -49,25 +97,210 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMessageInput() {
|
Widget _buildMessageInput() {
|
||||||
return Padding(
|
return SafeArea(
|
||||||
padding: const EdgeInsets.all(8.0),
|
// Добавляем SafeArea здесь
|
||||||
child: Row(
|
child: Padding(
|
||||||
children: [
|
padding: const EdgeInsets.all(8.0),
|
||||||
Expanded(
|
child: Row(
|
||||||
child: TextField(
|
children: [
|
||||||
controller: _controller,
|
Expanded(
|
||||||
decoration: const InputDecoration(hintText: "Напиши сообщение..."),
|
child: TextField(
|
||||||
|
controller: _controller,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: "Напиши сообщение...",
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
IconButton(
|
||||||
IconButton(
|
icon: const Icon(Icons.send),
|
||||||
icon: const Icon(Icons.send),
|
onPressed: () {
|
||||||
onPressed: () {
|
_sendMessage();
|
||||||
// Логика отправки через WebSocket или API
|
},
|
||||||
_controller.clear();
|
),
|
||||||
},
|
],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _sendMessage() async {
|
||||||
|
final rawText = _controller.text.trim();
|
||||||
|
if (rawText.isEmpty) return;
|
||||||
|
_controller.clear();
|
||||||
|
|
||||||
|
if (_currentContact.publicKey == null) {
|
||||||
|
await _loadContactKey();
|
||||||
|
if (_currentContact.publicKey == null) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final myPrivKey = await _cryptoService.getPrivateKey();
|
||||||
|
|
||||||
|
final sharedSecret = await _cryptoService.deriveSharedSecret(
|
||||||
|
myPrivKey!,
|
||||||
|
_currentContact.publicKey!,
|
||||||
|
);
|
||||||
|
|
||||||
|
final encryptedText = await _cryptoService.encryptMessage(
|
||||||
|
rawText,
|
||||||
|
sharedSecret,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Формируем payload для сервера
|
||||||
|
final payload = {
|
||||||
|
"type": "private_message",
|
||||||
|
"receiver_id": _currentContact.id,
|
||||||
|
"content": encryptedText,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Отправляем
|
||||||
|
print("ОТПРАВКА: $payload");
|
||||||
|
Provider.of<SocketService>(context, listen: false).sendMessage(payload);
|
||||||
|
|
||||||
|
// Обновляем UI (себе показываем расшифрованный текст)
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
messages.add(
|
||||||
|
MessageModel(
|
||||||
|
text: rawText,
|
||||||
|
isMe: true,
|
||||||
|
senderId: myId,
|
||||||
|
receiverId: _currentContact.id,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
_controller.clear();
|
||||||
|
} catch (e) {
|
||||||
|
_controller.text = rawText;
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text("Ошибка шифрования: $e")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
// Подписываемся на поток сообщений из сокета
|
||||||
|
final socketService = Provider.of<SocketService>(context, listen: false);
|
||||||
|
|
||||||
|
socketService.messages.listen((rawData) {
|
||||||
|
_handleIncomingMessage(rawData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleIncomingMessage(Map<String, dynamic> data) async {
|
||||||
|
if (data['type'] == 'private_message') {
|
||||||
|
final int senderId = int.parse(data['sender_id'].toString());
|
||||||
|
|
||||||
|
// 1. Проверяем, что сообщение именно от того, с кем мы сейчас общаемся
|
||||||
|
if (senderId == widget.contact.id) {
|
||||||
|
try {
|
||||||
|
final myPrivKey = await _cryptoService.getPrivateKey();
|
||||||
|
|
||||||
|
// 2. Вычисляем общий секрет для расшифровки
|
||||||
|
final sharedSecret = await _cryptoService.deriveSharedSecret(
|
||||||
|
myPrivKey!,
|
||||||
|
widget.contact.publicKey!,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Расшифровываем контент
|
||||||
|
final decryptedText = await _cryptoService.decryptMessage(
|
||||||
|
data['content'],
|
||||||
|
sharedSecret,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Добавляем в список и обновляем экран
|
||||||
|
await LocalDbService().saveMessages([data]);
|
||||||
|
setState(() {
|
||||||
|
messages.add(
|
||||||
|
MessageModel(
|
||||||
|
text: decryptedText,
|
||||||
|
isMe: false,
|
||||||
|
senderId: senderId,
|
||||||
|
receiverId: myId,
|
||||||
|
createdAt: DateTime.parse(data['timestamp']),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print("Ошибка расшифровки входящего сообщения: $e");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print(
|
||||||
|
"Сообщение от другого пользователя (ID: $senderId), игнорируем в этом чате",
|
||||||
|
);
|
||||||
|
// Тут можно добавить логику уведомления для списка чатов
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadHistory() async {
|
||||||
|
try {
|
||||||
|
final myPrivKey = await _cryptoService.getPrivateKey();
|
||||||
|
final sharedSecret = await _cryptoService.deriveSharedSecret(
|
||||||
|
myPrivKey!,
|
||||||
|
widget.contact.publicKey!,
|
||||||
|
);
|
||||||
|
final localDb = LocalDbService();
|
||||||
|
final cached = await localDb.getChatHistory(widget.contact.id, myId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<MessageModel> loadedLocalMessages = [];
|
||||||
|
for (var msg in cached) {
|
||||||
|
final decrypted = await _cryptoService.decryptMessage(
|
||||||
|
msg['content'],
|
||||||
|
sharedSecret,
|
||||||
|
);
|
||||||
|
loadedLocalMessages.add(
|
||||||
|
MessageModel(
|
||||||
|
text: decrypted,
|
||||||
|
isMe: msg['sender_id'] == myId,
|
||||||
|
senderId: msg['sender_id'],
|
||||||
|
receiverId: msg['receiver_id'],
|
||||||
|
createdAt: DateTime.parse(msg['timestamp']),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (cached.isNotEmpty) {
|
||||||
|
setState(() {
|
||||||
|
messages = loadedLocalMessages;
|
||||||
|
_isKeyLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
final history = await apiService.getChatHistory(widget.contact.id);
|
||||||
|
|
||||||
|
List<MessageModel> loadedMessages = [];
|
||||||
|
for (var msg in history) {
|
||||||
|
final decrypted = await _cryptoService.decryptMessage(
|
||||||
|
msg['content'],
|
||||||
|
sharedSecret,
|
||||||
|
);
|
||||||
|
loadedMessages.add(
|
||||||
|
MessageModel(
|
||||||
|
text: decrypted,
|
||||||
|
isMe: msg['sender_id'] == myId,
|
||||||
|
senderId: msg['sender_id'],
|
||||||
|
receiverId: msg['receiver_id'],
|
||||||
|
createdAt: DateTime.parse(msg['timestamp']),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await localDb.saveMessages(history);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
messages = loadedMessages;
|
||||||
|
_isKeyLoading = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print("Ошибка загрузки истории: $e");
|
||||||
|
setState(() => _isKeyLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,8 +7,10 @@ import Foundation
|
||||||
|
|
||||||
import flutter_secure_storage_darwin
|
import flutter_secure_storage_darwin
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
|
import sqflite_darwin
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
|
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
52
pubspec.lock
52
pubspec.lock
|
|
@ -273,7 +273,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
|
|
@ -365,6 +365,46 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.2"
|
version: "1.10.2"
|
||||||
|
sqflite:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: sqflite
|
||||||
|
sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2+1"
|
||||||
|
sqflite_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_android
|
||||||
|
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2+3"
|
||||||
|
sqflite_common:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_common
|
||||||
|
sha256: "5e8377564d95166761a968ed96104e0569b6b6cc611faac92a36ab8a169112c3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.6+1"
|
||||||
|
sqflite_darwin:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_darwin
|
||||||
|
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2"
|
||||||
|
sqflite_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_platform_interface
|
||||||
|
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -389,6 +429,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.1"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.0"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -471,4 +519,4 @@ packages:
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.10.0 <4.0.0"
|
dart: ">=3.10.0 <4.0.0"
|
||||||
flutter: ">=3.35.6"
|
flutter: ">=3.38.0"
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,8 @@ dependencies:
|
||||||
jwt_decoder: ^2.0.1
|
jwt_decoder: ^2.0.1
|
||||||
web_socket_channel: ^3.0.3
|
web_socket_channel: ^3.0.3
|
||||||
cryptography: ^2.5.0
|
cryptography: ^2.5.0
|
||||||
|
sqflite: ^2.3.0
|
||||||
|
path: ^1.9.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
from fastapi import Depends, APIRouter, HTTPException, Depends
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.db import models
|
||||||
|
from app.core.security import get_current_user
|
||||||
|
from app.api import schemas
|
||||||
|
|
||||||
|
|
||||||
|
# бд
|
||||||
|
def get_db():
|
||||||
|
db = models.SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
messagesRouter = APIRouter(
|
||||||
|
prefix="/messages",
|
||||||
|
tags=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
@messagesRouter.get("/history/{contact_id}")
|
||||||
|
async def get_chat_history(
|
||||||
|
contact_id: int,
|
||||||
|
current_user: models.User = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
limit: int = 50
|
||||||
|
):
|
||||||
|
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()
|
||||||
|
|
||||||
|
return messages
|
||||||
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
|
|
||||||
from fastapi import Depends, APIRouter
|
from fastapi import Depends, APIRouter, HTTPException, Depends
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from app.db import models
|
from app.db import models
|
||||||
from app.core.security import get_current_user
|
from app.core.security import get_current_user
|
||||||
|
from app.api import schemas
|
||||||
|
|
||||||
|
|
||||||
# бд
|
# бд
|
||||||
|
|
@ -13,17 +14,38 @@ def get_db():
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
usersRouter = APIRouter(
|
usersRouter = APIRouter(
|
||||||
prefix="/users",
|
prefix="/users",
|
||||||
tags=[],
|
tags=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Пример защищенного роута
|
# Пример защищенного роута
|
||||||
|
|
||||||
|
|
||||||
@usersRouter.get("/me")
|
@usersRouter.get("/me")
|
||||||
async def read_users_me(current_user: models.User = Depends(get_current_user)):
|
async def read_users_me(current_user: models.User = Depends(get_current_user)):
|
||||||
return {"id": current_user.id, "username": current_user.username, "first_name": current_user.first_name, "last_name": current_user.last_name, "public_key": current_user.public_key, "encrypted_private_key": current_user.encrypted_private_key}
|
return {"id": current_user.id, "username": current_user.username, "first_name": current_user.first_name, "last_name": current_user.last_name, "public_key": current_user.public_key, "encrypted_private_key": current_user.encrypted_private_key}
|
||||||
|
|
||||||
|
|
||||||
@usersRouter.get("/all")
|
@usersRouter.get("/all")
|
||||||
async def read_users_all(current_user: models.User = Depends(get_current_user), db: Session = Depends(get_db)):
|
async def read_users_all(current_user: models.User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||||
users = db.query(models.User).all()
|
users = db.query(models.User).all()
|
||||||
return [{"id": user.id, "username": user.username, "name": f"{user.first_name} {user.last_name or ''}".strip(), "public_key": user.public_key} for user in users]
|
return [{"id": user.id, "username": user.username, "name": f"{user.first_name} {user.last_name or ''}".strip(), "public_key": user.public_key} for user in users]
|
||||||
|
|
||||||
|
|
||||||
|
@usersRouter.get("/{user_id}", response_model=schemas.UserPublic)
|
||||||
|
def get_user_by_id(
|
||||||
|
user_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: models.User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Получить публичную информацию о пользователе, включая его публичный ключ.
|
||||||
|
"""
|
||||||
|
user = db.query(models.User).filter(models.User.id == user_id).first()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
class SetPublicKey(BaseModel):
|
class SetPublicKey(BaseModel):
|
||||||
public_key: str
|
public_key: str
|
||||||
|
|
@ -11,3 +12,13 @@ class SetupAccount(BaseModel):
|
||||||
last_name: str
|
last_name: str
|
||||||
public_key: str
|
public_key: str
|
||||||
encrypted_private_key: str
|
encrypted_private_key: str
|
||||||
|
|
||||||
|
class UserPublic(BaseModel):
|
||||||
|
id: int
|
||||||
|
username: str
|
||||||
|
first_name: Optional[str] = None
|
||||||
|
last_name: Optional[str] = None
|
||||||
|
public_key: Optional[str] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
from sqlalchemy import Column, Integer, String, create_engine
|
from sqlalchemy import Column, Integer, String, create_engine
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from sqlalchemy import Column, Integer, Text, ForeignKey, DateTime
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
SQLALCHEMY_DATABASE_URL = "sqlite:///./chepuhagram.db"
|
SQLALCHEMY_DATABASE_URL = "sqlite:///./chepuhagram.db"
|
||||||
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
|
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
|
@ -21,4 +23,12 @@ class User(Base):
|
||||||
public_key = Column(String, nullable=True)
|
public_key = Column(String, nullable=True)
|
||||||
encrypted_private_key = Column(String, nullable=True)
|
encrypted_private_key = Column(String, nullable=True)
|
||||||
|
|
||||||
|
class Message(Base):
|
||||||
|
__tablename__ = "messages"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
sender_id = Column(Integer, ForeignKey("users.id"))
|
||||||
|
receiver_id = Column(Integer, ForeignKey("users.id"))
|
||||||
|
content = Column(Text)
|
||||||
|
timestamp = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
@ -1,16 +1,28 @@
|
||||||
from fastapi import HTTPException, status, APIRouter, WebSocket, WebSocketDisconnect, Query
|
from fastapi import HTTPException, status, APIRouter, WebSocket, WebSocketDisconnect, Query, Depends
|
||||||
from app.core.security import test_token
|
from app.core.security import test_token
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.db import models
|
||||||
|
|
||||||
|
|
||||||
|
# бд
|
||||||
|
def get_db():
|
||||||
|
db = models.SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
wsRouter = APIRouter(
|
wsRouter = APIRouter(
|
||||||
prefix="/ws",
|
prefix='/ws'
|
||||||
tags=[],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@wsRouter.websocket("")
|
@wsRouter.websocket("")
|
||||||
async def websocket_endpoint(websocket: WebSocket, token: str = Query(None)):
|
async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), db: Session = Depends(get_db)):
|
||||||
if token is None:
|
if token is None:
|
||||||
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
||||||
return
|
return
|
||||||
|
|
@ -19,20 +31,39 @@ async def websocket_endpoint(websocket: WebSocket, token: str = Query(None)):
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
||||||
return
|
return
|
||||||
|
print("ПОДКЛЮЧЕНИЕ")
|
||||||
await manager.connect(user_id, websocket)
|
await manager.connect(user_id, websocket)
|
||||||
|
print("ПОДКЛЮЧЕНО")
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
data = await websocket.receive_json()
|
print("ОЖИДАНИЕ СООБЩЕНИЙ")
|
||||||
|
data = await websocket.receive_text()
|
||||||
|
message_data = json.loads(data)
|
||||||
|
print(f"DEBUG: Получены данные: {message_data}")
|
||||||
|
|
||||||
receiver_id = str(data.get("receiver_id"))
|
if message_data.get("type") == "private_message":
|
||||||
message_to_send = {
|
receiver_id = message_data.get("receiver_id")
|
||||||
"sender_id": user_id,
|
content = message_data.get("content")
|
||||||
"text": data.get("text"),
|
new_msg = models.Message(
|
||||||
"created_at": datetime.now().isoformat()
|
sender_id=user_id,
|
||||||
}
|
receiver_id=receiver_id,
|
||||||
|
content=content
|
||||||
|
)
|
||||||
|
db.add(new_msg)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(new_msg)
|
||||||
|
# Формируем пакет для получателя
|
||||||
|
outgoing_message = {
|
||||||
|
"id": new_msg.id,
|
||||||
|
"type": "private_message",
|
||||||
|
"sender_id": user_id,
|
||||||
|
"reciever_id": receiver_id,
|
||||||
|
"content": message_data.get("content"),
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
await manager.send_personal_message(message_to_send, receiver_id)
|
# Пересылаем получателю, если он в сети
|
||||||
await manager.send_personal_message(message_to_send, user_id)
|
await manager.send_personal_message(outgoing_message, str(receiver_id))
|
||||||
except WebSocketDisconnect:
|
except WebSocketDisconnect:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
|
|
@ -53,8 +84,11 @@ class ConnectionManager:
|
||||||
del self.active_connections[user_id]
|
del self.active_connections[user_id]
|
||||||
|
|
||||||
async def send_personal_message(self, message: dict, user_id: str):
|
async def send_personal_message(self, message: dict, user_id: str):
|
||||||
if user_id in self.active_connections:
|
if str(user_id) in self.active_connections:
|
||||||
await self.active_connections[user_id].send_json(message)
|
await self.active_connections[str(user_id)].send_json(message)
|
||||||
|
print('Sent to socket')
|
||||||
|
else:
|
||||||
|
print('User not active')
|
||||||
|
|
||||||
async def broadcast(self, message: dict):
|
async def broadcast(self, message: dict):
|
||||||
# Рассылка вообще всем (например, системное уведомление)
|
# Рассылка вообще всем (например, системное уведомление)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from app.api.endpoints import users, auth
|
from app.api.endpoints import users, auth, messages
|
||||||
from app.websocket.connection_manager import wsRouter
|
from app.websocket.connection_manager import wsRouter
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
|
@ -7,6 +7,7 @@ app = FastAPI()
|
||||||
|
|
||||||
app.include_router(auth.authRouter)
|
app.include_router(auth.authRouter)
|
||||||
app.include_router(users.usersRouter)
|
app.include_router(users.usersRouter)
|
||||||
|
app.include_router(messages.messagesRouter)
|
||||||
app.include_router(wsRouter)
|
app.include_router(wsRouter)
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue