Skip to content

Commit 020b96e

Browse files
authored
fix: osekai api (#1115)
* fix: new badges endpoint * perf: use compact format * fix: new ranking endpoint * fix: separate type for medal count entries * fix: use inex subdomain * refactor: rarity embed * refactor: medal count embed * refactor: remove unused ranking kinds * perf: use compact medals * feat: osekai ranking pagination * fix: supposed incorrect page * fix: medal retrieval * chore: cargo fmt * chore: cargo clippy * test: remove * chore: cargo clippy --all-targets
1 parent 0659966 commit 020b96e

40 files changed

Lines changed: 1414 additions & 951 deletions

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ You can also join its [discord server](https://discord.gg/n9fFstG) to keep up wi
5757
- [5joshi](https://osu.ppy.sh/users/4279650) for these CRAZY GOOD button emotes :)
5858
- [Mr Helix](https://osu.ppy.sh/users/2330619) and his website [huismetbenen](https://snipe.huismetbenen.nl/) for providing snipe data (honorable mention [molneya](https://osu.ppy.sh/users/8945180))
5959
- [Piotrekol](https://osu.ppy.sh/users/304520) and [Ezoda](https://osu.ppy.sh/users/1231180) and their website [osustats](https://osustats.ppy.sh/) for providing leaderboard data
60-
- [mulraf](https://osu.ppy.sh/users/1309242), [Tanza](https://osu.ppy.sh/users/10379965), and the rest of the [osekai](https://osekai.net/) team for providing medal data
60+
- [mulraf](https://osu.ppy.sh/users/1309242), [Tanza](https://osu.ppy.sh/users/10379965), and the rest of the [osekai](https://inex.osekai.net/) team for providing medal data
6161
- [OMKelderman](https://osu.ppy.sh/users/2756335) and his [flag conversion](https://osuflags.omkserver.nl/) service
6262
- [respektive](https://osu.ppy.sh/users/1023489) for his [score rank api](https://github.com/respektive/osu-profile#score-rank-api), [osustats api](https://github.com/respektive/osustats), and various other contributions
6363
- [MasterIO](https://osu.ppy.sh/users/12297980) and [Wieku](https://osu.ppy.sh/users/2584698) for providing the option to render scores via [ordr](https://ordr.issou.best/)
@@ -105,4 +105,4 @@ The bot also has various features that can be enabled in compilation:
105105
- `server`: Runs a server on `localhost:{SERVER_PORT}` (specified in `.env`) and enables the link command. In order for linking and its authentication to succeed, you must configure the redirect URL in your osu! (and twitch) settings and set `PUBLIC_URL` in the `.env` accordingly. E.g for osu! you go to your profile settings, check the oauth section for your own clients, edit the Application Callback URL to `http://localhost:27272/auth/osu` and in your `.env` make sure you have `SERVER_PORT=27272` and `PUBLIC_URL="http://localhost:27272"`. The server also exposes a `/metrics` endpoint providing prometheus data. If you're interested in visualizing them, you need to install [prometheus](https://prometheus.io/download/), [configure it](https://prometheus.io/docs/introduction/first_steps/), install and configure [grafana](https://grafana.com/grafana/), then create a dashboard in grafana for the bathbot metrics.
106106
- `full`: Enables all of the above
107107

108-
To enable these features, use e.g. `cargo run --features global_slash,server`
108+
To enable these features, use e.g. `cargo run --features global_slash,server`

bathbot-client/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ tracing = { workspace = true }
3232
twilight-interactions = { workspace = true }
3333
twilight-model = { workspace = true }
3434

35+
[dev-dependencies]
36+
dotenvy = { workspace = true }
37+
3538
[features]
3639
default = []
3740
twitch = []

bathbot-client/src/osekai.rs

Lines changed: 180 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,47 @@
1+
use std::num::NonZeroU8;
2+
13
use bathbot_model::{
2-
OsekaiBadge, OsekaiBadgeOwner, OsekaiBadgeOwners, OsekaiComment, OsekaiInex, OsekaiMap,
3-
OsekaiMedal, OsekaiRanking, OsekaiRankingEntries,
4+
CompactWrap, MedalCount, OsekaiBadge, OsekaiBadges, OsekaiComment, OsekaiInex, OsekaiMap,
5+
OsekaiMedal, OsekaiRanking, OsekaiRankingEntries, OsekaiRankingEntry, OsekaiRarityEntry,
6+
OsekaiUserEntry, Rarity,
47
};
58
use eyre::{Result, WrapErr};
9+
use serde::Serialize;
10+
11+
use crate::{Client, site::Site};
612

7-
use crate::{Client, multipart::Multipart, site::Site};
13+
const RANKING_PER_PAGE: usize = 50;
814

915
impl Client {
1016
/// Don't use this; use `RedisManager::badges` instead.
17+
///
18+
/// When `compress` is `true`, the API returns a compressed object format:
19+
/// `{"content": {"_t":true,"k":[...],"d":[...]}}`
1120
pub async fn get_osekai_badges(&self) -> Result<Vec<OsekaiBadge>> {
12-
let url = "https://osekai.net/badges/api/getBadges.php";
13-
14-
let bytes = self.make_get_request(url, Site::Osekai).await?;
15-
16-
serde_json::from_slice(&bytes).wrap_err_with(|| {
17-
let body = String::from_utf8_lossy(&bytes);
18-
19-
format!("Failed to deserialize: {body}")
20-
})
21-
}
21+
let url = "https://inex.osekai.net/api/badges/get_all?compress=true";
2222

23-
pub async fn get_osekai_badge_owners(&self, badge_id: u32) -> Result<Vec<OsekaiBadgeOwner>> {
24-
let url = format!("https://osekai.net/badges/api/getUsers.php?badge_id={badge_id}");
2523
let bytes = self.make_get_request(url, Site::Osekai).await?;
2624

27-
let OsekaiBadgeOwners(owners) = serde_json::from_slice(&bytes).wrap_err_with(|| {
28-
let body = String::from_utf8_lossy(&bytes);
29-
30-
format!("Failed to deserialize: {body}")
31-
})?;
25+
serde_json::from_slice::<OsekaiInex<OsekaiBadges>>(&bytes)
26+
.map(|inex| inex.content.0)
27+
.wrap_err_with(|| {
28+
let body = String::from_utf8_lossy(&bytes);
3229

33-
Ok(owners)
30+
format!("Failed to deserialize: {body}")
31+
})
3432
}
3533

3634
/// Don't use this; use `RedisManager::medals` instead.
3735
///
3836
/// Medals will be sorted by medal id.
3937
pub async fn get_osekai_medals(&self) -> Result<Vec<OsekaiMedal>> {
40-
let url = "https://inex.osekai.net/api/medals/get_all";
38+
let url = "https://inex.osekai.net/api/medals/get_all?compress=true";
4139

4240
let bytes = self.make_get_request(url, Site::Osekai).await?;
4341

44-
serde_json::from_slice::<OsekaiInex<Vec<OsekaiMedal>>>(&bytes)
42+
serde_json::from_slice::<OsekaiInex<CompactWrap<OsekaiMedal>>>(&bytes)
4543
.map(|inex| {
46-
let mut medals = inex.content;
44+
let mut medals = inex.content.0;
4745
medals.sort_unstable_by_key(|medal| medal.medal_id);
4846

4947
medals
@@ -56,12 +54,12 @@ impl Client {
5654
}
5755

5856
pub async fn get_osekai_beatmaps(&self, medal_id: u32) -> Result<Vec<OsekaiMap>> {
59-
let url = format!("https://inex.osekai.net/api/medals/{medal_id}/beatmaps");
57+
let url = format!("https://inex.osekai.net/api/medals/{medal_id}/beatmaps?compress=true");
6058

6159
let bytes = self.make_get_request(url, Site::Osekai).await?;
6260

63-
serde_json::from_slice::<OsekaiInex<Vec<OsekaiMap>>>(&bytes)
64-
.map(|inex| inex.content)
61+
serde_json::from_slice::<OsekaiInex<CompactWrap<OsekaiMap>>>(&bytes)
62+
.map(|inex| inex.content.0)
6563
.wrap_err_with(|| {
6664
let body = String::from_utf8_lossy(&bytes);
6765

@@ -70,12 +68,14 @@ impl Client {
7068
}
7169

7270
pub async fn get_osekai_comments(&self, medal_id: u32) -> Result<Vec<OsekaiComment>> {
73-
let url = format!("https://inex.osekai.net/api/comments/Medals_Data/{medal_id}/get");
71+
let url = format!(
72+
"https://inex.osekai.net/api/comments/Medals_Data/{medal_id}/get?compress=true"
73+
);
7474

7575
let bytes = self.make_get_request(url, Site::Osekai).await?;
7676

77-
serde_json::from_slice::<OsekaiInex<Vec<OsekaiComment>>>(&bytes)
78-
.map(|inex| inex.content)
77+
serde_json::from_slice::<OsekaiInex<CompactWrap<OsekaiComment>>>(&bytes)
78+
.map(|inex| inex.content.0)
7979
.wrap_err_with(|| {
8080
let body = String::from_utf8_lossy(&bytes);
8181

@@ -84,22 +84,163 @@ impl Client {
8484
}
8585

8686
/// Don't use this; use `RedisManager::osekai_ranking` instead.
87-
pub async fn get_osekai_ranking<R: OsekaiRanking>(&self) -> Result<Vec<R::Entry>> {
88-
let url = "https://osekai.net/rankings/api/api.php";
87+
pub async fn get_osekai_ranking(
88+
&self,
89+
ranking_kind: &str,
90+
ranking_options_kind: Option<&str>,
91+
country: Option<&str>,
92+
page: NonZeroU8,
93+
) -> Result<OsekaiRankingEntries<OsekaiRankingEntry>> {
94+
let url = "https://inex.osekai.net/api/rankings/get";
95+
96+
let mut body = OsekaiRankingBody::new(ranking_kind);
97+
98+
if let Some(options_kind) = ranking_options_kind {
99+
body.with_option_kind(options_kind);
100+
}
101+
102+
if let Some(country) = country {
103+
body.with_country(country);
104+
}
105+
106+
let offset = usize::from(page.get() - 1) * RANKING_PER_PAGE;
107+
let json = serde_json::to_vec(body.with_offset(offset)).unwrap();
108+
let bytes = self.make_json_post_request(url, Site::Osekai, json).await?;
109+
110+
serde_json::from_slice::<OsekaiInex<OsekaiRankingEntries<OsekaiRankingEntry>>>(&bytes)
111+
.map(|inex| inex.content)
112+
.wrap_err_with(|| {
113+
let body = String::from_utf8_lossy(&bytes);
114+
115+
format!("Failed to deserialize: {body}")
116+
})
117+
}
89118

90-
let mut form = Multipart::new();
91-
form.push_text("App", R::FORM);
119+
/// Don't use this; use `RedisManager::osekai_medal_count` instead.
120+
pub async fn get_osekai_medal_count(
121+
&self,
122+
country: Option<&str>,
123+
page: NonZeroU8,
124+
) -> Result<OsekaiRankingEntries<OsekaiUserEntry>> {
125+
let url = "https://inex.osekai.net/api/rankings/get";
92126

93-
let bytes = self
94-
.make_multipart_post_request(url, Site::Osekai, form)
95-
.await?;
127+
let mut body = OsekaiRankingBody::new(MedalCount::KIND);
96128

97-
serde_json::from_slice::<OsekaiRankingEntries<R>>(&bytes)
98-
.map(Vec::from)
129+
if let Some(country) = country {
130+
body.with_country(country);
131+
}
132+
133+
let offset = usize::from(page.get() - 1) * RANKING_PER_PAGE;
134+
let json = serde_json::to_vec(body.with_offset(offset)).unwrap();
135+
let bytes = self.make_json_post_request(url, Site::Osekai, json).await?;
136+
137+
serde_json::from_slice::<OsekaiInex<OsekaiRankingEntries<OsekaiUserEntry>>>(&bytes)
138+
.map(|inex| inex.content)
99139
.wrap_err_with(|| {
100140
let body = String::from_utf8_lossy(&bytes);
101141

102142
format!("Failed to deserialize: {body}")
103143
})
104144
}
145+
146+
/// Don't use this; use `RedisManager::osekai_rarity` instead.
147+
///
148+
/// Requests *all* pages and returns the full list. This is acceptable
149+
/// because there are only ~400 medals which means 8-9 requests if there are
150+
/// 50 medals per page.
151+
pub async fn get_osekai_rarity(&self) -> Result<Vec<OsekaiRarityEntry>> {
152+
let url = "https://inex.osekai.net/api/rankings/get";
153+
154+
let mut entries = Vec::with_capacity(512);
155+
156+
let mut offset = 0;
157+
let mut body = OsekaiRankingBody::new(Rarity::KIND);
158+
159+
let mut json_buf = Vec::with_capacity(128);
160+
161+
loop {
162+
body.with_offset(offset);
163+
serde_json::to_writer(&mut json_buf, &body).unwrap();
164+
165+
let bytes = self
166+
.make_json_post_request(url, Site::Osekai, json_buf.clone())
167+
.await?;
168+
169+
let inex: OsekaiInex<OsekaiRankingEntries<OsekaiRarityEntry>> =
170+
serde_json::from_slice(&bytes).wrap_err_with(|| {
171+
let body = String::from_utf8_lossy(&bytes);
172+
173+
format!("Failed to deserialize: {body}")
174+
})?;
175+
176+
if inex.content.data.is_empty() {
177+
break;
178+
}
179+
180+
offset += inex.content.data.len();
181+
182+
entries.extend(inex.content.data);
183+
184+
if entries.len() >= inex.content.max as usize {
185+
break;
186+
}
187+
188+
json_buf.clear();
189+
}
190+
191+
Ok(entries)
192+
}
193+
}
194+
195+
#[derive(Serialize)]
196+
struct OsekaiRankingBody<'a> {
197+
compress: bool,
198+
offset: usize,
199+
options: OsekaiRankingBodyOptions<'a>,
200+
#[serde(rename = "type")]
201+
kind: &'a str,
202+
}
203+
204+
#[derive(Serialize)]
205+
struct OsekaiRankingBodyOptions<'a> {
206+
#[serde(rename = "queryColumn", skip_serializing_if = "Option::is_none")]
207+
query_column: Option<&'static str>,
208+
#[serde(skip_serializing_if = "Option::is_none")]
209+
query: Option<&'a str>,
210+
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
211+
kind: Option<&'a str>,
212+
}
213+
214+
impl<'a> OsekaiRankingBody<'a> {
215+
const fn new(kind: &'a str) -> Self {
216+
Self {
217+
compress: true,
218+
offset: 0,
219+
options: OsekaiRankingBodyOptions {
220+
query_column: None,
221+
query: None,
222+
kind: None,
223+
},
224+
kind,
225+
}
226+
}
227+
228+
const fn with_offset(&mut self, offset: usize) -> &mut Self {
229+
self.offset = offset;
230+
231+
self
232+
}
233+
234+
const fn with_country(&mut self, country: &'a str) -> &mut Self {
235+
self.options.query = Some(country);
236+
self.options.query_column = Some("Country");
237+
238+
self
239+
}
240+
241+
const fn with_option_kind(&mut self, kind: &'a str) -> &mut Self {
242+
self.options.kind = Some(kind);
243+
244+
self
245+
}
105246
}

bathbot-model/src/deser.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ pub(super) mod datetime_rfc3339 {
290290
}
291291
}
292292
}
293+
293294
pub(super) mod option_datetime_rfc3339 {
294295
use super::{datetime_rfc3339::DateTimeVisitor, *};
295296

0 commit comments

Comments
 (0)