multiuser (whitelist via application.properties)
This commit is contained in:
parent
538e99beac
commit
6d6d70caf0
@ -7,3 +7,5 @@ spring.datasource.password=password
|
|||||||
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
||||||
#-
|
#-
|
||||||
#spring.jpa.hibernate.ddl-auto=create
|
#spring.jpa.hibernate.ddl-auto=create
|
||||||
|
#-
|
||||||
|
de.ph87.kleinanzeigen.telegram.chat.whitelist=101138682,269710244
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
package de.ph87.kleinanzeigen.kleinanzeigen;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class FetchResult {
|
|
||||||
|
|
||||||
private int created = 0;
|
|
||||||
|
|
||||||
private int updated = 0;
|
|
||||||
|
|
||||||
private int error = 0;
|
|
||||||
|
|
||||||
public void add(final MergeResult mergeResult) {
|
|
||||||
switch (mergeResult) {
|
|
||||||
case CREATED -> created++;
|
|
||||||
case UPDATED -> updated++;
|
|
||||||
case ERROR -> error++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void merge(final FetchResult other) {
|
|
||||||
this.created += other.created;
|
|
||||||
this.updated += other.updated;
|
|
||||||
this.error += other.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,14 +1,12 @@
|
|||||||
package de.ph87.kleinanzeigen.kleinanzeigen;
|
package de.ph87.kleinanzeigen.kleinanzeigen;
|
||||||
|
|
||||||
import de.ph87.kleinanzeigen.kleinanzeigen.offer.OfferCreate;
|
import de.ph87.kleinanzeigen.kleinanzeigen.offer.OfferCreate;
|
||||||
import de.ph87.kleinanzeigen.kleinanzeigen.offer.OfferDto;
|
|
||||||
import de.ph87.kleinanzeigen.kleinanzeigen.offer.OfferService;
|
import de.ph87.kleinanzeigen.kleinanzeigen.offer.OfferService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jsoup.Jsoup;
|
import org.jsoup.Jsoup;
|
||||||
import org.jsoup.nodes.Document;
|
import org.jsoup.nodes.Document;
|
||||||
import org.jsoup.nodes.Element;
|
import org.jsoup.nodes.Element;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -25,52 +23,28 @@ public class KleinanzeigenApi {
|
|||||||
|
|
||||||
private static final String VERSCHENKEN_EPPELBORN_30KM = "https://www.kleinanzeigen.de/s-zu-verschenken/66571/seite:%d/c192l339r%d";
|
private static final String VERSCHENKEN_EPPELBORN_30KM = "https://www.kleinanzeigen.de/s-zu-verschenken/66571/seite:%d/c192l339r%d";
|
||||||
|
|
||||||
private static final int FETCH_UNTIL_DUPLICATE_MAX_PAGES = 1;
|
|
||||||
|
|
||||||
private final OfferService offerService;
|
private final OfferService offerService;
|
||||||
|
|
||||||
private final ApplicationEventPublisher publisher;
|
|
||||||
|
|
||||||
private final KleinanzeigenConfig config;
|
private final KleinanzeigenConfig config;
|
||||||
|
|
||||||
@Scheduled(initialDelay = 0, fixedRate = 1, timeUnit = TimeUnit.MINUTES)
|
@Scheduled(initialDelay = 0, fixedRate = 1, timeUnit = TimeUnit.MINUTES)
|
||||||
public void fetch() {
|
public void fetch() {
|
||||||
fetchPagesUntilDuplicate();
|
final URI uri = URI.create(VERSCHENKEN_EPPELBORN_30KM.formatted(1, config.getRadiusKm()));
|
||||||
}
|
|
||||||
|
|
||||||
private void fetchPagesUntilDuplicate() {
|
|
||||||
int page = 0;
|
|
||||||
final FetchResult totalFetchResult = new FetchResult();
|
|
||||||
while (totalFetchResult.getUpdated() <= 0 && page < FETCH_UNTIL_DUPLICATE_MAX_PAGES) {
|
|
||||||
final FetchResult pageFetchResult = fetchPage(++page);
|
|
||||||
totalFetchResult.merge(pageFetchResult);
|
|
||||||
}
|
|
||||||
log.debug("FetchResult: {}", totalFetchResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
private FetchResult fetchPage(final int page) {
|
|
||||||
final FetchResult fetchResult = new FetchResult();
|
|
||||||
final Document document;
|
|
||||||
final URI uri = URI.create(VERSCHENKEN_EPPELBORN_30KM.formatted(page, config.getRadiusKm()));
|
|
||||||
try {
|
try {
|
||||||
log.debug("Fetching page: {}", uri);
|
log.debug("Fetching page: {}", uri);
|
||||||
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));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("Failed to fetch Kleinanzeigen: {}", e.toString());
|
log.error("Failed to fetch Kleinanzeigen: {}", e.toString());
|
||||||
return fetchResult;
|
|
||||||
}
|
}
|
||||||
document.select("li.ad-listitem:not(.is-topad) article.aditem").forEach(article -> tryParse(article, uri, fetchResult));
|
|
||||||
return fetchResult;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void tryParse(final Element article, final URI uri, final FetchResult fetchResult) {
|
private void tryParse(final Element article, final URI uri) {
|
||||||
try {
|
try {
|
||||||
final OfferCreate create = new OfferCreate(article, uri);
|
final OfferCreate create = new OfferCreate(article, uri);
|
||||||
final OfferDto dto = offerService.updateOrCreate(create, fetchResult);
|
offerService.updateOrCreate(create);
|
||||||
publisher.publishEvent(dto);
|
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
log.error("Failed to parse Offer:\n{}\n", article.outerHtml(), e);
|
log.error("Failed to parse Offer:\n{}\n", article.outerHtml(), e);
|
||||||
fetchResult.add(MergeResult.ERROR);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import org.springframework.stereotype.Component;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Data
|
@Data
|
||||||
@Component
|
@Component
|
||||||
@ConfigurationProperties(prefix = "de.ph87.kleinanzeigen")
|
@ConfigurationProperties(prefix = "de.ph87.kleinanzeigen.api")
|
||||||
public class KleinanzeigenConfig {
|
public class KleinanzeigenConfig {
|
||||||
|
|
||||||
private int radiusKm = 15;
|
private int radiusKm = 15;
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
package de.ph87.kleinanzeigen.kleinanzeigen;
|
|
||||||
|
|
||||||
public enum MergeResult {
|
|
||||||
CREATED, UPDATED, ERROR
|
|
||||||
}
|
|
||||||
@ -1,12 +1,17 @@
|
|||||||
package de.ph87.kleinanzeigen.kleinanzeigen.offer;
|
package de.ph87.kleinanzeigen.kleinanzeigen.offer;
|
||||||
|
|
||||||
|
import de.ph87.kleinanzeigen.telegram.chat.message.Message;
|
||||||
import jakarta.annotation.Nullable;
|
import jakarta.annotation.Nullable;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.*;
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Getter
|
@Getter
|
||||||
@ -29,17 +34,6 @@ public class Offer {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private ZonedDateTime last = first;
|
private ZonedDateTime last = first;
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Column(nullable = false)
|
|
||||||
private ZonedDateTime expiry = first.plusDays(3);
|
|
||||||
|
|
||||||
@Setter
|
|
||||||
@Column
|
|
||||||
private boolean hide = false;
|
|
||||||
|
|
||||||
@Column
|
|
||||||
private boolean remember = false;
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private String articleId;
|
private String articleId;
|
||||||
@ -76,35 +70,46 @@ public class Offer {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private String imageURL = null;
|
private String imageURL = null;
|
||||||
|
|
||||||
@Setter
|
@OneToMany(mappedBy = "offer")
|
||||||
@Column
|
private List<Message> messages = new ArrayList<>();
|
||||||
@Nullable
|
|
||||||
private Integer telegramMessageId = null;
|
|
||||||
|
|
||||||
public Offer(final @NonNull OfferCreate create) {
|
public Offer(final @NonNull OfferCreate create) {
|
||||||
this.articleId = create.getArticleId();
|
this.articleId = create.getArticleId();
|
||||||
this.articleDate = create.getArticleDate();
|
this.articleDate = create.getArticleDate();
|
||||||
update(create);
|
change(create);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void update(final OfferCreate create) {
|
public boolean change(final OfferCreate create) {
|
||||||
this.title = create.getTitle();
|
boolean changed = false;
|
||||||
this.zipcode = create.getZipcode();
|
if (!Objects.equals(this.title, create.getTitle())) {
|
||||||
this.location = create.getLocation();
|
this.title = create.getTitle();
|
||||||
this.distance = create.getDistance();
|
changed = true;
|
||||||
this.description = create.getDescription();
|
|
||||||
this.articleURL = create.getArticleURL();
|
|
||||||
this.imageURL = create.getImageURL();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRemember(final boolean newRemember) {
|
|
||||||
if (remember && !newRemember) {
|
|
||||||
final ZonedDateTime oneHour = ZonedDateTime.now().plusHours(1);
|
|
||||||
if (oneHour.isAfter(expiry)) {
|
|
||||||
expiry = oneHour;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
remember = newRemember;
|
if (!Objects.equals(this.zipcode, create.getZipcode())) {
|
||||||
|
this.zipcode = create.getZipcode();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(this.location, create.getLocation())) {
|
||||||
|
this.location = create.getLocation();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(this.distance, create.getDistance())) {
|
||||||
|
this.distance = create.getDistance();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(this.description, create.getDescription())) {
|
||||||
|
this.description = create.getDescription();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(this.articleURL, create.getArticleURL())) {
|
||||||
|
this.articleURL = create.getArticleURL();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(this.imageURL, create.getImageURL())) {
|
||||||
|
this.imageURL = create.getImageURL();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,13 +25,6 @@ public class OfferDto {
|
|||||||
@NonNull
|
@NonNull
|
||||||
private final ZonedDateTime last;
|
private final ZonedDateTime last;
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final ZonedDateTime expiry;
|
|
||||||
|
|
||||||
private final boolean hide;
|
|
||||||
|
|
||||||
private final boolean remember;
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@ToString.Include
|
@ToString.Include
|
||||||
private final String articleId;
|
private final String articleId;
|
||||||
@ -61,19 +54,11 @@ public class OfferDto {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private final String imageURL;
|
private final String imageURL;
|
||||||
|
|
||||||
@Nullable
|
public OfferDto(final @NonNull Offer offer) {
|
||||||
private final Integer telegramMessageId;
|
|
||||||
|
|
||||||
private final boolean _existing_;
|
|
||||||
|
|
||||||
public OfferDto(final @NonNull Offer offer, final boolean existing) {
|
|
||||||
id = offer.getId();
|
id = offer.getId();
|
||||||
version = offer.getVersion();
|
version = offer.getVersion();
|
||||||
first = offer.getFirst();
|
first = offer.getFirst();
|
||||||
last = offer.getLast();
|
last = offer.getLast();
|
||||||
expiry = offer.getExpiry();
|
|
||||||
hide = offer.isHide();
|
|
||||||
remember = offer.isRemember();
|
|
||||||
|
|
||||||
articleId = offer.getArticleId();
|
articleId = offer.getArticleId();
|
||||||
articleDate = offer.getArticleDate();
|
articleDate = offer.getArticleDate();
|
||||||
@ -84,9 +69,6 @@ public class OfferDto {
|
|||||||
description = offer.getDescription();
|
description = offer.getDescription();
|
||||||
articleURL = offer.getArticleURL();
|
articleURL = offer.getArticleURL();
|
||||||
imageURL = offer.getImageURL();
|
imageURL = offer.getImageURL();
|
||||||
telegramMessageId = offer.getTelegramMessageId();
|
|
||||||
|
|
||||||
_existing_ = existing;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String combineLocation() {
|
public String combineLocation() {
|
||||||
|
|||||||
@ -2,16 +2,10 @@ package de.ph87.kleinanzeigen.kleinanzeigen.offer;
|
|||||||
|
|
||||||
import org.springframework.data.repository.ListCrudRepository;
|
import org.springframework.data.repository.ListCrudRepository;
|
||||||
|
|
||||||
import java.time.ZonedDateTime;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface OfferRepository extends ListCrudRepository<Offer, Long> {
|
public interface OfferRepository extends ListCrudRepository<Offer, Long> {
|
||||||
|
|
||||||
List<Offer> findAllByExpiryBefore(final ZonedDateTime deadline);
|
|
||||||
|
|
||||||
Optional<Offer> findByArticleId(String articleId);
|
Optional<Offer> findByArticleId(String articleId);
|
||||||
|
|
||||||
Optional<Offer> findByTelegramMessageId(int telegramMessageId);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,85 +1,43 @@
|
|||||||
package de.ph87.kleinanzeigen.kleinanzeigen.offer;
|
package de.ph87.kleinanzeigen.kleinanzeigen.offer;
|
||||||
|
|
||||||
import de.ph87.kleinanzeigen.kleinanzeigen.FetchResult;
|
|
||||||
import de.ph87.kleinanzeigen.kleinanzeigen.MergeResult;
|
|
||||||
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;
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.time.ZonedDateTime;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
@Transactional
|
||||||
@EnableScheduling
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class OfferService {
|
public class OfferService {
|
||||||
|
|
||||||
private final OfferRepository repository;
|
private final OfferRepository offerRepository;
|
||||||
|
|
||||||
private final ApplicationEventPublisher publisher;
|
private final ApplicationEventPublisher publisher;
|
||||||
|
|
||||||
@Scheduled(initialDelay = 1, fixedRate = 1, timeUnit = TimeUnit.DAYS)
|
public void updateOrCreate(final OfferCreate create) {
|
||||||
public void cleanUp() {
|
offerRepository.findByArticleId(create.getArticleId()).ifPresentOrElse(
|
||||||
final List<Offer> list = repository.findAllByExpiryBefore(ZonedDateTime.now());
|
existing -> {
|
||||||
repository.deleteAll(list);
|
if (existing.change(create)) {
|
||||||
list.stream().map(offer -> toDto(offer, false)).forEach(publisher::publishEvent);
|
final OfferDto dto = toDto(existing);
|
||||||
}
|
publisher.publishEvent(dto);
|
||||||
|
|
||||||
private OfferDto toDto(final Offer offer, final boolean existing) {
|
|
||||||
return new OfferDto(offer, existing);
|
|
||||||
}
|
|
||||||
|
|
||||||
public OfferDto updateOrCreate(final OfferCreate create, final FetchResult fetchResult) {
|
|
||||||
return toDto(_updateOrCreate(create, fetchResult), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Offer _updateOrCreate(final OfferCreate create, final FetchResult fetchResult) {
|
|
||||||
return repository
|
|
||||||
.findByArticleId(create.getArticleId())
|
|
||||||
.stream().peek(
|
|
||||||
existing -> {
|
|
||||||
existing.update(create);
|
|
||||||
fetchResult.add(MergeResult.UPDATED);
|
|
||||||
})
|
|
||||||
.findFirst()
|
|
||||||
.orElseGet(() -> {
|
|
||||||
fetchResult.add(MergeResult.CREATED);
|
|
||||||
return repository.save(new Offer(create));
|
|
||||||
}
|
}
|
||||||
);
|
},
|
||||||
|
() -> {
|
||||||
|
final Offer offer = offerRepository.save(new Offer(create));
|
||||||
|
final OfferDto dto = toDto(offer);
|
||||||
|
publisher.publishEvent(dto);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("UnusedReturnValue")
|
public OfferDto toDto(final Offer offer) {
|
||||||
public Optional<OfferDto> hideByTelegramMessageId(final int messageId, final boolean hide) {
|
return new OfferDto(offer);
|
||||||
return repository.findByTelegramMessageId(messageId).stream().peek(offer -> offer.setHide(hide)).findFirst().map(offer -> toDto(offer, true));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<OfferDto> rememberByTelegramMessageId(final int messageId, final boolean remember) {
|
public Offer getByDto(final OfferDto offerDto) {
|
||||||
return repository.findByTelegramMessageId(messageId).stream().peek(offer -> offer.setRemember(remember)).findFirst().map(offer -> toDto(offer, true));
|
return offerRepository.findById(offerDto.getId()).orElseThrow();
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<OfferDto> findByTelegramMessageId(final int messageId) {
|
|
||||||
return repository.findByTelegramMessageId(messageId).map(offer -> toDto(offer, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTelegramMessageId(final OfferDto dto, final Integer telegramMessageId) {
|
|
||||||
findByDto(dto).ifPresent(offer -> offer.setTelegramMessageId(telegramMessageId));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setHide(final OfferDto dto, final boolean hide) {
|
|
||||||
findByDto(dto).ifPresent(offer -> offer.setHide(hide));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<Offer> findByDto(final OfferDto dto) {
|
|
||||||
return repository.findById(dto.getId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
package de.ph87.kleinanzeigen.telegram;
|
||||||
|
|
||||||
|
public class AccessDenied extends Exception {
|
||||||
|
|
||||||
|
}
|
||||||
@ -3,15 +3,19 @@ package de.ph87.kleinanzeigen.telegram;
|
|||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import de.ph87.kleinanzeigen.kleinanzeigen.offer.OfferDto;
|
import de.ph87.kleinanzeigen.kleinanzeigen.offer.OfferDto;
|
||||||
import de.ph87.kleinanzeigen.kleinanzeigen.offer.OfferService;
|
import de.ph87.kleinanzeigen.telegram.chat.ChatDto.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.PostConstruct;
|
||||||
import jakarta.annotation.PreDestroy;
|
import jakarta.annotation.PreDestroy;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.context.event.EventListener;
|
|
||||||
import org.springframework.stereotype.Service;
|
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.send.SendPhoto;
|
||||||
import org.telegram.telegrambots.meta.api.methods.updatingmessages.DeleteMessages;
|
import org.telegram.telegrambots.meta.api.methods.updatingmessages.DeleteMessage;
|
||||||
import org.telegram.telegrambots.meta.api.methods.updatingmessages.EditMessageCaption;
|
import org.telegram.telegrambots.meta.api.methods.updatingmessages.EditMessageCaption;
|
||||||
import org.telegram.telegrambots.meta.api.objects.*;
|
import org.telegram.telegrambots.meta.api.objects.*;
|
||||||
import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup;
|
import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup;
|
||||||
@ -26,26 +30,24 @@ import java.io.FileInputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class TelegramService {
|
public class TelegramService {
|
||||||
|
|
||||||
private static final long CHAT_ID = 101138682L;
|
|
||||||
|
|
||||||
private static final String ICON_CHECK = "✅";
|
private static final String ICON_CHECK = "✅";
|
||||||
|
|
||||||
private static final String ICON_REMOVE = "❌";
|
private static final String ICON_REMOVE = "❌";
|
||||||
|
|
||||||
|
private final ChatService chatService;
|
||||||
|
|
||||||
private byte[] NO_IMAGE = null;
|
private byte[] NO_IMAGE = null;
|
||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
private final OfferService offerService;
|
private final MessageService messageService;
|
||||||
|
|
||||||
private TelegramBot bot = null;
|
private TelegramBot bot = null;
|
||||||
|
|
||||||
@ -63,19 +65,22 @@ public class TelegramService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventListener(OfferDto.class)
|
@TransactionalEventListener(OfferDto.class)
|
||||||
public void onOfferChanged(final OfferDto offer) {
|
public void onOffer(final OfferDto offer) {
|
||||||
if (offer.is_existing_()) {
|
final List<MessageDto> existing = messageService.findAllDtoByOfferDto(offer);
|
||||||
if (offer.getTelegramMessageId() == null) {
|
for (ChatDto chat : chatService.findAllEnabled()) {
|
||||||
send(offer);
|
existing.stream()
|
||||||
} else {
|
.filter(m -> m.getChat().getId() == chat.getId())
|
||||||
updateMessage(CHAT_ID, offer.getTelegramMessageId());
|
.findFirst()
|
||||||
}
|
.ifPresentOrElse(this::update, () -> send(offer, chat));
|
||||||
} else {
|
|
||||||
remove(List.of(offer));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TransactionalEventListener(MessageDeleted.class)
|
||||||
|
public void onMessageDeleted(final MessageDeleted messageDeleted) {
|
||||||
|
remove(messageDeleted.getChatId(), messageDeleted.getMessageId());
|
||||||
|
}
|
||||||
|
|
||||||
@PreDestroy
|
@PreDestroy
|
||||||
public void stop() {
|
public void stop() {
|
||||||
log.info("Stopping Telegram bot...");
|
log.info("Stopping Telegram bot...");
|
||||||
@ -90,102 +95,113 @@ public class TelegramService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void onUpdateReceived(final Update update) {
|
private void onUpdateReceived(final Update update) {
|
||||||
if (update.hasMessage() && update.getMessage().hasText()) {
|
try {
|
||||||
log.info("#{} \"{}\": {}", update.getMessage().getChat().getId(), update.getMessage().getChat().getUserName(), update.getMessage().getText());
|
if (update.hasMessage() && update.getMessage().hasText()) {
|
||||||
} else if (update.hasCallbackQuery()) {
|
handleMessage(update.getMessage());
|
||||||
handleCallbackQuery(update.getCallbackQuery());
|
} else if (update.hasCallbackQuery()) {
|
||||||
|
handleCallback(update.getCallbackQuery());
|
||||||
|
}
|
||||||
|
} catch (AccessDenied e) {
|
||||||
|
log.warn("Access denied: {}", update);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleCallbackQuery(final CallbackQuery query) {
|
private void handleMessage(final Message message) throws AccessDenied {
|
||||||
final MaybeInaccessibleMessage message = query.getMessage();
|
chatService.setEnabled(message.getChatId(), !message.getText().equals("/stop"));
|
||||||
if (message.getChatId() != CHAT_ID) {
|
}
|
||||||
return;
|
|
||||||
}
|
private void handleCallback(final CallbackQuery callback) throws AccessDenied {
|
||||||
|
final MaybeInaccessibleMessage tlgMessage = callback.getMessage();
|
||||||
|
chatService.setEnabled(tlgMessage.getChatId(), true);
|
||||||
try {
|
try {
|
||||||
final InlineDto dto = objectMapper.readValue(query.getData(), InlineDto.class);
|
final InlineDto dto = objectMapper.readValue(callback.getData(), InlineDto.class);
|
||||||
switch (dto.getCommand()) {
|
switch (dto.getCommand()) {
|
||||||
case HIDE -> hide(message);
|
case HIDE -> hide(tlgMessage);
|
||||||
case REMEMBER -> remember(message, true);
|
case REMEMBER -> remember(tlgMessage, true);
|
||||||
case UNREMEMBER -> remember(message, false);
|
case UNREMEMBER -> remember(tlgMessage, false);
|
||||||
default -> updateMessage(message.getChatId(), message.getMessageId());
|
default -> update(tlgMessage);
|
||||||
}
|
}
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
log.error("Failed to read InlineDto.", e);
|
log.error("Failed to read InlineDto.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void hide(final MaybeInaccessibleMessage message) {
|
private void hide(final MaybeInaccessibleMessage tlgMessage) {
|
||||||
offerService.hideByTelegramMessageId(message.getMessageId(), true);
|
messageService.setHide(tlgMessage, true).ifPresentOrElse(this::update, () -> remove(tlgMessage));
|
||||||
_remove(message.getMessageId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void remember(final MaybeInaccessibleMessage message, final boolean remember) {
|
private void remember(final MaybeInaccessibleMessage tlgMessage, final boolean remember) {
|
||||||
offerService.rememberByTelegramMessageId(message.getMessageId(), remember).ifPresentOrElse(
|
messageService.setRemember(tlgMessage, remember).ifPresentOrElse(this::update, () -> remove(tlgMessage));
|
||||||
offer -> updateMessage(offer, message.getChatId(), message.getMessageId()),
|
|
||||||
() -> _remove(message.getMessageId())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMessage(final long chatId, final int messageId) {
|
private void update(final MaybeInaccessibleMessage tlgMessage) {
|
||||||
offerService.findByTelegramMessageId(messageId).ifPresentOrElse(
|
messageService.findDtoByTelegramMessage(tlgMessage).ifPresentOrElse(this::update, () -> remove(tlgMessage));
|
||||||
offer -> updateMessage(offer, chatId, messageId),
|
|
||||||
() -> _remove(messageId)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMessage(final OfferDto offer, final long chatId, final int messageId) {
|
private void send(final OfferDto offerDto, final ChatDto chatDto) {
|
||||||
if (offer.isHide()) {
|
chatService.findAllEnabled().forEach(chat -> {
|
||||||
|
try {
|
||||||
|
final InputFile inputFile = offerDto.getImageURL() == null ? new InputFile(new ByteArrayInputStream(NO_IMAGE), "[Kein Bild]") : new InputFile(offerDto.getImageURL());
|
||||||
|
final SendPhoto send = new SendPhoto(chat.getId() + "", inputFile);
|
||||||
|
send.setCaption(createText(offerDto));
|
||||||
|
send.setReplyMarkup(createKeyboard(false));
|
||||||
|
|
||||||
|
log.info("Sending Offer: {}", offerDto);
|
||||||
|
final Message message = bot.execute(send);
|
||||||
|
|
||||||
|
messageService.create(offerDto, chatDto, message);
|
||||||
|
} catch (TelegramApiException | JsonProcessingException e) {
|
||||||
|
log.error("Failed to send Message to #{}.", chat.getId(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update(final MessageDto messageDto) {
|
||||||
|
if (messageDto.isHide() && messageDto.getTelegramMessageId() != null) {
|
||||||
|
remove(messageDto.getChat().getId(), messageDto.getTelegramMessageId());
|
||||||
|
messageService.clearTelegramMessageId(messageDto);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final EditMessageCaption edit = new EditMessageCaption(chatId + "", messageId, null, createText(offer), createKeyboard(offer), null, null);
|
final EditMessageCaption edit = new EditMessageCaption(
|
||||||
|
messageDto.getChat().getId() + "",
|
||||||
|
messageDto.getTelegramMessageId(),
|
||||||
|
null,
|
||||||
|
createText(messageDto.getOffer()),
|
||||||
|
createKeyboard(messageDto.isRemember()),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
edit.setParseMode("Markdown");
|
edit.setParseMode("Markdown");
|
||||||
log.info("Editing Offer: {}", offer);
|
log.info("Editing Offer: {}", messageDto);
|
||||||
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: {}", offer);
|
log.info("Message has been deleted by User. Marking has hidden: {}", messageDto);
|
||||||
offerService.setHide(offer, true);
|
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")) {
|
} 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: {}", offer);
|
log.debug("Ignoring complaint from telegram-bot-api about unmodified message: {}", messageDto);
|
||||||
} else {
|
} else {
|
||||||
log.error("Failed to edit Message to #{}.", chatId, e);
|
log.error("Failed to edit Message chat={} message={}", messageDto.getChat().getId(), messageDto.getTelegramMessageId(), 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);
|
|
||||||
send.setCaption(createText(offer));
|
|
||||||
send.setReplyMarkup(createKeyboard(offer));
|
|
||||||
|
|
||||||
log.info("Sending Offer: {}", 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) {
|
private String createText(final OfferDto offer) {
|
||||||
return "%s\n%s\n%s\n%s\nv%d".formatted(
|
return "%s\n%s\n%s\n%s\n".formatted(
|
||||||
offer.getTitle().replaceAll("\\[", "(").replaceAll("]", ")"),
|
offer.getTitle().replaceAll("\\[", "(").replaceAll("]", ")"),
|
||||||
offer.combineLocation(),
|
offer.combineLocation(),
|
||||||
offer.getDescription(),
|
offer.getDescription(),
|
||||||
offer.getArticleURL(),
|
offer.getArticleURL()
|
||||||
offer.getVersion()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private InlineKeyboardMarkup createKeyboard(final OfferDto offer) throws JsonProcessingException {
|
private InlineKeyboardMarkup createKeyboard(final boolean remember) throws JsonProcessingException {
|
||||||
final InlineKeyboardMarkup markup = new InlineKeyboardMarkup();
|
final InlineKeyboardMarkup markup = new InlineKeyboardMarkup();
|
||||||
final ArrayList<List<InlineKeyboardButton>> keyboard = new ArrayList<>();
|
final ArrayList<List<InlineKeyboardButton>> keyboard = new ArrayList<>();
|
||||||
final ArrayList<InlineKeyboardButton> row = new ArrayList<>();
|
final ArrayList<InlineKeyboardButton> row = new ArrayList<>();
|
||||||
if (offer.isRemember()) {
|
if (remember) {
|
||||||
addButton(row, ICON_CHECK + ICON_CHECK + ICON_CHECK + " Gemerkt " + ICON_CHECK + ICON_CHECK + ICON_CHECK, InlineCommand.UNREMEMBER);
|
addButton(row, ICON_CHECK + ICON_CHECK + ICON_CHECK + " Gemerkt " + ICON_CHECK + ICON_CHECK + ICON_CHECK, InlineCommand.UNREMEMBER);
|
||||||
} else {
|
} else {
|
||||||
addButton(row, ICON_REMOVE + " Entfernen", InlineCommand.HIDE);
|
addButton(row, ICON_REMOVE + " Entfernen", InlineCommand.HIDE);
|
||||||
@ -201,16 +217,16 @@ public class TelegramService {
|
|||||||
row.add(new InlineKeyboardButton(caption, null, data, null, null, null, null, null, null));
|
row.add(new InlineKeyboardButton(caption, null, data, null, null, null, null, null, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void remove(final List<OfferDto> offers) {
|
private void remove(final MaybeInaccessibleMessage tlgMessage) {
|
||||||
_remove(offers.stream().map(OfferDto::getTelegramMessageId).filter(Objects::nonNull).toArray(Integer[]::new));
|
remove(tlgMessage.getChatId(), tlgMessage.getMessageId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void _remove(final Integer... messageIds) {
|
private void remove(final long chatId, final int messageId) {
|
||||||
try {
|
try {
|
||||||
log.info("Removing messages: {}", Arrays.toString(messageIds));
|
log.error("Removing Message chat={} message={}", chatId, messageId);
|
||||||
bot.execute(new DeleteMessages(CHAT_ID + "", Arrays.stream(messageIds).toList()));
|
bot.execute(new DeleteMessage(chatId + "", messageId));
|
||||||
} catch (TelegramApiException e) {
|
} catch (TelegramApiException e) {
|
||||||
log.error("Failed to remove Message #{}.", CHAT_ID, e);
|
log.error("Failed to remove Message chat={} message={}", chatId, messageId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
27
src/main/java/de/ph87/kleinanzeigen/telegram/chat/Chat.java
Normal file
27
src/main/java/de/ph87/kleinanzeigen/telegram/chat/Chat.java
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package de.ph87.kleinanzeigen.telegram.chat;
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class Chat {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
public Chat(final long id, final boolean enabled) {
|
||||||
|
this.id = id;
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package de.ph87.kleinanzeigen.telegram.chat;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "de.ph87.kleinanzeigen.telegram.chat")
|
||||||
|
public class ChatConfig {
|
||||||
|
|
||||||
|
private List<Long> whitelist = new ArrayList<>();
|
||||||
|
|
||||||
|
public boolean isOnWhitelist(final long id) {
|
||||||
|
return whitelist.contains(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package de.ph87.kleinanzeigen.telegram.chat.ChatDto;
|
||||||
|
|
||||||
|
import de.ph87.kleinanzeigen.telegram.chat.Chat;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ChatDto {
|
||||||
|
|
||||||
|
private final long id;
|
||||||
|
|
||||||
|
private final boolean enabled;
|
||||||
|
|
||||||
|
public ChatDto(final Chat chat) {
|
||||||
|
this.id = chat.getId();
|
||||||
|
this.enabled = chat.isEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package de.ph87.kleinanzeigen.telegram.chat;
|
||||||
|
|
||||||
|
import org.springframework.data.repository.ListCrudRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ChatRepository extends ListCrudRepository<Chat, Long> {
|
||||||
|
|
||||||
|
List<Chat> findAllByEnabledTrue();
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
package de.ph87.kleinanzeigen.telegram.chat;
|
||||||
|
|
||||||
|
import de.ph87.kleinanzeigen.telegram.AccessDenied;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.chat.ChatDto.ChatDto;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ChatService {
|
||||||
|
|
||||||
|
private final ChatRepository chatRepository;
|
||||||
|
|
||||||
|
private final ChatConfig config;
|
||||||
|
|
||||||
|
public List<ChatDto> findAllEnabled() {
|
||||||
|
return chatRepository.findAllByEnabledTrue().stream().filter(chat -> config.isOnWhitelist(chat.getId())).map(this::toDto).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(final long id, final boolean enabled) throws AccessDenied {
|
||||||
|
if (!config.isOnWhitelist(id)) {
|
||||||
|
throw new AccessDenied();
|
||||||
|
}
|
||||||
|
chatRepository
|
||||||
|
.findById(id)
|
||||||
|
.stream()
|
||||||
|
.peek(chat -> {
|
||||||
|
if (chat.isEnabled() != enabled) {
|
||||||
|
chat.setEnabled(enabled);
|
||||||
|
log.info("Chat {}: {}", enabled ? "ENABLED" : "DISABLED", chat);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.findFirst()
|
||||||
|
.orElseGet(() -> {
|
||||||
|
final Chat chat = chatRepository.save(new Chat(id, enabled));
|
||||||
|
log.info("Chat created: {}", chat);
|
||||||
|
return chat;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Chat getByDto(final ChatDto chatDto) {
|
||||||
|
return chatRepository.findById(chatDto.getId()).orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChatDto toDto(final Chat chat) {
|
||||||
|
return new ChatDto(chat);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
package de.ph87.kleinanzeigen.telegram.chat.message;
|
||||||
|
|
||||||
|
import de.ph87.kleinanzeigen.kleinanzeigen.offer.Offer;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.chat.Chat;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class Message {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@ManyToOne(optional = false)
|
||||||
|
private Chat chat;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@ManyToOne(optional = false)
|
||||||
|
private Offer offer;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column
|
||||||
|
@Nullable
|
||||||
|
private Integer telegramMessageId;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private ZonedDateTime expiry = ZonedDateTime.now().plusDays(3);
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column
|
||||||
|
private boolean hide = false;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private boolean remember = false;
|
||||||
|
|
||||||
|
public Message(@NonNull final Offer offer, @NonNull final Chat chat, final org.telegram.telegrambots.meta.api.objects.Message message) {
|
||||||
|
this.chat = chat;
|
||||||
|
this.offer = offer;
|
||||||
|
this.telegramMessageId = message.getMessageId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRemember(final boolean newRemember) {
|
||||||
|
if (remember && !newRemember) {
|
||||||
|
final ZonedDateTime oneHour = ZonedDateTime.now().plusHours(1);
|
||||||
|
if (oneHour.isAfter(expiry)) {
|
||||||
|
expiry = oneHour;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remember = newRemember;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package de.ph87.kleinanzeigen.telegram.chat.message;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class MessageDeleted {
|
||||||
|
|
||||||
|
private final long chatId;
|
||||||
|
|
||||||
|
private final int messageId;
|
||||||
|
|
||||||
|
public MessageDeleted(final long chatId, final int messageId) {
|
||||||
|
this.chatId = chatId;
|
||||||
|
this.messageId = messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
package de.ph87.kleinanzeigen.telegram.chat.message;
|
||||||
|
|
||||||
|
import de.ph87.kleinanzeigen.kleinanzeigen.offer.OfferDto;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.chat.ChatDto.ChatDto;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class MessageDto {
|
||||||
|
|
||||||
|
private final long id;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final ChatDto chat;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final OfferDto offer;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final Integer telegramMessageId;
|
||||||
|
|
||||||
|
private final ZonedDateTime expiry;
|
||||||
|
|
||||||
|
private final boolean hide;
|
||||||
|
|
||||||
|
private final boolean remember;
|
||||||
|
|
||||||
|
public MessageDto(final Message message, @NonNull final ChatDto chatDto, final @NonNull OfferDto offerDto) {
|
||||||
|
id = message.getId();
|
||||||
|
chat = chatDto;
|
||||||
|
offer = offerDto;
|
||||||
|
telegramMessageId = message.getTelegramMessageId();
|
||||||
|
expiry = message.getExpiry();
|
||||||
|
hide = message.isHide();
|
||||||
|
remember = message.isRemember();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package de.ph87.kleinanzeigen.telegram.chat.message;
|
||||||
|
|
||||||
|
import org.springframework.data.repository.ListCrudRepository;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface MessageRepository extends ListCrudRepository<Message, Long> {
|
||||||
|
|
||||||
|
Optional<Message> findByChat_IdAndTelegramMessageId(long chatId, int messageId);
|
||||||
|
|
||||||
|
List<Message> findAllByOffer_id(long id);
|
||||||
|
|
||||||
|
List<Message> findAllByExpiryBefore(ZonedDateTime deadline);
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
package de.ph87.kleinanzeigen.telegram.chat.message;
|
||||||
|
|
||||||
|
import de.ph87.kleinanzeigen.kleinanzeigen.offer.Offer;
|
||||||
|
import de.ph87.kleinanzeigen.kleinanzeigen.offer.OfferDto;
|
||||||
|
import de.ph87.kleinanzeigen.kleinanzeigen.offer.OfferRepository;
|
||||||
|
import de.ph87.kleinanzeigen.kleinanzeigen.offer.OfferService;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.chat.Chat;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.chat.ChatDto.ChatDto;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.chat.ChatService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
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;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
@EnableScheduling
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MessageService {
|
||||||
|
|
||||||
|
private final MessageRepository messageRepository;
|
||||||
|
|
||||||
|
private final OfferRepository offerRepository;
|
||||||
|
|
||||||
|
private final OfferService offerService;
|
||||||
|
|
||||||
|
private final ChatService chatService;
|
||||||
|
|
||||||
|
private final ApplicationEventPublisher publisher;
|
||||||
|
|
||||||
|
@Scheduled(initialDelay = 1, fixedRate = 1, timeUnit = TimeUnit.HOURS)
|
||||||
|
public void cleanUp() {
|
||||||
|
for (final Message message : messageRepository.findAllByExpiryBefore(ZonedDateTime.now())) {
|
||||||
|
message.getOffer().getMessages().remove(message);
|
||||||
|
messageRepository.delete(message);
|
||||||
|
if (message.getOffer().getMessages().isEmpty()) {
|
||||||
|
offerRepository.delete(message.getOffer());
|
||||||
|
}
|
||||||
|
if (message.getTelegramMessageId() != null) {
|
||||||
|
publisher.publishEvent(new MessageDeleted(message.getChat().getId(), message.getTelegramMessageId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void create(final OfferDto offerDto, final ChatDto chatDto, final org.telegram.telegrambots.meta.api.objects.Message message) {
|
||||||
|
final Offer offer = offerService.getByDto(offerDto);
|
||||||
|
final Chat chat = chatService.getByDto(chatDto);
|
||||||
|
messageRepository.save(new Message(offer, chat, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<MessageDto> setRemember(final MaybeInaccessibleMessage tlgMessage, final boolean remember) {
|
||||||
|
return findByTelegramMessage(tlgMessage).stream().peek(offer -> offer.setRemember(remember)).findFirst().map(this::toDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<MessageDto> findDtoByTelegramMessage(final MaybeInaccessibleMessage tlgMessage) {
|
||||||
|
return findByTelegramMessage(tlgMessage).map(this::toDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearTelegramMessageId(final MessageDto dto) {
|
||||||
|
findByDto(dto).ifPresent(message -> message.setTelegramMessageId(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHide(final MessageDto dto, final boolean hide) {
|
||||||
|
findByDto(dto).ifPresent(offer -> offer.setHide(hide));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Message> findByTelegramMessage(final MaybeInaccessibleMessage tlgMessage) {
|
||||||
|
return messageRepository.findByChat_IdAndTelegramMessageId(tlgMessage.getChatId(), tlgMessage.getMessageId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Message> findByDto(final MessageDto dto) {
|
||||||
|
return messageRepository.findById(dto.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<MessageDto> findAllDtoByOfferDto(final OfferDto offer) {
|
||||||
|
return messageRepository.findAllByOffer_id(offer.getId()).stream().map(this::toDto).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private MessageDto toDto(final Message message) {
|
||||||
|
final ChatDto chatDto = chatService.toDto(message.getChat());
|
||||||
|
final OfferDto offerDto = offerService.toDto(message.getOffer());
|
||||||
|
return new MessageDto(message, chatDto, offerDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user