import 'dart:convert'; import 'dart:developer'; import 'package:crypto/crypto.dart' as crypto; import 'package:http/http.dart' as http; class TelegramUtils { static const _botToken = '7057032753:AAFP-BCpdry-YuBUOqx1W8dGxiFQdSOJlpI'; /// Validates the data received from the Telegram web app /// Following the Python implementation exactly /// /// [initData] should be URL-encoded (as received from Telegram Web App) /// [isAlreadyDecoded] if true, treats initData as already decoded bool checkValidateInitData(String hashStr, String initData, String token, {String cStr = "WebAppData", bool isAlreadyDecoded = false}) { try { // Decode URL-encoded string if needed final decodedData = isAlreadyDecoded ? initData : Uri.decodeComponent(initData); // Split into chunks and filter out hash parameter final chunks = decodedData .split('&') .where((chunk) => !chunk.startsWith('hash=')) .map((chunk) => chunk.split('=')) .toList(); // Sort by first element (key) chunks.sort((a, b) => a[0].compareTo(b[0])); // Create data string with newline separator final dataString = chunks.map((rec) => '${rec[0]}=${rec[1]}').join('\n'); // Create secret key: HMAC_SHA256(cStr, token) final secretKey = crypto.Hmac(crypto.sha256, utf8.encode(cStr)) .convert(utf8.encode(token)); // Create data check hash: HMAC_SHA256(secretKey, dataString) final dataCheck = crypto.Hmac(crypto.sha256, secretKey.bytes) .convert(utf8.encode(dataString)); final calculatedHash = dataCheck.toString(); final isValid = calculatedHash == hashStr; if (!isValid) { log('checkValidateInitData: hash mismatch. Expected: ${hashStr.substring(0, hashStr.length > 20 ? 20 : hashStr.length)}..., Got: ${calculatedHash.substring(0, calculatedHash.length > 20 ? 20 : calculatedHash.length)}...'); } // Compare hex digests return isValid; } catch (e, s) { log('checkValidateInitData error: $e', error: e, stackTrace: s); return false; } } Future<({String id, String? username})?> getUserId(String initialData) async { try { log('getUserId: initialData length: ${initialData.length}'); log('getUserId: initialData preview: ${initialData.length > 150 ? initialData.substring(0, 150) : initialData}'); // Check if data is a toString() representation of TelegramInitData object // If it contains "raw: " pattern, extract the raw value // Pattern: raw: user=...&hash=...} (may have closing brace at the end) String? rawData; final rawMatch = RegExp(r'raw:\s*(.+)').firstMatch(initialData); if (rawMatch != null) { rawData = rawMatch.group(1)?.trim(); // Remove trailing } if present (from the closing brace of the object) if (rawData != null && rawData.endsWith('}')) { rawData = rawData.substring(0, rawData.length - 1); } log('getUserId: extracted raw data from TelegramInitData toString(), length: ${rawData?.length ?? 0}'); } // Use raw data if extracted, otherwise use initialData as-is final dataToProcess = rawData ?? initialData; // Try to determine if data is URL-encoded or already decoded // If it contains % characters, it's likely URL-encoded final isUrlEncoded = dataToProcess.contains('%'); log('getUserId: data appears to be ${isUrlEncoded ? "URL-encoded" : "decoded"}'); String dataToValidate; String hash; String decodedData; if (isUrlEncoded) { // Data is URL-encoded, extract hash from encoded string final hashMatchEncoded = RegExp(r'hash=([^&]+)').firstMatch(dataToProcess); if (hashMatchEncoded == null) { log('getUserId: hash not found in URL-encoded data'); return null; } final hashEncoded = hashMatchEncoded.group(1)!; hash = Uri.decodeComponent(hashEncoded); dataToValidate = dataToProcess; decodedData = Uri.decodeComponent(dataToProcess); // Validate with URL-encoded data (default behavior) if (!checkValidateInitData(hash, dataToValidate, _botToken)) { log('getUserId: validation failed with URL-encoded data'); return null; } } else { // Data appears to be already decoded, extract hash directly final hashMatch = RegExp(r'hash=([^&]+)').firstMatch(dataToProcess); if (hashMatch == null) { log('getUserId: hash not found in decoded data'); return null; } hash = hashMatch.group(1)!; dataToValidate = dataToProcess; decodedData = dataToProcess; // Validate with already decoded data if (!checkValidateInitData(hash, dataToValidate, _botToken, isAlreadyDecoded: true)) { log('getUserId: validation failed with decoded data'); return null; } } log('getUserId: hash extracted: ${hash.substring(0, hash.length > 20 ? 20 : hash.length)}...'); log('getUserId: validation passed'); // Extract user ID from user parameter (use decoded data) final userMatch = RegExp(r'user=([^&]+)').firstMatch(decodedData); if (userMatch == null) { log('getUserId: user parameter not found'); return null; } // If data was already decoded, user value is also already decoded final userValue = userMatch.group(1)!; final userJson = isUrlEncoded ? Uri.decodeComponent(userValue) : userValue; final userData = jsonDecode(userJson) as Map; final userId = userData['id']?.toString(); final username = userData['username']?.toString(); if (userId == null) { log('getUserId: user ID is null in user data'); return null; } log('getUserId: successfully extracted userId: $userId'); return (id: userId, username: username); } catch (e, s) { log('getUserId error: $e', error: e, stackTrace: s); return null; } } /// Send message to admin via Telegram Bot API static Future sendMessageToAdmin( String telegramUserId, String message) async { try { final url = Uri.parse('https://api.telegram.org/bot$_botToken/sendMessage'); final response = await http.post( url, headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'chat_id': telegramUserId, 'text': message, 'parse_mode': 'HTML', }), ); if (response.statusCode == 200) { final data = jsonDecode(response.body); return data['ok'] == true; } log('Failed to send Telegram message: ${response.statusCode} ${response.body}'); return false; } catch (e) { log('Error sending Telegram message: $e'); return false; } } }