6
6
import java .net .URI ;
7
7
import java .nio .file .Files ;
8
8
import java .nio .file .Path ;
9
+ import java .time .Duration ;
9
10
import java .util .Collection ;
10
11
import java .util .HashMap ;
11
12
import java .util .Map ;
22
23
import me .itzg .helpers .curseforge .model .ModsSearchResponse ;
23
24
import me .itzg .helpers .errors .GenericException ;
24
25
import me .itzg .helpers .errors .InvalidParameterException ;
26
+ import me .itzg .helpers .errors .RateLimitException ;
25
27
import me .itzg .helpers .http .FailedRequestException ;
26
28
import me .itzg .helpers .http .Fetch ;
27
29
import me .itzg .helpers .http .FileDownloadStatusHandler ;
28
30
import me .itzg .helpers .http .SharedFetch ;
31
+ import me .itzg .helpers .http .SharedFetch .Options ;
29
32
import me .itzg .helpers .http .UriBuilder ;
30
33
import me .itzg .helpers .json .ObjectMappers ;
31
34
import org .slf4j .Logger ;
@@ -41,18 +44,26 @@ public class CurseForgeApiClient implements AutoCloseable {
41
44
public static final String CATEGORY_MC_MODS = "mc-mods" ;
42
45
public static final String CATEGORY_BUKKIT_PLUGINS = "bukkit-plugins" ;
43
46
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/" ;
44
49
45
50
private static final String API_KEY_HEADER = "x-api-key" ;
46
51
static final String MINECRAFT_GAME_ID = "432" ;
47
52
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
+
48
59
private final SharedFetch preparedFetch ;
49
60
private final UriBuilder uriBuilder ;
50
61
private final UriBuilder downloadFallbackUriBuilder ;
51
62
private final String gameId ;
52
63
53
64
private final ApiCaching apiCaching ;
54
65
55
- public CurseForgeApiClient (String apiBaseUrl , String apiKey , SharedFetch . Options sharedFetchOptions , String gameId ,
66
+ public CurseForgeApiClient (String apiBaseUrl , String apiKey , Options sharedFetchOptions , String gameId ,
56
67
ApiCaching apiCaching
57
68
) {
58
69
this .apiCaching = apiCaching ;
@@ -61,7 +72,7 @@ public CurseForgeApiClient(String apiBaseUrl, String apiKey, SharedFetch.Options
61
72
}
62
73
63
74
this .preparedFetch = Fetch .sharedFetch ("install-curseforge" ,
64
- (sharedFetchOptions != null ? sharedFetchOptions : SharedFetch . Options .builder ().build ())
75
+ (sharedFetchOptions != null ? sharedFetchOptions : Options .builder ().build ())
65
76
.withHeader (API_KEY_HEADER , apiKey .trim ())
66
77
);
67
78
this .uriBuilder = UriBuilder .withBaseUrl (apiBaseUrl );
@@ -87,6 +98,10 @@ static FileDownloadStatusHandler modFileDownloadStatusHandler(Path outputDir, Lo
87
98
};
88
99
}
89
100
101
+ public static Map <String , Duration > getCacheDurations () {
102
+ return CACHE_DURATIONS ;
103
+ }
104
+
90
105
@ Override
91
106
public void close () {
92
107
preparedFetch .close ();
@@ -111,28 +126,34 @@ Mono<CategoryInfo> loadCategoryInfo(Collection<String> applicableClassIdSlugs) {
111
126
112
127
return Mono .just (new CategoryInfo (contentClassIds , slugIds ));
113
128
}
114
- );
129
+ )
130
+ .onErrorMap (FailedRequestException ::isForbidden , this ::errorMapForbidden );
115
131
}
116
132
117
133
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
+ );
136
157
}
137
158
138
159
/**
@@ -179,7 +200,8 @@ Mono<Integer> slugToId(CategoryInfo categoryInfo,
179
200
.findFirst ()
180
201
.map (CurseForgeMod ::getId )
181
202
.orElseThrow (() -> new GenericException ("Unable to resolve slug into ID (no matches): " + slug ))
182
- );
203
+ )
204
+ .onErrorMap (FailedRequestException ::isForbidden , this ::errorMapForbidden );
183
205
}
184
206
185
207
public Mono <CurseForgeMod > getModInfo (
@@ -193,6 +215,7 @@ public Mono<CurseForgeMod> getModInfo(
193
215
)
194
216
.toObject (GetModResponse .class )
195
217
.assemble ()
218
+ .onErrorMap (FailedRequestException ::isForbidden , this ::errorMapForbidden )
196
219
.checkpoint ("Getting mod info for " + projectID )
197
220
.map (GetModResponse ::getData ),
198
221
projectID
@@ -219,6 +242,7 @@ public Mono<CurseForgeFile> getModFileInfo(
219
242
}
220
243
return e ;
221
244
})
245
+ .onErrorMap (FailedRequestException ::isForbidden , this ::errorMapForbidden )
222
246
.map (GetModFileResponse ::getData )
223
247
.checkpoint (),
224
248
projectID , fileID
@@ -285,4 +309,21 @@ private static URI normalizeDownloadUrl(String downloadUrl) {
285
309
);
286
310
}
287
311
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
+ }
288
329
}
0 commit comments