mnemo_cards/mnemo_cards_backend/lib/user/telegram.dart

213 lines
7 KiB
Dart
Raw Normal View History

2025-11-16 11:25:27 +00:00
import 'dart:convert';
import 'dart:developer';
import 'package:crypto/crypto.dart' as crypto;
2025-12-02 23:17:15 +00:00
import 'package:http/http.dart' as http;
2025-11-16 11:25:27 +00:00
class TelegramUtils {
static const _botToken = '7057032753:AAFP-BCpdry-YuBUOqx1W8dGxiFQdSOJlpI';
/// Validates the data received from the Telegram web app
/// Following the Python implementation exactly
2025-12-20 18:26:15 +00:00
///
2025-12-17 02:20:42 +00:00
/// [initData] should be URL-encoded (as received from Telegram Web App)
2025-12-17 02:36:21 +00:00
/// [isAlreadyDecoded] if true, treats initData as already decoded
2025-12-20 18:26:15 +00:00
bool checkValidateInitData(
String hashStr,
String initData,
String token, {
String cStr = "WebAppData",
bool isAlreadyDecoded = false,
}) {
2025-11-16 11:25:27 +00:00
try {
2025-12-17 02:36:21 +00:00
// Decode URL-encoded string if needed
2025-12-20 18:26:15 +00:00
final decodedData = isAlreadyDecoded
? initData
: Uri.decodeComponent(initData);
2025-11-16 11:25:27 +00:00
// 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)
2025-12-20 18:26:15 +00:00
final secretKey = crypto.Hmac(
crypto.sha256,
utf8.encode(cStr),
).convert(utf8.encode(token));
2025-11-16 11:25:27 +00:00
// Create data check hash: HMAC_SHA256(secretKey, dataString)
2025-12-20 18:26:15 +00:00
final dataCheck = crypto.Hmac(
crypto.sha256,
secretKey.bytes,
).convert(utf8.encode(dataString));
2025-11-16 11:25:27 +00:00
2025-12-17 02:20:42 +00:00
final calculatedHash = dataCheck.toString();
final isValid = calculatedHash == hashStr;
2025-12-20 18:26:15 +00:00
2025-12-17 02:20:42 +00:00
if (!isValid) {
2025-12-20 18:26:15 +00:00
log(
'checkValidateInitData: hash mismatch. Expected: ${hashStr.substring(0, hashStr.length > 20 ? 20 : hashStr.length)}..., Got: ${calculatedHash.substring(0, calculatedHash.length > 20 ? 20 : calculatedHash.length)}...',
);
2025-12-17 02:20:42 +00:00
}
2025-12-20 18:26:15 +00:00
2025-11-16 11:25:27 +00:00
// Compare hex digests
2025-12-17 02:20:42 +00:00
return isValid;
} catch (e, s) {
log('checkValidateInitData error: $e', error: e, stackTrace: s);
2025-11-16 11:25:27 +00:00
return false;
}
}
2025-11-27 21:58:56 +00:00
Future<({String id, String? username})?> getUserId(String initialData) async {
2025-11-16 11:25:27 +00:00
try {
2025-12-17 02:20:42 +00:00
log('getUserId: initialData length: ${initialData.length}');
2025-12-20 18:26:15 +00:00
log(
'getUserId: initialData preview: ${initialData.length > 150 ? initialData.substring(0, 150) : initialData}',
);
2025-12-17 02:36:21 +00:00
// 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);
}
2025-12-20 18:26:15 +00:00
log(
'getUserId: extracted raw data from TelegramInitData toString(), length: ${rawData?.length ?? 0}',
);
2025-11-16 11:25:27 +00:00
}
2025-12-20 18:26:15 +00:00
2025-12-17 02:36:21 +00:00
// Use raw data if extracted, otherwise use initialData as-is
final dataToProcess = rawData ?? initialData;
2025-12-20 18:26:15 +00:00
2025-12-17 02:36:21 +00:00
// Try to determine if data is URL-encoded or already decoded
// If it contains % characters, it's likely URL-encoded
final isUrlEncoded = dataToProcess.contains('%');
2025-12-20 18:26:15 +00:00
log(
'getUserId: data appears to be ${isUrlEncoded ? "URL-encoded" : "decoded"}',
);
2025-12-17 02:36:21 +00:00
String dataToValidate;
String hash;
String decodedData;
2025-12-20 18:26:15 +00:00
2025-12-17 02:36:21 +00:00
if (isUrlEncoded) {
// Data is URL-encoded, extract hash from encoded string
2025-12-20 18:26:15 +00:00
final hashMatchEncoded = RegExp(
r'hash=([^&]+)',
).firstMatch(dataToProcess);
2025-12-17 02:36:21 +00:00
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
2025-12-20 18:26:15 +00:00
if (!checkValidateInitData(
hash,
dataToValidate,
_botToken,
isAlreadyDecoded: true,
)) {
2025-12-17 02:36:21 +00:00
log('getUserId: validation failed with decoded data');
return null;
}
2025-11-16 11:25:27 +00:00
}
2025-12-20 18:26:15 +00:00
log(
'getUserId: hash extracted: ${hash.substring(0, hash.length > 20 ? 20 : hash.length)}...',
);
2025-12-17 02:20:42 +00:00
log('getUserId: validation passed');
2025-11-16 11:25:27 +00:00
2025-12-17 02:36:21 +00:00
// Extract user ID from user parameter (use decoded data)
final userMatch = RegExp(r'user=([^&]+)').firstMatch(decodedData);
2025-11-16 11:25:27 +00:00
if (userMatch == null) {
2025-12-17 02:20:42 +00:00
log('getUserId: user parameter not found');
2025-11-16 11:25:27 +00:00
return null;
}
2025-12-17 02:36:21 +00:00
// If data was already decoded, user value is also already decoded
final userValue = userMatch.group(1)!;
2025-12-20 18:26:15 +00:00
final userJson = isUrlEncoded
? Uri.decodeComponent(userValue)
: userValue;
2025-11-16 11:25:27 +00:00
final userData = jsonDecode(userJson) as Map<String, dynamic>;
final userId = userData['id']?.toString();
2025-11-27 21:58:56 +00:00
final username = userData['username']?.toString();
if (userId == null) {
2025-12-17 02:20:42 +00:00
log('getUserId: user ID is null in user data');
2025-11-27 21:58:56 +00:00
return null;
}
2025-12-17 02:20:42 +00:00
log('getUserId: successfully extracted userId: $userId');
2025-11-27 21:58:56 +00:00
return (id: userId, username: username);
2025-12-17 02:20:42 +00:00
} catch (e, s) {
log('getUserId error: $e', error: e, stackTrace: s);
2025-11-16 11:25:27 +00:00
return null;
}
}
2025-12-02 23:17:15 +00:00
/// Send message to admin via Telegram Bot API
2025-12-11 19:00:11 +00:00
static Future<bool> sendMessageToAdmin(
2025-12-20 18:26:15 +00:00
String telegramUserId,
String message,
) async {
2025-12-02 23:17:15 +00:00
try {
2025-12-20 18:26:15 +00:00
final url = Uri.parse(
'https://api.telegram.org/bot$_botToken/sendMessage',
);
2025-12-02 23:17:15 +00:00
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;
}
2025-12-20 18:26:15 +00:00
log(
'Failed to send Telegram message: ${response.statusCode} ${response.body}',
);
2025-12-02 23:17:15 +00:00
return false;
} catch (e) {
log('Error sending Telegram message: $e');
return false;
}
}
2025-11-16 11:25:27 +00:00
}