Compare commits
10 Commits
038b0b76c9
...
b3236bb931
| Author | SHA1 | Date | |
|---|---|---|---|
| b3236bb931 | |||
| 0e7c94f39e | |||
| c6fb375995 | |||
| 36324f2723 | |||
| 08cc0e1474 | |||
| 13fa1939ad | |||
| fed7cd420a | |||
| d7a635649a | |||
| 25804e0680 | |||
| 1bcecaac51 |
1
pom.xml
1
pom.xml
@ -11,6 +11,7 @@
|
|||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
<maven.compiler.target>21</maven.compiler.target>
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<maven.compiler.release>21</maven.compiler.release>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
|||||||
5
src/main/java/de/ph87/kleinanzeigen/crud/CrudAction.java
Normal file
5
src/main/java/de/ph87/kleinanzeigen/crud/CrudAction.java
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package de.ph87.kleinanzeigen.crud;
|
||||||
|
|
||||||
|
public enum CrudAction {
|
||||||
|
CREATED, CHANGED, REMOVED
|
||||||
|
}
|
||||||
75
src/main/java/de/ph87/kleinanzeigen/crud/CrudFilter.java
Normal file
75
src/main/java/de/ph87/kleinanzeigen/crud/CrudFilter.java
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package de.ph87.kleinanzeigen.crud;
|
||||||
|
|
||||||
|
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||||
|
import jakarta.persistence.criteria.CriteriaQuery;
|
||||||
|
import jakarta.persistence.criteria.Predicate;
|
||||||
|
import jakarta.persistence.criteria.Root;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public class CrudFilter<T> {
|
||||||
|
|
||||||
|
protected final int page;
|
||||||
|
|
||||||
|
protected final int size;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
protected final List<Order> orders;
|
||||||
|
|
||||||
|
public CrudFilter(final int page, final int size, @NonNull final List<Order> orders) {
|
||||||
|
this.page = page;
|
||||||
|
this.size = size;
|
||||||
|
this.orders = orders;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public Pageable getPageable() {
|
||||||
|
return PageRequest.of(page, size, getSort());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Sort getSort() {
|
||||||
|
return Sort.by(orders.stream().map(Order::toOrder).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public Specification<T> getSpecification() {
|
||||||
|
return (root, query, criteriaBuilder) -> {
|
||||||
|
final List<Predicate> predicates = getPredicates(root, query, criteriaBuilder);
|
||||||
|
return criteriaBuilder.and(predicates.toArray(Predicate[]::new));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
protected List<Predicate> getPredicates(@NonNull final Root<T> root, final CriteriaQuery<?> query, @NonNull final CriteriaBuilder criteriaBuilder) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Order {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final String property;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final Sort.Direction direction;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public Sort.Order toOrder() {
|
||||||
|
return new Sort.Order(direction, property);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
package de.ph87.kleinanzeigen.crud;
|
||||||
|
|
||||||
|
import jakarta.persistence.criteria.*;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public abstract class CrudFilterSearch<T> extends CrudFilter<T> {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
protected final String search;
|
||||||
|
|
||||||
|
protected CrudFilterSearch(final int page, final int size, @NonNull final List<Order> orders, @NonNull final String search) {
|
||||||
|
super(page, size, orders);
|
||||||
|
this.search = search;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
protected List<Predicate> getPredicates(@NonNull final Root<T> root, final CriteriaQuery<?> query, @NonNull final CriteriaBuilder criteriaBuilder) {
|
||||||
|
return new ArrayList<>(search(root, criteriaBuilder));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public List<Predicate> search(@NonNull final Root<T> root, @NonNull final CriteriaBuilder criteriaBuilder) {
|
||||||
|
final String[] words = search.replaceAll("([0-9])([a-zA-Z])", "$1 $2")
|
||||||
|
.replaceAll("([a-zA-Z])([0-9])", "$1 $2")
|
||||||
|
.replaceAll("([a-z])([A-Z])", "$1 $2")
|
||||||
|
.replaceAll("^\\W+|\\W+$", "")
|
||||||
|
.toLowerCase(Locale.ROOT)
|
||||||
|
.split("\\W+");
|
||||||
|
final List<Path<String>> fields = getFields(root);
|
||||||
|
return predicateAllWordsEachInAnyField(criteriaBuilder, words, fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private List<Predicate> predicateAllWordsEachInAnyField(@NonNull final CriteriaBuilder criteriaBuilder, @NonNull final String[] words, @NonNull final List<Path<String>> fields) {
|
||||||
|
return Arrays.stream(words).map(word -> predicateWordInAnyField(criteriaBuilder, word, fields)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Predicate predicateWordInAnyField(@NonNull final CriteriaBuilder criteriaBuilder, @NonNull final String word, @NonNull final List<Path<String>> fields) {
|
||||||
|
return fields.stream().map(field -> criteriaBuilder.like(field, "%" + word + "%")).reduce(criteriaBuilder::or).orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
protected abstract List<Path<String>> getFields(@NonNull final Root<T> root);
|
||||||
|
|
||||||
|
}
|
||||||
@ -4,22 +4,20 @@ import jakarta.persistence.Column;
|
|||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.GeneratedValue;
|
import jakarta.persistence.GeneratedValue;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
import lombok.Getter;
|
import lombok.*;
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.NonNull;
|
|
||||||
import lombok.ToString;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.regex.Pattern;
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Getter
|
@Getter
|
||||||
|
@Setter
|
||||||
@ToString
|
@ToString
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class Blacklist {
|
public class Blacklist {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue
|
@GeneratedValue
|
||||||
|
@Setter(AccessLevel.NONE)
|
||||||
private long id;
|
private long id;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
@ -27,26 +25,15 @@ public class Blacklist {
|
|||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private String query;
|
private String regex;
|
||||||
|
|
||||||
public Blacklist(final BlacklistCreate create) {
|
public Blacklist(final BlacklistCreate create) {
|
||||||
enabled = create.isEnabled();
|
enabled = create.isEnabled();
|
||||||
query = create.getQuery();
|
regex = create.getQuery();
|
||||||
}
|
|
||||||
|
|
||||||
public void edit(final BlacklistDto edit) {
|
|
||||||
enabled = edit.isEnabled();
|
|
||||||
query = edit.getQuery();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean matches(@NonNull final String title) {
|
public boolean matches(@NonNull final String title) {
|
||||||
final String[] words = query.replaceAll("([0-9])([a-zA-Z])", "$1 $2")
|
return Pattern.compile(regex, Pattern.CASE_INSENSITIVE).matcher(title).find();
|
||||||
.replaceAll("([a-zA-Z])([0-9])", "$1 $2")
|
|
||||||
.replaceAll("([a-z])([A-Z])", "$1 $2")
|
|
||||||
.replaceAll("^\\W+|\\W+$", "")
|
|
||||||
.toLowerCase(Locale.ROOT)
|
|
||||||
.split("\\W+");
|
|
||||||
return Arrays.stream(words).map(".*%s.*"::formatted).allMatch(title::matches);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
package de.ph87.kleinanzeigen.kleinanzeigen.blacklist;
|
package de.ph87.kleinanzeigen.kleinanzeigen.blacklist;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import java.util.List;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@ -13,14 +13,29 @@ public class BlacklistController {
|
|||||||
|
|
||||||
private final BlacklistService blacklistService;
|
private final BlacklistService blacklistService;
|
||||||
|
|
||||||
|
@PostMapping("all")
|
||||||
|
public List<BlacklistDto> all() {
|
||||||
|
return blacklistService.all();
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("create")
|
@PostMapping("create")
|
||||||
public BlacklistDto create(@RequestBody BlacklistCreate create) {
|
public BlacklistDto create(@RequestBody BlacklistCreate create) {
|
||||||
return blacklistService.create(create);
|
return blacklistService.create(create);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("edit")
|
@PostMapping("{id}/delete")
|
||||||
public BlacklistDto edit(@RequestBody BlacklistDto edit) {
|
public BlacklistDto delete(@PathVariable final long id) {
|
||||||
return blacklistService.edit(edit);
|
return blacklistService.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/enabled")
|
||||||
|
public BlacklistDto enabled(@PathVariable final long id, @RequestBody final boolean enabled) {
|
||||||
|
return blacklistService.enabled(id, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/query")
|
||||||
|
public BlacklistDto query(@PathVariable final long id, @NonNull @RequestBody final String query) {
|
||||||
|
return blacklistService.query(id, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ public class BlacklistDto {
|
|||||||
public BlacklistDto(final Blacklist blacklist) {
|
public BlacklistDto(final Blacklist blacklist) {
|
||||||
this.id = blacklist.getId();
|
this.id = blacklist.getId();
|
||||||
this.enabled = blacklist.isEnabled();
|
this.enabled = blacklist.isEnabled();
|
||||||
this.query = blacklist.getQuery();
|
this.query = blacklist.getRegex();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
package de.ph87.kleinanzeigen.kleinanzeigen.blacklist;
|
package de.ph87.kleinanzeigen.kleinanzeigen.blacklist;
|
||||||
|
|
||||||
|
import de.ph87.kleinanzeigen.crud.CrudAction;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@ -18,26 +21,51 @@ public class BlacklistService {
|
|||||||
|
|
||||||
private final BlacklistRepository blacklistRepository;
|
private final BlacklistRepository blacklistRepository;
|
||||||
|
|
||||||
private BlacklistDto toDto(Blacklist blacklist) {
|
|
||||||
return new BlacklistDto(blacklist);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BlacklistDto create(final BlacklistCreate create) {
|
|
||||||
final BlacklistDto dto = toDto(blacklistRepository.save(new Blacklist(create)));
|
|
||||||
log.info("Blacklist CREATED: {}", dto);
|
|
||||||
return dto;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BlacklistDto edit(final BlacklistDto edit) {
|
|
||||||
final Blacklist blacklist = blacklistRepository.findById(edit.getId()).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
|
|
||||||
blacklist.edit(edit);
|
|
||||||
final BlacklistDto dto = toDto(blacklist);
|
|
||||||
log.info("Blacklist EDITED: {}", dto);
|
|
||||||
return dto;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<BlacklistDto> findAllBlacklisted(@NonNull final String title) {
|
public List<BlacklistDto> findAllBlacklisted(@NonNull final String title) {
|
||||||
return blacklistRepository.findAllByEnabledTrue().stream().filter(blacklist -> blacklist.matches(title)).map(BlacklistDto::new).toList();
|
return blacklistRepository.findAllByEnabledTrue().stream().filter(blacklist -> blacklist.matches(title)).map(BlacklistDto::new).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<BlacklistDto> all() {
|
||||||
|
return blacklistRepository.findAll().stream().map(BlacklistDto::new).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlacklistDto create(final BlacklistCreate create) {
|
||||||
|
final Blacklist blacklist = blacklistRepository.save(new Blacklist(create));
|
||||||
|
return publish(blacklist, CrudAction.CREATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public BlacklistDto delete(final long id) {
|
||||||
|
final Blacklist blacklist = getById(id);
|
||||||
|
return publish(blacklist, CrudAction.REMOVED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/enabled")
|
||||||
|
public BlacklistDto enabled(@PathVariable final long id, @RequestBody final boolean enabled) {
|
||||||
|
return set(id, blacklist -> blacklist.setEnabled(enabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/query")
|
||||||
|
public BlacklistDto query(@PathVariable final long id, @NonNull @RequestBody final String query) {
|
||||||
|
return set(id, blacklist -> blacklist.setRegex(query));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public BlacklistDto set(final long id, @NonNull final Consumer<Blacklist> setter) {
|
||||||
|
final Blacklist blacklist = getById(id);
|
||||||
|
setter.accept(blacklist);
|
||||||
|
return publish(blacklist, CrudAction.CHANGED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Blacklist getById(final long id) {
|
||||||
|
return blacklistRepository.findById(id).orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private BlacklistDto publish(@NonNull final Blacklist blacklist, @NonNull final CrudAction action) {
|
||||||
|
final BlacklistDto dto = new BlacklistDto(blacklist);
|
||||||
|
log.info("Blacklist {}: {}", action, dto);
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
package de.ph87.kleinanzeigen.kleinanzeigen.offer;
|
||||||
|
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@CrossOrigin
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("Offer")
|
||||||
|
public class OfferController {
|
||||||
|
|
||||||
|
private final OfferService offerService;
|
||||||
|
|
||||||
|
@GetMapping("filter")
|
||||||
|
public Page<OfferDto> getAllOffers(@Nullable @RequestBody(required = false) final OfferFilter filter) {
|
||||||
|
if (filter == null) {
|
||||||
|
return offerService.filter(OfferFilter.DEFAULT());
|
||||||
|
}
|
||||||
|
return offerService.filter(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -65,7 +65,7 @@ public class OfferCreate {
|
|||||||
articleDate = parseDate(article.select(".aditem-main--top--right").text());
|
articleDate = parseDate(article.select(".aditem-main--top--right").text());
|
||||||
articleURL = uri.resolve(article.select(".aditem-image a").attr("href")).toString();
|
articleURL = uri.resolve(article.select(".aditem-image a").attr("href")).toString();
|
||||||
final String locationString = article.select(".aditem-main--top--left").text();
|
final String locationString = article.select(".aditem-main--top--left").text();
|
||||||
final Matcher locationMatcher = Pattern.compile("^(?<zipcode>\\d+) (?<location>.+) \\((:?ca.)?\\s*(?<distance>\\d+(?:[,.]\\d+)?)\\s*km\\s*\\)$").matcher(locationString);
|
final Matcher locationMatcher = Pattern.compile("^(?<zipcode>\\d+) (?<location>.+) \\((:?ca.)?\\s*(?<distance>\\d+(?:[,.]\\d+)?)\\s*(?:km)?\\s*\\)$").matcher(locationString);
|
||||||
if (!locationMatcher.find()) {
|
if (!locationMatcher.find()) {
|
||||||
throw new LocationNotFound();
|
throw new LocationNotFound();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
package de.ph87.kleinanzeigen.kleinanzeigen.offer;
|
||||||
|
|
||||||
|
import de.ph87.kleinanzeigen.crud.CrudFilterSearch;
|
||||||
|
import jakarta.persistence.criteria.Path;
|
||||||
|
import jakarta.persistence.criteria.Root;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
public class OfferFilter extends CrudFilterSearch<Offer> {
|
||||||
|
|
||||||
|
public OfferFilter(final int page, final int size, @NonNull final List<Order> orders, @NonNull final String search) {
|
||||||
|
super(page, size, orders, search);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected @NonNull List<Path<String>> getFields(final @NonNull Root<Offer> root) {
|
||||||
|
return List.of(root.get("title"), root.get("description"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OfferFilter DEFAULT() {
|
||||||
|
return new OfferFilter(0, 30, List.of(new Order("articleDate", Sort.Direction.DESC)), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,10 +1,11 @@
|
|||||||
package de.ph87.kleinanzeigen.kleinanzeigen.offer;
|
package de.ph87.kleinanzeigen.kleinanzeigen.offer;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
import org.springframework.data.repository.ListCrudRepository;
|
import org.springframework.data.repository.ListCrudRepository;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface OfferRepository extends ListCrudRepository<Offer, Long> {
|
public interface OfferRepository extends ListCrudRepository<Offer, Long>, JpaSpecificationExecutor<Offer> {
|
||||||
|
|
||||||
Optional<Offer> findByArticleId(String articleId);
|
Optional<Offer> findByArticleId(String articleId);
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ 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;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@ -18,19 +19,16 @@ public class OfferService {
|
|||||||
private final ApplicationEventPublisher publisher;
|
private final ApplicationEventPublisher publisher;
|
||||||
|
|
||||||
public void updateOrCreate(@NonNull final OfferCreate create, final boolean resendOnPriceChange) {
|
public void updateOrCreate(@NonNull final OfferCreate create, final boolean resendOnPriceChange) {
|
||||||
offerRepository.findByArticleId(create.getArticleId()).ifPresentOrElse(
|
offerRepository.findByArticleId(create.getArticleId()).ifPresentOrElse(existing -> {
|
||||||
existing -> {
|
|
||||||
if (existing.update(create, resendOnPriceChange)) {
|
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, resendOnPriceChange));
|
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);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public OfferDto toDto(final Offer offer) {
|
public OfferDto toDto(final Offer offer) {
|
||||||
@ -41,4 +39,9 @@ public class OfferService {
|
|||||||
return offerRepository.findById(offerDto.getId()).orElseThrow();
|
return offerRepository.findById(offerDto.getId()).orElseThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public Page<OfferDto> filter(@NonNull final OfferFilter filter) {
|
||||||
|
return offerRepository.findAll(filter.getSpecification(), filter.getPageable()).map(OfferDto::new);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,19 +5,18 @@ import jakarta.persistence.Column;
|
|||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.GeneratedValue;
|
import jakarta.persistence.GeneratedValue;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
import lombok.Getter;
|
import lombok.*;
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.NonNull;
|
|
||||||
import lombok.ToString;
|
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Getter
|
@Getter
|
||||||
|
@Setter
|
||||||
@ToString
|
@ToString
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class Search {
|
public class Search {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue
|
@GeneratedValue
|
||||||
|
@Setter(AccessLevel.NONE)
|
||||||
private long id;
|
private long id;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
@ -49,12 +48,4 @@ public class Search {
|
|||||||
priceMax = create.getPriceMax();
|
priceMax = create.getPriceMax();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void edit(final SearchDto edit) {
|
|
||||||
enabled = edit.isEnabled();
|
|
||||||
query = edit.getQuery();
|
|
||||||
radius = edit.getRadius();
|
|
||||||
priceMin = edit.getPriceMin();
|
|
||||||
priceMax = edit.getPriceMax();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
package de.ph87.kleinanzeigen.kleinanzeigen.search;
|
package de.ph87.kleinanzeigen.kleinanzeigen.search;
|
||||||
|
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.NonNull;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import java.util.List;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@ -13,14 +14,49 @@ public class SearchController {
|
|||||||
|
|
||||||
private final SearchService searchService;
|
private final SearchService searchService;
|
||||||
|
|
||||||
|
@PostMapping("all")
|
||||||
|
public List<SearchDto> all() {
|
||||||
|
return searchService.all();
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("create")
|
@PostMapping("create")
|
||||||
public SearchDto create(@RequestBody SearchCreate create) {
|
public SearchDto create(@RequestBody SearchCreate create) {
|
||||||
return searchService.create(create);
|
return searchService.create(create);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("edit")
|
@PostMapping("{id}/delete")
|
||||||
public SearchDto edit(@RequestBody SearchDto edit) {
|
public SearchDto delete(@PathVariable final long id) {
|
||||||
return searchService.edit(edit);
|
return searchService.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/enabled")
|
||||||
|
public SearchDto enabled(@PathVariable final long id, @RequestBody final boolean enabled) {
|
||||||
|
return searchService.enabled(id, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/query")
|
||||||
|
public SearchDto query(@PathVariable final long id, @NonNull @RequestBody final String query) {
|
||||||
|
return searchService.query(id, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/radius")
|
||||||
|
public SearchDto radius(@PathVariable final long id, @RequestBody final int radius) {
|
||||||
|
return searchService.radius(id, radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/priceMin")
|
||||||
|
public SearchDto priceMin(@PathVariable final long id, @Nullable @RequestBody(required = false) final Integer priceMin) {
|
||||||
|
return searchService.priceMin(id, priceMin);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/priceMax")
|
||||||
|
public SearchDto priceMax(@PathVariable final long id, @Nullable @RequestBody(required = false) final Integer priceMax) {
|
||||||
|
return searchService.priceMax(id, priceMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/resendOnPriceChange")
|
||||||
|
public SearchDto resendOnPriceChange(@PathVariable final long id, @RequestBody final boolean resendOnPriceChange) {
|
||||||
|
return searchService.resendOnPriceChange(id, resendOnPriceChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,18 @@
|
|||||||
package de.ph87.kleinanzeigen.kleinanzeigen.search;
|
package de.ph87.kleinanzeigen.kleinanzeigen.search;
|
||||||
|
|
||||||
|
import de.ph87.kleinanzeigen.crud.CrudAction;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.NonNull;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@ -18,24 +23,69 @@ public class SearchService {
|
|||||||
private final SearchRepository searchRepository;
|
private final SearchRepository searchRepository;
|
||||||
|
|
||||||
public List<SearchDto> findAllEnabledDto() {
|
public List<SearchDto> findAllEnabledDto() {
|
||||||
return searchRepository.findAllByEnabledTrue().stream().map(this::toDto).toList();
|
return searchRepository.findAllByEnabledTrue().stream().map(SearchDto::new).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private SearchDto toDto(Search search) {
|
public List<SearchDto> all() {
|
||||||
return new SearchDto(search);
|
return searchRepository.findAll().stream().map(SearchDto::new).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public SearchDto create(final SearchCreate create) {
|
public SearchDto create(final SearchCreate create) {
|
||||||
final SearchDto dto = toDto(searchRepository.save(new Search(create)));
|
final Search search = searchRepository.save(new Search(create));
|
||||||
log.info("Search CREATED: {}", dto);
|
return publish(search, CrudAction.CREATED);
|
||||||
return dto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public SearchDto edit(final SearchDto edit) {
|
@NonNull
|
||||||
final Search search = searchRepository.findById(edit.getId()).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
|
public SearchDto delete(final long id) {
|
||||||
search.edit(edit);
|
final Search search = getById(id);
|
||||||
final SearchDto dto = toDto(search);
|
return publish(search, CrudAction.REMOVED);
|
||||||
log.info("Search EDITED: {}", dto);
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/enabled")
|
||||||
|
public SearchDto enabled(@PathVariable final long id, @RequestBody final boolean enabled) {
|
||||||
|
return set(id, search -> search.setEnabled(enabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/query")
|
||||||
|
public SearchDto query(@PathVariable final long id, @NonNull @RequestBody final String query) {
|
||||||
|
return set(id, search -> search.setQuery(query));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/radius")
|
||||||
|
public SearchDto radius(@PathVariable final long id, @RequestBody final int radius) {
|
||||||
|
return set(id, search -> search.setRadius(radius));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/priceMin")
|
||||||
|
public SearchDto priceMin(@PathVariable final long id, @Nullable @RequestBody(required = false) final Integer priceMin) {
|
||||||
|
return set(id, search -> search.setPriceMin(priceMin));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/priceMax")
|
||||||
|
public SearchDto priceMax(@PathVariable final long id, @Nullable @RequestBody(required = false) final Integer priceMax) {
|
||||||
|
return set(id, search -> search.setPriceMax(priceMax));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/resendOnPriceChange")
|
||||||
|
public SearchDto resendOnPriceChange(@PathVariable final long id, @RequestBody final boolean resendOnPriceChange) {
|
||||||
|
return set(id, search -> search.setResendOnPriceChange(resendOnPriceChange));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public SearchDto set(final long id, @NonNull final Consumer<Search> setter) {
|
||||||
|
final Search search = getById(id);
|
||||||
|
setter.accept(search);
|
||||||
|
return publish(search, CrudAction.CHANGED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Search getById(final long id) {
|
||||||
|
return searchRepository.findById(id).orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SearchDto publish(@NonNull final Search search, @NonNull final CrudAction action) {
|
||||||
|
final SearchDto dto = new SearchDto(search);
|
||||||
|
log.info("Search {}: {}", action, dto);
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
package de.ph87.kleinanzeigen.telegram;
|
|
||||||
|
|
||||||
public class AccessDenied extends Exception {
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
package de.ph87.kleinanzeigen.telegram;
|
package de.ph87.kleinanzeigen.telegram;
|
||||||
|
|
||||||
public enum InlineCommand {
|
public enum InlineCommand {
|
||||||
HIDE, REMEMBER, UNREMEMBER
|
HIDE, REMEMBER, UNREMEMBER, UNDO
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,20 +5,27 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import de.ph87.kleinanzeigen.kleinanzeigen.offer.OfferDto;
|
import de.ph87.kleinanzeigen.kleinanzeigen.offer.OfferDto;
|
||||||
import de.ph87.kleinanzeigen.telegram.chat.ChatDto;
|
import de.ph87.kleinanzeigen.telegram.chat.ChatDto;
|
||||||
import de.ph87.kleinanzeigen.telegram.chat.ChatService;
|
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.MessageDto;
|
||||||
import de.ph87.kleinanzeigen.telegram.chat.message.MessageService;
|
import de.ph87.kleinanzeigen.telegram.chat.message.MessageService;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.request.ChatRequestEnable;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.request.ChatRequestHelp;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.request.ChatRequestUndo;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.request.MessageRequestHide;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.request.MessageRequestRemember;
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import jakarta.annotation.PreDestroy;
|
import jakarta.annotation.PreDestroy;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
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.scheduling.annotation.Async;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.event.TransactionalEventListener;
|
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.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.InputFile;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.Message;
|
||||||
import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup;
|
import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup;
|
||||||
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton;
|
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton;
|
||||||
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
|
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
|
||||||
@ -33,37 +40,39 @@ import java.util.List;
|
|||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
|
@EnableAsync
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class TelegramService {
|
public class TelegramAdapter {
|
||||||
|
|
||||||
|
private static final String ICON_UNDO = "⎌";
|
||||||
|
|
||||||
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 TelegramConfig config;
|
|
||||||
|
|
||||||
private final ChatService chatService;
|
private final ChatService chatService;
|
||||||
|
|
||||||
private final MessageService messageService;
|
private final MessageService messageService;
|
||||||
|
|
||||||
|
private final TelegramBot bot;
|
||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
private byte[] NO_IMAGE = null;
|
private byte[] NO_IMAGE = null;
|
||||||
|
|
||||||
private TelegramBot bot = null;
|
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void postConstruct() throws IOException {
|
public void postConstruct() throws IOException {
|
||||||
final BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
|
final BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
|
||||||
final ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
final ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||||
ImageIO.write(img, "PNG", stream);
|
ImageIO.write(img, "PNG", stream);
|
||||||
NO_IMAGE = stream.toByteArray();
|
NO_IMAGE = stream.toByteArray();
|
||||||
|
|
||||||
try {
|
|
||||||
bot = new TelegramBot(config.getToken(), config.getUsername(), this::onUpdateReceived);
|
|
||||||
} catch (TelegramApiException | IOException e) {
|
|
||||||
log.error("Failed to start TelegramBot: {}", e.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void stop() {
|
||||||
|
log.info("Stopping Telegram bot...");
|
||||||
|
bot.stop();
|
||||||
|
log.info("Telegram bot stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
@TransactionalEventListener(OfferDto.class)
|
@TransactionalEventListener(OfferDto.class)
|
||||||
@ -75,72 +84,72 @@ public class TelegramService {
|
|||||||
.stream()
|
.stream()
|
||||||
.filter(m -> m.getChat().getId() == chat.getId())
|
.filter(m -> m.getChat().getId() == chat.getId())
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.ifPresentOrElse(message -> update(message, false), () -> send(offer, chat));
|
.ifPresentOrElse(this::update, () -> send(offer, chat));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TransactionalEventListener(MessageDeleted.class)
|
@Async
|
||||||
public void onMessageDeleted(final MessageDeleted messageDeleted) {
|
@EventListener(ChatRequestEnable.class)
|
||||||
remove(messageDeleted.getChatId(), messageDeleted.getMessageId());
|
public void onChatEnable(@NonNull final ChatRequestEnable request) {
|
||||||
|
chatService.setEnabled(request, request.isEnable(), request.getUsername());
|
||||||
|
messageService.undo(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreDestroy
|
@Async
|
||||||
public void stop() {
|
@EventListener(ChatRequestUndo.class)
|
||||||
log.info("Stopping Telegram bot...");
|
public void onChatUndo(@NonNull final ChatRequestUndo request) {
|
||||||
bot.stop();
|
messageService.undo(request);
|
||||||
log.info("Telegram bot stopped");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onUpdateReceived(@NonNull final Update update) {
|
@Async
|
||||||
try {
|
@EventListener(ChatRequestHelp.class)
|
||||||
if (update.hasMessage() && update.getMessage().hasText()) {
|
public void onChatHelp(@NonNull final ChatRequestHelp request) {
|
||||||
handleMessage(update.getMessage());
|
chatService.help(request);
|
||||||
} else if (update.hasCallbackQuery()) {
|
|
||||||
handleCallback(update.getCallbackQuery());
|
|
||||||
}
|
|
||||||
} catch (AccessDenied e) {
|
|
||||||
log.warn("Access denied: {}", update);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleMessage(@NonNull final Message tlgMessage) throws AccessDenied {
|
@Async
|
||||||
switch (tlgMessage.getText()) {
|
@EventListener(MessageRequestHide.class)
|
||||||
case "/stop" -> chatService.setEnabled(tlgMessage.getChatId(), false, tlgMessage.getFrom().getUserName());
|
public void onMessageHide(@NonNull final MessageRequestHide request) {
|
||||||
case "undo" -> undo(tlgMessage);
|
messageService.setHide(request);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void undo(@NonNull final Message tlgMessage) {
|
@Async
|
||||||
messageService.undo(tlgMessage.getChatId()).ifPresent(message -> update(message, true));
|
@EventListener(MessageRequestRemember.class)
|
||||||
remove(tlgMessage);
|
public void onMessageRemember(@NonNull final MessageRequestRemember request) {
|
||||||
|
messageService.setRemember(request, request.isRemember());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleCallback(@NonNull final CallbackQuery callback) throws AccessDenied {
|
@TransactionalEventListener(MessageDto.class)
|
||||||
final MaybeInaccessibleMessage tlgMessage = callback.getMessage();
|
public void update(@NonNull final MessageDto message) {
|
||||||
chatService.setEnabled(tlgMessage.getChatId(), true, callback.getFrom().getUserName());
|
if (maySend(message)) {
|
||||||
try {
|
return;
|
||||||
final InlineDto dto = objectMapper.readValue(callback.getData(), InlineDto.class);
|
|
||||||
switch (dto.getCommand()) {
|
|
||||||
case HIDE -> hide(tlgMessage);
|
|
||||||
case REMEMBER -> remember(tlgMessage, true);
|
|
||||||
case UNREMEMBER -> remember(tlgMessage, false);
|
|
||||||
default -> update(tlgMessage);
|
|
||||||
}
|
}
|
||||||
} catch (JsonProcessingException e) {
|
if (mayDelete(message)) {
|
||||||
log.error("Failed to read InlineDto.", e);
|
return;
|
||||||
}
|
}
|
||||||
|
if (message.getTelegramMessageId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
edit(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void hide(@NonNull final MaybeInaccessibleMessage tlgMessage) {
|
private boolean maySend(final @NonNull MessageDto message) {
|
||||||
messageService.setHide(tlgMessage, true).ifPresentOrElse(message -> update(message, false), () -> remove(tlgMessage));
|
if (message.needsToBeSent()) {
|
||||||
|
send(message.getOffer(), message.getChat());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void remember(@NonNull final MaybeInaccessibleMessage tlgMessage, final boolean remember) {
|
private boolean mayDelete(final @NonNull MessageDto message) {
|
||||||
messageService.setRemember(tlgMessage, remember).ifPresentOrElse(message -> update(message, false), () -> remove(tlgMessage));
|
if (message.needsToBeDeleted()) {
|
||||||
|
TlgMessage.of(bot, message).ifPresent(tlgMessage -> {
|
||||||
|
bot.delete(tlgMessage);
|
||||||
|
messageService.markDeleted(tlgMessage);
|
||||||
|
});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
private void update(@NonNull final MaybeInaccessibleMessage tlgMessage) {
|
|
||||||
messageService.findDtoByTelegramMessage(tlgMessage).ifPresentOrElse(message -> update(message, false), () -> remove(tlgMessage));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void send(@NonNull final OfferDto offerDto, @NonNull final ChatDto chatDto) {
|
private void send(@NonNull final OfferDto offerDto, @NonNull final ChatDto chatDto) {
|
||||||
@ -159,41 +168,7 @@ public class TelegramService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void update(@NonNull final MessageDto message, final boolean forceResend) {
|
private void edit(@NonNull final MessageDto message) {
|
||||||
if (resend(message, forceResend)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (deleteFromTelegram(message)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (message.getTelegramMessageId() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_update(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean resend(final MessageDto message, final boolean forceResend) {
|
|
||||||
final boolean resendOnPriceChange = message.getOffer().isResendOnPriceChange() && message.getOffer().getPriceChanged() != null && message.getHide() != null && !message.getOffer().getPriceChanged().isBefore(message.getHide());
|
|
||||||
if (forceResend || resendOnPriceChange) {
|
|
||||||
messageService.setHide(message, false);
|
|
||||||
if (message.getTelegramMessageId() == null) {
|
|
||||||
send(message.getOffer(), message.getChat());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean deleteFromTelegram(final MessageDto message) {
|
|
||||||
if (message.getHide() != null && message.getTelegramMessageId() != null) {
|
|
||||||
remove(message.getChat().getId(), message.getTelegramMessageId());
|
|
||||||
messageService.clearTelegramMessageId(message);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void _update(final MessageDto message) {
|
|
||||||
try {
|
try {
|
||||||
final EditMessageCaption edit = new EditMessageCaption(
|
final EditMessageCaption edit = new EditMessageCaption(
|
||||||
message.getChat().getId() + "",
|
message.getChat().getId() + "",
|
||||||
@ -210,7 +185,7 @@ public class TelegramService {
|
|||||||
} 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: {}", message);
|
log.info("Message has been deleted by User. Marking has hidden: {}", message);
|
||||||
messageService.setHide(message, true);
|
TlgMessage.of(bot, message).ifPresent(messageService::markDeleted);
|
||||||
} 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: {}", message);
|
log.debug("Ignoring complaint from telegram-bot-api about unmodified message: {}", message);
|
||||||
} else {
|
} else {
|
||||||
@ -219,7 +194,8 @@ public class TelegramService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String createText(final OfferDto offer) {
|
@NonNull
|
||||||
|
private String createText(@NonNull final OfferDto offer) {
|
||||||
return "%s\n%s\n%s %s (%d km)\n%s\n%s\n".formatted(
|
return "%s\n%s\n%s %s (%d km)\n%s\n%s\n".formatted(
|
||||||
offer.getTitle().replaceAll("\\[", "(").replaceAll("]", ")"),
|
offer.getTitle().replaceAll("\\[", "(").replaceAll("]", ")"),
|
||||||
offer.combinePrice(),
|
offer.combinePrice(),
|
||||||
@ -231,6 +207,7 @@ public class TelegramService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
private InlineKeyboardMarkup createKeyboard(final boolean remember) 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<>();
|
||||||
@ -239,29 +216,17 @@ public class TelegramService {
|
|||||||
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);
|
||||||
addButton(row, ICON_CHECK + " Merken", InlineCommand.REMEMBER);
|
// addButton(row, ICON_CHECK + " Merken", InlineCommand.REMEMBER);
|
||||||
|
addButton(row, ICON_UNDO + " Rückgängig", InlineCommand.UNDO);
|
||||||
}
|
}
|
||||||
keyboard.add(row);
|
keyboard.add(row);
|
||||||
markup.setKeyboard(keyboard);
|
markup.setKeyboard(keyboard);
|
||||||
return markup;
|
return markup;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addButton(final ArrayList<InlineKeyboardButton> row, final String caption, final InlineCommand command) throws JsonProcessingException {
|
private void addButton(@NonNull final ArrayList<InlineKeyboardButton> row, @NonNull final String caption, @NonNull final InlineCommand command) throws JsonProcessingException {
|
||||||
final String data = objectMapper.writeValueAsString(new InlineDto(command));
|
final String data = objectMapper.writeValueAsString(new InlineDto(command));
|
||||||
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 MaybeInaccessibleMessage tlgMessage) {
|
|
||||||
remove(tlgMessage.getChatId(), tlgMessage.getMessageId());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void remove(final long chatId, final int messageId) {
|
|
||||||
try {
|
|
||||||
log.info("Removing Message chat={} message={}", chatId, messageId);
|
|
||||||
bot.execute(new DeleteMessage(chatId + "", messageId));
|
|
||||||
} catch (TelegramApiException e) {
|
|
||||||
log.error("Failed to remove Message chat={} message={}", chatId, messageId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,44 +1,115 @@
|
|||||||
package de.ph87.kleinanzeigen.telegram;
|
package de.ph87.kleinanzeigen.telegram;
|
||||||
|
|
||||||
import lombok.Getter;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.request.ChatRequestEnable;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.request.ChatRequestHelp;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.request.ChatRequestUndo;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.request.MessageRequestHide;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.request.MessageRequestRemember;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
import lombok.NonNull;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
import org.telegram.telegrambots.bots.TelegramLongPollingBot;
|
import org.telegram.telegrambots.bots.TelegramLongPollingBot;
|
||||||
import org.telegram.telegrambots.meta.TelegramBotsApi;
|
import org.telegram.telegrambots.meta.TelegramBotsApi;
|
||||||
|
import org.telegram.telegrambots.meta.api.methods.updatingmessages.DeleteMessage;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.CallbackQuery;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.MaybeInaccessibleMessage;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.Message;
|
||||||
import org.telegram.telegrambots.meta.api.objects.Update;
|
import org.telegram.telegrambots.meta.api.objects.Update;
|
||||||
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
|
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
|
||||||
import org.telegram.telegrambots.updatesreceivers.DefaultBotSession;
|
import org.telegram.telegrambots.updatesreceivers.DefaultBotSession;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.util.Locale;
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@Service
|
||||||
public class TelegramBot extends TelegramLongPollingBot {
|
public class TelegramBot extends TelegramLongPollingBot {
|
||||||
|
|
||||||
|
private final TelegramConfig config;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
private final ApplicationEventPublisher publisher;
|
||||||
|
|
||||||
private final DefaultBotSession session;
|
private final DefaultBotSession session;
|
||||||
|
|
||||||
private final Consumer<Update> onUpdate;
|
public TelegramBot(final TelegramConfig config, final ObjectMapper objectMapper, final ApplicationEventPublisher publisher) throws TelegramApiException {
|
||||||
|
super(config.getToken());
|
||||||
|
|
||||||
@Getter
|
this.config = config;
|
||||||
private final String botUsername;
|
this.publisher = publisher;
|
||||||
|
|
||||||
public TelegramBot(final String token, final String username, final Consumer<Update> onUpdate) throws IOException, TelegramApiException {
|
|
||||||
super(token);
|
|
||||||
this.botUsername = username;
|
|
||||||
this.onUpdate = onUpdate;
|
|
||||||
getOptions().setGetUpdatesTimeout(10);
|
getOptions().setGetUpdatesTimeout(10);
|
||||||
log.info("Starting telegram bot...");
|
log.info("Starting telegram bot...");
|
||||||
final TelegramBotsApi api = new TelegramBotsApi(DefaultBotSession.class);
|
final TelegramBotsApi api = new TelegramBotsApi(DefaultBotSession.class);
|
||||||
session = (DefaultBotSession) api.registerBot(this);
|
session = (DefaultBotSession) api.registerBot(this);
|
||||||
log.info("Telegram bot registered.");
|
log.info("Telegram bot registered.");
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void stop() {
|
||||||
|
log.info("Stopping Telegram bot...");
|
||||||
|
session.stop();
|
||||||
|
log.info("Telegram bot stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUpdateReceived(final Update update) {
|
public String getBotUsername() {
|
||||||
onUpdate.accept(update);
|
return config.getUsername();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stop() {
|
@Override
|
||||||
session.stop();
|
public void onUpdateReceived(@NonNull final Update update) {
|
||||||
|
if (update.hasMessage() && update.getMessage().hasText()) {
|
||||||
|
handleMessage(update.getMessage());
|
||||||
|
} else if (update.hasCallbackQuery()) {
|
||||||
|
handleCallback(update.getCallbackQuery());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleMessage(@NonNull final Message message) {
|
||||||
|
final String[] command = message.getText().toLowerCase(Locale.ROOT).replaceAll("[^\\w\\s]+", "").replaceAll("\\s+", " ").split(" ");
|
||||||
|
switch (command[0]) {
|
||||||
|
case "start" -> publisher.publishEvent(new ChatRequestEnable(this, message, true));
|
||||||
|
case "stop" -> publisher.publishEvent(new ChatRequestEnable(this, message, false));
|
||||||
|
case "u", "r", "undo", "rückgängig" -> publisher.publishEvent(new ChatRequestUndo(this, message));
|
||||||
|
case "h", "hilfe", "help" -> publisher.publishEvent(new ChatRequestHelp(this, message));
|
||||||
|
}
|
||||||
|
delete(new TlgMessage(this, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCallback(@NonNull final CallbackQuery callback) {
|
||||||
|
final MaybeInaccessibleMessage message = callback.getMessage();
|
||||||
|
try {
|
||||||
|
final InlineDto dto = objectMapper.readValue(callback.getData(), InlineDto.class);
|
||||||
|
switch (dto.getCommand()) {
|
||||||
|
case HIDE -> hide(message);
|
||||||
|
case REMEMBER -> publisher.publishEvent(new MessageRequestRemember(this, message, true));
|
||||||
|
case UNREMEMBER -> publisher.publishEvent(new MessageRequestRemember(this, message, false));
|
||||||
|
case UNDO -> publisher.publishEvent(new ChatRequestUndo(this, message));
|
||||||
|
}
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.error("Failed to read InlineDto.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hide(final MaybeInaccessibleMessage message) {
|
||||||
|
final MessageRequestHide event = new MessageRequestHide(this, message);
|
||||||
|
delete(event);
|
||||||
|
publisher.publishEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(@NonNull final TlgMessage tlgMessage) {
|
||||||
|
try {
|
||||||
|
log.info("Removing TlgMessage: tlgMessage={}", tlgMessage);
|
||||||
|
execute(new DeleteMessage(tlgMessage.chat.idStr, tlgMessage.id));
|
||||||
|
} catch (TelegramApiException e) {
|
||||||
|
log.error("Failed to remove TlgMessage: tlgMessage={}", tlgMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/main/java/de/ph87/kleinanzeigen/telegram/TlgChat.java
Normal file
35
src/main/java/de/ph87/kleinanzeigen/telegram/TlgChat.java
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package de.ph87.kleinanzeigen.telegram;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
import org.telegram.telegrambots.meta.api.methods.BotApiMethod;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.MaybeInaccessibleMessage;
|
||||||
|
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@ToString
|
||||||
|
public class TlgChat {
|
||||||
|
|
||||||
|
public final TelegramBot bot;
|
||||||
|
|
||||||
|
public final long id;
|
||||||
|
|
||||||
|
public final String idStr;
|
||||||
|
|
||||||
|
public TlgChat(@NonNull final TelegramBot bot, final long id) {
|
||||||
|
this.bot = bot;
|
||||||
|
this.id = id;
|
||||||
|
this.idStr = this.id + "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public TlgChat(@NonNull final TelegramBot bot, @NonNull final MaybeInaccessibleMessage message) {
|
||||||
|
this(bot, message.getChatId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public <T extends Serializable, Method extends BotApiMethod<T>> T execute(@NonNull final Method method) throws TelegramApiException {
|
||||||
|
return bot.execute(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
41
src/main/java/de/ph87/kleinanzeigen/telegram/TlgMessage.java
Normal file
41
src/main/java/de/ph87/kleinanzeigen/telegram/TlgMessage.java
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package de.ph87.kleinanzeigen.telegram;
|
||||||
|
|
||||||
|
import de.ph87.kleinanzeigen.telegram.chat.message.MessageDto;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
import org.telegram.telegrambots.meta.api.methods.BotApiMethod;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.MaybeInaccessibleMessage;
|
||||||
|
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@ToString
|
||||||
|
public class TlgMessage {
|
||||||
|
|
||||||
|
public final TlgChat chat;
|
||||||
|
|
||||||
|
public final int id;
|
||||||
|
|
||||||
|
private TlgMessage(@NonNull final TelegramBot bot, final long chatId, final int id) {
|
||||||
|
this.chat = new TlgChat(bot, chatId);
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TlgMessage(@NonNull final TelegramBot bot, @NonNull final MaybeInaccessibleMessage message) {
|
||||||
|
this(bot, message.getChatId(), message.getMessageId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<TlgMessage> of(@NonNull final TelegramBot bot, @NonNull final MessageDto message) {
|
||||||
|
if (message.getTelegramMessageId() == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.of(new TlgMessage(bot, message.getChat().getId(), message.getTelegramMessageId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public <T extends Serializable, Method extends BotApiMethod<T>> T execute(@NonNull final Method method) throws TelegramApiException {
|
||||||
|
return chat.bot.execute(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package de.ph87.kleinanzeigen.telegram.chat;
|
package de.ph87.kleinanzeigen.telegram.chat;
|
||||||
|
|
||||||
import jakarta.annotation.Nullable;
|
import jakarta.annotation.Nullable;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
@ -18,9 +19,15 @@ public class Chat {
|
|||||||
private boolean enabled;
|
private boolean enabled;
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
@Nullable
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column
|
||||||
|
@Nullable
|
||||||
|
private Long keyboardMessageId = null;
|
||||||
|
|
||||||
public Chat(final long id, final boolean enabled, @NonNull final String name) {
|
public Chat(final long id, final boolean enabled, @NonNull final String name) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.enabled = enabled;
|
this.enabled = enabled;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package de.ph87.kleinanzeigen.telegram.chat;
|
package de.ph87.kleinanzeigen.telegram.chat;
|
||||||
|
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@ -11,10 +12,14 @@ public class ChatDto {
|
|||||||
|
|
||||||
private final String name;
|
private final String name;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final Long keyboardMessageId;
|
||||||
|
|
||||||
public ChatDto(final Chat chat) {
|
public ChatDto(final Chat chat) {
|
||||||
this.id = chat.getId();
|
this.id = chat.getId();
|
||||||
this.enabled = chat.isEnabled();
|
this.enabled = chat.isEnabled();
|
||||||
this.name = chat.getName();
|
this.name = chat.getName();
|
||||||
|
this.keyboardMessageId = chat.getKeyboardMessageId();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,18 @@
|
|||||||
package de.ph87.kleinanzeigen.telegram.chat;
|
package de.ph87.kleinanzeigen.telegram.chat;
|
||||||
|
|
||||||
import de.ph87.kleinanzeigen.telegram.AccessDenied;
|
|
||||||
import de.ph87.kleinanzeigen.telegram.TelegramConfig;
|
import de.ph87.kleinanzeigen.telegram.TelegramConfig;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.TlgChat;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.request.ChatRequestHelp;
|
||||||
|
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;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.replykeyboard.ReplyKeyboardMarkup;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.KeyboardButton;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.KeyboardRow;
|
||||||
|
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -23,26 +30,48 @@ public class ChatService {
|
|||||||
return chatRepository.findAllByEnabledTrue().stream().filter(chat -> config.isOnWhitelist(chat.getId())).map(this::toDto).toList();
|
return chatRepository.findAllByEnabledTrue().stream().filter(chat -> config.isOnWhitelist(chat.getId())).map(this::toDto).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setEnabled(final long id, final boolean enabled, final String name) throws AccessDenied {
|
public void setEnabled(@NonNull final TlgChat tlgChat, final boolean enabled, final @NonNull String username) {
|
||||||
if (!config.isOnWhitelist(id)) {
|
if (!config.isOnWhitelist(tlgChat.id)) {
|
||||||
throw new AccessDenied();
|
log.warn("Not on whitelist: {}", tlgChat);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
chatRepository
|
chatRepository
|
||||||
.findById(id)
|
.findById(tlgChat.id)
|
||||||
.stream()
|
.stream()
|
||||||
.peek(chat -> {
|
.peek(chat -> update(chat, enabled, username))
|
||||||
chat.setName(name);
|
.findFirst()
|
||||||
|
.orElseGet(() -> create(tlgChat, enabled, username));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Chat create(@NonNull final TlgChat tlgChat, final boolean enabled, final @NonNull String username) {
|
||||||
|
final Chat chat = chatRepository.save(new Chat(tlgChat.id, enabled, username));
|
||||||
|
log.info("Chat created: {}", chat);
|
||||||
|
return chat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void help(@NonNull final ChatRequestHelp request) {
|
||||||
|
final SendMessage send = new SendMessage(request.idStr, "");
|
||||||
|
final KeyboardRow row0 = new KeyboardRow(List.of(
|
||||||
|
new KeyboardButton("Rückgängig"),
|
||||||
|
new KeyboardButton("Alles löschen")
|
||||||
|
));
|
||||||
|
final List<KeyboardRow> keyboard = List.of(row0);
|
||||||
|
send.setReplyMarkup(new ReplyKeyboardMarkup(keyboard));
|
||||||
|
try {
|
||||||
|
request.execute(send);
|
||||||
|
} catch (TelegramApiException e) {
|
||||||
|
log.error("Failed to printHelp: request={}, error={}", request, e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update(@NonNull final Chat chat, final boolean enabled, final @NonNull String username) {
|
||||||
|
chat.setName(username);
|
||||||
if (chat.isEnabled() != enabled) {
|
if (chat.isEnabled() != enabled) {
|
||||||
chat.setEnabled(enabled);
|
chat.setEnabled(enabled);
|
||||||
log.info("Chat {}: {}", enabled ? "ENABLED" : "DISABLED", chat);
|
log.info("Chat {}: {}", enabled ? "ENABLED" : "DISABLED", chat);
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.findFirst()
|
|
||||||
.orElseGet(() -> {
|
|
||||||
final Chat chat = chatRepository.save(new Chat(id, enabled, name));
|
|
||||||
log.info("Chat created: {}", chat);
|
|
||||||
return chat;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Chat getByDto(final ChatDto chatDto) {
|
public Chat getByDto(final ChatDto chatDto) {
|
||||||
|
|||||||
@ -31,7 +31,6 @@ public class Message {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private Integer telegramMessageId;
|
private Integer telegramMessageId;
|
||||||
|
|
||||||
@Setter
|
|
||||||
@Column
|
@Column
|
||||||
@Nullable
|
@Nullable
|
||||||
private ZonedDateTime hide = null;
|
private ZonedDateTime hide = null;
|
||||||
@ -46,4 +45,19 @@ public class Message {
|
|||||||
this.telegramMessageId = tlgMessage.getMessageId();
|
this.telegramMessageId = tlgMessage.getMessageId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setHide(final boolean hide) {
|
||||||
|
if (hide) {
|
||||||
|
if (this.hide == null) {
|
||||||
|
this.hide = ZonedDateTime.now();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.hide = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public ZonedDateTime getHide() {
|
||||||
|
return hide;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -36,4 +36,12 @@ public class MessageDto {
|
|||||||
remember = message.isRemember();
|
remember = message.isRemember();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean needsToBeSent() {
|
||||||
|
return hide == null && telegramMessageId == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean needsToBeDeleted() {
|
||||||
|
return hide != null && telegramMessageId != null;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,26 +3,26 @@ package de.ph87.kleinanzeigen.telegram.chat.message;
|
|||||||
import de.ph87.kleinanzeigen.kleinanzeigen.offer.Offer;
|
import de.ph87.kleinanzeigen.kleinanzeigen.offer.Offer;
|
||||||
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.kleinanzeigen.offer.OfferService;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.TlgChat;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.TlgMessage;
|
||||||
import de.ph87.kleinanzeigen.telegram.chat.Chat;
|
import de.ph87.kleinanzeigen.telegram.chat.Chat;
|
||||||
import de.ph87.kleinanzeigen.telegram.chat.ChatDto;
|
import de.ph87.kleinanzeigen.telegram.chat.ChatDto;
|
||||||
import de.ph87.kleinanzeigen.telegram.chat.ChatService;
|
import de.ph87.kleinanzeigen.telegram.chat.ChatService;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Propagation;
|
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 java.time.ZonedDateTime;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
@Transactional
|
||||||
@EnableScheduling
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class MessageService {
|
public class MessageService {
|
||||||
|
|
||||||
@ -32,6 +32,8 @@ public class MessageService {
|
|||||||
|
|
||||||
private final ChatService chatService;
|
private final ChatService chatService;
|
||||||
|
|
||||||
|
private final ApplicationEventPublisher applicationEventPublisher;
|
||||||
|
|
||||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
public void updateOrCreate(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);
|
||||||
@ -42,48 +44,60 @@ public class MessageService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("UnusedReturnValue")
|
public void setHide(@NonNull final TlgMessage tlgMessage) {
|
||||||
public Optional<MessageDto> setHide(final MaybeInaccessibleMessage tlgMessage, final boolean hide) {
|
set(tlgMessage, message -> {
|
||||||
return findByTelegramMessage(tlgMessage).stream().peek(offer -> offer.setHide(hide ? ZonedDateTime.now() : null)).findFirst().map(this::toDto);
|
message.setHide(true);
|
||||||
|
message.setTelegramMessageId(null);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<MessageDto> setRemember(final MaybeInaccessibleMessage tlgMessage, final boolean remember) {
|
public void setRemember(@NonNull final TlgMessage tlgMessage, final boolean remember) {
|
||||||
return findByTelegramMessage(tlgMessage).stream().peek(offer -> offer.setRemember(remember)).findFirst().map(this::toDto);
|
set(tlgMessage, message -> message.setRemember(remember));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<MessageDto> findDtoByTelegramMessage(final MaybeInaccessibleMessage tlgMessage) {
|
@NonNull
|
||||||
return findByTelegramMessage(tlgMessage).map(this::toDto);
|
public List<MessageDto> findAllDtoByOfferDto(@NonNull final OfferDto offer) {
|
||||||
}
|
|
||||||
|
|
||||||
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 ? ZonedDateTime.now() : null));
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
return messageRepository.findAllByOffer_Id(offer.getId()).stream().map(this::toDto).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private MessageDto toDto(final Message message) {
|
public void undo(@NonNull final TlgChat chat) {
|
||||||
|
messageRepository.findFirstByChat_IdAndHideNotNullOrderByHideDesc(chat.id).ifPresentOrElse(
|
||||||
|
set(message -> message.setHide(false)),
|
||||||
|
() -> log.warn("Nothing to undo for chat: {}", chat)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markDeleted(@NonNull final TlgMessage tlgMessage) {
|
||||||
|
set(tlgMessage, message -> {
|
||||||
|
message.setHide(true);
|
||||||
|
message.setTelegramMessageId(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void set(@NonNull final TlgMessage tlgMessage, @NonNull final Consumer<Message> setter) {
|
||||||
|
find(tlgMessage).ifPresent(set(setter));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Consumer<Message> set(@NonNull final Consumer<Message> setter) {
|
||||||
|
return message -> {
|
||||||
|
setter.accept(message);
|
||||||
|
publish(message);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Message> find(final @NonNull TlgMessage tlgMessage) {
|
||||||
|
return messageRepository.findByChat_IdAndTelegramMessageId(tlgMessage.chat.id, tlgMessage.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void publish(@NonNull final Message message) {
|
||||||
|
applicationEventPublisher.publishEvent(toDto(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private MessageDto toDto(@NonNull final Message message) {
|
||||||
final ChatDto chatDto = chatService.toDto(message.getChat());
|
final ChatDto chatDto = chatService.toDto(message.getChat());
|
||||||
final OfferDto offerDto = offerService.toDto(message.getOffer());
|
final OfferDto offerDto = offerService.toDto(message.getOffer());
|
||||||
return new MessageDto(message, chatDto, offerDto);
|
return new MessageDto(message, chatDto, offerDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public Optional<MessageDto> undo(final long chatId) {
|
|
||||||
return messageRepository.findFirstByChat_IdAndHideNotNullOrderByHideDesc(chatId).stream().peek(message -> message.setHide(null)).map(this::toDto).findFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
package de.ph87.kleinanzeigen.telegram.request;
|
||||||
|
|
||||||
|
import de.ph87.kleinanzeigen.telegram.TelegramBot;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.TlgChat;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.Message;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class ChatRequestEnable extends TlgChat {
|
||||||
|
|
||||||
|
private final boolean enable;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final String username;
|
||||||
|
|
||||||
|
public ChatRequestEnable(@NonNull final TelegramBot bot, @NonNull final Message message, final boolean enable) {
|
||||||
|
super(bot, message);
|
||||||
|
this.enable = enable;
|
||||||
|
this.username = message.getFrom().getUserName() == null ? "" : message.getFrom().getUserName();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package de.ph87.kleinanzeigen.telegram.request;
|
||||||
|
|
||||||
|
import de.ph87.kleinanzeigen.telegram.TelegramBot;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.TlgChat;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.Message;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class ChatRequestHelp extends TlgChat {
|
||||||
|
|
||||||
|
public ChatRequestHelp(@NonNull final TelegramBot bot, @NonNull final Message message) {
|
||||||
|
super(bot, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package de.ph87.kleinanzeigen.telegram.request;
|
||||||
|
|
||||||
|
import de.ph87.kleinanzeigen.telegram.TelegramBot;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.TlgChat;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.MaybeInaccessibleMessage;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class ChatRequestUndo extends TlgChat {
|
||||||
|
|
||||||
|
public ChatRequestUndo(@NonNull final TelegramBot bot, @NonNull final MaybeInaccessibleMessage message) {
|
||||||
|
super(bot, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package de.ph87.kleinanzeigen.telegram.request;
|
||||||
|
|
||||||
|
import de.ph87.kleinanzeigen.telegram.TelegramBot;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.TlgMessage;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.MaybeInaccessibleMessage;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class MessageRequestHide extends TlgMessage {
|
||||||
|
|
||||||
|
public MessageRequestHide(@NonNull final TelegramBot bot, @NonNull final MaybeInaccessibleMessage message) {
|
||||||
|
super(bot, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package de.ph87.kleinanzeigen.telegram.request;
|
||||||
|
|
||||||
|
import de.ph87.kleinanzeigen.telegram.TelegramBot;
|
||||||
|
import de.ph87.kleinanzeigen.telegram.TlgMessage;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.MaybeInaccessibleMessage;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class MessageRequestRemember extends TlgMessage {
|
||||||
|
|
||||||
|
private final boolean remember;
|
||||||
|
|
||||||
|
public MessageRequestRemember(@NonNull final TelegramBot bot, @NonNull final MaybeInaccessibleMessage message, final boolean remember) {
|
||||||
|
super(bot, message);
|
||||||
|
this.remember = remember;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user