096c4d0a2d
Серверная часть (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 — спецификация протокола
164 lines
4.9 KiB
Dart
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));
|
|
}
|
|
}
|