mnemo_cards/mnemo_cards_telegram_bot/bin/db_manager.dart

299 lines
8.8 KiB
Dart
Raw Normal View History

2025-11-10 23:55:41 +00:00
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;
2025-11-16 16:31:22 +00:00
DBManager(this.notify, {this.backendUrl = 'http://localhost:8443'});
2025-11-10 23:55:41 +00:00
Future<void> 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<Isar> _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<String> 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<String> 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<List<String>> words() async {
final packs = await isar.txn(() => isar.cardPackModels.where().findAll());
final words = packs
.expand((p) => p.cards.map((c) => c.original))
.whereType<String>()
.toList();
return words;
}
/// 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 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<String, dynamic>;
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<bool> 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<bool> 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<void> 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<GameCardModel?> 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<void> dispose() async {
await isar.close();
}
}
extension on UserModel {
String get basicInfo => '$id ${email ?? ''} $name';
}
extension on DateTime {
String toShortString() => '$day.$month';
}