mirror of
https://github.com/Nutomic/ibis.git
synced 2025-02-04 00:31:35 +00:00
Split up api files
This commit is contained in:
parent
cdcc992b75
commit
ae44c169ba
8 changed files with 521 additions and 424 deletions
|
@ -1,406 +0,0 @@
|
|||
use crate::common::{
|
||||
article::*,
|
||||
instance::*,
|
||||
newtypes::{ArticleId, ConflictId, PersonId},
|
||||
user::*,
|
||||
utils::http_protocol_str,
|
||||
*,
|
||||
};
|
||||
use comment::{CreateCommentParams, DbCommentView, EditCommentParams};
|
||||
use http::{Method, StatusCode};
|
||||
use leptos::{prelude::ServerFnError, server_fn::error::NoCustomError};
|
||||
use log::{error, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Debug, sync::LazyLock};
|
||||
use url::Url;
|
||||
|
||||
pub static CLIENT: LazyLock<ApiClient> = LazyLock::new(|| {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
ApiClient::new(reqwest::Client::new(), None)
|
||||
}
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
ApiClient::new()
|
||||
}
|
||||
});
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ApiClient {
|
||||
#[cfg(feature = "ssr")]
|
||||
client: reqwest::Client,
|
||||
pub hostname: String,
|
||||
ssl: bool,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn new(client: reqwest::Client, hostname_: Option<String>) -> Self {
|
||||
use leptos::config::get_config_from_str;
|
||||
let leptos_options = get_config_from_str(include_str!("../../Cargo.toml")).unwrap();
|
||||
let mut hostname = leptos_options.site_addr.to_string();
|
||||
// required for tests
|
||||
if let Some(hostname_) = hostname_ {
|
||||
hostname = hostname_;
|
||||
}
|
||||
Self {
|
||||
client,
|
||||
hostname,
|
||||
ssl: false,
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn new() -> Self {
|
||||
use leptos_use::use_document;
|
||||
let hostname = use_document().location().unwrap().host().unwrap();
|
||||
let ssl = !cfg!(debug_assertions);
|
||||
Self { hostname, ssl }
|
||||
}
|
||||
|
||||
pub async fn get_article(&self, data: GetArticleParams) -> Option<DbArticleView> {
|
||||
self.get("/api/v1/article", Some(data)).await
|
||||
}
|
||||
|
||||
pub async fn list_articles(&self, data: ListArticlesParams) -> Option<Vec<DbArticle>> {
|
||||
Some(self.get("/api/v1/article/list", Some(data)).await.unwrap())
|
||||
}
|
||||
|
||||
pub async fn register(
|
||||
&self,
|
||||
params: RegisterUserParams,
|
||||
) -> Result<LocalUserView, ServerFnError> {
|
||||
self.post("/api/v1/account/register", Some(¶ms)).await
|
||||
}
|
||||
|
||||
pub async fn login(&self, params: LoginUserParams) -> Result<LocalUserView, ServerFnError> {
|
||||
self.post("/api/v1/account/login", Some(¶ms)).await
|
||||
}
|
||||
|
||||
pub async fn create_article(
|
||||
&self,
|
||||
data: &CreateArticleParams,
|
||||
) -> Result<DbArticleView, ServerFnError> {
|
||||
self.post("/api/v1/article", Some(&data)).await
|
||||
}
|
||||
|
||||
pub async fn edit_article_with_conflict(
|
||||
&self,
|
||||
params: &EditArticleParams,
|
||||
) -> Result<Option<ApiConflict>, ServerFnError> {
|
||||
self.patch("/api/v1/article", Some(¶ms)).await
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub async fn edit_article(&self, params: &EditArticleParams) -> Option<DbArticleView> {
|
||||
let edit_res = self
|
||||
.edit_article_with_conflict(params)
|
||||
.await
|
||||
.map_err(|e| error!("edit failed {e}"))
|
||||
.ok()?;
|
||||
assert_eq!(None, edit_res);
|
||||
|
||||
self.get_article(GetArticleParams {
|
||||
title: None,
|
||||
domain: None,
|
||||
id: Some(params.article_id),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_comment(
|
||||
&self,
|
||||
params: &CreateCommentParams,
|
||||
) -> Result<DbCommentView, ServerFnError> {
|
||||
self.post("/api/v1/comment", Some(¶ms)).await
|
||||
}
|
||||
|
||||
pub async fn edit_comment(
|
||||
&self,
|
||||
params: &EditCommentParams,
|
||||
) -> Result<DbCommentView, ServerFnError> {
|
||||
self.patch("/api/v1/comment", Some(¶ms)).await
|
||||
}
|
||||
|
||||
pub async fn notifications_list(&self) -> Option<Vec<Notification>> {
|
||||
self.get("/api/v1/user/notifications/list", None::<()>)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn notifications_count(&self) -> Option<usize> {
|
||||
self.get("/api/v1/user/notifications/count", None::<()>)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn approve_article(&self, article_id: ArticleId, approve: bool) -> Option<()> {
|
||||
let params = ApproveArticleParams {
|
||||
article_id,
|
||||
approve,
|
||||
};
|
||||
result_to_option(self.post("/api/v1/article/approve", Some(¶ms)).await)
|
||||
}
|
||||
|
||||
pub async fn delete_conflict(&self, conflict_id: ConflictId) -> Option<()> {
|
||||
let params = DeleteConflictParams { conflict_id };
|
||||
result_to_option(
|
||||
self.send(Method::DELETE, "/api/v1/conflict", Some(params))
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn search(
|
||||
&self,
|
||||
params: &SearchArticleParams,
|
||||
) -> Result<Vec<DbArticle>, ServerFnError> {
|
||||
self.send(Method::GET, "/api/v1/search", Some(params)).await
|
||||
}
|
||||
|
||||
pub async fn get_local_instance(&self) -> Option<InstanceView> {
|
||||
self.get("/api/v1/instance", None::<i32>).await
|
||||
}
|
||||
|
||||
pub async fn get_instance(&self, params: &GetInstanceParams) -> Option<InstanceView> {
|
||||
self.get("/api/v1/instance", Some(¶ms)).await
|
||||
}
|
||||
|
||||
pub async fn list_instances(&self) -> Option<Vec<DbInstance>> {
|
||||
self.get("/api/v1/instance/list", None::<i32>).await
|
||||
}
|
||||
|
||||
pub async fn update_local_instance(
|
||||
&self,
|
||||
params: &UpdateInstanceParams,
|
||||
) -> Result<DbInstance, ServerFnError> {
|
||||
self.patch("/api/v1/instance", Some(params)).await
|
||||
}
|
||||
|
||||
pub async fn follow_instance_with_resolve(&self, follow_instance: &str) -> Option<DbInstance> {
|
||||
// fetch beta instance on alpha
|
||||
let params = ResolveObjectParams {
|
||||
id: Url::parse(&format!("{}://{}", http_protocol_str(), follow_instance))
|
||||
.map_err(|e| error!("invalid url {e}"))
|
||||
.ok()?,
|
||||
};
|
||||
let instance_resolved: DbInstance =
|
||||
self.get("/api/v1/instance/resolve", Some(params)).await?;
|
||||
|
||||
// send follow
|
||||
let params = FollowInstanceParams {
|
||||
id: instance_resolved.id,
|
||||
};
|
||||
self.follow_instance(params).await?;
|
||||
Some(instance_resolved)
|
||||
}
|
||||
|
||||
pub async fn follow_instance(&self, params: FollowInstanceParams) -> Option<SuccessResponse> {
|
||||
result_to_option(self.post("/api/v1/instance/follow", Some(params)).await)
|
||||
}
|
||||
|
||||
pub async fn site(&self) -> Option<SiteView> {
|
||||
self.get("/api/v1/site", None::<()>).await
|
||||
}
|
||||
|
||||
pub async fn logout(&self) -> Option<SuccessResponse> {
|
||||
result_to_option(self.post("/api/v1/account/logout", None::<()>).await)
|
||||
}
|
||||
|
||||
pub async fn fork_article(
|
||||
&self,
|
||||
params: &ForkArticleParams,
|
||||
) -> Result<DbArticleView, ServerFnError> {
|
||||
self.post("/api/v1/article/fork", Some(params)).await
|
||||
}
|
||||
|
||||
pub async fn protect_article(
|
||||
&self,
|
||||
params: &ProtectArticleParams,
|
||||
) -> Result<DbArticle, ServerFnError> {
|
||||
self.post("/api/v1/article/protect", Some(params)).await
|
||||
}
|
||||
|
||||
pub async fn resolve_article(&self, id: Url) -> Result<DbArticleView, ServerFnError> {
|
||||
let resolve_object = ResolveObjectParams { id };
|
||||
self.send(Method::GET, "/api/v1/article/resolve", Some(resolve_object))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn resolve_instance(&self, id: Url) -> Result<DbInstance, ServerFnError> {
|
||||
let resolve_object = ResolveObjectParams { id };
|
||||
self.send(
|
||||
Method::GET,
|
||||
"/api/v1/instance/resolve",
|
||||
Some(resolve_object),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_user(&self, data: GetUserParams) -> Option<DbPerson> {
|
||||
self.get("/api/v1/user", Some(data)).await
|
||||
}
|
||||
|
||||
pub async fn update_user_profile(
|
||||
&self,
|
||||
data: UpdateUserParams,
|
||||
) -> Result<SuccessResponse, ServerFnError> {
|
||||
self.post("/api/v1/account/update", Some(data)).await
|
||||
}
|
||||
|
||||
pub async fn get_article_edits(&self, article_id: ArticleId) -> Option<Vec<EditView>> {
|
||||
let data = GetEditList {
|
||||
article_id: Some(article_id),
|
||||
..Default::default()
|
||||
};
|
||||
self.get("/api/v1/edit/list", Some(data)).await
|
||||
}
|
||||
|
||||
pub async fn get_person_edits(&self, person_id: PersonId) -> Option<Vec<EditView>> {
|
||||
let data = GetEditList {
|
||||
person_id: Some(person_id),
|
||||
..Default::default()
|
||||
};
|
||||
self.get("/api/v1/edit/list", Some(data)).await
|
||||
}
|
||||
|
||||
async fn get<T, R>(&self, endpoint: &str, query: Option<R>) -> Option<T>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
R: Serialize + Debug,
|
||||
{
|
||||
result_to_option(self.send(Method::GET, endpoint, query).await)
|
||||
}
|
||||
|
||||
async fn post<T, R>(&self, endpoint: &str, query: Option<R>) -> Result<T, ServerFnError>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
R: Serialize + Debug,
|
||||
{
|
||||
self.send(Method::POST, endpoint, query).await
|
||||
}
|
||||
|
||||
async fn patch<T, R>(&self, endpoint: &str, query: Option<R>) -> Result<T, ServerFnError>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
R: Serialize + Debug,
|
||||
{
|
||||
self.send(Method::PATCH, endpoint, query).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn send<P, T>(
|
||||
&self,
|
||||
method: Method,
|
||||
path: &str,
|
||||
params: Option<P>,
|
||||
) -> Result<T, ServerFnError>
|
||||
where
|
||||
P: Serialize + Debug,
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
use crate::common::{Auth, AUTH_COOKIE};
|
||||
use leptos::prelude::use_context;
|
||||
use reqwest::header::HeaderName;
|
||||
let mut req = self
|
||||
.client
|
||||
.request(method.clone(), self.request_endpoint(path));
|
||||
req = if method == Method::GET {
|
||||
req.query(¶ms)
|
||||
} else {
|
||||
req.form(¶ms)
|
||||
};
|
||||
let auth = use_context::<Auth>();
|
||||
if let Some(Auth(Some(auth))) = auth {
|
||||
req = req.header(HeaderName::from_static(AUTH_COOKIE), auth);
|
||||
}
|
||||
let res = req.send().await?;
|
||||
let status = res.status();
|
||||
let url = res.url().to_string();
|
||||
let text = res.text().await?.to_string();
|
||||
Self::response(status.into(), text, &url)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn send<'a, P, T>(
|
||||
&'a self,
|
||||
method: Method,
|
||||
path: &'a str,
|
||||
params: Option<P>,
|
||||
) -> impl std::future::Future<Output = Result<T, ServerFnError>> + Send + 'a
|
||||
where
|
||||
P: Serialize + Debug + 'a,
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
use gloo_net::http::*;
|
||||
use leptos::prelude::on_cleanup;
|
||||
use send_wrapper::SendWrapper;
|
||||
use web_sys::RequestCredentials;
|
||||
|
||||
SendWrapper::new(async move {
|
||||
let abort_controller = SendWrapper::new(web_sys::AbortController::new().ok());
|
||||
let abort_signal = abort_controller.as_ref().map(|a| a.signal());
|
||||
|
||||
// abort in-flight requests if, e.g., we've navigated away from this page
|
||||
on_cleanup(move || {
|
||||
if let Some(abort_controller) = abort_controller.take() {
|
||||
abort_controller.abort()
|
||||
}
|
||||
});
|
||||
|
||||
let path_with_endpoint = self.request_endpoint(path);
|
||||
let params_encoded = serde_urlencoded::to_string(¶ms).unwrap();
|
||||
let path = if method == Method::GET {
|
||||
// Cannot pass the form data directly but need to convert it manually
|
||||
// https://github.com/rustwasm/gloo/issues/378
|
||||
format!("{path_with_endpoint}?{params_encoded}")
|
||||
} else {
|
||||
path_with_endpoint
|
||||
};
|
||||
|
||||
let builder = RequestBuilder::new(&path)
|
||||
.method(method.clone())
|
||||
.abort_signal(abort_signal.as_ref())
|
||||
.credentials(RequestCredentials::Include);
|
||||
let req = if method != Method::GET {
|
||||
builder
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.body(params_encoded)
|
||||
} else {
|
||||
builder.build()
|
||||
}
|
||||
.unwrap();
|
||||
let res = req.send().await?;
|
||||
let status = res.status();
|
||||
let text = res.text().await?;
|
||||
Self::response(status, text, &res.url())
|
||||
})
|
||||
}
|
||||
|
||||
fn response<T>(status: u16, text: String, url: &str) -> Result<T, ServerFnError>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let json = serde_json::from_str(&text).map_err(|e| {
|
||||
info!("Failed to deserialize api response: {e} from {text} on {url}");
|
||||
ServerFnError::<NoCustomError>::Deserialization(text.clone())
|
||||
})?;
|
||||
if status == StatusCode::OK {
|
||||
Ok(json)
|
||||
} else {
|
||||
info!("API error: {text} on {url} status {status}");
|
||||
Err(ServerFnError::Response(text))
|
||||
}
|
||||
}
|
||||
|
||||
fn request_endpoint(&self, path: &str) -> String {
|
||||
let protocol = if self.ssl { "https" } else { "http" };
|
||||
format!("{protocol}://{}{path}", &self.hostname)
|
||||
}
|
||||
}
|
||||
|
||||
fn result_to_option<T>(val: Result<T, ServerFnError>) -> Option<T> {
|
||||
match val {
|
||||
Ok(v) => Some(v),
|
||||
Err(e) => {
|
||||
error!("API error: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
112
src/frontend/api/article.rs
Normal file
112
src/frontend/api/article.rs
Normal file
|
@ -0,0 +1,112 @@
|
|||
use super::{result_to_option, ApiClient};
|
||||
use crate::common::{
|
||||
article::{
|
||||
ApiConflict,
|
||||
ApproveArticleParams,
|
||||
CreateArticleParams,
|
||||
DbArticle,
|
||||
DbArticleView,
|
||||
DeleteConflictParams,
|
||||
EditArticleParams,
|
||||
EditView,
|
||||
ForkArticleParams,
|
||||
GetArticleParams,
|
||||
GetEditList,
|
||||
ListArticlesParams,
|
||||
ProtectArticleParams,
|
||||
},
|
||||
newtypes::{ArticleId, ConflictId},
|
||||
ResolveObjectParams,
|
||||
};
|
||||
use http::Method;
|
||||
use leptos::prelude::ServerFnError;
|
||||
use log::error;
|
||||
use url::Url;
|
||||
|
||||
impl ApiClient {
|
||||
pub async fn create_article(
|
||||
&self,
|
||||
data: &CreateArticleParams,
|
||||
) -> Result<DbArticleView, ServerFnError> {
|
||||
self.post("/api/v1/article", Some(&data)).await
|
||||
}
|
||||
|
||||
pub async fn get_article(&self, data: GetArticleParams) -> Option<DbArticleView> {
|
||||
self.get("/api/v1/article", Some(data)).await
|
||||
}
|
||||
|
||||
pub async fn list_articles(&self, data: ListArticlesParams) -> Option<Vec<DbArticle>> {
|
||||
Some(self.get("/api/v1/article/list", Some(data)).await.unwrap())
|
||||
}
|
||||
|
||||
pub async fn edit_article(
|
||||
&self,
|
||||
params: &EditArticleParams,
|
||||
) -> Result<Option<ApiConflict>, ServerFnError> {
|
||||
self.patch("/api/v1/article", Some(¶ms)).await
|
||||
}
|
||||
|
||||
pub async fn fork_article(
|
||||
&self,
|
||||
params: &ForkArticleParams,
|
||||
) -> Result<DbArticleView, ServerFnError> {
|
||||
self.post("/api/v1/article/fork", Some(params)).await
|
||||
}
|
||||
|
||||
pub async fn protect_article(
|
||||
&self,
|
||||
params: &ProtectArticleParams,
|
||||
) -> Result<DbArticle, ServerFnError> {
|
||||
self.post("/api/v1/article/protect", Some(params)).await
|
||||
}
|
||||
|
||||
pub async fn resolve_article(&self, id: Url) -> Result<DbArticleView, ServerFnError> {
|
||||
let resolve_object = ResolveObjectParams { id };
|
||||
self.send(Method::GET, "/api/v1/article/resolve", Some(resolve_object))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_article_edits(&self, article_id: ArticleId) -> Option<Vec<EditView>> {
|
||||
let data = GetEditList {
|
||||
article_id: Some(article_id),
|
||||
..Default::default()
|
||||
};
|
||||
self.get("/api/v1/edit/list", Some(data)).await
|
||||
}
|
||||
|
||||
pub async fn approve_article(&self, article_id: ArticleId, approve: bool) -> Option<()> {
|
||||
let params = ApproveArticleParams {
|
||||
article_id,
|
||||
approve,
|
||||
};
|
||||
result_to_option(self.post("/api/v1/article/approve", Some(¶ms)).await)
|
||||
}
|
||||
|
||||
pub async fn delete_conflict(&self, conflict_id: ConflictId) -> Option<()> {
|
||||
let params = DeleteConflictParams { conflict_id };
|
||||
result_to_option(
|
||||
self.send(Method::DELETE, "/api/v1/conflict", Some(params))
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub async fn edit_article_without_conflict(
|
||||
&self,
|
||||
params: &EditArticleParams,
|
||||
) -> Option<DbArticleView> {
|
||||
let edit_res = self
|
||||
.edit_article(params)
|
||||
.await
|
||||
.map_err(|e| error!("edit failed {e}"))
|
||||
.ok()?;
|
||||
assert_eq!(None, edit_res);
|
||||
|
||||
self.get_article(GetArticleParams {
|
||||
title: None,
|
||||
domain: None,
|
||||
id: Some(params.article_id),
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
19
src/frontend/api/comment.rs
Normal file
19
src/frontend/api/comment.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
use super::ApiClient;
|
||||
use crate::common::comment::{CreateCommentParams, DbCommentView, EditCommentParams};
|
||||
use leptos::prelude::ServerFnError;
|
||||
|
||||
impl ApiClient {
|
||||
pub async fn create_comment(
|
||||
&self,
|
||||
params: &CreateCommentParams,
|
||||
) -> Result<DbCommentView, ServerFnError> {
|
||||
self.post("/api/v1/comment", Some(¶ms)).await
|
||||
}
|
||||
|
||||
pub async fn edit_comment(
|
||||
&self,
|
||||
params: &EditCommentParams,
|
||||
) -> Result<DbCommentView, ServerFnError> {
|
||||
self.patch("/api/v1/comment", Some(¶ms)).await
|
||||
}
|
||||
}
|
94
src/frontend/api/instance.rs
Normal file
94
src/frontend/api/instance.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
use super::{result_to_option, ApiClient};
|
||||
use crate::common::{
|
||||
article::{DbArticle, SearchArticleParams},
|
||||
instance::{
|
||||
DbInstance,
|
||||
FollowInstanceParams,
|
||||
GetInstanceParams,
|
||||
InstanceView,
|
||||
SiteView,
|
||||
UpdateInstanceParams,
|
||||
},
|
||||
Notification,
|
||||
ResolveObjectParams,
|
||||
SuccessResponse,
|
||||
};
|
||||
use http::Method;
|
||||
use leptos::prelude::ServerFnError;
|
||||
use url::Url;
|
||||
|
||||
impl ApiClient {
|
||||
pub async fn get_local_instance(&self) -> Option<InstanceView> {
|
||||
self.get("/api/v1/instance", None::<i32>).await
|
||||
}
|
||||
|
||||
pub async fn get_instance(&self, params: &GetInstanceParams) -> Option<InstanceView> {
|
||||
self.get("/api/v1/instance", Some(¶ms)).await
|
||||
}
|
||||
|
||||
pub async fn list_instances(&self) -> Option<Vec<DbInstance>> {
|
||||
self.get("/api/v1/instance/list", None::<i32>).await
|
||||
}
|
||||
|
||||
pub async fn update_local_instance(
|
||||
&self,
|
||||
params: &UpdateInstanceParams,
|
||||
) -> Result<DbInstance, ServerFnError> {
|
||||
self.patch("/api/v1/instance", Some(params)).await
|
||||
}
|
||||
|
||||
pub async fn notifications_list(&self) -> Option<Vec<Notification>> {
|
||||
self.get("/api/v1/user/notifications/list", None::<()>)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn notifications_count(&self) -> Option<usize> {
|
||||
self.get("/api/v1/user/notifications/count", None::<()>)
|
||||
.await
|
||||
}
|
||||
pub async fn search(
|
||||
&self,
|
||||
params: &SearchArticleParams,
|
||||
) -> Result<Vec<DbArticle>, ServerFnError> {
|
||||
self.send(Method::GET, "/api/v1/search", Some(params)).await
|
||||
}
|
||||
|
||||
pub async fn resolve_instance(&self, id: Url) -> Result<DbInstance, ServerFnError> {
|
||||
let resolve_object = ResolveObjectParams { id };
|
||||
self.send(
|
||||
Method::GET,
|
||||
"/api/v1/instance/resolve",
|
||||
Some(resolve_object),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn follow_instance(&self, params: FollowInstanceParams) -> Option<SuccessResponse> {
|
||||
result_to_option(self.post("/api/v1/instance/follow", Some(params)).await)
|
||||
}
|
||||
|
||||
pub async fn site(&self) -> Option<SiteView> {
|
||||
self.get("/api/v1/site", None::<()>).await
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub async fn follow_instance_with_resolve(&self, follow_instance: &str) -> Option<DbInstance> {
|
||||
use crate::common::{utils::http_protocol_str, ResolveObjectParams};
|
||||
use log::error;
|
||||
use url::Url;
|
||||
let params = ResolveObjectParams {
|
||||
id: Url::parse(&format!("{}://{}", http_protocol_str(), follow_instance))
|
||||
.map_err(|e| error!("invalid url {e}"))
|
||||
.ok()?,
|
||||
};
|
||||
let instance_resolved: DbInstance =
|
||||
self.get("/api/v1/instance/resolve", Some(params)).await?;
|
||||
|
||||
// send follow
|
||||
let params = FollowInstanceParams {
|
||||
id: instance_resolved.id,
|
||||
};
|
||||
self.follow_instance(params).await?;
|
||||
Some(instance_resolved)
|
||||
}
|
||||
}
|
198
src/frontend/api/mod.rs
Normal file
198
src/frontend/api/mod.rs
Normal file
|
@ -0,0 +1,198 @@
|
|||
use http::{Method, StatusCode};
|
||||
use leptos::{prelude::ServerFnError, server_fn::error::NoCustomError};
|
||||
use log::{error, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Debug, sync::LazyLock};
|
||||
|
||||
pub mod article;
|
||||
pub mod comment;
|
||||
pub mod instance;
|
||||
pub mod user;
|
||||
|
||||
pub static CLIENT: LazyLock<ApiClient> = LazyLock::new(|| {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
ApiClient::new(reqwest::Client::new(), None)
|
||||
}
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
ApiClient::new()
|
||||
}
|
||||
});
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ApiClient {
|
||||
#[cfg(feature = "ssr")]
|
||||
client: reqwest::Client,
|
||||
pub hostname: String,
|
||||
ssl: bool,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn new(client: reqwest::Client, hostname_: Option<String>) -> Self {
|
||||
use leptos::config::get_config_from_str;
|
||||
let leptos_options = get_config_from_str(include_str!("../../../Cargo.toml")).unwrap();
|
||||
let mut hostname = leptos_options.site_addr.to_string();
|
||||
// required for tests
|
||||
if let Some(hostname_) = hostname_ {
|
||||
hostname = hostname_;
|
||||
}
|
||||
Self {
|
||||
client,
|
||||
hostname,
|
||||
ssl: false,
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn new() -> Self {
|
||||
use leptos_use::use_document;
|
||||
let hostname = use_document().location().unwrap().host().unwrap();
|
||||
let ssl = !cfg!(debug_assertions);
|
||||
Self { hostname, ssl }
|
||||
}
|
||||
|
||||
async fn get<T, R>(&self, endpoint: &str, query: Option<R>) -> Option<T>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
R: Serialize + Debug,
|
||||
{
|
||||
result_to_option(self.send(Method::GET, endpoint, query).await)
|
||||
}
|
||||
|
||||
async fn post<T, R>(&self, endpoint: &str, query: Option<R>) -> Result<T, ServerFnError>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
R: Serialize + Debug,
|
||||
{
|
||||
self.send(Method::POST, endpoint, query).await
|
||||
}
|
||||
|
||||
async fn patch<T, R>(&self, endpoint: &str, query: Option<R>) -> Result<T, ServerFnError>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
R: Serialize + Debug,
|
||||
{
|
||||
self.send(Method::PATCH, endpoint, query).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn send<P, T>(
|
||||
&self,
|
||||
method: Method,
|
||||
path: &str,
|
||||
params: Option<P>,
|
||||
) -> Result<T, ServerFnError>
|
||||
where
|
||||
P: Serialize + Debug,
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
use crate::common::{Auth, AUTH_COOKIE};
|
||||
use leptos::prelude::use_context;
|
||||
use reqwest::header::HeaderName;
|
||||
let mut req = self
|
||||
.client
|
||||
.request(method.clone(), self.request_endpoint(path));
|
||||
req = if method == Method::GET {
|
||||
req.query(¶ms)
|
||||
} else {
|
||||
req.form(¶ms)
|
||||
};
|
||||
let auth = use_context::<Auth>();
|
||||
if let Some(Auth(Some(auth))) = auth {
|
||||
req = req.header(HeaderName::from_static(AUTH_COOKIE), auth);
|
||||
}
|
||||
let res = req.send().await?;
|
||||
let status = res.status();
|
||||
let url = res.url().to_string();
|
||||
let text = res.text().await?.to_string();
|
||||
Self::response(status.into(), text, &url)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn send<'a, P, T>(
|
||||
&'a self,
|
||||
method: Method,
|
||||
path: &'a str,
|
||||
params: Option<P>,
|
||||
) -> impl std::future::Future<Output = Result<T, ServerFnError>> + Send + 'a
|
||||
where
|
||||
P: Serialize + Debug + 'a,
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
use gloo_net::http::*;
|
||||
use leptos::prelude::on_cleanup;
|
||||
use send_wrapper::SendWrapper;
|
||||
use web_sys::RequestCredentials;
|
||||
|
||||
SendWrapper::new(async move {
|
||||
let abort_controller = SendWrapper::new(web_sys::AbortController::new().ok());
|
||||
let abort_signal = abort_controller.as_ref().map(|a| a.signal());
|
||||
|
||||
// abort in-flight requests if, e.g., we've navigated away from this page
|
||||
on_cleanup(move || {
|
||||
if let Some(abort_controller) = abort_controller.take() {
|
||||
abort_controller.abort()
|
||||
}
|
||||
});
|
||||
|
||||
let path_with_endpoint = self.request_endpoint(path);
|
||||
let params_encoded = serde_urlencoded::to_string(¶ms).unwrap();
|
||||
let path = if method == Method::GET {
|
||||
// Cannot pass the form data directly but need to convert it manually
|
||||
// https://github.com/rustwasm/gloo/issues/378
|
||||
format!("{path_with_endpoint}?{params_encoded}")
|
||||
} else {
|
||||
path_with_endpoint
|
||||
};
|
||||
|
||||
let builder = RequestBuilder::new(&path)
|
||||
.method(method.clone())
|
||||
.abort_signal(abort_signal.as_ref())
|
||||
.credentials(RequestCredentials::Include);
|
||||
let req = if method != Method::GET {
|
||||
builder
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.body(params_encoded)
|
||||
} else {
|
||||
builder.build()
|
||||
}
|
||||
.unwrap();
|
||||
let res = req.send().await?;
|
||||
let status = res.status();
|
||||
let text = res.text().await?;
|
||||
Self::response(status, text, &res.url())
|
||||
})
|
||||
}
|
||||
|
||||
fn response<T>(status: u16, text: String, url: &str) -> Result<T, ServerFnError>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let json = serde_json::from_str(&text).map_err(|e| {
|
||||
info!("Failed to deserialize api response: {e} from {text} on {url}");
|
||||
ServerFnError::<NoCustomError>::Deserialization(text.clone())
|
||||
})?;
|
||||
if status == StatusCode::OK {
|
||||
Ok(json)
|
||||
} else {
|
||||
info!("API error: {text} on {url} status {status}");
|
||||
Err(ServerFnError::Response(text))
|
||||
}
|
||||
}
|
||||
|
||||
fn request_endpoint(&self, path: &str) -> String {
|
||||
let protocol = if self.ssl { "https" } else { "http" };
|
||||
format!("{protocol}://{}{path}", &self.hostname)
|
||||
}
|
||||
}
|
||||
|
||||
fn result_to_option<T>(val: Result<T, ServerFnError>) -> Option<T> {
|
||||
match val {
|
||||
Ok(v) => Some(v),
|
||||
Err(e) => {
|
||||
error!("API error: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
51
src/frontend/api/user.rs
Normal file
51
src/frontend/api/user.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
use super::{result_to_option, ApiClient};
|
||||
use crate::common::{
|
||||
article::{EditView, GetEditList},
|
||||
newtypes::PersonId,
|
||||
user::{
|
||||
DbPerson,
|
||||
GetUserParams,
|
||||
LocalUserView,
|
||||
LoginUserParams,
|
||||
RegisterUserParams,
|
||||
UpdateUserParams,
|
||||
},
|
||||
SuccessResponse,
|
||||
};
|
||||
use leptos::prelude::ServerFnError;
|
||||
|
||||
impl ApiClient {
|
||||
pub async fn register(
|
||||
&self,
|
||||
params: RegisterUserParams,
|
||||
) -> Result<LocalUserView, ServerFnError> {
|
||||
self.post("/api/v1/account/register", Some(¶ms)).await
|
||||
}
|
||||
|
||||
pub async fn login(&self, params: LoginUserParams) -> Result<LocalUserView, ServerFnError> {
|
||||
self.post("/api/v1/account/login", Some(¶ms)).await
|
||||
}
|
||||
|
||||
pub async fn logout(&self) -> Option<SuccessResponse> {
|
||||
result_to_option(self.post("/api/v1/account/logout", None::<()>).await)
|
||||
}
|
||||
|
||||
pub async fn get_user(&self, data: GetUserParams) -> Option<DbPerson> {
|
||||
self.get("/api/v1/user", Some(data)).await
|
||||
}
|
||||
|
||||
pub async fn update_user_profile(
|
||||
&self,
|
||||
data: UpdateUserParams,
|
||||
) -> Result<SuccessResponse, ServerFnError> {
|
||||
self.post("/api/v1/account/update", Some(data)).await
|
||||
}
|
||||
|
||||
pub async fn get_person_edits(&self, person_id: PersonId) -> Option<Vec<EditView>> {
|
||||
let data = GetEditList {
|
||||
person_id: Some(person_id),
|
||||
..Default::default()
|
||||
};
|
||||
self.get("/api/v1/edit/list", Some(data)).await
|
||||
}
|
||||
}
|
|
@ -95,7 +95,7 @@ pub fn EditArticle() -> impl IntoView {
|
|||
resolve_conflict_id,
|
||||
};
|
||||
set_wait_for_response.update(|w| *w = true);
|
||||
let res = CLIENT.edit_article_with_conflict(¶ms).await;
|
||||
let res = CLIENT.edit_article(¶ms).await;
|
||||
set_wait_for_response.update(|w| *w = false);
|
||||
match res {
|
||||
Ok(Some(conflict)) => {
|
||||
|
|
|
@ -64,7 +64,10 @@ async fn test_create_read_and_edit_local_article() -> Result<()> {
|
|||
previous_version_id: get_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
let edit_res = alpha.edit_article(&edit_params).await.unwrap();
|
||||
let edit_res = alpha
|
||||
.edit_article_without_conflict(&edit_params)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(edit_params.new_text, edit_res.article.text);
|
||||
let edits = alpha.get_article_edits(edit_res.article.id).await.unwrap();
|
||||
assert_eq!(2, edits.len());
|
||||
|
@ -164,7 +167,10 @@ async fn test_synchronize_articles() -> Result<()> {
|
|||
previous_version_id: create_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
let edit_res = alpha.edit_article(&edit_params).await.unwrap();
|
||||
let edit_res = alpha
|
||||
.edit_article_without_conflict(&edit_params)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// fetch alpha instance on beta, articles are also fetched automatically
|
||||
let instance = beta
|
||||
|
@ -251,7 +257,10 @@ async fn test_edit_local_article() -> Result<()> {
|
|||
previous_version_id: get_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
let edit_res = beta.edit_article(&edit_params).await.unwrap();
|
||||
let edit_res = beta
|
||||
.edit_article_without_conflict(&edit_params)
|
||||
.await
|
||||
.unwrap();
|
||||
let edits = beta.get_article_edits(edit_res.article.id).await.unwrap();
|
||||
assert_eq!(edit_res.article.text, edit_params.new_text);
|
||||
assert_eq!(edits.len(), 2);
|
||||
|
@ -328,7 +337,10 @@ async fn test_edit_remote_article() -> Result<()> {
|
|||
previous_version_id: get_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
let edit_res = alpha.edit_article(&edit_params).await.unwrap();
|
||||
let edit_res = alpha
|
||||
.edit_article_without_conflict(&edit_params)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(edit_params.new_text, edit_res.article.text);
|
||||
let edits = alpha.get_article_edits(edit_res.article.id).await.unwrap();
|
||||
assert_eq!(2, edits.len());
|
||||
|
@ -377,7 +389,10 @@ async fn test_local_edit_conflict() -> Result<()> {
|
|||
previous_version_id: create_res.latest_version.clone(),
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
let edit_res = alpha.edit_article(&edit_params).await.unwrap();
|
||||
let edit_res = alpha
|
||||
.edit_article_without_conflict(&edit_params)
|
||||
.await
|
||||
.unwrap();
|
||||
let edits = alpha.get_article_edits(edit_res.article.id).await.unwrap();
|
||||
assert_eq!(edit_res.article.text, edit_params.new_text);
|
||||
assert_eq!(2, edits.len());
|
||||
|
@ -390,11 +405,7 @@ async fn test_local_edit_conflict() -> Result<()> {
|
|||
previous_version_id: create_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
let edit_res = alpha
|
||||
.edit_article_with_conflict(&edit_params)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let edit_res = alpha.edit_article(&edit_params).await.unwrap().unwrap();
|
||||
assert_eq!("<<<<<<< ours\nIpsum Lorem\n||||||| original\nsome example text\n=======\nLorem Ipsum\n>>>>>>> theirs\n", edit_res.three_way_merge);
|
||||
|
||||
let notifications = alpha.notifications_list().await.unwrap();
|
||||
|
@ -411,7 +422,10 @@ async fn test_local_edit_conflict() -> Result<()> {
|
|||
previous_version_id: edit_res.previous_version_id,
|
||||
resolve_conflict_id: Some(edit_res.id),
|
||||
};
|
||||
let edit_res = alpha.edit_article(&edit_params).await.unwrap();
|
||||
let edit_res = alpha
|
||||
.edit_article_without_conflict(&edit_params)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(edit_params.new_text, edit_res.article.text);
|
||||
|
||||
assert_eq!(0, alpha.notifications_count().await.unwrap());
|
||||
|
@ -463,7 +477,10 @@ async fn test_federated_edit_conflict() -> Result<()> {
|
|||
previous_version_id: create_res.latest_version.clone(),
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
let edit_res = alpha.edit_article(&edit_params).await.unwrap();
|
||||
let edit_res = alpha
|
||||
.edit_article_without_conflict(&edit_params)
|
||||
.await
|
||||
.unwrap();
|
||||
let alpha_edits = alpha.get_article_edits(get_res.article.id).await.unwrap();
|
||||
assert_eq!(edit_res.article.text, edit_params.new_text);
|
||||
assert_eq!(2, alpha_edits.len());
|
||||
|
@ -483,7 +500,10 @@ async fn test_federated_edit_conflict() -> Result<()> {
|
|||
previous_version_id: create_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
let edit_res = gamma.edit_article(&edit_params).await.unwrap();
|
||||
let edit_res = gamma
|
||||
.edit_article_without_conflict(&edit_params)
|
||||
.await
|
||||
.unwrap();
|
||||
let gamma_edits = gamma.get_article_edits(edit_res.article.id).await.unwrap();
|
||||
assert_ne!(edit_params.new_text, edit_res.article.text);
|
||||
assert_eq!(2, gamma_edits.len());
|
||||
|
@ -505,7 +525,10 @@ async fn test_federated_edit_conflict() -> Result<()> {
|
|||
previous_version_id: conflict.previous_version_id.clone(),
|
||||
resolve_conflict_id: Some(conflict.id),
|
||||
};
|
||||
let edit_res = gamma.edit_article(&edit_params).await.unwrap();
|
||||
let edit_res = gamma
|
||||
.edit_article_without_conflict(&edit_params)
|
||||
.await
|
||||
.unwrap();
|
||||
let gamma_edits = gamma.get_article_edits(edit_res.article.id).await.unwrap();
|
||||
assert_eq!(edit_params.new_text, edit_res.article.text);
|
||||
assert_eq!(3, gamma_edits.len());
|
||||
|
@ -552,7 +575,10 @@ async fn test_overlapping_edits_no_conflict() -> Result<()> {
|
|||
previous_version_id: create_res.latest_version.clone(),
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
let edit_res = alpha.edit_article(&edit_params).await.unwrap();
|
||||
let edit_res = alpha
|
||||
.edit_article_without_conflict(&edit_params)
|
||||
.await
|
||||
.unwrap();
|
||||
let alpha_edits = alpha.get_article_edits(edit_res.article.id).await.unwrap();
|
||||
assert_eq!(edit_res.article.text, edit_params.new_text);
|
||||
assert_eq!(2, alpha_edits.len());
|
||||
|
@ -570,7 +596,10 @@ async fn test_overlapping_edits_no_conflict() -> Result<()> {
|
|||
previous_version_id: create_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
let edit_res = alpha.edit_article(&edit_params).await.unwrap();
|
||||
let edit_res = alpha
|
||||
.edit_article_without_conflict(&edit_params)
|
||||
.await
|
||||
.unwrap();
|
||||
let alpha_edits = alpha.get_article_edits(edit_res.article.id).await.unwrap();
|
||||
assert_eq!(0, alpha.notifications_count().await.unwrap());
|
||||
assert_eq!(3, alpha_edits.len());
|
||||
|
@ -748,7 +777,7 @@ async fn test_lock_article() -> Result<()> {
|
|||
previous_version_id: resolve_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
let edit_res = gamma.edit_article(&edit_params).await;
|
||||
let edit_res = gamma.edit_article_without_conflict(&edit_params).await;
|
||||
assert!(edit_res.is_none());
|
||||
|
||||
TestData::stop(alpha, beta, gamma)
|
||||
|
|
Loading…
Reference in a new issue