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:
SashegDev
2026-06-06 22:39:14 +00:00
commit 096c4d0a2d
40 changed files with 5054 additions and 0 deletions
+199
View File
@@ -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},
});
}
}