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,163 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user