import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:args/args.dart'; import 'package:isar/isar.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; class DBManager { late Isar isar; final Function(String message) notify; // Backend API URL for generating auth codes final String backendUrl; DBManager(this.notify, {this.backendUrl = 'http://localhost:8443'}); Future init(ArgResults results) async { final dir = results.option('isar') ?? '../mnemo_cards_backend/isar/'; final name = 'db'; // Try to get existing instance first try { isar = Isar.getInstance(name) ?? await _connectWithShareModel( dir: dir, inspector: results.flag('debug'), ); } catch (e) { // If instance exists but can't be obtained, try to close and reopen try { final existing = Isar.getInstance(name); if (existing != null) { await existing.close(); } } catch (_) { // Ignore errors when closing } isar = await _connectWithShareModel( dir: dir, inspector: results.flag('debug'), ); } var totalUsers = await isar.userModels.count(); // Removed automatic notification on startup isar.userModels.watchLazy().listen((_) async { final totalUsersUpd = await isar.userModels.count(); if (totalUsersUpd != totalUsers) { notify('Users: $totalUsers -> $totalUsersUpd!'); totalUsers = totalUsersUpd; } }); } /// Connect to Isar with ShareRequestModel schema included Future _connectWithShareModel({ required String dir, required bool inspector, }) async { await Isar.initializeIsarCore(download: true); return Isar.open( [ CardPackModelSchema, GameCardModelSchema, UserModelSchema, UserSubscriptionModelSchema, SubscriptionPlanModelSchema, TokenModelSchema, RefreshTokenModelSchema, TelegramAuthCodeModelSchema, TestModelSchema, TestQuestionModelSchema, PaymentModelSchema, TaskModelSchema, TestStatisticsModelSchema, UserDataModelSchema, PromoCodesCampaignModelSchema, PromoCodeModelSchema, DiscountCampaignModelSchema, DiscountModelSchema, UserTaskModelSchema, UserTaskProgressModelSchema, UserTaskResultModelSchema, ShareRequestModelSchema, ], directory: dir, name: 'db', inspector: inspector, ); } Future info() async { final users = await isar.txn(() => isar.userModels.where().findAll()); for (final user in users) { await user.userData.load(); } return 'Users (${users.length}):\n' '${users.map((user) => '${user.email ?? '${user.id} ${user.name}'} ${user.userData.value?.lastTimeOnline?.toShortString() ?? ''}').join('\n')}'; } Future userInfo(String userId) async { final id = int.tryParse(userId); final users = await isar.txn( () => isar.userModels .filter() .optional(id != null, (q) => q.idEqualTo(id)) .or() .emailContains(userId) .or() .nameContains(userId) .findAll(), ); if (users.isEmpty) { return 'Not found'; } if (users.length > 1) { return 'Found multiple:\n${users.map((u) => u.basicInfo).join('\n')}'; } final user = users.first; await user.packs.load(); await user.subscriptionModel.load(); final sub = user.subscriptionModel.value; return ''' ${user.basicInfo} Tags: ${user.userData.value?.tags.join(',') ?? ''} Packs (${user.packs.length}): ${user.packs.map((p) => '${p.id} ${p.title}').join('\n')} Purchases: ${user.purchases.length} Subscription ${sub == null ? '' : '(${sub.start} - ${sub.finish})'}: ${sub?.features.join(' ') ?? ''} ''' .trim(); } Future> words() async { final packs = await isar.txn(() => isar.cardPackModels.where().findAll()); final words = packs .expand((p) => p.cards.map((c) => c.original)) .whereType() .toList(); return words; } /// Generate a Telegram auth code for a user /// Calls backend API to generate the code Future generateAuthCode({ required String telegramUserId, String? telegramUsername, String? firstName, String? lastName, }) async { try { final client = HttpClient(); try { final uri = Uri.parse('$backendUrl/api/v2/auth/telegram/generate-code'); final request = await client.postUrl(uri); request.headers.contentType = ContentType.json; request.write(jsonEncode({ 'telegramUserId': telegramUserId, if (telegramUsername != null) 'telegramUsername': telegramUsername, if (firstName != null) 'firstName': firstName, if (lastName != null) 'lastName': lastName, })); final response = await request.close(); final responseBody = await response.transform(utf8.decoder).join(); if (response.statusCode != 200) { print('Failed to generate auth code: ' '${response.statusCode} - $responseBody'); return null; } final json = jsonDecode(responseBody) as Map; return json['code'] as String?; } finally { client.close(); } } 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 claimWebAuthCode({ required String code, required String telegramUserId, String? telegramUsername, String? firstName, String? lastName, }) async { try { final client = HttpClient(); try { final uri = Uri.parse('$backendUrl/api/v2/auth/telegram/claim-code'); final request = await client.postUrl(uri); request.headers.contentType = ContentType.json; request.write(jsonEncode({ 'code': code, 'telegramUserId': telegramUserId, if (telegramUsername != null) 'telegramUsername': telegramUsername, if (firstName != null) 'firstName': firstName, if (lastName != null) 'lastName': lastName, })); final response = await request.close(); final responseBody = await response.transform(utf8.decoder).join(); if (response.statusCode != 200) { print('Failed to claim auth code: ' '${response.statusCode} - $responseBody'); return false; } return true; } finally { client.close(); } } 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 canShareToday(String telegramUserId, int dailyLimit) async { try { final today = DateTime.now(); final todayStart = DateTime(today.year, today.month, today.day); final todayEnd = DateTime(today.year, today.month, today.day + 1); // Start of next day final sharesCount = await isar.shareRequestModels .filter() .telegramUserIdEqualTo(telegramUserId) .requestedAtBetween(todayStart, todayEnd) .count(); print('[SHARE_LIMIT] User $telegramUserId: $sharesCount shares today (limit: $dailyLimit)'); return sharesCount < dailyLimit; } 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 recordShareRequest({ required String telegramUserId, String? telegramUsername, int? sharedCardId, }) async { try { final request = ShareRequestModel( telegramUserId: telegramUserId, telegramUsername: telegramUsername, requestedAt: DateTime.now(), sharedCardId: sharedCardId, ); await isar.shareRequestModels.put(request); print('[SHARE_RECORDED] User $telegramUserId shared card $sharedCardId at ${request.requestedAt}'); } 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 getRandomCard() async { try { final cards = await isar.gameCardModels.where().findAll(); if (cards.isEmpty) { return null; } final random = Random(); return cards[random.nextInt(cards.length)]; } catch (e, s) { print('Error getting random card: $e\n$s'); return null; } } Future dispose() async { await isar.close(); } } extension on UserModel { String get basicInfo => '$id ${email ?? ''} $name'; } extension on DateTime { String toShortString() => '$day.$month'; }