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:
KitaitiMakoto 2022-05-08 09:14:53 +00:00
commit 66376afb36
25 changed files with 2064 additions and 1388 deletions

View file

@ -14,6 +14,7 @@
- Bump Rust to nightly 2022-01-26 (#1015) - Bump Rust to nightly 2022-01-26 (#1015)
- Remove "Latest articles" timeline (#1069) - Remove "Latest articles" timeline (#1069)
- Change order of timeline tabs (#1069, #1070, #1072) - Change order of timeline tabs (#1069, #1070, #1072)
- Migrate ActivityPub-related crates from activitypub 0.1 to activitystreams 0.7
### Fixed ### Fixed

777
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,6 @@ repository = "https://github.com/Plume-org/Plume"
edition = "2018" edition = "2018"
[dependencies] [dependencies]
activitypub = "0.1.3"
atom_syndication = "0.11.0" atom_syndication = "0.11.0"
clap = "2.33" clap = "2.33"
dotenv = "0.15.0" dotenv = "0.15.0"
@ -28,6 +27,7 @@ webfinger = "0.4.1"
tracing = "0.1.34" tracing = "0.1.34"
tracing-subscriber = "0.3.10" tracing-subscriber = "0.3.10"
riker = "0.4.2" riker = "0.4.2"
activitystreams = "0.7.0-alpha.18"
[[bin]] [[bin]]
name = "plume" name = "plume"

View file

@ -5,9 +5,6 @@ authors = ["Plume contributors"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
activitypub = "0.1.1"
activitystreams-derive = "0.1.1"
activitystreams-traits = "0.1.0"
array_tool = "1.0" array_tool = "1.0"
base64 = "0.13" base64 = "0.13"
heck = "0.4.0" heck = "0.4.0"
@ -23,6 +20,8 @@ syntect = "4.5.0"
regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] } regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] }
tracing = "0.1.34" tracing = "0.1.34"
askama_escape = "0.10.3" askama_escape = "0.10.3"
activitystreams = "0.7.0-alpha.18"
activitystreams-ext = "0.1.0-alpha.2"
url = "2.2.2" url = "2.2.2"
flume = "0.10.12" flume = "0.10.12"
tokio = { version = "1.18.1", features = ["full"] } tokio = { version = "1.18.1", features = ["full"] }
@ -38,6 +37,7 @@ git = "https://git.joinplu.me/Plume/pulldown-cmark"
branch = "bidi-plume" branch = "bidi-plume"
[dev-dependencies] [dev-dependencies]
assert-json-diff = "2.0.1"
once_cell = "1.10.0" once_cell = "1.10.0"
[features] [features]

View file

@ -10,8 +10,7 @@ use super::{request, sign::Signer};
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
/// # extern crate activitypub; /// # use activitystreams::{prelude::*, base::Base, actor::Person, activity::{Announce, Create}, object::Note, iri_string::types::IriString};
/// # use activitypub::{actor::Person, activity::{Announce, Create}, object::Note};
/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa}; /// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
/// # use once_cell::sync::Lazy; /// # use once_cell::sync::Lazy;
/// # use plume_common::activity_pub::inbox::*; /// # use plume_common::activity_pub::inbox::*;
@ -113,12 +112,13 @@ use super::{request, sign::Signer};
/// # } /// # }
/// # } /// # }
/// # /// #
/// # let mut act = Create::default(); /// # let mut person = Person::new();
/// # act.object_props.set_id_string(String::from("https://test.ap/activity")).unwrap(); /// # person.set_id("https://test.ap/actor".parse::<IriString>().unwrap());
/// # let mut person = Person::default(); /// # let mut act = Create::new(
/// # person.object_props.set_id_string(String::from("https://test.ap/actor")).unwrap(); /// # Base::retract(person).unwrap().into_generic().unwrap(),
/// # act.create_props.set_actor_object(person).unwrap(); /// # Base::retract(Note::new()).unwrap().into_generic().unwrap()
/// # act.create_props.set_object_object(Note::default()).unwrap(); /// # );
/// # act.set_id("https://test.ap/activity".parse::<IriString>().unwrap());
/// # let activity_json = serde_json::to_value(act).unwrap(); /// # let activity_json = serde_json::to_value(act).unwrap();
/// # /// #
/// # let conn = (); /// # let conn = ();
@ -197,29 +197,29 @@ where
} }
/// Registers an handler on this Inbox. /// 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 where
A: AsActor<&'a C> + FromId<C, Error = E>, 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: AsObject<A, V, &'a C, Error = E> + FromId<C, Error = E>,
M::Output: Into<R>, 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() { if serde_json::from_value::<V>(act.clone()).is_ok() {
let act_clone = act.clone(); let act_clone = act.clone();
let act_id = match act_clone["id"].as_str() { let act_id = match act_clone["id"].as_str() {
Some(x) => x, Some(x) => x,
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidID), None => return Self::NotHandled(ctx, act, InboxError::InvalidID),
}; };
// Get the actor ID // Get the actor ID
let actor_id = match get_id(act["actor"].clone()) { let actor_id = match get_id(act["actor"].clone()) {
Some(x) => x, 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) { 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) // 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 { if let Some(json) = json {
act["actor"] = 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" // Same logic for "object"
let obj_id = match get_id(act["object"].clone()) { let obj_id = match get_id(act["object"].clone()) {
Some(x) => x, 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( let obj = match M::from_id(
ctx, ctx,
@ -255,19 +255,19 @@ where
if let Some(json) = json { if let Some(json) = json {
act["object"] = 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 // Handle the activity
match obj.activity(ctx, actor, act_id) { match obj.activity(ctx, actor, act_id) {
Ok(res) => Inbox::Handled(res.into()), Ok(res) => Self::Handled(res.into()),
Err(e) => Inbox::Failed(e), Err(e) => Self::Failed(e),
} }
} else { } else {
// If the Activity type is not matching the expected one for // If the Activity type is not matching the expected one for
// this handler, try with the next one. // this handler, try with the next one.
Inbox::NotHandled(ctx, act, e) Self::NotHandled(ctx, act, e)
} }
} else { } else {
self self
@ -333,7 +333,7 @@ pub trait FromId<C>: Sized {
type Error: From<InboxError<Self::Error>> + Debug; type Error: From<InboxError<Self::Error>> + Debug;
/// The ActivityPub object type representing Self /// 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. /// 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. /// representing the Note by a Message type, without any specific context.
/// ///
/// ```rust /// ```rust
/// # extern crate activitypub; /// # use activitystreams::{prelude::*, activity::Create, actor::Person, object::Note};
/// # use activitypub::{activity::Create, actor::Person, object::Note};
/// # use plume_common::activity_pub::inbox::{AsActor, AsObject, FromId}; /// # 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 plume_common::activity_pub::sign::{gen_keypair, Error as SignError, Result as SignResult, Signer};
/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa}; /// # 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> { /// 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 { /// fn get_sender() -> &'static dyn Signer {
@ -521,7 +523,7 @@ pub trait AsActor<C> {
/// ``` /// ```
pub trait AsObject<A, V, C> pub trait AsObject<A, V, C>
where where
V: activitypub::Activity, V: activitystreams::markers::Activity,
{ {
/// What kind of error is returned when something fails /// What kind of error is returned when something fails
type Error; type Error;
@ -549,7 +551,13 @@ mod tests {
use crate::activity_pub::sign::{ use crate::activity_pub::sign::{
gen_keypair, Error as SignError, Result as SignResult, Signer, 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 once_cell::sync::Lazy;
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa}; use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
@ -598,11 +606,11 @@ mod tests {
type Object = Person; type Object = Person;
fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> { fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> {
Ok(MyActor) Ok(Self)
} }
fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> { fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> {
Ok(MyActor) Ok(Self)
} }
fn get_sender() -> &'static dyn Signer { fn get_sender() -> &'static dyn Signer {
@ -626,11 +634,11 @@ mod tests {
type Object = Note; type Object = Note;
fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> { fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> {
Ok(MyObject) Ok(Self)
} }
fn from_activity(_: &(), _obj: Note) -> Result<Self, Self::Error> { fn from_activity(_: &(), _obj: Note) -> Result<Self, Self::Error> {
Ok(MyObject) Ok(Self)
} }
fn get_sender() -> &'static dyn Signer { fn get_sender() -> &'static dyn Signer {
@ -678,21 +686,15 @@ mod tests {
} }
fn build_create() -> Create { fn build_create() -> Create {
let mut act = Create::default(); let mut person = Person::new();
act.object_props person.set_id("https://test.ap/actor".parse().unwrap());
.set_id_string(String::from("https://test.ap/activity")) let mut note = Note::new();
.unwrap(); note.set_id("https://test.ap/note".parse().unwrap());
let mut person = Person::default(); let mut act = Create::new(
person Base::retract(person).unwrap().into_generic().unwrap(),
.object_props Base::retract(note).unwrap().into_generic().unwrap(),
.set_id_string(String::from("https://test.ap/actor")) );
.unwrap(); act.set_id("https://test.ap/activity".parse().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();
act act
} }
@ -729,6 +731,16 @@ mod tests {
} }
struct FailingActor; 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 { impl FromId<()> for FailingActor {
type Error = (); type Error = ();
type Object = Person; type Object = Person;
@ -737,7 +749,7 @@ mod tests {
Err(()) Err(())
} }
fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> { fn from_activity(_: &(), _obj: Self::Object) -> Result<Self, Self::Error> {
Err(()) Err(())
} }
@ -745,15 +757,6 @@ mod tests {
&*MY_SIGNER &*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 { impl AsObject<FailingActor, Create, &()> for MyObject {
type Error = (); type Error = ();

View file

@ -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 array_tool::vec::Uniq;
use futures::future::join_all; use futures::future::join_all;
use reqwest::{header::HeaderValue, ClientBuilder, RequestBuilder, Url}; 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> { fn respond_to(self, request: &Request<'_>) -> Result<Response<'r>, Status> {
let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?; let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?;
json["@context"] = context(); json["@context"] = context();
@ -114,10 +124,11 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApRequest {
.unwrap_or(Outcome::Forward(())) .unwrap_or(Outcome::Forward(()))
} }
} }
pub fn broadcast<S, A, T, C>(sender: &S, act: A, to: Vec<T>, proxy: Option<reqwest::Proxy>) pub fn broadcast<S, A, T, C>(sender: &S, act: A, to: Vec<T>, proxy: Option<reqwest::Proxy>)
where where
S: sign::Signer, S: sign::Signer,
A: Activity, A: Activity + serde::Serialize,
T: inbox::AsActor<C>, T: inbox::AsActor<C>,
{ {
let boxes = to let boxes = to
@ -198,15 +209,12 @@ where
} }
headers.insert("Host", host_header_value.unwrap()); headers.insert("Host", host_header_value.unwrap());
headers.insert("Digest", request::Digest::digest(&body)); headers.insert("Digest", request::Digest::digest(&body));
let request_builder = client headers.insert(
.post(&inbox)
.headers(headers.clone())
.header(
"Signature", "Signature",
request::signature(sender, &headers, ("post", url.path(), url.query())) request::signature(sender, &headers, ("post", url.path(), url.query()))
.expect("activity_pub::broadcast: request signature error"), .expect("activity_pub::broadcast: request signature error"),
) );
.body(body); let request_builder = client.post(&inbox).headers(headers.clone()).body(body);
let _ = tx.send_async(request_builder).await; let _ = tx.send_async(request_builder).await;
} }
drop(tx); drop(tx);
@ -233,46 +241,193 @@ pub trait IntoId {
fn into_id(self) -> Id; fn into_id(self) -> Id;
} }
impl Link for Id {} #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ApSignature { pub struct ApSignature {
#[activitystreams(concrete(PublicKey), functional)] pub public_key: PublicKey,
pub public_key: Option<serde_json::Value>,
} }
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct PublicKey { pub struct PublicKey {
#[activitystreams(concrete(String), functional)] pub id: IriString,
pub id: Option<serde_json::Value>, pub owner: IriString,
pub public_key_pem: String,
#[activitystreams(concrete(String), functional)]
pub owner: Option<serde_json::Value>,
#[activitystreams(concrete(String), functional)]
pub public_key_pem: Option<serde_json::Value>,
} }
#[derive(Clone, Debug, Default, UnitString)] impl<U> UnparsedExtension<U> for ApSignature
#[activitystreams(Hashtag)] where
pub struct HashtagType; 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")] #[serde(rename_all = "camelCase")]
pub struct Hashtag { pub struct SourceProperty {
#[serde(rename = "type")] pub source: Source,
kind: HashtagType,
#[activitystreams(concrete(String), functional)]
pub href: Option<serde_json::Value>,
#[activitystreams(concrete(String), functional)]
pub name: Option<serde_json::Value>,
} }
#[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")] #[serde(rename_all = "camelCase")]
pub struct Source { pub struct Source {
pub media_type: String, pub media_type: String,
@ -280,13 +435,300 @@ pub struct Source {
pub content: String, 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)] fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
#[serde(rename_all = "camelCase")] Ok(Source {
pub struct Licensed { content: unparsed_mut.remove("content")?,
#[activitystreams(concrete(String), functional)] media_type: unparsed_mut.remove("mediaType")?,
pub license: Option<serde_json::Value>, })
} }
impl Object for Licensed {} 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(())
}
}
#[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
View file

@ -1,7 +1,5 @@
#![feature(associated_type_defaults)] #![feature(associated_type_defaults)]
#[macro_use]
extern crate activitystreams_derive;
#[macro_use] #[macro_use]
extern crate shrinkwraprs; extern crate shrinkwraprs;
#[macro_use] #[macro_use]

View file

@ -5,7 +5,6 @@ authors = ["Plume contributors"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
activitypub = "0.1.1"
ammonia = "3.2.0" ammonia = "3.2.0"
bcrypt = "0.12.1" bcrypt = "0.12.1"
guid-create = "0.2" guid-create = "0.2"
@ -35,6 +34,7 @@ riker = "0.4.2"
once_cell = "1.10.0" once_cell = "1.10.0"
lettre = "0.9.6" lettre = "0.9.6"
native-tls = "0.2.10" native-tls = "0.2.10"
activitystreams = "0.7.0-alpha.18"
[dependencies.chrono] [dependencies.chrono]
features = ["serde"] features = ["serde"]

View file

@ -1,12 +1,14 @@
use crate::{ 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, schema::blogs, users::User, Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE,
}; };
use activitypub::{ use activitystreams::{
actor::Group, actor::{ApActor, ApActorExt, AsApActor, Group},
base::AnyBase,
collection::{OrderedCollection, OrderedCollectionPage}, collection::{OrderedCollection, OrderedCollectionPage},
object::Image, iri_string::types::IriString,
CustomObject, object::{kind::ImageType, ApObject, Image, ObjectExt},
prelude::*,
}; };
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SaveChangesDsl}; use diesel::{self, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SaveChangesDsl};
@ -18,14 +20,12 @@ use openssl::{
}; };
use plume_common::activity_pub::{ use plume_common::activity_pub::{
inbox::{AsActor, FromId}, 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::*; use webfinger::*;
pub type CustomGroup = CustomObject<ApSignature, Group>; #[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)]
#[derive(Queryable, Identifiable, Clone, AsChangeset)]
#[changeset_options(treat_none_as_null = "true")] #[changeset_options(treat_none_as_null = "true")]
pub struct Blog { pub struct Blog {
pub id: i32, pub id: i32,
@ -161,104 +161,120 @@ impl Blog {
} }
pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> { pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> {
let mut blog = Group::default(); let mut blog = ApActor::new(self.inbox_url.parse()?, Group::new());
blog.ap_actor_props blog.set_preferred_username(self.actor_id.clone());
.set_preferred_username_string(self.actor_id.clone())?; blog.set_name(self.title.clone());
blog.object_props.set_name_string(self.title.clone())?; blog.set_outbox(self.outbox_url.parse()?);
blog.ap_actor_props blog.set_summary(self.summary_html.to_string());
.set_outbox_string(self.outbox_url.clone())?; let source = SourceProperty {
blog.ap_actor_props source: Source {
.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(), content: self.summary.clone(),
media_type: String::from("text/markdown"), media_type: String::from("text/markdown"),
})?; },
};
let mut icon = Image::default(); let mut icon = Image::new();
icon.object_props.set_url_string( let _ = self.icon_id.map(|id| {
self.icon_id Media::get(conn, id).and_then(|m| {
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok()) let _ = m
.unwrap_or_default(), .url()
)?; .and_then(|url| url.parse::<IriString>().map_err(|_| Error::Url))
icon.object_props.set_attributed_to_link( .map(|url| icon.set_url(url));
self.icon_id icon.set_attributed_to(
.and_then(|id| { User::get(conn, m.owner_id)?
Media::get(conn, id) .into_id()
.and_then(|m| Ok(User::get(conn, m.owner_id)?.into_id())) .parse::<IriString>()?,
.ok() );
Ok(())
}) })
.unwrap_or_else(|| Id::new(String::new())), });
)?; blog.set_icon(icon.into_any_base()?);
blog.object_props.set_icon_object(icon)?;
let mut banner = Image::default(); let mut banner = Image::new();
banner.object_props.set_url_string( let _ = self.banner_id.map(|id| {
self.banner_id Media::get(conn, id).and_then(|m| {
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok()) let _ = m
.unwrap_or_default(), .url()
)?; .and_then(|url| url.parse::<IriString>().map_err(|_| Error::Url))
banner.object_props.set_attributed_to_link( .map(|url| banner.set_url(url));
self.banner_id banner.set_attributed_to(
.and_then(|id| { User::get(conn, m.owner_id)?
Media::get(conn, id) .into_id()
.and_then(|m| Ok(User::get(conn, m.owner_id)?.into_id())) .parse::<IriString>()?,
.ok() );
Ok(())
}) })
.unwrap_or_else(|| Id::new(String::new())), });
)?; blog.set_image(banner.into_any_base()?);
blog.object_props.set_image_object(banner)?;
blog.object_props.set_id_string(self.ap_url.clone())?; blog.set_id(self.ap_url.parse()?);
let mut public_key = PublicKey::default(); let pub_key = PublicKey {
public_key.set_id_string(format!("{}#main-key", self.ap_url))?; id: format!("{}#main-key", self.ap_url).parse()?,
public_key.set_owner_string(self.ap_url.clone())?; owner: self.ap_url.parse()?,
public_key.set_public_key_pem_string(self.public_key.clone())?; public_key_pem: self.public_key.clone(),
let mut ap_signature = ApSignature::default(); };
ap_signature.set_public_key_publickey(public_key)?; 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>> { pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> {
let mut coll = OrderedCollection::default(); self.outbox_collection(conn).map(ActivityStream::new)
coll.collection_props.items = serde_json::to_value(self.get_activities(conn))?; }
coll.collection_props pub fn outbox_collection(&self, conn: &Connection) -> Result<OrderedCollection> {
.set_total_items_u64(self.get_activities(conn).len() as u64)?; let acts = self.get_activities(conn);
coll.collection_props let acts = acts
.set_first_link(Id::new(ap_url(&format!("{}?page=1", &self.outbox_url))))?; .iter()
coll.collection_props .filter_map(|value| AnyBase::from_arbitrary_json(value).ok())
.set_last_link(Id::new(ap_url(&format!( .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={}", "{}?page={}",
&self.outbox_url, &self.outbox_url,
(self.get_activities(conn).len() as u64 + ITEMS_PER_PAGE as u64 - 1) as u64 (n_acts as u64 + ITEMS_PER_PAGE as u64 - 1) as u64 / ITEMS_PER_PAGE as u64
/ ITEMS_PER_PAGE as u64 )
))))?; .parse::<IriString>()?,
Ok(ActivityStream::new(coll)) );
Ok(coll)
} }
pub fn outbox_page( pub fn outbox_page(
&self, &self,
conn: &Connection, conn: &Connection,
(min, max): (i32, i32), (min, max): (i32, i32),
) -> Result<ActivityStream<OrderedCollectionPage>> { ) -> 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)); let acts = self.get_activity_page(conn, (min, max));
//This still doesn't do anything because the outbox //This still doesn't do anything because the outbox
//doesn't do anything yet //doesn't do anything yet
coll.collection_page_props.set_next_link(Id::new(&format!( coll.set_next(
"{}?page={}", format!("{}?page={}", &self.outbox_url, min / ITEMS_PER_PAGE + 1)
&self.outbox_url, .parse::<IriString>()?,
min / ITEMS_PER_PAGE + 1 );
)))?; coll.set_prev(
coll.collection_page_props.set_prev_link(Id::new(&format!( format!("{}?page={}", &self.outbox_url, min / ITEMS_PER_PAGE - 1)
"{}?page={}", .parse::<IriString>()?,
&self.outbox_url, );
min / ITEMS_PER_PAGE - 1 coll.set_many_items(
)))?; acts.iter()
coll.collection_props.items = serde_json::to_value(acts)?; .filter_map(|value| AnyBase::from_arbitrary_json(value).ok()),
Ok(ActivityStream::new(coll)) );
Ok(coll)
} }
fn get_activities(&self, _conn: &Connection) -> Vec<serde_json::Value> { fn get_activities(&self, _conn: &Connection) -> Vec<serde_json::Value> {
vec![] vec![]
@ -354,9 +370,90 @@ impl FromId<DbConn> for Blog {
} }
fn from_activity(conn: &DbConn, acct: CustomGroup) -> Result<Self> { fn from_activity(conn: &DbConn, acct: CustomGroup) -> Result<Self> {
let url = Url::parse(&acct.object.object_props.id_string()?)?; let (name, outbox_url, inbox_url) = {
let inst = url.host_str().ok_or(Error::Url)?; let actor = acct.ap_actor_ref();
let instance = Instance::find_by_domain(conn, inst).or_else(|_| { 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( Instance::insert(
conn, conn,
NewInstance { NewInstance {
@ -373,75 +470,9 @@ impl FromId<DbConn> for Blog {
}, },
) )
})?; })?;
let icon_id = acct new_blog.instance_id = instance.id;
.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);
let banner_id = acct Blog::insert(conn, new_blog)
.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,
},
)
} }
fn get_sender() -> &'static dyn sign::Signer { 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, blog_authors::*, instance::tests as instance_tests, medias::NewMedia, tests::db,
users::tests as usersTests, Connection as Conn, users::tests as usersTests, Connection as Conn,
}; };
use assert_json_diff::assert_json_eq;
use diesel::Connection; use diesel::Connection;
use serde_json::to_value;
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Blog>) { pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Blog>) {
instance_tests::fill_database(conn); instance_tests::fill_database(conn);
let users = usersTests::fill_database(conn); let users = usersTests::fill_database(conn);
let blog1 = Blog::insert( let mut blog1 = Blog::insert(
conn, conn,
NewBlog::new_local( NewBlog::new_local(
"BlogName".to_owned(), "BlogName".to_owned(),
@ -590,6 +623,41 @@ pub(crate) mod tests {
}, },
) )
.unwrap(); .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]) (users, vec![blog1, blog2, blog3])
} }
@ -886,7 +954,6 @@ pub(crate) mod tests {
.id, .id,
); );
let _: Blog = blogs[0].save_changes(&**conn).unwrap(); let _: Blog = blogs[0].save_changes(&**conn).unwrap();
let ap_repr = blogs[0].to_activity(&conn).unwrap(); let ap_repr = blogs[0].to_activity(&conn).unwrap();
blogs[0].delete(&conn).unwrap(); blogs[0].delete(&conn).unwrap();
let blog = Blog::from_activity(&conn, ap_repr).unwrap(); let blog = Blog::from_activity(&conn, ap_repr).unwrap();
@ -907,4 +974,90 @@ pub(crate) mod tests {
Ok(()) 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(())
});
}
} }

View file

@ -11,18 +11,23 @@ use crate::{
users::User, users::User,
Connection, Error, Result, CONFIG, Connection, Error, Result, CONFIG,
}; };
use activitypub::{ use activitystreams::{
activity::{Create, Delete}, activity::{Create, Delete},
link, base::{AnyBase, Base},
iri_string::types::IriString,
link::{self, kind::MentionType},
object::{Note, Tombstone}, 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 diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use plume_common::{ use plume_common::{
activity_pub::{ activity_pub::{
inbox::{AsActor, AsObject, FromId}, inbox::{AsActor, AsObject, FromId},
sign::Signer, sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY, IntoId, ToAsString, ToAsUri, PUBLIC_VISIBILITY,
}, },
utils, utils,
}; };
@ -115,29 +120,32 @@ impl Comment {
Some(Media::get_media_processor(conn, vec![&author])), Some(Media::get_media_processor(conn, vec![&author])),
); );
let mut note = Note::default(); let mut note = Note::new();
let to = vec![Id::new(PUBLIC_VISIBILITY.to_string())]; let to = vec![PUBLIC_VISIBILITY.parse::<IriString>()?];
note.object_props note.set_id(
.set_id_string(self.ap_url.clone().unwrap_or_default())?; self.ap_url
note.object_props .clone()
.set_summary_string(self.spoiler_text.clone())?; .unwrap_or_default()
note.object_props.set_content_string(html)?; .parse::<IriString>()?,
note.object_props );
.set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else( note.set_summary(self.spoiler_text.clone());
|| Ok(Post::get(conn, self.post_id)?.ap_url), note.set_content(html);
|id| Ok(Comment::get(conn, id)?.ap_url.unwrap_or_default()) as Result<String>, note.set_in_reply_to(self.in_response_to_id.map_or_else(
)?))?; || Post::get(conn, self.post_id).map(|post| post.ap_url),
note.object_props |id| Comment::get(conn, id).map(|comment| comment.ap_url.unwrap_or_default()),
.set_published_utctime(Utc.from_utc_datetime(&self.creation_date))?; )?);
note.object_props.set_attributed_to_link(author.into_id())?; note.set_published(
note.object_props.set_to_link_vec(to)?; OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos().into())
note.object_props.set_tag_link_vec( .expect("OffsetDateTime"),
mentions );
.into_iter() note.set_attributed_to(author.into_id().parse::<IriString>()?);
.filter_map(|m| Mention::build_activity(conn, &m).ok()) note.set_many_tos(to);
.collect::<Vec<link::Mention>>(), 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) Ok(note)
} }
@ -145,17 +153,26 @@ impl Comment {
let author = User::get(conn, self.author_id)?; let author = User::get(conn, self.author_id)?;
let note = self.to_activity(conn)?; let note = self.to_activity(conn)?;
let mut act = Create::default(); let note_clone = note.clone();
act.create_props.set_actor_link(author.into_id())?;
act.create_props.set_object_object(note.clone())?; let mut act = Create::new(
act.object_props.set_id_string(format!( author.into_id().parse::<IriString>()?,
Base::retract(note)?.into_generic()?,
);
act.set_id(
format!(
"{}/activity", "{}/activity",
self.ap_url.clone().ok_or(Error::MissingApProperty)?, self.ap_url.clone().ok_or(Error::MissingApProperty)?,
))?; )
act.object_props .parse::<IriString>()?,
.set_to_link_vec(note.object_props.to_link_vec::<Id>()?)?; );
act.object_props act.set_many_tos(
.set_cc_link_vec(vec![Id::new(self.get_author(conn)?.followers_endpoint)])?; 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) Ok(act)
} }
@ -180,20 +197,21 @@ impl Comment {
} }
pub fn build_delete(&self, conn: &Connection) -> Result<Delete> { pub fn build_delete(&self, conn: &Connection) -> Result<Delete> {
let mut act = Delete::default(); let mut tombstone = Tombstone::new();
act.delete_props tombstone.set_id(
.set_actor_link(self.get_author(conn)?.into_id())?; self.ap_url
.as_ref()
.ok_or(Error::MissingApProperty)?
.parse::<IriString>()?,
);
let mut tombstone = Tombstone::default(); let mut act = Delete::new(
tombstone self.get_author(conn)?.into_id().parse::<IriString>()?,
.object_props Base::retract(tombstone)?.into_generic()?,
.set_id_string(self.ap_url.clone().ok_or(Error::MissingApProperty)?)?; );
act.delete_props.set_object_object(tombstone)?;
act.object_props act.set_id(format!("{}#delete", self.ap_url.clone().unwrap()).parse::<IriString>()?);
.set_id_string(format!("{}#delete", self.ap_url.clone().unwrap()))?; act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
act.object_props
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?;
Ok(act) Ok(act)
} }
@ -210,102 +228,104 @@ impl FromId<DbConn> for Comment {
fn from_activity(conn: &DbConn, note: Note) -> Result<Self> { fn from_activity(conn: &DbConn, note: Note) -> Result<Self> {
let comm = { let comm = {
let previous_url = note let previous_url = note
.object_props .in_reply_to()
.in_reply_to
.as_ref()
.ok_or(Error::MissingApProperty)? .ok_or(Error::MissingApProperty)?
.as_str()
.ok_or(Error::MissingApProperty)?;
let previous_comment = Comment::find_by_ap_url(conn, previous_url);
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() .iter()
.filter_map(serde_json::Value::as_str) .next()
.any(|s| s == PUBLIC_VISIBILITY), .ok_or(Error::MissingApProperty)?
serde_json::Value::String(s) => s == PUBLIC_VISIBILITY, .id()
_ => false, .ok_or(Error::MissingApProperty)?;
let previous_comment = Comment::find_by_ap_url(conn, previous_url.as_str());
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(&note.object_props.to) let public_visibility = is_public(&note.to())
|| is_public(&note.object_props.bto) || is_public(&note.bto())
|| is_public(&note.object_props.cc) || is_public(&note.cc())
|| is_public(&note.object_props.bcc); || is_public(&note.bcc());
let summary = note.summary().and_then(|summary| summary.to_as_string());
let sensitive = summary.is_some();
let comm = Comment::insert( let comm = Comment::insert(
conn, conn,
NewComment { NewComment {
content: SafeString::new(&note.object_props.content_string()?), content: SafeString::new(
spoiler_text: note.object_props.summary_string().unwrap_or_default(), &note
ap_url: note.object_props.id_string().ok(), .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(), in_response_to_id: previous_comment.iter().map(|c| c.id).next(),
post_id: previous_comment.map(|c| c.post_id).or_else(|_| { 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( author_id: User::from_id(
conn, conn,
&note.object_props.attributed_to_link::<Id>()?, &note
.attributed_to()
.ok_or(Error::MissingApProperty)?
.to_as_uri()
.ok_or(Error::MissingApProperty)?,
None, None,
CONFIG.proxy(), CONFIG.proxy(),
) )
.map_err(|(_, e)| e)? .map_err(|(_, e)| e)?
.id, .id,
sensitive: note.object_props.summary_string().is_ok(), sensitive,
public_visibility, public_visibility,
}, },
)?; )?;
// save mentions // save mentions
if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() { if let Some(tags) = note.tag() {
for tag in tags { let author_url = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0].ap_url;
serde_json::from_value::<link::Mention>(tag) for tag in tags.iter() {
.map_err(Error::from) let m = tag.clone().extend::<link::Mention, MentionType>()?; // FIXME: Don't clone
.and_then(|m| { if m.is_none() {
let author = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0]; continue;
let not_author = m.link_props.href_string()? != author.ap_url.clone(); }
Mention::from_activity(conn, &m, comm.id, false, not_author) let m = m.unwrap();
}) let not_author = m.href().ok_or(Error::MissingApProperty)? != author_url;
.ok(); let _ = Mention::from_activity(conn, &m, comm.id, false, not_author);
} }
} }
comm comm
}; };
if !comm.public_visibility { if !comm.public_visibility {
let receivers_ap_url = |v: Option<serde_json::Value>| { let mut receiver_ids = HashSet::new();
let filter = |e: serde_json::Value| { let mut receivers_id = |v: Option<&'_ OneOrMany<AnyBase>>| {
if let serde_json::Value::String(s) = e { if let Some(one_or_many) = v {
Some(s) for any_base in one_or_many.iter() {
} else { if let Some(id) = any_base.id() {
None 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 receivers_ap_url = receiver_ids
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)
.into_iter() .into_iter()
.flat_map(|v| { .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] vec![user]
} else { } else {
vec![] // TODO try to fetch collection vec![] // TODO try to fetch collection

View file

@ -2,7 +2,12 @@ use crate::{
ap_url, db_conn::DbConn, instance::Instance, notifications::*, schema::follows, users::User, ap_url, db_conn::DbConn, instance::Instance, notifications::*, schema::follows, users::User,
Connection, Error, Result, CONFIG, 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 diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use plume_common::activity_pub::{ use plume_common::activity_pub::{
broadcast, broadcast,
@ -53,15 +58,13 @@ impl Follow {
pub fn to_activity(&self, conn: &Connection) -> Result<FollowAct> { pub fn to_activity(&self, conn: &Connection) -> Result<FollowAct> {
let user = User::get(conn, self.follower_id)?; let user = User::get(conn, self.follower_id)?;
let target = User::get(conn, self.following_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) Ok(act)
} }
@ -94,7 +97,11 @@ impl Follow {
NewFollow { NewFollow {
follower_id: from_id, follower_id: from_id,
following_id: target_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)?; res.notify(conn)?;
@ -115,39 +122,35 @@ impl Follow {
target: &A, target: &A,
follow: FollowAct, follow: FollowAct,
) -> Result<Accept> { ) -> 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!( let accept_id = ap_url(&format!(
"{}/follows/{}/accept", "{}/follows/{}/accept",
CONFIG.base_url.as_str(), CONFIG.base_url.as_str(),
self.id self.id
)); ));
accept.object_props.set_id_string(accept_id)?; accept.set_id(accept_id.parse::<IriString>()?);
accept accept.set_many_tos(vec![from.clone().into_id().parse::<IriString>()?]);
.object_props accept.set_many_ccs(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
.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)?;
Ok(accept) Ok(accept)
} }
pub fn build_undo(&self, conn: &Connection) -> Result<Undo> { pub fn build_undo(&self, conn: &Connection) -> Result<Undo> {
let mut undo = Undo::default(); let mut undo = Undo::new(
undo.undo_props User::get(conn, self.follower_id)?
.set_actor_link(User::get(conn, self.follower_id)?.into_id())?; .ap_url
undo.object_props .parse::<IriString>()?,
.set_id_string(format!("{}/undo", self.ap_url))?; self.ap_url.parse::<IriString>()?,
undo.undo_props );
.set_object_link::<Id>(self.clone().into_id())?; undo.set_id(format!("{}/undo", self.ap_url).parse::<IriString>()?);
undo.object_props undo.set_many_tos(vec![User::get(conn, self.following_id)?
.set_to_link_vec(vec![User::get(conn, self.following_id)?.into_id()])?; .ap_url
undo.object_props .parse::<IriString>()?]);
.set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?; undo.set_many_ccs(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
Ok(undo) Ok(undo)
} }
} }
@ -159,11 +162,7 @@ impl AsObject<User, FollowAct, &DbConn> for User {
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Follow> { fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Follow> {
// Mastodon (at least) requires the full Follow object when accepting it, // Mastodon (at least) requires the full Follow object when accepting it,
// so we rebuilt it here // so we rebuilt it here
let mut follow = FollowAct::default(); let follow = FollowAct::new(actor.ap_url.parse::<IriString>()?, id.parse::<IriString>()?);
follow.object_props.set_id_string(id.to_string())?;
follow
.follow_props
.set_actor_link::<Id>(actor.clone().into_id())?;
Follow::accept_follow(conn, &actor, &self, follow, actor.id, self.id) 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> { fn from_activity(conn: &DbConn, follow: FollowAct) -> Result<Self> {
let actor = User::from_id( let actor = User::from_id(
conn, conn,
&follow.follow_props.actor_link::<Id>()?, follow
.actor_field_ref()
.as_single_id()
.ok_or(Error::MissingApProperty)?
.as_str(),
None, None,
CONFIG.proxy(), CONFIG.proxy(),
) )
@ -187,7 +190,11 @@ impl FromId<DbConn> for Follow {
let target = User::from_id( let target = User::from_id(
conn, conn,
&follow.follow_props.object_link::<Id>()?, follow
.object_field_ref()
.as_single_id()
.ok_or(Error::MissingApProperty)?
.as_str(),
None, None,
CONFIG.proxy(), CONFIG.proxy(),
) )

View file

@ -1,4 +1,4 @@
use activitypub::activity::*; use activitystreams::activity::{Announce, Create, Delete, Follow, Like, Undo, Update};
use crate::{ use crate::{
comments::Comment, comments::Comment,
@ -94,8 +94,8 @@ pub(crate) mod tests {
license: "WTFPL".to_owned(), license: "WTFPL".to_owned(),
creation_date: None, creation_date: None,
ap_url: format!("https://plu.me/~/{}/testing", blogs[0].actor_id), ap_url: format!("https://plu.me/~/{}/testing", blogs[0].actor_id),
subtitle: String::new(), subtitle: "Bye".to_string(),
source: String::new(), source: "Hello".to_string(),
cover_id: None, cover_id: None,
}, },
) )
@ -268,7 +268,7 @@ pub(crate) mod tests {
"actor": users[0].ap_url, "actor": users[0].ap_url,
"object": { "object": {
"type": "Article", "type": "Article",
"id": "https://plu.me/~/Blog/my-article", "id": "https://plu.me/~/BlogName/testing",
"attributedTo": [users[0].ap_url, blogs[0].ap_url], "attributedTo": [users[0].ap_url, blogs[0].ap_url],
"content": "Hello.", "content": "Hello.",
"name": "My Article", "name": "My Article",
@ -286,11 +286,11 @@ pub(crate) mod tests {
match super::inbox(&conn, act).unwrap() { match super::inbox(&conn, act).unwrap() {
super::InboxResult::Post(p) => { super::InboxResult::Post(p) => {
assert!(p.is_author(&conn, users[0].id).unwrap()); 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.blog_id, blogs[0].id);
assert_eq!(p.content, SafeString::new("Hello.")); assert_eq!(p.content, SafeString::new("Hello"));
assert_eq!(p.subtitle, "Bye.".to_owned()); assert_eq!(p.subtitle, "Bye".to_owned());
assert_eq!(p.title, "My Article".to_owned()); assert_eq!(p.title, "Testing".to_owned());
} }
_ => panic!("Unexpected result"), _ => panic!("Unexpected result"),
}; };

View file

@ -16,6 +16,7 @@ extern crate serde_json;
#[macro_use] #[macro_use]
extern crate tantivy; extern crate tantivy;
use activitystreams::iri_string;
pub use lettre; pub use lettre;
pub use lettre::smtp; pub use lettre::smtp;
use once_cell::sync::Lazy; 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 { impl From<serde_json::Error> for Error {
fn from(_: serde_json::Error) -> Self { fn from(_: serde_json::Error) -> Self {
Error::SerDe Error::SerDe
@ -118,12 +125,9 @@ impl From<reqwest::header::InvalidHeaderValue> for Error {
} }
} }
impl From<activitypub::Error> for Error { impl From<activitystreams::checked::CheckError> for Error {
fn from(err: activitypub::Error) -> Self { fn from(_: activitystreams::checked::CheckError) -> Error {
match err { Error::MissingApProperty
activitypub::Error::NotFound => Error::MissingApProperty,
_ => Error::SerDe,
}
} }
} }

View file

@ -2,13 +2,18 @@ use crate::{
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*, db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*,
users::User, Connection, Error, Result, CONFIG, 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 chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{ use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId}, inbox::{AsActor, AsObject, FromId},
sign::Signer, sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY, PUBLIC_VISIBILITY,
}; };
#[derive(Clone, Queryable, Identifiable)] #[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_ap_url, ap_url as &str);
find_by!(likes, find_by_user_on_post, user_id as i32, post_id as i32); 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> { pub fn to_activity(&self, conn: &Connection) -> Result<LikeAct> {
let mut act = activity::Like::default(); let mut act = LikeAct::new(
act.like_props User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?,
.set_actor_link(User::get(conn, self.user_id)?.into_id())?; Post::get(conn, self.post_id)?.ap_url.parse::<IriString>()?,
act.like_props );
.set_object_link(Post::get(conn, self.post_id)?.into_id())?; act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
act.object_props act.set_many_ccs(vec![User::get(conn, self.user_id)?
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?; .followers_endpoint
act.object_props.set_cc_link_vec(vec![Id::new( .parse::<IriString>()?]);
User::get(conn, self.user_id)?.followers_endpoint, act.set_id(self.ap_url.parse::<IriString>()?);
)])?;
act.object_props.set_id_string(self.ap_url.clone())?;
Ok(act) Ok(act)
} }
@ -67,24 +70,22 @@ impl Like {
Ok(()) Ok(())
} }
pub fn build_undo(&self, conn: &Connection) -> Result<activity::Undo> { pub fn build_undo(&self, conn: &Connection) -> Result<Undo> {
let mut act = activity::Undo::default(); let mut act = Undo::new(
act.undo_props User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?,
.set_actor_link(User::get(conn, self.user_id)?.into_id())?; AnyBase::from_extended(self.to_activity(conn)?)?,
act.undo_props.set_object_object(self.to_activity(conn)?)?; );
act.object_props act.set_id(format!("{}#delete", self.ap_url).parse::<IriString>()?);
.set_id_string(format!("{}#delete", self.ap_url))?; act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
act.object_props act.set_many_ccs(vec![User::get(conn, self.user_id)?
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?; .followers_endpoint
act.object_props.set_cc_link_vec(vec![Id::new( .parse::<IriString>()?]);
User::get(conn, self.user_id)?.followers_endpoint,
)])?;
Ok(act) Ok(act)
} }
} }
impl AsObject<User, activity::Like, &DbConn> for Post { impl AsObject<User, LikeAct, &DbConn> for Post {
type Error = Error; type Error = Error;
type Output = Like; type Output = Like;
@ -106,19 +107,22 @@ impl AsObject<User, activity::Like, &DbConn> for Post {
impl FromId<DbConn> for Like { impl FromId<DbConn> for Like {
type Error = Error; type Error = Error;
type Object = activity::Like; type Object = LikeAct;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> { fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
Like::find_by_ap_url(conn, id) 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( let res = Like::insert(
conn, conn,
NewLike { NewLike {
post_id: Post::from_id( post_id: Post::from_id(
conn, conn,
&act.like_props.object_link::<Id>()?, act.object_field_ref()
.as_single_id()
.ok_or(Error::MissingApProperty)?
.as_str(),
None, None,
CONFIG.proxy(), CONFIG.proxy(),
) )
@ -126,13 +130,19 @@ impl FromId<DbConn> for Like {
.id, .id,
user_id: User::from_id( user_id: User::from_id(
conn, conn,
&act.like_props.actor_link::<Id>()?, act.actor_field_ref()
.as_single_id()
.ok_or(Error::MissingApProperty)?
.as_str(),
None, None,
CONFIG.proxy(), CONFIG.proxy(),
) )
.map_err(|(_, e)| e)? .map_err(|(_, e)| e)?
.id, .id,
ap_url: act.object_props.id_string()?, ap_url: act
.id_unchecked()
.ok_or(Error::MissingApProperty)?
.to_string(),
}, },
)?; )?;
res.notify(conn)?; 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 Error = Error;
type Output = (); type Output = ();

View file

@ -2,11 +2,11 @@ use crate::{
ap_url, db_conn::DbConn, instance::Instance, safe_string::SafeString, schema::medias, ap_url, db_conn::DbConn, instance::Instance, safe_string::SafeString, schema::medias,
users::User, Connection, Error, Result, CONFIG, users::User, Connection, Error, Result, CONFIG,
}; };
use activitypub::object::Image; use activitystreams::{object::Image, prelude::*};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use guid_create::GUID; use guid_create::GUID;
use plume_common::{ use plume_common::{
activity_pub::{inbox::FromId, request, Id}, activity_pub::{inbox::FromId, request, ToAsString, ToAsUri},
utils::{escape, MediaProcessor}, utils::{escape, MediaProcessor},
}; };
use std::{ use std::{
@ -208,9 +208,9 @@ impl Media {
// TODO: merge with save_remote? // TODO: merge with save_remote?
pub fn from_activity(conn: &DbConn, image: &Image) -> Result<Media> { pub fn from_activity(conn: &DbConn, image: &Image) -> Result<Media> {
let remote_url = image let remote_url = image
.object_props .url()
.url_string() .and_then(|url| url.to_as_uri())
.or(Err(Error::MissingApProperty))?; .ok_or(Error::MissingApProperty)?;
let path = determine_mirror_file_path(&remote_url); let path = determine_mirror_file_path(&remote_url);
let parent = path.parent().ok_or(Error::InvalidValue)?; let parent = path.parent().ok_or(Error::InvalidValue)?;
if !parent.is_dir() { if !parent.is_dir() {
@ -231,11 +231,12 @@ impl Media {
let mut updated = false; let mut updated = false;
let alt_text = image let alt_text = image
.object_props .content()
.content_string() .and_then(|content| content.to_as_string())
.or(Err(Error::NotFound))?; .ok_or(Error::NotFound)?;
let sensitive = image.object_props.summary_string().is_ok(); let summary = image.summary().and_then(|summary| summary.to_as_string());
let content_warning = image.object_props.summary_string().ok(); let sensitive = summary.is_some();
let content_warning = summary;
if media.alt_text != alt_text { if media.alt_text != alt_text {
media.alt_text = alt_text; media.alt_text = alt_text;
updated = true; updated = true;
@ -262,28 +263,25 @@ impl Media {
Ok(media) Ok(media)
}) })
.or_else(|_| { .or_else(|_| {
let summary = image.summary().and_then(|summary| summary.to_as_string());
Media::insert( Media::insert(
conn, conn,
NewMedia { NewMedia {
file_path: path.to_str().ok_or(Error::InvalidValue)?.to_string(), file_path: path.to_str().ok_or(Error::InvalidValue)?.to_string(),
alt_text: image alt_text: image
.object_props .content()
.content_string() .and_then(|content| content.to_as_string())
.or(Err(Error::NotFound))?, .ok_or(Error::NotFound)?,
is_remote: false, is_remote: false,
remote_url: None, remote_url: None,
sensitive: image.object_props.summary_string().is_ok(), sensitive: summary.is_some(),
content_warning: image.object_props.summary_string().ok(), content_warning: summary,
owner_id: User::from_id( owner_id: User::from_id(
conn, conn,
image &image
.object_props .attributed_to()
.attributed_to_link_vec::<Id>() .and_then(|attributed_to| attributed_to.to_as_uri())
.or(Err(Error::NotFound))? .ok_or(Error::MissingApProperty)?,
.into_iter()
.next()
.ok_or(Error::NotFound)?
.as_ref(),
None, None,
CONFIG.proxy(), CONFIG.proxy(),
) )

View file

@ -2,7 +2,11 @@ use crate::{
comments::Comment, db_conn::DbConn, notifications::*, posts::Post, schema::mentions, comments::Comment, db_conn::DbConn, notifications::*, posts::Post, schema::mentions,
users::User, Connection, Error, Result, 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 diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::inbox::AsActor; use plume_common::activity_pub::inbox::AsActor;
@ -58,19 +62,17 @@ impl Mention {
pub fn build_activity(conn: &DbConn, ment: &str) -> Result<link::Mention> { pub fn build_activity(conn: &DbConn, ment: &str) -> Result<link::Mention> {
let user = User::find_by_fqn(conn, ment)?; let user = User::find_by_fqn(conn, ment)?;
let mut mention = link::Mention::default(); let mut mention = link::Mention::new();
mention.link_props.set_href_string(user.ap_url)?; mention.set_href(user.ap_url.parse::<IriString>()?);
mention.link_props.set_name_string(format!("@{}", ment))?; mention.set_name(format!("@{}", ment));
Ok(mention) Ok(mention)
} }
pub fn to_activity(&self, conn: &Connection) -> Result<link::Mention> { pub fn to_activity(&self, conn: &Connection) -> Result<link::Mention> {
let user = self.get_mentioned(conn)?; let user = self.get_mentioned(conn)?;
let mut mention = link::Mention::default(); let mut mention = link::Mention::new();
mention.link_props.set_href_string(user.ap_url.clone())?; mention.set_href(user.ap_url.parse::<IriString>()?);
mention mention.set_name(format!("@{}", user.fqn));
.link_props
.set_name_string(format!("@{}", user.fqn))?;
Ok(mention) Ok(mention)
} }
@ -81,8 +83,8 @@ impl Mention {
in_post: bool, in_post: bool,
notify: bool, notify: bool,
) -> Result<Self> { ) -> Result<Self> {
let ap_url = ment.link_props.href_string().or(Err(Error::NotFound))?; let ap_url = ment.href().ok_or(Error::NotFound)?.as_str();
let mentioned = User::find_by_ap_url(conn, &ap_url)?; let mentioned = User::find_by_ap_url(conn, ap_url)?;
if in_post { if in_post {
Post::get(conn, inside).and_then(|post| { Post::get(conn, inside).and_then(|post| {

View file

@ -3,20 +3,24 @@ use crate::{
post_authors::*, safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User, post_authors::*, safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User,
Connection, Error, PostEvent::*, Result, CONFIG, POST_CHAN, Connection, Error, PostEvent::*, Result, CONFIG, POST_CHAN,
}; };
use activitypub::{ use activitystreams::{
activity::{Create, Delete, Update}, activity::{Create, Delete, Update},
link, base::{AnyBase, Base},
object::{Article, Image, Tombstone}, iri_string::types::IriString,
CustomObject, 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 diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use plume_common::{ use plume_common::{
activity_pub::{ activity_pub::{
inbox::{AsActor, AsObject, FromId}, inbox::{AsActor, AsObject, FromId},
sign::Signer, 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}, utils::{iri_percent_encode_seg, md_to_html},
}; };
@ -24,8 +28,6 @@ use riker::actors::{Publish, Tell};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex}; 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())); static BLOG_FQN_CACHE: Lazy<Mutex<HashMap<i32, String>>> = Lazy::new(|| Mutex::new(HashMap::new()));
#[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)] #[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)]
@ -353,92 +355,92 @@ impl Post {
.collect::<Vec<serde_json::Value>>(); .collect::<Vec<serde_json::Value>>();
mentions_json.append(&mut tags_json); mentions_json.append(&mut tags_json);
let mut article = Article::default(); let mut article = ApObject::new(Article::new());
article.object_props.set_name_string(self.title.clone())?; article.set_name(self.title.clone());
article.object_props.set_id_string(self.ap_url.clone())?; article.set_id(self.ap_url.parse::<IriString>()?);
let mut authors = self let mut authors = self
.get_authors(conn)? .get_authors(conn)?
.into_iter() .into_iter()
.map(|x| Id::new(x.ap_url)) .filter_map(|x| x.ap_url.parse::<IriString>().ok())
.collect::<Vec<Id>>(); .collect::<Vec<IriString>>();
authors.push(self.get_blog(conn)?.into_id()); // add the blog URL here too authors.push(self.get_blog(conn)?.ap_url.parse::<IriString>()?); // add the blog URL here too
article article.set_many_attributed_tos(authors);
.object_props article.set_content(self.content.get().clone());
.set_attributed_to_link_vec::<Id>(authors)?; let source = AnyBase::from_arbitrary_json(serde_json::json!({
article "content": self.source,
.object_props "mediaType": "text/markdown",
.set_content_string(self.content.get().clone())?; }))?;
article.ap_object_props.set_source_object(Source { article.set_source(source);
content: self.source.clone(), article.set_published(
media_type: String::from("text/markdown"), OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos().into())
})?; .expect("OffsetDateTime"),
article );
.object_props article.set_summary(&*self.subtitle);
.set_published_utctime(Utc.from_utc_datetime(&self.creation_date))?; article.set_many_tags(
article mentions_json
.object_props .iter()
.set_summary_string(self.subtitle.clone())?; .filter_map(|mention_json| AnyBase::from_arbitrary_json(mention_json).ok()),
article.object_props.tag = Some(json!(mentions_json)); );
if let Some(media_id) = self.cover_id { if let Some(media_id) = self.cover_id {
let media = Media::get(conn, media_id)?; let media = Media::get(conn, media_id)?;
let mut cover = Image::default(); let mut cover = Image::new();
cover.object_props.set_url_string(media.url()?)?; cover.set_url(media.url()?);
if media.sensitive { if media.sensitive {
cover cover.set_summary(media.content_warning.unwrap_or_default());
.object_props
.set_summary_string(media.content_warning.unwrap_or_default())?;
} }
cover.object_props.set_content_string(media.alt_text)?; cover.set_content(media.alt_text);
cover cover.set_many_attributed_tos(vec![User::get(conn, media.owner_id)?
.object_props .ap_url
.set_attributed_to_link_vec(vec![User::get(conn, media.owner_id)?.into_id()])?; .parse::<IriString>()?]);
article.object_props.set_icon_object(cover)?; article.set_icon(cover.into_any_base()?);
} }
article.object_props.set_url_string(self.ap_url.clone())?; article.set_url(self.ap_url.parse::<IriString>()?);
article article.set_many_tos(
.object_props to.into_iter()
.set_to_link_vec::<Id>(to.into_iter().map(Id::new).collect())?; .filter_map(|to| to.parse::<IriString>().ok())
article .collect::<Vec<IriString>>(),
.object_props );
.set_cc_link_vec::<Id>(cc.into_iter().map(Id::new).collect())?; article.set_many_ccs(
let mut license = Licensed::default(); cc.into_iter()
license.set_license_string(self.license.clone())?; .filter_map(|cc| cc.parse::<IriString>().ok())
.collect::<Vec<IriString>>(),
);
let license = Licensed {
license: Some(self.license.clone()),
};
Ok(LicensedArticle::new(article, license)) Ok(LicensedArticle::new(article, license))
} }
pub fn create_activity(&self, conn: &Connection) -> Result<Create> { pub fn create_activity(&self, conn: &Connection) -> Result<Create> {
let article = self.to_activity(conn)?; let article = self.to_activity(conn)?;
let mut act = Create::default(); let to = article.to().ok_or(Error::MissingApProperty)?.clone();
act.object_props let cc = article.cc().ok_or(Error::MissingApProperty)?.clone();
.set_id_string(format!("{}/activity", self.ap_url))?; let mut act = Create::new(
act.object_props self.get_authors(conn)?[0].ap_url.parse::<IriString>()?,
.set_to_link_vec::<Id>(article.object.object_props.to_link_vec()?)?; Base::retract(article)?.into_generic()?,
act.object_props );
.set_cc_link_vec::<Id>(article.object.object_props.cc_link_vec()?)?; act.set_id(format!("{}/activity", self.ap_url).parse::<IriString>()?);
act.create_props act.set_many_tos(to);
.set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?; act.set_many_ccs(cc);
act.create_props.set_object_object(article)?;
Ok(act) Ok(act)
} }
pub fn update_activity(&self, conn: &Connection) -> Result<Update> { pub fn update_activity(&self, conn: &Connection) -> Result<Update> {
let article = self.to_activity(conn)?; let article = self.to_activity(conn)?;
let mut act = Update::default(); let to = article.to().ok_or(Error::MissingApProperty)?.clone();
act.object_props.set_id_string(format!( let cc = article.cc().ok_or(Error::MissingApProperty)?.clone();
"{}/update-{}", let mut act = Update::new(
self.ap_url, self.get_authors(conn)?[0].ap_url.parse::<IriString>()?,
Utc::now().timestamp() Base::retract(article)?.into_generic()?,
))?; );
act.object_props act.set_id(
.set_to_link_vec::<Id>(article.object.object_props.to_link_vec()?)?; format!("{}/update-{}", self.ap_url, Utc::now().timestamp()).parse::<IriString>()?,
act.object_props );
.set_cc_link_vec::<Id>(article.object.object_props.cc_link_vec()?)?; act.set_many_tos(to);
act.update_props act.set_many_ccs(cc);
.set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?;
act.update_props.set_object_object(article)?;
Ok(act) Ok(act)
} }
@ -447,10 +449,8 @@ impl Post {
.into_iter() .into_iter()
.map(|m| { .map(|m| {
( (
m.link_props m.href()
.href_string() .and_then(|ap_url| User::find_by_ap_url(conn, ap_url.as_ref()).ok())
.ok()
.and_then(|ap_url| User::find_by_ap_url(conn, &ap_url).ok())
.map(|u| u.id), .map(|u| u.id),
m, m,
) )
@ -485,7 +485,7 @@ impl Post {
pub fn update_tags(&self, conn: &Connection, tags: Vec<Hashtag>) -> Result<()> { pub fn update_tags(&self, conn: &Connection, tags: Vec<Hashtag>) -> Result<()> {
let tags_name = tags let tags_name = tags
.iter() .iter()
.filter_map(|t| t.name_string().ok()) .filter_map(|t| t.name.as_ref().map(|name| name.as_str().to_string()))
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
let old_tags = Tag::for_post(&*conn, self.id)?; let old_tags = Tag::for_post(&*conn, self.id)?;
@ -502,8 +502,9 @@ impl Post {
for t in tags { for t in tags {
if !t if !t
.name_string() .name
.map(|n| old_tags_name.contains(&n)) .as_ref()
.map(|n| old_tags_name.contains(n.as_str()))
.unwrap_or(true) .unwrap_or(true)
{ {
Tag::from_activity(conn, &t, self.id, false)?; 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<()> { pub fn update_hashtags(&self, conn: &Connection, tags: Vec<Hashtag>) -> Result<()> {
let tags_name = tags let tags_name = tags
.iter() .iter()
.filter_map(|t| t.name_string().ok()) .filter_map(|t| t.name.as_ref().map(|name| name.as_str().to_string()))
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
let old_tags = Tag::for_post(&*conn, self.id)?; let old_tags = Tag::for_post(&*conn, self.id)?;
@ -538,8 +539,9 @@ impl Post {
for t in tags { for t in tags {
if !t if !t
.name_string() .name
.map(|n| old_tags_name.contains(&n)) .as_ref()
.map(|n| old_tags_name.contains(n.as_str()))
.unwrap_or(true) .unwrap_or(true)
{ {
Tag::from_activity(conn, &t, self.id, true)?; Tag::from_activity(conn, &t, self.id, true)?;
@ -566,18 +568,19 @@ impl Post {
} }
pub fn build_delete(&self, conn: &Connection) -> Result<Delete> { pub fn build_delete(&self, conn: &Connection) -> Result<Delete> {
let mut act = Delete::default(); let mut tombstone = Tombstone::new();
act.delete_props tombstone.set_id(self.ap_url.parse()?);
.set_actor_link(self.get_authors(conn)?[0].clone().into_id())?;
let mut tombstone = Tombstone::default(); let mut act = Delete::new(
tombstone.object_props.set_id_string(self.ap_url.clone())?; self.get_authors(conn)?[0]
act.delete_props.set_object_object(tombstone)?; .clone()
.into_id()
.parse::<IriString>()?,
Base::retract(tombstone)?.into_generic()?,
);
act.object_props act.set_id(format!("{}#delete", self.ap_url).parse()?);
.set_id_string(format!("{}#delete", self.ap_url))?; act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
act.object_props
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?;
Ok(act) Ok(act)
} }
@ -621,47 +624,82 @@ impl FromId<DbConn> for Post {
} }
fn from_activity(conn: &DbConn, article: LicensedArticle) -> Result<Self> { fn from_activity(conn: &DbConn, article: LicensedArticle) -> Result<Self> {
let conn = conn; let license = article.ext_one.license.unwrap_or_default();
let license = article.custom_props.license_string().unwrap_or_default(); let article = article.inner;
let article = article.object;
let (blog, authors) = article let (blog, authors) = article
.object_props .ap_object_ref()
.attributed_to_link_vec::<Id>()? .attributed_to()
.into_iter() .ok_or(Error::MissingApProperty)?
.iter()
.fold((None, vec![]), |(blog, mut authors), link| { .fold((None, vec![]), |(blog, mut authors), link| {
let url = link; if let Some(url) = link.id() {
match User::from_id(conn, &url, None, CONFIG.proxy()) { match User::from_id(conn, url.as_str(), None, CONFIG.proxy()) {
Ok(u) => { Ok(u) => {
authors.push(u); authors.push(u);
(blog, authors) (blog, authors)
} }
Err(_) => ( Err(_) => (
blog.or_else(|| Blog::from_id(conn, &url, None, CONFIG.proxy()).ok()), blog.or_else(|| {
Blog::from_id(conn, url.as_str(), None, CONFIG.proxy()).ok()
}),
authors, 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 let cover = article.icon().and_then(|icon| {
.object_props icon.iter().next().and_then(|img| {
.icon_object::<Image>() let image = img.to_owned().extend::<Image, ImageType>().ok()??;
.ok() Media::from_activity(conn, &image).ok().map(|m| m.id)
.and_then(|img| Media::from_activity(conn, &img).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 let ap_url = article
.object_props .url()
.url_string() .and_then(|url| url.to_as_uri().or(id))
.or_else(|_| article.object_props.id_string())?; .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) let post = Post::from_db(conn, &ap_url)
.and_then(|mut post| { .and_then(|mut post| {
let mut updated = false; let mut updated = false;
let slug = Self::slug(&title); let slug = Self::slug(&title);
let content = SafeString::new(&article.object_props.content_string()?); let content = SafeString::new(
let subtitle = article.object_props.summary_string()?; &article
let source = article.ap_object_props.source_object::<Source>()?.content; .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 { if post.slug != slug {
post.slug = slug.to_string(); post.slug = slug.to_string();
updated = true; updated = true;
@ -683,7 +721,7 @@ impl FromId<DbConn> for Post {
updated = true; updated = true;
} }
if post.source != source { if post.source != source {
post.source = source; post.source = source.clone();
updated = true; updated = true;
} }
if post.cover_id != cover { if post.cover_id != cover {
@ -704,14 +742,27 @@ impl FromId<DbConn> for Post {
blog_id: blog.ok_or(Error::NotFound)?.id, blog_id: blog.ok_or(Error::NotFound)?.id,
slug: Self::slug(&title).to_string(), slug: Self::slug(&title).to_string(),
title, 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, published: true,
license, license,
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields // FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
ap_url, ap_url,
creation_date: Some(article.object_props.published_utctime()?.naive_utc()), creation_date: article.published().map(|published| {
subtitle: article.object_props.summary_string()?, let timestamp_secs = published.unix_timestamp();
source: article.ap_object_props.source_object::<Source>()?.content, 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, cover_id: cover,
}, },
) )
@ -735,22 +786,22 @@ impl FromId<DbConn> for Post {
.2 .2
.into_iter() .into_iter()
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
if let Some(serde_json::Value::Array(tags)) = article.object_props.tag { if let Some(tags) = article.tag() {
for tag in tags { for tag in tags.iter() {
serde_json::from_value::<link::Mention>(tag.clone()) tag.clone()
.map(|m| Mention::from_activity(conn, &m, post.id, true, true)) .extend::<link::Mention, MentionType>() // FIXME: Don't clone
.map(|mention| {
mention.map(|m| Mention::from_activity(conn, &m, post.id, true, true))
})
.ok(); .ok();
serde_json::from_value::<Hashtag>(tag.clone()) tag.clone()
.map_err(Error::from) .extend::<Hashtag, HashtagType>() // FIXME: Don't clone
.and_then(|t| { .map(|hashtag| {
let tag_name = t.name_string()?; hashtag.and_then(|t| {
Ok(Tag::from_activity( let tag_name = t.name.clone()?.as_str().to_string();
conn, Tag::from_activity(conn, &t, post.id, hashtags.remove(&tag_name)).ok()
&t, })
post.id,
hashtags.remove(&tag_name),
))
}) })
.ok(); .ok();
} }
@ -762,15 +813,15 @@ impl FromId<DbConn> for Post {
} }
fn get_sender() -> &'static dyn Signer { 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 { impl AsObject<User, Create, &DbConn> for Post {
type Error = Error; 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? // TODO: check that _actor is actually one of the author?
Ok(self) Ok(self)
} }
@ -780,7 +831,7 @@ impl AsObject<User, Delete, &DbConn> for Post {
type Error = Error; type Error = Error;
type Output = (); 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 let can_delete = self
.get_authors(conn)? .get_authors(conn)?
.into_iter() .into_iter()
@ -813,27 +864,54 @@ impl FromId<DbConn> for PostUpdate {
Err(Error::NotFound) Err(Error::NotFound)
} }
fn from_activity(conn: &DbConn, updated: LicensedArticle) -> Result<Self> { fn from_activity(conn: &DbConn, updated: Self::Object) -> Result<Self> {
Ok(PostUpdate { let mut post_update = PostUpdate {
ap_url: updated.object.object_props.id_string()?, ap_url: updated
title: updated.object.object_props.name_string().ok(), .ap_object_ref()
subtitle: updated.object.object_props.summary_string().ok(), .id_unchecked()
content: updated.object.object_props.content_string().ok(), .ok_or(Error::MissingApProperty)?
cover: updated .to_string(),
.object title: updated
.object_props .ap_object_ref()
.icon_object::<Image>() .name()
.ok() .and_then(|name| name.to_as_string()),
.and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id)), subtitle: updated
source: updated .ap_object_ref()
.object .summary()
.ap_object_props .and_then(|summary| summary.to_as_string()),
.source_object::<Source>() content: updated
.ok() .ap_object_ref()
.map(|x| x.content), .content()
license: updated.custom_props.license_string().ok(), .and_then(|content| content.to_as_string()),
tags: updated.object.object_props.tag, 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 { fn get_sender() -> &'static dyn Signer {
@ -893,8 +971,12 @@ impl AsObject<User, Update, &DbConn> for PostUpdate {
serde_json::from_value::<Hashtag>(tag.clone()) serde_json::from_value::<Hashtag>(tag.clone())
.map_err(Error::from) .map_err(Error::from)
.and_then(|t| { .and_then(|t| {
let tag_name = t.name_string()?; let tag_name = t.name.as_ref().ok_or(Error::MissingApProperty)?;
if txt_hashtags.remove(&tag_name) { 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); hashtags.push(t);
} else { } else {
tags.push(t); 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] #[test]
fn to_activity() { fn to_activity() {
let conn = db(); let conn = db();
@ -1074,10 +1113,10 @@ mod tests {
"name": "Testing", "name": "Testing",
"published": format_datetime(&post.creation_date), "published": format_datetime(&post.creation_date),
"source": { "source": {
"content": "", "content": "Hello",
"mediaType": "text/markdown" "mediaType": "text/markdown"
}, },
"summary": "", "summary": "Bye",
"tag": [ "tag": [
{ {
"href": "https://plu.me/@/user/", "href": "https://plu.me/@/user/",
@ -1116,10 +1155,10 @@ mod tests {
"name": "Testing", "name": "Testing",
"published": format_datetime(&post.creation_date), "published": format_datetime(&post.creation_date),
"source": { "source": {
"content": "", "content": "Hello",
"mediaType": "text/markdown" "mediaType": "text/markdown"
}, },
"summary": "", "summary": "Bye",
"tag": [ "tag": [
{ {
"href": "https://plu.me/@/user/", "href": "https://plu.me/@/user/",
@ -1161,10 +1200,10 @@ mod tests {
"name": "Testing", "name": "Testing",
"published": format_datetime(&post.creation_date), "published": format_datetime(&post.creation_date),
"source": { "source": {
"content": "", "content": "Hello",
"mediaType": "text/markdown" "mediaType": "text/markdown"
}, },
"summary": "", "summary": "Bye",
"tag": [ "tag": [
{ {
"href": "https://plu.me/@/user/", "href": "https://plu.me/@/user/",
@ -1200,10 +1239,36 @@ mod tests {
if key == "id" { if key == "id" {
continue; continue;
} }
assert_eq!(value, expected.get(key).unwrap()); assert_json_eq!(value, expected.get(key).unwrap());
} }
Ok(()) 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(())
});
}
} }

View file

@ -1,12 +1,16 @@
use crate::{ use crate::{
db_conn::{DbConn, DbPool}, db_conn::{DbConn, DbPool},
follows, follows,
posts::{LicensedArticle, Post}, posts::Post,
users::{User, UserEvent}, users::{User, UserEvent},
ACTOR_SYS, CONFIG, USER_CHAN, ACTOR_SYS, CONFIG, USER_CHAN,
}; };
use activitypub::activity::Create; use activitystreams::{
use plume_common::activity_pub::inbox::FromId; 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 riker::actors::{Actor, ActorFactoryArgs, ActorRefFactory, Context, Sender, Subscribe, Tell};
use std::sync::Arc; use std::sync::Arc;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
@ -68,13 +72,17 @@ fn fetch_and_cache_articles(user: &Arc<User>, conn: &DbConn) {
match create_acts { match create_acts {
Ok(create_acts) => { Ok(create_acts) => {
for create_act in create_acts { for create_act in create_acts {
match create_act.create_props.object_object::<LicensedArticle>() { match create_act.object_field_ref().as_single_base().map(|base| {
Ok(article) => { 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) Post::from_activity(conn, article)
.expect("Article from remote user couldn't be saved"); .expect("Article from remote user couldn't be saved");
info!("Fetched article from remote user"); 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"),
} }
} }
} }

View file

@ -2,13 +2,18 @@ use crate::{
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::reshares, db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::reshares,
timeline::*, users::User, Connection, Error, Result, CONFIG, 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 chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{ use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId}, inbox::{AsActor, AsObject, FromId},
sign::Signer, sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY, PUBLIC_VISIBILITY,
}; };
#[derive(Clone, Queryable, Identifiable)] #[derive(Clone, Queryable, Identifiable)]
@ -61,16 +66,16 @@ impl Reshare {
} }
pub fn to_activity(&self, conn: &Connection) -> Result<Announce> { pub fn to_activity(&self, conn: &Connection) -> Result<Announce> {
let mut act = Announce::default(); let mut act = Announce::new(
act.announce_props User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?,
.set_actor_link(User::get(conn, self.user_id)?.into_id())?; Post::get(conn, self.post_id)?.ap_url.parse::<IriString>()?,
act.announce_props );
.set_object_link(Post::get(conn, self.post_id)?.into_id())?; act.set_id(self.ap_url.parse::<IriString>()?);
act.object_props.set_id_string(self.ap_url.clone())?; act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
act.object_props act.set_many_ccs(vec![self
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?; .get_user(conn)?
act.object_props .followers_endpoint
.set_cc_link_vec(vec![Id::new(self.get_user(conn)?.followers_endpoint)])?; .parse::<IriString>()?]);
Ok(act) Ok(act)
} }
@ -93,16 +98,16 @@ impl Reshare {
} }
pub fn build_undo(&self, conn: &Connection) -> Result<Undo> { pub fn build_undo(&self, conn: &Connection) -> Result<Undo> {
let mut act = Undo::default(); let mut act = Undo::new(
act.undo_props User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?,
.set_actor_link(User::get(conn, self.user_id)?.into_id())?; AnyBase::from_extended(self.to_activity(conn)?)?,
act.undo_props.set_object_object(self.to_activity(conn)?)?; );
act.object_props act.set_id(format!("{}#delete", self.ap_url).parse::<IriString>()?);
.set_id_string(format!("{}#delete", self.ap_url))?; act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
act.object_props act.set_many_ccs(vec![self
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?; .get_user(conn)?
act.object_props .followers_endpoint
.set_cc_link_vec(vec![Id::new(self.get_user(conn)?.followers_endpoint)])?; .parse::<IriString>()?]);
Ok(act) Ok(act)
} }
@ -143,7 +148,10 @@ impl FromId<DbConn> for Reshare {
NewReshare { NewReshare {
post_id: Post::from_id( post_id: Post::from_id(
conn, conn,
&act.announce_props.object_link::<Id>()?, act.object_field_ref()
.as_single_id()
.ok_or(Error::MissingApProperty)?
.as_str(),
None, None,
CONFIG.proxy(), CONFIG.proxy(),
) )
@ -151,13 +159,19 @@ impl FromId<DbConn> for Reshare {
.id, .id,
user_id: User::from_id( user_id: User::from_id(
conn, conn,
&act.announce_props.actor_link::<Id>()?, act.actor_field_ref()
.as_single_id()
.ok_or(Error::MissingApProperty)?
.as_str(),
None, None,
CONFIG.proxy(), CONFIG.proxy(),
) )
.map_err(|(_, e)| e)? .map_err(|(_, e)| e)?
.id, .id,
ap_url: act.object_props.id_string()?, ap_url: act
.id_unchecked()
.ok_or(Error::MissingApProperty)?
.to_string(),
}, },
)?; )?;
res.notify(conn)?; res.notify(conn)?;

View file

@ -1,6 +1,7 @@
use crate::{ap_url, instance::Instance, schema::tags, Connection, Error, Result}; use crate::{ap_url, instance::Instance, schema::tags, Connection, Error, Result};
use activitystreams::iri_string::types::IriString;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::Hashtag; use plume_common::activity_pub::{Hashtag, HashtagExt};
#[derive(Clone, Identifiable, Queryable)] #[derive(Clone, Identifiable, Queryable)]
pub struct Tag { pub struct Tag {
@ -25,13 +26,16 @@ impl Tag {
list_by!(tags, for_post, post_id as i32); list_by!(tags, for_post, post_id as i32);
pub fn to_activity(&self) -> Result<Hashtag> { pub fn to_activity(&self) -> Result<Hashtag> {
let mut ht = Hashtag::default(); let mut ht = Hashtag::new();
ht.set_href_string(ap_url(&format!( ht.set_href(
ap_url(&format!(
"{}/tag/{}", "{}/tag/{}",
Instance::get_local()?.public_domain, Instance::get_local()?.public_domain,
self.tag self.tag
)))?; ))
ht.set_name_string(self.tag.clone())?; .parse::<IriString>()?,
);
ht.set_name(self.tag.clone());
Ok(ht) Ok(ht)
} }
@ -44,7 +48,7 @@ impl Tag {
Tag::insert( Tag::insert(
conn, conn,
NewTag { NewTag {
tag: tag.name_string()?, tag: tag.name().ok_or(Error::MissingApProperty)?.as_str().into(),
is_hashtag, is_hashtag,
post_id: post, post_id: post,
}, },
@ -52,13 +56,16 @@ impl Tag {
} }
pub fn build_activity(tag: String) -> Result<Hashtag> { pub fn build_activity(tag: String) -> Result<Hashtag> {
let mut ht = Hashtag::default(); let mut ht = Hashtag::new();
ht.set_href_string(ap_url(&format!( ht.set_href(
ap_url(&format!(
"{}/tag/{}", "{}/tag/{}",
Instance::get_local()?.public_domain, Instance::get_local()?.public_domain,
tag tag
)))?; ))
ht.set_name_string(tag)?; .parse::<IriString>()?,
);
ht.set_name(tag);
Ok(ht) Ok(ht)
} }
@ -78,6 +85,24 @@ mod tests {
use assert_json_diff::assert_json_eq; use assert_json_diff::assert_json_eq;
use serde_json::to_value; 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] #[test]
fn to_activity() { fn to_activity() {
let conn = &db(); 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] #[test]
fn build_activity() { fn build_activity() {
let conn = &db(); let conn = &db();

View file

@ -4,12 +4,15 @@ use crate::{
safe_string::SafeString, schema::users, timeline::Timeline, Connection, Error, Result, safe_string::SafeString, schema::users, timeline::Timeline, Connection, Error, Result,
UserEvent::*, CONFIG, ITEMS_PER_PAGE, USER_CHAN, UserEvent::*, CONFIG, ITEMS_PER_PAGE, USER_CHAN,
}; };
use activitypub::{ use activitystreams::{
activity::Delete, activity::Delete,
actor::Person, actor::{ApActor, AsApActor, Endpoints, Person},
base::{AnyBase, Base},
collection::{OrderedCollection, OrderedCollectionPage}, collection::{OrderedCollection, OrderedCollectionPage},
object::{Image, Tombstone}, iri_string::types::IriString,
Activity, CustomObject, Endpoint, markers::Activity,
object::{kind::ImageType, AsObject as _, Image, Tombstone},
prelude::*,
}; };
use chrono::{NaiveDateTime, Utc}; use chrono::{NaiveDateTime, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl}; use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl};
@ -25,7 +28,8 @@ use plume_common::{
inbox::{AsActor, AsObject, FromId}, inbox::{AsActor, AsObject, FromId},
request::get, request::get,
sign::{gen_keypair, Error as SignError, Result as SignResult, Signer}, 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, utils,
}; };
@ -39,11 +43,8 @@ use std::{
hash::{Hash, Hasher}, hash::{Hash, Hasher},
sync::Arc, sync::Arc,
}; };
use url::Url;
use webfinger::*; use webfinger::*;
pub type CustomPerson = CustomObject<ApSignature, Person>;
pub enum Role { pub enum Role {
Admin = 0, Admin = 0,
Moderator = 1, Moderator = 1,
@ -247,8 +248,18 @@ impl User {
let text = &res.text()?; let text = &res.text()?;
// without this workaround, publicKey is not correctly deserialized // without this workaround, publicKey is not correctly deserialized
let ap_sign = serde_json::from_str::<ApSignature>(text)?; let ap_sign = serde_json::from_str::<ApSignature>(text)?;
let mut json = serde_json::from_str::<CustomPerson>(text)?; let person = serde_json::from_str::<Person>(text)?;
json.custom_props = ap_sign; 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) Ok(json)
} }
@ -260,35 +271,56 @@ impl User {
User::fetch(&self.ap_url.clone()).and_then(|json| { User::fetch(&self.ap_url.clone()).and_then(|json| {
let avatar = Media::save_remote( let avatar = Media::save_remote(
conn, conn,
json.object json.ap_actor_ref()
.object_props .icon()
.icon_image()? // FIXME: Fails when icon is not set .ok_or(Error::MissingApProperty)? // FIXME: Fails when icon is not set
.object_props .iter()
.url_string()?, .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, self,
) )
.ok(); .ok();
let pub_key = &json.ext_one.public_key.public_key_pem;
diesel::update(self) diesel::update(self)
.set(( .set((
users::username.eq(json.object.ap_actor_props.preferred_username_string()?), users::username.eq(json
users::display_name.eq(json.object.object_props.name_string()?), .ap_actor_ref()
users::outbox_url.eq(json.object.ap_actor_props.outbox_string()?), .preferred_username()
users::inbox_url.eq(json.object.ap_actor_props.inbox_string()?), .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( users::summary.eq(SafeString::new(
&json &json
.object .ap_actor_ref()
.object_props .summary()
.summary_string() .and_then(|summary| summary.to_as_string())
.unwrap_or_default(), .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::avatar_id.eq(avatar.map(|a| a.id)),
users::last_fetched_date.eq(Utc::now().naive_utc()), users::last_fetched_date.eq(Utc::now().naive_utc()),
users::public_key.eq(json users::public_key.eq(pub_key),
.custom_props
.public_key_publickey()?
.public_key_pem_string()?),
)) ))
.execute(conn) .execute(conn)
.map(|_| ()) .map(|_| ())
@ -432,17 +464,16 @@ impl User {
Ok(ActivityStream::new(self.outbox_collection(conn)?)) Ok(ActivityStream::new(self.outbox_collection(conn)?))
} }
pub fn outbox_collection(&self, conn: &Connection) -> Result<OrderedCollection> { 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 first = &format!("{}?page=1", &self.outbox_url);
let last = &format!( let last = &format!(
"{}?page={}", "{}?page={}",
&self.outbox_url, &self.outbox_url,
self.get_activities_count(conn) / i64::from(ITEMS_PER_PAGE) + 1 self.get_activities_count(conn) / i64::from(ITEMS_PER_PAGE) + 1
); );
coll.collection_props.set_first_link(Id::new(first))?; coll.set_first(first.parse::<IriString>()?);
coll.collection_props.set_last_link(Id::new(last))?; coll.set_last(last.parse::<IriString>()?);
coll.collection_props coll.set_total_items(self.get_activities_count(conn) as u64);
.set_total_items_u64(self.get_activities_count(conn) as u64)?;
Ok(coll) Ok(coll)
} }
pub fn outbox_page( pub fn outbox_page(
@ -461,27 +492,31 @@ impl User {
) -> Result<OrderedCollectionPage> { ) -> Result<OrderedCollectionPage> {
let acts = self.get_activities_page(conn, (min, max))?; let acts = self.get_activities_page(conn, (min, max))?;
let n_acts = self.get_activities_count(conn); 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) { if n_acts - i64::from(min) >= i64::from(ITEMS_PER_PAGE) {
coll.collection_page_props.set_next_link(Id::new(&format!( coll.set_next(
"{}?page={}", format!("{}?page={}", &self.outbox_url, min / ITEMS_PER_PAGE + 2)
&self.outbox_url, .parse::<IriString>()?,
min / ITEMS_PER_PAGE + 2 );
)))?;
} }
if min > 0 { if min > 0 {
coll.collection_page_props.set_prev_link(Id::new(&format!( coll.set_prev(
"{}?page={}", format!("{}?page={}", &self.outbox_url, min / ITEMS_PER_PAGE)
&self.outbox_url, .parse::<IriString>()?,
min / ITEMS_PER_PAGE );
)))?;
} }
coll.collection_props.items = serde_json::to_value(acts)?; coll.set_many_items(
coll.collection_page_props acts.iter()
.set_part_of_link(Id::new(&self.outbox_url))?; .filter_map(|value| AnyBase::from_arbitrary_json(value).ok()),
);
coll.set_part_of(self.outbox_url.parse::<IriString>()?);
Ok(coll) 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 res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?;
let text = &res.text()?; let text = &res.text()?;
let json: serde_json::Value = serde_json::from_str(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()); let next = json.get("next").map(|x| x.as_str().unwrap().to_owned());
Ok((items, next)) 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( let res = get(
&self.outbox_url[..], &self.outbox_url[..],
Self::get_sender(), Self::get_sender(),
@ -740,71 +776,58 @@ impl User {
} }
pub fn to_activity(&self, conn: &Connection) -> Result<CustomPerson> { pub fn to_activity(&self, conn: &Connection) -> Result<CustomPerson> {
let mut actor = Person::default(); let mut actor = ApActor::new(self.inbox_url.parse()?, Person::new());
actor.object_props.set_id_string(self.ap_url.clone())?; let ap_url = self.ap_url.parse::<IriString>()?;
actor actor.set_id(ap_url.clone());
.object_props actor.set_name(self.display_name.clone());
.set_name_string(self.display_name.clone())?; actor.set_summary(self.summary_html.get().clone());
actor actor.set_url(ap_url.clone());
.object_props actor.set_inbox(self.inbox_url.parse()?);
.set_summary_string(self.summary_html.get().clone())?; actor.set_outbox(self.outbox_url.parse()?);
actor.object_props.set_url_string(self.ap_url.clone())?; actor.set_preferred_username(self.username.clone());
actor actor.set_followers(self.followers_endpoint.parse()?);
.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())?;
if let Some(shared_inbox_url) = self.shared_inbox_url.clone() { if let Some(shared_inbox_url) = self.shared_inbox_url.clone() {
let mut endpoints = Endpoint::default(); let endpoints = Endpoints {
endpoints.set_shared_inbox_string(shared_inbox_url)?; shared_inbox: Some(shared_inbox_url.parse::<IriString>()?),
actor.ap_actor_props.set_endpoints_endpoint(endpoints)?; ..Endpoints::default()
};
actor.set_endpoints(endpoints);
} }
let mut public_key = PublicKey::default(); let pub_key = PublicKey {
public_key.set_id_string(format!("{}#main-key", self.ap_url))?; id: format!("{}#main-key", self.ap_url).parse()?,
public_key.set_owner_string(self.ap_url.clone())?; owner: ap_url,
public_key.set_public_key_pem_string(self.public_key.clone())?; public_key_pem: self.public_key.clone(),
let mut ap_signature = ApSignature::default(); };
ap_signature.set_public_key_publickey(public_key)?; let ap_signature = ApSignature {
public_key: pub_key,
};
if let Some(avatar_id) = self.avatar_id { if let Some(avatar_id) = self.avatar_id {
let mut avatar = Image::default(); let mut avatar = Image::new();
avatar avatar.set_url(Media::get(conn, avatar_id)?.url()?.parse::<IriString>()?);
.object_props actor.set_icon(avatar.into_any_base()?);
.set_url_string(Media::get(conn, avatar_id)?.url()?)?;
actor.object_props.set_icon_object(avatar)?;
} }
Ok(CustomPerson::new(actor, ap_signature)) Ok(CustomPerson::new(actor, ap_signature))
} }
pub fn delete_activity(&self, conn: &Connection) -> Result<Delete> { 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(); let mut del = Delete::new(
tombstone.object_props.set_id_string(self.ap_url.clone())?; self.ap_url.parse::<IriString>()?,
Base::retract(tombstone)?.into_generic()?,
del.delete_props );
.set_actor_link(Id::new(self.ap_url.clone()))?; del.set_id(format!("{}#delete", self.ap_url).parse()?);
del.delete_props.set_object_object(tombstone)?; del.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
del.object_props del.set_many_ccs(
.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(
self.get_followers(conn)? self.get_followers(conn)?
.into_iter() .into_iter()
.map(|f| Id::new(f.ap_url)) .filter_map(|f| f.ap_url.parse::<IriString>().ok()),
.collect(), );
)?;
Ok(del) Ok(del)
} }
@ -930,9 +953,60 @@ impl FromId<DbConn> for User {
} }
fn from_activity(conn: &DbConn, acct: CustomPerson) -> Result<Self> { fn from_activity(conn: &DbConn, acct: CustomPerson) -> Result<Self> {
let url = Url::parse(&acct.object.object_props.id_string()?)?; let actor = acct.ap_actor_ref();
let inst = url.host_str().ok_or(Error::Url)?; let username = actor
let instance = Instance::find_by_domain(conn, inst).or_else(|_| { .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( Instance::insert(
conn, conn,
NewInstance { NewInstance {
@ -949,70 +1023,20 @@ impl FromId<DbConn> for User {
}, },
) )
})?; })?;
new_user.instance_id = instance.id;
let username = acct.object.ap_actor_props.preferred_username_string()?; new_user.fqn = if instance.local {
username
if username.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
return Err(Error::InvalidValue);
}
let fqn = if instance.local {
username.clone()
} else { } else {
format!("{}@{}", username, instance.public_domain) format!("{}@{}", username, instance.public_domain)
}; };
let user = User::insert( let user = User::insert(conn, new_user)?;
conn, if let Some(avatar_id) = avatar_id {
NewUser { let avatar = Media::save_remote(conn, avatar_id, &user);
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,
},
)?;
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 { if let Ok(avatar) = avatar {
user.set_avatar(conn, avatar.id)?; 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" "sharedInbox": "https://plu.me/inbox"
}, },
"followers": "https://plu.me/@/admin/followers", "followers": "https://plu.me/@/admin/followers",
"following": null,
"id": "https://plu.me/@/admin/", "id": "https://plu.me/@/admin/",
"inbox": "https://plu.me/@/admin/inbox", "inbox": "https://plu.me/@/admin/inbox",
"liked": null,
"name": "The admin", "name": "The admin",
"outbox": "https://plu.me/@/admin/outbox", "outbox": "https://plu.me/@/admin/outbox",
"preferredUsername": "admin", "preferredUsername": "admin",
@ -1461,14 +1483,12 @@ pub(crate) mod tests {
"sharedInbox": "https://plu.me/inbox" "sharedInbox": "https://plu.me/inbox"
}, },
"followers": "https://plu.me/@/other/followers", "followers": "https://plu.me/@/other/followers",
"following": null,
"icon": { "icon": {
"url": "https://plu.me/static/media/example.png", "url": "https://plu.me/static/media/example.png",
"type": "Image", "type": "Image",
}, },
"id": "https://plu.me/@/other/", "id": "https://plu.me/@/other/",
"inbox": "https://plu.me/@/other/inbox", "inbox": "https://plu.me/@/other/inbox",
"liked": null,
"name": "Another user", "name": "Another user",
"outbox": "https://plu.me/@/other/outbox", "outbox": "https://plu.me/@/other/outbox",
"preferredUsername": "other", "preferredUsername": "other",
@ -1524,7 +1544,6 @@ pub(crate) mod tests {
let expected = json!({ let expected = json!({
"first": "https://plu.me/@/admin/outbox?page=1", "first": "https://plu.me/@/admin/outbox?page=1",
"items": null,
"last": "https://plu.me/@/admin/outbox?page=5", "last": "https://plu.me/@/admin/outbox?page=5",
"totalItems": 51, "totalItems": 51,
"type": "OrderedCollection", "type": "OrderedCollection",

View file

@ -1,4 +1,4 @@
use activitypub::collection::{OrderedCollection, OrderedCollectionPage}; use activitystreams::collection::{OrderedCollection, OrderedCollectionPage};
use diesel::SaveChangesDsl; use diesel::SaveChangesDsl;
use rocket::{ use rocket::{
http::ContentType, http::ContentType,
@ -12,7 +12,7 @@ use validator::{Validate, ValidationError, ValidationErrors};
use crate::routes::{errors::ErrorPage, Page, RespondOrRedirect}; use crate::routes::{errors::ErrorPage, Page, RespondOrRedirect};
use crate::template_utils::{IntoContext, Ructe}; use crate::template_utils::{IntoContext, Ructe};
use crate::utils::requires_login; 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_common::utils;
use plume_models::{ use plume_models::{
blog_authors::*, blogs::*, db_conn::DbConn, instance::Instance, medias::*, posts::Post, blog_authors::*, blogs::*, db_conn::DbConn, instance::Instance, medias::*, posts::Post,

View file

@ -1,5 +1,5 @@
use crate::template_utils::Ructe; use crate::template_utils::Ructe;
use activitypub::object::Note; use activitystreams::object::Note;
use rocket::{ use rocket::{
request::LenientForm, request::LenientForm,
response::{Flash, Redirect}, response::{Flash, Redirect},

View file

@ -15,7 +15,7 @@ use crate::routes::{
}; };
use crate::template_utils::{IntoContext, Ructe}; use crate::template_utils::{IntoContext, Ructe};
use crate::utils::requires_login; 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_common::utils::md_to_html;
use plume_models::{ use plume_models::{
blogs::*, blogs::*,

View file

@ -1,4 +1,8 @@
use activitypub::collection::{OrderedCollection, OrderedCollectionPage}; use activitystreams::{
collection::{OrderedCollection, OrderedCollectionPage},
iri_string::types::IriString,
prelude::*,
};
use diesel::SaveChangesDsl; use diesel::SaveChangesDsl;
use rocket::{ use rocket::{
http::{uri::Uri, ContentType, Cookies}, http::{uri::Uri, ContentType, Cookies},
@ -15,7 +19,7 @@ use crate::routes::{
}; };
use crate::template_utils::{IntoContext, Ructe}; use crate::template_utils::{IntoContext, Ructe};
use crate::utils::requires_login; 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_common::utils::md_to_html;
use plume_models::{ use plume_models::{
blogs::Blog, blogs::Blog,
@ -561,17 +565,13 @@ pub fn ap_followers(
.get_followers(&conn) .get_followers(&conn)
.ok()? .ok()?
.into_iter() .into_iter()
.map(|f| Id::new(f.ap_url)) .filter_map(|f| f.ap_url.parse::<IriString>().ok())
.collect::<Vec<Id>>(); .collect::<Vec<IriString>>();
let mut coll = OrderedCollection::default(); let mut coll = OrderedCollection::new();
coll.object_props coll.set_id(user.followers_endpoint.parse::<IriString>().ok()?);
.set_id_string(user.followers_endpoint) coll.set_total_items(followers.len() as u64);
.ok()?; coll.set_many_items(followers);
coll.collection_props
.set_total_items_u64(followers.len() as u64)
.ok()?;
coll.collection_props.set_items_link_vec(followers).ok()?;
Some(ActivityStream::new(coll)) Some(ActivityStream::new(coll))
} }