Kleinanzeigen/src/main/java/de/ph87/kleinanzeigen/telegram/TelegramService.java

234 lines
9.1 KiB
Java

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<MessageDto> existingMessages = messageService.findAllDtoByOfferDto(offer);
final List<ChatDto> 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<List<InlineKeyboardButton>> keyboard = new ArrayList<>();
final ArrayList<InlineKeyboardButton> 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<InlineKeyboardButton> 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);
}
}
}