Skip to content

Commit ca71183

Browse files
authored
feat: add image for medal icons (#1039)
1 parent bcade2c commit ca71183

21 files changed

Lines changed: 623 additions & 30 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ http-body-util = { version = "0.1.2" }
2424
hyper = { version = "1.6.0", default-features = false }
2525
hyper-rustls = { version = "0.27.5", default-features = false, features = ["http2", "tls12", "webpki-roots"] }
2626
hyper-util = { version = "0.1.10", default-features = false, features = ["client", "client-legacy", "http2", "tokio"] }
27+
memchr = { version = "2.7.4" }
2728
metrics = { version = "0.24.1" }
2829
metrics-exporter-prometheus = { version = "0.16.2", default-features = false }
2930
metrics-util = { version = "0.19.0" }

bathbot-cache/src/cache/fetch.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,22 @@ impl Cache {
4242
Ok(Ok(CachedArchive::new(bytes)?))
4343
}
4444

45+
pub async fn fetch_raw<K>(
46+
&self,
47+
key: &K,
48+
) -> Result<Result<Vec<u8>, CacheConnection>, FetchError>
49+
where
50+
K: ToCacheKey + ?Sized,
51+
{
52+
let mut conn = self.connection().await?;
53+
54+
let Some(bytes) = conn.get(RedisKey::from(key)).await? else {
55+
return Ok(Err(CacheConnection(conn)));
56+
};
57+
58+
Ok(Ok(bytes))
59+
}
60+
4561
async fn fetch_discord_type<T>(&self, key: RedisKey<'_>) -> FetchResult<T>
4662
where
4763
T: Portable + Portable + for<'a> CheckBytes<ValidatorStrategy<'a>>,

bathbot-cache/src/cache/store.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,17 @@ impl Cache {
5656
Self::store(&mut conn, key, bytes, expire_seconds).await
5757
}
5858

59+
/// Store bytes through a new connection without expiration.
60+
pub async fn store_forever<K>(&self, key: &K, bytes: &[u8]) -> Result<()>
61+
where
62+
K: ToCacheKey + ?Sized,
63+
{
64+
let mut conn = self.connection().await?;
65+
let key = RedisKey::from(key);
66+
67+
conn.set(key, bytes).await.map_err(Report::new)
68+
}
69+
5970
/// Insert a value into a set.
6071
///
6172
/// Returns whether the value was newly inserted. That is:

bathbot-client/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ hyper-rustls = { workspace = true }
1616
hyper-util = { workspace = true }
1717
itoa = { version = "1.0.9", default-features = false }
1818
leaky-bucket-lite = { version = "0.5", features = ["parking_lot"] }
19+
memchr = { workspace = true }
1920
metrics = { workspace = true }
2021
rand = { version = "0.8" }
2122
rosu-v2 = { workspace = true }
@@ -33,4 +34,4 @@ twilight-model = { workspace = true }
3334

3435
[features]
3536
default = []
36-
twitch = []
37+
twitch = []

bathbot-client/src/osu.rs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
use bathbot_util::constants::OSU_BASE;
1+
use bathbot_model::{ScrapedMedal, ScrapedUser};
2+
use bathbot_util::{constants::OSU_BASE, html::decode_html_entities};
23
use bytes::Bytes;
3-
use eyre::{Report, Result, WrapErr};
4+
use eyre::{ContextCompat, Report, Result, WrapErr};
45
use http::response::Parts;
56
use hyper::{Request, header::USER_AGENT};
67

@@ -50,6 +51,38 @@ impl Client {
5051
.map_err(Report::new)
5152
}
5253

54+
pub async fn get_medal_icon(&self, url: &str) -> Result<Bytes> {
55+
self.make_get_request(url, Site::OsuMedalIcon)
56+
.await
57+
.map_err(Report::new)
58+
}
59+
60+
/// Don't use this; use `RedisManager::scraped_medals` instead.
61+
pub async fn get_medals(&self) -> Result<Box<[ScrapedMedal]>> {
62+
const KEY: &str = "data-initial-data=";
63+
64+
let bytes = self.peppy_profile().await?;
65+
let data = std::str::from_utf8(&bytes)?;
66+
let start = data.find(KEY).wrap_err("missing key")? + KEY.len() + 1;
67+
let end = memchr::memchr(b'"', &bytes[start..]).wrap_err("missing end quote")? + start;
68+
69+
let data_initial_data = &data[start..end];
70+
let decoded = decode_html_entities(data_initial_data);
71+
72+
let ScrapedUser { medals } = serde_json::from_str(&decoded)
73+
.wrap_err_with(|| format!("Failed to deserialize: {decoded}"))?;
74+
75+
Ok(medals)
76+
}
77+
78+
async fn peppy_profile(&self) -> Result<Bytes> {
79+
let url = "https://osu.ppy.sh/users/2";
80+
81+
self.make_get_request(url, Site::OsuProfile)
82+
.await
83+
.map_err(Report::new)
84+
}
85+
5386
/// Make sure you provide a valid url to a mapset cover
5487
pub async fn get_mapset_cover(&self, cover: &str) -> Result<Bytes> {
5588
self.make_get_request(&cover, Site::OsuMapsetCover)

bathbot-client/src/site.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ sites! {
6262
OsuBadge -> 10,
6363
OsuMapFile -> 2,
6464
OsuMapsetCover -> 10,
65+
OsuMedalIcon -> 25,
66+
OsuProfile -> 1,
6567
OsuStats -> 2,
6668
OsuTrack -> 2,
6769
Relax -> 2,

bathbot-model/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mod github;
66
mod huismetbenen;
77
mod kittenroleplay;
88
mod osekai;
9+
mod osu;
910
mod osu_stats;
1011
mod osutrack;
1112
mod ranking_entries;
@@ -24,6 +25,6 @@ pub mod rkyv_util;
2425

2526
pub use self::{
2627
country_code::*, deser::ModeAsSeed, either::Either, games::*, github::*, huismetbenen::*,
27-
kittenroleplay::*, osekai::*, osu_stats::*, osutrack::*, ranking_entries::*, relax::*,
28+
kittenroleplay::*, osekai::*, osu::*, osu_stats::*, osutrack::*, ranking_entries::*, relax::*,
2829
respektive::*, score_slim::*, twitch::*, user_stats::*,
2930
};

bathbot-model/src/osu.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
use rkyv::{niche::niching::Null, with::NicheInto};
2+
use serde::{Deserialize, Deserializer};
3+
4+
#[derive(Deserialize)]
5+
pub struct ScrapedUser {
6+
#[serde(rename = "achievements")]
7+
pub medals: Box<[ScrapedMedal]>,
8+
}
9+
10+
#[derive(Debug, Deserialize, rkyv::Archive, rkyv::Serialize)]
11+
pub struct ScrapedMedal {
12+
pub icon_url: Box<str>,
13+
pub id: u16,
14+
pub name: Box<str>,
15+
pub grouping: Box<str>,
16+
pub ordering: u8,
17+
pub description: Box<str>,
18+
#[serde(default, deserialize_with = "deser_mode")]
19+
#[rkyv(with = NicheInto<Null>)]
20+
pub mode: Option<Box<str>>,
21+
#[rkyv(with = NicheInto<Null>)]
22+
pub instructions: Option<Box<str>>,
23+
}
24+
25+
fn deser_mode<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Box<str>>, D::Error> {
26+
match Option::<&str>::deserialize(d) {
27+
Ok(Some("fruits")) => Ok(Some(Box::from("catch"))),
28+
Ok(Some(mode)) => Ok(Some(Box::from(mode))),
29+
Ok(None) => Ok(None),
30+
Err(err) => Err(err),
31+
}
32+
}

bathbot-util/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ authors.workspace = true
55
edition.workspace = true
66

77
[dependencies]
8+
memchr = { workspace = true }
89
metrics = { workspace = true }
910
metrics-util = { workspace = true }
1011
regex = { version = "1.0" }

0 commit comments

Comments
 (0)