diff --git a/profiles/src/apub/actions/apub/mod.rs b/profiles/src/apub/actions/apub/mod.rs index be40368..b58064c 100644 --- a/profiles/src/apub/actions/apub/mod.rs +++ b/profiles/src/apub/actions/apub/mod.rs @@ -86,7 +86,7 @@ where let res = (f)(activity, context); if res.as_ref().map(|r| r.is_ok()).unwrap_or(false) { - context.apub.store_object(any_base)?; + context.apub.store_object(&any_base)?; } res } diff --git a/profiles/src/apub/mod.rs b/profiles/src/apub/mod.rs index 7b69628..77322f2 100644 --- a/profiles/src/apub/mod.rs +++ b/profiles/src/apub/mod.rs @@ -6,9 +6,10 @@ use uuid::Uuid; mod actions; mod keys; +pub mod results; pub(crate) use actions::ingest; -use keys::ExtendedPerson; +use keys::{ExtendedPerson, PublicKey, PublicKeyInner}; pub trait ApubIds { fn gen_id(&self) -> Url; @@ -40,12 +41,11 @@ pub enum StoreError { pub struct Store { apub_id: Tree, id_apub: Tree, - deleted: Tree, objects: Tree, - seen: Tree, keys: Tree, profile_key: Tree, endpoints: Tree, + profile_endpoint: Tree, info: Arc, } @@ -159,24 +159,39 @@ impl Store { Ok(Store { id_apub: db.open_tree("/profiles/apub/id-apub")?, apub_id: db.open_tree("/profiles/apub/apub-id")?, - deleted: db.open_tree("/profiles/apub/deleted")?, objects: db.open_tree("/profiles/apub/objects")?, - seen: db.open_tree("/profiles/apub/seen")?, keys: db.open_tree("/profiles/apub/keys")?, profile_key: db.open_tree("/profiles/apub/profile/key")?, endpoints: db.open_tree("/profiles/apub/endpoints")?, + profile_endpoint: db.open_tree("/profiles/apub/profile/endpoint")?, info: Arc::new(ids), }) } - pub(crate) fn key_for_profile(&self, profile_id: Uuid) -> Result, StoreError> { + fn endpoints_for_profile(&self, profile_id: Uuid) -> Result, StoreError> { + let opt = self + .profile_endpoint + .get(profile_endpoint_id(profile_id))? + .and_then(url_from_ivec); + + if let Some(profile_id) = opt { + if let Some(ivec) = self.endpoints.get(profile_id.as_str())? { + let endpoints: Endpoints = serde_json::from_slice(&ivec)?; + return Ok(Some(endpoints)); + } + } + + Ok(None) + } + + fn key_for_profile(&self, profile_id: Uuid) -> Result, StoreError> { self.profile_key .get(profile_key_id(profile_id)) .map(|opt| opt.and_then(url_from_ivec)) .map_err(StoreError::from) } - pub(crate) fn public_key_for_id(&self, key_id: &Url) -> Result, StoreError> { + fn public_key_for_id(&self, key_id: &Url) -> Result, StoreError> { if let Some(ivec) = self.keys.get(key_id.as_str())? { let keys: Keys = serde_json::from_slice(&ivec)?; return Ok(Some(keys.public)); @@ -184,7 +199,29 @@ impl Store { Ok(None) } - pub(crate) fn store_public_key( + fn store_endpoints( + &self, + profile_id: Uuid, + apub_id: &Url, + endpoints: Endpoints, + ) -> Result<(), StoreError> { + let endpoints_vec = serde_json::to_vec(&endpoints)?; + self.endpoints.insert(apub_id.as_str(), endpoints_vec)?; + self.profile_endpoint + .insert(profile_endpoint_id(profile_id), apub_id.as_str())?; + Ok(()) + } + + fn gen_keys(&self, profile_id: Uuid, key_id: &Url) -> Result { + let keys = Keys::generate()?; + let keys_vec = serde_json::to_vec(&keys)?; + self.keys.insert(key_id.as_str(), keys_vec)?; + self.profile_key + .insert(profile_key_id(profile_id), key_id.as_str())?; + Ok(keys.public) + } + + fn store_public_key( &self, profile_id: Uuid, key_id: &Url, @@ -198,7 +235,7 @@ impl Store { Ok(()) } - pub(crate) fn object(&self, id: &Url) -> Result, StoreError> { + fn object(&self, id: &Url) -> Result, StoreError> { if let Some(ivec) = self.objects.get(id.as_str())? { let any_base: AnyBase = serde_json::from_slice(&ivec)?; return Ok(Some(any_base)); @@ -207,63 +244,67 @@ impl Store { Ok(None) } - pub(crate) fn store_object(&self, any_base: AnyBase) -> Result<(), StoreError> { + fn store_object(&self, any_base: &AnyBase) -> Result<(), StoreError> { if let Some(id) = any_base.id() { - let obj_vec = serde_json::to_vec(&any_base)?; + let obj_vec = serde_json::to_vec(any_base)?; self.objects.insert(id.as_str(), obj_vec)?; } Ok(()) } - pub(crate) fn is_seen(&self, url: &Url) -> Result { - Ok(self.seen.get(url.as_str())?.is_some()) + fn apub_for_submission(&self, id: Uuid) -> Result, StoreError> { + self.apub_for_id(StoredRecords::Submission(id)) } - pub(crate) fn seen(&self, url: &Url) -> Result<(), StoreError> { - self.seen.insert(url.as_str(), url.as_str())?; - - Ok(()) - } - - pub(crate) fn delete_id(&self, id: StoredRecords) -> Result<(), StoreError> { - if let Some(url_ivec) = self.id_apub.remove(id.to_string())? { - self.apub_id.remove(url_ivec)?; - } - Ok(()) - } - - pub(crate) fn delete_apub(&self, apub_id: &Url) -> Result<(), StoreError> { - if let Some(record_ivec) = self.apub_id.remove(apub_id.as_str())? { - self.id_apub.remove(record_ivec)?; - } - Ok(()) - } - - pub(crate) fn submission(&self, apub_id: &Url, id: Uuid) -> Result<(), StoreError> { + fn submission(&self, apub_id: &Url, id: Uuid) -> Result<(), StoreError> { self.establish(apub_id, StoredRecords::Submission(id)) } - pub(crate) fn profile(&self, apub_id: &Url, id: Uuid) -> Result<(), StoreError> { + fn apub_for_profile(&self, id: Uuid) -> Result, StoreError> { + self.apub_for_id(StoredRecords::Profile(id)) + } + + fn profile(&self, apub_id: &Url, id: Uuid) -> Result<(), StoreError> { self.establish(apub_id, StoredRecords::Profile(id)) } - pub(crate) fn comment(&self, apub_id: &Url, id: Uuid) -> Result<(), StoreError> { + fn apub_for_comment(&self, id: Uuid) -> Result, StoreError> { + self.apub_for_id(StoredRecords::Comment(id)) + } + + fn comment(&self, apub_id: &Url, id: Uuid) -> Result<(), StoreError> { self.establish(apub_id, StoredRecords::Comment(id)) } - pub(crate) fn react(&self, apub_id: &Url, id: Uuid) -> Result<(), StoreError> { + fn apub_for_react(&self, id: Uuid) -> Result, StoreError> { + self.apub_for_id(StoredRecords::React(id)) + } + + fn react(&self, apub_id: &Url, id: Uuid) -> Result<(), StoreError> { self.establish(apub_id, StoredRecords::React(id)) } - pub(crate) fn follow(&self, apub_id: &Url, id: Uuid) -> Result<(), StoreError> { + fn apub_for_follow(&self, id: Uuid) -> Result, StoreError> { + self.apub_for_id(StoredRecords::Follow(id)) + } + + fn follow(&self, apub_id: &Url, id: Uuid) -> Result<(), StoreError> { self.establish(apub_id, StoredRecords::Follow(id)) } - pub(crate) fn follow_request(&self, apub_id: &Url, id: Uuid) -> Result<(), StoreError> { + fn apub_for_follow_request(&self, id: Uuid) -> Result, StoreError> { + self.apub_for_id(StoredRecords::FollowRequest(id)) + } + + fn follow_request(&self, apub_id: &Url, id: Uuid) -> Result<(), StoreError> { self.establish(apub_id, StoredRecords::FollowRequest(id)) } - pub(crate) fn block(&self, apub_id: &Url, id: Uuid) -> Result<(), StoreError> { + fn apub_for_block(&self, id: Uuid) -> Result, StoreError> { + self.apub_for_id(StoredRecords::Block(id)) + } + + fn block(&self, apub_id: &Url, id: Uuid) -> Result<(), StoreError> { self.establish(apub_id, StoredRecords::Block(id)) } @@ -275,14 +316,14 @@ impl Store { Ok(()) } - pub(crate) fn id_for_apub(&self, apub_id: &Url) -> Result, StoreError> { + fn id_for_apub(&self, apub_id: &Url) -> Result, StoreError> { Ok(self .apub_id .get(apub_id.as_str())? .and_then(record_from_ivec)) } - pub(crate) fn apub_for_id(&self, id: StoredRecords) -> Result, StoreError> { + fn apub_for_id(&self, id: StoredRecords) -> Result, StoreError> { Ok(self.id_apub.get(id.to_string())?.and_then(url_from_ivec)) } } @@ -319,36 +360,25 @@ impl Keys { } } +fn profile_endpoint_id(profile_id: Uuid) -> String { + format!("/profile/{}/endpoint-id", profile_id) +} + fn profile_key_id(profile_id: Uuid) -> String { format!("/profile/{}/key-id", profile_id) } -fn record_endpoints_key(record: StoredRecords) -> String { - format!("/object/{}/endpoints", record) -} - -fn record_keys_key(record: StoredRecords) -> String { - format!("/object/{}/keys", record) -} - -fn object_record_key(object_id: &Url) -> String { - format!("/object/{}/record", object_id) -} - -fn owner_record_key(owner_id: &Url) -> String { - format!("/owner/{}/record", owner_id) -} - -fn record_object_key(record: StoredRecords) -> String { - format!("/record/{}/object", record) -} - impl fmt::Debug for Store { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("Store") .field("relationship", &"Tree") + .field("apub_id", &"Tree") + .field("id_apub", &"Tree") + .field("objects", &"Tree") .field("keys", &"Tree") + .field("profile_key", &"Tree") .field("endpoints", &"Tree") + .field("profile_endpoint", &"Tree") .field("info", &"Rc") .finish() } diff --git a/profiles/src/apub/results/block.rs b/profiles/src/apub/results/block.rs new file mode 100644 index 0000000..bed39a4 --- /dev/null +++ b/profiles/src/apub/results/block.rs @@ -0,0 +1,46 @@ +use super::{Blocked, Unblocked}; +use crate::{Completed, Context, Error, Required}; +use activitystreams::{ + activity::{Block, Undo}, + base::AnyBase, + context, + prelude::*, + security, +}; + +impl Completed for Blocked { + fn to_apub(&self, ctx: &Context) -> Result { + let block = ctx.store.view.blocks.by_id(self.block_id)?.req()?; + let actor_id = ctx.apub.apub_for_profile(block.right)?.req()?; + let object_id = ctx.apub.apub_for_profile(block.left)?.req()?; + let published = block.published; + + let mut block = Block::new(actor_id, object_id); + block + .set_id(ctx.apub.info.gen_id()) + .set_published(published.into()) + .add_context(context()) + .add_context(security()); + let block = block.into_any_base()?; + ctx.apub.store_object(&block)?; + + Ok(block) + } +} + +impl Completed for Unblocked { + fn to_apub(&self, ctx: &Context) -> Result { + let person_id = ctx.apub.apub_for_profile(self.profile_id)?.req()?; + let block_id = ctx.apub.apub_for_block(self.block_id)?.req()?; + let block = ctx.apub.object(&block_id)?.req()?; + + let mut undo = Undo::new(person_id, block); + undo.set_id(ctx.apub.info.gen_id()) + .add_context(context()) + .add_context(security()); + let undo = undo.into_any_base()?; + ctx.apub.store_object(&undo)?; + + Ok(undo) + } +} diff --git a/profiles/src/apub/results/comment.rs b/profiles/src/apub/results/comment.rs new file mode 100644 index 0000000..ebf083b --- /dev/null +++ b/profiles/src/apub/results/comment.rs @@ -0,0 +1,97 @@ +use super::{CommentCreated, CommentDeleted, CommentUpdated}; +use crate::{store::Comment, Completed, Context, Error, Required}; +use activitystreams::{ + activity::{Create, Delete, Update}, + base::AnyBase, + context, + object::Note, + prelude::*, + public, security, +}; +use url::Url; + +fn build_comment( + comment: Comment, + note_id: Url, + person_id: Url, + ctx: &Context, +) -> Result { + let published = comment.published(); + let endpoints = ctx + .apub + .endpoints_for_profile(comment.profile_id())? + .req()?; + + let mut note = Note::new(); + note.set_id(note_id) + .set_content(comment.body()) + .set_published(published.into()) + .set_attributed_to(person_id) + .add_to(endpoints.followers) + .add_cc(public()); + if let Some(comment_id) = comment.comment_id() { + let in_reply_to = ctx.apub.apub_for_comment(comment_id)?.req()?; + note.set_in_reply_to(in_reply_to); + } + + let any_base = note.into_any_base()?; + ctx.apub.store_object(&any_base)?; + + Ok(any_base) +} + +impl Completed for CommentCreated { + fn to_apub(&self, ctx: &Context) -> Result { + let comment = ctx.store.comments.by_id(self.comment_id)?.req()?; + let person_id = ctx.apub.apub_for_profile(comment.profile_id())?.req()?; + let note_id = ctx.apub.info.gen_id(); + let note = build_comment(comment, note_id, person_id.clone(), ctx)?; + + let mut create = Create::new(person_id, note); + create + .set_id(ctx.apub.info.gen_id()) + .add_context(context()) + .add_context(security()); + let any_base = create.into_any_base()?; + ctx.apub.store_object(&any_base)?; + + Ok(any_base) + } +} + +impl Completed for CommentUpdated { + fn to_apub(&self, ctx: &Context) -> Result { + let comment = ctx.store.comments.by_id(self.comment_id)?.req()?; + let person_id = ctx.apub.apub_for_profile(comment.profile_id())?.req()?; + let note_id = ctx.apub.apub_for_comment(comment.id())?.req()?; + let note = build_comment(comment, note_id, person_id.clone(), ctx)?; + + let mut update = Update::new(person_id, note); + update + .set_id(ctx.apub.info.gen_id()) + .add_context(context()) + .add_context(security()); + let any_base = update.into_any_base()?; + ctx.apub.store_object(&any_base)?; + + Ok(any_base) + } +} + +impl Completed for CommentDeleted { + fn to_apub(&self, ctx: &Context) -> Result { + let person_id = ctx.apub.apub_for_profile(self.profile_id)?.req()?; + let note_id = ctx.apub.apub_for_comment(self.comment_id)?.req()?; + let note = ctx.apub.object(¬e_id)?.req()?; + + let mut delete = Delete::new(person_id, note); + delete + .set_id(ctx.apub.info.gen_id()) + .add_context(context()) + .add_context(security()); + let any_base = delete.into_any_base()?; + ctx.apub.store_object(&any_base)?; + + Ok(any_base) + } +} diff --git a/profiles/src/apub/results/follow_request.rs b/profiles/src/apub/results/follow_request.rs new file mode 100644 index 0000000..66f41e2 --- /dev/null +++ b/profiles/src/apub/results/follow_request.rs @@ -0,0 +1,136 @@ +use super::{ + FollowRequestAccepted, FollowRequestDeleted, FollowRequestRejected, FollowRequested, + UndoneFollowRequestAccepted, Unfollowed, +}; +use crate::{Completed, Context, Error, Required}; +use activitystreams::{ + activity::{Accept, Follow, Reject, Undo}, + base::AnyBase, + context, + prelude::*, + security, +}; + +impl Completed for FollowRequested { + fn to_apub(&self, ctx: &Context) -> Result { + let follow = ctx + .store + .view + .follow_requests + .by_id(self.follow_request_id)? + .req()?; + let actor_id = ctx.apub.apub_for_profile(follow.right)?.req()?; + let object_id = ctx.apub.apub_for_profile(follow.left)?.req()?; + let published = follow.published; + + let mut follow = Follow::new(actor_id, object_id); + follow + .set_id(ctx.apub.info.gen_id()) + .set_published(published.into()) + .add_context(context()) + .add_context(security()); + let follow = follow.into_any_base()?; + ctx.apub.store_object(&follow)?; + + Ok(follow) + } +} + +impl Completed for FollowRequestDeleted { + fn to_apub(&self, ctx: &Context) -> Result { + let person_id = ctx.apub.apub_for_profile(self.profile_id)?.req()?; + let follow_request_id = ctx + .apub + .apub_for_follow_request(self.follow_request_id)? + .req()?; + let follow_request = ctx.apub.object(&follow_request_id)?.req()?; + + let mut undo = Undo::new(person_id, follow_request); + undo.set_id(ctx.apub.info.gen_id()) + .add_context(context()) + .add_context(security()); + let undo = undo.into_any_base()?; + ctx.apub.store_object(&undo)?; + + Ok(undo) + } +} + +impl Completed for FollowRequestRejected { + fn to_apub(&self, ctx: &Context) -> Result { + let person_id = ctx.apub.apub_for_profile(self.profile_id)?.req()?; + let follow_request_id = ctx + .apub + .apub_for_follow_request(self.follow_request_id)? + .req()?; + let follow_request = ctx.apub.object(&follow_request_id)?.req()?; + + let mut reject = Reject::new(person_id, follow_request); + reject + .set_id(ctx.apub.info.gen_id()) + .add_context(context()) + .add_context(security()); + let reject = reject.into_any_base()?; + ctx.apub.store_object(&reject)?; + + Ok(reject) + } +} + +impl Completed for FollowRequestAccepted { + fn to_apub(&self, ctx: &Context) -> Result { + let person_id = ctx.apub.apub_for_profile(self.profile_id)?.req()?; + let follow_request_id = ctx + .apub + .apub_for_follow_request(self.follow_request_id)? + .req()?; + let follow_request = ctx.apub.object(&follow_request_id)?.req()?; + + let accept_id = ctx.apub.info.gen_id(); + ctx.apub.follow(&accept_id, self.follow_id)?; + + let mut accept = Accept::new(person_id, follow_request); + accept + .set_id(accept_id) + .add_context(context()) + .add_context(security()); + let accept = accept.into_any_base()?; + ctx.apub.store_object(&accept)?; + + Ok(accept) + } +} + +impl Completed for UndoneFollowRequestAccepted { + fn to_apub(&self, ctx: &Context) -> Result { + let person_id = ctx.apub.apub_for_profile(self.profile_id)?.req()?; + let follow_id = ctx.apub.apub_for_follow(self.follow_id)?.req()?; + let follow = ctx.apub.object(&follow_id)?.req()?; + + let mut undo = Undo::new(person_id, follow); + undo.set_id(ctx.apub.info.gen_id()) + .add_context(context()) + .add_context(security()); + let undo = undo.into_any_base()?; + ctx.apub.store_object(&undo)?; + + Ok(undo) + } +} + +impl Completed for Unfollowed { + fn to_apub(&self, ctx: &Context) -> Result { + let person_id = ctx.apub.apub_for_profile(self.profile_id)?.req()?; + let follow_id = ctx.apub.apub_for_follow(self.follow_id)?.req()?; + let follow = ctx.apub.object(&follow_id)?.req()?; + + let mut undo = Undo::new(person_id, follow); + undo.set_id(ctx.apub.info.gen_id()) + .add_context(context()) + .add_context(security()); + let undo = undo.into_any_base()?; + ctx.apub.store_object(&undo)?; + + Ok(undo) + } +} diff --git a/profiles/src/apub/results/mod.rs b/profiles/src/apub/results/mod.rs new file mode 100644 index 0000000..7bfce92 --- /dev/null +++ b/profiles/src/apub/results/mod.rs @@ -0,0 +1,94 @@ +use uuid::Uuid; + +mod block; +mod comment; +mod follow_request; +mod profile; +mod react; +mod submission; + +pub(crate) struct ProfileCreated { + profile_id: Uuid, +} + +pub(crate) struct ProfileUpdated { + profile_id: Uuid, +} + +pub(crate) struct ProfileDeleted { + profile_id: Uuid, +} + +pub(crate) struct SubmissionCreated { + submission_id: Uuid, +} + +pub(crate) struct SubmissionUpdated { + submission_id: Uuid, +} + +pub(crate) struct SubmissionDeleted { + profile_id: Uuid, + submission_id: Uuid, +} + +pub(crate) struct CommentCreated { + comment_id: Uuid, +} + +pub(crate) struct CommentUpdated { + comment_id: Uuid, +} + +pub(crate) struct CommentDeleted { + profile_id: Uuid, + comment_id: Uuid, +} + +pub(crate) struct ReactCreated { + react_id: Uuid, +} + +pub(crate) struct ReactDeleted { + profile_id: Uuid, + react_id: Uuid, +} + +pub(crate) struct FollowRequested { + follow_request_id: Uuid, +} + +pub(crate) struct FollowRequestDeleted { + profile_id: Uuid, + follow_request_id: Uuid, +} + +pub(crate) struct FollowRequestRejected { + profile_id: Uuid, + follow_request_id: Uuid, +} + +pub(crate) struct FollowRequestAccepted { + profile_id: Uuid, + follow_id: Uuid, + follow_request_id: Uuid, +} + +pub(crate) struct UndoneFollowRequestAccepted { + profile_id: Uuid, + follow_id: Uuid, +} + +pub(crate) struct Unfollowed { + profile_id: Uuid, + follow_id: Uuid, +} + +pub(crate) struct Blocked { + block_id: Uuid, +} + +pub(crate) struct Unblocked { + profile_id: Uuid, + block_id: Uuid, +} diff --git a/profiles/src/apub/results/profile.rs b/profiles/src/apub/results/profile.rs new file mode 100644 index 0000000..d4437b0 --- /dev/null +++ b/profiles/src/apub/results/profile.rs @@ -0,0 +1,185 @@ +use super::{ProfileCreated, ProfileDeleted, ProfileUpdated}; +use crate::{ + apub::{ExtendedPerson, PublicKey, PublicKeyInner}, + store::FileSource, + Completed, Context, Error, Required, +}; +use activitystreams::{ + activity::{Create, Delete, Update}, + actor::{ApActor, Endpoints, Person}, + base::AnyBase, + context, + prelude::*, + public, security, +}; + +impl Completed for ProfileCreated { + fn to_apub(&self, ctx: &Context) -> Result { + let profile = ctx.store.profiles.by_id(self.profile_id)?.req()?; + + let person_id = ctx.apub.info.gen_id(); + let public_key_id = ctx.apub.info.public_key(&person_id).req()?; + let following = ctx.apub.info.following(&person_id).req()?; + let followers = ctx.apub.info.followers(&person_id).req()?; + let inbox = ctx.apub.info.inbox(&person_id).req()?; + let outbox = ctx.apub.info.outbox(&person_id).req()?; + let shared_inbox = ctx.apub.info.shared_inbox(); + + ctx.apub.profile(&person_id, profile.id())?; + ctx.apub.store_endpoints( + profile.id(), + &person_id, + crate::apub::Endpoints { + inbox: inbox.clone(), + outbox: outbox.clone(), + following: following.clone(), + followers: followers.clone(), + shared_inbox: shared_inbox.clone(), + public_key: public_key_id.clone(), + }, + )?; + let public_key_pem = ctx.apub.gen_keys(profile.id(), &person_id)?; + + let mut person = ExtendedPerson::new( + ApActor::new(inbox, Person::new()), + PublicKey { + public_key: PublicKeyInner { + id: public_key_id, + owner: person_id.clone(), + public_key_pem, + }, + }, + ); + + person + .set_id(person_id.clone()) + .set_following(following) + .set_followers(followers) + .set_outbox(outbox) + .set_endpoints(Endpoints { + shared_inbox: Some(shared_inbox), + ..Endpoints::default() + }) + .set_preferred_username(profile.handle()) + .set_published(profile.published().into()); + + if let Some(display_name) = profile.display_name() { + person.set_name(display_name); + } + if let Some(description) = profile.description() { + person.set_summary(description); + } + if let Some(icon) = profile.icon() { + let file = ctx.store.files.by_id(icon)?.req()?; + let FileSource::PictRs(pictrs_file) = file.source(); + person.set_icon(ctx.pictrs.image_url(pictrs_file.key())); + } + if let Some(banner) = profile.banner() { + let file = ctx.store.files.by_id(banner)?.req()?; + let FileSource::PictRs(pictrs_file) = file.source(); + person.set_image(ctx.pictrs.image_url(pictrs_file.key())); + } + if !profile.login_required() { + person.add_to(public()); + } + + let any_base = person.into_any_base()?; + ctx.apub.store_object(&any_base)?; + + let mut create = Create::new(person_id, any_base); + create + .set_id(ctx.apub.info.gen_id()) + .add_context(context()) + .add_context(security()); + + let any_base = create.into_any_base()?; + ctx.apub.store_object(&any_base)?; + + Ok(any_base) + } +} + +impl Completed for ProfileUpdated { + fn to_apub(&self, ctx: &Context) -> Result { + let profile = ctx.store.profiles.by_id(self.profile_id)?.req()?; + let endpoints = ctx.apub.endpoints_for_profile(self.profile_id)?.req()?; + let public_key_id = ctx.apub.key_for_profile(profile.id())?.req()?; + let public_key_pem = ctx.apub.public_key_for_id(&public_key_id)?.req()?; + let person_id = ctx.apub.apub_for_profile(self.profile_id)?.req()?; + + let mut person = ExtendedPerson::new( + ApActor::new(endpoints.inbox, Person::new()), + PublicKey { + public_key: PublicKeyInner { + id: public_key_id, + owner: person_id.clone(), + public_key_pem, + }, + }, + ); + + person + .set_id(person_id.clone()) + .set_following(endpoints.following) + .set_followers(endpoints.followers) + .set_outbox(endpoints.outbox) + .set_endpoints(Endpoints { + shared_inbox: Some(endpoints.shared_inbox), + ..Endpoints::default() + }) + .set_preferred_username(profile.handle()) + .set_published(profile.published().into()); + + if let Some(display_name) = profile.display_name() { + person.set_name(display_name); + } + if let Some(description) = profile.description() { + person.set_summary(description); + } + if let Some(icon) = profile.icon() { + let file = ctx.store.files.by_id(icon)?.req()?; + let FileSource::PictRs(pictrs_file) = file.source(); + person.set_icon(ctx.pictrs.image_url(pictrs_file.key())); + } + if let Some(banner) = profile.banner() { + let file = ctx.store.files.by_id(banner)?.req()?; + let FileSource::PictRs(pictrs_file) = file.source(); + person.set_image(ctx.pictrs.image_url(pictrs_file.key())); + } + if !profile.login_required() { + person.add_to(public()); + } + + let any_base = person.into_any_base()?; + ctx.apub.store_object(&any_base)?; + + let mut update = Update::new(person_id, any_base); + update + .set_id(ctx.apub.info.gen_id()) + .add_context(context()) + .add_context(security()); + + let any_base = update.into_any_base()?; + ctx.apub.store_object(&any_base)?; + + Ok(any_base) + } +} + +impl Completed for ProfileDeleted { + fn to_apub(&self, ctx: &Context) -> Result { + let person_id = ctx.apub.apub_for_profile(self.profile_id)?.req()?; + let person = ctx.apub.object(&person_id)?.req()?; + + let mut delete = Delete::new(person_id, person); + delete + .set_id(ctx.apub.info.gen_id()) + .add_context(context()) + .add_context(security()); + + let any_base = delete.into_any_base()?; + ctx.apub.store_object(&any_base)?; + + Ok(any_base) + } +} diff --git a/profiles/src/apub/results/react.rs b/profiles/src/apub/results/react.rs new file mode 100644 index 0000000..64e70ba --- /dev/null +++ b/profiles/src/apub/results/react.rs @@ -0,0 +1,65 @@ +use super::{ReactCreated, ReactDeleted}; +use crate::{Completed, Context, Error, Required}; +use activitystreams::{ + activity::{Create, Delete, Like}, + base::AnyBase, + context, + prelude::*, + public, security, +}; + +impl Completed for ReactCreated { + fn to_apub(&self, ctx: &Context) -> Result { + let react = ctx.store.reacts.by_id(self.react_id)?.req()?; + let person_id = ctx.apub.apub_for_profile(react.profile_id())?.req()?; + + let note_id = if let Some(comment_id) = react.comment_id() { + ctx.apub.apub_for_comment(comment_id)?.req()? + } else { + ctx.apub.apub_for_submission(react.submission_id())?.req()? + }; + + let published = react.published(); + let endpoints = ctx.apub.endpoints_for_profile(react.profile_id())?.req()?; + + let mut like = Like::new(person_id.clone(), note_id); + like.set_id(ctx.apub.info.gen_id()) + .set_content(react.react()) + .set_published(published.into()) + .set_attributed_to(person_id.clone()) + .add_to(endpoints.followers) + .add_cc(public()); + + let like = like.into_any_base()?; + ctx.apub.store_object(&like)?; + + let mut create = Create::new(person_id, like); + create + .set_id(ctx.apub.info.gen_id()) + .add_context(context()) + .add_context(security()); + + let create = create.into_any_base()?; + ctx.apub.store_object(&create)?; + + Ok(create) + } +} + +impl Completed for ReactDeleted { + fn to_apub(&self, ctx: &Context) -> Result { + let person_id = ctx.apub.apub_for_profile(self.profile_id)?.req()?; + let like_id = ctx.apub.apub_for_react(self.react_id)?.req()?; + let like = ctx.apub.object(&like_id)?.req()?; + + let mut delete = Delete::new(person_id, like); + delete + .set_id(ctx.apub.info.gen_id()) + .add_context(context()) + .add_context(security()); + let delete = delete.into_any_base()?; + ctx.apub.store_object(&delete)?; + + Ok(delete) + } +} diff --git a/profiles/src/apub/results/submission.rs b/profiles/src/apub/results/submission.rs new file mode 100644 index 0000000..b53617b --- /dev/null +++ b/profiles/src/apub/results/submission.rs @@ -0,0 +1,115 @@ +use super::{SubmissionCreated, SubmissionDeleted, SubmissionUpdated}; +use crate::{ + store::{FileSource, Submission, Visibility}, + Completed, Context, Error, Required, +}; +use activitystreams::{ + activity::{Create, Delete, Update}, + base::AnyBase, + context, + object::Note, + prelude::*, + public, security, +}; +use url::Url; + +fn build_submission( + submission: Submission, + note_id: Url, + person_id: Url, + ctx: &Context, +) -> Result { + let published = submission.published().req()?; + + let endpoints = ctx + .apub + .endpoints_for_profile(submission.profile_id())? + .req()?; + + let mut note = Note::new(); + note.set_id(note_id) + .set_summary(submission.title()) + .set_published(published.into()) + .set_attributed_to(person_id); + if let Some(description) = submission.description() { + note.set_content(description); + } + match submission.visibility() { + Visibility::Public => { + note.add_to(public()).add_cc(endpoints.followers); + } + Visibility::Unlisted => { + note.add_to(endpoints.followers).add_cc(public()); + } + Visibility::Followers => { + note.add_to(endpoints.followers); + } + } + for file in submission.files() { + let file = ctx.store.files.by_id(*file)?.req()?; + + let FileSource::PictRs(pictrs_file) = file.source(); + + note.add_attachment(ctx.pictrs.image_url(pictrs_file.key())); + } + let any_base = note.into_any_base()?; + ctx.apub.store_object(&any_base)?; + + Ok(any_base) +} + +impl Completed for SubmissionCreated { + fn to_apub(&self, ctx: &Context) -> Result { + let submission = ctx.store.submissions.by_id(self.submission_id)?.req()?; + let person_id = ctx.apub.apub_for_profile(submission.profile_id())?.req()?; + let note_id = ctx.apub.info.gen_id(); + let note = build_submission(submission, note_id, person_id.clone(), ctx)?; + + let mut create = Create::new(person_id, note); + create + .set_id(ctx.apub.info.gen_id()) + .add_context(context()) + .add_context(security()); + let any_base = create.into_any_base()?; + ctx.apub.store_object(&any_base)?; + + Ok(any_base) + } +} + +impl Completed for SubmissionUpdated { + fn to_apub(&self, ctx: &Context) -> Result { + let submission = ctx.store.submissions.by_id(self.submission_id)?.req()?; + let person_id = ctx.apub.apub_for_profile(submission.profile_id())?.req()?; + let note_id = ctx.apub.apub_for_submission(submission.id())?.req()?; + let note = build_submission(submission, person_id.clone(), note_id, ctx)?; + + let mut update = Update::new(person_id, note); + update + .set_id(ctx.apub.info.gen_id()) + .add_context(context()) + .add_context(security()); + let any_base = update.into_any_base()?; + ctx.apub.store_object(&any_base)?; + + Ok(any_base) + } +} + +impl Completed for SubmissionDeleted { + fn to_apub(&self, ctx: &Context) -> Result { + let person_id = ctx.apub.apub_for_profile(self.profile_id)?.req()?; + let note_id = ctx.apub.apub_for_submission(self.submission_id)?.req()?; + let note = ctx.apub.object(¬e_id)?.req()?; + + let mut delete = Delete::new(person_id, note); + delete + .set_id(ctx.apub.info.gen_id()) + .add_context(context()) + .add_context(security()); + let any_base = delete.into_any_base()?; + ctx.apub.store_object(&any_base)?; + + Ok(any_base) + } +} diff --git a/profiles/src/lib.rs b/profiles/src/lib.rs index 0eee7d3..88a4f19 100644 --- a/profiles/src/lib.rs +++ b/profiles/src/lib.rs @@ -1,6 +1,6 @@ use activitystreams::base::AnyBase; use actix_rt::Arbiter; -use actix_web::client::Client; +use actix_web::{client::Client, dev::Payload, HttpRequest}; use sled::Db; use std::{fmt, sync::Arc}; use url::Url; @@ -24,9 +24,10 @@ use apub::ApubIds; use pictrs::ImageInfo; #[derive(Clone)] -struct Context { +pub struct Context { store: store::Store, apub: apub::Store, + pictrs: Arc, arbiter: Arbiter, spawner: Arc, } @@ -37,6 +38,7 @@ impl Context { Context { store: state.store.clone(), apub: state.apub.clone(), + pictrs: state.pictrs.info.clone(), arbiter: Arbiter::current(), spawner: state.spawner.clone(), } @@ -51,6 +53,12 @@ pub enum Error { #[error("ActivityPub object is malformed")] Invalid, + #[error("Error deleting file: {0}")] + DeleteFile(#[from] pictrs::DeleteError), + + #[error("Error uploading file: {0}")] + UploadFile(#[from] pictrs::UploadError), + #[error("Failed to serialize or deseiralize data: {0}")] Json(#[from] serde_json::Error), @@ -66,6 +74,7 @@ pub trait Spawner { fn download_images(&self, images: Vec, stack: Vec); fn purge_file(&self, file_id: Uuid); fn process(&self, any_base: AnyBase, stack: Vec); + fn deliver(&self, completed: &dyn Completed); } enum RecoverableError { @@ -77,6 +86,10 @@ trait Action { fn perform(&self, context: &Context) -> Result<(), Error>; } +pub trait Completed { + fn to_apub(&self, context: &Context) -> Result; +} + trait Required { fn req(self) -> Result; } @@ -99,7 +112,7 @@ pub struct State { impl State { pub fn build( pictrs_upstream: Url, - image_info: impl ImageInfo + 'static, + image_info: impl ImageInfo + Send + Sync + 'static, apub_info: impl ApubIds + Send + Sync + 'static, spawner: impl Spawner + Send + Sync + 'static, client: Client, @@ -120,6 +133,55 @@ impl State { apub::ingest(any_base, &context, stack)?; Ok(()) } + + pub async fn download_image(&self, url: &Url) -> Result { + let images = self.pictrs.download_image(url).await?; + + // safe because we already checked emptiness + let image = images.images().next().unwrap(); + + let file_source = store::FileSource::PictRs(store::PictRsFile::new( + image.key(), + image.token(), + image.width(), + image.height(), + image.mime(), + )); + let file = self.store.files.create(&file_source)?; + + Ok(file.id()) + } + + pub async fn upload_image(&self, req: HttpRequest, body: Payload) -> Result, Error> { + let images = self.pictrs.proxy_upload(req, body).await?; + + let mut files = vec![]; + for image in images.images() { + let file_source = store::FileSource::PictRs(store::PictRsFile::new( + image.key(), + image.token(), + image.width(), + image.height(), + image.mime(), + )); + let file = self.store.files.create(&file_source)?; + files.push(file.id()); + } + + Ok(files) + } + + pub async fn delete_file(&self, file_id: Uuid) -> Result<(), Error> { + let file = self.store.files.by_id(file_id)?.req()?; + + let store::FileSource::PictRs(image) = file.source(); + + self.pictrs.delete_image(image.key(), image.token()).await?; + + self.store.files.delete(file_id)?; + + Ok(()) + } } impl fmt::Debug for State { diff --git a/profiles/src/pictrs.rs b/profiles/src/pictrs.rs index 9d26280..a8c5a08 100644 --- a/profiles/src/pictrs.rs +++ b/profiles/src/pictrs.rs @@ -1,5 +1,5 @@ use actix_web::{body::BodyStream, client::Client, dev::Payload, HttpRequest, HttpResponse}; -use std::{fmt, rc::Rc}; +use std::{fmt, sync::Arc}; use url::Url; #[derive(Debug, Clone)] @@ -7,12 +7,6 @@ struct Serde { inner: T, } -impl Serde { - pub(crate) fn new(inner: T) -> Self { - Serde { inner } - } -} - impl serde::Serialize for Serde where T: std::fmt::Display, @@ -66,6 +60,18 @@ impl Image { pub(crate) fn token(&self) -> &str { &self.delete_token } + + pub(crate) fn width(&self) -> usize { + self.details.width + } + + pub(crate) fn height(&self) -> usize { + self.details.height + } + + pub(crate) fn mime(&self) -> mime::Mime { + self.details.content_type.inner.clone() + } } #[derive(Debug, serde::Deserialize)] @@ -75,14 +81,10 @@ pub struct Images { } impl Images { - pub(crate) fn is_err(&self) -> bool { + fn is_err(&self) -> bool { self.files.is_none() } - pub(crate) fn message(&self) -> &str { - &self.msg - } - pub(crate) fn images(&self) -> impl DoubleEndedIterator { self.files.iter().flat_map(|v| v.iter()) } @@ -95,16 +97,21 @@ pub enum UploadError { #[error("Failed to parse image json")] Json, + + #[error("Error in pict-rs: {0}")] + Other(String), +} + +#[derive(Debug, thiserror::Error)] +pub enum DeleteError { + #[error("Failed to send request")] + Request, + + #[error("Status was not 2XX")] + Status, } pub trait ImageInfo { - fn icon_sizes(&self) -> &[u64]; - fn banner_sizes(&self) -> &[u64]; - fn thumbnail_sizes(&self) -> &[u64]; - - fn icon_url(&self, key: &str, kind: ImageType, size: u64) -> Url; - fn banner_url(&self, key: &str, kind: ImageType, size: u64) -> Url; - fn thumbnail_url(&self, key: &str, kind: ImageType, size: u64) -> Url; fn image_url(&self, key: &str) -> Url; } @@ -123,24 +130,24 @@ pub enum ImageType { #[derive(Clone)] pub struct State { upstream: Url, - image_info: Rc, + pub(super) info: Arc, client: Client, } impl State { - pub(crate) fn new(upstream: Url, image_info: impl ImageInfo + 'static, client: Client) -> Self { + pub(super) fn new( + upstream: Url, + image_info: impl ImageInfo + Send + Sync + 'static, + client: Client, + ) -> Self { State { upstream, - image_info: Rc::new(image_info), + info: Arc::new(image_info), client, } } - pub(super) fn image_info(&self) -> &dyn ImageInfo { - &*self.image_info - } - - pub(crate) async fn serve_thumbnail( + pub async fn serve_thumbnail( &self, req: &HttpRequest, key: &str, @@ -150,7 +157,7 @@ impl State { self.serve_image(self.thumbnail(key, kind, size), req).await } - pub(crate) async fn serve_banner( + pub async fn serve_banner( &self, req: &HttpRequest, key: &str, @@ -160,7 +167,7 @@ impl State { self.serve_image(self.banner(key, kind, size), req).await } - pub(crate) async fn serve_icon( + pub async fn serve_icon( &self, req: &HttpRequest, key: &str, @@ -170,7 +177,7 @@ impl State { self.serve_image(self.icon(key, kind, size), req).await } - pub(crate) async fn serve_full( + pub async fn serve_full( &self, req: &HttpRequest, key: &str, @@ -178,29 +185,6 @@ impl State { self.serve_image(self.full_size(key), req).await } - pub(crate) async fn proxy_upload( - &self, - req: HttpRequest, - body: Payload, - ) -> Result { - let client_req = self.client.request_from(self.upload().as_str(), req.head()); - - let client_req = if let Some(addr) = req.head().peer_addr { - client_req.header("X-Forwarded-For", addr.to_string()) - } else { - client_req - }; - - let mut res = client_req - .send_stream(body) - .await - .map_err(|_| UploadError::Request)?; - - let images = res.json::().await.map_err(|_| UploadError::Json)?; - - Ok(images) - } - async fn serve_image( &self, url: Url, @@ -224,13 +208,81 @@ impl State { Ok(client_res.body(BodyStream::new(res))) } - pub(crate) fn upload(&self) -> Url { + pub(super) async fn delete_image(&self, key: &str, token: &str) -> Result<(), DeleteError> { + let res = self + .client + .delete(self.delete(key, token).as_str()) + .send() + .await + .map_err(|e| { + log::error!("{}", e); + DeleteError::Request + })?; + + if !res.status().is_success() { + return Err(DeleteError::Status); + } + + Ok(()) + } + + pub(super) async fn proxy_upload( + &self, + req: HttpRequest, + body: Payload, + ) -> Result { + let client_req = self.client.request_from(self.upload().as_str(), req.head()); + + let client_req = if let Some(addr) = req.head().peer_addr { + client_req.header("X-Forwarded-For", addr.to_string()) + } else { + client_req + }; + + let mut res = client_req + .send_stream(body) + .await + .map_err(|_| UploadError::Request)?; + + let images = res.json::().await.map_err(|_| UploadError::Json)?; + + if images.is_err() { + return Err(UploadError::Other(images.msg)); + } + + Ok(images) + } + + pub(super) async fn download_image(&self, url: &Url) -> Result { + let mut res = self + .client + .get(self.download(url).as_str()) + .send() + .await + .map_err(|e| { + log::error!("Error in download: {}", e); + UploadError::Request + })?; + + let images: Images = res.json().await.map_err(|e| { + log::error!("Error in download: {}", e); + UploadError::Json + })?; + + if images.is_err() { + return Err(UploadError::Other(images.msg)); + } + + Ok(images) + } + + fn upload(&self) -> Url { let mut url = self.upstream.clone(); url.set_path("/image"); url } - pub(crate) fn full_size(&self, key: &str) -> Url { + fn full_size(&self, key: &str) -> Url { let mut url = self.upstream.clone(); url.set_path(&format!("/image/original/{}", key)); url @@ -242,25 +294,32 @@ impl State { url } - pub(crate) fn icon(&self, key: &str, kind: ImageType, size: u64) -> Url { + fn icon(&self, key: &str, kind: ImageType, size: u64) -> Url { let mut url = self.process(kind); url.set_query(Some(&format!("src={}&crop=1x1&resize={}", key, size))); url } - pub(crate) fn banner(&self, key: &str, kind: ImageType, size: u64) -> Url { + fn banner(&self, key: &str, kind: ImageType, size: u64) -> Url { let mut url = self.process(kind); url.set_query(Some(&format!("src={}&crop=3x1&resize={}", key, size))); url } - pub(crate) fn thumbnail(&self, key: &str, kind: ImageType, size: u64) -> Url { + fn thumbnail(&self, key: &str, kind: ImageType, size: u64) -> Url { let mut url = self.process(kind); url.set_query(Some(&format!("src={}&resize={}", key, size))); url } - pub(crate) fn delete(&self, key: &str, token: &str) -> Url { + fn download(&self, image_url: &Url) -> Url { + let mut url = self.upstream.clone(); + url.set_path("/image/download"); + url.set_query(Some(&format!("url={}", image_url))); + url + } + + fn delete(&self, key: &str, token: &str) -> Url { let mut url = self.upstream.clone(); url.set_path(&format!("/image/delete/{}/{}", token, key)); url diff --git a/profiles/src/store/mod.rs b/profiles/src/store/mod.rs index 9b01757..a970a0f 100644 --- a/profiles/src/store/mod.rs +++ b/profiles/src/store/mod.rs @@ -5,7 +5,6 @@ use uuid::Uuid; mod comment; mod file; -mod owner; mod profile; mod react; mod submission; @@ -13,7 +12,6 @@ pub mod view; #[derive(Clone)] pub struct Store { - pub owners: owner::Store, pub profiles: profile::Store, pub files: file::Store, pub submissions: submission::Store, @@ -25,7 +23,6 @@ pub struct Store { impl Store { pub fn build(db: &Db) -> Result { Ok(Store { - owners: owner::Store::build(db)?, profiles: profile::Store::build(db)?, files: file::Store::build(db)?, submissions: submission::Store::build(db)?, @@ -48,22 +45,6 @@ impl OwnerSource { } } -#[derive(Clone, Debug)] -pub struct Owner { - id: Uuid, - source: OwnerSource, -} - -impl Owner { - pub(crate) fn id(&self) -> Uuid { - self.id - } - - pub(crate) fn source(&self) -> &OwnerSource { - &self.source - } -} - #[derive(Clone, Debug)] pub struct Profile { id: Uuid, @@ -104,10 +85,6 @@ impl Profile { &self.handle } - pub(crate) fn domain(&self) -> &str { - &self.domain - } - pub(crate) fn display_name(&self) -> Option<&str> { self.display_name.as_ref().map(|dn| dn.as_str()) } @@ -143,12 +120,28 @@ pub struct PictRsFile { } impl PictRsFile { + pub(crate) fn new( + key: &str, + token: &str, + width: usize, + height: usize, + media_type: mime::Mime, + ) -> Self { + PictRsFile { + key: key.to_owned(), + token: token.to_owned(), + width, + height, + media_type, + } + } + pub(crate) fn key(&self) -> &str { &self.key } - pub(crate) fn mime(&self) -> mime::Mime { - self.media_type.clone() + pub(crate) fn token(&self) -> &str { + &self.token } } @@ -164,6 +157,10 @@ pub struct File { } impl File { + pub(crate) fn id(&self) -> Uuid { + self.id + } + pub(crate) fn source(&self) -> &FileSource { &self.source } @@ -576,7 +573,6 @@ impl fmt::Display for OwnerSource { impl fmt::Debug for Store { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("Store") - .field("owners", &"OwnerStore") .field("profiles", &"ProfileStore") .field("files", &"FileStore") .field("submissions", &"SubmissionStore") diff --git a/profiles/src/store/owner.rs b/profiles/src/store/owner.rs deleted file mode 100644 index 6e5b879..0000000 --- a/profiles/src/store/owner.rs +++ /dev/null @@ -1,155 +0,0 @@ -use super::{Owner, OwnerSource, StoreError, Undo}; -use chrono::{DateTime, Utc}; -use sled::{Db, Transactional, Tree}; -use std::io::Cursor; -use uuid::Uuid; - -#[derive(Clone, Debug)] -pub struct Store { - owner_tree: Tree, - source_tree: Tree, -} - -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -enum StoredOwnerSource<'a> { - Local(Uuid), - Remote(&'a str), -} - -#[derive(Debug, serde::Deserialize, serde::Serialize)] -struct StoredOwner<'a> { - id: Uuid, - #[serde(borrow)] - source: StoredOwnerSource<'a>, - created_at: DateTime, - updated_at: DateTime, -} - -impl Store { - pub fn build(db: &Db) -> Result { - Ok(Store { - owner_tree: db.open_tree("profiles/owners")?, - source_tree: db.open_tree("profiles/owners/source")?, - }) - } - - pub fn create(&self, source: &OwnerSource) -> Result { - let mut id; - let mut stored_owner; - - let now = Utc::now().into(); - let mut stored_owner_vec = vec![]; - - let stored_source = match source { - OwnerSource::Local(id) => StoredOwnerSource::Local(*id), - OwnerSource::Remote(s) => StoredOwnerSource::Remote(&s), - }; - - while { - stored_owner_vec.clear(); - let writer = Cursor::new(&mut stored_owner_vec); - id = Uuid::new_v4(); - stored_owner = StoredOwner { - id, - source: stored_source.clone(), - created_at: now, - updated_at: now, - }; - serde_json::to_writer(writer, &stored_owner)?; - self.owner_tree - .compare_and_swap( - id_owner_key(id), - None as Option<&[u8]>, - Some(stored_owner_vec.as_slice()), - )? - .is_err() - } {} - - let source_key = source_key(stored_source.clone()); - - if let Err(e) = self - .source_tree - .insert(source_key, id.to_string().as_bytes()) - { - self.owner_tree.remove(id_owner_key(id))?; - return Err(e.into()); - } - - Ok(stored_owner.into()) - } - - pub fn by_source(&self, source: &OwnerSource) -> Result, StoreError> { - let stored_source = match source { - OwnerSource::Local(id) => StoredOwnerSource::Local(*id), - OwnerSource::Remote(s) => StoredOwnerSource::Remote(&s), - }; - - let source_key = source_key(stored_source); - - let id_ivec = match self.source_tree.get(source_key)? { - Some(ivec) => ivec, - None => return Ok(None), - }; - - let id_str = String::from_utf8_lossy(&id_ivec); - let id: Uuid = id_str.parse().expect("Uuid is valid"); - - let owner_ivec = match self.owner_tree.get(id_owner_key(id))? { - Some(ivec) => ivec, - None => return Ok(None), - }; - - let stored_owner: StoredOwner = serde_json::from_slice(&owner_ivec)?; - - Ok(Some(stored_owner.into())) - } - - pub fn delete(&self, owner_id: Uuid) -> Result>, StoreError> { - let owner_ivec = match self.owner_tree.get(id_owner_key(owner_id))? { - Some(ivec) => ivec, - None => return Ok(None), - }; - - let stored_owner: StoredOwner = serde_json::from_slice(&owner_ivec)?; - - let id = owner_id; - let source_key = source_key(stored_owner.source.clone()); - - let owner = stored_owner.into(); - - &[&self.owner_tree, &self.source_tree].transaction(move |trees| { - let owner_tree = &trees[0]; - let source_tree = &trees[1]; - - source_tree.remove(source_key.as_bytes())?; - owner_tree.remove(id_owner_key(id).as_bytes())?; - - Ok(()) - })?; - - Ok(Some(Undo(owner))) - } -} - -fn id_owner_key(id: Uuid) -> String { - format!("/owner/{}/data", id) -} - -fn source_key<'a>(source: StoredOwnerSource<'a>) -> String { - match source { - StoredOwnerSource::Local(id) => format!("/source/local:{}", id), - StoredOwnerSource::Remote(s) => format!("/source/remote:{}", s), - } -} - -impl<'a> From> for Owner { - fn from(so: StoredOwner<'a>) -> Self { - Owner { - id: so.id, - source: match so.source { - StoredOwnerSource::Local(id) => OwnerSource::Local(id), - StoredOwnerSource::Remote(s) => OwnerSource::Remote(s.to_owned()), - }, - } - } -}