steam_miniprofile/
miniprofile.rs

1use anyhow::Result;
2use reqwest::Client;
3use scraper::ElementRef;
4
5#[derive(Debug, Clone, Default)]
6pub struct MiniProfile {
7    pub name: String,
8    pub state: State,
9    pub game_name: Option<String>,
10    pub rich_presence: Option<String>,
11    pub avatar_hash: String,
12}
13
14#[allow(dead_code)]
15#[derive(Debug, Clone, Default, PartialEq)]
16pub enum AvatarSize {
17    Small,
18    Medium,
19
20    #[default]
21    Full,
22}
23
24#[allow(dead_code)]
25#[derive(Debug, Clone, Default, PartialEq)]
26pub enum State {
27    Online,
28    InGame,
29    Offline,
30    Other(String),
31
32    #[default]
33    None,
34}
35
36#[allow(dead_code)]
37impl MiniProfile {
38    pub async fn get(steamid: u64, client: &Client) -> Result<Self> {
39        let url = format!(
40            "https://steamcommunity.com/miniprofile/{}",
41            steam_id_64_to_3(steamid)
42        );
43
44        let response = client.get(&url).send().await?.text().await?;
45        let fragment = scraper::Html::parse_fragment(&response);
46
47        Ok(Self {
48            name: get_text(".persona", &fragment).unwrap_or("Unknown".to_string()),
49            state: State::from_fragment(&fragment),
50            game_name: get_text(".miniprofile_game_name", &fragment),
51            rich_presence: get_text(".rich_presence", &fragment),
52            avatar_hash: find_first(".playersection_avatar > img", &fragment)?
53                .value()
54                .attr("src")
55                .unwrap_or("")
56                .split("/")
57                .last()
58                .unwrap_or("")
59                .split("_")
60                .next()
61                .unwrap_or("")
62                .to_string(),
63        })
64    }
65
66    pub fn get_avatar_url(&self, size: AvatarSize) -> String {
67        let size = match size {
68            AvatarSize::Small => "",
69            AvatarSize::Medium => "_medium",
70            AvatarSize::Full => "_full",
71        };
72
73        format!(
74            "https://avatars.cloudflare.steamstatic.com/{}{}.jpg",
75            self.avatar_hash, size
76        )
77    }
78}
79
80impl State {
81    fn from_fragment(fragment: &scraper::Html) -> Self {
82        let game_state = get_text(".game_state", &fragment);
83        let online_state = get_text("span[class*=\"friend_status_\"]", &fragment);
84
85        if let Some(game_state) = game_state {
86            if game_state == "In-Game" {
87                return State::InGame;
88            }
89
90            return State::Other(game_state);
91        } else if let Some(online_state) = online_state {
92            match online_state.as_str() {
93                "Online" => State::Online,
94                "Offline" => State::Offline,
95                _ => State::Other(online_state),
96            }
97        } else {
98            State::None
99        }
100    }
101}
102
103fn get_text(selector: &str, fragment: &scraper::Html) -> Option<String> {
104    let element = find_first(selector, fragment).ok()?;
105    let text = element.text().collect::<String>();
106    Some(text)
107}
108
109fn find_first<'a>(selector: &str, fragment: &'a scraper::Html) -> Result<ElementRef<'a>> {
110    let selector = scraper::Selector::parse(&selector).map_err(|e| anyhow::anyhow!("{}", e))?;
111    let element = fragment.select(&selector).next().map_or_else(
112        || {
113            Err(anyhow::anyhow!(
114                "No element found for selector: {:?}",
115                selector
116            ))
117        },
118        Ok,
119    )?;
120
121    Ok(element)
122}
123
124fn steam_id_64_to_3(steamid: u64) -> u64 {
125    let steamid_3 = steamid - 76561197960265728;
126    steamid_3
127}