use std::sync::OnceLock;
use scraper::{Html, Selector};
use steamid::SteamID;
static SEL_COMMENT_THREAD: OnceLock<Selector> = OnceLock::new();
fn sel_comment_thread() -> &'static Selector {
SEL_COMMENT_THREAD.get_or_init(|| Selector::parse(".commentthread_comment").expect("valid CSS selector"))
}
static SEL_COMMENT_TEXT: OnceLock<Selector> = OnceLock::new();
fn sel_comment_text() -> &'static Selector {
SEL_COMMENT_TEXT.get_or_init(|| Selector::parse(".commentthread_comment_text").expect("valid CSS selector"))
}
static SEL_COMMENT_TIMESTAMP: OnceLock<Selector> = OnceLock::new();
fn sel_comment_timestamp() -> &'static Selector {
SEL_COMMENT_TIMESTAMP.get_or_init(|| Selector::parse(".commentthread_comment_timestamp").expect("valid CSS selector"))
}
static SEL_COMMENT_AVATAR_IMG: OnceLock<Selector> = OnceLock::new();
fn sel_comment_avatar_img() -> &'static Selector {
SEL_COMMENT_AVATAR_IMG.get_or_init(|| Selector::parse(".commentthread_comment_avatar a img").expect("valid CSS selector"))
}
static SEL_COMMENT_AUTHOR: OnceLock<Selector> = OnceLock::new();
fn sel_comment_author() -> &'static Selector {
SEL_COMMENT_AUTHOR.get_or_init(|| Selector::parse(".commentthread_comment_author a").expect("valid CSS selector"))
}
use crate::{
client::SteamUser,
endpoint::steam_endpoint,
error::SteamUserError,
types::{CommentAuthor, UserComment},
utils::avatar::{extract_custom_url, get_avatar_hash_from_url},
};
impl SteamUser {
#[tracing::instrument(skip(self))]
pub async fn get_my_comments(&self) -> Result<Vec<UserComment>, SteamUserError> {
let steam_id = self.session.steam_id.ok_or(SteamUserError::NotLoggedIn)?;
self.get_user_comments(steam_id).await
}
#[steam_endpoint(GET, host = Community, path = "/comment/Profile/render/{steam_id}/-1/", kind = Read)]
pub async fn get_user_comments(&self, steam_id: SteamID) -> Result<Vec<UserComment>, SteamUserError> {
let path = format!("/comment/Profile/render/{}/-1/", steam_id.steam_id64());
let response: serde_json::Value = self.get_path(&path).send().await?.json().await?;
let comments_html = response.get("comments_html").and_then(|v| v.as_str()).unwrap_or("");
let mut comments = parse_comments(comments_html);
let total_count = response.get("total_count").and_then(|v| v.as_u64()).unwrap_or(0);
let pagesize = response.get("pagesize").and_then(|v| v.as_u64()).unwrap_or(50);
if (comments.len() as u64) < total_count {
let mut start = comments.len() as u64;
while start < total_count {
let form = [("start", start.to_string()), ("totalcount", total_count.to_string()), ("count", pagesize.to_string()), ("feature2", "-1".to_string())];
let page_response: serde_json::Value = self.post_path(&path).form(&form).send().await?.json().await?;
if let Some(html) = page_response.get("comments_html").and_then(|v| v.as_str()) {
comments.extend(parse_comments(html));
}
start += pagesize;
}
}
Ok(comments)
}
#[steam_endpoint(POST, host = Community, path = "/comment/Profile/post/{steam_id}/-1/", kind = Write)]
pub async fn post_comment(&self, steam_id: SteamID, message: &str) -> Result<Option<UserComment>, SteamUserError> {
let form = [("comment", message), ("count", "6"), ("feature2", "-1")];
let response: serde_json::Value = self.post_path(format!("/comment/Profile/post/{}/-1/", steam_id.steam_id64())).form(&form).send().await?.json().await?;
if let Some(html) = response.get("comments_html").and_then(|v| v.as_str()) {
let comments = parse_comments(html);
let last_post = response.get("timelastpost").and_then(|v| v.as_u64());
if let Some(ts) = last_post {
return Ok(comments.into_iter().find(|c| c.timestamp == ts));
}
return Ok(comments.first().cloned());
}
Ok(None)
}
#[steam_endpoint(POST, host = Community, path = "/comment/Profile/delete/{steam_id}/-1/", kind = Write)]
pub async fn delete_comment(&self, steam_id: SteamID, gidcomment: &str) -> Result<(), SteamUserError> {
let form = [("gidcomment", gidcomment), ("start", "0"), ("count", "6"), ("feature2", "-1")];
let response: serde_json::Value = self.post_path(format!("/comment/Profile/delete/{}/-1/", steam_id.steam_id64())).form(&form).send().await?.json().await?;
let success = response.get("success").and_then(|v| v.as_bool()).unwrap_or(false) || response.get("success").and_then(|v| v.as_i64()).map(|n| n == 1).unwrap_or(false);
if success {
Ok(())
} else {
Err(SteamUserError::SteamError("Failed to delete comment".into()))
}
}
}
fn parse_comments(html: &str) -> Vec<UserComment> {
let document = Html::parse_document(&format!("<div>{}</div>", html));
let mut comments = Vec::new();
for element in document.select(sel_comment_thread()) {
let id = element.value().attr("id").unwrap_or("").replace("comment_", "");
let content = element.select(sel_comment_text()).next().map(|el| el.text().collect::<String>().trim().to_string()).unwrap_or_default();
let timestamp = element.select(sel_comment_timestamp()).next().and_then(|el| el.value().attr("data-timestamp")).and_then(|s| s.parse::<u64>().ok()).unwrap_or(0);
let avatar_el = element.select(sel_comment_avatar_img()).next();
let avatar = avatar_el.and_then(|el| el.value().attr("src")).unwrap_or("").to_string();
let avatar_hash = get_avatar_hash_from_url(&avatar).unwrap_or_default();
let author_el = element.select(sel_comment_author()).next();
let profile_url = author_el.and_then(|el| el.value().attr("href")).map(|s| s.to_string());
let name = author_el.map(|el| el.text().collect::<String>().trim().to_string()).unwrap_or_default();
let miniprofile = author_el.and_then(|el| el.value().attr("data-miniprofile")).and_then(|s| s.parse::<u64>().ok());
let steam_id = miniprofile.and_then(|mp| u32::try_from(mp).ok()).map(SteamID::from_individual_account_id);
let custom_url = profile_url.as_deref().and_then(extract_custom_url);
comments.push(UserComment {
id,
content,
timestamp,
author: CommentAuthor { steam_id, name, avatar, avatar_hash, profile_url, custom_url, miniprofile },
});
}
comments
}