MANY CHANGES:
- highres image from article page - no-image-placeholder - all messages via SendPhoto - rememberUntil
This commit is contained in:
parent
071a7199ad
commit
bf66f81925
@ -4,45 +4,58 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.telegram.telegrambots.bots.TelegramLongPollingBot;
|
||||
import org.telegram.telegrambots.meta.TelegramBotsApi;
|
||||
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
|
||||
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.EditMessageCaption;
|
||||
import org.telegram.telegrambots.meta.api.methods.updatingmessages.EditMessageText;
|
||||
import org.telegram.telegrambots.meta.api.objects.*;
|
||||
import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup;
|
||||
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton;
|
||||
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
|
||||
import org.telegram.telegrambots.updatesreceivers.DefaultBotSession;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static de.ph87.kleinanzeigen.api.JSON.objectMapper;
|
||||
|
||||
@Slf4j
|
||||
public class Bot extends TelegramLongPollingBot {
|
||||
|
||||
private static final long CHAT_ID = 101138682L;
|
||||
|
||||
private static final String ICON_CHECK = "✅";
|
||||
|
||||
private static final String ICON_REMOVE = "❌";
|
||||
|
||||
private static final long CHAT_ID = 101138682L;
|
||||
private final byte[] NO_IMAGE;
|
||||
|
||||
private final DefaultBotSession session;
|
||||
|
||||
private final Consumer<MaybeInaccessibleMessage> ignore;
|
||||
|
||||
private final Function<MaybeInaccessibleMessage, Optional<Offer>> find;
|
||||
|
||||
private final BiFunction<MaybeInaccessibleMessage, Boolean, Optional<Offer>> remember;
|
||||
|
||||
public Bot(final Consumer<MaybeInaccessibleMessage> ignore, final BiFunction<MaybeInaccessibleMessage, Boolean, Optional<Offer>> remember) throws IOException, TelegramApiException {
|
||||
public Bot(final Consumer<MaybeInaccessibleMessage> ignore, final Function<MaybeInaccessibleMessage, Optional<Offer>> find, final BiFunction<MaybeInaccessibleMessage, Boolean, Optional<Offer>> remember) throws IOException, TelegramApiException {
|
||||
super(readToken());
|
||||
|
||||
final BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
|
||||
final ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
ImageIO.write(img, "PNG", stream);
|
||||
NO_IMAGE = stream.toByteArray();
|
||||
|
||||
this.ignore = ignore;
|
||||
this.find = find;
|
||||
this.remember = remember;
|
||||
log.info("Starting telegram bot...");
|
||||
final TelegramBotsApi api = new TelegramBotsApi(DefaultBotSession.class);
|
||||
@ -73,8 +86,9 @@ public class Bot extends TelegramLongPollingBot {
|
||||
final InlineDto dto = objectMapper.readValue(query.getData(), InlineDto.class);
|
||||
switch (dto.getCommand()) {
|
||||
case IGNORE -> ignore(message);
|
||||
case REMEMBER -> remember(message, dto);
|
||||
case UNREMEMBER -> unremember(message, dto);
|
||||
case REMEMBER -> remember(message);
|
||||
case UNREMEMBER -> unremember(message);
|
||||
default -> updateMessage(message);
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Failed to read InlineDto.", e);
|
||||
@ -86,30 +100,34 @@ public class Bot extends TelegramLongPollingBot {
|
||||
remove(message);
|
||||
}
|
||||
|
||||
private void remember(final MaybeInaccessibleMessage message, final InlineDto dto) {
|
||||
remember.apply(message, true).ifPresent(offer -> editMessage(message, offer, dto));
|
||||
private void remember(final MaybeInaccessibleMessage message) {
|
||||
remember.apply(message, true).ifPresentOrElse(
|
||||
offer -> updateMessage(message, offer),
|
||||
() -> remove(message)
|
||||
);
|
||||
}
|
||||
|
||||
private void unremember(final MaybeInaccessibleMessage message, final InlineDto dto) {
|
||||
remember.apply(message, false).ifPresent(offer -> editMessage(message, offer, dto));
|
||||
private void unremember(final MaybeInaccessibleMessage message) {
|
||||
remember.apply(message, false).ifPresentOrElse(
|
||||
offer -> updateMessage(message, offer),
|
||||
() -> remove(message)
|
||||
);
|
||||
}
|
||||
|
||||
private void editMessage(final MaybeInaccessibleMessage message, final Offer offer, final InlineDto dto) {
|
||||
private void updateMessage(final MaybeInaccessibleMessage message) {
|
||||
find.apply(message).ifPresentOrElse(
|
||||
offer -> updateMessage(message, offer),
|
||||
() -> remove(message)
|
||||
);
|
||||
}
|
||||
|
||||
private void updateMessage(final MaybeInaccessibleMessage message, final Offer offer) {
|
||||
try {
|
||||
switch (dto.getType()) {
|
||||
case TEXT -> {
|
||||
final EditMessageText edit = new EditMessageText(message.getChatId() + "", message.getMessageId(), null, getText(offer), null, null, getKeyboard(offer), null, null);
|
||||
edit.setParseMode("Markdown");
|
||||
execute(edit);
|
||||
}
|
||||
case PHOTO -> {
|
||||
final EditMessageCaption edit = new EditMessageCaption(message.getChatId() + "", message.getMessageId(), null, getText(offer), getKeyboard(offer), null, null);
|
||||
edit.setParseMode("Markdown");
|
||||
execute(edit);
|
||||
}
|
||||
}
|
||||
final EditMessageCaption edit = new EditMessageCaption(message.getChatId() + "", message.getMessageId(), null, createText(offer), createKeyboard(offer), null, null);
|
||||
edit.setParseMode("Markdown");
|
||||
execute(edit);
|
||||
} catch (TelegramApiException | JsonProcessingException e) {
|
||||
log.error("Failed to edit Message to #{}.", CHAT_ID, e);
|
||||
log.error("Failed to edit Message to #{}.", message.getChatId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,19 +139,12 @@ public class Bot extends TelegramLongPollingBot {
|
||||
|
||||
public void send(final Offer offer) {
|
||||
try {
|
||||
final Message message;
|
||||
if (offer.getImage().isEmpty()) {
|
||||
final SendMessage send = new SendMessage(CHAT_ID + "", getText(offer));
|
||||
send.setParseMode("Markdown");
|
||||
send.setReplyMarkup(getKeyboard(offer));
|
||||
message = execute(send);
|
||||
} else {
|
||||
final SendPhoto send = new SendPhoto(CHAT_ID + "", new InputFile(offer.getImage()));
|
||||
send.setCaption(getText(offer));
|
||||
send.setParseMode("Markdown");
|
||||
send.setReplyMarkup(getKeyboard(offer));
|
||||
message = execute(send);
|
||||
}
|
||||
final InputFile inputFile = offer.getImage().isEmpty() ? new InputFile(new ByteArrayInputStream(NO_IMAGE), "[Kein Bild]") : new InputFile(offer.getImage());
|
||||
final SendPhoto send = new SendPhoto(CHAT_ID + "", inputFile);
|
||||
send.setCaption(createText(offer));
|
||||
send.setParseMode("Markdown");
|
||||
send.setReplyMarkup(createKeyboard(offer));
|
||||
final Message message = execute(send);
|
||||
|
||||
offer.setTelegramMessageId(message.getMessageId());
|
||||
} catch (TelegramApiException | JsonProcessingException e) {
|
||||
@ -141,39 +152,32 @@ public class Bot extends TelegramLongPollingBot {
|
||||
}
|
||||
}
|
||||
|
||||
private String getText(final Offer offer) {
|
||||
return "[%s](%s)\nVor %s\n%s\n%s".formatted(
|
||||
escape(offer.getTitle()),
|
||||
private String createText(final Offer offer) {
|
||||
return "[%s](%s)\n%s\n%s".formatted(
|
||||
offer.getTitle().replaceAll("\\[", "(").replaceAll("]", ")"),
|
||||
offer.getHref(),
|
||||
offer.ageString(),
|
||||
offer.calculateLocationString(),
|
||||
offer.getDescription()
|
||||
);
|
||||
}
|
||||
|
||||
private String escape(final String text) {
|
||||
// TODO make smarter (real escaping)
|
||||
return text.replaceAll("\\[", "(").replaceAll("]", ")");
|
||||
}
|
||||
|
||||
private InlineKeyboardMarkup getKeyboard(final Offer offer) throws JsonProcessingException {
|
||||
private InlineKeyboardMarkup createKeyboard(final Offer offer) throws JsonProcessingException {
|
||||
final InlineKeyboardMarkup markup = new InlineKeyboardMarkup();
|
||||
final ArrayList<List<InlineKeyboardButton>> keyboard = new ArrayList<>();
|
||||
final ArrayList<InlineKeyboardButton> row = new ArrayList<>();
|
||||
if (offer.isRemember()) {
|
||||
addButton(row, ICON_CHECK + ICON_CHECK + ICON_CHECK + " Gemerkt " + ICON_CHECK + ICON_CHECK + ICON_CHECK, InlineCommand.UNREMEMBER, offer);
|
||||
addButton(row, ICON_CHECK + ICON_CHECK + ICON_CHECK + " Gemerkt " + ICON_CHECK + ICON_CHECK + ICON_CHECK, InlineCommand.UNREMEMBER);
|
||||
} else {
|
||||
addButton(row, ICON_REMOVE + " Ignorieren", InlineCommand.IGNORE, offer);
|
||||
addButton(row, ICON_CHECK + " Merken", InlineCommand.REMEMBER, offer);
|
||||
addButton(row, ICON_REMOVE + " Entfernen", InlineCommand.IGNORE);
|
||||
addButton(row, ICON_CHECK + " Merken", InlineCommand.REMEMBER);
|
||||
}
|
||||
keyboard.add(row);
|
||||
markup.setKeyboard(keyboard);
|
||||
return markup;
|
||||
}
|
||||
|
||||
private void addButton(final ArrayList<InlineKeyboardButton> row, final String caption, final InlineCommand command, final Offer offer) throws JsonProcessingException {
|
||||
final InlineDto dto = new InlineDto(command, InlineType.of(offer));
|
||||
final String data = objectMapper.writeValueAsString(dto);
|
||||
private void addButton(final ArrayList<InlineKeyboardButton> row, final String caption, final InlineCommand command) throws JsonProcessingException {
|
||||
final String data = objectMapper.writeValueAsString(new InlineDto(command));
|
||||
row.add(new InlineKeyboardButton(caption, null, data, null, null, null, null, null, null));
|
||||
}
|
||||
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
package de.ph87.kleinanzeigen.api;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class InlineDto {
|
||||
|
||||
private InlineCommand command;
|
||||
|
||||
private InlineType type;
|
||||
|
||||
public InlineDto(final InlineCommand command, final InlineType type) {
|
||||
public InlineDto(final InlineCommand command) {
|
||||
this.command = command;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
package de.ph87.kleinanzeigen.api;
|
||||
|
||||
public enum InlineType {
|
||||
TEXT, PHOTO;
|
||||
|
||||
public static InlineType of(final Offer offer) {
|
||||
if (offer.getImage() == null) {
|
||||
return TEXT;
|
||||
}
|
||||
return PHOTO;
|
||||
}
|
||||
|
||||
}
|
||||
@ -70,9 +70,9 @@ public class Kleinanzeigen {
|
||||
offers.sort(Comparator.comparing(Offer::getDate));
|
||||
|
||||
final List<Offer> deleted = new ArrayList<>();
|
||||
final List<Offer> notToRemember = new ArrayList<>(offers.stream().filter(offer -> !offer.isRemember()).toList());
|
||||
while (!notToRemember.isEmpty() && notToRemember.size() > Kleinanzeigen.KEEP_LAST_OFFERS_COUNT) {
|
||||
final Offer offer = notToRemember.removeFirst();
|
||||
final List<Offer> removable = new ArrayList<>(offers.stream().filter(offer -> !offer.isRemember() && offer.getRememberUntil() == null).toList());
|
||||
while (!removable.isEmpty() && removable.size() > Kleinanzeigen.KEEP_LAST_OFFERS_COUNT) {
|
||||
final Offer offer = removable.removeFirst();
|
||||
offers.remove(offer);
|
||||
offer.markDeleted();
|
||||
deleted.add(offer);
|
||||
@ -105,7 +105,6 @@ public class Kleinanzeigen {
|
||||
final String description = article.select(".aditem-main--middle--description").text();
|
||||
final ZonedDateTime date = parseDate(article.select(".aditem-main--top--right").text());
|
||||
final String articleURL = VERSCHENKEN_EPPELBORN_30KM.resolve(article.select(".aditem-image a").attr("href")).toString();
|
||||
final String imageURL = article.select(".aditem-image img").attr("src");
|
||||
final String zipcode;
|
||||
final String location;
|
||||
final Integer distance;
|
||||
@ -120,23 +119,34 @@ public class Kleinanzeigen {
|
||||
location = locationMatcher.group("location");
|
||||
distance = Integer.parseInt(locationMatcher.group("distance"));
|
||||
}
|
||||
|
||||
final String imageURL = getImageURL(articleURL);
|
||||
|
||||
return new Offer(id, date, title, zipcode, location, distance, description, articleURL, imageURL);
|
||||
} catch (NumberFormatException e) {
|
||||
} catch (NumberFormatException | IOException e) {
|
||||
throw new OfferParseException(article, e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getImageURL(final String articleURL) throws IOException {
|
||||
final String imageURL;
|
||||
final Document document = Jsoup.parse(URI.create(articleURL).toURL(), 3000);
|
||||
final Element image = document.select(".galleryimage-element img").first();
|
||||
if (image == null) {
|
||||
imageURL = "";
|
||||
} else {
|
||||
imageURL = image.attr("src");
|
||||
}
|
||||
return imageURL;
|
||||
}
|
||||
|
||||
private void merge(final Offer offer) {
|
||||
synchronized (offer) {
|
||||
offers.stream()
|
||||
.filter(existing -> existing.getId().equals(offer.getId()))
|
||||
.peek(existing -> existing.merge(offer))
|
||||
.findFirst()
|
||||
.orElseGet(() -> {
|
||||
log.info("Created: {}", offer);
|
||||
offers.add(offer);
|
||||
return offer;
|
||||
});
|
||||
offers.stream().filter(existing -> existing.getId().equals(offer.getId())).peek(existing -> existing.merge(offer)).findFirst().orElseGet(() -> {
|
||||
log.info("Created: {}", offer);
|
||||
offers.add(offer);
|
||||
return offer;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,15 +159,7 @@ public class Kleinanzeigen {
|
||||
|
||||
final Matcher localDateMatcher = Pattern.compile("(?<day>\\d+).(?<month>\\d+).(?<year>\\d+)").matcher(text);
|
||||
if (localDateMatcher.find()) {
|
||||
return ZonedDateTime.of(
|
||||
LocalDate.of(
|
||||
Integer.parseInt(localDateMatcher.group("day")),
|
||||
Integer.parseInt(localDateMatcher.group("month")),
|
||||
Integer.parseInt(localDateMatcher.group("year"))
|
||||
),
|
||||
LocalTime.MIDNIGHT,
|
||||
TimeZone.getDefault().toZoneId()
|
||||
);
|
||||
return ZonedDateTime.of(LocalDate.of(Integer.parseInt(localDateMatcher.group("day")), Integer.parseInt(localDateMatcher.group("month")), Integer.parseInt(localDateMatcher.group("year"))), LocalTime.MIDNIGHT, TimeZone.getDefault().toZoneId());
|
||||
}
|
||||
throw new NumberFormatException("Failed to parse date: " + text);
|
||||
}
|
||||
@ -188,8 +190,10 @@ public class Kleinanzeigen {
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Offer> findByTelegramMessageId(final MaybeInaccessibleMessage message) {
|
||||
return offers.stream().filter(offer -> Objects.equals(offer.getTelegramMessageId(), message.getMessageId())).findFirst();
|
||||
public Optional<Offer> findByTelegramMessageId(final MaybeInaccessibleMessage message) {
|
||||
synchronized (offers) {
|
||||
return offers.stream().filter(offer -> Objects.equals(offer.getTelegramMessageId(), message.getMessageId())).findFirst();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ public class Main {
|
||||
private static final Kleinanzeigen kleinanzeigen = new Kleinanzeigen(offer -> bot.remove(List.of(offer)));
|
||||
|
||||
public static void main(String[] args) throws IOException, TelegramApiException {
|
||||
bot = new Bot(kleinanzeigen::ignore, kleinanzeigen::remember);
|
||||
bot = new Bot(kleinanzeigen::ignore, kleinanzeigen::findByTelegramMessageId, kleinanzeigen::remember);
|
||||
try {
|
||||
while (true) {
|
||||
handle(bot);
|
||||
|
||||
@ -6,7 +6,6 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
@Getter
|
||||
@ -43,9 +42,12 @@ public class Offer {
|
||||
|
||||
private boolean ignore = false;
|
||||
|
||||
@Setter
|
||||
private boolean remember = false;
|
||||
|
||||
@Setter
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
|
||||
private ZonedDateTime rememberUntil = null;
|
||||
|
||||
@JsonIgnore
|
||||
private boolean _deleted_ = false;
|
||||
|
||||
@ -72,17 +74,6 @@ public class Offer {
|
||||
}
|
||||
}
|
||||
|
||||
public String ageString() {
|
||||
final Duration age = Duration.between(date, ZonedDateTime.now());
|
||||
if (age.toDays() > 0) {
|
||||
return "%d Tag%s".formatted(age.toDays(), age.toDays() == 1 ? "" : "en");
|
||||
} else if (age.toHours() > 0) {
|
||||
return "%d Stunde%s".formatted(age.toHours(), age.toHours() == 1 ? "" : "n");
|
||||
} else {
|
||||
return "%d Minute%s".formatted(age.toMinutes(), age.toMinutes() == 1 ? "" : "n");
|
||||
}
|
||||
}
|
||||
|
||||
public void merge(final Offer other) {
|
||||
verifyNotDeleted();
|
||||
other.verifyNotDeleted();
|
||||
@ -125,6 +116,16 @@ public class Offer {
|
||||
public void ignore() {
|
||||
ignore = true;
|
||||
remember = false;
|
||||
rememberUntil = null;
|
||||
}
|
||||
|
||||
public void setRemember(final boolean remember) {
|
||||
if (remember) {
|
||||
this.rememberUntil = null;
|
||||
} else if (this.remember) {
|
||||
this.rememberUntil = ZonedDateTime.now().plusHours(1);
|
||||
}
|
||||
this.remember = remember;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import org.jsoup.nodes.Element;
|
||||
|
||||
public class OfferParseException extends Exception {
|
||||
|
||||
public OfferParseException(final Element element, final RuntimeException cause) {
|
||||
public OfferParseException(final Element element, final Exception cause) {
|
||||
super(element.outerHtml(), cause);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user