diff --git a/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/KleinanzeigenApi.java b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/KleinanzeigenApi.java index 8c3d462..7eb2ff3 100644 --- a/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/KleinanzeigenApi.java +++ b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/KleinanzeigenApi.java @@ -42,7 +42,7 @@ public class KleinanzeigenApi { @Scheduled(initialDelay = 0, fixedRate = 1, timeUnit = TimeUnit.MINUTES) public void fetch() { - fetch(VERSCHENKEN_EPPELBORN_RADIUS.formatted(config.getRadiusKm()), config.getRadiusKm()); + fetch(VERSCHENKEN_EPPELBORN_RADIUS.formatted(config.getRadiusKm()), config.getRadiusKm(), false); searchService.findAllEnabledDto().forEach(this::fetch); } @@ -53,21 +53,21 @@ public class KleinanzeigenApi { search.getQueryEscaped(), search.getRadius() ); - fetch(url, search.getRadius()); + fetch(url, search.getRadius(), search.isResendOnPriceChange()); } - public void fetch(final String url, final int radius) { + public void fetch(final String url, final int radius, final boolean resendOnPriceChange) { final URI uri = URI.create(url); try { log.debug("Fetching page: {}", uri); final Document document = Jsoup.parse(uri.toURL(), 3000); - document.select("li.ad-listitem:not(.is-topad) article.aditem").forEach(article -> tryParse(article, uri, radius)); + document.select("li.ad-listitem:not(.is-topad) article.aditem").forEach(article -> tryParse(article, uri, radius, resendOnPriceChange)); } catch (IOException e) { log.error("Failed to fetch Kleinanzeigen: {}", e.toString()); } } - private void tryParse(final Element article, final URI uri, final int radius) { + private void tryParse(final Element article, final URI uri, final int radius, final boolean resendOnPriceChange) { try { final OfferCreate create = new OfferCreate(article, uri); if (create.getDistance() > radius) { @@ -78,7 +78,7 @@ public class KleinanzeigenApi { log.info("Offer is blacklisted due to: blacklists={}, offer={}", blacklist.stream().map(BlacklistDto::getQuery).toList(), create); return; } - offerService.updateOrCreate(create); + offerService.updateOrCreate(create, resendOnPriceChange); } catch (NumberFormatException | DateTimeException | LocationNotFound e) { log.error("Failed to parse Offer:\n{}\n", article.outerHtml(), e); } diff --git a/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/Offer.java b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/Offer.java index b6adf32..1db1f12 100644 --- a/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/Offer.java +++ b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/Offer.java @@ -73,6 +73,17 @@ public class Offer { @Nullable private BigDecimal price = null; + @Column + @Nullable + private BigDecimal priceLast = null; + + @Column + @Nullable + private ZonedDateTime priceChanged = null; + + @Column(nullable = false) + private boolean resendOnPriceChange; + @Column(nullable = false) private boolean negotiable; @@ -80,14 +91,16 @@ public class Offer { @OneToMany(mappedBy = "offer") private List messages = new ArrayList<>(); - public Offer(final @NonNull OfferCreate create) { + public Offer(@NonNull final OfferCreate create, final boolean resendOnPriceChange) { this.articleId = create.getArticleId(); this.articleDate = create.getArticleDate(); - change(create); + this.resendOnPriceChange = resendOnPriceChange; + update(create, resendOnPriceChange); } - public boolean change(final OfferCreate create) { + public boolean update(@NonNull final OfferCreate create, final boolean resendOnPriceChange) { boolean changed = false; + this.resendOnPriceChange = resendOnPriceChange; if (!Objects.equals(this.title, create.getTitle())) { this.title = create.getTitle(); changed = true; @@ -117,7 +130,9 @@ public class Offer { changed = true; } if ((this.price == null) != (create.getPrice() == null) || (this.price != null && this.price.compareTo(create.getPrice()) != 0)) { + this.priceLast = this.price; this.price = create.getPrice(); + this.priceChanged = ZonedDateTime.now(); changed = true; } if (!Objects.equals(this.negotiable, create.isNegotiable())) { diff --git a/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferDto.java b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferDto.java index ba26cf3..d2fc7ea 100644 --- a/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferDto.java +++ b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferDto.java @@ -55,6 +55,14 @@ public class OfferDto { @Nullable private final BigDecimal price; + @Nullable + private final BigDecimal priceLast; + + @Nullable + private final ZonedDateTime priceChanged; + + private final boolean resendOnPriceChange; + private final boolean negotiable; public OfferDto(final @NonNull Offer offer) { @@ -73,6 +81,9 @@ public class OfferDto { articleURL = offer.getArticleURL(); imageURL = offer.getImageURL(); price = offer.getPrice(); + priceLast = offer.getPriceLast(); + priceChanged = offer.getPriceChanged(); + resendOnPriceChange = offer.isResendOnPriceChange(); negotiable = offer.isNegotiable(); } diff --git a/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferService.java b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferService.java index fdff080..9bea355 100644 --- a/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferService.java +++ b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferService.java @@ -1,5 +1,6 @@ package de.ph87.kleinanzeigen.kleinanzeigen.offer; +import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; @@ -16,16 +17,16 @@ public class OfferService { private final ApplicationEventPublisher publisher; - public void updateOrCreate(final OfferCreate create) { + public void updateOrCreate(@NonNull final OfferCreate create, final boolean resendOnPriceChange) { offerRepository.findByArticleId(create.getArticleId()).ifPresentOrElse( existing -> { - if (existing.change(create)) { + if (existing.update(create, resendOnPriceChange)) { final OfferDto dto = toDto(existing); publisher.publishEvent(dto); } }, () -> { - final Offer offer = offerRepository.save(new Offer(create)); + final Offer offer = offerRepository.save(new Offer(create, resendOnPriceChange)); final OfferDto dto = toDto(offer); publisher.publishEvent(dto); } diff --git a/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/search/Search.java b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/search/Search.java index 98c2574..4d92783 100644 --- a/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/search/Search.java +++ b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/search/Search.java @@ -30,6 +30,9 @@ public class Search { @Column(nullable = false) private int radius; + @Column(nullable = false) + private boolean resendOnPriceChange; + @Column @Nullable private Integer priceMin = null; diff --git a/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/search/SearchCreate.java b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/search/SearchCreate.java index 78c5121..87409e0 100644 --- a/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/search/SearchCreate.java +++ b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/search/SearchCreate.java @@ -20,12 +20,15 @@ public class SearchCreate { @Nullable private final Integer priceMax; - public SearchCreate(final boolean enabled, @NonNull final String query, final int radius, @Nullable final Integer priceMin, @Nullable final Integer priceMax) { + private final boolean resendOnPriceChange; + + public SearchCreate(final boolean enabled, @NonNull final String query, final int radius, @Nullable final Integer priceMin, @Nullable final Integer priceMax, final boolean resendOnPriceChange) { this.enabled = enabled; this.query = query; this.radius = radius; this.priceMin = priceMin; this.priceMax = priceMax; + this.resendOnPriceChange = resendOnPriceChange; } } diff --git a/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/search/SearchDto.java b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/search/SearchDto.java index aa57614..d08012c 100644 --- a/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/search/SearchDto.java +++ b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/search/SearchDto.java @@ -24,6 +24,8 @@ public class SearchDto { @Nullable private final Integer priceMax; + private final boolean resendOnPriceChange; + public SearchDto(final Search search) { this.id = search.getId(); this.enabled = search.isEnabled(); @@ -31,6 +33,7 @@ public class SearchDto { this.radius = search.getRadius(); this.priceMin = search.getPriceMin(); this.priceMax = search.getPriceMax(); + this.resendOnPriceChange = search.isResendOnPriceChange(); } public String getQueryEscaped() { diff --git a/src/main/java/de/ph87/kleinanzeigen/telegram/TelegramService.java b/src/main/java/de/ph87/kleinanzeigen/telegram/TelegramService.java index bd3e95c..a97047c 100644 --- a/src/main/java/de/ph87/kleinanzeigen/telegram/TelegramService.java +++ b/src/main/java/de/ph87/kleinanzeigen/telegram/TelegramService.java @@ -10,6 +10,7 @@ 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.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -66,7 +67,7 @@ public class TelegramService { } @TransactionalEventListener(OfferDto.class) - public void onOffer(final OfferDto offer) { + public void onOffer(@NonNull final OfferDto offer) { final List existingMessages = messageService.findAllDtoByOfferDto(offer); final List chats = chatService.findAllEnabled(); for (final ChatDto chat : chats) { @@ -144,43 +145,55 @@ public class TelegramService { log.info("Sending: offer={} to chat={}", offerDto, chatDto); final Message tlgMessage = bot.execute(send); - messageService.create(offerDto, chatDto, tlgMessage); + messageService.updateOrCreate(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); + private void update(@NonNull final MessageDto message) { + // resendOnPriceChange + if (message.getOffer().isResendOnPriceChange() && message.getOffer().getPriceChanged() != null && message.getHide() != null && !message.getOffer().getPriceChanged().isBefore(message.getHide())) { + messageService.setHide(message, false); + if (message.getTelegramMessageId() == null) { + send(message.getOffer(), message.getChat()); + return; + } + } + + // hide: delete message from telegram + if (message.getHide() != null && message.getTelegramMessageId() != null) { + remove(message.getChat().getId(), message.getTelegramMessageId()); + messageService.clearTelegramMessageId(message); return; } - if (messageDto.getTelegramMessageId() == null) { + + // message + if (message.getTelegramMessageId() == null) { return; } try { final EditMessageCaption edit = new EditMessageCaption( - messageDto.getChat().getId() + "", - messageDto.getTelegramMessageId(), + message.getChat().getId() + "", + message.getTelegramMessageId(), null, - createText(messageDto.getOffer()), - createKeyboard(messageDto.isRemember()), + createText(message.getOffer()), + createKeyboard(message.isRemember()), null, null ); edit.setParseMode("Markdown"); - log.info("Editing Message: {}", messageDto); + 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: {}", messageDto); - messageService.setHide(messageDto, true); + log.info("Message has been deleted by User. Marking has hidden: {}", message); + messageService.setHide(message, 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); + log.debug("Ignoring complaint from telegram-bot-api about unmodified message: {}", message); } else { - log.error("Failed to edit Message chat={} message={}", messageDto.getChat().getId(), messageDto.getTelegramMessageId(), e); + log.error("Failed to edit Message chat={} message={}", message.getChat().getId(), message.getTelegramMessageId(), e); } } } diff --git a/src/main/java/de/ph87/kleinanzeigen/telegram/chat/message/Message.java b/src/main/java/de/ph87/kleinanzeigen/telegram/chat/message/Message.java index b05ef02..f664b27 100644 --- a/src/main/java/de/ph87/kleinanzeigen/telegram/chat/message/Message.java +++ b/src/main/java/de/ph87/kleinanzeigen/telegram/chat/message/Message.java @@ -6,6 +6,8 @@ import jakarta.annotation.Nullable; import jakarta.persistence.*; import lombok.*; +import java.time.ZonedDateTime; + @Entity @Getter @ToString @@ -31,7 +33,8 @@ public class Message { @Setter @Column - private boolean hide = false; + @Nullable + private ZonedDateTime hide = null; @Setter @Column diff --git a/src/main/java/de/ph87/kleinanzeigen/telegram/chat/message/MessageDto.java b/src/main/java/de/ph87/kleinanzeigen/telegram/chat/message/MessageDto.java index adf2b09..47b7325 100644 --- a/src/main/java/de/ph87/kleinanzeigen/telegram/chat/message/MessageDto.java +++ b/src/main/java/de/ph87/kleinanzeigen/telegram/chat/message/MessageDto.java @@ -6,6 +6,8 @@ import jakarta.annotation.Nullable; import lombok.Data; import lombok.NonNull; +import java.time.ZonedDateTime; + @Data public class MessageDto { @@ -20,7 +22,8 @@ public class MessageDto { @Nullable private final Integer telegramMessageId; - private final boolean hide; + @Nullable + private final ZonedDateTime hide; private final boolean remember; @@ -29,7 +32,7 @@ public class MessageDto { chat = chatDto; offer = offerDto; telegramMessageId = message.getTelegramMessageId(); - hide = message.isHide(); + hide = message.getHide(); remember = message.isRemember(); } diff --git a/src/main/java/de/ph87/kleinanzeigen/telegram/chat/message/MessageRepository.java b/src/main/java/de/ph87/kleinanzeigen/telegram/chat/message/MessageRepository.java index 24dedc7..8223a68 100644 --- a/src/main/java/de/ph87/kleinanzeigen/telegram/chat/message/MessageRepository.java +++ b/src/main/java/de/ph87/kleinanzeigen/telegram/chat/message/MessageRepository.java @@ -1,5 +1,7 @@ package de.ph87.kleinanzeigen.telegram.chat.message; +import de.ph87.kleinanzeigen.kleinanzeigen.offer.Offer; +import de.ph87.kleinanzeigen.telegram.chat.Chat; import org.springframework.data.repository.ListCrudRepository; import java.util.List; @@ -11,4 +13,6 @@ public interface MessageRepository extends ListCrudRepository { List findAllByOffer_Id(long id); + Optional findByChatAndOffer(Chat chat, Offer offer); + } diff --git a/src/main/java/de/ph87/kleinanzeigen/telegram/chat/message/MessageService.java b/src/main/java/de/ph87/kleinanzeigen/telegram/chat/message/MessageService.java index ee3f230..7966c9e 100644 --- a/src/main/java/de/ph87/kleinanzeigen/telegram/chat/message/MessageService.java +++ b/src/main/java/de/ph87/kleinanzeigen/telegram/chat/message/MessageService.java @@ -14,6 +14,7 @@ import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.telegram.telegrambots.meta.api.objects.MaybeInaccessibleMessage; +import java.time.ZonedDateTime; import java.util.List; import java.util.Optional; @@ -31,15 +32,18 @@ public class MessageService { private final ChatService chatService; @Transactional(propagation = Propagation.REQUIRES_NEW) - public void create(final OfferDto offerDto, final ChatDto chatDto, final org.telegram.telegrambots.meta.api.objects.Message tlgMessage) { + public void updateOrCreate(final OfferDto offerDto, final ChatDto chatDto, final org.telegram.telegrambots.meta.api.objects.Message tlgMessage) { final Offer offer = offerService.getByDto(offerDto); final Chat chat = chatService.getByDto(chatDto); - messageRepository.save(new Message(offer, chat, tlgMessage)); + messageRepository.findByChatAndOffer(chat, offer).ifPresentOrElse( + existing -> existing.setTelegramMessageId(tlgMessage.getMessageId()), + () -> messageRepository.save(new Message(offer, chat, tlgMessage)) + ); } @SuppressWarnings("UnusedReturnValue") public Optional setHide(final MaybeInaccessibleMessage tlgMessage, final boolean hide) { - return findByTelegramMessage(tlgMessage).stream().peek(offer -> offer.setHide(hide)).findFirst().map(this::toDto); + return findByTelegramMessage(tlgMessage).stream().peek(offer -> offer.setHide(hide ? ZonedDateTime.now() : null)).findFirst().map(this::toDto); } public Optional setRemember(final MaybeInaccessibleMessage tlgMessage, final boolean remember) { @@ -55,7 +59,7 @@ public class MessageService { } public void setHide(final MessageDto dto, final boolean hide) { - findByDto(dto).ifPresent(offer -> offer.setHide(hide)); + findByDto(dto).ifPresent(offer -> offer.setHide(hide ? ZonedDateTime.now() : null)); } private Optional findByTelegramMessage(final MaybeInaccessibleMessage tlgMessage) {