resendOnPriceChange

This commit is contained in:
Patrick Haßel 2024-07-25 16:34:39 +02:00
parent 10f4cdac2a
commit a856c8e01c
12 changed files with 99 additions and 36 deletions

View File

@ -42,7 +42,7 @@ public class KleinanzeigenApi {
@Scheduled(initialDelay = 0, fixedRate = 1, timeUnit = TimeUnit.MINUTES) @Scheduled(initialDelay = 0, fixedRate = 1, timeUnit = TimeUnit.MINUTES)
public void fetch() { 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); searchService.findAllEnabledDto().forEach(this::fetch);
} }
@ -53,21 +53,21 @@ public class KleinanzeigenApi {
search.getQueryEscaped(), search.getQueryEscaped(),
search.getRadius() 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); final URI uri = URI.create(url);
try { try {
log.debug("Fetching page: {}", uri); log.debug("Fetching page: {}", uri);
final Document document = Jsoup.parse(uri.toURL(), 3000); 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) { } catch (IOException e) {
log.error("Failed to fetch Kleinanzeigen: {}", e.toString()); 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 { try {
final OfferCreate create = new OfferCreate(article, uri); final OfferCreate create = new OfferCreate(article, uri);
if (create.getDistance() > radius) { 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); log.info("Offer is blacklisted due to: blacklists={}, offer={}", blacklist.stream().map(BlacklistDto::getQuery).toList(), create);
return; return;
} }
offerService.updateOrCreate(create); offerService.updateOrCreate(create, resendOnPriceChange);
} catch (NumberFormatException | DateTimeException | LocationNotFound e) { } catch (NumberFormatException | DateTimeException | LocationNotFound e) {
log.error("Failed to parse Offer:\n{}\n", article.outerHtml(), e); log.error("Failed to parse Offer:\n{}\n", article.outerHtml(), e);
} }

View File

@ -73,6 +73,17 @@ public class Offer {
@Nullable @Nullable
private BigDecimal price = null; 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) @Column(nullable = false)
private boolean negotiable; private boolean negotiable;
@ -80,14 +91,16 @@ public class Offer {
@OneToMany(mappedBy = "offer") @OneToMany(mappedBy = "offer")
private List<Message> messages = new ArrayList<>(); private List<Message> messages = new ArrayList<>();
public Offer(final @NonNull OfferCreate create) { public Offer(@NonNull final OfferCreate create, final boolean resendOnPriceChange) {
this.articleId = create.getArticleId(); this.articleId = create.getArticleId();
this.articleDate = create.getArticleDate(); 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; boolean changed = false;
this.resendOnPriceChange = resendOnPriceChange;
if (!Objects.equals(this.title, create.getTitle())) { if (!Objects.equals(this.title, create.getTitle())) {
this.title = create.getTitle(); this.title = create.getTitle();
changed = true; changed = true;
@ -117,7 +130,9 @@ public class Offer {
changed = true; changed = true;
} }
if ((this.price == null) != (create.getPrice() == null) || (this.price != null && this.price.compareTo(create.getPrice()) != 0)) { 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.price = create.getPrice();
this.priceChanged = ZonedDateTime.now();
changed = true; changed = true;
} }
if (!Objects.equals(this.negotiable, create.isNegotiable())) { if (!Objects.equals(this.negotiable, create.isNegotiable())) {

View File

@ -55,6 +55,14 @@ public class OfferDto {
@Nullable @Nullable
private final BigDecimal price; private final BigDecimal price;
@Nullable
private final BigDecimal priceLast;
@Nullable
private final ZonedDateTime priceChanged;
private final boolean resendOnPriceChange;
private final boolean negotiable; private final boolean negotiable;
public OfferDto(final @NonNull Offer offer) { public OfferDto(final @NonNull Offer offer) {
@ -73,6 +81,9 @@ public class OfferDto {
articleURL = offer.getArticleURL(); articleURL = offer.getArticleURL();
imageURL = offer.getImageURL(); imageURL = offer.getImageURL();
price = offer.getPrice(); price = offer.getPrice();
priceLast = offer.getPriceLast();
priceChanged = offer.getPriceChanged();
resendOnPriceChange = offer.isResendOnPriceChange();
negotiable = offer.isNegotiable(); negotiable = offer.isNegotiable();
} }

View File

@ -1,5 +1,6 @@
package de.ph87.kleinanzeigen.kleinanzeigen.offer; package de.ph87.kleinanzeigen.kleinanzeigen.offer;
import lombok.NonNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
@ -16,16 +17,16 @@ public class OfferService {
private final ApplicationEventPublisher publisher; 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( offerRepository.findByArticleId(create.getArticleId()).ifPresentOrElse(
existing -> { existing -> {
if (existing.change(create)) { if (existing.update(create, resendOnPriceChange)) {
final OfferDto dto = toDto(existing); final OfferDto dto = toDto(existing);
publisher.publishEvent(dto); 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); final OfferDto dto = toDto(offer);
publisher.publishEvent(dto); publisher.publishEvent(dto);
} }

View File

@ -30,6 +30,9 @@ public class Search {
@Column(nullable = false) @Column(nullable = false)
private int radius; private int radius;
@Column(nullable = false)
private boolean resendOnPriceChange;
@Column @Column
@Nullable @Nullable
private Integer priceMin = null; private Integer priceMin = null;

View File

@ -20,12 +20,15 @@ public class SearchCreate {
@Nullable @Nullable
private final Integer priceMax; 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.enabled = enabled;
this.query = query; this.query = query;
this.radius = radius; this.radius = radius;
this.priceMin = priceMin; this.priceMin = priceMin;
this.priceMax = priceMax; this.priceMax = priceMax;
this.resendOnPriceChange = resendOnPriceChange;
} }
} }

View File

@ -24,6 +24,8 @@ public class SearchDto {
@Nullable @Nullable
private final Integer priceMax; private final Integer priceMax;
private final boolean resendOnPriceChange;
public SearchDto(final Search search) { public SearchDto(final Search search) {
this.id = search.getId(); this.id = search.getId();
this.enabled = search.isEnabled(); this.enabled = search.isEnabled();
@ -31,6 +33,7 @@ public class SearchDto {
this.radius = search.getRadius(); this.radius = search.getRadius();
this.priceMin = search.getPriceMin(); this.priceMin = search.getPriceMin();
this.priceMax = search.getPriceMax(); this.priceMax = search.getPriceMax();
this.resendOnPriceChange = search.isResendOnPriceChange();
} }
public String getQueryEscaped() { public String getQueryEscaped() {

View File

@ -10,6 +10,7 @@ import de.ph87.kleinanzeigen.telegram.chat.message.MessageDto;
import de.ph87.kleinanzeigen.telegram.chat.message.MessageService; import de.ph87.kleinanzeigen.telegram.chat.message.MessageService;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import lombok.NonNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -66,7 +67,7 @@ public class TelegramService {
} }
@TransactionalEventListener(OfferDto.class) @TransactionalEventListener(OfferDto.class)
public void onOffer(final OfferDto offer) { public void onOffer(@NonNull final OfferDto offer) {
final List<MessageDto> existingMessages = messageService.findAllDtoByOfferDto(offer); final List<MessageDto> existingMessages = messageService.findAllDtoByOfferDto(offer);
final List<ChatDto> chats = chatService.findAllEnabled(); final List<ChatDto> chats = chatService.findAllEnabled();
for (final ChatDto chat : chats) { for (final ChatDto chat : chats) {
@ -144,43 +145,55 @@ public class TelegramService {
log.info("Sending: offer={} to chat={}", offerDto, chatDto); log.info("Sending: offer={} to chat={}", offerDto, chatDto);
final Message tlgMessage = bot.execute(send); final Message tlgMessage = bot.execute(send);
messageService.create(offerDto, chatDto, tlgMessage); messageService.updateOrCreate(offerDto, chatDto, tlgMessage);
} catch (TelegramApiException | JsonProcessingException e) { } catch (TelegramApiException | JsonProcessingException e) {
log.error("Failed to send: chat={}, offer={}: {}", chatDto, offerDto, e.toString()); log.error("Failed to send: chat={}, offer={}: {}", chatDto, offerDto, e.toString());
} }
} }
private void update(final MessageDto messageDto) { private void update(@NonNull final MessageDto message) {
if (messageDto.isHide() && messageDto.getTelegramMessageId() != null) { // resendOnPriceChange
remove(messageDto.getChat().getId(), messageDto.getTelegramMessageId()); if (message.getOffer().isResendOnPriceChange() && message.getOffer().getPriceChanged() != null && message.getHide() != null && !message.getOffer().getPriceChanged().isBefore(message.getHide())) {
messageService.clearTelegramMessageId(messageDto); messageService.setHide(message, false);
if (message.getTelegramMessageId() == null) {
send(message.getOffer(), message.getChat());
return; return;
} }
if (messageDto.getTelegramMessageId() == null) { }
// hide: delete message from telegram
if (message.getHide() != null && message.getTelegramMessageId() != null) {
remove(message.getChat().getId(), message.getTelegramMessageId());
messageService.clearTelegramMessageId(message);
return;
}
// message
if (message.getTelegramMessageId() == null) {
return; return;
} }
try { try {
final EditMessageCaption edit = new EditMessageCaption( final EditMessageCaption edit = new EditMessageCaption(
messageDto.getChat().getId() + "", message.getChat().getId() + "",
messageDto.getTelegramMessageId(), message.getTelegramMessageId(),
null, null,
createText(messageDto.getOffer()), createText(message.getOffer()),
createKeyboard(messageDto.isRemember()), createKeyboard(message.isRemember()),
null, null,
null null
); );
edit.setParseMode("Markdown"); edit.setParseMode("Markdown");
log.info("Editing Message: {}", messageDto); log.info("Editing Message: {}", message);
bot.execute(edit); bot.execute(edit);
} catch (TelegramApiException | JsonProcessingException e) { } catch (TelegramApiException | JsonProcessingException e) {
if (e.toString().endsWith("Bad Request: message to edit not found")) { if (e.toString().endsWith("Bad Request: message to edit not found")) {
log.info("Message has been deleted by User. Marking has hidden: {}", messageDto); log.info("Message has been deleted by User. Marking has hidden: {}", message);
messageService.setHide(messageDto, true); 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")) { } 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 { } 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);
} }
} }
} }

View File

@ -6,6 +6,8 @@ import jakarta.annotation.Nullable;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.*; import lombok.*;
import java.time.ZonedDateTime;
@Entity @Entity
@Getter @Getter
@ToString @ToString
@ -31,7 +33,8 @@ public class Message {
@Setter @Setter
@Column @Column
private boolean hide = false; @Nullable
private ZonedDateTime hide = null;
@Setter @Setter
@Column @Column

View File

@ -6,6 +6,8 @@ import jakarta.annotation.Nullable;
import lombok.Data; import lombok.Data;
import lombok.NonNull; import lombok.NonNull;
import java.time.ZonedDateTime;
@Data @Data
public class MessageDto { public class MessageDto {
@ -20,7 +22,8 @@ public class MessageDto {
@Nullable @Nullable
private final Integer telegramMessageId; private final Integer telegramMessageId;
private final boolean hide; @Nullable
private final ZonedDateTime hide;
private final boolean remember; private final boolean remember;
@ -29,7 +32,7 @@ public class MessageDto {
chat = chatDto; chat = chatDto;
offer = offerDto; offer = offerDto;
telegramMessageId = message.getTelegramMessageId(); telegramMessageId = message.getTelegramMessageId();
hide = message.isHide(); hide = message.getHide();
remember = message.isRemember(); remember = message.isRemember();
} }

View File

@ -1,5 +1,7 @@
package de.ph87.kleinanzeigen.telegram.chat.message; 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 org.springframework.data.repository.ListCrudRepository;
import java.util.List; import java.util.List;
@ -11,4 +13,6 @@ public interface MessageRepository extends ListCrudRepository<Message, Long> {
List<Message> findAllByOffer_Id(long id); List<Message> findAllByOffer_Id(long id);
Optional<Message> findByChatAndOffer(Chat chat, Offer offer);
} }

View File

@ -14,6 +14,7 @@ import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.telegram.telegrambots.meta.api.objects.MaybeInaccessibleMessage; import org.telegram.telegrambots.meta.api.objects.MaybeInaccessibleMessage;
import java.time.ZonedDateTime;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -31,15 +32,18 @@ public class MessageService {
private final ChatService chatService; private final ChatService chatService;
@Transactional(propagation = Propagation.REQUIRES_NEW) @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 Offer offer = offerService.getByDto(offerDto);
final Chat chat = chatService.getByDto(chatDto); 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") @SuppressWarnings("UnusedReturnValue")
public Optional<MessageDto> setHide(final MaybeInaccessibleMessage tlgMessage, final boolean hide) { public Optional<MessageDto> 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<MessageDto> setRemember(final MaybeInaccessibleMessage tlgMessage, final boolean remember) { public Optional<MessageDto> setRemember(final MaybeInaccessibleMessage tlgMessage, final boolean remember) {
@ -55,7 +59,7 @@ public class MessageService {
} }
public void setHide(final MessageDto dto, final boolean hide) { 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<Message> findByTelegramMessage(final MaybeInaccessibleMessage tlgMessage) { private Optional<Message> findByTelegramMessage(final MaybeInaccessibleMessage tlgMessage) {