mnemo_cards/mnemo_cards_telegram_bot/lib/backend_client.dart
Dmitry 620999534d
Some checks are pending
Mobile App CI / test (push) Waiting to run
Mobile App CI / build-android (push) Blocked by required conditions
Mobile App CI / build-ios (push) Blocked by required conditions
Web App CI / test (push) Waiting to run
Web App CI / build (push) Blocked by required conditions
Deploy Telegram Bot / Deploy Telegram Bot (push) Waiting to run
telegram
2025-12-14 00:00:13 +03:00

266 lines
7.9 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
/// HTTP client for backend API calls
/// Replaces direct Isar DB access with API endpoints
class BackendClient {
final String backendUrl;
final String apiKey;
final HttpClient _httpClient;
BackendClient({
required this.backendUrl,
required this.apiKey,
}) : _httpClient = HttpClient();
/// Make HTTP request with retry logic
Future<Map<String, dynamic>> _request({
required String method,
required String path,
Map<String, dynamic>? body,
int maxRetries = 3,
Duration timeout = const Duration(seconds: 10),
}) async {
Exception? lastException;
for (int attempt = 0; attempt < maxRetries; attempt++) {
try {
final uri = Uri.parse('$backendUrl/api/v2$path');
final request = await _httpClient.openUrl(method, uri)
.timeout(timeout);
request.headers.contentType = ContentType.json;
request.headers.set('X-API-Key', apiKey);
if (body != null) {
request.write(jsonEncode(body));
}
final response = await request.close().timeout(timeout);
final responseBody = await response
.transform(utf8.decoder)
.join()
.timeout(timeout);
if (response.statusCode >= 200 && response.statusCode < 300) {
if (responseBody.isEmpty) {
return {};
}
return jsonDecode(responseBody) as Map<String, dynamic>;
} else {
throw HttpException(
'HTTP ${response.statusCode}: $responseBody',
uri: uri,
);
}
} catch (e) {
lastException = e is Exception ? e : Exception(e.toString());
if (attempt < maxRetries - 1) {
// Exponential backoff: 1s, 2s, 4s
await Future.delayed(Duration(seconds: 1 << attempt));
}
}
}
throw lastException ?? Exception('Request failed after $maxRetries attempts');
}
/// Generate a Telegram auth code for a user
/// Calls backend API to generate the code
Future<String?> generateAuthCode({
required String telegramUserId,
String? telegramUsername,
String? firstName,
String? lastName,
}) async {
try {
final response = await _request(
method: 'POST',
path: '/auth/telegram/generate-code',
body: {
'telegramUserId': telegramUserId,
if (telegramUsername != null) 'telegramUsername': telegramUsername,
if (firstName != null) 'firstName': firstName,
if (lastName != null) 'lastName': lastName,
},
);
return response['code'] as String?;
} catch (e, s) {
print('Error generating Telegram auth code: $e\n$s');
return null;
}
}
/// Claim a web-generated Telegram auth code for a user.
/// Returns true when the backend confirmed the claim.
Future<bool> claimWebAuthCode({
required String code,
required String telegramUserId,
String? telegramUsername,
String? firstName,
String? lastName,
}) async {
try {
await _request(
method: 'POST',
path: '/auth/telegram/claim-code',
body: {
'code': code,
'telegramUserId': telegramUserId,
if (telegramUsername != null) 'telegramUsername': telegramUsername,
if (firstName != null) 'firstName': firstName,
if (lastName != null) 'lastName': lastName,
},
);
return true;
} catch (e, s) {
print('Error claiming Telegram auth code: $e\n$s');
return false;
}
}
/// Check if user can share today (rate limiting)
/// Returns true if user has not exceeded daily limit
Future<bool> canShareToday(String telegramUserId, int dailyLimit) async {
try {
final response = await _request(
method: 'POST',
path: '/telegram-bot/share/check-limit',
body: {
'telegramUserId': telegramUserId,
'dailyLimit': dailyLimit,
},
);
return response['canShare'] as bool? ?? true;
} catch (e, s) {
print('Error checking share limit: $e\n$s');
return true; // Allow on error to prevent blocking users
}
}
/// Record a share request for a user
Future<void> recordShareRequest({
required String telegramUserId,
String? telegramUsername,
String? sharedCardId,
}) async {
try {
await _request(
method: 'POST',
path: '/telegram-bot/share/record',
body: {
'telegramUserId': telegramUserId,
if (telegramUsername != null) 'telegramUsername': telegramUsername,
if (sharedCardId != null) 'sharedCardId': sharedCardId,
},
);
print(
'[SHARE_RECORDED] User $telegramUserId shared card $sharedCardId');
} catch (e, s) {
print('Error recording share request: $e\n$s');
}
}
/// Get a random card from the database
/// Returns null if no cards are found
Future<GameCardModel?> getRandomCard() async {
try {
final response = await _request(
method: 'GET',
path: '/telegram-bot/random-card',
);
if (response.isEmpty) {
return null;
}
// Convert response to GameCardModel
// Note: GameCardModel requires non-nullable fields, so we provide defaults
return GameCardModel(
id: response['id'] as String,
original: response['original'] as String? ?? '',
translation: response['translation'] as String? ?? '',
mnemo: response['mnemo'] as String? ?? '',
image: response['image'] as String? ?? '',
transcription: response['transcription'] as String?,
transcriptionMnemo: response['transcriptionMnemo'] as String?,
imageBack: response['imageBack'] as String?,
back: response['back'] as String?,
);
} catch (e, s) {
print('Error getting random card: $e\n$s');
return null;
}
}
/// Get user information
Future<String> userInfo(String userId) async {
try {
final response = await _request(
method: 'GET',
path: '/telegram-bot/users/info?userId=$userId',
);
if (response['multiple'] == true) {
final users = response['users'] as List;
return 'Found multiple:\n${users.map((u) => '${u['id']} ${u['email'] ?? ''} ${u['name'] ?? ''}').join('\n')}';
}
final sub = response['subscription'] as Map<String, dynamic>?;
final packs = response['packs'] as List?;
return '''
${response['id']} ${response['email'] ?? ''} ${response['name'] ?? ''}
Tags: ${response['tags'] ?? ''}
Packs (${packs?.length ?? 0}):
${packs?.map((p) => '${p['id']} ${p['title']}').join('\n') ?? ''}
Purchases: ${response['purchases'] ?? 0}
Subscription ${sub == null ? '' : '(${sub['start']} - ${sub['finish']})'}:
${sub?['features']?.join(' ') ?? ''}
'''
.trim();
} catch (e, s) {
print('Error getting user info: $e\n$s');
return 'Error: $e';
}
}
/// Get all users summary
Future<String> info() async {
try {
final response = await _request(
method: 'GET',
path: '/telegram-bot/users/info',
);
final users = response['users'] as List;
return 'Users (${response['total']}):\n'
'${users.map((u) => '${u['email'] ?? '${u['id']}'} ${u['lastTimeOnline'] ?? ''}').join('\n')}';
} catch (e, s) {
print('Error getting users info: $e\n$s');
return 'Error: $e';
}
}
/// Get all words from cards
Future<List<String>> words() async {
try {
final response = await _request(
method: 'GET',
path: '/telegram-bot/words',
);
final wordsList = response['words'] as List?;
return wordsList?.map((w) => w.toString()).toList() ?? [];
} catch (e, s) {
print('Error getting words: $e\n$s');
return [];
}
}
void dispose() {
_httpClient.close(force: true);
}
}