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.MessageDto; import de.ph87.kleinanzeigen.telegram.chat.message.MessageService; import de.ph87.kleinanzeigen.telegram.request.ChatRequestEnable; import de.ph87.kleinanzeigen.telegram.request.ChatRequestHelp; import de.ph87.kleinanzeigen.telegram.request.ChatRequestUndo; import de.ph87.kleinanzeigen.telegram.request.MessageRequestHide; import de.ph87.kleinanzeigen.telegram.request.MessageRequestRemember; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.EnableAsync; 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.EditMessageCaption; import org.telegram.telegrambots.meta.api.objects.InputFile; import org.telegram.telegrambots.meta.api.objects.Message; 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 @EnableAsync @RequiredArgsConstructor public class TelegramAdapter { private static final String ICON_UNDO = "⎌"; private static final String ICON_CHECK = "✅"; private static final String ICON_REMOVE = "❌"; private final ChatService chatService; private final MessageService messageService; private final TelegramBot bot; private final ObjectMapper objectMapper; private byte[] NO_IMAGE = 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(); } @PreDestroy public void stop() { log.info("Stopping Telegram bot..."); bot.stop(); log.info("Telegram bot stopped"); } @TransactionalEventListener(OfferDto.class) public void onOffer(@NonNull 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)); } } @Async @EventListener(ChatRequestEnable.class) public void onChatEnable(@NonNull final ChatRequestEnable request) { chatService.setEnabled(request, request.isEnable(), request.getUsername()); } @Async @EventListener(ChatRequestUndo.class) public void onChatUndo(@NonNull final ChatRequestUndo request) { messageService.undo(request); } @Async @EventListener(ChatRequestHelp.class) public void onChatHelp(@NonNull final ChatRequestHelp request) { chatService.help(request); } @Async @EventListener(MessageRequestHide.class) public void onMessageHide(@NonNull final MessageRequestHide request) { messageService.setHide(request); } @Async @EventListener(MessageRequestRemember.class) public void onMessageRemember(@NonNull final MessageRequestRemember request) { messageService.setRemember(request, request.isRemember()); } @TransactionalEventListener(MessageDto.class) public void update(@NonNull final MessageDto message) { if (maySend(message)) { return; } if (mayDelete(message)) { return; } if (message.getTelegramMessageId() == null) { return; } edit(message); } private boolean maySend(final @NonNull MessageDto message) { if (message.needsToBeSent()) { send(message.getOffer(), message.getChat()); return true; } return false; } private boolean mayDelete(final @NonNull MessageDto message) { if (message.needsToBeDeleted()) { TlgMessage.of(bot, message).ifPresent(tlgMessage -> { bot.delete(tlgMessage); messageService.markDeleted(tlgMessage); }); return true; } return false; } private void send(@NonNull final OfferDto offerDto, @NonNull 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: offer={} to chat={}", offerDto, chatDto); final Message tlgMessage = bot.execute(send); messageService.updateOrCreate(offerDto, chatDto, tlgMessage); } catch (TelegramApiException | JsonProcessingException e) { log.error("Failed to send: chat={}, offer={}: {}", chatDto, offerDto, e.toString()); } } private void edit(@NonNull final MessageDto message) { try { final EditMessageCaption edit = new EditMessageCaption( message.getChat().getId() + "", message.getTelegramMessageId(), null, createText(message.getOffer()), createKeyboard(message.isRemember()), null, null ); edit.setParseMode("Markdown"); log.info("Editing Message: {}", message); 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: {}", message); TlgMessage.of(bot, message).ifPresent(messageService::markDeleted); } 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: {}", message); } else { log.error("Failed to edit Message chat={} message={}", message.getChat().getId(), message.getTelegramMessageId(), e); } } } @NonNull private String createText(@NonNull 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() ); } @NonNull 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); addButton(row, ICON_UNDO + " Rückgängig", InlineCommand.UNDO); } keyboard.add(row); markup.setKeyboard(keyboard); return markup; } private void addButton(@NonNull final ArrayList row, @NonNull final String caption, @NonNull 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)); } }