player filter improvements

This commit is contained in:
Robin Appelman 2022-05-07 15:12:55 +02:00
commit b611e54bae
3 changed files with 122 additions and 26 deletions

View file

@ -20,10 +20,13 @@ hex = "0.4"
steamid-ng = "1" steamid-ng = "1"
bytes = "1.1.0" bytes = "1.1.0"
futures-util = "0.3.21" futures-util = "0.3.21"
tracing = "0.1.33"
tinyvec = "1.5.1"
[dev-dependencies] [dev-dependencies]
tokio = { version = "1", features = ["macros"] } tokio = { version = "1", features = ["macros"] }
sqlx = { version = "0.5", features = ["postgres", "runtime-tokio-rustls"] } sqlx = { version = "0.5", features = ["postgres", "runtime-tokio-rustls"] }
tracing-subscriber = { version = "0.3.11", features = ["env-filter"] }
[features] [features]
default = ["default-tls"] default = ["default-tls"]

View file

@ -3,18 +3,20 @@ use futures_util::{Stream, StreamExt};
use reqwest::{multipart, Client, IntoUrl, StatusCode, Url}; use reqwest::{multipart, Client, IntoUrl, StatusCode, Url};
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::borrow::Cow; use std::borrow::Cow;
use std::fmt; use std::fmt::{self, Debug, Display, Formatter};
use std::str::FromStr; use std::str::FromStr;
pub use steamid_ng::SteamID; pub use steamid_ng::SteamID;
use thiserror::Error; use thiserror::Error;
use time::OffsetDateTime; use time::OffsetDateTime;
use tinyvec::TinyVec;
use tracing::{debug, instrument};
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
#[error("Invalid base url: {0}")] #[error("Invalid base url: {0}")]
InvalidBaseUrl(reqwest::Error), InvalidBaseUrl(reqwest::Error),
#[error("Request failed: {0}")] #[error("Request failed: {0}")]
Request(#[from] reqwest::Error), Request(reqwest::Error),
#[error("Invalid page requested")] #[error("Invalid page requested")]
InvalidPage, InvalidPage,
#[error("Invalid api key")] #[error("Invalid api key")]
@ -29,6 +31,17 @@ pub enum Error {
DemoNotFound(u32), DemoNotFound(u32),
} }
impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Self {
match error.status() {
Some(StatusCode::UNAUTHORIZED) => Error::InvalidApiKey,
Some(StatusCode::PRECONDITION_FAILED) => Error::HashMisMatch,
Some(status) if status.is_server_error() => Error::ServerError(status.as_u16()),
_ => Error::Request(error),
}
}
}
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Demo { pub struct Demo {
@ -58,7 +71,8 @@ pub struct Demo {
impl Demo { impl Demo {
/// Return either the stored players info or get the players from the api /// Return either the stored players info or get the players from the api
pub async fn get_players<'a>(&'a self, client: &ApiClient) -> Result<Cow<'a, [Player]>, Error> { #[instrument]
pub async fn get_players(&self, client: &ApiClient) -> Result<Cow<'_, [Player]>, Error> {
if self.players.is_empty() { if self.players.is_empty() {
let demo = client.get(self.id).await?; let demo = client.get(self.id).await?;
Ok(Cow::Owned(demo.players)) Ok(Cow::Owned(demo.players))
@ -71,6 +85,7 @@ impl Demo {
&self, &self,
client: &ApiClient, client: &ApiClient,
) -> Result<impl Stream<Item = Result<Bytes, Error>>, Error> { ) -> Result<impl Stream<Item = Result<Bytes, Error>>, Error> {
debug!(id = self.id, "starting download");
Ok(client Ok(client
.client .client
.get(&self.url) .get(&self.url)
@ -105,7 +120,8 @@ impl UserRef {
} }
/// Return either the stored user info or get the user information from the api /// Return either the stored user info or get the user information from the api
pub async fn resolve<'a>(&'a self, client: &ApiClient) -> Result<Cow<'a, User>, Error> { #[instrument]
pub async fn resolve(&self, client: &ApiClient) -> Result<Cow<'_, User>, Error> {
match self { match self {
UserRef::User(ref user) => Ok(Cow::Borrowed(user)), UserRef::User(ref user) => Ok(Cow::Borrowed(user)),
UserRef::Id(id) => Ok(Cow::Owned(client.get_user(*id).await?)), UserRef::Id(id) => Ok(Cow::Owned(client.get_user(*id).await?)),
@ -225,9 +241,9 @@ impl Default for ListOrder {
} }
} }
impl fmt::Display for ListOrder { impl Display for ListOrder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
<&str>::from(*self).fmt(f) Display::fmt(<&str>::from(*self), f)
} }
} }
@ -265,22 +281,55 @@ where
} }
#[derive(Default, Debug)] #[derive(Default, Debug)]
struct PlayerList(Vec<SteamID>); struct PlayerList(TinyVec<[SteamID; 2]>);
impl PlayerList {
fn new<T: Into<SteamID>, I: IntoIterator<Item = T>>(players: I) -> Self {
PlayerList(players.into_iter().map(Into::into).collect())
}
}
impl Display for PlayerList {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut first = true;
for steam_id in &self.0 {
if first {
first = false;
write!(f, "{}", u64::from(*steam_id))?;
} else {
write!(f, ",{}", u64::from(*steam_id))?;
}
}
Ok(())
}
}
impl Serialize for PlayerList { impl Serialize for PlayerList {
fn serialize<S>(&self, serializer: S) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error> fn serialize<S>(&self, serializer: S) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error>
where where
S: Serializer, S: Serializer,
{ {
self.0 serializer.collect_str(&self)
.iter()
.map(|steamid| format!("{}", u64::from(*steamid)))
.collect::<Vec<_>>()
.join(",")
.serialize(serializer)
} }
} }
#[test]
fn test_serialize_player_list() {
assert_eq!(
"76561198024494988",
PlayerList::new([76561198024494988]).to_string()
);
assert_eq!(
"76561198024494988,76561197963701107",
PlayerList::new([76561198024494988, 76561197963701107]).to_string()
);
assert_eq!(
"76561198024494988,76561197963701107,76561197963701106",
PlayerList::new([76561198024494988, 76561197963701107, 76561197963701106]).to_string()
);
}
impl ListParams { impl ListParams {
#[must_use] #[must_use]
pub fn with_backend(self, backend: impl Into<String>) -> Self { pub fn with_backend(self, backend: impl Into<String>) -> Self {
@ -301,7 +350,7 @@ impl ListParams {
#[must_use] #[must_use]
pub fn with_players<T: Into<SteamID>, I: IntoIterator<Item = T>>(self, players: I) -> Self { pub fn with_players<T: Into<SteamID>, I: IntoIterator<Item = T>>(self, players: I) -> Self {
ListParams { ListParams {
players: PlayerList(players.into_iter().map(Into::into).collect()), players: PlayerList::new(players),
..self ..self
} }
} }
@ -348,6 +397,14 @@ impl Default for ApiClient {
} }
} }
impl Debug for ApiClient {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("ApiClient")
.field("base_url", &self.base_url.to_string())
.finish_non_exhaustive()
}
}
/// Api client for demos.tf /// Api client for demos.tf
/// ///
/// # Example /// # Example
@ -405,6 +462,7 @@ impl ApiClient {
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
#[instrument]
pub async fn list(&self, params: ListParams, page: u32) -> Result<Vec<Demo>, Error> { pub async fn list(&self, params: ListParams, page: u32) -> Result<Vec<Demo>, Error> {
if page == 0 { if page == 0 {
return Err(Error::InvalidPage); return Err(Error::InvalidPage);
@ -419,6 +477,7 @@ impl ApiClient {
.query(&params) .query(&params)
.send() .send()
.await? .await?
.error_for_status()?
.json() .json()
.await?) .await?)
} }
@ -446,6 +505,7 @@ impl ApiClient {
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
#[instrument]
pub async fn list_uploads( pub async fn list_uploads(
&self, &self,
uploader: SteamID, uploader: SteamID,
@ -465,6 +525,7 @@ impl ApiClient {
.query(&params) .query(&params)
.send() .send()
.await? .await?
.error_for_status()?
.json() .json()
.await?) .await?)
} }
@ -491,6 +552,7 @@ impl ApiClient {
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
#[instrument]
pub async fn get(&self, demo_id: u32) -> Result<Demo, Error> { pub async fn get(&self, demo_id: u32) -> Result<Demo, Error> {
let mut url = self.base_url.clone(); let mut url = self.base_url.clone();
url.set_path(&format!("/demos/{}", demo_id)); url.set_path(&format!("/demos/{}", demo_id));
@ -520,6 +582,7 @@ impl ApiClient {
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
#[instrument]
pub async fn get_user(&self, user_id: u32) -> Result<User, Error> { pub async fn get_user(&self, user_id: u32) -> Result<User, Error> {
let mut url = self.base_url.clone(); let mut url = self.base_url.clone();
url.set_path(&format!("/users/{}", user_id)); url.set_path(&format!("/users/{}", user_id));
@ -545,12 +608,14 @@ impl ApiClient {
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
#[instrument]
pub async fn get_chat(&self, demo_id: u32) -> Result<Vec<ChatMessage>, Error> { pub async fn get_chat(&self, demo_id: u32) -> Result<Vec<ChatMessage>, Error> {
let mut url = self.base_url.clone(); let mut url = self.base_url.clone();
url.set_path(&format!("/demos/{}/chat", demo_id)); url.set_path(&format!("/demos/{}/chat", demo_id));
Ok(self.client.get(url).send().await?.json().await?) Ok(self.client.get(url).send().await?.json().await?)
} }
#[instrument]
pub async fn set_url( pub async fn set_url(
&self, &self,
demo_id: u32, demo_id: u32,
@ -563,8 +628,7 @@ impl ApiClient {
let mut api_url = self.base_url.clone(); let mut api_url = self.base_url.clone();
api_url.set_path(&format!("/demos/{}/url", demo_id)); api_url.set_path(&format!("/demos/{}/url", demo_id));
let respose = self self.client
.client
.post(api_url) .post(api_url)
.form(&[ .form(&[
("hash", hex::encode(hash).as_str()), ("hash", hex::encode(hash).as_str()),
@ -574,18 +638,13 @@ impl ApiClient {
("key", key), ("key", key),
]) ])
.send() .send()
.await?; .await?
.error_for_status()?;
match respose.status() { Ok(())
StatusCode::UNAUTHORIZED => Err(Error::InvalidApiKey),
StatusCode::PRECONDITION_FAILED => Err(Error::HashMisMatch),
_ if respose.status().is_server_error() => {
Err(Error::ServerError(respose.status().as_u16()))
}
_ => Ok(()),
}
} }
#[instrument(skip(body))]
pub async fn upload_demo( pub async fn upload_demo(
&self, &self,
file_name: String, file_name: String,
@ -612,6 +671,7 @@ impl ApiClient {
.multipart(form) .multipart(form)
.send() .send()
.await? .await?
.error_for_status()?
.text() .text()
.await?; .await?;

View file

@ -2,10 +2,15 @@ use demostf_client::{ApiClient, Error, ListOrder, ListParams};
use sqlx::postgres::PgPoolOptions; use sqlx::postgres::PgPoolOptions;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use steamid_ng::SteamID; use steamid_ng::SteamID;
use tracing_subscriber::EnvFilter;
static SETUP_DONE: AtomicBool = AtomicBool::new(false); static SETUP_DONE: AtomicBool = AtomicBool::new(false);
async fn setup() { async fn setup() {
let _ = tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.try_init();
if SETUP_DONE.swap(true, Ordering::SeqCst) { if SETUP_DONE.swap(true, Ordering::SeqCst) {
return; return;
} }
@ -234,3 +239,31 @@ async fn test_list_upload() {
.unwrap(); .unwrap();
assert_eq!(demos[0].id, 1); assert_eq!(demos[0].id, 1);
} }
#[tokio::test]
async fn test_list_players() {
let client = test_client().await;
let demos = client
.list(ListParams::default().with_players([76561198010628997]), 1)
.await
.unwrap();
assert_eq!(demos.len(), 1);
assert_eq!(demos[0].id, 1);
let demos = client
.list(
ListParams::default().with_players([76561198010628997, 76561198111527393]),
1,
)
.await
.unwrap();
assert_eq!(demos.len(), 1);
assert_eq!(demos[0].id, 1);
let demos = client
.list(ListParams::default().with_players([76561198010628990]), 1)
.await
.unwrap();
assert_eq!(demos.len(), 0);
}