Big refactoring of the Inbox (#443)
* Big refactoring of the Inbox We now have a type that routes an activity through the registered handlers until one of them matches. Each Actor/Activity/Object combination is represented by an implementation of AsObject These combinations are then registered on the Inbox type, which will try to deserialize the incoming activity in the requested types. Advantages: - nicer syntax: the final API is clearer and more idiomatic - more generic: only two traits (`AsActor` and `AsObject`) instead of one for each kind of activity - it is easier to see which activities we handle and which one we don't * Small fixes - Avoid panics - Don't search for AP ID infinitely - Code style issues * Fix tests * Introduce a new trait: FromId It should be implemented for any AP object. It allows to look for an object in database using its AP ID, or to dereference it if it was not present in database Also moves the inbox code to plume-models to test it (and write a basic test for each activity type we handle) * Use if let instead of match * Don't require PlumeRocket::intl for tests * Return early and remove a forgotten dbg! * Add more tests to try to understand where the issues come from * Also add a test for comment federation * Don't check creation_date is the same for blogs * Make user and blog federation more tolerant to errors/missing fields * Make clippy happy * Use the correct Accept header when dereferencing * Fix follow approval with Mastodon * Add spaces to characters that should not be in usernames And validate blog names too * Smarter dereferencing: only do it once for each actor/object * Forgot some files * Cargo fmt * Delete plume_test * Delete plume_tests * Update get_id docs + Remove useless : Sized * Appease cargo fmt * Remove dbg! + Use as_ref instead of clone when possible + Use and_then instead of map when possible * Remove .po~ * send unfollow to local instance * read cover from update activity * Make sure "cc" and "to" are never empty and fix a typo in a constant name * Cargo fmt
This commit is contained in:
+63
-115
@@ -10,29 +10,23 @@ use serde_json;
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
use validator::{Validate, ValidationError, ValidationErrors};
|
||||
|
||||
use inbox::{Inbox, SignedJson};
|
||||
use plume_common::activity_pub::{
|
||||
broadcast,
|
||||
inbox::{Deletable, FromActivity, Notify},
|
||||
sign::{verify_http_headers, Signable},
|
||||
ActivityStream, ApRequest, Id, IntoId,
|
||||
};
|
||||
use inbox;
|
||||
use plume_common::activity_pub::{broadcast, inbox::FromId, ActivityStream, ApRequest, Id};
|
||||
use plume_common::utils;
|
||||
use plume_models::{
|
||||
blogs::Blog,
|
||||
db_conn::DbConn,
|
||||
follows,
|
||||
headers::Headers,
|
||||
inbox::inbox as local_inbox,
|
||||
instance::Instance,
|
||||
posts::{LicensedArticle, Post},
|
||||
reshares::Reshare,
|
||||
users::*,
|
||||
Error,
|
||||
Error, PlumeRocket,
|
||||
};
|
||||
use routes::{errors::ErrorPage, Page, PlumeRocket};
|
||||
use routes::{errors::ErrorPage, Page};
|
||||
use template_utils::Ructe;
|
||||
use Searcher;
|
||||
use Worker;
|
||||
|
||||
#[get("/me")]
|
||||
pub fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
|
||||
@@ -46,21 +40,19 @@ pub fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
|
||||
pub fn details(
|
||||
name: String,
|
||||
rockets: PlumeRocket,
|
||||
fetch_articles_conn: DbConn,
|
||||
fetch_followers_conn: DbConn,
|
||||
fetch_rockets: PlumeRocket,
|
||||
fetch_followers_rockets: PlumeRocket,
|
||||
update_conn: DbConn,
|
||||
) -> Result<Ructe, ErrorPage> {
|
||||
let conn = rockets.conn;
|
||||
let user = User::find_by_fqn(&*conn, &name)?;
|
||||
let conn = &*rockets.conn;
|
||||
let user = User::find_by_fqn(&rockets, &name)?;
|
||||
let recents = Post::get_recents_for_author(&*conn, &user, 6)?;
|
||||
let reshares = Reshare::get_recents_for_author(&*conn, &user, 6)?;
|
||||
let searcher = rockets.searcher;
|
||||
let worker = rockets.worker;
|
||||
|
||||
if !user.get_instance(&*conn)?.local {
|
||||
// Fetch new articles
|
||||
let user_clone = user.clone();
|
||||
let searcher = searcher.clone();
|
||||
worker.execute(move || {
|
||||
for create_act in user_clone
|
||||
.fetch_outbox::<Create>()
|
||||
@@ -68,12 +60,8 @@ pub fn details(
|
||||
{
|
||||
match create_act.create_props.object_object::<LicensedArticle>() {
|
||||
Ok(article) => {
|
||||
Post::from_activity(
|
||||
&(&*fetch_articles_conn, &searcher),
|
||||
article,
|
||||
user_clone.clone().into_id(),
|
||||
)
|
||||
.expect("Article from remote user couldn't be saved");
|
||||
Post::from_activity(&fetch_rockets, article)
|
||||
.expect("Article from remote user couldn't be saved");
|
||||
println!("Fetched article from remote user");
|
||||
}
|
||||
Err(e) => println!("Error while fetching articles in background: {:?}", e),
|
||||
@@ -88,10 +76,10 @@ pub fn details(
|
||||
.fetch_followers_ids()
|
||||
.expect("Remote user: fetching followers error")
|
||||
{
|
||||
let follower = User::from_url(&*fetch_followers_conn, &user_id)
|
||||
let follower = User::from_id(&fetch_followers_rockets, &user_id, None)
|
||||
.expect("user::details: Couldn't fetch follower");
|
||||
follows::Follow::insert(
|
||||
&*fetch_followers_conn,
|
||||
&*fetch_followers_rockets.conn,
|
||||
follows::NewFollow {
|
||||
follower_id: follower.id,
|
||||
following_id: user_clone.id,
|
||||
@@ -153,16 +141,19 @@ pub fn dashboard_auth(i18n: I18n) -> Flash<Redirect> {
|
||||
}
|
||||
|
||||
#[post("/@/<name>/follow")]
|
||||
pub fn follow(
|
||||
name: String,
|
||||
conn: DbConn,
|
||||
user: User,
|
||||
worker: Worker,
|
||||
) -> Result<Redirect, ErrorPage> {
|
||||
let target = User::find_by_fqn(&*conn, &name)?;
|
||||
pub fn follow(name: String, user: User, rockets: PlumeRocket) -> Result<Redirect, ErrorPage> {
|
||||
let conn = &*rockets.conn;
|
||||
let target = User::find_by_fqn(&rockets, &name)?;
|
||||
if let Ok(follow) = follows::Follow::find(&*conn, user.id, target.id) {
|
||||
let delete_act = follow.delete(&*conn)?;
|
||||
worker.execute(move || broadcast(&user, delete_act, vec![target]));
|
||||
let delete_act = follow.build_undo(&*conn)?;
|
||||
local_inbox(
|
||||
&rockets,
|
||||
serde_json::to_value(&delete_act).map_err(Error::from)?,
|
||||
)?;
|
||||
|
||||
rockets
|
||||
.worker
|
||||
.execute(move || broadcast(&user, delete_act, vec![target]));
|
||||
} else {
|
||||
let f = follows::Follow::insert(
|
||||
&*conn,
|
||||
@@ -175,7 +166,9 @@ pub fn follow(
|
||||
f.notify(&*conn)?;
|
||||
|
||||
let act = f.to_activity(&*conn)?;
|
||||
worker.execute(move || broadcast(&user, act, vec![target]));
|
||||
rockets
|
||||
.worker
|
||||
.execute(move || broadcast(&user, act, vec![target]));
|
||||
}
|
||||
Ok(Redirect::to(uri!(details: name = name)))
|
||||
}
|
||||
@@ -194,19 +187,19 @@ pub fn follow_auth(name: String, i18n: I18n) -> Flash<Redirect> {
|
||||
#[get("/@/<name>/followers?<page>", rank = 2)]
|
||||
pub fn followers(
|
||||
name: String,
|
||||
conn: DbConn,
|
||||
account: Option<User>,
|
||||
page: Option<Page>,
|
||||
intl: I18n,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Ructe, ErrorPage> {
|
||||
let conn = &*rockets.conn;
|
||||
let page = page.unwrap_or_default();
|
||||
let user = User::find_by_fqn(&*conn, &name)?;
|
||||
let user = User::find_by_fqn(&rockets, &name)?;
|
||||
let followers_count = user.count_followers(&*conn)?;
|
||||
|
||||
Ok(render!(users::followers(
|
||||
&(&*conn, &intl.catalog, account.clone()),
|
||||
&(&*conn, &rockets.intl.catalog, rockets.user.clone()),
|
||||
user.clone(),
|
||||
account
|
||||
rockets
|
||||
.user
|
||||
.and_then(|x| x.is_following(&*conn, user.id).ok())
|
||||
.unwrap_or(false),
|
||||
user.instance_id != Instance::get_local(&*conn)?.id,
|
||||
@@ -220,19 +213,19 @@ pub fn followers(
|
||||
#[get("/@/<name>/followed?<page>", rank = 2)]
|
||||
pub fn followed(
|
||||
name: String,
|
||||
conn: DbConn,
|
||||
account: Option<User>,
|
||||
page: Option<Page>,
|
||||
intl: I18n,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Ructe, ErrorPage> {
|
||||
let conn = &*rockets.conn;
|
||||
let page = page.unwrap_or_default();
|
||||
let user = User::find_by_fqn(&*conn, &name)?;
|
||||
let user = User::find_by_fqn(&rockets, &name)?;
|
||||
let followed_count = user.count_followed(&*conn)?;
|
||||
|
||||
Ok(render!(users::followed(
|
||||
&(&*conn, &intl.catalog, account.clone()),
|
||||
&(&*conn, &rockets.intl.catalog, rockets.user.clone()),
|
||||
user.clone(),
|
||||
account
|
||||
rockets
|
||||
.user
|
||||
.and_then(|x| x.is_following(&*conn, user.id).ok())
|
||||
.unwrap_or(false),
|
||||
user.instance_id != Instance::get_local(&*conn)?.id,
|
||||
@@ -246,11 +239,11 @@ pub fn followed(
|
||||
#[get("/@/<name>", rank = 1)]
|
||||
pub fn activity_details(
|
||||
name: String,
|
||||
conn: DbConn,
|
||||
rockets: PlumeRocket,
|
||||
_ap: ApRequest,
|
||||
) -> Option<ActivityStream<CustomPerson>> {
|
||||
let user = User::find_by_fqn(&*conn, &name).ok()?;
|
||||
Some(ActivityStream::new(user.to_activity(&*conn).ok()?))
|
||||
let user = User::find_by_fqn(&rockets, &name).ok()?;
|
||||
Some(ActivityStream::new(user.to_activity(&*rockets.conn).ok()?))
|
||||
}
|
||||
|
||||
#[get("/users/new")]
|
||||
@@ -329,14 +322,13 @@ pub fn update(
|
||||
#[post("/@/<name>/delete")]
|
||||
pub fn delete(
|
||||
name: String,
|
||||
conn: DbConn,
|
||||
user: User,
|
||||
mut cookies: Cookies,
|
||||
searcher: Searcher,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Redirect, ErrorPage> {
|
||||
let account = User::find_by_fqn(&*conn, &name)?;
|
||||
let account = User::find_by_fqn(&rockets, &name)?;
|
||||
if user.id == account.id {
|
||||
account.delete(&*conn, &searcher)?;
|
||||
account.delete(&*rockets.conn, &rockets.searcher)?;
|
||||
|
||||
if let Some(cookie) = cookies.get_private(AUTH_COOKIE) {
|
||||
cookies.remove_private(cookie);
|
||||
@@ -439,76 +431,31 @@ pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Resul
|
||||
}
|
||||
|
||||
#[get("/@/<name>/outbox")]
|
||||
pub fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection>> {
|
||||
let user = User::find_by_fqn(&*conn, &name).ok()?;
|
||||
user.outbox(&*conn).ok()
|
||||
pub fn outbox(name: String, rockets: PlumeRocket) -> Option<ActivityStream<OrderedCollection>> {
|
||||
let user = User::find_by_fqn(&rockets, &name).ok()?;
|
||||
user.outbox(&*rockets.conn).ok()
|
||||
}
|
||||
|
||||
#[post("/@/<name>/inbox", data = "<data>")]
|
||||
pub fn inbox(
|
||||
name: String,
|
||||
conn: DbConn,
|
||||
data: SignedJson<serde_json::Value>,
|
||||
data: inbox::SignedJson<serde_json::Value>,
|
||||
headers: Headers,
|
||||
searcher: Searcher,
|
||||
) -> Result<String, Option<status::BadRequest<&'static str>>> {
|
||||
let user = User::find_by_fqn(&*conn, &name).map_err(|_| None)?;
|
||||
let act = data.1.into_inner();
|
||||
let sig = data.0;
|
||||
|
||||
let activity = act.clone();
|
||||
let actor_id = activity["actor"]
|
||||
.as_str()
|
||||
.or_else(|| activity["actor"]["id"].as_str())
|
||||
.ok_or(Some(status::BadRequest(Some(
|
||||
"Missing actor id for activity",
|
||||
))))?;
|
||||
|
||||
let actor = User::from_url(&conn, actor_id).expect("user::inbox: user error");
|
||||
if !verify_http_headers(&actor, &headers.0, &sig).is_secure() && !act.clone().verify(&actor) {
|
||||
// maybe we just know an old key?
|
||||
actor
|
||||
.refetch(&conn)
|
||||
.and_then(|_| User::get(&conn, actor.id))
|
||||
.and_then(|actor| {
|
||||
if verify_http_headers(&actor, &headers.0, &sig).is_secure()
|
||||
|| act.clone().verify(&actor)
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Signature)
|
||||
}
|
||||
})
|
||||
.map_err(|_| {
|
||||
println!(
|
||||
"Rejected invalid activity supposedly from {}, with headers {:?}",
|
||||
actor.username, headers.0
|
||||
);
|
||||
status::BadRequest(Some("Invalid signature"))
|
||||
})?;
|
||||
}
|
||||
|
||||
if Instance::is_blocked(&*conn, actor_id).map_err(|_| None)? {
|
||||
return Ok(String::new());
|
||||
}
|
||||
Ok(match user.received(&*conn, &searcher, act) {
|
||||
Ok(_) => String::new(),
|
||||
Err(e) => {
|
||||
println!("User inbox error: {}\n{}", e.as_fail(), e.backtrace());
|
||||
format!("Error: {}", e.as_fail())
|
||||
}
|
||||
})
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<String, status::BadRequest<&'static str>> {
|
||||
User::find_by_fqn(&rockets, &name).map_err(|_| status::BadRequest(Some("User not found")))?;
|
||||
inbox::handle_incoming(rockets, data, headers)
|
||||
}
|
||||
|
||||
#[get("/@/<name>/followers", rank = 1)]
|
||||
pub fn ap_followers(
|
||||
name: String,
|
||||
conn: DbConn,
|
||||
rockets: PlumeRocket,
|
||||
_ap: ApRequest,
|
||||
) -> Option<ActivityStream<OrderedCollection>> {
|
||||
let user = User::find_by_fqn(&*conn, &name).ok()?;
|
||||
let user = User::find_by_fqn(&rockets, &name).ok()?;
|
||||
let followers = user
|
||||
.get_followers(&*conn)
|
||||
.get_followers(&*rockets.conn)
|
||||
.ok()?
|
||||
.into_iter()
|
||||
.map(|f| Id::new(f.ap_url))
|
||||
@@ -526,18 +473,19 @@ pub fn ap_followers(
|
||||
}
|
||||
|
||||
#[get("/@/<name>/atom.xml")]
|
||||
pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
|
||||
let author = User::find_by_fqn(&*conn, &name).ok()?;
|
||||
pub fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>> {
|
||||
let conn = &*rockets.conn;
|
||||
let author = User::find_by_fqn(&rockets, &name).ok()?;
|
||||
let feed = FeedBuilder::default()
|
||||
.title(author.display_name.clone())
|
||||
.id(Instance::get_local(&*conn)
|
||||
.id(Instance::get_local(conn)
|
||||
.unwrap()
|
||||
.compute_box("@", &name, "atom.xml"))
|
||||
.entries(
|
||||
Post::get_recents_for_author(&*conn, &author, 15)
|
||||
Post::get_recents_for_author(conn, &author, 15)
|
||||
.ok()?
|
||||
.into_iter()
|
||||
.map(|p| super::post_to_atom(p, &*conn))
|
||||
.map(|p| super::post_to_atom(p, conn))
|
||||
.collect::<Vec<Entry>>(),
|
||||
)
|
||||
.build()
|
||||
|
||||
Reference in New Issue
Block a user