wip: Add diesel and postgres

This commit is contained in:
Felix Ableitner 2023-11-29 16:41:29 +01:00
parent 4371fc2edc
commit e030419cc5
31 changed files with 627 additions and 261 deletions

62
Cargo.lock generated
View File

@ -264,6 +264,12 @@ version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.5.0" version = "1.5.0"
@ -486,6 +492,51 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "diesel"
version = "2.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62c6fcf842f17f8c78ecf7c81d75c5ce84436b41ee07e03f490fbb5f5a8731d8"
dependencies = [
"bitflags 2.4.1",
"byteorder",
"diesel_derives",
"itoa",
"pq-sys",
]
[[package]]
name = "diesel-derive-newtype"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7267437d5b12df60ae29bd97f8d120f1c3a6272d6f213551afa56bbb2ecfbb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.39",
]
[[package]]
name = "diesel_derives"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef8337737574f55a468005a83499da720f20c65586241ffea339db9ecdfd2b44"
dependencies = [
"diesel_table_macro_syntax",
"proc-macro2",
"quote",
"syn 2.0.39",
]
[[package]]
name = "diesel_table_macro_syntax"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5"
dependencies = [
"syn 2.0.39",
]
[[package]] [[package]]
name = "diffy" name = "diffy"
version = "0.3.0" version = "0.3.0"
@ -615,6 +666,8 @@ dependencies = [
"axum", "axum",
"axum-macros", "axum-macros",
"chrono", "chrono",
"diesel",
"diesel-derive-newtype",
"diffy", "diffy",
"enum_delegate", "enum_delegate",
"env_logger", "env_logger",
@ -1408,6 +1461,15 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "pq-sys"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31c0052426df997c0cbd30789eb44ca097e3541717a7b8fa36b1c464ee7edebd"
dependencies = [
"vcpkg",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.69" version = "1.0.69"

View File

@ -10,6 +10,8 @@ async-trait = "0.1.74"
axum = "0.6.20" axum = "0.6.20"
axum-macros = "0.3.8" axum-macros = "0.3.8"
chrono = { version = "0.4.31", features = ["serde"] } chrono = { version = "0.4.31", features = ["serde"] }
diesel = {version = "2.1.4", features = ["postgres"] }
diesel-derive-newtype = "2.1.0"
diffy = "0.3.0" diffy = "0.3.0"
enum_delegate = "0.2.0" enum_delegate = "0.2.0"
env_logger = { version = "0.10.1", default-features = false } env_logger = { version = "0.10.1", default-features = false }

9
diesel.toml Normal file
View File

@ -0,0 +1,9 @@
# For documentation on how to configure this file,
# see https://diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/database/schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId"]
[migrations_directory]
dir = "migrations"

View File

@ -0,0 +1,6 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
DROP FUNCTION IF EXISTS diesel_set_updated_at();

View File

@ -0,0 +1,36 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
-- Sets up a trigger for the given table to automatically set a column called
-- `updated_at` whenever the row is modified (unless `updated_at` was included
-- in the modified columns)
--
-- # Example
--
-- ```sql
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
--
-- SELECT diesel_manage_updated_at('users');
-- ```
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

View File

@ -0,0 +1,2 @@
drop table edit;
drop table article;

View File

@ -0,0 +1,18 @@
create table article (
id serial primary key,
title text not null,
text text not null,
ap_id varchar(255) not null,
instance_id varchar(255) not null,
latest_version text not null,
local bool not null
);
create table edit (
id serial primary key,
ap_id varchar(255) not null,
diff text not null,
article_id int REFERENCES article ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
version text not null,
local bool not null
)

View File

@ -1,9 +1,10 @@
use crate::database::{DatabaseHandle, DbConflict}; use crate::database::article::{DbArticle, DbArticleForm};
use crate::database::dburl::DbUrl;
use crate::database::edit::{DbEdit, EditVersion};
use crate::database::{DbConflict, MyDataHandle};
use crate::error::MyResult; use crate::error::MyResult;
use crate::federation::activities::create_article::CreateArticle; use crate::federation::activities::create_article::CreateArticle;
use crate::federation::activities::submit_article_update; use crate::federation::activities::submit_article_update;
use crate::federation::objects::article::DbArticle;
use crate::federation::objects::edit::EditVersion;
use crate::federation::objects::instance::DbInstance; use crate::federation::objects::instance::DbInstance;
use crate::utils::generate_article_version; use crate::utils::generate_article_version;
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
@ -42,7 +43,7 @@ pub struct CreateArticleData {
/// Create a new article with empty text, and federate it to followers. /// Create a new article with empty text, and federate it to followers.
#[debug_handler] #[debug_handler]
async fn create_article( async fn create_article(
data: Data<DatabaseHandle>, data: Data<MyDataHandle>,
Form(create_article): Form<CreateArticleData>, Form(create_article): Form<CreateArticleData>,
) -> MyResult<Json<DbArticle>> { ) -> MyResult<Json<DbArticle>> {
{ {
@ -55,26 +56,23 @@ async fn create_article(
} }
} }
let local_instance_id = data.local_instance().ap_id; let instance_id: DbUrl = data.local_instance().ap_id.into();
let ap_id = ObjectId::parse(&format!( let ap_id = Url::parse(&format!(
"http://{}:{}/article/{}", "http://{}:{}/article/{}",
local_instance_id.inner().domain().unwrap(), instance_id.domain().unwrap(),
local_instance_id.inner().port().unwrap(), instance_id.port().unwrap(),
create_article.title create_article.title
))?; ))?
let article = DbArticle { .into();
let form = DbArticleForm {
title: create_article.title, title: create_article.title,
text: String::new(), text: String::new(),
ap_id, ap_id,
latest_version: EditVersion::default(), latest_version: Default::default(),
edits: vec![], instance_id,
instance: local_instance_id,
local: true, local: true,
}; };
{ let article = DbArticle::create(&form, &data.db_connection)?;
let mut articles = data.articles.lock().unwrap();
articles.insert(article.ap_id.inner().clone(), article.clone());
}
CreateArticle::send_to_followers(article.clone(), &data).await?; CreateArticle::send_to_followers(article.clone(), &data).await?;
@ -114,7 +112,7 @@ pub struct ApiConflict {
/// Conflicts are stored in the database so they can be retrieved later from `/api/v3/edit_conflicts`. /// Conflicts are stored in the database so they can be retrieved later from `/api/v3/edit_conflicts`.
#[debug_handler] #[debug_handler]
async fn edit_article( async fn edit_article(
data: Data<DatabaseHandle>, data: Data<MyDataHandle>,
Form(edit_form): Form<EditArticleData>, Form(edit_form): Form<EditArticleData>,
) -> MyResult<Json<Option<ApiConflict>>> { ) -> MyResult<Json<Option<ApiConflict>>> {
// resolve conflict if any // resolve conflict if any
@ -138,14 +136,14 @@ async fn edit_article(
} else { } else {
// There have been other changes since this edit was initiated. Get the common ancestor // There have been other changes since this edit was initiated. Get the common ancestor
// version and generate a diff to find out what exactly has changed. // version and generate a diff to find out what exactly has changed.
let ancestor = let edits = DbEdit::for_article(original_article.id, &data.db_connection)?;
generate_article_version(&original_article.edits, &edit_form.previous_version)?; let ancestor = generate_article_version(&edits, &edit_form.previous_version)?;
let patch = create_patch(&ancestor, &edit_form.new_text); let patch = create_patch(&ancestor, &edit_form.new_text);
let db_conflict = DbConflict { let db_conflict = DbConflict {
id: random(), id: random(),
diff: patch.to_string(), diff: patch.to_string(),
article_id: original_article.ap_id.clone(), article_id: original_article.ap_id.clone().into(),
previous_version: edit_form.previous_version, previous_version: edit_form.previous_version,
}; };
{ {
@ -158,23 +156,16 @@ async fn edit_article(
#[derive(Deserialize, Serialize, Clone)] #[derive(Deserialize, Serialize, Clone)]
pub struct GetArticleData { pub struct GetArticleData {
pub ap_id: ObjectId<DbArticle>, pub id: i32,
} }
/// 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]
async fn get_article( async fn get_article(
Query(query): Query<GetArticleData>, Query(query): Query<GetArticleData>,
data: Data<DatabaseHandle>, data: Data<MyDataHandle>,
) -> MyResult<Json<DbArticle>> { ) -> MyResult<Json<DbArticle>> {
let articles = data.articles.lock().unwrap(); Ok(Json(DbArticle::read(query.id, &data.db_connection)?))
let article = articles
.iter()
.find(|a| a.1.ap_id == query.ap_id)
.ok_or(anyhow!("not found"))?
.1
.clone();
Ok(Json(article))
} }
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
@ -187,7 +178,7 @@ pub struct ResolveObject {
#[debug_handler] #[debug_handler]
async fn resolve_instance( async fn resolve_instance(
Query(query): Query<ResolveObject>, Query(query): Query<ResolveObject>,
data: Data<DatabaseHandle>, data: Data<MyDataHandle>,
) -> MyResult<Json<DbInstance>> { ) -> MyResult<Json<DbInstance>> {
let instance: DbInstance = ObjectId::from(query.id).dereference(&data).await?; let instance: DbInstance = ObjectId::from(query.id).dereference(&data).await?;
Ok(Json(instance)) Ok(Json(instance))
@ -198,7 +189,7 @@ async fn resolve_instance(
#[debug_handler] #[debug_handler]
async fn resolve_article( async fn resolve_article(
Query(query): Query<ResolveObject>, Query(query): Query<ResolveObject>,
data: Data<DatabaseHandle>, data: Data<MyDataHandle>,
) -> MyResult<Json<DbArticle>> { ) -> MyResult<Json<DbArticle>> {
let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?; let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?;
Ok(Json(article)) Ok(Json(article))
@ -206,7 +197,7 @@ async fn resolve_article(
/// Retrieve the local instance info. /// Retrieve the local instance info.
#[debug_handler] #[debug_handler]
async fn get_local_instance(data: Data<DatabaseHandle>) -> MyResult<Json<DbInstance>> { async fn get_local_instance(data: Data<MyDataHandle>) -> MyResult<Json<DbInstance>> {
Ok(Json(data.local_instance())) Ok(Json(data.local_instance()))
} }
@ -219,7 +210,7 @@ pub struct FollowInstance {
/// updated articles. /// updated articles.
#[debug_handler] #[debug_handler]
async fn follow_instance( async fn follow_instance(
data: Data<DatabaseHandle>, data: Data<MyDataHandle>,
Form(query): Form<FollowInstance>, Form(query): Form<FollowInstance>,
) -> MyResult<()> { ) -> MyResult<()> {
let instance = query.instance_id.dereference(&data).await?; let instance = query.instance_id.dereference(&data).await?;
@ -229,7 +220,7 @@ async fn follow_instance(
/// Get a list of all unresolved edit conflicts. /// Get a list of all unresolved edit conflicts.
#[debug_handler] #[debug_handler]
async fn edit_conflicts(data: Data<DatabaseHandle>) -> MyResult<Json<Vec<ApiConflict>>> { async fn edit_conflicts(data: Data<MyDataHandle>) -> MyResult<Json<Vec<ApiConflict>>> {
let conflicts = { data.conflicts.lock().unwrap().to_vec() }; let conflicts = { data.conflicts.lock().unwrap().to_vec() };
let conflicts: Vec<ApiConflict> = try_join_all(conflicts.into_iter().map(|c| { let conflicts: Vec<ApiConflict> = try_join_all(conflicts.into_iter().map(|c| {
let data = data.reset_request_count(); let data = data.reset_request_count();
@ -253,7 +244,7 @@ pub struct SearchArticleData {
#[debug_handler] #[debug_handler]
async fn search_article( async fn search_article(
Query(query): Query<SearchArticleData>, Query(query): Query<SearchArticleData>,
data: Data<DatabaseHandle>, data: Data<MyDataHandle>,
) -> MyResult<Json<Vec<DbArticle>>> { ) -> MyResult<Json<Vec<DbArticle>>> {
let articles = data.articles.lock().unwrap(); let articles = data.articles.lock().unwrap();
let article = articles let article = articles
@ -277,7 +268,7 @@ pub struct ForkArticleData {
/// how an article should be edited. /// how an article should be edited.
#[debug_handler] #[debug_handler]
async fn fork_article( async fn fork_article(
data: Data<DatabaseHandle>, data: Data<MyDataHandle>,
Form(fork_form): Form<ForkArticleData>, Form(fork_form): Form<ForkArticleData>,
) -> MyResult<Json<DbArticle>> { ) -> MyResult<Json<DbArticle>> {
let article = { let article = {
@ -296,28 +287,27 @@ async fn fork_article(
.clone() .clone()
}; };
let local_instance_id = data.local_instance().ap_id; let instance_id: DbUrl = data.local_instance().ap_id.into();
let ap_id = ObjectId::parse(&format!( let ap_id = Url::parse(&format!(
"http://{}:{}/article/{}", "http://{}:{}/article/{}",
local_instance_id.inner().domain().unwrap(), instance_id.domain().unwrap(),
local_instance_id.inner().port().unwrap(), instance_id.port().unwrap(),
original_article.title original_article.title
))?; ))?
let forked_article = DbArticle { .into();
let form = DbArticleForm {
title: original_article.title.clone(), title: original_article.title.clone(),
text: original_article.text.clone(), text: original_article.text.clone(),
ap_id, ap_id,
latest_version: original_article.latest_version.clone(), latest_version: original_article.latest_version.0.clone(),
edits: original_article.edits.clone(), instance_id,
instance: local_instance_id,
local: true, local: true,
}; };
{ let article = DbArticle::create(&form, &data.db_connection)?;
let mut articles = data.articles.lock().unwrap();
articles.insert(forked_article.ap_id.inner().clone(), forked_article.clone());
}
CreateArticle::send_to_followers(forked_article.clone(), &data).await?; // TODO: need to copy edits separately with db query
Ok(Json(forked_article)) CreateArticle::send_to_followers(article.clone(), &data).await?;
Ok(Json(article))
} }

78
src/database/article.rs Normal file
View File

@ -0,0 +1,78 @@
use crate::database::dburl::DbUrl;
use crate::database::edit::EditVersion;
use crate::database::schema::article;
use crate::error::MyResult;
use crate::federation::objects::edits_collection::DbEditCollection;
use activitypub_federation::fetch::collection_id::CollectionId;
use diesel::pg::PgConnection;
use diesel::ExpressionMethods;
use diesel::{
insert_into, AsChangeset, Identifiable, Insertable, QueryDsl, Queryable, RunQueryDsl,
Selectable,
};
use serde::{Deserialize, Serialize};
use std::ops::DerefMut;
use std::sync::Mutex;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Queryable, Selectable, Identifiable)]
#[diesel(table_name = article, check_for_backend(diesel::pg::Pg))]
pub struct DbArticle {
pub id: i32,
pub title: String,
pub text: String,
pub ap_id: DbUrl,
pub instance_id: DbUrl,
/// List of all edits which make up this article, oldest first.
// TODO
//pub edits: Vec<DbEdit>,
pub latest_version: EditVersion,
pub local: bool,
}
#[derive(Debug, Clone, Insertable, AsChangeset)]
#[diesel(table_name = article, check_for_backend(diesel::pg::Pg))]
pub struct DbArticleForm {
pub title: String,
pub text: String,
pub ap_id: DbUrl,
// TODO: change to foreign key
pub instance_id: DbUrl,
// TODO: instead of this we can use latest entry in edits table
pub latest_version: String,
pub local: bool,
}
impl DbArticle {
pub fn edits_id(&self) -> MyResult<CollectionId<DbEditCollection>> {
Ok(CollectionId::parse(&format!("{}/edits", self.ap_id))?)
}
pub fn create(form: &DbArticleForm, conn: &Mutex<PgConnection>) -> MyResult<DbArticle> {
let mut conn = conn.lock().unwrap().deref_mut();
Ok(insert_into(article::table)
.values(form)
.on_conflict(article::dsl::ap_id)
.do_update()
.set(form)
.get_result(conn)?)
}
pub fn update_text(id: i32, text: &str, conn: &Mutex<PgConnection>) -> MyResult<Self> {
let mut conn = conn.lock().unwrap();
Ok(diesel::update(article::dsl::article.find(id))
.set(article::dsl::text.eq(text))
.get_result::<Self>(&mut conn)?)
}
pub fn read(id: i32, conn: &Mutex<PgConnection>) -> MyResult<DbArticle> {
let mut conn = conn.lock().unwrap();
Ok(article::table.find(id).get_result(&mut conn)?)
}
pub fn read_from_ap_id(ap_id: &DbUrl, conn: &Mutex<PgConnection>) -> MyResult<DbArticle> {
let mut conn = conn.lock().unwrap();
Ok(article::table
.filter(article::dsl::ap_id.eq(ap_id))
.get_result(&mut conn)?)
}
}

96
src/database/dburl.rs Normal file
View File

@ -0,0 +1,96 @@
use activitypub_federation::fetch::collection_id::CollectionId;
use activitypub_federation::fetch::object_id::ObjectId;
use activitypub_federation::traits::{Collection, Object};
use diesel::backend::Backend;
use diesel::deserialize::FromSql;
use diesel::pg::Pg;
use diesel::{AsExpression, FromSqlRow};
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::ops::Deref;
use url::Url;
/// Copied from lemmy, could be moved into common library
#[repr(transparent)]
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash, AsExpression, FromSqlRow)]
#[diesel(sql_type = diesel::sql_types::Text)]
pub struct DbUrl(pub(crate) Box<Url>);
// TODO: Lemmy doesnt need this, but for some reason derive fails to generate it
impl FromSql<diesel::sql_types::Text, Pg> for DbUrl {
fn from_sql(bytes: <Pg as Backend>::RawValue<'_>) -> diesel::deserialize::Result<Self> {
todo!()
}
}
impl Display for DbUrl {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.clone().0.fmt(f)
}
}
// the project doesnt compile with From
#[allow(clippy::from_over_into)]
impl Into<DbUrl> for Url {
fn into(self) -> DbUrl {
DbUrl(Box::new(self))
}
}
#[allow(clippy::from_over_into)]
impl Into<Url> for DbUrl {
fn into(self) -> Url {
*self.0
}
}
impl<T> From<DbUrl> for ObjectId<T>
where
T: Object + Send + 'static,
for<'de2> <T as Object>::Kind: Deserialize<'de2>,
{
fn from(value: DbUrl) -> Self {
let url: Url = value.into();
ObjectId::from(url)
}
}
impl<T> From<DbUrl> for CollectionId<T>
where
T: Collection + Send + 'static,
for<'de2> <T as Collection>::Kind: Deserialize<'de2>,
{
fn from(value: DbUrl) -> Self {
let url: Url = value.into();
CollectionId::from(url)
}
}
impl<T> From<CollectionId<T>> for DbUrl
where
T: Collection,
for<'de2> <T as Collection>::Kind: Deserialize<'de2>,
{
fn from(value: CollectionId<T>) -> Self {
let url: Url = value.into();
url.into()
}
}
impl<T> From<ObjectId<T>> for DbUrl
where
T: Object,
for<'de2> <T as Object>::Kind: Deserialize<'de2>,
{
fn from(value: ObjectId<T>) -> Self {
let url: Url = value.into();
url.into()
}
}
impl Deref for DbUrl {
type Target = Url;
fn deref(&self) -> &Self::Target {
&self.0
}
}

86
src/database/edit.rs Normal file
View File

@ -0,0 +1,86 @@
use crate::database::article::DbArticle;
use crate::database::dburl::DbUrl;
use crate::database::schema::edit;
use crate::error::MyResult;
use activitypub_federation::fetch::object_id::ObjectId;
use diesel::ExpressionMethods;
use diesel::{
insert_into, AsChangeset, Identifiable, Insertable, PgConnection, QueryDsl, Queryable,
RunQueryDsl, Selectable,
};
use diesel_derive_newtype::DieselNewType;
use diffy::create_patch;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha224};
use std::sync::Mutex;
/// Represents a single change to the article.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Queryable, Selectable, Identifiable)]
#[diesel(table_name = edit, check_for_backend(diesel::pg::Pg))]
pub struct DbEdit {
pub id: i32,
pub ap_id: DbUrl,
pub diff: String,
pub article_id: i32,
pub version: EditVersion,
// TODO: there is already `local` field on article, do we need this?
pub local: bool,
}
#[derive(Debug, Clone, Insertable, AsChangeset)]
#[diesel(table_name = edit, check_for_backend(diesel::pg::Pg))]
pub struct DbEditForm {
pub ap_id: DbUrl,
pub diff: String,
pub article_id: i32,
pub version: EditVersion,
pub local: bool,
}
impl DbEditForm {
pub fn new(original_article: &DbArticle, updated_text: &str) -> MyResult<Self> {
let diff = create_patch(&original_article.text, updated_text);
let mut sha224 = Sha224::new();
sha224.update(diff.to_bytes());
let hash = format!("{:X}", sha224.finalize());
let edit_id = ObjectId::parse(&format!("{}/{}", original_article.ap_id, hash))?;
Ok(DbEditForm {
ap_id: edit_id.into(),
diff: diff.to_string(),
article_id: original_article.ap_id.clone(),
version: EditVersion(hash),
local: true,
})
}
}
impl DbEdit {
pub fn create(form: &DbEditForm, conn: &Mutex<PgConnection>) -> MyResult<Self> {
let mut conn = conn.lock().unwrap();
Ok(insert_into(edit::table)
.values(form)
.on_conflict(edit::dsl::ap_id)
.do_update()
.set(form)
.get_result(&mut conn)?)
}
pub fn for_article(id: i32, conn: &Mutex<PgConnection>) -> MyResult<Vec<Self>> {
let mut conn = conn.lock().unwrap();
Ok(edit::table
.filter(edit::dsl::id.eq(id))
.order_by(edit::dsl::id.asc())
.get_results(&mut conn)?)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, DieselNewType)]
pub struct EditVersion(pub String);
impl Default for EditVersion {
fn default() -> Self {
let sha224 = Sha224::new();
let hash = format!("{:X}", sha224.finalize());
EditVersion(hash)
}
}

View File

@ -1,26 +1,48 @@
use crate::api::ApiConflict; use crate::api::ApiConflict;
use crate::database::article::DbArticle;
use crate::database::edit::DbEdit;
use crate::error::MyResult; use crate::error::MyResult;
use crate::federation::activities::submit_article_update; use crate::federation::activities::submit_article_update;
use crate::federation::objects::article::DbArticle;
use crate::federation::objects::edit::EditVersion;
use crate::federation::objects::instance::DbInstance; use crate::federation::objects::instance::DbInstance;
use crate::utils::generate_article_version; use crate::utils::generate_article_version;
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use activitypub_federation::fetch::object_id::ObjectId; use activitypub_federation::fetch::object_id::ObjectId;
use diesel::{Identifiable, PgConnection, QueryDsl};
use diffy::{apply, merge, Patch}; use diffy::{apply, merge, Patch};
use edit::EditVersion;
use std::collections::HashMap; use std::collections::HashMap;
use std::ops::Deref;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use url::Url; use url::Url;
pub type DatabaseHandle = Arc<Database>; pub mod article;
pub mod dburl;
pub mod edit;
mod schema;
pub struct Database { #[derive(Clone)]
pub struct MyData {
pub db_connection: Arc<Mutex<PgConnection>>,
pub fake_db: Arc<FakeDatabase>,
}
impl Deref for MyData {
type Target = Arc<FakeDatabase>;
fn deref(&self) -> &Self::Target {
&self.fake_db
}
}
pub type MyDataHandle = MyData;
pub struct FakeDatabase {
pub instances: Mutex<HashMap<Url, DbInstance>>, pub instances: Mutex<HashMap<Url, DbInstance>>,
// TODO: remove this
pub articles: Mutex<HashMap<Url, DbArticle>>, pub articles: Mutex<HashMap<Url, DbArticle>>,
pub conflicts: Mutex<Vec<DbConflict>>, pub conflicts: Mutex<Vec<DbConflict>>,
} }
impl Database { impl FakeDatabase {
pub fn local_instance(&self) -> DbInstance { pub fn local_instance(&self) -> DbInstance {
let lock = self.instances.lock().unwrap(); let lock = self.instances.lock().unwrap();
lock.iter().find(|i| i.1.local).unwrap().1.clone() lock.iter().find(|i| i.1.local).unwrap().1.clone()
@ -38,7 +60,7 @@ pub struct DbConflict {
impl DbConflict { impl DbConflict {
pub async fn to_api_conflict( pub async fn to_api_conflict(
&self, &self,
data: &Data<DatabaseHandle>, data: &Data<MyDataHandle>,
) -> MyResult<Option<ApiConflict>> { ) -> MyResult<Option<ApiConflict>> {
let original_article = { let original_article = {
let mut lock = data.articles.lock().unwrap(); let mut lock = data.articles.lock().unwrap();
@ -47,7 +69,8 @@ impl DbConflict {
}; };
// create common ancestor version // create common ancestor version
let ancestor = generate_article_version(&original_article.edits, &self.previous_version)?; let edits = DbEdit::for_article(original_article.id, &data.db_connection)?;
let ancestor = generate_article_version(&edits, &self.previous_version)?;
let patch = Patch::from_str(&self.diff)?; let patch = Patch::from_str(&self.diff)?;
// apply self.diff to ancestor to get `ours` // apply self.diff to ancestor to get `ours`
@ -67,7 +90,7 @@ impl DbConflict {
Ok(Some(ApiConflict { Ok(Some(ApiConflict {
id: self.id, id: self.id,
three_way_merge, three_way_merge,
article_id: original_article.ap_id.clone(), article_id: original_article.ap_id.into(),
previous_version: original_article.latest_version, previous_version: original_article.latest_version,
})) }))
} }

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

@ -0,0 +1,31 @@
// @generated automatically by Diesel CLI.
diesel::table! {
article (id) {
id -> Int4,
title -> Text,
text -> Text,
#[max_length = 255]
ap_id -> Varchar,
#[max_length = 255]
instance_id -> Varchar,
latest_version -> Text,
local -> Bool,
}
}
diesel::table! {
edit (id) {
id -> Int4,
#[max_length = 255]
ap_id -> Varchar,
diff -> Text,
article_id -> Int4,
version -> Text,
local -> Bool,
}
}
diesel::joinable!(edit -> article (article_id));
diesel::allow_tables_to_appear_in_same_query!(article, edit,);

View File

@ -1,7 +1,7 @@
use crate::error::MyResult; use crate::error::MyResult;
use crate::federation::objects::instance::DbInstance; use crate::federation::objects::instance::DbInstance;
use crate::utils::generate_activity_id; use crate::utils::generate_activity_id;
use crate::{database::DatabaseHandle, federation::activities::follow::Follow}; use crate::{database::MyDataHandle, federation::activities::follow::Follow};
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,
}; };
@ -32,7 +32,7 @@ impl Accept {
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActivityHandler for Accept { impl ActivityHandler for Accept {
type DataType = DatabaseHandle; type DataType = MyDataHandle;
type Error = crate::error::Error; type Error = crate::error::Error;
fn id(&self) -> &Url { fn id(&self) -> &Url {

View File

@ -1,6 +1,6 @@
use crate::database::DatabaseHandle; use crate::database::{article::DbArticle, MyDataHandle};
use crate::error::MyResult; use crate::error::MyResult;
use crate::federation::objects::article::{ApubArticle, DbArticle}; use crate::federation::objects::article::ApubArticle;
use crate::federation::objects::instance::DbInstance; use crate::federation::objects::instance::DbInstance;
use crate::utils::generate_activity_id; use crate::utils::generate_activity_id;
use activitypub_federation::kinds::activity::CreateType; use activitypub_federation::kinds::activity::CreateType;
@ -26,10 +26,7 @@ pub struct CreateArticle {
} }
impl CreateArticle { impl CreateArticle {
pub async fn send_to_followers( pub async fn send_to_followers(article: DbArticle, data: &Data<MyDataHandle>) -> MyResult<()> {
article: DbArticle,
data: &Data<DatabaseHandle>,
) -> MyResult<()> {
let local_instance = data.local_instance(); let local_instance = data.local_instance();
let object = article.clone().into_json(data).await?; let object = article.clone().into_json(data).await?;
let id = generate_activity_id(local_instance.ap_id.inner())?; let id = generate_activity_id(local_instance.ap_id.inner())?;
@ -48,7 +45,7 @@ impl CreateArticle {
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActivityHandler for CreateArticle { impl ActivityHandler for CreateArticle {
type DataType = DatabaseHandle; type DataType = MyDataHandle;
type Error = crate::error::Error; type Error = crate::error::Error;
fn id(&self) -> &Url { fn id(&self) -> &Url {

View File

@ -1,8 +1,6 @@
use crate::error::MyResult; use crate::error::MyResult;
use crate::federation::objects::instance::DbInstance; use crate::federation::objects::instance::DbInstance;
use crate::{ use crate::{database::MyDataHandle, federation::activities::accept::Accept, generate_activity_id};
database::DatabaseHandle, federation::activities::accept::Accept, generate_activity_id,
};
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,
fetch::object_id::ObjectId, fetch::object_id::ObjectId,
@ -36,7 +34,7 @@ impl Follow {
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActivityHandler for Follow { impl ActivityHandler for Follow {
type DataType = DatabaseHandle; type DataType = MyDataHandle;
type Error = crate::error::Error; type Error = crate::error::Error;
fn id(&self) -> &Url { fn id(&self) -> &Url {

View File

@ -1,10 +1,12 @@
use crate::database::DatabaseHandle; use crate::database::article::DbArticle;
use crate::database::edit::{DbEdit, DbEditForm};
use crate::database::MyDataHandle;
use crate::error::Error; use crate::error::Error;
use crate::federation::activities::update_local_article::UpdateLocalArticle; use crate::federation::activities::update_local_article::UpdateLocalArticle;
use crate::federation::activities::update_remote_article::UpdateRemoteArticle; use crate::federation::activities::update_remote_article::UpdateRemoteArticle;
use crate::federation::objects::article::DbArticle; use crate::federation::objects::instance::DbInstance;
use crate::federation::objects::edit::DbEdit;
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use activitypub_federation::fetch::object_id::ObjectId;
pub mod accept; pub mod accept;
pub mod create_article; pub mod create_article;
@ -14,29 +16,27 @@ pub mod update_local_article;
pub mod update_remote_article; pub mod update_remote_article;
pub async fn submit_article_update( pub async fn submit_article_update(
data: &Data<DatabaseHandle>, data: &Data<MyDataHandle>,
new_text: String, new_text: String,
original_article: &DbArticle, original_article: &DbArticle,
) -> Result<(), Error> { ) -> Result<(), Error> {
let edit = DbEdit::new(original_article, &new_text)?; let form = DbEditForm::new(original_article, &new_text)?;
let edit = DbEdit::create(&form, &data.db_connection)?;
if original_article.local { if original_article.local {
let updated_article = { let updated_article = {
let mut lock = data.articles.lock().unwrap(); let mut lock = data.articles.lock().unwrap();
let article = lock.get_mut(original_article.ap_id.inner()).unwrap(); let article = lock.get_mut(&original_article.ap_id).unwrap();
article.text = new_text; article.text = new_text;
article.latest_version = edit.version.clone(); article.latest_version = edit.version.clone();
article.edits.push(edit.clone());
article.clone() article.clone()
}; };
UpdateLocalArticle::send(updated_article, vec![], data).await?; UpdateLocalArticle::send(updated_article, vec![], data).await?;
} else { } else {
UpdateRemoteArticle::send( let instance: DbInstance = ObjectId::from(original_article.instance_id.clone())
edit, .dereference(data)
original_article.instance.dereference(data).await?, .await?;
data, UpdateRemoteArticle::send(edit, instance, data).await?;
)
.await?;
} }
Ok(()) Ok(())
} }

View File

@ -1,4 +1,4 @@
use crate::database::DatabaseHandle; use crate::database::MyDataHandle;
use crate::error::MyResult; use crate::error::MyResult;
use crate::federation::objects::edit::ApubEdit; use crate::federation::objects::edit::ApubEdit;
use crate::federation::objects::instance::DbInstance; use crate::federation::objects::instance::DbInstance;
@ -30,7 +30,7 @@ impl RejectEdit {
pub async fn send( pub async fn send(
edit: ApubEdit, edit: ApubEdit,
user_instance: DbInstance, user_instance: DbInstance,
data: &Data<DatabaseHandle>, data: &Data<MyDataHandle>,
) -> MyResult<()> { ) -> MyResult<()> {
let local_instance = data.local_instance(); let local_instance = data.local_instance();
let id = generate_activity_id(local_instance.ap_id.inner())?; let id = generate_activity_id(local_instance.ap_id.inner())?;
@ -50,7 +50,7 @@ impl RejectEdit {
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActivityHandler for RejectEdit { impl ActivityHandler for RejectEdit {
type DataType = DatabaseHandle; type DataType = MyDataHandle;
type Error = crate::error::Error; type Error = crate::error::Error;
fn id(&self) -> &Url { fn id(&self) -> &Url {

View File

@ -1,6 +1,6 @@
use crate::database::DatabaseHandle; use crate::database::{article::DbArticle, MyDataHandle};
use crate::error::MyResult; use crate::error::MyResult;
use crate::federation::objects::article::{ApubArticle, DbArticle}; use crate::federation::objects::article::ApubArticle;
use crate::federation::objects::instance::DbInstance; use crate::federation::objects::instance::DbInstance;
use crate::utils::generate_activity_id; use crate::utils::generate_activity_id;
@ -32,7 +32,7 @@ impl UpdateLocalArticle {
pub async fn send( pub async fn send(
article: DbArticle, article: DbArticle,
extra_recipients: Vec<DbInstance>, extra_recipients: Vec<DbInstance>,
data: &Data<DatabaseHandle>, data: &Data<MyDataHandle>,
) -> MyResult<()> { ) -> MyResult<()> {
debug_assert!(article.local); debug_assert!(article.local);
let local_instance = data.local_instance(); let local_instance = data.local_instance();
@ -55,7 +55,7 @@ impl UpdateLocalArticle {
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActivityHandler for UpdateLocalArticle { impl ActivityHandler for UpdateLocalArticle {
type DataType = DatabaseHandle; type DataType = MyDataHandle;
type Error = crate::error::Error; type Error = crate::error::Error;
fn id(&self) -> &Url { fn id(&self) -> &Url {

View File

@ -1,7 +1,7 @@
use crate::database::DatabaseHandle; use crate::database::MyDataHandle;
use crate::error::MyResult; use crate::error::MyResult;
use crate::federation::objects::edit::{ApubEdit, DbEdit}; use crate::federation::objects::edit::ApubEdit;
use crate::federation::objects::instance::DbInstance; use crate::federation::objects::instance::DbInstance;
use crate::utils::generate_activity_id; use crate::utils::generate_activity_id;
use activitypub_federation::kinds::activity::UpdateType; use activitypub_federation::kinds::activity::UpdateType;
@ -13,6 +13,8 @@ use activitypub_federation::{
}; };
use diffy::{apply, Patch}; use diffy::{apply, Patch};
use crate::database::article::DbArticle;
use crate::database::edit::DbEdit;
use crate::federation::activities::reject::RejectEdit; use crate::federation::activities::reject::RejectEdit;
use crate::federation::activities::update_local_article::UpdateLocalArticle; use crate::federation::activities::update_local_article::UpdateLocalArticle;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -35,7 +37,7 @@ impl UpdateRemoteArticle {
pub async fn send( pub async fn send(
edit: DbEdit, edit: DbEdit,
article_instance: DbInstance, article_instance: DbInstance,
data: &Data<DatabaseHandle>, data: &Data<MyDataHandle>,
) -> MyResult<()> { ) -> MyResult<()> {
let local_instance = data.local_instance(); let local_instance = data.local_instance();
let id = generate_activity_id(local_instance.ap_id.inner())?; let id = generate_activity_id(local_instance.ap_id.inner())?;
@ -55,7 +57,7 @@ impl UpdateRemoteArticle {
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActivityHandler for UpdateRemoteArticle { impl ActivityHandler for UpdateRemoteArticle {
type DataType = DatabaseHandle; type DataType = MyDataHandle;
type Error = crate::error::Error; type Error = crate::error::Error;
fn id(&self) -> &Url { fn id(&self) -> &Url {
@ -80,13 +82,9 @@ impl ActivityHandler for UpdateRemoteArticle {
match apply(&article_text, &patch) { match apply(&article_text, &patch) {
Ok(applied) => { Ok(applied) => {
let article = { let edit = DbEdit::from_json(self.object.clone(), data).await?;
let edit = DbEdit::from_json(self.object.clone(), data).await?; let article =
let mut lock = data.articles.lock().unwrap(); DbArticle::update_text(edit.article_id, &applied, &mut data.db_connection)?;
let article = lock.get_mut(edit.article_id.inner()).unwrap();
article.text = applied;
article.clone()
};
UpdateLocalArticle::send(article, vec![self.actor.dereference(data).await?], data) UpdateLocalArticle::send(article, vec![self.actor.dereference(data).await?], data)
.await?; .await?;
} }

View File

@ -1,5 +1,6 @@
use crate::database::{Database, DatabaseHandle}; use crate::database::{FakeDatabase, MyData, MyDataHandle};
use crate::error::Error; use crate::error::Error;
use crate::establish_db_connection;
use crate::federation::objects::instance::DbInstance; use crate::federation::objects::instance::DbInstance;
use activitypub_federation::config::FederationConfig; use activitypub_federation::config::FederationConfig;
use activitypub_federation::fetch::collection_id::CollectionId; use activitypub_federation::fetch::collection_id::CollectionId;
@ -13,13 +14,13 @@ pub mod activities;
pub mod objects; pub mod objects;
pub mod routes; pub mod routes;
pub async fn federation_config(hostname: &str) -> Result<FederationConfig<DatabaseHandle>, Error> { pub async fn federation_config(hostname: &str) -> Result<FederationConfig<MyDataHandle>, Error> {
let ap_id = Url::parse(&format!("http://{}", hostname))?.into(); let ap_id = Url::parse(&format!("http://{}", hostname))?;
let articles_id = CollectionId::parse(&format!("http://{}/all_articles", hostname))?; let articles_id = CollectionId::parse(&format!("http://{}/all_articles", hostname))?;
let inbox = Url::parse(&format!("http://{}/inbox", hostname))?; let inbox = Url::parse(&format!("http://{}/inbox", hostname))?;
let keypair = generate_actor_keypair()?; let keypair = generate_actor_keypair()?;
let local_instance = DbInstance { let local_instance = DbInstance {
ap_id, ap_id: ap_id.into(),
articles_id, articles_id,
inbox, inbox,
public_key: keypair.public_key, public_key: keypair.public_key,
@ -29,7 +30,7 @@ pub async fn federation_config(hostname: &str) -> Result<FederationConfig<Databa
follows: vec![], follows: vec![],
local: true, local: true,
}; };
let database = Arc::new(Database { let fake_db = Arc::new(FakeDatabase {
instances: Mutex::new(HashMap::from([( instances: Mutex::new(HashMap::from([(
local_instance.ap_id.inner().clone(), local_instance.ap_id.inner().clone(),
local_instance, local_instance,
@ -37,9 +38,14 @@ pub async fn federation_config(hostname: &str) -> Result<FederationConfig<Databa
articles: Mutex::new(HashMap::new()), articles: Mutex::new(HashMap::new()),
conflicts: Mutex::new(vec![]), conflicts: Mutex::new(vec![]),
}); });
let db_connection = Arc::new(Mutex::new(establish_db_connection()?));
let data = MyData {
db_connection,
fake_db,
};
let config = FederationConfig::builder() let config = FederationConfig::builder()
.domain(hostname) .domain(hostname)
.app_data(database) .app_data(data)
.debug(true) .debug(true)
.build() .build()
.await?; .await?;

View File

@ -1,44 +1,26 @@
use crate::error::MyResult; use crate::database::article::DbArticleForm;
use crate::federation::objects::edit::{DbEdit, EditVersion}; use crate::database::edit::EditVersion;
use crate::database::{article::DbArticle, MyDataHandle};
use crate::error::Error;
use crate::federation::objects::edits_collection::DbEditCollection; use crate::federation::objects::edits_collection::DbEditCollection;
use crate::federation::objects::instance::DbInstance; use crate::federation::objects::instance::DbInstance;
use crate::{database::DatabaseHandle, error::Error}; use activitypub_federation::config::Data;
use activitypub_federation::fetch::collection_id::CollectionId; use activitypub_federation::fetch::collection_id::CollectionId;
use activitypub_federation::kinds::object::ArticleType; 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::{
config::Data, fetch::object_id::ObjectId, protocol::helpers::deserialize_one_or_many, traits::Object,
fetch::object_id::ObjectId,
kinds::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;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct DbArticle {
pub title: String,
pub text: String,
pub ap_id: ObjectId<DbArticle>,
pub instance: ObjectId<DbInstance>,
/// List of all edits which make up this article, oldest first.
pub edits: Vec<DbEdit>,
pub latest_version: EditVersion,
pub local: bool,
}
impl DbArticle {
fn edits_id(&self) -> MyResult<CollectionId<DbEditCollection>> {
Ok(CollectionId::parse(&format!("{}/edits", self.ap_id))?)
}
}
#[derive(Deserialize, Serialize, Debug, Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ApubArticle { pub struct ApubArticle {
#[serde(rename = "type")] #[serde(rename = "type")]
kind: ArticleType, pub(crate) kind: ArticleType,
id: ObjectId<DbArticle>, pub(crate) id: ObjectId<DbArticle>,
pub(crate) attributed_to: ObjectId<DbInstance>, pub(crate) attributed_to: ObjectId<DbInstance>,
#[serde(deserialize_with = "deserialize_one_or_many")] #[serde(deserialize_with = "deserialize_one_or_many")]
pub(crate) to: Vec<Url>, pub(crate) to: Vec<Url>,
@ -50,7 +32,7 @@ pub struct ApubArticle {
#[async_trait::async_trait] #[async_trait::async_trait]
impl Object for DbArticle { impl Object for DbArticle {
type DataType = DatabaseHandle; type DataType = MyDataHandle;
type Kind = ApubArticle; type Kind = ApubArticle;
type Error = Error; type Error = Error;
@ -58,21 +40,18 @@ impl Object for DbArticle {
object_id: Url, object_id: Url,
data: &Data<Self::DataType>, data: &Data<Self::DataType>,
) -> Result<Option<Self>, Self::Error> { ) -> Result<Option<Self>, Self::Error> {
let posts = data.articles.lock().unwrap(); let article = DbArticle::read_from_ap_id(&object_id.into(), &mut data.db_connection).ok();
let res = posts Ok(article)
.clone()
.into_iter()
.find(|u| u.1.ap_id.inner() == &object_id)
.map(|u| u.1);
Ok(res)
} }
async fn into_json(self, data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> { async fn into_json(self, data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
let instance = self.instance.dereference_local(data).await?; let instance: DbInstance = ObjectId::from(self.instance_id)
.dereference_local(data)
.await?;
Ok(ApubArticle { Ok(ApubArticle {
kind: Default::default(), kind: Default::default(),
id: self.ap_id.clone(), id: self.ap_id.clone().into(),
attributed_to: self.instance.clone(), attributed_to: self.instance_id.clone().into(),
to: vec![public(), instance.followers_url()?], to: vec![public(), instance.followers_url()?],
edits: self.edits_id()?, edits: self.edits_id()?,
latest_version: self.latest_version, latest_version: self.latest_version,
@ -91,26 +70,22 @@ impl Object for DbArticle {
} }
async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> { async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> {
let mut article = DbArticle { let form = DbArticleForm {
title: json.name, title: json.name,
text: json.content, text: json.content,
ap_id: json.id, ap_id: json.id.into(),
instance: json.attributed_to, latest_version: json.latest_version.0,
// TODO: shouldnt overwrite existing edits
edits: vec![],
latest_version: json.latest_version,
local: false, local: false,
instance_id: json.attributed_to.into(),
}; };
let mut article = DbArticle::create(&form, &data.db_connection)?;
{ {
let mut lock = data.articles.lock().unwrap(); let mut lock = data.articles.lock().unwrap();
lock.insert(article.ap_id.inner().clone(), article.clone()); lock.insert(article.ap_id.clone().into(), article.clone());
} }
let edits = json.edits.dereference(&article, data).await?; json.edits.dereference(&article, data).await?;
// include edits in return value (they are already written to db, no need to do that here)
article.edits = edits.0;
Ok(article) Ok(article)
} }

View File

@ -1,6 +1,6 @@
use crate::database::DatabaseHandle; use crate::database::{article::DbArticle, MyDataHandle};
use crate::error::Error; use crate::error::Error;
use crate::federation::objects::article::{ApubArticle, DbArticle}; use crate::federation::objects::article::ApubArticle;
use crate::federation::objects::instance::DbInstance; use crate::federation::objects::instance::DbInstance;
use activitypub_federation::kinds::collection::CollectionType; use activitypub_federation::kinds::collection::CollectionType;
@ -28,7 +28,7 @@ pub struct DbArticleCollection(Vec<DbArticle>);
#[async_trait::async_trait] #[async_trait::async_trait]
impl Collection for DbArticleCollection { impl Collection for DbArticleCollection {
type Owner = DbInstance; type Owner = DbInstance;
type DataType = DatabaseHandle; type DataType = MyDataHandle;
type Kind = ArticleCollection; type Kind = ArticleCollection;
type Error = Error; type Error = Error;

View File

@ -1,54 +1,13 @@
use crate::database::DatabaseHandle; use crate::database::article::DbArticle;
use crate::error::{Error, MyResult}; use crate::database::edit::{DbEdit, DbEditForm, EditVersion};
use crate::database::MyDataHandle;
use crate::federation::objects::article::DbArticle; use crate::error::Error;
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use activitypub_federation::fetch::object_id::ObjectId; use activitypub_federation::fetch::object_id::ObjectId;
use activitypub_federation::traits::Object; use activitypub_federation::traits::Object;
use diffy::create_patch;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::Digest;
use sha2::Sha224;
use url::Url; use url::Url;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct EditVersion(String);
impl Default for EditVersion {
fn default() -> Self {
let sha224 = Sha224::new();
let hash = format!("{:X}", sha224.finalize());
EditVersion(hash)
}
}
/// Represents a single change to the article.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct DbEdit {
pub id: ObjectId<DbEdit>,
pub diff: String,
pub article_id: ObjectId<DbArticle>,
pub version: EditVersion,
pub local: bool,
}
impl DbEdit {
pub fn new(original_article: &DbArticle, updated_text: &str) -> MyResult<Self> {
let diff = create_patch(&original_article.text, updated_text);
let mut sha224 = Sha224::new();
sha224.update(diff.to_bytes());
let hash = format!("{:X}", sha224.finalize());
let edit_id = ObjectId::parse(&format!("{}/{}", original_article.ap_id, hash))?;
Ok(DbEdit {
id: edit_id,
diff: diff.to_string(),
article_id: original_article.ap_id.clone(),
version: EditVersion(hash),
local: true,
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub enum EditType { pub enum EditType {
Edit, Edit,
@ -68,7 +27,7 @@ pub struct ApubEdit {
#[async_trait::async_trait] #[async_trait::async_trait]
impl Object for DbEdit { impl Object for DbEdit {
type DataType = DatabaseHandle; type DataType = MyDataHandle;
type Kind = ApubEdit; type Kind = ApubEdit;
type Error = Error; type Error = Error;
@ -80,18 +39,15 @@ impl Object for DbEdit {
} }
async fn into_json(self, data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> { async fn into_json(self, data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
let article_version = { let article = DbArticle::read(self.article_id, &mut data.db_connection)?;
let mut lock = data.articles.lock().unwrap();
let article = lock.get_mut(self.article_id.inner()).unwrap();
article.latest_version.clone()
};
Ok(ApubEdit { Ok(ApubEdit {
kind: EditType::Edit, kind: EditType::Edit,
id: self.id, id: self.ap_id.into(),
content: self.diff, content: self.diff,
version: self.version, version: self.version,
previous_version: article_version, // TODO: this is wrong
object: self.article_id, previous_version: article.latest_version,
object: article.ap_id.into(),
}) })
} }
@ -104,16 +60,15 @@ impl Object for DbEdit {
} }
async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> { async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> {
let edit = Self { let article = json.object.dereference(data).await?;
id: json.id, let form = DbEditForm {
ap_id: json.id.into(),
diff: json.content, diff: json.content,
article_id: json.object, article_id: article.id,
version: json.version, version: json.version,
local: false, local: false,
}; };
let mut lock = data.articles.lock().unwrap(); let edit = DbEdit::create(&form, &mut data.db_connection)?;
let article = lock.get_mut(edit.article_id.inner()).unwrap();
article.edits.push(edit.clone());
Ok(edit) Ok(edit)
} }
} }

View File

@ -1,8 +1,9 @@
use crate::database::DatabaseHandle; use crate::database::article::DbArticle;
use crate::database::MyDataHandle;
use crate::error::Error; use crate::error::Error;
use crate::federation::objects::article::DbArticle; use crate::federation::objects::edit::ApubEdit;
use crate::federation::objects::edit::{ApubEdit, DbEdit};
use crate::database::edit::DbEdit;
use activitypub_federation::kinds::collection::OrderedCollectionType; use activitypub_federation::kinds::collection::OrderedCollectionType;
use activitypub_federation::{ use activitypub_federation::{
config::Data, config::Data,
@ -28,7 +29,7 @@ pub struct DbEditCollection(pub Vec<DbEdit>);
#[async_trait::async_trait] #[async_trait::async_trait]
impl Collection for DbEditCollection { impl Collection for DbEditCollection {
type Owner = DbArticle; type Owner = DbArticle;
type DataType = DatabaseHandle; type DataType = MyDataHandle;
type Kind = ApubEditCollection; type Kind = ApubEditCollection;
type Error = Error; type Error = Error;
@ -36,10 +37,7 @@ impl Collection for DbEditCollection {
owner: &Self::Owner, owner: &Self::Owner,
data: &Data<Self::DataType>, data: &Data<Self::DataType>,
) -> Result<Self::Kind, Self::Error> { ) -> Result<Self::Kind, Self::Error> {
let edits = { let edits = DbEditCollection(DbEdit::for_article(owner.id, &mut data.db_connection)?);
let lock = data.articles.lock().unwrap();
DbEditCollection(lock.get(owner.ap_id.inner()).unwrap().edits.clone())
};
let edits = future::try_join_all( let edits = future::try_join_all(
edits edits

View File

@ -1,6 +1,6 @@
use crate::error::{Error, MyResult}; use crate::error::{Error, MyResult};
use crate::federation::objects::articles_collection::DbArticleCollection; use crate::federation::objects::articles_collection::DbArticleCollection;
use crate::{database::DatabaseHandle, federation::activities::follow::Follow}; use crate::{database::MyDataHandle, federation::activities::follow::Follow};
use activitypub_federation::activity_sending::SendActivityTask; use activitypub_federation::activity_sending::SendActivityTask;
use activitypub_federation::fetch::collection_id::CollectionId; use activitypub_federation::fetch::collection_id::CollectionId;
use activitypub_federation::kinds::actor::ServiceType; use activitypub_federation::kinds::actor::ServiceType;
@ -55,11 +55,7 @@ impl DbInstance {
.collect() .collect()
} }
pub async fn follow( pub async fn follow(&self, other: &DbInstance, data: &Data<MyDataHandle>) -> Result<(), Error> {
&self,
other: &DbInstance,
data: &Data<DatabaseHandle>,
) -> Result<(), Error> {
let follow = Follow::new(self.ap_id.clone(), other.ap_id.clone())?; let follow = Follow::new(self.ap_id.clone(), other.ap_id.clone())?;
self.send(follow, vec![other.shared_inbox_or_inbox()], data) self.send(follow, vec![other.shared_inbox_or_inbox()], data)
.await?; .await?;
@ -70,7 +66,7 @@ impl DbInstance {
&self, &self,
activity: Activity, activity: Activity,
extra_recipients: Vec<DbInstance>, extra_recipients: Vec<DbInstance>,
data: &Data<DatabaseHandle>, data: &Data<MyDataHandle>,
) -> Result<(), <Activity as ActivityHandler>::Error> ) -> Result<(), <Activity as ActivityHandler>::Error>
where where
Activity: ActivityHandler + Serialize + Debug + Send + Sync, Activity: ActivityHandler + Serialize + Debug + Send + Sync,
@ -91,7 +87,7 @@ impl DbInstance {
&self, &self,
activity: Activity, activity: Activity,
recipients: Vec<Url>, recipients: Vec<Url>,
data: &Data<DatabaseHandle>, data: &Data<MyDataHandle>,
) -> Result<(), <Activity as ActivityHandler>::Error> ) -> Result<(), <Activity as ActivityHandler>::Error>
where where
Activity: ActivityHandler + Serialize + Debug + Send + Sync, Activity: ActivityHandler + Serialize + Debug + Send + Sync,
@ -111,7 +107,7 @@ impl DbInstance {
#[async_trait::async_trait] #[async_trait::async_trait]
impl Object for DbInstance { impl Object for DbInstance {
type DataType = DatabaseHandle; type DataType = MyDataHandle;
type Kind = ApubInstance; type Kind = ApubInstance;
type Error = Error; type Error = Error;

View File

@ -1,4 +1,4 @@
use crate::database::DatabaseHandle; use crate::database::MyDataHandle;
use crate::error::MyResult; use crate::error::MyResult;
use crate::federation::activities::accept::Accept; use crate::federation::activities::accept::Accept;
use crate::federation::activities::follow::Follow; use crate::federation::activities::follow::Follow;
@ -37,7 +37,7 @@ pub fn federation_routes() -> Router {
#[debug_handler] #[debug_handler]
async fn http_get_instance( async fn http_get_instance(
data: Data<DatabaseHandle>, data: Data<MyDataHandle>,
) -> MyResult<FederationJson<WithContext<ApubInstance>>> { ) -> MyResult<FederationJson<WithContext<ApubInstance>>> {
let db_instance = data.local_instance(); let db_instance = data.local_instance();
let json_instance = db_instance.into_json(&data).await?; let json_instance = db_instance.into_json(&data).await?;
@ -46,7 +46,7 @@ async fn http_get_instance(
#[debug_handler] #[debug_handler]
async fn http_get_all_articles( async fn http_get_all_articles(
data: Data<DatabaseHandle>, data: Data<MyDataHandle>,
) -> MyResult<FederationJson<WithContext<ArticleCollection>>> { ) -> MyResult<FederationJson<WithContext<ArticleCollection>>> {
let collection = DbArticleCollection::read_local(&data.local_instance(), &data).await?; let collection = DbArticleCollection::read_local(&data.local_instance(), &data).await?;
Ok(FederationJson(WithContext::new_default(collection))) Ok(FederationJson(WithContext::new_default(collection)))
@ -55,7 +55,7 @@ async fn http_get_all_articles(
#[debug_handler] #[debug_handler]
async fn http_get_article( async fn http_get_article(
Path(title): Path<String>, Path(title): Path<String>,
data: Data<DatabaseHandle>, data: Data<MyDataHandle>,
) -> MyResult<FederationJson<WithContext<ApubArticle>>> { ) -> MyResult<FederationJson<WithContext<ApubArticle>>> {
let article = { let article = {
let lock = data.articles.lock().unwrap(); let lock = data.articles.lock().unwrap();
@ -68,7 +68,7 @@ async fn http_get_article(
#[debug_handler] #[debug_handler]
async fn http_get_article_edits( async fn http_get_article_edits(
Path(title): Path<String>, Path(title): Path<String>,
data: Data<DatabaseHandle>, data: Data<MyDataHandle>,
) -> MyResult<FederationJson<WithContext<ApubEditCollection>>> { ) -> MyResult<FederationJson<WithContext<ApubEditCollection>>> {
let article = { let article = {
let lock = data.articles.lock().unwrap(); let lock = data.articles.lock().unwrap();
@ -93,12 +93,9 @@ pub enum InboxActivities {
#[debug_handler] #[debug_handler]
pub async fn http_post_inbox( pub async fn http_post_inbox(
data: Data<DatabaseHandle>, data: Data<MyDataHandle>,
activity_data: ActivityData, activity_data: ActivityData,
) -> impl IntoResponse { ) -> impl IntoResponse {
receive_activity::<WithContext<InboxActivities>, DbInstance, DatabaseHandle>( receive_activity::<WithContext<InboxActivities>, DbInstance, MyDataHandle>(activity_data, &data)
activity_data, .await
&data,
)
.await
} }

View File

@ -1,11 +1,11 @@
use crate::utils::generate_activity_id;
use activitypub_federation::config::FederationMiddleware;
use axum::{Router, Server};
use crate::api::api_routes; use crate::api::api_routes;
use crate::error::MyResult; use crate::error::MyResult;
use crate::federation::routes::federation_routes; use crate::federation::routes::federation_routes;
use crate::utils::generate_activity_id;
use activitypub_federation::config::FederationMiddleware;
use axum::{Router, Server};
use diesel::Connection;
use diesel::PgConnection;
use federation::federation_config; use federation::federation_config;
use std::net::ToSocketAddrs; use std::net::ToSocketAddrs;
use tracing::info; use tracing::info;
@ -36,3 +36,9 @@ pub async fn start(hostname: &str) -> MyResult<()> {
Ok(()) Ok(())
} }
pub fn establish_db_connection() -> MyResult<PgConnection> {
// TODO: read from config file
let database_url = "postgres://fediwiki:password@localhost:5432/fediwiki";
Ok(PgConnection::establish(&database_url)?)
}

View File

@ -1,5 +1,6 @@
use crate::database::edit::DbEdit;
use crate::database::edit::EditVersion;
use crate::error::MyResult; use crate::error::MyResult;
use crate::federation::objects::edit::{DbEdit, EditVersion};
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};

View File

@ -2,8 +2,8 @@ use activitypub_federation::fetch::object_id::ObjectId;
use fediwiki::api::{ use fediwiki::api::{
ApiConflict, CreateArticleData, EditArticleData, FollowInstance, GetArticleData, ResolveObject, ApiConflict, CreateArticleData, EditArticleData, FollowInstance, GetArticleData, ResolveObject,
}; };
use fediwiki::database::DbArticle;
use fediwiki::error::MyResult; use fediwiki::error::MyResult;
use fediwiki::federation::objects::article::DbArticle;
use fediwiki::federation::objects::instance::DbInstance; use fediwiki::federation::objects::instance::DbInstance;
use fediwiki::start; use fediwiki::start;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;

View File

@ -10,8 +10,8 @@ use common::get;
use fediwiki::api::{ use fediwiki::api::{
ApiConflict, EditArticleData, ForkArticleData, ResolveObject, SearchArticleData, ApiConflict, EditArticleData, ForkArticleData, ResolveObject, SearchArticleData,
}; };
use fediwiki::database::DbArticle;
use fediwiki::error::MyResult; use fediwiki::error::MyResult;
use fediwiki::federation::objects::article::DbArticle;
use fediwiki::federation::objects::edit::ApubEdit; use fediwiki::federation::objects::edit::ApubEdit;
use fediwiki::federation::objects::instance::DbInstance; use fediwiki::federation::objects::instance::DbInstance;
use serial_test::serial; use serial_test::serial;
@ -438,7 +438,7 @@ async fn test_fork_article() -> MyResult<()> {
assert!(fork_res.local); assert!(fork_res.local);
let beta_instance: DbInstance = get(data.hostname_beta, "instance").await?; let beta_instance: DbInstance = get(data.hostname_beta, "instance").await?;
assert_eq!(fork_res.instance, beta_instance.ap_id); assert_eq!(fork_res.instance_id, beta_instance.ap_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 = SearchArticleData {