66import java .net .URI ;
77import java .nio .file .Files ;
88import java .nio .file .Path ;
9+ import java .time .Duration ;
910import java .util .Collection ;
1011import java .util .HashMap ;
1112import java .util .Map ;
2223import me .itzg .helpers .curseforge .model .ModsSearchResponse ;
2324import me .itzg .helpers .errors .GenericException ;
2425import me .itzg .helpers .errors .InvalidParameterException ;
26+ import me .itzg .helpers .errors .RateLimitException ;
2527import me .itzg .helpers .http .FailedRequestException ;
2628import me .itzg .helpers .http .Fetch ;
2729import me .itzg .helpers .http .FileDownloadStatusHandler ;
2830import me .itzg .helpers .http .SharedFetch ;
31+ import me .itzg .helpers .http .SharedFetch .Options ;
2932import me .itzg .helpers .http .UriBuilder ;
3033import me .itzg .helpers .json .ObjectMappers ;
3134import org .slf4j .Logger ;
@@ -41,18 +44,26 @@ public class CurseForgeApiClient implements AutoCloseable {
4144 public static final String CATEGORY_MC_MODS = "mc-mods" ;
4245 public static final String CATEGORY_BUKKIT_PLUGINS = "bukkit-plugins" ;
4346 public static final String CATEGORY_WORLDS = "worlds" ;
47+ public static final String API_KEY_VAR = "CF_API_KEY" ;
48+ public static final String ETERNAL_DEVELOPER_CONSOLE_URL = "https://console.curseforge.com/" ;
4449
4550 private static final String API_KEY_HEADER = "x-api-key" ;
4651 static final String MINECRAFT_GAME_ID = "432" ;
4752
53+ public static final String OP_SEARCH_MOD_WITH_GAME_ID_SLUG_CLASS_ID = "searchModWithGameIdSlugClassId" ;
54+ private static final Map <String , Duration > CACHE_DURATIONS = new HashMap <>();
55+ static {
56+ CACHE_DURATIONS .put (OP_SEARCH_MOD_WITH_GAME_ID_SLUG_CLASS_ID , Duration .ofHours (1 ));
57+ }
58+
4859 private final SharedFetch preparedFetch ;
4960 private final UriBuilder uriBuilder ;
5061 private final UriBuilder downloadFallbackUriBuilder ;
5162 private final String gameId ;
5263
5364 private final ApiCaching apiCaching ;
5465
55- public CurseForgeApiClient (String apiBaseUrl , String apiKey , SharedFetch . Options sharedFetchOptions , String gameId ,
66+ public CurseForgeApiClient (String apiBaseUrl , String apiKey , Options sharedFetchOptions , String gameId ,
5667 ApiCaching apiCaching
5768 ) {
5869 this .apiCaching = apiCaching ;
@@ -61,7 +72,7 @@ public CurseForgeApiClient(String apiBaseUrl, String apiKey, SharedFetch.Options
6172 }
6273
6374 this .preparedFetch = Fetch .sharedFetch ("install-curseforge" ,
64- (sharedFetchOptions != null ? sharedFetchOptions : SharedFetch . Options .builder ().build ())
75+ (sharedFetchOptions != null ? sharedFetchOptions : Options .builder ().build ())
6576 .withHeader (API_KEY_HEADER , apiKey .trim ())
6677 );
6778 this .uriBuilder = UriBuilder .withBaseUrl (apiBaseUrl );
@@ -87,6 +98,10 @@ static FileDownloadStatusHandler modFileDownloadStatusHandler(Path outputDir, Lo
8798 };
8899 }
89100
101+ public static Map <String , Duration > getCacheDurations () {
102+ return CACHE_DURATIONS ;
103+ }
104+
90105 @ Override
91106 public void close () {
92107 preparedFetch .close ();
@@ -111,28 +126,34 @@ Mono<CategoryInfo> loadCategoryInfo(Collection<String> applicableClassIdSlugs) {
111126
112127 return Mono .just (new CategoryInfo (contentClassIds , slugIds ));
113128 }
114- );
129+ )
130+ .onErrorMap (FailedRequestException ::isForbidden , this ::errorMapForbidden );
115131 }
116132
117133 Mono <CurseForgeMod > searchMod (String slug , int classId ) {
118- return preparedFetch .fetch (
119- uriBuilder .resolve ("/v1/mods/search?gameId={gameId}&slug={slug}&classId={classId}" ,
120- gameId , slug , classId
121- )
122- )
123- .toObject (ModsSearchResponse .class )
124- .assemble ()
125- .flatMap (searchResponse -> {
126- if (searchResponse .getData () == null || searchResponse .getData ().isEmpty ()) {
127- return Mono .error (new GenericException ("No mods found with slug=" + slug ));
128- }
129- else if (searchResponse .getData ().size () > 1 ) {
130- return Mono .error (new GenericException ("More than one mod found with slug=" + slug ));
131- }
132- else {
133- return Mono .just (searchResponse .getData ().get (0 ));
134- }
135- });
134+ return
135+ apiCaching .cache (OP_SEARCH_MOD_WITH_GAME_ID_SLUG_CLASS_ID , CurseForgeMod .class ,
136+ preparedFetch .fetch (
137+ uriBuilder .resolve ("/v1/mods/search?gameId={gameId}&slug={slug}&classId={classId}" ,
138+ gameId , slug , classId
139+ )
140+ )
141+ .toObject (ModsSearchResponse .class )
142+ .assemble ()
143+ .flatMap (searchResponse -> {
144+ if (searchResponse .getData () == null || searchResponse .getData ().isEmpty ()) {
145+ return Mono .error (new GenericException ("No mods found with slug=" + slug ));
146+ }
147+ else if (searchResponse .getData ().size () > 1 ) {
148+ return Mono .error (new GenericException ("More than one mod found with slug=" + slug ));
149+ }
150+ else {
151+ return Mono .just (searchResponse .getData ().get (0 ));
152+ }
153+ })
154+ .onErrorMap (FailedRequestException ::isForbidden , this ::errorMapForbidden ),
155+ gameId , slug , classId
156+ );
136157 }
137158
138159 /**
@@ -179,7 +200,8 @@ Mono<Integer> slugToId(CategoryInfo categoryInfo,
179200 .findFirst ()
180201 .map (CurseForgeMod ::getId )
181202 .orElseThrow (() -> new GenericException ("Unable to resolve slug into ID (no matches): " + slug ))
182- );
203+ )
204+ .onErrorMap (FailedRequestException ::isForbidden , this ::errorMapForbidden );
183205 }
184206
185207 public Mono <CurseForgeMod > getModInfo (
@@ -193,6 +215,7 @@ public Mono<CurseForgeMod> getModInfo(
193215 )
194216 .toObject (GetModResponse .class )
195217 .assemble ()
218+ .onErrorMap (FailedRequestException ::isForbidden , this ::errorMapForbidden )
196219 .checkpoint ("Getting mod info for " + projectID )
197220 .map (GetModResponse ::getData ),
198221 projectID
@@ -219,6 +242,7 @@ public Mono<CurseForgeFile> getModFileInfo(
219242 }
220243 return e ;
221244 })
245+ .onErrorMap (FailedRequestException ::isForbidden , this ::errorMapForbidden )
222246 .map (GetModFileResponse ::getData )
223247 .checkpoint (),
224248 projectID , fileID
@@ -285,4 +309,21 @@ private static URI normalizeDownloadUrl(String downloadUrl) {
285309 );
286310 }
287311
312+ public Throwable errorMapForbidden (Throwable throwable ) {
313+ final FailedRequestException e = (FailedRequestException ) throwable ;
314+
315+ log .debug ("Failed request details: {}" , e .toString ());
316+
317+ if (e .getBody ().contains ("There might be too much traffic" )) {
318+ return new RateLimitException (null , String .format ("Access to %s has been rate-limited." , uriBuilder .getBaseUrl ()), e );
319+ }
320+ else {
321+ return new InvalidParameterException (String .format ("Access to %s is forbidden or rate-limit has been exceeded."
322+ + " Ensure %s is set to a valid API key from %s or allow rate-limit to reset." ,
323+ uriBuilder .getBaseUrl (), API_KEY_VAR , ETERNAL_DEVELOPER_CONSOLE_URL
324+ ), e
325+ );
326+ }
327+
328+ }
288329}
0 commit comments