package de.ph87.kleinanzeigen.telegram; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import de.ph87.kleinanzeigen.kleinanzeigen.offer.OfferDto; import de.ph87.kleinanzeigen.telegram.chat.ChatDto; import de.ph87.kleinanzeigen.telegram.chat.ChatService; import de.ph87.kleinanzeigen.telegram.chat.message.MessageDeleted; import de.ph87.kleinanzeigen.telegram.chat.message.MessageDto; import de.ph87.kleinanzeigen.telegram.chat.message.MessageService; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.event.TransactionalEventListener; import org.telegram.telegrambots.meta.api.methods.send.SendPhoto; import org.telegram.telegrambots.meta.api.methods.updatingmessages.DeleteMessage; import org.telegram.telegrambots.meta.api.methods.updatingmessages.EditMessageCaption; import org.telegram.telegrambots.meta.api.objects.*; 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 javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; @Slf4j @Service @RequiredArgsConstructor public class TelegramService { private static final String ICON_CHECK = "✅"; private static final String ICON_REMOVE = "❌"; private final TelegramConfig config; private final ChatService chatService; private final MessageService messageService; private final ObjectMapper objectMapper; private byte[] NO_IMAGE = null; private TelegramBot bot = null; @PostConstruct public void postConstruct() throws IOException { final BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); final ByteArrayOutputStream stream = new ByteArrayOutputStream(); ImageIO.write(img, "PNG", stream); NO_IMAGE = stream.toByteArray(); try { bot = new TelegramBot(config.getToken(), config.getUsername(), this::onUpdateReceived); } catch (TelegramApiException | IOException e) { log.error("Failed to start TelegramBot: {}", e.toString()); } } @TransactionalEventListener(OfferDto.class) public void onOffer(final OfferDto offer) { final List existingMessages = messageService.findAllDtoByOfferDto(offer); final List chats = chatService.findAllEnabled(); for (final ChatDto chat : chats) { existingMessages .stream() .filter(m -> m.getChat().getId() == chat.getId()) .findFirst() .ifPresentOrElse(this::update, () -> send(offer, chat)); } } @TransactionalEventListener(MessageDeleted.class) public void onMessageDeleted(final MessageDeleted messageDeleted) { remove(messageDeleted.getChatId(), messageDeleted.getMessageId()); } @PreDestroy public void stop() { log.info("Stopping Telegram bot..."); bot.stop(); log.info("Telegram bot stopped"); } private void onUpdateReceived(final Update update) { try { if (update.hasMessage() && update.getMessage().hasText()) { handleMessage(update.getMessage()); } else if (update.hasCallbackQuery()) { handleCallback(update.getCallbackQuery()); } } catch (AccessDenied e) { log.warn("Access denied: {}", update); } } private void handleMessage(final Message tlgMessage) throws AccessDenied { chatService.setEnabled(tlgMessage.getChatId(), !tlgMessage.getText().equals("/stop"), tlgMessage.getFrom().getUserName()); } private void handleCallback(final CallbackQuery callback) throws AccessDenied { final MaybeInaccessibleMessage tlgMessage = callback.getMessage(); chatService.setEnabled(tlgMessage.getChatId(), true, callback.getFrom().getUserName()); try { final InlineDto dto = objectMapper.readValue(callback.getData(), InlineDto.class); switch (dto.getCommand()) { case HIDE -> hide(tlgMessage); case REMEMBER -> remember(tlgMessage, true); case UNREMEMBER -> remember(tlgMessage, false); default -> update(tlgMessage); } } catch (JsonProcessingException e) { log.error("Failed to read InlineDto.", e); } } private void hide(final MaybeInaccessibleMessage tlgMessage) { messageService.setHide(tlgMessage, true).ifPresentOrElse(this::update, () -> remove(tlgMessage)); } private void remember(final MaybeInaccessibleMessage tlgMessage, final boolean remember) { messageService.setRemember(tlgMessage, remember).ifPresentOrElse(this::update, () -> remove(tlgMessage)); } private void update(final MaybeInaccessibleMessage tlgMessage) { messageService.findDtoByTelegramMessage(tlgMessage).ifPresentOrElse(this::update, () -> remove(tlgMessage)); } private void send(final OfferDto offerDto, final ChatDto chatDto) { try { final InputFile inputFile = offerDto.getImageURL() == null ? new InputFile(new ByteArrayInputStream(NO_IMAGE), "[Kein Bild]") : new InputFile(offerDto.getImageURL()); final SendPhoto send = new SendPhoto(chatDto.getId() + "", inputFile); send.setCaption(createText(offerDto)); send.setReplyMarkup(createKeyboard(false)); log.info("Sending: chat={}, offer={}", chatDto, offerDto); final Message tlgMessage = bot.execute(send); messageService.create(offerDto, chatDto, tlgMessage); } catch (TelegramApiException | JsonProcessingException e) { log.error("Failed to send: chat={}, offer={}: {}", chatDto, offerDto, e.toString()); } } private void update(final MessageDto messageDto) { if (messageDto.isHide() && messageDto.getTelegramMessageId() != null) { remove(messageDto.getChat().getId(), messageDto.getTelegramMessageId()); messageService.clearTelegramMessageId(messageDto); return; } if (messageDto.getTelegramMessageId() == null) { return; } try { final EditMessageCaption edit = new EditMessageCaption( messageDto.getChat().getId() + "", messageDto.getTelegramMessageId(), null, createText(messageDto.getOffer()), createKeyboard(messageDto.isRemember()), null, null ); edit.setParseMode("Markdown"); log.info("Editing Message: {}", messageDto); bot.execute(edit); } catch (TelegramApiException | JsonProcessingException e) { if (e.toString().endsWith("Bad Request: message to edit not found")) { log.info("Message has been deleted by User. Marking has hidden: {}", messageDto); messageService.setHide(messageDto, true); } else if (e.toString().endsWith("Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message")) { log.debug("Ignoring complaint from telegram-bot-api about unmodified message: {}", messageDto); } else { log.error("Failed to edit Message chat={} message={}", messageDto.getChat().getId(), messageDto.getTelegramMessageId(), e); } } } private String createText(final OfferDto offer) { return "%s\n%s\n%s %s (%d km)\n%s\n%s\n".formatted( offer.getTitle().replaceAll("\\[", "(").replaceAll("]", ")"), offer.combinePrice(), offer.getZipcode(), offer.getLocation(), offer.getDistance(), offer.getDescription(), offer.getArticleURL() ); } private InlineKeyboardMarkup createKeyboard(final boolean remember) throws JsonProcessingException { final InlineKeyboardMarkup markup = new InlineKeyboardMarkup(); final ArrayList> keyboard = new ArrayList<>(); final ArrayList row = new ArrayList<>(); if (remember) { addButton(row, ICON_CHECK + ICON_CHECK + ICON_CHECK + " Gemerkt " + ICON_CHECK + ICON_CHECK + ICON_CHECK, InlineCommand.UNREMEMBER); } else { addButton(row, ICON_REMOVE + " Entfernen", InlineCommand.HIDE); addButton(row, ICON_CHECK + " Merken", InlineCommand.REMEMBER); } keyboard.add(row); markup.setKeyboard(keyboard); return markup; } private void addButton(final ArrayList row, final String caption, final InlineCommand command) throws JsonProcessingException { final String data = objectMapper.writeValueAsString(new InlineDto(command)); row.add(new InlineKeyboardButton(caption, null, data, null, null, null, null, null, null)); } private void remove(final MaybeInaccessibleMessage tlgMessage) { remove(tlgMessage.getChatId(), tlgMessage.getMessageId()); } private void remove(final long chatId, final int messageId) { try { log.error("Removing Message chat={} message={}", chatId, messageId); bot.execute(new DeleteMessage(chatId + "", messageId)); } catch (TelegramApiException e) { log.error("Failed to remove Message chat={} message={}", chatId, messageId); } } }