Rmove FromId

This commit is contained in:
Kitaiti Makoto 2022-05-02 12:58:01 +09:00
parent 2165c286ae
commit 2804f44a06
12 changed files with 107 additions and 929 deletions

View file

@ -318,72 +318,6 @@ fn get_id(json: serde_json::Value) -> Option<String> {
}
}
/// A trait for ActivityPub objects that can be retrieved or constructed from ID.
///
/// The two functions to implement are `from_activity` to create (and save) a new object
/// of this type from its AP representation, and `from_db` to try to find it in the database
/// using its ID.
///
/// When dealing with the "object" field of incoming activities, `Inbox` will try to see if it is
/// a full object, and if so, save it with `from_activity`. If it is only an ID, it will try to find
/// it in the database with `from_db`, and otherwise dereference (fetch) the full object and parse it
/// with `from_activity`.
pub trait FromId<C>: Sized {
/// The type representing a failure
type Error: From<InboxError<Self::Error>> + Debug;
/// The ActivityPub object type representing Self
type Object: activitypub::Object;
/// Tries to get an instance of `Self` from an ActivityPub ID.
///
/// # Parameters
///
/// - `ctx`: a context to get this instance (= a database in which to search)
/// - `id`: the ActivityPub ID of the object to find
/// - `object`: optional object that will be used if the object was not found in the database
/// If absent, the ID will be dereferenced.
fn from_id(
ctx: &C,
id: &str,
object: Option<Self::Object>,
proxy: Option<&reqwest::Proxy>,
) -> Result<Self, (Option<serde_json::Value>, Self::Error)> {
match Self::from_db(ctx, id) {
Ok(x) => Ok(x),
_ => match object {
Some(o) => Self::from_activity(ctx, o).map_err(|e| (None, e)),
None => Self::from_activity(ctx, Self::deref(id, proxy.cloned())?)
.map_err(|e| (None, e)),
},
}
}
/// Dereferences an ID
fn deref(
id: &str,
proxy: Option<reqwest::Proxy>,
) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
request::get(id, Self::get_sender(), proxy)
.map_err(|_| (None, InboxError::DerefError))
.and_then(|mut r| {
let json: serde_json::Value = r
.json()
.map_err(|_| (None, InboxError::InvalidObject(None)))?;
serde_json::from_value(json.clone())
.map_err(|_| (Some(json), InboxError::InvalidObject(None)))
})
.map_err(|(json, e)| (json, e.into()))
}
/// Builds a `Self` from its ActivityPub representation
fn from_activity(ctx: &C, activity: Self::Object) -> Result<Self, Self::Error>;
/// Tries to find a `Self` with a given ID (`id`), using `ctx` (a database)
fn from_db(ctx: &C, id: &str) -> Result<Self, Self::Error>;
fn get_sender() -> &'static dyn Signer;
}
/// A trait for ActivityPub objects that can be retrieved or constructed from ID.
///
/// The two functions to implement are `from_activity` to create (and save) a new object
@ -808,22 +742,6 @@ mod tests {
}
struct MyActor;
impl FromId<()> for MyActor {
type Error = ();
type Object = Person;
fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> {
Ok(MyActor)
}
fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> {
Ok(MyActor)
}
fn get_sender() -> &'static dyn Signer {
&*MY_SIGNER
}
}
impl FromId07<()> for MyActor {
type Error = ();
type Object = Person07;
@ -852,22 +770,6 @@ mod tests {
}
struct MyObject;
impl FromId<()> for MyObject {
type Error = ();
type Object = Note;
fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> {
Ok(MyObject)
}
fn from_activity(_: &(), _obj: Note) -> Result<Self, Self::Error> {
Ok(MyObject)
}
fn get_sender() -> &'static dyn Signer {
&*MY_SIGNER
}
}
impl AsObject<MyActor, Create, &()> for MyObject {
type Error = ();
type Output = ();
@ -1017,15 +919,6 @@ mod tests {
act
}
#[test]
fn test_inbox_basic() {
let act = serde_json::to_value(build_create()).unwrap();
let res: Result<(), ()> = Inbox::handle(&(), act)
.with::<MyActor, Create, MyObject>(None)
.done();
assert!(res.is_ok());
}
#[test]
fn test_inbox_basic07() {
let act = serde_json::to_value(build_create07()).unwrap();
@ -1035,18 +928,6 @@ mod tests {
assert!(res.is_ok());
}
#[test]
fn test_inbox_multi_handlers() {
let act = serde_json::to_value(build_create()).unwrap();
let res: Result<(), ()> = Inbox::handle(&(), act)
.with::<MyActor, Announce, MyObject>(None)
.with::<MyActor, Delete, MyObject>(None)
.with::<MyActor, Create, MyObject>(None)
.with::<MyActor, Like, MyObject>(None)
.done();
assert!(res.is_ok());
}
#[test]
fn test_inbox_multi_handlers07() {
let act = serde_json::to_value(build_create()).unwrap();
@ -1059,17 +940,6 @@ mod tests {
assert!(res.is_ok());
}
#[test]
fn test_inbox_failure() {
let act = serde_json::to_value(build_create()).unwrap();
// Create is not handled by this inbox
let res: Result<(), ()> = Inbox::handle(&(), act)
.with::<MyActor, Announce, MyObject>(None)
.with::<MyActor, Like, MyObject>(None)
.done();
assert!(res.is_err());
}
#[test]
fn test_inbox_failure07() {
let act = serde_json::to_value(build_create07()).unwrap();
@ -1082,22 +952,6 @@ mod tests {
}
struct FailingActor;
impl FromId<()> for FailingActor {
type Error = ();
type Object = Person;
fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> {
Err(())
}
fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> {
Err(())
}
fn get_sender() -> &'static dyn Signer {
&*MY_SIGNER
}
}
impl AsActor<&()> for FailingActor {
fn get_inbox_url(&self) -> String {
String::from("https://test.ap/failing-actor/inbox")
@ -1155,22 +1009,6 @@ mod tests {
}
}
#[test]
fn test_inbox_actor_failure() {
let act = serde_json::to_value(build_create()).unwrap();
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
.with::<FailingActor, Create, MyObject>(None)
.done();
assert!(res.is_err());
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
.with::<FailingActor, Create, MyObject>(None)
.with::<MyActor, Create, MyObject>(None)
.done();
assert!(res.is_ok());
}
#[test]
fn test_inbox_actor_failure07() {
let act = serde_json::to_value(build_create07()).unwrap();

View file

@ -27,11 +27,10 @@ use openssl::{
sign::{Signer, Verifier},
};
use plume_common::activity_pub::{
inbox::{AsActor, FromId, FromId07},
inbox::{AsActor, FromId07},
sign, ActivityStream, ApSignature, ApSignature07, CustomGroup as CustomGroup07, Id, IntoId,
PublicKey, PublicKey07, Source, SourceProperty, ToAsString, ToAsUri,
};
use url::Url;
use webfinger::*;
pub type CustomGroup = CustomObject<ApSignature, Group>;
@ -161,7 +160,7 @@ impl Blog {
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
.ok_or(Error::Webfinger)
.and_then(|l| {
Blog::from_id(
Blog::from_id07(
conn,
&l.href.ok_or(Error::MissingApProperty)?,
None,
@ -471,110 +470,6 @@ impl IntoId for Blog {
}
}
impl FromId<DbConn> for Blog {
type Error = Error;
type Object = CustomGroup;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
Self::find_by_ap_url(conn, id)
}
fn from_activity(conn: &DbConn, acct: CustomGroup) -> Result<Self> {
let url = Url::parse(&acct.object.object_props.id_string()?)?;
let inst = url.host_str().ok_or(Error::Url)?;
let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
Instance::insert(
conn,
NewInstance {
public_domain: inst.to_owned(),
name: inst.to_owned(),
local: false,
// We don't really care about all the following for remote instances
long_description: SafeString::new(""),
short_description: SafeString::new(""),
default_license: String::new(),
open_registrations: true,
short_description_html: String::new(),
long_description_html: String::new(),
},
)
})?;
let icon_id = acct
.object
.object_props
.icon_image()
.ok()
.and_then(|icon| {
let owner = icon.object_props.attributed_to_link::<Id>().ok()?;
Media::save_remote(
conn,
icon.object_props.url_string().ok()?,
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
)
.ok()
})
.map(|m| m.id);
let banner_id = acct
.object
.object_props
.image_image()
.ok()
.and_then(|banner| {
let owner = banner.object_props.attributed_to_link::<Id>().ok()?;
Media::save_remote(
conn,
banner.object_props.url_string().ok()?,
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
)
.ok()
})
.map(|m| m.id);
let name = acct.object.ap_actor_props.preferred_username_string()?;
if name.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
return Err(Error::InvalidValue);
}
Blog::insert(
conn,
NewBlog {
actor_id: name.clone(),
title: acct.object.object_props.name_string().unwrap_or(name),
outbox_url: acct.object.ap_actor_props.outbox_string()?,
inbox_url: acct.object.ap_actor_props.inbox_string()?,
summary: acct
.object
.ap_object_props
.source_object::<Source>()
.map(|s| s.content)
.unwrap_or_default(),
instance_id: instance.id,
ap_url: acct.object.object_props.id_string()?,
public_key: acct
.custom_props
.public_key_publickey()?
.public_key_pem_string()?,
private_key: None,
banner_id,
icon_id,
summary_html: SafeString::new(
&acct
.object
.object_props
.summary_string()
.unwrap_or_default(),
),
theme: None,
},
)
}
fn get_sender() -> &'static dyn sign::Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl FromId07<DbConn> for Blog {
type Error = Error;
type Object = CustomGroup07;
@ -648,7 +543,7 @@ impl FromId07<DbConn> for Blog {
Media::save_remote(
conn,
banner.url()?.to_as_uri()?,
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
&User::from_id07(conn, &owner, None, CONFIG.proxy()).ok()?,
)
.ok()
})
@ -1130,33 +1025,6 @@ pub(crate) mod tests {
})
}
#[test]
fn self_federation() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (_users, blogs) = fill_database(&conn);
let ap_repr = blogs[0].to_activity(&conn).unwrap();
blogs[0].delete(&conn).unwrap();
let blog = Blog::from_activity(&conn, ap_repr).unwrap();
assert_eq!(blog.actor_id, blogs[0].actor_id);
assert_eq!(blog.title, blogs[0].title);
assert_eq!(blog.summary, blogs[0].summary);
assert_eq!(blog.outbox_url, blogs[0].outbox_url);
assert_eq!(blog.inbox_url, blogs[0].inbox_url);
assert_eq!(blog.instance_id, blogs[0].instance_id);
assert_eq!(blog.ap_url, blogs[0].ap_url);
assert_eq!(blog.public_key, blogs[0].public_key);
assert_eq!(blog.fqn, blogs[0].fqn);
assert_eq!(blog.summary_html, blogs[0].summary_html);
assert_eq!(blog.icon_url(&conn), blogs[0].icon_url(&conn));
assert_eq!(blog.banner_url(&conn), blogs[0].banner_url(&conn));
Ok(())
})
}
#[test]
fn self_federation07() {
let conn = &db();

View file

@ -30,7 +30,7 @@ use chrono::{self, NaiveDateTime, TimeZone, Utc};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use plume_common::{
activity_pub::{
inbox::{AsActor, AsObject, AsObject07, FromId, FromId07},
inbox::{AsActor, AsObject, AsObject07, FromId07},
sign::Signer,
Id, IntoId, ToAsString, ToAsUri, PUBLIC_VISIBILITY,
},
@ -294,141 +294,6 @@ impl Comment {
}
}
impl FromId<DbConn> for Comment {
type Error = Error;
type Object = Note;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
Self::find_by_ap_url(conn, id)
}
fn from_activity(conn: &DbConn, note: Note) -> Result<Self> {
let comm = {
let previous_url = note
.object_props
.in_reply_to
.as_ref()
.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()
.filter_map(serde_json::Value::as_str)
.any(|s| s == PUBLIC_VISIBILITY),
serde_json::Value::String(s) => s == PUBLIC_VISIBILITY,
_ => false,
};
let public_visibility = is_public(&note.object_props.to)
|| is_public(&note.object_props.bto)
|| is_public(&note.object_props.cc)
|| is_public(&note.object_props.bcc);
let comm = Comment::insert(
conn,
NewComment {
content: SafeString::new(&note.object_props.content_string()?),
spoiler_text: note.object_props.summary_string().unwrap_or_default(),
ap_url: note.object_props.id_string().ok(),
in_response_to_id: previous_comment.iter().map(|c| c.id).next(),
post_id: previous_comment.map(|c| c.post_id).or_else(|_| {
Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result<i32>
})?,
author_id: User::from_id(
conn,
&note.object_props.attributed_to_link::<Id>()?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?
.id,
sensitive: note.object_props.summary_string().is_ok(),
public_visibility,
},
)?;
// save mentions
if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() {
for tag in tags {
serde_json::from_value::<link::Mention>(tag)
.map_err(Error::from)
.and_then(|m| {
let author = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0];
let not_author = m.link_props.href_string()? != author.ap_url.clone();
Mention::from_activity(conn, &m, comm.id, false, not_author)
})
.ok();
}
}
comm
};
if !comm.public_visibility {
let receivers_ap_url = |v: Option<serde_json::Value>| {
let filter = |e: serde_json::Value| {
if let serde_json::Value::String(s) = e {
Some(s)
} else {
None
}
};
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;
let to = receivers_ap_url(note.object_props.to.take());
let cc = receivers_ap_url(note.object_props.cc.take());
let bto = receivers_ap_url(note.object_props.bto.take());
let bcc = receivers_ap_url(note.object_props.bcc.take());
let receivers_ap_url = to
.chain(cc)
.chain(bto)
.chain(bcc)
.collect::<HashSet<_>>() // remove duplicates (don't do a query more than once)
.into_iter()
.flat_map(|v| {
if let Ok(user) = User::from_id(conn, &v, None, CONFIG.proxy()) {
vec![user]
} else {
vec![] // TODO try to fetch collection
}
})
.filter(|u| u.get_instance(conn).map(|i| i.local).unwrap_or(false))
.collect::<HashSet<User>>(); //remove duplicates (prevent db error)
for user in &receivers_ap_url {
CommentSeers::insert(
conn,
NewCommentSeers {
comment_id: comm.id,
user_id: user.id,
},
)?;
}
}
comm.notify(conn)?;
Ok(comm)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl FromId07<DbConn> for Comment {
type Error = Error;
type Object = Note07;
@ -484,7 +349,7 @@ impl FromId07<DbConn> for Comment {
post_id: previous_comment.map(|c| c.post_id).or_else(|_| {
Ok(Post::find_by_ap_url(conn, previous_url.as_str())?.id) as Result<i32>
})?,
author_id: User::from_id(
author_id: User::from_id07(
conn,
&note
.attributed_to()
@ -537,7 +402,7 @@ impl FromId07<DbConn> for Comment {
let receivers_ap_url = receiver_ids
.into_iter()
.flat_map(|v| {
if let Ok(user) = User::from_id(conn, v.as_ref(), None, CONFIG.proxy()) {
if let Ok(user) = User::from_id07(conn, v.as_ref(), None, CONFIG.proxy()) {
vec![user]
} else {
vec![] // TODO try to fetch collection

View file

@ -12,7 +12,7 @@ use activitystreams::{
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use plume_common::activity_pub::{
broadcast, broadcast07,
inbox::{AsActor, AsObject, AsObject07, FromId, FromId07},
inbox::{AsActor, AsObject, AsObject07, FromId07},
sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY,
};
@ -272,38 +272,6 @@ impl AsObject07<User, FollowAct07, &DbConn> for User {
}
}
impl FromId<DbConn> for Follow {
type Error = Error;
type Object = FollowAct;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
Follow::find_by_ap_url(conn, id)
}
fn from_activity(conn: &DbConn, follow: FollowAct) -> Result<Self> {
let actor = User::from_id(
conn,
&follow.follow_props.actor_link::<Id>()?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?;
let target = User::from_id(
conn,
&follow.follow_props.object_link::<Id>()?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?;
Follow::accept_follow(conn, &actor, &target, follow, actor.id, target.id)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl FromId07<DbConn> for Follow {
type Error = Error;
type Object = FollowAct07;

View file

@ -12,7 +12,7 @@ use activitystreams::{
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{
inbox::{AsActor, AsObject, AsObject07, FromId, FromId07},
inbox::{AsActor, AsObject, AsObject07, FromId07},
sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY,
};
@ -158,46 +158,6 @@ impl AsObject07<User, Like07, &DbConn> for Post {
}
}
impl FromId<DbConn> for Like {
type Error = Error;
type Object = activity::Like;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
Like::find_by_ap_url(conn, id)
}
fn from_activity(conn: &DbConn, act: activity::Like) -> Result<Self> {
let res = Like::insert(
conn,
NewLike {
post_id: Post::from_id(
conn,
&act.like_props.object_link::<Id>()?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?
.id,
user_id: User::from_id(
conn,
&act.like_props.actor_link::<Id>()?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?
.id,
ap_url: act.object_props.id_string()?,
},
)?;
res.notify(conn)?;
Ok(res)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl FromId07<DbConn> for Like {
type Error = Error;
type Object = Like07;
@ -210,7 +170,7 @@ impl FromId07<DbConn> for Like {
let res = Like::insert(
conn,
NewLike {
post_id: Post::from_id(
post_id: Post::from_id07(
conn,
act.object_field_ref()
.as_single_id()

View file

@ -7,7 +7,7 @@ use activitystreams::{object::Image as Image07, prelude::*};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use guid_create::GUID;
use plume_common::{
activity_pub::{inbox::FromId, request, Id, ToAsString, ToAsUri},
activity_pub::{inbox::FromId07, request, Id, ToAsString, ToAsUri},
utils::{escape, MediaProcessor},
};
use std::{
@ -222,7 +222,7 @@ impl Media {
// TODO: conditional GET
request::get(
remote_url.as_str(),
User::get_sender(),
User::get_sender07(),
CONFIG.proxy().cloned(),
)?
.copy_to(&mut dest)?;
@ -275,7 +275,7 @@ impl Media {
remote_url: None,
sensitive: image.object_props.summary_string().is_ok(),
content_warning: image.object_props.summary_string().ok(),
owner_id: User::from_id(
owner_id: User::from_id07(
conn,
image
.object_props
@ -311,7 +311,7 @@ impl Media {
// TODO: conditional GET
request::get(
remote_url.as_str(),
User::get_sender(),
User::get_sender07(),
CONFIG.proxy().cloned(),
)?
.copy_to(&mut dest)?;
@ -362,7 +362,7 @@ impl Media {
remote_url: None,
sensitive: summary.is_some(),
content_warning: summary,
owner_id: User::from_id(
owner_id: User::from_id07(
conn,
&image
.attributed_to()

View file

@ -26,7 +26,7 @@ use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl};
use once_cell::sync::Lazy;
use plume_common::{
activity_pub::{
inbox::{AsActor, AsObject, AsObject07, FromId, FromId07},
inbox::{AsActor, AsObject, AsObject07, FromId07},
sign::Signer,
Hashtag, Hashtag07, HashtagType07, Id, IntoId, Licensed, Licensed07,
LicensedArticle as LicensedArticle07, Source, SourceProperty, ToAsString, ToAsUri,
@ -863,160 +863,6 @@ impl Post {
}
}
impl FromId<DbConn> for Post {
type Error = Error;
type Object = LicensedArticle;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
Self::find_by_ap_url(conn, id)
}
fn from_activity(conn: &DbConn, article: LicensedArticle) -> Result<Self> {
let conn = conn;
let license = article.custom_props.license_string().unwrap_or_default();
let article = article.object;
let (blog, authors) = article
.object_props
.attributed_to_link_vec::<Id>()?
.into_iter()
.fold((None, vec![]), |(blog, mut authors), link| {
let url = link;
match User::from_id(conn, &url, None, CONFIG.proxy()) {
Ok(u) => {
authors.push(u);
(blog, authors)
}
Err(_) => (
blog.or_else(|| Blog::from_id(conn, &url, None, CONFIG.proxy()).ok()),
authors,
),
}
});
let cover = article
.object_props
.icon_object::<Image>()
.ok()
.and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id));
let title = article.object_props.name_string()?;
let ap_url = article
.object_props
.url_string()
.or_else(|_| article.object_props.id_string())?;
let post = Post::from_db(conn, &ap_url)
.and_then(|mut post| {
let mut updated = false;
let slug = Self::slug(&title);
let content = SafeString::new(&article.object_props.content_string()?);
let subtitle = article.object_props.summary_string()?;
let source = article.ap_object_props.source_object::<Source>()?.content;
if post.slug != slug {
post.slug = slug.to_string();
updated = true;
}
if post.title != title {
post.title = title.clone();
updated = true;
}
if post.content != content {
post.content = content;
updated = true;
}
if post.license != license {
post.license = license.clone();
updated = true;
}
if post.subtitle != subtitle {
post.subtitle = subtitle;
updated = true;
}
if post.source != source {
post.source = source;
updated = true;
}
if post.cover_id != cover {
post.cover_id = cover;
updated = true;
}
if updated {
post.update(conn)?;
}
Ok(post)
})
.or_else(|_| {
Post::insert(
conn,
NewPost {
blog_id: blog.ok_or(Error::NotFound)?.id,
slug: Self::slug(&title).to_string(),
title,
content: SafeString::new(&article.object_props.content_string()?),
published: true,
license,
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
ap_url,
creation_date: Some(article.object_props.published_utctime()?.naive_utc()),
subtitle: article.object_props.summary_string()?,
source: article.ap_object_props.source_object::<Source>()?.content,
cover_id: cover,
},
)
.and_then(|post| {
for author in authors {
PostAuthor::insert(
conn,
NewPostAuthor {
post_id: post.id,
author_id: author.id,
},
)?;
}
Ok(post)
})
})?;
// save mentions and tags
let mut hashtags = md_to_html(&post.source, None, false, None)
.2
.into_iter()
.collect::<HashSet<_>>();
if let Some(serde_json::Value::Array(tags)) = article.object_props.tag {
for tag in tags {
serde_json::from_value::<link::Mention>(tag.clone())
.map(|m| Mention::from_activity(conn, &m, post.id, true, true))
.ok();
serde_json::from_value::<Hashtag>(tag.clone())
.map_err(Error::from)
.and_then(|t| {
let tag_name = t.name_string()?;
Ok(Tag::from_activity(
conn,
&t,
post.id,
hashtags.remove(&tag_name),
))
})
.ok();
}
}
Timeline::add_to_all_timelines(conn, &post, Kind::Original)?;
Ok(post)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl FromId07<DbConn> for Post {
type Error = Error;
type Object = LicensedArticle07;
@ -1273,43 +1119,6 @@ pub struct PostUpdate {
pub tags: Option<serde_json::Value>,
}
impl FromId<DbConn> for PostUpdate {
type Error = Error;
type Object = LicensedArticle;
fn from_db(_: &DbConn, _: &str) -> Result<Self> {
// Always fail because we always want to deserialize the AP object
Err(Error::NotFound)
}
fn from_activity(conn: &DbConn, updated: LicensedArticle) -> Result<Self> {
Ok(PostUpdate {
ap_url: updated.object.object_props.id_string()?,
title: updated.object.object_props.name_string().ok(),
subtitle: updated.object.object_props.summary_string().ok(),
content: updated.object.object_props.content_string().ok(),
cover: updated
.object
.object_props
.icon_object::<Image>()
.ok()
.and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id)),
source: updated
.object
.ap_object_props
.source_object::<Source>()
.ok()
.map(|x| x.content),
license: updated.custom_props.license_string().ok(),
tags: updated.object.object_props.tag,
})
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl FromId07<DbConn> for PostUpdate {
type Error = Error;
type Object = LicensedArticle07;
@ -1373,7 +1182,7 @@ impl AsObject<User, Update, &DbConn> for PostUpdate {
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
let mut post =
Post::from_id(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?;
Post::from_id07(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?;
if !post.is_author(conn, actor.id)? {
// TODO: maybe the author was added in the meantime
@ -1445,7 +1254,7 @@ impl AsObject07<User, Update07, &DbConn> for PostUpdate {
fn activity07(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
let mut post =
Post::from_id(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?;
Post::from_id07(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?;
if !post.is_author(conn, actor.id)? {
// TODO: maybe the author was added in the meantime

View file

@ -1,12 +1,16 @@
use crate::{
db_conn::{DbConn, DbPool},
follows,
posts::{LicensedArticle, Post},
posts::Post,
users::{User, UserEvent},
ACTOR_SYS, CONFIG, USER_CHAN,
};
use activitypub::activity::Create;
use plume_common::activity_pub::inbox::FromId;
use activitystreams::{
activity::{ActorAndObjectRef, Create as Create07},
base::AnyBase,
object::kind::ArticleType,
};
use plume_common::activity_pub::{inbox::FromId07, LicensedArticle as LicensedArticle07};
use riker::actors::{Actor, ActorFactoryArgs, ActorRefFactory, Context, Sender, Subscribe, Tell};
use std::sync::Arc;
use tracing::{error, info, warn};
@ -64,17 +68,23 @@ impl ActorFactoryArgs<DbPool> for RemoteFetchActor {
}
fn fetch_and_cache_articles(user: &Arc<User>, conn: &DbConn) {
let create_acts = user.fetch_outbox::<Create>();
let create_acts = user.fetch_outbox07::<Create07>();
match create_acts {
Ok(create_acts) => {
for create_act in create_acts {
match create_act.create_props.object_object::<LicensedArticle>() {
Ok(article) => {
Post::from_activity(conn, article)
match create_act
.object_field_ref()
.as_single_base()
.and_then(|base| {
let any_base = AnyBase::from_base(base.clone()); // FIXME: Don't clone()
any_base.extend::<LicensedArticle07, ArticleType>().ok()
}) {
Some(Some(article)) => {
Post::from_activity07(conn, article)
.expect("Article from remote user couldn't be saved");
info!("Fetched article from remote user");
}
Err(e) => warn!("Error while fetching articles in background: {:?}", e),
_ => warn!("Error while fetching articles in background"),
}
}
}
@ -89,7 +99,7 @@ fn fetch_and_cache_followers(user: &Arc<User>, conn: &DbConn) {
match follower_ids {
Ok(user_ids) => {
for user_id in user_ids {
let follower = User::from_id(conn, &user_id, None, CONFIG.proxy());
let follower = User::from_id07(conn, &user_id, None, CONFIG.proxy());
match follower {
Ok(follower) => {
let inserted = follows::Follow::insert(

View file

@ -12,7 +12,7 @@ use activitystreams::{
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{
inbox::{AsActor, AsObject, AsObject07, FromId, FromId07},
inbox::{AsActor, AsObject, AsObject07, FromId07},
sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY,
};
@ -186,46 +186,6 @@ impl AsObject07<User, Announce07, &DbConn> for Post {
}
}
impl FromId<DbConn> for Reshare {
type Error = Error;
type Object = Announce;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
Reshare::find_by_ap_url(conn, id)
}
fn from_activity(conn: &DbConn, act: Announce) -> Result<Self> {
let res = Reshare::insert(
conn,
NewReshare {
post_id: Post::from_id(
conn,
&act.announce_props.object_link::<Id>()?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?
.id,
user_id: User::from_id(
conn,
&act.announce_props.actor_link::<Id>()?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?
.id,
ap_url: act.object_props.id_string()?,
},
)?;
res.notify(conn)?;
Ok(res)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl FromId07<DbConn> for Reshare {
type Error = Error;
type Object = Announce07;
@ -238,7 +198,7 @@ impl FromId07<DbConn> for Reshare {
let res = Reshare::insert(
conn,
NewReshare {
post_id: Post::from_id(
post_id: Post::from_id07(
conn,
act.object_field_ref()
.as_single_id()
@ -249,7 +209,7 @@ impl FromId07<DbConn> for Reshare {
)
.map_err(|(_, e)| e)?
.id,
user_id: User::from_id(
user_id: User::from_id07(
conn,
act.actor_field_ref()
.as_single_id()

View file

@ -13,7 +13,7 @@ use activitypub::{
};
use activitystreams::{
activity::Delete as Delete07,
actor::AsApActor,
actor::{ApActor, AsApActor},
actor::{ApActor as ApActor07, Endpoints as Endpoints07, Person as Person07},
base::{AnyBase, Base},
collection::{
@ -21,7 +21,7 @@ use activitystreams::{
},
iri_string::types::IriString,
markers::Activity as Activity07,
object::{AsObject as _, Image as Image07, Tombstone as Tombstone07},
object::{kind::ImageType, AsObject as _, Image as Image07, Tombstone as Tombstone07},
prelude::*,
};
use chrono::{NaiveDateTime, Utc};
@ -35,7 +35,7 @@ use openssl::{
};
use plume_common::{
activity_pub::{
inbox::{AsActor, AsObject, AsObject07, FromId, FromId07},
inbox::{AsActor, AsObject, AsObject07, FromId07},
request::get,
sign::{gen_keypair, Error as SignError, Result as SignResult, Signer},
ActivityStream, ApSignature, ApSignature07, CustomPerson as CustomPerson07, Id, IntoId,
@ -53,7 +53,6 @@ use std::{
hash::{Hash, Hasher},
sync::Arc,
};
use url::Url;
use webfinger::*;
pub type CustomPerson = CustomObject<ApSignature, Person>;
@ -238,7 +237,7 @@ impl User {
.into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
.ok_or(Error::Webfinger)?;
User::from_id(
User::from_id07(
conn,
link.href.as_ref().ok_or(Error::Webfinger)?,
None,
@ -256,53 +255,84 @@ impl User {
.ok_or(Error::Webfinger)
}
fn fetch(url: &str) -> Result<CustomPerson> {
let mut res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?;
fn fetch(url: &str) -> Result<CustomPerson07> {
let mut res = get(url, Self::get_sender07(), CONFIG.proxy().cloned())?;
let text = &res.text()?;
// without this workaround, publicKey is not correctly deserialized
let ap_sign = serde_json::from_str::<ApSignature>(text)?;
let mut json = serde_json::from_str::<CustomPerson>(text)?;
json.custom_props = ap_sign;
let ap_sign = serde_json::from_str::<ApSignature07>(text)?;
let person = serde_json::from_str::<Person07>(text)?;
let json = CustomPerson07::new(
ApActor::new(
person
.clone()
.id_unchecked()
.ok_or(Error::MissingApProperty)?
.to_owned(),
person,
),
ap_sign,
); // FIXME: Don't clone()
Ok(json)
}
pub fn fetch_from_url(conn: &DbConn, url: &str) -> Result<User> {
User::fetch(url).and_then(|json| User::from_activity(conn, json))
User::fetch(url).and_then(|json| User::from_activity07(conn, json))
}
pub fn refetch(&self, conn: &Connection) -> Result<()> {
User::fetch(&self.ap_url.clone()).and_then(|json| {
let avatar = Media::save_remote(
conn,
json.object
.object_props
.icon_image()? // FIXME: Fails when icon is not set
.object_props
.url_string()?,
json.ap_actor_ref()
.icon()
.ok_or(Error::MissingApProperty)? // FIXME: Fails when icon is not set
.iter()
.next()
.and_then(|i| {
i.clone()
.extend::<Image07, ImageType>() // FIXME: Don't clone()
.ok()?
.and_then(|url| Some(url.id_unchecked()?.to_string()))
})
.ok_or(Error::MissingApProperty)?,
self,
)
.ok();
let pub_key = &json.ext_one.public_key.public_key_pem;
diesel::update(self)
.set((
users::username.eq(json.object.ap_actor_props.preferred_username_string()?),
users::display_name.eq(json.object.object_props.name_string()?),
users::outbox_url.eq(json.object.ap_actor_props.outbox_string()?),
users::inbox_url.eq(json.object.ap_actor_props.inbox_string()?),
users::username.eq(json
.ap_actor_ref()
.preferred_username()
.ok_or(Error::MissingApProperty)?),
users::display_name.eq(json
.ap_actor_ref()
.name()
.ok_or(Error::MissingApProperty)?
.to_as_string()
.ok_or(Error::MissingApProperty)?),
users::outbox_url.eq(json
.ap_actor_ref()
.outbox()?
.ok_or(Error::MissingApProperty)?
.as_str()),
users::inbox_url.eq(json.ap_actor_ref().inbox()?.as_str()),
users::summary.eq(SafeString::new(
&json
.object
.object_props
.summary_string()
.ap_actor_ref()
.summary()
.and_then(|summary| summary.to_as_string())
.unwrap_or_default(),
)),
users::followers_endpoint.eq(json.object.ap_actor_props.followers_string()?),
users::followers_endpoint.eq(json
.ap_actor_ref()
.followers()?
.ok_or(Error::MissingApProperty)?
.as_str()),
users::avatar_id.eq(avatar.map(|a| a.id)),
users::last_fetched_date.eq(Utc::now().naive_utc()),
users::public_key.eq(json
.custom_props
.public_key_publickey()?
.public_key_pem_string()?),
users::public_key.eq(pub_key),
))
.execute(conn)
.map(|_| ())
@ -548,7 +578,7 @@ impl User {
Ok(coll)
}
fn fetch_outbox_page<T: Activity>(&self, url: &str) -> Result<(Vec<T>, Option<String>)> {
let mut res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?;
let mut res = get(url, Self::get_sender07(), CONFIG.proxy().cloned())?;
let text = &res.text()?;
let json: serde_json::Value = serde_json::from_str(text)?;
let items = json["items"]
@ -565,7 +595,7 @@ impl User {
&self,
url: &str,
) -> Result<(Vec<T>, Option<String>)> {
let mut res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?;
let mut res = get(url, Self::get_sender07(), CONFIG.proxy().cloned())?;
let text = &res.text()?;
let json: serde_json::Value = serde_json::from_str(text)?;
let items = json["items"]
@ -581,7 +611,7 @@ impl User {
pub fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> {
let mut res = get(
&self.outbox_url[..],
Self::get_sender(),
Self::get_sender07(),
CONFIG.proxy().cloned(),
)?;
let text = &res.text()?;
@ -617,7 +647,7 @@ impl User {
pub fn fetch_outbox07<T: Activity07 + serde::de::DeserializeOwned>(&self) -> Result<Vec<T>> {
let mut res = get(
&self.outbox_url[..],
Self::get_sender(),
Self::get_sender07(),
CONFIG.proxy().cloned(),
)?;
let text = &res.text()?;
@ -653,7 +683,7 @@ impl User {
pub fn fetch_followers_ids(&self) -> Result<Vec<String>> {
let mut res = get(
&self.followers_endpoint[..],
Self::get_sender(),
Self::get_sender07(),
CONFIG.proxy().cloned(),
)?;
let text = &res.text()?;
@ -1097,110 +1127,6 @@ impl IntoId for User {
impl Eq for User {}
impl FromId<DbConn> for User {
type Error = Error;
type Object = CustomPerson;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
Self::find_by_ap_url(conn, id)
}
fn from_activity(conn: &DbConn, acct: CustomPerson) -> Result<Self> {
let url = Url::parse(&acct.object.object_props.id_string()?)?;
let inst = url.host_str().ok_or(Error::Url)?;
let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
Instance::insert(
conn,
NewInstance {
name: inst.to_owned(),
public_domain: inst.to_owned(),
local: false,
// We don't really care about all the following for remote instances
long_description: SafeString::new(""),
short_description: SafeString::new(""),
default_license: String::new(),
open_registrations: true,
short_description_html: String::new(),
long_description_html: String::new(),
},
)
})?;
let username = acct.object.ap_actor_props.preferred_username_string()?;
if username.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
return Err(Error::InvalidValue);
}
let fqn = if instance.local {
username.clone()
} else {
format!("{}@{}", username, instance.public_domain)
};
let user = User::insert(
conn,
NewUser {
display_name: acct
.object
.object_props
.name_string()
.unwrap_or_else(|_| username.clone()),
username,
outbox_url: acct.object.ap_actor_props.outbox_string()?,
inbox_url: acct.object.ap_actor_props.inbox_string()?,
role: 2,
summary: acct
.object
.object_props
.summary_string()
.unwrap_or_default(),
summary_html: SafeString::new(
&acct
.object
.object_props
.summary_string()
.unwrap_or_default(),
),
email: None,
hashed_password: None,
instance_id: instance.id,
ap_url: acct.object.object_props.id_string()?,
public_key: acct
.custom_props
.public_key_publickey()?
.public_key_pem_string()?,
private_key: None,
shared_inbox_url: acct
.object
.ap_actor_props
.endpoints_endpoint()
.and_then(|e| e.shared_inbox_string())
.ok(),
followers_endpoint: acct.object.ap_actor_props.followers_string()?,
fqn,
avatar_id: None,
},
)?;
if let Ok(icon) = acct.object.object_props.icon_image() {
if let Ok(url) = icon.object_props.url_string() {
let avatar = Media::save_remote(conn, url, &user);
if let Ok(avatar) = avatar {
user.set_avatar(conn, avatar.id)?;
}
}
}
Ok(user)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl FromId07<DbConn> for User {
type Error = Error;
type Object = CustomPerson07;
@ -1690,32 +1616,6 @@ pub(crate) mod tests {
});
}
#[test]
fn self_federation() {
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let users = fill_database(&conn);
let ap_repr = users[0].to_activity(&conn).unwrap();
users[0].delete(&conn).unwrap();
let user = User::from_activity(&conn, ap_repr).unwrap();
assert_eq!(user.username, users[0].username);
assert_eq!(user.display_name, users[0].display_name);
assert_eq!(user.outbox_url, users[0].outbox_url);
assert_eq!(user.inbox_url, users[0].inbox_url);
assert_eq!(user.instance_id, users[0].instance_id);
assert_eq!(user.ap_url, users[0].ap_url);
assert_eq!(user.public_key, users[0].public_key);
assert_eq!(user.shared_inbox_url, users[0].shared_inbox_url);
assert_eq!(user.followers_endpoint, users[0].followers_endpoint);
assert_eq!(user.avatar_url(&conn), users[0].avatar_url(&conn));
assert_eq!(user.fqn, users[0].fqn);
assert_eq!(user.summary_html, users[0].summary_html);
Ok(())
});
}
#[test]
fn self_federation07() {
let conn = db();

View file

@ -1,5 +1,5 @@
use plume_common::activity_pub::{
inbox::FromId,
inbox::FromId07,
request::Digest,
sign::{verify_http_headers, Signable},
};
@ -26,7 +26,7 @@ pub fn handle_incoming(
.or_else(|| activity["actor"]["id"].as_str())
.ok_or(status::BadRequest(Some("Missing actor id for activity")))?;
let actor = User::from_id(&conn, actor_id, None, CONFIG.proxy())
let actor = User::from_id07(&conn, actor_id, None, CONFIG.proxy())
.expect("instance::shared_inbox: user error");
if !verify_http_headers(&actor, &headers.0, &sig).is_secure() && !act.clone().verify(&actor) {
// maybe we just know an old key?

View file

@ -11,7 +11,7 @@ use validator::{Validate, ValidationErrors};
use crate::inbox;
use crate::routes::{errors::ErrorPage, rocket_uri_macro_static_files, Page, RespondOrRedirect};
use crate::template_utils::{IntoContext, Ructe};
use plume_common::activity_pub::{broadcast, inbox::FromId};
use plume_common::activity_pub::{broadcast, inbox::FromId07};
use plume_models::{
admin::*,
blocklisted_emails::*,
@ -404,7 +404,7 @@ pub fn interact(conn: DbConn, user: Option<User>, target: String) -> Option<Redi
return Some(Redirect::to(uri!(super::user::details: name = target)));
}
if let Ok(post) = Post::from_id(&conn, &target, None, CONFIG.proxy()) {
if let Ok(post) = Post::from_id07(&conn, &target, None, CONFIG.proxy()) {
return Some(Redirect::to(uri!(
super::posts::details: blog = post.get_blog(&conn).expect("Can't retrieve blog").fqn,
slug = &post.slug,
@ -412,7 +412,7 @@ pub fn interact(conn: DbConn, user: Option<User>, target: String) -> Option<Redi
)));
}
if let Ok(comment) = Comment::from_id(&conn, &target, None, CONFIG.proxy()) {
if let Ok(comment) = Comment::from_id07(&conn, &target, None, CONFIG.proxy()) {
if comment.can_see(&conn, user.as_ref()) {
let post = comment.get_post(&conn).expect("Can't retrieve post");
return Some(Redirect::to(uri!(