From 73495010868e28b7f03783f2ac3c5ea1c0e35d19 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Fri, 7 Nov 2025 23:45:30 +0700 Subject: [PATCH] Add scheduled app update checks and notifications Introduces a scheduled task that checks for apps not updated within a warning threshold and sends notifications to groups. Adds scheduling utilities, constants for scheduling, and logic to format and send update reports for both Apple and Google apps. Also includes a new NonUpdatedApp record and improvements to the Table class for better formatting. --- .../java/com/miti99/storescraperbot/Main.java | 26 +++++ .../storescraperbot/bot/StoreScrapeBot.java | 97 +++++++++++++++++++ .../bot/entity/NonUpdatedApp.java | 5 + .../storescraperbot/bot/table/Table.java | 6 +- .../storescraperbot/constant/Constant.java | 12 +++ .../storescraperbot/util/SchedulerUtil.java | 12 +++ 6 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/miti99/storescraperbot/bot/entity/NonUpdatedApp.java create mode 100644 src/main/java/com/miti99/storescraperbot/util/SchedulerUtil.java diff --git a/src/main/java/com/miti99/storescraperbot/Main.java b/src/main/java/com/miti99/storescraperbot/Main.java index c8ade4e..89c1b9a 100644 --- a/src/main/java/com/miti99/storescraperbot/Main.java +++ b/src/main/java/com/miti99/storescraperbot/Main.java @@ -1,5 +1,8 @@ package com.miti99.storescraperbot; +import static com.miti99.storescraperbot.constant.Constant.SCHEDULE_CHECK_APP_TIME; +import static com.miti99.storescraperbot.constant.Constant.SECONDS_PER_DAY; +import static com.miti99.storescraperbot.constant.Constant.VIETNAM_ZONE_ID; import static com.miti99.storescraperbot.env.Environment.CREATOR_ID; import static com.miti99.storescraperbot.env.Environment.SOURCE_COMMIT; @@ -8,6 +11,12 @@ import com.miti99.storescraperbot.bot.StoreScrapeBotTelegramClient; import com.miti99.storescraperbot.env.Environment; import com.miti99.storescraperbot.repository.AdminRepository; import com.miti99.storescraperbot.type.Env; +import com.miti99.storescraperbot.util.SchedulerUtil; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.concurrent.TimeUnit; import lombok.extern.log4j.Log4j2; import org.telegram.telegrambots.longpolling.TelegramBotsLongPollingApplication; @@ -24,9 +33,26 @@ public class Main { StoreScrapeBotTelegramClient.INSTANCE.sendMessage( CREATOR_ID, "Bot started! Version %s".formatted(SOURCE_COMMIT)); } + scheduleCheckApp(); Thread.currentThread().join(); } catch (Exception e) { log.error("Error while running bot", e); } } + + private static void scheduleCheckApp() { + var now = LocalDateTime.now(); + + var checkTime = + LocalDateTime.of(LocalDate.now(VIETNAM_ZONE_ID), SCHEDULE_CHECK_APP_TIME) + .atZone(VIETNAM_ZONE_ID) + .withZoneSameInstant(ZoneId.systemDefault()) + .toLocalDateTime(); + long initialDelay = Duration.between(now, checkTime).getSeconds(); + if (initialDelay < 0) { + initialDelay += SECONDS_PER_DAY; + } + SchedulerUtil.SCHEDULER.scheduleAtFixedRate( + StoreScrapeBot.INSTANCE::runCheckApp, initialDelay, SECONDS_PER_DAY, TimeUnit.SECONDS); + } } diff --git a/src/main/java/com/miti99/storescraperbot/bot/StoreScrapeBot.java b/src/main/java/com/miti99/storescraperbot/bot/StoreScrapeBot.java index 8670c0c..513a7d9 100644 --- a/src/main/java/com/miti99/storescraperbot/bot/StoreScrapeBot.java +++ b/src/main/java/com/miti99/storescraperbot/bot/StoreScrapeBot.java @@ -1,5 +1,10 @@ package com.miti99.storescraperbot.bot; +import static com.miti99.storescraperbot.constant.Constant.VIETNAM_ZONE_ID; +import static com.miti99.storescraperbot.constant.Constant.WEEKENDS; + +import com.miti99.storescraperbot.api.apple.AppStoreScraper; +import com.miti99.storescraperbot.api.google.GooglePlayScraper; import com.miti99.storescraperbot.bot.command.AddAppleAppCommand; import com.miti99.storescraperbot.bot.command.AddGoogleAppCommand; import com.miti99.storescraperbot.bot.command.AddGroupCommand; @@ -10,9 +15,19 @@ import com.miti99.storescraperbot.bot.command.DeleteGroupCommand; import com.miti99.storescraperbot.bot.command.InfoCommand; import com.miti99.storescraperbot.bot.command.ListAppCommand; import com.miti99.storescraperbot.bot.command.ListGroupCommand; +import com.miti99.storescraperbot.bot.entity.NonUpdatedApp; +import com.miti99.storescraperbot.bot.table.Table; +import com.miti99.storescraperbot.constant.Constant; +import com.miti99.storescraperbot.repository.AdminRepository; +import com.miti99.storescraperbot.repository.GroupRepository; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import lombok.extern.log4j.Log4j2; import org.telegram.telegrambots.extensions.bots.commandbot.CommandLongPollingTelegramBot; +import org.telegram.telegrambots.meta.api.methods.ParseMode; import org.telegram.telegrambots.meta.api.methods.commands.SetMyCommands; +import org.telegram.telegrambots.meta.api.methods.send.SendMessage; import org.telegram.telegrambots.meta.api.objects.Update; import org.telegram.telegrambots.meta.api.objects.commands.BotCommand; import org.telegram.telegrambots.meta.exceptions.TelegramApiException; @@ -56,4 +71,86 @@ public class StoreScrapeBot extends CommandLongPollingTelegramBot { public void processNonCommandUpdate(Update update) { // Ignore } + + public void runCheckApp() { + var admin = AdminRepository.INSTANCE.load(); + for (var groupId : admin.getGroups()) { + checkAppForGroup(groupId); + } + } + + public void checkAppForGroup(long groupId) { + var group = GroupRepository.INSTANCE.load(groupId); + var now = LocalDate.now(); + + var nonUpdatedAppleApps = new ArrayList(); + for (var app : group.getAppleApps()) { + var appId = app.appId(); + var updated = AppStoreScraper.getAppUpdated(appId, app.country()); + long days = ChronoUnit.DAYS.between(updated, now); + if (days > Constant.NUM_DAYS_WARNING_NOT_UPDATED) { + nonUpdatedAppleApps.add(new NonUpdatedApp(appId, updated, days)); + } + } + var nonUpdatedGoogleApps = new ArrayList(); + for (var app : group.getGoogleApps()) { + var appId = app.appId(); + var updated = GooglePlayScraper.getLastUpdateOfApp(appId, app.country()); + long days = ChronoUnit.DAYS.between(updated, now); + if (days > Constant.NUM_DAYS_WARNING_NOT_UPDATED) { + nonUpdatedGoogleApps.add(new NonUpdatedApp(appId, updated, days)); + } + } + int numNonUpdatedApps = nonUpdatedAppleApps.size() + nonUpdatedGoogleApps.size(); + if (numNonUpdatedApps == 0) return; + + var sb = + new StringBuilder("You have %d app(s) need to be updated!\n".formatted(numNonUpdatedApps)); + if (!nonUpdatedAppleApps.isEmpty()) { + sb.append("%d Apple Apps:\n".formatted(nonUpdatedAppleApps.size())); + sb.append("\n"); + var appleTable = new Table("#", "AppId", "Updated", "Days"); + int i = 0; + for (var app : nonUpdatedAppleApps) { + i++; + var appId = app.appId(); + var updated = app.updated(); + long days = app.days(); + appleTable.addRow(i, appId, updated, days); + } + sb.append(appleTable); + sb.append("\n"); + } + + if (!nonUpdatedGoogleApps.isEmpty()) { + sb.append("%d Google Apps:\n".formatted(nonUpdatedGoogleApps.size())); + sb.append("\n"); + var googleTable = new Table("#", "AppId", "Updated", "Days"); + int i = 0; + for (var app : nonUpdatedGoogleApps) { + i++; + var appId = app.appId(); + var updated = app.updated(); + long days = app.days(); + googleTable.addRow(i, appId, updated, days); + } + sb.append(googleTable); + sb.append(""); + } + + var dayOfWeek = LocalDate.now(VIETNAM_ZONE_ID).getDayOfWeek(); + boolean mute = WEEKENDS.contains(dayOfWeek); + try { + var sendMessage = + SendMessage.builder() + .parseMode(ParseMode.HTML) + .chatId(groupId) + .disableNotification(mute) + .text(sb.toString()) + .build(); + StoreScrapeBotTelegramClient.INSTANCE.execute(sendMessage); + } catch (Exception e) { + log.error("sendMessage error", e); + } + } } diff --git a/src/main/java/com/miti99/storescraperbot/bot/entity/NonUpdatedApp.java b/src/main/java/com/miti99/storescraperbot/bot/entity/NonUpdatedApp.java new file mode 100644 index 0000000..c3ba838 --- /dev/null +++ b/src/main/java/com/miti99/storescraperbot/bot/entity/NonUpdatedApp.java @@ -0,0 +1,5 @@ +package com.miti99.storescraperbot.bot.entity; + +import java.time.LocalDate; + +public record NonUpdatedApp(String appId, LocalDate updated, long days) {} diff --git a/src/main/java/com/miti99/storescraperbot/bot/table/Table.java b/src/main/java/com/miti99/storescraperbot/bot/table/Table.java index e976195..5350721 100644 --- a/src/main/java/com/miti99/storescraperbot/bot/table/Table.java +++ b/src/main/java/com/miti99/storescraperbot/bot/table/Table.java @@ -44,9 +44,13 @@ public class Table { sb.append(formater.formatted((Object[]) headers)); var rule = Arrays.stream(maxWidths).mapToObj("─"::repeat).collect(Collectors.joining("─┼─", "", "\n")); - sb.append(rule); + int i = 0; for (var row : rows) { + if (i % 5 == 0) { + sb.append(rule); + } sb.append(formater.formatted((Object[]) row)); + i++; } return sb.toString(); } diff --git a/src/main/java/com/miti99/storescraperbot/constant/Constant.java b/src/main/java/com/miti99/storescraperbot/constant/Constant.java index 8f1859c..de5f361 100644 --- a/src/main/java/com/miti99/storescraperbot/constant/Constant.java +++ b/src/main/java/com/miti99/storescraperbot/constant/Constant.java @@ -1,7 +1,19 @@ package com.miti99.storescraperbot.constant; +import java.time.DayOfWeek; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.Set; + public class Constant { public static final String COMMON_COLLECTION_NAME = "common"; public static final long APP_CACHE_SECONDS = 600; public static final long NUM_DAYS_WARNING_NOT_UPDATED = 30; + public static final LocalTime SCHEDULE_CHECK_APP_TIME = LocalTime.of(7, 0); + + public static final String VIETNAM_ZONE_ID_STRING = "Asia/Ho_Chi_Minh"; + public static final ZoneId VIETNAM_ZONE_ID = ZoneId.of(VIETNAM_ZONE_ID_STRING); + public static final long SECONDS_PER_DAY = ChronoUnit.DAYS.getDuration().getSeconds(); + public static final Set WEEKENDS = Set.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY); } diff --git a/src/main/java/com/miti99/storescraperbot/util/SchedulerUtil.java b/src/main/java/com/miti99/storescraperbot/util/SchedulerUtil.java new file mode 100644 index 0000000..3eac3af --- /dev/null +++ b/src/main/java/com/miti99/storescraperbot/util/SchedulerUtil.java @@ -0,0 +1,12 @@ +package com.miti99.storescraperbot.util; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class SchedulerUtil { + public static final ScheduledExecutorService SCHEDULER = + Executors.newSingleThreadScheduledExecutor(); +}