No warnings, expose top-level methods for image uploads

This commit is contained in:
asonix 2021-01-04 20:23:17 -06:00
parent 397f67ade6
commit fff74bf063
13 changed files with 1036 additions and 306 deletions

View file

@ -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
}

View file

@ -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<dyn ApubIds + Send + Sync>,
}
@ -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<Option<Url>, StoreError> {
fn endpoints_for_profile(&self, profile_id: Uuid) -> Result<Option<Endpoints>, 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<Option<Url>, 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<Option<String>, StoreError> {
fn public_key_for_id(&self, key_id: &Url) -> Result<Option<String>, 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<String, StoreError> {
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<Option<AnyBase>, StoreError> {
fn object(&self, id: &Url) -> Result<Option<AnyBase>, 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<bool, StoreError> {
Ok(self.seen.get(url.as_str())?.is_some())
fn apub_for_submission(&self, id: Uuid) -> Result<Option<Url>, 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<Option<Url>, 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<Option<Url>, 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<Option<Url>, 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<Option<Url>, 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<Option<Url>, 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<Option<Url>, 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<Option<StoredRecords>, StoreError> {
fn id_for_apub(&self, apub_id: &Url) -> Result<Option<StoredRecords>, 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<Option<Url>, StoreError> {
fn apub_for_id(&self, id: StoredRecords) -> Result<Option<Url>, 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<dyn ApubIds>")
.finish()
}

View file

@ -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<AnyBase, Error> {
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<AnyBase, Error> {
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)
}
}

View file

@ -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<AnyBase, Error> {
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<AnyBase, Error> {
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<AnyBase, Error> {
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<AnyBase, Error> {
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(&note_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)
}
}

View file

@ -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<AnyBase, Error> {
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<AnyBase, Error> {
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<AnyBase, Error> {
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<AnyBase, Error> {
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<AnyBase, Error> {
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<AnyBase, Error> {
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)
}
}

View file

@ -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,
}

View file

@ -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<AnyBase, Error> {
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<AnyBase, Error> {
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<AnyBase, Error> {
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)
}
}

View file

@ -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<AnyBase, Error> {
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<AnyBase, Error> {
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)
}
}

View file

@ -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<AnyBase, Error> {
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<AnyBase, Error> {
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<AnyBase, Error> {
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<AnyBase, Error> {
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(&note_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)
}
}

View file

@ -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<dyn pictrs::ImageInfo + Send + Sync>,
arbiter: Arbiter,
spawner: Arc<dyn Spawner + Send + Sync>,
}
@ -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<Url>, stack: Vec<AnyBase>);
fn purge_file(&self, file_id: Uuid);
fn process(&self, any_base: AnyBase, stack: Vec<AnyBase>);
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<AnyBase, Error>;
}
trait Required<T> {
fn req(self) -> Result<T, Error>;
}
@ -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<Uuid, Error> {
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<Vec<Uuid>, 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 {

View file

@ -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<T> {
inner: T,
}
impl<T> Serde<T> {
pub(crate) fn new(inner: T) -> Self {
Serde { inner }
}
}
impl<T> serde::Serialize for Serde<T>
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<Item = &Image> {
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<dyn ImageInfo>,
pub(super) info: Arc<dyn ImageInfo + Send + Sync>,
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<Images, UploadError> {
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::<Images>().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<Images, UploadError> {
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::<Images>().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<Images, UploadError> {
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

View file

@ -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<Store, sled::Error> {
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")

View file

@ -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<Utc>,
updated_at: DateTime<Utc>,
}
impl Store {
pub fn build(db: &Db) -> Result<Self, sled::Error> {
Ok(Store {
owner_tree: db.open_tree("profiles/owners")?,
source_tree: db.open_tree("profiles/owners/source")?,
})
}
pub fn create(&self, source: &OwnerSource) -> Result<Owner, StoreError> {
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<Option<Owner>, 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<Option<Undo<Owner>>, 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<StoredOwner<'a>> 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()),
},
}
}
}