package com.screenshot.server.bot;
import com.screenshot.server.config.TelegramBotConfig;
import com.screenshot.server.service.StorageService;
import org.springframework.stereotype.Component;
import org.telegram.telegrambots.bots.TelegramLongPollingBot;
import org.telegram.telegrambots.meta.api.methods.send.SendDocument;
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
import org.telegram.telegrambots.meta.api.objects.InputFile;
import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup;
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton;
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class ScreenshotBot extends TelegramLongPollingBot {
private final TelegramBotConfig botConfig;
private final StorageService storageService;
// Состояния пользователей
private final Map<Long, UserSession> userSessions = new ConcurrentHashMap<>();
private static final String WELCOME_MESSAGE = """
🖼 *Screenshot Storage Bot*
⚠️ *Только для частного использования*
Этот бот предназначен исключительно для авторизованных пользователей.
📋 *Доступные команды:*
/start - Начало работы
/dates - Показать доступные даты
/download - Скачать скриншоты
/status - Статус хранилища
/help - Помощь
""";
private static final String UNAUTHORIZED_MESSAGE = """
⛔ *Доступ запрещён*
Вы не авторизованы для использования этого бота.
Ваш Chat ID: `%d`
Обратитесь к администратору для получения доступа.
""";
public ScreenshotBot(TelegramBotConfig botConfig, StorageService storageService) {
super(botConfig.getToken());
this.botConfig = botConfig;
this.storageService = storageService;
}
@Override
public String getBotUsername() {
return botConfig.getUsername();
}
@Override
public void onUpdateReceived(Update update) {
if (update.hasMessage() && update.getMessage().hasText()) {
handleTextMessage(update);
} else if (update.hasCallbackQuery()) {
handleCallbackQuery(update);
}
}
private void handleTextMessage(Update update) {
Long chatId = update.getMessage().getChatId();
String text = update.getMessage().getText();
// Проверка авторизации
if (!botConfig.isUserAllowed(chatId)) {
sendUnauthorizedMessage(chatId);
return;
}
// Проверяем, ожидаем ли ввод времени
UserSession session = userSessions.get(chatId);
if (session != null && session.state == SessionState.AWAITING_TIME_RANGE) {
handleTimeRangeInput(chatId, text, session);
return;
}
// Обработка команд
switch (text) {
case "/start", "/help" -> sendWelcomeMessage(chatId);
case "/dates" -> sendAvailableDates(chatId);
case "/download" -> startDownloadFlow(chatId);
case "/status" -> sendStorageStatus(chatId);
default -> sendMessage(chatId, "Неизвестная команда. Используйте /help");
}
}
private void handleCallbackQuery(Update update) {
Long chatId = update.getCallbackQuery().getMessage().getChatId();
String data = update.getCallbackQuery().getData();
if (!botConfig.isUserAllowed(chatId)) {
sendUnauthorizedMessage(chatId);
return;
}
if (data.startsWith("date:")) {
handleDateSelection(chatId, data.substring(5));
} else if (data.startsWith("period:")) {
handlePeriodSelection(chatId, data.substring(7));
} else if (data.equals("all_day")) {
downloadAllDay(chatId);
} else if (data.equals("custom_time")) {
askForTimeRange(chatId);
} else if (data.equals("cancel")) {
cancelOperation(chatId);
}
}
private void sendWelcomeMessage(Long chatId) {
sendMarkdownMessage(chatId, WELCOME_MESSAGE);
}
private void sendUnauthorizedMessage(Long chatId) {
sendMarkdownMessage(chatId, String.format(UNAUTHORIZED_MESSAGE, chatId));
}
private void sendAvailableDates(Long chatId) {
List<LocalDate> dates = storageService.getAvailableDates();
if (dates.isEmpty()) {
sendMessage(chatId, "📭 Нет доступных скриншотов");
return;
}
StringBuilder sb = new StringBuilder("📅 *Доступные даты:*\n\n");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy (EEEE)", new Locale("ru"));
int count = 0;
for (LocalDate date : dates) {
if (count++ >= 30) {
sb.append("\n_...и ещё ").append(dates.size() - 30).append(" дат_");
break;
}
int filesCount = storageService.getFilesForDate(date).size();
sb.append("• `").append(date).append("` - ")
.append(date.format(formatter))
.append(" (").append(filesCount).append(" файлов)\n");
}
sendMarkdownMessage(chatId, sb.toString());
}
private void startDownloadFlow(Long chatId) {
List<LocalDate> dates = storageService.getAvailableDates();
if (dates.isEmpty()) {
sendMessage(chatId, "📭 Нет доступных скриншотов для скачивания");
return;
}
// Создаём клавиатуру с датами (последние 10)
InlineKeyboardMarkup markup = new InlineKeyboardMarkup();
List<List<InlineKeyboardButton>> keyboard = new ArrayList<>();
int count = 0;
List<InlineKeyboardButton> row = new ArrayList<>();
for (LocalDate date : dates) {
if (count++ >= 12) break;
InlineKeyboardButton button = new InlineKeyboardButton();
button.setText(date.format(DateTimeFormatter.ofPattern("dd.MM")));
button.setCallbackData("date:" + date);
row.add(button);
if (row.size() == 3) {
keyboard.add(new ArrayList<>(row));
row.clear();
}
}
if (!row.isEmpty()) {
keyboard.add(row);
}
// Кнопка отмены
List<InlineKeyboardButton> cancelRow = new ArrayList<>();
InlineKeyboardButton cancelBtn = new InlineKeyboardButton();
cancelBtn.setText("❌ Отмена");
cancelBtn.setCallbackData("cancel");
cancelRow.add(cancelBtn);
keyboard.add(cancelRow);
markup.setKeyboard(keyboard);
sendMessageWithKeyboard(chatId, "📅 Выберите дату:", markup);
}
private void handleDateSelection(Long chatId, String dateStr) {
try {
LocalDate date = LocalDate.parse(dateStr);
// Сохраняем выбранную дату в сессию
UserSession session = new UserSession();
session.selectedDate = date;
session.state = SessionState.DATE_SELECTED;
userSessions.put(chatId, session);
int filesCount = storageService.getFilesForDate(date).size();
// Предлагаем выбрать период
InlineKeyboardMarkup markup = new InlineKeyboardMarkup();
List<List<InlineKeyboardButton>> keyboard = new ArrayList<>();
// Весь день
List<InlineKeyboardButton> row1 = new ArrayList<>();
InlineKeyboardButton allDayBtn = new InlineKeyboardButton();
allDayBtn.setText("📆 Весь день (" + filesCount + " файлов)");
allDayBtn.setCallbackData("all_day");
row1.add(allDayBtn);
keyboard.add(row1);
// Предустановленные периоды
String[][] periods = {
{"🌅 Утро (06-12)", "period:06:00-12:00"},
{"☀️ День (12-18)", "period:12:00-18:00"},
{"🌙 Вечер (18-00)", "period:18:00-23:59"}
};
for (String[] period : periods) {
List<InlineKeyboardButton> row = new ArrayList<>();
InlineKeyboardButton btn = new InlineKeyboardButton();
btn.setText(period[0]);
btn.setCallbackData(period[1]);
row.add(btn);
keyboard.add(row);
}
// Свой период
List<InlineKeyboardButton> customRow = new ArrayList<>();
InlineKeyboardButton customBtn = new InlineKeyboardButton();
customBtn.setText("⏰ Указать время");
customBtn.setCallbackData("custom_time");
customRow.add(customBtn);
keyboard.add(customRow);
// Отмена
List<InlineKeyboardButton> cancelRow = new ArrayList<>();
InlineKeyboardButton cancelBtn = new InlineKeyboardButton();
cancelBtn.setText("❌ Отмена");
cancelBtn.setCallbackData("cancel");
cancelRow.add(cancelBtn);
keyboard.add(cancelRow);
markup.setKeyboard(keyboard);
String message = String.format("📅 Выбрана дата: *%s*\n\nВыберите период:",
date.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")));
sendMarkdownMessageWithKeyboard(chatId, message, markup);
} catch (DateTimeParseException e) {
sendMessage(chatId, "❌ Ошибка парсинга даты");
}
}
private void handlePeriodSelection(Long chatId, String periodStr) {
UserSession session = userSessions.get(chatId);
if (session == null || session.selectedDate == null) {
sendMessage(chatId, "❌ Сначала выберите дату. Используйте /download");
return;
}
String[] parts = periodStr.split("-");
LocalTime startTime = LocalTime.parse(parts[0]);
LocalTime endTime = LocalTime.parse(parts[1]);
downloadFilesForPeriod(chatId, session.selectedDate, startTime, endTime);
}
private void downloadAllDay(Long chatId) {
UserSession session = userSessions.get(chatId);
if (session == null || session.selectedDate == null) {
sendMessage(chatId, "❌ Сначала выберите дату");
return;
}
downloadFilesForPeriod(chatId, session.selectedDate, LocalTime.MIN, LocalTime.MAX);
}
private void askForTimeRange(Long chatId) {
UserSession session = userSessions.get(chatId);
if (session == null) {
session = new UserSession();
userSessions.put(chatId, session);
}
session.state = SessionState.AWAITING_TIME_RANGE;
sendMessage(chatId, """
⏰ Введите период времени в формате:
`HH:MM-HH:MM`
Например: `09:00-18:30`
Для отмены отправьте /cancel
""");
}
private void handleTimeRangeInput(Long chatId, String text, UserSession session) {
if (text.equals("/cancel")) {
cancelOperation(chatId);
return;
}
try {
String[] parts = text.split("-");
if (parts.length != 2) {
sendMessage(chatId, "❌ Неверный формат. Используйте HH:MM-HH:MM");
return;
}
LocalTime startTime = LocalTime.parse(parts[0].trim());
LocalTime endTime = LocalTime.parse(parts[1].trim());
if (session.selectedDate == null) {
sendMessage(chatId, "❌ Дата не выбрана. Используйте /download");
return;
}
downloadFilesForPeriod(chatId, session.selectedDate, startTime, endTime);
} catch (DateTimeParseException e) {
sendMessage(chatId, "❌ Неверный формат времени. Используйте HH:MM-HH:MM");
}
}
private void downloadFilesForPeriod(Long chatId, LocalDate date, LocalTime startTime, LocalTime endTime) {
sendMessage(chatId, "⏳ Подготовка архива...");
try {
List<Path> files = storageService.getFilesForDateAndPeriod(date, startTime, endTime);
if (files.isEmpty()) {
sendMessage(chatId, "📭 Нет файлов за указанный период");
userSessions.remove(chatId);
return;
}
// Создаём ZIP архивы
List<Path> archives = storageService.createZipArchives(
files,
botConfig.getMaxFileSizeBytes()
);
sendMessage(chatId, String.format(
"📦 Найдено %d файлов. Отправка %d архива(ов)...",
files.size(), archives.size()
));
// Отправляем архивы
for (int i = 0; i < archives.size(); i++) {
Path archive = archives.get(i);
String caption = String.format("📦 Архив %d/%d - %s (%s)",
i + 1, archives.size(),
date.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")),
storageService.formatSize(Files.size(archive))
);
sendDocument(chatId, archive.toFile(), caption);
// Удаляем временный файл
Files.deleteIfExists(archive);
}
sendMessage(chatId, "✅ Отправка завершена!");
} catch (Exception e) {
sendMessage(chatId, "❌ Ошибка: " + e.getMessage());
e.printStackTrace();
} finally {
userSessions.remove(chatId);
}
}
private void sendStorageStatus(Long chatId) {
StorageService.StorageInfo info = storageService.getStorageInfo();
List<LocalDate> dates = storageService.getAvailableDates();
String message = String.format("""
📊 *Статус хранилища*
📁 Путь: `%s`
📄 Всего файлов: %d
💾 Размер: %s
📅 Дат с данными: %d
""",
info.path(),
info.fileCount(),
info.formattedSize(),
dates.size()
);
sendMarkdownMessage(chatId, message);
}
private void cancelOperation(Long chatId) {
userSessions.remove(chatId);
sendMessage(chatId, "❌ Операция отменена");
}
// === Вспомогательные методы отправки ===
private void sendMessage(Long chatId, String text) {
try {
SendMessage message = new SendMessage();
message.setChatId(chatId);
message.setText(text);
execute(message);
} catch (TelegramApiException e) {
e.printStackTrace();
}
}
private void sendMarkdownMessage(Long chatId, String text) {
try {
SendMessage message = new SendMessage();
message.setChatId(chatId);
message.setText(text);
message.setParseMode("Markdown");
execute(message);
} catch (TelegramApiException e) {
e.printStackTrace();
}
}
private void sendMessageWithKeyboard(Long chatId, String text, InlineKeyboardMarkup markup) {
try {
SendMessage message = new SendMessage();
message.setChatId(chatId);
message.setText(text);
message.setReplyMarkup(markup);
execute(message);
} catch (TelegramApiException e) {
e.printStackTrace();
}
}
private void sendMarkdownMessageWithKeyboard(Long chatId, String text, InlineKeyboardMarkup markup) {
try {
SendMessage message = new SendMessage();
message.setChatId(chatId);
message.setText(text);
message.setParseMode("Markdown");
message.setReplyMarkup(markup);
execute(message);
} catch (TelegramApiException e) {
e.printStackTrace();
}
}
private void sendDocument(Long chatId, File file, String caption) {
try {
SendDocument sendDocument = new SendDocument();
sendDocument.setChatId(chatId);
sendDocument.setDocument(new InputFile(file));
sendDocument.setCaption(caption);
execute(sendDocument);
} catch (TelegramApiException e) {
e.printStackTrace();
}
}
// === Вспомогательные классы ===
private enum SessionState {
NONE,
DATE_SELECTED,
AWAITING_TIME_RANGE
}
private static class UserSession {
SessionState state = SessionState.NONE;
LocalDate selectedDate;
}
}