From e46c842b3eb90a737b0c8406bef788aae2ed698d Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Thu, 6 Nov 2025 22:21:33 +0700 Subject: [PATCH] Add app score checking and refactor app info retrieval Introduces CheckAppScoreCommand to display app scores and reviews/ratings for Apple and Google apps. Refactors AppStoreScraper and GooglePlayScraper to provide methods for retrieving app scores, reviews, and updated dates, and improves AddAppleAppCommand and AddGoogleAppCommand to validate and fetch app info before adding. Updates AdminRepository to use a common collection and single-key logic, and modifies Constant to use a common collection name. --- .../api/apple/AppStoreScraper.java | 35 ++++++++-- .../api/apple/request/AppleAppRequest.java | 8 ++- .../api/google/GooglePlayScraper.java | 30 +++++++-- .../bot/command/AddAppleAppCommand.java | 29 +++++++- .../bot/command/AddGoogleAppCommand.java | 19 +++++- .../bot/command/CheckAppCommand.java | 3 +- .../bot/command/CheckAppScoreCommand.java | 67 +++++++++++++++++++ .../storescraperbot/constant/Constant.java | 2 +- .../model/entity/AppleAppInfo.java | 3 + .../model/entity/GoogleAppInfo.java | 3 + .../repository/AbstractRepository.java | 8 ++- .../repository/AdminRepository.java | 45 +++++++++++-- 12 files changed, 228 insertions(+), 24 deletions(-) create mode 100644 src/main/java/com/miti99/storescraperbot/bot/command/CheckAppScoreCommand.java create mode 100644 src/main/java/com/miti99/storescraperbot/model/entity/AppleAppInfo.java create mode 100644 src/main/java/com/miti99/storescraperbot/model/entity/GoogleAppInfo.java diff --git a/src/main/java/com/miti99/storescraperbot/api/apple/AppStoreScraper.java b/src/main/java/com/miti99/storescraperbot/api/apple/AppStoreScraper.java index a518cf1..be55fbd 100644 --- a/src/main/java/com/miti99/storescraperbot/api/apple/AppStoreScraper.java +++ b/src/main/java/com/miti99/storescraperbot/api/apple/AppStoreScraper.java @@ -43,24 +43,45 @@ public class AppStoreScraper { } } - public static LocalDate getLastUpdateOfApp(String appId) { + public static AppleAppResponse getResponse(String appId) { boolean isInCache = AppleAppRepository.INSTANCE.exist(appId); - AppleAppResponse response = null; if (isInCache) { var app = AppleAppRepository.INSTANCE.load(appId); - response = app.getApp(); + return app.getApp(); } else { - response = app(new AppleAppRequest(appId)); + var response = app(new AppleAppRequest(appId)); AppleAppRepository.INSTANCE.init(appId); var app = AppleAppRepository.INSTANCE.load(appId); app.setApp(response); AppleAppRepository.INSTANCE.save(appId, app); + return response; } - if (response != null) { - return LocalDate.ofInstant(Instant.parse(response.updated()), ZoneId.systemDefault()); - } else { + } + + public static LocalDate getAppUpdated(String appId) { + var response = getResponse(appId); + if (response == null) { log.error("response is null"); return LocalDate.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); } + return LocalDate.ofInstant(Instant.parse(response.updated()), ZoneId.systemDefault()); + } + + public static double getAppScore(String appId) { + var response = getResponse(appId); + if (response == null) { + log.error("response is null"); + return 0.0; + } + return response.score(); + } + + public static long getAppReviews(String appId) { + var response = getResponse(appId); + if (response == null) { + log.error("response is null"); + return 0L; + } + return response.reviews(); } } diff --git a/src/main/java/com/miti99/storescraperbot/api/apple/request/AppleAppRequest.java b/src/main/java/com/miti99/storescraperbot/api/apple/request/AppleAppRequest.java index 526c97b..5541cd8 100644 --- a/src/main/java/com/miti99/storescraperbot/api/apple/request/AppleAppRequest.java +++ b/src/main/java/com/miti99/storescraperbot/api/apple/request/AppleAppRequest.java @@ -1,5 +1,11 @@ package com.miti99.storescraperbot.api.apple.request; -public record AppleAppRequest(String appId) { +public record AppleAppRequest(String appId, Long id) { + public AppleAppRequest(String appId) { + this(appId, null); + } + public AppleAppRequest(Long id) { + this(null, id); + } } diff --git a/src/main/java/com/miti99/storescraperbot/api/google/GooglePlayScraper.java b/src/main/java/com/miti99/storescraperbot/api/google/GooglePlayScraper.java index 159d466..b643943 100644 --- a/src/main/java/com/miti99/storescraperbot/api/google/GooglePlayScraper.java +++ b/src/main/java/com/miti99/storescraperbot/api/google/GooglePlayScraper.java @@ -43,19 +43,23 @@ public class GooglePlayScraper { } } - public static LocalDate getLastUpdateOfApp(String appId) { + private static GoogleAppResponse getResponse(String appId) { boolean isInCache = GoogleAppRepository.INSTANCE.exist(appId); - GoogleAppResponse response = null; if (isInCache) { var app = GoogleAppRepository.INSTANCE.load(appId); - response = app.getApp(); + return app.getApp(); } else { - response = app(new GoogleAppRequest(appId)); + var response = app(new GoogleAppRequest(appId)); GoogleAppRepository.INSTANCE.init(appId); var app = GoogleAppRepository.INSTANCE.load(appId); app.setApp(response); GoogleAppRepository.INSTANCE.save(appId, app); + return response; } + } + + public static LocalDate getLastUpdateOfApp(String appId) { + var response = getResponse(appId); long lastUpdateMillis = 0; if (response != null) { lastUpdateMillis = response.updated(); @@ -64,4 +68,22 @@ public class GooglePlayScraper { } return LocalDate.ofInstant(Instant.ofEpochMilli(lastUpdateMillis), ZoneId.systemDefault()); } + + public static double getAppScore(String appId) { + var response = getResponse(appId); + if (response == null) { + log.error("response is null"); + return 0.0; + } + return response.score(); + } + + public static long getAppRatings(String appId) { + var response = getResponse(appId); + if (response == null) { + log.error("response is null"); + return 0L; + } + return response.ratings(); + } } diff --git a/src/main/java/com/miti99/storescraperbot/bot/command/AddAppleAppCommand.java b/src/main/java/com/miti99/storescraperbot/bot/command/AddAppleAppCommand.java index 631e3df..dac916d 100644 --- a/src/main/java/com/miti99/storescraperbot/bot/command/AddAppleAppCommand.java +++ b/src/main/java/com/miti99/storescraperbot/bot/command/AddAppleAppCommand.java @@ -1,12 +1,17 @@ package com.miti99.storescraperbot.bot.command; +import com.miti99.storescraperbot.api.apple.AppStoreScraper; +import com.miti99.storescraperbot.api.apple.request.AppleAppRequest; +import com.miti99.storescraperbot.api.apple.response.AppleAppResponse; import com.miti99.storescraperbot.bot.StoreScrapeBotTelegramClient; import com.miti99.storescraperbot.repository.AdminRepository; import com.miti99.storescraperbot.repository.GroupRepository; +import lombok.extern.log4j.Log4j2; import org.telegram.telegrambots.meta.api.objects.User; import org.telegram.telegrambots.meta.api.objects.chat.Chat; import org.telegram.telegrambots.meta.generics.TelegramClient; +@Log4j2 public class AddAppleAppCommand extends BaseStoreScraperBotCommand { public static final AddAppleAppCommand INSTANCE = new AddAppleAppCommand(); @@ -24,12 +29,32 @@ public class AddAppleAppCommand extends BaseStoreScraperBotCommand { return; } - if (arguments.length != 1) { + if (arguments.length < 1 || arguments.length > 2) { StoreScrapeBotTelegramClient.INSTANCE.sendMessage(chat.getId(), "Invalid arguments"); return; } var appId = arguments[0]; + long id = -1; + AppleAppResponse response = null; + try { + try { + id = Long.parseLong(appId); + } catch (Exception e) { + // Input không phải id, bỏ qua + } + if (id != -1) { + response = AppStoreScraper.app(new AppleAppRequest(id)); + } else { + response = AppStoreScraper.app(new AppleAppRequest(appId)); + } + } catch (Exception e) { + log.error("request app error for appId: '{}', id: '{}'", appId, id, e); + StoreScrapeBotTelegramClient.INSTANCE.sendMessage(chat.getId(), "Error when request app info"); + return; + } + appId = response.appId(); + long groupId = chat.getId(); var group = GroupRepository.INSTANCE.load(groupId); @@ -40,6 +65,6 @@ public class AddAppleAppCommand extends BaseStoreScraperBotCommand { group.getAppleApps().add(appId); GroupRepository.INSTANCE.save(groupId, group); - StoreScrapeBotTelegramClient.INSTANCE.sendMessage(chat.getId(), "Apple app added successfully"); + StoreScrapeBotTelegramClient.INSTANCE.sendMessage(chat.getId(), "Apple app %s added successfully".formatted(appId)); } } diff --git a/src/main/java/com/miti99/storescraperbot/bot/command/AddGoogleAppCommand.java b/src/main/java/com/miti99/storescraperbot/bot/command/AddGoogleAppCommand.java index ed85769..9a83276 100644 --- a/src/main/java/com/miti99/storescraperbot/bot/command/AddGoogleAppCommand.java +++ b/src/main/java/com/miti99/storescraperbot/bot/command/AddGoogleAppCommand.java @@ -1,17 +1,22 @@ package com.miti99.storescraperbot.bot.command; +import com.miti99.storescraperbot.api.google.GooglePlayScraper; +import com.miti99.storescraperbot.api.google.request.GoogleAppRequest; +import com.miti99.storescraperbot.api.google.response.GoogleAppResponse; import com.miti99.storescraperbot.bot.StoreScrapeBotTelegramClient; import com.miti99.storescraperbot.repository.AdminRepository; import com.miti99.storescraperbot.repository.GroupRepository; +import lombok.extern.log4j.Log4j2; import org.telegram.telegrambots.meta.api.objects.User; import org.telegram.telegrambots.meta.api.objects.chat.Chat; import org.telegram.telegrambots.meta.generics.TelegramClient; +@Log4j2 public class AddGoogleAppCommand extends BaseStoreScraperBotCommand { public static final AddGoogleAppCommand INSTANCE = new AddGoogleAppCommand(); AddGoogleAppCommand() { - super("addgoogle", ". Thêm Google app vào danh sách theo dõi của nhóm"); + super("addgoogle", " [country]. Thêm Google app vào danh sách theo dõi của nhóm. Một số app cần country để hoạt động đúng"); } @Override @@ -30,6 +35,16 @@ public class AddGoogleAppCommand extends BaseStoreScraperBotCommand { } var appId = arguments[0]; + GoogleAppResponse response = null; + try { + response = GooglePlayScraper.app(new GoogleAppRequest(appId)); + } catch (Exception e) { + log.error("request app error for appId: '{}'", appId, e); + StoreScrapeBotTelegramClient.INSTANCE.sendMessage( + chat.getId(), "Error when request app info"); + return; + } + long groupId = chat.getId(); var group = GroupRepository.INSTANCE.load(groupId); @@ -42,6 +57,6 @@ public class AddGoogleAppCommand extends BaseStoreScraperBotCommand { group.getGoogleApps().add(appId); GroupRepository.INSTANCE.save(groupId, group); StoreScrapeBotTelegramClient.INSTANCE.sendMessage( - chat.getId(), "Google app added successfully"); + chat.getId(), "Google app %s added successfully".formatted(appId)); } } diff --git a/src/main/java/com/miti99/storescraperbot/bot/command/CheckAppCommand.java b/src/main/java/com/miti99/storescraperbot/bot/command/CheckAppCommand.java index eca8d0c..be60201 100644 --- a/src/main/java/com/miti99/storescraperbot/bot/command/CheckAppCommand.java +++ b/src/main/java/com/miti99/storescraperbot/bot/command/CheckAppCommand.java @@ -7,7 +7,6 @@ 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.LocalDateTime; import java.time.temporal.ChronoUnit; import org.telegram.telegrambots.meta.api.objects.User; import org.telegram.telegrambots.meta.api.objects.chat.Chat; @@ -46,7 +45,7 @@ public class CheckAppCommand extends BaseStoreScraperBotCommand { sb.append("-".repeat(46)); sb.append("\n"); for (var appId : group.getAppleApps()) { - var updated = AppStoreScraper.getLastUpdateOfApp(appId); + var updated = AppStoreScraper.getAppUpdated(appId); long days = ChronoUnit.DAYS.between(updated, now); boolean passed = days <= Constant.NUM_DAYS_WARNING_NOT_UPDATED; sb.append( diff --git a/src/main/java/com/miti99/storescraperbot/bot/command/CheckAppScoreCommand.java b/src/main/java/com/miti99/storescraperbot/bot/command/CheckAppScoreCommand.java new file mode 100644 index 0000000..850172d --- /dev/null +++ b/src/main/java/com/miti99/storescraperbot/bot/command/CheckAppScoreCommand.java @@ -0,0 +1,67 @@ +package com.miti99.storescraperbot.bot.command; + +import com.miti99.storescraperbot.api.apple.AppStoreScraper; +import com.miti99.storescraperbot.api.google.GooglePlayScraper; +import com.miti99.storescraperbot.bot.StoreScrapeBotTelegramClient; +import com.miti99.storescraperbot.constant.Constant; +import com.miti99.storescraperbot.repository.AdminRepository; +import com.miti99.storescraperbot.repository.GroupRepository; +import java.time.temporal.ChronoUnit; +import org.telegram.telegrambots.meta.api.objects.User; +import org.telegram.telegrambots.meta.api.objects.chat.Chat; +import org.telegram.telegrambots.meta.generics.TelegramClient; + +public class CheckAppScoreCommand extends BaseStoreScraperBotCommand { + public static final CheckAppScoreCommand INSTANCE = new CheckAppScoreCommand(); + + CheckAppScoreCommand() { + super("checkappscore", "Kiểm tra điểm đánh giá các app (sao)"); + } + + @Override + protected void executeCommand( + TelegramClient telegramClient, User user, Chat chat, String[] arguments) { + var admin = AdminRepository.INSTANCE.load(); + if (!admin.getGroups().contains(chat.getId())) { + StoreScrapeBotTelegramClient.INSTANCE.sendMessage( + chat.getId(), "Group is not allowed to use bot"); + return; + } + + if (arguments.length != 0) { + StoreScrapeBotTelegramClient.INSTANCE.sendMessage(chat.getId(), "Invalid arguments"); + return; + } + + long groupId = chat.getId(); + var group = GroupRepository.INSTANCE.load(groupId); + + var sb = new StringBuilder(); + sb.append("Apple Apps:\n"); + sb.append("\n"); + sb.append("%-20s | %-10s | %-10s\n".formatted("AppId", "Score", "Reviews")); + sb.append("-".repeat(32)); + sb.append("\n"); + for (var appId : group.getAppleApps()) { + double score = AppStoreScraper.getAppScore(appId); + long reviews = AppStoreScraper.getAppReviews(appId); + sb.append("%-20s | %-10s | %-10s\n".formatted(appId, score, reviews)); + } + sb.append("\n"); + sb.append("\n"); + sb.append("Google Apps:\n"); + sb.append("\n"); + sb.append("%-20s | %-10s | %-10s\n".formatted("AppId", "Score", "Ratings")); + sb.append("-".repeat(46)); + sb.append("\n"); + for (var appId : group.getGoogleApps()) { + double score = GooglePlayScraper.getAppScore(appId); + long ratings = GooglePlayScraper.getAppRatings(appId); + sb.append("%-20s | %-10s | %-10s\n".formatted(appId, score, ratings)); + } + sb.append(""); + sb.append("\n"); + + StoreScrapeBotTelegramClient.INSTANCE.sendMessage(chat.getId(), 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 24b3445..8f1859c 100644 --- a/src/main/java/com/miti99/storescraperbot/constant/Constant.java +++ b/src/main/java/com/miti99/storescraperbot/constant/Constant.java @@ -1,7 +1,7 @@ package com.miti99.storescraperbot.constant; public class Constant { - public static final String APP_NAME = "store_scraper_bot"; + 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; } diff --git a/src/main/java/com/miti99/storescraperbot/model/entity/AppleAppInfo.java b/src/main/java/com/miti99/storescraperbot/model/entity/AppleAppInfo.java new file mode 100644 index 0000000..10a6305 --- /dev/null +++ b/src/main/java/com/miti99/storescraperbot/model/entity/AppleAppInfo.java @@ -0,0 +1,3 @@ +package com.miti99.storescraperbot.model.entity; + +public class AppleAppInfo {} diff --git a/src/main/java/com/miti99/storescraperbot/model/entity/GoogleAppInfo.java b/src/main/java/com/miti99/storescraperbot/model/entity/GoogleAppInfo.java new file mode 100644 index 0000000..eb51c94 --- /dev/null +++ b/src/main/java/com/miti99/storescraperbot/model/entity/GoogleAppInfo.java @@ -0,0 +1,3 @@ +package com.miti99.storescraperbot.model.entity; + +public class GoogleAppInfo {} diff --git a/src/main/java/com/miti99/storescraperbot/repository/AbstractRepository.java b/src/main/java/com/miti99/storescraperbot/repository/AbstractRepository.java index d2865b8..594ab5a 100644 --- a/src/main/java/com/miti99/storescraperbot/repository/AbstractRepository.java +++ b/src/main/java/com/miti99/storescraperbot/repository/AbstractRepository.java @@ -16,9 +16,15 @@ public abstract class AbstractRepository> { // protected final Class classK = getKeyClass(); protected final Class classV = getDataClass(); protected final String scopeName = Config.ENV.name().toLowerCase(); - protected final String collectionName = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, classV.getSimpleName()); + protected final String collectionName; + + protected AbstractRepository(String collectionName) { + this.collectionName = collectionName; + CouchbaseUtil.createCollection(scopeName, collectionName); + } protected AbstractRepository() { + collectionName = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, classV.getSimpleName()); CouchbaseUtil.createCollection(scopeName, collectionName); } diff --git a/src/main/java/com/miti99/storescraperbot/repository/AdminRepository.java b/src/main/java/com/miti99/storescraperbot/repository/AdminRepository.java index 5911acf..a24f5a8 100644 --- a/src/main/java/com/miti99/storescraperbot/repository/AdminRepository.java +++ b/src/main/java/com/miti99/storescraperbot/repository/AdminRepository.java @@ -1,20 +1,57 @@ package com.miti99.storescraperbot.repository; +import com.miti99.storescraperbot.constant.Constant; import com.miti99.storescraperbot.model.Admin; -/** Đây là repository chỉ chứa 1 key duy nhất, key là "admin" */ +/** + * Đây là repository chỉ chứa 1 key duy nhất. Nên lưu trong default collection của Couchbase + * ("_default") + * + *

TODO: Refactor các logic của một single key repository sang abstract class để dễ hiểu hơn. + * Code hiện tại này chỉ là trick tạm thời nên chưa tốt lắm. Cần thiết kế lại tốt hơn + */ public class AdminRepository extends AbstractRepository { public static final AdminRepository INSTANCE = new AdminRepository(); + public static final String KEY = "admin"; + + public AdminRepository() { + super(Constant.COMMON_COLLECTION_NAME); + } + + @Override + public void init(String key) { + super.init(KEY); + } + + @Override + public void save(String key, Admin data) { + super.save(KEY, data); + } + + @Override + public boolean exist(String key) { + return super.exist(KEY); + } + + @Override + public Admin load(String key) { + return super.load(KEY); + } + + @Override + public void delete(String key) { + super.delete(KEY); + } public void init() { - init("admin"); + init(KEY); } public Admin load() { - return load("admin"); + return load(KEY); } public void save(Admin data) { - save("admin", data); + save(KEY, data); } }