diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..08303a5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cmake.sourceDirectory": "/run/media/invisedivine/Drive/! My projects/Komet/linux/runner" +} \ No newline at end of file diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..620e46e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/lib/backend/api.dart b/lib/backend/api.dart index e69de29..f014ad8 100644 --- a/lib/backend/api.dart +++ b/lib/backend/api.dart @@ -0,0 +1,258 @@ +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'; + +import 'package:device_info_plus/device_info_plus.dart'; +import 'dart:io'; +import 'package:timezone/data/latest_all.dart' as tz; +import 'package:timezone/timezone.dart' as tz; +import 'package:flutter_timezone/flutter_timezone.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() async { + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + + // Если платформа Linux или Windows, то ставим DESKTOP, если нет, то проверяем на Android или IOS; + String deviceType = (Platform.isLinux || Platform.isWindows) ? "DESKTOP" : (Platform.isAndroid) ? "ANDROID" : "IOS"; + String osVersion = ""; + String deviceName = "Unknown"; + String architecture = "arm64"; + + tz.initializeTimeZones(); + + final now = DateTime.now(); + String timezone = "Europe/Moscow"; + + tz.initializeTimeZones(); + final timeZoneName = await FlutterTimezone.getLocalTimezone(); + timezone = timeZoneName.identifier; + + // На каждой платформе свое инфо, поэтому делаем такую проверку + if (Platform.isLinux) { + LinuxDeviceInfo linuxInfo = await deviceInfo.linuxInfo; + + osVersion = linuxInfo.name; + // Platform.version содержит в себе что-то такое + // 3.11.1 (stable) (Tue Feb 24 00:03:07 2026 -0800) on "linux_x64" + // Поэтому мы находим '_', прибавляем к его индексу 1 и берем символы до length - 1 + architecture = Platform.version.substring(Platform.version.indexOf('_') + 1, Platform.version.length - 1); + } else if (Platform.isIOS) { + IosDeviceInfo iosInfo = await deviceInfo.iosInfo; + + osVersion = iosInfo.systemVersion; + deviceName = iosInfo.utsname.machine; + } else if (Platform.isAndroid) { + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + + osVersion = "Android ${androidInfo.version.release}"; + deviceName = "${androidInfo.manufacturer} ${androidInfo.model}"; + architecture = androidInfo.supportedAbis.first; + } else if (Platform.isWindows) { + WindowsDeviceInfo windowsInfo = await deviceInfo.windowsInfo; + + osVersion = windowsInfo.productName; + architecture = Platform.version.substring(Platform.version.indexOf('_') + 1, Platform.version.length - 1); + } + + print(deviceType); + final payload = { + 'mt_instanceid': '550e8400-e29b-41d4-a716-446655440000', + 'clientSessionId': 42, + 'deviceId': 'a1b2c3d4e5f6a7b8', + 'userAgent': { + 'deviceType': deviceType, + // Первые два символа из locale это и есть нужный нам аргумент + 'locale': "ru", + 'deviceLocale': Platform.localeName.substring(0, 2), + 'osVersion': osVersion, + 'deviceName': deviceName, + 'appVersion': '26.8.1', + 'screen': '1920x1080', + // 'screen': screenSize.width + 'x' + screenSize.height, + 'timezone': timezone, + 'pushDeviceType': 'GCM', + 'arch': architecture, + 'buildNumber': 6606, + }, + }; + + print(payload); + print(Platform.version); + 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..1c65b81 --- /dev/null +++ b/lib/core/config/countries.dart @@ -0,0 +1,472 @@ +import 'dart:convert'; + +class CountryName { + final String code; // ISO 3166-1 alpha-2, например "RU" + final String en; + final String ru; + final String phoneCode; // Код страны для звонков, например "+7" + final int phoneDigits; // Количество цифр номера после кода страны + final String phoneMask; // Маска абонентского номера, например "(###) ###-##-##" + final List phoneGroupSizes; // Размеры групп цифр, например [3, 3, 2, 2] + final List phoneGroupSeparators; // Разделители вокруг групп, например ["(", ") ", "-", "-", ""] + + const CountryName({ + required this.code, + required this.en, + required this.ru, + required this.phoneCode, + required this.phoneDigits, + required this.phoneMask, + required this.phoneGroupSizes, + required this.phoneGroupSeparators, + }); +} + +/// Полный список стран (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; + + final phoneCode = item['phoneCode'] as String; + final phoneDigits = item['phoneDigits'] as int; + final phoneMask = item['phoneMask'] as String; + final phoneGroupSizes = (item['phoneGroupSizes'] as List).cast(); + final phoneGroupSeparators = (item['phoneGroupSeparators'] as List).cast(); + + return CountryName(code: code, en: en, ru: ru, phoneCode: phoneCode, phoneDigits: phoneDigits, phoneMask: phoneMask, phoneGroupSizes: phoneGroupSizes, phoneGroupSeparators: phoneGroupSeparators); + }).toList(); + + countries.sort((a, b) => a.en.compareTo(b.en)); + return countries; +} + +const String _countriesEnJson = '''[ +{"id": 4, "alpha2": "af", "alpha3": "afg", "name": "Afghanistan", "phoneCode": "+93", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 8, "alpha2": "al", "alpha3": "alb", "name": "Albania", "phoneCode": "+355", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 12, "alpha2": "dz", "alpha3": "dza", "name": "Algeria", "phoneCode": "+213", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 20, "alpha2": "ad", "alpha3": "and", "name": "Andorra", "phoneCode": "+376", "phoneDigits": 6, "phoneMask": "######", "phoneGroupSizes": [6], "phoneGroupSeparators": ["", ""]} +{"id": 24, "alpha2": "ao", "alpha3": "ago", "name": "Angola", "phoneCode": "+244", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 28, "alpha2": "ag", "alpha3": "atg", "name": "Antigua and Barbuda", "phoneCode": "+1268", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 32, "alpha2": "ar", "alpha3": "arg", "name": "Argentina", "phoneCode": "+54", "phoneDigits": 10, "phoneMask": "(##) ####-####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 51, "alpha2": "am", "alpha3": "arm", "name": "Armenia", "phoneCode": "+374", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 36, "alpha2": "au", "alpha3": "aus", "name": "Australia", "phoneCode": "+61", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 40, "alpha2": "at", "alpha3": "aut", "name": "Austria", "phoneCode": "+43", "phoneDigits": 10, "phoneMask": "### ######", "phoneGroupSizes": [3, 6], "phoneGroupSeparators": ["", " ", ""]} +{"id": 31, "alpha2": "az", "alpha3": "aze", "name": "Azerbaijan", "phoneCode": "+994", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 44, "alpha2": "bs", "alpha3": "bhs", "name": "Bahamas", "phoneCode": "+1242", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 48, "alpha2": "bh", "alpha3": "bhr", "name": "Bahrain", "phoneCode": "+973", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 50, "alpha2": "bd", "alpha3": "bgd", "name": "Bangladesh", "phoneCode": "+880", "phoneDigits": 10, "phoneMask": "####-######", "phoneGroupSizes": [4, 6], "phoneGroupSeparators": ["", "-", ""]} +{"id": 52, "alpha2": "bb", "alpha3": "brb", "name": "Barbados", "phoneCode": "+1246", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 112, "alpha2": "by", "alpha3": "blr", "name": "Belarus", "phoneCode": "+375", "phoneDigits": 9, "phoneMask": "## ###-##-##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", "-", "-", ""]} +{"id": 56, "alpha2": "be", "alpha3": "bel", "name": "Belgium", "phoneCode": "+32", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 84, "alpha2": "bz", "alpha3": "blz", "name": "Belize", "phoneCode": "+501", "phoneDigits": 7, "phoneMask": "###-####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", "-", ""]} +{"id": 204, "alpha2": "bj", "alpha3": "ben", "name": "Benin", "phoneCode": "+229", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 64, "alpha2": "bt", "alpha3": "btn", "name": "Bhutan", "phoneCode": "+975", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 68, "alpha2": "bo", "alpha3": "bol", "name": "Bolivia, Plurinational State of", "phoneCode": "+591", "phoneDigits": 8, "phoneMask": "########", "phoneGroupSizes": [8], "phoneGroupSeparators": ["", ""]} +{"id": 70, "alpha2": "ba", "alpha3": "bih", "name": "Bosnia and Herzegovina", "phoneCode": "+387", "phoneDigits": 8, "phoneMask": "## ###-###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", "-", ""]} +{"id": 72, "alpha2": "bw", "alpha3": "bwa", "name": "Botswana", "phoneCode": "+267", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 76, "alpha2": "br", "alpha3": "bra", "name": "Brazil", "phoneCode": "+55", "phoneDigits": 11, "phoneMask": "(##) #####-####", "phoneGroupSizes": [2, 5, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 96, "alpha2": "bn", "alpha3": "brn", "name": "Brunei Darussalam", "phoneCode": "+673", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 100, "alpha2": "bg", "alpha3": "bgr", "name": "Bulgaria", "phoneCode": "+359", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 854, "alpha2": "bf", "alpha3": "bfa", "name": "Burkina Faso", "phoneCode": "+226", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 108, "alpha2": "bi", "alpha3": "bdi", "name": "Burundi", "phoneCode": "+257", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 132, "alpha2": "cv", "alpha3": "cpv", "name": "Cabo Verde", "phoneCode": "+238", "phoneDigits": 7, "phoneMask": "### ## ##", "phoneGroupSizes": [3, 2, 2], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 116, "alpha2": "kh", "alpha3": "khm", "name": "Cambodia", "phoneCode": "+855", "phoneDigits": 9, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 120, "alpha2": "cm", "alpha3": "cmr", "name": "Cameroon", "phoneCode": "+237", "phoneDigits": 9, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 124, "alpha2": "ca", "alpha3": "can", "name": "Canada", "phoneCode": "+1", "phoneDigits": 10, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 140, "alpha2": "cf", "alpha3": "caf", "name": "Central African Republic", "phoneCode": "+236", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 148, "alpha2": "td", "alpha3": "tcd", "name": "Chad", "phoneCode": "+235", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 152, "alpha2": "cl", "alpha3": "chl", "name": "Chile", "phoneCode": "+56", "phoneDigits": 9, "phoneMask": "# #### ####", "phoneGroupSizes": [1, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 156, "alpha2": "cn", "alpha3": "chn", "name": "China", "phoneCode": "+86", "phoneDigits": 11, "phoneMask": "### #### ####", "phoneGroupSizes": [3, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 170, "alpha2": "co", "alpha3": "col", "name": "Colombia", "phoneCode": "+57", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 174, "alpha2": "km", "alpha3": "com", "name": "Comoros", "phoneCode": "+269", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 178, "alpha2": "cg", "alpha3": "cog", "name": "Congo", "phoneCode": "+242", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 180, "alpha2": "cd", "alpha3": "cod", "name": "Congo, Democratic Republic of the", "phoneCode": "+243", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 188, "alpha2": "cr", "alpha3": "cri", "name": "Costa Rica", "phoneCode": "+506", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 384, "alpha2": "ci", "alpha3": "civ", "name": "Côte d'Ivoire", "phoneCode": "+225", "phoneDigits": 10, "phoneMask": "## ## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", " ", ""]} +{"id": 191, "alpha2": "hr", "alpha3": "hrv", "name": "Croatia", "phoneCode": "+385", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 192, "alpha2": "cu", "alpha3": "cub", "name": "Cuba", "phoneCode": "+53", "phoneDigits": 8, "phoneMask": "########", "phoneGroupSizes": [8], "phoneGroupSeparators": ["", ""]} +{"id": 196, "alpha2": "cy", "alpha3": "cyp", "name": "Cyprus", "phoneCode": "+357", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 203, "alpha2": "cz", "alpha3": "cze", "name": "Czechia", "phoneCode": "+420", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 208, "alpha2": "dk", "alpha3": "dnk", "name": "Denmark", "phoneCode": "+45", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 262, "alpha2": "dj", "alpha3": "dji", "name": "Djibouti", "phoneCode": "+253", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 212, "alpha2": "dm", "alpha3": "dma", "name": "Dominica", "phoneCode": "+1767", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 214, "alpha2": "do", "alpha3": "dom", "name": "Dominican Republic", "phoneCode": "+1809", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 218, "alpha2": "ec", "alpha3": "ecu", "name": "Ecuador", "phoneCode": "+593", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 818, "alpha2": "eg", "alpha3": "egy", "name": "Egypt", "phoneCode": "+20", "phoneDigits": 10, "phoneMask": "## #### ####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 222, "alpha2": "sv", "alpha3": "slv", "name": "El Salvador", "phoneCode": "+503", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 226, "alpha2": "gq", "alpha3": "gnq", "name": "Equatorial Guinea", "phoneCode": "+240", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 232, "alpha2": "er", "alpha3": "eri", "name": "Eritrea", "phoneCode": "+291", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 233, "alpha2": "ee", "alpha3": "est", "name": "Estonia", "phoneCode": "+372", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 748, "alpha2": "sz", "alpha3": "swz", "name": "Eswatini", "phoneCode": "+268", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 231, "alpha2": "et", "alpha3": "eth", "name": "Ethiopia", "phoneCode": "+251", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 242, "alpha2": "fj", "alpha3": "fji", "name": "Fiji", "phoneCode": "+679", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 246, "alpha2": "fi", "alpha3": "fin", "name": "Finland", "phoneCode": "+358", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 250, "alpha2": "fr", "alpha3": "fra", "name": "France", "phoneCode": "+33", "phoneDigits": 9, "phoneMask": "# ## ## ## ##", "phoneGroupSizes": [1, 2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", " ", ""]} +{"id": 266, "alpha2": "ga", "alpha3": "gab", "name": "Gabon", "phoneCode": "+241", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 270, "alpha2": "gm", "alpha3": "gmb", "name": "Gambia", "phoneCode": "+220", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 268, "alpha2": "ge", "alpha3": "geo", "name": "Georgia", "phoneCode": "+995", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 276, "alpha2": "de", "alpha3": "deu", "name": "Germany", "phoneCode": "+49", "phoneDigits": 10, "phoneMask": "#### #######", "phoneGroupSizes": [4, 7], "phoneGroupSeparators": ["", " ", ""]} +{"id": 288, "alpha2": "gh", "alpha3": "gha", "name": "Ghana", "phoneCode": "+233", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 300, "alpha2": "gr", "alpha3": "grc", "name": "Greece", "phoneCode": "+30", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 308, "alpha2": "gd", "alpha3": "grd", "name": "Grenada", "phoneCode": "+1473", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 320, "alpha2": "gt", "alpha3": "gtm", "name": "Guatemala", "phoneCode": "+502", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 324, "alpha2": "gn", "alpha3": "gin", "name": "Guinea", "phoneCode": "+224", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 624, "alpha2": "gw", "alpha3": "gnb", "name": "Guinea-Bissau", "phoneCode": "+245", "phoneDigits": 9, "phoneMask": "### ######", "phoneGroupSizes": [3, 6], "phoneGroupSeparators": ["", " ", ""]} +{"id": 328, "alpha2": "gy", "alpha3": "guy", "name": "Guyana", "phoneCode": "+592", "phoneDigits": 7, "phoneMask": "###-####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", "-", ""]} +{"id": 332, "alpha2": "ht", "alpha3": "hti", "name": "Haiti", "phoneCode": "+509", "phoneDigits": 8, "phoneMask": "## ## ####", "phoneGroupSizes": [2, 2, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 340, "alpha2": "hn", "alpha3": "hnd", "name": "Honduras", "phoneCode": "+504", "phoneDigits": 8, "phoneMask": "####-####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", "-", ""]} +{"id": 348, "alpha2": "hu", "alpha3": "hun", "name": "Hungary", "phoneCode": "+36", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 352, "alpha2": "is", "alpha3": "isl", "name": "Iceland", "phoneCode": "+354", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 356, "alpha2": "in", "alpha3": "ind", "name": "India", "phoneCode": "+91", "phoneDigits": 10, "phoneMask": "##### #####", "phoneGroupSizes": [5, 5], "phoneGroupSeparators": ["", " ", ""]} +{"id": 360, "alpha2": "id", "alpha3": "idn", "name": "Indonesia", "phoneCode": "+62", "phoneDigits": 10, "phoneMask": "### #### ####", "phoneGroupSizes": [3, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 364, "alpha2": "ir", "alpha3": "irn", "name": "Iran, Islamic Republic of", "phoneCode": "+98", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 368, "alpha2": "iq", "alpha3": "irq", "name": "Iraq", "phoneCode": "+964", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 372, "alpha2": "ie", "alpha3": "irl", "name": "Ireland", "phoneCode": "+353", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 376, "alpha2": "il", "alpha3": "isr", "name": "Israel", "phoneCode": "+972", "phoneDigits": 9, "phoneMask": "##-###-####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", "-", "-", ""]} +{"id": 380, "alpha2": "it", "alpha3": "ita", "name": "Italy", "phoneCode": "+39", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 388, "alpha2": "jm", "alpha3": "jam", "name": "Jamaica", "phoneCode": "+1876", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 392, "alpha2": "jp", "alpha3": "jpn", "name": "Japan", "phoneCode": "+81", "phoneDigits": 10, "phoneMask": "##-####-####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", "-", "-", ""]} +{"id": 400, "alpha2": "jo", "alpha3": "jor", "name": "Jordan", "phoneCode": "+962", "phoneDigits": 9, "phoneMask": "# #### ####", "phoneGroupSizes": [1, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 398, "alpha2": "kz", "alpha3": "kaz", "name": "Kazakhstan", "phoneCode": "+7", "phoneDigits": 10, "phoneMask": "(###) ###-##-##", "phoneGroupSizes": [3, 3, 2, 2], "phoneGroupSeparators": ["(", ") ", "-", "-", ""]} +{"id": 404, "alpha2": "ke", "alpha3": "ken", "name": "Kenya", "phoneCode": "+254", "phoneDigits": 9, "phoneMask": "### ######", "phoneGroupSizes": [3, 6], "phoneGroupSeparators": ["", " ", ""]} +{"id": 296, "alpha2": "ki", "alpha3": "kir", "name": "Kiribati", "phoneCode": "+686", "phoneDigits": 8, "phoneMask": "## ######", "phoneGroupSizes": [2, 6], "phoneGroupSeparators": ["", " ", ""]} +{"id": 408, "alpha2": "kp", "alpha3": "prk", "name": "Korea, Democratic People's Republic of", "phoneCode": "+850", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 410, "alpha2": "kr", "alpha3": "kor", "name": "Korea, Republic of", "phoneCode": "+82", "phoneDigits": 10, "phoneMask": "##-####-####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", "-", "-", ""]} +{"id": 414, "alpha2": "kw", "alpha3": "kwt", "name": "Kuwait", "phoneCode": "+965", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 417, "alpha2": "kg", "alpha3": "kgz", "name": "Kyrgyzstan", "phoneCode": "+996", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 418, "alpha2": "la", "alpha3": "lao", "name": "Lao People's Democratic Republic", "phoneCode": "+856", "phoneDigits": 9, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 428, "alpha2": "lv", "alpha3": "lva", "name": "Latvia", "phoneCode": "+371", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 422, "alpha2": "lb", "alpha3": "lbn", "name": "Lebanon", "phoneCode": "+961", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 426, "alpha2": "ls", "alpha3": "lso", "name": "Lesotho", "phoneCode": "+266", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 430, "alpha2": "lr", "alpha3": "lbr", "name": "Liberia", "phoneCode": "+231", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 434, "alpha2": "ly", "alpha3": "lby", "name": "Libya", "phoneCode": "+218", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 438, "alpha2": "li", "alpha3": "lie", "name": "Liechtenstein", "phoneCode": "+423", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 440, "alpha2": "lt", "alpha3": "ltu", "name": "Lithuania", "phoneCode": "+370", "phoneDigits": 8, "phoneMask": "### #####", "phoneGroupSizes": [3, 5], "phoneGroupSeparators": ["", " ", ""]} +{"id": 442, "alpha2": "lu", "alpha3": "lux", "name": "Luxembourg", "phoneCode": "+352", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 450, "alpha2": "mg", "alpha3": "mdg", "name": "Madagascar", "phoneCode": "+261", "phoneDigits": 9, "phoneMask": "## ## ### ##", "phoneGroupSizes": [2, 2, 3, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 454, "alpha2": "mw", "alpha3": "mwi", "name": "Malawi", "phoneCode": "+265", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 458, "alpha2": "my", "alpha3": "mys", "name": "Malaysia", "phoneCode": "+60", "phoneDigits": 10, "phoneMask": "##-#### ####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", "-", " ", ""]} +{"id": 462, "alpha2": "mv", "alpha3": "mdv", "name": "Maldives", "phoneCode": "+960", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 466, "alpha2": "ml", "alpha3": "mli", "name": "Mali", "phoneCode": "+223", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 470, "alpha2": "mt", "alpha3": "mlt", "name": "Malta", "phoneCode": "+356", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 584, "alpha2": "mh", "alpha3": "mhl", "name": "Marshall Islands", "phoneCode": "+692", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 478, "alpha2": "mr", "alpha3": "mrt", "name": "Mauritania", "phoneCode": "+222", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 480, "alpha2": "mu", "alpha3": "mus", "name": "Mauritius", "phoneCode": "+230", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 484, "alpha2": "mx", "alpha3": "mex", "name": "Mexico", "phoneCode": "+52", "phoneDigits": 10, "phoneMask": "## #### ####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 583, "alpha2": "fm", "alpha3": "fsm", "name": "Micronesia, Federated States of", "phoneCode": "+691", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 498, "alpha2": "md", "alpha3": "mda", "name": "Moldova, Republic of", "phoneCode": "+373", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 492, "alpha2": "mc", "alpha3": "mco", "name": "Monaco", "phoneCode": "+377", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 496, "alpha2": "mn", "alpha3": "mng", "name": "Mongolia", "phoneCode": "+976", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 499, "alpha2": "me", "alpha3": "mne", "name": "Montenegro", "phoneCode": "+382", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 504, "alpha2": "ma", "alpha3": "mar", "name": "Morocco", "phoneCode": "+212", "phoneDigits": 9, "phoneMask": "###-###-###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", "-", "-", ""]} +{"id": 508, "alpha2": "mz", "alpha3": "moz", "name": "Mozambique", "phoneCode": "+258", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 104, "alpha2": "mm", "alpha3": "mmr", "name": "Myanmar", "phoneCode": "+95", "phoneDigits": 9, "phoneMask": "# ### ####", "phoneGroupSizes": [1, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 516, "alpha2": "na", "alpha3": "nam", "name": "Namibia", "phoneCode": "+264", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 520, "alpha2": "nr", "alpha3": "nru", "name": "Nauru", "phoneCode": "+674", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 524, "alpha2": "np", "alpha3": "npl", "name": "Nepal", "phoneCode": "+977", "phoneDigits": 10, "phoneMask": "##-### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", "-", " ", ""]} +{"id": 528, "alpha2": "nl", "alpha3": "nld", "name": "Netherlands", "phoneCode": "+31", "phoneDigits": 9, "phoneMask": "# ## ## ## ##", "phoneGroupSizes": [1, 2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", " ", ""]} +{"id": 554, "alpha2": "nz", "alpha3": "nzl", "name": "New Zealand", "phoneCode": "+64", "phoneDigits": 9, "phoneMask": "##-### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", "-", " ", ""]} +{"id": 558, "alpha2": "ni", "alpha3": "nic", "name": "Nicaragua", "phoneCode": "+505", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 562, "alpha2": "ne", "alpha3": "ner", "name": "Niger", "phoneCode": "+227", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 566, "alpha2": "ng", "alpha3": "nga", "name": "Nigeria", "phoneCode": "+234", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 807, "alpha2": "mk", "alpha3": "mkd", "name": "North Macedonia", "phoneCode": "+389", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 578, "alpha2": "no", "alpha3": "nor", "name": "Norway", "phoneCode": "+47", "phoneDigits": 8, "phoneMask": "### ## ###", "phoneGroupSizes": [3, 2, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 512, "alpha2": "om", "alpha3": "omn", "name": "Oman", "phoneCode": "+968", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 586, "alpha2": "pk", "alpha3": "pak", "name": "Pakistan", "phoneCode": "+92", "phoneDigits": 10, "phoneMask": "### #######", "phoneGroupSizes": [3, 7], "phoneGroupSeparators": ["", " ", ""]} +{"id": 585, "alpha2": "pw", "alpha3": "plw", "name": "Palau", "phoneCode": "+680", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 591, "alpha2": "pa", "alpha3": "pan", "name": "Panama", "phoneCode": "+507", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 598, "alpha2": "pg", "alpha3": "png", "name": "Papua New Guinea", "phoneCode": "+675", "phoneDigits": 8, "phoneMask": "### ## ###", "phoneGroupSizes": [3, 2, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 600, "alpha2": "py", "alpha3": "pry", "name": "Paraguay", "phoneCode": "+595", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 604, "alpha2": "pe", "alpha3": "per", "name": "Peru", "phoneCode": "+51", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 608, "alpha2": "ph", "alpha3": "phl", "name": "Philippines", "phoneCode": "+63", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 616, "alpha2": "pl", "alpha3": "pol", "name": "Poland", "phoneCode": "+48", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 620, "alpha2": "pt", "alpha3": "prt", "name": "Portugal", "phoneCode": "+351", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 634, "alpha2": "qa", "alpha3": "qat", "name": "Qatar", "phoneCode": "+974", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 642, "alpha2": "ro", "alpha3": "rou", "name": "Romania", "phoneCode": "+40", "phoneDigits": 10, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 643, "alpha2": "ru", "alpha3": "rus", "name": "Russian Federation", "phoneCode": "+7", "phoneDigits": 10, "phoneMask": "(###) ###-##-##", "phoneGroupSizes": [3, 3, 2, 2], "phoneGroupSeparators": ["(", ") ", "-", "-", ""]} +{"id": 646, "alpha2": "rw", "alpha3": "rwa", "name": "Rwanda", "phoneCode": "+250", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 659, "alpha2": "kn", "alpha3": "kna", "name": "Saint Kitts and Nevis", "phoneCode": "+1869", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 662, "alpha2": "lc", "alpha3": "lca", "name": "Saint Lucia", "phoneCode": "+1758", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 670, "alpha2": "vc", "alpha3": "vct", "name": "Saint Vincent and the Grenadines", "phoneCode": "+1784", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 882, "alpha2": "ws", "alpha3": "wsm", "name": "Samoa", "phoneCode": "+685", "phoneDigits": 7, "phoneMask": "## #####", "phoneGroupSizes": [2, 5], "phoneGroupSeparators": ["", " ", ""]} +{"id": 674, "alpha2": "sm", "alpha3": "smr", "name": "San Marino", "phoneCode": "+378", "phoneDigits": 10, "phoneMask": "## ## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", " ", ""]} +{"id": 678, "alpha2": "st", "alpha3": "stp", "name": "Sao Tome and Principe", "phoneCode": "+239", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 682, "alpha2": "sa", "alpha3": "sau", "name": "Saudi Arabia", "phoneCode": "+966", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 686, "alpha2": "sn", "alpha3": "sen", "name": "Senegal", "phoneCode": "+221", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 688, "alpha2": "rs", "alpha3": "srb", "name": "Serbia", "phoneCode": "+381", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 690, "alpha2": "sc", "alpha3": "syc", "name": "Seychelles", "phoneCode": "+248", "phoneDigits": 7, "phoneMask": "# ### ###", "phoneGroupSizes": [1, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 694, "alpha2": "sl", "alpha3": "sle", "name": "Sierra Leone", "phoneCode": "+232", "phoneDigits": 8, "phoneMask": "## ######", "phoneGroupSizes": [2, 6], "phoneGroupSeparators": ["", " ", ""]} +{"id": 702, "alpha2": "sg", "alpha3": "sgp", "name": "Singapore", "phoneCode": "+65", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 703, "alpha2": "sk", "alpha3": "svk", "name": "Slovakia", "phoneCode": "+421", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 705, "alpha2": "si", "alpha3": "svn", "name": "Slovenia", "phoneCode": "+386", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 90, "alpha2": "sb", "alpha3": "slb", "name": "Solomon Islands", "phoneCode": "+677", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 706, "alpha2": "so", "alpha3": "som", "name": "Somalia", "phoneCode": "+252", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 710, "alpha2": "za", "alpha3": "zaf", "name": "South Africa", "phoneCode": "+27", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 728, "alpha2": "ss", "alpha3": "ssd", "name": "South Sudan", "phoneCode": "+211", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 724, "alpha2": "es", "alpha3": "esp", "name": "Spain", "phoneCode": "+34", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 144, "alpha2": "lk", "alpha3": "lka", "name": "Sri Lanka", "phoneCode": "+94", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 729, "alpha2": "sd", "alpha3": "sdn", "name": "Sudan", "phoneCode": "+249", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 740, "alpha2": "sr", "alpha3": "sur", "name": "Suriname", "phoneCode": "+597", "phoneDigits": 7, "phoneMask": "###-####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", "-", ""]} +{"id": 752, "alpha2": "se", "alpha3": "swe", "name": "Sweden", "phoneCode": "+46", "phoneDigits": 9, "phoneMask": "##-### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", "-", " ", " ", ""]} +{"id": 756, "alpha2": "ch", "alpha3": "che", "name": "Switzerland", "phoneCode": "+41", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 760, "alpha2": "sy", "alpha3": "syr", "name": "Syrian Arab Republic", "phoneCode": "+963", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 762, "alpha2": "tj", "alpha3": "tjk", "name": "Tajikistan", "phoneCode": "+992", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 834, "alpha2": "tz", "alpha3": "tza", "name": "Tanzania, United Republic of", "phoneCode": "+255", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 764, "alpha2": "th", "alpha3": "tha", "name": "Thailand", "phoneCode": "+66", "phoneDigits": 9, "phoneMask": "##-### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", "-", " ", ""]} +{"id": 626, "alpha2": "tl", "alpha3": "tls", "name": "Timor-Leste", "phoneCode": "+670", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 768, "alpha2": "tg", "alpha3": "tgo", "name": "Togo", "phoneCode": "+228", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 776, "alpha2": "to", "alpha3": "ton", "name": "Tonga", "phoneCode": "+676", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 780, "alpha2": "tt", "alpha3": "tto", "name": "Trinidad and Tobago", "phoneCode": "+1868", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 788, "alpha2": "tn", "alpha3": "tun", "name": "Tunisia", "phoneCode": "+216", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 792, "alpha2": "tr", "alpha3": "tur", "name": "Türkiye", "phoneCode": "+90", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 795, "alpha2": "tm", "alpha3": "tkm", "name": "Turkmenistan", "phoneCode": "+993", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 798, "alpha2": "tv", "alpha3": "tuv", "name": "Tuvalu", "phoneCode": "+688", "phoneDigits": 6, "phoneMask": "######", "phoneGroupSizes": [6], "phoneGroupSeparators": ["", ""]} +{"id": 800, "alpha2": "ug", "alpha3": "uga", "name": "Uganda", "phoneCode": "+256", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 804, "alpha2": "ua", "alpha3": "ukr", "name": "Ukraine", "phoneCode": "+380", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 784, "alpha2": "ae", "alpha3": "are", "name": "United Arab Emirates", "phoneCode": "+971", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 826, "alpha2": "gb", "alpha3": "gbr", "name": "United Kingdom of Great Britain and Northern Ireland", "phoneCode": "+44", "phoneDigits": 10, "phoneMask": "#### ######", "phoneGroupSizes": [4, 6], "phoneGroupSeparators": ["", " ", ""]} +{"id": 840, "alpha2": "us", "alpha3": "usa", "name": "United States of America", "phoneCode": "+1", "phoneDigits": 10, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 858, "alpha2": "uy", "alpha3": "ury", "name": "Uruguay", "phoneCode": "+598", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 860, "alpha2": "uz", "alpha3": "uzb", "name": "Uzbekistan", "phoneCode": "+998", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 548, "alpha2": "vu", "alpha3": "vut", "name": "Vanuatu", "phoneCode": "+678", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 862, "alpha2": "ve", "alpha3": "ven", "name": "Venezuela, Bolivarian Republic of", "phoneCode": "+58", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 704, "alpha2": "vn", "alpha3": "vnm", "name": "Viet Nam", "phoneCode": "+84", "phoneDigits": 10, "phoneMask": "## #### ####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 887, "alpha2": "ye", "alpha3": "yem", "name": "Yemen", "phoneCode": "+967", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 894, "alpha2": "zm", "alpha3": "zmb", "name": "Zambia", "phoneCode": "+260", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 716, "alpha2": "zw", "alpha3": "zwe", "name": "Zimbabwe", "phoneCode": "+263", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +]'''; + +const String _countriesRuJson = '''[ +{"id": 36, "alpha2": "au", "alpha3": "aus", "name": "Австралия", "phoneCode": "+61", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 40, "alpha2": "at", "alpha3": "aut", "name": "Австрия", "phoneCode": "+43", "phoneDigits": 10, "phoneMask": "### ######", "phoneGroupSizes": [3, 6], "phoneGroupSeparators": ["", " ", ""]} +{"id": 31, "alpha2": "az", "alpha3": "aze", "name": "Азербайджан", "phoneCode": "+994", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 8, "alpha2": "al", "alpha3": "alb", "name": "Албания", "phoneCode": "+355", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 12, "alpha2": "dz", "alpha3": "dza", "name": "Алжир", "phoneCode": "+213", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 24, "alpha2": "ao", "alpha3": "ago", "name": "Ангола", "phoneCode": "+244", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 20, "alpha2": "ad", "alpha3": "and", "name": "Андорра", "phoneCode": "+376", "phoneDigits": 6, "phoneMask": "######", "phoneGroupSizes": [6], "phoneGroupSeparators": ["", ""]} +{"id": 28, "alpha2": "ag", "alpha3": "atg", "name": "Антигуа и Барбуда", "phoneCode": "+1268", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 32, "alpha2": "ar", "alpha3": "arg", "name": "Аргентина", "phoneCode": "+54", "phoneDigits": 10, "phoneMask": "(##) ####-####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 51, "alpha2": "am", "alpha3": "arm", "name": "Армения", "phoneCode": "+374", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 4, "alpha2": "af", "alpha3": "afg", "name": "Афганистан", "phoneCode": "+93", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 44, "alpha2": "bs", "alpha3": "bhs", "name": "Багамские Острова", "phoneCode": "+1242", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 50, "alpha2": "bd", "alpha3": "bgd", "name": "Бангладеш", "phoneCode": "+880", "phoneDigits": 10, "phoneMask": "####-######", "phoneGroupSizes": [4, 6], "phoneGroupSeparators": ["", "-", ""]} +{"id": 52, "alpha2": "bb", "alpha3": "brb", "name": "Барбадос", "phoneCode": "+1246", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 48, "alpha2": "bh", "alpha3": "bhr", "name": "Бахрейн", "phoneCode": "+973", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 84, "alpha2": "bz", "alpha3": "blz", "name": "Белиз", "phoneCode": "+501", "phoneDigits": 7, "phoneMask": "###-####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", "-", ""]} +{"id": 112, "alpha2": "by", "alpha3": "blr", "name": "Беларусь", "phoneCode": "+375", "phoneDigits": 9, "phoneMask": "## ###-##-##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", "-", "-", ""]} +{"id": 56, "alpha2": "be", "alpha3": "bel", "name": "Бельгия", "phoneCode": "+32", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 204, "alpha2": "bj", "alpha3": "ben", "name": "Бенин", "phoneCode": "+229", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 100, "alpha2": "bg", "alpha3": "bgr", "name": "Болгария", "phoneCode": "+359", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 68, "alpha2": "bo", "alpha3": "bol", "name": "Боливия", "phoneCode": "+591", "phoneDigits": 8, "phoneMask": "########", "phoneGroupSizes": [8], "phoneGroupSeparators": ["", ""]} +{"id": 70, "alpha2": "ba", "alpha3": "bih", "name": "Босния и Герцеговина", "phoneCode": "+387", "phoneDigits": 8, "phoneMask": "## ###-###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", "-", ""]} +{"id": 72, "alpha2": "bw", "alpha3": "bwa", "name": "Ботсвана", "phoneCode": "+267", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 76, "alpha2": "br", "alpha3": "bra", "name": "Бразилия", "phoneCode": "+55", "phoneDigits": 11, "phoneMask": "(##) #####-####", "phoneGroupSizes": [2, 5, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 96, "alpha2": "bn", "alpha3": "brn", "name": "Бруней", "phoneCode": "+673", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 854, "alpha2": "bf", "alpha3": "bfa", "name": "Буркина-Фасо", "phoneCode": "+226", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 108, "alpha2": "bi", "alpha3": "bdi", "name": "Бурунди", "phoneCode": "+257", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 64, "alpha2": "bt", "alpha3": "btn", "name": "Бутан", "phoneCode": "+975", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 548, "alpha2": "vu", "alpha3": "vut", "name": "Вануату", "phoneCode": "+678", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 826, "alpha2": "gb", "alpha3": "gbr", "name": "Великобритания", "phoneCode": "+44", "phoneDigits": 10, "phoneMask": "#### ######", "phoneGroupSizes": [4, 6], "phoneGroupSeparators": ["", " ", ""]} +{"id": 348, "alpha2": "hu", "alpha3": "hun", "name": "Венгрия", "phoneCode": "+36", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 862, "alpha2": "ve", "alpha3": "ven", "name": "Венесуэла", "phoneCode": "+58", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 626, "alpha2": "tl", "alpha3": "tls", "name": "Восточный Тимор", "phoneCode": "+670", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 704, "alpha2": "vn", "alpha3": "vnm", "name": "Вьетнам", "phoneCode": "+84", "phoneDigits": 10, "phoneMask": "## #### ####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 266, "alpha2": "ga", "alpha3": "gab", "name": "Габон", "phoneCode": "+241", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 332, "alpha2": "ht", "alpha3": "hti", "name": "Гаити", "phoneCode": "+509", "phoneDigits": 8, "phoneMask": "## ## ####", "phoneGroupSizes": [2, 2, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 328, "alpha2": "gy", "alpha3": "guy", "name": "Гайана", "phoneCode": "+592", "phoneDigits": 7, "phoneMask": "###-####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", "-", ""]} +{"id": 270, "alpha2": "gm", "alpha3": "gmb", "name": "Гамбия", "phoneCode": "+220", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 288, "alpha2": "gh", "alpha3": "gha", "name": "Гана", "phoneCode": "+233", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 320, "alpha2": "gt", "alpha3": "gtm", "name": "Гватемала", "phoneCode": "+502", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 324, "alpha2": "gn", "alpha3": "gin", "name": "Гвинея", "phoneCode": "+224", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 624, "alpha2": "gw", "alpha3": "gnb", "name": "Гвинея-Бисау", "phoneCode": "+245", "phoneDigits": 9, "phoneMask": "### ######", "phoneGroupSizes": [3, 6], "phoneGroupSeparators": ["", " ", ""]} +{"id": 276, "alpha2": "de", "alpha3": "deu", "name": "Германия", "phoneCode": "+49", "phoneDigits": 10, "phoneMask": "#### #######", "phoneGroupSizes": [4, 7], "phoneGroupSeparators": ["", " ", ""]} +{"id": 340, "alpha2": "hn", "alpha3": "hnd", "name": "Гондурас", "phoneCode": "+504", "phoneDigits": 8, "phoneMask": "####-####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", "-", ""]} +{"id": 308, "alpha2": "gd", "alpha3": "grd", "name": "Гренада", "phoneCode": "+1473", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 300, "alpha2": "gr", "alpha3": "grc", "name": "Греция", "phoneCode": "+30", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 268, "alpha2": "ge", "alpha3": "geo", "name": "Грузия", "phoneCode": "+995", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 208, "alpha2": "dk", "alpha3": "dnk", "name": "Дания", "phoneCode": "+45", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 262, "alpha2": "dj", "alpha3": "dji", "name": "Джибути", "phoneCode": "+253", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 212, "alpha2": "dm", "alpha3": "dma", "name": "Доминика", "phoneCode": "+1767", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 214, "alpha2": "do", "alpha3": "dom", "name": "Доминиканская Республика", "phoneCode": "+1809", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 180, "alpha2": "cd", "alpha3": "cod", "name": "ДР Конго", "phoneCode": "+243", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 818, "alpha2": "eg", "alpha3": "egy", "name": "Египет", "phoneCode": "+20", "phoneDigits": 10, "phoneMask": "## #### ####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 894, "alpha2": "zm", "alpha3": "zmb", "name": "Замбия", "phoneCode": "+260", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 716, "alpha2": "zw", "alpha3": "zwe", "name": "Зимбабве", "phoneCode": "+263", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 376, "alpha2": "il", "alpha3": "isr", "name": "Израиль", "phoneCode": "+972", "phoneDigits": 9, "phoneMask": "##-###-####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", "-", "-", ""]} +{"id": 356, "alpha2": "in", "alpha3": "ind", "name": "Индия", "phoneCode": "+91", "phoneDigits": 10, "phoneMask": "##### #####", "phoneGroupSizes": [5, 5], "phoneGroupSeparators": ["", " ", ""]} +{"id": 360, "alpha2": "id", "alpha3": "idn", "name": "Индонезия", "phoneCode": "+62", "phoneDigits": 10, "phoneMask": "### #### ####", "phoneGroupSizes": [3, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 400, "alpha2": "jo", "alpha3": "jor", "name": "Иордания", "phoneCode": "+962", "phoneDigits": 9, "phoneMask": "# #### ####", "phoneGroupSizes": [1, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 368, "alpha2": "iq", "alpha3": "irq", "name": "Ирак", "phoneCode": "+964", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 364, "alpha2": "ir", "alpha3": "irn", "name": "Иран", "phoneCode": "+98", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 372, "alpha2": "ie", "alpha3": "irl", "name": "Ирландия", "phoneCode": "+353", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 352, "alpha2": "is", "alpha3": "isl", "name": "Исландия", "phoneCode": "+354", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 724, "alpha2": "es", "alpha3": "esp", "name": "Испания", "phoneCode": "+34", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 380, "alpha2": "it", "alpha3": "ita", "name": "Италия", "phoneCode": "+39", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 887, "alpha2": "ye", "alpha3": "yem", "name": "Йемен", "phoneCode": "+967", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 132, "alpha2": "cv", "alpha3": "cpv", "name": "Кабо-Верде", "phoneCode": "+238", "phoneDigits": 7, "phoneMask": "### ## ##", "phoneGroupSizes": [3, 2, 2], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 398, "alpha2": "kz", "alpha3": "kaz", "name": "Казахстан", "phoneCode": "+7", "phoneDigits": 10, "phoneMask": "(###) ###-##-##", "phoneGroupSizes": [3, 3, 2, 2], "phoneGroupSeparators": ["(", ") ", "-", "-", ""]} +{"id": 116, "alpha2": "kh", "alpha3": "khm", "name": "Камбоджа", "phoneCode": "+855", "phoneDigits": 9, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 120, "alpha2": "cm", "alpha3": "cmr", "name": "Камерун", "phoneCode": "+237", "phoneDigits": 9, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 124, "alpha2": "ca", "alpha3": "can", "name": "Канада", "phoneCode": "+1", "phoneDigits": 10, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 634, "alpha2": "qa", "alpha3": "qat", "name": "Катар", "phoneCode": "+974", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 404, "alpha2": "ke", "alpha3": "ken", "name": "Кения", "phoneCode": "+254", "phoneDigits": 9, "phoneMask": "### ######", "phoneGroupSizes": [3, 6], "phoneGroupSeparators": ["", " ", ""]} +{"id": 196, "alpha2": "cy", "alpha3": "cyp", "name": "Кипр", "phoneCode": "+357", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 417, "alpha2": "kg", "alpha3": "kgz", "name": "Кыргызстан", "phoneCode": "+996", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 296, "alpha2": "ki", "alpha3": "kir", "name": "Кирибати", "phoneCode": "+686", "phoneDigits": 8, "phoneMask": "## ######", "phoneGroupSizes": [2, 6], "phoneGroupSeparators": ["", " ", ""]} +{"id": 408, "alpha2": "kp", "alpha3": "prk", "name": "КНДР (Корейская Народно-Демократическая Республика)", "phoneCode": "+850", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 156, "alpha2": "cn", "alpha3": "chn", "name": "Китай (Китайская Народная Республика)", "phoneCode": "+86", "phoneDigits": 11, "phoneMask": "### #### ####", "phoneGroupSizes": [3, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 170, "alpha2": "co", "alpha3": "col", "name": "Колумбия", "phoneCode": "+57", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 174, "alpha2": "km", "alpha3": "com", "name": "Коморы", "phoneCode": "+269", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 188, "alpha2": "cr", "alpha3": "cri", "name": "Коста-Рика", "phoneCode": "+506", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 384, "alpha2": "ci", "alpha3": "civ", "name": "Кот-д’Ивуар", "phoneCode": "+225", "phoneDigits": 10, "phoneMask": "## ## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", " ", ""]} +{"id": 192, "alpha2": "cu", "alpha3": "cub", "name": "Куба", "phoneCode": "+53", "phoneDigits": 8, "phoneMask": "########", "phoneGroupSizes": [8], "phoneGroupSeparators": ["", ""]} +{"id": 414, "alpha2": "kw", "alpha3": "kwt", "name": "Кувейт", "phoneCode": "+965", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 418, "alpha2": "la", "alpha3": "lao", "name": "Лаос", "phoneCode": "+856", "phoneDigits": 9, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 428, "alpha2": "lv", "alpha3": "lva", "name": "Латвия", "phoneCode": "+371", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 426, "alpha2": "ls", "alpha3": "lso", "name": "Лесото", "phoneCode": "+266", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 430, "alpha2": "lr", "alpha3": "lbr", "name": "Либерия", "phoneCode": "+231", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 422, "alpha2": "lb", "alpha3": "lbn", "name": "Ливан", "phoneCode": "+961", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 434, "alpha2": "ly", "alpha3": "lby", "name": "Ливия", "phoneCode": "+218", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 440, "alpha2": "lt", "alpha3": "ltu", "name": "Литва", "phoneCode": "+370", "phoneDigits": 8, "phoneMask": "### #####", "phoneGroupSizes": [3, 5], "phoneGroupSeparators": ["", " ", ""]} +{"id": 438, "alpha2": "li", "alpha3": "lie", "name": "Лихтенштейн", "phoneCode": "+423", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 442, "alpha2": "lu", "alpha3": "lux", "name": "Люксембург", "phoneCode": "+352", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 480, "alpha2": "mu", "alpha3": "mus", "name": "Маврикий", "phoneCode": "+230", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 478, "alpha2": "mr", "alpha3": "mrt", "name": "Мавритания", "phoneCode": "+222", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 450, "alpha2": "mg", "alpha3": "mdg", "name": "Мадагаскар", "phoneCode": "+261", "phoneDigits": 9, "phoneMask": "## ## ### ##", "phoneGroupSizes": [2, 2, 3, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 807, "alpha2": "mk", "alpha3": "mkd", "name": "Северная Македония", "phoneCode": "+389", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 454, "alpha2": "mw", "alpha3": "mwi", "name": "Малави", "phoneCode": "+265", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 458, "alpha2": "my", "alpha3": "mys", "name": "Малайзия", "phoneCode": "+60", "phoneDigits": 10, "phoneMask": "##-#### ####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", "-", " ", ""]} +{"id": 466, "alpha2": "ml", "alpha3": "mli", "name": "Мали", "phoneCode": "+223", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 462, "alpha2": "mv", "alpha3": "mdv", "name": "Мальдивы", "phoneCode": "+960", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 470, "alpha2": "mt", "alpha3": "mlt", "name": "Мальта", "phoneCode": "+356", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 504, "alpha2": "ma", "alpha3": "mar", "name": "Марокко", "phoneCode": "+212", "phoneDigits": 9, "phoneMask": "###-###-###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", "-", "-", ""]} +{"id": 584, "alpha2": "mh", "alpha3": "mhl", "name": "Маршалловы Острова", "phoneCode": "+692", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 484, "alpha2": "mx", "alpha3": "mex", "name": "Мексика", "phoneCode": "+52", "phoneDigits": 10, "phoneMask": "## #### ####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 583, "alpha2": "fm", "alpha3": "fsm", "name": "Микронезия", "phoneCode": "+691", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 508, "alpha2": "mz", "alpha3": "moz", "name": "Мозамбик", "phoneCode": "+258", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 498, "alpha2": "md", "alpha3": "mda", "name": "Молдова", "phoneCode": "+373", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 492, "alpha2": "mc", "alpha3": "mco", "name": "Монако", "phoneCode": "+377", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 496, "alpha2": "mn", "alpha3": "mng", "name": "Монголия", "phoneCode": "+976", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 104, "alpha2": "mm", "alpha3": "mmr", "name": "Мьянма", "phoneCode": "+95", "phoneDigits": 9, "phoneMask": "# ### ####", "phoneGroupSizes": [1, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 516, "alpha2": "na", "alpha3": "nam", "name": "Намибия", "phoneCode": "+264", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 520, "alpha2": "nr", "alpha3": "nru", "name": "Науру", "phoneCode": "+674", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 524, "alpha2": "np", "alpha3": "npl", "name": "Непал", "phoneCode": "+977", "phoneDigits": 10, "phoneMask": "##-### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", "-", " ", ""]} +{"id": 562, "alpha2": "ne", "alpha3": "ner", "name": "Нигер", "phoneCode": "+227", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 566, "alpha2": "ng", "alpha3": "nga", "name": "Нигерия", "phoneCode": "+234", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 528, "alpha2": "nl", "alpha3": "nld", "name": "Нидерланды", "phoneCode": "+31", "phoneDigits": 9, "phoneMask": "# ## ## ## ##", "phoneGroupSizes": [1, 2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", " ", ""]} +{"id": 558, "alpha2": "ni", "alpha3": "nic", "name": "Никарагуа", "phoneCode": "+505", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 554, "alpha2": "nz", "alpha3": "nzl", "name": "Новая Зеландия", "phoneCode": "+64", "phoneDigits": 9, "phoneMask": "##-### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", "-", " ", ""]} +{"id": 578, "alpha2": "no", "alpha3": "nor", "name": "Норвегия", "phoneCode": "+47", "phoneDigits": 8, "phoneMask": "### ## ###", "phoneGroupSizes": [3, 2, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 784, "alpha2": "ae", "alpha3": "are", "name": "ОАЭ", "phoneCode": "+971", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 512, "alpha2": "om", "alpha3": "omn", "name": "Оман", "phoneCode": "+968", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 586, "alpha2": "pk", "alpha3": "pak", "name": "Пакистан", "phoneCode": "+92", "phoneDigits": 10, "phoneMask": "### #######", "phoneGroupSizes": [3, 7], "phoneGroupSeparators": ["", " ", ""]} +{"id": 585, "alpha2": "pw", "alpha3": "plw", "name": "Палау", "phoneCode": "+680", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 591, "alpha2": "pa", "alpha3": "pan", "name": "Панама", "phoneCode": "+507", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 598, "alpha2": "pg", "alpha3": "png", "name": "Папуа — Новая Гвинея", "phoneCode": "+675", "phoneDigits": 8, "phoneMask": "### ## ###", "phoneGroupSizes": [3, 2, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 600, "alpha2": "py", "alpha3": "pry", "name": "Парагвай", "phoneCode": "+595", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 604, "alpha2": "pe", "alpha3": "per", "name": "Перу", "phoneCode": "+51", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 616, "alpha2": "pl", "alpha3": "pol", "name": "Польша", "phoneCode": "+48", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 620, "alpha2": "pt", "alpha3": "prt", "name": "Португалия", "phoneCode": "+351", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 178, "alpha2": "cg", "alpha3": "cog", "name": "Республика Конго", "phoneCode": "+242", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 410, "alpha2": "kr", "alpha3": "kor", "name": "Республика Корея", "phoneCode": "+82", "phoneDigits": 10, "phoneMask": "##-####-####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", "-", "-", ""]} +{"id": 643, "alpha2": "ru", "alpha3": "rus", "name": "Россия", "phoneCode": "+7", "phoneDigits": 10, "phoneMask": "(###) ###-##-##", "phoneGroupSizes": [3, 3, 2, 2], "phoneGroupSeparators": ["(", ") ", "-", "-", ""]} +{"id": 646, "alpha2": "rw", "alpha3": "rwa", "name": "Руанда", "phoneCode": "+250", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 642, "alpha2": "ro", "alpha3": "rou", "name": "Румыния", "phoneCode": "+40", "phoneDigits": 10, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 222, "alpha2": "sv", "alpha3": "slv", "name": "Сальвадор", "phoneCode": "+503", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 882, "alpha2": "ws", "alpha3": "wsm", "name": "Самоа", "phoneCode": "+685", "phoneDigits": 7, "phoneMask": "## #####", "phoneGroupSizes": [2, 5], "phoneGroupSeparators": ["", " ", ""]} +{"id": 674, "alpha2": "sm", "alpha3": "smr", "name": "Сан-Марино", "phoneCode": "+378", "phoneDigits": 10, "phoneMask": "## ## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", " ", ""]} +{"id": 678, "alpha2": "st", "alpha3": "stp", "name": "Сан-Томе и Принсипи", "phoneCode": "+239", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 682, "alpha2": "sa", "alpha3": "sau", "name": "Саудовская Аравия", "phoneCode": "+966", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 690, "alpha2": "sc", "alpha3": "syc", "name": "Сейшельские Острова", "phoneCode": "+248", "phoneDigits": 7, "phoneMask": "# ### ###", "phoneGroupSizes": [1, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 686, "alpha2": "sn", "alpha3": "sen", "name": "Сенегал", "phoneCode": "+221", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 670, "alpha2": "vc", "alpha3": "vct", "name": "Сент-Винсент и Гренадины", "phoneCode": "+1784", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 659, "alpha2": "kn", "alpha3": "kna", "name": "Сент-Китс и Невис", "phoneCode": "+1869", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 662, "alpha2": "lc", "alpha3": "lca", "name": "Сент-Люсия", "phoneCode": "+1758", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 688, "alpha2": "rs", "alpha3": "srb", "name": "Сербия", "phoneCode": "+381", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 702, "alpha2": "sg", "alpha3": "sgp", "name": "Сингапур", "phoneCode": "+65", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 760, "alpha2": "sy", "alpha3": "syr", "name": "Сирия", "phoneCode": "+963", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 703, "alpha2": "sk", "alpha3": "svk", "name": "Словакия", "phoneCode": "+421", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 705, "alpha2": "si", "alpha3": "svn", "name": "Словения", "phoneCode": "+386", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 90, "alpha2": "sb", "alpha3": "slb", "name": "Соломоновы Острова", "phoneCode": "+677", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 706, "alpha2": "so", "alpha3": "som", "name": "Сомали", "phoneCode": "+252", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 729, "alpha2": "sd", "alpha3": "sdn", "name": "Судан", "phoneCode": "+249", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 740, "alpha2": "sr", "alpha3": "sur", "name": "Суринам", "phoneCode": "+597", "phoneDigits": 7, "phoneMask": "###-####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", "-", ""]} +{"id": 840, "alpha2": "us", "alpha3": "usa", "name": "США", "phoneCode": "+1", "phoneDigits": 10, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 694, "alpha2": "sl", "alpha3": "sle", "name": "Сьерра-Леоне", "phoneCode": "+232", "phoneDigits": 8, "phoneMask": "## ######", "phoneGroupSizes": [2, 6], "phoneGroupSeparators": ["", " ", ""]} +{"id": 762, "alpha2": "tj", "alpha3": "tjk", "name": "Таджикистан", "phoneCode": "+992", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 764, "alpha2": "th", "alpha3": "tha", "name": "Таиланд", "phoneCode": "+66", "phoneDigits": 9, "phoneMask": "##-### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", "-", " ", ""]} +{"id": 834, "alpha2": "tz", "alpha3": "tza", "name": "Танзания", "phoneCode": "+255", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 768, "alpha2": "tg", "alpha3": "tgo", "name": "Того", "phoneCode": "+228", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 776, "alpha2": "to", "alpha3": "ton", "name": "Тонга", "phoneCode": "+676", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 780, "alpha2": "tt", "alpha3": "tto", "name": "Тринидад и Тобаго", "phoneCode": "+1868", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 798, "alpha2": "tv", "alpha3": "tuv", "name": "Тувалу", "phoneCode": "+688", "phoneDigits": 6, "phoneMask": "######", "phoneGroupSizes": [6], "phoneGroupSeparators": ["", ""]} +{"id": 788, "alpha2": "tn", "alpha3": "tun", "name": "Тунис", "phoneCode": "+216", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 795, "alpha2": "tm", "alpha3": "tkm", "name": "Туркменистан", "phoneCode": "+993", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 792, "alpha2": "tr", "alpha3": "tur", "name": "Турция", "phoneCode": "+90", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 800, "alpha2": "ug", "alpha3": "uga", "name": "Уганда", "phoneCode": "+256", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 860, "alpha2": "uz", "alpha3": "uzb", "name": "Узбекистан", "phoneCode": "+998", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 804, "alpha2": "ua", "alpha3": "ukr", "name": "Украина", "phoneCode": "+380", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 858, "alpha2": "uy", "alpha3": "ury", "name": "Уругвай", "phoneCode": "+598", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 242, "alpha2": "fj", "alpha3": "fji", "name": "Фиджи", "phoneCode": "+679", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 608, "alpha2": "ph", "alpha3": "phl", "name": "Филиппины", "phoneCode": "+63", "phoneDigits": 10, "phoneMask": "### ### ####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 246, "alpha2": "fi", "alpha3": "fin", "name": "Финляндия", "phoneCode": "+358", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 250, "alpha2": "fr", "alpha3": "fra", "name": "Франция", "phoneCode": "+33", "phoneDigits": 9, "phoneMask": "# ## ## ## ##", "phoneGroupSizes": [1, 2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", " ", ""]} +{"id": 191, "alpha2": "hr", "alpha3": "hrv", "name": "Хорватия", "phoneCode": "+385", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 140, "alpha2": "cf", "alpha3": "caf", "name": "ЦАР", "phoneCode": "+236", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 148, "alpha2": "td", "alpha3": "tcd", "name": "Чад", "phoneCode": "+235", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 499, "alpha2": "me", "alpha3": "mne", "name": "Черногория", "phoneCode": "+382", "phoneDigits": 8, "phoneMask": "## ### ###", "phoneGroupSizes": [2, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 203, "alpha2": "cz", "alpha3": "cze", "name": "Чехия", "phoneCode": "+420", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 152, "alpha2": "cl", "alpha3": "chl", "name": "Чили", "phoneCode": "+56", "phoneDigits": 9, "phoneMask": "# #### ####", "phoneGroupSizes": [1, 4, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 756, "alpha2": "ch", "alpha3": "che", "name": "Швейцария", "phoneCode": "+41", "phoneDigits": 9, "phoneMask": "## ### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 752, "alpha2": "se", "alpha3": "swe", "name": "Швеция", "phoneCode": "+46", "phoneDigits": 9, "phoneMask": "##-### ## ##", "phoneGroupSizes": [2, 3, 2, 2], "phoneGroupSeparators": ["", "-", " ", " ", ""]} +{"id": 144, "alpha2": "lk", "alpha3": "lka", "name": "Шри-Ланка", "phoneCode": "+94", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 218, "alpha2": "ec", "alpha3": "ecu", "name": "Эквадор", "phoneCode": "+593", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 226, "alpha2": "gq", "alpha3": "gnq", "name": "Экваториальная Гвинея", "phoneCode": "+240", "phoneDigits": 9, "phoneMask": "### ## ## ##", "phoneGroupSizes": [3, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 232, "alpha2": "er", "alpha3": "eri", "name": "Эритрея", "phoneCode": "+291", "phoneDigits": 7, "phoneMask": "### ####", "phoneGroupSizes": [3, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 748, "alpha2": "sz", "alpha3": "swz", "name": "Эсватини", "phoneCode": "+268", "phoneDigits": 8, "phoneMask": "## ## ## ##", "phoneGroupSizes": [2, 2, 2, 2], "phoneGroupSeparators": ["", " ", " ", " ", ""]} +{"id": 233, "alpha2": "ee", "alpha3": "est", "name": "Эстония", "phoneCode": "+372", "phoneDigits": 8, "phoneMask": "#### ####", "phoneGroupSizes": [4, 4], "phoneGroupSeparators": ["", " ", ""]} +{"id": 231, "alpha2": "et", "alpha3": "eth", "name": "Эфиопия", "phoneCode": "+251", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 710, "alpha2": "za", "alpha3": "zaf", "name": "ЮАР", "phoneCode": "+27", "phoneDigits": 9, "phoneMask": "## ### ####", "phoneGroupSizes": [2, 3, 4], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 728, "alpha2": "ss", "alpha3": "ssd", "name": "Южный Судан", "phoneCode": "+211", "phoneDigits": 9, "phoneMask": "### ### ###", "phoneGroupSizes": [3, 3, 3], "phoneGroupSeparators": ["", " ", " ", ""]} +{"id": 388, "alpha2": "jm", "alpha3": "jam", "name": "Ямайка", "phoneCode": "+1876", "phoneDigits": 7, "phoneMask": "(###) ###-####", "phoneGroupSizes": [3, 3, 4], "phoneGroupSeparators": ["(", ") ", "-", ""]} +{"id": 392, "alpha2": "jp", "alpha3": "jpn", "name": "Япония", "phoneCode": "+81", "phoneDigits": 10, "phoneMask": "##-####-####", "phoneGroupSizes": [2, 4, 4], "phoneGroupSeparators": ["", "-", "-", ""]} +]'''; 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..a7c2d62 100644 --- a/lib/core/protocol/packet.dart +++ b/lib/core/protocol/packet.dart @@ -4,132 +4,182 @@ import 'package:msgpack_dart/msgpack_dart.dart' as msgpack; import '../utils/logger.dart'; -final int apiVersion = 10; -int seq = 1; +/// ver(1) + cmd(1) + seq(2) + 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; // пуш от сервера - api - версия API (по умолчанию 10 для сокета) - cmd - от кого был прислан пакет ( - 0 - клиент (если отправляется серверу) - 1 - сервер - 3 - отправляется вместе с ошибкой) - seq - счёт пакетов (начинается с 1) - opcode - показывает, для чего отправляется пакет (подробнее в /lib/core/protocol/opcode_map.dart) - payload - содержимое пакета (отправляется с сервера ввиде msgpack) -*/ + static const int ok = 1; // ответ: ок + static const int notFound = 2; // ответ: не найдено + static const int error = 3; // ответ: ошибка +} + +/// Распакованный бинарный пакет +/// +/// Формат заголовка (10 байт): +/// ``` +/// [0] ver — версия протокола (uint8) (по умолчанию 10) +/// [1] cmd — тип команды (uint8) (при отправке от клиента равно 0) +/// [2..3] seq — порядковый номер (uint16 BE) +/// [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.setUint8(1, CmdType.request); + header.setUint16(2, seq, Endian.big); + 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); + ByteData packetData = ByteData.view( + packet.buffer, + packet.offsetInBytes, + packet.lengthInBytes, + ); // Объяснение каждой переменной смотри в классе Packet // API версия и cmd представляют из себя 8 битные числа - var apiVer = packetData.getUint8(0) & 0xFF; - var cmd = packetData.getUint8(1) & 0xFF; + final apiVer = packetData.getUint8(0) & 0xFF; + final cmd = packetData.getUint8(1) & 0xFF; // Sequence и OPCode представляют из себя 16 битные числа - var seq = packetData.getUint16(2) & 0xFFFF; - var opcode = packetData.getUint16(4) & 0xFFFF; + final seq = packetData.getUint16(2) & 0xFFFF; + final opcode = packetData.getUint16(4) & 0xFFFF; // После базовых переменных идет длина пакета, является 32 битным числом - var packedLen = packetData.getUint32(6); + final packedLen = packetData.getUint32(6); // Compression flag показывает, сжат ли payload - var compFlag = packedLen >> 24; - - // Длина payload'а - var payloadLength = packedLen & 0xFFFFFF; + final compFlag = packedLen >> 24; + + // Длина payload'а + final payloadLength = packedLen & 0xFFFFFF; // Байты payload'а, могут быть сжаты LZ4 var payloadBytes = packet.buffer.asUint8List(10, payloadLength); - var payload = {}; - + dynamic payload; + print(compFlag); // Если 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); + } catch (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/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b..4b81f9b 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig index c2efd0b..5caa9d1 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..ff5ddb3 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end 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 + diff --git a/pubspec.lock b/pubspec.lock index 3e29bd2..6bb2187 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: "4df8babf73058181227e18b08e6ea3520cf5fc5d796888d33b7cb0f33f984b7c" + url: "https://pub.dev" + source: hosted + version: "12.3.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + url: "https://pub.dev" + source: hosted + version: "7.0.3" + equatable: + dependency: transitive + description: + name: equatable + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + url: "https://pub.dev" + source: hosted + version: "2.0.8" fake_async: dependency: transitive description: @@ -65,6 +89,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -83,6 +123,35 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_timezone: + dependency: "direct main" + description: + name: flutter_timezone + sha256: "978192f2f9ea6d019a4de4f0211d76a9af955ca24865828fa98ca4e20cf0cb3c" + url: "https://pub.dev" + source: hosted + version: "5.0.1" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" leak_tracker: dependency: transitive description: @@ -163,6 +232,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" sky_engine: dependency: transitive description: flutter @@ -216,6 +293,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.7" + timezone: + dependency: "direct main" + description: + name: timezone + sha256: "784a5e34d2eb62e1326f24d6f600aaaee452eb8ca8ef2f384a59244e292d158b" + url: "https://pub.dev" + source: hosted + version: "0.11.0" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" vector_math: dependency: transitive description: @@ -232,6 +325,30 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" + url: "https://pub.dev" + source: hosted + version: "2.1.0" sdks: dart: ">=3.10.4 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index a704a79..252d74e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,9 @@ dependencies: dart_lz4: ^1.0.0 msgpack_dart: ^1.0.1 logger: ^2.6.2 + device_info_plus: ^12.3.0 + flutter_timezone: ^5.0.1 + timezone: ^0.11.0 dev_dependencies: flutter_test: