1#![warn(clippy::all)]
2
3pub mod response;
4pub mod types;
5pub mod util;
6
7use moka::{future::Cache, Expiry};
8use response::{
9 boosters_response::BoostersResponse,
10 counts_response::CountsResponse,
11 guild_response::GuildResponse,
12 key_response::KeyResponse,
13 leaderboards_response::LeaderboardsResponse,
14 player_response::PlayerResponse,
15 punishment_stats_response::PunishmentStatsResponse,
16 recent_games_response::RecentGamesResponse,
17 skyblock::{
18 skyblock_auction_response::SkyblockAuctionResponse,
19 skyblock_auctions_ended_response::SkyblockAuctionsEndedResponse,
20 skyblock_auctions_response::SkyblockAuctionsResponse,
21 skyblock_bazaar_response::SkyblockBazaarResponse,
22 skyblock_bingo_response::SkyblockBingoResponse,
23 skyblock_fire_sales_response::SkyblockFireSalesResponse,
24 skyblock_news_response::SkyblockNewsResponse,
25 skyblock_profile_response::SkyblockProfileResponse,
26 skyblock_profiles_response::SkyblockProfilesResponse,
27 },
28 status_response::StatusResponse,
29};
30use serde::de::DeserializeOwned;
31use serde_json::Value;
32use std::{
33 any::Any,
34 cmp::max,
35 collections::HashMap,
36 sync::Arc,
37 time::{Duration, Instant},
38};
39use surf::Client;
40use util::{
41 error::Error,
42 minecraft::{self, ApiType, Response},
43 utils::get_timestamp_millis,
44};
45
46pub struct RsPixel {
47 pub config: Config,
48 key: Key,
49}
50
51impl RsPixel {
52 pub async fn new(key: impl Into<String>) -> Result<RsPixel, Error> {
53 RsPixel::from_config(key, ConfigBuilder::default().into()).await
54 }
55
56 pub async fn from_config(key: impl Into<String>, config: Config) -> Result<RsPixel, Error> {
57 let mut rs_pixel = RsPixel {
58 config,
59 key: Key::new(key),
60 };
61
62 rs_pixel.get_key().await.map(|_| rs_pixel)
63 }
64
65 pub async fn username_to_uuid(&self, username: &str) -> Result<Response, Error> {
66 minecraft::username_to_uuid(self, username).await
67 }
68
69 pub async fn uuid_to_username(&self, uuid: &str) -> Result<Response, Error> {
70 minecraft::uuid_to_username(self, uuid).await
71 }
72
73 pub fn to_params(&self, key: &str, value: &str) -> HashMap<String, String> {
74 let mut params = HashMap::new();
75 params.insert(key.to_string(), value.to_string());
76 params
77 }
78
79 pub fn is_cached(&mut self, path: &str, params: HashMap<String, String>) -> bool {
80 if let Some(cache) = &self.config.cache {
81 cache.contains_key(&format!("{}-{:?}", path, params))
82 } else {
83 false
84 }
85 }
86
87 pub async fn get<T>(
88 &mut self,
89 endpoint: HypixelEndpoint,
90 params: HashMap<String, String>,
91 ) -> Result<Arc<T>, Error>
92 where
93 for<'a> T: DeserializeOwned + Send + Sync + 'a,
94 {
95 let cache_key = format!("{}-{:?}", endpoint.0, params);
96
97 if let Some(cache) = &self.config.cache {
98 if let Some(cached) = cache.get(&cache_key) {
99 if let Ok(cached_downcasted) = cached.1.downcast::<T>() {
100 return Ok(cached_downcasted);
101 }
102 }
103 }
104
105 if self.key.is_rate_limited() {
106 let time_till_reset = self.key.get_time_till_reset();
107 match self.config.rate_limit_strategy {
108 RateLimitStrategy::Delay => {
109 println!("Sleeping for {time_till_reset} seconds");
110 std::thread::sleep(Duration::from_secs(time_till_reset as u64));
111 }
112 RateLimitStrategy::Error => {
113 return Err(Error::RateLimit(self.key.time_till_reset));
114 }
115 }
116 }
117
118 let mut req = self
119 .config
120 .client
121 .get(format!("https://api.hypixel.net/{}", endpoint.0))
122 .query(¶ms)?;
123 if endpoint.1 {
124 req = req.header("API-Key", self.key.key.clone());
125 }
126 match req.send().await {
127 Ok(mut res_unwrap) => {
128 if let Some(remaining_limit) = res_unwrap
129 .header("RateLimit-Remaining")
130 .and_then(|header| header.as_str().parse::<i64>().ok())
131 {
132 self.key.update_remaining_limit(remaining_limit);
133 }
134 if let Some(time_till_reset) = res_unwrap
135 .header("RateLimit-Reset")
136 .and_then(|header| header.as_str().parse::<i64>().ok())
137 {
138 self.key.update_time_till_reset(time_till_reset);
139 }
140
141 if res_unwrap.status() == 200 {
142 match res_unwrap.body_json::<T>().await {
143 Ok(json) => {
144 let json_arc = Arc::new(json);
145 if let Some(cache) = &self.config.cache {
146 if let Some(hypixel_cache_ttl) =
147 self.config.hypixel_cache_ttls.get(&endpoint)
148 {
149 cache
150 .insert(cache_key, (*hypixel_cache_ttl, json_arc.clone()))
151 .await;
152 }
153 }
154 Ok(json_arc)
155 }
156 Err(err) => Err(Error::from(err)),
157 }
158 } else {
159 match res_unwrap.body_json::<Value>().await {
160 Ok(json) => Err(Error::from((
161 res_unwrap.status(),
162 json.get("cause")
163 .and_then(serde_json::Value::as_str)
164 .unwrap_or("Unknown fail cause")
165 .to_string(),
166 ))),
167 Err(err) => Err(Error::from(err)),
168 }
169 }
170 }
171 Err(err) => Err(Error::from(err)),
172 }
173 }
174
175 pub async fn simple_get<T>(&mut self, path: HypixelEndpoint) -> Result<Arc<T>, Error>
176 where
177 for<'a> T: DeserializeOwned + Send + Sync + 'a,
178 {
179 self.get(path, HashMap::new()).await
180 }
181
182 pub async fn get_key(&mut self) -> Result<Arc<KeyResponse>, Error> {
183 self.simple_get(HypixelEndpoint::KEY).await
184 }
185
186 pub async fn get_boosters(&mut self) -> Result<Arc<BoostersResponse>, Error> {
187 self.simple_get(HypixelEndpoint::BOOSTERS).await
188 }
189
190 pub async fn get_leaderboards(&mut self) -> Result<Arc<LeaderboardsResponse>, Error> {
191 self.simple_get(HypixelEndpoint::LEADERBOARDS).await
192 }
193
194 pub async fn get_punishment_stats(&mut self) -> Result<Arc<PunishmentStatsResponse>, Error> {
195 self.simple_get(HypixelEndpoint::PUNISHMENT_STATS).await
196 }
197
198 pub async fn get_player(&mut self, uuid: &str) -> Result<Arc<PlayerResponse>, Error> {
199 self.get(HypixelEndpoint::PLAYER, self.to_params("uuid", uuid))
200 .await
201 }
202 pub async fn get_guild_by_player(&mut self, player: &str) -> Result<Arc<GuildResponse>, Error> {
203 self.get(HypixelEndpoint::GUILD, self.to_params("player", player))
204 .await
205 }
206
207 pub async fn get_guild_by_name(&mut self, name: &str) -> Result<Arc<GuildResponse>, Error> {
208 self.get(HypixelEndpoint::GUILD, self.to_params("name", name))
209 .await
210 }
211
212 pub async fn get_guild_by_id(&mut self, id: &str) -> Result<Arc<GuildResponse>, Error> {
213 self.get(HypixelEndpoint::GUILD, self.to_params("id", id))
214 .await
215 }
216
217 pub async fn get_skyblock_auction_by_uuid(
218 &mut self,
219 uuid: &str,
220 ) -> Result<Arc<SkyblockAuctionResponse>, Error> {
221 self.get(
222 HypixelEndpoint::SKYBLOCK_AUCTION,
223 self.to_params("uuid", uuid),
224 )
225 .await
226 }
227
228 pub async fn get_skyblock_auction_by_player(
229 &mut self,
230 player: &str,
231 ) -> Result<Arc<SkyblockAuctionResponse>, Error> {
232 self.get(
233 HypixelEndpoint::SKYBLOCK_AUCTION,
234 self.to_params("player", player),
235 )
236 .await
237 }
238
239 pub async fn get_skyblock_auction_by_profile(
240 &mut self,
241 profile: &str,
242 ) -> Result<Arc<SkyblockAuctionResponse>, Error> {
243 self.get(
244 HypixelEndpoint::SKYBLOCK_AUCTION,
245 self.to_params("profile", profile),
246 )
247 .await
248 }
249
250 pub async fn get_counts(&mut self) -> Result<Arc<CountsResponse>, Error> {
251 self.simple_get(HypixelEndpoint::COUNTS).await
252 }
253
254 pub async fn get_status(&mut self, uuid: &str) -> Result<Arc<StatusResponse>, Error> {
255 self.get(HypixelEndpoint::STATUS, self.to_params("uuid", uuid))
256 .await
257 }
258
259 pub async fn get_recent_games(
260 &mut self,
261 uuid: &str,
262 ) -> Result<Arc<RecentGamesResponse>, Error> {
263 self.get(HypixelEndpoint::RECENT_GAMES, self.to_params("uuid", uuid))
264 .await
265 }
266
267 pub async fn get_skyblock_profiles(
268 &mut self,
269 uuid: &str,
270 ) -> Result<Arc<SkyblockProfilesResponse>, Error> {
271 self.get(
272 HypixelEndpoint::SKYBLOCK_PROFILES,
273 self.to_params("uuid", uuid),
274 )
275 .await
276 }
277
278 pub async fn get_skyblock_profile(
279 &mut self,
280 profile: &str,
281 ) -> Result<Arc<SkyblockProfileResponse>, Error> {
282 self.get(
283 HypixelEndpoint::SKYBLOCK_PROFILE,
284 self.to_params("profile", profile),
285 )
286 .await
287 }
288
289 pub async fn get_skyblock_bingo(
290 &mut self,
291 uuid: &str,
292 ) -> Result<Arc<SkyblockBingoResponse>, Error> {
293 self.get(
294 HypixelEndpoint::SKYBLOCK_BINGO,
295 self.to_params("uuid", uuid),
296 )
297 .await
298 }
299
300 pub async fn get_skyblock_news(&mut self) -> Result<Arc<SkyblockNewsResponse>, Error> {
301 self.simple_get(HypixelEndpoint::SKYBLOCK_NEWS).await
302 }
303
304 pub async fn get_skyblock_auctions(
305 &mut self,
306 page: i64,
307 ) -> Result<Arc<SkyblockAuctionsResponse>, Error> {
308 self.get(
309 HypixelEndpoint::SKYBLOCK_AUCTIONS,
310 self.to_params("page", &page.to_string()),
311 )
312 .await
313 }
314
315 pub async fn get_skyblock_auctions_ended(
316 &mut self,
317 ) -> Result<Arc<SkyblockAuctionsEndedResponse>, Error> {
318 self.simple_get(HypixelEndpoint::SKYBLOCK_AUCTIONS_ENDED)
319 .await
320 }
321
322 pub async fn get_skyblock_bazaar(&mut self) -> Result<Arc<SkyblockBazaarResponse>, Error> {
323 self.simple_get(HypixelEndpoint::SKYBLOCK_BAZAAR).await
324 }
325
326 pub async fn get_skyblock_fire_sales(
327 &mut self,
328 ) -> Result<Arc<SkyblockFireSalesResponse>, Error> {
329 self.simple_get(HypixelEndpoint::SKYBLOCK_FIRESALES).await
330 }
331
332 pub async fn get_resources(&mut self, resource: HypixelEndpoint) -> Result<Arc<Value>, Error> {
333 if !resource.0.starts_with("resources/") {
334 Err(Error::UnknownResource)
335 } else {
336 self.simple_get(resource).await
337 }
338 }
339}
340
341#[derive(Eq, Hash, PartialEq)]
342pub struct HypixelEndpoint(&'static str, bool);
343
344impl HypixelEndpoint {
345 pub fn get_path(&self) -> String {
346 self.0.to_string()
347 }
348
349 pub const KEY: Self = Self("key", true);
350 pub const BOOSTERS: Self = Self("boosters", true);
351 pub const LEADERBOARDS: Self = Self("leaderboards", true);
352 pub const PUNISHMENT_STATS: Self = Self("punishmentstats", true);
353 pub const PLAYER: Self = Self("player", true);
354 pub const GUILD: Self = Self("guild", true);
355 pub const COUNTS: Self = Self("counts", true);
356 pub const STATUS: Self = Self("status", true);
357 pub const RECENT_GAMES: Self = Self("recentGames", true);
358 pub const SKYBLOCK_PROFILES: Self = Self("skyblock/profiles", true);
359 pub const SKYBLOCK_PROFILE: Self = Self("skyblock/profile", true);
360 pub const SKYBLOCK_BINGO: Self = Self("skyblock/bingo", true);
361 pub const SKYBLOCK_NEWS: Self = Self("skyblock/news", true);
362 pub const SKYBLOCK_AUCTION: Self = Self("skyblock/auction", true);
363 pub const SKYBLOCK_AUCTIONS: Self = Self("skyblock/auctions", false);
364 pub const SKYBLOCK_AUCTIONS_ENDED: Self = Self("skyblock/auctions_ended", false);
365 pub const SKYBLOCK_BAZAAR: Self = Self("skyblock/bazaar", false);
366 pub const SKYBLOCK_FIRESALES: Self = Self("skyblock/firesales", false);
367 pub const RESOURCES_GAMES: Self = Self("resources/games", false);
368 pub const RESOURCES_ACHIEVEMENTS: Self = Self("resources/achievements", false);
369 pub const RESOURCES_CHALLENGES: Self = Self("resources/challenges", false);
370 pub const RESOURCES_QUESTS: Self = Self("resources/quests", false);
371 pub const RESOURCES_GUILD_ACHIEVEMENTS: Self = Self("resources/guild/achievements", false);
372 pub const RESOURCES_VANITY_PETS: Self = Self("resources/vanity/pets", false);
373 pub const RESOURCES_VANITY_COMPANIONS: Self = Self("resources/vanity/companions", false);
374 pub const RESOURCES_SKYBLOCK_COLLECTIONS: Self = Self("resources/skyblock/collections", false);
375 pub const RESOURCES_SKYBLOCK_SKILLS: Self = Self("resources/skyblock/skills", false);
376 pub const RESOURCES_SKYBLOCK_ITEMS: Self = Self("resources/skyblock/items", false);
377 pub const RESOURCES_SKYBLOCK_ELECTION: Self = Self("resources/skyblock/election", false);
378 pub const RESOURCES_SKYBLOCK_BINGO: Self = Self("resources/skyblock/bingo", false);
379}
380
381struct Key {
382 pub key: String,
383 remaining_limit: i64,
384 time_till_reset: i64,
385 time: i64,
386}
387
388impl Key {
389 pub fn new(key: impl Into<String>) -> Key {
390 Key {
391 key: key.into(),
392 remaining_limit: 0,
393 time_till_reset: 0,
394 time: 0,
395 }
396 }
397
398 pub fn update_remaining_limit(&mut self, remaining_limit: i64) {
399 self.remaining_limit = remaining_limit;
400 self.time = get_timestamp_millis();
401 }
402
403 pub fn update_time_till_reset(&mut self, time_till_reset: i64) {
404 self.time_till_reset = time_till_reset;
405 self.time = get_timestamp_millis();
406 }
407
408 pub fn is_rate_limited(&self) -> bool {
409 self.remaining_limit <= 1
410 && self.time_till_reset > 0
411 && self.time + self.time_till_reset * 1000 > get_timestamp_millis()
412 }
413
414 pub fn get_time_till_reset(&self) -> i64 {
415 max(
416 0,
417 ((self.time + self.time_till_reset * 1000) - get_timestamp_millis()) / 1000,
418 )
419 }
420}
421
422#[derive(Default)]
423pub enum RateLimitStrategy {
424 #[default]
425 Delay,
426 Error,
427}
428
429pub struct Config {
430 pub client: Client,
431 pub minecraft_api_type: ApiType,
432 pub rate_limit_strategy: RateLimitStrategy,
433 pub uuid_to_username_cache: Option<Cache<String, String>>,
434 pub cache: Option<Cache<String, (Duration, Arc<dyn Any + Send + Sync>)>>,
435 pub hypixel_cache_ttls: HashMap<HypixelEndpoint, Duration>,
436}
437
438#[derive(Default)]
439pub struct ConfigBuilder {
440 client: Option<Client>,
441 minecraft_api_type: Option<ApiType>,
442 rate_limit_strategy: Option<RateLimitStrategy>,
443 minecraft_cache_ttl: Option<Duration>,
444 hypixel_cache_ttls: HashMap<HypixelEndpoint, Duration>,
445}
446
447impl ConfigBuilder {
448 pub fn client(mut self, client: Client) -> ConfigBuilder {
450 self.client = Some(client);
451 self
452 }
453
454 pub fn minecraft_api_type(mut self, minecraft_api_type: ApiType) -> ConfigBuilder {
456 self.minecraft_api_type = Some(minecraft_api_type);
457 self
458 }
459
460 pub fn rate_limit_strategy(mut self, rate_limit_strategy: RateLimitStrategy) -> ConfigBuilder {
462 self.rate_limit_strategy = Some(rate_limit_strategy);
463 self
464 }
465
466 pub fn minecraft_cache_ttl(mut self, minecraft_cache_ttl: Duration) -> ConfigBuilder {
468 self.minecraft_cache_ttl = Some(minecraft_cache_ttl);
469 self
470 }
471
472 pub fn add_hypixel_cache_ttl(
474 mut self,
475 endpoint: HypixelEndpoint,
476 ttl: Duration,
477 ) -> ConfigBuilder {
478 self.hypixel_cache_ttls.insert(endpoint, ttl);
479 self
480 }
481}
482
483impl From<ConfigBuilder> for Config {
484 fn from(c: ConfigBuilder) -> Self {
485 Config {
486 client: c.client.unwrap_or_default(),
487 minecraft_api_type: c.minecraft_api_type.unwrap_or_default(),
488 rate_limit_strategy: c.rate_limit_strategy.unwrap_or_default(),
489 uuid_to_username_cache: c
490 .minecraft_cache_ttl
491 .map(|ttl| Cache::builder().time_to_live(ttl).build()),
492 cache: if !c.hypixel_cache_ttls.is_empty() {
493 Some(Cache::builder().expire_after(HypixelCacheExpiry).build())
494 } else {
495 None
496 },
497 hypixel_cache_ttls: c.hypixel_cache_ttls,
498 }
499 }
500}
501
502struct HypixelCacheExpiry;
503
504impl Expiry<String, (Duration, Arc<dyn Any + Send + Sync>)> for HypixelCacheExpiry {
505 fn expire_after_create(
506 &self,
507 _key: &String,
508 value: &(Duration, Arc<dyn Any + Send + Sync>),
509 _current_time: Instant,
510 ) -> Option<Duration> {
511 Some(value.0)
512 }
513}