From ce718aad9a208d9226be78d62882730de41f190b Mon Sep 17 00:00:00 2001 From: klockky Date: Sat, 14 Mar 2026 22:29:53 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=BA=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D1=83,=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=BE=D0=BF=D0=BA=D0=BE?= =?UTF-8?q?=D0=B4=D1=8B=20=D0=B2=20opcode=5Fmap.dart=20=D0=B8=20=D0=B8?= =?UTF-8?q?=D1=85=20=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D0=B5,=20?= =?UTF-8?q?=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D1=83=20sessionInit?= =?UTF-8?q?=20=D0=B8=20=D0=BF=D0=B8=D0=BD=D0=B3=D0=B8=20=D0=BA=D0=B0=D0=B6?= =?UTF-8?q?=D0=B4=D1=8B=D0=B5=2030=20=D1=81=D0=B5=D0=BA=D1=83=D0=BD=D0=B4?= =?UTF-8?q?=20=D1=82=D0=B0=D0=BA=D0=B6=D0=B5=20=D0=B4=D0=BE=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=B0=D0=BB=20packet.dart=20(=D1=82.=D0=BA.?= =?UTF-8?q?=20=D1=81=D1=82=D0=B0=D0=BD=D0=B4=D0=B0=D1=80=D1=82=D0=BD=D0=B0?= =?UTF-8?q?=D1=8F=20=D0=B1=D0=B8=D0=B1=D0=BB=D0=B0=20dart=5Flz4=20=D0=BD?= =?UTF-8?q?=D0=B5=20=D1=83=D0=BC=D0=B5=D0=B5=D1=82=20=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D0=B0=D1=82=D1=8C=20=D1=81=20=D0=B1=D0=BB=D0=BE?= =?UTF-8?q?=D0=BA=D0=B0=D0=BC=D0=B8)=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20countries.dart=20(=D0=B2=20=D0=B1=D1=83=D0=B4?= =?UTF-8?q?=D1=83=D1=89=D0=B5=D0=BC=20=D0=BF=D1=80=D0=B8=D0=B3=D0=BE=D0=B4?= =?UTF-8?q?=D0=B8=D1=82=D1=81=D1=8F=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8=20=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20=D0=BA=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D1=8B=D0=B5=20=D0=B4=D0=BE=D1=81=D1=82=D1=83?= =?UTF-8?q?=D0=BF=D0=B5=D0=BD=20=D0=B2=D1=85=D0=BE=D0=B4=20=D0=B2=20MAX)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/backend/api.dart | 198 +++++++++++ lib/core/config/config.dart | 7 + lib/core/config/countries.dart | 456 +++++++++++++++++++++++++ lib/core/protocol/opcode_map.dart | 356 +++++++++++++++++++ lib/core/protocol/packet.dart | 200 ++++++----- lib/core/transport/connection.dart | 86 +++++ lib/core/transport/dispatcher.dart | 115 +++++++ lib/core/transport/receiver.dart | 57 ++++ lib/core/transport/sender.dart | 25 ++ lib/core/utils/logger.dart | 2 +- lib/main.dart | 7 +- macos/Runner/DebugProfile.entitlements | 2 + macos/Runner/Release.entitlements | 2 + 13 files changed, 1430 insertions(+), 83 deletions(-) create mode 100644 lib/core/config/countries.dart diff --git a/lib/backend/api.dart b/lib/backend/api.dart index e69de29..92f4e2c 100644 --- a/lib/backend/api.dart +++ b/lib/backend/api.dart @@ -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.broadcast(); + + Stream get stateStream => _stateController.stream; + SessionState get state => _sessionState; + + StreamSubscription? _dataSubscription; + StreamSubscription? _socketStateSubscription; + Timer? _pingTimer; + Timer? _reconnectTimer; + int _reconnectAttempts = 0; + bool _autoReconnect = false; + + // Публичное API + + /// Подключается к серверу, шлёт хэндшейк, запускает пинг. + Future 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 disconnect() async { + _autoReconnect = false; + _reconnectTimer?.cancel(); + _cleanup(); + await _connection.disconnect(); + _setSessionState(SessionState.disconnected); + } + + /// Отправляет хэндшейк (opcode 6). + Future sendHandshake() { + // TODO: заменить захардкоженные данные на реальные + final payload = { + '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 sendRequest( + int opcode, + Map 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 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); + } +} diff --git a/lib/core/config/config.dart b/lib/core/config/config.dart index e69de29..4ef18b3 100644 --- a/lib/core/config/config.dart +++ b/lib/core/config/config.dart @@ -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; +} diff --git a/lib/core/config/countries.dart b/lib/core/config/countries.dart new file mode 100644 index 0000000..860f069 --- /dev/null +++ b/lib/core/config/countries.dart @@ -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 allCountries = _buildCountries(); + +/// Быстрый доступ по ISO-коду. +final Map countriesByCode = { + for (final country in allCountries) country.code: country, +}; + +/// Пример использования: +/// ```dart +/// final country = exampleCountryLookup('RU'); +/// print(country?['ru']); // Россия +/// print(country?['en']); // Russian Federation +/// ``` +Map? exampleCountryLookup(String code) { + final country = countriesByCode[code.toUpperCase()]; + if (country == null) return null; + + return { + 'code': country.code, + 'ru': country.ru, + 'en': country.en, + }; +} + +List _buildCountries() { + final enList = (jsonDecode(_countriesEnJson) as List) + .cast>(); + final ruList = (jsonDecode(_countriesRuJson) as List) + .cast>(); + + final ruByCode = { + 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":"Япония"} +]'''; diff --git a/lib/core/protocol/opcode_map.dart b/lib/core/protocol/opcode_map.dart index e69de29..75cc983 100644 --- a/lib/core/protocol/opcode_map.dart +++ b/lib/core/protocol/opcode_map.dart @@ -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 _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', + }; +} diff --git a/lib/core/protocol/packet.dart b/lib/core/protocol/packet.dart index 34fc2ba..d7c95f9 100644 --- a/lib/core/protocol/packet.dart +++ b/lib/core/protocol/packet.dart @@ -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 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 payload) { - // Объяснение каждой переменной смотри в классе Packet +/// Упаковка пакета для отправки на сервер +Uint8List packPacket(int opcode, Map 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.generate(matchLen, (i) => out[start + (i % offset)])); + + if (dst.length > maxSize) throw StateError('LZ4: превышен лимит'); + } + + return dst.toBytes(); } diff --git a/lib/core/transport/connection.dart b/lib/core/transport/connection.dart index e69de29..16c4444 100644 --- a/lib/core/transport/connection.dart +++ b/lib/core/transport/connection.dart @@ -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.broadcast(); + final _stateController = StreamController.broadcast(); + + Stream get dataStream => _dataController.stream; + Stream 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 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 disconnect() async { + final socket = _socket; + _socket = null; + + if (socket != null) { + try { + socket.destroy(); + } catch (_) {} + } + + _setState(SocketState.disconnected); + } + + void dispose() { + disconnect(); + _dataController.close(); + _stateController.close(); + } +} diff --git a/lib/core/transport/dispatcher.dart b/lib/core/transport/dispatcher.dart index e69de29..efeec15 100644 --- a/lib/core/transport/dispatcher.dart +++ b/lib/core/transport/dispatcher.dart @@ -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> _pendingRequests = {}; + final Map _requestTimestamps = {}; + final Map _pushHandlers = {}; + + final _pushController = StreamController.broadcast(); + + /// Стрим всех входящих пушей (cmd == 1) + Stream get pushStream => _pushController.stream; + + Timer? _cleanupTimer; + + PacketDispatcher() { + _cleanupTimer = Timer.periodic( + const Duration(seconds: 10), + (_) => _cleanupStaleRequests(), + ); + } + + /// Регистрирует ожидание ответа — future завершится когда + /// придёт пакет с совпадающим seq. + Future registerPending(int seq) { + final completer = Completer(); + _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 = []; + + _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(); + } +} diff --git a/lib/core/transport/receiver.dart b/lib/core/transport/receiver.dart index e69de29..25af729 100644 --- a/lib/core/transport/receiver.dart +++ b/lib/core/transport/receiver.dart @@ -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 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 = []; + + 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); + } +} diff --git a/lib/core/transport/sender.dart b/lib/core/transport/sender.dart index e69de29..31a5c2c 100644 --- a/lib/core/transport/sender.dart +++ b/lib/core/transport/sender.dart @@ -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 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; + } +} diff --git a/lib/core/utils/logger.dart b/lib/core/utils/logger.dart index 5fbcd7b..580e134 100644 --- a/lib/core/utils/logger.dart +++ b/lib/core/utils/logger.dart @@ -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, diff --git a/lib/main.dart b/lib/main.dart index 244a702..429dfbf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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()); } diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index dddb8a3..08c3ab1 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -8,5 +8,7 @@ com.apple.security.network.server + com.apple.security.network.client + diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index 852fa1a..ee95ab7 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -4,5 +4,7 @@ com.apple.security.app-sandbox + com.apple.security.network.client +