Initial commit: JustAMessenger v0.1.0
Серверная часть (Go): - WebSocket сервер с бинарным протоколом - XChaCha20-Poly1305 шифрование - zstd сжатие с дедупликацией (64KB чанки) - SQLite хранилище (WAL режим) - Управление гильдиями, каналами, ролями - Федерация между серверами (ed25519) - REST API + WebSocket endpoints Клиентская часть (Flutter): - Material Design 3 тёмная тема (Discord-like) - WebSocket соединение с сервером - Экраны: сплэш, логин, домашний, гильдии, чат - Модели: пользователи, гильдии, каналы, сообщения, роли - Сервисы: соединение, API, криптография, тема - Виджеты: иконки гильдий, сообщения, ввод чата - Web сборка (PWA) Документация: - AGENTS.md — контекст для ИИ ассистентов - docs/protocol.md — спецификация протокола
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import '../models/message.dart';
|
||||
|
||||
enum ConnectionState { disconnected, connecting, connected }
|
||||
|
||||
class ConnectionService extends ChangeNotifier {
|
||||
WebSocketChannel? _channel;
|
||||
ConnectionState _state = ConnectionState.disconnected;
|
||||
String _serverUrl = 'ws://localhost:8443/ws';
|
||||
String? _userId;
|
||||
String? _username;
|
||||
String? _token;
|
||||
Timer? _heartbeatTimer;
|
||||
int _seq = 0;
|
||||
|
||||
final StreamController<Map<String, dynamic>> _messageController =
|
||||
StreamController<Map<String, dynamic>>.broadcast();
|
||||
|
||||
Stream<Map<String, dynamic>> get messageStream => _messageController.stream;
|
||||
|
||||
ConnectionState get state => _state;
|
||||
String? get userId => _userId;
|
||||
String? get username => _username;
|
||||
String? get token => _token;
|
||||
bool get isConnected => _state == ConnectionState.connected;
|
||||
|
||||
String get serverUrl => _serverUrl;
|
||||
set serverUrl(String url) {
|
||||
_serverUrl = url;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
disconnect();
|
||||
_messageController.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void init() {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> connect() async {
|
||||
_state = ConnectionState.connecting;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_channel = WebSocketChannel.connect(Uri.parse(_serverUrl));
|
||||
await _channel!.ready;
|
||||
|
||||
_channel!.stream.listen(
|
||||
(data) {
|
||||
_handleMessage(data);
|
||||
},
|
||||
onError: (error) {
|
||||
_state = ConnectionState.disconnected;
|
||||
notifyListeners();
|
||||
},
|
||||
onDone: () {
|
||||
_state = ConnectionState.disconnected;
|
||||
_heartbeatTimer?.cancel();
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
|
||||
_state = ConnectionState.connected;
|
||||
_startHeartbeat();
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_state = ConnectionState.disconnected;
|
||||
notifyListeners();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void disconnect() {
|
||||
_heartbeatTimer?.cancel();
|
||||
_channel?.sink.close();
|
||||
_channel = null;
|
||||
_state = ConnectionState.disconnected;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _startHeartbeat() {
|
||||
_heartbeatTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||
send({'op': 0});
|
||||
});
|
||||
}
|
||||
|
||||
void _handleMessage(dynamic data) {
|
||||
try {
|
||||
final Map<String, dynamic> packet = jsonDecode(data as String);
|
||||
final int op = packet['op'] as int;
|
||||
|
||||
switch (op) {
|
||||
case 1: // Hello
|
||||
break;
|
||||
case 3: // Authenticated
|
||||
final d = packet['d'] as Map<String, dynamic>;
|
||||
_userId = d['user_id'] as String?;
|
||||
_username = d['username'] as String?;
|
||||
_token = d['token'] as String?;
|
||||
notifyListeners();
|
||||
break;
|
||||
case 10: // MessageCreate
|
||||
_messageController.add(packet['d'] as Map<String, dynamic>);
|
||||
break;
|
||||
case 13: // Reaction
|
||||
_messageController.add(packet['d'] as Map<String, dynamic>);
|
||||
break;
|
||||
case 40: // VoiceState
|
||||
_messageController.add(packet['d'] as Map<String, dynamic>);
|
||||
break;
|
||||
case 60: // Typing
|
||||
_messageController.add(packet['d'] as Map<String, dynamic>);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Failed to handle message: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void send(Map<String, dynamic> packet) {
|
||||
if (_channel == null) return;
|
||||
packet['s'] = ++_seq;
|
||||
_channel!.sink.add(jsonEncode(packet));
|
||||
}
|
||||
|
||||
void authenticate({String? token, String? username}) {
|
||||
send({
|
||||
'op': 2,
|
||||
'd': {
|
||||
if (token != null) 'token': token,
|
||||
if (username != null) 'username': username,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
void sendMessage({
|
||||
required String channelId,
|
||||
required Uint8List content,
|
||||
bool encrypted = false,
|
||||
Uint8List? nonce,
|
||||
String messageType = 'text',
|
||||
String? replyTo,
|
||||
}) {
|
||||
send({
|
||||
'op': 10,
|
||||
'd': {
|
||||
'channel_id': channelId,
|
||||
'content': content.toList(),
|
||||
'encrypted': encrypted,
|
||||
if (nonce != null) 'nonce': nonce.toList(),
|
||||
'message_type': messageType,
|
||||
if (replyTo != null) 'reply_to': replyTo,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
void sendReaction(String messageId, String emoji, {bool add = true}) {
|
||||
send({
|
||||
'op': 13,
|
||||
'd': {
|
||||
'message_id': messageId,
|
||||
'emoji': emoji,
|
||||
'add': add,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
void updateVoiceState({
|
||||
required String guildId,
|
||||
String? channelId,
|
||||
bool muted = false,
|
||||
bool deafened = false,
|
||||
}) {
|
||||
send({
|
||||
'op': 40,
|
||||
'd': {
|
||||
'guild_id': guildId,
|
||||
'channel_id': channelId ?? '',
|
||||
'muted': muted,
|
||||
'deafened': deafened,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
void sendTyping(String channelId) {
|
||||
send({
|
||||
'op': 60,
|
||||
'd': {'channel_id': channelId},
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user