From 17c604ce74d245dafe8946d4f53c05e446231948 Mon Sep 17 00:00:00 2001 From: Chris Sdogkos Date: Mon, 29 Dec 2025 15:09:22 +0300 Subject: [PATCH 1/4] refactor(RSSHandlerRoutine): switch to Instant Signed-off-by: Chris Sdogkos --- .../tjbot/features/rss/FailureState.java | 4 +- .../tjbot/features/rss/RSSHandlerRoutine.java | 67 ++++++++----------- 2 files changed, 31 insertions(+), 40 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/rss/FailureState.java b/application/src/main/java/org/togetherjava/tjbot/features/rss/FailureState.java index a5f8d41f40..02f7c309d5 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/rss/FailureState.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/rss/FailureState.java @@ -1,6 +1,6 @@ package org.togetherjava.tjbot.features.rss; -import java.time.ZonedDateTime; +import java.time.Instant; -record FailureState(int count, ZonedDateTime lastFailure) { +record FailureState(int count, Instant lastFailure) { } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java b/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java index e3d96af021..d72a42d212 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java @@ -28,11 +28,10 @@ import javax.annotation.Nonnull; import java.io.IOException; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; +import java.time.Instant; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -74,8 +73,6 @@ public final class RSSHandlerRoutine implements Routine { private static final Logger logger = LoggerFactory.getLogger(RSSHandlerRoutine.class); private static final int MAX_CONTENTS = 1000; - private static final ZonedDateTime ZONED_TIME_MIN = - ZonedDateTime.of(LocalDateTime.MIN, ZoneId.systemDefault()); private static final String HTTP_USER_AGENT = "TJ-Bot/1.0 (+https://github.com/Together-Java/TJ-Bot)"; private final RssReader rssReader; @@ -181,7 +178,7 @@ private void sendRSS(JDA jda, RSSFeed feedConfig) { private Optional> prepareItemPostPredicate(RSSFeed feedConfig, List rssItems) { Optional rssFeedRecord = getRssFeedRecordFromDatabase(feedConfig); - Optional lastPostedDate = + Optional lastPostedDate = getLatestPostDateFromItems(rssItems, feedConfig.dateFormatterPattern()); lastPostedDate.ifPresent( @@ -191,16 +188,15 @@ private Optional> prepareItemPostPredicate(RSSFeed feedConfig, return Optional.empty(); } - Optional lastSavedDate = getLastSavedDateFromDatabaseRecord( - rssFeedRecord.orElseThrow(), feedConfig.dateFormatterPattern()); + Optional lastSavedDate = + getLastSavedDateFromDatabaseRecord(rssFeedRecord.orElseThrow()); if (lastSavedDate.isEmpty()) { return Optional.empty(); } return Optional.of(item -> { - ZonedDateTime itemPubDate = - getDateTimeFromItem(item, feedConfig.dateFormatterPattern()); + Instant itemPubDate = getDateTimeFromItem(item, feedConfig.dateFormatterPattern()); return itemPubDate.isAfter(lastSavedDate.orElseThrow()); }); } @@ -223,16 +219,13 @@ private Optional getRssFeedRecordFromDatabase(RSSFeed feedConfig) * record. * * @param rssRecord an existing RSS feed record to retrieve the last saved date from - * @param dateFormatterPattern the pattern used to parse the date from the database record * @return An {@link Optional} containing the last saved date if it could be retrieved and * parsed successfully, otherwise an empty {@link Optional} */ - private Optional getLastSavedDateFromDatabaseRecord(RssFeedRecord rssRecord, - String dateFormatterPattern) throws DateTimeParseException { + private Optional getLastSavedDateFromDatabaseRecord(RssFeedRecord rssRecord) + throws DateTimeParseException { try { - ZonedDateTime savedDate = - getZonedDateTime(rssRecord.getLastDate(), dateFormatterPattern); - return Optional.of(savedDate); + return Optional.of(Instant.parse(rssRecord.getLastDate())); } catch (DateTimeParseException _) { return Optional.empty(); } @@ -243,13 +236,13 @@ private Optional getLastSavedDateFromDatabaseRecord(RssFeedRecord * * @param items the list of items to retrieve the latest post date from * @param dateFormatterPattern the pattern used to parse the date from the database record - * @return the latest post date as a {@link ZonedDateTime} object, or null if the list is empty + * @return the latest post date as a {@link Instant} object, or null if the list is empty */ - private Optional getLatestPostDateFromItems(List items, + private Optional getLatestPostDateFromItems(List items, String dateFormatterPattern) { return items.stream() .map(item -> getDateTimeFromItem(item, dateFormatterPattern)) - .max(ZonedDateTime::compareTo); + .max(Instant::compareTo); } /** @@ -277,10 +270,10 @@ private void postItem(List textChannels, Item rssItem, RSSFeed feed * @throws DateTimeParseException if the date cannot be parsed */ private void updateLastDateToDatabase(RSSFeed feedConfig, @Nullable RssFeedRecord rssFeedRecord, - ZonedDateTime lastPostedDate) { + Instant lastPostedDate) { DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(feedConfig.dateFormatterPattern()); - String lastDateStr = lastPostedDate.format(dateTimeFormatter); + String lastDateStr = lastPostedDate.toString(); if (rssFeedRecord == null) { database.write(context -> context.newRecord(RSS_FEED) @@ -297,22 +290,22 @@ private void updateLastDateToDatabase(RSSFeed feedConfig, @Nullable RssFeedRecor } /** - * Attempts to get a {@link ZonedDateTime} from an {@link Item} with a provided string date time + * Attempts to get a {@link Instant} from an {@link Item} with a provided string date time * format. *

* If either of the function inputs are null or a {@link DateTimeParseException} is caught, the - * oldest-possible {@link ZonedDateTime} will get returned instead. + * oldest-possible {@link Instant} will get returned instead. * * @param item The {@link Item} from which to extract the date. * @param dateTimeFormat The format of the date time string. - * @return The computed {@link ZonedDateTime} + * @return The computed {@link Instant} * @throws DateTimeParseException if the date cannot be parsed */ - private static ZonedDateTime getDateTimeFromItem(Item item, String dateTimeFormat) + private static Instant getDateTimeFromItem(Item item, String dateTimeFormat) throws DateTimeParseException { Optional pubDate = item.getPubDate(); - return pubDate.map(s -> getZonedDateTime(s, dateTimeFormat)).orElse(ZONED_TIME_MIN); + return pubDate.map(s -> getInstantTimeFromFormat(s, dateTimeFormat)).orElse(Instant.MIN); } @@ -334,7 +327,7 @@ private static boolean isValidDateFormat(Item rssItem, RSSFeed feedConfig) { // If this throws a DateTimeParseException then it's certain // that the format pattern defined in the config and the // feed's actual format differ. - getZonedDateTime(firstRssFeedPubDate.get(), feedConfig.dateFormatterPattern()); + getInstantTimeFromFormat(firstRssFeedPubDate.get(), feedConfig.dateFormatterPattern()); } catch (DateTimeParseException _) { return false; } @@ -384,7 +377,7 @@ private MessageCreateData constructMessage(Item item, RSSFeed feedConfig) { // Set the item's timestamp to the embed if found item.getPubDate() .ifPresent(date -> embedBuilder - .setTimestamp(getZonedDateTime(date, feedConfig.dateFormatterPattern()))); + .setTimestamp(getInstantTimeFromFormat(date, feedConfig.dateFormatterPattern()))); embedBuilder.setTitle(title, titleLink); embedBuilder.setAuthor(item.getChannel().getLink()); @@ -428,7 +421,7 @@ private List fetchRSSItemsFromURL(String rssUrl) { "Possibly dead RSS feed URL: {} - Failed {} times. Please remove it from config.", rssUrl, newCount); } - circuitBreaker.put(rssUrl, new FailureState(newCount, ZonedDateTime.now())); + circuitBreaker.put(rssUrl, new FailureState(newCount, Instant.now())); long blacklistedHours = calculateWaitHours(newCount); @@ -441,21 +434,19 @@ private List fetchRSSItemsFromURL(String rssUrl) { } /** - * Helper function for parsing a given date value to a {@link ZonedDateTime} with a given - * format. + * Helper function for parsing a given date value to a {@link Instant} with a given format. * * @param date the date value to parse, can be null - * @param format the format pattern to use for parsing - * @return the parsed {@link ZonedDateTime} object + * @return the parsed {@link Instant} object * @throws DateTimeParseException if the date cannot be parsed */ - private static ZonedDateTime getZonedDateTime(@Nullable String date, String format) + private static Instant getInstantTimeFromFormat(@Nullable String date, String datePattern) throws DateTimeParseException { if (date == null) { - return ZONED_TIME_MIN; + return Instant.MIN; } - return ZonedDateTime.parse(date, DateTimeFormatter.ofPattern(format)); + return Instant.from(DateTimeFormatter.ofPattern(datePattern).parse(date)); } private long calculateWaitHours(int failureCount) { @@ -469,8 +460,8 @@ private boolean isBackingOff(String url) { return false; long waitHours = calculateWaitHours(state.count()); - ZonedDateTime retryAt = state.lastFailure().plusHours(waitHours); + Instant retryAt = state.lastFailure().plus(waitHours, ChronoUnit.HOURS); - return ZonedDateTime.now().isBefore(retryAt); + return Instant.now().isBefore(retryAt); } } From d688bb290ca18ae730ccbba5047af204db492087 Mon Sep 17 00:00:00 2001 From: Chris Sdogkos Date: Mon, 29 Dec 2025 15:32:49 +0300 Subject: [PATCH 2/4] Remove unnecessary DateTimeFormatter variable Signed-off-by: Chris Sdogkos --- .../org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java b/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java index 2f262a04a6..bdb58d0d2f 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java @@ -271,8 +271,6 @@ private void postItem(List textChannels, Item rssItem, RSSFeed feed */ private void updateLastDateToDatabase(RSSFeed feedConfig, @Nullable RssFeedRecord rssFeedRecord, Instant lastPostedDate) { - DateTimeFormatter dateTimeFormatter = - DateTimeFormatter.ofPattern(feedConfig.dateFormatterPattern()); String lastDateStr = lastPostedDate.toString(); if (rssFeedRecord == null) { From 85dc995b56ac4afb93694eb882779f0d1b341809 Mon Sep 17 00:00:00 2001 From: Chris Sdogkos Date: Sat, 10 Jan 2026 11:40:06 +0200 Subject: [PATCH 3/4] Address code review comments from @Zabuzard Signed-off-by: Chris Sdogkos --- .../tjbot/features/rss/RSSHandlerRoutine.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java b/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java index bdb58d0d2f..617bacf457 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java @@ -303,7 +303,7 @@ private static Instant getDateTimeFromItem(Item item, String dateTimeFormat) throws DateTimeParseException { Optional pubDate = item.getPubDate(); - return pubDate.map(s -> getInstantTimeFromFormat(s, dateTimeFormat)).orElse(Instant.MIN); + return pubDate.map(s -> parseDateTime(s, dateTimeFormat)).orElse(Instant.MIN); } @@ -325,7 +325,7 @@ private static boolean isValidDateFormat(Item rssItem, RSSFeed feedConfig) { // If this throws a DateTimeParseException then it's certain // that the format pattern defined in the config and the // feed's actual format differ. - getInstantTimeFromFormat(firstRssFeedPubDate.get(), feedConfig.dateFormatterPattern()); + parseDateTime(firstRssFeedPubDate.get(), feedConfig.dateFormatterPattern()); } catch (DateTimeParseException _) { return false; } @@ -374,8 +374,8 @@ private MessageCreateData constructMessage(Item item, RSSFeed feedConfig) { // Set the item's timestamp to the embed if found item.getPubDate() - .ifPresent(date -> embedBuilder - .setTimestamp(getInstantTimeFromFormat(date, feedConfig.dateFormatterPattern()))); + .ifPresent(dateTime -> embedBuilder + .setTimestamp(parseDateTime(dateTime, feedConfig.dateFormatterPattern()))); embedBuilder.setTitle(title, titleLink); embedBuilder.setAuthor(item.getChannel().getLink()); @@ -434,17 +434,17 @@ private List fetchRSSItemsFromURL(String rssUrl) { /** * Helper function for parsing a given date value to a {@link Instant} with a given format. * - * @param date the date value to parse, can be null + * @param dateTime the date and time value to parse, can be null * @return the parsed {@link Instant} object * @throws DateTimeParseException if the date cannot be parsed */ - private static Instant getInstantTimeFromFormat(@Nullable String date, String datePattern) + private static Instant parseDateTime(@Nullable String dateTime, String datePattern) throws DateTimeParseException { - if (date == null) { + if (dateTime == null) { return Instant.MIN; } - return Instant.from(DateTimeFormatter.ofPattern(datePattern).parse(date)); + return Instant.from(DateTimeFormatter.ofPattern(datePattern).parse(dateTime)); } private long calculateWaitHours(int failureCount) { From c0a3dedf6819910d1237e7214850f03799e9974b Mon Sep 17 00:00:00 2001 From: Chris Sdogkos Date: Sat, 10 Jan 2026 12:20:36 +0200 Subject: [PATCH 4/4] Handle updating of wrongly formatted timestamps Handle rewriting timestamps from the specific RSS feed's format to a more unified Instant string format. If a DateTimeParseException is thrown while attempting to work with the date format, use that opportunity to attempt to parse that original date with the given date format pattern, then convert it into a string using 'Instant#toString'. If the conversion fails, simply return an empty Optional and let the rest of the code handle it from there since the new value will be overwritten in any case. Signed-off-by: Chris Sdogkos --- .../tjbot/features/rss/RSSHandlerRoutine.java | 52 +++++++++++++++---- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java b/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java index 617bacf457..55f262269e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.time.Instant; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.time.temporal.ChronoUnit; @@ -188,16 +189,24 @@ private Optional> prepareItemPostPredicate(RSSFeed feedConfig, return Optional.empty(); } - Optional lastSavedDate = - getLastSavedDateFromDatabaseRecord(rssFeedRecord.orElseThrow()); + Instant lastSavedDate; + try { + lastSavedDate = getLastSavedDateFromDatabaseRecord(rssFeedRecord.orElseThrow()); + } catch (DateTimeParseException _) { + Optional convertedDate = convertDateTimeToInstant(feedConfig); - if (lastSavedDate.isEmpty()) { - return Optional.empty(); + if (convertedDate.isEmpty()) { + return Optional.empty(); + } + + lastSavedDate = convertedDate.get(); } + final Instant convertedLastSavedDate = lastSavedDate; + return Optional.of(item -> { Instant itemPubDate = getDateTimeFromItem(item, feedConfig.dateFormatterPattern()); - return itemPubDate.isAfter(lastSavedDate.orElseThrow()); + return itemPubDate.isAfter(convertedLastSavedDate); }); } @@ -222,13 +231,9 @@ private Optional getRssFeedRecordFromDatabase(RSSFeed feedConfig) * @return An {@link Optional} containing the last saved date if it could be retrieved and * parsed successfully, otherwise an empty {@link Optional} */ - private Optional getLastSavedDateFromDatabaseRecord(RssFeedRecord rssRecord) + private Instant getLastSavedDateFromDatabaseRecord(RssFeedRecord rssRecord) throws DateTimeParseException { - try { - return Optional.of(Instant.parse(rssRecord.getLastDate())); - } catch (DateTimeParseException _) { - return Optional.empty(); - } + return Instant.parse(rssRecord.getLastDate()); } /** @@ -332,6 +337,31 @@ private static boolean isValidDateFormat(Item rssItem, RSSFeed feedConfig) { return true; } + private Optional convertDateTimeToInstant(RSSFeed feedConfig) { + Optional feedOptional = getRssFeedRecordFromDatabase(feedConfig); + String dateTimeFormat = feedConfig.dateFormatterPattern(); + + if (feedOptional.isEmpty() || dateTimeFormat.isEmpty()) { + return Optional.empty(); + } + + RssFeedRecord feedRecord = feedOptional.get(); + String lastDate = feedRecord.getLastDate(); + + ZonedDateTime zonedDateTime; + try { + zonedDateTime = + ZonedDateTime.parse(lastDate, DateTimeFormatter.ofPattern(dateTimeFormat)); + } catch (DateTimeParseException exception) { + logger.error( + "Attempted to convert date time from database ({}) to instant, but failed:", + lastDate, exception); + return Optional.empty(); + } + + return Optional.of(zonedDateTime.toInstant()); + } + /** * Attempts to find text channels from a given RSS feed configuration. *