use lazy_static::lazy_static;
lazy_static! {
static ref USER_AGENT: String = format!("{}/v{}", "rs-steamwebapi", env!("CARGO_PKG_VERSION"));
}
use serde::{Deserialize, Serialize};
use std::{fs, io, path::PathBuf, path::Path, collections::HashMap, fmt};
use reqwest::blocking::Client;
use serde_json::Value;
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct WorkshopItem {
pub result: i8,
pub publishedfileid: String,
pub creator: String,
#[serde(alias = "creator_appid")]
pub creator_app_id: u32,
#[serde(alias = "consumer_appid")]
pub consumer_app_id: u32,
pub filename: String,
pub file_size: u64,
pub file_url: String,
pub preview_url: String,
pub hcontent_preview: String,
pub title: String,
#[serde(alias = "file_description")]
pub description: String,
pub time_created: usize,
pub time_updated: usize,
pub subscriptions: u32,
pub favorited: u32,
pub views: u32,
pub tags: Vec<WorkshopItemTag>
}
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct ItemResponse {
pub result: i8,
pub publishedfileid: String,
}
impl fmt::Display for WorkshopItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} - {}", self.title, self.publishedfileid)
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct WorkshopItemTag {
tag: String
}
#[doc(hidden)]
#[derive(Serialize, Deserialize)]
struct WSItemResponse<T> {
response: WSItemResponseBody<T>
}
#[doc(hidden)]
#[derive(Serialize, Deserialize)]
struct WSItemResponseBody<T> {
publishedfiledetails: Vec<T>
}
#[doc(hidden)]
#[derive(Serialize, Deserialize)]
struct WSSearchIdBody {
result: u8,
publishedfileid: String,
}
#[doc(hidden)]
#[derive(Serialize, Deserialize)]
struct WSSearchResponse<T> {
response: Option<WSItemResponseBody<T>>,
total: u8
}
#[doc(hidden)]
#[derive(Serialize, Deserialize)]
struct WSCollectionResponse {
response: WSCollectionResponseBody
}
#[doc(hidden)]
#[derive(Serialize, Deserialize)]
struct WSCollectionResponseBody {
result: u8,
resultcount: u8,
collectiondetails: Vec<WSCollectionBody>
}
#[doc(hidden)]
#[derive(Serialize, Deserialize)]
struct WSCollectionBody {
publishedfileid: String,
result: u8,
children: Vec<WSCollectionChildren>
}
#[doc(hidden)]
#[derive(Serialize, Deserialize)]
struct WSCollectionChildren {
publishedfileid: String,
sortorder: u8,
filetype: u8
}
pub struct Workshop {
client: Client,
}
pub struct AuthedWorkshop {
apikey: String,
client: Client,
}
pub struct ProxyWorkshop {
client: Client,
url: String
}
#[allow(dead_code)]
impl Workshop {
pub fn new(client: Option<Client>) -> Workshop {
let client = match client {
Some(client) => client,
None => Client::new()
};
Workshop {
client,
}
}
pub fn login(self, apikey: String) -> AuthedWorkshop {
AuthedWorkshop {
apikey: apikey,
client: self.client
}
}
pub fn proxy(self, url: String) -> ProxyWorkshop {
ProxyWorkshop {
url: url,
client: self.client.clone(),
}
}
pub fn get_vpks_in_folder(dir: &Path) -> Result<Vec<String>, String> {
let mut entries: Vec<PathBuf> = match fs::read_dir(dir) {
Ok(file) => {
match file.map(|res| res.map(|e| e.path()))
.collect::<Result<Vec<_>, io::Error>>() {
Ok(files) => files,
Err(err) => return Err(err.to_string())
}
},
Err(err) => return Err(err.to_string())
};
entries.sort();
let mut vpks: Vec<String> = Vec::new();
for entry in entries {
if !entry.is_dir() {
if let Some("vpk") = entry.extension().and_then(std::ffi::OsStr::to_str) {
vpks.push(entry.file_stem().unwrap().to_str().unwrap().to_owned())
}
}
}
Ok(vpks)
}
pub fn get_published_file_details(&self, fileids: &[String]) -> Result<Vec<WorkshopItem>, reqwest::Error> {
let mut params = HashMap::new();
let length = fileids.len().to_string();
params.insert("itemcount".to_string(), length);
for (i, vpk) in fileids.iter().enumerate() {
if !vpk.parse::<u64>().is_ok() {
panic!("Item is not valid publishedfileid: {}", vpk);
}
let name = format!("publishedfileids[{i}]", i=i);
params.insert(name, vpk.to_string());
}
let mut details = self.client
.post("https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/")
.header("User-Agent", &USER_AGENT.to_string())
.form(¶ms)
.send()?
.error_for_status()?
.json::<Value>()?;
Ok(details["response"]["publishedfiledetails"].as_array_mut().unwrap().iter_mut()
.filter(|v| v["result"] == 1)
.map(|v| serde_json::from_value(v.take()).unwrap())
.collect()
)
}
pub fn get_collection_details(&self, fileid: &str) -> Result<Option<Vec<String>>, reqwest::Error> {
let mut params = HashMap::new();
params.insert("collectioncount", "1");
params.insert("publishedfileids[0]", &fileid);
let details: WSCollectionResponse = self.client
.post("https://api.steampowered.com/ISteamRemoteStorage/GetCollectionDetails/v1/")
.header("User-Agent", USER_AGENT.to_string())
.form(¶ms)
.send()?
.error_for_status()?
.json::<WSCollectionResponse>()?;
if details.response.resultcount > 0 {
let mut ids: Vec<String> = Vec::new();
for children in &details.response.collectiondetails[0].children {
ids.push(children.publishedfileid.to_string());
}
Ok(Some(ids))
} else {
Ok(None)
}
}
}
impl AuthedWorkshop {
pub fn search_ids(&self, appid: u64, query: &str, count: usize) -> Result<Vec<String>, reqwest::Error> {
let details = self.client.get("https://api.steampowered.com/IPublishedFileService/QueryFiles/v1/?")
.header("User-Agent", USER_AGENT.to_string())
.header("Content-Type", "application/x-www-form-urlencoded")
.query(&[
("page", "1"),
("numperpage", &count.to_string()),
("search_text", query),
("appid", &appid.to_string()),
("key", &self.apikey),
])
.send()?
.json::<WSSearchResponse<WSSearchIdBody>>()?;
let mut fileids: Vec<String> = Vec::new();
if details.total > 0 {
for res in &details.response.unwrap().publishedfiledetails {
fileids.push(res.publishedfileid.to_string());
}
}
Ok(fileids)
}
pub fn search_full(&self, appid: u64, query: &str, count: usize) -> Result<Vec<WorkshopItem>, reqwest::Error> {
let details = self.client.get("https://api.steampowered.com/IPublishedFileService/QueryFiles/v1/?")
.header("User-Agent", USER_AGENT.to_string())
.header("Content-Type", "application/x-www-form-urlencoded")
.query(&[
("page", "1"),
("numperpage", &count.to_string()),
("search_text", query),
("appid", &appid.to_string()),
("return_metadata", "1"),
("key", &self.apikey),
])
.send()?
.json::<WSSearchResponse<WorkshopItem>>()?;
if details.total > 0 {
Ok(details.response.unwrap().publishedfiledetails)
} else {
Ok(vec!())
}
}
pub fn can_subscribe(&self, fileid: &str) -> Result<bool, reqwest::Error> {
let details: Value = self.client
.get("https://api.steampowered.com/IPublishedFileService/CanSubscribe/v1/?key=7250BBE4BC2ECA0E16197B38E3675988&publishedfileid=122447941")
.header("User-Agent", USER_AGENT.to_string())
.query(&[
"key", &self.apikey,
"publishedfileid", fileid
])
.send()?
.error_for_status()?
.json()?;
Ok(details["response"]["can_subscribe"].as_bool().unwrap_or(false))
}
}
impl ProxyWorkshop {
pub fn search_ids(&self, appid: u64, query: &str, count: usize) -> Result<Vec<String>, reqwest::Error> {
let details = self.client.get(&self.url)
.header("User-Agent", USER_AGENT.to_string())
.header("Content-Type", "application/x-www-form-urlencoded")
.query(&[
("page", "1"),
("numperpage", &count.to_string()),
("search_text", query),
("appid", &appid.to_string()),
("v", &env!("CARGO_PKG_VERSION")),
])
.send()?
.json::<WSSearchResponse<WSSearchIdBody>>()?;
let mut fileids: Vec<String> = Vec::new();
if details.total > 0 {
for res in &details.response.unwrap().publishedfiledetails {
fileids.push(res.publishedfileid.to_string());
}
}
Ok(fileids)
}
pub fn search_full(&self, appid: u64, query: &str, count:usize) -> Result<Vec<WorkshopItem>, reqwest::Error> {
let details = self.client.get(&self.url)
.header("User-Agent", USER_AGENT.to_string())
.header("Content-Type", "application/x-www-form-urlencoded")
.query(&[
("page", "1"),
("numperpage", &count.to_string()),
("search_text", query),
("appid", &appid.to_string()),
("return_metadata", "1"),
])
.send()?
.json::<WSSearchResponse<WorkshopItem>>()?;
if details.total > 0 {
Ok(details.response.unwrap().publishedfiledetails)
} else {
Ok(vec!())
}
}
}