Files
SashegDev 096c4d0a2d 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 — спецификация протокола
2026-06-06 22:39:14 +00:00

164 lines
4.9 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:http_parser/http_parser.dart';
import 'package:http/http.dart' as http;
import 'package:mime/mime.dart';
import '../models/guild.dart';
import '../models/channel.dart';
import '../models/message.dart';
import '../models/user.dart';
import '../models/role.dart';
class ApiService {
String _baseUrl = 'http://localhost:8443';
String? _token;
ApiService({String? baseUrl}) {
if (baseUrl != null) _baseUrl = baseUrl;
}
void setBaseUrl(String url) {
_baseUrl = url;
}
void updateFromWsUrl(String wsUrl) {
_baseUrl = wsUrl.replaceFirst('ws://', 'http://').replaceFirst('wss://', 'https://');
if (_baseUrl.endsWith('/ws')) {
_baseUrl = _baseUrl.substring(0, _baseUrl.length - 3);
}
}
Map<String, String> get _headers => {
'Content-Type': 'application/json',
if (_token != null) 'Authorization': 'Bearer $_token',
};
Future<bool> healthCheck() async {
try {
final resp = await http.get(
Uri.parse('$_baseUrl/api/health'),
).timeout(const Duration(seconds: 5));
return resp.statusCode == 200;
} catch (_) {
return false;
}
}
Future<List<Guild>> getGuilds(String userId) async {
final resp = await http.get(
Uri.parse('$_baseUrl/api/guilds?user_id=$userId'),
headers: _headers,
);
if (resp.statusCode != 200) throw Exception('Failed to load guilds');
final List<dynamic> data = jsonDecode(resp.body);
return data.map((g) => Guild.fromJson(g)).toList();
}
Future<String> createGuild(String name, String ownerId, {String? description}) async {
final resp = await http.post(
Uri.parse('$_baseUrl/api/guilds'),
headers: _headers,
body: jsonEncode({
'name': name,
'owner_id': ownerId,
'description': description ?? '',
}),
);
if (resp.statusCode != 201) throw Exception('Failed to create guild');
final data = jsonDecode(resp.body);
return data['id'] as String;
}
Future<Map<String, dynamic>> getGuild(String guildId) async {
final resp = await http.get(
Uri.parse('$_baseUrl/api/guilds/$guildId'),
headers: _headers,
);
if (resp.statusCode != 200) throw Exception('Failed to load guild');
return jsonDecode(resp.body);
}
Future<Channel> createChannel({
required String guildId,
required String name,
String type = 'text',
String? categoryId,
String? topic,
}) async {
final resp = await http.post(
Uri.parse('$_baseUrl/api/channels'),
headers: _headers,
body: jsonEncode({
'guild_id': guildId,
'category_id': categoryId ?? '',
'name': name,
'type': type,
'topic': topic ?? '',
'position': 0,
}),
);
if (resp.statusCode != 201) throw Exception('Failed to create channel');
return Channel.fromJson(jsonDecode(resp.body));
}
Future<List<Message>> getMessages(String channelId, {int limit = 50, String? before}) async {
final params = 'channel_id=$channelId&limit=$limit${before != null ? '&before=$before' : ''}';
final resp = await http.get(
Uri.parse('$_baseUrl/api/messages?$params'),
headers: _headers,
);
if (resp.statusCode != 200) throw Exception('Failed to load messages');
final List<dynamic> data = jsonDecode(resp.body);
return data.map((m) => Message.fromJson(m)).toList();
}
Future<String> uploadFile(File file) async {
final uri = Uri.parse('$_baseUrl/api/upload');
final request = http.MultipartRequest('POST', uri);
final mimeType = lookupMimeType(file.path) ?? 'application/octet-stream';
final parts = mimeType.split('/');
request.files.add(await http.MultipartFile.fromPath(
'file',
file.path,
contentType: MediaType(parts[0], parts[1]),
));
if (_token != null) request.headers['Authorization'] = 'Bearer $_token';
final resp = await request.send();
if (resp.statusCode != 200) throw Exception('Upload failed');
final data = jsonDecode(await resp.stream.bytesToString());
return data['url'] as String;
}
Future<User> getUser(String userId) async {
final resp = await http.get(
Uri.parse('$_baseUrl/api/users/$userId'),
headers: _headers,
);
if (resp.statusCode != 200) throw Exception('Failed to load user');
return User.fromJson(jsonDecode(resp.body));
}
Future<Role> createRole({
required String guildId,
required String name,
int color = 0,
List<String>? permissions,
}) async {
final resp = await http.post(
Uri.parse('$_baseUrl/api/roles/'),
headers: _headers,
body: jsonEncode({
'guild_id': guildId,
'name': name,
'color': color,
'position': 0,
'permissions': permissions ?? [],
}),
);
if (resp.statusCode != 201) throw Exception('Failed to create role');
return Role.fromJson(jsonDecode(resp.body));
}
}