steam_miniprofile/
miniprofile.rs1use 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}