mnemo_cards/mnemo_cards_telegram_bot/bin/main.dart
2025-11-11 02:55:41 +03:00

624 lines
19 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:developer';
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:args/args.dart';
import 'package:process_run/process_run.dart';
import 'package:teledart/teledart.dart';
import 'package:teledart/telegram.dart';
import 'db_manager.dart';
const String version = '0.0.1';
void printUsage(ArgParser argParser) {
print('Usage: dart mnemo_cards_telegram_bot.dart <flags> [arguments]');
print(argParser.usage);
}
void main(List<String> arguments) async {
return _run(arguments);
}
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<void> _handleCodeClaim({
required DBManager dbManager,
required TeleDart teledartInstance,
required dynamic message,
required String code,
}) async {
final from = message.from;
if (from == null) {
return;
}
final claimed = await dbManager.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<void> _run(List<String> arguments) async {
final ArgParser argParser = buildParser();
log('Starting bot version $version');
DBManager? dbManager;
TeleDart? teledart;
try {
final ArgResults results = argParser.parse(arguments);
final config = BotConfig.fromArgs(results);
final lines = File('admins').readAsLinesSync();
final adminIds = lines.map((e) => e.trim()).toList();
if (adminIds.isEmpty) {
return;
}
var BOT_TOKEN = '7057032753:AAFP-BCpdry-YuBUOqx1W8dGxiFQdSOJlpI';
final username = (await Telegram(BOT_TOKEN).getMe()).username;
teledart = TeleDart(BOT_TOKEN, Event(username!));
Future<void> notifyAdmins(String message) async {
for (final admin in adminIds) {
try {
await teledart!.sendMessage(admin, message);
} catch (e) {
log('Failed to notify admin $admin', error: e);
}
}
}
log('Using backend API at ${config.backendUrl}');
dbManager = DBManager(
notifyAdmins,
backendUrl: config.backendUrl,
);
await dbManager.init(results);
// At this point all variables are initialized and non-null
final teledartInstance = teledart;
final dbManagerInstance = dbManager;
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(
dbManager: dbManagerInstance,
teledartInstance: teledartInstance,
message: message,
code: payloadCode,
);
return;
}
// Generate auth code for the user (legacy flow)
final code = await dbManagerInstance.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 dbManagerInstance.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 dbManagerInstance.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('Что-то пошло не так');
}
log('app error', error: e, stackTrace: s);
}
});
teledartInstance.onCommand('info').listen((message) async {
if (adminIds.contains(message.from?.id.toString())) {
final info = await dbManagerInstance.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 dbManagerInstance.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 dbManagerInstance.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) {
log('app error', error: e, stackTrace: 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');
log('clear cache error $dir', error: e, stackTrace: 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<File>();
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) {
log('app error', error: e, stackTrace: 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<File>()
.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) {
log('app error', error: e, stackTrace: 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<File>().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<File>();
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) {
log('backup error', error: e, stackTrace: 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 dbManagerInstance.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 dbManagerInstance.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 dbManagerInstance.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(
dbManager: dbManagerInstance,
teledartInstance: teledartInstance,
message: message,
code: code,
);
return;
}
await message.reply('Спасибо, переслал.');
teledartInstance.forwardMessage(
adminIds.first,
message.chat.id,
message.messageId,
messageThreadId: message.messageThreadId,
);
});
teledartInstance.start();
log('Bot started');
} on Exception catch (e, s) {
print('Error: $e');
print('');
printUsage(argParser);
print('Error while starting bot: $e');
print('Stack trace: $s');
// Cleanup resources before restart
try {
await dbManager?.dispose();
} catch (_) {
// Ignore cleanup errors
}
print('Restarting in 5 seconds...');
await Future.delayed(const Duration(seconds: 5));
return _run(arguments);
}
}
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')}';
}