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)
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);
}

View File

@ -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<Message> 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())) {

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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() {

View File

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

View File

@ -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

View File

@ -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();
}

View File

@ -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<Message, Long> {
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.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<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) {
@ -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<Message> findByTelegramMessage(final MaybeInaccessibleMessage tlgMessage) {