mirror of
https://codeberg.org/demostf/api-client.git
synced 2026-06-03 08:34:15 +02:00
player filter improvements
This commit is contained in:
parent
91b3f879c9
commit
b611e54bae
3 changed files with 122 additions and 26 deletions
|
|
@ -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"]
|
||||||
|
|
|
||||||
112
src/lib.rs
112
src/lib.rs
|
|
@ -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(¶ms)
|
.query(¶ms)
|
||||||
.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(¶ms)
|
.query(¶ms)
|
||||||
.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?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue