mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-25 21:01:08 +00:00
commit
e568f109f9
29 changed files with 1182 additions and 287 deletions
187
Cargo.lock
generated
187
Cargo.lock
generated
|
@ -232,6 +232,19 @@ version = "0.21.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
|
||||
|
||||
[[package]]
|
||||
name = "bcrypt"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d1c9c15093eb224f0baa400f38fcd713fc1391a6f1c389d886beef146d60a3"
|
||||
dependencies = [
|
||||
"base64 0.21.5",
|
||||
"blowfish",
|
||||
"getrandom",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
|
@ -253,6 +266,16 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blowfish"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.14.0"
|
||||
|
@ -338,6 +361,16 @@ dependencies = [
|
|||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.3.0"
|
||||
|
@ -449,6 +482,15 @@ dependencies = [
|
|||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder"
|
||||
version = "0.12.0"
|
||||
|
@ -672,6 +714,7 @@ dependencies = [
|
|||
"async-trait",
|
||||
"axum",
|
||||
"axum-macros",
|
||||
"bcrypt",
|
||||
"chrono",
|
||||
"diesel",
|
||||
"diesel-derive-newtype",
|
||||
|
@ -681,11 +724,13 @@ dependencies = [
|
|||
"env_logger",
|
||||
"futures",
|
||||
"hex",
|
||||
"jsonwebtoken",
|
||||
"once_cell",
|
||||
"pretty_assertions",
|
||||
"rand",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tokio",
|
||||
"tracing",
|
||||
|
@ -844,8 +889,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1088,6 +1135,15 @@ dependencies = [
|
|||
"hashbrown 0.14.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
|
@ -1138,6 +1194,21 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonwebtoken"
|
||||
version = "9.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c7ea04a7c5c055c175f189b6dc6ba036fd62306b58c66c9f6389036c503a3f4"
|
||||
dependencies = [
|
||||
"base64 0.21.5",
|
||||
"js-sys",
|
||||
"pem",
|
||||
"ring",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simple_asn1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
|
@ -1318,6 +1389,27 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.17"
|
||||
|
@ -1431,6 +1523,16 @@ dependencies = [
|
|||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310"
|
||||
dependencies = [
|
||||
"base64 0.21.5",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.0"
|
||||
|
@ -1491,6 +1593,12 @@ dependencies = [
|
|||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
|
@ -1694,6 +1802,20 @@ dependencies = [
|
|||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"getrandom",
|
||||
"libc",
|
||||
"spin",
|
||||
"untrusted",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.23"
|
||||
|
@ -1907,6 +2029,18 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simple_asn1"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"thiserror",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "skeptic"
|
||||
version = "0.13.7"
|
||||
|
@ -1957,12 +2091,24 @@ dependencies = [
|
|||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
|
@ -2060,6 +2206,35 @@ dependencies = [
|
|||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20"
|
||||
dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.6.0"
|
||||
|
@ -2271,6 +2446,12 @@ dependencies = [
|
|||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.4.1"
|
||||
|
@ -2555,3 +2736,9 @@ name = "yansi"
|
|||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
|
||||
|
|
|
@ -9,6 +9,7 @@ anyhow = "1.0.75"
|
|||
async-trait = "0.1.74"
|
||||
axum = "0.6.20"
|
||||
axum-macros = "0.3.8"
|
||||
bcrypt = "0.15.0"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
diesel = {version = "2.1.4", features = ["postgres", "chrono", "uuid"] }
|
||||
diesel-derive-newtype = "2.1.0"
|
||||
|
@ -18,8 +19,10 @@ enum_delegate = "0.2.0"
|
|||
env_logger = { version = "0.10.1", default-features = false }
|
||||
futures = "0.3.29"
|
||||
hex = "0.4.3"
|
||||
jsonwebtoken = "9.2.0"
|
||||
rand = "0.8.5"
|
||||
serde = "1.0.192"
|
||||
serde_json = "1.0.108"
|
||||
sha2 = "0.10.8"
|
||||
tokio = { version = "1.34.0", features = ["full"] }
|
||||
tracing = "0.1.40"
|
||||
|
|
|
@ -2,4 +2,6 @@ drop table conflict;
|
|||
drop table edit;
|
||||
drop table article;
|
||||
drop table instance_follow;
|
||||
drop table local_user;
|
||||
drop table person;
|
||||
drop table instance;
|
|
@ -9,10 +9,27 @@ create table instance (
|
|||
local bool not null
|
||||
);
|
||||
|
||||
create table person (
|
||||
id serial primary key,
|
||||
username text not null,
|
||||
ap_id varchar(255) not null unique,
|
||||
inbox_url text not null,
|
||||
public_key text not null,
|
||||
private_key text,
|
||||
last_refreshed_at timestamptz not null default now(),
|
||||
local bool not null
|
||||
);
|
||||
|
||||
create table local_user (
|
||||
id serial primary key,
|
||||
password_encrypted text not null,
|
||||
person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL
|
||||
);
|
||||
|
||||
create table instance_follow (
|
||||
id serial primary key,
|
||||
instance_id int REFERENCES instance ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
|
||||
follower_id int REFERENCES instance ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
|
||||
follower_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
|
||||
pending boolean not null,
|
||||
unique(instance_id, follower_id)
|
||||
);
|
|
@ -1,39 +1,23 @@
|
|||
use crate::database::article::{ArticleView, DbArticle, DbArticleForm};
|
||||
use crate::database::conflict::{ApiConflict, DbConflict, DbConflictForm};
|
||||
use crate::database::edit::{DbEdit, DbEditForm};
|
||||
use crate::database::instance::{DbInstance, InstanceView};
|
||||
use crate::database::instance::DbInstance;
|
||||
use crate::database::user::LocalUserView;
|
||||
use crate::database::version::EditVersion;
|
||||
use crate::database::MyDataHandle;
|
||||
use crate::error::MyResult;
|
||||
use crate::federation::activities::create_article::CreateArticle;
|
||||
use crate::federation::activities::follow::Follow;
|
||||
use crate::federation::activities::submit_article_update;
|
||||
use crate::utils::generate_article_version;
|
||||
use activitypub_federation::config::Data;
|
||||
use activitypub_federation::fetch::object_id::ObjectId;
|
||||
use axum::extract::Query;
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Form, Json, Router};
|
||||
use axum::Extension;
|
||||
use axum::Form;
|
||||
use axum::Json;
|
||||
use axum_macros::debug_handler;
|
||||
use diffy::create_patch;
|
||||
use futures::future::try_join_all;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
pub fn api_routes() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/article",
|
||||
get(get_article).post(create_article).patch(edit_article),
|
||||
)
|
||||
.route("/article/fork", post(fork_article))
|
||||
.route("/edit_conflicts", get(edit_conflicts))
|
||||
.route("/resolve_instance", get(resolve_instance))
|
||||
.route("/resolve_article", get(resolve_article))
|
||||
.route("/instance", get(get_local_instance))
|
||||
.route("/instance/follow", post(follow_instance))
|
||||
.route("/search", get(search_article))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct CreateArticleData {
|
||||
|
@ -42,7 +26,8 @@ pub struct CreateArticleData {
|
|||
|
||||
/// Create a new article with empty text, and federate it to followers.
|
||||
#[debug_handler]
|
||||
async fn create_article(
|
||||
pub(in crate::api) async fn create_article(
|
||||
Extension(_user): Extension<LocalUserView>,
|
||||
data: Data<MyDataHandle>,
|
||||
Form(create_article): Form<CreateArticleData>,
|
||||
) -> MyResult<Json<ArticleView>> {
|
||||
|
@ -91,7 +76,8 @@ pub struct EditArticleData {
|
|||
///
|
||||
/// Conflicts are stored in the database so they can be retrieved later from `/api/v3/edit_conflicts`.
|
||||
#[debug_handler]
|
||||
async fn edit_article(
|
||||
pub(in crate::api) async fn edit_article(
|
||||
Extension(_user): Extension<LocalUserView>,
|
||||
data: Data<MyDataHandle>,
|
||||
Form(edit_form): Form<EditArticleData>,
|
||||
) -> MyResult<Json<Option<ApiConflict>>> {
|
||||
|
@ -137,7 +123,7 @@ pub struct GetArticleData {
|
|||
|
||||
/// Retrieve an article by ID. It must already be stored in the local database.
|
||||
#[debug_handler]
|
||||
async fn get_article(
|
||||
pub(in crate::api) async fn get_article(
|
||||
Query(query): Query<GetArticleData>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<ArticleView>> {
|
||||
|
@ -147,97 +133,6 @@ async fn get_article(
|
|||
)?))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct ResolveObject {
|
||||
pub id: Url,
|
||||
}
|
||||
|
||||
/// Fetch a remote instance actor. This automatically synchronizes the remote articles collection to
|
||||
/// the local instance, and allows for interactions such as following.
|
||||
#[debug_handler]
|
||||
async fn resolve_instance(
|
||||
Query(query): Query<ResolveObject>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<DbInstance>> {
|
||||
let instance: DbInstance = ObjectId::from(query.id).dereference(&data).await?;
|
||||
Ok(Json(instance))
|
||||
}
|
||||
|
||||
/// Fetch a remote article, including edits collection. Allows viewing and editing. Note that new
|
||||
/// article changes can only be received if we follow the instance, or if it is refetched manually.
|
||||
#[debug_handler]
|
||||
async fn resolve_article(
|
||||
Query(query): Query<ResolveObject>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<ArticleView>> {
|
||||
let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?;
|
||||
let edits = DbEdit::read_for_article(&article, &data.db_connection)?;
|
||||
let latest_version = edits.last().unwrap().hash.clone();
|
||||
Ok(Json(ArticleView {
|
||||
article,
|
||||
edits,
|
||||
latest_version,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Retrieve the local instance info.
|
||||
#[debug_handler]
|
||||
async fn get_local_instance(data: Data<MyDataHandle>) -> MyResult<Json<InstanceView>> {
|
||||
let local_instance = DbInstance::read_local_view(&data.db_connection)?;
|
||||
Ok(Json(local_instance))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct FollowInstance {
|
||||
pub id: i32,
|
||||
}
|
||||
|
||||
/// Make the local instance follow a given remote instance, to receive activities about new and
|
||||
/// updated articles.
|
||||
#[debug_handler]
|
||||
async fn follow_instance(
|
||||
data: Data<MyDataHandle>,
|
||||
Form(query): Form<FollowInstance>,
|
||||
) -> MyResult<()> {
|
||||
let local_instance = DbInstance::read_local_instance(&data.db_connection)?;
|
||||
let target = DbInstance::read(query.id, &data.db_connection)?;
|
||||
let pending = !target.local;
|
||||
DbInstance::follow(local_instance.id, target.id, pending, &data)?;
|
||||
let instance = DbInstance::read(query.id, &data.db_connection)?;
|
||||
Follow::send(local_instance, instance, &data).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a list of all unresolved edit conflicts.
|
||||
#[debug_handler]
|
||||
async fn edit_conflicts(data: Data<MyDataHandle>) -> MyResult<Json<Vec<ApiConflict>>> {
|
||||
let conflicts = DbConflict::list(&data.db_connection)?;
|
||||
let conflicts: Vec<ApiConflict> = try_join_all(conflicts.into_iter().map(|c| {
|
||||
let data = data.reset_request_count();
|
||||
async move { c.to_api_conflict(&data).await }
|
||||
}))
|
||||
.await?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect();
|
||||
Ok(Json(conflicts))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct SearchArticleData {
|
||||
pub query: String,
|
||||
}
|
||||
|
||||
/// Search articles for matching title or body text.
|
||||
#[debug_handler]
|
||||
async fn search_article(
|
||||
Query(query): Query<SearchArticleData>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<Vec<DbArticle>>> {
|
||||
let article = DbArticle::search(&query.query, &data.db_connection)?;
|
||||
Ok(Json(article))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct ForkArticleData {
|
||||
// TODO: could add optional param new_title so there is no problem with title collision
|
||||
|
@ -249,7 +144,8 @@ pub struct ForkArticleData {
|
|||
/// Fork a remote article to local instance. This is useful if there are disagreements about
|
||||
/// how an article should be edited.
|
||||
#[debug_handler]
|
||||
async fn fork_article(
|
||||
pub(in crate::api) async fn fork_article(
|
||||
Extension(_user): Extension<LocalUserView>,
|
||||
data: Data<MyDataHandle>,
|
||||
Form(fork_form): Form<ForkArticleData>,
|
||||
) -> MyResult<Json<ArticleView>> {
|
40
src/api/instance.rs
Normal file
40
src/api/instance.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
use crate::database::instance::{DbInstance, InstanceView};
|
||||
use crate::database::user::LocalUserView;
|
||||
use crate::database::MyDataHandle;
|
||||
use crate::error::MyResult;
|
||||
use crate::federation::activities::follow::Follow;
|
||||
use activitypub_federation::config::Data;
|
||||
use axum::Extension;
|
||||
use axum::{Form, Json};
|
||||
use axum_macros::debug_handler;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Retrieve the local instance info.
|
||||
#[debug_handler]
|
||||
pub(in crate::api) async fn get_local_instance(
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<InstanceView>> {
|
||||
let local_instance = DbInstance::read_local_view(&data.db_connection)?;
|
||||
Ok(Json(local_instance))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct FollowInstance {
|
||||
pub id: i32,
|
||||
}
|
||||
|
||||
/// Make the local instance follow a given remote instance, to receive activities about new and
|
||||
/// updated articles.
|
||||
#[debug_handler]
|
||||
pub(in crate::api) async fn follow_instance(
|
||||
Extension(user): Extension<LocalUserView>,
|
||||
data: Data<MyDataHandle>,
|
||||
Form(query): Form<FollowInstance>,
|
||||
) -> MyResult<()> {
|
||||
let target = DbInstance::read(query.id, &data.db_connection)?;
|
||||
let pending = !target.local;
|
||||
DbInstance::follow(&user.person, &target, pending, &data)?;
|
||||
let instance = DbInstance::read(query.id, &data.db_connection)?;
|
||||
Follow::send(user.person, instance, &data).await?;
|
||||
Ok(())
|
||||
}
|
133
src/api/mod.rs
Normal file
133
src/api/mod.rs
Normal file
|
@ -0,0 +1,133 @@
|
|||
use crate::api::article::create_article;
|
||||
use crate::api::article::{edit_article, fork_article, get_article};
|
||||
use crate::api::instance::follow_instance;
|
||||
use crate::api::instance::get_local_instance;
|
||||
use crate::api::user::login_user;
|
||||
use crate::api::user::register_user;
|
||||
use crate::api::user::validate;
|
||||
use crate::database::article::{ArticleView, DbArticle};
|
||||
use crate::database::conflict::{ApiConflict, DbConflict};
|
||||
use crate::database::edit::DbEdit;
|
||||
use crate::database::instance::DbInstance;
|
||||
use crate::database::MyDataHandle;
|
||||
use crate::error::MyResult;
|
||||
use activitypub_federation::config::Data;
|
||||
use activitypub_federation::fetch::object_id::ObjectId;
|
||||
use axum::extract::Query;
|
||||
use axum::routing::{get, post};
|
||||
use axum::{
|
||||
extract::TypedHeader,
|
||||
headers::authorization::{Authorization, Bearer},
|
||||
http::Request,
|
||||
http::StatusCode,
|
||||
middleware::{self, Next},
|
||||
response::Response,
|
||||
};
|
||||
use axum::{Json, Router};
|
||||
use axum_macros::debug_handler;
|
||||
use futures::future::try_join_all;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::warn;
|
||||
use url::Url;
|
||||
|
||||
pub mod article;
|
||||
pub mod instance;
|
||||
pub mod user;
|
||||
|
||||
pub fn api_routes() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/article",
|
||||
get(get_article).post(create_article).patch(edit_article),
|
||||
)
|
||||
.route("/article/fork", post(fork_article))
|
||||
.route("/edit_conflicts", get(edit_conflicts))
|
||||
.route("/resolve_instance", get(resolve_instance))
|
||||
.route("/resolve_article", get(resolve_article))
|
||||
.route("/instance", get(get_local_instance))
|
||||
.route("/instance/follow", post(follow_instance))
|
||||
.route("/search", get(search_article))
|
||||
.route("/user/register", post(register_user))
|
||||
.route("/user/login", post(login_user))
|
||||
.route_layer(middleware::from_fn(auth))
|
||||
}
|
||||
|
||||
async fn auth<B>(
|
||||
data: Data<MyDataHandle>,
|
||||
auth: Option<TypedHeader<Authorization<Bearer>>>,
|
||||
mut request: Request<B>,
|
||||
next: Next<B>,
|
||||
) -> Result<Response, StatusCode> {
|
||||
if let Some(auth) = auth {
|
||||
let user = validate(auth.token(), &data).await.map_err(|e| {
|
||||
warn!("Failed to validate auth token: {e}");
|
||||
StatusCode::UNAUTHORIZED
|
||||
})?;
|
||||
request.extensions_mut().insert(user);
|
||||
}
|
||||
let response = next.run(request).await;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct ResolveObject {
|
||||
pub id: Url,
|
||||
}
|
||||
|
||||
/// Fetch a remote instance actor. This automatically synchronizes the remote articles collection to
|
||||
/// the local instance, and allows for interactions such as following.
|
||||
#[debug_handler]
|
||||
async fn resolve_instance(
|
||||
Query(query): Query<ResolveObject>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<DbInstance>> {
|
||||
let instance: DbInstance = ObjectId::from(query.id).dereference(&data).await?;
|
||||
Ok(Json(instance))
|
||||
}
|
||||
|
||||
/// Fetch a remote article, including edits collection. Allows viewing and editing. Note that new
|
||||
/// article changes can only be received if we follow the instance, or if it is refetched manually.
|
||||
#[debug_handler]
|
||||
async fn resolve_article(
|
||||
Query(query): Query<ResolveObject>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<ArticleView>> {
|
||||
let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?;
|
||||
let edits = DbEdit::read_for_article(&article, &data.db_connection)?;
|
||||
let latest_version = edits.last().unwrap().hash.clone();
|
||||
Ok(Json(ArticleView {
|
||||
article,
|
||||
edits,
|
||||
latest_version,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get a list of all unresolved edit conflicts.
|
||||
#[debug_handler]
|
||||
async fn edit_conflicts(data: Data<MyDataHandle>) -> MyResult<Json<Vec<ApiConflict>>> {
|
||||
let conflicts = DbConflict::list(&data.db_connection)?;
|
||||
let conflicts: Vec<ApiConflict> = try_join_all(conflicts.into_iter().map(|c| {
|
||||
let data = data.reset_request_count();
|
||||
async move { c.to_api_conflict(&data).await }
|
||||
}))
|
||||
.await?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect();
|
||||
Ok(Json(conflicts))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct SearchArticleData {
|
||||
pub query: String,
|
||||
}
|
||||
|
||||
/// Search articles for matching title or body text.
|
||||
#[debug_handler]
|
||||
async fn search_article(
|
||||
Query(query): Query<SearchArticleData>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<Vec<DbArticle>>> {
|
||||
let article = DbArticle::search(&query.query, &data.db_connection)?;
|
||||
Ok(Json(article))
|
||||
}
|
92
src/api/user.rs
Normal file
92
src/api/user.rs
Normal file
|
@ -0,0 +1,92 @@
|
|||
use crate::database::user::{DbLocalUser, DbPerson, LocalUserView};
|
||||
use crate::database::MyDataHandle;
|
||||
use crate::error::MyResult;
|
||||
use activitypub_federation::config::Data;
|
||||
use anyhow::anyhow;
|
||||
use axum::{Form, Json};
|
||||
use axum_macros::debug_handler;
|
||||
use bcrypt::verify;
|
||||
use chrono::Utc;
|
||||
use jsonwebtoken::DecodingKey;
|
||||
use jsonwebtoken::Validation;
|
||||
use jsonwebtoken::{decode, get_current_timestamp};
|
||||
use jsonwebtoken::{encode, EncodingKey, Header};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Claims {
|
||||
/// local_user.id
|
||||
pub sub: String,
|
||||
/// hostname
|
||||
pub iss: String,
|
||||
/// Creation time as unix timestamp
|
||||
pub iat: i64,
|
||||
/// Expiration time
|
||||
pub exp: u64,
|
||||
}
|
||||
|
||||
// TODO: move to config
|
||||
const SECRET: &[u8] = "secret".as_bytes();
|
||||
|
||||
pub(in crate::api) fn generate_login_token(
|
||||
local_user: DbLocalUser,
|
||||
data: &Data<MyDataHandle>,
|
||||
) -> MyResult<LoginResponse> {
|
||||
let hostname = data.domain().to_string();
|
||||
let claims = Claims {
|
||||
sub: local_user.id.to_string(),
|
||||
iss: hostname,
|
||||
iat: Utc::now().timestamp(),
|
||||
exp: get_current_timestamp(),
|
||||
};
|
||||
|
||||
let key = EncodingKey::from_secret(SECRET);
|
||||
let jwt = encode(&Header::default(), &claims, &key)?;
|
||||
Ok(LoginResponse { jwt })
|
||||
}
|
||||
|
||||
pub async fn validate(jwt: &str, data: &Data<MyDataHandle>) -> MyResult<LocalUserView> {
|
||||
let validation = Validation::default();
|
||||
let key = DecodingKey::from_secret(SECRET);
|
||||
let claims = decode::<Claims>(jwt, &key, &validation)?;
|
||||
DbPerson::read_local_from_id(claims.claims.sub.parse()?, data)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct RegisterUserData {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct LoginResponse {
|
||||
pub jwt: String,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub(in crate::api) async fn register_user(
|
||||
data: Data<MyDataHandle>,
|
||||
Form(form): Form<RegisterUserData>,
|
||||
) -> MyResult<Json<LoginResponse>> {
|
||||
let user = DbPerson::create_local(form.username, form.password, &data)?;
|
||||
Ok(Json(generate_login_token(user.local_user, &data)?))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct LoginUserData {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub(in crate::api) async fn login_user(
|
||||
data: Data<MyDataHandle>,
|
||||
Form(form): Form<LoginUserData>,
|
||||
) -> MyResult<Json<LoginResponse>> {
|
||||
let user = DbPerson::read_local_from_name(&form.username, &data)?;
|
||||
let valid = verify(&form.password, &user.local_user.password_encrypted)?;
|
||||
if !valid {
|
||||
return Err(anyhow!("Invalid login").into());
|
||||
}
|
||||
Ok(Json(generate_login_token(user.local_user, &data)?))
|
||||
}
|
|
@ -57,7 +57,7 @@ impl DbEditForm {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn generate_ap_id(
|
||||
pub fn generate_ap_id(
|
||||
article: &DbArticle,
|
||||
version: &EditVersion,
|
||||
) -> MyResult<ObjectId<DbEdit>> {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::database::schema::{instance, instance_follow};
|
||||
use crate::database::user::DbPerson;
|
||||
use crate::database::MyDataHandle;
|
||||
use crate::error::MyResult;
|
||||
use crate::federation::objects::articles_collection::DbArticleCollection;
|
||||
|
@ -24,11 +25,11 @@ pub struct DbInstance {
|
|||
pub articles_url: CollectionId<DbArticleCollection>,
|
||||
pub inbox_url: String,
|
||||
#[serde(skip)]
|
||||
pub(crate) public_key: String,
|
||||
pub public_key: String,
|
||||
#[serde(skip)]
|
||||
pub(crate) private_key: Option<String>,
|
||||
pub private_key: Option<String>,
|
||||
#[serde(skip)]
|
||||
pub(crate) last_refreshed_at: DateTime<Utc>,
|
||||
pub last_refreshed_at: DateTime<Utc>,
|
||||
pub local: bool,
|
||||
}
|
||||
|
||||
|
@ -38,9 +39,9 @@ pub struct DbInstanceForm {
|
|||
pub ap_id: ObjectId<DbInstance>,
|
||||
pub articles_url: CollectionId<DbArticleCollection>,
|
||||
pub inbox_url: String,
|
||||
pub(crate) public_key: String,
|
||||
pub(crate) private_key: Option<String>,
|
||||
pub(crate) last_refreshed_at: DateTime<Utc>,
|
||||
pub public_key: String,
|
||||
pub private_key: Option<String>,
|
||||
pub last_refreshed_at: DateTime<Utc>,
|
||||
pub local: bool,
|
||||
}
|
||||
|
||||
|
@ -48,7 +49,7 @@ pub struct DbInstanceForm {
|
|||
#[diesel(table_name = article, check_for_backend(diesel::pg::Pg))]
|
||||
pub struct InstanceView {
|
||||
pub instance: DbInstance,
|
||||
pub followers: Vec<DbInstance>,
|
||||
pub followers: Vec<DbPerson>,
|
||||
pub following: Vec<DbInstance>,
|
||||
}
|
||||
|
||||
|
@ -98,36 +99,36 @@ impl DbInstance {
|
|||
}
|
||||
|
||||
pub fn follow(
|
||||
follower_id_: i32,
|
||||
instance_id_: i32,
|
||||
follower: &DbPerson,
|
||||
instance: &DbInstance,
|
||||
pending_: bool,
|
||||
data: &Data<MyDataHandle>,
|
||||
) -> MyResult<()> {
|
||||
debug_assert_ne!(follower_id_, instance_id_);
|
||||
use instance_follow::dsl::{follower_id, instance_id, pending};
|
||||
let mut conn = data.db_connection.lock().unwrap();
|
||||
let form = (
|
||||
instance_id.eq(instance_id_),
|
||||
follower_id.eq(follower_id_),
|
||||
instance_id.eq(instance.id),
|
||||
follower_id.eq(follower.id),
|
||||
pending.eq(pending_),
|
||||
);
|
||||
dbg!(follower_id_, instance_id_, pending_);
|
||||
insert_into(instance_follow::table)
|
||||
let rows = insert_into(instance_follow::table)
|
||||
.values(form)
|
||||
.on_conflict((instance_id, follower_id))
|
||||
.do_update()
|
||||
.set(form)
|
||||
.execute(conn.deref_mut())?;
|
||||
assert_eq!(1, rows);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_followers(id_: i32, conn: &Mutex<PgConnection>) -> MyResult<Vec<Self>> {
|
||||
pub fn read_followers(id_: i32, conn: &Mutex<PgConnection>) -> MyResult<Vec<DbPerson>> {
|
||||
use crate::database::schema::person;
|
||||
use instance_follow::dsl::{follower_id, instance_id};
|
||||
let mut conn = conn.lock().unwrap();
|
||||
Ok(instance_follow::table
|
||||
.inner_join(instance::table.on(follower_id.eq(instance::dsl::id)))
|
||||
.inner_join(person::table.on(follower_id.eq(person::dsl::id)))
|
||||
.filter(instance_id.eq(id_))
|
||||
.select(instance::all_columns)
|
||||
.select(person::all_columns)
|
||||
.get_results(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ pub mod conflict;
|
|||
pub mod edit;
|
||||
pub mod instance;
|
||||
mod schema;
|
||||
pub mod user;
|
||||
pub mod version;
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
|
@ -57,8 +57,39 @@ diesel::table! {
|
|||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
local_user (id) {
|
||||
id -> Int4,
|
||||
password_encrypted -> Text,
|
||||
person_id -> Int4,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
person (id) {
|
||||
id -> Int4,
|
||||
username -> Text,
|
||||
#[max_length = 255]
|
||||
ap_id -> Varchar,
|
||||
inbox_url -> Text,
|
||||
public_key -> Text,
|
||||
private_key -> Nullable<Text>,
|
||||
last_refreshed_at -> Timestamptz,
|
||||
local -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::joinable!(article -> instance (instance_id));
|
||||
diesel::joinable!(conflict -> article (article_id));
|
||||
diesel::joinable!(edit -> article (article_id));
|
||||
diesel::joinable!(local_user -> person (person_id));
|
||||
|
||||
diesel::allow_tables_to_appear_in_same_query!(article, conflict, edit, instance, instance_follow,);
|
||||
diesel::allow_tables_to_appear_in_same_query!(
|
||||
article,
|
||||
conflict,
|
||||
edit,
|
||||
instance,
|
||||
instance_follow,
|
||||
local_user,
|
||||
person,
|
||||
);
|
||||
|
|
149
src/database/user.rs
Normal file
149
src/database/user.rs
Normal file
|
@ -0,0 +1,149 @@
|
|||
use crate::database::schema::{local_user, person};
|
||||
use crate::database::MyDataHandle;
|
||||
use crate::error::MyResult;
|
||||
use activitypub_federation::config::Data;
|
||||
use activitypub_federation::fetch::object_id::ObjectId;
|
||||
use activitypub_federation::http_signatures::generate_actor_keypair;
|
||||
use bcrypt::hash;
|
||||
use bcrypt::DEFAULT_COST;
|
||||
use chrono::{DateTime, Local, Utc};
|
||||
use diesel::ExpressionMethods;
|
||||
use diesel::QueryDsl;
|
||||
use diesel::{
|
||||
insert_into, AsChangeset, Identifiable, Insertable, PgConnection, Queryable, RunQueryDsl,
|
||||
Selectable,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::DerefMut;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Queryable)]
|
||||
#[diesel(check_for_backend(diesel::pg::Pg))]
|
||||
pub struct LocalUserView {
|
||||
pub person: DbPerson,
|
||||
pub local_user: DbLocalUser,
|
||||
}
|
||||
|
||||
/// A user with account registered on local instance.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Queryable, Selectable, Identifiable)]
|
||||
#[diesel(table_name = local_user, check_for_backend(diesel::pg::Pg))]
|
||||
pub struct DbLocalUser {
|
||||
pub id: i32,
|
||||
pub password_encrypted: String,
|
||||
pub person_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Insertable, AsChangeset)]
|
||||
#[diesel(table_name = local_user, check_for_backend(diesel::pg::Pg))]
|
||||
pub struct DbLocalUserForm {
|
||||
pub password_encrypted: String,
|
||||
pub person_id: i32,
|
||||
}
|
||||
|
||||
/// Federation related data from a local or remote user.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Queryable, Selectable, Identifiable)]
|
||||
#[diesel(table_name = person, check_for_backend(diesel::pg::Pg))]
|
||||
pub struct DbPerson {
|
||||
pub id: i32,
|
||||
pub username: String,
|
||||
pub ap_id: ObjectId<DbPerson>,
|
||||
pub inbox_url: String,
|
||||
#[serde(skip)]
|
||||
pub public_key: String,
|
||||
#[serde(skip)]
|
||||
pub private_key: Option<String>,
|
||||
#[serde(skip)]
|
||||
pub last_refreshed_at: DateTime<Utc>,
|
||||
pub local: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Insertable, AsChangeset)]
|
||||
#[diesel(table_name = person, check_for_backend(diesel::pg::Pg))]
|
||||
pub struct DbPersonForm {
|
||||
pub username: String,
|
||||
pub ap_id: ObjectId<DbPerson>,
|
||||
pub inbox_url: String,
|
||||
pub public_key: String,
|
||||
pub private_key: Option<String>,
|
||||
pub last_refreshed_at: DateTime<Utc>,
|
||||
pub local: bool,
|
||||
}
|
||||
|
||||
impl DbPerson {
|
||||
pub fn create(person_form: &DbPersonForm, conn: &Mutex<PgConnection>) -> MyResult<Self> {
|
||||
let mut conn = conn.lock().unwrap();
|
||||
Ok(insert_into(person::table)
|
||||
.values(person_form)
|
||||
.on_conflict(person::dsl::ap_id)
|
||||
.do_update()
|
||||
.set(person_form)
|
||||
.get_result::<DbPerson>(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn create_local(
|
||||
username: String,
|
||||
password: String,
|
||||
data: &Data<MyDataHandle>,
|
||||
) -> MyResult<LocalUserView> {
|
||||
let mut conn = data.db_connection.lock().unwrap();
|
||||
let hostname = data.domain();
|
||||
let ap_id = ObjectId::parse(&format!("http://{hostname}/user/{username}"))?;
|
||||
let inbox_url = format!("http://{hostname}/inbox");
|
||||
let keypair = generate_actor_keypair()?;
|
||||
let person_form = DbPersonForm {
|
||||
username,
|
||||
ap_id,
|
||||
inbox_url,
|
||||
public_key: keypair.public_key,
|
||||
private_key: Some(keypair.private_key),
|
||||
last_refreshed_at: Local::now().into(),
|
||||
local: true,
|
||||
};
|
||||
|
||||
let person = insert_into(person::table)
|
||||
.values(person_form)
|
||||
.get_result::<DbPerson>(conn.deref_mut())?;
|
||||
|
||||
let local_user_form = DbLocalUserForm {
|
||||
password_encrypted: hash(password, DEFAULT_COST)?,
|
||||
person_id: person.id,
|
||||
};
|
||||
|
||||
let local_user = insert_into(local_user::table)
|
||||
.values(local_user_form)
|
||||
.get_result::<DbLocalUser>(conn.deref_mut())?;
|
||||
|
||||
Ok(LocalUserView { local_user, person })
|
||||
}
|
||||
|
||||
pub fn read_from_ap_id(
|
||||
ap_id: &ObjectId<DbPerson>,
|
||||
data: &Data<MyDataHandle>,
|
||||
) -> MyResult<DbPerson> {
|
||||
let mut conn = data.db_connection.lock().unwrap();
|
||||
Ok(person::table
|
||||
.filter(person::dsl::ap_id.eq(ap_id))
|
||||
.get_result(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn read_local_from_name(
|
||||
username: &str,
|
||||
data: &Data<MyDataHandle>,
|
||||
) -> MyResult<LocalUserView> {
|
||||
let mut conn = data.db_connection.lock().unwrap();
|
||||
Ok(person::table
|
||||
.inner_join(local_user::table)
|
||||
.filter(person::dsl::local)
|
||||
.filter(person::dsl::username.eq(username))
|
||||
.get_result(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn read_local_from_id(id: i32, data: &Data<MyDataHandle>) -> MyResult<LocalUserView> {
|
||||
let mut conn = data.db_connection.lock().unwrap();
|
||||
Ok(person::table
|
||||
.inner_join(local_user::table)
|
||||
.filter(person::dsl::local)
|
||||
.filter(person::dsl::id.eq(id))
|
||||
.get_result(conn.deref_mut())?)
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ use std::fmt::{Display, Formatter};
|
|||
pub type MyResult<T> = Result<T, Error>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Error(pub(crate) anyhow::Error);
|
||||
pub struct Error(pub anyhow::Error);
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use crate::database::instance::DbInstance;
|
||||
use crate::error::MyResult;
|
||||
use crate::federation::send_activity;
|
||||
use crate::utils::generate_activity_id;
|
||||
use crate::{database::MyDataHandle, federation::activities::follow::Follow};
|
||||
use activitypub_federation::traits::Actor;
|
||||
use activitypub_federation::{
|
||||
config::Data, fetch::object_id::ObjectId, kinds::activity::AcceptType, traits::ActivityHandler,
|
||||
};
|
||||
|
@ -19,14 +21,28 @@ pub struct Accept {
|
|||
}
|
||||
|
||||
impl Accept {
|
||||
pub fn new(actor: ObjectId<DbInstance>, object: Follow) -> MyResult<Accept> {
|
||||
let id = generate_activity_id(actor.inner())?;
|
||||
Ok(Accept {
|
||||
actor,
|
||||
pub async fn send(
|
||||
local_instance: DbInstance,
|
||||
object: Follow,
|
||||
data: &Data<MyDataHandle>,
|
||||
) -> MyResult<()> {
|
||||
let id = generate_activity_id(local_instance.ap_id.inner())?;
|
||||
let follower = object.actor.dereference(data).await?;
|
||||
let accept = Accept {
|
||||
actor: local_instance.ap_id.clone(),
|
||||
object,
|
||||
kind: Default::default(),
|
||||
id,
|
||||
})
|
||||
};
|
||||
dbg!(&accept);
|
||||
send_activity(
|
||||
&local_instance,
|
||||
accept,
|
||||
vec![follower.shared_inbox_or_inbox()],
|
||||
data,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,10 +64,11 @@ impl ActivityHandler for Accept {
|
|||
}
|
||||
|
||||
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
dbg!(&self);
|
||||
// add to follows
|
||||
let local_instance = DbInstance::read_local_instance(&data.db_connection)?;
|
||||
let actor = self.actor.dereference(data).await?;
|
||||
DbInstance::follow(local_instance.id, actor.id, false, data)?;
|
||||
let person = self.object.actor.dereference_local(data).await?;
|
||||
let instance = self.actor.dereference(data).await?;
|
||||
DbInstance::follow(&person, &instance, false, data)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use crate::database::instance::DbInstance;
|
||||
use crate::database::user::DbPerson;
|
||||
use crate::error::MyResult;
|
||||
use crate::federation::send_activity;
|
||||
use crate::{database::MyDataHandle, federation::activities::accept::Accept, generate_activity_id};
|
||||
use activitypub_federation::{
|
||||
config::Data,
|
||||
|
@ -13,29 +15,24 @@ use url::Url;
|
|||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Follow {
|
||||
pub(crate) actor: ObjectId<DbInstance>,
|
||||
pub(crate) object: ObjectId<DbInstance>,
|
||||
pub actor: ObjectId<DbPerson>,
|
||||
pub object: ObjectId<DbInstance>,
|
||||
#[serde(rename = "type")]
|
||||
kind: FollowType,
|
||||
id: Url,
|
||||
}
|
||||
|
||||
impl Follow {
|
||||
pub async fn send(
|
||||
local_instance: DbInstance,
|
||||
to: DbInstance,
|
||||
data: &Data<MyDataHandle>,
|
||||
) -> MyResult<()> {
|
||||
let id = generate_activity_id(local_instance.ap_id.inner())?;
|
||||
pub async fn send(actor: DbPerson, to: DbInstance, data: &Data<MyDataHandle>) -> MyResult<()> {
|
||||
let id = generate_activity_id(actor.ap_id.inner())?;
|
||||
let follow = Follow {
|
||||
actor: local_instance.ap_id.clone(),
|
||||
actor: actor.ap_id.clone(),
|
||||
object: to.ap_id.clone(),
|
||||
kind: Default::default(),
|
||||
id,
|
||||
};
|
||||
local_instance
|
||||
.send(follow, vec![to.shared_inbox_or_inbox()], data)
|
||||
.await?;
|
||||
|
||||
send_activity(&actor, follow, vec![to.shared_inbox_or_inbox()], data).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -58,16 +55,14 @@ impl ActivityHandler for Follow {
|
|||
}
|
||||
|
||||
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
dbg!(&self);
|
||||
let actor = self.actor.dereference(data).await?;
|
||||
let local_instance = DbInstance::read_local_instance(&data.db_connection)?;
|
||||
DbInstance::follow(actor.id, local_instance.id, false, data)?;
|
||||
dbg!(&actor.ap_id, &local_instance.ap_id);
|
||||
DbInstance::follow(&actor, &local_instance, false, data)?;
|
||||
|
||||
// send back an accept
|
||||
let follower = self.actor.dereference(data).await?;
|
||||
let accept = Accept::new(local_instance.ap_id.clone(), self)?;
|
||||
local_instance
|
||||
.send(accept, vec![follower.shared_inbox_or_inbox()], data)
|
||||
.await?;
|
||||
Accept::send(local_instance, self, data).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ use activitypub_federation::{
|
|||
traits::ActivityHandler,
|
||||
};
|
||||
|
||||
use crate::federation::send_activity;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
|
@ -41,9 +42,13 @@ impl RejectEdit {
|
|||
kind: Default::default(),
|
||||
id,
|
||||
};
|
||||
local_instance
|
||||
.send(reject, vec![Url::parse(&user_instance.inbox_url)?], data)
|
||||
.await?;
|
||||
send_activity(
|
||||
&local_instance,
|
||||
reject,
|
||||
vec![Url::parse(&user_instance.inbox_url)?],
|
||||
data,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use crate::database::instance::DbInstance;
|
|||
use crate::federation::activities::reject::RejectEdit;
|
||||
use crate::federation::activities::update_local_article::UpdateLocalArticle;
|
||||
use crate::federation::objects::edit::ApubEdit;
|
||||
use crate::federation::send_activity;
|
||||
use crate::utils::generate_activity_id;
|
||||
use activitypub_federation::kinds::activity::UpdateType;
|
||||
use activitypub_federation::{
|
||||
|
@ -47,9 +48,13 @@ impl UpdateRemoteArticle {
|
|||
kind: Default::default(),
|
||||
id,
|
||||
};
|
||||
local_instance
|
||||
.send(update, vec![Url::parse(&article_instance.inbox_url)?], data)
|
||||
.await?;
|
||||
send_activity(
|
||||
&local_instance,
|
||||
update,
|
||||
vec![Url::parse(&article_instance.inbox_url)?],
|
||||
data,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,34 @@
|
|||
use crate::database::MyDataHandle;
|
||||
use activitypub_federation::activity_sending::SendActivityTask;
|
||||
use activitypub_federation::config::Data;
|
||||
use activitypub_federation::protocol::context::WithContext;
|
||||
use activitypub_federation::traits::{ActivityHandler, Actor};
|
||||
use serde::Serialize;
|
||||
use std::fmt::Debug;
|
||||
use tracing::log::warn;
|
||||
use url::Url;
|
||||
|
||||
pub mod activities;
|
||||
pub mod objects;
|
||||
pub mod routes;
|
||||
|
||||
pub async fn send_activity<Activity, ActorType: Actor>(
|
||||
actor: &ActorType,
|
||||
activity: Activity,
|
||||
recipients: Vec<Url>,
|
||||
data: &Data<MyDataHandle>,
|
||||
) -> Result<(), <Activity as ActivityHandler>::Error>
|
||||
where
|
||||
Activity: ActivityHandler + Serialize + Debug + Send + Sync,
|
||||
<Activity as ActivityHandler>::Error: From<activitypub_federation::error::Error>,
|
||||
{
|
||||
let activity = WithContext::new_default(activity);
|
||||
let sends = SendActivityTask::prepare(&activity, actor, recipients, data).await?;
|
||||
for send in sends {
|
||||
let send = send.sign_and_send(data).await;
|
||||
if let Err(e) = send {
|
||||
warn!("Failed to send activity {:?}: {e}", activity);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -19,11 +19,11 @@ use url::Url;
|
|||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApubArticle {
|
||||
#[serde(rename = "type")]
|
||||
pub(crate) kind: ArticleType,
|
||||
pub(crate) id: ObjectId<DbArticle>,
|
||||
pub(crate) attributed_to: ObjectId<DbInstance>,
|
||||
pub kind: ArticleType,
|
||||
pub id: ObjectId<DbArticle>,
|
||||
pub attributed_to: ObjectId<DbInstance>,
|
||||
#[serde(deserialize_with = "deserialize_one_or_many")]
|
||||
pub(crate) to: Vec<Url>,
|
||||
pub to: Vec<Url>,
|
||||
pub edits: CollectionId<DbEditCollection>,
|
||||
latest_version: EditVersion,
|
||||
content: String,
|
||||
|
|
|
@ -16,10 +16,10 @@ use url::Url;
|
|||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ArticleCollection {
|
||||
pub(crate) r#type: CollectionType,
|
||||
pub(crate) id: Url,
|
||||
pub(crate) total_items: i32,
|
||||
pub(crate) items: Vec<ApubArticle>,
|
||||
pub r#type: CollectionType,
|
||||
pub id: Url,
|
||||
pub total_items: i32,
|
||||
pub items: Vec<ApubArticle>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
|
@ -18,10 +18,10 @@ use url::Url;
|
|||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApubEditCollection {
|
||||
pub(crate) r#type: OrderedCollectionType,
|
||||
pub(crate) id: Url,
|
||||
pub(crate) total_items: i32,
|
||||
pub(crate) items: Vec<ApubEdit>,
|
||||
pub r#type: OrderedCollectionType,
|
||||
pub id: Url,
|
||||
pub total_items: i32,
|
||||
pub items: Vec<ApubEdit>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
|
@ -2,6 +2,7 @@ use crate::database::instance::{DbInstance, DbInstanceForm};
|
|||
use crate::database::MyDataHandle;
|
||||
use crate::error::{Error, MyResult};
|
||||
use crate::federation::objects::articles_collection::DbArticleCollection;
|
||||
use crate::federation::send_activity;
|
||||
use activitypub_federation::activity_sending::SendActivityTask;
|
||||
use activitypub_federation::fetch::collection_id::CollectionId;
|
||||
use activitypub_federation::kinds::actor::ServiceType;
|
||||
|
@ -62,28 +63,7 @@ impl DbInstance {
|
|||
.into_iter()
|
||||
.map(|i| Url::parse(&i.inbox_url).unwrap()),
|
||||
);
|
||||
self.send(activity, inboxes, data).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send<Activity>(
|
||||
&self,
|
||||
activity: Activity,
|
||||
recipients: Vec<Url>,
|
||||
data: &Data<MyDataHandle>,
|
||||
) -> Result<(), <Activity as ActivityHandler>::Error>
|
||||
where
|
||||
Activity: ActivityHandler + Serialize + Debug + Send + Sync,
|
||||
<Activity as ActivityHandler>::Error: From<activitypub_federation::error::Error>,
|
||||
{
|
||||
let activity = WithContext::new_default(activity);
|
||||
let sends = SendActivityTask::prepare(&activity, self, recipients, data).await?;
|
||||
for send in sends {
|
||||
let send = send.sign_and_send(data).await;
|
||||
if let Err(e) = send {
|
||||
warn!("Failed to send activity {:?}: {e}", activity);
|
||||
}
|
||||
}
|
||||
send_activity(self, activity, inboxes, data).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,3 +3,4 @@ pub mod articles_collection;
|
|||
pub mod edit;
|
||||
pub mod edits_collection;
|
||||
pub mod instance;
|
||||
pub mod user;
|
||||
|
|
93
src/federation/objects/user.rs
Normal file
93
src/federation/objects/user.rs
Normal file
|
@ -0,0 +1,93 @@
|
|||
use crate::database::user::{DbPerson, DbPersonForm};
|
||||
use crate::database::MyDataHandle;
|
||||
use crate::error::Error;
|
||||
use activitypub_federation::kinds::actor::PersonType;
|
||||
use activitypub_federation::{
|
||||
config::Data,
|
||||
fetch::object_id::ObjectId,
|
||||
protocol::{public_key::PublicKey, verification::verify_domains_match},
|
||||
traits::{Actor, Object},
|
||||
};
|
||||
use chrono::{DateTime, Local, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Debug;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApubUser {
|
||||
#[serde(rename = "type")]
|
||||
kind: PersonType,
|
||||
id: ObjectId<DbPerson>,
|
||||
preferred_username: String,
|
||||
inbox: Url,
|
||||
public_key: PublicKey,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Object for DbPerson {
|
||||
type DataType = MyDataHandle;
|
||||
type Kind = ApubUser;
|
||||
type Error = Error;
|
||||
|
||||
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
|
||||
Some(self.last_refreshed_at)
|
||||
}
|
||||
|
||||
async fn read_from_id(
|
||||
object_id: Url,
|
||||
data: &Data<Self::DataType>,
|
||||
) -> Result<Option<Self>, Self::Error> {
|
||||
Ok(DbPerson::read_from_ap_id(&object_id.into(), data).ok())
|
||||
}
|
||||
|
||||
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
|
||||
Ok(ApubUser {
|
||||
kind: Default::default(),
|
||||
id: self.ap_id.clone(),
|
||||
preferred_username: self.username.clone(),
|
||||
inbox: Url::parse(&self.inbox_url)?,
|
||||
public_key: self.public_key(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn verify(
|
||||
json: &Self::Kind,
|
||||
expected_domain: &Url,
|
||||
_data: &Data<Self::DataType>,
|
||||
) -> Result<(), Self::Error> {
|
||||
verify_domains_match(json.id.inner(), expected_domain)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> {
|
||||
let form = DbPersonForm {
|
||||
username: json.preferred_username,
|
||||
ap_id: json.id,
|
||||
inbox_url: json.inbox.to_string(),
|
||||
public_key: json.public_key.public_key_pem,
|
||||
private_key: None,
|
||||
last_refreshed_at: Local::now().into(),
|
||||
local: false,
|
||||
};
|
||||
DbPerson::create(&form, &data.db_connection)
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for DbPerson {
|
||||
fn id(&self) -> Url {
|
||||
self.ap_id.inner().clone()
|
||||
}
|
||||
|
||||
fn public_key_pem(&self) -> &str {
|
||||
&self.public_key
|
||||
}
|
||||
|
||||
fn private_key_pem(&self) -> Option<String> {
|
||||
self.private_key.clone()
|
||||
}
|
||||
|
||||
fn inbox(&self) -> Url {
|
||||
Url::parse(&self.inbox_url).unwrap()
|
||||
}
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
use crate::database::article::DbArticle;
|
||||
use crate::database::instance::DbInstance;
|
||||
use crate::database::user::DbPerson;
|
||||
use crate::database::MyDataHandle;
|
||||
use crate::error::MyResult;
|
||||
use crate::error::{Error, MyResult};
|
||||
use crate::federation::activities::accept::Accept;
|
||||
use crate::federation::activities::create_article::CreateArticle;
|
||||
use crate::federation::activities::follow::Follow;
|
||||
|
@ -12,10 +13,12 @@ use crate::federation::objects::article::ApubArticle;
|
|||
use crate::federation::objects::articles_collection::{ArticleCollection, DbArticleCollection};
|
||||
use crate::federation::objects::edits_collection::{ApubEditCollection, DbEditCollection};
|
||||
use crate::federation::objects::instance::ApubInstance;
|
||||
use crate::federation::objects::user::ApubUser;
|
||||
use activitypub_federation::axum::inbox::{receive_activity, ActivityData};
|
||||
use activitypub_federation::axum::json::FederationJson;
|
||||
use activitypub_federation::config::Data;
|
||||
use activitypub_federation::protocol::context::WithContext;
|
||||
use activitypub_federation::traits::Actor;
|
||||
use activitypub_federation::traits::Object;
|
||||
use activitypub_federation::traits::{ActivityHandler, Collection};
|
||||
use axum::extract::Path;
|
||||
|
@ -23,12 +26,14 @@ use axum::response::IntoResponse;
|
|||
use axum::routing::{get, post};
|
||||
use axum::Router;
|
||||
use axum_macros::debug_handler;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
pub fn federation_routes() -> Router {
|
||||
Router::new()
|
||||
.route("/", get(http_get_instance))
|
||||
.route("/user/:name", get(http_get_person))
|
||||
.route("/all_articles", get(http_get_all_articles))
|
||||
.route("/article/:title", get(http_get_article))
|
||||
.route("/article/:title/edits", get(http_get_article_edits))
|
||||
|
@ -44,6 +49,16 @@ async fn http_get_instance(
|
|||
Ok(FederationJson(WithContext::new_default(json_instance)))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn http_get_person(
|
||||
Path(name): Path<String>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<FederationJson<WithContext<ApubUser>>> {
|
||||
let person = DbPerson::read_local_from_name(&name, &data)?.person;
|
||||
let json_person = person.into_json(&data).await?;
|
||||
Ok(FederationJson(WithContext::new_default(json_person)))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn http_get_all_articles(
|
||||
data: Data<MyDataHandle>,
|
||||
|
@ -91,6 +106,119 @@ pub async fn http_post_inbox(
|
|||
data: Data<MyDataHandle>,
|
||||
activity_data: ActivityData,
|
||||
) -> impl IntoResponse {
|
||||
receive_activity::<WithContext<InboxActivities>, DbInstance, MyDataHandle>(activity_data, &data)
|
||||
.await
|
||||
receive_activity::<WithContext<InboxActivities>, UserOrInstance, MyDataHandle>(
|
||||
activity_data,
|
||||
&data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum UserOrInstance {
|
||||
User(DbPerson),
|
||||
Instance(DbInstance),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum PersonOrInstance {
|
||||
Person(ApubUser),
|
||||
Instance(ApubInstance),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum PersonOrInstanceType {
|
||||
Person,
|
||||
Group,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Object for UserOrInstance {
|
||||
type DataType = MyDataHandle;
|
||||
type Kind = PersonOrInstance;
|
||||
type Error = Error;
|
||||
|
||||
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
|
||||
Some(match self {
|
||||
UserOrInstance::User(p) => p.last_refreshed_at,
|
||||
UserOrInstance::Instance(p) => p.last_refreshed_at,
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn read_from_id(
|
||||
object_id: Url,
|
||||
data: &Data<Self::DataType>,
|
||||
) -> Result<Option<Self>, Error> {
|
||||
let person = DbPerson::read_from_id(object_id.clone(), data).await?;
|
||||
Ok(match person {
|
||||
Some(o) => Some(UserOrInstance::User(o)),
|
||||
None => DbInstance::read_from_id(object_id, data)
|
||||
.await?
|
||||
.map(UserOrInstance::Instance),
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn delete(self, data: &Data<Self::DataType>) -> Result<(), Error> {
|
||||
match self {
|
||||
UserOrInstance::User(p) => p.delete(data).await,
|
||||
UserOrInstance::Instance(p) => p.delete(data).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn verify(
|
||||
apub: &Self::Kind,
|
||||
expected_domain: &Url,
|
||||
data: &Data<Self::DataType>,
|
||||
) -> Result<(), Error> {
|
||||
match apub {
|
||||
PersonOrInstance::Person(a) => DbPerson::verify(a, expected_domain, data).await,
|
||||
PersonOrInstance::Instance(a) => DbInstance::verify(a, expected_domain, data).await,
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn from_json(apub: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Error> {
|
||||
Ok(match apub {
|
||||
PersonOrInstance::Person(p) => {
|
||||
UserOrInstance::User(DbPerson::from_json(p, data).await?)
|
||||
}
|
||||
PersonOrInstance::Instance(p) => {
|
||||
UserOrInstance::Instance(DbInstance::from_json(p, data).await?)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for UserOrInstance {
|
||||
fn id(&self) -> Url {
|
||||
match self {
|
||||
UserOrInstance::User(u) => u.id(),
|
||||
UserOrInstance::Instance(c) => c.id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn public_key_pem(&self) -> &str {
|
||||
match self {
|
||||
UserOrInstance::User(p) => p.public_key_pem(),
|
||||
UserOrInstance::Instance(p) => p.public_key_pem(),
|
||||
}
|
||||
}
|
||||
|
||||
fn private_key_pem(&self) -> Option<String> {
|
||||
match self {
|
||||
UserOrInstance::User(p) => p.private_key_pem(),
|
||||
UserOrInstance::Instance(p) => p.private_key_pem(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inbox(&self) -> Url {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
use crate::api::api_routes;
|
||||
use crate::database::instance::{DbInstance, DbInstanceForm};
|
||||
use crate::database::MyData;
|
||||
use crate::error::MyResult;
|
||||
|
@ -8,6 +7,7 @@ use activitypub_federation::config::{FederationConfig, FederationMiddleware};
|
|||
use activitypub_federation::fetch::collection_id::CollectionId;
|
||||
use activitypub_federation::fetch::object_id::ObjectId;
|
||||
use activitypub_federation::http_signatures::generate_actor_keypair;
|
||||
use api::api_routes;
|
||||
use axum::{Router, Server};
|
||||
use chrono::Local;
|
||||
use diesel::Connection;
|
||||
|
|
128
tests/common.rs
128
tests/common.rs
|
@ -1,7 +1,9 @@
|
|||
use anyhow::anyhow;
|
||||
use fediwiki::api::{
|
||||
CreateArticleData, EditArticleData, FollowInstance, GetArticleData, ResolveObject,
|
||||
};
|
||||
use fediwiki::api::article::{CreateArticleData, EditArticleData, ForkArticleData, GetArticleData};
|
||||
use fediwiki::api::instance::FollowInstance;
|
||||
use fediwiki::api::user::RegisterUserData;
|
||||
use fediwiki::api::user::{LoginResponse, LoginUserData};
|
||||
use fediwiki::api::ResolveObject;
|
||||
use fediwiki::database::article::ArticleView;
|
||||
use fediwiki::database::conflict::ApiConflict;
|
||||
use fediwiki::database::instance::DbInstance;
|
||||
|
@ -12,6 +14,7 @@ use reqwest::{Client, RequestBuilder, StatusCode};
|
|||
use serde::de::Deserialize;
|
||||
use serde::ser::Serialize;
|
||||
use std::env::current_dir;
|
||||
use std::fs::create_dir_all;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use std::sync::Once;
|
||||
|
@ -30,7 +33,7 @@ pub struct TestData {
|
|||
}
|
||||
|
||||
impl TestData {
|
||||
pub fn start() -> Self {
|
||||
pub async fn start() -> Self {
|
||||
static INIT: Once = Once::new();
|
||||
INIT.call_once(|| {
|
||||
env_logger::builder()
|
||||
|
@ -66,9 +69,9 @@ impl TestData {
|
|||
}
|
||||
|
||||
Self {
|
||||
alpha: FediwikiInstance::start(alpha_db_path, port_alpha),
|
||||
beta: FediwikiInstance::start(beta_db_path, port_beta),
|
||||
gamma: FediwikiInstance::start(gamma_db_path, port_gamma),
|
||||
alpha: FediwikiInstance::start(alpha_db_path, port_alpha, "alpha").await,
|
||||
beta: FediwikiInstance::start(beta_db_path, port_beta, "beta").await,
|
||||
gamma: FediwikiInstance::start(gamma_db_path, port_gamma, "gamma").await,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,14 +85,17 @@ impl TestData {
|
|||
|
||||
/// Generate a unique db path for each postgres so that tests can run in parallel.
|
||||
fn generate_db_path(name: &'static str, port: i32) -> String {
|
||||
format!(
|
||||
let path = format!(
|
||||
"{}/target/test_db/{name}-{port}",
|
||||
current_dir().unwrap().display()
|
||||
)
|
||||
);
|
||||
create_dir_all(&path).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
pub struct FediwikiInstance {
|
||||
pub hostname: String,
|
||||
pub jwt: String,
|
||||
db_path: String,
|
||||
db_handle: JoinHandle<()>,
|
||||
}
|
||||
|
@ -106,16 +112,21 @@ impl FediwikiInstance {
|
|||
})
|
||||
}
|
||||
|
||||
fn start(db_path: String, port: i32) -> Self {
|
||||
async fn start(db_path: String, port: i32, username: &str) -> Self {
|
||||
let db_url = format!("postgresql://lemmy:password@/lemmy?host={db_path}");
|
||||
let hostname = format!("localhost:{port}");
|
||||
let hostname_ = hostname.clone();
|
||||
let handle = tokio::task::spawn(async move {
|
||||
start(&hostname_, &db_url).await.unwrap();
|
||||
});
|
||||
// wait a moment for the server to start
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
let register_res = register(&hostname, username, "hunter2").await.unwrap();
|
||||
assert!(!register_res.jwt.is_empty());
|
||||
Self {
|
||||
db_path,
|
||||
jwt: register_res.jwt,
|
||||
hostname,
|
||||
db_path,
|
||||
db_handle: handle,
|
||||
}
|
||||
}
|
||||
|
@ -135,11 +146,16 @@ impl FediwikiInstance {
|
|||
|
||||
pub const TEST_ARTICLE_DEFAULT_TEXT: &str = "some\nexample\ntext\n";
|
||||
|
||||
pub async fn create_article(hostname: &str, title: String) -> MyResult<ArticleView> {
|
||||
pub async fn create_article(instance: &FediwikiInstance, title: String) -> MyResult<ArticleView> {
|
||||
let create_form = CreateArticleData {
|
||||
title: title.clone(),
|
||||
};
|
||||
let article: ArticleView = post(hostname, "article", &create_form).await?;
|
||||
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,
|
||||
|
@ -147,7 +163,7 @@ pub async fn create_article(hostname: &str, title: String) -> MyResult<ArticleVi
|
|||
previous_version_id: article.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
edit_article(hostname, &edit_form).await
|
||||
edit_article(&instance, &edit_form).await
|
||||
}
|
||||
|
||||
pub async fn get_article(hostname: &str, article_id: i32) -> MyResult<ArticleView> {
|
||||
|
@ -156,19 +172,23 @@ pub async fn get_article(hostname: &str, article_id: i32) -> MyResult<ArticleVie
|
|||
}
|
||||
|
||||
pub async fn edit_article_with_conflict(
|
||||
hostname: &str,
|
||||
instance: &FediwikiInstance,
|
||||
edit_form: &EditArticleData,
|
||||
) -> MyResult<Option<ApiConflict>> {
|
||||
let req = CLIENT
|
||||
.patch(format!("http://{}/api/v1/article", hostname))
|
||||
.form(edit_form);
|
||||
.patch(format!("http://{}/api/v1/article", instance.hostname))
|
||||
.form(edit_form)
|
||||
.bearer_auth(&instance.jwt);
|
||||
handle_json_res(req).await
|
||||
}
|
||||
|
||||
pub async fn edit_article(hostname: &str, edit_form: &EditArticleData) -> MyResult<ArticleView> {
|
||||
let edit_res = edit_article_with_conflict(hostname, edit_form).await?;
|
||||
pub async fn edit_article(
|
||||
instance: &FediwikiInstance,
|
||||
edit_form: &EditArticleData,
|
||||
) -> MyResult<ArticleView> {
|
||||
let edit_res = edit_article_with_conflict(instance, edit_form).await?;
|
||||
assert!(edit_res.is_none());
|
||||
get_article(hostname, edit_form.article_id).await
|
||||
get_article(&instance.hostname, edit_form.article_id).await
|
||||
}
|
||||
|
||||
pub async fn get<T>(hostname: &str, endpoint: &str) -> MyResult<T>
|
||||
|
@ -190,46 +210,82 @@ where
|
|||
handle_json_res(req).await
|
||||
}
|
||||
|
||||
pub async fn post<T: Serialize, R>(hostname: &str, endpoint: &str, form: &T) -> MyResult<R>
|
||||
where
|
||||
R: for<'de> Deserialize<'de>,
|
||||
{
|
||||
pub async fn fork_article(
|
||||
instance: &FediwikiInstance,
|
||||
form: &ForkArticleData,
|
||||
) -> MyResult<ArticleView> {
|
||||
let req = CLIENT
|
||||
.post(format!("http://{}/api/v1/{}", hostname, endpoint))
|
||||
.form(form);
|
||||
.post(format!("http://{}/api/v1/article/fork", instance.hostname))
|
||||
.form(form)
|
||||
.bearer_auth(&instance.jwt);
|
||||
handle_json_res(req).await
|
||||
}
|
||||
|
||||
async fn handle_json_res<T>(req: RequestBuilder) -> MyResult<T>
|
||||
pub async fn handle_json_res<T>(req: RequestBuilder) -> MyResult<T>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let res = req.send().await?;
|
||||
if res.status() == StatusCode::OK {
|
||||
Ok(res.json().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 {
|
||||
let text = res.text().await?;
|
||||
Err(anyhow!("Post API response {text}").into())
|
||||
Err(anyhow!("API error: {text}").into())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn follow_instance(api_instance: &str, follow_instance: &str) -> MyResult<()> {
|
||||
pub async fn follow_instance(instance: &FediwikiInstance, 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(api_instance, "resolve_instance", Some(resolve_form)).await?;
|
||||
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
|
||||
CLIENT
|
||||
.post(format!("http://{}/api/v1/instance/follow", api_instance))
|
||||
let res = CLIENT
|
||||
.post(format!(
|
||||
"http://{}/api/v1/instance/follow",
|
||||
instance.hostname
|
||||
))
|
||||
.form(&follow_form)
|
||||
.bearer_auth(&instance.jwt)
|
||||
.send()
|
||||
.await?;
|
||||
Ok(())
|
||||
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(®ister_form);
|
||||
handle_json_res(req).await
|
||||
}
|
||||
|
||||
pub async fn login(
|
||||
instance: &FediwikiInstance,
|
||||
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
|
||||
}
|
||||
|
|
120
tests/test.rs
120
tests/test.rs
|
@ -2,27 +2,29 @@ extern crate fediwiki;
|
|||
|
||||
mod common;
|
||||
|
||||
use crate::common::register;
|
||||
use crate::common::{
|
||||
create_article, edit_article, edit_article_with_conflict, follow_instance, get_article,
|
||||
get_query, post, TestData, TEST_ARTICLE_DEFAULT_TEXT,
|
||||
get_query, TestData, CLIENT, TEST_ARTICLE_DEFAULT_TEXT,
|
||||
};
|
||||
use crate::common::{fork_article, handle_json_res, login};
|
||||
use common::get;
|
||||
use fediwiki::api::{EditArticleData, ForkArticleData, ResolveObject, SearchArticleData};
|
||||
use fediwiki::api::article::{CreateArticleData, EditArticleData, ForkArticleData};
|
||||
use fediwiki::api::{ResolveObject, SearchArticleData};
|
||||
use fediwiki::database::article::{ArticleView, DbArticle};
|
||||
use fediwiki::error::MyResult;
|
||||
|
||||
use fediwiki::database::conflict::ApiConflict;
|
||||
use fediwiki::database::instance::{DbInstance, InstanceView};
|
||||
use fediwiki::error::MyResult;
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
use url::Url;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_read_and_edit_article() -> MyResult<()> {
|
||||
let data = TestData::start();
|
||||
let data = TestData::start().await;
|
||||
|
||||
// create article
|
||||
let title = "Manu_Chao".to_string();
|
||||
let create_res = create_article(&data.alpha.hostname, title.clone()).await?;
|
||||
let create_res = create_article(&data.alpha, title.clone()).await?;
|
||||
assert_eq!(title, create_res.article.title);
|
||||
assert!(create_res.article.local);
|
||||
|
||||
|
@ -43,7 +45,7 @@ async fn test_create_read_and_edit_article() -> MyResult<()> {
|
|||
previous_version_id: get_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
|
||||
let edit_res = edit_article(&data.alpha, &edit_form).await?;
|
||||
assert_eq!(edit_form.new_text, edit_res.article.text);
|
||||
assert_eq!(2, edit_res.edits.len());
|
||||
|
||||
|
@ -60,15 +62,15 @@ async fn test_create_read_and_edit_article() -> MyResult<()> {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_create_duplicate_article() -> MyResult<()> {
|
||||
let data = TestData::start();
|
||||
let data = TestData::start().await;
|
||||
|
||||
// create article
|
||||
let title = "Manu_Chao".to_string();
|
||||
let create_res = create_article(&data.alpha.hostname, title.clone()).await?;
|
||||
let create_res = create_article(&data.alpha, title.clone()).await?;
|
||||
assert_eq!(title, create_res.article.title);
|
||||
assert!(create_res.article.local);
|
||||
|
||||
let create_res = create_article(&data.alpha.hostname, title.clone()).await;
|
||||
let create_res = create_article(&data.alpha, title.clone()).await;
|
||||
assert!(create_res.is_err());
|
||||
|
||||
data.stop()
|
||||
|
@ -76,7 +78,7 @@ async fn test_create_duplicate_article() -> MyResult<()> {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_follow_instance() -> MyResult<()> {
|
||||
let data = TestData::start();
|
||||
let data = TestData::start().await;
|
||||
|
||||
// check initial state
|
||||
let alpha_instance: InstanceView = get(&data.alpha.hostname, "instance").await?;
|
||||
|
@ -86,7 +88,7 @@ async fn test_follow_instance() -> MyResult<()> {
|
|||
assert_eq!(0, beta_instance.followers.len());
|
||||
assert_eq!(0, beta_instance.following.len());
|
||||
|
||||
follow_instance(&data.alpha.hostname, &data.beta.hostname).await?;
|
||||
follow_instance(&data.alpha, &data.beta.hostname).await?;
|
||||
|
||||
// check that follow was federated
|
||||
let alpha_instance: InstanceView = get(&data.alpha.hostname, "instance").await?;
|
||||
|
@ -100,9 +102,10 @@ async fn test_follow_instance() -> MyResult<()> {
|
|||
let beta_instance: InstanceView = get(&data.beta.hostname, "instance").await?;
|
||||
assert_eq!(0, beta_instance.following.len());
|
||||
assert_eq!(1, beta_instance.followers.len());
|
||||
// TODO: compare full ap_id of alpha user, but its not available through api yet
|
||||
assert_eq!(
|
||||
alpha_instance.instance.ap_id,
|
||||
beta_instance.followers[0].ap_id
|
||||
alpha_instance.instance.ap_id.inner().domain(),
|
||||
beta_instance.followers[0].ap_id.inner().domain()
|
||||
);
|
||||
|
||||
data.stop()
|
||||
|
@ -110,11 +113,11 @@ async fn test_follow_instance() -> MyResult<()> {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_synchronize_articles() -> MyResult<()> {
|
||||
let data = TestData::start();
|
||||
let data = TestData::start().await;
|
||||
|
||||
// create article on alpha
|
||||
let title = "Manu_Chao".to_string();
|
||||
let create_res = create_article(&data.alpha.hostname, title.clone()).await?;
|
||||
let create_res = create_article(&data.alpha, title.clone()).await?;
|
||||
assert_eq!(title, create_res.article.title);
|
||||
assert_eq!(1, create_res.edits.len());
|
||||
assert!(create_res.article.local);
|
||||
|
@ -126,7 +129,7 @@ async fn test_synchronize_articles() -> MyResult<()> {
|
|||
previous_version_id: create_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
edit_article(&data.alpha.hostname, &edit_form).await?;
|
||||
edit_article(&data.alpha, &edit_form).await?;
|
||||
|
||||
// article is not yet on beta
|
||||
let get_res = get_article(&data.beta.hostname, create_res.article.id).await;
|
||||
|
@ -156,13 +159,13 @@ async fn test_synchronize_articles() -> MyResult<()> {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_edit_local_article() -> MyResult<()> {
|
||||
let data = TestData::start();
|
||||
let data = TestData::start().await;
|
||||
|
||||
follow_instance(&data.alpha.hostname, &data.beta.hostname).await?;
|
||||
follow_instance(&data.alpha, &data.beta.hostname).await?;
|
||||
|
||||
// create new article
|
||||
let title = "Manu_Chao".to_string();
|
||||
let create_res = create_article(&data.beta.hostname, title.clone()).await?;
|
||||
let create_res = create_article(&data.beta, title.clone()).await?;
|
||||
assert_eq!(title, create_res.article.title);
|
||||
assert!(create_res.article.local);
|
||||
|
||||
|
@ -180,7 +183,7 @@ async fn test_edit_local_article() -> MyResult<()> {
|
|||
previous_version_id: get_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
let edit_res = edit_article(&data.beta.hostname, &edit_form).await?;
|
||||
let edit_res = edit_article(&data.beta, &edit_form).await?;
|
||||
assert_eq!(edit_res.article.text, edit_form.new_text);
|
||||
assert_eq!(edit_res.edits.len(), 2);
|
||||
assert!(edit_res.edits[0]
|
||||
|
@ -199,14 +202,14 @@ async fn test_edit_local_article() -> MyResult<()> {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_edit_remote_article() -> MyResult<()> {
|
||||
let data = TestData::start();
|
||||
let data = TestData::start().await;
|
||||
|
||||
follow_instance(&data.alpha.hostname, &data.beta.hostname).await?;
|
||||
follow_instance(&data.gamma.hostname, &data.beta.hostname).await?;
|
||||
follow_instance(&data.alpha, &data.beta.hostname).await?;
|
||||
follow_instance(&data.gamma, &data.beta.hostname).await?;
|
||||
|
||||
// create new article
|
||||
let title = "Manu_Chao".to_string();
|
||||
let create_res = create_article(&data.beta.hostname, title.clone()).await?;
|
||||
let create_res = create_article(&data.beta, title.clone()).await?;
|
||||
assert_eq!(title, create_res.article.title);
|
||||
assert!(create_res.article.local);
|
||||
|
||||
|
@ -226,7 +229,7 @@ async fn test_edit_remote_article() -> MyResult<()> {
|
|||
previous_version_id: get_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
|
||||
let edit_res = edit_article(&data.alpha, &edit_form).await?;
|
||||
assert_eq!(edit_form.new_text, edit_res.article.text);
|
||||
assert_eq!(2, edit_res.edits.len());
|
||||
assert!(!edit_res.article.local);
|
||||
|
@ -251,11 +254,11 @@ async fn test_edit_remote_article() -> MyResult<()> {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_local_edit_conflict() -> MyResult<()> {
|
||||
let data = TestData::start();
|
||||
let data = TestData::start().await;
|
||||
|
||||
// create new article
|
||||
let title = "Manu_Chao".to_string();
|
||||
let create_res = create_article(&data.alpha.hostname, title.clone()).await?;
|
||||
let create_res = create_article(&data.alpha, title.clone()).await?;
|
||||
assert_eq!(title, create_res.article.title);
|
||||
assert!(create_res.article.local);
|
||||
|
||||
|
@ -266,7 +269,7 @@ async fn test_local_edit_conflict() -> MyResult<()> {
|
|||
previous_version_id: create_res.latest_version.clone(),
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
|
||||
let edit_res = edit_article(&data.alpha, &edit_form).await?;
|
||||
assert_eq!(edit_res.article.text, edit_form.new_text);
|
||||
assert_eq!(2, edit_res.edits.len());
|
||||
|
||||
|
@ -277,7 +280,7 @@ async fn test_local_edit_conflict() -> MyResult<()> {
|
|||
previous_version_id: create_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
let edit_res = edit_article_with_conflict(&data.alpha.hostname, &edit_form)
|
||||
let edit_res = edit_article_with_conflict(&data.alpha, &edit_form)
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!("<<<<<<< ours\nIpsum Lorem\n||||||| original\nsome\nexample\ntext\n=======\nLorem Ipsum\n>>>>>>> theirs\n", edit_res.three_way_merge);
|
||||
|
@ -293,7 +296,7 @@ async fn test_local_edit_conflict() -> MyResult<()> {
|
|||
previous_version_id: edit_res.previous_version_id,
|
||||
resolve_conflict_id: Some(edit_res.id),
|
||||
};
|
||||
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
|
||||
let edit_res = edit_article(&data.alpha, &edit_form).await?;
|
||||
assert_eq!(edit_form.new_text, edit_res.article.text);
|
||||
|
||||
let conflicts: Vec<ApiConflict> =
|
||||
|
@ -305,13 +308,13 @@ async fn test_local_edit_conflict() -> MyResult<()> {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_federated_edit_conflict() -> MyResult<()> {
|
||||
let data = TestData::start();
|
||||
let data = TestData::start().await;
|
||||
|
||||
follow_instance(&data.alpha.hostname, &data.beta.hostname).await?;
|
||||
follow_instance(&data.alpha, &data.beta.hostname).await?;
|
||||
|
||||
// create new article
|
||||
let title = "Manu_Chao".to_string();
|
||||
let create_res = create_article(&data.beta.hostname, title.clone()).await?;
|
||||
let create_res = create_article(&data.beta, title.clone()).await?;
|
||||
assert_eq!(title, create_res.article.title);
|
||||
assert!(create_res.article.local);
|
||||
|
||||
|
@ -334,7 +337,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
|
|||
previous_version_id: create_res.latest_version.clone(),
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
|
||||
let edit_res = edit_article(&data.alpha, &edit_form).await?;
|
||||
assert_eq!(edit_res.article.text, edit_form.new_text);
|
||||
assert_eq!(2, edit_res.edits.len());
|
||||
assert!(!edit_res.article.local);
|
||||
|
@ -351,7 +354,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
|
|||
previous_version_id: create_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
let edit_res = edit_article(&data.gamma.hostname, &edit_form).await?;
|
||||
let edit_res = edit_article(&data.gamma, &edit_form).await?;
|
||||
assert_ne!(edit_form.new_text, edit_res.article.text);
|
||||
assert_eq!(1, edit_res.edits.len());
|
||||
assert!(!edit_res.article.local);
|
||||
|
@ -367,7 +370,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
|
|||
previous_version_id: conflicts[0].previous_version_id.clone(),
|
||||
resolve_conflict_id: Some(conflicts[0].id.clone()),
|
||||
};
|
||||
let edit_res = edit_article(&data.gamma.hostname, &edit_form).await?;
|
||||
let edit_res = edit_article(&data.gamma, &edit_form).await?;
|
||||
assert_eq!(edit_form.new_text, edit_res.article.text);
|
||||
assert_eq!(3, edit_res.edits.len());
|
||||
|
||||
|
@ -380,11 +383,11 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
|
||||
let data = TestData::start();
|
||||
let data = TestData::start().await;
|
||||
|
||||
// create new article
|
||||
let title = "Manu_Chao".to_string();
|
||||
let create_res = create_article(&data.alpha.hostname, title.clone()).await?;
|
||||
let create_res = create_article(&data.alpha, title.clone()).await?;
|
||||
assert_eq!(title, create_res.article.title);
|
||||
assert!(create_res.article.local);
|
||||
|
||||
|
@ -395,7 +398,7 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
|
|||
previous_version_id: create_res.latest_version.clone(),
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
|
||||
let edit_res = edit_article(&data.alpha, &edit_form).await?;
|
||||
assert_eq!(edit_res.article.text, edit_form.new_text);
|
||||
assert_eq!(2, edit_res.edits.len());
|
||||
|
||||
|
@ -406,7 +409,7 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
|
|||
previous_version_id: create_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
|
||||
let edit_res = edit_article(&data.alpha, &edit_form).await?;
|
||||
let conflicts: Vec<ApiConflict> =
|
||||
get_query(&data.alpha.hostname, "edit_conflicts", None::<()>).await?;
|
||||
assert_eq!(0, conflicts.len());
|
||||
|
@ -418,11 +421,11 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_fork_article() -> MyResult<()> {
|
||||
let data = TestData::start();
|
||||
let data = TestData::start().await;
|
||||
|
||||
// create article
|
||||
let title = "Manu_Chao".to_string();
|
||||
let create_res = create_article(&data.alpha.hostname, title.clone()).await?;
|
||||
let create_res = create_article(&data.alpha, title.clone()).await?;
|
||||
assert_eq!(title, create_res.article.title);
|
||||
assert!(create_res.article.local);
|
||||
|
||||
|
@ -439,7 +442,7 @@ async fn test_fork_article() -> MyResult<()> {
|
|||
let fork_form = ForkArticleData {
|
||||
article_id: resolved_article.id,
|
||||
};
|
||||
let fork_res: ArticleView = post(&data.beta.hostname, "article/fork", &fork_form).await?;
|
||||
let fork_res = fork_article(&data.beta, &fork_form).await?;
|
||||
let forked_article = fork_res.article;
|
||||
assert_eq!(resolved_article.title, forked_article.title);
|
||||
assert_eq!(resolved_article.text, forked_article.text);
|
||||
|
@ -464,3 +467,32 @@ async fn test_fork_article() -> MyResult<()> {
|
|||
|
||||
data.stop()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_user_registration_login() -> MyResult<()> {
|
||||
let data = TestData::start().await;
|
||||
let username = "my_user";
|
||||
let password = "hunter2";
|
||||
let register = register(&data.alpha.hostname, username, password).await?;
|
||||
assert!(!register.jwt.is_empty());
|
||||
|
||||
let invalid_login = login(&data.alpha, username, "asd123").await;
|
||||
assert!(invalid_login.is_err());
|
||||
|
||||
let valid_login = login(&data.alpha, username, password).await?;
|
||||
assert!(!valid_login.jwt.is_empty());
|
||||
|
||||
let title = "Manu_Chao".to_string();
|
||||
let create_form = CreateArticleData {
|
||||
title: title.clone(),
|
||||
};
|
||||
|
||||
let req = CLIENT
|
||||
.post(format!("http://{}/api/v1/article", &data.alpha.hostname))
|
||||
.form(&create_form)
|
||||
.bearer_auth(valid_login.jwt);
|
||||
let create_res: ArticleView = handle_json_res(req).await?;
|
||||
assert_eq!(title, create_res.article.title);
|
||||
|
||||
data.stop()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue