forked from Sanders/TelegaBot
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad0cd40099 | ||
|
|
43c12b96e3 | ||
|
|
97bfe3dc50 | ||
|
|
3fa0fe1996 | ||
|
|
4f92087d9f | ||
|
|
9056b4dbd9 | ||
|
|
8b11ae080e | ||
|
|
a5ec9c38d2 | ||
|
|
202a091d6e | ||
|
|
c0b07df439 | ||
|
|
f21e3ec2b2 |
@@ -7,6 +7,8 @@
|
||||
<env name="DB_URL" value="jdbc:postgresql://192.168.0.10:5432/telegabot" />
|
||||
<env name="DB_USER" value="postgres" />
|
||||
<env name="OPENROUTER_API_KEY" value="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" />
|
||||
<env name="PROXY_HOST" value="192.168.0.5" />
|
||||
<env name="PROXY_PORT" value="1984" />
|
||||
</envs>
|
||||
<module name="TelegaBot.main" />
|
||||
<option name="SPRING_BOOT_MAIN_CLASS" value="ru.cathub.telegabot.TelegaBotApplication" />
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ COPY gradle ./gradle
|
||||
RUN ./gradlew clean build -x test
|
||||
|
||||
# Запуск приложения
|
||||
FROM openjdk:21-jdk-slim
|
||||
FROM eclipse-temurin:21-jre-jammy
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/build/libs/*.jar app.jar
|
||||
|
||||
|
||||
@@ -17,16 +17,21 @@ import static ru.cathub.telegabot.utils.Constants.*;
|
||||
|
||||
public class KeyboardHelper {
|
||||
|
||||
public static ReplyKeyboardMarkup getProfileMenuKeyboard() {
|
||||
// Изменена сигнатура метода
|
||||
public static ReplyKeyboardMarkup getProfileMenuKeyboard(boolean isAdmin) {
|
||||
List<KeyboardRow> keyboardRows = new ArrayList<>();
|
||||
|
||||
KeyboardRow row1 = new KeyboardRow();
|
||||
row1.add(new KeyboardButton(SET_BOOKS_GOAL));
|
||||
|
||||
// Добавляем кнопку редактирования модели ИИ только для админа
|
||||
if (isAdmin) {
|
||||
row1.add(new KeyboardButton(EDIT_AI_MODEL));
|
||||
}
|
||||
|
||||
KeyboardRow row2 = new KeyboardRow();
|
||||
row2.add(new KeyboardButton(BACK));
|
||||
|
||||
//keyboardRows.add(row1);
|
||||
keyboardRows.add(row1);
|
||||
keyboardRows.add(row2);
|
||||
|
||||
ReplyKeyboardMarkup keyboardMarkup = new ReplyKeyboardMarkup(keyboardRows);
|
||||
@@ -89,6 +94,17 @@ public class KeyboardHelper {
|
||||
return keyboardMarkup;
|
||||
}
|
||||
|
||||
public static InlineKeyboardMarkup getEditTargerBooksKeyboard(){
|
||||
List<InlineKeyboardRow> rows = new ArrayList<>();
|
||||
InlineKeyboardRow navRow = new InlineKeyboardRow();
|
||||
navRow.add(InlineKeyboardButton.builder()
|
||||
.text("Изменить")
|
||||
.callbackData("EDIT_TARGET")
|
||||
.build());
|
||||
rows.add(navRow);
|
||||
return new InlineKeyboardMarkup(rows);
|
||||
}
|
||||
|
||||
public static InlineKeyboardMarkup getBookListKeyboard(int currentPage, boolean hasNext, Page<Book> books, boolean forEdit) {
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package ru.cathub.telegabot.configuration;
|
||||
|
||||
import lombok.Setter;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import java.net.Proxy;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties
|
||||
@@ -14,11 +18,22 @@ import org.springframework.web.client.RestTemplate;
|
||||
public class OpenRouterConfig {
|
||||
private String key;
|
||||
private String url;
|
||||
private String model;
|
||||
|
||||
@Value("${proxy.host:}")
|
||||
private String proxyHost;
|
||||
|
||||
@Value("${proxy.port:0}")
|
||||
private int proxyPort;
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
return new RestTemplate();
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
|
||||
if (proxyHost != null && !proxyHost.isBlank() && proxyPort > 0) {
|
||||
factory.setProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)));
|
||||
}
|
||||
|
||||
return new RestTemplate(factory);
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
@@ -28,8 +43,4 @@ public class OpenRouterConfig {
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package ru.cathub.telegabot.configuration;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -9,10 +10,20 @@ import org.telegram.telegrambots.meta.generics.TelegramClient;
|
||||
@Configuration
|
||||
public class TelegramConfig {
|
||||
|
||||
@Value("${proxy.host:}")
|
||||
private String proxyHost;
|
||||
|
||||
@Value("${proxy.port:0}")
|
||||
private int proxyPort;
|
||||
|
||||
@Bean
|
||||
public TelegramClient telegramClient(@Value("${bot.token}") String token) {
|
||||
return new OkHttpTelegramClient(token);
|
||||
OkHttpClient.Builder builder = new OkHttpClient.Builder();
|
||||
if (proxyHost != null && !proxyHost.isBlank() && proxyPort > 0) {
|
||||
builder.proxy(new java.net.Proxy(java.net.Proxy.Type.HTTP, new java.net.InetSocketAddress(proxyHost, proxyPort)));
|
||||
}
|
||||
OkHttpClient okHttpClient = builder.build();
|
||||
|
||||
return new OkHttpTelegramClient(okHttpClient, token);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package ru.cathub.telegabot.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Entity
|
||||
// Указываем имя таблицы и схему из application.properties
|
||||
@Table(name = "bot_settings", schema = "telegabot")
|
||||
public class BotSetting {
|
||||
|
||||
@Id
|
||||
@Column(name = "setting_key", nullable = false, unique = true)
|
||||
private String settingKey; // Ключ настройки (например, "ENABLE_WELCOME_MESSAGE")
|
||||
|
||||
@Column(name = "setting_value", columnDefinition = "TEXT") // Используем TEXT для гибкости
|
||||
private String settingValue; // Значение настройки
|
||||
|
||||
// Можно добавить поле для описания, если нужно
|
||||
// @Column(name = "description")
|
||||
// private String description;
|
||||
}
|
||||
@@ -21,7 +21,9 @@ public class BotUser {
|
||||
USER, ADMIN }
|
||||
|
||||
public enum WorkingMode {
|
||||
BOOK,TORRENT,AI,EDIT_PROFILE,NO_MODE,AI_FILTER,AI_SUGGEST
|
||||
BOOK, TORRENT, AI, EDIT_PROFILE, NO_MODE, AI_FILTER, AI_SUGGEST,
|
||||
// Новые состояния для редактирования профиля
|
||||
EDIT_PROFILE_BOOKS_GOAL, EDIT_PROFILE_AI_MODEL
|
||||
}
|
||||
|
||||
private Integer booksToRead;
|
||||
|
||||
@@ -2,12 +2,12 @@ package ru.cathub.telegabot.model;
|
||||
|
||||
public enum TextRole {
|
||||
DEFAULT("Ты книжный ассистент в Telegram-боте. Отвечай, используй эмоджи, разметку Markdown (*для\n" +
|
||||
" выделения*, _для наклонного_). Отвечай ТОЛЬКО на русском языке."),
|
||||
" выделения*, _для наклонного_). Помни, что эти символы нельзя использовать просто так, не для выделения текста. Отвечай ТОЛЬКО на русском языке."),
|
||||
SUGGESTER("Ты книжный ассистент в Telegram-боте, который помогает находить новые книги. Отвечай, используй эмоджи, разметку Markdown (*для\n" +
|
||||
" выделения*, _для наклонного_). Тебе будет передан список книг пользователя и ты должен проанализировать\n" +
|
||||
" выделения*, _для наклонного_). Помни, что эти символы нельзя использовать просто так, не для выделения текста. Тебе будет передан список книг пользователя и ты должен проанализировать\n" +
|
||||
" этот список, и далее отвечать операясь на предпочтения пользоватья из этого списка"),
|
||||
FILTERER("Ты книжный ассистент в Telegram-боте, который фильтрует списки. Отвечай, используй эмоджи, разметку Markdown (*для\n" +
|
||||
" выделения*, _для наклонного_). Тебе будет передан список книг пользователя и ты должен вывести книги,\n"+
|
||||
" выделения*, _для наклонного_). Помни, что эти символы нельзя использовать просто так, не для выделения текста.Тебе будет передан список книг пользователя и ты должен вывести книги,\n"+
|
||||
" которые удовлетворяют запросу пользователя из этого списка. Если таких книг нет, то отвечай, что книги не найдены"),
|
||||
RECOMMENDER("Рекомендуй книги на основе предпочтений");
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package ru.cathub.telegabot.repository;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import ru.cathub.telegabot.model.BotSetting;
|
||||
|
||||
@Repository
|
||||
public interface BotSettingRepository extends JpaRepository<BotSetting, String> {
|
||||
// JpaRepository уже предоставляет findById (для получения по ключу), save (для сохранения/обновления), findAll и т.д.
|
||||
// Дополнительные методы пока не требуются.
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package ru.cathub.telegabot.service;
|
||||
|
||||
import ru.cathub.telegabot.exception.ServiceException;
|
||||
import ru.cathub.telegabot.model.BotUser;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface BotSettingService {
|
||||
|
||||
/**
|
||||
* Получает значение настройки по ключу.
|
||||
* @param key Ключ настройки.
|
||||
* @return Optional со значением, если найдено, иначе Optional.empty().
|
||||
*/
|
||||
Optional<String> getSettingValue(String key);
|
||||
|
||||
/**
|
||||
* Получает значение настройки по ключу или значение по умолчанию.
|
||||
* @param key Ключ настройки.
|
||||
* @param defaultValue Значение по умолчанию.
|
||||
* @return Значение настройки или defaultValue.
|
||||
*/
|
||||
String getSettingValueOrDefault(String key, String defaultValue);
|
||||
|
||||
/**
|
||||
* Устанавливает значение настройки. Требует прав администратора.
|
||||
* @param key Ключ настройки.
|
||||
* @param value Новое значение.
|
||||
* @param requestingUser Пользователь, выполняющий операцию.
|
||||
* @throws ServiceException если у пользователя нет прав администратора.
|
||||
*/
|
||||
void setSettingValue(String key, String value, BotUser requestingUser) throws ServiceException;
|
||||
|
||||
/**
|
||||
* Получает все настройки в виде Map. Требует прав администратора.
|
||||
* @param requestingUser Пользователь, выполняющий операцию.
|
||||
* @return Map, где ключ - settingKey, значение - settingValue.
|
||||
* @throws ServiceException если у пользователя нет прав администратора.
|
||||
*/
|
||||
Map<String, String> getAllSettings(BotUser requestingUser) throws ServiceException;
|
||||
|
||||
/**
|
||||
* Проверяет, является ли пользователь администратором.
|
||||
* @param user Пользователь для проверки.
|
||||
* @return true, если пользователь администратор, иначе false.
|
||||
*/
|
||||
boolean isAdmin(BotUser user);
|
||||
}
|
||||
@@ -7,5 +7,11 @@ public interface ProfileService {
|
||||
void enterProfileEditingMode(BotUser user);
|
||||
void handleProfileUpdate(BotUser user, String input) throws ServiceException;
|
||||
|
||||
void handleBooksGoalUpdate(BotUser user);
|
||||
// Метод для инициации запроса на изменение цели по книгам (переименован)
|
||||
void handleBooksGoalUpdateInitiation(BotUser user);
|
||||
|
||||
// Новый метод для инициации запроса на изменение модели ИИ
|
||||
void handleAiModelUpdateInitiation(BotUser user) throws ServiceException;
|
||||
|
||||
void handleBooksGoalShow(BotUser user);
|
||||
}
|
||||
|
||||
@@ -78,10 +78,11 @@ public class BookCreationServiceImpl implements BookCreationService {
|
||||
case ADDING_RATING -> {
|
||||
int rating = validateAndParseRating(text);
|
||||
cache.setRating(rating);
|
||||
cache.setCreationState(BookCreationCache.CreationState.ADDING_DATE);
|
||||
//cache.setCreationState(BookCreationCache.CreationState.ADDING_DATE);
|
||||
bookCacheService.updateCache(user.getId(), cache);
|
||||
telegramClientService.sendMessage(user, "📅 Введите дату прочтения (в формате ДД.ММ.ГГГГ): \n" +
|
||||
"✅ Для указания текущей даты введите букву \"т\"");
|
||||
createAndSaveBookFromCache(user, cache);
|
||||
//telegramClientService.sendMessage(user, "📅 Введите дату прочтения (в формате ДД.ММ.ГГГГ): \n" +
|
||||
// "✅ Для указания текущей даты введите букву \"т\"");
|
||||
}
|
||||
|
||||
case ADDING_DATE -> {
|
||||
@@ -128,7 +129,7 @@ public class BookCreationServiceImpl implements BookCreationService {
|
||||
}
|
||||
|
||||
private Book createBookFromCache(BookCreationCache cache, BotUser user) {
|
||||
return Book.builder().title(cache.getTitle()).author(cache.getAuthor()).rating(cache.getRating()).readDate(cache.getReadDate()).botUser(user).build();
|
||||
return Book.builder().title(cache.getTitle()).author(cache.getAuthor()).rating(cache.getRating()).readDate(Date.valueOf(LocalDate.now())).botUser(user).build();
|
||||
}
|
||||
|
||||
private void createAndSaveBookFromCache(BotUser user, BookCreationCache cache) {
|
||||
@@ -142,6 +143,9 @@ public class BookCreationServiceImpl implements BookCreationService {
|
||||
int weeksLeft = Math.toIntExact(ChronoUnit.WEEKS.between(today, endOfYear));
|
||||
|
||||
BotUser freshUser = userRepository.findById(user.getId()).orElseThrow();
|
||||
|
||||
bookRepository.save(book);
|
||||
|
||||
if (freshUser.getBooksToRead() != null) {
|
||||
int target = freshUser.getBooksToRead();
|
||||
int read = freshUser.getBooks() != null ? freshUser.getBooks().size() : 0;
|
||||
@@ -153,7 +157,7 @@ public class BookCreationServiceImpl implements BookCreationService {
|
||||
} else if (left == 0) {
|
||||
stats = "";
|
||||
try {
|
||||
String prompt = String.format("Придумай креативное поздравление с тем, что пользователь выполнил большое достижение по прочтению цель по проятению %d книг за год", freshUser.getBooksToRead());
|
||||
String prompt = String.format("Придумай креативное поздравление с тем, что пользователь выполнил большое достижение по прочтению цель по прочтению %d книг за год", freshUser.getBooksToRead());
|
||||
String congrat = openRouterService.getChatResponse(prompt, TextRole.DEFAULT);
|
||||
log.info("Congratulation target generated: {}", congrat);
|
||||
telegramClientService.sendMessageWithMarkdown(user, "🎉 *Поздравляю с прочтением\\!*\n\n" + escapeNonMarkdown(congrat));
|
||||
@@ -166,14 +170,13 @@ public class BookCreationServiceImpl implements BookCreationService {
|
||||
}
|
||||
telegramClientService.sendMessageWithMarkdown(user, message + stats);
|
||||
}
|
||||
bookRepository.save(book);
|
||||
|
||||
user.setLastReadDate(new Date(System.currentTimeMillis()));
|
||||
user.setLastNotificationDate(null);
|
||||
userRepository.save(user);
|
||||
|
||||
try {
|
||||
String prompt = String.format("Придумай креативное поздравление с прочтением книги \"%s\" от автора \"%s\". Учитывай что пользователь поставил оценку %d/10. ", cache.getTitle(), cache.getAuthor(), cache.getRating());
|
||||
String prompt = String.format("Придумай креативное поздравление с прочтением книги \"%s\" от автора \"%s\". ", cache.getTitle(), cache.getAuthor());
|
||||
String congrat = openRouterService.getChatResponse(prompt, TextRole.DEFAULT);
|
||||
log.info("Congratulation generated: {}", congrat);
|
||||
telegramClientService.sendMessageWithMarkdown(user, "🎉 *Поздравляю с прочтением\\!*\n\n" + escapeNonMarkdown(congrat));
|
||||
|
||||
@@ -134,13 +134,14 @@ public class BookManageServiceImpl implements BookManageService {
|
||||
|
||||
|
||||
private String getWelcomeMessage() {
|
||||
return "✨ *Добро пожаловать в Бок Книжник\\!* 📚\n" +
|
||||
return "✨ *Добро пожаловать в Бот Книжник\\!* 📚\n" +
|
||||
"_Ваш персональный помощник для управления библиотекой_\n\n" +
|
||||
"Чтобы начать, отправьте:\n" +
|
||||
"👉 /start — активировать бота и открыть меню\n\n" +
|
||||
"**С нами вы сможете:**\n" +
|
||||
"🔸 Добавлять книги с рейтингом\n" +
|
||||
"🔸 Просматривать свою коллекцию\n" +
|
||||
"🔸 Устанавливать цель на год\n" +
|
||||
"🔸 Редактировать записи\n" +
|
||||
"🔸 Сортировать по рейтингу\n\n" +
|
||||
"📌 *Не забывайте: всегда можно вернуться в главное меню командой* /start\n\n" +
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package ru.cathub.telegabot.service.impl;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.cathub.telegabot.exception.ServiceException;
|
||||
import ru.cathub.telegabot.model.BotSetting;
|
||||
import ru.cathub.telegabot.model.BotUser;
|
||||
import ru.cathub.telegabot.repository.BotSettingRepository;
|
||||
import ru.cathub.telegabot.service.BotSettingService;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class BotSettingServiceImpl implements BotSettingService {
|
||||
|
||||
private final BotSettingRepository settingRepository;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true) // Указываем, что метод только для чтения
|
||||
public Optional<String> getSettingValue(String key) {
|
||||
Optional<BotSetting> setting = settingRepository.findById(key);
|
||||
log.trace("Getting setting '{}'. Found: {}", key, setting.isPresent()); // Используем trace для менее важных логов
|
||||
return setting.map(BotSetting::getSettingValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true) // Указываем, что метод только для чтения
|
||||
public String getSettingValueOrDefault(String key, String defaultValue) {
|
||||
String value = getSettingValue(key).orElse(defaultValue);
|
||||
log.trace("Getting setting '{}' with default '{}'. Result: '{}'", key, defaultValue, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void setSettingValue(String key, String value, BotUser requestingUser) throws ServiceException {
|
||||
if (!isAdmin(requestingUser)) {
|
||||
log.warn("User {} (ID: {}) attempted to set setting '{}' without admin rights.",
|
||||
requestingUser.getChatId(), requestingUser.getId(), key); // Логируем попытку неавторизованного доступа
|
||||
throw new ServiceException("У вас нет прав для изменения настроек бота.");
|
||||
}
|
||||
|
||||
BotSetting setting = settingRepository.findById(key)
|
||||
.orElse(new BotSetting(key, null)); // Создаем новую, если не найдена
|
||||
|
||||
String oldValue = setting.getSettingValue();
|
||||
log.info("Admin User {} (ID: {}) is setting '{}' from '{}' to '{}'",
|
||||
requestingUser.getChatId(), requestingUser.getId(), key, oldValue, value);
|
||||
|
||||
setting.setSettingValue(value);
|
||||
settingRepository.save(setting);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true) // Указываем, что метод только для чтения
|
||||
public Map<String, String> getAllSettings(BotUser requestingUser) throws ServiceException {
|
||||
if (!isAdmin(requestingUser)) {
|
||||
log.warn("User {} (ID: {}) attempted to list all settings without admin rights.",
|
||||
requestingUser.getChatId(), requestingUser.getId());
|
||||
throw new ServiceException("У вас нет прав для просмотра настроек бота.");
|
||||
}
|
||||
log.debug("Admin User {} (ID: {}) requested all settings.", requestingUser.getChatId(), requestingUser.getId());
|
||||
return settingRepository.findAll().stream()
|
||||
.collect(Collectors.toMap(BotSetting::getSettingKey, setting -> Optional.ofNullable(setting.getSettingValue()).orElse(""))); // Собираем в Map, заменяя null на пустую строку
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isAdmin(BotUser user) {
|
||||
// Используем существующее поле UserType
|
||||
boolean isAdmin = user != null && user.getUserType() == BotUser.UserType.ADMIN;
|
||||
log.trace("Checking admin status for user ID {}: {}", user != null ? user.getId() : "null", isAdmin);
|
||||
return isAdmin;
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,19 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.*; // Импортируем MediaType и HttpStatus
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
// import org.springframework.http.client.SimpleClientHttpRequestFactory; // <-- Удалить, если RestTemplate инжектируется
|
||||
// import java.net.Proxy; // <-- Удалить, если RestTemplate инжектируется
|
||||
// import java.net.InetSocketAddress; // <-- Удалить, если RestTemplate инжектируется
|
||||
import ru.cathub.telegabot.configuration.OpenRouterConfig;
|
||||
import ru.cathub.telegabot.exception.OpenRouterException;
|
||||
import ru.cathub.telegabot.model.TextRole;
|
||||
import ru.cathub.telegabot.service.BotSettingService; // <-- Добавить импорт
|
||||
import ru.cathub.telegabot.service.OpenRouterService;
|
||||
import ru.cathub.telegabot.utils.Constants; // <-- Добавить импорт
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
@@ -23,75 +28,144 @@ import java.util.Map;
|
||||
@Slf4j
|
||||
public class OpenRouterServiceImpl implements OpenRouterService {
|
||||
|
||||
private final OpenRouterConfig config;
|
||||
private final RestTemplate restTemplate;
|
||||
private final OpenRouterConfig config; // Оставляем для URL и Key
|
||||
private final RestTemplate restTemplate; // <-- Инжектируем RestTemplate из бина
|
||||
private final BotSettingService botSettingService; // <-- Инжектируем сервис настроек
|
||||
|
||||
// private RestTemplate createProxyRestTemplate() { // <-- Удалить этот метод
|
||||
// SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
// factory.setProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("192.168.0.5", 1984)));
|
||||
// return new RestTemplate(factory);
|
||||
// }
|
||||
|
||||
@Override
|
||||
public String getChatResponse(String userMessage, TextRole role) throws OpenRouterException {
|
||||
List<Map<String, String>> messages = new ArrayList<>();
|
||||
messages.add(Map.of("role", "system", "content", role.getPrompt()));
|
||||
messages.add(Map.of("role", "user", "content", userMessage));
|
||||
return sendChatRequest(messages);
|
||||
// Передаем userMessage во второй аргумент для использования в sendChatRequest (например, для логгирования)
|
||||
return sendChatRequest(messages, userMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getChatResponseWithContext(String userMessage, List<Map<String, String>> context) throws OpenRouterException {
|
||||
context.add(Map.of("role", "user", "content", userMessage));
|
||||
return sendChatRequest(context);
|
||||
// Создаем копию контекста, чтобы не модифицировать исходный список
|
||||
List<Map<String, String>> messages = new ArrayList<>(context);
|
||||
// Добавляем текущее сообщение пользователя в конец контекста, если его там еще нет
|
||||
if (messages.isEmpty() || !messages.get(messages.size() - 1).get("role").equals("user")) {
|
||||
messages.add(Map.of("role", "user", "content", userMessage));
|
||||
} else {
|
||||
// Если последнее сообщение уже от user, возможно, это повторный вызов, просто используем userMessage для логгирования
|
||||
log.trace("Last message in context is already from user. Using provided userMessage for logging.");
|
||||
}
|
||||
return sendChatRequest(messages, userMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSimpleResponse(String message) throws OpenRouterException {
|
||||
// Передаем message во второй аргумент для использования в sendChatRequest
|
||||
return sendChatRequest(List.of(
|
||||
Map.of("role", "user", "content", message)
|
||||
));
|
||||
), message);
|
||||
}
|
||||
|
||||
private String sendChatRequest(List<Map<String, String>> messages) throws OpenRouterException {
|
||||
// Добавляем второй аргумент для информации, которая может быть полезна для логгирования
|
||||
private String sendChatRequest(List<Map<String, String>> messages, String originalUserMessage) throws OpenRouterException {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBearerAuth(config.getKey());
|
||||
headers.add("HTTP-Referer", "https://github.com/your-username/telegabot");
|
||||
headers.add("X-Title", "TelegaBot");
|
||||
headers.setBearerAuth(config.getKey()); // Используем ключ из конфига
|
||||
headers.setContentType(MediaType.APPLICATION_JSON); // Указываем тип контента
|
||||
// Можно оставить старые заголовки или использовать новые, если нужно
|
||||
headers.add("HTTP-Referer", "https://github.com/your-repo"); // Замените на ваш репозиторий
|
||||
headers.add("X-Title", "TelegaBot Book Assistant"); // Замените на имя вашего бота
|
||||
|
||||
// Получаем имя модели из настроек бота
|
||||
String currentModel = botSettingService.getSettingValueOrDefault(
|
||||
Constants.AI_MODEL_SETTING_KEY,
|
||||
Constants.DEFAULT_AI_MODEL // Значение по умолчанию
|
||||
);
|
||||
log.debug("Using AI model: {}", currentModel); // Логируем используемую модель
|
||||
|
||||
Map<String, Object> request = new HashMap<>();
|
||||
request.put("model", config.getModel());
|
||||
request.put("model", currentModel); // <-- Используем модель из настроек
|
||||
request.put("messages", messages);
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);
|
||||
|
||||
try {
|
||||
// Логгируем часть исходного сообщения пользователя
|
||||
String messageSnippet = originalUserMessage != null ? originalUserMessage.substring(0, Math.min(originalUserMessage.length(), 100)) : "[null]";
|
||||
log.info("Sending request to OpenRouter. Model: {}. Message snippet: '{}...'", currentModel, messageSnippet);
|
||||
|
||||
ResponseEntity<Map> response = restTemplate.postForEntity(
|
||||
config.getUrl() + "/chat/completions",
|
||||
new HttpEntity<>(request, headers),
|
||||
config.getUrl() + "/chat/completions", // Используем URL из конфига
|
||||
entity, // Используем созданный entity
|
||||
Map.class
|
||||
);
|
||||
|
||||
// Обработка ответа остается как была, но можно добавить больше логов
|
||||
log.debug("Received response from OpenRouter. Status: {}", response.getStatusCode());
|
||||
return extractResponse(response.getBody());
|
||||
|
||||
} catch (RestClientException e) {
|
||||
log.error("OpenRouter API error: {}", e.getMessage());
|
||||
throw new OpenRouterException("API request failed: " + e.getMessage());
|
||||
log.error("OpenRouter API RestClientException: {}. URL: {}", e.getMessage(), config.getUrl(), e); // Добавляем stack trace в лог
|
||||
// Можно добавить детали в сообщение об ошибке
|
||||
throw new OpenRouterException("Ошибка сети при обращении к API OpenRouter: " + e.getMessage(), e);
|
||||
} catch (Exception e) { // Ловим другие возможные ошибки
|
||||
log.error("Unexpected error during OpenRouter API call: {}", e.getMessage(), e);
|
||||
throw new OpenRouterException("Непредвиденная ошибка при вызове API OpenRouter.", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Метод extractResponse остается без изменений, но с улучшенным логгированием
|
||||
private String extractResponse(Map<String, Object> response) throws OpenRouterException {
|
||||
try {
|
||||
if (!response.containsKey("choices")) {
|
||||
throw new OpenRouterException("Invalid API response format");
|
||||
if (response == null) {
|
||||
log.error("OpenRouter API response body is null.");
|
||||
throw new OpenRouterException("API вернуло пустой ответ.");
|
||||
}
|
||||
|
||||
if (response.containsKey("error")) {
|
||||
Object errorObj = response.get("error");
|
||||
String errorMessage = errorObj instanceof Map ? ((Map<?, ?>) errorObj).get("message").toString() : String.valueOf(errorObj);
|
||||
log.error("OpenRouter API error in response: {}", errorMessage);
|
||||
throw new OpenRouterException("API OpenRouter вернуло ошибку: " + errorMessage);
|
||||
}
|
||||
if (!response.containsKey("choices")) {
|
||||
log.error("Invalid API response format: 'choices' key is missing. Response: {}", response);
|
||||
throw new OpenRouterException("Неверный формат ответа API: отсутствует поле 'choices'.");
|
||||
}
|
||||
|
||||
List<?> choices = (List<?>) response.get("choices");
|
||||
if (choices.isEmpty()) {
|
||||
throw new OpenRouterException("No choices in API response");
|
||||
log.warn("OpenRouter API response has empty 'choices' list.");
|
||||
throw new OpenRouterException("API не вернуло вариантов ответа ('choices' пуст).");
|
||||
}
|
||||
|
||||
|
||||
Map<?, ?> firstChoice = (Map<?, ?>) choices.get(0);
|
||||
Map<?, ?> message = (Map<?, ?>) firstChoice.get("message");
|
||||
|
||||
if (message == null || !message.containsKey("content")) {
|
||||
throw new OpenRouterException("Malformed message in API response");
|
||||
if (firstChoice == null) {
|
||||
log.error("First choice in API response is null. Choices: {}", choices);
|
||||
throw new OpenRouterException("Неверный формат ответа API: первый вариант ответа пуст.");
|
||||
}
|
||||
|
||||
return (String) message.get("content");
|
||||
Map<?, ?> message = (Map<?, ?>) firstChoice.get("message");
|
||||
|
||||
if (message == null || !message.containsKey("content")) {
|
||||
log.error("Malformed message in API response: 'message' or 'content' is missing. Choice: {}", firstChoice);
|
||||
throw new OpenRouterException("Неверный формат сообщения в ответе API.");
|
||||
}
|
||||
|
||||
Object contentObj = message.get("content");
|
||||
if (contentObj == null) {
|
||||
log.warn("Content in API response message is null. Message: {}", message);
|
||||
// Возвращаем пустую строку или выбрасываем исключение, в зависимости от ожидаемого поведения
|
||||
return "";
|
||||
// throw new OpenRouterException("Содержимое ответа API пусто (null).");
|
||||
}
|
||||
|
||||
String content = (String) contentObj;
|
||||
log.info("Successfully extracted content from OpenRouter response. Length: {}", content.length());
|
||||
return content;
|
||||
} catch (ClassCastException e) {
|
||||
throw new OpenRouterException("Type casting error in API response");
|
||||
log.error("Type casting error while parsing OpenRouter API response: {}", e.getMessage(), e);
|
||||
throw new OpenRouterException("Ошибка при обработке ответа API (неверные типы данных).", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,77 +2,192 @@ package ru.cathub.telegabot.service.impl;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import lombok.extern.slf4j.Slf4j; // Добавим логгер
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.cathub.telegabot.bot.KeyboardHelper;
|
||||
import ru.cathub.telegabot.exception.ServiceException;
|
||||
import ru.cathub.telegabot.model.BotUser;
|
||||
import ru.cathub.telegabot.repository.UserRepository;
|
||||
import ru.cathub.telegabot.service.BotSettingService; // <-- Добавить импорт
|
||||
import ru.cathub.telegabot.service.ProfileService;
|
||||
import ru.cathub.telegabot.service.TelegramClientService;
|
||||
import ru.cathub.telegabot.utils.Constants;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.time.LocalDate;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import static ru.cathub.telegabot.utils.Constants.SET_BOOKS_GOAL;
|
||||
import static ru.cathub.telegabot.bot.KeyboardHelper.getEditTargerBooksKeyboard;
|
||||
import static ru.cathub.telegabot.utils.BookFormatUtils.formatBookLeftStats;
|
||||
// Удаляем неиспользуемый импорт SET_BOOKS_GOAL
|
||||
// import static ru.cathub.telegabot.utils.Constants.SET_BOOKS_GOAL;
|
||||
import static ru.cathub.telegabot.utils.Constants.*; // Импортируем все константы
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j // Добавляем логгер
|
||||
public class ProfileServiceImpl implements ProfileService {
|
||||
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final TelegramClientService telegramClientService;
|
||||
private final BotSettingService botSettingService; // <-- Добавить зависимость
|
||||
|
||||
@Override
|
||||
public void enterProfileEditingMode(BotUser user) {
|
||||
user.setWorkingMode(BotUser.WorkingMode.EDIT_PROFILE);
|
||||
user.setWorkingMode(BotUser.WorkingMode.EDIT_PROFILE); // Режим выбора опции
|
||||
userRepository.save(user);
|
||||
boolean isAdmin = botSettingService.isAdmin(user); // <-- Проверить админа
|
||||
log.debug("Entering profile edit mode for user {}. Is admin: {}", user.getId(), isAdmin);
|
||||
telegramClientService.sendMessage(
|
||||
user,
|
||||
user,
|
||||
"⌨️ Выберите параметр для редактирования:",
|
||||
KeyboardHelper.getProfileMenuKeyboard()
|
||||
// Передать флаг isAdmin в хелпер клавиатуры
|
||||
KeyboardHelper.getProfileMenuKeyboard(isAdmin)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleProfileUpdate(BotUser user, String input) throws ServiceException {
|
||||
log.debug("Handling profile update for user {} in mode {}. Input: '{}'", user.getId(), user.getWorkingMode(), input);
|
||||
try {
|
||||
if (input.equals(SET_BOOKS_GOAL)) {
|
||||
handleBooksGoalUpdate(user);
|
||||
} else {
|
||||
processFieldUpdate(user, input);
|
||||
// Определяем, какое поле обновляем, по текущему режиму работы
|
||||
switch (user.getWorkingMode()) {
|
||||
case EDIT_PROFILE_BOOKS_GOAL:
|
||||
processBooksGoalUpdate(user, input); // <-- Вызвать переименованный метод
|
||||
break;
|
||||
case EDIT_PROFILE_AI_MODEL:
|
||||
processAiModelUpdate(user, input); // <-- Вызвать новый метод
|
||||
break;
|
||||
default:
|
||||
// Если мы здесь, значит пользователь ввел текст, когда не должен был (например, в режиме EDIT_PROFILE)
|
||||
log.warn("User {} provided unexpected input '{}' in mode {}", user.getId(), input, user.getWorkingMode());
|
||||
boolean isAdmin = botSettingService.isAdmin(user);
|
||||
telegramClientService.sendMessage(user, "Пожалуйста, выберите опцию из меню.", KeyboardHelper.getProfileMenuKeyboard(isAdmin));
|
||||
// Не сбрасываем режим, даем пользователю выбрать опцию
|
||||
return; // Выходим, чтобы не сбросить режим в finally
|
||||
}
|
||||
} finally {
|
||||
resetEditingState(user);
|
||||
// Сбрасываем режим только если обновление прошло успешно или была ошибка в обработке
|
||||
if (user.getWorkingMode() != BotUser.WorkingMode.EDIT_PROFILE) { // Не сбрасываем, если просто показали меню
|
||||
log.debug("Resetting editing state for user {}", user.getId());
|
||||
resetEditingState(user);
|
||||
} else {
|
||||
log.debug("Not resetting state for user {} as they are still in EDIT_PROFILE mode", user.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleBooksGoalUpdate(BotUser user) {
|
||||
user.setWorkingMode(BotUser.WorkingMode.EDIT_PROFILE);
|
||||
// Переименован из handleBooksGoalUpdate
|
||||
public void handleBooksGoalUpdateInitiation(BotUser user) {
|
||||
user.setWorkingMode(BotUser.WorkingMode.EDIT_PROFILE_BOOKS_GOAL); // <-- Установить конкретный режим
|
||||
userRepository.save(user);
|
||||
log.info("User {} initiated books goal update.", user.getId());
|
||||
telegramClientService.sendMessage(
|
||||
user,
|
||||
user,
|
||||
"Текущая цель: " + (user.getBooksToRead() != null ? String.valueOf(user.getBooksToRead()) : "Не задано") + "\nВведите новое количество книг:"
|
||||
);
|
||||
}
|
||||
|
||||
private void processFieldUpdate(BotUser user, String input) throws ServiceException {
|
||||
// <-- Новый метод -->
|
||||
@Override
|
||||
public void handleAiModelUpdateInitiation(BotUser user) throws ServiceException {
|
||||
if (!botSettingService.isAdmin(user)) {
|
||||
log.warn("Non-admin user {} attempted to initiate AI model update.", user.getId());
|
||||
throw new ServiceException("У вас нет прав для изменения этой настройки.");
|
||||
}
|
||||
user.setWorkingMode(BotUser.WorkingMode.EDIT_PROFILE_AI_MODEL); // <-- Установить конкретный режим
|
||||
userRepository.save(user);
|
||||
log.info("Admin user {} initiated AI model update.", user.getId());
|
||||
|
||||
String currentModel = botSettingService.getSettingValueOrDefault(
|
||||
Constants.AI_MODEL_SETTING_KEY,
|
||||
Constants.DEFAULT_AI_MODEL
|
||||
);
|
||||
|
||||
telegramClientService.sendMessageWithMarkdown( // Используем Markdown для форматирования `code`
|
||||
user,
|
||||
String.format(Constants.ENTER_NEW_AI_MODEL, currentModel)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true) // Добавим readOnly = true
|
||||
public void handleBooksGoalShow(BotUser user) {
|
||||
|
||||
// Используем user напрямую, если он актуален, или получаем свежую копию
|
||||
BotUser freshUser = userRepository.findById(user.getId()).orElseThrow(() -> new RuntimeException("User not found"));
|
||||
Integer targetOpt = freshUser.getBooksToRead(); // Цель может быть null
|
||||
int target = targetOpt != null ? targetOpt : 0; // Если null, считаем целью 0
|
||||
int read = freshUser.getBooks() != null ? freshUser.getBooks().size() : 0;
|
||||
int left = target - read;
|
||||
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDate endOfYear = LocalDate.of(today.getYear(), 12, 31);
|
||||
int weeksLeft = Math.toIntExact(ChronoUnit.WEEKS.between(today, endOfYear));
|
||||
|
||||
var stats = formatBookLeftStats(weeksLeft, target, read, left);
|
||||
|
||||
telegramClientService.sendMessageWithMarkdown(user, stats, getEditTargerBooksKeyboard());
|
||||
}
|
||||
|
||||
// Переименован из processFieldUpdate
|
||||
private void processBooksGoalUpdate(BotUser user, String input) throws ServiceException {
|
||||
try {
|
||||
int goal = Integer.parseInt(input);
|
||||
if (goal < 0) throw new NumberFormatException();
|
||||
|
||||
int goal = Integer.parseInt(input.trim()); // Обрезаем пробелы
|
||||
if (goal < 0) {
|
||||
log.warn("User {} entered invalid negative goal: {}", user.getId(), goal);
|
||||
throw new NumberFormatException("Goal cannot be negative");
|
||||
}
|
||||
|
||||
user.setBooksToRead(goal);
|
||||
// userRepository.save(user); // Сохранение будет в resetEditingState
|
||||
log.info("User {} updated books goal to {}", user.getId(), goal);
|
||||
telegramClientService.sendMessage(
|
||||
user,
|
||||
Constants.PROFILE_UPDATED,
|
||||
KeyboardHelper.getMainMenuKeyboard()
|
||||
user,
|
||||
Constants.PROFILE_UPDATED,
|
||||
KeyboardHelper.getMainMenuKeyboard() // Возвращаем в главное меню после успешного обновления
|
||||
);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ServiceException("❌ Ошибка! Введите целое положительное число");
|
||||
log.warn("User {} entered invalid number format for goal: {}", user.getId(), input);
|
||||
throw new ServiceException("❌ Ошибка! Введите целое положительное число.");
|
||||
}
|
||||
}
|
||||
|
||||
// <-- Новый метод -->
|
||||
private void processAiModelUpdate(BotUser user, String input) throws ServiceException {
|
||||
// Дополнительная проверка прав не нужна, т.к. она есть в handleAiModelUpdateInitiation
|
||||
// и setSettingValue внутри себя тоже проверяет права
|
||||
try {
|
||||
String newModel = input.trim();
|
||||
if (newModel.isEmpty()) {
|
||||
log.warn("Admin user {} entered empty AI model name.", user.getId());
|
||||
throw new ServiceException("❌ Имя модели не может быть пустым.");
|
||||
}
|
||||
botSettingService.setSettingValue(Constants.AI_MODEL_SETTING_KEY, newModel, user);
|
||||
log.info("Admin user {} updated AI model to '{}'", user.getId(), newModel);
|
||||
telegramClientService.sendMessage(
|
||||
user,
|
||||
Constants.AI_MODEL_UPDATED,
|
||||
KeyboardHelper.getMainMenuKeyboard() // Возвращаем в главное меню
|
||||
);
|
||||
} catch (ServiceException e) {
|
||||
// Ловим ошибку, если вдруг setSettingValue ее выбросит (например, проблемы с БД или правами)
|
||||
log.error("ServiceException while admin {} updating AI model: {}", user.getId(), e.getMessage(), e);
|
||||
throw new ServiceException("❌ Не удалось обновить модель ИИ: " + e.getMessage(), e);
|
||||
} catch (Exception e) {
|
||||
// Ловим другие возможные ошибки
|
||||
log.error("Unexpected error while admin {} updating AI model: {}", user.getId(), e.getMessage(), e);
|
||||
throw new ServiceException("❌ Произошла непредвиденная ошибка при обновлении модели ИИ.", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void resetEditingState(BotUser user) {
|
||||
user.setWorkingMode(BotUser.WorkingMode.NO_MODE);
|
||||
user.setWorkingMode(BotUser.WorkingMode.NO_MODE); // <-- Установить NO_MODE
|
||||
userRepository.save(user);
|
||||
log.debug("User {} state reset to NO_MODE.", user.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,53 @@
|
||||
package ru.cathub.telegabot.service.impl;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j; // <-- Добавить логгер
|
||||
import org.springframework.stereotype.Service;
|
||||
import ru.cathub.telegabot.bot.KeyboardHelper;
|
||||
import ru.cathub.telegabot.exception.ServiceException;
|
||||
import ru.cathub.telegabot.model.BotUser;
|
||||
import ru.cathub.telegabot.repository.UserRepository;
|
||||
import ru.cathub.telegabot.service.BookManageService;
|
||||
import ru.cathub.telegabot.service.ProfileService;
|
||||
import ru.cathub.telegabot.service.TelegramBotService;
|
||||
import ru.cathub.telegabot.service.TelegramClientService;
|
||||
import ru.cathub.telegabot.service.*; // <-- Импортировать все сервисы
|
||||
|
||||
import static ru.cathub.telegabot.utils.Constants.*;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j // <-- Добавить аннотацию логгера
|
||||
public class TelegramBotServiceImpl implements TelegramBotService {
|
||||
|
||||
private final TelegramClientService telegramClientService;
|
||||
private final BookManageService bookManageService;
|
||||
private final ProfileService profileService;
|
||||
private final UserRepository userRepository;
|
||||
private final BookAiServiceImpl bookAiServiceImpl;
|
||||
private final BookAiService bookAiService; // <-- Используем интерфейс
|
||||
private final BotSettingService botSettingService; // <-- Добавить зависимость
|
||||
|
||||
@Override
|
||||
public void processInput(BotUser botUser, String text) throws ServiceException {
|
||||
switch (text) {
|
||||
case EDIT_PROFILE:
|
||||
profileService.enterProfileEditingMode(botUser);
|
||||
break;
|
||||
// case "Отправить текст ИИ":
|
||||
log.debug("Processing input '{}' for user {} in mode {}", text, botUser.getId(), botUser.getWorkingMode());
|
||||
try { // Обернем весь метод в try-catch для ServiceException
|
||||
switch (text) {
|
||||
case EDIT_PROFILE:
|
||||
profileService.enterProfileEditingMode(botUser);
|
||||
break;
|
||||
// Обработка нажатия кнопок из меню профиля
|
||||
case SET_BOOKS_GOAL:
|
||||
profileService.handleBooksGoalUpdateInitiation(botUser);
|
||||
break;
|
||||
case EDIT_AI_MODEL: // <-- Добавить обработку новой кнопки
|
||||
try {
|
||||
profileService.handleAiModelUpdateInitiation(botUser);
|
||||
} catch (ServiceException e) {
|
||||
// Отправляем сообщение об ошибке прав доступа пользователю
|
||||
log.warn("User {} failed to initiate AI model update: {}", botUser.getId(), e.getMessage());
|
||||
telegramClientService.sendMessage(botUser, e.getMessage());
|
||||
// Возвращаем пользователя в главное меню, так как он не может редактировать
|
||||
processStart(botUser);
|
||||
}
|
||||
break;
|
||||
|
||||
// case "Отправить текст ИИ":
|
||||
// sendMessage(chatId, "Введите ваш текст:");
|
||||
// break;
|
||||
//
|
||||
@@ -43,52 +61,128 @@ public class TelegramBotServiceImpl implements TelegramBotService {
|
||||
|
||||
case "Помощь":
|
||||
telegramClientService.sendMessage(botUser, "Выберите действие из меню.");
|
||||
break;
|
||||
bookManageService.handleStartMessage(botUser);
|
||||
break;
|
||||
|
||||
case BACK:
|
||||
processStart(botUser);
|
||||
break;
|
||||
case EDIT_BOOKS:
|
||||
bookManageService.processBookInput(botUser,text);
|
||||
break;
|
||||
case SET_BOOKS_GOAL_FROM_BOOKS:
|
||||
profileService.handleBooksGoalUpdate(botUser);
|
||||
break;
|
||||
default:
|
||||
switch (botUser.getWorkingMode()) {
|
||||
case BOOK:
|
||||
bookManageService.processBookInput(botUser,text);
|
||||
break;
|
||||
case EDIT_PROFILE:
|
||||
profileService.handleProfileUpdate(botUser, text);
|
||||
break;
|
||||
case AI_FILTER,AI_SUGGEST:
|
||||
bookAiServiceImpl.processAiRequest(botUser,text,botUser.getWorkingMode());
|
||||
break;
|
||||
}
|
||||
|
||||
case BACK:
|
||||
log.debug("Processing BACK button for user {} in mode {}", botUser.getId(), botUser.getWorkingMode());
|
||||
// Определяем, откуда был нажат BACK
|
||||
if (botUser.getWorkingMode() == BotUser.WorkingMode.EDIT_PROFILE ||
|
||||
botUser.getWorkingMode() == BotUser.WorkingMode.EDIT_PROFILE_AI_MODEL ||
|
||||
botUser.getWorkingMode() == BotUser.WorkingMode.EDIT_PROFILE_BOOKS_GOAL) {
|
||||
// Если были в редактировании профиля, возвращаемся в главное меню
|
||||
log.debug("Back from profile edit, returning to main menu for user {}", botUser.getId());
|
||||
processStart(botUser);
|
||||
} else if (botUser.getWorkingMode() == BotUser.WorkingMode.BOOK ||
|
||||
botUser.getWorkingMode() == BotUser.WorkingMode.AI_FILTER ||
|
||||
botUser.getWorkingMode() == BotUser.WorkingMode.AI_SUGGEST) {
|
||||
// Если были в режиме Книжника или AI, возвращаемся в меню Книжника
|
||||
log.debug("Back from book/AI mode, returning to book menu for user {}", botUser.getId());
|
||||
bookManageService.handleStartMessage(botUser);
|
||||
}
|
||||
else {
|
||||
// По умолчанию возвращаемся в главное меню
|
||||
log.debug("Back from unknown/NO_MODE, returning to main menu for user {}", botUser.getId());
|
||||
processStart(botUser);
|
||||
}
|
||||
break;
|
||||
case EDIT_BOOKS:
|
||||
case ADD_BOOK: // Добавим обработку этих кнопок здесь, если они ведут в режим BOOK
|
||||
case LIST_BOOKS:
|
||||
case FILTER_BOOKS:
|
||||
case RECOMMEND_BOOKS:
|
||||
bookManageService.processBookInput(botUser, text);
|
||||
break;
|
||||
case SET_BOOKS_GOAL_FROM_BOOKS:
|
||||
profileService.handleBooksGoalShow(botUser);
|
||||
break;
|
||||
default:
|
||||
log.debug("Processing default text input for user {} in mode {}", botUser.getId(), botUser.getWorkingMode());
|
||||
// Обработка ввода текста в зависимости от режима
|
||||
switch (botUser.getWorkingMode()) {
|
||||
case BOOK:
|
||||
bookManageService.processBookInput(botUser, text);
|
||||
break;
|
||||
// Обработка ввода для конкретных полей профиля
|
||||
case EDIT_PROFILE_BOOKS_GOAL:
|
||||
case EDIT_PROFILE_AI_MODEL: // <-- Добавить обработку ввода для AI модели
|
||||
profileService.handleProfileUpdate(botUser, text);
|
||||
break;
|
||||
case AI_FILTER:
|
||||
case AI_SUGGEST:
|
||||
bookAiService.processAiRequest(botUser, text, botUser.getWorkingMode());
|
||||
break;
|
||||
case EDIT_PROFILE:
|
||||
// Пользователь ввел текст вместо нажатия кнопки в меню профиля
|
||||
log.warn("User {} entered text '{}' while in EDIT_PROFILE mode", botUser.getId(), text);
|
||||
boolean isAdmin = botSettingService.isAdmin(botUser);
|
||||
telegramClientService.sendMessage(botUser, "Пожалуйста, выберите опцию из меню.", KeyboardHelper.getProfileMenuKeyboard(isAdmin));
|
||||
break;
|
||||
case NO_MODE:
|
||||
default:
|
||||
// Неожиданный ввод текста, когда не ожидается
|
||||
log.warn("User {} entered unexpected text '{}' in mode {}", botUser.getId(), text, botUser.getWorkingMode());
|
||||
telegramClientService.sendMessage(botUser, "Неизвестная команда или неверный режим. Используйте /start для возврата в главное меню.");
|
||||
processStart(botUser); // Возвращаем в начало
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (ServiceException e) {
|
||||
log.error("ServiceException during processing input '{}' for user {}: {}", text, botUser.getId(), e.getMessage(), e);
|
||||
telegramClientService.sendMessage(botUser, "Произошла ошибка: " + e.getMessage());
|
||||
// Можно вернуть пользователя в главное меню при ошибке
|
||||
processStart(botUser);
|
||||
} catch (Exception e) { // Ловим остальные неожиданные ошибки
|
||||
log.error("Unexpected exception during processing input '{}' for user {}: {}", text, botUser.getId(), e.getMessage(), e);
|
||||
telegramClientService.sendMessage(botUser, "Произошла непредвиденная ошибка. Попробуйте позже.");
|
||||
processStart(botUser);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void processCommand(BotUser botUser, String text) {
|
||||
switch (text) {
|
||||
case START:
|
||||
processStart(botUser);
|
||||
break;
|
||||
// Можно добавить админские команды здесь, если нужно
|
||||
}
|
||||
}
|
||||
|
||||
public void processStart(BotUser botUser){
|
||||
telegramClientService.sendMessage(botUser, "Добро пожаловать! Выберите действие:", KeyboardHelper.getMainMenuKeyboard());
|
||||
log.info("Processing /start for user {}", botUser.getId());
|
||||
// Сбрасываем режим при старте
|
||||
botUser.setWorkingMode(BotUser.WorkingMode.NO_MODE);
|
||||
userRepository.save(botUser);
|
||||
telegramClientService.sendMessage(botUser, "Добро пожаловать! Выберите действие:", KeyboardHelper.getMainMenuKeyboard());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processQuery(BotUser botUser, String callbackData)
|
||||
{
|
||||
if (callbackData.startsWith("BOOK_")) {
|
||||
bookManageService.processBookQuery(botUser,callbackData);
|
||||
log.debug("Processing query '{}' for user {}", callbackData, botUser.getId());
|
||||
try { // Обернем в try-catch
|
||||
if (callbackData.startsWith("BOOK_")) {
|
||||
bookManageService.processBookQuery(botUser, callbackData);
|
||||
} else if (callbackData.equals("EDIT_TARGET")) {
|
||||
// Эта кнопка теперь инициирует редактирование цели
|
||||
profileService.handleBooksGoalUpdateInitiation(botUser);
|
||||
} else {
|
||||
log.warn("Unknown callback query received: {} for user {}", callbackData, botUser.getId());
|
||||
// Можно отправить сообщение пользователю или просто проигнорировать
|
||||
telegramClientService.sendMessage(botUser, "Неизвестный запрос.");
|
||||
}
|
||||
// } catch (ServiceException e) {
|
||||
// log.error("ServiceException during processing query '{}' for user {}: {}", callbackData, botUser.getId(), e.getMessage(), e);
|
||||
// telegramClientService.sendMessage(botUser, "Ошибка обработки запроса: " + e.getMessage());
|
||||
// processStart(botUser); // Возвращаем в главное меню при ошибке
|
||||
} catch (Exception e) {
|
||||
log.error("Unexpected exception during processing query '{}' for user {}: {}", callbackData, botUser.getId(), e.getMessage(), e);
|
||||
telegramClientService.sendMessage(botUser, "Произошла непредвиденная ошибка при обработке запроса.");
|
||||
processStart(botUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,5 +46,12 @@ public class Constants {
|
||||
public static final String AI_FILTER_RATING = "Отфильтруй книги по по рейтингу больше 8";
|
||||
|
||||
public static final String AI_SUGGEST_SIMILAR = "Предложи похожие книги";
|
||||
public static final String AI_SUGGEST_GENRE = "Предложи еше книги в жанре фентези";
|
||||
public static final String AI_SUGGEST_GENRE = "Предложи еше книги в жанре фэнтези";
|
||||
|
||||
// AI Settings (Admin)
|
||||
public static final String EDIT_AI_MODEL = "Модель ИИ";
|
||||
public static final String AI_MODEL_SETTING_KEY = "OPENROUTER_API_MODEL";
|
||||
public static final String DEFAULT_AI_MODEL = "google/gemini-2.5-pro-exp-03-25:free"; // Значение по умолчанию
|
||||
public static final String AI_MODEL_UPDATED = "Модель ИИ обновлена! \uD83E\uDD16";
|
||||
public static final String ENTER_NEW_AI_MODEL = "Текущая модель: `%s`\nВведите новое имя модели OpenRouter:"; // %s будет заменено на текущую модель
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
|
||||
spring.datasource.driver-class-name=org.postgresql.Driver
|
||||
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
|
||||
spring.jpa.properties.hibernate.default_schema=telegabot
|
||||
spring.datasource.hikari.keepalive-time=300000
|
||||
|
||||
# Flyway Configuration
|
||||
spring.flyway.enabled=true
|
||||
@@ -29,7 +30,7 @@ logging.level.org.springframework.cache=INFO
|
||||
|
||||
# OpenRouter AI
|
||||
openrouter.api.url=https://openrouter.ai/api/v1
|
||||
openrouter.api.model=qwen/qwen-max
|
||||
# openrouter.api.model=google/gemini-2.5-pro-exp-03-25:free # <-- Закомментировано или удалено
|
||||
# Для безопасности лучше установить через переменную окружения:
|
||||
|
||||
|
||||
@@ -42,13 +43,21 @@ spring.datasource.url=${DB_URL}
|
||||
spring.datasource.username=${DB_USER}
|
||||
spring.datasource.password=${DB_PASSWORD}
|
||||
|
||||
|
||||
# OpenRouter AI
|
||||
openrouter.api.key=${OPENROUTER_API_KEY}
|
||||
|
||||
# Proxy Configuration (optional)
|
||||
proxy.host=${PROXY_HOST:}
|
||||
proxy.port=${PROXY_PORT:0}
|
||||
|
||||
# JobRunr Configuration
|
||||
|
||||
org.jobrunr.job-scheduler.enabled=true
|
||||
org.jobrunr.background-job-server.enabled=true
|
||||
org.jobrunr.database.type=sql
|
||||
org.jobrunr.dashboard.enabled=true
|
||||
org.jobrunr.database.tablePrefix=telegabot.
|
||||
org.jobrunr.database.tablePrefix=telegabot.
|
||||
|
||||
|
||||
#logging.level.ru.cathub.telegabot=TRACE
|
||||
Reference in New Issue
Block a user