mnemo_cards/mnemo_cards_telegram_bot/bin/main.dart

625 lines
19 KiB
Dart
Raw Normal View History

2025-11-10 23:55:41 +00:00
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')}';
}