1
0
Fork 0
mirror of https://github.com/Nutomic/ibis.git synced 2025-02-04 06:01:36 +00:00

Also split up files in src/common.rs

This commit is contained in:
Felix Ableitner 2025-01-15 14:28:19 +01:00
parent 95ee090275
commit 0194ddf663
51 changed files with 504 additions and 448 deletions

View file

@ -11,25 +11,27 @@ use crate::{
utils::{error::MyResult, generate_article_version}, utils::{error::MyResult, generate_article_version},
}, },
common::{ common::{
article::{
ApiConflict,
ApproveArticleForm,
ArticleView,
CreateArticleForm,
DbArticle,
DbEdit,
DeleteConflictForm,
EditArticleForm,
EditVersion,
ForkArticleForm,
GetArticleForm,
ListArticlesForm,
ProtectArticleForm,
SearchArticleForm,
},
instance::DbInstance,
user::LocalUserView,
utils::{extract_domain, http_protocol_str}, utils::{extract_domain, http_protocol_str},
validation::can_edit_article, validation::can_edit_article,
ApiConflict,
ApproveArticleForm,
ArticleView,
CreateArticleForm,
DbArticle,
DbEdit,
DbInstance,
DeleteConflictForm,
EditArticleForm,
EditVersion,
ForkArticleForm,
GetArticleForm,
ListArticlesForm,
LocalUserView,
ProtectArticleForm,
ResolveObject, ResolveObject,
SearchArticleForm,
}, },
}; };
use activitypub_federation::{config::Data, fetch::object_id::ObjectId}; use activitypub_federation::{config::Data, fetch::object_id::ObjectId};

View file

@ -1,11 +1,8 @@
use crate::{ use crate::{
backend::{database::IbisData, federation::activities::follow::Follow, utils::error::MyResult}, backend::{database::IbisData, federation::activities::follow::Follow, utils::error::MyResult},
common::{ common::{
DbInstance, instance::{DbInstance, FollowInstance, GetInstance, InstanceView},
FollowInstance, user::LocalUserView,
GetInstance,
InstanceView,
LocalUserView,
ResolveObject, ResolveObject,
SuccessResponse, SuccessResponse,
}, },

View file

@ -18,7 +18,11 @@ use crate::{
database::IbisData, database::IbisData,
utils::error::MyResult, utils::error::MyResult,
}, },
common::{DbEdit, EditView, GetEditList, LocalUserView, SiteView}, common::{
article::{DbEdit, EditView, GetEditList},
instance::SiteView,
user::LocalUserView,
},
}; };
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use anyhow::anyhow; use anyhow::anyhow;

View file

@ -5,15 +5,17 @@ use crate::{
utils::error::MyResult, utils::error::MyResult,
}, },
common::{ common::{
DbArticle, article::DbArticle,
DbPerson, user::{
GetUserForm, DbPerson,
LocalUserView, GetUserForm,
LoginUserForm, LocalUserView,
LoginUserForm,
RegisterUserForm,
UpdateUserForm,
},
Notification, Notification,
RegisterUserForm,
SuccessResponse, SuccessResponse,
UpdateUserForm,
AUTH_COOKIE, AUTH_COOKIE,
}, },
}; };

View file

@ -1,4 +1,4 @@
use crate::{backend::utils::error::MyResult, common::Options}; use crate::{backend::utils::error::MyResult, common::instance::Options};
use config::Config; use config::Config;
use doku::Document; use doku::Document;
use serde::Deserialize; use serde::Deserialize;

View file

@ -8,11 +8,9 @@ use crate::{
utils::error::MyResult, utils::error::MyResult,
}, },
common::{ common::{
article::{ArticleView, DbArticle, EditVersion},
instance::DbInstance,
newtypes::{ArticleId, InstanceId}, newtypes::{ArticleId, InstanceId},
ArticleView,
DbArticle,
DbInstance,
EditVersion,
}, },
}; };
use activitypub_federation::fetch::{collection_id::CollectionId, object_id::ObjectId}; use activitypub_federation::fetch::{collection_id::CollectionId, object_id::ObjectId};

View file

@ -5,12 +5,9 @@ use crate::{
utils::{error::MyResult, generate_article_version}, utils::{error::MyResult, generate_article_version},
}, },
common::{ common::{
article::{ApiConflict, DbArticle, DbEdit, EditVersion},
newtypes::{ArticleId, ConflictId, PersonId}, newtypes::{ArticleId, ConflictId, PersonId},
ApiConflict, user::DbPerson,
DbArticle,
DbEdit,
DbPerson,
EditVersion,
}, },
}; };
use activitypub_federation::config::Data; use activitypub_federation::config::Data;

View file

@ -5,11 +5,8 @@ use crate::{
IbisData, IbisData,
}, },
common::{ common::{
article::{DbArticle, DbEdit, EditVersion, EditView},
newtypes::{ArticleId, PersonId}, newtypes::{ArticleId, PersonId},
DbArticle,
DbEdit,
EditVersion,
EditView,
}, },
}; };
use activitypub_federation::fetch::object_id::ObjectId; use activitypub_federation::fetch::object_id::ObjectId;

View file

@ -10,7 +10,11 @@ use crate::{
}, },
utils::error::MyResult, utils::error::MyResult,
}, },
common::{newtypes::InstanceId, DbInstance, DbPerson, InstanceView}, common::{
instance::{DbInstance, InstanceView},
newtypes::InstanceId,
user::DbPerson,
},
}; };
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,

View file

@ -7,13 +7,10 @@ use crate::{
utils::{error::MyResult, generate_keypair}, utils::{error::MyResult, generate_keypair},
}, },
common::{ common::{
instance::DbInstance,
newtypes::PersonId, newtypes::PersonId,
user::{DbLocalUser, DbPerson, LocalUserView, UpdateUserForm},
utils::http_protocol_str, utils::http_protocol_str,
DbInstance,
DbLocalUser,
DbPerson,
LocalUserView,
UpdateUserForm,
}, },
}; };
use activitypub_federation::{config::Data, fetch::object_id::ObjectId}; use activitypub_federation::{config::Data, fetch::object_id::ObjectId};

View file

@ -7,7 +7,7 @@ use crate::{
generate_activity_id, generate_activity_id,
}, },
}, },
common::DbInstance, common::instance::DbInstance,
}; };
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,

View file

@ -7,7 +7,7 @@ use crate::{
generate_activity_id, generate_activity_id,
}, },
}, },
common::{DbArticle, DbInstance}, common::{article::DbArticle, instance::DbInstance},
}; };
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,

View file

@ -5,7 +5,7 @@ use crate::{
generate_activity_id, generate_activity_id,
utils::error::{Error, MyResult}, utils::error::{Error, MyResult},
}, },
common::{DbInstance, DbPerson}, common::{instance::DbInstance, user::DbPerson},
}; };
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,

View file

@ -8,11 +8,9 @@ use crate::{
utils::error::Error, utils::error::Error,
}, },
common::{ common::{
article::{DbArticle, DbEdit, EditVersion},
instance::DbInstance,
newtypes::{EditId, PersonId}, newtypes::{EditId, PersonId},
DbArticle,
DbEdit,
DbInstance,
EditVersion,
}, },
}; };
use activitypub_federation::config::Data; use activitypub_federation::config::Data;

View file

@ -10,7 +10,7 @@ use crate::{
generate_activity_id, generate_activity_id,
}, },
}, },
common::{DbInstance, EditVersion}, common::{article::EditVersion, instance::DbInstance},
}; };
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,

View file

@ -7,7 +7,7 @@ use crate::{
generate_activity_id, generate_activity_id,
}, },
}, },
common::{DbArticle, DbInstance}, common::{article::DbArticle, instance::DbInstance},
}; };
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,

View file

@ -11,7 +11,11 @@ use crate::{
generate_activity_id, generate_activity_id,
}, },
}, },
common::{validation::can_edit_article, DbArticle, DbEdit, DbInstance}, common::{
article::{DbArticle, DbEdit},
instance::DbInstance,
validation::can_edit_article,
},
}; };
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,

View file

@ -4,7 +4,10 @@ use crate::{
federation::objects::edits_collection::DbEditCollection, federation::objects::edits_collection::DbEditCollection,
utils::error::Error, utils::error::Error,
}, },
common::{DbArticle, DbInstance, EditVersion}, common::{
article::{DbArticle, EditVersion},
instance::DbInstance,
},
}; };
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,

View file

@ -4,7 +4,7 @@ use crate::{
federation::objects::article::ApubArticle, federation::objects::article::ApubArticle,
utils::error::{Error, MyResult}, utils::error::{Error, MyResult},
}, },
common::{utils::http_protocol_str, DbArticle}, common::{article::DbArticle, utils::http_protocol_str},
}; };
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,

View file

@ -3,7 +3,10 @@ use crate::{
database::{edit::DbEditForm, IbisData}, database::{edit::DbEditForm, IbisData},
utils::error::Error, utils::error::Error,
}, },
common::{DbArticle, DbEdit, DbPerson, EditVersion}, common::{
article::{DbArticle, DbEdit, EditVersion},
user::DbPerson,
},
}; };
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
backend::{database::IbisData, federation::objects::edit::ApubEdit, utils::error::Error}, backend::{database::IbisData, federation::objects::edit::ApubEdit, utils::error::Error},
common::{DbArticle, DbEdit}, common::article::{DbArticle, DbEdit},
}; };
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,

View file

@ -5,7 +5,7 @@ use crate::{
federation::{objects::articles_collection::DbArticleCollection, send_activity}, federation::{objects::articles_collection::DbArticleCollection, send_activity},
utils::error::{Error, MyResult}, utils::error::{Error, MyResult},
}, },
common::{utils::extract_domain, DbInstance}, common::{instance::DbInstance, utils::extract_domain},
}; };
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,

View file

@ -4,7 +4,7 @@ use crate::{
database::IbisData, database::IbisData,
utils::error::{Error, MyResult}, utils::error::{Error, MyResult},
}, },
common::{utils::http_protocol_str, DbInstance}, common::{instance::DbInstance, utils::http_protocol_str},
}; };
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,

View file

@ -3,7 +3,7 @@ use crate::{
database::{user::DbPersonForm, IbisData}, database::{user::DbPersonForm, IbisData},
utils::error::Error, utils::error::Error,
}, },
common::DbPerson, common::user::DbPerson,
}; };
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,

View file

@ -21,7 +21,7 @@ use crate::{
}, },
utils::error::{Error, MyResult}, utils::error::{Error, MyResult},
}, },
common::{DbArticle, DbInstance, DbPerson}, common::{article::DbArticle, instance::DbInstance, user::DbPerson},
}; };
use activitypub_federation::{ use activitypub_federation::{
axum::{ axum::{

View file

@ -9,11 +9,10 @@ use crate::{
}, },
}, },
common::{ common::{
article::{DbArticle, EditVersion},
instance::DbInstance,
user::DbPerson,
utils::http_protocol_str, utils::http_protocol_str,
DbArticle,
DbInstance,
DbPerson,
EditVersion,
MAIN_PAGE_NAME, MAIN_PAGE_NAME,
}, },
}; };

View file

@ -1,6 +1,9 @@
use crate::{ use crate::{
backend::{database::IbisData, utils::error::MyResult}, backend::{database::IbisData, utils::error::MyResult},
common::{utils, DbEdit, EditVersion}, common::{
article::{DbEdit, EditVersion},
utils,
},
}; };
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,
@ -57,8 +60,8 @@ pub(super) fn generate_article_version(
mod test { mod test {
use super::*; use super::*;
use crate::common::{ use crate::common::{
article::DbEdit,
newtypes::{ArticleId, EditId, PersonId}, newtypes::{ArticleId, EditId, PersonId},
DbEdit,
}; };
use activitypub_federation::fetch::object_id::ObjectId; use activitypub_federation::fetch::object_id::ObjectId;
use chrono::Utc; use chrono::Utc;

193
src/common/article.rs Normal file
View file

@ -0,0 +1,193 @@
use super::{
instance::DbInstance,
newtypes::{ArticleId, ConflictId, EditId, InstanceId, PersonId},
user::DbPerson,
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use uuid::Uuid;
#[cfg(feature = "ssr")]
use {
crate::backend::database::schema::{article, edit},
activitypub_federation::fetch::object_id::ObjectId,
diesel::{Identifiable, Queryable, Selectable},
};
/// Should be an enum Title/Id but fails due to https://github.com/nox/serde_urlencoded/issues/66
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
pub struct GetArticleForm {
pub title: Option<String>,
pub domain: Option<String>,
pub id: Option<ArticleId>,
}
#[derive(Deserialize, Serialize, Clone, Default, Debug)]
pub struct ListArticlesForm {
pub only_local: Option<bool>,
pub instance_id: Option<InstanceId>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable))]
#[cfg_attr(feature = "ssr", diesel(table_name = article, check_for_backend(diesel::pg::Pg)))]
pub struct ArticleView {
pub article: DbArticle,
pub instance: DbInstance,
pub latest_version: EditVersion,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Identifiable))]
#[cfg_attr(feature = "ssr", diesel(table_name = article, check_for_backend(diesel::pg::Pg), belongs_to(DbInstance, foreign_key = instance_id)))]
pub struct DbArticle {
pub id: ArticleId,
pub title: String,
pub text: String,
#[cfg(feature = "ssr")]
pub ap_id: ObjectId<DbArticle>,
#[cfg(not(feature = "ssr"))]
pub ap_id: String,
pub instance_id: InstanceId,
pub local: bool,
pub protected: bool,
pub approved: bool,
pub published: DateTime<Utc>,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct CreateArticleForm {
pub title: String,
pub text: String,
pub summary: String,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct EditArticleForm {
/// Id of the article to edit
pub article_id: ArticleId,
/// Full, new text of the article. A diff against `previous_version` is generated on the backend
/// side to handle conflicts.
pub new_text: String,
/// What was changed
pub summary: String,
/// The version that this edit is based on, ie [DbArticle.latest_version] or
/// [ApiConflict.previous_version]
pub previous_version_id: EditVersion,
/// If you are resolving a conflict, pass the id to delete conflict from the database
pub resolve_conflict_id: Option<ConflictId>,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct ProtectArticleForm {
pub article_id: ArticleId,
pub protected: bool,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct ForkArticleForm {
pub article_id: ArticleId,
pub new_title: String,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct ApproveArticleForm {
pub article_id: ArticleId,
pub approve: bool,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct SearchArticleForm {
pub query: String,
}
/// Represents a single change to the article.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable))]
#[cfg_attr(feature = "ssr", diesel(table_name = edit, check_for_backend(diesel::pg::Pg)))]
pub struct DbEdit {
// TODO: we could use hash as primary key, but that gives errors on forking because
// the same edit is used for multiple articles
pub id: EditId,
#[serde(skip)]
pub creator_id: PersonId,
/// UUID built from sha224 hash of diff
pub hash: EditVersion,
#[cfg(feature = "ssr")]
pub ap_id: ObjectId<DbEdit>,
#[cfg(not(feature = "ssr"))]
pub ap_id: String,
pub diff: String,
pub summary: String,
pub article_id: ArticleId,
/// First edit of an article always has `EditVersion::default()` here
pub previous_version_id: EditVersion,
pub published: DateTime<Utc>,
}
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
pub struct GetEditList {
pub article_id: Option<ArticleId>,
pub person_id: Option<PersonId>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable))]
#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))]
pub struct EditView {
pub edit: DbEdit,
pub article: DbArticle,
pub creator: DbPerson,
}
/// The version hash of a specific edit. Generated by taking an SHA256 hash of the diff
/// and using the first 16 bytes so that it fits into UUID.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "ssr", derive(diesel_derive_newtype::DieselNewType))]
pub struct EditVersion(pub(crate) Uuid);
impl EditVersion {
pub fn new(diff: &str) -> Self {
let mut sha256 = Sha256::new();
sha256.update(diff);
let hash_bytes = sha256.finalize();
let uuid =
Uuid::from_slice(&hash_bytes.as_slice()[..16]).expect("hash is correct size for uuid");
EditVersion(uuid)
}
pub fn hash(&self) -> String {
hex::encode(self.0.into_bytes())
}
}
impl Default for EditVersion {
fn default() -> Self {
EditVersion::new("")
}
}
#[derive(Deserialize, Serialize, Debug)]
pub struct DeleteConflictForm {
pub conflict_id: ConflictId,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct ApiConflict {
pub id: ConflictId,
pub hash: EditVersion,
pub three_way_merge: String,
pub summary: String,
pub article: DbArticle,
pub previous_version_id: EditVersion,
pub published: DateTime<Utc>,
}
#[test]
fn test_edit_versions() {
let default = EditVersion::default();
assert_eq!("e3b0c44298fc1c149afbf4c8996fb924", default.hash());
let version = EditVersion::new("test");
assert_eq!("9f86d081884c7d659a2feaa0c55ad015", version.hash());
}

93
src/common/instance.rs Normal file
View file

@ -0,0 +1,93 @@
use super::{
newtypes::InstanceId,
user::{DbPerson, LocalUserView},
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use smart_default::SmartDefault;
use url::Url;
#[cfg(feature = "ssr")]
use {
crate::backend::{
database::schema::instance,
federation::objects::articles_collection::DbArticleCollection,
federation::objects::instance_collection::DbInstanceCollection,
},
activitypub_federation::fetch::{collection_id::CollectionId, object_id::ObjectId},
diesel::{Identifiable, Queryable, Selectable},
doku::Document,
};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Identifiable))]
#[cfg_attr(feature = "ssr", diesel(table_name = instance, check_for_backend(diesel::pg::Pg)))]
pub struct DbInstance {
pub id: InstanceId,
pub domain: String,
#[cfg(feature = "ssr")]
pub ap_id: ObjectId<DbInstance>,
#[cfg(not(feature = "ssr"))]
pub ap_id: String,
pub description: Option<String>,
#[cfg(feature = "ssr")]
pub articles_url: Option<CollectionId<DbArticleCollection>>,
#[cfg(not(feature = "ssr"))]
pub articles_url: String,
pub inbox_url: String,
#[serde(skip)]
pub public_key: String,
#[serde(skip)]
pub private_key: Option<String>,
pub last_refreshed_at: DateTime<Utc>,
pub local: bool,
#[cfg(feature = "ssr")]
pub instances_url: Option<CollectionId<DbInstanceCollection>>,
}
impl DbInstance {
pub fn inbox_url(&self) -> Url {
Url::parse(&self.inbox_url).expect("can parse inbox url")
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable))]
#[cfg_attr(feature = "ssr", diesel(table_name = article, check_for_backend(diesel::pg::Pg)))]
pub struct InstanceView {
pub instance: DbInstance,
pub followers: Vec<DbPerson>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, SmartDefault)]
#[serde(default)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "ssr", derive(Queryable, Document))]
#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))]
pub struct Options {
/// Whether users can create new accounts
#[default = true]
#[cfg_attr(feature = "ssr", doku(example = "true"))]
pub registration_open: bool,
/// Whether admins need to approve new articles
#[default = false]
#[cfg_attr(feature = "ssr", doku(example = "false"))]
pub article_approval: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
#[cfg_attr(feature = "ssr", derive(Queryable))]
#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))]
pub struct SiteView {
pub my_profile: Option<LocalUserView>,
pub config: Options,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct GetInstance {
pub id: Option<InstanceId>,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct FollowInstance {
pub id: InstanceId,
}

View file

@ -1,25 +1,14 @@
pub mod article;
pub mod instance;
pub mod newtypes; pub mod newtypes;
pub mod user;
pub mod utils; pub mod utils;
pub mod validation; pub mod validation;
use article::{ApiConflict, DbArticle};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use newtypes::{ArticleId, ConflictId, EditId, InstanceId, PersonId};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use smart_default::SmartDefault;
use url::Url; use url::Url;
use uuid::Uuid;
#[cfg(feature = "ssr")]
use {
crate::backend::{
database::schema::{article, edit, instance, local_user, person},
federation::objects::articles_collection::DbArticleCollection,
federation::objects::instance_collection::DbInstanceCollection,
},
activitypub_federation::fetch::{collection_id::CollectionId, object_id::ObjectId},
diesel::{Identifiable, Queryable, Selectable},
doku::Document,
};
pub const MAIN_PAGE_NAME: &str = "Main_Page"; pub const MAIN_PAGE_NAME: &str = "Main_Page";
@ -28,231 +17,6 @@ pub static AUTH_COOKIE: &str = "auth";
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Auth(pub Option<String>); pub struct Auth(pub Option<String>);
/// Should be an enum Title/Id but fails due to https://github.com/nox/serde_urlencoded/issues/66
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
pub struct GetArticleForm {
pub title: Option<String>,
pub domain: Option<String>,
pub id: Option<ArticleId>,
}
#[derive(Deserialize, Serialize, Clone, Default, Debug)]
pub struct ListArticlesForm {
pub only_local: Option<bool>,
pub instance_id: Option<InstanceId>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable))]
#[cfg_attr(feature = "ssr", diesel(table_name = article, check_for_backend(diesel::pg::Pg)))]
pub struct ArticleView {
pub article: DbArticle,
pub instance: DbInstance,
pub latest_version: EditVersion,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Identifiable))]
#[cfg_attr(feature = "ssr", diesel(table_name = article, check_for_backend(diesel::pg::Pg), belongs_to(DbInstance, foreign_key = instance_id)))]
pub struct DbArticle {
pub id: ArticleId,
pub title: String,
pub text: String,
#[cfg(feature = "ssr")]
pub ap_id: ObjectId<DbArticle>,
#[cfg(not(feature = "ssr"))]
pub ap_id: String,
pub instance_id: InstanceId,
pub local: bool,
pub protected: bool,
pub approved: bool,
pub published: DateTime<Utc>,
}
/// Represents a single change to the article.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable))]
#[cfg_attr(feature = "ssr", diesel(table_name = edit, check_for_backend(diesel::pg::Pg)))]
pub struct DbEdit {
// TODO: we could use hash as primary key, but that gives errors on forking because
// the same edit is used for multiple articles
pub id: EditId,
#[serde(skip)]
pub creator_id: PersonId,
/// UUID built from sha224 hash of diff
pub hash: EditVersion,
#[cfg(feature = "ssr")]
pub ap_id: ObjectId<DbEdit>,
#[cfg(not(feature = "ssr"))]
pub ap_id: String,
pub diff: String,
pub summary: String,
pub article_id: ArticleId,
/// First edit of an article always has `EditVersion::default()` here
pub previous_version_id: EditVersion,
pub published: DateTime<Utc>,
}
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
pub struct GetEditList {
pub article_id: Option<ArticleId>,
pub person_id: Option<PersonId>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable))]
#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))]
pub struct EditView {
pub edit: DbEdit,
pub article: DbArticle,
pub creator: DbPerson,
}
/// The version hash of a specific edit. Generated by taking an SHA256 hash of the diff
/// and using the first 16 bytes so that it fits into UUID.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "ssr", derive(diesel_derive_newtype::DieselNewType))]
pub struct EditVersion(pub(crate) Uuid);
impl EditVersion {
pub fn new(diff: &str) -> Self {
let mut sha256 = Sha256::new();
sha256.update(diff);
let hash_bytes = sha256.finalize();
let uuid =
Uuid::from_slice(&hash_bytes.as_slice()[..16]).expect("hash is correct size for uuid");
EditVersion(uuid)
}
pub fn hash(&self) -> String {
hex::encode(self.0.into_bytes())
}
}
impl Default for EditVersion {
fn default() -> Self {
EditVersion::new("")
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct RegisterUserForm {
pub username: String,
pub password: String,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct LoginUserForm {
pub username: String,
pub password: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable))]
#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))]
pub struct LocalUserView {
pub person: DbPerson,
pub local_user: DbLocalUser,
pub following: Vec<DbInstance>,
}
/// A user with account registered on local instance.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Identifiable))]
#[cfg_attr(feature = "ssr", diesel(table_name = local_user, check_for_backend(diesel::pg::Pg)))]
pub struct DbLocalUser {
pub id: InstanceId,
#[serde(skip)]
pub password_encrypted: String,
pub person_id: PersonId,
pub admin: bool,
}
/// Federation related data from a local or remote user.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Identifiable))]
#[cfg_attr(feature = "ssr", diesel(table_name = person, check_for_backend(diesel::pg::Pg)))]
pub struct DbPerson {
pub id: PersonId,
pub username: String,
#[cfg(feature = "ssr")]
pub ap_id: ObjectId<DbPerson>,
#[cfg(not(feature = "ssr"))]
pub ap_id: String,
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,
pub display_name: Option<String>,
pub bio: Option<String>,
}
impl DbPerson {
pub fn inbox_url(&self) -> Url {
Url::parse(&self.inbox_url).expect("can parse inbox url")
}
}
#[derive(Deserialize, Serialize, Debug)]
pub struct CreateArticleForm {
pub title: String,
pub text: String,
pub summary: String,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct EditArticleForm {
/// Id of the article to edit
pub article_id: ArticleId,
/// Full, new text of the article. A diff against `previous_version` is generated on the backend
/// side to handle conflicts.
pub new_text: String,
/// What was changed
pub summary: String,
/// The version that this edit is based on, ie [DbArticle.latest_version] or
/// [ApiConflict.previous_version]
pub previous_version_id: EditVersion,
/// If you are resolving a conflict, pass the id to delete conflict from the database
pub resolve_conflict_id: Option<ConflictId>,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct ProtectArticleForm {
pub article_id: ArticleId,
pub protected: bool,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct ForkArticleForm {
pub article_id: ArticleId,
pub new_title: String,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct ApproveArticleForm {
pub article_id: ArticleId,
pub approve: bool,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct DeleteConflictForm {
pub conflict_id: ConflictId,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct GetInstance {
pub id: Option<InstanceId>,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct FollowInstance {
pub id: InstanceId,
}
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
pub struct SuccessResponse { pub struct SuccessResponse {
success: bool, success: bool,
@ -264,27 +28,11 @@ impl Default for SuccessResponse {
} }
} }
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct SearchArticleForm {
pub query: String,
}
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
pub struct ResolveObject { pub struct ResolveObject {
pub id: Url, pub id: Url,
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct ApiConflict {
pub id: ConflictId,
pub hash: EditVersion,
pub three_way_merge: String,
pub summary: String,
pub article: DbArticle,
pub previous_version_id: EditVersion,
pub published: DateTime<Utc>,
}
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Notification { pub enum Notification {
EditConflict(ApiConflict), EditConflict(ApiConflict),
@ -299,89 +47,3 @@ impl Notification {
} }
} }
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Identifiable))]
#[cfg_attr(feature = "ssr", diesel(table_name = instance, check_for_backend(diesel::pg::Pg)))]
pub struct DbInstance {
pub id: InstanceId,
pub domain: String,
#[cfg(feature = "ssr")]
pub ap_id: ObjectId<DbInstance>,
#[cfg(not(feature = "ssr"))]
pub ap_id: String,
pub description: Option<String>,
#[cfg(feature = "ssr")]
pub articles_url: Option<CollectionId<DbArticleCollection>>,
#[cfg(not(feature = "ssr"))]
pub articles_url: String,
pub inbox_url: String,
#[serde(skip)]
pub public_key: String,
#[serde(skip)]
pub private_key: Option<String>,
pub last_refreshed_at: DateTime<Utc>,
pub local: bool,
#[cfg(feature = "ssr")]
pub instances_url: Option<CollectionId<DbInstanceCollection>>,
}
impl DbInstance {
pub fn inbox_url(&self) -> Url {
Url::parse(&self.inbox_url).expect("can parse inbox url")
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable))]
#[cfg_attr(feature = "ssr", diesel(table_name = article, check_for_backend(diesel::pg::Pg)))]
pub struct InstanceView {
pub instance: DbInstance,
pub followers: Vec<DbPerson>,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct GetUserForm {
pub name: String,
pub domain: Option<String>,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct UpdateUserForm {
pub person_id: PersonId,
pub display_name: Option<String>,
pub bio: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, SmartDefault)]
#[serde(default)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "ssr", derive(Queryable, Document))]
#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))]
pub struct Options {
/// Whether users can create new accounts
#[default = true]
#[cfg_attr(feature = "ssr", doku(example = "true"))]
pub registration_open: bool,
/// Whether admins need to approve new articles
#[default = false]
#[cfg_attr(feature = "ssr", doku(example = "false"))]
pub article_approval: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
#[cfg_attr(feature = "ssr", derive(Queryable))]
#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))]
pub struct SiteView {
pub my_profile: Option<LocalUserView>,
pub config: Options,
}
#[test]
fn test_edit_versions() {
let default = EditVersion::default();
assert_eq!("e3b0c44298fc1c149afbf4c8996fb924", default.hash());
let version = EditVersion::new("test");
assert_eq!("9f86d081884c7d659a2feaa0c55ad015", version.hash());
}

88
src/common/user.rs Normal file
View file

@ -0,0 +1,88 @@
use super::{
instance::DbInstance,
newtypes::{InstanceId, PersonId},
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use url::Url;
#[cfg(feature = "ssr")]
use {
crate::backend::database::schema::{local_user, person},
activitypub_federation::fetch::object_id::ObjectId,
diesel::{Identifiable, Queryable, Selectable},
};
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct RegisterUserForm {
pub username: String,
pub password: String,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct LoginUserForm {
pub username: String,
pub password: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable))]
#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))]
pub struct LocalUserView {
pub person: DbPerson,
pub local_user: DbLocalUser,
pub following: Vec<DbInstance>,
}
/// A user with account registered on local instance.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Identifiable))]
#[cfg_attr(feature = "ssr", diesel(table_name = local_user, check_for_backend(diesel::pg::Pg)))]
pub struct DbLocalUser {
pub id: InstanceId,
#[serde(skip)]
pub password_encrypted: String,
pub person_id: PersonId,
pub admin: bool,
}
/// Federation related data from a local or remote user.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Identifiable))]
#[cfg_attr(feature = "ssr", diesel(table_name = person, check_for_backend(diesel::pg::Pg)))]
pub struct DbPerson {
pub id: PersonId,
pub username: String,
#[cfg(feature = "ssr")]
pub ap_id: ObjectId<DbPerson>,
#[cfg(not(feature = "ssr"))]
pub ap_id: String,
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,
pub display_name: Option<String>,
pub bio: Option<String>,
}
impl DbPerson {
pub fn inbox_url(&self) -> Url {
Url::parse(&self.inbox_url).expect("can parse inbox url")
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct GetUserForm {
pub name: String,
pub domain: Option<String>,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct UpdateUserForm {
pub person_id: PersonId,
pub display_name: Option<String>,
pub bio: Option<String>,
}

View file

@ -1,4 +1,4 @@
use crate::common::DbArticle; use crate::common::article::DbArticle;
use leptos::server_fn::error::ServerFnErrorErr; use leptos::server_fn::error::ServerFnErrorErr;
pub fn can_edit_article(article: &DbArticle, is_admin: bool) -> Result<(), ServerFnErrorErr> { pub fn can_edit_article(article: &DbArticle, is_admin: bool) -> Result<(), ServerFnErrorErr> {

View file

@ -1,12 +1,14 @@
use crate::common::{ use crate::common::{
newtypes::{ArticleId, ConflictId}, article::*,
instance::*,
newtypes::{ArticleId, ConflictId, PersonId},
user::*,
utils::http_protocol_str, utils::http_protocol_str,
*, *,
}; };
use http::{Method, StatusCode}; use http::{Method, StatusCode};
use leptos::{prelude::ServerFnError, server_fn::error::NoCustomError}; use leptos::{prelude::ServerFnError, server_fn::error::NoCustomError};
use log::error; use log::error;
use newtypes::PersonId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{fmt::Debug, sync::LazyLock}; use std::{fmt::Debug, sync::LazyLock};
use url::Url; use url::Url;

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
common::SiteView, common::instance::SiteView,
frontend::{ frontend::{
api::CLIENT, api::CLIENT,
components::{nav::Nav, protected_route::IbisProtectedRoute}, components::{nav::Nav, protected_route::IbisProtectedRoute},

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
common::{validation::can_edit_article, ArticleView}, common::{article::ArticleView, validation::can_edit_article},
frontend::{ frontend::{
app::{is_admin, is_logged_in}, app::{is_admin, is_logged_in},
article_path, article_path,

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
common::{utils::extract_domain, EditView}, common::{article::EditView, utils::extract_domain},
frontend::{article_link, render_date_time, user_link}, frontend::{article_link, render_date_time, user_link},
}; };
use leptos::{either::Either, prelude::*}; use leptos::{either::Either, prelude::*};

View file

@ -1,5 +1,8 @@
use crate::{ use crate::{
common::{newtypes::InstanceId, DbInstance, FollowInstance}, common::{
instance::{DbInstance, FollowInstance},
newtypes::InstanceId,
},
frontend::{ frontend::{
api::CLIENT, api::CLIENT,
app::{site, DefaultResource}, app::{site, DefaultResource},

View file

@ -1,4 +1,4 @@
use crate::common::{utils::extract_domain, DbArticle, DbPerson}; use crate::common::{article::DbArticle, user::DbPerson, utils::extract_domain};
use chrono::{DateTime, Duration, Local, Utc}; use chrono::{DateTime, Duration, Local, Utc};
use codee::string::FromToStringCodec; use codee::string::FromToStringCodec;
use leptos::prelude::*; use leptos::prelude::*;

View file

@ -1,5 +1,8 @@
use crate::{ use crate::{
common::{newtypes::ArticleId, ForkArticleForm, ProtectArticleForm}, common::{
article::{ForkArticleForm, ProtectArticleForm},
newtypes::ArticleId,
},
frontend::{ frontend::{
api::CLIENT, api::CLIENT,
app::is_admin, app::is_admin,

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
common::CreateArticleForm, common::article::CreateArticleForm,
frontend::{ frontend::{
api::CLIENT, api::CLIENT,
app::{is_admin, site, DefaultResource}, app::{is_admin, site, DefaultResource},

View file

@ -1,9 +1,7 @@
use crate::{ use crate::{
common::{ common::{
article::{ApiConflict, ArticleView, EditArticleForm},
newtypes::ConflictId, newtypes::ConflictId,
ApiConflict,
ArticleView,
EditArticleForm,
Notification, Notification,
MAIN_PAGE_NAME, MAIN_PAGE_NAME,
}, },

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
common::ListArticlesForm, common::article::ListArticlesForm,
frontend::{ frontend::{
api::CLIENT, api::CLIENT,
app::DefaultResource, app::DefaultResource,

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
common::{utils::http_protocol_str, DbInstance, ListArticlesForm}, common::{article::ListArticlesForm, instance::DbInstance, utils::http_protocol_str},
frontend::{ frontend::{
api::CLIENT, api::CLIENT,
article_path, article_path,

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
common::LoginUserForm, common::user::LoginUserForm,
frontend::{api::CLIENT, app::site, components::credentials::*}, frontend::{api::CLIENT, app::site, components::credentials::*},
}; };
use leptos::prelude::*; use leptos::prelude::*;

View file

@ -1,5 +1,8 @@
use crate::{ use crate::{
common::{ArticleView, EditView, GetArticleForm, MAIN_PAGE_NAME}, common::{
article::{ArticleView, EditView, GetArticleForm},
MAIN_PAGE_NAME,
},
frontend::api::CLIENT, frontend::api::CLIENT,
}; };
use leptos::prelude::*; use leptos::prelude::*;

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
common::RegisterUserForm, common::user::RegisterUserForm,
frontend::{api::CLIENT, app::site, components::credentials::*}, frontend::{api::CLIENT, app::site, components::credentials::*},
}; };
use leptos::prelude::*; use leptos::prelude::*;

View file

@ -1,5 +1,8 @@
use crate::{ use crate::{
common::{DbArticle, DbInstance, SearchArticleForm}, common::{
article::{DbArticle, SearchArticleForm},
instance::DbInstance,
},
frontend::{api::CLIENT, article_path, article_title}, frontend::{api::CLIENT, article_path, article_title},
}; };
use leptos::prelude::*; use leptos::prelude::*;

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
common::UpdateUserForm, common::user::UpdateUserForm,
frontend::{ frontend::{
api::CLIENT, api::CLIENT,
app::{site, DefaultResource}, app::{site, DefaultResource},

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
common::GetUserForm, common::user::GetUserForm,
frontend::{ frontend::{
api::CLIENT, api::CLIENT,
components::edit_list::EditList, components::edit_list::EditList,

View file

@ -6,7 +6,7 @@ use ibis::{
config::{IbisConfig, IbisConfigDatabase, IbisConfigFederation}, config::{IbisConfig, IbisConfigDatabase, IbisConfigFederation},
start, start,
}, },
common::{Options, RegisterUserForm}, common::{instance::Options, user::RegisterUserForm},
frontend::api::ApiClient, frontend::api::ApiClient,
}; };
use reqwest::ClientBuilder; use reqwest::ClientBuilder;

View file

@ -5,19 +5,19 @@ mod common;
use crate::common::{TestData, TEST_ARTICLE_DEFAULT_TEXT}; use crate::common::{TestData, TEST_ARTICLE_DEFAULT_TEXT};
use anyhow::Result; use anyhow::Result;
use ibis::common::{ use ibis::common::{
article::{
ArticleView,
CreateArticleForm,
EditArticleForm,
ForkArticleForm,
GetArticleForm,
ListArticlesForm,
ProtectArticleForm,
SearchArticleForm,
},
user::{GetUserForm, LoginUserForm, RegisterUserForm},
utils::extract_domain, utils::extract_domain,
ArticleView,
CreateArticleForm,
EditArticleForm,
ForkArticleForm,
GetArticleForm,
GetUserForm,
ListArticlesForm,
LoginUserForm,
Notification, Notification,
ProtectArticleForm,
RegisterUserForm,
SearchArticleForm,
}; };
use pretty_assertions::{assert_eq, assert_ne}; use pretty_assertions::{assert_eq, assert_ne};
use retry_future::{LinearRetryStrategy, RetryFuture, RetryPolicy}; use retry_future::{LinearRetryStrategy, RetryFuture, RetryPolicy};