oxibooru-client/szurubooru-client/src/client.rs
2024-08-11 14:48:59 -07:00

1652 lines
62 KiB
Rust

#![warn(missing_docs)]
use crate::{errors::*, models::*, tokens::*};
use base64::{engine::general_purpose::STANDARD, Engine as _};
use futures_util::{TryFutureExt, TryStreamExt};
use reqwest::header::CONTENT_TYPE;
use reqwest::{
header::{HeaderMap, ACCEPT, AUTHORIZATION},
multipart::{Form, Part},
Client, ClientBuilder, Method, RequestBuilder, Response,
};
use serde::{de::DeserializeOwned, Serialize};
use serde_json::Value;
use sha1::{Digest, Sha1};
use std::fmt::{Display, Formatter};
use std::io::{BufWriter, Write};
use std::path::Path;
use std::{fs::File, io::Read};
use url::Url;
///
/// The base Szurubooru Client
///
/// Use this `struct` to create requests to run against a Szurubooru instance.
///
#[derive(Debug)]
pub struct SzurubooruClient {
base_url: Url,
client: Client,
auth: SzurubooruAuth,
}
impl SzurubooruClient {
///
/// Construct a new `SzurubooruClient` using a username and token.
///
/// * `host` - The host to connect to, including `http` or `https`. Any trailing slashes will
/// be stripped
/// * `username` - The username to authenticate as
/// * `token` - The token used to authenticate as `username`
/// * `allow_insecure` - Whether to disable SSL verification
///
/// ## Returns
///
/// A [SzurubooruResult] containing the client. May return a [SzurubooruClientError::UrlParseError]
/// if the host URL isn't a proper URL.
///
/// ```no_run
/// use szurubooru_client::SzurubooruClient;
/// let client = SzurubooruClient::new_with_token("http://localhost:5001", "myuser", "sz-123456", true).unwrap();
/// ```
pub fn new_with_token(
host: &str,
username: &str,
token: &str,
allow_insecure: bool,
) -> SzurubooruResult<Self> {
let encoded_auth = STANDARD.encode(format!("{username}:{token}").as_bytes());
let token_header_value = format!("Token {encoded_auth}");
let auth = SzurubooruAuth::TokenAuth(token_header_value);
SzurubooruClient::new(host, auth, allow_insecure)
}
///
/// Construct a new `SzurubooruClient` using a username and token.
///
/// * `host` - The host to connect to, including `http` or `https`
/// * `username` - The username to authenticate as
/// * `password` - The password used to authenticate as `username`
/// * `allow_insecure` - Whether to disable SSL verification
///
/// ## Returns
///
/// A [SzurubooruResult] containing the client. May return a [SzurubooruClientError::UrlParseError]
/// if the host URL isn't a proper URL.
///
/// ```no_run
/// use szurubooru_client::SzurubooruClient;
/// let client = SzurubooruClient::new_with_basic_auth("http://localhost:5001", "myuser",
/// "mypassword", true).unwrap();
/// ```
pub fn new_with_basic_auth(
host: &str,
username: &str,
password: &str,
allow_insecure: bool,
) -> SzurubooruResult<Self> {
let auth = SzurubooruAuth::BasicAuth(username.to_string(), password.to_string());
SzurubooruClient::new(host, auth, allow_insecure)
}
/// Create a new client with anonymous credentials
pub fn new_anonymous(host: &str, allow_insecure: bool) -> SzurubooruResult<Self> {
let auth = SzurubooruAuth::None;
SzurubooruClient::new(host, auth, allow_insecure)
}
fn new(host: &str, auth: SzurubooruAuth, allow_insecure: bool) -> SzurubooruResult<Self> {
let host = if host.ends_with("/") {
&host[0..host.len() - 1]
} else {
host
};
let mut base_url = Url::parse(host).map_err(|e| SzurubooruClientError::UrlParseError {
source: e,
url: host.to_string(),
})?;
base_url.set_fragment(None);
let mut header_map = HeaderMap::new();
//header_map.append(AUTHORIZATION, token_header_value.parse().unwrap());
header_map.append(ACCEPT, "application/json".parse().unwrap());
header_map.append(CONTENT_TYPE, "application/json".parse().unwrap());
let client = ClientBuilder::new()
.danger_accept_invalid_certs(allow_insecure)
.default_headers(header_map)
.build()
.unwrap();
Ok(Self {
base_url,
client,
auth,
})
}
/// Construct a new request using the existing client auth and base URL
/// All requests start with the [SzurubooruClient] struct.
/// The (request)[SzurubooruClient::request],
/// (with_fields)[SzurubooruClient::fields],
/// (limit)[SzurubooruClient::limit] and
/// (offset)[SzurubooruClient::offset] methods all return a [SzurubooruRequest] struct that will
/// enable you to actually make the requests.
/// ```no_run
/// # use szurubooru_client::SzurubooruClient;
/// # #[allow(unused)]
/// # async {
/// let client = SzurubooruClient::new_with_token("http://localhost:5001", "myuser", "sz-123456", true).unwrap();
/// let new_request = client.request();
/// let tag_categories = new_request.list_tag_categories().await;
/// # };
/// # ()
/// ```
pub fn request(&self) -> SzurubooruRequest {
SzurubooruRequest::new(self)
}
/// Construct a new request while selecting only the given fields
/// The Szurubooru API supports selecting a subset of fields for a given resource.
/// Most resource (models)[szurubooru_client::models] have [Option] fields because of that.
/// The default is to return all fields for a given resource.
/// See [here](https://github.com/rr-/szurubooru/blob/master/doc/API.md#field-selecting) for
/// more details
///
/// For example, to select only the `version`, `id` and `content_url` fields of a
/// (PostResource)[szurubooru_client::models::PostResource]
/// ```no_run
/// # use szurubooru_client::SzurubooruClient;
/// # #[allow(unused)]
/// # async {
/// let client = SzurubooruClient::new_with_token("http://localhost:5001", "myuser", "sz-123456", true).unwrap();
/// let new_request = client.request().with_fields(vec!["version", "id", "content_url"]);
/// # };
/// # ()
/// ```
pub fn with_fields<'a>(&'a self, fields: Vec<&'a str>) -> SzurubooruRequest {
self.request().with_fields(fields)
}
/// Construct a new request with the given limit
/// The Szurubooru API supports limiting the number of resources returned for Paginated
/// API endpoints.
///
/// For example, to limit the number of pools returned by (list_pools)[SzurubooruRequest::list_pools]
/// ```no_run
/// # use szurubooru_client::SzurubooruClient;
/// # #[allow(unused)]
/// # async {
/// let client = SzurubooruClient::new_with_token("http://localhost:5001", "myuser", "sz-123456", true).unwrap();
/// // Limit the number of results per page to 10
/// let pools_result = client.with_limit(10)
/// .list_pools(None)
/// .await;
/// # };
/// # ()
/// ```
pub fn with_limit(&self, limit: u32) -> SzurubooruRequest {
self.request().with_limit(limit)
}
/// Construct a new request starting at the given offset
/// The Szurubooru API supports offsetting the results returned from Paginated API
/// endpoints. Use this offset in combination with the limit to page through
/// large result sets.
///
/// For example, to offset the list of pools returned by (list_pools)[SzurubooruRequest::list_pools]
/// ```no_run
/// # use szurubooru_client::SzurubooruClient;
/// # #[allow(unused)]
/// # async {
/// let client = SzurubooruClient::new_with_token("http://localhost:5001", "myuser", "sz-123456", true).unwrap();
/// // Skip the first ten pools in the list
/// let pools_result = client.with_offset(10)
/// .list_pools(None)
/// .await;
/// # };
/// # ()
/// ```
pub fn with_offset(&self, offset: u32) -> SzurubooruRequest {
self.request().with_offset(offset)
}
}
#[derive(Debug)]
/// A type that represents a single Szurubooru request.
pub struct SzurubooruRequest<'a> {
fields: Option<Vec<&'a str>>,
limit: Option<u32>,
offset: Option<u32>,
client: &'a SzurubooruClient,
}
impl<'a> SzurubooruRequest<'a> {
pub(super) fn new(client: &'a SzurubooruClient) -> Self {
Self {
client,
fields: None,
limit: None,
offset: None,
}
}
/// Select which fields to return from the query.
/// The Szurubooru API supports selecting a subset of fields for a given resource.
/// Most resource (models)[szurubooru_client::models] have [Option] fields because of that.
/// The default is to return all fields for a given resource.
/// See [here](https://github.com/rr-/szurubooru/blob/master/doc/API.md#field-selecting) for
/// more details
///
/// For example, to select only the `version`, `id` and `content_url` fields of a
/// (PostResource)[szurubooru_client::models::PostResource]
/// ```no_run
/// # use szurubooru_client::SzurubooruClient;
/// # #[allow(unused)]
/// # async {
/// let client = SzurubooruClient::new_with_token("http://localhost:5001", "myuser", "sz-123456", true).unwrap();
/// let new_request = client.request().with_fields(vec!["version", "id", "content_url"]);
/// # };
/// # ()
/// ```
pub fn with_fields(mut self, fields: Vec<&'a str>) -> Self {
self.fields = Some(fields);
self
}
/// Limit the number of returned results
/// The Szurubooru API supports limiting the number of resources returned for Paginated
/// API endpoints.
///
/// For example, to limit the number of pools returned by (list_pools)[SzurubooruRequest::list_pools]
/// ```no_run
/// # use szurubooru_client::SzurubooruClient;
/// # #[allow(unused)]
/// # async {
/// let client = SzurubooruClient::new_with_token("http://localhost:5001", "myuser", "sz-123456", true).unwrap();
/// // Limit the number of results per page to 10
/// let pools_result = client.with_limit(10)
/// .list_pools(None)
/// .await;
/// # };
/// # ()
/// ```
pub fn with_limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
/// Skip a certain number of records
/// The Szurubooru API supports offsetting the results returned from Paginated API
/// endpoints. Use this offset in combination with the limit to page through
/// large result sets.
///
/// For example, to offset the list of pools returned by (list_pools)[SzurubooruRequest::list_pools]
/// ```no_run
/// # use szurubooru_client::SzurubooruClient;
/// # #[allow(unused)]
/// # async {
/// let client = SzurubooruClient::new_with_token("http://localhost:5001", "myuser", "sz-123456", true).unwrap();
/// // Skip the first ten pools in the list
/// let pools_result = client.with_offset(10)
/// .list_pools(None)
/// .await;
/// # };
/// # ()
/// ```
pub fn with_offset(mut self, offset: u32) -> Self {
self.offset = Some(offset);
self
}
#[doc(hidden)]
fn prep_request<T>(
&self,
method: Method,
path: T,
query: Option<&Vec<QueryToken>>,
) -> reqwest::RequestBuilder
where
T: AsRef<str> + Display,
{
let mut req_url = self.client.base_url.clone();
req_url.set_path(path.as_ref());
if let Some(query_vec) = query {
let mut qpm = req_url.query_pairs_mut();
let query_string = query_vec.to_query_string();
qpm.append_pair("query", &query_string);
}
if let Some(fields) = &self.fields {
let mut qpm = req_url.query_pairs_mut();
let fields_list = fields.join(",");
qpm.append_pair("fields", &fields_list);
}
if let Some(limit) = &self.limit {
let mut qpm = req_url.query_pairs_mut();
qpm.append_pair("limit", &limit.to_string());
}
if let Some(offset) = &self.offset {
let mut qpm = req_url.query_pairs_mut();
qpm.append_pair("offset", &offset.to_string());
}
// This doesn't detect the required `mut` for some reason
#[allow(unused_mut)]
let mut req = self.client.client.request(method, req_url);
match &self.client.auth {
SzurubooruAuth::TokenAuth(t) => {
let mut header_map = HeaderMap::new();
header_map.append(AUTHORIZATION, t.parse().unwrap());
req.headers(header_map)
}
SzurubooruAuth::BasicAuth(u, p) => req.basic_auth(u, Some(p)),
SzurubooruAuth::None => req,
}
}
#[tracing::instrument(skip(self), fields(base_url=self.client.base_url.to_string()))]
async fn do_request<T, B, P>(
&self,
method: Method,
path: P,
query: Option<&Vec<QueryToken>>,
body: Option<&B>,
) -> SzurubooruResult<T>
where
T: DeserializeOwned,
B: Serialize + std::fmt::Debug,
P: AsRef<str> + Display + std::fmt::Debug,
{
let mut request = self.prep_request(method, path, query);
if let Some(b) = body {
let b_str =
serde_json::to_string(b).map_err(SzurubooruClientError::JSONSerializationError)?;
request = request.body(b_str);
}
self.handle_request(request).await
}
async fn handle_response(&self, response: Response) -> SzurubooruResult<Response> {
if response.status().is_client_error() || response.status().is_server_error() {
let resp_json = response
.text()
.await
.map_err(SzurubooruClientError::RequestError)?;
let server_error = serde_json::from_str::<SzurubooruServerError>(&resp_json)
.map_err(|e| SzurubooruClientError::ResponseParsingError(e, resp_json))?;
Err(SzurubooruClientError::SzurubooruServerError(server_error))
} else {
Ok(response)
}
}
async fn handle_request<T: DeserializeOwned>(
&self,
request: RequestBuilder,
) -> SzurubooruResult<T> {
let request = request
.build()
.map_err(SzurubooruClientError::RequestBuilderError)?;
let response = self.client.client.execute(request).await;
let response = self
.handle_response(response.map_err(SzurubooruClientError::RequestError)?)
.await?;
//.error_for_status()
//.map_err(SzurubooruClientError::RequestError)?;
let response_text = response
.text()
.await
.map_err(SzurubooruClientError::RequestError)?;
serde_json::from_str::<SzuruEither<T, SzurubooruServerError>>(&response_text)
.map_err(|e| SzurubooruClientError::ResponseParsingError(e, response_text))?
.into_result()
}
/// Lists all tag categories. Doesn't use paging.
pub async fn list_tag_categories(
&self,
) -> SzurubooruResult<UnpagedSearchResult<TagCategoryResource>> {
self.do_request(Method::GET, "/api/tag-categories", None, None::<&String>)
.await
}
/// Creates a new tag category using specified parameters. Name must match
/// `tag_category_name_regex` from server's configuration. First category created
/// becomes the default category.
pub async fn create_tag_category(
&self,
new_cat: &CreateUpdateTagCategory,
) -> SzurubooruResult<TagCategoryResource> {
self.do_request(Method::POST, "/api/tag-categories", None, Some(new_cat))
.await
}
/// Updates an existing tag category using specified parameters. Name must match
/// `tag_category_name_regex` from server's configuration. All fields except
/// [version](models::TagCategoryResource::version) are optional - update concerns only provided fields.
pub async fn update_tag_category<T>(
&self,
name: T,
update_tag_cat: &CreateUpdateTagCategory,
) -> SzurubooruResult<TagCategoryResource>
where
T: AsRef<str> + Display,
{
let path = format!("/api/tag-category/{name}");
self.do_request(Method::PUT, &path, None, Some(update_tag_cat))
.await
}
/// Retrieves information about an existing tag category.
pub async fn get_tag_category<T>(&self, name: T) -> SzurubooruResult<TagCategoryResource>
where
T: AsRef<str> + Display,
{
let path = format!("/api/tag-category/{name}");
self.do_request(Method::GET, &path, None, None::<&String>)
.await
}
/// Deletes existing tag category. The tag category to be deleted must have no usages.
pub async fn delete_tag_category<T>(&self, name: T, version: u32) -> SzurubooruResult<()>
where
T: AsRef<str> + Display,
{
let path = format!("/api/tag-category/{name}");
let version_obj = ResourceVersion { version };
self.do_request::<Value, _, _>(Method::DELETE, &path, None, Some(&version_obj))
.await
.map(|_| ())
}
/// Sets given tag category as default. All new tags created manually or automatically will
/// have this category.
pub async fn set_default_tag_category<T>(&self, name: T) -> SzurubooruResult<()>
where
T: AsRef<str> + Display,
{
let path = format!("/api/tag-category/{name}/default");
self.do_request(Method::PUT, &path, None, None::<&String>)
.await
}
/// Searches for tags.
/// See the (named tokens)[tokens::TagNamedToken] and (sort tokens)[tokens::TagSortToken] for
/// all possible query tokens, or use (QueryToken)[tokens::QueryToken] for a custom token
pub async fn list_tags(
&self,
query: Option<&Vec<QueryToken>>,
) -> SzurubooruResult<PagedSearchResult<TagResource>> {
self.do_request(Method::GET, "/api/tags", query, None::<&String>)
.await
}
/// Creates a new tag using specified parameters. Names, suggestions and implications must
/// match `tag_name_regex` from server's configuration. Category must exist and is the same
/// as the `name` field within (TagCategoryResource)[models::TagCategoryResource] resource.
/// Suggestions and implications are optional. If specified implied tags or suggested tags do
/// not exist yet, they will be automatically created. Tags created automatically have no
/// implications, no suggestions, one name and their category is set to the first tag category
/// found. If there are no tag categories established yet, an error will be thrown.
pub async fn create_tag(&self, new_tag: &CreateUpdateTag) -> SzurubooruResult<TagResource> {
self.do_request(Method::POST, "/api/tags", None, Some(new_tag))
.await
}
/// Updates an existing tag using specified parameters. Names, suggestions and implications must
/// match `tag_name_regex` from server's configuration. Category must exist and is the same
/// as the `name` field within (TagCategoryResource)[models::TagCategoryResource] resource.
/// Suggestions and implications are optional. If specified implied tags or suggested tags do
/// not exist yet, they will be automatically created. Tags created automatically have no
/// implications, no suggestions, one name and their category is set to the first tag category
/// found. If there are no tag categories established yet, an error will be thrown.
pub async fn update_tag<T>(
&self,
name: T,
update_tag: &CreateUpdateTag,
) -> SzurubooruResult<TagResource>
where
T: AsRef<str> + Display,
{
let path = format!("/api/tag/{name}");
self.do_request(Method::PUT, &path, None, Some(update_tag))
.await
}
/// Retrieves information about an existing tag.
pub async fn get_tag<T>(&self, name: T) -> SzurubooruResult<TagResource>
where
T: AsRef<str> + Display,
{
let path = format!("/api/tag/{name}");
self.do_request(Method::GET, &path, None, None::<&String>)
.await
}
/// Deletes existing tag. The tag to be deleted must have no usages.
pub async fn delete_tag<T>(&self, name: T, version: u32) -> SzurubooruResult<()>
where
T: AsRef<str> + Display,
{
let path = format!("/api/tag/{name}");
let version_obj = ResourceVersion { version };
self.do_request::<Value, _, _>(Method::DELETE, &path, None, Some(&version_obj))
.await
.map(|_| ())
}
/// Removes source tag and merges all of its usages, suggestions and implications to the
/// target tag. Other tag properties such as category and aliases do not get transferred
/// and are discarded.
pub async fn merge_tag(&self, merge_opts: &MergeTags) -> SzurubooruResult<TagResource> {
self.do_request(Method::POST, "/api/tag-merge", None, Some(merge_opts))
.await
}
/// Lists siblings of given tag, e.g. tags that were used in the same posts as the given tag.
/// The (occurrences)[models::TagSibling::occurrences] field signifies how many times a given
/// sibling appears with given tag. Results are sorted by occurrences count and the list is
/// truncated to the first 50 elements. Doesn't use paging.
pub async fn get_tag_siblings<T>(
&self,
name: T,
) -> SzurubooruResult<UnpagedSearchResult<TagSibling>>
where
T: AsRef<str> + Display,
{
let path = format!("/api/tag-siblings/{name}");
self.do_request(Method::GET, &path, None, None::<&String>)
.await
}
/// Searches for posts.
/// See (PostNamedToken)[tokens::PostNamedToken], (PostSortToken)[tokens::PostSortToken] and
/// (PostSpecialToken)[tokens::PostSpecialToken] for valid tokens to use with this method, or
/// use (QueryToken)[tokens::QueryToken] to construct a custom token
pub async fn list_posts(
&self,
query: Option<&Vec<QueryToken>>,
) -> SzurubooruResult<PagedSearchResult<PostResource>> {
self.do_request(Method::GET, "/api/posts", query, None::<&String>)
.await
}
async fn create_update_post_from_url(
&self,
path: &str,
method: Method,
cupost: &CreateUpdatePost,
) -> SzurubooruResult<PostResource> {
self.do_request(method, path, None, Some(cupost)).await
}
/// Create a new post based on the `contentUrl` field, which the server will use to download
/// the image.
/// If specified tags do not exist yet, they will be automatically created. Tags created
/// automatically have no implications, no suggestions, one name and their category is set to
/// the first tag category found. (safety)[models::CreateUpdatePost::safety] must be any of
/// `safe`, `sketchy` or `unsafe`.
/// Relations must contain valid post IDs. If `flag` is omitted, they will be defined by
/// default (`"loop"` will be set for all video posts, and `"sound"` will be auto-detected).
/// Sending empty thumbnail will cause the post to use default thumbnail. If `anonymous` is set
/// to `true`, the uploader name won't be recorded (privilege verification still applies;
/// it's possible to disallow anonymous uploads completely from config.)
pub async fn create_post_from_url(
&self,
new_post: &CreateUpdatePost,
) -> SzurubooruResult<PostResource> {
self.create_update_post_from_url("/api/posts", Method::POST, new_post)
.await
}
/// Update an existing post
/// See [SzurubooruRequest::create_post_from_url] for more details about the fields in
/// (CreateUpdatePost)[models::CreateUpdatePost]
pub async fn update_post(
&self,
post_id: u32,
update_post: &CreateUpdatePost,
) -> SzurubooruResult<PostResource> {
let path = format!("/api/post/{post_id}");
self.create_update_post_from_url(&path, Method::PUT, update_post)
.await
}
fn part_from_file(&self, file: &mut File) -> SzurubooruResult<Part> {
let mut bytes = vec![];
file.read_to_end(&mut bytes)
.map_err(SzurubooruClientError::IOError)?;
Ok(Part::stream(bytes))
}
async fn create_update_post_from_file<T>(
&self,
file: Option<&mut File>,
thumbnail: Option<&mut File>,
file_name: Option<T>,
path: &str,
method: Method,
cupost: &CreateUpdatePost,
) -> SzurubooruResult<PostResource>
where
T: AsRef<str>,
{
let request = self.prep_request(method, path, None);
let metadata_str =
serde_json::to_string(cupost).map_err(SzurubooruClientError::JSONSerializationError)?;
let metadata_part = Part::text(metadata_str);
let mut form = Form::new().part("metadata", metadata_part);
if let Some(file) = file {
let content_part = self
.part_from_file(file)?
.file_name(file_name.as_ref().unwrap().as_ref().to_string());
form = form.part("content", content_part);
}
if let Some(thumbnail) = thumbnail {
let thumbnail_part = self
.part_from_file(thumbnail)?
.file_name(format!("thumbnail_{}", file_name.unwrap().as_ref()));
form = form.part("thumbnail", thumbnail_part);
}
self.handle_request(request.multipart(form)).await
}
/// Create a new post from a file handle
/// See [SzurubooruRequest::create_post_from_url] for more details about the fields in
/// (CreateUpdatePost)[models::CreateUpdatePost]
pub async fn create_post_from_file<T>(
&self,
file: &mut File,
thumbnail: Option<&mut File>,
file_name: T,
new_post: &CreateUpdatePost,
) -> SzurubooruResult<PostResource>
where
T: AsRef<str>,
{
self.create_update_post_from_file(
Some(file),
thumbnail,
Some(file_name),
"/api/posts",
Method::POST,
new_post,
)
.await
}
/// Create a new post from a file path
/// See [SzurubooruRequest::create_post_from_url] for more details about the fields in
/// (CreateUpdatePost)[models::CreateUpdatePost]
pub async fn create_post_from_file_path(
&self,
file_path: impl AsRef<Path>,
thumbnail: Option<impl AsRef<Path>>,
new_post: &CreateUpdatePost,
) -> SzurubooruResult<PostResource> {
let mut file = File::open(&file_path).map_err(SzurubooruClientError::IOError)?;
let filename = file_path.as_ref().file_name().unwrap().to_str().unwrap();
let mut thumbnail_file = if let Some(t) = thumbnail {
Some(File::open(t).map_err(SzurubooruClientError::IOError)?)
} else {
None
};
self.create_post_from_file(&mut file, thumbnail_file.as_mut(), filename, new_post)
.await
}
/// Create a post from a token previously generated by
/// (upload_temporary_file_from_path)[SzurubooruRequest::upload_temporary_file_from_path]
pub async fn create_post_from_token(
&self,
new_post: &CreateUpdatePost,
) -> SzurubooruResult<PostResource> {
assert!(new_post.content_token.is_some());
self.create_update_post_from_file(
None,
None,
None::<String>,
"/api/posts",
Method::POST,
new_post,
)
.await
}
/// Update an existing post from an open File handle
/// See [SzurubooruRequest::create_post_from_url] for more details about the fields in
/// (CreateUpdatePost)[models::CreateUpdatePost]
pub async fn update_post_from_file(
&self,
post_id: u32,
file: Option<&mut File>,
thumbnail: Option<&mut File>,
file_name: impl AsRef<str>,
update_post: &CreateUpdatePost,
) -> SzurubooruResult<PostResource> {
let path = format!("/api/post/{post_id}");
self.create_update_post_from_file(
file,
thumbnail,
Some(file_name),
&path,
Method::PUT,
update_post,
)
.await
}
/// Update an existing post from a file path
/// See [SzurubooruRequest::create_post_from_url] for more details about the fields in
/// (CreateUpdatePost)[models::CreateUpdatePost]
pub async fn update_post_from_file_path(
&self,
post_id: u32,
file_path: Option<impl AsRef<Path>>,
thumbnail: Option<impl AsRef<Path>>,
update_post: &CreateUpdatePost,
) -> SzurubooruResult<PostResource> {
let mut filename = None;
let mut file = if let Some(f) = file_path {
filename = Some(
f.as_ref()
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_string(),
);
Some(File::open(f).map_err(SzurubooruClientError::IOError)?)
} else {
None
};
let mut thumbnail_file = if let Some(t) = thumbnail {
if filename.is_none() {
filename = Some(
t.as_ref()
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_string(),
);
}
Some(File::open(t).map_err(SzurubooruClientError::IOError)?)
} else {
None
};
self.update_post_from_file(
post_id,
file.as_mut(),
thumbnail_file.as_mut(),
filename.unwrap(),
update_post,
)
.await
}
async fn get_post_content(
&self,
post_id: u32,
get_thumbnail: bool,
) -> SzurubooruResult<Response> {
let post_resource = self.get_post(post_id).await?;
let content_path = if get_thumbnail {
post_resource.thumbnail_url.unwrap()
} else {
post_resource.content_url.unwrap()
};
let req = self.prep_request(Method::GET, content_path, None);
let request = req
.build()
.map_err(SzurubooruClientError::RequestBuilderError)?;
let resp_res = self
.client
.client
.execute(request)
.await
.map_err(SzurubooruClientError::RequestError)?;
self.handle_response(resp_res).await
}
///Fetches the given post ID's image as a stream of bytes
pub async fn get_image_bytestream(
&self,
post_id: u32,
) -> SzurubooruResult<impl futures_util::Stream<Item = Result<bytes::Bytes, reqwest::Error>>>
{
self.get_post_content(post_id, false)
.await
.map(|cr| cr.bytes_stream())
}
///Fetches the given post ID's thumbnail as a stream of bytes
pub async fn get_thumbnail_bytestream(
&self,
post_id: u32,
) -> SzurubooruResult<impl futures_util::Stream<Item = Result<bytes::Bytes, reqwest::Error>>>
{
self.get_post_content(post_id, true)
.await
.map(|cr| cr.bytes_stream())
}
///Fetches the given post ID's image as a (Bytes)[bytes::Bytes] struct
pub async fn get_image_bytes(&self, post_id: u32) -> SzurubooruResult<bytes::Bytes> {
let content_response = self.get_post_content(post_id, false).await?;
content_response
.bytes()
.await
.map_err(SzurubooruClientError::RequestError)
}
///Fetches the given post ID's thumbnail as a (Bytes)[bytes::Bytes] struct
pub async fn get_thumbnail_bytes(&self, post_id: u32) -> SzurubooruResult<bytes::Bytes> {
let content_response = self.get_post_content(post_id, true).await?;
content_response
.bytes()
.await
.map_err(SzurubooruClientError::RequestError)
}
///Gets a post's image's URL
pub async fn get_image_url(&self, post_id: u32) -> SzurubooruResult<String> {
let post_resource = self.get_post(post_id).await?;
Ok(format!(
"{}{}",
self.client.base_url,
post_resource.content_url.unwrap()
))
}
///Gets a post's image's URL
pub async fn get_thumbnail_url(&self, post_id: u32) -> SzurubooruResult<String> {
let post_resource = self.get_post(post_id).await?;
Ok(format!(
"{}{}",
self.client.base_url,
post_resource.thumbnail_url.unwrap()
))
}
async fn write_content_to_file<S>(
&self,
file: &mut File,
stream: &mut S,
) -> SzurubooruResult<()>
where
S: futures_util::Stream<Item = Result<bytes::Bytes, reqwest::Error>> + Unpin,
{
let mut writer = BufWriter::new(file);
while let Some(bytes) = stream
.try_next()
.await
.map_err(SzurubooruClientError::RequestError)?
{
writer
.write_all(bytes.as_ref())
.map_err(SzurubooruClientError::IOError)?;
}
Ok(())
}
///Downloads a post's image and writes it to the given file handle
pub async fn download_image_to_file(
&self,
post_id: u32,
file: &mut File,
) -> SzurubooruResult<()> {
let mut stream = self.get_image_bytestream(post_id).await?;
self.write_content_to_file(file, &mut stream).await
}
///Downloads a post's image and writes it to the given path
pub async fn download_image_to_path(
&self,
post_id: u32,
path: impl AsRef<Path>,
) -> SzurubooruResult<()> {
let mut stream = self.get_image_bytestream(post_id).await?;
let mut file = File::open(path.as_ref()).map_err(SzurubooruClientError::IOError)?;
self.write_content_to_file(&mut file, &mut stream).await
}
///Downloads a post's thumbnail and writes it to the given file handle
pub async fn download_thumbnail_to_file(
&self,
post_id: u32,
file: &mut File,
) -> SzurubooruResult<()> {
let mut stream = self.get_thumbnail_bytestream(post_id).await?;
self.write_content_to_file(file, &mut stream).await
}
///Downloads a post's thumbnail and writes it to the given path
pub async fn download_thumbnail_to_path(
&self,
post_id: u32,
path: impl AsRef<Path>,
) -> SzurubooruResult<()> {
let mut stream = self.get_thumbnail_bytestream(post_id).await?;
let mut file = File::open(path.as_ref()).map_err(SzurubooruClientError::IOError)?;
self.write_content_to_file(&mut file, &mut stream).await
}
/// Retrieves posts that look like the input image
pub async fn reverse_search_file(
&self,
file: &mut File,
file_path: impl AsRef<str>,
) -> SzurubooruResult<ImageSearchResult> {
let request = self.prep_request(Method::POST, "/api/posts/reverse-search", None);
let image_part = self
.part_from_file(file)?
.file_name(file_path.as_ref().to_string());
let form = Form::new().part("content", image_part);
self.handle_request(request.multipart(form)).await
}
/// Retrieves posts that look like the input image from the given file path
pub async fn reverse_search_file_path(
&self,
file_path: impl AsRef<Path>,
) -> SzurubooruResult<ImageSearchResult> {
let mut file = File::open(&file_path).map_err(SzurubooruClientError::IOError)?;
let filename = file_path.as_ref().file_name().unwrap().to_str().unwrap();
self.reverse_search_file(&mut file, filename).await
}
/// Searches for an exact match of a file based on the SHA1 checksum
pub async fn posts_for_file(
&self,
mut file: &mut File,
) -> SzurubooruResult<PagedSearchResult<PostResource>> {
let mut hasher = Sha1::new();
std::io::copy(&mut file, &mut hasher).map_err(SzurubooruClientError::IOError)?;
let hash = hasher.finalize();
let hex_string = hex::encode(hash);
let qt = QueryToken::token(PostNamedToken::ContentChecksum, hex_string);
self.list_posts(Some(&vec![qt])).await
}
/// Searches for an exact match of a file path based on the SHA1 checksum
pub async fn posts_for_file_path(
&self,
file_path: impl AsRef<Path>,
) -> SzurubooruResult<PagedSearchResult<PostResource>> {
let mut file = File::open(file_path).map_err(SzurubooruClientError::IOError)?;
self.posts_for_file(&mut file).await
}
/// Retrieves information about an existing post.
pub async fn get_post(&self, post_id: u32) -> SzurubooruResult<PostResource> {
let path = format!("/api/post/{post_id}");
self.do_request(Method::GET, &path, None, None::<&String>)
.await
}
/// Retrieves information about posts that are before or after an existing post.
pub async fn get_around_post(&self, post_id: u32) -> SzurubooruResult<AroundPostResult> {
let path = format!("/api/post/{post_id}/around");
self.do_request(Method::GET, &path, None, None::<&String>)
.await
}
/// Deletes existing post. Related posts and tags are kept.
pub async fn delete_post(&self, post_id: u32, version: u32) -> SzurubooruResult<()> {
let path = format!("/api/post/{post_id}");
let version_obj = ResourceVersion { version };
self.do_request::<Value, _, _>(Method::DELETE, &path, None, Some(&version_obj))
.await
.map(|_| ())
}
///
/// Removes source post and merges all of its tags, relations, scores, favorites and comments to
/// the target post. If [MergePost::replace_content] is set to `true`, content of the target post
/// is replaced using the content of the source post; otherwise it remains unchanged. Source
/// post properties such as its safety, source, whether to loop the video and other scalar
/// values do not get transferred and are discarded.
///
pub async fn merge_post(&self, merge_opts: &MergePost) -> SzurubooruResult<PostResource> {
self.do_request(Method::POST, "/api/post-merge/", None, Some(merge_opts))
.await
}
/// Updates score of authenticated user for given post. Valid scores are -1, 0 and 1.
pub async fn rate_post(&self, post_id: u32, score: i8) -> SzurubooruResult<PostResource> {
let rating_obj = RateResource { score };
let path = format!("/api/post/{post_id}/score");
self.do_request(Method::PUT, &path, None, Some(&rating_obj))
.await
}
/// Marks the post as favorite for authenticated user.
pub async fn favorite_post(&self, post_id: u32) -> SzurubooruResult<PostResource> {
let path = format!("/api/post/{post_id}/favorite");
self.do_request(Method::POST, &path, None, None::<&String>)
.await
}
/// Unmarks the post as favorite for authenticated user.
pub async fn unfavorite_post(&self, post_id: u32) -> SzurubooruResult<PostResource> {
let path = format!("/api/post/{post_id}/favorite");
self.do_request(Method::DELETE, &path, None, None::<&String>)
.await
}
/// Retrieves the post that is currently featured on the main page in web client. If no post is
/// featured, <post> is [Option::None]. Note that this method exists mostly for compatibility
/// with setting featured post - most of the time, you'd want to use query global info which
/// contains more information.
pub async fn get_featured_post(&self) -> SzurubooruResult<Option<PostResource>> {
self.do_request(Method::GET, "/api/featured-post", None, None::<&String>)
.await
}
/// Features a post on the main page
pub async fn set_featured_post(&self, post_id: u32) -> SzurubooruResult<PostResource> {
let id_object = PostId { id: post_id };
self.do_request(Method::POST, "/api/featured-post", None, Some(&id_object))
.await
}
/// Lists all pool categories. Doesn't use paging.
pub async fn list_pool_categories(
&self,
) -> SzurubooruResult<UnpagedSearchResult<PoolCategoryResource>> {
self.do_request(Method::GET, "/api/pool-categories", None, None::<&String>)
.await
}
/// Creates a new pool category using specified parameters. Name must match
/// `pool_category_name_regex` from server's configuration. First category created becomes
/// the default category.
pub async fn create_pool_category(
&self,
new_cat: &CreateUpdatePoolCategory,
) -> SzurubooruResult<PoolCategoryResource> {
self.do_request(Method::POST, "/api/pool-categories", None, Some(new_cat))
.await
}
/// Updates an existing tag category using specified parameters. Name must match
/// `tag_category_name_regex` from server's configuration. All fields except the
/// [version](models::CreateUpdatePoolCategory::version) field are optional - update concerns
/// only the provided fields.
pub async fn update_pool_category<T>(
&self,
category_name: T,
update_cat: &CreateUpdatePoolCategory,
) -> SzurubooruResult<PoolCategoryResource>
where
T: AsRef<str> + Display,
{
let path = format!("/api/pool-category/{category_name}");
self.do_request(Method::PUT, &path, None, Some(update_cat))
.await
}
/// Retrieves information about an existing pool category.
pub async fn get_pool_category<T>(
&self,
category_name: T,
) -> SzurubooruResult<PoolCategoryResource>
where
T: AsRef<str> + Display,
{
let path = format!("/api/pool-category/{category_name}");
self.do_request(Method::GET, &path, None, None::<&String>)
.await
}
/// Deletes existing pool category. The pool category to be deleted must have no usages.
pub async fn delete_pool_category<T>(
&self,
category_name: T,
version: u32,
) -> SzurubooruResult<()>
where
T: AsRef<str> + Display,
{
let path = format!("/api/pool-category/{category_name}");
let resource_obj = ResourceVersion { version };
self.do_request::<Value, _, _>(Method::DELETE, &path, None, Some(&resource_obj))
.await
.map(|_| ())
}
/// Sets given pool category as default. All new pools created manually or automatically will
/// have this category.
pub async fn set_default_pool_category<T>(
&self,
category_name: T,
) -> SzurubooruResult<PoolCategoryResource>
where
T: AsRef<str> + Display,
{
let path = format!("/api/pool-category/{category_name}/default");
self.do_request(Method::PUT, &path, None, None::<&String>)
.await
}
/// Searches for pools.
/// Anonymous tokens are the same as the [name](tokens::PoolNamedToken::Name) token
pub async fn list_pools(
&self,
query: Option<&Vec<QueryToken>>,
) -> SzurubooruResult<PagedSearchResult<PoolResource>> {
self.do_request(Method::GET, "/api/pools", query, None::<&String>)
.await
}
/// Creates a new pool using specified parameters. Names, suggestions and implications must
/// match `pool_name_regex` from server's configuration. Category must exist and is the same as
/// [name](models::PoolCategoryResource::name) field.
/// [posts](models::CreateUpdatePool::posts) is an optional list of integer post IDs. If the
/// specified posts do not exist, an error will be thrown.
pub async fn create_pool(
&self,
create_update_pool: &CreateUpdatePool,
) -> SzurubooruResult<PoolResource> {
self.do_request(Method::POST, "/api/pool", None, Some(create_update_pool))
.await
}
/// Updates an existing pool using specified parameters. [name](models::CreateUpdatePool::name),
/// must match `pool_name_regex` from server's configuration.
/// [category](models::CreateUpdatePool::category) must exist and is the same as
/// [name](models::PoolCategoryResource::name) field. [posts](models::CreateUpdatePool::posts)
/// is an optional list of integer post IDs. If the specified posts do not exist yet, an error
/// will be thrown. The full list of post IDs must be provided if they are being updated, and
/// the previous list of posts will be replaced with the new one. All fields except
/// [version](models::CreateUpdatePool::version) are optional - update concerns only provided
/// fields.
pub async fn update_pool(
&self,
pool_id: u32,
create_update_pool: &CreateUpdatePool,
) -> SzurubooruResult<PoolResource> {
let path = format!("/api/pool/{pool_id}");
self.do_request(Method::PUT, &path, None, Some(create_update_pool))
.await
}
/// Retrieves information about an existing pool.
pub async fn get_pool(&self, pool_id: u32) -> SzurubooruResult<PoolResource> {
let path = format!("/api/pool/{pool_id}");
self.do_request(Method::GET, &path, None, None::<&String>)
.await
}
/// Deletes existing pool. All posts in the pool will only have their relation to the pool
/// removed.
pub async fn delete_pool(&self, pool_id: u32, version: u32) -> SzurubooruResult<()> {
let path = format!("/api/pool/{pool_id}");
let version_obj = ResourceVersion { version };
self.do_request::<Value, _, _>(Method::DELETE, &path, None, Some(&version_obj))
.await
.map(|_| ())
}
/// Removes source pool and merges all of its posts with the target pool. Other pool properties
/// such as category and aliases do not get transferred and are discarded.
pub async fn merge_pools(&self, merge_pool: &MergePool) -> SzurubooruResult<PoolResource> {
self.do_request(Method::POST, "/api/pool-merge", None, Some(merge_pool))
.await
}
/// Searches for comments.
/// Anonymous tokens are the same as the [text](tokens::CommentNamedToken::text) token
pub async fn list_comments(
&self,
query: Option<&Vec<QueryToken>>,
) -> SzurubooruResult<PagedSearchResult<CommentResource>> {
self.do_request(Method::GET, "/api/comments", query, None::<&String>)
.await
}
/// Creates a new comment under given post
pub async fn create_comment(
&self,
new_comment: &CreateUpdateComment,
) -> SzurubooruResult<CommentResource> {
self.do_request(Method::POST, "/api/comments", None, Some(new_comment))
.await
}
/// Updates an existing comment text
pub async fn update_comment(
&self,
comment_id: u32,
update_comment: &CreateUpdateComment,
) -> SzurubooruResult<CommentResource> {
let path = format!("/api/comment/{comment_id}");
self.do_request(Method::PUT, &path, None, Some(update_comment))
.await
}
/// Retrieves information about an existing comment
pub async fn get_comment(&self, comment_id: u32) -> SzurubooruResult<CommentResource> {
let path = format!("/api/comment/{comment_id}");
self.do_request(Method::GET, &path, None, None::<&String>)
.await
}
/// Deletes existing comment
pub async fn delete_comment(&self, comment_id: u32, version: u32) -> SzurubooruResult<()> {
let path = format!("/api/comment/{comment_id}");
let version_obj = ResourceVersion { version };
self.do_request::<Value, _, _>(Method::DELETE, &path, None, Some(&version_obj))
.await
.map(|_| ())
}
/// Updates score of authenticated user for given comment. Valid scores are -1, 0 and 1.
pub async fn rate_comment(
&self,
comment_id: u32,
score: i8,
) -> SzurubooruResult<CommentResource> {
let path = format!("/api/comment/{comment_id}/score");
let rating = RateResource { score };
self.do_request(Method::PUT, &path, None, Some(&rating))
.await
}
/// Searches for users
/// Anonymous tokens are the same as the [name](tokens::UserNamedToken)
/// See [UserNamedToken](tokens::UserNamedToken) and [UserSortToken](tokens::UserSortToken)
/// for type-safe tokens
pub async fn list_users(
&self,
query: Option<&Vec<QueryToken>>,
) -> SzurubooruResult<PagedSearchResult<UserResource>> {
self.do_request(Method::GET, "/api/users", query, None::<&String>)
.await
}
async fn create_update_user(
&self,
method: Method,
path: &str,
new_user: &CreateUpdateUser,
file: Option<&mut File>,
file_name: Option<impl AsRef<str>>,
) -> SzurubooruResult<UserResource> {
match file {
None => self.do_request(method, path, None, Some(new_user)).await,
Some(file) => {
let request = self.prep_request(method, path, None);
let metadata_str = serde_json::to_string(&new_user)
.map_err(SzurubooruClientError::JSONSerializationError)?;
let metadata_part = Part::text(metadata_str);
let content_part = self
.part_from_file(file)?
.file_name(file_name.unwrap().as_ref().to_string());
let form = Form::new()
.part("avatar", content_part)
.part("metadata", metadata_part);
self.handle_request(request.multipart(form)).await
}
}
}
/// Creates a new user using specified parameters. Names and passwords must match
/// `user_name_regex` and `password_regex` from server's configuration, respectively.
/// Email address, rank and avatar fields are optional. Avatar style can be either
/// [gravatar](models::UserAvatarStyle::Gravatar) or [manual](models::UserAvatarStyle::Manual).
/// `manual` avatar style requires client to pass also the `avatar` file.
/// If the rank is empty and the user happens to be the first user ever created,
/// become an administrator, whereas subsequent users will be given the rank indicated by
/// `default_rank` in the server's configuration.
pub async fn create_user(&self, new_user: &CreateUpdateUser) -> SzurubooruResult<UserResource> {
self.do_request(Method::POST, "/api/users", None, Some(new_user))
.await
}
/// Create a [UserResource](models::UserResource) with the included Avatar file
/// See [create_user](SzurubooruRequest::create_user) for other applicable fields and
/// restrictions
pub async fn create_user_with_avatar_file(
&self,
avatar: &mut File,
file_name: impl AsRef<str>,
new_user: &CreateUpdateUser,
) -> SzurubooruResult<UserResource> {
self.create_update_user(
Method::POST,
"/api/users",
new_user,
Some(avatar),
Some(file_name),
)
.await
}
/// Create a [UserResource](models::UserResource) with the included Avatar file path
/// See [create_user](SzurubooruRequest::create_user) for other applicable fields and
/// restrictions
pub async fn create_user_with_avatar_path(
&self,
avatar_path: impl AsRef<Path>,
new_user: &CreateUpdateUser,
) -> SzurubooruResult<UserResource> {
let mut file = File::open(&avatar_path).map_err(SzurubooruClientError::IOError)?;
let filename = avatar_path.as_ref().file_name().unwrap().to_str().unwrap();
self.create_update_user(
Method::POST,
"/api/users",
new_user,
Some(&mut file),
Some(filename),
)
.await
}
/// Updates user using specified parameters. Names and passwords must match
/// `user_name_regex` and `password_regex` from server's configuration, respectively.
/// Email address, rank and avatar fields are optional. Avatar style can be either
/// [gravatar](models::UserAvatarStyle::Gravatar) or [manual](models::UserAvatarStyle::Manual).
/// `manual` avatar style requires client to pass also the `avatar` file.
/// All fields except the [version](models::CreateUpdateUser::version) are optional
/// - update concerns only provided fields.
pub async fn update_user<T>(
&self,
name: T,
update_user: &CreateUpdateUser,
) -> SzurubooruResult<UserResource>
where
T: AsRef<str> + Display,
{
let path = format!("/api/user/{name}");
self.do_request(Method::PUT, path, None, Some(update_user))
.await
}
/// Update a [UserResource](models::UserResource) with the included Avatar file
/// See [update_user](SzurubooruRequest::update_user) for other applicable fields and
/// restrictions
pub async fn update_user_with_avatar_file<T>(
&self,
name: T,
avatar: &mut File,
file_name: impl AsRef<str>,
update_user: &CreateUpdateUser,
) -> SzurubooruResult<UserResource>
where
T: AsRef<str> + Display,
{
let path = format!("/api/user/{name}");
self.create_update_user(
Method::PUT,
&path,
update_user,
Some(avatar),
Some(file_name),
)
.await
}
/// Update a [UserResource](models::UserResource) with the included Avatar file path
/// See [update_user](SzurubooruRequest::update_user) for other applicable fields and
/// restrictions
pub async fn update_user_with_avatar_path<T>(
&self,
name: T,
avatar_path: impl AsRef<Path>,
new_user: &CreateUpdateUser,
) -> SzurubooruResult<UserResource>
where
T: AsRef<str> + Display,
{
let path = format!("/api/user/{name}");
let mut file = File::open(&avatar_path).map_err(SzurubooruClientError::IOError)?;
let filename = avatar_path.as_ref().file_name().unwrap().to_str().unwrap();
self.create_update_user(
Method::PUT,
&path,
new_user,
Some(&mut file),
Some(filename),
)
.await
}
/// Retrieves information about an existing user
pub async fn get_user<T>(&self, name: T) -> SzurubooruResult<UserResource>
where
T: AsRef<str> + Display,
{
let path = format!("/api/user/{name}");
self.do_request(Method::GET, &path, None, None::<&String>)
.await
}
/// Retrieves the user's avatar URL
pub async fn get_user_avatar_url<T>(&self, name: T) -> SzurubooruResult<String>
where
T: AsRef<str> + Display,
{
let user = self.get_user(name).await?;
Ok(format!(
"{}{}",
self.client.base_url,
user.avatar_url.unwrap()
))
}
/// Deletes existing user
pub async fn delete_user<T>(&self, name: T, version: u32) -> SzurubooruResult<()>
where
T: AsRef<str> + Display,
{
let path = format!("/api/user/{name}");
let version_obj = ResourceVersion { version };
self.do_request::<Value, _, _>(Method::DELETE, &path, None, Some(&version_obj))
.await
.map(|_| ())
}
/// Listing user tokens for the given user.
pub async fn list_user_tokens<T>(
&self,
name: T,
) -> SzurubooruResult<UnpagedSearchResult<UserAuthTokenResource>>
where
T: AsRef<str> + Display,
{
let path = format!("/api/user-tokens/{name}");
self.do_request(Method::GET, &path, None, None::<&String>)
.await
}
/// Creates a new user token that can be used for authentication of API endpoints
/// instead of a password.
pub async fn create_user_token<T>(
&self,
name: T,
create_token: &CreateUpdateUserAuthToken,
) -> SzurubooruResult<UserAuthTokenResource>
where
T: AsRef<str> + Display,
{
let path = format!("/api/user-token/{name}");
self.do_request(Method::POST, &path, None, Some(create_token))
.await
}
/// Updates an existing user token using specified parameters. All fields except the
/// [version](models::CreateUpdateUserAuthToken::version) are optional - update concerns only
/// provided fields.
pub async fn update_user_token<T>(
&self,
name: T,
token: T,
update_token: &CreateUpdateUserAuthToken,
) -> SzurubooruResult<UserAuthTokenResource>
where
T: AsRef<str> + Display,
{
let path = format!("/api/user-token/{name}/{token}");
self.do_request(Method::PUT, &path, None, Some(update_token))
.await
}
/// Deletes an existing user token using specified parameters. All fields except the
/// [version](models::CreateUpdateUserAuthToken::version) are optional - update concerns only
/// provided fields.
pub async fn delete_user_token<T>(
&self,
name: T,
token: T,
version: u32,
) -> SzurubooruResult<()>
where
T: AsRef<str> + Display,
{
let path = format!("/api/user-token/{name}/{token}");
let version_obj = ResourceVersion { version };
self.do_request::<Value, _, _>(Method::DELETE, &path, None, Some(&version_obj))
.await
.map(|_| ())
}
/// Sends a confirmation email to given user. The email contains link containing a token. The
/// token cannot be guessed, thus using such link proves that the person who requested to reset
/// the password also owns the mailbox, which is a strong indication they are the rightful
/// owner of the account.
/// Argument is either the user's username or email address
pub async fn password_reset_request<T>(&self, email_or_name: T) -> SzurubooruResult<()>
where
T: AsRef<str> + Display,
{
let encoded = STANDARD.encode(email_or_name.as_ref().as_bytes());
let path = format!("/api/password-reset/{encoded}");
self.do_request(Method::GET, &path, None, None::<&String>)
.await
}
/// Generates a new password for given user. Password is sent as plain-text, so it is
/// recommended to connect through HTTPS.
pub async fn password_reset_confirm<T>(
&self,
email_or_name: T,
token: impl AsRef<str>,
) -> SzurubooruResult<TemporaryPassword>
where
T: AsRef<str> + Display,
{
let encoded = STANDARD.encode(email_or_name.as_ref().as_bytes());
let path = format!("/api/password-reset/{encoded}");
let token_obj = PasswordResetToken {
token: token.as_ref().to_string(),
};
self.do_request(Method::POST, &path, None, Some(&token_obj))
.await
}
/// Lists recent resource snapshots.
/// See [SnapshotNamedToken](tokens::SnapshotNamedToken) for query tokens.
/// There are no sort tokens. The snapshots are always sorted by creation time.
pub async fn list_snapshots(
&self,
query: Option<&Vec<QueryToken>>,
) -> SzurubooruResult<PagedSearchResult<SnapshotResource>> {
self.do_request(Method::GET, "/api/snapshots", query, None::<&String>)
.await
}
/// Retrieves simple statistics. [featured_post](models::GlobalInfo::featured_post) is
/// [None](Option::None) if there is no featured post yet.
/// [server_time](models::GlobalInfo::server_time) is pretty much the same as the Date HTTP
/// field, only formatted in a manner consistent with other dates. Values in config key are
/// taken directly from the server config, with the exception of privilege array keys being
/// converted to lower camel case to match the API convention.
pub async fn get_global_info(&self) -> SzurubooruResult<GlobalInfo> {
self.do_request(Method::GET, "/api/info", None, None::<&String>)
.await
}
/// Puts a file in temporary storage and assigns it a token that can be used in other requests.
/// The files uploaded that way are deleted after a short while so clients shouldn't use it
/// as a free upload service.
pub async fn upload_temporary_file(
&self,
file: &mut File,
file_name: impl AsRef<str>,
) -> SzurubooruResult<TemporaryFileUpload> {
let request = self.prep_request(Method::POST, "/api/uploads", None);
let content_part = self
.part_from_file(file)?
.file_name(file_name.as_ref().to_string());
let form = Form::new().part("content", content_part);
self.handle_request(request.multipart(form)).await
}
/// Puts a file from a given file path in temporary storage and assigns it a token that can be
/// used in other requests.
/// The files uploaded that way are deleted after a short while so clients shouldn't use it
/// as a free upload service.
pub async fn upload_temporary_file_from_path(
&self,
file_path: impl AsRef<Path>,
) -> SzurubooruResult<TemporaryFileUpload> {
let mut file = File::open(&file_path).map_err(SzurubooruClientError::IOError)?;
let filename = file_path.as_ref().file_name().unwrap().to_str().unwrap();
self.upload_temporary_file(&mut file, filename).await
}
}
/// Which kind of authentication is used. Automatically hides any sensitive information when printed
/// using [Debug](std::fmt::Debug)
enum SzurubooruAuth {
// The encoded token
TokenAuth(String),
BasicAuth(String, String),
#[allow(dead_code)]
None,
}
impl std::fmt::Debug for SzurubooruAuth {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "SzurubooruAuth ()")
}
}