This commit is contained in:
Felix Ableitner 2024-01-02 15:56:49 +01:00
parent e1c69799b8
commit 166426e48f
13 changed files with 263 additions and 14 deletions

36
Cargo.lock generated
View File

@ -99,6 +99,16 @@ version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "api_client"
version = "0.1.0"
dependencies = [
"anyhow",
"once_cell",
"reqwest",
"serde_json",
]
[[package]]
name = "async-lock"
version = "2.8.0"
@ -861,6 +871,7 @@ dependencies = [
"log",
"reqwest",
"serde",
"url",
]
[[package]]
@ -1164,6 +1175,12 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "http-range-header"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f"
[[package]]
name = "http-signature-normalization"
version = "0.7.0"
@ -1290,6 +1307,7 @@ dependencies = [
"serde_json",
"sha2",
"tokio",
"tower-http",
"tracing",
"url",
"uuid",
@ -2923,6 +2941,24 @@ dependencies = [
"tracing",
]
[[package]]
name = "tower-http"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140"
dependencies = [
"bitflags 2.4.1",
"bytes",
"futures-core",
"futures-util",
"http 0.2.11",
"http-body 0.4.5",
"http-range-header",
"pin-project-lite",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.2"

View File

@ -7,4 +7,5 @@ edition = "2021"
members = [
"frontend",
"backend",
"api_client",
]

10
api_client/Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[package]
name = "api_client"
version = "0.1.0"
edition = "2021"
[dependencies]
reqwest = "0.11.22"
serde_json = "1.0.108"
once_cell = "1.18.0"
anyhow = "1.0.75"

157
api_client/src/lib.rs Normal file
View File

@ -0,0 +1,157 @@
use reqwest::Client;use once_cell::sync::Lazy;use anyhow::anyhow;
pub static CLIENT: Lazy<Client> = Lazy::new(Client::new);
pub async fn create_article(instance: &IbisInstance, title: String) -> MyResult<ArticleView> {
let create_form = CreateArticleData {
title: title.clone(),
};
let req = CLIENT
.post(format!("http://{}/api/v1/article", &instance.hostname))
.form(&create_form)
.bearer_auth(&instance.jwt);
let article: ArticleView = handle_json_res(req).await?;
// create initial edit to ensure that conflicts are generated (there are no conflicts on empty file)
let edit_form = EditArticleData {
article_id: article.article.id,
new_text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
previous_version_id: article.latest_version,
resolve_conflict_id: None,
};
edit_article(instance, &edit_form).await
}
pub async fn get_article(hostname: &str, article_id: i32) -> MyResult<ArticleView> {
let get_article = GetArticleData { article_id };
get_query::<ArticleView, _>(hostname, "article", Some(get_article.clone())).await
}
pub async fn edit_article_with_conflict(
instance: &IbisInstance,
edit_form: &EditArticleData,
) -> MyResult<Option<ApiConflict>> {
let req = CLIENT
.patch(format!("http://{}/api/v1/article", instance.hostname))
.form(edit_form)
.bearer_auth(&instance.jwt);
handle_json_res(req).await
}
pub async fn get_conflicts(instance: &IbisInstance) -> MyResult<Vec<ApiConflict>> {
let req = CLIENT
.get(format!(
"http://{}/api/v1/edit_conflicts",
&instance.hostname
))
.bearer_auth(&instance.jwt);
handle_json_res(req).await
}
pub async fn edit_article(
instance: &IbisInstance,
edit_form: &EditArticleData,
) -> MyResult<ArticleView> {
let edit_res = edit_article_with_conflict(instance, edit_form).await?;
assert!(edit_res.is_none());
get_article(&instance.hostname, edit_form.article_id).await
}
pub async fn get<T>(hostname: &str, endpoint: &str) -> MyResult<T>
where
T: for<'de> Deserialize<'de>,
{
get_query(hostname, endpoint, None::<i32>).await
}
pub async fn get_query<T, R>(hostname: &str, endpoint: &str, query: Option<R>) -> MyResult<T>
where
T: for<'de> Deserialize<'de>,
R: Serialize,
{
let mut req = CLIENT.get(format!("http://{}/api/v1/{}", hostname, endpoint));
if let Some(query) = query {
req = req.query(&query);
}
handle_json_res(req).await
}
pub async fn fork_article(
instance: &IbisInstance,
form: &ForkArticleData,
) -> MyResult<ArticleView> {
let req = CLIENT
.post(format!("http://{}/api/v1/article/fork", instance.hostname))
.form(form)
.bearer_auth(&instance.jwt);
handle_json_res(req).await
}
pub async fn handle_json_res<T>(req: RequestBuilder) -> MyResult<T>
where
T: for<'de> Deserialize<'de>,
{
let res = req.send().await?;
let status = res.status();
let text = res.text().await?;
if status == StatusCode::OK {
Ok(serde_json::from_str(&text).map_err(|e| anyhow!("Json error on {text}: {e}"))?)
} else {
Err(anyhow!("API error: {text}").into())
}
}
pub async fn follow_instance(instance: &IbisInstance, follow_instance: &str) -> MyResult<()> {
// fetch beta instance on alpha
let resolve_form = ResolveObject {
id: Url::parse(&format!("http://{}", follow_instance))?,
};
let instance_resolved: DbInstance =
get_query(&instance.hostname, "resolve_instance", Some(resolve_form)).await?;
// send follow
let follow_form = FollowInstance {
id: instance_resolved.id,
};
// cant use post helper because follow doesnt return json
let res = CLIENT
.post(format!(
"http://{}/api/v1/instance/follow",
instance.hostname
))
.form(&follow_form)
.bearer_auth(&instance.jwt)
.send()
.await?;
if res.status() == StatusCode::OK {
Ok(())
} else {
Err(anyhow!("API error: {}", res.text().await?).into())
}
}
pub async fn register(hostname: &str, username: &str, password: &str) -> MyResult<LoginResponse> {
let register_form = RegisterUserData {
username: username.to_string(),
password: password.to_string(),
};
let req = CLIENT
.post(format!("http://{}/api/v1/user/register", hostname))
.form(&register_form);
handle_json_res(req).await
}
pub async fn login(
instance: &IbisInstance,
username: &str,
password: &str,
) -> MyResult<LoginResponse> {
let login_form = LoginUserData {
username: username.to_string(),
password: password.to_string(),
};
let req = CLIENT
.post(format!("http://{}/api/v1/user/login", instance.hostname))
.form(&login_form);
handle_json_res(req).await
}

View File

@ -28,6 +28,7 @@ tokio = { version = "1.34.0", features = ["full"] }
tracing = "0.1.40"
url = "2.4.1"
uuid = { version = "1.6.1", features = ["serde"] }
tower-http = { version = "0.4.0", features = ["cors"] }
[dev-dependencies]
once_cell = "1.18.0"

View File

@ -17,7 +17,7 @@ use diesel_migrations::EmbeddedMigrations;
use diesel_migrations::MigrationHarness;
use std::net::ToSocketAddrs;
use std::sync::{Arc, Mutex};
use tracing::info;
use tracing::info;use tower_http::cors::CorsLayer;
pub mod api;
pub mod database;
@ -64,7 +64,8 @@ pub async fn start(hostname: &str, database_url: &str) -> MyResult<()> {
let app = Router::new()
.nest("", federation_routes())
.nest("/api/v1", api_routes())
.layer(FederationMiddleware::new(config));
.layer(FederationMiddleware::new(config))
.layer(CorsLayer::permissive());
let addr = hostname
.to_socket_addrs()?

View File

@ -10,3 +10,4 @@ console_log = "1"
console_error_panic_hook = "0.1"
log = "0.4"
serde = { version = "1", features = ["derive"] }
url = {version = "2.4.1", features = ["serde"] }

View File

@ -694,16 +694,16 @@ imports.wbg.__wbindgen_memory = function() {
const ret = wasm.memory;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper4878 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 530, __wbg_adapter_28);
imports.wbg.__wbindgen_closure_wrapper5350 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 595, __wbg_adapter_28);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper4880 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 528, __wbg_adapter_31);
imports.wbg.__wbindgen_closure_wrapper5352 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 593, __wbg_adapter_31);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper10314 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 634, __wbg_adapter_34);
imports.wbg.__wbindgen_closure_wrapper10787 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 699, __wbg_adapter_34);
return addHeapObject(ret);
};
@ -744,7 +744,7 @@ async function __wbg_init(input) {
if (wasm !== undefined) return wasm;
if (typeof input === 'undefined') {
input = new URL('frontend-0e306a0cbb659ffacc2bde4bf6be791bdf3c4fd12ec49427513525c99e29cee2c56ae2ebb40ced499f14e919ae9c9bc1_bg.wasm', import.meta.url);
input = new URL('frontend-570f3815d7fd92194c0bd8caac6e873c37520262d948b899db110969bdd20a82965a58d200fb559dee4285a369e3013a_bg.wasm', import.meta.url);
}
const imports = __wbg_get_imports();

View File

@ -1,8 +1,8 @@
<!DOCTYPE html><html><head>
<script type="module">
import init, * as bindings from '/frontend-0e306a0cbb659ffacc2bde4bf6be791bdf3c4fd12ec49427513525c99e29cee2c56ae2ebb40ced499f14e919ae9c9bc1.js';
init('/frontend-0e306a0cbb659ffacc2bde4bf6be791bdf3c4fd12ec49427513525c99e29cee2c56ae2ebb40ced499f14e919ae9c9bc1_bg.wasm');
import init, * as bindings from '/frontend-570f3815d7fd92194c0bd8caac6e873c37520262d948b899db110969bdd20a82965a58d200fb559dee4285a369e3013a.js';
init('/frontend-570f3815d7fd92194c0bd8caac6e873c37520262d948b899db110969bdd20a82965a58d200fb559dee4285a369e3013a_bg.wasm');
window.wasmBindings = bindings;
</script>
@ -18,8 +18,8 @@ window.wasmBindings = bindings;
background-color: lightpink;
}
</style>
<link rel="preload" href="/frontend-0e306a0cbb659ffacc2bde4bf6be791bdf3c4fd12ec49427513525c99e29cee2c56ae2ebb40ced499f14e919ae9c9bc1_bg.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" integrity="sha384-DjBqDLtln_rMK95L9r55G988T9EuxJQnUTUlyZ4pzuLFauLrtAztSZ8U6RmunJvB">
<link rel="modulepreload" href="/frontend-0e306a0cbb659ffacc2bde4bf6be791bdf3c4fd12ec49427513525c99e29cee2c56ae2ebb40ced499f14e919ae9c9bc1.js" crossorigin="anonymous" integrity="sha384-HQ2tmiGi8E_nvn6D0oxqS6V_CrDQoH8CwgvZVzZ4Q55AQ5zpe83OGEg-McwNCVnE"></head>
<link rel="preload" href="/frontend-570f3815d7fd92194c0bd8caac6e873c37520262d948b899db110969bdd20a82965a58d200fb559dee4285a369e3013a_bg.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" integrity="sha384-Vw84Fdf9khlMC9jKrG6HPDdSAmLZSLiZ2xEJab3SCoKWWljSAPtVne5ChaNp4wE6">
<link rel="modulepreload" href="/frontend-570f3815d7fd92194c0bd8caac6e873c37520262d948b899db110969bdd20a82965a58d200fb559dee4285a369e3013a.js" crossorigin="anonymous" integrity="sha384-uvU8cJ-rIWGrY5c2Ms2A0n6dIWdhqLgCPj6u5m_zninPENzyXtefBjknYTthlevK"></head>
<body>
<script>"use strict";

View File

@ -1,6 +1,8 @@
use leptos::error::Result;
use leptos::*;
use log::info;
use serde::{Deserialize, Serialize};
use url::Url;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
@ -35,6 +37,35 @@ async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
}
}
// TODO: import this from backend somehow
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct DbInstance {
pub id: i32,
pub ap_id: Url,
pub articles_url: Url,
pub inbox_url: String,
#[serde(skip)]
pub public_key: String,
#[serde(skip)]
pub private_key: Option<String>,
pub local: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct InstanceView {
pub instance: DbInstance,
pub following: Vec<DbInstance>,
}
async fn fetch_instance(url: &str) -> Result<InstanceView> {
let res = reqwest::get(url)
.await?
.json::<InstanceView>()
.await?;
info!("{:?}", &res);
Ok(res)
}
pub fn fetch_example() -> impl IntoView {
let (cat_count, set_cat_count) = create_signal::<CatCount>(0);
@ -43,6 +74,7 @@ pub fn fetch_example() -> impl IntoView {
// 2) we're not doing server-side rendering in this example anyway
// (during SSR, create_resource will begin loading on the server and resolve on the client)
let cats = create_local_resource(move || cat_count.get(), fetch_cats);
let instance = create_local_resource(move || "http://localhost:8131/api/v1/instance", fetch_instance);
let fallback = move |errors: RwSignal<Errors>| {
let error_list = move || {
@ -74,7 +106,14 @@ pub fn fetch_example() -> impl IntoView {
})
};
let instance_view = move || {
instance.and_then(|data| {
view! { <h1>{data.instance.ap_id.to_string()}</h1> }
})
};
view! {
{instance_view}
<div>
<label>
"How many cats would you like?"

3
rust-toolchain.toml_ Normal file
View File

@ -0,0 +1,3 @@
[toolchain]
profile = "default"
channel = "nightly"

View File

@ -1,3 +1,3 @@
pub async fn main() {
pub fn main() {
unimplemented!();
}