Merge pull request 'Upgrade activitystreams to 0.7, again' (#1022) from ap07 into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1022
This commit is contained in:
		
						commit
						66376afb36
					
				| @ -14,6 +14,7 @@ | ||||
| - Bump Rust to nightly 2022-01-26 (#1015) | ||||
| - Remove "Latest articles" timeline (#1069) | ||||
| - Change order of timeline tabs (#1069, #1070, #1072) | ||||
| - Migrate ActivityPub-related crates from activitypub 0.1 to activitystreams 0.7 | ||||
| 
 | ||||
| ### Fixed | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										777
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										777
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -6,7 +6,6 @@ repository = "https://github.com/Plume-org/Plume" | ||||
| edition = "2018" | ||||
| 
 | ||||
| [dependencies] | ||||
| activitypub = "0.1.3" | ||||
| atom_syndication = "0.11.0" | ||||
| clap = "2.33" | ||||
| dotenv = "0.15.0" | ||||
| @ -28,6 +27,7 @@ webfinger = "0.4.1" | ||||
| tracing = "0.1.34" | ||||
| tracing-subscriber = "0.3.10" | ||||
| riker = "0.4.2" | ||||
| activitystreams = "0.7.0-alpha.18" | ||||
| 
 | ||||
| [[bin]] | ||||
| name = "plume" | ||||
|  | ||||
| @ -5,9 +5,6 @@ authors = ["Plume contributors"] | ||||
| edition = "2018" | ||||
| 
 | ||||
| [dependencies] | ||||
| activitypub = "0.1.1" | ||||
| activitystreams-derive = "0.1.1" | ||||
| activitystreams-traits = "0.1.0" | ||||
| array_tool = "1.0" | ||||
| base64 = "0.13" | ||||
| heck = "0.4.0" | ||||
| @ -23,6 +20,8 @@ syntect = "4.5.0" | ||||
| regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] } | ||||
| tracing = "0.1.34" | ||||
| askama_escape = "0.10.3" | ||||
| activitystreams = "0.7.0-alpha.18" | ||||
| activitystreams-ext = "0.1.0-alpha.2" | ||||
| url = "2.2.2" | ||||
| flume = "0.10.12" | ||||
| tokio = { version = "1.18.1", features = ["full"] } | ||||
| @ -38,6 +37,7 @@ git = "https://git.joinplu.me/Plume/pulldown-cmark" | ||||
| branch = "bidi-plume" | ||||
| 
 | ||||
| [dev-dependencies] | ||||
| assert-json-diff = "2.0.1" | ||||
| once_cell = "1.10.0" | ||||
| 
 | ||||
| [features] | ||||
|  | ||||
| @ -10,8 +10,7 @@ use super::{request, sign::Signer}; | ||||
| /// # Example
 | ||||
| ///
 | ||||
| /// ```rust
 | ||||
| /// # extern crate activitypub;
 | ||||
| /// # use activitypub::{actor::Person, activity::{Announce, Create}, object::Note};
 | ||||
| /// # use activitystreams::{prelude::*, base::Base, actor::Person, activity::{Announce, Create}, object::Note, iri_string::types::IriString};
 | ||||
| /// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
 | ||||
| /// # use once_cell::sync::Lazy;
 | ||||
| /// # use plume_common::activity_pub::inbox::*;
 | ||||
| @ -113,12 +112,13 @@ use super::{request, sign::Signer}; | ||||
| /// #     }
 | ||||
| /// # }
 | ||||
| /// #
 | ||||
| /// # let mut act = Create::default();
 | ||||
| /// # act.object_props.set_id_string(String::from("https://test.ap/activity")).unwrap();
 | ||||
| /// # let mut person = Person::default();
 | ||||
| /// # person.object_props.set_id_string(String::from("https://test.ap/actor")).unwrap();
 | ||||
| /// # act.create_props.set_actor_object(person).unwrap();
 | ||||
| /// # act.create_props.set_object_object(Note::default()).unwrap();
 | ||||
| /// # let mut person = Person::new();
 | ||||
| /// # person.set_id("https://test.ap/actor".parse::<IriString>().unwrap());
 | ||||
| /// # let mut act = Create::new(
 | ||||
| /// #     Base::retract(person).unwrap().into_generic().unwrap(),
 | ||||
| /// #     Base::retract(Note::new()).unwrap().into_generic().unwrap()
 | ||||
| /// # );
 | ||||
| /// # act.set_id("https://test.ap/activity".parse::<IriString>().unwrap());
 | ||||
| /// # let activity_json = serde_json::to_value(act).unwrap();
 | ||||
| /// #
 | ||||
| /// # let conn = ();
 | ||||
| @ -197,29 +197,29 @@ where | ||||
|     } | ||||
| 
 | ||||
|     /// Registers an handler on this Inbox.
 | ||||
|     pub fn with<A, V, M>(self, proxy: Option<&reqwest::Proxy>) -> Inbox<'a, C, E, R> | ||||
|     pub fn with<A, V, M>(self, proxy: Option<&reqwest::Proxy>) -> Self | ||||
|     where | ||||
|         A: AsActor<&'a C> + FromId<C, Error = E>, | ||||
|         V: activitypub::Activity, | ||||
|         V: activitystreams::markers::Activity + serde::de::DeserializeOwned, | ||||
|         M: AsObject<A, V, &'a C, Error = E> + FromId<C, Error = E>, | ||||
|         M::Output: Into<R>, | ||||
|     { | ||||
|         if let Inbox::NotHandled(ctx, mut act, e) = self { | ||||
|         if let Self::NotHandled(ctx, mut act, e) = self { | ||||
|             if serde_json::from_value::<V>(act.clone()).is_ok() { | ||||
|                 let act_clone = act.clone(); | ||||
|                 let act_id = match act_clone["id"].as_str() { | ||||
|                     Some(x) => x, | ||||
|                     None => return Inbox::NotHandled(ctx, act, InboxError::InvalidID), | ||||
|                     None => return Self::NotHandled(ctx, act, InboxError::InvalidID), | ||||
|                 }; | ||||
| 
 | ||||
|                 // Get the actor ID
 | ||||
|                 let actor_id = match get_id(act["actor"].clone()) { | ||||
|                     Some(x) => x, | ||||
|                     None => return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(None)), | ||||
|                     None => return Self::NotHandled(ctx, act, InboxError::InvalidActor(None)), | ||||
|                 }; | ||||
| 
 | ||||
|                 if Self::is_spoofed_activity(&actor_id, &act) { | ||||
|                     return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(None)); | ||||
|                     return Self::NotHandled(ctx, act, InboxError::InvalidObject(None)); | ||||
|                 } | ||||
| 
 | ||||
|                 // Transform this actor to a model (see FromId for details about the from_id function)
 | ||||
| @ -235,14 +235,14 @@ where | ||||
|                         if let Some(json) = json { | ||||
|                             act["actor"] = json; | ||||
|                         } | ||||
|                         return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(Some(e))); | ||||
|                         return Self::NotHandled(ctx, act, InboxError::InvalidActor(Some(e))); | ||||
|                     } | ||||
|                 }; | ||||
| 
 | ||||
|                 // Same logic for "object"
 | ||||
|                 let obj_id = match get_id(act["object"].clone()) { | ||||
|                     Some(x) => x, | ||||
|                     None => return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(None)), | ||||
|                     None => return Self::NotHandled(ctx, act, InboxError::InvalidObject(None)), | ||||
|                 }; | ||||
|                 let obj = match M::from_id( | ||||
|                     ctx, | ||||
| @ -255,19 +255,19 @@ where | ||||
|                         if let Some(json) = json { | ||||
|                             act["object"] = json; | ||||
|                         } | ||||
|                         return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(Some(e))); | ||||
|                         return Self::NotHandled(ctx, act, InboxError::InvalidObject(Some(e))); | ||||
|                     } | ||||
|                 }; | ||||
| 
 | ||||
|                 // Handle the activity
 | ||||
|                 match obj.activity(ctx, actor, act_id) { | ||||
|                     Ok(res) => Inbox::Handled(res.into()), | ||||
|                     Err(e) => Inbox::Failed(e), | ||||
|                     Ok(res) => Self::Handled(res.into()), | ||||
|                     Err(e) => Self::Failed(e), | ||||
|                 } | ||||
|             } else { | ||||
|                 // If the Activity type is not matching the expected one for
 | ||||
|                 // this handler, try with the next one.
 | ||||
|                 Inbox::NotHandled(ctx, act, e) | ||||
|                 Self::NotHandled(ctx, act, e) | ||||
|             } | ||||
|         } else { | ||||
|             self | ||||
| @ -333,7 +333,7 @@ pub trait FromId<C>: Sized { | ||||
|     type Error: From<InboxError<Self::Error>> + Debug; | ||||
| 
 | ||||
|     /// The ActivityPub object type representing Self
 | ||||
|     type Object: activitypub::Object; | ||||
|     type Object: activitystreams::markers::Object + serde::de::DeserializeOwned; | ||||
| 
 | ||||
|     /// Tries to get an instance of `Self` from an ActivityPub ID.
 | ||||
|     ///
 | ||||
| @ -418,8 +418,7 @@ pub trait AsActor<C> { | ||||
| /// representing the Note by a Message type, without any specific context.
 | ||||
| ///
 | ||||
| /// ```rust
 | ||||
| /// # extern crate activitypub;
 | ||||
| /// # use activitypub::{activity::Create, actor::Person, object::Note};
 | ||||
| /// # use activitystreams::{prelude::*, activity::Create, actor::Person, object::Note};
 | ||||
| /// # use plume_common::activity_pub::inbox::{AsActor, AsObject, FromId};
 | ||||
| /// # use plume_common::activity_pub::sign::{gen_keypair, Error as SignError, Result as SignResult, Signer};
 | ||||
| /// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
 | ||||
| @ -501,7 +500,10 @@ pub trait AsActor<C> { | ||||
| ///     }
 | ||||
| ///
 | ||||
| ///     fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> {
 | ||||
| ///         Ok(Message { text: obj.object_props.content_string().map_err(|_| ())? })
 | ||||
| ///         Ok(Message {
 | ||||
| ///             text: obj.content()
 | ||||
| ///                 .and_then(|content| content.to_owned().single_xsd_string()).ok_or(())?
 | ||||
| ///         })
 | ||||
| ///     }
 | ||||
| ///
 | ||||
| ///     fn get_sender() -> &'static dyn Signer {
 | ||||
| @ -521,7 +523,7 @@ pub trait AsActor<C> { | ||||
| /// ```
 | ||||
| pub trait AsObject<A, V, C> | ||||
| where | ||||
|     V: activitypub::Activity, | ||||
|     V: activitystreams::markers::Activity, | ||||
| { | ||||
|     /// What kind of error is returned when something fails
 | ||||
|     type Error; | ||||
| @ -549,7 +551,13 @@ mod tests { | ||||
|     use crate::activity_pub::sign::{ | ||||
|         gen_keypair, Error as SignError, Result as SignResult, Signer, | ||||
|     }; | ||||
|     use activitypub::{activity::*, actor::Person, object::Note}; | ||||
|     use activitystreams::{ | ||||
|         activity::{Announce, Create, Delete, Like}, | ||||
|         actor::Person, | ||||
|         base::Base, | ||||
|         object::Note, | ||||
|         prelude::*, | ||||
|     }; | ||||
|     use once_cell::sync::Lazy; | ||||
|     use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa}; | ||||
| 
 | ||||
| @ -598,11 +606,11 @@ mod tests { | ||||
|         type Object = Person; | ||||
| 
 | ||||
|         fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> { | ||||
|             Ok(MyActor) | ||||
|             Ok(Self) | ||||
|         } | ||||
| 
 | ||||
|         fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> { | ||||
|             Ok(MyActor) | ||||
|             Ok(Self) | ||||
|         } | ||||
| 
 | ||||
|         fn get_sender() -> &'static dyn Signer { | ||||
| @ -626,11 +634,11 @@ mod tests { | ||||
|         type Object = Note; | ||||
| 
 | ||||
|         fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> { | ||||
|             Ok(MyObject) | ||||
|             Ok(Self) | ||||
|         } | ||||
| 
 | ||||
|         fn from_activity(_: &(), _obj: Note) -> Result<Self, Self::Error> { | ||||
|             Ok(MyObject) | ||||
|             Ok(Self) | ||||
|         } | ||||
| 
 | ||||
|         fn get_sender() -> &'static dyn Signer { | ||||
| @ -678,21 +686,15 @@ mod tests { | ||||
|     } | ||||
| 
 | ||||
|     fn build_create() -> Create { | ||||
|         let mut act = Create::default(); | ||||
|         act.object_props | ||||
|             .set_id_string(String::from("https://test.ap/activity")) | ||||
|             .unwrap(); | ||||
|         let mut person = Person::default(); | ||||
|         person | ||||
|             .object_props | ||||
|             .set_id_string(String::from("https://test.ap/actor")) | ||||
|             .unwrap(); | ||||
|         act.create_props.set_actor_object(person).unwrap(); | ||||
|         let mut note = Note::default(); | ||||
|         note.object_props | ||||
|             .set_id_string(String::from("https://test.ap/note")) | ||||
|             .unwrap(); | ||||
|         act.create_props.set_object_object(note).unwrap(); | ||||
|         let mut person = Person::new(); | ||||
|         person.set_id("https://test.ap/actor".parse().unwrap()); | ||||
|         let mut note = Note::new(); | ||||
|         note.set_id("https://test.ap/note".parse().unwrap()); | ||||
|         let mut act = Create::new( | ||||
|             Base::retract(person).unwrap().into_generic().unwrap(), | ||||
|             Base::retract(note).unwrap().into_generic().unwrap(), | ||||
|         ); | ||||
|         act.set_id("https://test.ap/activity".parse().unwrap()); | ||||
|         act | ||||
|     } | ||||
| 
 | ||||
| @ -729,6 +731,16 @@ mod tests { | ||||
|     } | ||||
| 
 | ||||
|     struct FailingActor; | ||||
|     impl AsActor<&()> for FailingActor { | ||||
|         fn get_inbox_url(&self) -> String { | ||||
|             String::from("https://test.ap/failing-actor/inbox") | ||||
|         } | ||||
| 
 | ||||
|         fn is_local(&self) -> bool { | ||||
|             false | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     impl FromId<()> for FailingActor { | ||||
|         type Error = (); | ||||
|         type Object = Person; | ||||
| @ -737,7 +749,7 @@ mod tests { | ||||
|             Err(()) | ||||
|         } | ||||
| 
 | ||||
|         fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> { | ||||
|         fn from_activity(_: &(), _obj: Self::Object) -> Result<Self, Self::Error> { | ||||
|             Err(()) | ||||
|         } | ||||
| 
 | ||||
| @ -745,15 +757,6 @@ mod tests { | ||||
|             &*MY_SIGNER | ||||
|         } | ||||
|     } | ||||
|     impl AsActor<&()> for FailingActor { | ||||
|         fn get_inbox_url(&self) -> String { | ||||
|             String::from("https://test.ap/failing-actor/inbox") | ||||
|         } | ||||
| 
 | ||||
|         fn is_local(&self) -> bool { | ||||
|             false | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     impl AsObject<FailingActor, Create, &()> for MyObject { | ||||
|         type Error = (); | ||||
|  | ||||
| @ -1,4 +1,14 @@ | ||||
| use activitypub::{Activity, Link, Object}; | ||||
| use activitystreams::{ | ||||
|     actor::{ApActor, Group, Person}, | ||||
|     base::{AnyBase, Base, Extends}, | ||||
|     iri_string::types::IriString, | ||||
|     kind, | ||||
|     markers::{self, Activity}, | ||||
|     object::{ApObject, Article, Object}, | ||||
|     primitives::{AnyString, OneOrMany}, | ||||
|     unparsed::UnparsedMutExt, | ||||
| }; | ||||
| use activitystreams_ext::{Ext1, Ext2, UnparsedExtension}; | ||||
| use array_tool::vec::Uniq; | ||||
| use futures::future::join_all; | ||||
| use reqwest::{header::HeaderValue, ClientBuilder, RequestBuilder, Url}; | ||||
| @ -67,7 +77,7 @@ impl<T> ActivityStream<T> { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<'r, O: Object> Responder<'r> for ActivityStream<O> { | ||||
| impl<'r, O: serde::Serialize> Responder<'r> for ActivityStream<O> { | ||||
|     fn respond_to(self, request: &Request<'_>) -> Result<Response<'r>, Status> { | ||||
|         let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?; | ||||
|         json["@context"] = context(); | ||||
| @ -114,10 +124,11 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApRequest { | ||||
|             .unwrap_or(Outcome::Forward(())) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub fn broadcast<S, A, T, C>(sender: &S, act: A, to: Vec<T>, proxy: Option<reqwest::Proxy>) | ||||
| where | ||||
|     S: sign::Signer, | ||||
|     A: Activity, | ||||
|     A: Activity + serde::Serialize, | ||||
|     T: inbox::AsActor<C>, | ||||
| { | ||||
|     let boxes = to | ||||
| @ -198,15 +209,12 @@ where | ||||
|             } | ||||
|             headers.insert("Host", host_header_value.unwrap()); | ||||
|             headers.insert("Digest", request::Digest::digest(&body)); | ||||
|             let request_builder = client | ||||
|                 .post(&inbox) | ||||
|                 .headers(headers.clone()) | ||||
|                 .header( | ||||
|                     "Signature", | ||||
|                     request::signature(sender, &headers, ("post", url.path(), url.query())) | ||||
|                         .expect("activity_pub::broadcast: request signature error"), | ||||
|                 ) | ||||
|                 .body(body); | ||||
|             headers.insert( | ||||
|                 "Signature", | ||||
|                 request::signature(sender, &headers, ("post", url.path(), url.query())) | ||||
|                     .expect("activity_pub::broadcast: request signature error"), | ||||
|             ); | ||||
|             let request_builder = client.post(&inbox).headers(headers.clone()).body(body); | ||||
|             let _ = tx.send_async(request_builder).await; | ||||
|         } | ||||
|         drop(tx); | ||||
| @ -233,46 +241,193 @@ pub trait IntoId { | ||||
|     fn into_id(self) -> Id; | ||||
| } | ||||
| 
 | ||||
| impl Link for Id {} | ||||
| 
 | ||||
| #[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)] | ||||
| #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct ApSignature { | ||||
|     #[activitystreams(concrete(PublicKey), functional)] | ||||
|     pub public_key: Option<serde_json::Value>, | ||||
|     pub public_key: PublicKey, | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)] | ||||
| #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct PublicKey { | ||||
|     #[activitystreams(concrete(String), functional)] | ||||
|     pub id: Option<serde_json::Value>, | ||||
| 
 | ||||
|     #[activitystreams(concrete(String), functional)] | ||||
|     pub owner: Option<serde_json::Value>, | ||||
| 
 | ||||
|     #[activitystreams(concrete(String), functional)] | ||||
|     pub public_key_pem: Option<serde_json::Value>, | ||||
|     pub id: IriString, | ||||
|     pub owner: IriString, | ||||
|     pub public_key_pem: String, | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Debug, Default, UnitString)] | ||||
| #[activitystreams(Hashtag)] | ||||
| pub struct HashtagType; | ||||
| impl<U> UnparsedExtension<U> for ApSignature | ||||
| where | ||||
|     U: UnparsedMutExt, | ||||
| { | ||||
|     type Error = serde_json::Error; | ||||
| 
 | ||||
| #[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)] | ||||
|     fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> { | ||||
|         Ok(ApSignature { | ||||
|             public_key: unparsed_mut.remove("publicKey")?, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> { | ||||
|         unparsed_mut.insert("publicKey", self.public_key)?; | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct Hashtag { | ||||
|     #[serde(rename = "type")] | ||||
|     kind: HashtagType, | ||||
| 
 | ||||
|     #[activitystreams(concrete(String), functional)] | ||||
|     pub href: Option<serde_json::Value>, | ||||
| 
 | ||||
|     #[activitystreams(concrete(String), functional)] | ||||
|     pub name: Option<serde_json::Value>, | ||||
| pub struct SourceProperty { | ||||
|     pub source: Source, | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Debug, Default, Deserialize, Serialize)] | ||||
| impl<U> UnparsedExtension<U> for SourceProperty | ||||
| where | ||||
|     U: UnparsedMutExt, | ||||
| { | ||||
|     type Error = serde_json::Error; | ||||
| 
 | ||||
|     fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> { | ||||
|         Ok(SourceProperty { | ||||
|             source: unparsed_mut.remove("source")?, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> { | ||||
|         unparsed_mut.insert("source", self.source)?; | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub type CustomPerson = Ext1<ApActor<Person>, ApSignature>; | ||||
| pub type CustomGroup = Ext2<ApActor<Group>, ApSignature, SourceProperty>; | ||||
| 
 | ||||
| kind!(HashtagType, Hashtag); | ||||
| 
 | ||||
| #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] | ||||
| pub struct Hashtag { | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub href: Option<IriString>, | ||||
| 
 | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub name: Option<AnyString>, | ||||
| 
 | ||||
|     #[serde(flatten)] | ||||
|     inner: Object<HashtagType>, | ||||
| } | ||||
| 
 | ||||
| impl Hashtag { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             href: None, | ||||
|             name: None, | ||||
|             inner: Object::new(), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn extending(mut inner: Object<HashtagType>) -> Result<Self, serde_json::Error> { | ||||
|         let href = inner.remove("href")?; | ||||
|         let name = inner.remove("name")?; | ||||
| 
 | ||||
|         Ok(Self { href, name, inner }) | ||||
|     } | ||||
| 
 | ||||
|     pub fn retracting(self) -> Result<Object<HashtagType>, serde_json::Error> { | ||||
|         let Self { | ||||
|             href, | ||||
|             name, | ||||
|             mut inner, | ||||
|         } = self; | ||||
| 
 | ||||
|         inner.insert("href", href)?; | ||||
|         inner.insert("name", name)?; | ||||
|         Ok(inner) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub trait AsHashtag: markers::Object { | ||||
|     fn hashtag_ref(&self) -> &Hashtag; | ||||
| 
 | ||||
|     fn hashtag_mut(&mut self) -> &mut Hashtag; | ||||
| } | ||||
| 
 | ||||
| pub trait HashtagExt: AsHashtag { | ||||
|     fn href(&self) -> Option<&IriString> { | ||||
|         self.hashtag_ref().href.as_ref() | ||||
|     } | ||||
| 
 | ||||
|     fn set_href<T>(&mut self, href: T) -> &mut Self | ||||
|     where | ||||
|         T: Into<IriString>, | ||||
|     { | ||||
|         self.hashtag_mut().href = Some(href.into()); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     fn take_href(&mut self) -> Option<IriString> { | ||||
|         self.hashtag_mut().href.take() | ||||
|     } | ||||
| 
 | ||||
|     fn delete_href(&mut self) -> &mut Self { | ||||
|         self.hashtag_mut().href = None; | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     fn name(&self) -> Option<&AnyString> { | ||||
|         self.hashtag_ref().name.as_ref() | ||||
|     } | ||||
| 
 | ||||
|     fn set_name<T>(&mut self, name: T) -> &mut Self | ||||
|     where | ||||
|         T: Into<AnyString>, | ||||
|     { | ||||
|         self.hashtag_mut().name = Some(name.into()); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     fn take_name(&mut self) -> Option<AnyString> { | ||||
|         self.hashtag_mut().name.take() | ||||
|     } | ||||
| 
 | ||||
|     fn delete_name(&mut self) -> &mut Self { | ||||
|         self.hashtag_mut().name = None; | ||||
|         self | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Default for Hashtag { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl AsHashtag for Hashtag { | ||||
|     fn hashtag_ref(&self) -> &Self { | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     fn hashtag_mut(&mut self) -> &mut Self { | ||||
|         self | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Extends<HashtagType> for Hashtag { | ||||
|     type Error = serde_json::Error; | ||||
| 
 | ||||
|     fn extends(base: Base<HashtagType>) -> Result<Self, Self::Error> { | ||||
|         let inner = Object::extends(base)?; | ||||
|         Self::extending(inner) | ||||
|     } | ||||
| 
 | ||||
|     fn retracts(self) -> Result<Base<HashtagType>, Self::Error> { | ||||
|         let inner = self.retracting()?; | ||||
|         inner.retracts() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl markers::Base for Hashtag {} | ||||
| impl markers::Object for Hashtag {} | ||||
| impl<T> HashtagExt for T where T: AsHashtag {} | ||||
| 
 | ||||
| #[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct Source { | ||||
|     pub media_type: String, | ||||
| @ -280,13 +435,300 @@ pub struct Source { | ||||
|     pub content: String, | ||||
| } | ||||
| 
 | ||||
| impl Object for Source {} | ||||
| impl<U> UnparsedExtension<U> for Source | ||||
| where | ||||
|     U: UnparsedMutExt, | ||||
| { | ||||
|     type Error = serde_json::Error; | ||||
| 
 | ||||
| #[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct Licensed { | ||||
|     #[activitystreams(concrete(String), functional)] | ||||
|     pub license: Option<serde_json::Value>, | ||||
|     fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> { | ||||
|         Ok(Source { | ||||
|             content: unparsed_mut.remove("content")?, | ||||
|             media_type: unparsed_mut.remove("mediaType")?, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> { | ||||
|         unparsed_mut.insert("content", self.content)?; | ||||
|         unparsed_mut.insert("mediaType", self.media_type)?; | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Object for Licensed {} | ||||
| #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct Licensed { | ||||
|     pub license: Option<String>, | ||||
| } | ||||
| 
 | ||||
| impl<U> UnparsedExtension<U> for Licensed | ||||
| where | ||||
|     U: UnparsedMutExt, | ||||
| { | ||||
|     type Error = serde_json::Error; | ||||
| 
 | ||||
|     fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> { | ||||
|         Ok(Licensed { | ||||
|             license: unparsed_mut.remove("license")?, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> { | ||||
|         unparsed_mut.insert("license", self.license)?; | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub type LicensedArticle = Ext1<ApObject<Article>, Licensed>; | ||||
| 
 | ||||
| pub trait ToAsString { | ||||
|     fn to_as_string(&self) -> Option<String>; | ||||
| } | ||||
| 
 | ||||
| impl ToAsString for OneOrMany<&AnyString> { | ||||
|     fn to_as_string(&self) -> Option<String> { | ||||
|         self.as_as_str().map(|s| s.to_string()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| trait AsAsStr { | ||||
|     fn as_as_str(&self) -> Option<&str>; | ||||
| } | ||||
| 
 | ||||
| impl AsAsStr for OneOrMany<&AnyString> { | ||||
|     fn as_as_str(&self) -> Option<&str> { | ||||
|         self.iter().next().map(|prop| prop.as_str()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub trait ToAsUri { | ||||
|     fn to_as_uri(&self) -> Option<String>; | ||||
| } | ||||
| 
 | ||||
| impl ToAsUri for OneOrMany<AnyBase> { | ||||
|     fn to_as_uri(&self) -> Option<String> { | ||||
|         self.iter() | ||||
|             .next() | ||||
|             .and_then(|prop| prop.as_xsd_any_uri().map(|uri| uri.to_string())) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
|     use activitystreams::{ | ||||
|         activity::{ActorAndObjectRef, Create}, | ||||
|         object::kind::ArticleType, | ||||
|     }; | ||||
|     use assert_json_diff::assert_json_eq; | ||||
|     use serde_json::{from_str, json, to_value}; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn se_ap_signature() { | ||||
|         let ap_signature = ApSignature { | ||||
|             public_key: PublicKey { | ||||
|                 id: "https://example.com/pubkey".parse().unwrap(), | ||||
|                 owner: "https://example.com/owner".parse().unwrap(), | ||||
|                 public_key_pem: "pubKeyPem".into(), | ||||
|             }, | ||||
|         }; | ||||
|         let expected = json!({ | ||||
|             "publicKey": { | ||||
|                 "id": "https://example.com/pubkey", | ||||
|                 "owner": "https://example.com/owner", | ||||
|                 "publicKeyPem": "pubKeyPem" | ||||
|             } | ||||
|         }); | ||||
|         assert_json_eq!(to_value(ap_signature).unwrap(), expected); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn de_ap_signature() { | ||||
|         let value: ApSignature = from_str( | ||||
|             r#" | ||||
|               { | ||||
|                 "publicKey": { | ||||
|                   "id": "https://example.com/", | ||||
|                   "owner": "https://example.com/", | ||||
|                   "publicKeyPem": "" | ||||
|                 } | ||||
|               } | ||||
|             "#,
 | ||||
|         ) | ||||
|         .unwrap(); | ||||
|         let expected = ApSignature { | ||||
|             public_key: PublicKey { | ||||
|                 id: "https://example.com/".parse().unwrap(), | ||||
|                 owner: "https://example.com/".parse().unwrap(), | ||||
|                 public_key_pem: "".into(), | ||||
|             }, | ||||
|         }; | ||||
|         assert_eq!(value, expected); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn se_custom_person() { | ||||
|         let actor = ApActor::new("https://example.com/inbox".parse().unwrap(), Person::new()); | ||||
|         let person = CustomPerson::new( | ||||
|             actor, | ||||
|             ApSignature { | ||||
|                 public_key: PublicKey { | ||||
|                     id: "https://example.com/pubkey".parse().unwrap(), | ||||
|                     owner: "https://example.com/owner".parse().unwrap(), | ||||
|                     public_key_pem: "pubKeyPem".into(), | ||||
|                 }, | ||||
|             }, | ||||
|         ); | ||||
|         let expected = json!({ | ||||
|             "inbox": "https://example.com/inbox", | ||||
|             "type": "Person", | ||||
|             "publicKey": { | ||||
|                 "id": "https://example.com/pubkey", | ||||
|                 "owner": "https://example.com/owner", | ||||
|                 "publicKeyPem": "pubKeyPem" | ||||
|             } | ||||
|         }); | ||||
|         assert_eq!(to_value(person).unwrap(), expected); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn de_custom_group() { | ||||
|         let group = CustomGroup::new( | ||||
|             ApActor::new("https://example.com/inbox".parse().unwrap(), Group::new()), | ||||
|             ApSignature { | ||||
|                 public_key: PublicKey { | ||||
|                     id: "https://example.com/pubkey".parse().unwrap(), | ||||
|                     owner: "https://example.com/owner".parse().unwrap(), | ||||
|                     public_key_pem: "pubKeyPem".into(), | ||||
|                 }, | ||||
|             }, | ||||
|             SourceProperty { | ||||
|                 source: Source { | ||||
|                     content: String::from("This is a *custom* group."), | ||||
|                     media_type: String::from("text/markdown"), | ||||
|                 }, | ||||
|             }, | ||||
|         ); | ||||
|         let expected = json!({ | ||||
|             "inbox": "https://example.com/inbox", | ||||
|             "type": "Group", | ||||
|             "publicKey": { | ||||
|                 "id": "https://example.com/pubkey", | ||||
|                 "owner": "https://example.com/owner", | ||||
|                 "publicKeyPem": "pubKeyPem" | ||||
|             }, | ||||
|             "source": { | ||||
|                 "content": "This is a *custom* group.", | ||||
|                 "mediaType": "text/markdown" | ||||
|             } | ||||
|         }); | ||||
|         assert_eq!(to_value(group).unwrap(), expected); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn se_licensed_article() { | ||||
|         let object = ApObject::new(Article::new()); | ||||
|         let licensed_article = LicensedArticle::new( | ||||
|             object, | ||||
|             Licensed { | ||||
|                 license: Some("CC-0".into()), | ||||
|             }, | ||||
|         ); | ||||
|         let expected = json!({ | ||||
|             "type": "Article", | ||||
|             "license": "CC-0", | ||||
|         }); | ||||
|         assert_json_eq!(to_value(licensed_article).unwrap(), expected); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn de_licensed_article() { | ||||
|         let value: LicensedArticle = from_str( | ||||
|             r#" | ||||
|               { | ||||
|                 "type": "Article", | ||||
|                 "id": "https://plu.me/~/Blog/my-article", | ||||
|                 "attributedTo": ["https://plu.me/@/Admin", "https://plu.me/~/Blog"], | ||||
|                 "content": "Hello.", | ||||
|                 "name": "My Article", | ||||
|                 "summary": "Bye.", | ||||
|                 "source": { | ||||
|                   "content": "Hello.", | ||||
|                   "mediaType": "text/markdown" | ||||
|                 }, | ||||
|                 "published": "2014-12-12T12:12:12Z", | ||||
|                 "to": ["https://www.w3.org/ns/activitystreams#Public"], | ||||
|                 "license": "CC-0" | ||||
|              } | ||||
|            "#,
 | ||||
|         ) | ||||
|         .unwrap(); | ||||
|         let expected = json!({ | ||||
|             "type": "Article", | ||||
|             "id": "https://plu.me/~/Blog/my-article", | ||||
|             "attributedTo": ["https://plu.me/@/Admin", "https://plu.me/~/Blog"], | ||||
|             "content": "Hello.", | ||||
|             "name": "My Article", | ||||
|             "summary": "Bye.", | ||||
|             "source": { | ||||
|                 "content": "Hello.", | ||||
|                 "mediaType": "text/markdown" | ||||
|             }, | ||||
|             "published": "2014-12-12T12:12:12Z", | ||||
|             "to": ["https://www.w3.org/ns/activitystreams#Public"], | ||||
|             "license": "CC-0" | ||||
|         }); | ||||
| 
 | ||||
|         assert_eq!(to_value(value).unwrap(), expected); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn de_create_with_licensed_article() { | ||||
|         let create: Create = from_str( | ||||
|             r#" | ||||
|               { | ||||
|                 "id": "https://plu.me/~/Blog/my-article", | ||||
|                 "type": "Create", | ||||
|                 "actor": "https://plu.me/@/Admin", | ||||
|                 "to": "https://www.w3.org/ns/activitystreams#Public", | ||||
|                 "object": { | ||||
|                    "type": "Article", | ||||
|                    "id": "https://plu.me/~/Blog/my-article", | ||||
|                    "attributedTo": ["https://plu.me/@/Admin", "https://plu.me/~/Blog"], | ||||
|                    "content": "Hello.", | ||||
|                    "name": "My Article", | ||||
|                    "summary": "Bye.", | ||||
|                    "source": { | ||||
|                      "content": "Hello.", | ||||
|                      "mediaType": "text/markdown" | ||||
|                    }, | ||||
|                    "published": "2014-12-12T12:12:12Z", | ||||
|                    "to": ["https://www.w3.org/ns/activitystreams#Public"], | ||||
|                    "license": "CC-0" | ||||
|                  } | ||||
|                } | ||||
|             "#,
 | ||||
|         ) | ||||
|         .unwrap(); | ||||
|         let base = create.object_field_ref().as_single_base().unwrap(); | ||||
|         let any_base = AnyBase::from_base(base.clone()); | ||||
|         let value = any_base.extend::<LicensedArticle, ArticleType>().unwrap(); | ||||
|         let expected = json!({ | ||||
|             "type": "Article", | ||||
|             "id": "https://plu.me/~/Blog/my-article", | ||||
|             "attributedTo": ["https://plu.me/@/Admin", "https://plu.me/~/Blog"], | ||||
|             "content": "Hello.", | ||||
|             "name": "My Article", | ||||
|             "summary": "Bye.", | ||||
|             "source": { | ||||
|                 "content": "Hello.", | ||||
|                 "mediaType": "text/markdown" | ||||
|             }, | ||||
|             "published": "2014-12-12T12:12:12Z", | ||||
|             "to": ["https://www.w3.org/ns/activitystreams#Public"], | ||||
|             "license": "CC-0" | ||||
|         }); | ||||
| 
 | ||||
|         assert_eq!(to_value(value).unwrap(), expected); | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										2
									
								
								plume-common/src/lib.rs
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										2
									
								
								plume-common/src/lib.rs
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @ -1,7 +1,5 @@ | ||||
| #![feature(associated_type_defaults)] | ||||
| 
 | ||||
| #[macro_use] | ||||
| extern crate activitystreams_derive; | ||||
| #[macro_use] | ||||
| extern crate shrinkwraprs; | ||||
| #[macro_use] | ||||
|  | ||||
| @ -5,7 +5,6 @@ authors = ["Plume contributors"] | ||||
| edition = "2018" | ||||
| 
 | ||||
| [dependencies] | ||||
| activitypub = "0.1.1" | ||||
| ammonia = "3.2.0" | ||||
| bcrypt = "0.12.1" | ||||
| guid-create = "0.2" | ||||
| @ -35,6 +34,7 @@ riker = "0.4.2" | ||||
| once_cell = "1.10.0" | ||||
| lettre = "0.9.6" | ||||
| native-tls = "0.2.10" | ||||
| activitystreams = "0.7.0-alpha.18" | ||||
| 
 | ||||
| [dependencies.chrono] | ||||
| features = ["serde"] | ||||
|  | ||||
| @ -1,12 +1,14 @@ | ||||
| use crate::{ | ||||
|     ap_url, db_conn::DbConn, instance::*, medias::Media, posts::Post, safe_string::SafeString, | ||||
|     db_conn::DbConn, instance::*, medias::Media, posts::Post, safe_string::SafeString, | ||||
|     schema::blogs, users::User, Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE, | ||||
| }; | ||||
| use activitypub::{ | ||||
|     actor::Group, | ||||
| use activitystreams::{ | ||||
|     actor::{ApActor, ApActorExt, AsApActor, Group}, | ||||
|     base::AnyBase, | ||||
|     collection::{OrderedCollection, OrderedCollectionPage}, | ||||
|     object::Image, | ||||
|     CustomObject, | ||||
|     iri_string::types::IriString, | ||||
|     object::{kind::ImageType, ApObject, Image, ObjectExt}, | ||||
|     prelude::*, | ||||
| }; | ||||
| use chrono::NaiveDateTime; | ||||
| use diesel::{self, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SaveChangesDsl}; | ||||
| @ -18,14 +20,12 @@ use openssl::{ | ||||
| }; | ||||
| use plume_common::activity_pub::{ | ||||
|     inbox::{AsActor, FromId}, | ||||
|     sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source, | ||||
|     sign, ActivityStream, ApSignature, CustomGroup, Id, IntoId, PublicKey, Source, SourceProperty, | ||||
|     ToAsString, ToAsUri, | ||||
| }; | ||||
| use url::Url; | ||||
| use webfinger::*; | ||||
| 
 | ||||
| pub type CustomGroup = CustomObject<ApSignature, Group>; | ||||
| 
 | ||||
| #[derive(Queryable, Identifiable, Clone, AsChangeset)] | ||||
| #[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)] | ||||
| #[changeset_options(treat_none_as_null = "true")] | ||||
| pub struct Blog { | ||||
|     pub id: i32, | ||||
| @ -161,104 +161,120 @@ impl Blog { | ||||
|     } | ||||
| 
 | ||||
|     pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> { | ||||
|         let mut blog = Group::default(); | ||||
|         blog.ap_actor_props | ||||
|             .set_preferred_username_string(self.actor_id.clone())?; | ||||
|         blog.object_props.set_name_string(self.title.clone())?; | ||||
|         blog.ap_actor_props | ||||
|             .set_outbox_string(self.outbox_url.clone())?; | ||||
|         blog.ap_actor_props | ||||
|             .set_inbox_string(self.inbox_url.clone())?; | ||||
|         blog.object_props | ||||
|             .set_summary_string(self.summary_html.to_string())?; | ||||
|         blog.ap_object_props.set_source_object(Source { | ||||
|             content: self.summary.clone(), | ||||
|             media_type: String::from("text/markdown"), | ||||
|         })?; | ||||
|         let mut blog = ApActor::new(self.inbox_url.parse()?, Group::new()); | ||||
|         blog.set_preferred_username(self.actor_id.clone()); | ||||
|         blog.set_name(self.title.clone()); | ||||
|         blog.set_outbox(self.outbox_url.parse()?); | ||||
|         blog.set_summary(self.summary_html.to_string()); | ||||
|         let source = SourceProperty { | ||||
|             source: Source { | ||||
|                 content: self.summary.clone(), | ||||
|                 media_type: String::from("text/markdown"), | ||||
|             }, | ||||
|         }; | ||||
| 
 | ||||
|         let mut icon = Image::default(); | ||||
|         icon.object_props.set_url_string( | ||||
|             self.icon_id | ||||
|                 .and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok()) | ||||
|                 .unwrap_or_default(), | ||||
|         )?; | ||||
|         icon.object_props.set_attributed_to_link( | ||||
|             self.icon_id | ||||
|                 .and_then(|id| { | ||||
|                     Media::get(conn, id) | ||||
|                         .and_then(|m| Ok(User::get(conn, m.owner_id)?.into_id())) | ||||
|                         .ok() | ||||
|                 }) | ||||
|                 .unwrap_or_else(|| Id::new(String::new())), | ||||
|         )?; | ||||
|         blog.object_props.set_icon_object(icon)?; | ||||
|         let mut icon = Image::new(); | ||||
|         let _ = self.icon_id.map(|id| { | ||||
|             Media::get(conn, id).and_then(|m| { | ||||
|                 let _ = m | ||||
|                     .url() | ||||
|                     .and_then(|url| url.parse::<IriString>().map_err(|_| Error::Url)) | ||||
|                     .map(|url| icon.set_url(url)); | ||||
|                 icon.set_attributed_to( | ||||
|                     User::get(conn, m.owner_id)? | ||||
|                         .into_id() | ||||
|                         .parse::<IriString>()?, | ||||
|                 ); | ||||
|                 Ok(()) | ||||
|             }) | ||||
|         }); | ||||
|         blog.set_icon(icon.into_any_base()?); | ||||
| 
 | ||||
|         let mut banner = Image::default(); | ||||
|         banner.object_props.set_url_string( | ||||
|             self.banner_id | ||||
|                 .and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok()) | ||||
|                 .unwrap_or_default(), | ||||
|         )?; | ||||
|         banner.object_props.set_attributed_to_link( | ||||
|             self.banner_id | ||||
|                 .and_then(|id| { | ||||
|                     Media::get(conn, id) | ||||
|                         .and_then(|m| Ok(User::get(conn, m.owner_id)?.into_id())) | ||||
|                         .ok() | ||||
|                 }) | ||||
|                 .unwrap_or_else(|| Id::new(String::new())), | ||||
|         )?; | ||||
|         blog.object_props.set_image_object(banner)?; | ||||
|         let mut banner = Image::new(); | ||||
|         let _ = self.banner_id.map(|id| { | ||||
|             Media::get(conn, id).and_then(|m| { | ||||
|                 let _ = m | ||||
|                     .url() | ||||
|                     .and_then(|url| url.parse::<IriString>().map_err(|_| Error::Url)) | ||||
|                     .map(|url| banner.set_url(url)); | ||||
|                 banner.set_attributed_to( | ||||
|                     User::get(conn, m.owner_id)? | ||||
|                         .into_id() | ||||
|                         .parse::<IriString>()?, | ||||
|                 ); | ||||
|                 Ok(()) | ||||
|             }) | ||||
|         }); | ||||
|         blog.set_image(banner.into_any_base()?); | ||||
| 
 | ||||
|         blog.object_props.set_id_string(self.ap_url.clone())?; | ||||
|         blog.set_id(self.ap_url.parse()?); | ||||
| 
 | ||||
|         let mut public_key = PublicKey::default(); | ||||
|         public_key.set_id_string(format!("{}#main-key", self.ap_url))?; | ||||
|         public_key.set_owner_string(self.ap_url.clone())?; | ||||
|         public_key.set_public_key_pem_string(self.public_key.clone())?; | ||||
|         let mut ap_signature = ApSignature::default(); | ||||
|         ap_signature.set_public_key_publickey(public_key)?; | ||||
|         let pub_key = PublicKey { | ||||
|             id: format!("{}#main-key", self.ap_url).parse()?, | ||||
|             owner: self.ap_url.parse()?, | ||||
|             public_key_pem: self.public_key.clone(), | ||||
|         }; | ||||
|         let ap_signature = ApSignature { | ||||
|             public_key: pub_key, | ||||
|         }; | ||||
| 
 | ||||
|         Ok(CustomGroup::new(blog, ap_signature)) | ||||
|         Ok(CustomGroup::new(blog, ap_signature, source)) | ||||
|     } | ||||
| 
 | ||||
|     pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> { | ||||
|         let mut coll = OrderedCollection::default(); | ||||
|         coll.collection_props.items = serde_json::to_value(self.get_activities(conn))?; | ||||
|         coll.collection_props | ||||
|             .set_total_items_u64(self.get_activities(conn).len() as u64)?; | ||||
|         coll.collection_props | ||||
|             .set_first_link(Id::new(ap_url(&format!("{}?page=1", &self.outbox_url))))?; | ||||
|         coll.collection_props | ||||
|             .set_last_link(Id::new(ap_url(&format!( | ||||
|         self.outbox_collection(conn).map(ActivityStream::new) | ||||
|     } | ||||
|     pub fn outbox_collection(&self, conn: &Connection) -> Result<OrderedCollection> { | ||||
|         let acts = self.get_activities(conn); | ||||
|         let acts = acts | ||||
|             .iter() | ||||
|             .filter_map(|value| AnyBase::from_arbitrary_json(value).ok()) | ||||
|             .collect::<Vec<AnyBase>>(); | ||||
|         let n_acts = acts.len(); | ||||
|         let mut coll = OrderedCollection::new(); | ||||
|         coll.set_many_items(acts); | ||||
|         coll.set_total_items(n_acts as u64); | ||||
|         coll.set_first(format!("{}?page=1", &self.outbox_url).parse::<IriString>()?); | ||||
|         coll.set_last( | ||||
|             format!( | ||||
|                 "{}?page={}", | ||||
|                 &self.outbox_url, | ||||
|                 (self.get_activities(conn).len() as u64 + ITEMS_PER_PAGE as u64 - 1) as u64 | ||||
|                     / ITEMS_PER_PAGE as u64 | ||||
|             ))))?; | ||||
|         Ok(ActivityStream::new(coll)) | ||||
|                 (n_acts as u64 + ITEMS_PER_PAGE as u64 - 1) as u64 / ITEMS_PER_PAGE as u64 | ||||
|             ) | ||||
|             .parse::<IriString>()?, | ||||
|         ); | ||||
|         Ok(coll) | ||||
|     } | ||||
|     pub fn outbox_page( | ||||
|         &self, | ||||
|         conn: &Connection, | ||||
|         (min, max): (i32, i32), | ||||
|     ) -> Result<ActivityStream<OrderedCollectionPage>> { | ||||
|         let mut coll = OrderedCollectionPage::default(); | ||||
|         self.outbox_collection_page(conn, (min, max)) | ||||
|             .map(ActivityStream::new) | ||||
|     } | ||||
|     pub fn outbox_collection_page( | ||||
|         &self, | ||||
|         conn: &Connection, | ||||
|         (min, max): (i32, i32), | ||||
|     ) -> Result<OrderedCollectionPage> { | ||||
|         let mut coll = OrderedCollectionPage::new(); | ||||
|         let acts = self.get_activity_page(conn, (min, max)); | ||||
|         //This still doesn't do anything because the outbox
 | ||||
|         //doesn't do anything yet
 | ||||
|         coll.collection_page_props.set_next_link(Id::new(&format!( | ||||
|             "{}?page={}", | ||||
|             &self.outbox_url, | ||||
|             min / ITEMS_PER_PAGE + 1 | ||||
|         )))?; | ||||
|         coll.collection_page_props.set_prev_link(Id::new(&format!( | ||||
|             "{}?page={}", | ||||
|             &self.outbox_url, | ||||
|             min / ITEMS_PER_PAGE - 1 | ||||
|         )))?; | ||||
|         coll.collection_props.items = serde_json::to_value(acts)?; | ||||
|         Ok(ActivityStream::new(coll)) | ||||
|         coll.set_next( | ||||
|             format!("{}?page={}", &self.outbox_url, min / ITEMS_PER_PAGE + 1) | ||||
|                 .parse::<IriString>()?, | ||||
|         ); | ||||
|         coll.set_prev( | ||||
|             format!("{}?page={}", &self.outbox_url, min / ITEMS_PER_PAGE - 1) | ||||
|                 .parse::<IriString>()?, | ||||
|         ); | ||||
|         coll.set_many_items( | ||||
|             acts.iter() | ||||
|                 .filter_map(|value| AnyBase::from_arbitrary_json(value).ok()), | ||||
|         ); | ||||
|         Ok(coll) | ||||
|     } | ||||
|     fn get_activities(&self, _conn: &Connection) -> Vec<serde_json::Value> { | ||||
|         vec![] | ||||
| @ -354,9 +370,90 @@ impl FromId<DbConn> for Blog { | ||||
|     } | ||||
| 
 | ||||
|     fn from_activity(conn: &DbConn, acct: CustomGroup) -> Result<Self> { | ||||
|         let url = Url::parse(&acct.object.object_props.id_string()?)?; | ||||
|         let inst = url.host_str().ok_or(Error::Url)?; | ||||
|         let instance = Instance::find_by_domain(conn, inst).or_else(|_| { | ||||
|         let (name, outbox_url, inbox_url) = { | ||||
|             let actor = acct.ap_actor_ref(); | ||||
|             let name = actor | ||||
|                 .preferred_username() | ||||
|                 .ok_or(Error::MissingApProperty)? | ||||
|                 .to_string(); | ||||
|             if name.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) { | ||||
|                 return Err(Error::InvalidValue); | ||||
|             } | ||||
|             ( | ||||
|                 name, | ||||
|                 actor.outbox()?.ok_or(Error::MissingApProperty)?.to_string(), | ||||
|                 actor.inbox()?.to_string(), | ||||
|             ) | ||||
|         }; | ||||
| 
 | ||||
|         let mut new_blog = NewBlog { | ||||
|             actor_id: name.to_string(), | ||||
|             outbox_url, | ||||
|             inbox_url, | ||||
|             public_key: acct.ext_one.public_key.public_key_pem.to_string(), | ||||
|             private_key: None, | ||||
|             theme: None, | ||||
|             ..NewBlog::default() | ||||
|         }; | ||||
| 
 | ||||
|         let object = ApObject::new(acct.inner); | ||||
|         new_blog.title = object | ||||
|             .name() | ||||
|             .and_then(|name| name.to_as_string()) | ||||
|             .unwrap_or(name); | ||||
|         new_blog.summary_html = SafeString::new( | ||||
|             &object | ||||
|                 .summary() | ||||
|                 .and_then(|summary| summary.to_as_string()) | ||||
|                 .unwrap_or_default(), | ||||
|         ); | ||||
| 
 | ||||
|         let icon_id = object | ||||
|             .icon() | ||||
|             .and_then(|icons| { | ||||
|                 icons.iter().next().and_then(|icon| { | ||||
|                     let icon = icon.to_owned().extend::<Image, ImageType>().ok()??; | ||||
|                     let owner = icon.attributed_to()?.to_as_uri()?; | ||||
|                     Media::save_remote( | ||||
|                         conn, | ||||
|                         icon.url()?.to_as_uri()?, | ||||
|                         &User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?, | ||||
|                     ) | ||||
|                     .ok() | ||||
|                 }) | ||||
|             }) | ||||
|             .map(|m| m.id); | ||||
|         new_blog.icon_id = icon_id; | ||||
| 
 | ||||
|         let banner_id = object | ||||
|             .image() | ||||
|             .and_then(|banners| { | ||||
|                 banners.iter().next().and_then(|banner| { | ||||
|                     let banner = banner.to_owned().extend::<Image, ImageType>().ok()??; | ||||
|                     let owner = banner.attributed_to()?.to_as_uri()?; | ||||
|                     Media::save_remote( | ||||
|                         conn, | ||||
|                         banner.url()?.to_as_uri()?, | ||||
|                         &User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?, | ||||
|                     ) | ||||
|                     .ok() | ||||
|                 }) | ||||
|             }) | ||||
|             .map(|m| m.id); | ||||
|         new_blog.banner_id = banner_id; | ||||
| 
 | ||||
|         new_blog.summary = acct.ext_two.source.content; | ||||
| 
 | ||||
|         let any_base = AnyBase::from_extended(object)?; | ||||
|         let id = any_base.id().ok_or(Error::MissingApProperty)?; | ||||
|         new_blog.ap_url = id.to_string(); | ||||
| 
 | ||||
|         let inst = id | ||||
|             .authority_components() | ||||
|             .ok_or(Error::Url)? | ||||
|             .host() | ||||
|             .to_string(); | ||||
|         let instance = Instance::find_by_domain(conn, &inst).or_else(|_| { | ||||
|             Instance::insert( | ||||
|                 conn, | ||||
|                 NewInstance { | ||||
| @ -373,75 +470,9 @@ impl FromId<DbConn> for Blog { | ||||
|                 }, | ||||
|             ) | ||||
|         })?; | ||||
|         let icon_id = acct | ||||
|             .object | ||||
|             .object_props | ||||
|             .icon_image() | ||||
|             .ok() | ||||
|             .and_then(|icon| { | ||||
|                 let owner = icon.object_props.attributed_to_link::<Id>().ok()?; | ||||
|                 Media::save_remote( | ||||
|                     conn, | ||||
|                     icon.object_props.url_string().ok()?, | ||||
|                     &User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?, | ||||
|                 ) | ||||
|                 .ok() | ||||
|             }) | ||||
|             .map(|m| m.id); | ||||
|         new_blog.instance_id = instance.id; | ||||
| 
 | ||||
|         let banner_id = acct | ||||
|             .object | ||||
|             .object_props | ||||
|             .image_image() | ||||
|             .ok() | ||||
|             .and_then(|banner| { | ||||
|                 let owner = banner.object_props.attributed_to_link::<Id>().ok()?; | ||||
|                 Media::save_remote( | ||||
|                     conn, | ||||
|                     banner.object_props.url_string().ok()?, | ||||
|                     &User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?, | ||||
|                 ) | ||||
|                 .ok() | ||||
|             }) | ||||
|             .map(|m| m.id); | ||||
| 
 | ||||
|         let name = acct.object.ap_actor_props.preferred_username_string()?; | ||||
|         if name.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) { | ||||
|             return Err(Error::InvalidValue); | ||||
|         } | ||||
| 
 | ||||
|         Blog::insert( | ||||
|             conn, | ||||
|             NewBlog { | ||||
|                 actor_id: name.clone(), | ||||
|                 title: acct.object.object_props.name_string().unwrap_or(name), | ||||
|                 outbox_url: acct.object.ap_actor_props.outbox_string()?, | ||||
|                 inbox_url: acct.object.ap_actor_props.inbox_string()?, | ||||
|                 summary: acct | ||||
|                     .object | ||||
|                     .ap_object_props | ||||
|                     .source_object::<Source>() | ||||
|                     .map(|s| s.content) | ||||
|                     .unwrap_or_default(), | ||||
|                 instance_id: instance.id, | ||||
|                 ap_url: acct.object.object_props.id_string()?, | ||||
|                 public_key: acct | ||||
|                     .custom_props | ||||
|                     .public_key_publickey()? | ||||
|                     .public_key_pem_string()?, | ||||
|                 private_key: None, | ||||
|                 banner_id, | ||||
|                 icon_id, | ||||
|                 summary_html: SafeString::new( | ||||
|                     &acct | ||||
|                         .object | ||||
|                         .object_props | ||||
|                         .summary_string() | ||||
|                         .unwrap_or_default(), | ||||
|                 ), | ||||
|                 theme: None, | ||||
|             }, | ||||
|         ) | ||||
|         Blog::insert(conn, new_blog) | ||||
|     } | ||||
| 
 | ||||
|     fn get_sender() -> &'static dyn sign::Signer { | ||||
| @ -512,12 +543,14 @@ pub(crate) mod tests { | ||||
|         blog_authors::*, instance::tests as instance_tests, medias::NewMedia, tests::db, | ||||
|         users::tests as usersTests, Connection as Conn, | ||||
|     }; | ||||
|     use assert_json_diff::assert_json_eq; | ||||
|     use diesel::Connection; | ||||
|     use serde_json::to_value; | ||||
| 
 | ||||
|     pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Blog>) { | ||||
|         instance_tests::fill_database(conn); | ||||
|         let users = usersTests::fill_database(conn); | ||||
|         let blog1 = Blog::insert( | ||||
|         let mut blog1 = Blog::insert( | ||||
|             conn, | ||||
|             NewBlog::new_local( | ||||
|                 "BlogName".to_owned(), | ||||
| @ -590,6 +623,41 @@ pub(crate) mod tests { | ||||
|             }, | ||||
|         ) | ||||
|         .unwrap(); | ||||
| 
 | ||||
|         blog1.icon_id = Some( | ||||
|             Media::insert( | ||||
|                 conn, | ||||
|                 NewMedia { | ||||
|                     file_path: "aaa.png".into(), | ||||
|                     alt_text: String::new(), | ||||
|                     is_remote: false, | ||||
|                     remote_url: None, | ||||
|                     sensitive: false, | ||||
|                     content_warning: None, | ||||
|                     owner_id: users[0].id, | ||||
|                 }, | ||||
|             ) | ||||
|             .unwrap() | ||||
|             .id, | ||||
|         ); | ||||
|         blog1.banner_id = Some( | ||||
|             Media::insert( | ||||
|                 conn, | ||||
|                 NewMedia { | ||||
|                     file_path: "bbb.png".into(), | ||||
|                     alt_text: String::new(), | ||||
|                     is_remote: false, | ||||
|                     remote_url: None, | ||||
|                     sensitive: false, | ||||
|                     content_warning: None, | ||||
|                     owner_id: users[0].id, | ||||
|                 }, | ||||
|             ) | ||||
|             .unwrap() | ||||
|             .id, | ||||
|         ); | ||||
|         let _: Blog = blog1.save_changes(&*conn).unwrap(); | ||||
| 
 | ||||
|         (users, vec![blog1, blog2, blog3]) | ||||
|     } | ||||
| 
 | ||||
| @ -886,7 +954,6 @@ pub(crate) mod tests { | ||||
|                 .id, | ||||
|             ); | ||||
|             let _: Blog = blogs[0].save_changes(&**conn).unwrap(); | ||||
| 
 | ||||
|             let ap_repr = blogs[0].to_activity(&conn).unwrap(); | ||||
|             blogs[0].delete(&conn).unwrap(); | ||||
|             let blog = Blog::from_activity(&conn, ap_repr).unwrap(); | ||||
| @ -907,4 +974,90 @@ pub(crate) mod tests { | ||||
|             Ok(()) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn to_activity() { | ||||
|         let conn = &db(); | ||||
|         conn.test_transaction::<_, Error, _>(|| { | ||||
|             let (_users, blogs) = fill_database(&conn); | ||||
|             let blog = &blogs[0]; | ||||
|             let act = blog.to_activity(conn)?; | ||||
| 
 | ||||
|             let expected = json!({ | ||||
|                 "icon": { | ||||
|                     "attributedTo": "https://plu.me/@/admin/", | ||||
|                     "type": "Image", | ||||
|                     "url": "https://plu.me/aaa.png" | ||||
|                 }, | ||||
|                 "id": "https://plu.me/~/BlogName/", | ||||
|                 "image": { | ||||
|                     "attributedTo": "https://plu.me/@/admin/", | ||||
|                     "type": "Image", | ||||
|                     "url": "https://plu.me/bbb.png" | ||||
|                 }, | ||||
|                 "inbox": "https://plu.me/~/BlogName/inbox", | ||||
|                 "name": "Blog name", | ||||
|                 "outbox": "https://plu.me/~/BlogName/outbox", | ||||
|                 "preferredUsername": "BlogName", | ||||
|                 "publicKey": { | ||||
|                     "id": "https://plu.me/~/BlogName/#main-key", | ||||
|                     "owner": "https://plu.me/~/BlogName/", | ||||
|                     "publicKeyPem": blog.public_key | ||||
|                 }, | ||||
|                 "source": { | ||||
|                     "content": "This is a small blog", | ||||
|                     "mediaType": "text/markdown" | ||||
|                 }, | ||||
|                 "summary": "", | ||||
|                 "type": "Group" | ||||
|             }); | ||||
| 
 | ||||
|             assert_json_eq!(to_value(act)?, expected); | ||||
| 
 | ||||
|             Ok(()) | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn outbox_collection() { | ||||
|         let conn = &db(); | ||||
|         conn.test_transaction::<_, Error, _>(|| { | ||||
|             let (_users, blogs) = fill_database(conn); | ||||
|             let blog = &blogs[0]; | ||||
|             let act = blog.outbox_collection(conn)?; | ||||
| 
 | ||||
|             let expected = json!({ | ||||
|                 "items": [], | ||||
|                 "totalItems": 0, | ||||
|                 "first": "https://plu.me/~/BlogName/outbox?page=1", | ||||
|                 "last": "https://plu.me/~/BlogName/outbox?page=0", | ||||
|                 "type": "OrderedCollection" | ||||
|             }); | ||||
| 
 | ||||
|             assert_json_eq!(to_value(act)?, expected); | ||||
| 
 | ||||
|             Ok(()) | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn outbox_collection_page() { | ||||
|         let conn = &db(); | ||||
|         conn.test_transaction::<_, Error, _>(|| { | ||||
|             let (_users, blogs) = fill_database(conn); | ||||
|             let blog = &blogs[0]; | ||||
|             let act = blog.outbox_collection_page(conn, (33, 36))?; | ||||
| 
 | ||||
|             let expected = json!({ | ||||
|                 "next": "https://plu.me/~/BlogName/outbox?page=3", | ||||
|                 "prev": "https://plu.me/~/BlogName/outbox?page=1", | ||||
|                 "items": [], | ||||
|                 "type": "OrderedCollectionPage" | ||||
|             }); | ||||
| 
 | ||||
|             assert_json_eq!(to_value(act)?, expected); | ||||
| 
 | ||||
|             Ok(()) | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -11,18 +11,23 @@ use crate::{ | ||||
|     users::User, | ||||
|     Connection, Error, Result, CONFIG, | ||||
| }; | ||||
| use activitypub::{ | ||||
| use activitystreams::{ | ||||
|     activity::{Create, Delete}, | ||||
|     link, | ||||
|     base::{AnyBase, Base}, | ||||
|     iri_string::types::IriString, | ||||
|     link::{self, kind::MentionType}, | ||||
|     object::{Note, Tombstone}, | ||||
|     prelude::*, | ||||
|     primitives::OneOrMany, | ||||
|     time::OffsetDateTime, | ||||
| }; | ||||
| use chrono::{self, NaiveDateTime, TimeZone, Utc}; | ||||
| use chrono::{self, NaiveDateTime}; | ||||
| use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; | ||||
| use plume_common::{ | ||||
|     activity_pub::{ | ||||
|         inbox::{AsActor, AsObject, FromId}, | ||||
|         sign::Signer, | ||||
|         Id, IntoId, PUBLIC_VISIBILITY, | ||||
|         IntoId, ToAsString, ToAsUri, PUBLIC_VISIBILITY, | ||||
|     }, | ||||
|     utils, | ||||
| }; | ||||
| @ -115,29 +120,32 @@ impl Comment { | ||||
|             Some(Media::get_media_processor(conn, vec![&author])), | ||||
|         ); | ||||
| 
 | ||||
|         let mut note = Note::default(); | ||||
|         let to = vec![Id::new(PUBLIC_VISIBILITY.to_string())]; | ||||
|         let mut note = Note::new(); | ||||
|         let to = vec![PUBLIC_VISIBILITY.parse::<IriString>()?]; | ||||
| 
 | ||||
|         note.object_props | ||||
|             .set_id_string(self.ap_url.clone().unwrap_or_default())?; | ||||
|         note.object_props | ||||
|             .set_summary_string(self.spoiler_text.clone())?; | ||||
|         note.object_props.set_content_string(html)?; | ||||
|         note.object_props | ||||
|             .set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else( | ||||
|                 || Ok(Post::get(conn, self.post_id)?.ap_url), | ||||
|                 |id| Ok(Comment::get(conn, id)?.ap_url.unwrap_or_default()) as Result<String>, | ||||
|             )?))?; | ||||
|         note.object_props | ||||
|             .set_published_utctime(Utc.from_utc_datetime(&self.creation_date))?; | ||||
|         note.object_props.set_attributed_to_link(author.into_id())?; | ||||
|         note.object_props.set_to_link_vec(to)?; | ||||
|         note.object_props.set_tag_link_vec( | ||||
|             mentions | ||||
|                 .into_iter() | ||||
|                 .filter_map(|m| Mention::build_activity(conn, &m).ok()) | ||||
|                 .collect::<Vec<link::Mention>>(), | ||||
|         )?; | ||||
|         note.set_id( | ||||
|             self.ap_url | ||||
|                 .clone() | ||||
|                 .unwrap_or_default() | ||||
|                 .parse::<IriString>()?, | ||||
|         ); | ||||
|         note.set_summary(self.spoiler_text.clone()); | ||||
|         note.set_content(html); | ||||
|         note.set_in_reply_to(self.in_response_to_id.map_or_else( | ||||
|             || Post::get(conn, self.post_id).map(|post| post.ap_url), | ||||
|             |id| Comment::get(conn, id).map(|comment| comment.ap_url.unwrap_or_default()), | ||||
|         )?); | ||||
|         note.set_published( | ||||
|             OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos().into()) | ||||
|                 .expect("OffsetDateTime"), | ||||
|         ); | ||||
|         note.set_attributed_to(author.into_id().parse::<IriString>()?); | ||||
|         note.set_many_tos(to); | ||||
|         note.set_many_tags(mentions.into_iter().filter_map(|m| { | ||||
|             Mention::build_activity(conn, &m) | ||||
|                 .map(|mention| mention.into_any_base().expect("Can convert")) | ||||
|                 .ok() | ||||
|         })); | ||||
|         Ok(note) | ||||
|     } | ||||
| 
 | ||||
| @ -145,17 +153,26 @@ impl Comment { | ||||
|         let author = User::get(conn, self.author_id)?; | ||||
| 
 | ||||
|         let note = self.to_activity(conn)?; | ||||
|         let mut act = Create::default(); | ||||
|         act.create_props.set_actor_link(author.into_id())?; | ||||
|         act.create_props.set_object_object(note.clone())?; | ||||
|         act.object_props.set_id_string(format!( | ||||
|             "{}/activity", | ||||
|             self.ap_url.clone().ok_or(Error::MissingApProperty)?, | ||||
|         ))?; | ||||
|         act.object_props | ||||
|             .set_to_link_vec(note.object_props.to_link_vec::<Id>()?)?; | ||||
|         act.object_props | ||||
|             .set_cc_link_vec(vec![Id::new(self.get_author(conn)?.followers_endpoint)])?; | ||||
|         let note_clone = note.clone(); | ||||
| 
 | ||||
|         let mut act = Create::new( | ||||
|             author.into_id().parse::<IriString>()?, | ||||
|             Base::retract(note)?.into_generic()?, | ||||
|         ); | ||||
|         act.set_id( | ||||
|             format!( | ||||
|                 "{}/activity", | ||||
|                 self.ap_url.clone().ok_or(Error::MissingApProperty)?, | ||||
|             ) | ||||
|             .parse::<IriString>()?, | ||||
|         ); | ||||
|         act.set_many_tos( | ||||
|             note_clone | ||||
|                 .to() | ||||
|                 .iter() | ||||
|                 .flat_map(|tos| tos.iter().map(|to| to.to_owned())), | ||||
|         ); | ||||
|         act.set_many_ccs(vec![self.get_author(conn)?.followers_endpoint]); | ||||
|         Ok(act) | ||||
|     } | ||||
| 
 | ||||
| @ -180,20 +197,21 @@ impl Comment { | ||||
|     } | ||||
| 
 | ||||
|     pub fn build_delete(&self, conn: &Connection) -> Result<Delete> { | ||||
|         let mut act = Delete::default(); | ||||
|         act.delete_props | ||||
|             .set_actor_link(self.get_author(conn)?.into_id())?; | ||||
|         let mut tombstone = Tombstone::new(); | ||||
|         tombstone.set_id( | ||||
|             self.ap_url | ||||
|                 .as_ref() | ||||
|                 .ok_or(Error::MissingApProperty)? | ||||
|                 .parse::<IriString>()?, | ||||
|         ); | ||||
| 
 | ||||
|         let mut tombstone = Tombstone::default(); | ||||
|         tombstone | ||||
|             .object_props | ||||
|             .set_id_string(self.ap_url.clone().ok_or(Error::MissingApProperty)?)?; | ||||
|         act.delete_props.set_object_object(tombstone)?; | ||||
|         let mut act = Delete::new( | ||||
|             self.get_author(conn)?.into_id().parse::<IriString>()?, | ||||
|             Base::retract(tombstone)?.into_generic()?, | ||||
|         ); | ||||
| 
 | ||||
|         act.object_props | ||||
|             .set_id_string(format!("{}#delete", self.ap_url.clone().unwrap()))?; | ||||
|         act.object_props | ||||
|             .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?; | ||||
|         act.set_id(format!("{}#delete", self.ap_url.clone().unwrap()).parse::<IriString>()?); | ||||
|         act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]); | ||||
| 
 | ||||
|         Ok(act) | ||||
|     } | ||||
| @ -210,102 +228,104 @@ impl FromId<DbConn> for Comment { | ||||
|     fn from_activity(conn: &DbConn, note: Note) -> Result<Self> { | ||||
|         let comm = { | ||||
|             let previous_url = note | ||||
|                 .object_props | ||||
|                 .in_reply_to | ||||
|                 .as_ref() | ||||
|                 .in_reply_to() | ||||
|                 .ok_or(Error::MissingApProperty)? | ||||
|                 .as_str() | ||||
|                 .iter() | ||||
|                 .next() | ||||
|                 .ok_or(Error::MissingApProperty)? | ||||
|                 .id() | ||||
|                 .ok_or(Error::MissingApProperty)?; | ||||
|             let previous_comment = Comment::find_by_ap_url(conn, previous_url); | ||||
|             let previous_comment = Comment::find_by_ap_url(conn, previous_url.as_str()); | ||||
| 
 | ||||
|             let is_public = |v: &Option<serde_json::Value>| match v | ||||
|                 .as_ref() | ||||
|                 .unwrap_or(&serde_json::Value::Null) | ||||
|             { | ||||
|                 serde_json::Value::Array(v) => v | ||||
|                     .iter() | ||||
|                     .filter_map(serde_json::Value::as_str) | ||||
|                     .any(|s| s == PUBLIC_VISIBILITY), | ||||
|                 serde_json::Value::String(s) => s == PUBLIC_VISIBILITY, | ||||
|                 _ => false, | ||||
|             let is_public = |v: &Option<&OneOrMany<AnyBase>>| match v { | ||||
|                 Some(one_or_many) => one_or_many.iter().any(|any_base| { | ||||
|                     let id = any_base.id(); | ||||
|                     id.is_some() && id.unwrap() == PUBLIC_VISIBILITY | ||||
|                 }), | ||||
|                 None => false, | ||||
|             }; | ||||
| 
 | ||||
|             let public_visibility = is_public(¬e.object_props.to) | ||||
|                 || is_public(¬e.object_props.bto) | ||||
|                 || is_public(¬e.object_props.cc) | ||||
|                 || is_public(¬e.object_props.bcc); | ||||
|             let public_visibility = is_public(¬e.to()) | ||||
|                 || is_public(¬e.bto()) | ||||
|                 || is_public(¬e.cc()) | ||||
|                 || is_public(¬e.bcc()); | ||||
| 
 | ||||
|             let summary = note.summary().and_then(|summary| summary.to_as_string()); | ||||
|             let sensitive = summary.is_some(); | ||||
|             let comm = Comment::insert( | ||||
|                 conn, | ||||
|                 NewComment { | ||||
|                     content: SafeString::new(¬e.object_props.content_string()?), | ||||
|                     spoiler_text: note.object_props.summary_string().unwrap_or_default(), | ||||
|                     ap_url: note.object_props.id_string().ok(), | ||||
|                     content: SafeString::new( | ||||
|                         ¬e | ||||
|                             .content() | ||||
|                             .ok_or(Error::MissingApProperty)? | ||||
|                             .to_as_string() | ||||
|                             .ok_or(Error::InvalidValue)?, | ||||
|                     ), | ||||
|                     spoiler_text: summary.unwrap_or_default(), | ||||
|                     ap_url: Some( | ||||
|                         note.id_unchecked() | ||||
|                             .ok_or(Error::MissingApProperty)? | ||||
|                             .to_string(), | ||||
|                     ), | ||||
|                     in_response_to_id: previous_comment.iter().map(|c| c.id).next(), | ||||
|                     post_id: previous_comment.map(|c| c.post_id).or_else(|_| { | ||||
|                         Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result<i32> | ||||
|                         Ok(Post::find_by_ap_url(conn, previous_url.as_str())?.id) as Result<i32> | ||||
|                     })?, | ||||
|                     author_id: User::from_id( | ||||
|                         conn, | ||||
|                         ¬e.object_props.attributed_to_link::<Id>()?, | ||||
|                         ¬e | ||||
|                             .attributed_to() | ||||
|                             .ok_or(Error::MissingApProperty)? | ||||
|                             .to_as_uri() | ||||
|                             .ok_or(Error::MissingApProperty)?, | ||||
|                         None, | ||||
|                         CONFIG.proxy(), | ||||
|                     ) | ||||
|                     .map_err(|(_, e)| e)? | ||||
|                     .id, | ||||
|                     sensitive: note.object_props.summary_string().is_ok(), | ||||
|                     sensitive, | ||||
|                     public_visibility, | ||||
|                 }, | ||||
|             )?; | ||||
| 
 | ||||
|             // save mentions
 | ||||
|             if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() { | ||||
|                 for tag in tags { | ||||
|                     serde_json::from_value::<link::Mention>(tag) | ||||
|                         .map_err(Error::from) | ||||
|                         .and_then(|m| { | ||||
|                             let author = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0]; | ||||
|                             let not_author = m.link_props.href_string()? != author.ap_url.clone(); | ||||
|                             Mention::from_activity(conn, &m, comm.id, false, not_author) | ||||
|                         }) | ||||
|                         .ok(); | ||||
|             if let Some(tags) = note.tag() { | ||||
|                 let author_url = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0].ap_url; | ||||
|                 for tag in tags.iter() { | ||||
|                     let m = tag.clone().extend::<link::Mention, MentionType>()?; // FIXME: Don't clone
 | ||||
|                     if m.is_none() { | ||||
|                         continue; | ||||
|                     } | ||||
|                     let m = m.unwrap(); | ||||
|                     let not_author = m.href().ok_or(Error::MissingApProperty)? != author_url; | ||||
|                     let _ = Mention::from_activity(conn, &m, comm.id, false, not_author); | ||||
|                 } | ||||
|             } | ||||
|             comm | ||||
|         }; | ||||
| 
 | ||||
|         if !comm.public_visibility { | ||||
|             let receivers_ap_url = |v: Option<serde_json::Value>| { | ||||
|                 let filter = |e: serde_json::Value| { | ||||
|                     if let serde_json::Value::String(s) = e { | ||||
|                         Some(s) | ||||
|                     } else { | ||||
|                         None | ||||
|             let mut receiver_ids = HashSet::new(); | ||||
|             let mut receivers_id = |v: Option<&'_ OneOrMany<AnyBase>>| { | ||||
|                 if let Some(one_or_many) = v { | ||||
|                     for any_base in one_or_many.iter() { | ||||
|                         if let Some(id) = any_base.id() { | ||||
|                             receiver_ids.insert(id.to_string()); | ||||
|                         } | ||||
|                     } | ||||
|                 }; | ||||
|                 match v.unwrap_or(serde_json::Value::Null) { | ||||
|                     serde_json::Value::Array(v) => v, | ||||
|                     v => vec![v], | ||||
|                 } | ||||
|                 .into_iter() | ||||
|                 .filter_map(filter) | ||||
|             }; | ||||
| 
 | ||||
|             let mut note = note; | ||||
|             receivers_id(note.to()); | ||||
|             receivers_id(note.cc()); | ||||
|             receivers_id(note.bto()); | ||||
|             receivers_id(note.bcc()); | ||||
| 
 | ||||
|             let to = receivers_ap_url(note.object_props.to.take()); | ||||
|             let cc = receivers_ap_url(note.object_props.cc.take()); | ||||
|             let bto = receivers_ap_url(note.object_props.bto.take()); | ||||
|             let bcc = receivers_ap_url(note.object_props.bcc.take()); | ||||
| 
 | ||||
|             let receivers_ap_url = to | ||||
|                 .chain(cc) | ||||
|                 .chain(bto) | ||||
|                 .chain(bcc) | ||||
|                 .collect::<HashSet<_>>() // remove duplicates (don't do a query more than once)
 | ||||
|             let receivers_ap_url = receiver_ids | ||||
|                 .into_iter() | ||||
|                 .flat_map(|v| { | ||||
|                     if let Ok(user) = User::from_id(conn, &v, None, CONFIG.proxy()) { | ||||
|                     if let Ok(user) = User::from_id(conn, v.as_ref(), None, CONFIG.proxy()) { | ||||
|                         vec![user] | ||||
|                     } else { | ||||
|                         vec![] // TODO try to fetch collection
 | ||||
|  | ||||
| @ -2,7 +2,12 @@ use crate::{ | ||||
|     ap_url, db_conn::DbConn, instance::Instance, notifications::*, schema::follows, users::User, | ||||
|     Connection, Error, Result, CONFIG, | ||||
| }; | ||||
| use activitypub::activity::{Accept, Follow as FollowAct, Undo}; | ||||
| use activitystreams::{ | ||||
|     activity::{Accept, ActorAndObjectRef, Follow as FollowAct, Undo}, | ||||
|     base::AnyBase, | ||||
|     iri_string::types::IriString, | ||||
|     prelude::*, | ||||
| }; | ||||
| use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; | ||||
| use plume_common::activity_pub::{ | ||||
|     broadcast, | ||||
| @ -53,15 +58,13 @@ impl Follow { | ||||
|     pub fn to_activity(&self, conn: &Connection) -> Result<FollowAct> { | ||||
|         let user = User::get(conn, self.follower_id)?; | ||||
|         let target = User::get(conn, self.following_id)?; | ||||
|         let target_id = target.ap_url.parse::<IriString>()?; | ||||
| 
 | ||||
|         let mut act = FollowAct::new(user.ap_url.parse::<IriString>()?, target_id.clone()); | ||||
|         act.set_id(self.ap_url.parse::<IriString>()?); | ||||
|         act.set_many_tos(vec![target_id]); | ||||
|         act.set_many_ccs(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]); | ||||
| 
 | ||||
|         let mut act = FollowAct::default(); | ||||
|         act.follow_props.set_actor_link::<Id>(user.into_id())?; | ||||
|         act.follow_props | ||||
|             .set_object_link::<Id>(target.clone().into_id())?; | ||||
|         act.object_props.set_id_string(self.ap_url.clone())?; | ||||
|         act.object_props.set_to_link_vec(vec![target.into_id()])?; | ||||
|         act.object_props | ||||
|             .set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?; | ||||
|         Ok(act) | ||||
|     } | ||||
| 
 | ||||
| @ -94,7 +97,11 @@ impl Follow { | ||||
|             NewFollow { | ||||
|                 follower_id: from_id, | ||||
|                 following_id: target_id, | ||||
|                 ap_url: follow.object_props.id_string()?, | ||||
|                 ap_url: follow | ||||
|                     .object_field_ref() | ||||
|                     .as_single_id() | ||||
|                     .ok_or(Error::MissingApProperty)? | ||||
|                     .to_string(), | ||||
|             }, | ||||
|         )?; | ||||
|         res.notify(conn)?; | ||||
| @ -115,39 +122,35 @@ impl Follow { | ||||
|         target: &A, | ||||
|         follow: FollowAct, | ||||
|     ) -> Result<Accept> { | ||||
|         let mut accept = Accept::default(); | ||||
|         let mut accept = Accept::new( | ||||
|             target.clone().into_id().parse::<IriString>()?, | ||||
|             AnyBase::from_extended(follow)?, | ||||
|         ); | ||||
|         let accept_id = ap_url(&format!( | ||||
|             "{}/follows/{}/accept", | ||||
|             CONFIG.base_url.as_str(), | ||||
|             self.id | ||||
|         )); | ||||
|         accept.object_props.set_id_string(accept_id)?; | ||||
|         accept | ||||
|             .object_props | ||||
|             .set_to_link_vec(vec![from.clone().into_id()])?; | ||||
|         accept | ||||
|             .object_props | ||||
|             .set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?; | ||||
|         accept | ||||
|             .accept_props | ||||
|             .set_actor_link::<Id>(target.clone().into_id())?; | ||||
|         accept.accept_props.set_object_object(follow)?; | ||||
|         accept.set_id(accept_id.parse::<IriString>()?); | ||||
|         accept.set_many_tos(vec![from.clone().into_id().parse::<IriString>()?]); | ||||
|         accept.set_many_ccs(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]); | ||||
| 
 | ||||
|         Ok(accept) | ||||
|     } | ||||
| 
 | ||||
|     pub fn build_undo(&self, conn: &Connection) -> Result<Undo> { | ||||
|         let mut undo = Undo::default(); | ||||
|         undo.undo_props | ||||
|             .set_actor_link(User::get(conn, self.follower_id)?.into_id())?; | ||||
|         undo.object_props | ||||
|             .set_id_string(format!("{}/undo", self.ap_url))?; | ||||
|         undo.undo_props | ||||
|             .set_object_link::<Id>(self.clone().into_id())?; | ||||
|         undo.object_props | ||||
|             .set_to_link_vec(vec![User::get(conn, self.following_id)?.into_id()])?; | ||||
|         undo.object_props | ||||
|             .set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?; | ||||
|         let mut undo = Undo::new( | ||||
|             User::get(conn, self.follower_id)? | ||||
|                 .ap_url | ||||
|                 .parse::<IriString>()?, | ||||
|             self.ap_url.parse::<IriString>()?, | ||||
|         ); | ||||
|         undo.set_id(format!("{}/undo", self.ap_url).parse::<IriString>()?); | ||||
|         undo.set_many_tos(vec![User::get(conn, self.following_id)? | ||||
|             .ap_url | ||||
|             .parse::<IriString>()?]); | ||||
|         undo.set_many_ccs(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]); | ||||
| 
 | ||||
|         Ok(undo) | ||||
|     } | ||||
| } | ||||
| @ -159,11 +162,7 @@ impl AsObject<User, FollowAct, &DbConn> for User { | ||||
|     fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Follow> { | ||||
|         // Mastodon (at least) requires the full Follow object when accepting it,
 | ||||
|         // so we rebuilt it here
 | ||||
|         let mut follow = FollowAct::default(); | ||||
|         follow.object_props.set_id_string(id.to_string())?; | ||||
|         follow | ||||
|             .follow_props | ||||
|             .set_actor_link::<Id>(actor.clone().into_id())?; | ||||
|         let follow = FollowAct::new(actor.ap_url.parse::<IriString>()?, id.parse::<IriString>()?); | ||||
|         Follow::accept_follow(conn, &actor, &self, follow, actor.id, self.id) | ||||
|     } | ||||
| } | ||||
| @ -179,7 +178,11 @@ impl FromId<DbConn> for Follow { | ||||
|     fn from_activity(conn: &DbConn, follow: FollowAct) -> Result<Self> { | ||||
|         let actor = User::from_id( | ||||
|             conn, | ||||
|             &follow.follow_props.actor_link::<Id>()?, | ||||
|             follow | ||||
|                 .actor_field_ref() | ||||
|                 .as_single_id() | ||||
|                 .ok_or(Error::MissingApProperty)? | ||||
|                 .as_str(), | ||||
|             None, | ||||
|             CONFIG.proxy(), | ||||
|         ) | ||||
| @ -187,7 +190,11 @@ impl FromId<DbConn> for Follow { | ||||
| 
 | ||||
|         let target = User::from_id( | ||||
|             conn, | ||||
|             &follow.follow_props.object_link::<Id>()?, | ||||
|             follow | ||||
|                 .object_field_ref() | ||||
|                 .as_single_id() | ||||
|                 .ok_or(Error::MissingApProperty)? | ||||
|                 .as_str(), | ||||
|             None, | ||||
|             CONFIG.proxy(), | ||||
|         ) | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| use activitypub::activity::*; | ||||
| use activitystreams::activity::{Announce, Create, Delete, Follow, Like, Undo, Update}; | ||||
| 
 | ||||
| use crate::{ | ||||
|     comments::Comment, | ||||
| @ -94,8 +94,8 @@ pub(crate) mod tests { | ||||
|                 license: "WTFPL".to_owned(), | ||||
|                 creation_date: None, | ||||
|                 ap_url: format!("https://plu.me/~/{}/testing", blogs[0].actor_id), | ||||
|                 subtitle: String::new(), | ||||
|                 source: String::new(), | ||||
|                 subtitle: "Bye".to_string(), | ||||
|                 source: "Hello".to_string(), | ||||
|                 cover_id: None, | ||||
|             }, | ||||
|         ) | ||||
| @ -268,7 +268,7 @@ pub(crate) mod tests { | ||||
|                 "actor": users[0].ap_url, | ||||
|                 "object": { | ||||
|                     "type": "Article", | ||||
|                     "id": "https://plu.me/~/Blog/my-article", | ||||
|                     "id": "https://plu.me/~/BlogName/testing", | ||||
|                     "attributedTo": [users[0].ap_url, blogs[0].ap_url], | ||||
|                     "content": "Hello.", | ||||
|                     "name": "My Article", | ||||
| @ -286,11 +286,11 @@ pub(crate) mod tests { | ||||
|             match super::inbox(&conn, act).unwrap() { | ||||
|                 super::InboxResult::Post(p) => { | ||||
|                     assert!(p.is_author(&conn, users[0].id).unwrap()); | ||||
|                     assert_eq!(p.source, "Hello.".to_owned()); | ||||
|                     assert_eq!(p.source, "Hello".to_owned()); | ||||
|                     assert_eq!(p.blog_id, blogs[0].id); | ||||
|                     assert_eq!(p.content, SafeString::new("Hello.")); | ||||
|                     assert_eq!(p.subtitle, "Bye.".to_owned()); | ||||
|                     assert_eq!(p.title, "My Article".to_owned()); | ||||
|                     assert_eq!(p.content, SafeString::new("Hello")); | ||||
|                     assert_eq!(p.subtitle, "Bye".to_owned()); | ||||
|                     assert_eq!(p.title, "Testing".to_owned()); | ||||
|                 } | ||||
|                 _ => panic!("Unexpected result"), | ||||
|             }; | ||||
|  | ||||
| @ -16,6 +16,7 @@ extern crate serde_json; | ||||
| #[macro_use] | ||||
| extern crate tantivy; | ||||
| 
 | ||||
| use activitystreams::iri_string; | ||||
| pub use lettre; | ||||
| pub use lettre::smtp; | ||||
| use once_cell::sync::Lazy; | ||||
| @ -100,6 +101,12 @@ impl From<url::ParseError> for Error { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<iri_string::validate::Error> for Error { | ||||
|     fn from(_: iri_string::validate::Error) -> Self { | ||||
|         Error::Url | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<serde_json::Error> for Error { | ||||
|     fn from(_: serde_json::Error) -> Self { | ||||
|         Error::SerDe | ||||
| @ -118,12 +125,9 @@ impl From<reqwest::header::InvalidHeaderValue> for Error { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<activitypub::Error> for Error { | ||||
|     fn from(err: activitypub::Error) -> Self { | ||||
|         match err { | ||||
|             activitypub::Error::NotFound => Error::MissingApProperty, | ||||
|             _ => Error::SerDe, | ||||
|         } | ||||
| impl From<activitystreams::checked::CheckError> for Error { | ||||
|     fn from(_: activitystreams::checked::CheckError) -> Error { | ||||
|         Error::MissingApProperty | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -2,13 +2,18 @@ use crate::{ | ||||
|     db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*, | ||||
|     users::User, Connection, Error, Result, CONFIG, | ||||
| }; | ||||
| use activitypub::activity; | ||||
| use activitystreams::{ | ||||
|     activity::{ActorAndObjectRef, Like as LikeAct, Undo}, | ||||
|     base::AnyBase, | ||||
|     iri_string::types::IriString, | ||||
|     prelude::*, | ||||
| }; | ||||
| use chrono::NaiveDateTime; | ||||
| use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; | ||||
| use plume_common::activity_pub::{ | ||||
|     inbox::{AsActor, AsObject, FromId}, | ||||
|     sign::Signer, | ||||
|     Id, IntoId, PUBLIC_VISIBILITY, | ||||
|     PUBLIC_VISIBILITY, | ||||
| }; | ||||
| 
 | ||||
| #[derive(Clone, Queryable, Identifiable)] | ||||
| @ -34,18 +39,16 @@ impl Like { | ||||
|     find_by!(likes, find_by_ap_url, ap_url as &str); | ||||
|     find_by!(likes, find_by_user_on_post, user_id as i32, post_id as i32); | ||||
| 
 | ||||
|     pub fn to_activity(&self, conn: &Connection) -> Result<activity::Like> { | ||||
|         let mut act = activity::Like::default(); | ||||
|         act.like_props | ||||
|             .set_actor_link(User::get(conn, self.user_id)?.into_id())?; | ||||
|         act.like_props | ||||
|             .set_object_link(Post::get(conn, self.post_id)?.into_id())?; | ||||
|         act.object_props | ||||
|             .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?; | ||||
|         act.object_props.set_cc_link_vec(vec![Id::new( | ||||
|             User::get(conn, self.user_id)?.followers_endpoint, | ||||
|         )])?; | ||||
|         act.object_props.set_id_string(self.ap_url.clone())?; | ||||
|     pub fn to_activity(&self, conn: &Connection) -> Result<LikeAct> { | ||||
|         let mut act = LikeAct::new( | ||||
|             User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?, | ||||
|             Post::get(conn, self.post_id)?.ap_url.parse::<IriString>()?, | ||||
|         ); | ||||
|         act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]); | ||||
|         act.set_many_ccs(vec![User::get(conn, self.user_id)? | ||||
|             .followers_endpoint | ||||
|             .parse::<IriString>()?]); | ||||
|         act.set_id(self.ap_url.parse::<IriString>()?); | ||||
| 
 | ||||
|         Ok(act) | ||||
|     } | ||||
| @ -67,24 +70,22 @@ impl Like { | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     pub fn build_undo(&self, conn: &Connection) -> Result<activity::Undo> { | ||||
|         let mut act = activity::Undo::default(); | ||||
|         act.undo_props | ||||
|             .set_actor_link(User::get(conn, self.user_id)?.into_id())?; | ||||
|         act.undo_props.set_object_object(self.to_activity(conn)?)?; | ||||
|         act.object_props | ||||
|             .set_id_string(format!("{}#delete", self.ap_url))?; | ||||
|         act.object_props | ||||
|             .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?; | ||||
|         act.object_props.set_cc_link_vec(vec![Id::new( | ||||
|             User::get(conn, self.user_id)?.followers_endpoint, | ||||
|         )])?; | ||||
|     pub fn build_undo(&self, conn: &Connection) -> Result<Undo> { | ||||
|         let mut act = Undo::new( | ||||
|             User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?, | ||||
|             AnyBase::from_extended(self.to_activity(conn)?)?, | ||||
|         ); | ||||
|         act.set_id(format!("{}#delete", self.ap_url).parse::<IriString>()?); | ||||
|         act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]); | ||||
|         act.set_many_ccs(vec![User::get(conn, self.user_id)? | ||||
|             .followers_endpoint | ||||
|             .parse::<IriString>()?]); | ||||
| 
 | ||||
|         Ok(act) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl AsObject<User, activity::Like, &DbConn> for Post { | ||||
| impl AsObject<User, LikeAct, &DbConn> for Post { | ||||
|     type Error = Error; | ||||
|     type Output = Like; | ||||
| 
 | ||||
| @ -106,19 +107,22 @@ impl AsObject<User, activity::Like, &DbConn> for Post { | ||||
| 
 | ||||
| impl FromId<DbConn> for Like { | ||||
|     type Error = Error; | ||||
|     type Object = activity::Like; | ||||
|     type Object = LikeAct; | ||||
| 
 | ||||
|     fn from_db(conn: &DbConn, id: &str) -> Result<Self> { | ||||
|         Like::find_by_ap_url(conn, id) | ||||
|     } | ||||
| 
 | ||||
|     fn from_activity(conn: &DbConn, act: activity::Like) -> Result<Self> { | ||||
|     fn from_activity(conn: &DbConn, act: LikeAct) -> Result<Self> { | ||||
|         let res = Like::insert( | ||||
|             conn, | ||||
|             NewLike { | ||||
|                 post_id: Post::from_id( | ||||
|                     conn, | ||||
|                     &act.like_props.object_link::<Id>()?, | ||||
|                     act.object_field_ref() | ||||
|                         .as_single_id() | ||||
|                         .ok_or(Error::MissingApProperty)? | ||||
|                         .as_str(), | ||||
|                     None, | ||||
|                     CONFIG.proxy(), | ||||
|                 ) | ||||
| @ -126,13 +130,19 @@ impl FromId<DbConn> for Like { | ||||
|                 .id, | ||||
|                 user_id: User::from_id( | ||||
|                     conn, | ||||
|                     &act.like_props.actor_link::<Id>()?, | ||||
|                     act.actor_field_ref() | ||||
|                         .as_single_id() | ||||
|                         .ok_or(Error::MissingApProperty)? | ||||
|                         .as_str(), | ||||
|                     None, | ||||
|                     CONFIG.proxy(), | ||||
|                 ) | ||||
|                 .map_err(|(_, e)| e)? | ||||
|                 .id, | ||||
|                 ap_url: act.object_props.id_string()?, | ||||
|                 ap_url: act | ||||
|                     .id_unchecked() | ||||
|                     .ok_or(Error::MissingApProperty)? | ||||
|                     .to_string(), | ||||
|             }, | ||||
|         )?; | ||||
|         res.notify(conn)?; | ||||
| @ -144,7 +154,7 @@ impl FromId<DbConn> for Like { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl AsObject<User, activity::Undo, &DbConn> for Like { | ||||
| impl AsObject<User, Undo, &DbConn> for Like { | ||||
|     type Error = Error; | ||||
|     type Output = (); | ||||
| 
 | ||||
|  | ||||
| @ -2,11 +2,11 @@ use crate::{ | ||||
|     ap_url, db_conn::DbConn, instance::Instance, safe_string::SafeString, schema::medias, | ||||
|     users::User, Connection, Error, Result, CONFIG, | ||||
| }; | ||||
| use activitypub::object::Image; | ||||
| use activitystreams::{object::Image, prelude::*}; | ||||
| use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; | ||||
| use guid_create::GUID; | ||||
| use plume_common::{ | ||||
|     activity_pub::{inbox::FromId, request, Id}, | ||||
|     activity_pub::{inbox::FromId, request, ToAsString, ToAsUri}, | ||||
|     utils::{escape, MediaProcessor}, | ||||
| }; | ||||
| use std::{ | ||||
| @ -208,9 +208,9 @@ impl Media { | ||||
|     // TODO: merge with save_remote?
 | ||||
|     pub fn from_activity(conn: &DbConn, image: &Image) -> Result<Media> { | ||||
|         let remote_url = image | ||||
|             .object_props | ||||
|             .url_string() | ||||
|             .or(Err(Error::MissingApProperty))?; | ||||
|             .url() | ||||
|             .and_then(|url| url.to_as_uri()) | ||||
|             .ok_or(Error::MissingApProperty)?; | ||||
|         let path = determine_mirror_file_path(&remote_url); | ||||
|         let parent = path.parent().ok_or(Error::InvalidValue)?; | ||||
|         if !parent.is_dir() { | ||||
| @ -231,11 +231,12 @@ impl Media { | ||||
|                 let mut updated = false; | ||||
| 
 | ||||
|                 let alt_text = image | ||||
|                     .object_props | ||||
|                     .content_string() | ||||
|                     .or(Err(Error::NotFound))?; | ||||
|                 let sensitive = image.object_props.summary_string().is_ok(); | ||||
|                 let content_warning = image.object_props.summary_string().ok(); | ||||
|                     .content() | ||||
|                     .and_then(|content| content.to_as_string()) | ||||
|                     .ok_or(Error::NotFound)?; | ||||
|                 let summary = image.summary().and_then(|summary| summary.to_as_string()); | ||||
|                 let sensitive = summary.is_some(); | ||||
|                 let content_warning = summary; | ||||
|                 if media.alt_text != alt_text { | ||||
|                     media.alt_text = alt_text; | ||||
|                     updated = true; | ||||
| @ -262,28 +263,25 @@ impl Media { | ||||
|                 Ok(media) | ||||
|             }) | ||||
|             .or_else(|_| { | ||||
|                 let summary = image.summary().and_then(|summary| summary.to_as_string()); | ||||
|                 Media::insert( | ||||
|                     conn, | ||||
|                     NewMedia { | ||||
|                         file_path: path.to_str().ok_or(Error::InvalidValue)?.to_string(), | ||||
|                         alt_text: image | ||||
|                             .object_props | ||||
|                             .content_string() | ||||
|                             .or(Err(Error::NotFound))?, | ||||
|                             .content() | ||||
|                             .and_then(|content| content.to_as_string()) | ||||
|                             .ok_or(Error::NotFound)?, | ||||
|                         is_remote: false, | ||||
|                         remote_url: None, | ||||
|                         sensitive: image.object_props.summary_string().is_ok(), | ||||
|                         content_warning: image.object_props.summary_string().ok(), | ||||
|                         sensitive: summary.is_some(), | ||||
|                         content_warning: summary, | ||||
|                         owner_id: User::from_id( | ||||
|                             conn, | ||||
|                             image | ||||
|                                 .object_props | ||||
|                                 .attributed_to_link_vec::<Id>() | ||||
|                                 .or(Err(Error::NotFound))? | ||||
|                                 .into_iter() | ||||
|                                 .next() | ||||
|                                 .ok_or(Error::NotFound)? | ||||
|                                 .as_ref(), | ||||
|                             &image | ||||
|                                 .attributed_to() | ||||
|                                 .and_then(|attributed_to| attributed_to.to_as_uri()) | ||||
|                                 .ok_or(Error::MissingApProperty)?, | ||||
|                             None, | ||||
|                             CONFIG.proxy(), | ||||
|                         ) | ||||
|  | ||||
| @ -2,7 +2,11 @@ use crate::{ | ||||
|     comments::Comment, db_conn::DbConn, notifications::*, posts::Post, schema::mentions, | ||||
|     users::User, Connection, Error, Result, | ||||
| }; | ||||
| use activitypub::link; | ||||
| use activitystreams::{ | ||||
|     base::BaseExt, | ||||
|     iri_string::types::IriString, | ||||
|     link::{self, LinkExt}, | ||||
| }; | ||||
| use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; | ||||
| use plume_common::activity_pub::inbox::AsActor; | ||||
| 
 | ||||
| @ -58,19 +62,17 @@ impl Mention { | ||||
| 
 | ||||
|     pub fn build_activity(conn: &DbConn, ment: &str) -> Result<link::Mention> { | ||||
|         let user = User::find_by_fqn(conn, ment)?; | ||||
|         let mut mention = link::Mention::default(); | ||||
|         mention.link_props.set_href_string(user.ap_url)?; | ||||
|         mention.link_props.set_name_string(format!("@{}", ment))?; | ||||
|         let mut mention = link::Mention::new(); | ||||
|         mention.set_href(user.ap_url.parse::<IriString>()?); | ||||
|         mention.set_name(format!("@{}", ment)); | ||||
|         Ok(mention) | ||||
|     } | ||||
| 
 | ||||
|     pub fn to_activity(&self, conn: &Connection) -> Result<link::Mention> { | ||||
|         let user = self.get_mentioned(conn)?; | ||||
|         let mut mention = link::Mention::default(); | ||||
|         mention.link_props.set_href_string(user.ap_url.clone())?; | ||||
|         mention | ||||
|             .link_props | ||||
|             .set_name_string(format!("@{}", user.fqn))?; | ||||
|         let mut mention = link::Mention::new(); | ||||
|         mention.set_href(user.ap_url.parse::<IriString>()?); | ||||
|         mention.set_name(format!("@{}", user.fqn)); | ||||
|         Ok(mention) | ||||
|     } | ||||
| 
 | ||||
| @ -81,8 +83,8 @@ impl Mention { | ||||
|         in_post: bool, | ||||
|         notify: bool, | ||||
|     ) -> Result<Self> { | ||||
|         let ap_url = ment.link_props.href_string().or(Err(Error::NotFound))?; | ||||
|         let mentioned = User::find_by_ap_url(conn, &ap_url)?; | ||||
|         let ap_url = ment.href().ok_or(Error::NotFound)?.as_str(); | ||||
|         let mentioned = User::find_by_ap_url(conn, ap_url)?; | ||||
| 
 | ||||
|         if in_post { | ||||
|             Post::get(conn, inside).and_then(|post| { | ||||
|  | ||||
| @ -3,20 +3,24 @@ use crate::{ | ||||
|     post_authors::*, safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User, | ||||
|     Connection, Error, PostEvent::*, Result, CONFIG, POST_CHAN, | ||||
| }; | ||||
| use activitypub::{ | ||||
| use activitystreams::{ | ||||
|     activity::{Create, Delete, Update}, | ||||
|     link, | ||||
|     object::{Article, Image, Tombstone}, | ||||
|     CustomObject, | ||||
|     base::{AnyBase, Base}, | ||||
|     iri_string::types::IriString, | ||||
|     link::{self, kind::MentionType}, | ||||
|     object::{kind::ImageType, ApObject, Article, AsApObject, Image, ObjectExt, Tombstone}, | ||||
|     prelude::*, | ||||
|     time::OffsetDateTime, | ||||
| }; | ||||
| use chrono::{NaiveDateTime, TimeZone, Utc}; | ||||
| use chrono::{NaiveDateTime, Utc}; | ||||
| use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl}; | ||||
| use once_cell::sync::Lazy; | ||||
| use plume_common::{ | ||||
|     activity_pub::{ | ||||
|         inbox::{AsActor, AsObject, FromId}, | ||||
|         sign::Signer, | ||||
|         Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILITY, | ||||
|         Hashtag, HashtagType, Id, IntoId, Licensed, LicensedArticle, ToAsString, ToAsUri, | ||||
|         PUBLIC_VISIBILITY, | ||||
|     }, | ||||
|     utils::{iri_percent_encode_seg, md_to_html}, | ||||
| }; | ||||
| @ -24,8 +28,6 @@ use riker::actors::{Publish, Tell}; | ||||
| use std::collections::{HashMap, HashSet}; | ||||
| use std::sync::{Arc, Mutex}; | ||||
| 
 | ||||
| pub type LicensedArticle = CustomObject<Licensed, Article>; | ||||
| 
 | ||||
| static BLOG_FQN_CACHE: Lazy<Mutex<HashMap<i32, String>>> = Lazy::new(|| Mutex::new(HashMap::new())); | ||||
| 
 | ||||
| #[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)] | ||||
| @ -353,92 +355,92 @@ impl Post { | ||||
|             .collect::<Vec<serde_json::Value>>(); | ||||
|         mentions_json.append(&mut tags_json); | ||||
| 
 | ||||
|         let mut article = Article::default(); | ||||
|         article.object_props.set_name_string(self.title.clone())?; | ||||
|         article.object_props.set_id_string(self.ap_url.clone())?; | ||||
|         let mut article = ApObject::new(Article::new()); | ||||
|         article.set_name(self.title.clone()); | ||||
|         article.set_id(self.ap_url.parse::<IriString>()?); | ||||
| 
 | ||||
|         let mut authors = self | ||||
|             .get_authors(conn)? | ||||
|             .into_iter() | ||||
|             .map(|x| Id::new(x.ap_url)) | ||||
|             .collect::<Vec<Id>>(); | ||||
|         authors.push(self.get_blog(conn)?.into_id()); // add the blog URL here too
 | ||||
|         article | ||||
|             .object_props | ||||
|             .set_attributed_to_link_vec::<Id>(authors)?; | ||||
|         article | ||||
|             .object_props | ||||
|             .set_content_string(self.content.get().clone())?; | ||||
|         article.ap_object_props.set_source_object(Source { | ||||
|             content: self.source.clone(), | ||||
|             media_type: String::from("text/markdown"), | ||||
|         })?; | ||||
|         article | ||||
|             .object_props | ||||
|             .set_published_utctime(Utc.from_utc_datetime(&self.creation_date))?; | ||||
|         article | ||||
|             .object_props | ||||
|             .set_summary_string(self.subtitle.clone())?; | ||||
|         article.object_props.tag = Some(json!(mentions_json)); | ||||
|             .filter_map(|x| x.ap_url.parse::<IriString>().ok()) | ||||
|             .collect::<Vec<IriString>>(); | ||||
|         authors.push(self.get_blog(conn)?.ap_url.parse::<IriString>()?); // add the blog URL here too
 | ||||
|         article.set_many_attributed_tos(authors); | ||||
|         article.set_content(self.content.get().clone()); | ||||
|         let source = AnyBase::from_arbitrary_json(serde_json::json!({ | ||||
|             "content": self.source, | ||||
|             "mediaType": "text/markdown", | ||||
|         }))?; | ||||
|         article.set_source(source); | ||||
|         article.set_published( | ||||
|             OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos().into()) | ||||
|                 .expect("OffsetDateTime"), | ||||
|         ); | ||||
|         article.set_summary(&*self.subtitle); | ||||
|         article.set_many_tags( | ||||
|             mentions_json | ||||
|                 .iter() | ||||
|                 .filter_map(|mention_json| AnyBase::from_arbitrary_json(mention_json).ok()), | ||||
|         ); | ||||
| 
 | ||||
|         if let Some(media_id) = self.cover_id { | ||||
|             let media = Media::get(conn, media_id)?; | ||||
|             let mut cover = Image::default(); | ||||
|             cover.object_props.set_url_string(media.url()?)?; | ||||
|             let mut cover = Image::new(); | ||||
|             cover.set_url(media.url()?); | ||||
|             if media.sensitive { | ||||
|                 cover | ||||
|                     .object_props | ||||
|                     .set_summary_string(media.content_warning.unwrap_or_default())?; | ||||
|                 cover.set_summary(media.content_warning.unwrap_or_default()); | ||||
|             } | ||||
|             cover.object_props.set_content_string(media.alt_text)?; | ||||
|             cover | ||||
|                 .object_props | ||||
|                 .set_attributed_to_link_vec(vec![User::get(conn, media.owner_id)?.into_id()])?; | ||||
|             article.object_props.set_icon_object(cover)?; | ||||
|             cover.set_content(media.alt_text); | ||||
|             cover.set_many_attributed_tos(vec![User::get(conn, media.owner_id)? | ||||
|                 .ap_url | ||||
|                 .parse::<IriString>()?]); | ||||
|             article.set_icon(cover.into_any_base()?); | ||||
|         } | ||||
| 
 | ||||
|         article.object_props.set_url_string(self.ap_url.clone())?; | ||||
|         article | ||||
|             .object_props | ||||
|             .set_to_link_vec::<Id>(to.into_iter().map(Id::new).collect())?; | ||||
|         article | ||||
|             .object_props | ||||
|             .set_cc_link_vec::<Id>(cc.into_iter().map(Id::new).collect())?; | ||||
|         let mut license = Licensed::default(); | ||||
|         license.set_license_string(self.license.clone())?; | ||||
|         article.set_url(self.ap_url.parse::<IriString>()?); | ||||
|         article.set_many_tos( | ||||
|             to.into_iter() | ||||
|                 .filter_map(|to| to.parse::<IriString>().ok()) | ||||
|                 .collect::<Vec<IriString>>(), | ||||
|         ); | ||||
|         article.set_many_ccs( | ||||
|             cc.into_iter() | ||||
|                 .filter_map(|cc| cc.parse::<IriString>().ok()) | ||||
|                 .collect::<Vec<IriString>>(), | ||||
|         ); | ||||
|         let license = Licensed { | ||||
|             license: Some(self.license.clone()), | ||||
|         }; | ||||
|         Ok(LicensedArticle::new(article, license)) | ||||
|     } | ||||
| 
 | ||||
|     pub fn create_activity(&self, conn: &Connection) -> Result<Create> { | ||||
|         let article = self.to_activity(conn)?; | ||||
|         let mut act = Create::default(); | ||||
|         act.object_props | ||||
|             .set_id_string(format!("{}/activity", self.ap_url))?; | ||||
|         act.object_props | ||||
|             .set_to_link_vec::<Id>(article.object.object_props.to_link_vec()?)?; | ||||
|         act.object_props | ||||
|             .set_cc_link_vec::<Id>(article.object.object_props.cc_link_vec()?)?; | ||||
|         act.create_props | ||||
|             .set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?; | ||||
|         act.create_props.set_object_object(article)?; | ||||
|         let to = article.to().ok_or(Error::MissingApProperty)?.clone(); | ||||
|         let cc = article.cc().ok_or(Error::MissingApProperty)?.clone(); | ||||
|         let mut act = Create::new( | ||||
|             self.get_authors(conn)?[0].ap_url.parse::<IriString>()?, | ||||
|             Base::retract(article)?.into_generic()?, | ||||
|         ); | ||||
|         act.set_id(format!("{}/activity", self.ap_url).parse::<IriString>()?); | ||||
|         act.set_many_tos(to); | ||||
|         act.set_many_ccs(cc); | ||||
|         Ok(act) | ||||
|     } | ||||
| 
 | ||||
|     pub fn update_activity(&self, conn: &Connection) -> Result<Update> { | ||||
|         let article = self.to_activity(conn)?; | ||||
|         let mut act = Update::default(); | ||||
|         act.object_props.set_id_string(format!( | ||||
|             "{}/update-{}", | ||||
|             self.ap_url, | ||||
|             Utc::now().timestamp() | ||||
|         ))?; | ||||
|         act.object_props | ||||
|             .set_to_link_vec::<Id>(article.object.object_props.to_link_vec()?)?; | ||||
|         act.object_props | ||||
|             .set_cc_link_vec::<Id>(article.object.object_props.cc_link_vec()?)?; | ||||
|         act.update_props | ||||
|             .set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?; | ||||
|         act.update_props.set_object_object(article)?; | ||||
|         let to = article.to().ok_or(Error::MissingApProperty)?.clone(); | ||||
|         let cc = article.cc().ok_or(Error::MissingApProperty)?.clone(); | ||||
|         let mut act = Update::new( | ||||
|             self.get_authors(conn)?[0].ap_url.parse::<IriString>()?, | ||||
|             Base::retract(article)?.into_generic()?, | ||||
|         ); | ||||
|         act.set_id( | ||||
|             format!("{}/update-{}", self.ap_url, Utc::now().timestamp()).parse::<IriString>()?, | ||||
|         ); | ||||
|         act.set_many_tos(to); | ||||
|         act.set_many_ccs(cc); | ||||
|         Ok(act) | ||||
|     } | ||||
| 
 | ||||
| @ -447,10 +449,8 @@ impl Post { | ||||
|             .into_iter() | ||||
|             .map(|m| { | ||||
|                 ( | ||||
|                     m.link_props | ||||
|                         .href_string() | ||||
|                         .ok() | ||||
|                         .and_then(|ap_url| User::find_by_ap_url(conn, &ap_url).ok()) | ||||
|                     m.href() | ||||
|                         .and_then(|ap_url| User::find_by_ap_url(conn, ap_url.as_ref()).ok()) | ||||
|                         .map(|u| u.id), | ||||
|                     m, | ||||
|                 ) | ||||
| @ -485,7 +485,7 @@ impl Post { | ||||
|     pub fn update_tags(&self, conn: &Connection, tags: Vec<Hashtag>) -> Result<()> { | ||||
|         let tags_name = tags | ||||
|             .iter() | ||||
|             .filter_map(|t| t.name_string().ok()) | ||||
|             .filter_map(|t| t.name.as_ref().map(|name| name.as_str().to_string())) | ||||
|             .collect::<HashSet<_>>(); | ||||
| 
 | ||||
|         let old_tags = Tag::for_post(&*conn, self.id)?; | ||||
| @ -502,8 +502,9 @@ impl Post { | ||||
| 
 | ||||
|         for t in tags { | ||||
|             if !t | ||||
|                 .name_string() | ||||
|                 .map(|n| old_tags_name.contains(&n)) | ||||
|                 .name | ||||
|                 .as_ref() | ||||
|                 .map(|n| old_tags_name.contains(n.as_str())) | ||||
|                 .unwrap_or(true) | ||||
|             { | ||||
|                 Tag::from_activity(conn, &t, self.id, false)?; | ||||
| @ -521,7 +522,7 @@ impl Post { | ||||
|     pub fn update_hashtags(&self, conn: &Connection, tags: Vec<Hashtag>) -> Result<()> { | ||||
|         let tags_name = tags | ||||
|             .iter() | ||||
|             .filter_map(|t| t.name_string().ok()) | ||||
|             .filter_map(|t| t.name.as_ref().map(|name| name.as_str().to_string())) | ||||
|             .collect::<HashSet<_>>(); | ||||
| 
 | ||||
|         let old_tags = Tag::for_post(&*conn, self.id)?; | ||||
| @ -538,8 +539,9 @@ impl Post { | ||||
| 
 | ||||
|         for t in tags { | ||||
|             if !t | ||||
|                 .name_string() | ||||
|                 .map(|n| old_tags_name.contains(&n)) | ||||
|                 .name | ||||
|                 .as_ref() | ||||
|                 .map(|n| old_tags_name.contains(n.as_str())) | ||||
|                 .unwrap_or(true) | ||||
|             { | ||||
|                 Tag::from_activity(conn, &t, self.id, true)?; | ||||
| @ -566,18 +568,19 @@ impl Post { | ||||
|     } | ||||
| 
 | ||||
|     pub fn build_delete(&self, conn: &Connection) -> Result<Delete> { | ||||
|         let mut act = Delete::default(); | ||||
|         act.delete_props | ||||
|             .set_actor_link(self.get_authors(conn)?[0].clone().into_id())?; | ||||
|         let mut tombstone = Tombstone::new(); | ||||
|         tombstone.set_id(self.ap_url.parse()?); | ||||
| 
 | ||||
|         let mut tombstone = Tombstone::default(); | ||||
|         tombstone.object_props.set_id_string(self.ap_url.clone())?; | ||||
|         act.delete_props.set_object_object(tombstone)?; | ||||
|         let mut act = Delete::new( | ||||
|             self.get_authors(conn)?[0] | ||||
|                 .clone() | ||||
|                 .into_id() | ||||
|                 .parse::<IriString>()?, | ||||
|             Base::retract(tombstone)?.into_generic()?, | ||||
|         ); | ||||
| 
 | ||||
|         act.object_props | ||||
|             .set_id_string(format!("{}#delete", self.ap_url))?; | ||||
|         act.object_props | ||||
|             .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?; | ||||
|         act.set_id(format!("{}#delete", self.ap_url).parse()?); | ||||
|         act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]); | ||||
|         Ok(act) | ||||
|     } | ||||
| 
 | ||||
| @ -621,47 +624,82 @@ impl FromId<DbConn> for Post { | ||||
|     } | ||||
| 
 | ||||
|     fn from_activity(conn: &DbConn, article: LicensedArticle) -> Result<Self> { | ||||
|         let conn = conn; | ||||
|         let license = article.custom_props.license_string().unwrap_or_default(); | ||||
|         let article = article.object; | ||||
|         let license = article.ext_one.license.unwrap_or_default(); | ||||
|         let article = article.inner; | ||||
| 
 | ||||
|         let (blog, authors) = article | ||||
|             .object_props | ||||
|             .attributed_to_link_vec::<Id>()? | ||||
|             .into_iter() | ||||
|             .ap_object_ref() | ||||
|             .attributed_to() | ||||
|             .ok_or(Error::MissingApProperty)? | ||||
|             .iter() | ||||
|             .fold((None, vec![]), |(blog, mut authors), link| { | ||||
|                 let url = link; | ||||
|                 match User::from_id(conn, &url, None, CONFIG.proxy()) { | ||||
|                     Ok(u) => { | ||||
|                         authors.push(u); | ||||
|                         (blog, authors) | ||||
|                 if let Some(url) = link.id() { | ||||
|                     match User::from_id(conn, url.as_str(), None, CONFIG.proxy()) { | ||||
|                         Ok(u) => { | ||||
|                             authors.push(u); | ||||
|                             (blog, authors) | ||||
|                         } | ||||
|                         Err(_) => ( | ||||
|                             blog.or_else(|| { | ||||
|                                 Blog::from_id(conn, url.as_str(), None, CONFIG.proxy()).ok() | ||||
|                             }), | ||||
|                             authors, | ||||
|                         ), | ||||
|                     } | ||||
|                     Err(_) => ( | ||||
|                         blog.or_else(|| Blog::from_id(conn, &url, None, CONFIG.proxy()).ok()), | ||||
|                         authors, | ||||
|                     ), | ||||
|                 } else { | ||||
|                     // logically, url possible to be an object without id proprty like {"type":"Person", "name":"Sally"} but we ignore the case
 | ||||
|                     (blog, authors) | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|         let cover = article | ||||
|             .object_props | ||||
|             .icon_object::<Image>() | ||||
|             .ok() | ||||
|             .and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id)); | ||||
|         let cover = article.icon().and_then(|icon| { | ||||
|             icon.iter().next().and_then(|img| { | ||||
|                 let image = img.to_owned().extend::<Image, ImageType>().ok()??; | ||||
|                 Media::from_activity(conn, &image).ok().map(|m| m.id) | ||||
|             }) | ||||
|         }); | ||||
| 
 | ||||
|         let title = article.object_props.name_string()?; | ||||
|         let title = article | ||||
|             .name() | ||||
|             .and_then(|name| name.to_as_string()) | ||||
|             .ok_or(Error::MissingApProperty)?; | ||||
|         let id = AnyBase::from_extended(article.clone()) // FIXME: Don't clone
 | ||||
|             .ok() | ||||
|             .ok_or(Error::MissingApProperty)? | ||||
|             .id() | ||||
|             .map(|id| id.to_string()); | ||||
|         let ap_url = article | ||||
|             .object_props | ||||
|             .url_string() | ||||
|             .or_else(|_| article.object_props.id_string())?; | ||||
|             .url() | ||||
|             .and_then(|url| url.to_as_uri().or(id)) | ||||
|             .ok_or(Error::MissingApProperty)?; | ||||
|         let source = article | ||||
|             .source() | ||||
|             .and_then(|s| { | ||||
|                 serde_json::to_value(s).ok().and_then(|obj| { | ||||
|                     if !obj.is_object() { | ||||
|                         return None; | ||||
|                     } | ||||
|                     obj.get("content") | ||||
|                         .and_then(|content| content.as_str().map(|c| c.to_string())) | ||||
|                 }) | ||||
|             }) | ||||
|             .unwrap_or_default(); | ||||
|         let post = Post::from_db(conn, &ap_url) | ||||
|             .and_then(|mut post| { | ||||
|                 let mut updated = false; | ||||
| 
 | ||||
|                 let slug = Self::slug(&title); | ||||
|                 let content = SafeString::new(&article.object_props.content_string()?); | ||||
|                 let subtitle = article.object_props.summary_string()?; | ||||
|                 let source = article.ap_object_props.source_object::<Source>()?.content; | ||||
|                 let content = SafeString::new( | ||||
|                     &article | ||||
|                         .content() | ||||
|                         .and_then(|content| content.to_as_string()) | ||||
|                         .ok_or(Error::MissingApProperty)?, | ||||
|                 ); | ||||
|                 let subtitle = article | ||||
|                     .summary() | ||||
|                     .and_then(|summary| summary.to_as_string()) | ||||
|                     .ok_or(Error::MissingApProperty)?; | ||||
| 
 | ||||
|                 if post.slug != slug { | ||||
|                     post.slug = slug.to_string(); | ||||
|                     updated = true; | ||||
| @ -683,7 +721,7 @@ impl FromId<DbConn> for Post { | ||||
|                     updated = true; | ||||
|                 } | ||||
|                 if post.source != source { | ||||
|                     post.source = source; | ||||
|                     post.source = source.clone(); | ||||
|                     updated = true; | ||||
|                 } | ||||
|                 if post.cover_id != cover { | ||||
| @ -704,14 +742,27 @@ impl FromId<DbConn> for Post { | ||||
|                         blog_id: blog.ok_or(Error::NotFound)?.id, | ||||
|                         slug: Self::slug(&title).to_string(), | ||||
|                         title, | ||||
|                         content: SafeString::new(&article.object_props.content_string()?), | ||||
|                         content: SafeString::new( | ||||
|                             &article | ||||
|                                 .content() | ||||
|                                 .and_then(|content| content.to_as_string()) | ||||
|                                 .ok_or(Error::MissingApProperty)?, | ||||
|                         ), | ||||
|                         published: true, | ||||
|                         license, | ||||
|                         // FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
 | ||||
|                         ap_url, | ||||
|                         creation_date: Some(article.object_props.published_utctime()?.naive_utc()), | ||||
|                         subtitle: article.object_props.summary_string()?, | ||||
|                         source: article.ap_object_props.source_object::<Source>()?.content, | ||||
|                         creation_date: article.published().map(|published| { | ||||
|                             let timestamp_secs = published.unix_timestamp(); | ||||
|                             let timestamp_nanos = published.unix_timestamp_nanos() | ||||
|                                 - (timestamp_secs as i128) * 1000i128 * 1000i128 * 1000i128; | ||||
|                             NaiveDateTime::from_timestamp(timestamp_secs, timestamp_nanos as u32) | ||||
|                         }), | ||||
|                         subtitle: article | ||||
|                             .summary() | ||||
|                             .and_then(|summary| summary.to_as_string()) | ||||
|                             .ok_or(Error::MissingApProperty)?, | ||||
|                         source, | ||||
|                         cover_id: cover, | ||||
|                     }, | ||||
|                 ) | ||||
| @ -735,22 +786,22 @@ impl FromId<DbConn> for Post { | ||||
|             .2 | ||||
|             .into_iter() | ||||
|             .collect::<HashSet<_>>(); | ||||
|         if let Some(serde_json::Value::Array(tags)) = article.object_props.tag { | ||||
|             for tag in tags { | ||||
|                 serde_json::from_value::<link::Mention>(tag.clone()) | ||||
|                     .map(|m| Mention::from_activity(conn, &m, post.id, true, true)) | ||||
|         if let Some(tags) = article.tag() { | ||||
|             for tag in tags.iter() { | ||||
|                 tag.clone() | ||||
|                     .extend::<link::Mention, MentionType>() // FIXME: Don't clone
 | ||||
|                     .map(|mention| { | ||||
|                         mention.map(|m| Mention::from_activity(conn, &m, post.id, true, true)) | ||||
|                     }) | ||||
|                     .ok(); | ||||
| 
 | ||||
|                 serde_json::from_value::<Hashtag>(tag.clone()) | ||||
|                     .map_err(Error::from) | ||||
|                     .and_then(|t| { | ||||
|                         let tag_name = t.name_string()?; | ||||
|                         Ok(Tag::from_activity( | ||||
|                             conn, | ||||
|                             &t, | ||||
|                             post.id, | ||||
|                             hashtags.remove(&tag_name), | ||||
|                         )) | ||||
|                 tag.clone() | ||||
|                     .extend::<Hashtag, HashtagType>() // FIXME: Don't clone
 | ||||
|                     .map(|hashtag| { | ||||
|                         hashtag.and_then(|t| { | ||||
|                             let tag_name = t.name.clone()?.as_str().to_string(); | ||||
|                             Tag::from_activity(conn, &t, post.id, hashtags.remove(&tag_name)).ok() | ||||
|                         }) | ||||
|                     }) | ||||
|                     .ok(); | ||||
|             } | ||||
| @ -762,15 +813,15 @@ impl FromId<DbConn> for Post { | ||||
|     } | ||||
| 
 | ||||
|     fn get_sender() -> &'static dyn Signer { | ||||
|         Instance::get_local_instance_user().expect("Failed to local instance user") | ||||
|         Instance::get_local_instance_user().expect("Failed to get local instance user") | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl AsObject<User, Create, &DbConn> for Post { | ||||
|     type Error = Error; | ||||
|     type Output = Post; | ||||
|     type Output = Self; | ||||
| 
 | ||||
|     fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result<Post> { | ||||
|     fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result<Self::Output> { | ||||
|         // TODO: check that _actor is actually one of the author?
 | ||||
|         Ok(self) | ||||
|     } | ||||
| @ -780,7 +831,7 @@ impl AsObject<User, Delete, &DbConn> for Post { | ||||
|     type Error = Error; | ||||
|     type Output = (); | ||||
| 
 | ||||
|     fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { | ||||
|     fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<Self::Output> { | ||||
|         let can_delete = self | ||||
|             .get_authors(conn)? | ||||
|             .into_iter() | ||||
| @ -813,27 +864,54 @@ impl FromId<DbConn> for PostUpdate { | ||||
|         Err(Error::NotFound) | ||||
|     } | ||||
| 
 | ||||
|     fn from_activity(conn: &DbConn, updated: LicensedArticle) -> Result<Self> { | ||||
|         Ok(PostUpdate { | ||||
|             ap_url: updated.object.object_props.id_string()?, | ||||
|             title: updated.object.object_props.name_string().ok(), | ||||
|             subtitle: updated.object.object_props.summary_string().ok(), | ||||
|             content: updated.object.object_props.content_string().ok(), | ||||
|             cover: updated | ||||
|                 .object | ||||
|                 .object_props | ||||
|                 .icon_object::<Image>() | ||||
|                 .ok() | ||||
|                 .and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id)), | ||||
|             source: updated | ||||
|                 .object | ||||
|                 .ap_object_props | ||||
|                 .source_object::<Source>() | ||||
|                 .ok() | ||||
|                 .map(|x| x.content), | ||||
|             license: updated.custom_props.license_string().ok(), | ||||
|             tags: updated.object.object_props.tag, | ||||
|         }) | ||||
|     fn from_activity(conn: &DbConn, updated: Self::Object) -> Result<Self> { | ||||
|         let mut post_update = PostUpdate { | ||||
|             ap_url: updated | ||||
|                 .ap_object_ref() | ||||
|                 .id_unchecked() | ||||
|                 .ok_or(Error::MissingApProperty)? | ||||
|                 .to_string(), | ||||
|             title: updated | ||||
|                 .ap_object_ref() | ||||
|                 .name() | ||||
|                 .and_then(|name| name.to_as_string()), | ||||
|             subtitle: updated | ||||
|                 .ap_object_ref() | ||||
|                 .summary() | ||||
|                 .and_then(|summary| summary.to_as_string()), | ||||
|             content: updated | ||||
|                 .ap_object_ref() | ||||
|                 .content() | ||||
|                 .and_then(|content| content.to_as_string()), | ||||
|             cover: None, | ||||
|             source: updated.source().and_then(|s| { | ||||
|                 serde_json::to_value(s).ok().and_then(|obj| { | ||||
|                     if !obj.is_object() { | ||||
|                         return None; | ||||
|                     } | ||||
|                     obj.get("content") | ||||
|                         .and_then(|content| content.as_str().map(|c| c.to_string())) | ||||
|                 }) | ||||
|             }), | ||||
|             license: None, | ||||
|             tags: updated | ||||
|                 .tag() | ||||
|                 .and_then(|tags| serde_json::to_value(tags).ok()), | ||||
|         }; | ||||
|         post_update.cover = updated.ap_object_ref().icon().and_then(|img| { | ||||
|             img.iter() | ||||
|                 .next() | ||||
|                 .and_then(|img| { | ||||
|                     img.clone() | ||||
|                         .extend::<Image, ImageType>() | ||||
|                         .map(|img| img.and_then(|img| Media::from_activity(conn, &img).ok())) | ||||
|                         .ok() | ||||
|                 }) | ||||
|                 .and_then(|m| m.map(|m| m.id)) | ||||
|         }); | ||||
|         post_update.license = updated.ext_one.license; | ||||
| 
 | ||||
|         Ok(post_update) | ||||
|     } | ||||
| 
 | ||||
|     fn get_sender() -> &'static dyn Signer { | ||||
| @ -893,8 +971,12 @@ impl AsObject<User, Update, &DbConn> for PostUpdate { | ||||
|                 serde_json::from_value::<Hashtag>(tag.clone()) | ||||
|                     .map_err(Error::from) | ||||
|                     .and_then(|t| { | ||||
|                         let tag_name = t.name_string()?; | ||||
|                         if txt_hashtags.remove(&tag_name) { | ||||
|                         let tag_name = t.name.as_ref().ok_or(Error::MissingApProperty)?; | ||||
|                         let tag_name_str = tag_name | ||||
|                             .as_xsd_string() | ||||
|                             .or_else(|| tag_name.as_rdf_lang_string().map(|rls| &*rls.value)) | ||||
|                             .ok_or(Error::MissingApProperty)?; | ||||
|                         if txt_hashtags.remove(tag_name_str) { | ||||
|                             hashtags.push(t); | ||||
|                         } else { | ||||
|                             tags.push(t); | ||||
| @ -1015,49 +1097,6 @@ mod tests { | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn licensed_article_serde() { | ||||
|         let mut article = Article::default(); | ||||
|         article.object_props.set_id_string("Yo".into()).unwrap(); | ||||
|         let mut license = Licensed::default(); | ||||
|         license.set_license_string("WTFPL".into()).unwrap(); | ||||
|         let full_article = LicensedArticle::new(article, license); | ||||
| 
 | ||||
|         let json = serde_json::to_value(full_article).unwrap(); | ||||
|         let article_from_json: LicensedArticle = serde_json::from_value(json).unwrap(); | ||||
|         assert_eq!( | ||||
|             "Yo", | ||||
|             &article_from_json.object.object_props.id_string().unwrap() | ||||
|         ); | ||||
|         assert_eq!( | ||||
|             "WTFPL", | ||||
|             &article_from_json.custom_props.license_string().unwrap() | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn licensed_article_deserialization() { | ||||
|         let json = json!({ | ||||
|             "type": "Article", | ||||
|             "id": "https://plu.me/~/Blog/my-article", | ||||
|             "attributedTo": ["https://plu.me/@/Admin", "https://plu.me/~/Blog"], | ||||
|             "content": "Hello.", | ||||
|             "name": "My Article", | ||||
|             "summary": "Bye.", | ||||
|             "source": { | ||||
|                 "content": "Hello.", | ||||
|                 "mediaType": "text/markdown" | ||||
|             }, | ||||
|             "published": "2014-12-12T12:12:12Z", | ||||
|             "to": [plume_common::activity_pub::PUBLIC_VISIBILITY] | ||||
|         }); | ||||
|         let article: LicensedArticle = serde_json::from_value(json).unwrap(); | ||||
|         assert_eq!( | ||||
|             "https://plu.me/~/Blog/my-article", | ||||
|             &article.object.object_props.id_string().unwrap() | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn to_activity() { | ||||
|         let conn = db(); | ||||
| @ -1074,10 +1113,10 @@ mod tests { | ||||
|                 "name": "Testing", | ||||
|                 "published": format_datetime(&post.creation_date), | ||||
|                 "source": { | ||||
|                     "content": "", | ||||
|                     "content": "Hello", | ||||
|                     "mediaType": "text/markdown" | ||||
|                 }, | ||||
|                 "summary": "", | ||||
|                 "summary": "Bye", | ||||
|                 "tag": [ | ||||
|                     { | ||||
|                         "href": "https://plu.me/@/user/", | ||||
| @ -1116,10 +1155,10 @@ mod tests { | ||||
|                     "name": "Testing", | ||||
|                     "published": format_datetime(&post.creation_date), | ||||
|                     "source": { | ||||
|                         "content": "", | ||||
|                         "content": "Hello", | ||||
|                         "mediaType": "text/markdown" | ||||
|                     }, | ||||
|                     "summary": "", | ||||
|                     "summary": "Bye", | ||||
|                     "tag": [ | ||||
|                         { | ||||
|                             "href": "https://plu.me/@/user/", | ||||
| @ -1161,10 +1200,10 @@ mod tests { | ||||
|                     "name": "Testing", | ||||
|                     "published": format_datetime(&post.creation_date), | ||||
|                     "source": { | ||||
|                         "content": "", | ||||
|                         "content": "Hello", | ||||
|                         "mediaType": "text/markdown" | ||||
|                     }, | ||||
|                     "summary": "", | ||||
|                     "summary": "Bye", | ||||
|                     "tag": [ | ||||
|                         { | ||||
|                             "href": "https://plu.me/@/user/", | ||||
| @ -1200,10 +1239,36 @@ mod tests { | ||||
|                 if key == "id" { | ||||
|                     continue; | ||||
|                 } | ||||
|                 assert_eq!(value, expected.get(key).unwrap()); | ||||
|                 assert_json_eq!(value, expected.get(key).unwrap()); | ||||
|             } | ||||
| 
 | ||||
|             Ok(()) | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn build_delete() { | ||||
|         let conn = db(); | ||||
|         conn.test_transaction::<_, Error, _>(|| { | ||||
|             let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn); | ||||
|             let act = post.build_delete(&conn)?; | ||||
| 
 | ||||
|             let expected = json!({ | ||||
|                 "actor": "https://plu.me/@/admin/", | ||||
|                 "id": "https://plu.me/~/BlogName/testing#delete", | ||||
|                 "object": { | ||||
|                     "id": "https://plu.me/~/BlogName/testing", | ||||
|                     "type": "Tombstone" | ||||
|                 }, | ||||
|                 "to": [ | ||||
|                     "https://www.w3.org/ns/activitystreams#Public" | ||||
|                 ], | ||||
|                 "type": "Delete" | ||||
|             }); | ||||
| 
 | ||||
|             assert_json_eq!(to_value(act)?, expected); | ||||
| 
 | ||||
|             Ok(()) | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,12 +1,16 @@ | ||||
| use crate::{ | ||||
|     db_conn::{DbConn, DbPool}, | ||||
|     follows, | ||||
|     posts::{LicensedArticle, Post}, | ||||
|     posts::Post, | ||||
|     users::{User, UserEvent}, | ||||
|     ACTOR_SYS, CONFIG, USER_CHAN, | ||||
| }; | ||||
| use activitypub::activity::Create; | ||||
| use plume_common::activity_pub::inbox::FromId; | ||||
| use activitystreams::{ | ||||
|     activity::{ActorAndObjectRef, Create}, | ||||
|     base::AnyBase, | ||||
|     object::kind::ArticleType, | ||||
| }; | ||||
| use plume_common::activity_pub::{inbox::FromId, LicensedArticle}; | ||||
| use riker::actors::{Actor, ActorFactoryArgs, ActorRefFactory, Context, Sender, Subscribe, Tell}; | ||||
| use std::sync::Arc; | ||||
| use tracing::{error, info, warn}; | ||||
| @ -68,13 +72,17 @@ fn fetch_and_cache_articles(user: &Arc<User>, conn: &DbConn) { | ||||
|     match create_acts { | ||||
|         Ok(create_acts) => { | ||||
|             for create_act in create_acts { | ||||
|                 match create_act.create_props.object_object::<LicensedArticle>() { | ||||
|                     Ok(article) => { | ||||
|                 match create_act.object_field_ref().as_single_base().map(|base| { | ||||
|                     let any_base = AnyBase::from_base(base.clone()); // FIXME: Don't clone()
 | ||||
|                     any_base.extend::<LicensedArticle, ArticleType>() | ||||
|                 }) { | ||||
|                     Some(Ok(Some(article))) => { | ||||
|                         Post::from_activity(conn, article) | ||||
|                             .expect("Article from remote user couldn't be saved"); | ||||
|                         info!("Fetched article from remote user"); | ||||
|                     } | ||||
|                     Err(e) => warn!("Error while fetching articles in background: {:?}", e), | ||||
|                     Some(Err(e)) => warn!("Error while fetching articles in background: {:?}", e), | ||||
|                     _ => warn!("Error while fetching articles in background"), | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @ -2,13 +2,18 @@ use crate::{ | ||||
|     db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::reshares, | ||||
|     timeline::*, users::User, Connection, Error, Result, CONFIG, | ||||
| }; | ||||
| use activitypub::activity::{Announce, Undo}; | ||||
| use activitystreams::{ | ||||
|     activity::{ActorAndObjectRef, Announce, Undo}, | ||||
|     base::AnyBase, | ||||
|     iri_string::types::IriString, | ||||
|     prelude::*, | ||||
| }; | ||||
| use chrono::NaiveDateTime; | ||||
| use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; | ||||
| use plume_common::activity_pub::{ | ||||
|     inbox::{AsActor, AsObject, FromId}, | ||||
|     sign::Signer, | ||||
|     Id, IntoId, PUBLIC_VISIBILITY, | ||||
|     PUBLIC_VISIBILITY, | ||||
| }; | ||||
| 
 | ||||
| #[derive(Clone, Queryable, Identifiable)] | ||||
| @ -61,16 +66,16 @@ impl Reshare { | ||||
|     } | ||||
| 
 | ||||
|     pub fn to_activity(&self, conn: &Connection) -> Result<Announce> { | ||||
|         let mut act = Announce::default(); | ||||
|         act.announce_props | ||||
|             .set_actor_link(User::get(conn, self.user_id)?.into_id())?; | ||||
|         act.announce_props | ||||
|             .set_object_link(Post::get(conn, self.post_id)?.into_id())?; | ||||
|         act.object_props.set_id_string(self.ap_url.clone())?; | ||||
|         act.object_props | ||||
|             .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?; | ||||
|         act.object_props | ||||
|             .set_cc_link_vec(vec![Id::new(self.get_user(conn)?.followers_endpoint)])?; | ||||
|         let mut act = Announce::new( | ||||
|             User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?, | ||||
|             Post::get(conn, self.post_id)?.ap_url.parse::<IriString>()?, | ||||
|         ); | ||||
|         act.set_id(self.ap_url.parse::<IriString>()?); | ||||
|         act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]); | ||||
|         act.set_many_ccs(vec![self | ||||
|             .get_user(conn)? | ||||
|             .followers_endpoint | ||||
|             .parse::<IriString>()?]); | ||||
| 
 | ||||
|         Ok(act) | ||||
|     } | ||||
| @ -93,16 +98,16 @@ impl Reshare { | ||||
|     } | ||||
| 
 | ||||
|     pub fn build_undo(&self, conn: &Connection) -> Result<Undo> { | ||||
|         let mut act = Undo::default(); | ||||
|         act.undo_props | ||||
|             .set_actor_link(User::get(conn, self.user_id)?.into_id())?; | ||||
|         act.undo_props.set_object_object(self.to_activity(conn)?)?; | ||||
|         act.object_props | ||||
|             .set_id_string(format!("{}#delete", self.ap_url))?; | ||||
|         act.object_props | ||||
|             .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?; | ||||
|         act.object_props | ||||
|             .set_cc_link_vec(vec![Id::new(self.get_user(conn)?.followers_endpoint)])?; | ||||
|         let mut act = Undo::new( | ||||
|             User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?, | ||||
|             AnyBase::from_extended(self.to_activity(conn)?)?, | ||||
|         ); | ||||
|         act.set_id(format!("{}#delete", self.ap_url).parse::<IriString>()?); | ||||
|         act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]); | ||||
|         act.set_many_ccs(vec![self | ||||
|             .get_user(conn)? | ||||
|             .followers_endpoint | ||||
|             .parse::<IriString>()?]); | ||||
| 
 | ||||
|         Ok(act) | ||||
|     } | ||||
| @ -143,7 +148,10 @@ impl FromId<DbConn> for Reshare { | ||||
|             NewReshare { | ||||
|                 post_id: Post::from_id( | ||||
|                     conn, | ||||
|                     &act.announce_props.object_link::<Id>()?, | ||||
|                     act.object_field_ref() | ||||
|                         .as_single_id() | ||||
|                         .ok_or(Error::MissingApProperty)? | ||||
|                         .as_str(), | ||||
|                     None, | ||||
|                     CONFIG.proxy(), | ||||
|                 ) | ||||
| @ -151,13 +159,19 @@ impl FromId<DbConn> for Reshare { | ||||
|                 .id, | ||||
|                 user_id: User::from_id( | ||||
|                     conn, | ||||
|                     &act.announce_props.actor_link::<Id>()?, | ||||
|                     act.actor_field_ref() | ||||
|                         .as_single_id() | ||||
|                         .ok_or(Error::MissingApProperty)? | ||||
|                         .as_str(), | ||||
|                     None, | ||||
|                     CONFIG.proxy(), | ||||
|                 ) | ||||
|                 .map_err(|(_, e)| e)? | ||||
|                 .id, | ||||
|                 ap_url: act.object_props.id_string()?, | ||||
|                 ap_url: act | ||||
|                     .id_unchecked() | ||||
|                     .ok_or(Error::MissingApProperty)? | ||||
|                     .to_string(), | ||||
|             }, | ||||
|         )?; | ||||
|         res.notify(conn)?; | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| use crate::{ap_url, instance::Instance, schema::tags, Connection, Error, Result}; | ||||
| use activitystreams::iri_string::types::IriString; | ||||
| use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; | ||||
| use plume_common::activity_pub::Hashtag; | ||||
| use plume_common::activity_pub::{Hashtag, HashtagExt}; | ||||
| 
 | ||||
| #[derive(Clone, Identifiable, Queryable)] | ||||
| pub struct Tag { | ||||
| @ -25,13 +26,16 @@ impl Tag { | ||||
|     list_by!(tags, for_post, post_id as i32); | ||||
| 
 | ||||
|     pub fn to_activity(&self) -> Result<Hashtag> { | ||||
|         let mut ht = Hashtag::default(); | ||||
|         ht.set_href_string(ap_url(&format!( | ||||
|             "{}/tag/{}", | ||||
|             Instance::get_local()?.public_domain, | ||||
|             self.tag | ||||
|         )))?; | ||||
|         ht.set_name_string(self.tag.clone())?; | ||||
|         let mut ht = Hashtag::new(); | ||||
|         ht.set_href( | ||||
|             ap_url(&format!( | ||||
|                 "{}/tag/{}", | ||||
|                 Instance::get_local()?.public_domain, | ||||
|                 self.tag | ||||
|             )) | ||||
|             .parse::<IriString>()?, | ||||
|         ); | ||||
|         ht.set_name(self.tag.clone()); | ||||
|         Ok(ht) | ||||
|     } | ||||
| 
 | ||||
| @ -44,7 +48,7 @@ impl Tag { | ||||
|         Tag::insert( | ||||
|             conn, | ||||
|             NewTag { | ||||
|                 tag: tag.name_string()?, | ||||
|                 tag: tag.name().ok_or(Error::MissingApProperty)?.as_str().into(), | ||||
|                 is_hashtag, | ||||
|                 post_id: post, | ||||
|             }, | ||||
| @ -52,13 +56,16 @@ impl Tag { | ||||
|     } | ||||
| 
 | ||||
|     pub fn build_activity(tag: String) -> Result<Hashtag> { | ||||
|         let mut ht = Hashtag::default(); | ||||
|         ht.set_href_string(ap_url(&format!( | ||||
|             "{}/tag/{}", | ||||
|             Instance::get_local()?.public_domain, | ||||
|             tag | ||||
|         )))?; | ||||
|         ht.set_name_string(tag)?; | ||||
|         let mut ht = Hashtag::new(); | ||||
|         ht.set_href( | ||||
|             ap_url(&format!( | ||||
|                 "{}/tag/{}", | ||||
|                 Instance::get_local()?.public_domain, | ||||
|                 tag | ||||
|             )) | ||||
|             .parse::<IriString>()?, | ||||
|         ); | ||||
|         ht.set_name(tag); | ||||
|         Ok(ht) | ||||
|     } | ||||
| 
 | ||||
| @ -78,6 +85,24 @@ mod tests { | ||||
|     use assert_json_diff::assert_json_eq; | ||||
|     use serde_json::to_value; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn from_activity() { | ||||
|         let conn = &db(); | ||||
|         conn.test_transaction::<_, Error, _>(|| { | ||||
|             let (posts, _users, _blogs) = fill_database(conn); | ||||
|             let post_id = posts[0].id; | ||||
|             let mut ht = Hashtag::new(); | ||||
|             ht.set_href(ap_url(&format!("https://plu.me/tag/a_tag")).parse::<IriString>()?); | ||||
|             ht.set_name("a_tag".to_string()); | ||||
|             let tag = Tag::from_activity(conn, &ht, post_id, true)?; | ||||
| 
 | ||||
|             assert_eq!(&tag.tag, "a_tag"); | ||||
|             assert!(tag.is_hashtag); | ||||
| 
 | ||||
|             Ok(()) | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn to_activity() { | ||||
|         let conn = &db(); | ||||
| @ -102,24 +127,6 @@ mod tests { | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn from_activity() { | ||||
|         let conn = &db(); | ||||
|         conn.test_transaction::<_, Error, _>(|| { | ||||
|             let (posts, _users, _blogs) = fill_database(conn); | ||||
|             let post_id = posts[0].id; | ||||
|             let mut ht = Hashtag::default(); | ||||
|             ht.set_href_string(ap_url(&format!("https://plu.me/tag/a_tag")))?; | ||||
|             ht.set_name_string("a_tag".into())?; | ||||
|             let tag = Tag::from_activity(conn, &ht, post_id, true)?; | ||||
| 
 | ||||
|             assert_eq!(&tag.tag, "a_tag"); | ||||
|             assert!(tag.is_hashtag); | ||||
| 
 | ||||
|             Ok(()) | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn build_activity() { | ||||
|         let conn = &db(); | ||||
|  | ||||
| @ -4,12 +4,15 @@ use crate::{ | ||||
|     safe_string::SafeString, schema::users, timeline::Timeline, Connection, Error, Result, | ||||
|     UserEvent::*, CONFIG, ITEMS_PER_PAGE, USER_CHAN, | ||||
| }; | ||||
| use activitypub::{ | ||||
| use activitystreams::{ | ||||
|     activity::Delete, | ||||
|     actor::Person, | ||||
|     actor::{ApActor, AsApActor, Endpoints, Person}, | ||||
|     base::{AnyBase, Base}, | ||||
|     collection::{OrderedCollection, OrderedCollectionPage}, | ||||
|     object::{Image, Tombstone}, | ||||
|     Activity, CustomObject, Endpoint, | ||||
|     iri_string::types::IriString, | ||||
|     markers::Activity, | ||||
|     object::{kind::ImageType, AsObject as _, Image, Tombstone}, | ||||
|     prelude::*, | ||||
| }; | ||||
| use chrono::{NaiveDateTime, Utc}; | ||||
| use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl}; | ||||
| @ -25,7 +28,8 @@ use plume_common::{ | ||||
|         inbox::{AsActor, AsObject, FromId}, | ||||
|         request::get, | ||||
|         sign::{gen_keypair, Error as SignError, Result as SignResult, Signer}, | ||||
|         ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY, | ||||
|         ActivityStream, ApSignature, CustomPerson, Id, IntoId, PublicKey, ToAsString, ToAsUri, | ||||
|         PUBLIC_VISIBILITY, | ||||
|     }, | ||||
|     utils, | ||||
| }; | ||||
| @ -39,11 +43,8 @@ use std::{ | ||||
|     hash::{Hash, Hasher}, | ||||
|     sync::Arc, | ||||
| }; | ||||
| use url::Url; | ||||
| use webfinger::*; | ||||
| 
 | ||||
| pub type CustomPerson = CustomObject<ApSignature, Person>; | ||||
| 
 | ||||
| pub enum Role { | ||||
|     Admin = 0, | ||||
|     Moderator = 1, | ||||
| @ -247,8 +248,18 @@ impl User { | ||||
|         let text = &res.text()?; | ||||
|         // without this workaround, publicKey is not correctly deserialized
 | ||||
|         let ap_sign = serde_json::from_str::<ApSignature>(text)?; | ||||
|         let mut json = serde_json::from_str::<CustomPerson>(text)?; | ||||
|         json.custom_props = ap_sign; | ||||
|         let person = serde_json::from_str::<Person>(text)?; | ||||
|         let json = CustomPerson::new( | ||||
|             ApActor::new( | ||||
|                 person | ||||
|                     .clone() | ||||
|                     .id_unchecked() | ||||
|                     .ok_or(Error::MissingApProperty)? | ||||
|                     .to_owned(), | ||||
|                 person, | ||||
|             ), | ||||
|             ap_sign, | ||||
|         ); // FIXME: Don't clone()
 | ||||
|         Ok(json) | ||||
|     } | ||||
| 
 | ||||
| @ -260,35 +271,56 @@ impl User { | ||||
|         User::fetch(&self.ap_url.clone()).and_then(|json| { | ||||
|             let avatar = Media::save_remote( | ||||
|                 conn, | ||||
|                 json.object | ||||
|                     .object_props | ||||
|                     .icon_image()? // FIXME: Fails when icon is not set
 | ||||
|                     .object_props | ||||
|                     .url_string()?, | ||||
|                 json.ap_actor_ref() | ||||
|                     .icon() | ||||
|                     .ok_or(Error::MissingApProperty)? // FIXME: Fails when icon is not set
 | ||||
|                     .iter() | ||||
|                     .next() | ||||
|                     .and_then(|i| { | ||||
|                         i.clone() | ||||
|                             .extend::<Image, ImageType>() // FIXME: Don't clone()
 | ||||
|                             .ok()? | ||||
|                             .and_then(|url| Some(url.id_unchecked()?.to_string())) | ||||
|                     }) | ||||
|                     .ok_or(Error::MissingApProperty)?, | ||||
|                 self, | ||||
|             ) | ||||
|             .ok(); | ||||
| 
 | ||||
|             let pub_key = &json.ext_one.public_key.public_key_pem; | ||||
|             diesel::update(self) | ||||
|                 .set(( | ||||
|                     users::username.eq(json.object.ap_actor_props.preferred_username_string()?), | ||||
|                     users::display_name.eq(json.object.object_props.name_string()?), | ||||
|                     users::outbox_url.eq(json.object.ap_actor_props.outbox_string()?), | ||||
|                     users::inbox_url.eq(json.object.ap_actor_props.inbox_string()?), | ||||
|                     users::username.eq(json | ||||
|                         .ap_actor_ref() | ||||
|                         .preferred_username() | ||||
|                         .ok_or(Error::MissingApProperty)?), | ||||
|                     users::display_name.eq(json | ||||
|                         .ap_actor_ref() | ||||
|                         .name() | ||||
|                         .ok_or(Error::MissingApProperty)? | ||||
|                         .to_as_string() | ||||
|                         .ok_or(Error::MissingApProperty)?), | ||||
|                     users::outbox_url.eq(json | ||||
|                         .ap_actor_ref() | ||||
|                         .outbox()? | ||||
|                         .ok_or(Error::MissingApProperty)? | ||||
|                         .as_str()), | ||||
|                     users::inbox_url.eq(json.ap_actor_ref().inbox()?.as_str()), | ||||
|                     users::summary.eq(SafeString::new( | ||||
|                         &json | ||||
|                             .object | ||||
|                             .object_props | ||||
|                             .summary_string() | ||||
|                             .ap_actor_ref() | ||||
|                             .summary() | ||||
|                             .and_then(|summary| summary.to_as_string()) | ||||
|                             .unwrap_or_default(), | ||||
|                     )), | ||||
|                     users::followers_endpoint.eq(json.object.ap_actor_props.followers_string()?), | ||||
|                     users::followers_endpoint.eq(json | ||||
|                         .ap_actor_ref() | ||||
|                         .followers()? | ||||
|                         .ok_or(Error::MissingApProperty)? | ||||
|                         .as_str()), | ||||
|                     users::avatar_id.eq(avatar.map(|a| a.id)), | ||||
|                     users::last_fetched_date.eq(Utc::now().naive_utc()), | ||||
|                     users::public_key.eq(json | ||||
|                         .custom_props | ||||
|                         .public_key_publickey()? | ||||
|                         .public_key_pem_string()?), | ||||
|                     users::public_key.eq(pub_key), | ||||
|                 )) | ||||
|                 .execute(conn) | ||||
|                 .map(|_| ()) | ||||
| @ -432,17 +464,16 @@ impl User { | ||||
|         Ok(ActivityStream::new(self.outbox_collection(conn)?)) | ||||
|     } | ||||
|     pub fn outbox_collection(&self, conn: &Connection) -> Result<OrderedCollection> { | ||||
|         let mut coll = OrderedCollection::default(); | ||||
|         let mut coll = OrderedCollection::new(); | ||||
|         let first = &format!("{}?page=1", &self.outbox_url); | ||||
|         let last = &format!( | ||||
|             "{}?page={}", | ||||
|             &self.outbox_url, | ||||
|             self.get_activities_count(conn) / i64::from(ITEMS_PER_PAGE) + 1 | ||||
|         ); | ||||
|         coll.collection_props.set_first_link(Id::new(first))?; | ||||
|         coll.collection_props.set_last_link(Id::new(last))?; | ||||
|         coll.collection_props | ||||
|             .set_total_items_u64(self.get_activities_count(conn) as u64)?; | ||||
|         coll.set_first(first.parse::<IriString>()?); | ||||
|         coll.set_last(last.parse::<IriString>()?); | ||||
|         coll.set_total_items(self.get_activities_count(conn) as u64); | ||||
|         Ok(coll) | ||||
|     } | ||||
|     pub fn outbox_page( | ||||
| @ -461,27 +492,31 @@ impl User { | ||||
|     ) -> Result<OrderedCollectionPage> { | ||||
|         let acts = self.get_activities_page(conn, (min, max))?; | ||||
|         let n_acts = self.get_activities_count(conn); | ||||
|         let mut coll = OrderedCollectionPage::default(); | ||||
|         let mut coll = OrderedCollectionPage::new(); | ||||
|         if n_acts - i64::from(min) >= i64::from(ITEMS_PER_PAGE) { | ||||
|             coll.collection_page_props.set_next_link(Id::new(&format!( | ||||
|                 "{}?page={}", | ||||
|                 &self.outbox_url, | ||||
|                 min / ITEMS_PER_PAGE + 2 | ||||
|             )))?; | ||||
|             coll.set_next( | ||||
|                 format!("{}?page={}", &self.outbox_url, min / ITEMS_PER_PAGE + 2) | ||||
|                     .parse::<IriString>()?, | ||||
|             ); | ||||
|         } | ||||
|         if min > 0 { | ||||
|             coll.collection_page_props.set_prev_link(Id::new(&format!( | ||||
|                 "{}?page={}", | ||||
|                 &self.outbox_url, | ||||
|                 min / ITEMS_PER_PAGE | ||||
|             )))?; | ||||
|             coll.set_prev( | ||||
|                 format!("{}?page={}", &self.outbox_url, min / ITEMS_PER_PAGE) | ||||
|                     .parse::<IriString>()?, | ||||
|             ); | ||||
|         } | ||||
|         coll.collection_props.items = serde_json::to_value(acts)?; | ||||
|         coll.collection_page_props | ||||
|             .set_part_of_link(Id::new(&self.outbox_url))?; | ||||
|         coll.set_many_items( | ||||
|             acts.iter() | ||||
|                 .filter_map(|value| AnyBase::from_arbitrary_json(value).ok()), | ||||
|         ); | ||||
|         coll.set_part_of(self.outbox_url.parse::<IriString>()?); | ||||
|         Ok(coll) | ||||
|     } | ||||
|     fn fetch_outbox_page<T: Activity>(&self, url: &str) -> Result<(Vec<T>, Option<String>)> { | ||||
| 
 | ||||
|     pub fn fetch_outbox_page<T: Activity + serde::de::DeserializeOwned>( | ||||
|         &self, | ||||
|         url: &str, | ||||
|     ) -> Result<(Vec<T>, Option<String>)> { | ||||
|         let res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?; | ||||
|         let text = &res.text()?; | ||||
|         let json: serde_json::Value = serde_json::from_str(text)?; | ||||
| @ -495,7 +530,8 @@ impl User { | ||||
|         let next = json.get("next").map(|x| x.as_str().unwrap().to_owned()); | ||||
|         Ok((items, next)) | ||||
|     } | ||||
|     pub fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> { | ||||
| 
 | ||||
|     pub fn fetch_outbox<T: Activity + serde::de::DeserializeOwned>(&self) -> Result<Vec<T>> { | ||||
|         let res = get( | ||||
|             &self.outbox_url[..], | ||||
|             Self::get_sender(), | ||||
| @ -740,71 +776,58 @@ impl User { | ||||
|     } | ||||
| 
 | ||||
|     pub fn to_activity(&self, conn: &Connection) -> Result<CustomPerson> { | ||||
|         let mut actor = Person::default(); | ||||
|         actor.object_props.set_id_string(self.ap_url.clone())?; | ||||
|         actor | ||||
|             .object_props | ||||
|             .set_name_string(self.display_name.clone())?; | ||||
|         actor | ||||
|             .object_props | ||||
|             .set_summary_string(self.summary_html.get().clone())?; | ||||
|         actor.object_props.set_url_string(self.ap_url.clone())?; | ||||
|         actor | ||||
|             .ap_actor_props | ||||
|             .set_inbox_string(self.inbox_url.clone())?; | ||||
|         actor | ||||
|             .ap_actor_props | ||||
|             .set_outbox_string(self.outbox_url.clone())?; | ||||
|         actor | ||||
|             .ap_actor_props | ||||
|             .set_preferred_username_string(self.username.clone())?; | ||||
|         actor | ||||
|             .ap_actor_props | ||||
|             .set_followers_string(self.followers_endpoint.clone())?; | ||||
|         let mut actor = ApActor::new(self.inbox_url.parse()?, Person::new()); | ||||
|         let ap_url = self.ap_url.parse::<IriString>()?; | ||||
|         actor.set_id(ap_url.clone()); | ||||
|         actor.set_name(self.display_name.clone()); | ||||
|         actor.set_summary(self.summary_html.get().clone()); | ||||
|         actor.set_url(ap_url.clone()); | ||||
|         actor.set_inbox(self.inbox_url.parse()?); | ||||
|         actor.set_outbox(self.outbox_url.parse()?); | ||||
|         actor.set_preferred_username(self.username.clone()); | ||||
|         actor.set_followers(self.followers_endpoint.parse()?); | ||||
| 
 | ||||
|         if let Some(shared_inbox_url) = self.shared_inbox_url.clone() { | ||||
|             let mut endpoints = Endpoint::default(); | ||||
|             endpoints.set_shared_inbox_string(shared_inbox_url)?; | ||||
|             actor.ap_actor_props.set_endpoints_endpoint(endpoints)?; | ||||
|             let endpoints = Endpoints { | ||||
|                 shared_inbox: Some(shared_inbox_url.parse::<IriString>()?), | ||||
|                 ..Endpoints::default() | ||||
|             }; | ||||
|             actor.set_endpoints(endpoints); | ||||
|         } | ||||
| 
 | ||||
|         let mut public_key = PublicKey::default(); | ||||
|         public_key.set_id_string(format!("{}#main-key", self.ap_url))?; | ||||
|         public_key.set_owner_string(self.ap_url.clone())?; | ||||
|         public_key.set_public_key_pem_string(self.public_key.clone())?; | ||||
|         let mut ap_signature = ApSignature::default(); | ||||
|         ap_signature.set_public_key_publickey(public_key)?; | ||||
|         let pub_key = PublicKey { | ||||
|             id: format!("{}#main-key", self.ap_url).parse()?, | ||||
|             owner: ap_url, | ||||
|             public_key_pem: self.public_key.clone(), | ||||
|         }; | ||||
|         let ap_signature = ApSignature { | ||||
|             public_key: pub_key, | ||||
|         }; | ||||
| 
 | ||||
|         if let Some(avatar_id) = self.avatar_id { | ||||
|             let mut avatar = Image::default(); | ||||
|             avatar | ||||
|                 .object_props | ||||
|                 .set_url_string(Media::get(conn, avatar_id)?.url()?)?; | ||||
|             actor.object_props.set_icon_object(avatar)?; | ||||
|             let mut avatar = Image::new(); | ||||
|             avatar.set_url(Media::get(conn, avatar_id)?.url()?.parse::<IriString>()?); | ||||
|             actor.set_icon(avatar.into_any_base()?); | ||||
|         } | ||||
| 
 | ||||
|         Ok(CustomPerson::new(actor, ap_signature)) | ||||
|     } | ||||
| 
 | ||||
|     pub fn delete_activity(&self, conn: &Connection) -> Result<Delete> { | ||||
|         let mut del = Delete::default(); | ||||
|         let mut tombstone = Tombstone::new(); | ||||
|         tombstone.set_id(self.ap_url.parse()?); | ||||
| 
 | ||||
|         let mut tombstone = Tombstone::default(); | ||||
|         tombstone.object_props.set_id_string(self.ap_url.clone())?; | ||||
| 
 | ||||
|         del.delete_props | ||||
|             .set_actor_link(Id::new(self.ap_url.clone()))?; | ||||
|         del.delete_props.set_object_object(tombstone)?; | ||||
|         del.object_props | ||||
|             .set_id_string(format!("{}#delete", self.ap_url))?; | ||||
|         del.object_props | ||||
|             .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?; | ||||
|         del.object_props.set_cc_link_vec( | ||||
|         let mut del = Delete::new( | ||||
|             self.ap_url.parse::<IriString>()?, | ||||
|             Base::retract(tombstone)?.into_generic()?, | ||||
|         ); | ||||
|         del.set_id(format!("{}#delete", self.ap_url).parse()?); | ||||
|         del.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]); | ||||
|         del.set_many_ccs( | ||||
|             self.get_followers(conn)? | ||||
|                 .into_iter() | ||||
|                 .map(|f| Id::new(f.ap_url)) | ||||
|                 .collect(), | ||||
|         )?; | ||||
|                 .filter_map(|f| f.ap_url.parse::<IriString>().ok()), | ||||
|         ); | ||||
| 
 | ||||
|         Ok(del) | ||||
|     } | ||||
| @ -930,9 +953,60 @@ impl FromId<DbConn> for User { | ||||
|     } | ||||
| 
 | ||||
|     fn from_activity(conn: &DbConn, acct: CustomPerson) -> Result<Self> { | ||||
|         let url = Url::parse(&acct.object.object_props.id_string()?)?; | ||||
|         let inst = url.host_str().ok_or(Error::Url)?; | ||||
|         let instance = Instance::find_by_domain(conn, inst).or_else(|_| { | ||||
|         let actor = acct.ap_actor_ref(); | ||||
|         let username = actor | ||||
|             .preferred_username() | ||||
|             .ok_or(Error::MissingApProperty)? | ||||
|             .to_string(); | ||||
| 
 | ||||
|         if username.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) { | ||||
|             return Err(Error::InvalidValue); | ||||
|         } | ||||
| 
 | ||||
|         let summary = acct | ||||
|             .object_ref() | ||||
|             .summary() | ||||
|             .and_then(|prop| prop.to_as_string()) | ||||
|             .unwrap_or_default(); | ||||
|         let mut new_user = NewUser { | ||||
|             display_name: acct | ||||
|                 .object_ref() | ||||
|                 .name() | ||||
|                 .and_then(|prop| prop.to_as_string()) | ||||
|                 .unwrap_or_else(|| username.clone()), | ||||
|             username: username.clone(), | ||||
|             outbox_url: actor.outbox()?.ok_or(Error::MissingApProperty)?.to_string(), | ||||
|             inbox_url: actor.inbox()?.to_string(), | ||||
|             role: 2, | ||||
|             summary_html: SafeString::new(&summary), | ||||
|             summary, | ||||
|             public_key: acct.ext_one.public_key.public_key_pem.to_string(), | ||||
|             shared_inbox_url: actor | ||||
|                 .endpoints()? | ||||
|                 .and_then(|e| e.shared_inbox.map(|inbox| inbox.to_string())), | ||||
|             followers_endpoint: actor | ||||
|                 .followers()? | ||||
|                 .ok_or(Error::MissingApProperty)? | ||||
|                 .to_string(), | ||||
|             ..NewUser::default() | ||||
|         }; | ||||
| 
 | ||||
|         let avatar_id = acct.object_ref().icon().and_then(|icon| icon.to_as_uri()); | ||||
| 
 | ||||
|         let (ap_url, inst) = { | ||||
|             let any_base = acct.into_any_base()?; | ||||
|             let id = any_base.id().ok_or(Error::MissingApProperty)?; | ||||
|             ( | ||||
|                 id.to_string(), | ||||
|                 id.authority_components() | ||||
|                     .ok_or(Error::Url)? | ||||
|                     .host() | ||||
|                     .to_string(), | ||||
|             ) | ||||
|         }; | ||||
|         new_user.ap_url = ap_url; | ||||
| 
 | ||||
|         let instance = Instance::find_by_domain(conn, &inst).or_else(|_| { | ||||
|             Instance::insert( | ||||
|                 conn, | ||||
|                 NewInstance { | ||||
| @ -949,70 +1023,20 @@ impl FromId<DbConn> for User { | ||||
|                 }, | ||||
|             ) | ||||
|         })?; | ||||
| 
 | ||||
|         let username = acct.object.ap_actor_props.preferred_username_string()?; | ||||
| 
 | ||||
|         if username.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) { | ||||
|             return Err(Error::InvalidValue); | ||||
|         } | ||||
| 
 | ||||
|         let fqn = if instance.local { | ||||
|             username.clone() | ||||
|         new_user.instance_id = instance.id; | ||||
|         new_user.fqn = if instance.local { | ||||
|             username | ||||
|         } else { | ||||
|             format!("{}@{}", username, instance.public_domain) | ||||
|         }; | ||||
| 
 | ||||
|         let user = User::insert( | ||||
|             conn, | ||||
|             NewUser { | ||||
|                 display_name: acct | ||||
|                     .object | ||||
|                     .object_props | ||||
|                     .name_string() | ||||
|                     .unwrap_or_else(|_| username.clone()), | ||||
|                 username, | ||||
|                 outbox_url: acct.object.ap_actor_props.outbox_string()?, | ||||
|                 inbox_url: acct.object.ap_actor_props.inbox_string()?, | ||||
|                 role: 2, | ||||
|                 summary: acct | ||||
|                     .object | ||||
|                     .object_props | ||||
|                     .summary_string() | ||||
|                     .unwrap_or_default(), | ||||
|                 summary_html: SafeString::new( | ||||
|                     &acct | ||||
|                         .object | ||||
|                         .object_props | ||||
|                         .summary_string() | ||||
|                         .unwrap_or_default(), | ||||
|                 ), | ||||
|                 email: None, | ||||
|                 hashed_password: None, | ||||
|                 instance_id: instance.id, | ||||
|                 ap_url: acct.object.object_props.id_string()?, | ||||
|                 public_key: acct | ||||
|                     .custom_props | ||||
|                     .public_key_publickey()? | ||||
|                     .public_key_pem_string()?, | ||||
|                 private_key: None, | ||||
|                 shared_inbox_url: acct | ||||
|                     .object | ||||
|                     .ap_actor_props | ||||
|                     .endpoints_endpoint() | ||||
|                     .and_then(|e| e.shared_inbox_string()) | ||||
|                     .ok(), | ||||
|                 followers_endpoint: acct.object.ap_actor_props.followers_string()?, | ||||
|                 fqn, | ||||
|                 avatar_id: None, | ||||
|             }, | ||||
|         )?; | ||||
|         let user = User::insert(conn, new_user)?; | ||||
|         if let Some(avatar_id) = avatar_id { | ||||
|             let avatar = Media::save_remote(conn, avatar_id, &user); | ||||
| 
 | ||||
|         if let Ok(icon) = acct.object.object_props.icon_image() { | ||||
|             if let Ok(url) = icon.object_props.url_string() { | ||||
|                 let avatar = Media::save_remote(conn, url, &user); | ||||
| 
 | ||||
|                 if let Ok(avatar) = avatar { | ||||
|                     user.set_avatar(conn, avatar.id)?; | ||||
|             if let Ok(avatar) = avatar { | ||||
|                 if let Err(e) = user.set_avatar(conn, avatar.id) { | ||||
|                     tracing::error!("{:?}", e); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| @ -1435,10 +1459,8 @@ pub(crate) mod tests { | ||||
|                     "sharedInbox": "https://plu.me/inbox" | ||||
|                 }, | ||||
|                 "followers": "https://plu.me/@/admin/followers", | ||||
|                 "following": null, | ||||
|                 "id": "https://plu.me/@/admin/", | ||||
|                 "inbox": "https://plu.me/@/admin/inbox", | ||||
|                 "liked": null, | ||||
|                 "name": "The admin", | ||||
|                 "outbox": "https://plu.me/@/admin/outbox", | ||||
|                 "preferredUsername": "admin", | ||||
| @ -1461,14 +1483,12 @@ pub(crate) mod tests { | ||||
|                     "sharedInbox": "https://plu.me/inbox" | ||||
|                 }, | ||||
|                 "followers": "https://plu.me/@/other/followers", | ||||
|                 "following": null, | ||||
|                 "icon": { | ||||
|                     "url": "https://plu.me/static/media/example.png", | ||||
|                     "type": "Image", | ||||
|                 }, | ||||
|                 "id": "https://plu.me/@/other/", | ||||
|                 "inbox": "https://plu.me/@/other/inbox", | ||||
|                 "liked": null, | ||||
|                 "name": "Another user", | ||||
|                 "outbox": "https://plu.me/@/other/outbox", | ||||
|                 "preferredUsername": "other", | ||||
| @ -1524,7 +1544,6 @@ pub(crate) mod tests { | ||||
| 
 | ||||
|             let expected = json!({ | ||||
|                 "first": "https://plu.me/@/admin/outbox?page=1", | ||||
|                 "items": null, | ||||
|                 "last": "https://plu.me/@/admin/outbox?page=5", | ||||
|                 "totalItems": 51, | ||||
|                 "type": "OrderedCollection", | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| use activitypub::collection::{OrderedCollection, OrderedCollectionPage}; | ||||
| use activitystreams::collection::{OrderedCollection, OrderedCollectionPage}; | ||||
| use diesel::SaveChangesDsl; | ||||
| use rocket::{ | ||||
|     http::ContentType, | ||||
| @ -12,7 +12,7 @@ use validator::{Validate, ValidationError, ValidationErrors}; | ||||
| use crate::routes::{errors::ErrorPage, Page, RespondOrRedirect}; | ||||
| use crate::template_utils::{IntoContext, Ructe}; | ||||
| use crate::utils::requires_login; | ||||
| use plume_common::activity_pub::{ActivityStream, ApRequest}; | ||||
| use plume_common::activity_pub::{ActivityStream, ApRequest, CustomGroup}; | ||||
| use plume_common::utils; | ||||
| use plume_models::{ | ||||
|     blog_authors::*, blogs::*, db_conn::DbConn, instance::Instance, medias::*, posts::Post, | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| use crate::template_utils::Ructe; | ||||
| use activitypub::object::Note; | ||||
| use activitystreams::object::Note; | ||||
| use rocket::{ | ||||
|     request::LenientForm, | ||||
|     response::{Flash, Redirect}, | ||||
|  | ||||
| @ -15,7 +15,7 @@ use crate::routes::{ | ||||
| }; | ||||
| use crate::template_utils::{IntoContext, Ructe}; | ||||
| use crate::utils::requires_login; | ||||
| use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest}; | ||||
| use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest, LicensedArticle}; | ||||
| use plume_common::utils::md_to_html; | ||||
| use plume_models::{ | ||||
|     blogs::*, | ||||
|  | ||||
| @ -1,4 +1,8 @@ | ||||
| use activitypub::collection::{OrderedCollection, OrderedCollectionPage}; | ||||
| use activitystreams::{ | ||||
|     collection::{OrderedCollection, OrderedCollectionPage}, | ||||
|     iri_string::types::IriString, | ||||
|     prelude::*, | ||||
| }; | ||||
| use diesel::SaveChangesDsl; | ||||
| use rocket::{ | ||||
|     http::{uri::Uri, ContentType, Cookies}, | ||||
| @ -15,7 +19,7 @@ use crate::routes::{ | ||||
| }; | ||||
| use crate::template_utils::{IntoContext, Ructe}; | ||||
| use crate::utils::requires_login; | ||||
| use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest, Id}; | ||||
| use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest, CustomPerson}; | ||||
| use plume_common::utils::md_to_html; | ||||
| use plume_models::{ | ||||
|     blogs::Blog, | ||||
| @ -561,17 +565,13 @@ pub fn ap_followers( | ||||
|         .get_followers(&conn) | ||||
|         .ok()? | ||||
|         .into_iter() | ||||
|         .map(|f| Id::new(f.ap_url)) | ||||
|         .collect::<Vec<Id>>(); | ||||
|         .filter_map(|f| f.ap_url.parse::<IriString>().ok()) | ||||
|         .collect::<Vec<IriString>>(); | ||||
| 
 | ||||
|     let mut coll = OrderedCollection::default(); | ||||
|     coll.object_props | ||||
|         .set_id_string(user.followers_endpoint) | ||||
|         .ok()?; | ||||
|     coll.collection_props | ||||
|         .set_total_items_u64(followers.len() as u64) | ||||
|         .ok()?; | ||||
|     coll.collection_props.set_items_link_vec(followers).ok()?; | ||||
|     let mut coll = OrderedCollection::new(); | ||||
|     coll.set_id(user.followers_endpoint.parse::<IriString>().ok()?); | ||||
|     coll.set_total_items(followers.len() as u64); | ||||
|     coll.set_many_items(followers); | ||||
|     Some(ActivityStream::new(coll)) | ||||
| } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user