mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-24 10:11:10 +00:00
wip: Add diesel and postgres
This commit is contained in:
parent
4371fc2edc
commit
e030419cc5
31 changed files with 627 additions and 261 deletions
62
Cargo.lock
generated
62
Cargo.lock
generated
|
@ -264,6 +264,12 @@ version = "0.6.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.5.0"
|
||||
|
@ -486,6 +492,51 @@ dependencies = [
|
|||
"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]]
|
||||
name = "diffy"
|
||||
version = "0.3.0"
|
||||
|
@ -615,6 +666,8 @@ dependencies = [
|
|||
"axum",
|
||||
"axum-macros",
|
||||
"chrono",
|
||||
"diesel",
|
||||
"diesel-derive-newtype",
|
||||
"diffy",
|
||||
"enum_delegate",
|
||||
"env_logger",
|
||||
|
@ -1408,6 +1461,15 @@ version = "0.2.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.69"
|
||||
|
|
|
@ -10,6 +10,8 @@ async-trait = "0.1.74"
|
|||
axum = "0.6.20"
|
||||
axum-macros = "0.3.8"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
diesel = {version = "2.1.4", features = ["postgres"] }
|
||||
diesel-derive-newtype = "2.1.0"
|
||||
diffy = "0.3.0"
|
||||
enum_delegate = "0.2.0"
|
||||
env_logger = { version = "0.10.1", default-features = false }
|
||||
|
|
9
diesel.toml
Normal file
9
diesel.toml
Normal 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"
|
6
migrations/00000000000000_diesel_initial_setup/down.sql
Normal file
6
migrations/00000000000000_diesel_initial_setup/down.sql
Normal 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();
|
36
migrations/00000000000000_diesel_initial_setup/up.sql
Normal file
36
migrations/00000000000000_diesel_initial_setup/up.sql
Normal 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;
|
2
migrations/2023-11-28-150402_article/down.sql
Normal file
2
migrations/2023-11-28-150402_article/down.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
drop table edit;
|
||||
drop table article;
|
18
migrations/2023-11-28-150402_article/up.sql
Normal file
18
migrations/2023-11-28-150402_article/up.sql
Normal 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
|
||||
)
|
96
src/api.rs
96
src/api.rs
|
@ -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::federation::activities::create_article::CreateArticle;
|
||||
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::utils::generate_article_version;
|
||||
use activitypub_federation::config::Data;
|
||||
|
@ -42,7 +43,7 @@ pub struct CreateArticleData {
|
|||
/// Create a new article with empty text, and federate it to followers.
|
||||
#[debug_handler]
|
||||
async fn create_article(
|
||||
data: Data<DatabaseHandle>,
|
||||
data: Data<MyDataHandle>,
|
||||
Form(create_article): Form<CreateArticleData>,
|
||||
) -> MyResult<Json<DbArticle>> {
|
||||
{
|
||||
|
@ -55,26 +56,23 @@ async fn create_article(
|
|||
}
|
||||
}
|
||||
|
||||
let local_instance_id = data.local_instance().ap_id;
|
||||
let ap_id = ObjectId::parse(&format!(
|
||||
let instance_id: DbUrl = data.local_instance().ap_id.into();
|
||||
let ap_id = Url::parse(&format!(
|
||||
"http://{}:{}/article/{}",
|
||||
local_instance_id.inner().domain().unwrap(),
|
||||
local_instance_id.inner().port().unwrap(),
|
||||
instance_id.domain().unwrap(),
|
||||
instance_id.port().unwrap(),
|
||||
create_article.title
|
||||
))?;
|
||||
let article = DbArticle {
|
||||
))?
|
||||
.into();
|
||||
let form = DbArticleForm {
|
||||
title: create_article.title,
|
||||
text: String::new(),
|
||||
ap_id,
|
||||
latest_version: EditVersion::default(),
|
||||
edits: vec![],
|
||||
instance: local_instance_id,
|
||||
latest_version: Default::default(),
|
||||
instance_id,
|
||||
local: true,
|
||||
};
|
||||
{
|
||||
let mut articles = data.articles.lock().unwrap();
|
||||
articles.insert(article.ap_id.inner().clone(), article.clone());
|
||||
}
|
||||
let article = DbArticle::create(&form, &data.db_connection)?;
|
||||
|
||||
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`.
|
||||
#[debug_handler]
|
||||
async fn edit_article(
|
||||
data: Data<DatabaseHandle>,
|
||||
data: Data<MyDataHandle>,
|
||||
Form(edit_form): Form<EditArticleData>,
|
||||
) -> MyResult<Json<Option<ApiConflict>>> {
|
||||
// resolve conflict if any
|
||||
|
@ -138,14 +136,14 @@ async fn edit_article(
|
|||
} else {
|
||||
// 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.
|
||||
let ancestor =
|
||||
generate_article_version(&original_article.edits, &edit_form.previous_version)?;
|
||||
let edits = DbEdit::for_article(original_article.id, &data.db_connection)?;
|
||||
let ancestor = generate_article_version(&edits, &edit_form.previous_version)?;
|
||||
let patch = create_patch(&ancestor, &edit_form.new_text);
|
||||
|
||||
let db_conflict = DbConflict {
|
||||
id: random(),
|
||||
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,
|
||||
};
|
||||
{
|
||||
|
@ -158,23 +156,16 @@ async fn edit_article(
|
|||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
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.
|
||||
#[debug_handler]
|
||||
async fn get_article(
|
||||
Query(query): Query<GetArticleData>,
|
||||
data: Data<DatabaseHandle>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<DbArticle>> {
|
||||
let articles = data.articles.lock().unwrap();
|
||||
let article = articles
|
||||
.iter()
|
||||
.find(|a| a.1.ap_id == query.ap_id)
|
||||
.ok_or(anyhow!("not found"))?
|
||||
.1
|
||||
.clone();
|
||||
Ok(Json(article))
|
||||
Ok(Json(DbArticle::read(query.id, &data.db_connection)?))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
|
@ -187,7 +178,7 @@ pub struct ResolveObject {
|
|||
#[debug_handler]
|
||||
async fn resolve_instance(
|
||||
Query(query): Query<ResolveObject>,
|
||||
data: Data<DatabaseHandle>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<DbInstance>> {
|
||||
let instance: DbInstance = ObjectId::from(query.id).dereference(&data).await?;
|
||||
Ok(Json(instance))
|
||||
|
@ -198,7 +189,7 @@ async fn resolve_instance(
|
|||
#[debug_handler]
|
||||
async fn resolve_article(
|
||||
Query(query): Query<ResolveObject>,
|
||||
data: Data<DatabaseHandle>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<DbArticle>> {
|
||||
let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?;
|
||||
Ok(Json(article))
|
||||
|
@ -206,7 +197,7 @@ async fn resolve_article(
|
|||
|
||||
/// Retrieve the local instance info.
|
||||
#[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()))
|
||||
}
|
||||
|
||||
|
@ -219,7 +210,7 @@ pub struct FollowInstance {
|
|||
/// updated articles.
|
||||
#[debug_handler]
|
||||
async fn follow_instance(
|
||||
data: Data<DatabaseHandle>,
|
||||
data: Data<MyDataHandle>,
|
||||
Form(query): Form<FollowInstance>,
|
||||
) -> MyResult<()> {
|
||||
let instance = query.instance_id.dereference(&data).await?;
|
||||
|
@ -229,7 +220,7 @@ async fn follow_instance(
|
|||
|
||||
/// Get a list of all unresolved edit conflicts.
|
||||
#[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: Vec<ApiConflict> = try_join_all(conflicts.into_iter().map(|c| {
|
||||
let data = data.reset_request_count();
|
||||
|
@ -253,7 +244,7 @@ pub struct SearchArticleData {
|
|||
#[debug_handler]
|
||||
async fn search_article(
|
||||
Query(query): Query<SearchArticleData>,
|
||||
data: Data<DatabaseHandle>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<Vec<DbArticle>>> {
|
||||
let articles = data.articles.lock().unwrap();
|
||||
let article = articles
|
||||
|
@ -277,7 +268,7 @@ pub struct ForkArticleData {
|
|||
/// how an article should be edited.
|
||||
#[debug_handler]
|
||||
async fn fork_article(
|
||||
data: Data<DatabaseHandle>,
|
||||
data: Data<MyDataHandle>,
|
||||
Form(fork_form): Form<ForkArticleData>,
|
||||
) -> MyResult<Json<DbArticle>> {
|
||||
let article = {
|
||||
|
@ -296,28 +287,27 @@ async fn fork_article(
|
|||
.clone()
|
||||
};
|
||||
|
||||
let local_instance_id = data.local_instance().ap_id;
|
||||
let ap_id = ObjectId::parse(&format!(
|
||||
let instance_id: DbUrl = data.local_instance().ap_id.into();
|
||||
let ap_id = Url::parse(&format!(
|
||||
"http://{}:{}/article/{}",
|
||||
local_instance_id.inner().domain().unwrap(),
|
||||
local_instance_id.inner().port().unwrap(),
|
||||
instance_id.domain().unwrap(),
|
||||
instance_id.port().unwrap(),
|
||||
original_article.title
|
||||
))?;
|
||||
let forked_article = DbArticle {
|
||||
))?
|
||||
.into();
|
||||
let form = DbArticleForm {
|
||||
title: original_article.title.clone(),
|
||||
text: original_article.text.clone(),
|
||||
ap_id,
|
||||
latest_version: original_article.latest_version.clone(),
|
||||
edits: original_article.edits.clone(),
|
||||
instance: local_instance_id,
|
||||
latest_version: original_article.latest_version.0.clone(),
|
||||
instance_id,
|
||||
local: true,
|
||||
};
|
||||
{
|
||||
let mut articles = data.articles.lock().unwrap();
|
||||
articles.insert(forked_article.ap_id.inner().clone(), forked_article.clone());
|
||||
}
|
||||
let article = DbArticle::create(&form, &data.db_connection)?;
|
||||
|
||||
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
78
src/database/article.rs
Normal 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
96
src/database/dburl.rs
Normal 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
86
src/database/edit.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -1,26 +1,48 @@
|
|||
use crate::api::ApiConflict;
|
||||
use crate::database::article::DbArticle;
|
||||
use crate::database::edit::DbEdit;
|
||||
use crate::error::MyResult;
|
||||
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::utils::generate_article_version;
|
||||
use activitypub_federation::config::Data;
|
||||
use activitypub_federation::fetch::object_id::ObjectId;
|
||||
use diesel::{Identifiable, PgConnection, QueryDsl};
|
||||
use diffy::{apply, merge, Patch};
|
||||
use edit::EditVersion;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use std::sync::{Arc, Mutex};
|
||||
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>>,
|
||||
// TODO: remove this
|
||||
pub articles: Mutex<HashMap<Url, DbArticle>>,
|
||||
pub conflicts: Mutex<Vec<DbConflict>>,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
impl FakeDatabase {
|
||||
pub fn local_instance(&self) -> DbInstance {
|
||||
let lock = self.instances.lock().unwrap();
|
||||
lock.iter().find(|i| i.1.local).unwrap().1.clone()
|
||||
|
@ -38,7 +60,7 @@ pub struct DbConflict {
|
|||
impl DbConflict {
|
||||
pub async fn to_api_conflict(
|
||||
&self,
|
||||
data: &Data<DatabaseHandle>,
|
||||
data: &Data<MyDataHandle>,
|
||||
) -> MyResult<Option<ApiConflict>> {
|
||||
let original_article = {
|
||||
let mut lock = data.articles.lock().unwrap();
|
||||
|
@ -47,7 +69,8 @@ impl DbConflict {
|
|||
};
|
||||
|
||||
// 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)?;
|
||||
// apply self.diff to ancestor to get `ours`
|
||||
|
@ -67,7 +90,7 @@ impl DbConflict {
|
|||
Ok(Some(ApiConflict {
|
||||
id: self.id,
|
||||
three_way_merge,
|
||||
article_id: original_article.ap_id.clone(),
|
||||
article_id: original_article.ap_id.into(),
|
||||
previous_version: original_article.latest_version,
|
||||
}))
|
||||
}
|
31
src/database/schema.rs
Normal file
31
src/database/schema.rs
Normal 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,);
|
|
@ -1,7 +1,7 @@
|
|||
use crate::error::MyResult;
|
||||
use crate::federation::objects::instance::DbInstance;
|
||||
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::{
|
||||
config::Data, fetch::object_id::ObjectId, kinds::activity::AcceptType, traits::ActivityHandler,
|
||||
};
|
||||
|
@ -32,7 +32,7 @@ impl Accept {
|
|||
|
||||
#[async_trait::async_trait]
|
||||
impl ActivityHandler for Accept {
|
||||
type DataType = DatabaseHandle;
|
||||
type DataType = MyDataHandle;
|
||||
type Error = crate::error::Error;
|
||||
|
||||
fn id(&self) -> &Url {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::database::DatabaseHandle;
|
||||
use crate::database::{article::DbArticle, MyDataHandle};
|
||||
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::utils::generate_activity_id;
|
||||
use activitypub_federation::kinds::activity::CreateType;
|
||||
|
@ -26,10 +26,7 @@ pub struct CreateArticle {
|
|||
}
|
||||
|
||||
impl CreateArticle {
|
||||
pub async fn send_to_followers(
|
||||
article: DbArticle,
|
||||
data: &Data<DatabaseHandle>,
|
||||
) -> MyResult<()> {
|
||||
pub async fn send_to_followers(article: DbArticle, data: &Data<MyDataHandle>) -> MyResult<()> {
|
||||
let local_instance = data.local_instance();
|
||||
let object = article.clone().into_json(data).await?;
|
||||
let id = generate_activity_id(local_instance.ap_id.inner())?;
|
||||
|
@ -48,7 +45,7 @@ impl CreateArticle {
|
|||
}
|
||||
#[async_trait::async_trait]
|
||||
impl ActivityHandler for CreateArticle {
|
||||
type DataType = DatabaseHandle;
|
||||
type DataType = MyDataHandle;
|
||||
type Error = crate::error::Error;
|
||||
|
||||
fn id(&self) -> &Url {
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
use crate::error::MyResult;
|
||||
use crate::federation::objects::instance::DbInstance;
|
||||
use crate::{
|
||||
database::DatabaseHandle, federation::activities::accept::Accept, generate_activity_id,
|
||||
};
|
||||
use crate::{database::MyDataHandle, federation::activities::accept::Accept, generate_activity_id};
|
||||
use activitypub_federation::{
|
||||
config::Data,
|
||||
fetch::object_id::ObjectId,
|
||||
|
@ -36,7 +34,7 @@ impl Follow {
|
|||
|
||||
#[async_trait::async_trait]
|
||||
impl ActivityHandler for Follow {
|
||||
type DataType = DatabaseHandle;
|
||||
type DataType = MyDataHandle;
|
||||
type Error = crate::error::Error;
|
||||
|
||||
fn id(&self) -> &Url {
|
||||
|
|
|
@ -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::federation::activities::update_local_article::UpdateLocalArticle;
|
||||
use crate::federation::activities::update_remote_article::UpdateRemoteArticle;
|
||||
use crate::federation::objects::article::DbArticle;
|
||||
use crate::federation::objects::edit::DbEdit;
|
||||
use crate::federation::objects::instance::DbInstance;
|
||||
use activitypub_federation::config::Data;
|
||||
use activitypub_federation::fetch::object_id::ObjectId;
|
||||
|
||||
pub mod accept;
|
||||
pub mod create_article;
|
||||
|
@ -14,29 +16,27 @@ pub mod update_local_article;
|
|||
pub mod update_remote_article;
|
||||
|
||||
pub async fn submit_article_update(
|
||||
data: &Data<DatabaseHandle>,
|
||||
data: &Data<MyDataHandle>,
|
||||
new_text: String,
|
||||
original_article: &DbArticle,
|
||||
) -> 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 {
|
||||
let updated_article = {
|
||||
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.latest_version = edit.version.clone();
|
||||
article.edits.push(edit.clone());
|
||||
article.clone()
|
||||
};
|
||||
|
||||
UpdateLocalArticle::send(updated_article, vec![], data).await?;
|
||||
} else {
|
||||
UpdateRemoteArticle::send(
|
||||
edit,
|
||||
original_article.instance.dereference(data).await?,
|
||||
data,
|
||||
)
|
||||
.await?;
|
||||
let instance: DbInstance = ObjectId::from(original_article.instance_id.clone())
|
||||
.dereference(data)
|
||||
.await?;
|
||||
UpdateRemoteArticle::send(edit, instance, data).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::database::DatabaseHandle;
|
||||
use crate::database::MyDataHandle;
|
||||
use crate::error::MyResult;
|
||||
use crate::federation::objects::edit::ApubEdit;
|
||||
use crate::federation::objects::instance::DbInstance;
|
||||
|
@ -30,7 +30,7 @@ impl RejectEdit {
|
|||
pub async fn send(
|
||||
edit: ApubEdit,
|
||||
user_instance: DbInstance,
|
||||
data: &Data<DatabaseHandle>,
|
||||
data: &Data<MyDataHandle>,
|
||||
) -> MyResult<()> {
|
||||
let local_instance = data.local_instance();
|
||||
let id = generate_activity_id(local_instance.ap_id.inner())?;
|
||||
|
@ -50,7 +50,7 @@ impl RejectEdit {
|
|||
|
||||
#[async_trait::async_trait]
|
||||
impl ActivityHandler for RejectEdit {
|
||||
type DataType = DatabaseHandle;
|
||||
type DataType = MyDataHandle;
|
||||
type Error = crate::error::Error;
|
||||
|
||||
fn id(&self) -> &Url {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::database::DatabaseHandle;
|
||||
use crate::database::{article::DbArticle, MyDataHandle};
|
||||
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::utils::generate_activity_id;
|
||||
|
@ -32,7 +32,7 @@ impl UpdateLocalArticle {
|
|||
pub async fn send(
|
||||
article: DbArticle,
|
||||
extra_recipients: Vec<DbInstance>,
|
||||
data: &Data<DatabaseHandle>,
|
||||
data: &Data<MyDataHandle>,
|
||||
) -> MyResult<()> {
|
||||
debug_assert!(article.local);
|
||||
let local_instance = data.local_instance();
|
||||
|
@ -55,7 +55,7 @@ impl UpdateLocalArticle {
|
|||
|
||||
#[async_trait::async_trait]
|
||||
impl ActivityHandler for UpdateLocalArticle {
|
||||
type DataType = DatabaseHandle;
|
||||
type DataType = MyDataHandle;
|
||||
type Error = crate::error::Error;
|
||||
|
||||
fn id(&self) -> &Url {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::database::DatabaseHandle;
|
||||
use crate::database::MyDataHandle;
|
||||
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::utils::generate_activity_id;
|
||||
use activitypub_federation::kinds::activity::UpdateType;
|
||||
|
@ -13,6 +13,8 @@ use activitypub_federation::{
|
|||
};
|
||||
use diffy::{apply, Patch};
|
||||
|
||||
use crate::database::article::DbArticle;
|
||||
use crate::database::edit::DbEdit;
|
||||
use crate::federation::activities::reject::RejectEdit;
|
||||
use crate::federation::activities::update_local_article::UpdateLocalArticle;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -35,7 +37,7 @@ impl UpdateRemoteArticle {
|
|||
pub async fn send(
|
||||
edit: DbEdit,
|
||||
article_instance: DbInstance,
|
||||
data: &Data<DatabaseHandle>,
|
||||
data: &Data<MyDataHandle>,
|
||||
) -> MyResult<()> {
|
||||
let local_instance = data.local_instance();
|
||||
let id = generate_activity_id(local_instance.ap_id.inner())?;
|
||||
|
@ -55,7 +57,7 @@ impl UpdateRemoteArticle {
|
|||
|
||||
#[async_trait::async_trait]
|
||||
impl ActivityHandler for UpdateRemoteArticle {
|
||||
type DataType = DatabaseHandle;
|
||||
type DataType = MyDataHandle;
|
||||
type Error = crate::error::Error;
|
||||
|
||||
fn id(&self) -> &Url {
|
||||
|
@ -80,13 +82,9 @@ impl ActivityHandler for UpdateRemoteArticle {
|
|||
|
||||
match apply(&article_text, &patch) {
|
||||
Ok(applied) => {
|
||||
let article = {
|
||||
let edit = DbEdit::from_json(self.object.clone(), data).await?;
|
||||
let mut lock = data.articles.lock().unwrap();
|
||||
let article = lock.get_mut(edit.article_id.inner()).unwrap();
|
||||
article.text = applied;
|
||||
article.clone()
|
||||
};
|
||||
let edit = DbEdit::from_json(self.object.clone(), data).await?;
|
||||
let article =
|
||||
DbArticle::update_text(edit.article_id, &applied, &mut data.db_connection)?;
|
||||
UpdateLocalArticle::send(article, vec![self.actor.dereference(data).await?], data)
|
||||
.await?;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::database::{Database, DatabaseHandle};
|
||||
use crate::database::{FakeDatabase, MyData, MyDataHandle};
|
||||
use crate::error::Error;
|
||||
use crate::establish_db_connection;
|
||||
use crate::federation::objects::instance::DbInstance;
|
||||
use activitypub_federation::config::FederationConfig;
|
||||
use activitypub_federation::fetch::collection_id::CollectionId;
|
||||
|
@ -13,13 +14,13 @@ pub mod activities;
|
|||
pub mod objects;
|
||||
pub mod routes;
|
||||
|
||||
pub async fn federation_config(hostname: &str) -> Result<FederationConfig<DatabaseHandle>, Error> {
|
||||
let ap_id = Url::parse(&format!("http://{}", hostname))?.into();
|
||||
pub async fn federation_config(hostname: &str) -> Result<FederationConfig<MyDataHandle>, Error> {
|
||||
let ap_id = Url::parse(&format!("http://{}", hostname))?;
|
||||
let articles_id = CollectionId::parse(&format!("http://{}/all_articles", hostname))?;
|
||||
let inbox = Url::parse(&format!("http://{}/inbox", hostname))?;
|
||||
let keypair = generate_actor_keypair()?;
|
||||
let local_instance = DbInstance {
|
||||
ap_id,
|
||||
ap_id: ap_id.into(),
|
||||
articles_id,
|
||||
inbox,
|
||||
public_key: keypair.public_key,
|
||||
|
@ -29,7 +30,7 @@ pub async fn federation_config(hostname: &str) -> Result<FederationConfig<Databa
|
|||
follows: vec![],
|
||||
local: true,
|
||||
};
|
||||
let database = Arc::new(Database {
|
||||
let fake_db = Arc::new(FakeDatabase {
|
||||
instances: Mutex::new(HashMap::from([(
|
||||
local_instance.ap_id.inner().clone(),
|
||||
local_instance,
|
||||
|
@ -37,9 +38,14 @@ pub async fn federation_config(hostname: &str) -> Result<FederationConfig<Databa
|
|||
articles: Mutex::new(HashMap::new()),
|
||||
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()
|
||||
.domain(hostname)
|
||||
.app_data(database)
|
||||
.app_data(data)
|
||||
.debug(true)
|
||||
.build()
|
||||
.await?;
|
||||
|
|
|
@ -1,44 +1,26 @@
|
|||
use crate::error::MyResult;
|
||||
use crate::federation::objects::edit::{DbEdit, EditVersion};
|
||||
use crate::database::article::DbArticleForm;
|
||||
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::instance::DbInstance;
|
||||
use crate::{database::DatabaseHandle, error::Error};
|
||||
use activitypub_federation::config::Data;
|
||||
use activitypub_federation::fetch::collection_id::CollectionId;
|
||||
use activitypub_federation::kinds::object::ArticleType;
|
||||
use activitypub_federation::kinds::public;
|
||||
use activitypub_federation::protocol::verification::verify_domains_match;
|
||||
use activitypub_federation::{
|
||||
config::Data,
|
||||
fetch::object_id::ObjectId,
|
||||
kinds::public,
|
||||
protocol::{helpers::deserialize_one_or_many, verification::verify_domains_match},
|
||||
traits::Object,
|
||||
fetch::object_id::ObjectId, protocol::helpers::deserialize_one_or_many, traits::Object,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApubArticle {
|
||||
#[serde(rename = "type")]
|
||||
kind: ArticleType,
|
||||
id: ObjectId<DbArticle>,
|
||||
pub(crate) kind: ArticleType,
|
||||
pub(crate) id: ObjectId<DbArticle>,
|
||||
pub(crate) attributed_to: ObjectId<DbInstance>,
|
||||
#[serde(deserialize_with = "deserialize_one_or_many")]
|
||||
pub(crate) to: Vec<Url>,
|
||||
|
@ -50,7 +32,7 @@ pub struct ApubArticle {
|
|||
|
||||
#[async_trait::async_trait]
|
||||
impl Object for DbArticle {
|
||||
type DataType = DatabaseHandle;
|
||||
type DataType = MyDataHandle;
|
||||
type Kind = ApubArticle;
|
||||
type Error = Error;
|
||||
|
||||
|
@ -58,21 +40,18 @@ impl Object for DbArticle {
|
|||
object_id: Url,
|
||||
data: &Data<Self::DataType>,
|
||||
) -> Result<Option<Self>, Self::Error> {
|
||||
let posts = data.articles.lock().unwrap();
|
||||
let res = posts
|
||||
.clone()
|
||||
.into_iter()
|
||||
.find(|u| u.1.ap_id.inner() == &object_id)
|
||||
.map(|u| u.1);
|
||||
Ok(res)
|
||||
let article = DbArticle::read_from_ap_id(&object_id.into(), &mut data.db_connection).ok();
|
||||
Ok(article)
|
||||
}
|
||||
|
||||
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 {
|
||||
kind: Default::default(),
|
||||
id: self.ap_id.clone(),
|
||||
attributed_to: self.instance.clone(),
|
||||
id: self.ap_id.clone().into(),
|
||||
attributed_to: self.instance_id.clone().into(),
|
||||
to: vec![public(), instance.followers_url()?],
|
||||
edits: self.edits_id()?,
|
||||
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> {
|
||||
let mut article = DbArticle {
|
||||
let form = DbArticleForm {
|
||||
title: json.name,
|
||||
text: json.content,
|
||||
ap_id: json.id,
|
||||
instance: json.attributed_to,
|
||||
// TODO: shouldnt overwrite existing edits
|
||||
edits: vec![],
|
||||
latest_version: json.latest_version,
|
||||
ap_id: json.id.into(),
|
||||
latest_version: json.latest_version.0,
|
||||
local: false,
|
||||
instance_id: json.attributed_to.into(),
|
||||
};
|
||||
let mut article = DbArticle::create(&form, &data.db_connection)?;
|
||||
|
||||
{
|
||||
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?;
|
||||
|
||||
// include edits in return value (they are already written to db, no need to do that here)
|
||||
article.edits = edits.0;
|
||||
json.edits.dereference(&article, data).await?;
|
||||
|
||||
Ok(article)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::database::DatabaseHandle;
|
||||
use crate::database::{article::DbArticle, MyDataHandle};
|
||||
use crate::error::Error;
|
||||
use crate::federation::objects::article::{ApubArticle, DbArticle};
|
||||
use crate::federation::objects::article::ApubArticle;
|
||||
use crate::federation::objects::instance::DbInstance;
|
||||
|
||||
use activitypub_federation::kinds::collection::CollectionType;
|
||||
|
@ -28,7 +28,7 @@ pub struct DbArticleCollection(Vec<DbArticle>);
|
|||
#[async_trait::async_trait]
|
||||
impl Collection for DbArticleCollection {
|
||||
type Owner = DbInstance;
|
||||
type DataType = DatabaseHandle;
|
||||
type DataType = MyDataHandle;
|
||||
type Kind = ArticleCollection;
|
||||
type Error = Error;
|
||||
|
||||
|
|
|
@ -1,54 +1,13 @@
|
|||
use crate::database::DatabaseHandle;
|
||||
use crate::error::{Error, MyResult};
|
||||
|
||||
use crate::federation::objects::article::DbArticle;
|
||||
use crate::database::article::DbArticle;
|
||||
use crate::database::edit::{DbEdit, DbEditForm, EditVersion};
|
||||
use crate::database::MyDataHandle;
|
||||
use crate::error::Error;
|
||||
use activitypub_federation::config::Data;
|
||||
use activitypub_federation::fetch::object_id::ObjectId;
|
||||
use activitypub_federation::traits::Object;
|
||||
use diffy::create_patch;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Digest;
|
||||
use sha2::Sha224;
|
||||
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)]
|
||||
pub enum EditType {
|
||||
Edit,
|
||||
|
@ -68,7 +27,7 @@ pub struct ApubEdit {
|
|||
|
||||
#[async_trait::async_trait]
|
||||
impl Object for DbEdit {
|
||||
type DataType = DatabaseHandle;
|
||||
type DataType = MyDataHandle;
|
||||
type Kind = ApubEdit;
|
||||
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> {
|
||||
let article_version = {
|
||||
let mut lock = data.articles.lock().unwrap();
|
||||
let article = lock.get_mut(self.article_id.inner()).unwrap();
|
||||
article.latest_version.clone()
|
||||
};
|
||||
let article = DbArticle::read(self.article_id, &mut data.db_connection)?;
|
||||
Ok(ApubEdit {
|
||||
kind: EditType::Edit,
|
||||
id: self.id,
|
||||
id: self.ap_id.into(),
|
||||
content: self.diff,
|
||||
version: self.version,
|
||||
previous_version: article_version,
|
||||
object: self.article_id,
|
||||
// TODO: this is wrong
|
||||
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> {
|
||||
let edit = Self {
|
||||
id: json.id,
|
||||
let article = json.object.dereference(data).await?;
|
||||
let form = DbEditForm {
|
||||
ap_id: json.id.into(),
|
||||
diff: json.content,
|
||||
article_id: json.object,
|
||||
article_id: article.id,
|
||||
version: json.version,
|
||||
local: false,
|
||||
};
|
||||
let mut lock = data.articles.lock().unwrap();
|
||||
let article = lock.get_mut(edit.article_id.inner()).unwrap();
|
||||
article.edits.push(edit.clone());
|
||||
let edit = DbEdit::create(&form, &mut data.db_connection)?;
|
||||
Ok(edit)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use crate::database::DatabaseHandle;
|
||||
use crate::database::article::DbArticle;
|
||||
use crate::database::MyDataHandle;
|
||||
use crate::error::Error;
|
||||
use crate::federation::objects::article::DbArticle;
|
||||
use crate::federation::objects::edit::{ApubEdit, DbEdit};
|
||||
use crate::federation::objects::edit::ApubEdit;
|
||||
|
||||
use crate::database::edit::DbEdit;
|
||||
use activitypub_federation::kinds::collection::OrderedCollectionType;
|
||||
use activitypub_federation::{
|
||||
config::Data,
|
||||
|
@ -28,7 +29,7 @@ pub struct DbEditCollection(pub Vec<DbEdit>);
|
|||
#[async_trait::async_trait]
|
||||
impl Collection for DbEditCollection {
|
||||
type Owner = DbArticle;
|
||||
type DataType = DatabaseHandle;
|
||||
type DataType = MyDataHandle;
|
||||
type Kind = ApubEditCollection;
|
||||
type Error = Error;
|
||||
|
||||
|
@ -36,10 +37,7 @@ impl Collection for DbEditCollection {
|
|||
owner: &Self::Owner,
|
||||
data: &Data<Self::DataType>,
|
||||
) -> Result<Self::Kind, Self::Error> {
|
||||
let edits = {
|
||||
let lock = data.articles.lock().unwrap();
|
||||
DbEditCollection(lock.get(owner.ap_id.inner()).unwrap().edits.clone())
|
||||
};
|
||||
let edits = DbEditCollection(DbEdit::for_article(owner.id, &mut data.db_connection)?);
|
||||
|
||||
let edits = future::try_join_all(
|
||||
edits
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::error::{Error, MyResult};
|
||||
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::fetch::collection_id::CollectionId;
|
||||
use activitypub_federation::kinds::actor::ServiceType;
|
||||
|
@ -55,11 +55,7 @@ impl DbInstance {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub async fn follow(
|
||||
&self,
|
||||
other: &DbInstance,
|
||||
data: &Data<DatabaseHandle>,
|
||||
) -> Result<(), Error> {
|
||||
pub async fn follow(&self, other: &DbInstance, data: &Data<MyDataHandle>) -> Result<(), Error> {
|
||||
let follow = Follow::new(self.ap_id.clone(), other.ap_id.clone())?;
|
||||
self.send(follow, vec![other.shared_inbox_or_inbox()], data)
|
||||
.await?;
|
||||
|
@ -70,7 +66,7 @@ impl DbInstance {
|
|||
&self,
|
||||
activity: Activity,
|
||||
extra_recipients: Vec<DbInstance>,
|
||||
data: &Data<DatabaseHandle>,
|
||||
data: &Data<MyDataHandle>,
|
||||
) -> Result<(), <Activity as ActivityHandler>::Error>
|
||||
where
|
||||
Activity: ActivityHandler + Serialize + Debug + Send + Sync,
|
||||
|
@ -91,7 +87,7 @@ impl DbInstance {
|
|||
&self,
|
||||
activity: Activity,
|
||||
recipients: Vec<Url>,
|
||||
data: &Data<DatabaseHandle>,
|
||||
data: &Data<MyDataHandle>,
|
||||
) -> Result<(), <Activity as ActivityHandler>::Error>
|
||||
where
|
||||
Activity: ActivityHandler + Serialize + Debug + Send + Sync,
|
||||
|
@ -111,7 +107,7 @@ impl DbInstance {
|
|||
|
||||
#[async_trait::async_trait]
|
||||
impl Object for DbInstance {
|
||||
type DataType = DatabaseHandle;
|
||||
type DataType = MyDataHandle;
|
||||
type Kind = ApubInstance;
|
||||
type Error = Error;
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::database::DatabaseHandle;
|
||||
use crate::database::MyDataHandle;
|
||||
use crate::error::MyResult;
|
||||
use crate::federation::activities::accept::Accept;
|
||||
use crate::federation::activities::follow::Follow;
|
||||
|
@ -37,7 +37,7 @@ pub fn federation_routes() -> Router {
|
|||
|
||||
#[debug_handler]
|
||||
async fn http_get_instance(
|
||||
data: Data<DatabaseHandle>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<FederationJson<WithContext<ApubInstance>>> {
|
||||
let db_instance = data.local_instance();
|
||||
let json_instance = db_instance.into_json(&data).await?;
|
||||
|
@ -46,7 +46,7 @@ async fn http_get_instance(
|
|||
|
||||
#[debug_handler]
|
||||
async fn http_get_all_articles(
|
||||
data: Data<DatabaseHandle>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<FederationJson<WithContext<ArticleCollection>>> {
|
||||
let collection = DbArticleCollection::read_local(&data.local_instance(), &data).await?;
|
||||
Ok(FederationJson(WithContext::new_default(collection)))
|
||||
|
@ -55,7 +55,7 @@ async fn http_get_all_articles(
|
|||
#[debug_handler]
|
||||
async fn http_get_article(
|
||||
Path(title): Path<String>,
|
||||
data: Data<DatabaseHandle>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<FederationJson<WithContext<ApubArticle>>> {
|
||||
let article = {
|
||||
let lock = data.articles.lock().unwrap();
|
||||
|
@ -68,7 +68,7 @@ async fn http_get_article(
|
|||
#[debug_handler]
|
||||
async fn http_get_article_edits(
|
||||
Path(title): Path<String>,
|
||||
data: Data<DatabaseHandle>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<FederationJson<WithContext<ApubEditCollection>>> {
|
||||
let article = {
|
||||
let lock = data.articles.lock().unwrap();
|
||||
|
@ -93,12 +93,9 @@ pub enum InboxActivities {
|
|||
|
||||
#[debug_handler]
|
||||
pub async fn http_post_inbox(
|
||||
data: Data<DatabaseHandle>,
|
||||
data: Data<MyDataHandle>,
|
||||
activity_data: ActivityData,
|
||||
) -> impl IntoResponse {
|
||||
receive_activity::<WithContext<InboxActivities>, DbInstance, DatabaseHandle>(
|
||||
activity_data,
|
||||
&data,
|
||||
)
|
||||
.await
|
||||
receive_activity::<WithContext<InboxActivities>, DbInstance, MyDataHandle>(activity_data, &data)
|
||||
.await
|
||||
}
|
||||
|
|
16
src/lib.rs
16
src/lib.rs
|
@ -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::error::MyResult;
|
||||
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 std::net::ToSocketAddrs;
|
||||
use tracing::info;
|
||||
|
@ -36,3 +36,9 @@ pub async fn start(hostname: &str) -> MyResult<()> {
|
|||
|
||||
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)?)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::database::edit::DbEdit;
|
||||
use crate::database::edit::EditVersion;
|
||||
use crate::error::MyResult;
|
||||
use crate::federation::objects::edit::{DbEdit, EditVersion};
|
||||
use anyhow::anyhow;
|
||||
use diffy::{apply, Patch};
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
|
|
|
@ -2,8 +2,8 @@ use activitypub_federation::fetch::object_id::ObjectId;
|
|||
use fediwiki::api::{
|
||||
ApiConflict, CreateArticleData, EditArticleData, FollowInstance, GetArticleData, ResolveObject,
|
||||
};
|
||||
use fediwiki::database::DbArticle;
|
||||
use fediwiki::error::MyResult;
|
||||
use fediwiki::federation::objects::article::DbArticle;
|
||||
use fediwiki::federation::objects::instance::DbInstance;
|
||||
use fediwiki::start;
|
||||
use once_cell::sync::Lazy;
|
||||
|
|
|
@ -10,8 +10,8 @@ use common::get;
|
|||
use fediwiki::api::{
|
||||
ApiConflict, EditArticleData, ForkArticleData, ResolveObject, SearchArticleData,
|
||||
};
|
||||
use fediwiki::database::DbArticle;
|
||||
use fediwiki::error::MyResult;
|
||||
use fediwiki::federation::objects::article::DbArticle;
|
||||
use fediwiki::federation::objects::edit::ApubEdit;
|
||||
use fediwiki::federation::objects::instance::DbInstance;
|
||||
use serial_test::serial;
|
||||
|
@ -438,7 +438,7 @@ async fn test_fork_article() -> MyResult<()> {
|
|||
assert!(fork_res.local);
|
||||
|
||||
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)
|
||||
let search_form = SearchArticleData {
|
||||
|
|
Loading…
Reference in a new issue