import 'dart:io'; import 'package:path/path.dart'; import 'package:mnemo_cards_telegram_bot/bot_config.dart'; import 'package:mnemo_cards_telegram_bot/image_generator.dart'; import 'package:process_run/process_run.dart'; import 'package:teledart/teledart.dart'; import 'package:teledart/telegram.dart'; import 'package:mnemo_cards_telegram_bot/backend_client.dart'; const String version = '0.0.1'; void main() async { return _run(); } String? _extractCodeFromText(String? rawText) { if (rawText == null) { return null; } final text = rawText.trim(); if (text.isEmpty) { return null; } if (text.startsWith('/start')) { final parts = text.split(' '); if (parts.length > 1) { final payload = parts.sublist(1).join(' ').trim(); return _extractCodeFromPayload(payload); } return null; } return _extractCodeFromPayload(text); } String? _extractCodeFromPayload(String payload) { final sanitized = payload.trim(); if (sanitized.isEmpty) { return null; } final match = RegExp(r'(\d{6})$').firstMatch(sanitized); if (match == null) { return null; } final code = match.group(1); if (code == null) { return null; } return RegExp(r'^\d{6}$').hasMatch(code) ? code : null; } Future _handleCodeClaim({ required BackendClient backendClient, required TeleDart teledartInstance, required dynamic message, required String code, }) async { final from = message.from; if (from == null) { return; } final claimed = await backendClient.claimWebAuthCode( code: code, telegramUserId: from.id.toString(), telegramUsername: from.username, firstName: from.firstName, lastName: from.lastName, ); if (claimed) { await teledartInstance.sendMessage( message.chat.id, 'Код подтверждён ✅\n' 'Вернись на сайт и нажми «Войти» в течение 5 минут.', replyToMessageId: message.messageId, ); } else { await teledartInstance.sendMessage( message.chat.id, 'Не удалось подтвердить код 😔\n' 'Возможно он уже использован или истёк. Сгенерируй новый код на сайте.', replyToMessageId: message.messageId, ); } } Future _run() async { print('[BOT] Starting bot version $version'); BackendClient? backendClient; TeleDart? teledart; try { final config = BotConfig.fromEnvironment(); final lines = File('admins').readAsLinesSync(); final adminIds = lines.map((e) => e.trim()).toList(); if (adminIds.isEmpty) { print('[BOT] ERROR: No admins found in admins file'); return; } final username = (await Telegram(config.botToken).getMe()).username; teledart = TeleDart(config.botToken, Event(username!)); Future notifyAdmins(String message) async { for (final admin in adminIds) { try { await teledart!.sendMessage(admin, message); } catch (e) { print('[BOT] ERROR: Failed to notify admin $admin: $e'); } } } print('[BOT] Using backend API at ${config.backendUrl}'); backendClient = BackendClient( backendUrl: config.backendUrl, apiKey: config.apiKey, ); // At this point all variables are initialized and non-null final teledartInstance = teledart; final backendClientInstance = backendClient; teledartInstance.onCommand('logs').listen( (message) async { String result; try { final r = await Process.run( 'journalctl', ['-u', 'mnemo_cards_backend', '--since', "5min ago"], ); result = '${r.stdout.toString()} ${r.stderr}'; } catch (e) { result = 'Error: $e'; } message.reply(result); }, ); teledart.onCommand('start').listen( (message) async { final from = message.from; if (from == null) { return; } final payloadCode = _extractCodeFromText(message.text); if (payloadCode != null) { await _handleCodeClaim( backendClient: backendClientInstance, teledartInstance: teledartInstance, message: message, code: payloadCode, ); return; } // Generate auth code for the user (legacy flow) final code = await backendClientInstance.generateAuthCode( telegramUserId: from.id.toString(), telegramUsername: from.username, firstName: from.firstName, lastName: from.lastName, ); if (code == null) { await message.reply( 'Ошибка при генерации кода. Попробуй позже.', ); } else { await message.reply( 'Привет! Для авторизации на сайте используй этот код:\n\n' '**$code**\n\n' 'Код действителен 5 минут.', ); } }, ); teledart.onCommand('login').listen( (message) async { final from = message.from; if (from == null) { return; } // Generate auth code for the user final code = await backendClientInstance.generateAuthCode( telegramUserId: from.id.toString(), telegramUsername: from.username, firstName: from.firstName, lastName: from.lastName, ); if (code == null) { await message.reply( 'Ошибка при генерации кода. Попробуй позже.', ); } else { await message.reply( 'Код для авторизации на сайте:\n\n' '**$code**\n\n' 'Код действителен 5 минут.\n' 'Введи его на сайте для входа.', ); } }, ); teledart.onCommand('code').listen( (message) async { final from = message.from; if (from == null) { return; } // Generate auth code for the user final code = await backendClientInstance.generateAuthCode( telegramUserId: from.id.toString(), telegramUsername: from.username, firstName: from.firstName, lastName: from.lastName, ); if (code == null) { await message.reply( 'Ошибка при генерации кода. Попробуй позже.', ); } else { await message.reply( 'Твой код авторизации:\n\n' '**$code**\n\n' '⏱ Действителен 5 минут', ); } }, ); teledartInstance.onCommand('app').listen((message) { try { final file = File('apks/app.apk'); final t = file.lastModifiedSync(); teledartInstance.sendMessage(message.chat.id, 'Загрузка apk...'); message.replyDocument(file, caption: t.simpleString); } on Object catch (e, s) { if (adminIds.contains(message.from?.id.toString())) { message.reply(e.toString()); } else { message.reply('Что-то пошло не так'); } print('[BOT] ERROR: app error: $e\n$s'); } }); teledartInstance.onCommand('info').listen((message) async { if (adminIds.contains(message.from?.id.toString())) { final info = await backendClientInstance.info(); message.reply(info); } }); teledartInstance.onCommand('user').listen((message) async { if (adminIds.contains(message.from?.id.toString())) { final userId = message.text?.replaceFirst('/user', '').trim() ?? ''; final info = await backendClientInstance.userInfo(userId); message.reply(info); } }); teledartInstance.onCommand('words').listen((message) async { if (adminIds.contains(message.from?.id.toString())) { final separator = message.text?.replaceFirst('/words', '').trim() ?? ''; final words = await backendClientInstance.words(); message.reply(words.join(separator.isEmpty ? ',\n' : separator)); } }); teledartInstance.onCommand('admin_app').listen((message) { if (adminIds.contains(message.from?.id.toString())) { try { final file = File('apks/admin_app.apk'); final t = file.lastModifiedSync(); teledartInstance.sendMessage(message.chat.id, 'Загрузка apk...'); message.replyDocument(file, caption: t.simpleString); } on Object catch (e, s) { print('[BOT] ERROR: app error: $e\n$s'); message.reply(e.toString()); } } }); teledartInstance.onCommand('clear_cache').listen((message) async { var answer = StringBuffer(); if (adminIds.contains(message.from?.id.toString())) { final types = ['images', 'cards']; final sizes = ['big', 'small', 'medium', 'extraSmall']; for (final type in types) { for (final size in sizes) { final dir = Directory('../mnemo_cards_backend/data/$type/$size'); if (await dir.exists()) { try { await dir.delete(recursive: true); answer.write('$type/$size - ok\n'); } catch (e, s) { answer.write('$type/$size - error\n'); print('[BOT] ERROR: clear cache error $dir: $e\n$s'); } } else { answer.write('$type/$size - not exists\n'); } } } message.reply(answer.toString()); } }); teledartInstance.onCommand('images').listen((message) async { if (adminIds.contains(message.from?.id.toString())) { try { final loadingMessage = await teledartInstance.sendMessage( message.chat.id, 'Загрузка...', ); final cardsDir = Directory('../mnemo_cards_backend/data/cards/'); final dir = Directory('zips'); if (dir.existsSync()) { dir.deleteSync(recursive: true); } dir.createSync(); var shell = Shell(); await shell.run( 'zip -s 49m -vr ${join(dir.path, 'images.zip')} ${cardsDir.path}'); final zips = dir.listSync().whereType(); int i = 0; print('Will send ${zips.length} zip files:'); print('${zips.map((e) => e.path).toList()}'); teledartInstance.deleteMessage( loadingMessage.chat.id, loadingMessage.messageId, ); for (final zip in zips) { i++; await teledartInstance.sendDocument( message.chat.id, zip, caption: '$i/${zips.length}', ); } } on Object catch (e, s) { print('[BOT] ERROR: app error: $e\n$s'); message.reply(e.toString()); } } }); teledartInstance.onCommand('cards').listen((message) async { if (adminIds.contains(message.from?.id.toString())) { try { final cardsDir = Directory('../mnemo_cards_backend/data/cards/'); final names = cardsDir .listSync() .whereType() .map((f) => basename(f.path)) .toList() ..sort((p, n) { final pParts = p.split('_'); final nParts = n.split('_'); return (int.tryParse(pParts.isNotEmpty ? pParts.first : '') ?? 99999) .compareTo( int.tryParse(nParts.isNotEmpty ? nParts.first : '') ?? 9999); }); message.reply( names.take(50).join('\n'), ); if (names.length > 50) { message.reply( names.sublist(50).join('\n'), ); } } on Object catch (e, s) { print('[BOT] ERROR: app error: $e\n$s'); message.reply(e.toString()); } } }); teledartInstance.onCommand('backup').listen((message) async { if (adminIds.contains(message.from?.id.toString())) { try { final dir = Directory('backups/'); if (!dir.existsSync()) { message.reply('No backups'); } final files = dir.listSync().whereType().toList(); files.sort( (a, b) => b.lastModifiedSync().compareTo(a.lastModifiedSync())); if (files.isNotEmpty) { teledartInstance.sendMessage(message.chat.id, 'Загрузка backup...'); var shell = Shell(); final file = files.first; final name = basename(file.path).split('.').first; final dir = Directory('zips'); if (dir.existsSync()) { dir.deleteSync(recursive: true); } dir.createSync(); await shell .run('zip -s 49m ${join(dir.path, '$name.zip')} ${file.path}'); final zips = dir.listSync().whereType(); int i = 0; print('Will send ${zips.length} zip files:'); print('${zips.map((e) => e.path).toList()}'); for (final zip in zips) { i++; await teledartInstance.sendDocument( message.chat.id, zip, caption: '$i/${zips.length}', ); } // dir.deleteSync(); } else { message.reply('No backups'); } } on Object catch (e, s) { print('[BOT] ERROR: backup error: $e\n$s'); message.reply(e.toString()); } } }); teledartInstance.onCommand('share').listen((message) async { try { final from = message.from; if (from == null) { await message.reply('Ошибка: не удалось определить пользователя'); return; } final telegramUserId = from.id.toString(); print('[SHARE_CMD] User $telegramUserId requested /share command'); // Check rate limit if (!await backendClientInstance.canShareToday( telegramUserId, config.shareDailyLimit, )) { print('[SHARE_BLOCKED] User $telegramUserId reached daily limit'); await message.reply( 'Ты уже поделился сегодня. Попробуй завтра! ' '(Лимит: ${config.shareDailyLimit} раз в день)', ); return; } // Send loading message final loadingMsg = await teledartInstance.sendMessage( message.chat.id, 'Готовлю красивое сообщение...', ); // Get random card final card = await backendClientInstance.getRandomCard(); if (card == null) { try { await teledartInstance.editMessageText( 'К сожалению, карточки недоступны. Попробуй позже.', chatId: loadingMsg.chat.id.toString(), messageId: loadingMsg.messageId, ); } catch (e) { print('Error editing message: $e'); } return; } // Generate image final imageGenerator = ImageGenerator(); final imageBytes = await imageGenerator.generateShareImage(card); if (imageBytes == null || imageBytes.isEmpty) { try { await teledartInstance.editMessageText( 'Не удалось создать изображение. Попробуй позже.', chatId: loadingMsg.chat.id.toString(), messageId: loadingMsg.messageId, ); } catch (e) { print('Error editing message: $e'); } return; } // Prepare share message final shareMessage = 'Приветствую! Я тестирую приложение mnemo cards ' 'для изучения языков.\n\n' 'Присоединяйся и начни учиться вместе со мной!'; // Delete loading message try { await teledartInstance.deleteMessage( loadingMsg.chat.id, loadingMsg.messageId, ); } catch (e) { print('Error deleting loading message: $e'); } // Send image with caption message.reply('Sending image...'); File? temp; try { final dir = await Directory.systemTemp.createTemp('share_image'); temp = await File("${dir.path}/${DateTime.now().millisecondsSinceEpoch}.png").create(); await temp.writeAsBytes(imageBytes); await message.replyPhoto( temp, caption: shareMessage, ); print('[SHARE_SUCCESS] Image sent to user $telegramUserId'); } catch (e) { message.reply('Error sending image: $e'); print('Error sending image: $e'); } finally { await temp?.delete(); } // Record the share request await backendClientInstance.recordShareRequest( telegramUserId: telegramUserId, telegramUsername: from.username, sharedCardId: card.id, ); print('[SHARE_COMPLETE] User $telegramUserId completed share flow'); } catch (e, s) { print('Error in /share command: $e'); print('Stack trace: $s'); await message.reply( 'Ошибка при создании сообщения. Пожалуйста, попробуй позже.', ); } }); teledartInstance.onMessage().listen((message) async { final text = message.text?.trim(); // Skip command updates - handled by command listeners if (text != null && text.startsWith('/')) { return; } final code = _extractCodeFromText(text); if (code != null) { await _handleCodeClaim( backendClient: backendClientInstance, teledartInstance: teledartInstance, message: message, code: code, ); return; } await message.reply('Спасибо, переслал.'); teledartInstance.forwardMessage( adminIds.first, message.chat.id, message.messageId, messageThreadId: message.messageThreadId, ); }); teledartInstance.start(); print('[BOT] Bot started successfully'); } on Exception catch (e, s) { print('[BOT] ERROR: Error: $e\n$s'); print('[BOT] Error while starting bot: $e'); print('Stack trace: $s'); print(''); print('Required environment variables:'); print(' - TELEGRAM_BOT_TOKEN (required)'); print(' - TELEGRAM_BOT_API_KEY (required)'); print(' - BACKEND_URL or MNEMO_BACKEND_URL (optional, default: https://api.mnemo-cards.online)'); print(' - BOT_SHARE_DAILY_LIMIT (optional, default: 1)'); // Cleanup resources before restart try { backendClient?.dispose(); } catch (_) { // Ignore cleanup errors } print('Restarting in 5 seconds...'); await Future.delayed(const Duration(seconds: 5)); return _run(); } } extension on DateTime { String get simpleString => '${year}.${month.toString().padLeft(2, '0')}.${day.toString().padLeft(2, '0')} ' '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}'; }