Merge branch 'master' into ci-release

This commit is contained in:
Felix Ableitner 2024-03-06 16:49:40 +01:00
commit f7c5d0d28c
57 changed files with 910 additions and 522 deletions

4
.rustfmt.toml Normal file
View File

@ -0,0 +1,4 @@
edition = "2021"
imports_layout = "HorizontalVertical"
imports_granularity = "Crate"
group_imports = "One"

View File

@ -4,13 +4,14 @@ variables:
steps: steps:
cargo_fmt: cargo_fmt:
image: *rust_image image: rustlang/rust:nightly
environment: environment:
# store cargo data in repo folder so that it gets cached between steps # store cargo data in repo folder so that it gets cached between steps
CARGO_HOME: .cargo_home CARGO_HOME: .cargo_home
commands: commands:
- rustup component add rustfmt - rustup component add rustfmt
- cargo fmt -- --check - cargo +nightly fmt -- --check
check_config_defaults_updated: check_config_defaults_updated:
image: *rust_image image: *rust_image

View File

@ -43,7 +43,8 @@ create table article (
text text not null, text text not null,
ap_id varchar(255) not null unique, ap_id varchar(255) not null unique,
instance_id int REFERENCES instance ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, instance_id int REFERENCES instance ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
local bool not null local bool not null,
protected bool not null
); );
create table edit ( create table edit (

View File

@ -1,27 +1,38 @@
use crate::backend::database::article::DbArticleForm; use crate::{
use crate::backend::database::conflict::{DbConflict, DbConflictForm}; backend::{
use crate::backend::database::edit::DbEditForm; database::{
use crate::backend::database::IbisData; article::DbArticleForm,
use crate::backend::error::MyResult; conflict::{DbConflict, DbConflictForm},
use crate::backend::federation::activities::create_article::CreateArticle; edit::DbEditForm,
use crate::backend::federation::activities::submit_article_update; IbisData,
use crate::backend::utils::generate_article_version; },
use crate::common::utils::extract_domain; error::MyResult,
use crate::common::utils::http_protocol_str; federation::activities::{create_article::CreateArticle, submit_article_update},
use crate::common::validation::can_edit_article; utils::generate_article_version,
use crate::common::LocalUserView; },
use crate::common::{ApiConflict, ResolveObject}; common::{
use crate::common::{ArticleView, DbArticle, DbEdit}; utils::{extract_domain, http_protocol_str},
use crate::common::{CreateArticleData, EditArticleData, EditVersion, ForkArticleData}; validation::can_edit_article,
use crate::common::{DbInstance, SearchArticleData}; ApiConflict,
use crate::common::{GetArticleData, ListArticlesData}; ArticleView,
use activitypub_federation::config::Data; CreateArticleForm,
use activitypub_federation::fetch::object_id::ObjectId; DbArticle,
DbEdit,
DbInstance,
EditArticleForm,
EditVersion,
ForkArticleForm,
GetArticleForm,
ListArticlesForm,
LocalUserView,
ProtectArticleForm,
ResolveObject,
SearchArticleForm,
},
};
use activitypub_federation::{config::Data, fetch::object_id::ObjectId};
use anyhow::anyhow; use anyhow::anyhow;
use axum::extract::Query; use axum::{extract::Query, Extension, Form, Json};
use axum::Extension;
use axum::Form;
use axum::Json;
use axum_macros::debug_handler; use axum_macros::debug_handler;
use chrono::Utc; use chrono::Utc;
use diffy::create_patch; use diffy::create_patch;
@ -31,7 +42,7 @@ use diffy::create_patch;
pub(in crate::backend::api) async fn create_article( pub(in crate::backend::api) async fn create_article(
Extension(user): Extension<LocalUserView>, Extension(user): Extension<LocalUserView>,
data: Data<IbisData>, data: Data<IbisData>,
Form(create_article): Form<CreateArticleData>, Form(create_article): Form<CreateArticleForm>,
) -> MyResult<Json<ArticleView>> { ) -> MyResult<Json<ArticleView>> {
if create_article.title.is_empty() { if create_article.title.is_empty() {
return Err(anyhow!("Title must not be empty").into()); return Err(anyhow!("Title must not be empty").into());
@ -50,10 +61,11 @@ pub(in crate::backend::api) async fn create_article(
ap_id, ap_id,
instance_id: local_instance.id, instance_id: local_instance.id,
local: true, local: true,
protected: false,
}; };
let article = DbArticle::create(form, &data)?; let article = DbArticle::create(form, &data)?;
let edit_data = EditArticleData { let edit_data = EditArticleForm {
article_id: article.id, article_id: article.id,
new_text: create_article.text, new_text: create_article.text,
summary: create_article.summary, summary: create_article.summary,
@ -81,7 +93,7 @@ pub(in crate::backend::api) async fn create_article(
pub(in crate::backend::api) async fn edit_article( pub(in crate::backend::api) async fn edit_article(
Extension(user): Extension<LocalUserView>, Extension(user): Extension<LocalUserView>,
data: Data<IbisData>, data: Data<IbisData>,
Form(mut edit_form): Form<EditArticleData>, Form(mut edit_form): Form<EditArticleForm>,
) -> MyResult<Json<Option<ApiConflict>>> { ) -> MyResult<Json<Option<ApiConflict>>> {
// resolve conflict if any // resolve conflict if any
if let Some(resolve_conflict_id) = edit_form.resolve_conflict_id { if let Some(resolve_conflict_id) = edit_form.resolve_conflict_id {
@ -136,7 +148,7 @@ pub(in crate::backend::api) async fn edit_article(
/// Retrieve an article by ID. It must already be stored in the local database. /// Retrieve an article by ID. It must already be stored in the local database.
#[debug_handler] #[debug_handler]
pub(in crate::backend::api) async fn get_article( pub(in crate::backend::api) async fn get_article(
Query(query): Query<GetArticleData>, Query(query): Query<GetArticleForm>,
data: Data<IbisData>, data: Data<IbisData>,
) -> MyResult<Json<ArticleView>> { ) -> MyResult<Json<ArticleView>> {
match (query.title, query.id) { match (query.title, query.id) {
@ -157,7 +169,7 @@ pub(in crate::backend::api) async fn get_article(
#[debug_handler] #[debug_handler]
pub(in crate::backend::api) async fn list_articles( pub(in crate::backend::api) async fn list_articles(
Query(query): Query<ListArticlesData>, Query(query): Query<ListArticlesForm>,
data: Data<IbisData>, data: Data<IbisData>,
) -> MyResult<Json<Vec<DbArticle>>> { ) -> MyResult<Json<Vec<DbArticle>>> {
let only_local = query.only_local.unwrap_or(false); let only_local = query.only_local.unwrap_or(false);
@ -170,7 +182,7 @@ pub(in crate::backend::api) async fn list_articles(
pub(in crate::backend::api) async fn fork_article( pub(in crate::backend::api) async fn fork_article(
Extension(_user): Extension<LocalUserView>, Extension(_user): Extension<LocalUserView>,
data: Data<IbisData>, data: Data<IbisData>,
Form(fork_form): Form<ForkArticleData>, Form(fork_form): Form<ForkArticleForm>,
) -> MyResult<Json<ArticleView>> { ) -> MyResult<Json<ArticleView>> {
// TODO: lots of code duplicated from create_article(), can move it into helper // TODO: lots of code duplicated from create_article(), can move it into helper
let original_article = DbArticle::read(fork_form.article_id, &data)?; let original_article = DbArticle::read(fork_form.article_id, &data)?;
@ -188,6 +200,7 @@ pub(in crate::backend::api) async fn fork_article(
ap_id, ap_id,
instance_id: local_instance.id, instance_id: local_instance.id,
local: true, local: true,
protected: false,
}; };
let article = DbArticle::create(form, &data)?; let article = DbArticle::create(form, &data)?;
@ -242,7 +255,7 @@ pub(super) async fn resolve_article(
/// Search articles for matching title or body text. /// Search articles for matching title or body text.
#[debug_handler] #[debug_handler]
pub(super) async fn search_article( pub(super) async fn search_article(
Query(query): Query<SearchArticleData>, Query(query): Query<SearchArticleForm>,
data: Data<IbisData>, data: Data<IbisData>,
) -> MyResult<Json<Vec<DbArticle>>> { ) -> MyResult<Json<Vec<DbArticle>>> {
if query.query.is_empty() { if query.query.is_empty() {
@ -251,3 +264,17 @@ pub(super) async fn search_article(
let article = DbArticle::search(&query.query, &data)?; let article = DbArticle::search(&query.query, &data)?;
Ok(Json(article)) Ok(Json(article))
} }
#[debug_handler]
pub(in crate::backend::api) async fn protect_article(
Extension(user): Extension<LocalUserView>,
data: Data<IbisData>,
Form(lock_params): Form<ProtectArticleForm>,
) -> MyResult<Json<DbArticle>> {
if !user.local_user.admin {
return Err(anyhow!("Only admin can lock articles").into());
}
let article =
DbArticle::update_protected(lock_params.article_id, lock_params.protected, &data)?;
Ok(Json(article))
}

View File

@ -1,13 +1,9 @@
use crate::backend::database::IbisData; use crate::{
use crate::backend::error::MyResult; backend::{database::IbisData, error::MyResult, federation::activities::follow::Follow},
use crate::backend::federation::activities::follow::Follow; common::{DbInstance, FollowInstance, InstanceView, LocalUserView, ResolveObject},
use crate::common::{DbInstance, InstanceView, ResolveObject}; };
use crate::common::{FollowInstance, LocalUserView}; use activitypub_federation::{config::Data, fetch::object_id::ObjectId};
use activitypub_federation::config::Data; use axum::{extract::Query, Extension, Form, Json};
use activitypub_federation::fetch::object_id::ObjectId;
use axum::extract::Query;
use axum::Extension;
use axum::{Form, Json};
use axum_macros::debug_handler; use axum_macros::debug_handler;
/// Retrieve the local instance info. /// Retrieve the local instance info.

View File

@ -1,28 +1,42 @@
use crate::backend::api::article::{ use crate::{
create_article, list_articles, resolve_article, search_article, backend::{
api::{
article::{
create_article,
edit_article,
fork_article,
get_article,
list_articles,
protect_article,
resolve_article,
search_article,
},
instance::{follow_instance, get_local_instance, resolve_instance},
user::{
get_user,
login_user,
logout_user,
my_profile,
register_user,
validate,
AUTH_COOKIE,
},
},
database::{conflict::DbConflict, IbisData},
error::MyResult,
},
common::{ApiConflict, LocalUserView},
}; };
use crate::backend::api::article::{edit_article, fork_article, get_article};
use crate::backend::api::instance::get_local_instance;
use crate::backend::api::instance::{follow_instance, resolve_instance};
use crate::backend::api::user::validate;
use crate::backend::api::user::{get_user, register_user};
use crate::backend::api::user::{login_user, logout_user};
use crate::backend::api::user::{my_profile, AUTH_COOKIE};
use crate::backend::database::conflict::DbConflict;
use crate::backend::database::IbisData;
use crate::backend::error::MyResult;
use crate::common::ApiConflict;
use crate::common::LocalUserView;
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use axum::routing::{get, post};
use axum::{ use axum::{
http::Request, http::{Request, StatusCode},
http::StatusCode,
middleware::{self, Next}, middleware::{self, Next},
response::Response, response::Response,
routing::{get, post},
Extension, Extension,
Json,
Router,
}; };
use axum::{Json, Router};
use axum_extra::extract::CookieJar; use axum_extra::extract::CookieJar;
use axum_macros::debug_handler; use axum_macros::debug_handler;
use futures::future::try_join_all; use futures::future::try_join_all;
@ -40,6 +54,7 @@ pub fn api_routes() -> Router {
.route("/article/list", get(list_articles)) .route("/article/list", get(list_articles))
.route("/article/fork", post(fork_article)) .route("/article/fork", post(fork_article))
.route("/article/resolve", get(resolve_article)) .route("/article/resolve", get(resolve_article))
.route("/article/protect", post(protect_article))
.route("/edit_conflicts", get(edit_conflicts)) .route("/edit_conflicts", get(edit_conflicts))
.route("/instance", get(get_local_instance)) .route("/instance", get(get_local_instance))
.route("/instance/follow", post(follow_instance)) .route("/instance/follow", post(follow_instance))

View File

@ -1,18 +1,26 @@
use crate::backend::database::{read_jwt_secret, IbisData}; use crate::{
use crate::backend::error::MyResult; backend::{
use crate::common::{DbPerson, GetUserData, LocalUserView, LoginUserData, RegisterUserData}; database::{read_jwt_secret, IbisData},
error::MyResult,
},
common::{DbPerson, GetUserForm, LocalUserView, LoginUserForm, RegisterUserForm},
};
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use anyhow::anyhow; use anyhow::anyhow;
use axum::extract::Query; use axum::{extract::Query, Form, Json};
use axum::{Form, Json};
use axum_extra::extract::cookie::{Cookie, CookieJar, Expiration, SameSite}; use axum_extra::extract::cookie::{Cookie, CookieJar, Expiration, SameSite};
use axum_macros::debug_handler; use axum_macros::debug_handler;
use bcrypt::verify; use bcrypt::verify;
use chrono::Utc; use chrono::Utc;
use jsonwebtoken::DecodingKey; use jsonwebtoken::{
use jsonwebtoken::Validation; decode,
use jsonwebtoken::{decode, get_current_timestamp}; encode,
use jsonwebtoken::{encode, EncodingKey, Header}; get_current_timestamp,
DecodingKey,
EncodingKey,
Header,
Validation,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use time::{Duration, OffsetDateTime}; use time::{Duration, OffsetDateTime};
@ -57,7 +65,7 @@ pub async fn validate(jwt: &str, data: &Data<IbisData>) -> MyResult<LocalUserVie
pub(in crate::backend::api) async fn register_user( pub(in crate::backend::api) async fn register_user(
data: Data<IbisData>, data: Data<IbisData>,
jar: CookieJar, jar: CookieJar,
Form(form): Form<RegisterUserData>, Form(form): Form<RegisterUserForm>,
) -> MyResult<(CookieJar, Json<LocalUserView>)> { ) -> MyResult<(CookieJar, Json<LocalUserView>)> {
if !data.config.registration_open { if !data.config.registration_open {
return Err(anyhow!("Registration is closed").into()); return Err(anyhow!("Registration is closed").into());
@ -72,7 +80,7 @@ pub(in crate::backend::api) async fn register_user(
pub(in crate::backend::api) async fn login_user( pub(in crate::backend::api) async fn login_user(
data: Data<IbisData>, data: Data<IbisData>,
jar: CookieJar, jar: CookieJar,
Form(form): Form<LoginUserData>, Form(form): Form<LoginUserForm>,
) -> MyResult<(CookieJar, Json<LocalUserView>)> { ) -> MyResult<(CookieJar, Json<LocalUserView>)> {
let user = DbPerson::read_local_from_name(&form.username, &data)?; let user = DbPerson::read_local_from_name(&form.username, &data)?;
let valid = verify(&form.password, &user.local_user.password_encrypted)?; let valid = verify(&form.password, &user.local_user.password_encrypted)?;
@ -126,7 +134,7 @@ pub(in crate::backend::api) async fn logout_user(
#[debug_handler] #[debug_handler]
pub(in crate::backend::api) async fn get_user( pub(in crate::backend::api) async fn get_user(
params: Query<GetUserData>, params: Query<GetUserForm>,
data: Data<IbisData>, data: Data<IbisData>,
) -> MyResult<Json<DbPerson>> { ) -> MyResult<Json<DbPerson>> {
Ok(Json(DbPerson::read_from_name( Ok(Json(DbPerson::read_from_name(

View File

@ -1,17 +1,24 @@
use crate::backend::database::schema::{article, edit, instance}; use crate::{
use crate::backend::database::IbisData; backend::{
use crate::backend::error::MyResult; database::{
use crate::backend::federation::objects::edits_collection::DbEditCollection; schema::{article, edit, instance},
use crate::common::DbEdit; IbisData,
use crate::common::EditVersion; },
use crate::common::{ArticleView, DbArticle}; error::MyResult,
use activitypub_federation::fetch::collection_id::CollectionId; federation::objects::edits_collection::DbEditCollection,
use activitypub_federation::fetch::object_id::ObjectId; },
use diesel::dsl::max; common::{ArticleView, DbArticle, DbEdit, EditVersion},
};
use diesel::ExpressionMethods; use activitypub_federation::fetch::{collection_id::CollectionId, object_id::ObjectId};
use diesel::{ use diesel::{
insert_into, AsChangeset, BoolExpressionMethods, Insertable, PgTextExpressionMethods, QueryDsl, dsl::max,
insert_into,
AsChangeset,
BoolExpressionMethods,
ExpressionMethods,
Insertable,
PgTextExpressionMethods,
QueryDsl,
RunQueryDsl, RunQueryDsl,
}; };
use std::ops::DerefMut; use std::ops::DerefMut;
@ -24,6 +31,7 @@ pub struct DbArticleForm {
pub ap_id: ObjectId<DbArticle>, pub ap_id: ObjectId<DbArticle>,
pub instance_id: i32, pub instance_id: i32,
pub local: bool, pub local: bool,
pub protected: bool,
} }
// TODO: get rid of unnecessary methods // TODO: get rid of unnecessary methods
@ -58,6 +66,13 @@ impl DbArticle {
.get_result::<Self>(conn.deref_mut())?) .get_result::<Self>(conn.deref_mut())?)
} }
pub fn update_protected(id: i32, locked: bool, data: &IbisData) -> MyResult<Self> {
let mut conn = data.db_pool.get()?;
Ok(diesel::update(article::dsl::article.find(id))
.set(article::dsl::protected.eq(locked))
.get_result::<Self>(conn.deref_mut())?)
}
pub fn read(id: i32, data: &IbisData) -> MyResult<Self> { pub fn read(id: i32, data: &IbisData) -> MyResult<Self> {
let mut conn = data.db_pool.get()?; let mut conn = data.db_pool.get()?;
Ok(article::table.find(id).get_result(conn.deref_mut())?) Ok(article::table.find(id).get_result(conn.deref_mut())?)

View File

@ -1,16 +1,23 @@
use crate::backend::database::schema::conflict; use crate::{
use crate::backend::database::IbisData; backend::{
use crate::backend::error::MyResult; database::{schema::conflict, IbisData},
use crate::backend::federation::activities::submit_article_update; error::MyResult,
use crate::backend::utils::generate_article_version; federation::activities::submit_article_update,
use crate::common::DbEdit; utils::generate_article_version,
use crate::common::DbLocalUser; },
use crate::common::EditVersion; common::{ApiConflict, DbArticle, DbEdit, DbLocalUser, EditVersion},
use crate::common::{ApiConflict, DbArticle}; };
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use diesel::ExpressionMethods;
use diesel::{ use diesel::{
delete, insert_into, Identifiable, Insertable, QueryDsl, Queryable, RunQueryDsl, Selectable, delete,
insert_into,
ExpressionMethods,
Identifiable,
Insertable,
QueryDsl,
Queryable,
RunQueryDsl,
Selectable,
}; };
use diffy::{apply, merge, Patch}; use diffy::{apply, merge, Patch};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@ -1,12 +1,14 @@
use crate::backend::database::schema::{edit, person}; use crate::{
use crate::backend::error::MyResult; backend::{
use crate::backend::IbisData; database::schema::{edit, person},
use crate::common::{DbArticle, DbEdit}; error::MyResult,
use crate::common::{EditVersion, EditView}; IbisData,
},
common::{DbArticle, DbEdit, EditVersion, EditView},
};
use activitypub_federation::fetch::object_id::ObjectId; use activitypub_federation::fetch::object_id::ObjectId;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use diesel::ExpressionMethods; use diesel::{insert_into, AsChangeset, ExpressionMethods, Insertable, QueryDsl, RunQueryDsl};
use diesel::{insert_into, AsChangeset, Insertable, QueryDsl, RunQueryDsl};
use diffy::create_patch; use diffy::create_patch;
use std::ops::DerefMut; use std::ops::DerefMut;

View File

@ -1,16 +1,29 @@
use crate::backend::database::schema::{instance, instance_follow}; use crate::{
use crate::backend::database::IbisData; backend::{
use crate::backend::error::MyResult; database::{
use crate::backend::federation::objects::articles_collection::DbArticleCollection; schema::{instance, instance_follow},
use crate::common::{DbInstance, DbPerson, InstanceView}; IbisData,
use activitypub_federation::config::Data; },
use activitypub_federation::fetch::collection_id::CollectionId; error::MyResult,
use activitypub_federation::fetch::object_id::ObjectId; federation::objects::articles_collection::DbArticleCollection,
},
common::{DbInstance, DbPerson, InstanceView},
};
use activitypub_federation::{
config::Data,
fetch::{collection_id::CollectionId, object_id::ObjectId},
};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use diesel::ExpressionMethods; use diesel::{
use diesel::{insert_into, AsChangeset, Insertable, JoinOnDsl, QueryDsl, RunQueryDsl}; insert_into,
use std::fmt::Debug; AsChangeset,
use std::ops::DerefMut; ExpressionMethods,
Insertable,
JoinOnDsl,
QueryDsl,
RunQueryDsl,
};
use std::{fmt::Debug, ops::DerefMut};
#[derive(Debug, Clone, Insertable, AsChangeset)] #[derive(Debug, Clone, Insertable, AsChangeset)]
#[diesel(table_name = instance, check_for_backend(diesel::pg::Pg))] #[diesel(table_name = instance, check_for_backend(diesel::pg::Pg))]

View File

@ -1,11 +1,10 @@
use crate::backend::config::IbisConfig; use crate::backend::{config::IbisConfig, database::schema::jwt_secret, error::MyResult};
use crate::backend::database::schema::jwt_secret; use diesel::{
use crate::backend::error::MyResult; r2d2::{ConnectionManager, Pool},
use diesel::r2d2::ConnectionManager; PgConnection,
use diesel::r2d2::Pool; QueryDsl,
use diesel::PgConnection; RunQueryDsl,
use diesel::{QueryDsl, RunQueryDsl}; };
use std::ops::DerefMut; use std::ops::DerefMut;
pub mod article; pub mod article;

View File

@ -9,6 +9,7 @@ diesel::table! {
ap_id -> Varchar, ap_id -> Varchar,
instance_id -> Int4, instance_id -> Int4,
local -> Bool, local -> Bool,
protected -> Bool,
} }
} }

View File

@ -1,18 +1,30 @@
use crate::backend::database::schema::{instance, instance_follow}; use crate::{
use crate::backend::database::schema::{local_user, person}; backend::{
use crate::backend::database::IbisData; database::{
use crate::backend::error::MyResult; schema::{instance, instance_follow, local_user, person},
use crate::common::utils::http_protocol_str; IbisData,
use crate::common::{DbInstance, DbLocalUser, DbPerson, LocalUserView}; },
use activitypub_federation::config::Data; error::MyResult,
use activitypub_federation::fetch::object_id::ObjectId; },
use activitypub_federation::http_signatures::generate_actor_keypair; common::{utils::http_protocol_str, DbInstance, DbLocalUser, DbPerson, LocalUserView},
use bcrypt::hash; };
use bcrypt::DEFAULT_COST; use activitypub_federation::{
config::Data,
fetch::object_id::ObjectId,
http_signatures::generate_actor_keypair,
};
use bcrypt::{hash, DEFAULT_COST};
use chrono::{DateTime, Local, Utc}; use chrono::{DateTime, Local, Utc};
use diesel::{insert_into, AsChangeset, Insertable, RunQueryDsl}; use diesel::{
use diesel::{ExpressionMethods, JoinOnDsl}; insert_into,
use diesel::{PgTextExpressionMethods, QueryDsl}; AsChangeset,
ExpressionMethods,
Insertable,
JoinOnDsl,
PgTextExpressionMethods,
QueryDsl,
RunQueryDsl,
};
use std::ops::DerefMut; use std::ops::DerefMut;
#[derive(Debug, Clone, Insertable, AsChangeset)] #[derive(Debug, Clone, Insertable, AsChangeset)]

View File

@ -1,11 +1,17 @@
use crate::backend::error::MyResult; use crate::{
use crate::backend::federation::send_activity; backend::{
use crate::backend::utils::generate_activity_id; database::IbisData,
use crate::backend::{database::IbisData, federation::activities::follow::Follow}; error::MyResult,
use crate::common::DbInstance; federation::{activities::follow::Follow, send_activity},
use activitypub_federation::traits::Actor; utils::generate_activity_id,
},
common::DbInstance,
};
use activitypub_federation::{ use activitypub_federation::{
config::Data, fetch::object_id::ObjectId, kinds::activity::AcceptType, traits::ActivityHandler, config::Data,
fetch::object_id::ObjectId,
kinds::activity::AcceptType,
traits::{ActivityHandler, Actor},
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;

View File

@ -1,13 +1,16 @@
use crate::backend::database::IbisData; use crate::{
use crate::backend::error::MyResult; backend::{
use crate::backend::federation::objects::article::ApubArticle; database::IbisData,
use crate::backend::utils::generate_activity_id; error::MyResult,
use crate::common::DbArticle; federation::objects::article::ApubArticle,
use crate::common::DbInstance; utils::generate_activity_id,
use activitypub_federation::kinds::activity::CreateType; },
common::{DbArticle, DbInstance},
};
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,
fetch::object_id::ObjectId, fetch::object_id::ObjectId,
kinds::activity::CreateType,
protocol::helpers::deserialize_one_or_many, protocol::helpers::deserialize_one_or_many,
traits::{ActivityHandler, Object}, traits::{ActivityHandler, Object},
}; };

View File

@ -1,15 +1,17 @@
use crate::backend::error::MyResult; use crate::{
use crate::backend::federation::send_activity; backend::{
use crate::backend::{ database::IbisData,
database::IbisData, federation::activities::accept::Accept, generate_activity_id, error::MyResult,
federation::{activities::accept::Accept, send_activity},
generate_activity_id,
},
common::{DbInstance, DbPerson},
}; };
use crate::common::DbInstance;
use crate::common::DbPerson;
use activitypub_federation::protocol::verification::verify_urls_match;
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,
fetch::object_id::ObjectId, fetch::object_id::ObjectId,
kinds::activity::FollowType, kinds::activity::FollowType,
protocol::verification::verify_urls_match,
traits::{ActivityHandler, Actor}, traits::{ActivityHandler, Actor},
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@ -1,11 +1,14 @@
use crate::backend::database::edit::DbEditForm; use crate::{
use crate::backend::database::IbisData; backend::{
use crate::backend::error::Error; database::{edit::DbEditForm, IbisData},
use crate::backend::federation::activities::update_local_article::UpdateLocalArticle; error::Error,
use crate::backend::federation::activities::update_remote_article::UpdateRemoteArticle; federation::activities::{
use crate::common::DbInstance; update_local_article::UpdateLocalArticle,
use crate::common::EditVersion; update_remote_article::UpdateRemoteArticle,
use crate::common::{DbArticle, DbEdit}; },
},
common::{DbArticle, DbEdit, DbInstance, EditVersion},
};
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use chrono::Utc; use chrono::Utc;

View File

@ -1,17 +1,22 @@
use crate::backend::database::conflict::{DbConflict, DbConflictForm}; use crate::{
use crate::backend::database::IbisData; backend::{
use crate::backend::error::MyResult; database::{
use crate::backend::federation::objects::edit::ApubEdit; conflict::{DbConflict, DbConflictForm},
use crate::backend::utils::generate_activity_id; IbisData,
use crate::common::DbInstance; },
use crate::common::EditVersion; error::MyResult,
use activitypub_federation::kinds::activity::RejectType; federation::{objects::edit::ApubEdit, send_activity},
utils::generate_activity_id,
},
common::{DbInstance, EditVersion},
};
use activitypub_federation::{ use activitypub_federation::{
config::Data, fetch::object_id::ObjectId, protocol::helpers::deserialize_one_or_many, config::Data,
fetch::object_id::ObjectId,
kinds::activity::RejectType,
protocol::helpers::deserialize_one_or_many,
traits::ActivityHandler, traits::ActivityHandler,
}; };
use crate::backend::federation::send_activity;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;

View File

@ -1,14 +1,16 @@
use crate::backend::database::IbisData; use crate::{
use crate::backend::error::MyResult; backend::{
use crate::backend::federation::objects::article::ApubArticle; database::IbisData,
error::MyResult,
use crate::backend::utils::generate_activity_id; federation::objects::article::ApubArticle,
use crate::common::DbArticle; utils::generate_activity_id,
use crate::common::DbInstance; },
use activitypub_federation::kinds::activity::UpdateType; common::{DbArticle, DbInstance},
};
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,
fetch::object_id::ObjectId, fetch::object_id::ObjectId,
kinds::activity::UpdateType,
protocol::helpers::deserialize_one_or_many, protocol::helpers::deserialize_one_or_many,
traits::{ActivityHandler, Object}, traits::{ActivityHandler, Object},
}; };

View File

@ -1,19 +1,20 @@
use crate::backend::database::IbisData; use crate::{
use crate::backend::error::MyResult; backend::{
database::IbisData,
use crate::backend::federation::activities::reject::RejectEdit; error::MyResult,
use crate::backend::federation::activities::update_local_article::UpdateLocalArticle; federation::{
use crate::backend::federation::objects::edit::ApubEdit; activities::{reject::RejectEdit, update_local_article::UpdateLocalArticle},
use crate::backend::federation::send_activity; objects::edit::ApubEdit,
use crate::backend::utils::generate_activity_id; send_activity,
use crate::common::validation::can_edit_article; },
use crate::common::DbArticle; utils::generate_activity_id,
use crate::common::DbEdit; },
use crate::common::DbInstance; common::{validation::can_edit_article, DbArticle, DbEdit, DbInstance},
use activitypub_federation::kinds::activity::UpdateType; };
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,
fetch::object_id::ObjectId, fetch::object_id::ObjectId,
kinds::activity::UpdateType,
protocol::helpers::deserialize_one_or_many, protocol::helpers::deserialize_one_or_many,
traits::{ActivityHandler, Object}, traits::{ActivityHandler, Object},
}; };

View File

@ -1,10 +1,11 @@
use crate::backend::config::IbisConfig; use crate::backend::{config::IbisConfig, database::IbisData};
use crate::backend::database::IbisData; use activitypub_federation::{
use activitypub_federation::activity_queue::queue_activity; activity_queue::queue_activity,
use activitypub_federation::config::{Data, UrlVerifier}; config::{Data, UrlVerifier},
use activitypub_federation::error::Error as ActivityPubError; error::Error as ActivityPubError,
use activitypub_federation::protocol::context::WithContext; protocol::context::WithContext,
use activitypub_federation::traits::{ActivityHandler, Actor}; traits::{ActivityHandler, Actor},
};
use async_trait::async_trait; use async_trait::async_trait;
use serde::Serialize; use serde::Serialize;
use std::fmt::Debug; use std::fmt::Debug;

View File

@ -1,17 +1,17 @@
use crate::backend::database::article::DbArticleForm; use crate::{
use crate::backend::database::IbisData; backend::{
use crate::backend::error::Error; database::{article::DbArticleForm, IbisData},
use crate::backend::federation::objects::edits_collection::DbEditCollection; error::Error,
use crate::common::DbArticle; federation::objects::edits_collection::DbEditCollection,
use crate::common::DbInstance; },
use crate::common::EditVersion; common::{DbArticle, DbInstance, EditVersion},
use activitypub_federation::config::Data; };
use activitypub_federation::fetch::collection_id::CollectionId;
use activitypub_federation::kinds::object::ArticleType;
use activitypub_federation::kinds::public;
use activitypub_federation::protocol::verification::verify_domains_match;
use activitypub_federation::{ use activitypub_federation::{
fetch::object_id::ObjectId, protocol::helpers::deserialize_one_or_many, traits::Object, config::Data,
fetch::{collection_id::CollectionId, object_id::ObjectId},
kinds::{object::ArticleType, public},
protocol::{helpers::deserialize_one_or_many, verification::verify_domains_match},
traits::Object,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
@ -29,6 +29,7 @@ pub struct ApubArticle {
latest_version: EditVersion, latest_version: EditVersion,
content: String, content: String,
name: String, name: String,
protected: bool,
} }
#[async_trait::async_trait] #[async_trait::async_trait]
@ -56,6 +57,7 @@ impl Object for DbArticle {
latest_version: self.latest_edit_version(data)?, latest_version: self.latest_edit_version(data)?,
content: self.text, content: self.text,
name: self.title, name: self.title,
protected: self.protected,
}) })
} }
@ -76,6 +78,7 @@ impl Object for DbArticle {
ap_id: json.id, ap_id: json.id,
local: false, local: false,
instance_id: instance.id, instance_id: instance.id,
protected: json.protected,
}; };
let article = DbArticle::create_or_update(form, data)?; let article = DbArticle::create_or_update(form, data)?;

View File

@ -1,17 +1,14 @@
use crate::backend::database::IbisData; use crate::{
use crate::backend::error::Error; backend::{database::IbisData, error::Error, federation::objects::article::ApubArticle},
use crate::backend::federation::objects::article::ApubArticle; common::{DbArticle, DbInstance},
use crate::common::DbInstance; };
use crate::common::DbArticle;
use activitypub_federation::kinds::collection::CollectionType;
use activitypub_federation::protocol::verification::verify_domains_match;
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,
kinds::collection::CollectionType,
protocol::verification::verify_domains_match,
traits::{Collection, Object}, traits::{Collection, Object},
}; };
use futures::future; use futures::{future, future::try_join_all};
use futures::future::try_join_all;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;

View File

@ -1,13 +1,16 @@
use crate::backend::database::edit::DbEditForm; use crate::{
use crate::backend::database::IbisData; backend::{
use crate::backend::error::Error; database::{edit::DbEditForm, IbisData},
use crate::common::DbPerson; error::Error,
use crate::common::EditVersion; },
use crate::common::{DbArticle, DbEdit}; common::{DbArticle, DbEdit, DbPerson, EditVersion},
use activitypub_federation::config::Data; };
use activitypub_federation::fetch::object_id::ObjectId; use activitypub_federation::{
use activitypub_federation::protocol::verification::verify_domains_match; config::Data,
use activitypub_federation::traits::Object; fetch::object_id::ObjectId,
protocol::verification::verify_domains_match,
traits::Object,
};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;

View File

@ -1,18 +1,14 @@
use crate::backend::database::IbisData; use crate::{
use crate::backend::error::Error; backend::{database::IbisData, error::Error, federation::objects::edit::ApubEdit},
use crate::backend::federation::objects::edit::ApubEdit; common::{DbArticle, DbEdit, DbInstance},
use crate::common::DbArticle; };
use crate::common::DbEdit;
use crate::common::DbInstance;
use activitypub_federation::kinds::collection::OrderedCollectionType;
use activitypub_federation::protocol::verification::verify_domains_match;
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,
kinds::collection::OrderedCollectionType,
protocol::verification::verify_domains_match,
traits::{Collection, Object}, traits::{Collection, Object},
}; };
use futures::future; use futures::{future, future::try_join_all};
use futures::future::try_join_all;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;

View File

@ -1,19 +1,17 @@
use crate::backend::database::instance::DbInstanceForm; use crate::{
use crate::backend::database::IbisData; backend::{
use crate::backend::error::Error; database::{instance::DbInstanceForm, IbisData},
use crate::backend::error::MyResult; error::{Error, MyResult},
use crate::backend::federation::objects::articles_collection::DbArticleCollection; federation::{objects::articles_collection::DbArticleCollection, send_activity},
use crate::backend::federation::send_activity; },
use crate::common::utils::extract_domain; common::{utils::extract_domain, DbInstance},
use crate::common::DbInstance; };
use activitypub_federation::fetch::collection_id::CollectionId;
use activitypub_federation::kinds::actor::ServiceType;
use activitypub_federation::traits::ActivityHandler;
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,
fetch::object_id::ObjectId, fetch::{collection_id::CollectionId, object_id::ObjectId},
kinds::actor::ServiceType,
protocol::{public_key::PublicKey, verification::verify_domains_match}, protocol::{public_key::PublicKey, verification::verify_domains_match},
traits::{Actor, Object}, traits::{ActivityHandler, Actor, Object},
}; };
use chrono::{DateTime, Local, Utc}; use chrono::{DateTime, Local, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@ -1,11 +1,14 @@
use crate::backend::database::user::DbPersonForm; use crate::{
use crate::backend::database::IbisData; backend::{
use crate::backend::error::Error; database::{user::DbPersonForm, IbisData},
use crate::common::DbPerson; error::Error,
use activitypub_federation::kinds::actor::PersonType; },
common::DbPerson,
};
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,
fetch::object_id::ObjectId, fetch::object_id::ObjectId,
kinds::actor::PersonType,
protocol::{public_key::PublicKey, verification::verify_domains_match}, protocol::{public_key::PublicKey, verification::verify_domains_match},
traits::{Actor, Object}, traits::{Actor, Object},
}; };

View File

@ -1,33 +1,42 @@
use crate::backend::database::IbisData; use crate::{
use crate::backend::error::Error; backend::{
use crate::backend::error::MyResult; database::IbisData,
use crate::backend::federation::activities::accept::Accept; error::{Error, MyResult},
use crate::backend::federation::activities::create_article::CreateArticle; federation::{
use crate::backend::federation::activities::follow::Follow; activities::{
use crate::backend::federation::activities::reject::RejectEdit; accept::Accept,
use crate::backend::federation::activities::update_local_article::UpdateLocalArticle; create_article::CreateArticle,
use crate::backend::federation::activities::update_remote_article::UpdateRemoteArticle; follow::Follow,
use crate::backend::federation::objects::article::ApubArticle; reject::RejectEdit,
use crate::backend::federation::objects::articles_collection::{ update_local_article::UpdateLocalArticle,
ArticleCollection, DbArticleCollection, update_remote_article::UpdateRemoteArticle,
},
objects::{
article::ApubArticle,
articles_collection::{ArticleCollection, DbArticleCollection},
edits_collection::{ApubEditCollection, DbEditCollection},
instance::ApubInstance,
user::ApubUser,
},
},
},
common::{DbArticle, DbInstance, DbPerson},
};
use activitypub_federation::{
axum::{
inbox::{receive_activity, ActivityData},
json::FederationJson,
},
config::Data,
protocol::context::WithContext,
traits::{ActivityHandler, Actor, Collection, Object},
};
use axum::{
extract::Path,
response::IntoResponse,
routing::{get, post},
Router,
}; };
use crate::backend::federation::objects::edits_collection::{ApubEditCollection, DbEditCollection};
use crate::backend::federation::objects::instance::ApubInstance;
use crate::backend::federation::objects::user::ApubUser;
use crate::common::DbArticle;
use crate::common::DbInstance;
use crate::common::DbPerson;
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;
use axum::response::IntoResponse;
use axum::routing::{get, post};
use axum::Router;
use axum_macros::debug_handler; use axum_macros::debug_handler;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@ -1,38 +1,45 @@
use crate::backend::config::IbisConfig; use crate::{
use crate::backend::database::article::DbArticleForm; backend::{
use crate::backend::database::instance::DbInstanceForm; config::IbisConfig,
use crate::backend::database::IbisData; database::{article::DbArticleForm, instance::DbInstanceForm, IbisData},
use crate::backend::error::Error; error::{Error, MyResult},
use crate::backend::error::MyResult; federation::{activities::submit_article_update, routes::federation_routes, VerifyUrlData},
use crate::backend::federation::activities::submit_article_update; utils::generate_activity_id,
use crate::backend::federation::routes::federation_routes; },
use crate::backend::federation::VerifyUrlData; common::{
use crate::backend::utils::generate_activity_id; utils::http_protocol_str,
use crate::common::utils::http_protocol_str; DbArticle,
use crate::common::{DbArticle, DbInstance, DbPerson, EditVersion, MAIN_PAGE_NAME}; DbInstance,
use crate::frontend::app::App; DbPerson,
use activitypub_federation::config::{Data, FederationConfig, FederationMiddleware}; EditVersion,
use activitypub_federation::fetch::collection_id::CollectionId; MAIN_PAGE_NAME,
use activitypub_federation::fetch::object_id::ObjectId; },
use activitypub_federation::http_signatures::generate_actor_keypair; frontend::app::App,
};
use activitypub_federation::{
config::{Data, FederationConfig, FederationMiddleware},
fetch::{collection_id::CollectionId, object_id::ObjectId},
http_signatures::generate_actor_keypair,
};
use api::api_routes; use api::api_routes;
use axum::debug_handler; use axum::{
use axum::headers::HeaderMap; debug_handler,
use axum::http::{HeaderValue, Request}; headers::HeaderMap,
use axum::response::IntoResponse; http::{HeaderValue, Request},
use axum::routing::get; middleware::Next,
use axum::Server; response::{IntoResponse, Response},
use axum::ServiceExt; routing::get,
use axum::{middleware::Next, response::Response, Router}; Router,
Server,
ServiceExt,
};
use chrono::Local; use chrono::Local;
use diesel::r2d2::ConnectionManager; use diesel::{
use diesel::r2d2::Pool; r2d2::{ConnectionManager, Pool},
use diesel::PgConnection; PgConnection,
use diesel_migrations::embed_migrations; };
use diesel_migrations::EmbeddedMigrations; use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
use diesel_migrations::MigrationHarness; use leptos::{leptos_config::get_config_from_str, *};
use leptos::leptos_config::get_config_from_str;
use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes}; use leptos_axum::{generate_route_list, LeptosRoutes};
use log::info; use log::info;
use tower::Layer; use tower::Layer;
@ -179,6 +186,7 @@ async fn setup(data: &Data<IbisData>) -> Result<(), Error> {
))?, ))?,
instance_id: instance.id, instance_id: instance.id,
local: true, local: true,
protected: true,
}; };
let article = DbArticle::create(form, data)?; let article = DbArticle::create(form, data)?;
// also create an article so its included in most recently edited list // also create an article so its included in most recently edited list

View File

@ -1,14 +1,12 @@
use crate::backend::error::MyResult; use crate::{
use crate::common::EditView; backend::error::MyResult,
use crate::common::{utils, EditVersion}; common::{utils, utils::extract_domain, EditVersion, EditView},
use activitypub_federation::fetch::object_id::ObjectId; };
use activitypub_federation::traits::Object; use activitypub_federation::{fetch::object_id::ObjectId, traits::Object};
use anyhow::anyhow; use anyhow::anyhow;
use diffy::{apply, Patch}; use diffy::{apply, Patch};
use rand::{distributions::Alphanumeric, thread_rng, Rng}; use rand::{distributions::Alphanumeric, thread_rng, Rng};
use serde::Deserialize; use serde::Deserialize;
use crate::common::utils::extract_domain;
use url::{ParseError, Url}; use url::{ParseError, Url};
pub fn generate_activity_id<T>(for_url: &ObjectId<T>) -> Result<Url, ParseError> pub fn generate_activity_id<T>(for_url: &ObjectId<T>) -> Result<Url, ParseError>

View File

@ -20,14 +20,14 @@ pub const MAIN_PAGE_NAME: &str = "Main_Page";
/// Should be an enum Title/Id but fails due to https://github.com/nox/serde_urlencoded/issues/66 /// Should be an enum Title/Id but fails due to https://github.com/nox/serde_urlencoded/issues/66
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug)]
pub struct GetArticleData { pub struct GetArticleForm {
pub title: Option<String>, pub title: Option<String>,
pub domain: Option<String>, pub domain: Option<String>,
pub id: Option<i32>, pub id: Option<i32>,
} }
#[derive(Deserialize, Serialize, Clone)] #[derive(Deserialize, Serialize, Clone)]
pub struct ListArticlesData { pub struct ListArticlesForm {
pub only_local: Option<bool>, pub only_local: Option<bool>,
} }
@ -53,6 +53,7 @@ pub struct DbArticle {
pub ap_id: String, pub ap_id: String,
pub instance_id: i32, pub instance_id: i32,
pub local: bool, pub local: bool,
pub protected: bool,
} }
/// Represents a single change to the article. /// Represents a single change to the article.
@ -115,13 +116,13 @@ impl Default for EditVersion {
} }
#[derive(Deserialize, Serialize, Clone)] #[derive(Deserialize, Serialize, Clone)]
pub struct RegisterUserData { pub struct RegisterUserForm {
pub username: String, pub username: String,
pub password: String, pub password: String,
} }
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct LoginUserData { pub struct LoginUserForm {
pub username: String, pub username: String,
pub password: String, pub password: String,
} }
@ -175,14 +176,14 @@ impl DbPerson {
} }
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct CreateArticleData { pub struct CreateArticleForm {
pub title: String, pub title: String,
pub text: String, pub text: String,
pub summary: String, pub summary: String,
} }
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
pub struct EditArticleData { pub struct EditArticleForm {
/// Id of the article to edit /// Id of the article to edit
pub article_id: i32, pub article_id: i32,
/// Full, new text of the article. A diff against `previous_version` is generated on the backend /// Full, new text of the article. A diff against `previous_version` is generated on the backend
@ -197,8 +198,14 @@ pub struct EditArticleData {
pub resolve_conflict_id: Option<i32>, pub resolve_conflict_id: Option<i32>,
} }
#[derive(Deserialize, Serialize, Debug)]
pub struct ProtectArticleForm {
pub article_id: i32,
pub protected: bool,
}
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct ForkArticleData { pub struct ForkArticleForm {
pub article_id: i32, pub article_id: i32,
pub new_title: String, pub new_title: String,
} }
@ -209,7 +216,7 @@ pub struct FollowInstance {
} }
#[derive(Deserialize, Serialize, Clone)] #[derive(Deserialize, Serialize, Clone)]
pub struct SearchArticleData { pub struct SearchArticleForm {
pub query: String, pub query: String,
} }
@ -269,7 +276,7 @@ pub struct InstanceView {
} }
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug)]
pub struct GetUserData { pub struct GetUserForm {
pub name: String, pub name: String,
pub domain: Option<String>, pub domain: Option<String>,
} }

View File

@ -1,14 +1,14 @@
use crate::common::{DbArticle, MAIN_PAGE_NAME}; use crate::common::DbArticle;
use anyhow::anyhow; use anyhow::{anyhow, Result};
use anyhow::Result;
pub fn can_edit_article(article: &DbArticle, is_admin: bool) -> Result<()> { pub fn can_edit_article(article: &DbArticle, is_admin: bool) -> Result<()> {
if article.title == MAIN_PAGE_NAME { let err = anyhow!("Article is protected, only admins on origin instance can edit");
if article.protected {
if !article.local { if !article.local {
return Err(anyhow!("Cannot edit main page of remote instance")); return Err(err);
} }
if article.local && !is_admin { if !is_admin {
return Err(anyhow!("Only admin can edit main page")); return Err(err);
} }
} }
Ok(()) Ok(())

117
src/database/schema.rs Normal file
View File

@ -0,0 +1,117 @@
// @generated automatically by Diesel CLI.
diesel::table! {
article (id) {
id -> Int4,
title -> Text,
text -> Text,
#[max_length = 255]
ap_id -> Varchar,
instance_id -> Int4,
local -> Bool,
protected -> Bool,
}
}
diesel::table! {
conflict (id) {
id -> Int4,
hash -> Uuid,
diff -> Text,
summary -> Text,
creator_id -> Int4,
article_id -> Int4,
previous_version_id -> Uuid,
}
}
diesel::table! {
edit (id) {
id -> Int4,
creator_id -> Int4,
hash -> Uuid,
#[max_length = 255]
ap_id -> Varchar,
diff -> Text,
summary -> Text,
article_id -> Int4,
previous_version_id -> Uuid,
created -> Timestamptz,
}
}
diesel::table! {
instance (id) {
id -> Int4,
domain -> Text,
#[max_length = 255]
ap_id -> Varchar,
description -> Nullable<Text>,
inbox_url -> Text,
#[max_length = 255]
articles_url -> Varchar,
public_key -> Text,
private_key -> Nullable<Text>,
last_refreshed_at -> Timestamptz,
local -> Bool,
}
}
diesel::table! {
instance_follow (id) {
id -> Int4,
instance_id -> Int4,
follower_id -> Int4,
pending -> Bool,
}
}
diesel::table! {
jwt_secret (id) {
id -> Int4,
secret -> Varchar,
}
}
diesel::table! {
local_user (id) {
id -> Int4,
password_encrypted -> Text,
person_id -> Int4,
admin -> Bool,
}
}
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!(conflict -> local_user (creator_id));
diesel::joinable!(edit -> article (article_id));
diesel::joinable!(edit -> person (creator_id));
diesel::joinable!(instance_follow -> instance (instance_id));
diesel::joinable!(instance_follow -> person (follower_id));
diesel::joinable!(local_user -> person (person_id));
diesel::allow_tables_to_appear_in_same_query!(
article,
conflict,
edit,
instance,
instance_follow,
jwt_secret,
local_user,
person,
);

View File

@ -1,11 +1,28 @@
use crate::common::utils::http_protocol_str; use crate::{
use crate::common::{ApiConflict, ListArticlesData}; common::{
use crate::common::{ArticleView, LoginUserData, RegisterUserData}; utils::http_protocol_str,
use crate::common::{CreateArticleData, EditArticleData, ForkArticleData, LocalUserView}; ApiConflict,
use crate::common::{DbArticle, GetArticleData}; ArticleView,
use crate::common::{DbInstance, FollowInstance, InstanceView, SearchArticleData}; CreateArticleForm,
use crate::common::{DbPerson, GetUserData, ResolveObject}; DbArticle,
use crate::frontend::error::MyResult; DbInstance,
DbPerson,
EditArticleForm,
FollowInstance,
ForkArticleForm,
GetArticleForm,
GetUserForm,
InstanceView,
ListArticlesForm,
LocalUserView,
LoginUserForm,
ProtectArticleForm,
RegisterUserForm,
ResolveObject,
SearchArticleForm,
},
frontend::error::MyResult,
};
use anyhow::anyhow; use anyhow::anyhow;
use reqwest::{Client, RequestBuilder, StatusCode}; use reqwest::{Client, RequestBuilder, StatusCode};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -34,15 +51,15 @@ impl ApiClient {
handle_json_res::<T>(req).await handle_json_res::<T>(req).await
} }
pub async fn get_article(&self, data: GetArticleData) -> MyResult<ArticleView> { pub async fn get_article(&self, data: GetArticleForm) -> MyResult<ArticleView> {
self.get_query("/api/v1/article", Some(data)).await self.get_query("/api/v1/article", Some(data)).await
} }
pub async fn list_articles(&self, data: ListArticlesData) -> MyResult<Vec<DbArticle>> { pub async fn list_articles(&self, data: ListArticlesForm) -> MyResult<Vec<DbArticle>> {
self.get_query("/api/v1/article/list", Some(data)).await self.get_query("/api/v1/article/list", Some(data)).await
} }
pub async fn register(&self, register_form: RegisterUserData) -> MyResult<LocalUserView> { pub async fn register(&self, register_form: RegisterUserForm) -> MyResult<LocalUserView> {
let req = self let req = self
.client .client
.post(self.request_endpoint("/api/v1/account/register")) .post(self.request_endpoint("/api/v1/account/register"))
@ -50,7 +67,7 @@ impl ApiClient {
handle_json_res::<LocalUserView>(req).await handle_json_res::<LocalUserView>(req).await
} }
pub async fn login(&self, login_form: LoginUserData) -> MyResult<LocalUserView> { pub async fn login(&self, login_form: LoginUserForm) -> MyResult<LocalUserView> {
let req = self let req = self
.client .client
.post(self.request_endpoint("/api/v1/account/login")) .post(self.request_endpoint("/api/v1/account/login"))
@ -58,7 +75,7 @@ impl ApiClient {
handle_json_res::<LocalUserView>(req).await handle_json_res::<LocalUserView>(req).await
} }
pub async fn create_article(&self, data: &CreateArticleData) -> MyResult<ArticleView> { pub async fn create_article(&self, data: &CreateArticleForm) -> MyResult<ArticleView> {
let req = self let req = self
.client .client
.post(self.request_endpoint("/api/v1/article")) .post(self.request_endpoint("/api/v1/article"))
@ -68,7 +85,7 @@ impl ApiClient {
pub async fn edit_article_with_conflict( pub async fn edit_article_with_conflict(
&self, &self,
edit_form: &EditArticleData, edit_form: &EditArticleForm,
) -> MyResult<Option<ApiConflict>> { ) -> MyResult<Option<ApiConflict>> {
let req = self let req = self
.client .client
@ -77,11 +94,11 @@ impl ApiClient {
handle_json_res(req).await handle_json_res(req).await
} }
pub async fn edit_article(&self, edit_form: &EditArticleData) -> MyResult<ArticleView> { pub async fn edit_article(&self, edit_form: &EditArticleForm) -> MyResult<ArticleView> {
let edit_res = self.edit_article_with_conflict(edit_form).await?; let edit_res = self.edit_article_with_conflict(edit_form).await?;
assert!(edit_res.is_none()); assert!(edit_res.is_none());
self.get_article(GetArticleData { self.get_article(GetArticleForm {
title: None, title: None,
domain: None, domain: None,
id: Some(edit_form.article_id), id: Some(edit_form.article_id),
@ -89,7 +106,7 @@ impl ApiClient {
.await .await
} }
pub async fn search(&self, search_form: &SearchArticleData) -> MyResult<Vec<DbArticle>> { pub async fn search(&self, search_form: &SearchArticleForm) -> MyResult<Vec<DbArticle>> {
self.get_query("/api/v1/search", Some(search_form)).await self.get_query("/api/v1/search", Some(search_form)).await
} }
@ -147,7 +164,7 @@ impl ApiClient {
Ok(()) Ok(())
} }
pub async fn fork_article(&self, form: &ForkArticleData) -> MyResult<ArticleView> { pub async fn fork_article(&self, form: &ForkArticleForm) -> MyResult<ArticleView> {
let req = self let req = self
.client .client
.post(self.request_endpoint("/api/v1/article/fork")) .post(self.request_endpoint("/api/v1/article/fork"))
@ -155,6 +172,14 @@ impl ApiClient {
Ok(handle_json_res(req).await.unwrap()) Ok(handle_json_res(req).await.unwrap())
} }
pub async fn protect_article(&self, params: &ProtectArticleForm) -> MyResult<DbArticle> {
let req = self
.client
.post(self.request_endpoint("/api/v1/article/protect"))
.form(params);
handle_json_res(req).await
}
pub async fn get_conflicts(&self) -> MyResult<Vec<ApiConflict>> { pub async fn get_conflicts(&self) -> MyResult<Vec<ApiConflict>> {
let req = self let req = self
.client .client
@ -173,7 +198,7 @@ impl ApiClient {
self.get_query("/api/v1/instance/resolve", Some(resolve_object)) self.get_query("/api/v1/instance/resolve", Some(resolve_object))
.await .await
} }
pub async fn get_user(&self, data: GetUserData) -> MyResult<DbPerson> { pub async fn get_user(&self, data: GetUserForm) -> MyResult<DbPerson> {
self.get_query("/api/v1/user", Some(data)).await self.get_query("/api/v1/user", Some(data)).await
} }

View File

@ -1,29 +1,44 @@
use crate::common::LocalUserView; use crate::{
use crate::frontend::api::ApiClient; common::LocalUserView,
use crate::frontend::backend_hostname; frontend::{
use crate::frontend::components::nav::Nav; api::ApiClient,
use crate::frontend::pages::article::actions::ArticleActions; backend_hostname,
use crate::frontend::pages::article::create::CreateArticle; components::nav::Nav,
use crate::frontend::pages::article::edit::EditArticle; pages::{
use crate::frontend::pages::article::history::ArticleHistory; article::{
use crate::frontend::pages::article::list::ListArticles; actions::ArticleActions,
use crate::frontend::pages::article::read::ReadArticle; create::CreateArticle,
use crate::frontend::pages::conflicts::Conflicts; edit::EditArticle,
use crate::frontend::pages::diff::EditDiff; history::ArticleHistory,
use crate::frontend::pages::instance_details::InstanceDetails; list::ListArticles,
use crate::frontend::pages::login::Login; read::ReadArticle,
use crate::frontend::pages::register::Register; },
use crate::frontend::pages::search::Search; conflicts::Conflicts,
use crate::frontend::pages::user_profile::UserProfile; diff::EditDiff,
use leptos::{ instance_details::InstanceDetails,
component, create_local_resource, create_rw_signal, expect_context, provide_context, login::Login,
use_context, view, IntoView, RwSignal, SignalGet, SignalGetUntracked, SignalUpdate, register::Register,
search::Search,
user_profile::UserProfile,
},
},
}; };
use leptos_meta::provide_meta_context; use leptos::{
use leptos_meta::*; component,
use leptos_router::Route; create_local_resource,
use leptos_router::Router; create_rw_signal,
use leptos_router::Routes; expect_context,
provide_context,
use_context,
view,
IntoView,
RwSignal,
SignalGet,
SignalGetUntracked,
SignalUpdate,
};
use leptos_meta::{provide_meta_context, *};
use leptos_router::{Route, Router, Routes};
use reqwest::Client; use reqwest::Client;
// https://book.leptos.dev/15_global_state.html // https://book.leptos.dev/15_global_state.html

View File

@ -1,7 +1,7 @@
use crate::common::validation::can_edit_article; use crate::{
use crate::common::ArticleView; common::{validation::can_edit_article, ArticleView},
use crate::frontend::app::GlobalState; frontend::{app::GlobalState, article_link},
use crate::frontend::article_link; };
use leptos::*; use leptos::*;
use leptos_router::*; use leptos_router::*;
@ -13,6 +13,7 @@ pub fn ArticleNav(article: Resource<Option<String>, ArticleView>) -> impl IntoVi
{move || article.get().map(|article| { {move || article.get().map(|article| {
let article_link = article_link(&article.article); let article_link = article_link(&article.article);
let article_link_ = article_link.clone(); let article_link_ = article_link.clone();
let protected = article.article.protected;
view!{ view!{
<nav class="inner"> <nav class="inner">
<A href=article_link.clone()>"Read"</A> <A href=article_link.clone()>"Read"</A>
@ -26,6 +27,9 @@ pub fn ArticleNav(article: Resource<Option<String>, ArticleView>) -> impl IntoVi
<Show when=move || global_state.with(|state| state.my_profile.is_some())> <Show when=move || global_state.with(|state| state.my_profile.is_some())>
<A href={format!("{article_link_}/actions")}>"Actions"</A> <A href={format!("{article_link_}/actions")}>"Actions"</A>
</Show> </Show>
<Show when=move || protected>
<span title="Article can only be edited by local admins">"Protected"</span>
</Show>
</nav> </nav>
}})} }})}
</Suspense> </Suspense>

View File

@ -1,6 +1,5 @@
use crate::frontend::app::GlobalState; use crate::frontend::app::GlobalState;
use leptos::*; use leptos::{component, use_context, view, IntoView, RwSignal, SignalWith, *};
use leptos::{component, use_context, view, IntoView, RwSignal, SignalWith};
use leptos_router::*; use leptos_router::*;
#[component] #[component]

View File

@ -1,6 +1,11 @@
use crate::frontend::backend_hostname; use crate::frontend::backend_hostname;
use markdown_it::parser::inline::{InlineRule, InlineState}; use markdown_it::{
use markdown_it::{MarkdownIt, Node, NodeValue, Renderer}; parser::inline::{InlineRule, InlineState},
MarkdownIt,
Node,
NodeValue,
Renderer,
};
pub fn markdown_parser() -> MarkdownIt { pub fn markdown_parser() -> MarkdownIt {
let mut parser = MarkdownIt::new(); let mut parser = MarkdownIt::new();

View File

@ -1,5 +1,4 @@
use crate::common::utils::extract_domain; use crate::common::{utils::extract_domain, DbArticle, DbPerson};
use crate::common::{DbArticle, DbPerson};
use leptos::*; use leptos::*;
pub mod api; pub mod api;

View File

@ -1,10 +1,14 @@
use crate::common::ForkArticleData; use crate::{
use crate::frontend::app::GlobalState; common::ForkArticleForm,
use crate::frontend::article_link; frontend::{
use crate::frontend::article_title; app::GlobalState,
use crate::frontend::components::article_nav::ArticleNav; article_link,
use crate::frontend::pages::article_resource; article_title,
use crate::frontend::DbArticle; components::article_nav::ArticleNav,
pages::article_resource,
DbArticle,
},
};
use leptos::*; use leptos::*;
use leptos_router::Redirect; use leptos_router::Redirect;
@ -15,7 +19,7 @@ pub fn ArticleActions() -> impl IntoView {
let (fork_response, set_fork_response) = create_signal(Option::<DbArticle>::None); let (fork_response, set_fork_response) = create_signal(Option::<DbArticle>::None);
let (error, set_error) = create_signal(None::<String>); let (error, set_error) = create_signal(None::<String>);
let fork_action = create_action(move |(article_id, new_title): &(i32, String)| { let fork_action = create_action(move |(article_id, new_title): &(i32, String)| {
let params = ForkArticleData { let params = ForkArticleForm {
article_id: *article_id, article_id: *article_id,
new_title: new_title.to_string(), new_title: new_title.to_string(),
}; };

View File

@ -1,5 +1,4 @@
use crate::common::CreateArticleData; use crate::{common::CreateArticleForm, frontend::app::GlobalState};
use crate::frontend::app::GlobalState;
use leptos::*; use leptos::*;
use leptos_router::Redirect; use leptos_router::Redirect;
@ -18,7 +17,7 @@ pub fn CreateArticle() -> impl IntoView {
let text = text.clone(); let text = text.clone();
let summary = summary.clone(); let summary = summary.clone();
async move { async move {
let form = CreateArticleData { let form = CreateArticleForm {
title, title,
text, text,
summary, summary,

View File

@ -1,8 +1,12 @@
use crate::common::{ApiConflict, ArticleView, EditArticleData}; use crate::{
use crate::frontend::app::GlobalState; common::{ApiConflict, ArticleView, EditArticleForm},
use crate::frontend::article_title; frontend::{
use crate::frontend::components::article_nav::ArticleNav; app::GlobalState,
use crate::frontend::pages::article_resource; article_title,
components::article_nav::ArticleNav,
pages::article_resource,
},
};
use leptos::*; use leptos::*;
use leptos_router::use_params_map; use leptos_router::use_params_map;
@ -65,7 +69,7 @@ pub fn EditArticle() -> impl IntoView {
}; };
async move { async move {
set_edit_error.update(|e| *e = None); set_edit_error.update(|e| *e = None);
let form = EditArticleData { let form = EditArticleForm {
article_id: article.article.id, article_id: article.article.id,
new_text, new_text,
summary, summary,

View File

@ -1,6 +1,9 @@
use crate::frontend::components::article_nav::ArticleNav; use crate::frontend::{
use crate::frontend::pages::article_resource; article_title,
use crate::frontend::{article_title, user_link}; components::article_nav::ArticleNav,
pages::article_resource,
user_link,
};
use leptos::*; use leptos::*;
#[component] #[component]

View File

@ -1,6 +1,7 @@
use crate::common::ListArticlesData; use crate::{
use crate::frontend::app::GlobalState; common::ListArticlesForm,
use crate::frontend::{article_link, article_title}; frontend::{app::GlobalState, article_link, article_title},
};
use leptos::*; use leptos::*;
use web_sys::wasm_bindgen::JsCast; use web_sys::wasm_bindgen::JsCast;
@ -11,7 +12,7 @@ pub fn ListArticles() -> impl IntoView {
move || only_local.get(), move || only_local.get(),
|only_local| async move { |only_local| async move {
GlobalState::api_client() GlobalState::api_client()
.list_articles(ListArticlesData { .list_articles(ListArticlesForm {
only_local: Some(only_local), only_local: Some(only_local),
}) })
.await .await

View File

@ -1,7 +1,9 @@
use crate::frontend::article_title; use crate::frontend::{
use crate::frontend::components::article_nav::ArticleNav; article_title,
use crate::frontend::markdown::markdown_parser; components::article_nav::ArticleNav,
use crate::frontend::pages::article_resource; markdown::markdown_parser,
pages::article_resource,
};
use leptos::*; use leptos::*;
#[component] #[component]

View File

@ -1,6 +1,4 @@
use crate::frontend::app::GlobalState; use crate::frontend::{app::GlobalState, article_link, article_title};
use crate::frontend::article_link;
use crate::frontend::article_title;
use leptos::*; use leptos::*;
#[component] #[component]

View File

@ -1,6 +1,4 @@
use crate::frontend::components::article_nav::ArticleNav; use crate::frontend::{components::article_nav::ArticleNav, pages::article_resource, user_link};
use crate::frontend::pages::article_resource;
use crate::frontend::user_link;
use leptos::*; use leptos::*;
use leptos_router::*; use leptos_router::*;

View File

@ -1,6 +1,7 @@
use crate::common::utils::http_protocol_str; use crate::{
use crate::common::{DbInstance, FollowInstance}; common::{utils::http_protocol_str, DbInstance, FollowInstance},
use crate::frontend::app::GlobalState; frontend::app::GlobalState,
};
use leptos::*; use leptos::*;
use leptos_router::use_params_map; use leptos_router::use_params_map;
use url::Url; use url::Url;

View File

@ -1,6 +1,7 @@
use crate::common::LoginUserData; use crate::{
use crate::frontend::app::GlobalState; common::LoginUserForm,
use crate::frontend::components::credentials::*; frontend::{app::GlobalState, components::credentials::*},
};
use leptos::*; use leptos::*;
use leptos_router::Redirect; use leptos_router::Redirect;
@ -13,7 +14,7 @@ pub fn Login() -> impl IntoView {
let login_action = create_action(move |(email, password): &(String, String)| { let login_action = create_action(move |(email, password): &(String, String)| {
let username = email.to_string(); let username = email.to_string();
let password = password.to_string(); let password = password.to_string();
let credentials = LoginUserData { username, password }; let credentials = LoginUserForm { username, password };
async move { async move {
set_wait_for_response.update(|w| *w = true); set_wait_for_response.update(|w| *w = true);
let result = GlobalState::api_client().login(credentials).await; let result = GlobalState::api_client().login(credentials).await;

View File

@ -1,5 +1,7 @@
use crate::common::{ArticleView, GetArticleData, MAIN_PAGE_NAME}; use crate::{
use crate::frontend::app::GlobalState; common::{ArticleView, GetArticleForm, MAIN_PAGE_NAME},
frontend::app::GlobalState,
};
use leptos::{create_resource, Resource, SignalGet}; use leptos::{create_resource, Resource, SignalGet};
use leptos_router::use_params_map; use leptos_router::use_params_map;
@ -23,7 +25,7 @@ fn article_resource() -> Resource<Option<String>, ArticleView> {
domain = Some(domain_.to_string()); domain = Some(domain_.to_string());
} }
GlobalState::api_client() GlobalState::api_client()
.get_article(GetArticleData { .get_article(GetArticleForm {
title: Some(title), title: Some(title),
domain, domain,
id: None, id: None,

View File

@ -1,7 +1,7 @@
use crate::common::{LocalUserView, RegisterUserData}; use crate::{
use crate::frontend::app::GlobalState; common::{LocalUserView, RegisterUserForm},
use crate::frontend::components::credentials::*; frontend::{app::GlobalState, components::credentials::*, error::MyResult},
use crate::frontend::error::MyResult; };
use leptos::{logging::log, *}; use leptos::{logging::log, *};
#[component] #[component]
@ -13,7 +13,7 @@ pub fn Register() -> impl IntoView {
let register_action = create_action(move |(email, password): &(String, String)| { let register_action = create_action(move |(email, password): &(String, String)| {
let username = email.to_string(); let username = email.to_string();
let password = password.to_string(); let password = password.to_string();
let credentials = RegisterUserData { username, password }; let credentials = RegisterUserForm { username, password };
log!("Try to register new account for {}", credentials.username); log!("Try to register new account for {}", credentials.username);
async move { async move {
set_wait_for_response.update(|w| *w = true); set_wait_for_response.update(|w| *w = true);

View File

@ -1,6 +1,7 @@
use crate::common::{DbArticle, DbInstance, SearchArticleData}; use crate::{
use crate::frontend::app::GlobalState; common::{DbArticle, DbInstance, SearchArticleForm},
use crate::frontend::{article_link, article_title}; frontend::{app::GlobalState, article_link, article_title},
};
use leptos::*; use leptos::*;
use leptos_router::use_query_map; use leptos_router::use_query_map;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -28,7 +29,7 @@ pub fn Search() -> impl IntoView {
let mut search_results = SearchResults::default(); let mut search_results = SearchResults::default();
let api_client = GlobalState::api_client(); let api_client = GlobalState::api_client();
let url = Url::parse(&query); let url = Url::parse(&query);
let search_data = SearchArticleData { query }; let search_data = SearchArticleForm { query };
let search = api_client.search(&search_data); let search = api_client.search(&search_data);
match search.await { match search.await {

View File

@ -1,6 +1,7 @@
use crate::common::{DbPerson, GetUserData}; use crate::{
use crate::frontend::app::GlobalState; common::{DbPerson, GetUserForm},
use crate::frontend::user_title; frontend::{app::GlobalState, user_title},
};
use leptos::*; use leptos::*;
use leptos_router::use_params_map; use leptos_router::use_params_map;
@ -16,7 +17,7 @@ pub fn UserProfile() -> impl IntoView {
name = title_.to_string(); name = title_.to_string();
domain = Some(domain_.to_string()); domain = Some(domain_.to_string());
} }
let params = GetUserData { name, domain }; let params = GetUserForm { name, domain };
GlobalState::api_client().get_user(params).await.unwrap() GlobalState::api_client().get_user(params).await.unwrap()
}); });

View File

@ -23,8 +23,7 @@ pub async fn main() -> ibis_lib::backend::error::MyResult<()> {
#[cfg(not(feature = "ssr"))] #[cfg(not(feature = "ssr"))]
fn main() { fn main() {
use ibis_lib::frontend::app::App; use ibis_lib::frontend::app::App;
use leptos::mount_to_body; use leptos::{mount_to_body, view};
use leptos::view;
_ = console_log::init_with_level(log::Level::Debug); _ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();

View File

@ -1,19 +1,26 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use ibis_lib::backend::config::{IbisConfig, IbisConfigDatabase, IbisConfigFederation}; use ibis_lib::{
use ibis_lib::backend::start; backend::{
use ibis_lib::common::RegisterUserData; config::{IbisConfig, IbisConfigDatabase, IbisConfigFederation},
use ibis_lib::frontend::api::ApiClient; start,
use ibis_lib::frontend::error::MyResult; },
common::RegisterUserForm,
frontend::{api::ApiClient, error::MyResult},
};
use reqwest::ClientBuilder; use reqwest::ClientBuilder;
use std::env::current_dir; use std::{
use std::fs::{create_dir_all, remove_dir_all}; env::current_dir,
use std::ops::Deref; fs::{create_dir_all, remove_dir_all},
use std::process::{Command, Stdio}; ops::Deref,
use std::sync::atomic::{AtomicI32, Ordering}; process::{Command, Stdio},
use std::sync::Once; sync::{
use std::thread::{sleep, spawn}; atomic::{AtomicI32, Ordering},
use std::time::Duration; Once,
},
thread::{sleep, spawn},
time::Duration,
};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tracing::log::LevelFilter; use tracing::log::LevelFilter;
@ -128,7 +135,7 @@ impl IbisInstance {
}); });
// wait a moment for the backend to start // wait a moment for the backend to start
tokio::time::sleep(Duration::from_millis(5000)).await; tokio::time::sleep(Duration::from_millis(5000)).await;
let form = RegisterUserData { let form = RegisterUserForm {
username: username.to_string(), username: username.to_string(),
password: "hunter2".to_string(), password: "hunter2".to_string(),
}; };

View File

@ -5,13 +5,23 @@ extern crate ibis_lib;
mod common; mod common;
use crate::common::{TestData, TEST_ARTICLE_DEFAULT_TEXT}; use crate::common::{TestData, TEST_ARTICLE_DEFAULT_TEXT};
use ibis_lib::common::utils::extract_domain; use ibis_lib::{
use ibis_lib::common::{ common::{
ArticleView, EditArticleData, ForkArticleData, GetArticleData, GetUserData, ListArticlesData, utils::extract_domain,
ArticleView,
CreateArticleForm,
EditArticleForm,
ForkArticleForm,
GetArticleForm,
GetUserForm,
ListArticlesForm,
LoginUserForm,
ProtectArticleForm,
RegisterUserForm,
SearchArticleForm,
},
frontend::error::MyResult,
}; };
use ibis_lib::common::{CreateArticleData, SearchArticleData};
use ibis_lib::common::{LoginUserData, RegisterUserData};
use ibis_lib::frontend::error::MyResult;
use pretty_assertions::{assert_eq, assert_ne}; use pretty_assertions::{assert_eq, assert_ne};
use url::Url; use url::Url;
@ -20,7 +30,7 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> {
let data = TestData::start().await; let data = TestData::start().await;
// create article // create article
let create_form = CreateArticleData { let create_form = CreateArticleForm {
title: "Manu_Chao".to_string(), title: "Manu_Chao".to_string(),
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(), summary: "create article".to_string(),
@ -30,7 +40,7 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> {
assert!(create_res.article.local); assert!(create_res.article.local);
// now article can be read // now article can be read
let get_article_data = GetArticleData { let get_article_data = GetArticleForm {
title: Some(create_res.article.title.clone()), title: Some(create_res.article.title.clone()),
domain: None, domain: None,
id: None, id: None,
@ -45,7 +55,7 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> {
assert!(not_found.is_err()); assert!(not_found.is_err());
// edit article // edit article
let edit_form = EditArticleData { let edit_form = EditArticleForm {
article_id: create_res.article.id, article_id: create_res.article.id,
new_text: "Lorem Ipsum 2\n".to_string(), new_text: "Lorem Ipsum 2\n".to_string(),
summary: "summary".to_string(), summary: "summary".to_string(),
@ -57,7 +67,7 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> {
assert_eq!(2, edit_res.edits.len()); assert_eq!(2, edit_res.edits.len());
assert_eq!(edit_form.summary, edit_res.edits[1].edit.summary); assert_eq!(edit_form.summary, edit_res.edits[1].edit.summary);
let search_form = SearchArticleData { let search_form = SearchArticleForm {
query: create_form.title.clone(), query: create_form.title.clone(),
}; };
let search_res = data.alpha.search(&search_form).await?; let search_res = data.alpha.search(&search_form).await?;
@ -66,7 +76,7 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> {
let list_articles = data let list_articles = data
.alpha .alpha
.list_articles(ListArticlesData { .list_articles(ListArticlesForm {
only_local: Some(false), only_local: Some(false),
}) })
.await?; .await?;
@ -81,7 +91,7 @@ async fn test_create_duplicate_article() -> MyResult<()> {
let data = TestData::start().await; let data = TestData::start().await;
// create article // create article
let create_form = CreateArticleData { let create_form = CreateArticleForm {
title: "Manu_Chao".to_string(), title: "Manu_Chao".to_string(),
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(), summary: "create article".to_string(),
@ -127,7 +137,7 @@ async fn test_synchronize_articles() -> MyResult<()> {
let data = TestData::start().await; let data = TestData::start().await;
// create article on alpha // create article on alpha
let create_form = CreateArticleData { let create_form = CreateArticleForm {
title: "Manu_Chao".to_string(), title: "Manu_Chao".to_string(),
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(), summary: "create article".to_string(),
@ -138,7 +148,7 @@ async fn test_synchronize_articles() -> MyResult<()> {
assert!(create_res.article.local); assert!(create_res.article.local);
// edit the article // edit the article
let edit_form = EditArticleData { let edit_form = EditArticleForm {
article_id: create_res.article.id, article_id: create_res.article.id,
new_text: "Lorem Ipsum 2\n".to_string(), new_text: "Lorem Ipsum 2\n".to_string(),
summary: "summary".to_string(), summary: "summary".to_string(),
@ -153,7 +163,7 @@ async fn test_synchronize_articles() -> MyResult<()> {
.resolve_instance(Url::parse(&format!("http://{}", &data.alpha.hostname))?) .resolve_instance(Url::parse(&format!("http://{}", &data.alpha.hostname))?)
.await?; .await?;
let mut get_article_data = GetArticleData { let mut get_article_data = GetArticleForm {
title: Some(create_res.article.title), title: Some(create_res.article.title),
domain: None, domain: None,
id: None, id: None,
@ -185,7 +195,7 @@ async fn test_edit_local_article() -> MyResult<()> {
.await?; .await?;
// create new article // create new article
let create_form = CreateArticleData { let create_form = CreateArticleForm {
title: "Manu_Chao".to_string(), title: "Manu_Chao".to_string(),
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(), summary: "create article".to_string(),
@ -195,7 +205,7 @@ async fn test_edit_local_article() -> MyResult<()> {
assert!(create_res.article.local); assert!(create_res.article.local);
// article should be federated to alpha // article should be federated to alpha
let get_article_data = GetArticleData { let get_article_data = GetArticleForm {
title: Some(create_res.article.title.to_string()), title: Some(create_res.article.title.to_string()),
domain: Some(beta_instance.domain), domain: Some(beta_instance.domain),
id: None, id: None,
@ -207,7 +217,7 @@ async fn test_edit_local_article() -> MyResult<()> {
assert_eq!(create_res.article.text, get_res.article.text); assert_eq!(create_res.article.text, get_res.article.text);
// edit the article // edit the article
let edit_form = EditArticleData { let edit_form = EditArticleForm {
article_id: create_res.article.id, article_id: create_res.article.id,
new_text: "Lorem Ipsum 2\n".to_string(), new_text: "Lorem Ipsum 2\n".to_string(),
summary: "summary".to_string(), summary: "summary".to_string(),
@ -246,7 +256,7 @@ async fn test_edit_remote_article() -> MyResult<()> {
.await?; .await?;
// create new article // create new article
let create_form = CreateArticleData { let create_form = CreateArticleForm {
title: "Manu_Chao".to_string(), title: "Manu_Chao".to_string(),
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(), summary: "create article".to_string(),
@ -256,7 +266,7 @@ async fn test_edit_remote_article() -> MyResult<()> {
assert!(create_res.article.local); assert!(create_res.article.local);
// article should be federated to alpha and gamma // article should be federated to alpha and gamma
let get_article_data_alpha = GetArticleData { let get_article_data_alpha = GetArticleForm {
title: Some(create_res.article.title.to_string()), title: Some(create_res.article.title.to_string()),
domain: Some(beta_id_on_alpha.domain), domain: Some(beta_id_on_alpha.domain),
id: None, id: None,
@ -269,7 +279,7 @@ async fn test_edit_remote_article() -> MyResult<()> {
assert_eq!(1, get_res.edits.len()); assert_eq!(1, get_res.edits.len());
assert!(!get_res.article.local); assert!(!get_res.article.local);
let get_article_data_gamma = GetArticleData { let get_article_data_gamma = GetArticleForm {
title: Some(create_res.article.title.to_string()), title: Some(create_res.article.title.to_string()),
domain: Some(beta_id_on_gamma.domain), domain: Some(beta_id_on_gamma.domain),
id: None, id: None,
@ -281,7 +291,7 @@ async fn test_edit_remote_article() -> MyResult<()> {
assert_eq!(create_res.article.title, get_res.article.title); assert_eq!(create_res.article.title, get_res.article.title);
assert_eq!(create_res.article.text, get_res.article.text); assert_eq!(create_res.article.text, get_res.article.text);
let edit_form = EditArticleData { let edit_form = EditArticleForm {
article_id: get_res.article.id, article_id: get_res.article.id,
new_text: "Lorem Ipsum 2\n".to_string(), new_text: "Lorem Ipsum 2\n".to_string(),
summary: "summary".to_string(), summary: "summary".to_string(),
@ -317,7 +327,7 @@ async fn test_local_edit_conflict() -> MyResult<()> {
let data = TestData::start().await; let data = TestData::start().await;
// create new article // create new article
let create_form = CreateArticleData { let create_form = CreateArticleForm {
title: "Manu_Chao".to_string(), title: "Manu_Chao".to_string(),
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(), summary: "create article".to_string(),
@ -327,7 +337,7 @@ async fn test_local_edit_conflict() -> MyResult<()> {
assert!(create_res.article.local); assert!(create_res.article.local);
// one user edits article // one user edits article
let edit_form = EditArticleData { let edit_form = EditArticleForm {
article_id: create_res.article.id, article_id: create_res.article.id,
new_text: "Lorem Ipsum\n".to_string(), new_text: "Lorem Ipsum\n".to_string(),
summary: "summary".to_string(), summary: "summary".to_string(),
@ -339,7 +349,7 @@ async fn test_local_edit_conflict() -> MyResult<()> {
assert_eq!(2, edit_res.edits.len()); assert_eq!(2, edit_res.edits.len());
// another user edits article, without being aware of previous edit // another user edits article, without being aware of previous edit
let edit_form = EditArticleData { let edit_form = EditArticleForm {
article_id: create_res.article.id, article_id: create_res.article.id,
new_text: "Ipsum Lorem\n".to_string(), new_text: "Ipsum Lorem\n".to_string(),
summary: "summary".to_string(), summary: "summary".to_string(),
@ -357,7 +367,7 @@ async fn test_local_edit_conflict() -> MyResult<()> {
assert_eq!(1, conflicts.len()); assert_eq!(1, conflicts.len());
assert_eq!(conflicts[0], edit_res); assert_eq!(conflicts[0], edit_res);
let edit_form = EditArticleData { let edit_form = EditArticleForm {
article_id: create_res.article.id, article_id: create_res.article.id,
new_text: "Lorem Ipsum and Ipsum Lorem\n".to_string(), new_text: "Lorem Ipsum and Ipsum Lorem\n".to_string(),
summary: "summary".to_string(), summary: "summary".to_string(),
@ -383,7 +393,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
.await?; .await?;
// create new article // create new article
let create_form = CreateArticleData { let create_form = CreateArticleForm {
title: "Manu_Chao".to_string(), title: "Manu_Chao".to_string(),
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(), summary: "create article".to_string(),
@ -400,7 +410,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
assert_eq!(create_res.article.text, resolve_res.article.text); assert_eq!(create_res.article.text, resolve_res.article.text);
// alpha edits article // alpha edits article
let get_article_data = GetArticleData { let get_article_data = GetArticleForm {
title: Some(create_form.title.to_string()), title: Some(create_form.title.to_string()),
domain: Some(beta_id_on_alpha.domain), domain: Some(beta_id_on_alpha.domain),
id: None, id: None,
@ -408,7 +418,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
let get_res = data.alpha.get_article(get_article_data).await?; let get_res = data.alpha.get_article(get_article_data).await?;
assert_eq!(&create_res.edits.len(), &get_res.edits.len()); assert_eq!(&create_res.edits.len(), &get_res.edits.len());
assert_eq!(&create_res.edits[0].edit.hash, &get_res.edits[0].edit.hash); assert_eq!(&create_res.edits[0].edit.hash, &get_res.edits[0].edit.hash);
let edit_form = EditArticleData { let edit_form = EditArticleForm {
article_id: get_res.article.id, article_id: get_res.article.id,
new_text: "Lorem Ipsum\n".to_string(), new_text: "Lorem Ipsum\n".to_string(),
summary: "summary".to_string(), summary: "summary".to_string(),
@ -427,7 +437,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
// gamma also edits, as its not the latest version there is a conflict. local version should // gamma also edits, as its not the latest version there is a conflict. local version should
// not be updated with this conflicting version, instead user needs to handle the conflict // not be updated with this conflicting version, instead user needs to handle the conflict
let edit_form = EditArticleData { let edit_form = EditArticleForm {
article_id: resolve_res.article.id, article_id: resolve_res.article.id,
new_text: "aaaa\n".to_string(), new_text: "aaaa\n".to_string(),
summary: "summary".to_string(), summary: "summary".to_string(),
@ -443,7 +453,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
assert_eq!(1, conflicts.len()); assert_eq!(1, conflicts.len());
// resolve the conflict // resolve the conflict
let edit_form = EditArticleData { let edit_form = EditArticleForm {
article_id: resolve_res.article.id, article_id: resolve_res.article.id,
new_text: "aaaa\n".to_string(), new_text: "aaaa\n".to_string(),
summary: "summary".to_string(), summary: "summary".to_string(),
@ -465,7 +475,7 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
let data = TestData::start().await; let data = TestData::start().await;
// create new article // create new article
let create_form = CreateArticleData { let create_form = CreateArticleForm {
title: "Manu_Chao".to_string(), title: "Manu_Chao".to_string(),
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(), summary: "create article".to_string(),
@ -475,7 +485,7 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
assert!(create_res.article.local); assert!(create_res.article.local);
// one user edits article // one user edits article
let edit_form = EditArticleData { let edit_form = EditArticleForm {
article_id: create_res.article.id, article_id: create_res.article.id,
new_text: "my\nexample\ntext\n".to_string(), new_text: "my\nexample\ntext\n".to_string(),
summary: "summary".to_string(), summary: "summary".to_string(),
@ -487,7 +497,7 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
assert_eq!(2, edit_res.edits.len()); assert_eq!(2, edit_res.edits.len());
// another user edits article, without being aware of previous edit // another user edits article, without being aware of previous edit
let edit_form = EditArticleData { let edit_form = EditArticleForm {
article_id: create_res.article.id, article_id: create_res.article.id,
new_text: "some\nexample\narticle\n".to_string(), new_text: "some\nexample\narticle\n".to_string(),
summary: "summary".to_string(), summary: "summary".to_string(),
@ -508,7 +518,7 @@ async fn test_fork_article() -> MyResult<()> {
let data = TestData::start().await; let data = TestData::start().await;
// create article // create article
let create_form = CreateArticleData { let create_form = CreateArticleForm {
title: "Manu_Chao".to_string(), title: "Manu_Chao".to_string(),
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(), summary: "create article".to_string(),
@ -526,7 +536,7 @@ async fn test_fork_article() -> MyResult<()> {
assert_eq!(create_res.edits.len(), resolve_res.edits.len()); assert_eq!(create_res.edits.len(), resolve_res.edits.len());
// fork the article to local instance // fork the article to local instance
let fork_form = ForkArticleData { let fork_form = ForkArticleForm {
article_id: resolved_article.id, article_id: resolved_article.id,
new_title: resolved_article.title.clone(), new_title: resolved_article.title.clone(),
}; };
@ -546,7 +556,7 @@ async fn test_fork_article() -> MyResult<()> {
assert_eq!(forked_article.instance_id, beta_instance.instance.id); assert_eq!(forked_article.instance_id, beta_instance.instance.id);
// now search returns two articles for this title (original and forked) // now search returns two articles for this title (original and forked)
let search_form = SearchArticleData { let search_form = SearchArticleForm {
query: create_form.title.clone(), query: create_form.title.clone(),
}; };
let search_res = data.beta.search(&search_form).await?; let search_res = data.beta.search(&search_form).await?;
@ -560,20 +570,20 @@ async fn test_user_registration_login() -> MyResult<()> {
let data = TestData::start().await; let data = TestData::start().await;
let username = "my_user"; let username = "my_user";
let password = "hunter2"; let password = "hunter2";
let register_data = RegisterUserData { let register_data = RegisterUserForm {
username: username.to_string(), username: username.to_string(),
password: password.to_string(), password: password.to_string(),
}; };
data.alpha.register(register_data).await?; data.alpha.register(register_data).await?;
let login_data = LoginUserData { let login_data = LoginUserForm {
username: username.to_string(), username: username.to_string(),
password: "asd123".to_string(), password: "asd123".to_string(),
}; };
let invalid_login = data.alpha.login(login_data).await; let invalid_login = data.alpha.login(login_data).await;
assert!(invalid_login.is_err()); assert!(invalid_login.is_err());
let login_data = LoginUserData { let login_data = LoginUserForm {
username: username.to_string(), username: username.to_string(),
password: password.to_string(), password: password.to_string(),
}; };
@ -595,7 +605,7 @@ async fn test_user_profile() -> MyResult<()> {
let data = TestData::start().await; let data = TestData::start().await;
// Create an article and federate it, in order to federate the user who created it // Create an article and federate it, in order to federate the user who created it
let create_form = CreateArticleData { let create_form = CreateArticleForm {
title: "Manu_Chao".to_string(), title: "Manu_Chao".to_string(),
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(), summary: "create article".to_string(),
@ -607,7 +617,7 @@ async fn test_user_profile() -> MyResult<()> {
let domain = extract_domain(&data.alpha.my_profile().await?.person.ap_id); let domain = extract_domain(&data.alpha.my_profile().await?.person.ap_id);
// Now we can fetch the remote user from local api // Now we can fetch the remote user from local api
let params = GetUserData { let params = GetUserForm {
name: "alpha".to_string(), name: "alpha".to_string(),
domain: Some(domain), domain: Some(domain),
}; };
@ -617,3 +627,50 @@ async fn test_user_profile() -> MyResult<()> {
data.stop() data.stop()
} }
#[tokio::test]
async fn test_lock_article() -> MyResult<()> {
let data = TestData::start().await;
// create article
let create_form = CreateArticleForm {
title: "Manu_Chao".to_string(),
text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
summary: "create article".to_string(),
};
let create_res = data.alpha.create_article(&create_form).await?;
assert!(!create_res.article.protected);
// lock from normal user fails
let lock_form = ProtectArticleForm {
article_id: create_res.article.id,
protected: true,
};
let lock_res = data.alpha.protect_article(&lock_form).await;
assert!(lock_res.is_err());
// login as admin to lock article
let form = LoginUserForm {
username: "ibis".to_string(),
password: "ibis".to_string(),
};
data.alpha.login(form).await?;
let lock_res = data.alpha.protect_article(&lock_form).await?;
assert!(lock_res.protected);
let resolve_res: ArticleView = data
.gamma
.resolve_article(create_res.article.ap_id.inner().clone())
.await?;
let edit_form = EditArticleForm {
article_id: resolve_res.article.id,
new_text: "test".to_string(),
summary: "test".to_string(),
previous_version_id: resolve_res.latest_version,
resolve_conflict_id: None,
};
let edit_res = data.gamma.edit_article(&edit_form).await;
assert!(edit_res.is_err());
data.stop()
}