rs_pixel/
lib.rs

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(&params)?;
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    /// Set the Surf client to use for HTTP requests. Defaults to
449    pub fn client(mut self, client: Client) -> ConfigBuilder {
450        self.client = Some(client);
451        self
452    }
453
454    /// Set API for username and uuid conversions. Defaults to `ApiType::Mojang`.
455    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    /// Set how Hypixle API rate limits should be handled. Defaults to `RateLimitStrategy::Delay`.
461    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    /// Set the time to live for uuid and username caches. A TTL must be set to enable caching.
467    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    /// Set the time to live for Hypixel API caching. Only endpoints with a TTL set will be cached.
473    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}