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:process_run/process_run.dart';
|
|
|
|
|
|
import 'package:teledart/teledart.dart';
|
|
|
|
|
|
import 'package:teledart/telegram.dart';
|
2025-12-11 17:49:15 +00:00
|
|
|
|
import 'package:mnemo_cards_telegram_bot/backend_client.dart';
|
2025-11-10 23:55:41 +00:00
|
|
|
|
|
|
|
|
|
|
const String version = '0.0.1';
|
|
|
|
|
|
|
2025-12-11 17:49:15 +00:00
|
|
|
|
void main() async {
|
|
|
|
|
|
return _run();
|
2025-11-10 23:55:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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({
|
2025-12-11 17:49:15 +00:00
|
|
|
|
required BackendClient backendClient,
|
2025-11-10 23:55:41 +00:00
|
|
|
|
required TeleDart teledartInstance,
|
|
|
|
|
|
required dynamic message,
|
|
|
|
|
|
required String code,
|
|
|
|
|
|
}) async {
|
|
|
|
|
|
final from = message.from;
|
|
|
|
|
|
if (from == null) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-11 17:49:15 +00:00
|
|
|
|
final claimed = await backendClient.claimWebAuthCode(
|
2025-11-10 23:55:41 +00:00
|
|
|
|
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,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-11 17:49:15 +00:00
|
|
|
|
Future<void> _run() async {
|
2025-11-10 23:55:41 +00:00
|
|
|
|
log('Starting bot version $version');
|
2025-12-11 17:49:15 +00:00
|
|
|
|
BackendClient? backendClient;
|
2025-11-10 23:55:41 +00:00
|
|
|
|
TeleDart? teledart;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2025-12-11 17:49:15 +00:00
|
|
|
|
final config = BotConfig.fromEnvironment();
|
2025-11-10 23:55:41 +00:00
|
|
|
|
|
|
|
|
|
|
final lines = File('admins').readAsLinesSync();
|
|
|
|
|
|
|
|
|
|
|
|
final adminIds = lines.map((e) => e.trim()).toList();
|
|
|
|
|
|
if (adminIds.isEmpty) {
|
2025-12-11 17:49:15 +00:00
|
|
|
|
log('No admins found in admins file');
|
2025-11-10 23:55:41 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-11 17:49:15 +00:00
|
|
|
|
final username = (await Telegram(config.botToken).getMe()).username;
|
|
|
|
|
|
teledart = TeleDart(config.botToken, Event(username!));
|
2025-11-10 23:55:41 +00:00
|
|
|
|
|
|
|
|
|
|
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}');
|
|
|
|
|
|
|
2025-12-11 17:49:15 +00:00
|
|
|
|
backendClient = BackendClient(
|
2025-11-10 23:55:41 +00:00
|
|
|
|
backendUrl: config.backendUrl,
|
2025-12-11 17:49:15 +00:00
|
|
|
|
apiKey: config.apiKey,
|
2025-11-10 23:55:41 +00:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// At this point all variables are initialized and non-null
|
|
|
|
|
|
final teledartInstance = teledart;
|
2025-12-11 17:49:15 +00:00
|
|
|
|
final backendClientInstance = backendClient;
|
2025-11-10 23:55:41 +00:00
|
|
|
|
|
|
|
|
|
|
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(
|
2025-12-11 17:49:15 +00:00
|
|
|
|
backendClient: backendClientInstance,
|
2025-11-10 23:55:41 +00:00
|
|
|
|
teledartInstance: teledartInstance,
|
|
|
|
|
|
message: message,
|
|
|
|
|
|
code: payloadCode,
|
|
|
|
|
|
);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Generate auth code for the user (legacy flow)
|
2025-12-11 17:49:15 +00:00
|
|
|
|
final code = await backendClientInstance.generateAuthCode(
|
2025-11-10 23:55:41 +00:00
|
|
|
|
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
|
2025-12-11 17:49:15 +00:00
|
|
|
|
final code = await backendClientInstance.generateAuthCode(
|
2025-11-10 23:55:41 +00:00
|
|
|
|
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
|
2025-12-11 17:49:15 +00:00
|
|
|
|
final code = await backendClientInstance.generateAuthCode(
|
2025-11-10 23:55:41 +00:00
|
|
|
|
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())) {
|
2025-12-11 17:49:15 +00:00
|
|
|
|
final info = await backendClientInstance.info();
|
2025-11-10 23:55:41 +00:00
|
|
|
|
message.reply(info);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
teledartInstance.onCommand('user').listen((message) async {
|
|
|
|
|
|
if (adminIds.contains(message.from?.id.toString())) {
|
|
|
|
|
|
final userId = message.text?.replaceFirst('/user', '').trim() ?? '';
|
2025-12-11 17:49:15 +00:00
|
|
|
|
final info = await backendClientInstance.userInfo(userId);
|
2025-11-10 23:55:41 +00:00
|
|
|
|
message.reply(info);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
teledartInstance.onCommand('words').listen((message) async {
|
|
|
|
|
|
if (adminIds.contains(message.from?.id.toString())) {
|
|
|
|
|
|
final separator = message.text?.replaceFirst('/words', '').trim() ?? '';
|
2025-12-11 17:49:15 +00:00
|
|
|
|
final words = await backendClientInstance.words();
|
2025-11-10 23:55:41 +00:00
|
|
|
|
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
|
2025-12-11 17:49:15 +00:00
|
|
|
|
if (!await backendClientInstance.canShareToday(
|
2025-11-10 23:55:41 +00:00
|
|
|
|
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
|
2025-12-11 17:49:15 +00:00
|
|
|
|
final card = await backendClientInstance.getRandomCard();
|
2025-11-10 23:55:41 +00:00
|
|
|
|
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
|
2025-12-11 17:49:15 +00:00
|
|
|
|
await backendClientInstance.recordShareRequest(
|
2025-11-10 23:55:41 +00:00
|
|
|
|
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(
|
2025-12-11 17:49:15 +00:00
|
|
|
|
backendClient: backendClientInstance,
|
2025-11-10 23:55:41 +00:00
|
|
|
|
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) {
|
2025-12-11 17:49:15 +00:00
|
|
|
|
log('Error: $e', error: e, stackTrace: s);
|
2025-11-10 23:55:41 +00:00
|
|
|
|
print('Error while starting bot: $e');
|
|
|
|
|
|
print('Stack trace: $s');
|
2025-12-11 17:49:15 +00:00
|
|
|
|
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)');
|
2025-11-10 23:55:41 +00:00
|
|
|
|
|
|
|
|
|
|
// Cleanup resources before restart
|
|
|
|
|
|
try {
|
2025-12-11 17:49:15 +00:00
|
|
|
|
backendClient?.dispose();
|
2025-11-10 23:55:41 +00:00
|
|
|
|
} catch (_) {
|
|
|
|
|
|
// Ignore cleanup errors
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
print('Restarting in 5 seconds...');
|
|
|
|
|
|
await Future.delayed(const Duration(seconds: 5));
|
2025-12-11 17:49:15 +00:00
|
|
|
|
return _run();
|
2025-11-10 23:55:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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')}';
|
|
|
|
|
|
}
|