mirror of
https://git.dirksys.ovh/dirk/bankserver.git
synced 2025-12-20 11:09:21 +01:00
471 lines
12 KiB
Rust
471 lines
12 KiB
Rust
use std::sync::Arc;
|
|
|
|
use async_lock::RwLock;
|
|
use bank_core::{
|
|
ApiError, NameOrUuid, TokenResponse,
|
|
account::{Account, UserAccountInfo},
|
|
chat::{Chat, ChatInfo, ChatMessage, SendMessage, StartChat},
|
|
meta::{Bank, Motd},
|
|
pagination::{Pagination, RequestPagination},
|
|
transaction::{Direction, Transaction},
|
|
user::{User, UserAccounts, UserBalance},
|
|
};
|
|
use reqwest::{Method, RequestBuilder, header::CONTENT_TYPE};
|
|
use serde::de::DeserializeOwned;
|
|
use serde_json::json;
|
|
use thiserror::Error;
|
|
use url::Url;
|
|
use uuid::Uuid;
|
|
|
|
pub struct HttpClient {
|
|
url: Url,
|
|
client: reqwest::Client,
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum ClientError {
|
|
#[error("url: {_0}")]
|
|
Url(#[from] url::ParseError),
|
|
#[error("request: {_0}")]
|
|
Reqwest(#[from] reqwest::Error),
|
|
#[error("unexpected content type")]
|
|
UnexpectedContentType,
|
|
#[error("api: {_0:?}")]
|
|
Api(ApiError<'static>),
|
|
#[error("json: {_0:?}")]
|
|
Json(serde_json::Error),
|
|
}
|
|
|
|
impl HttpClient {
|
|
fn make_request(
|
|
&self,
|
|
method: reqwest::Method,
|
|
url: impl MakeUrl,
|
|
) -> Result<RequestBuilder, url::ParseError> {
|
|
let url = url.make_url(self.url.clone())?;
|
|
Ok(self.client.request(method, url))
|
|
}
|
|
|
|
pub fn get(&self, url: impl MakeUrl) -> Result<RequestBuilder, url::ParseError> {
|
|
self.make_request(Method::GET, url)
|
|
}
|
|
|
|
pub fn post(&self, url: impl MakeUrl) -> Result<RequestBuilder, url::ParseError> {
|
|
self.make_request(Method::POST, url)
|
|
}
|
|
|
|
pub fn put(&self, url: impl MakeUrl) -> Result<RequestBuilder, url::ParseError> {
|
|
self.make_request(Method::PUT, url)
|
|
}
|
|
|
|
async fn credentials_request(
|
|
&self,
|
|
path: &str,
|
|
username: &str,
|
|
password: &str,
|
|
) -> Result<TokenResponse, ClientError> {
|
|
let response = self
|
|
.client
|
|
.post(path)
|
|
.json(&json!({ "username": username, "password": password }))
|
|
.send()
|
|
.await?;
|
|
let body = handle_response::<TokenResponse>(response).await?;
|
|
Ok(body)
|
|
}
|
|
|
|
pub fn login(
|
|
&self,
|
|
username: &str,
|
|
password: &str,
|
|
) -> impl Future<Output = Result<TokenResponse, ClientError>> {
|
|
self.credentials_request("/api/login", username, password)
|
|
}
|
|
|
|
pub fn register(
|
|
&self,
|
|
username: &str,
|
|
password: &str,
|
|
) -> impl Future<Output = Result<TokenResponse, ClientError>> {
|
|
self.credentials_request("/api/register", username, password)
|
|
}
|
|
}
|
|
|
|
pub struct ApiClient {
|
|
client: HttpClient,
|
|
token: RwLock<Arc<str>>,
|
|
}
|
|
|
|
impl ApiClient {
|
|
async fn credentials_request(
|
|
&self,
|
|
fut: impl Future<Output = Result<TokenResponse, ClientError>>,
|
|
) -> Result<(), ClientError> {
|
|
let mut token = self.token.write().await;
|
|
let body = fut.await?;
|
|
*token = body.token.into();
|
|
Ok(())
|
|
}
|
|
|
|
pub fn login(
|
|
&self,
|
|
username: &str,
|
|
password: &str,
|
|
) -> impl Future<Output = Result<(), ClientError>> {
|
|
self.credentials_request(self.client.login(username, password))
|
|
}
|
|
|
|
pub fn register(
|
|
&self,
|
|
username: &str,
|
|
password: &str,
|
|
) -> impl Future<Output = Result<(), ClientError>> {
|
|
self.credentials_request(self.client.register(username, password))
|
|
}
|
|
|
|
async fn token(&self) -> Arc<str> {
|
|
(*self.token.read().await).clone()
|
|
}
|
|
|
|
#[inline]
|
|
pub const fn users(&self) -> UsersApi {
|
|
UsersApi { api: self }
|
|
}
|
|
|
|
#[inline]
|
|
pub const fn accounts(&self) -> AccountsApi {
|
|
AccountsApi { api: self }
|
|
}
|
|
|
|
#[inline]
|
|
pub const fn chats(&self) -> ChatsApi {
|
|
ChatsApi { api: self }
|
|
}
|
|
}
|
|
|
|
pub async fn handle_response<T: DeserializeOwned>(
|
|
response: reqwest::Response,
|
|
) -> Result<T, ClientError> {
|
|
let response = response.error_for_status()?;
|
|
let content_type = response.headers().get(CONTENT_TYPE).expect("content-type");
|
|
if content_type != "application/json" {
|
|
return Err(ClientError::UnexpectedContentType);
|
|
}
|
|
let status = response.status();
|
|
let bytes = response.bytes().await?;
|
|
if status.is_success() {
|
|
serde_json::from_slice::<T>(&bytes).map_err(ClientError::Json)
|
|
} else {
|
|
let mut error =
|
|
serde_json::from_slice::<ApiError<'static>>(&bytes).map_err(ClientError::Json)?;
|
|
error.status = status;
|
|
Err(ClientError::Api(error))
|
|
}
|
|
}
|
|
|
|
macro_rules! request {
|
|
($api:expr, $method:ident, $path:expr) => {{
|
|
let api = &$api;
|
|
api.client.$method($path)?.auth(api).await
|
|
}};
|
|
}
|
|
|
|
pub struct MetaApi<'a> {
|
|
api: &'a ApiClient,
|
|
}
|
|
|
|
impl MetaApi<'_> {
|
|
pub async fn motd(&self) -> Result<Motd, ClientError> {
|
|
let response = request!(self.api, get, "/api/meta/motd").send().await?;
|
|
let body = handle_response(response).await?;
|
|
Ok(body)
|
|
}
|
|
|
|
pub async fn bank(&self) -> Result<Bank, ClientError> {
|
|
let response = request!(self.api, get, "/api/meta/bank").send().await?;
|
|
let body = handle_response(response).await?;
|
|
Ok(body)
|
|
}
|
|
}
|
|
|
|
pub struct UsersApi<'a> {
|
|
api: &'a ApiClient,
|
|
}
|
|
|
|
impl UsersApi<'_> {
|
|
pub async fn list(&self, limit: u64, offset: u64) -> Result<Pagination<User>, ClientError> {
|
|
let response = request!(
|
|
self.api,
|
|
get,
|
|
RequestUrl::simple(["api", "users"], limit, offset)
|
|
)
|
|
.send()
|
|
.await?;
|
|
let body = handle_response(response).await?;
|
|
Ok(body)
|
|
}
|
|
|
|
pub async fn accounts(&self) -> Result<Vec<UserAccountInfo>, ClientError> {
|
|
let response = request!(self.api, get, "/api/users/@me/accounts")
|
|
.send()
|
|
.await?;
|
|
let body = handle_response::<UserAccounts>(response).await?;
|
|
Ok(body.result)
|
|
}
|
|
|
|
pub async fn balance(&self) -> Result<u64, ClientError> {
|
|
let response = request!(self.api, get, "/api/users/@me/balance")
|
|
.send()
|
|
.await?;
|
|
let body = handle_response::<UserBalance>(response).await?;
|
|
Ok(body.balance)
|
|
}
|
|
|
|
pub async fn transactions(
|
|
&self,
|
|
direction: Option<Direction>,
|
|
limit: u64,
|
|
offset: u64,
|
|
) -> Result<Pagination<Transaction>, ClientError> {
|
|
let response = request!(
|
|
self.api,
|
|
get,
|
|
RequestUrl {
|
|
path: ["api", "users", "@me", "transactions"],
|
|
pagination: RequestPagination { limit, offset },
|
|
extra_query: direction.map(|dir| ("direction", dir))
|
|
}
|
|
)
|
|
.send()
|
|
.await?;
|
|
let body = handle_response(response).await?;
|
|
Ok(body)
|
|
}
|
|
}
|
|
|
|
pub struct AccountsApi<'a> {
|
|
api: &'a ApiClient,
|
|
}
|
|
|
|
impl AccountsApi<'_> {
|
|
pub async fn list(&self, limit: u64, offset: u64) -> Result<Pagination<Account>, ClientError> {
|
|
let response = request!(
|
|
self.api,
|
|
get,
|
|
RequestUrl::simple(["api", "accounts"], limit, offset)
|
|
)
|
|
.send()
|
|
.await?;
|
|
let body = handle_response(response).await?;
|
|
Ok(body)
|
|
}
|
|
|
|
pub async fn transactions(
|
|
&self,
|
|
id: Uuid,
|
|
direction: Option<Direction>,
|
|
limit: u64,
|
|
offset: u64,
|
|
) -> Result<Pagination<Transaction>, ClientError> {
|
|
let response = request!(
|
|
self.api,
|
|
get,
|
|
RequestUrl {
|
|
path: ["api", "accounts", &id.to_string(), "transactions"],
|
|
pagination: RequestPagination { limit, offset },
|
|
extra_query: direction.map(|dir| ("direction", dir))
|
|
}
|
|
)
|
|
.send()
|
|
.await?;
|
|
let body = handle_response(response).await?;
|
|
Ok(body)
|
|
}
|
|
}
|
|
|
|
pub struct ChatsApi<'a> {
|
|
api: &'a ApiClient,
|
|
}
|
|
|
|
impl ChatsApi<'_> {
|
|
pub async fn list(&self, limit: u64, offset: u64) -> Result<Pagination<ChatInfo>, ClientError> {
|
|
let response = request!(
|
|
self.api,
|
|
get,
|
|
RequestUrl::simple(["api", "chats"], limit, offset)
|
|
)
|
|
.send()
|
|
.await?;
|
|
let body = handle_response(response).await?;
|
|
Ok(body)
|
|
}
|
|
|
|
pub async fn start(&self, user: impl Into<NameOrUuid>) -> Result<Chat, ClientError> {
|
|
let response = request!(self.api, get, "/api/chats")
|
|
.json(&StartChat { user: user.into() })
|
|
.send()
|
|
.await?;
|
|
let body = handle_response(response).await?;
|
|
Ok(body)
|
|
}
|
|
|
|
pub async fn info(&self, id: Uuid) -> Result<ChatInfo, ClientError> {
|
|
let response = request!(self.api, get, ["api", "chats", &id.to_string()])
|
|
.send()
|
|
.await?;
|
|
let body = handle_response(response).await?;
|
|
Ok(body)
|
|
}
|
|
|
|
pub async fn messages(
|
|
&self,
|
|
chat: Uuid,
|
|
limit: u64,
|
|
offset: u64,
|
|
) -> Result<Pagination<ChatMessage>, ClientError> {
|
|
let response = request!(
|
|
self.api,
|
|
get,
|
|
RequestUrl::simple(["api", "chats", &chat.to_string()], limit, offset)
|
|
)
|
|
.send()
|
|
.await?;
|
|
let body = handle_response(response).await?;
|
|
Ok(body)
|
|
}
|
|
|
|
pub async fn send(
|
|
&self,
|
|
chat: Uuid,
|
|
message: &SendMessage,
|
|
) -> Result<Pagination<ChatMessage>, ClientError> {
|
|
let response = request!(
|
|
self.api,
|
|
get,
|
|
["api", "chats", &chat.to_string(), "messages"]
|
|
)
|
|
.json(message)
|
|
.send()
|
|
.await?;
|
|
let body = handle_response(response).await?;
|
|
Ok(body)
|
|
}
|
|
|
|
pub async fn mark_read(
|
|
&self,
|
|
chat: Uuid,
|
|
message: Uuid,
|
|
) -> Result<Pagination<ChatMessage>, ClientError> {
|
|
let response = request!(
|
|
self.api,
|
|
get,
|
|
[
|
|
"api",
|
|
"chats",
|
|
&chat.to_string(),
|
|
"messages",
|
|
&message.to_string(),
|
|
"read"
|
|
]
|
|
)
|
|
.send()
|
|
.await?;
|
|
let body = handle_response(response).await?;
|
|
Ok(body)
|
|
}
|
|
}
|
|
|
|
trait RequestBuilderExt {
|
|
async fn auth(self, api: &ApiClient) -> Self;
|
|
}
|
|
|
|
impl RequestBuilderExt for RequestBuilder {
|
|
async fn auth(self, api: &ApiClient) -> Self {
|
|
self.bearer_auth(api.token().await)
|
|
}
|
|
}
|
|
|
|
pub trait MakeUrl {
|
|
fn make_url(self, base: Url) -> Result<Url, url::ParseError>;
|
|
}
|
|
|
|
impl MakeUrl for &'_ str {
|
|
fn make_url(self, base: Url) -> Result<Url, url::ParseError> {
|
|
base.join(self)
|
|
}
|
|
}
|
|
|
|
impl<'a> MakeUrl for &'a [&'a str] {
|
|
fn make_url(self, mut base: Url) -> Result<Url, url::ParseError> {
|
|
base.path_segments_mut().unwrap().extend(self);
|
|
Ok(base)
|
|
}
|
|
}
|
|
impl<'a, const N: usize> MakeUrl for [&'a str; N] {
|
|
fn make_url(self, mut base: Url) -> Result<Url, url::ParseError> {
|
|
base.path_segments_mut().unwrap().extend(self);
|
|
Ok(base)
|
|
}
|
|
}
|
|
|
|
impl<'a, P, S, Q, K, V> MakeUrl for (P, Q)
|
|
where
|
|
Q: IntoIterator<Item = (K, V)>,
|
|
P: IntoIterator<Item = S>,
|
|
S: AsRef<str>,
|
|
K: AsRef<str>,
|
|
V: AsRef<str>,
|
|
{
|
|
fn make_url(self, mut base: Url) -> Result<Url, url::ParseError> {
|
|
base.path_segments_mut().unwrap().extend(self.0);
|
|
base.query_pairs_mut().extend_pairs(self.1);
|
|
Ok(base)
|
|
}
|
|
}
|
|
|
|
pub struct RequestUrl<P, Q> {
|
|
path: P,
|
|
pagination: RequestPagination,
|
|
extra_query: Q,
|
|
}
|
|
|
|
impl<'a, P: IntoIterator<Item = &'a str>> RequestUrl<P, ()> {
|
|
pub fn simple(
|
|
path: P,
|
|
limit: u64,
|
|
offset: u64,
|
|
) -> RequestUrl<P, std::iter::Empty<(String, String)>> {
|
|
RequestUrl {
|
|
path,
|
|
pagination: RequestPagination { limit, offset },
|
|
extra_query: std::iter::empty(),
|
|
}
|
|
}
|
|
}
|
|
|
|
macro_rules! num {
|
|
($num:expr) => {
|
|
itoa::Buffer::new().format($num)
|
|
};
|
|
}
|
|
|
|
impl<'a, P, Q, K, V> MakeUrl for RequestUrl<P, Q>
|
|
where
|
|
P: IntoIterator<Item = &'a str>,
|
|
Q: IntoIterator<Item = (K, V)>,
|
|
K: AsRef<str>,
|
|
V: AsRef<str>,
|
|
{
|
|
fn make_url(self, mut base: Url) -> Result<Url, url::ParseError> {
|
|
base.path_segments_mut().unwrap().extend(self.path);
|
|
let mut query = base.query_pairs_mut();
|
|
query.extend_pairs([
|
|
("limit", num!(self.pagination.limit)),
|
|
("offset", num!(self.pagination.offset)),
|
|
]);
|
|
query.extend_pairs(self.extra_query);
|
|
drop(query);
|
|
Ok(base)
|
|
}
|
|
}
|