Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
131 lines
4.6 KiB
Dart
131 lines
4.6 KiB
Dart
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)
|
|
bool checkValidateInitData(String hashStr, String initData, String token,
|
|
{String cStr = "WebAppData"}) {
|
|
try {
|
|
// Decode URL-encoded string
|
|
final decodedData = 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}');
|
|
final tgWebAppData = Uri.decodeComponent(initialData);
|
|
log('getUserId: decoded data length: ${tgWebAppData.length}');
|
|
|
|
// Extract hash from the data
|
|
final hashMatch = RegExp(r'hash=([^&]+)').firstMatch(tgWebAppData);
|
|
if (hashMatch == null) {
|
|
log('getUserId: hash not found in data');
|
|
return null;
|
|
}
|
|
final hash = hashMatch.group(1)!;
|
|
log('getUserId: hash extracted: ${hash.substring(0, hash.length > 20 ? 20 : hash.length)}...');
|
|
|
|
// Validate using the new method
|
|
// Note: checkValidateInitData expects the original URL-encoded data
|
|
// (as received from Telegram Web App), so we pass initialData, not tgWebAppData
|
|
if (!checkValidateInitData(hash, initialData, _botToken)) {
|
|
log('getUserId: validation failed');
|
|
return null;
|
|
}
|
|
log('getUserId: validation passed');
|
|
|
|
// Extract user ID from user parameter
|
|
final userMatch = RegExp(r'user=([^&]+)').firstMatch(tgWebAppData);
|
|
if (userMatch == null) {
|
|
log('getUserId: user parameter not found');
|
|
return null;
|
|
}
|
|
|
|
final userJson = Uri.decodeComponent(userMatch.group(1)!);
|
|
final userData = jsonDecode(userJson) as Map<String, dynamic>;
|
|
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<bool> 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;
|
|
}
|
|
}
|
|
}
|