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.kleinanzeigen.offer.OfferService; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.telegram.telegrambots.meta.api.methods.send.SendPhoto; import org.telegram.telegrambots.meta.api.methods.updatingmessages.DeleteMessages; 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.FileInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; @Slf4j @Service @RequiredArgsConstructor public class TelegramService { private static final long CHAT_ID = 101138682L; private static final String ICON_CHECK = "✅"; private static final String ICON_REMOVE = "❌"; private byte[] NO_IMAGE = null; private final ObjectMapper objectMapper; private final OfferService offerService; 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(readToken(), this::onUpdateReceived); } catch (TelegramApiException | IOException e) { log.error("Failed to start TelegramBot: {}", e.toString()); } } @EventListener(OfferDto.class) public void onOfferChanged(final OfferDto offer) { if (offer.is_existing_()) { if (offer.getTelegramMessageId() == null) { send(offer); }else{ updateMessage(offer.getTelegramMessageId(), offer.getTelegramMessageId().getChatId()); } } else { remove(List.of(offer)); } } @PreDestroy public void stop() { log.info("Stopping Telegram bot..."); bot.stop(); log.info("Telegram bot stopped"); } private static String readToken() throws IOException { try (final FileInputStream stream = new FileInputStream("./telegram.token")) { return new String(stream.readAllBytes(), StandardCharsets.UTF_8).trim(); } } private void onUpdateReceived(final Update update) { if (update.hasMessage() && update.getMessage().hasText()) { log.info("#{} \"{}\": {}", update.getMessage().getChat().getId(), update.getMessage().getChat().getUserName(), update.getMessage().getText()); } else if (update.hasCallbackQuery()) { handleCallbackQuery(update.getCallbackQuery()); } } private void handleCallbackQuery(final CallbackQuery query) { final MaybeInaccessibleMessage message = query.getMessage(); if (message.getChatId() != CHAT_ID) { return; } try { final InlineDto dto = objectMapper.readValue(query.getData(), InlineDto.class); switch (dto.getCommand()) { case HIDE -> hide(message); case REMEMBER -> remember(message, true); case UNREMEMBER -> remember(message, false); default -> updateMessage(message, message.getChatId()); } } catch (JsonProcessingException e) { log.error("Failed to read InlineDto.", e); } } private void hide(final MaybeInaccessibleMessage message) { offerService.hideByTelegramMessageId(message.getMessageId(), true); remove(message); } private void remember(final MaybeInaccessibleMessage message, final boolean remember) { offerService.rememberByTelegramMessageId(message.getMessageId(), remember).ifPresentOrElse( offer -> updateMessage(offer, message.getChatId(), message.getMessageId()), () -> remove(message) ); } private void updateMessage(final MaybeInaccessibleMessage message, final Long chatId) { offerService.findByTelegramMessageId(message.getMessageId()).ifPresentOrElse( offer -> updateMessage(offer, chatId, message.getMessageId()), () -> remove(message) ); } private void updateMessage(final OfferDto offer, final long chatId, final int messageId) { try { final EditMessageCaption edit = new EditMessageCaption(chatId + "", messageId, null, createText(offer), createKeyboard(offer), null, null); edit.setParseMode("Markdown"); bot.execute(edit); } catch (TelegramApiException | JsonProcessingException e) { log.error("Failed to edit Message to #{}.", chatId, e); } } private void send(final OfferDto offer) { try { final InputFile inputFile = offer.getImageURL() == null ? new InputFile(new ByteArrayInputStream(NO_IMAGE), "[Kein Bild]") : new InputFile(offer.getImageURL()); final SendPhoto send = new SendPhoto(CHAT_ID + "", inputFile); log.debug("{}", send); send.setCaption(createText(offer)); send.setReplyMarkup(createKeyboard(offer)); final Message message = bot.execute(send); offerService.setTelegramMessageId(offer, message.getMessageId()); } catch (TelegramApiException | JsonProcessingException e) { log.error("Failed to send Message to #{}.", CHAT_ID, e); } } private String createText(final OfferDto offer) { return "[%s](%s)\n%s\n%s".formatted( offer.getTitle().replaceAll("\\[", "(").replaceAll("]", ")"), offer.getArticleURL(), offer.combineLocation(), offer.getDescription() ); } private InlineKeyboardMarkup createKeyboard(final OfferDto offer) throws JsonProcessingException { final InlineKeyboardMarkup markup = new InlineKeyboardMarkup(); final ArrayList> keyboard = new ArrayList<>(); final ArrayList row = new ArrayList<>(); if (offer.isRemember()) { 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 List offers) { _remove(offers.stream().map(OfferDto::getTelegramMessageId).filter(Objects::nonNull).toList()); } private void remove(final MaybeInaccessibleMessage... messages) { _remove(Arrays.stream(messages).map(MaybeInaccessibleMessage::getMessageId).toList()); } private void _remove(final List messageIds) { if (messageIds.isEmpty()) { return; } try { bot.execute(new DeleteMessages(CHAT_ID + "", messageIds)); } catch (TelegramApiException e) { log.error("Failed to remove Message to #{}.", CHAT_ID, e); } } }