Сделал подключение к серверу, добавил опкоды в opcode_map.dart и их значение, отправку sessionInit и пинги каждые 30 секунд

также доработал packet.dart (т.к. стандартная библа dart_lz4 не умеет работать с блоками)
добавил countries.dart (в будущем пригодится для подгрузки стран через которые доступен вход в MAX)
This commit is contained in:
klockky
2026-03-14 22:29:53 +03:00
parent 06ee2f92e5
commit ce718aad9a
13 changed files with 1430 additions and 83 deletions
+198
View File
@@ -0,0 +1,198 @@
import 'dart:async';
import 'dart:typed_data';
import '../core/config/config.dart';
import '../core/protocol/opcode_map.dart';
import '../core/protocol/packet.dart';
import '../core/transport/connection.dart';
import '../core/transport/dispatcher.dart';
import '../core/transport/receiver.dart';
import '../core/transport/sender.dart';
import '../core/utils/logger.dart';
enum SessionState { disconnected, connecting, connected, online }
/// Клиент API.
///
/// Подключение, хэндшейк, пинг, реконнект.
class Api {
final Connection _connection = Connection();
final PacketReceiver _receiver = PacketReceiver();
final PacketSender _sender = PacketSender();
final PacketDispatcher _dispatcher = PacketDispatcher();
SessionState _sessionState = SessionState.disconnected;
final _stateController = StreamController<SessionState>.broadcast();
Stream<SessionState> get stateStream => _stateController.stream;
SessionState get state => _sessionState;
StreamSubscription<Uint8List>? _dataSubscription;
StreamSubscription<SocketState>? _socketStateSubscription;
Timer? _pingTimer;
Timer? _reconnectTimer;
int _reconnectAttempts = 0;
bool _autoReconnect = false;
// Публичное API
/// Подключается к серверу, шлёт хэндшейк, запускает пинг.
Future<void> connect() async {
if (_sessionState != SessionState.disconnected) return;
_autoReconnect = true;
_setSessionState(SessionState.connecting);
_dataSubscription = _connection.dataStream.listen(_onDataReceived);
_socketStateSubscription = _connection.stateStream.listen((socketState) {
if (socketState == SocketState.disconnected &&
_sessionState != SessionState.disconnected) {
_onDisconnected();
}
});
try {
await _connection.connect(ServerConfig.host, ServerConfig.port);
} catch (e) {
logger.e('Не удалось подключиться: $e');
_cleanup();
_setSessionState(SessionState.disconnected);
_scheduleReconnect();
return;
}
_setSessionState(SessionState.connected);
_reconnectAttempts = 0;
try {
final response = await sendHandshake();
if (response.isOk) {
_setSessionState(SessionState.online);
_startPinging();
logger.i('Сессия онлайн, хэндшейк ок');
} else {
logger.e('Хэндшейк отклонён: ${response.payload}');
}
} catch (e) {
logger.e('Ошибка хэндшейка: $e');
}
}
/// Отключается без автореконнекта.
Future<void> disconnect() async {
_autoReconnect = false;
_reconnectTimer?.cancel();
_cleanup();
await _connection.disconnect();
_setSessionState(SessionState.disconnected);
}
/// Отправляет хэндшейк (opcode 6).
Future<Packet> sendHandshake() {
// TODO: заменить захардкоженные данные на реальные
final payload = <dynamic, dynamic>{
'mt_instanceid': '550e8400-e29b-41d4-a716-446655440000',
'clientSessionId': 42,
'deviceId': 'a1b2c3d4e5f6a7b8',
'userAgent': {
'deviceType': "ANDROID",
'locale': 'en',
'deviceLocale': 'en_US',
'osVersion': 'Ondroid 14',
'deviceName': 'KometPhone',
'appVersion': '26.8.1',
'screen': '1920x1080',
'timezone': 'Europe/Moscow',
'pushDeviceType': 'GCM',
'arch': 'arm64',
'buildNumber': 6606,
},
};
return sendRequest(Opcode.sessionInit, payload);
}
/// Отправляет запрос и ждёт ответ от сервера.
Future<Packet> sendRequest(
int opcode,
Map<dynamic, dynamic> payload,
) {
final seq = _sender.send(_connection, opcode, payload);
return _dispatcher.registerPending(seq).timeout(
ServerConfig.requestTimeout,
onTimeout: () =>
throw TimeoutException('${Opcode.name(opcode)} таймаут'),
);
}
/// Вешает обработчик на пуши с указанным опкодом.
void registerPushHandler(int opcode, void Function(Packet) handler) {
_dispatcher.registerHandler(opcode, handler);
}
/// Стрим всех входящих пушей от сервера.
Stream<Packet> get pushStream => _dispatcher.pushStream;
void dispose() {
_autoReconnect = false;
_reconnectTimer?.cancel();
_cleanup();
_dispatcher.dispose();
_connection.dispose();
_stateController.close();
}
// Внутрянка
void _setSessionState(SessionState state) {
if (_sessionState == state) return;
_sessionState = state;
_stateController.add(state);
logger.i('Сессия: ${state.name}');
}
void _onDataReceived(Uint8List data) {
for (final packet in _receiver.feed(data)) {
_dispatcher.dispatch(packet);
}
}
void _onDisconnected() {
_cleanup();
_setSessionState(SessionState.disconnected);
if (_autoReconnect) _scheduleReconnect();
}
void _cleanup() {
_pingTimer?.cancel();
_dataSubscription?.cancel();
_socketStateSubscription?.cancel();
_dataSubscription = null;
_socketStateSubscription = null;
_receiver.reset();
_dispatcher.clearPending();
}
void _startPinging() {
_pingTimer?.cancel();
_pingTimer = Timer.periodic(ServerConfig.pingInterval, (_) {
if (_connection.isConnected) {
_sender.send(_connection, Opcode.ping, {});
}
});
}
void _scheduleReconnect() {
if (_reconnectAttempts >= ServerConfig.maxReconnectAttempts) {
logger.e('Лимит попыток реконнекта');
return;
}
final delaySec = (2 * (1 << _reconnectAttempts)).clamp(2, 30);
_reconnectAttempts++;
logger.i('Реконнект через ${delaySec}с (попытка $_reconnectAttempts)');
_reconnectTimer?.cancel();
_reconnectTimer = Timer(Duration(seconds: delaySec), connect);
}
}
+7
View File
@@ -0,0 +1,7 @@
abstract class ServerConfig {
static const String host = 'api.oneme.ru';
static const int port = 443;
static const Duration pingInterval = Duration(seconds: 30);
static const Duration requestTimeout = Duration(seconds: 30);
static const int maxReconnectAttempts = 50;
}
+456
View File
@@ -0,0 +1,456 @@
import 'dart:convert';
class CountryName {
final String code; // ISO 3166-1 alpha-2, например "RU"
final String en;
final String ru;
const CountryName({
required this.code,
required this.en,
required this.ru,
});
}
/// Полный список стран (195 государств) с названием на русском и английском.
///
/// Источник данных:
/// https://github.com/stefangabos/world_countries
final List<CountryName> allCountries = _buildCountries();
/// Быстрый доступ по ISO-коду.
final Map<String, CountryName> countriesByCode = {
for (final country in allCountries) country.code: country,
};
/// Пример использования:
/// ```dart
/// final country = exampleCountryLookup('RU');
/// print(country?['ru']); // Россия
/// print(country?['en']); // Russian Federation
/// ```
Map<String, String>? exampleCountryLookup(String code) {
final country = countriesByCode[code.toUpperCase()];
if (country == null) return null;
return {
'code': country.code,
'ru': country.ru,
'en': country.en,
};
}
List<CountryName> _buildCountries() {
final enList = (jsonDecode(_countriesEnJson) as List)
.cast<Map<String, dynamic>>();
final ruList = (jsonDecode(_countriesRuJson) as List)
.cast<Map<String, dynamic>>();
final ruByCode = <String, String>{
for (final item in ruList)
(item['alpha2'] as String).toUpperCase(): item['name'] as String,
};
final countries = enList.map((item) {
final code = (item['alpha2'] as String).toUpperCase();
final en = item['name'] as String;
final ru = ruByCode[code] ?? en;
return CountryName(code: code, en: en, ru: ru);
}).toList();
countries.sort((a, b) => a.en.compareTo(b.en));
return countries;
}
const String _countriesEnJson = '''[
{"id":4,"alpha2":"af","alpha3":"afg","name":"Afghanistan"},
{"id":8,"alpha2":"al","alpha3":"alb","name":"Albania"},
{"id":12,"alpha2":"dz","alpha3":"dza","name":"Algeria"},
{"id":20,"alpha2":"ad","alpha3":"and","name":"Andorra"},
{"id":24,"alpha2":"ao","alpha3":"ago","name":"Angola"},
{"id":28,"alpha2":"ag","alpha3":"atg","name":"Antigua and Barbuda"},
{"id":32,"alpha2":"ar","alpha3":"arg","name":"Argentina"},
{"id":51,"alpha2":"am","alpha3":"arm","name":"Armenia"},
{"id":36,"alpha2":"au","alpha3":"aus","name":"Australia"},
{"id":40,"alpha2":"at","alpha3":"aut","name":"Austria"},
{"id":31,"alpha2":"az","alpha3":"aze","name":"Azerbaijan"},
{"id":44,"alpha2":"bs","alpha3":"bhs","name":"Bahamas"},
{"id":48,"alpha2":"bh","alpha3":"bhr","name":"Bahrain"},
{"id":50,"alpha2":"bd","alpha3":"bgd","name":"Bangladesh"},
{"id":52,"alpha2":"bb","alpha3":"brb","name":"Barbados"},
{"id":112,"alpha2":"by","alpha3":"blr","name":"Belarus"},
{"id":56,"alpha2":"be","alpha3":"bel","name":"Belgium"},
{"id":84,"alpha2":"bz","alpha3":"blz","name":"Belize"},
{"id":204,"alpha2":"bj","alpha3":"ben","name":"Benin"},
{"id":64,"alpha2":"bt","alpha3":"btn","name":"Bhutan"},
{"id":68,"alpha2":"bo","alpha3":"bol","name":"Bolivia, Plurinational State of"},
{"id":70,"alpha2":"ba","alpha3":"bih","name":"Bosnia and Herzegovina"},
{"id":72,"alpha2":"bw","alpha3":"bwa","name":"Botswana"},
{"id":76,"alpha2":"br","alpha3":"bra","name":"Brazil"},
{"id":96,"alpha2":"bn","alpha3":"brn","name":"Brunei Darussalam"},
{"id":100,"alpha2":"bg","alpha3":"bgr","name":"Bulgaria"},
{"id":854,"alpha2":"bf","alpha3":"bfa","name":"Burkina Faso"},
{"id":108,"alpha2":"bi","alpha3":"bdi","name":"Burundi"},
{"id":132,"alpha2":"cv","alpha3":"cpv","name":"Cabo Verde"},
{"id":116,"alpha2":"kh","alpha3":"khm","name":"Cambodia"},
{"id":120,"alpha2":"cm","alpha3":"cmr","name":"Cameroon"},
{"id":124,"alpha2":"ca","alpha3":"can","name":"Canada"},
{"id":140,"alpha2":"cf","alpha3":"caf","name":"Central African Republic"},
{"id":148,"alpha2":"td","alpha3":"tcd","name":"Chad"},
{"id":152,"alpha2":"cl","alpha3":"chl","name":"Chile"},
{"id":156,"alpha2":"cn","alpha3":"chn","name":"China"},
{"id":170,"alpha2":"co","alpha3":"col","name":"Colombia"},
{"id":174,"alpha2":"km","alpha3":"com","name":"Comoros"},
{"id":178,"alpha2":"cg","alpha3":"cog","name":"Congo"},
{"id":180,"alpha2":"cd","alpha3":"cod","name":"Congo, Democratic Republic of the"},
{"id":188,"alpha2":"cr","alpha3":"cri","name":"Costa Rica"},
{"id":384,"alpha2":"ci","alpha3":"civ","name":"Côte d'Ivoire"},
{"id":191,"alpha2":"hr","alpha3":"hrv","name":"Croatia"},
{"id":192,"alpha2":"cu","alpha3":"cub","name":"Cuba"},
{"id":196,"alpha2":"cy","alpha3":"cyp","name":"Cyprus"},
{"id":203,"alpha2":"cz","alpha3":"cze","name":"Czechia"},
{"id":208,"alpha2":"dk","alpha3":"dnk","name":"Denmark"},
{"id":262,"alpha2":"dj","alpha3":"dji","name":"Djibouti"},
{"id":212,"alpha2":"dm","alpha3":"dma","name":"Dominica"},
{"id":214,"alpha2":"do","alpha3":"dom","name":"Dominican Republic"},
{"id":218,"alpha2":"ec","alpha3":"ecu","name":"Ecuador"},
{"id":818,"alpha2":"eg","alpha3":"egy","name":"Egypt"},
{"id":222,"alpha2":"sv","alpha3":"slv","name":"El Salvador"},
{"id":226,"alpha2":"gq","alpha3":"gnq","name":"Equatorial Guinea"},
{"id":232,"alpha2":"er","alpha3":"eri","name":"Eritrea"},
{"id":233,"alpha2":"ee","alpha3":"est","name":"Estonia"},
{"id":748,"alpha2":"sz","alpha3":"swz","name":"Eswatini"},
{"id":231,"alpha2":"et","alpha3":"eth","name":"Ethiopia"},
{"id":242,"alpha2":"fj","alpha3":"fji","name":"Fiji"},
{"id":246,"alpha2":"fi","alpha3":"fin","name":"Finland"},
{"id":250,"alpha2":"fr","alpha3":"fra","name":"France"},
{"id":266,"alpha2":"ga","alpha3":"gab","name":"Gabon"},
{"id":270,"alpha2":"gm","alpha3":"gmb","name":"Gambia"},
{"id":268,"alpha2":"ge","alpha3":"geo","name":"Georgia"},
{"id":276,"alpha2":"de","alpha3":"deu","name":"Germany"},
{"id":288,"alpha2":"gh","alpha3":"gha","name":"Ghana"},
{"id":300,"alpha2":"gr","alpha3":"grc","name":"Greece"},
{"id":308,"alpha2":"gd","alpha3":"grd","name":"Grenada"},
{"id":320,"alpha2":"gt","alpha3":"gtm","name":"Guatemala"},
{"id":324,"alpha2":"gn","alpha3":"gin","name":"Guinea"},
{"id":624,"alpha2":"gw","alpha3":"gnb","name":"Guinea-Bissau"},
{"id":328,"alpha2":"gy","alpha3":"guy","name":"Guyana"},
{"id":332,"alpha2":"ht","alpha3":"hti","name":"Haiti"},
{"id":340,"alpha2":"hn","alpha3":"hnd","name":"Honduras"},
{"id":348,"alpha2":"hu","alpha3":"hun","name":"Hungary"},
{"id":352,"alpha2":"is","alpha3":"isl","name":"Iceland"},
{"id":356,"alpha2":"in","alpha3":"ind","name":"India"},
{"id":360,"alpha2":"id","alpha3":"idn","name":"Indonesia"},
{"id":364,"alpha2":"ir","alpha3":"irn","name":"Iran, Islamic Republic of"},
{"id":368,"alpha2":"iq","alpha3":"irq","name":"Iraq"},
{"id":372,"alpha2":"ie","alpha3":"irl","name":"Ireland"},
{"id":376,"alpha2":"il","alpha3":"isr","name":"Israel"},
{"id":380,"alpha2":"it","alpha3":"ita","name":"Italy"},
{"id":388,"alpha2":"jm","alpha3":"jam","name":"Jamaica"},
{"id":392,"alpha2":"jp","alpha3":"jpn","name":"Japan"},
{"id":400,"alpha2":"jo","alpha3":"jor","name":"Jordan"},
{"id":398,"alpha2":"kz","alpha3":"kaz","name":"Kazakhstan"},
{"id":404,"alpha2":"ke","alpha3":"ken","name":"Kenya"},
{"id":296,"alpha2":"ki","alpha3":"kir","name":"Kiribati"},
{"id":408,"alpha2":"kp","alpha3":"prk","name":"Korea, Democratic People's Republic of"},
{"id":410,"alpha2":"kr","alpha3":"kor","name":"Korea, Republic of"},
{"id":414,"alpha2":"kw","alpha3":"kwt","name":"Kuwait"},
{"id":417,"alpha2":"kg","alpha3":"kgz","name":"Kyrgyzstan"},
{"id":418,"alpha2":"la","alpha3":"lao","name":"Lao People's Democratic Republic"},
{"id":428,"alpha2":"lv","alpha3":"lva","name":"Latvia"},
{"id":422,"alpha2":"lb","alpha3":"lbn","name":"Lebanon"},
{"id":426,"alpha2":"ls","alpha3":"lso","name":"Lesotho"},
{"id":430,"alpha2":"lr","alpha3":"lbr","name":"Liberia"},
{"id":434,"alpha2":"ly","alpha3":"lby","name":"Libya"},
{"id":438,"alpha2":"li","alpha3":"lie","name":"Liechtenstein"},
{"id":440,"alpha2":"lt","alpha3":"ltu","name":"Lithuania"},
{"id":442,"alpha2":"lu","alpha3":"lux","name":"Luxembourg"},
{"id":450,"alpha2":"mg","alpha3":"mdg","name":"Madagascar"},
{"id":454,"alpha2":"mw","alpha3":"mwi","name":"Malawi"},
{"id":458,"alpha2":"my","alpha3":"mys","name":"Malaysia"},
{"id":462,"alpha2":"mv","alpha3":"mdv","name":"Maldives"},
{"id":466,"alpha2":"ml","alpha3":"mli","name":"Mali"},
{"id":470,"alpha2":"mt","alpha3":"mlt","name":"Malta"},
{"id":584,"alpha2":"mh","alpha3":"mhl","name":"Marshall Islands"},
{"id":478,"alpha2":"mr","alpha3":"mrt","name":"Mauritania"},
{"id":480,"alpha2":"mu","alpha3":"mus","name":"Mauritius"},
{"id":484,"alpha2":"mx","alpha3":"mex","name":"Mexico"},
{"id":583,"alpha2":"fm","alpha3":"fsm","name":"Micronesia, Federated States of"},
{"id":498,"alpha2":"md","alpha3":"mda","name":"Moldova, Republic of"},
{"id":492,"alpha2":"mc","alpha3":"mco","name":"Monaco"},
{"id":496,"alpha2":"mn","alpha3":"mng","name":"Mongolia"},
{"id":499,"alpha2":"me","alpha3":"mne","name":"Montenegro"},
{"id":504,"alpha2":"ma","alpha3":"mar","name":"Morocco"},
{"id":508,"alpha2":"mz","alpha3":"moz","name":"Mozambique"},
{"id":104,"alpha2":"mm","alpha3":"mmr","name":"Myanmar"},
{"id":516,"alpha2":"na","alpha3":"nam","name":"Namibia"},
{"id":520,"alpha2":"nr","alpha3":"nru","name":"Nauru"},
{"id":524,"alpha2":"np","alpha3":"npl","name":"Nepal"},
{"id":528,"alpha2":"nl","alpha3":"nld","name":"Netherlands"},
{"id":554,"alpha2":"nz","alpha3":"nzl","name":"New Zealand"},
{"id":558,"alpha2":"ni","alpha3":"nic","name":"Nicaragua"},
{"id":562,"alpha2":"ne","alpha3":"ner","name":"Niger"},
{"id":566,"alpha2":"ng","alpha3":"nga","name":"Nigeria"},
{"id":807,"alpha2":"mk","alpha3":"mkd","name":"North Macedonia"},
{"id":578,"alpha2":"no","alpha3":"nor","name":"Norway"},
{"id":512,"alpha2":"om","alpha3":"omn","name":"Oman"},
{"id":586,"alpha2":"pk","alpha3":"pak","name":"Pakistan"},
{"id":585,"alpha2":"pw","alpha3":"plw","name":"Palau"},
{"id":591,"alpha2":"pa","alpha3":"pan","name":"Panama"},
{"id":598,"alpha2":"pg","alpha3":"png","name":"Papua New Guinea"},
{"id":600,"alpha2":"py","alpha3":"pry","name":"Paraguay"},
{"id":604,"alpha2":"pe","alpha3":"per","name":"Peru"},
{"id":608,"alpha2":"ph","alpha3":"phl","name":"Philippines"},
{"id":616,"alpha2":"pl","alpha3":"pol","name":"Poland"},
{"id":620,"alpha2":"pt","alpha3":"prt","name":"Portugal"},
{"id":634,"alpha2":"qa","alpha3":"qat","name":"Qatar"},
{"id":642,"alpha2":"ro","alpha3":"rou","name":"Romania"},
{"id":643,"alpha2":"ru","alpha3":"rus","name":"Russian Federation"},
{"id":646,"alpha2":"rw","alpha3":"rwa","name":"Rwanda"},
{"id":659,"alpha2":"kn","alpha3":"kna","name":"Saint Kitts and Nevis"},
{"id":662,"alpha2":"lc","alpha3":"lca","name":"Saint Lucia"},
{"id":670,"alpha2":"vc","alpha3":"vct","name":"Saint Vincent and the Grenadines"},
{"id":882,"alpha2":"ws","alpha3":"wsm","name":"Samoa"},
{"id":674,"alpha2":"sm","alpha3":"smr","name":"San Marino"},
{"id":678,"alpha2":"st","alpha3":"stp","name":"Sao Tome and Principe"},
{"id":682,"alpha2":"sa","alpha3":"sau","name":"Saudi Arabia"},
{"id":686,"alpha2":"sn","alpha3":"sen","name":"Senegal"},
{"id":688,"alpha2":"rs","alpha3":"srb","name":"Serbia"},
{"id":690,"alpha2":"sc","alpha3":"syc","name":"Seychelles"},
{"id":694,"alpha2":"sl","alpha3":"sle","name":"Sierra Leone"},
{"id":702,"alpha2":"sg","alpha3":"sgp","name":"Singapore"},
{"id":703,"alpha2":"sk","alpha3":"svk","name":"Slovakia"},
{"id":705,"alpha2":"si","alpha3":"svn","name":"Slovenia"},
{"id":90,"alpha2":"sb","alpha3":"slb","name":"Solomon Islands"},
{"id":706,"alpha2":"so","alpha3":"som","name":"Somalia"},
{"id":710,"alpha2":"za","alpha3":"zaf","name":"South Africa"},
{"id":728,"alpha2":"ss","alpha3":"ssd","name":"South Sudan"},
{"id":724,"alpha2":"es","alpha3":"esp","name":"Spain"},
{"id":144,"alpha2":"lk","alpha3":"lka","name":"Sri Lanka"},
{"id":729,"alpha2":"sd","alpha3":"sdn","name":"Sudan"},
{"id":740,"alpha2":"sr","alpha3":"sur","name":"Suriname"},
{"id":752,"alpha2":"se","alpha3":"swe","name":"Sweden"},
{"id":756,"alpha2":"ch","alpha3":"che","name":"Switzerland"},
{"id":760,"alpha2":"sy","alpha3":"syr","name":"Syrian Arab Republic"},
{"id":762,"alpha2":"tj","alpha3":"tjk","name":"Tajikistan"},
{"id":834,"alpha2":"tz","alpha3":"tza","name":"Tanzania, United Republic of"},
{"id":764,"alpha2":"th","alpha3":"tha","name":"Thailand"},
{"id":626,"alpha2":"tl","alpha3":"tls","name":"Timor-Leste"},
{"id":768,"alpha2":"tg","alpha3":"tgo","name":"Togo"},
{"id":776,"alpha2":"to","alpha3":"ton","name":"Tonga"},
{"id":780,"alpha2":"tt","alpha3":"tto","name":"Trinidad and Tobago"},
{"id":788,"alpha2":"tn","alpha3":"tun","name":"Tunisia"},
{"id":792,"alpha2":"tr","alpha3":"tur","name":"Türkiye"},
{"id":795,"alpha2":"tm","alpha3":"tkm","name":"Turkmenistan"},
{"id":798,"alpha2":"tv","alpha3":"tuv","name":"Tuvalu"},
{"id":800,"alpha2":"ug","alpha3":"uga","name":"Uganda"},
{"id":804,"alpha2":"ua","alpha3":"ukr","name":"Ukraine"},
{"id":784,"alpha2":"ae","alpha3":"are","name":"United Arab Emirates"},
{"id":826,"alpha2":"gb","alpha3":"gbr","name":"United Kingdom of Great Britain and Northern Ireland"},
{"id":840,"alpha2":"us","alpha3":"usa","name":"United States of America"},
{"id":858,"alpha2":"uy","alpha3":"ury","name":"Uruguay"},
{"id":860,"alpha2":"uz","alpha3":"uzb","name":"Uzbekistan"},
{"id":548,"alpha2":"vu","alpha3":"vut","name":"Vanuatu"},
{"id":862,"alpha2":"ve","alpha3":"ven","name":"Venezuela, Bolivarian Republic of"},
{"id":704,"alpha2":"vn","alpha3":"vnm","name":"Viet Nam"},
{"id":887,"alpha2":"ye","alpha3":"yem","name":"Yemen"},
{"id":894,"alpha2":"zm","alpha3":"zmb","name":"Zambia"},
{"id":716,"alpha2":"zw","alpha3":"zwe","name":"Zimbabwe"}
]''';
const String _countriesRuJson = '''[
{"id":36,"alpha2":"au","alpha3":"aus","name":"Австралия"},
{"id":40,"alpha2":"at","alpha3":"aut","name":"Австрия"},
{"id":31,"alpha2":"az","alpha3":"aze","name":"Азербайджан"},
{"id":8,"alpha2":"al","alpha3":"alb","name":"Албания"},
{"id":12,"alpha2":"dz","alpha3":"dza","name":"Алжир"},
{"id":24,"alpha2":"ao","alpha3":"ago","name":"Ангола"},
{"id":20,"alpha2":"ad","alpha3":"and","name":"Андорра"},
{"id":28,"alpha2":"ag","alpha3":"atg","name":"Антигуа и Барбуда"},
{"id":32,"alpha2":"ar","alpha3":"arg","name":"Аргентина"},
{"id":51,"alpha2":"am","alpha3":"arm","name":"Армения"},
{"id":4,"alpha2":"af","alpha3":"afg","name":"Афганистан"},
{"id":44,"alpha2":"bs","alpha3":"bhs","name":"Багамские Острова"},
{"id":50,"alpha2":"bd","alpha3":"bgd","name":"Бангладеш"},
{"id":52,"alpha2":"bb","alpha3":"brb","name":"Барбадос"},
{"id":48,"alpha2":"bh","alpha3":"bhr","name":"Бахрейн"},
{"id":84,"alpha2":"bz","alpha3":"blz","name":"Белиз"},
{"id":112,"alpha2":"by","alpha3":"blr","name":"Беларусь"},
{"id":56,"alpha2":"be","alpha3":"bel","name":"Бельгия"},
{"id":204,"alpha2":"bj","alpha3":"ben","name":"Бенин"},
{"id":100,"alpha2":"bg","alpha3":"bgr","name":"Болгария"},
{"id":68,"alpha2":"bo","alpha3":"bol","name":"Боливия"},
{"id":70,"alpha2":"ba","alpha3":"bih","name":"Босния и Герцеговина"},
{"id":72,"alpha2":"bw","alpha3":"bwa","name":"Ботсвана"},
{"id":76,"alpha2":"br","alpha3":"bra","name":"Бразилия"},
{"id":96,"alpha2":"bn","alpha3":"brn","name":"Бруней"},
{"id":854,"alpha2":"bf","alpha3":"bfa","name":"Буркина-Фасо"},
{"id":108,"alpha2":"bi","alpha3":"bdi","name":"Бурунди"},
{"id":64,"alpha2":"bt","alpha3":"btn","name":"Бутан"},
{"id":548,"alpha2":"vu","alpha3":"vut","name":"Вануату"},
{"id":826,"alpha2":"gb","alpha3":"gbr","name":"Великобритания"},
{"id":348,"alpha2":"hu","alpha3":"hun","name":"Венгрия"},
{"id":862,"alpha2":"ve","alpha3":"ven","name":"Венесуэла"},
{"id":626,"alpha2":"tl","alpha3":"tls","name":"Восточный Тимор"},
{"id":704,"alpha2":"vn","alpha3":"vnm","name":"Вьетнам"},
{"id":266,"alpha2":"ga","alpha3":"gab","name":"Габон"},
{"id":332,"alpha2":"ht","alpha3":"hti","name":"Гаити"},
{"id":328,"alpha2":"gy","alpha3":"guy","name":"Гайана"},
{"id":270,"alpha2":"gm","alpha3":"gmb","name":"Гамбия"},
{"id":288,"alpha2":"gh","alpha3":"gha","name":"Гана"},
{"id":320,"alpha2":"gt","alpha3":"gtm","name":"Гватемала"},
{"id":324,"alpha2":"gn","alpha3":"gin","name":"Гвинея"},
{"id":624,"alpha2":"gw","alpha3":"gnb","name":"Гвинея-Бисау"},
{"id":276,"alpha2":"de","alpha3":"deu","name":"Германия"},
{"id":340,"alpha2":"hn","alpha3":"hnd","name":"Гондурас"},
{"id":308,"alpha2":"gd","alpha3":"grd","name":"Гренада"},
{"id":300,"alpha2":"gr","alpha3":"grc","name":"Греция"},
{"id":268,"alpha2":"ge","alpha3":"geo","name":"Грузия"},
{"id":208,"alpha2":"dk","alpha3":"dnk","name":"Дания"},
{"id":262,"alpha2":"dj","alpha3":"dji","name":"Джибути"},
{"id":212,"alpha2":"dm","alpha3":"dma","name":"Доминика"},
{"id":214,"alpha2":"do","alpha3":"dom","name":"Доминиканская Республика"},
{"id":180,"alpha2":"cd","alpha3":"cod","name":"ДР Конго"},
{"id":818,"alpha2":"eg","alpha3":"egy","name":"Египет"},
{"id":894,"alpha2":"zm","alpha3":"zmb","name":"Замбия"},
{"id":716,"alpha2":"zw","alpha3":"zwe","name":"Зимбабве"},
{"id":376,"alpha2":"il","alpha3":"isr","name":"Израиль"},
{"id":356,"alpha2":"in","alpha3":"ind","name":"Индия"},
{"id":360,"alpha2":"id","alpha3":"idn","name":"Индонезия"},
{"id":400,"alpha2":"jo","alpha3":"jor","name":"Иордания"},
{"id":368,"alpha2":"iq","alpha3":"irq","name":"Ирак"},
{"id":364,"alpha2":"ir","alpha3":"irn","name":"Иран"},
{"id":372,"alpha2":"ie","alpha3":"irl","name":"Ирландия"},
{"id":352,"alpha2":"is","alpha3":"isl","name":"Исландия"},
{"id":724,"alpha2":"es","alpha3":"esp","name":"Испания"},
{"id":380,"alpha2":"it","alpha3":"ita","name":"Италия"},
{"id":887,"alpha2":"ye","alpha3":"yem","name":"Йемен"},
{"id":132,"alpha2":"cv","alpha3":"cpv","name":"Кабо-Верде"},
{"id":398,"alpha2":"kz","alpha3":"kaz","name":"Казахстан"},
{"id":116,"alpha2":"kh","alpha3":"khm","name":"Камбоджа"},
{"id":120,"alpha2":"cm","alpha3":"cmr","name":"Камерун"},
{"id":124,"alpha2":"ca","alpha3":"can","name":"Канада"},
{"id":634,"alpha2":"qa","alpha3":"qat","name":"Катар"},
{"id":404,"alpha2":"ke","alpha3":"ken","name":"Кения"},
{"id":196,"alpha2":"cy","alpha3":"cyp","name":"Кипр"},
{"id":417,"alpha2":"kg","alpha3":"kgz","name":"Кыргызстан"},
{"id":296,"alpha2":"ki","alpha3":"kir","name":"Кирибати"},
{"id":408,"alpha2":"kp","alpha3":"prk","name":"КНДР (Корейская Народно-Демократическая Республика)"},
{"id":156,"alpha2":"cn","alpha3":"chn","name":"Китай (Китайская Народная Республика)"},
{"id":170,"alpha2":"co","alpha3":"col","name":"Колумбия"},
{"id":174,"alpha2":"km","alpha3":"com","name":"Коморы"},
{"id":188,"alpha2":"cr","alpha3":"cri","name":"Коста-Рика"},
{"id":384,"alpha2":"ci","alpha3":"civ","name":"Кот-д’Ивуар"},
{"id":192,"alpha2":"cu","alpha3":"cub","name":"Куба"},
{"id":414,"alpha2":"kw","alpha3":"kwt","name":"Кувейт"},
{"id":418,"alpha2":"la","alpha3":"lao","name":"Лаос"},
{"id":428,"alpha2":"lv","alpha3":"lva","name":"Латвия"},
{"id":426,"alpha2":"ls","alpha3":"lso","name":"Лесото"},
{"id":430,"alpha2":"lr","alpha3":"lbr","name":"Либерия"},
{"id":422,"alpha2":"lb","alpha3":"lbn","name":"Ливан"},
{"id":434,"alpha2":"ly","alpha3":"lby","name":"Ливия"},
{"id":440,"alpha2":"lt","alpha3":"ltu","name":"Литва"},
{"id":438,"alpha2":"li","alpha3":"lie","name":"Лихтенштейн"},
{"id":442,"alpha2":"lu","alpha3":"lux","name":"Люксембург"},
{"id":480,"alpha2":"mu","alpha3":"mus","name":"Маврикий"},
{"id":478,"alpha2":"mr","alpha3":"mrt","name":"Мавритания"},
{"id":450,"alpha2":"mg","alpha3":"mdg","name":"Мадагаскар"},
{"id":807,"alpha2":"mk","alpha3":"mkd","name":"Северная Македония"},
{"id":454,"alpha2":"mw","alpha3":"mwi","name":"Малави"},
{"id":458,"alpha2":"my","alpha3":"mys","name":"Малайзия"},
{"id":466,"alpha2":"ml","alpha3":"mli","name":"Мали"},
{"id":462,"alpha2":"mv","alpha3":"mdv","name":"Мальдивы"},
{"id":470,"alpha2":"mt","alpha3":"mlt","name":"Мальта"},
{"id":504,"alpha2":"ma","alpha3":"mar","name":"Марокко"},
{"id":584,"alpha2":"mh","alpha3":"mhl","name":"Маршалловы Острова"},
{"id":484,"alpha2":"mx","alpha3":"mex","name":"Мексика"},
{"id":583,"alpha2":"fm","alpha3":"fsm","name":"Микронезия"},
{"id":508,"alpha2":"mz","alpha3":"moz","name":"Мозамбик"},
{"id":498,"alpha2":"md","alpha3":"mda","name":"Молдова"},
{"id":492,"alpha2":"mc","alpha3":"mco","name":"Монако"},
{"id":496,"alpha2":"mn","alpha3":"mng","name":"Монголия"},
{"id":104,"alpha2":"mm","alpha3":"mmr","name":"Мьянма"},
{"id":516,"alpha2":"na","alpha3":"nam","name":"Намибия"},
{"id":520,"alpha2":"nr","alpha3":"nru","name":"Науру"},
{"id":524,"alpha2":"np","alpha3":"npl","name":"Непал"},
{"id":562,"alpha2":"ne","alpha3":"ner","name":"Нигер"},
{"id":566,"alpha2":"ng","alpha3":"nga","name":"Нигерия"},
{"id":528,"alpha2":"nl","alpha3":"nld","name":"Нидерланды"},
{"id":558,"alpha2":"ni","alpha3":"nic","name":"Никарагуа"},
{"id":554,"alpha2":"nz","alpha3":"nzl","name":"Новая Зеландия"},
{"id":578,"alpha2":"no","alpha3":"nor","name":"Норвегия"},
{"id":784,"alpha2":"ae","alpha3":"are","name":"ОАЭ"},
{"id":512,"alpha2":"om","alpha3":"omn","name":"Оман"},
{"id":586,"alpha2":"pk","alpha3":"pak","name":"Пакистан"},
{"id":585,"alpha2":"pw","alpha3":"plw","name":"Палау"},
{"id":591,"alpha2":"pa","alpha3":"pan","name":"Панама"},
{"id":598,"alpha2":"pg","alpha3":"png","name":"Папуа — Новая Гвинея"},
{"id":600,"alpha2":"py","alpha3":"pry","name":"Парагвай"},
{"id":604,"alpha2":"pe","alpha3":"per","name":"Перу"},
{"id":616,"alpha2":"pl","alpha3":"pol","name":"Польша"},
{"id":620,"alpha2":"pt","alpha3":"prt","name":"Португалия"},
{"id":178,"alpha2":"cg","alpha3":"cog","name":"Республика Конго"},
{"id":410,"alpha2":"kr","alpha3":"kor","name":"Республика Корея"},
{"id":643,"alpha2":"ru","alpha3":"rus","name":"Россия"},
{"id":646,"alpha2":"rw","alpha3":"rwa","name":"Руанда"},
{"id":642,"alpha2":"ro","alpha3":"rou","name":"Румыния"},
{"id":222,"alpha2":"sv","alpha3":"slv","name":"Сальвадор"},
{"id":882,"alpha2":"ws","alpha3":"wsm","name":"Самоа"},
{"id":674,"alpha2":"sm","alpha3":"smr","name":"Сан-Марино"},
{"id":678,"alpha2":"st","alpha3":"stp","name":"Сан-Томе и Принсипи"},
{"id":682,"alpha2":"sa","alpha3":"sau","name":"Саудовская Аравия"},
{"id":690,"alpha2":"sc","alpha3":"syc","name":"Сейшельские Острова"},
{"id":686,"alpha2":"sn","alpha3":"sen","name":"Сенегал"},
{"id":670,"alpha2":"vc","alpha3":"vct","name":"Сент-Винсент и Гренадины"},
{"id":659,"alpha2":"kn","alpha3":"kna","name":"Сент-Китс и Невис"},
{"id":662,"alpha2":"lc","alpha3":"lca","name":"Сент-Люсия"},
{"id":688,"alpha2":"rs","alpha3":"srb","name":"Сербия"},
{"id":702,"alpha2":"sg","alpha3":"sgp","name":"Сингапур"},
{"id":760,"alpha2":"sy","alpha3":"syr","name":"Сирия"},
{"id":703,"alpha2":"sk","alpha3":"svk","name":"Словакия"},
{"id":705,"alpha2":"si","alpha3":"svn","name":"Словения"},
{"id":90,"alpha2":"sb","alpha3":"slb","name":"Соломоновы Острова"},
{"id":706,"alpha2":"so","alpha3":"som","name":"Сомали"},
{"id":729,"alpha2":"sd","alpha3":"sdn","name":"Судан"},
{"id":740,"alpha2":"sr","alpha3":"sur","name":"Суринам"},
{"id":840,"alpha2":"us","alpha3":"usa","name":"США"},
{"id":694,"alpha2":"sl","alpha3":"sle","name":"Сьерра-Леоне"},
{"id":762,"alpha2":"tj","alpha3":"tjk","name":"Таджикистан"},
{"id":764,"alpha2":"th","alpha3":"tha","name":"Таиланд"},
{"id":834,"alpha2":"tz","alpha3":"tza","name":"Танзания"},
{"id":768,"alpha2":"tg","alpha3":"tgo","name":"Того"},
{"id":776,"alpha2":"to","alpha3":"ton","name":"Тонга"},
{"id":780,"alpha2":"tt","alpha3":"tto","name":"Тринидад и Тобаго"},
{"id":798,"alpha2":"tv","alpha3":"tuv","name":"Тувалу"},
{"id":788,"alpha2":"tn","alpha3":"tun","name":"Тунис"},
{"id":795,"alpha2":"tm","alpha3":"tkm","name":"Туркменистан"},
{"id":792,"alpha2":"tr","alpha3":"tur","name":"Турция"},
{"id":800,"alpha2":"ug","alpha3":"uga","name":"Уганда"},
{"id":860,"alpha2":"uz","alpha3":"uzb","name":"Узбекистан"},
{"id":804,"alpha2":"ua","alpha3":"ukr","name":"Украина"},
{"id":858,"alpha2":"uy","alpha3":"ury","name":"Уругвай"},
{"id":242,"alpha2":"fj","alpha3":"fji","name":"Фиджи"},
{"id":608,"alpha2":"ph","alpha3":"phl","name":"Филиппины"},
{"id":246,"alpha2":"fi","alpha3":"fin","name":"Финляндия"},
{"id":250,"alpha2":"fr","alpha3":"fra","name":"Франция"},
{"id":191,"alpha2":"hr","alpha3":"hrv","name":"Хорватия"},
{"id":140,"alpha2":"cf","alpha3":"caf","name":"ЦАР"},
{"id":148,"alpha2":"td","alpha3":"tcd","name":"Чад"},
{"id":499,"alpha2":"me","alpha3":"mne","name":"Черногория"},
{"id":203,"alpha2":"cz","alpha3":"cze","name":"Чехия"},
{"id":152,"alpha2":"cl","alpha3":"chl","name":"Чили"},
{"id":756,"alpha2":"ch","alpha3":"che","name":"Швейцария"},
{"id":752,"alpha2":"se","alpha3":"swe","name":"Швеция"},
{"id":144,"alpha2":"lk","alpha3":"lka","name":"Шри-Ланка"},
{"id":218,"alpha2":"ec","alpha3":"ecu","name":"Эквадор"},
{"id":226,"alpha2":"gq","alpha3":"gnq","name":"Экваториальная Гвинея"},
{"id":232,"alpha2":"er","alpha3":"eri","name":"Эритрея"},
{"id":748,"alpha2":"sz","alpha3":"swz","name":"Эсватини"},
{"id":233,"alpha2":"ee","alpha3":"est","name":"Эстония"},
{"id":231,"alpha2":"et","alpha3":"eth","name":"Эфиопия"},
{"id":710,"alpha2":"za","alpha3":"zaf","name":"ЮАР"},
{"id":728,"alpha2":"ss","alpha3":"ssd","name":"Южный Судан"},
{"id":388,"alpha2":"jm","alpha3":"jam","name":"Ямайка"},
{"id":392,"alpha2":"jp","alpha3":"jpn","name":"Япония"}
]''';
+356
View File
@@ -0,0 +1,356 @@
/// All protocol operation codes.
///
/// Naming follows the server-side convention.
/// Use [Opcode.name] to get a human-readable label for logging.
///
/// файл писла нейронка (я че ебанутый чтоль чтобы вручную хуярить опкоды и их значения)
abstract class Opcode {
// ── Session ────────────────────────────────────────────────────────
static const int ping = 1; // Пинг
static const int debug = 2; // Отладка / понг
static const int reconnect = 3; // Реконнект
static const int log = 5; // Аналитика / события
static const int sessionInit = 6; // Инициализация сессии (хэндшейк)
// ── Profile ────────────────────────────────────────────────────────
static const int profile = 16; // Обновление профиля
// ── Auth ───────────────────────────────────────────────────────────
static const int authRequest = 17; // Запрос OTP-кода
static const int auth = 18; // Проверка OTP-кода
static const int login = 19; // Вход (загрузка чатов/контактов)
static const int logout = 20; // Выход из аккаунта
static const int sync = 21; // Синхронизация данных
static const int config = 22; // Настройки приватности / конфиг
static const int authConfirm = 23; // Завершение регистрации
// ── Auth 2FA ───────────────────────────────────────────────────────
static const int authLoginRestorePassword = 101; // Восстановление пароля
static const int auth2faDetails = 104; // Детали 2FA
static const int externalCallback = 105; // Внешний коллбэк
static const int authValidatePassword = 107; // Валидация пароля
static const int authValidateHint = 108; // Валидация подсказки пароля
static const int authVerifyEmail = 109; // Верификация email
static const int authCheckEmail = 110; // Проверка email
static const int authSet2fa = 111; // Установка 2FA
static const int authCreateTrack = 112; // Создание трека авторизации
static const int authCheckPassword = 113; // Проверка пароля при регистрации
static const int authLoginCheckPassword = 115; // Проверка пароля при входе
static const int authLoginProfileDelete = 116; // Удаление профиля при входе
// ── Assets (стикеры) ───────────────────────────────────────────────
static const int presetAvatars = 25; // Пресетные аватарки
static const int assetsGet = 26; // Получение стикерпаков
static const int assetsUpdate = 27; // Синхронизация стикеров
static const int assetsGetByIds = 28; // Стикерпаки по ID
static const int assetsAdd = 29; // Добавление стикерпака
static const int assetsRemove = 259; // Удаление стикерпака
static const int assetsMove = 260; // Перемещение стикерпака
static const int assetsListModify = 261; // Изменение списка стикерпаков
// ── Contacts ───────────────────────────────────────────────────────
static const int contactInfo = 32; // Информация о контакте
static const int contactAdd = 33; // Добавление контакта
static const int contactUpdate = 34; // Обновление контакта (блок и т.д.)
static const int contactPresence = 35; // Запрос присутствия по ID
static const int contactList = 36; // Список заблокированных
static const int contactSearch = 37; // Поиск контакта
static const int contactMutual = 38; // Общие контакты
static const int contactPhotos = 39; // Фото контакта
static const int contactSort = 40; // Сортировка контактов
static const int contactVerify = 42; // Верификация контакта
static const int removeContactPhoto = 43; // Удаление фото контакта
static const int contactInfoByPhone = 46; // Поиск контакта по номеру
// ── Chats ──────────────────────────────────────────────────────────
static const int chatInfo = 48; // Информация о чате / создание группы
static const int chatHistory = 49; // История сообщений
static const int chatMark = 50; // Отметка прочитанным
static const int chatMedia = 51; // Медиа чата
static const int chatDelete = 52; // Удаление чата
static const int chatsList = 53; // Список чатов
static const int chatClear = 54; // Очистка истории чата
static const int chatUpdate = 55; // Обновление настроек чата
static const int chatCheckLink = 56; // Проверка ссылки чата
static const int chatJoin = 57; // Вступление в группу / канал
static const int chatLeave = 58; // Выход из чата
static const int chatMembers = 59; // Участники группы
static const int publicSearch = 60; // Глобальный поиск
static const int chatPersonalConfig = 61; // Персональные настройки чата
static const int chatCreate = 63; // Создание чата
// ── Messages ───────────────────────────────────────────────────────
static const int msgSend = 64; // Отправка сообщения
static const int msgTyping = 65; // Индикатор набора текста
static const int msgDelete = 66; // Удаление сообщения
static const int msgEdit = 67; // Редактирование сообщения
static const int chatSearch = 68; // Поиск по чату
static const int msgSharePreview = 70; // Превью ссылки
static const int msgGet = 71; // Получение сообщения по ID
static const int msgSearchTouch = 72; // Точечный поиск сообщений
static const int msgSearch = 73; // Поиск сообщений
static const int msgGetStat = 74; // Статистика сообщения
static const int chatSubscribe = 75; // Подписка на обновления чата
static const int msgDeleteRange = 92; // Удаление диапазона сообщений
// ── Reactions ──────────────────────────────────────────────────────
static const int msgReaction = 178; // Отправка реакции
static const int msgCancelReaction = 179; // Отмена реакции
static const int msgGetReactions = 180; // Получение реакций
static const int msgGetDetailedReactions = 181; // Детальные реакции
static const int chatReactionsSettingsSet = 257; // Установка настроек реакций
static const int reactionsSettingsGetByChatId = 258; // Настройки реакций чата
// ── Calls & Video ──────────────────────────────────────────────────
static const int videoChatStart = 76; // Начало видеочата
static const int chatMembersUpdate = 77; // Обновление участников / добавление
static const int videoChatStartActive = 78; // Инициация активного звонка
static const int videoChatHistory = 79; // История звонков
static const int videoChatCreateJoinLink = 84; // Ссылка для входа в видеочат
static const int videoChatMembers = 195; // Участники видеочата
static const int getInboundCalls = 103; // Входящие звонки
// ── Media & Files ──────────────────────────────────────────────────
static const int photoUpload = 80; // Загрузка фото
static const int stickerUpload = 81; // Загрузка стикера
static const int videoUpload = 82; // Загрузка видео
static const int videoPlay = 83; // URL видео
static const int chatPinSetVisibility = 86; // Видимость закрепов
static const int fileUpload = 87; // Загрузка файла
static const int fileDownload = 88; // Скачивание файла
static const int linkInfo = 89; // Информация по ссылке / вход в канал
static const int audioPlay = 301; // Воспроизведение аудио
// ── Sessions ───────────────────────────────────────────────────────
static const int sessionsInfo = 96; // Запрос активных сессий
static const int sessionsClose = 97; // Закрытие всех сессий
static const int phoneBind_request = 98; // Запрос привязки телефона
static const int phoneBindConfirm = 99; // Подтверждение привязки телефона
// ── Bots ───────────────────────────────────────────────────────────
static const int chatComplain = 117; // Жалоба на чат
static const int msgSendCallback = 118; // Коллбэк бота
static const int suspendBot = 119; // Приостановка бота
static const int chatBotCommands = 144; // Команды бота
static const int botInfo = 145; // Информация о боте
// ── Location ───────────────────────────────────────────────────────
static const int locationStop = 124; // Остановка трансляции геолокации
// ── Mentions ───────────────────────────────────────────────────────
static const int getLastMentions = 127; // Последние упоминания
// ── Stickers (creation) ────────────────────────────────────────────
static const int stickerCreate = 193; // Создание стикера
static const int stickerSuggest = 194; // Предложение стикера
// ── Notifications (push от сервера) ────────────────────────────────
static const int notifMessage = 128; // Новое сообщение
static const int notifTyping = 129; // Индикатор набора
static const int notifMark = 130; // Прочитано
static const int notifContact = 131; // Обновление контакта
static const int notifPresence = 132; // Статус онлайн
static const int notifConfig = 134; // Обновление конфига
static const int notifChat = 135; // Обновление чата
static const int notifAttach = 136; // Загрузка файла завершена
static const int notifCallStart = 137; // Входящий звонок
static const int notifContactSort = 139; // Пересортировка контактов
static const int notifMsgDeleteRange = 140; // Удаление диапазона
static const int notifMsgDelete = 142; // Удаление сообщения
static const int notifCallbackAnswer = 143; // Ответ бота
static const int notifLocation = 147; // Обновление геолокации
static const int notifLocationRequest = 148; // Запрос геолокации
static const int notifAssetsUpdate = 150; // Обновление стикеров
static const int notifDraft = 152; // Черновик
static const int notifDraftDiscard = 153; // Сброс черновика
static const int notifMsgDelayed = 154; // Отложенное сообщение
static const int notifMsgReactionsChanged = 155; // Изменение реакций
static const int notifMsgYouReacted = 156; // Ваша реакция
static const int notifProfile = 159; // Обновление профиля
static const int notifBanners = 292; // Баннеры
static const int notifFolders = 277; // Обновление папок
// ── Misc ───────────────────────────────────────────────────────────
static const int okToken = 158; // OK-токен
static const int webAppInitData = 160; // Данные WebApp
static const int complain = 161; // Жалоба
static const int complainReasonsGet = 162; // Причины жалобы
static const int draftSave = 176; // Сохранение черновика
static const int draftDiscard = 177; // Удаление черновика
static const int chatHide = 196; // Скрытие чата
static const int chatSearchCommonParticipants = 198; // Общие участники
static const int profileDelete = 199; // Удаление профиля
static const int profileDeleteTime = 200; // Таймер удаления профиля
static const int authQrApprove = 290; // Подтверждение QR-входа
static const int chatSuggest = 300; // Предложения чатов
// ── Polls ──────────────────────────────────────────────────────────
static const int sendVote = 304; // Голосование
static const int votersListByAnswer = 305; // Список голосовавших
static const int getPollUpdates = 306; // Обновления опроса
// ── Folders ────────────────────────────────────────────────────────
static const int foldersGet = 272; // Получение папок
static const int foldersGetById = 273; // Папка по ID
static const int foldersUpdate = 274; // Обновление / создание папки
static const int foldersReorder = 275; // Сортировка папок
static const int foldersDelete = 276; // Удаление папки
// ── Human-readable names ───────────────────────────────────────────
static String name(int opcode) => _names[opcode] ?? 'UNKNOWN($opcode)';
static const Map<int, String> _names = {
ping: 'PING',
debug: 'DEBUG',
reconnect: 'RECONNECT',
log: 'LOG',
sessionInit: 'SESSION_INIT',
profile: 'PROFILE',
authRequest: 'AUTH_REQUEST',
auth: 'AUTH',
login: 'LOGIN',
logout: 'LOGOUT',
sync: 'SYNC',
config: 'CONFIG',
authConfirm: 'AUTH_CONFIRM',
authLoginRestorePassword: 'AUTH_LOGIN_RESTORE_PASSWORD',
auth2faDetails: 'AUTH_2FA_DETAILS',
externalCallback: 'EXTERNAL_CALLBACK',
authValidatePassword: 'AUTH_VALIDATE_PASSWORD',
authValidateHint: 'AUTH_VALIDATE_HINT',
authVerifyEmail: 'AUTH_VERIFY_EMAIL',
authCheckEmail: 'AUTH_CHECK_EMAIL',
authSet2fa: 'AUTH_SET_2FA',
authCreateTrack: 'AUTH_CREATE_TRACK',
authCheckPassword: 'AUTH_CHECK_PASSWORD',
authLoginCheckPassword: 'AUTH_LOGIN_CHECK_PASSWORD',
authLoginProfileDelete: 'AUTH_LOGIN_PROFILE_DELETE',
presetAvatars: 'PRESET_AVATARS',
assetsGet: 'ASSETS_GET',
assetsUpdate: 'ASSETS_UPDATE',
assetsGetByIds: 'ASSETS_GET_BY_IDS',
assetsAdd: 'ASSETS_ADD',
assetsRemove: 'ASSETS_REMOVE',
assetsMove: 'ASSETS_MOVE',
assetsListModify: 'ASSETS_LIST_MODIFY',
contactInfo: 'CONTACT_INFO',
contactAdd: 'CONTACT_ADD',
contactUpdate: 'CONTACT_UPDATE',
contactPresence: 'CONTACT_PRESENCE',
contactList: 'CONTACT_LIST',
contactSearch: 'CONTACT_SEARCH',
contactMutual: 'CONTACT_MUTUAL',
contactPhotos: 'CONTACT_PHOTOS',
contactSort: 'CONTACT_SORT',
contactVerify: 'CONTACT_VERIFY',
removeContactPhoto: 'REMOVE_CONTACT_PHOTO',
contactInfoByPhone: 'CONTACT_INFO_BY_PHONE',
chatInfo: 'CHAT_INFO',
chatHistory: 'CHAT_HISTORY',
chatMark: 'CHAT_MARK',
chatMedia: 'CHAT_MEDIA',
chatDelete: 'CHAT_DELETE',
chatsList: 'CHATS_LIST',
chatClear: 'CHAT_CLEAR',
chatUpdate: 'CHAT_UPDATE',
chatCheckLink: 'CHAT_CHECK_LINK',
chatJoin: 'CHAT_JOIN',
chatLeave: 'CHAT_LEAVE',
chatMembers: 'CHAT_MEMBERS',
publicSearch: 'PUBLIC_SEARCH',
chatPersonalConfig: 'CHAT_PERSONAL_CONFIG',
chatCreate: 'CHAT_CREATE',
msgSend: 'MSG_SEND',
msgTyping: 'MSG_TYPING',
msgDelete: 'MSG_DELETE',
msgEdit: 'MSG_EDIT',
chatSearch: 'CHAT_SEARCH',
msgSharePreview: 'MSG_SHARE_PREVIEW',
msgGet: 'MSG_GET',
msgSearchTouch: 'MSG_SEARCH_TOUCH',
msgSearch: 'MSG_SEARCH',
msgGetStat: 'MSG_GET_STAT',
chatSubscribe: 'CHAT_SUBSCRIBE',
msgDeleteRange: 'MSG_DELETE_RANGE',
msgReaction: 'MSG_REACTION',
msgCancelReaction: 'MSG_CANCEL_REACTION',
msgGetReactions: 'MSG_GET_REACTIONS',
msgGetDetailedReactions: 'MSG_GET_DETAILED_REACTIONS',
chatReactionsSettingsSet: 'CHAT_REACTIONS_SETTINGS_SET',
reactionsSettingsGetByChatId: 'REACTIONS_SETTINGS_GET_BY_CHAT_ID',
videoChatStart: 'VIDEO_CHAT_START',
chatMembersUpdate: 'CHAT_MEMBERS_UPDATE',
videoChatStartActive: 'VIDEO_CHAT_START_ACTIVE',
videoChatHistory: 'VIDEO_CHAT_HISTORY',
videoChatCreateJoinLink: 'VIDEO_CHAT_CREATE_JOIN_LINK',
videoChatMembers: 'VIDEO_CHAT_MEMBERS',
getInboundCalls: 'GET_INBOUND_CALLS',
photoUpload: 'PHOTO_UPLOAD',
stickerUpload: 'STICKER_UPLOAD',
videoUpload: 'VIDEO_UPLOAD',
videoPlay: 'VIDEO_PLAY',
chatPinSetVisibility: 'CHAT_PIN_SET_VISIBILITY',
fileUpload: 'FILE_UPLOAD',
fileDownload: 'FILE_DOWNLOAD',
linkInfo: 'LINK_INFO',
audioPlay: 'AUDIO_PLAY',
sessionsInfo: 'SESSIONS_INFO',
sessionsClose: 'SESSIONS_CLOSE',
phoneBind_request: 'PHONE_BIND_REQUEST',
phoneBindConfirm: 'PHONE_BIND_CONFIRM',
chatComplain: 'CHAT_COMPLAIN',
msgSendCallback: 'MSG_SEND_CALLBACK',
suspendBot: 'SUSPEND_BOT',
chatBotCommands: 'CHAT_BOT_COMMANDS',
botInfo: 'BOT_INFO',
locationStop: 'LOCATION_STOP',
getLastMentions: 'GET_LAST_MENTIONS',
stickerCreate: 'STICKER_CREATE',
stickerSuggest: 'STICKER_SUGGEST',
notifMessage: 'NOTIF_MESSAGE',
notifTyping: 'NOTIF_TYPING',
notifMark: 'NOTIF_MARK',
notifContact: 'NOTIF_CONTACT',
notifPresence: 'NOTIF_PRESENCE',
notifConfig: 'NOTIF_CONFIG',
notifChat: 'NOTIF_CHAT',
notifAttach: 'NOTIF_ATTACH',
notifCallStart: 'NOTIF_CALL_START',
notifContactSort: 'NOTIF_CONTACT_SORT',
notifMsgDeleteRange: 'NOTIF_MSG_DELETE_RANGE',
notifMsgDelete: 'NOTIF_MSG_DELETE',
notifCallbackAnswer: 'NOTIF_CALLBACK_ANSWER',
notifLocation: 'NOTIF_LOCATION',
notifLocationRequest: 'NOTIF_LOCATION_REQUEST',
notifAssetsUpdate: 'NOTIF_ASSETS_UPDATE',
notifDraft: 'NOTIF_DRAFT',
notifDraftDiscard: 'NOTIF_DRAFT_DISCARD',
notifMsgDelayed: 'NOTIF_MSG_DELAYED',
notifMsgReactionsChanged: 'NOTIF_MSG_REACTIONS_CHANGED',
notifMsgYouReacted: 'NOTIF_MSG_YOU_REACTED',
notifProfile: 'NOTIF_PROFILE',
notifBanners: 'NOTIF_BANNERS',
notifFolders: 'NOTIF_FOLDERS',
okToken: 'OK_TOKEN',
webAppInitData: 'WEB_APP_INIT_DATA',
complain: 'COMPLAIN',
complainReasonsGet: 'COMPLAIN_REASONS_GET',
draftSave: 'DRAFT_SAVE',
draftDiscard: 'DRAFT_DISCARD',
chatHide: 'CHAT_HIDE',
chatSearchCommonParticipants: 'CHAT_SEARCH_COMMON_PARTICIPANTS',
profileDelete: 'PROFILE_DELETE',
profileDeleteTime: 'PROFILE_DELETE_TIME',
authQrApprove: 'AUTH_QR_APPROVE',
chatSuggest: 'CHAT_SUGGEST',
sendVote: 'SEND_VOTE',
votersListByAnswer: 'VOTERS_LIST_BY_ANSWER',
getPollUpdates: 'GET_POLL_UPDATES',
foldersGet: 'FOLDERS_GET',
foldersGetById: 'FOLDERS_GET_BY_ID',
foldersUpdate: 'FOLDERS_UPDATE',
foldersReorder: 'FOLDERS_REORDER',
foldersDelete: 'FOLDERS_DELETE',
};
}
+119 -81
View File
@@ -4,132 +4,170 @@ import 'package:msgpack_dart/msgpack_dart.dart' as msgpack;
import '../utils/logger.dart';
final int apiVersion = 10;
int seq = 1;
/// ver(1) + cmd(2) + seq(1) + opcode(2) + packedLen(4) = 10
const int headerSize = 10;
const int _maxDecompressedSize = 1048576; // 1 MB
/*
Базовый класс пакета
/// Типы команд в протоколе
abstract class CmdType {
static const int request = 0; // запрос клиента
static const int push = 1; // пуш от сервера
static const int ok = 0x100; // ответ: ок
static const int notFound = 0x200; // ответ: не найдено
static const int error = 0x300; // ответ: ошибка
}
api - версия API (по умолчанию 10 для сокета)
cmd - от кого был прислан пакет (
0 - клиент (если отправляется серверу)
1 - сервер
3 - отправляется вместе с ошибкой)
seq - счёт пакетов (начинается с 1)
opcode - показывает, для чего отправляется пакет (подробнее в /lib/core/protocol/opcode_map.dart)
payload - содержимое пакета (отправляется с сервера ввиде msgpack)
*/
/// Бинарный пакет
///
/// Формат заголовка (10 байт):
/// ```
/// [0] ver — версия протокола (uint8)
/// [1..2] cmd — тип команды (uint16 BE)
/// [3] seq — порядковый номер 0..255 (uint8)
/// [4..5] opcode — код операции (uint16 BE)
/// [6..9] packedLen — флаг сжатия [6] + длина payload [7..9] (uint32 BE)
/// [10..] payload — данные в MsgPack, опционально сжатые LZ4
/// ```
class Packet {
int api;
int cmd;
int seq;
int opcode;
Map<dynamic, dynamic> payload;
dynamic payload;
Packet({
this.api = 10,
this.cmd = 0,
this.seq = 1,
this.seq = 0,
this.opcode = 0,
this.payload = const {},
this.payload,
});
/// Выводит пакет в консоль
void printPacket() {
logger.i(
"Api: $api\nCmd: $cmd\nSeq: $seq\nOPCode: $opcode\nPayload: $payload",
);
}
bool get isOk => cmd == CmdType.ok;
bool get isError => cmd == CmdType.error;
bool get isPush => cmd == CmdType.push;
@override
String toString() =>
'Packet(ver=$api cmd=$cmd seq=$seq opcode=$opcode payload=$payload)';
}
/// Функция запаковки пакета
/// Принимает только opcode и payload, так как api и cmd
/// константы (10 и 0 соответственно), а seq считается менеджером сокета
/// Возращает запакованный пакет
Uint8List packPacket(int opcode, Map<dynamic, dynamic> payload) {
// Объяснение каждой переменной смотри в классе Packet
/// Упаковка пакета для отправки на сервер
Uint8List packPacket(int opcode, Map<dynamic, dynamic> payload, {int seq = 0}) {
final header = ByteData(headerSize);
header.setUint8(0, 10);
header.setUint16(1, CmdType.request, Endian.big);
header.setUint8(3, seq);
header.setUint16(4, opcode, Endian.big);
final apiVerB = Uint8List(1)..[0] = apiVersion;
final cmdB = Uint8List(1)..[0] = 0;
final seqB = Uint8List(2)..buffer.asByteData().setUint16(0, seq, Endian.big);
final opcodeB = Uint8List(2)
..buffer.asByteData().setUint16(0, opcode, Endian.big);
// Перед получением длины пакуем в msgpack
final payloadBytes = msgpack.serialize(payload);
final payloadLen = payloadBytes.length & 0xFFFFFF;
header.setUint32(6, payloadLen, Endian.big);
final payloadLenB = Uint8List(4)
..buffer.asByteData().setUint32(0, payloadLen, Endian.big);
return Uint8List.fromList(
apiVerB + cmdB + seqB + opcodeB + payloadLenB + payloadBytes,
);
return Uint8List.fromList(header.buffer.asUint8List() + payloadBytes);
}
/// Функция распаковки пакета
/// Принимает байты пакета, которые отправляет сервер
/// Возращает Packet
/// Распаковка пакета от сервера
Packet unpackPacket(Uint8List packet) {
// Для удобства расшифровки пакета переводим в ByteData
ByteData packetData = ByteData.view(packet.buffer);
final data = ByteData.view(
packet.buffer,
packet.offsetInBytes,
packet.lengthInBytes,
);
// Объяснение каждой переменной смотри в классе Packet
final apiVer = data.getUint8(0);
final cmd = data.getUint16(1, Endian.big);
final seq = data.getUint8(3);
final opcode = data.getUint16(4, Endian.big);
final packedLen = data.getUint32(6, Endian.big);
// API версия и cmd представляют из себя 8 битные числа
var apiVer = packetData.getUint8(0) & 0xFF;
var cmd = packetData.getUint8(1) & 0xFF;
final compFlag = packedLen >> 24;
final payloadLength = packedLen & 0xFFFFFF;
// Sequence и OPCode представляют из себя 16 битные числа
var seq = packetData.getUint16(2) & 0xFFFF;
var opcode = packetData.getUint16(4) & 0xFFFF;
var payloadBytes = Uint8List.sublistView(
packet,
headerSize,
headerSize + payloadLength,
);
dynamic payload;
// После базовых переменных идет длина пакета, является 32 битным числом
var packedLen = packetData.getUint32(6);
// Compression flag показывает, сжат ли payload
var compFlag = packedLen >> 24;
// Длина payload'а
var payloadLength = packedLen & 0xFFFFFF;
// Байты payload'а, могут быть сжаты LZ4
var payloadBytes = packet.buffer.asUint8List(10, payloadLength);
var payload = {};
// Если payload пустой, ничего не делаем (так может быть при получении пинга)
if (payloadBytes.buffer.lengthInBytes > 0) {
// Если пакет сжат, используем LZ4
if (payloadBytes.isNotEmpty) {
if (compFlag != 0) {
try {
final decompressedBytes = lz4Decompress(
payloadBytes = lz4Decompress(
payloadBytes,
decompressedSize: 99999,
decompressedSize: _maxDecompressedSize,
);
payloadBytes = decompressedBytes;
} catch (e) {
logger.e("Ошибка при декомпрессировании: $e", error: e);
} catch (_) {
// dart_lz4 не умеет block-формат, фолбэк на ручной декомпрессор
try {
payloadBytes = _lz4BlockDecompress(payloadBytes, _maxDecompressedSize);
} catch (e) {
logger.e("Ошибка декомпрессии LZ4: $e", error: e);
}
}
}
// Пробуем десериализовать msgpack
try {
payload = msgpack.deserialize(payloadBytes);
} catch (e) {
logger.e("Ошибка при десериализации msgpack: $e", error: e);
logger.e("Ошибка десериализации MsgPack: $e", error: e);
}
}
// Собираем целый пакет и возращаем его
final fullPacket = Packet(
return Packet(
api: apiVer,
cmd: cmd,
seq: seq,
opcode: opcode,
payload: payload,
);
}
return fullPacket;
/// LZ4 block декомпрессия (без frame-заголовка).
/// Сервер шлёт именно block-формат, dart_lz4 его не поддерживает.
Uint8List _lz4BlockDecompress(Uint8List src, int maxSize) {
final dst = BytesBuilder(copy: false);
int pos = 0;
while (pos < src.length) {
final token = src[pos++];
var litLen = token >> 4;
if (litLen == 15) {
while (pos < src.length) {
final b = src[pos++];
litLen += b;
if (b != 255) break;
}
}
if (litLen > 0) {
dst.add(src.sublist(pos, pos + litLen));
pos += litLen;
}
if (pos >= src.length) break;
final offset = src[pos] | (src[pos + 1] << 8);
pos += 2;
if (offset == 0) throw StateError('LZ4: offset = 0');
var matchLen = (token & 0x0F) + 4;
if ((token & 0x0F) == 0x0F) {
while (pos < src.length) {
final b = src[pos++];
matchLen += b;
if (b != 255) break;
}
}
final out = dst.toBytes();
final start = out.length - offset;
dst.add(List<int>.generate(matchLen, (i) => out[start + (i % offset)]));
if (dst.length > maxSize) throw StateError('LZ4: превышен лимит');
}
return dst.toBytes();
}
+86
View File
@@ -0,0 +1,86 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import '../utils/logger.dart';
enum SocketState { disconnected, connecting, connected }
/// Обёртка над TCP + TLS сокетом.
/// Отдаёт сырые байты через [dataStream], сборкой пакетов занимается [PacketReceiver].
class Connection {
SecureSocket? _socket;
SocketState _state = SocketState.disconnected;
final _dataController = StreamController<Uint8List>.broadcast();
final _stateController = StreamController<SocketState>.broadcast();
Stream<Uint8List> get dataStream => _dataController.stream;
Stream<SocketState> get stateStream => _stateController.stream;
SocketState get state => _state;
bool get isConnected => _state == SocketState.connected;
void _setState(SocketState newState) {
if (_state == newState) return;
_state = newState;
_stateController.add(newState);
}
Future<void> connect(String host, int port) async {
if (_state != SocketState.disconnected) return;
_setState(SocketState.connecting);
try {
final socket = await Socket.connect(host, port);
_socket = await SecureSocket.secure(
socket,
onBadCertificate: (_) => true,
);
_setState(SocketState.connected);
logger.i('Подключено к $host:$port');
_socket!.listen(
(data) => _dataController.add(Uint8List.fromList(data)),
onError: (Object error) {
logger.e('Ошибка сокета: $error');
disconnect();
},
onDone: () {
logger.w('Сокет закрыт сервером');
disconnect();
},
);
} catch (e) {
logger.e('Не удалось подключиться: $e');
_setState(SocketState.disconnected);
rethrow;
}
}
void write(Uint8List data) {
if (_socket == null || !isConnected) {
throw StateError('Нельзя писать: сокет не подключён');
}
_socket!.add(data);
}
Future<void> disconnect() async {
final socket = _socket;
_socket = null;
if (socket != null) {
try {
socket.destroy();
} catch (_) {}
}
_setState(SocketState.disconnected);
}
void dispose() {
disconnect();
_dataController.close();
_stateController.close();
}
}
+115
View File
@@ -0,0 +1,115 @@
import 'dart:async';
import '../protocol/packet.dart';
import '../protocol/opcode_map.dart';
import '../utils/logger.dart';
typedef PacketHandler = void Function(Packet packet);
/// Роутер входящих пакетов.
///
/// Ответы на запросы матчатся по seq (через [registerPending]),
/// пуши — по opcode (через [registerHandler]).
class PacketDispatcher {
final Map<int, Completer<Packet>> _pendingRequests = {};
final Map<int, DateTime> _requestTimestamps = {};
final Map<int, PacketHandler> _pushHandlers = {};
final _pushController = StreamController<Packet>.broadcast();
/// Стрим всех входящих пушей (cmd == 1)
Stream<Packet> get pushStream => _pushController.stream;
Timer? _cleanupTimer;
PacketDispatcher() {
_cleanupTimer = Timer.periodic(
const Duration(seconds: 10),
(_) => _cleanupStaleRequests(),
);
}
/// Регистрирует ожидание ответа — future завершится когда
/// придёт пакет с совпадающим seq.
Future<Packet> registerPending(int seq) {
final completer = Completer<Packet>();
_pendingRequests[seq] = completer;
_requestTimestamps[seq] = DateTime.now();
return completer.future;
}
/// Вешает обработчик на пуши с конкретным опкодом.
void registerHandler(int opcode, PacketHandler handler) {
_pushHandlers[opcode] = handler;
}
void unregisterHandler(int opcode) {
_pushHandlers.remove(opcode);
}
void dispatch(Packet packet) {
final tag = Opcode.name(packet.opcode);
if (packet.cmd == CmdType.ok ||
packet.cmd == CmdType.error ||
packet.cmd == CmdType.notFound) {
final status = packet.isOk ? 'OK' : packet.isError ? 'ERR' : 'NOT_FOUND';
logger.i('<= [$tag] seq=${packet.seq} $status\n payload: ${packet.payload}');
final completer = _pendingRequests.remove(packet.seq);
_requestTimestamps.remove(packet.seq);
if (completer == null) {
if (packet.opcode != Opcode.ping) {
logger.w('Нет ожидающего запроса для seq=${packet.seq} [$tag]');
}
return;
}
if (packet.isError) {
completer.completeError(packet);
} else {
completer.complete(packet);
}
} else if (packet.isPush) {
logger.i('<= push [$tag] ${packet.payload}');
_pushHandlers[packet.opcode]?.call(packet);
_pushController.add(packet);
}
}
/// Чистит зависшие запросы старше 30 секунд
void _cleanupStaleRequests() {
final now = DateTime.now();
final staleKeys = <int>[];
_requestTimestamps.forEach((seq, ts) {
if (now.difference(ts).inSeconds > 30) staleKeys.add(seq);
});
for (final seq in staleKeys) {
final completer = _pendingRequests.remove(seq);
_requestTimestamps.remove(seq);
if (completer != null && !completer.isCompleted) {
completer.completeError(TimeoutException('Таймаут запроса seq=$seq'));
}
}
}
/// Обрывает все ожидающие запросы (при дисконнекте)
void clearPending() {
for (final entry in _pendingRequests.entries) {
if (!entry.value.isCompleted) {
entry.value.completeError(StateError('Соединение закрыто'));
}
}
_pendingRequests.clear();
_requestTimestamps.clear();
}
void dispose() {
_cleanupTimer?.cancel();
clearPending();
_pushController.close();
}
}
+57
View File
@@ -0,0 +1,57 @@
import 'dart:typed_data';
import '../protocol/packet.dart';
import '../utils/logger.dart';
/// Буфер входящих данных.
/// Копит сырые байты из сокета, собирает из них целые пакеты.
class PacketReceiver {
Uint8List _buffer = Uint8List(0);
static const int _maxBufferSize = 2 * 1024 * 1024; // 2 мегабуйта
/// Добавляет байты в буфер, возвращает все собранные пакеты.
/// Неполные данные остаются в буфере до следующего вызова.
List<Packet> feed(Uint8List data) {
final newBuffer = Uint8List(_buffer.length + data.length);
newBuffer.setAll(0, _buffer);
newBuffer.setAll(_buffer.length, data);
_buffer = newBuffer;
if (_buffer.length > _maxBufferSize) {
logger.e('PacketReceiver: переполнение буфера (${_buffer.length} B), сброс');
reset();
return [];
}
final packets = <Packet>[];
while (_buffer.length >= headerSize) {
final bd = ByteData.view(
_buffer.buffer,
_buffer.offsetInBytes,
headerSize,
);
final packedLen = bd.getUint32(6, Endian.big);
final payloadLength = packedLen & 0xFFFFFF;
final totalLength = headerSize + payloadLength;
if (_buffer.length < totalLength) break;
final packetBytes = Uint8List.sublistView(_buffer, 0, totalLength);
_buffer = _buffer.sublist(totalLength);
try {
packets.add(unpackPacket(packetBytes));
} catch (e) {
logger.e('PacketReceiver: ошибка распаковки: $e');
}
}
return packets;
}
void reset() {
_buffer = Uint8List(0);
}
}
+25
View File
@@ -0,0 +1,25 @@
import '../protocol/packet.dart';
import '../protocol/opcode_map.dart';
import '../utils/logger.dart';
import 'connection.dart';
/// Упаковывает и отправляет пакеты, ведёт счётчик seq.
class PacketSender {
int _seq = 0;
int get currentSeq => _seq;
int _nextSeq() {
_seq = (_seq + 1) % 256;
return _seq;
}
/// Отправляет пакет, возвращает присвоенный seq.
int send(Connection connection, int opcode, Map<dynamic, dynamic> payload) {
final seq = _nextSeq();
final data = packPacket(opcode, payload, seq: seq);
connection.write(data);
logger.i('=> [${Opcode.name(opcode)}] seq=$seq\n payload: $payload');
return seq;
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ import 'package:logger/logger.dart';
final logger = Logger(
// устанавливает минимальный уровень логов.
// доступны уровни: all, trace, debug, info, warning, error, fatal, off
level: Level.info,
level: Level.all,
printer: PrettyPrinter(
methodCount: 0,
errorMethodCount: 5,
+6 -1
View File
@@ -1,6 +1,11 @@
import 'package:flutter/material.dart';
import 'backend/api.dart';
void main() {
final api = Api();
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await api.connect();
runApp(const MyApp());
}
+2
View File
@@ -8,5 +8,7 @@
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
+2
View File
@@ -4,5 +4,7 @@
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>