Plume/plume-models/src/posts.rs

1769 lines
59 KiB
Rust
Raw Normal View History

2020-01-21 07:02:03 +01:00
use crate::{
ap_url, blogs::Blog, db_conn::DbConn, instance::Instance, medias::Media, mentions::Mention,
post_authors::*, safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User,
Connection, Error, PostEvent::*, Result, CONFIG, POST_CHAN,
2020-01-21 07:02:03 +01:00
};
use activitypub::{
2018-09-06 23:39:22 +02:00
activity::{Create, Delete, Update},
2018-06-20 21:42:16 +02:00
link,
object::{Article, Image, Tombstone},
2019-03-20 17:56:17 +01:00
CustomObject,
};
2022-03-06 16:15:36 +01:00
use activitystreams::{
2022-03-21 02:32:12 +01:00
activity::{Create as Create07, Delete as Delete07, Update as Update07},
2022-03-21 01:34:33 +01:00
base::{AnyBase, Base},
2022-03-06 16:15:36 +01:00
iri_string::types::IriString,
2022-03-21 16:20:53 +01:00
link::{self as link07, kind::MentionType},
object::{
2022-05-01 02:56:03 +02:00
kind::ImageType, ApObject, Article as Article07, AsApObject, Image as Image07,
Tombstone as Tombstone07,
},
2022-03-06 16:15:36 +01:00
prelude::*,
time::OffsetDateTime,
};
use chrono::{NaiveDateTime, TimeZone, Utc};
2022-01-02 18:21:34 +01:00
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl};
2021-01-19 15:31:31 +01:00
use once_cell::sync::Lazy;
use plume_common::{
activity_pub::{
2022-05-02 10:43:03 +02:00
inbox::{AsActor, AsObject, FromId},
2021-11-24 14:50:16 +01:00
sign::Signer,
2022-03-21 16:20:53 +01:00
Hashtag, Hashtag07, HashtagType07, Id, IntoId, Licensed, Licensed07,
LicensedArticle as LicensedArticle07, Source, SourceProperty, ToAsString, ToAsUri,
PUBLIC_VISIBILITY,
},
utils::{iri_percent_encode_seg, md_to_html},
};
use riker::actors::{Publish, Tell};
2021-01-19 15:31:31 +01:00
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
2018-04-23 15:41:43 +02:00
pub type LicensedArticle = CustomObject<Licensed, Article>;
2021-01-19 15:31:31 +01:00
static BLOG_FQN_CACHE: Lazy<Mutex<HashMap<i32, String>>> = Lazy::new(|| Mutex::new(HashMap::new()));
2021-01-06 19:39:49 +01:00
#[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)]
#[changeset_options(treat_none_as_null = "true")]
2018-04-23 15:41:43 +02:00
pub struct Post {
pub id: i32,
pub blog_id: i32,
pub slug: String,
pub title: String,
pub content: SafeString,
2018-04-23 15:41:43 +02:00
pub published: bool,
2018-04-30 19:46:27 +02:00
pub license: String,
2018-09-27 23:06:40 +02:00
pub creation_date: NaiveDateTime,
2018-09-04 13:26:13 +02:00
pub ap_url: String,
pub subtitle: String,
pub source: String,
pub cover_id: Option<i32>,
2018-04-23 15:41:43 +02:00
}
#[derive(Insertable)]
#[table_name = "posts"]
pub struct NewPost {
2018-09-04 13:26:13 +02:00
pub blog_id: i32,
2018-04-23 15:41:43 +02:00
pub slug: String,
pub title: String,
pub content: SafeString,
2018-04-23 15:41:43 +02:00
pub published: bool,
2018-05-10 12:52:56 +02:00
pub license: String,
pub creation_date: Option<NaiveDateTime>,
2018-09-04 13:26:13 +02:00
pub ap_url: String,
pub subtitle: String,
pub source: String,
pub cover_id: Option<i32>,
2018-04-23 15:41:43 +02:00
}
impl Post {
get!(posts);
find_by!(posts, find_by_slug, slug as &str, blog_id as i32);
find_by!(posts, find_by_ap_url, ap_url as &str);
2018-04-23 16:25:39 +02:00
last!(posts);
2022-01-02 18:21:34 +01:00
pub fn insert(conn: &Connection, mut new: NewPost) -> Result<Self> {
if new.ap_url.is_empty() {
let blog = Blog::get(conn, new.blog_id)?;
new.ap_url = Self::ap_url(blog, &new.slug);
}
diesel::insert_into(posts::table)
.values(new)
.execute(conn)?;
2022-01-02 18:21:34 +01:00
let post = Self::last(conn)?;
if post.published {
post.publish_published();
}
Ok(post)
}
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
2019-04-17 19:31:47 +02:00
pub fn update(&self, conn: &Connection) -> Result<Self> {
2019-03-20 17:56:17 +01:00
diesel::update(self).set(self).execute(conn)?;
let post = Self::get(conn, self.id)?;
// TODO: Call publish_published() when newly published
if post.published {
let blog = post.get_blog(conn);
if blog.is_ok() && blog.unwrap().is_local() {
self.publish_updated();
}
}
Ok(post)
}
pub fn delete(&self, conn: &Connection) -> Result<()> {
2021-11-27 23:53:13 +01:00
for m in Mention::list_for_post(conn, self.id)? {
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
2019-04-17 19:31:47 +02:00
m.delete(conn)?;
}
diesel::delete(self).execute(conn)?;
self.publish_deleted();
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
2019-04-17 19:31:47 +02:00
Ok(())
}
2019-03-20 17:56:17 +01:00
pub fn list_by_tag(
conn: &Connection,
tag: String,
(min, max): (i32, i32),
) -> Result<Vec<Post>> {
2020-01-21 07:02:03 +01:00
use crate::schema::tags;
2018-09-06 14:06:04 +02:00
let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id);
posts::table
.filter(posts::id.eq_any(ids))
.filter(posts::published.eq(true))
2018-09-06 14:06:04 +02:00
.order(posts::creation_date.desc())
.offset(min.into())
.limit((max - min).into())
2018-09-27 23:06:40 +02:00
.load(conn)
.map_err(Error::from)
2018-09-06 14:06:04 +02:00
}
pub fn count_for_tag(conn: &Connection, tag: String) -> Result<i64> {
2020-01-21 07:02:03 +01:00
use crate::schema::tags;
2018-09-06 14:06:04 +02:00
let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id);
posts::table
.filter(posts::id.eq_any(ids))
.filter(posts::published.eq(true))
2018-09-06 14:06:04 +02:00
.count()
.load(conn)?
2021-01-15 15:17:00 +01:00
.get(0)
.cloned()
.ok_or(Error::NotFound)
2018-09-06 14:06:04 +02:00
}
pub fn count_local(conn: &Connection) -> Result<i64> {
2020-01-21 07:02:03 +01:00
use crate::schema::post_authors;
use crate::schema::users;
let local_authors = users::table
.filter(users::instance_id.eq(Instance::get_local()?.id))
.select(users::id);
let local_posts_id = post_authors::table
.filter(post_authors::author_id.eq_any(local_authors))
.select(post_authors::post_id);
posts::table
.filter(posts::id.eq_any(local_posts_id))
.filter(posts::published.eq(true))
.count()
.get_result(conn)
.map_err(Error::from)
2018-06-10 21:33:42 +02:00
}
pub fn count(conn: &Connection) -> Result<i64> {
posts::table
.filter(posts::published.eq(true))
.count()
.get_result(conn)
.map_err(Error::from)
2018-07-25 15:20:09 +02:00
}
pub fn list_filtered(
conn: &Connection,
title: Option<String>,
subtitle: Option<String>,
content: Option<String>,
) -> Result<Vec<Post>> {
let mut query = posts::table.into_boxed();
if let Some(title) = title {
query = query.filter(posts::title.eq(title));
}
if let Some(subtitle) = subtitle {
query = query.filter(posts::subtitle.eq(subtitle));
}
if let Some(content) = content {
query = query.filter(posts::content.eq(content));
}
query.get_results::<Post>(conn).map_err(Error::from)
}
2019-03-20 17:56:17 +01:00
pub fn get_recents_for_author(
conn: &Connection,
author: &User,
limit: i64,
) -> Result<Vec<Post>> {
2020-01-21 07:02:03 +01:00
use crate::schema::post_authors;
let posts = PostAuthor::belonging_to(author).select(post_authors::post_id);
posts::table
.filter(posts::id.eq_any(posts))
.filter(posts::published.eq(true))
.order(posts::creation_date.desc())
.limit(limit)
.load::<Post>(conn)
.map_err(Error::from)
}
pub fn get_recents_for_blog(conn: &Connection, blog: &Blog, limit: i64) -> Result<Vec<Post>> {
posts::table
.filter(posts::blog_id.eq(blog.id))
.filter(posts::published.eq(true))
.order(posts::creation_date.desc())
.limit(limit)
.load::<Post>(conn)
.map_err(Error::from)
}
pub fn get_for_blog(conn: &Connection, blog: &Blog) -> Result<Vec<Post>> {
posts::table
.filter(posts::blog_id.eq(blog.id))
.filter(posts::published.eq(true))
.load::<Post>(conn)
.map_err(Error::from)
}
pub fn count_for_blog(conn: &Connection, blog: &Blog) -> Result<i64> {
posts::table
.filter(posts::blog_id.eq(blog.id))
.filter(posts::published.eq(true))
.count()
.get_result(conn)
.map_err(Error::from)
}
pub fn blog_page(conn: &Connection, blog: &Blog, (min, max): (i32, i32)) -> Result<Vec<Post>> {
posts::table
.filter(posts::blog_id.eq(blog.id))
.filter(posts::published.eq(true))
.order(posts::creation_date.desc())
.offset(min.into())
.limit((max - min).into())
.load::<Post>(conn)
.map_err(Error::from)
}
pub fn drafts_by_author(conn: &Connection, author: &User) -> Result<Vec<Post>> {
2020-01-21 07:02:03 +01:00
use crate::schema::post_authors;
let posts = PostAuthor::belonging_to(author).select(post_authors::post_id);
posts::table
.order(posts::creation_date.desc())
.filter(posts::published.eq(false))
2018-09-27 23:06:40 +02:00
.filter(posts::id.eq_any(posts))
.load::<Post>(conn)
.map_err(Error::from)
}
2021-04-09 03:55:09 +02:00
pub fn ap_url(blog: Blog, slug: &str) -> String {
ap_url(&format!(
"{}/~/{}/{}/",
CONFIG.base_url,
blog.fqn,
iri_percent_encode_seg(slug)
))
2021-04-09 03:55:09 +02:00
}
2021-04-10 09:30:50 +02:00
// It's better to calc slug in insert and update
pub fn slug(title: &str) -> &str {
title
}
pub fn get_authors(&self, conn: &Connection) -> Result<Vec<User>> {
2020-01-21 07:02:03 +01:00
use crate::schema::post_authors;
use crate::schema::users;
2018-04-30 18:50:35 +02:00
let author_list = PostAuthor::belonging_to(self).select(post_authors::author_id);
users::table
.filter(users::id.eq_any(author_list))
.load::<User>(conn)
.map_err(Error::from)
2018-04-30 18:50:35 +02:00
}
2018-05-03 17:22:40 +02:00
pub fn is_author(&self, conn: &Connection, author_id: i32) -> Result<bool> {
2020-01-21 07:02:03 +01:00
use crate::schema::post_authors;
Ok(PostAuthor::belonging_to(self)
.filter(post_authors::author_id.eq(author_id))
.count()
2019-03-20 17:56:17 +01:00
.get_result::<i64>(conn)?
> 0)
}
pub fn get_blog(&self, conn: &Connection) -> Result<Blog> {
2020-01-21 07:02:03 +01:00
use crate::schema::blogs;
blogs::table
.filter(blogs::id.eq(self.blog_id))
Add support for generic timeline (#525) * Begin adding support for timeline * fix some bugs with parser * fmt * add error reporting for parser * add tests for timeline query parser * add rejection tests for parse * begin adding support for lists also run migration before compiling, so schema.rs is up to date * add sqlite migration * end adding lists still miss tests and query integration * cargo fmt * try to add some tests * Add some constraint to db, and fix list test and refactor other tests to use begin_transaction * add more tests for lists * add support for lists in query executor * add keywords for including/excluding boosts and likes * cargo fmt * add function to list lists used by query will make it easier to warn users when creating timeline with unknown lists * add lang support * add timeline creation error message when using unexisting lists * Update .po files * WIP: interface for timelines * don't use diesel for migrations not sure how it passed the ci on the other branch * add some tests for timeline add an int representing the order of timelines (first one will be on top, second just under...) use first() instead of limit(1).get().into_iter().nth(0) remove migrations from build artifacts as they are now compiled in * cargo fmt * remove timeline order * fix tests * add tests for timeline creation failure * cargo fmt * add tests for timelines * add test for matching direct lists and keywords * add test for language filtering * Add a more complex test for Timeline::matches, and fix TQ::matches for TQ::Or * Make the main crate compile + FMT * Use the new timeline system - Replace the old "feed" system with timelines - Display all timelines someone can access on their home page (either their personal ones, or instance timelines) - Remove functions that were used to get user/local/federated feed - Add new posts to timelines - Create a default timeline called "My feed" for everyone, and "Local feed"/"Federated feed" with timelines @fdb-hiroshima I don't know if that's how you pictured it? If you imagined it differently I can of course make changes. I hope I didn't forgot anything… * Cargo fmt * Try to fix the migration * Fix tests * Fix the test (for real this time ?) * Fix the tests ? + fmt * Use Kind::Like and Kind::Reshare when needed * Forgot to run cargo fmt once again * revert translations * fix reviewed stuff * reduce code duplication by macros * cargo fmt
2019-10-07 19:08:20 +02:00
.first(conn)
.map_err(Error::from)
2018-05-03 17:22:40 +02:00
}
2018-05-10 12:52:56 +02:00
2021-01-19 15:31:31 +01:00
/// This method exists for use in templates to reduce database access.
/// This should not be used for other purpose.
///
/// This caches query result. The best way to cache query result is holding it in `Post`s field
/// but Diesel doesn't allow it currently.
/// If sometime Diesel allow it, this method should be removed.
pub fn get_blog_fqn(&self, conn: &Connection) -> String {
if let Some(blog_fqn) = BLOG_FQN_CACHE.lock().unwrap().get(&self.blog_id) {
return blog_fqn.to_string();
}
let blog_fqn = self.get_blog(conn).unwrap().fqn;
BLOG_FQN_CACHE
.lock()
.unwrap()
.insert(self.blog_id, blog_fqn.clone());
blog_fqn
}
pub fn count_likes(&self, conn: &Connection) -> Result<i64> {
2020-01-21 07:02:03 +01:00
use crate::schema::likes;
likes::table
.filter(likes::post_id.eq(self.id))
.count()
.get_result(conn)
.map_err(Error::from)
2018-05-10 18:38:03 +02:00
}
pub fn count_reshares(&self, conn: &Connection) -> Result<i64> {
2020-01-21 07:02:03 +01:00
use crate::schema::reshares;
reshares::table
.filter(reshares::post_id.eq(self.id))
.count()
.get_result(conn)
.map_err(Error::from)
2018-05-19 11:57:39 +02:00
}
pub fn get_receivers_urls(&self, conn: &Connection) -> Result<Vec<String>> {
2021-01-15 15:17:00 +01:00
Ok(self
2021-01-15 17:13:45 +01:00
.get_authors(conn)?
.into_iter()
.filter_map(|a| a.get_followers(conn).ok())
.fold(vec![], |mut acc, f| {
for x in f {
acc.push(x.ap_url);
}
acc
}))
2018-05-10 17:36:32 +02:00
}
2018-05-19 00:04:30 +02:00
pub fn to_activity(&self, conn: &Connection) -> Result<LicensedArticle> {
let cc = self.get_receivers_urls(conn)?;
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
2019-04-17 19:31:47 +02:00
let to = vec![PUBLIC_VISIBILITY.to_string()];
2018-05-19 00:04:30 +02:00
let mut mentions_json = Mention::list_for_post(conn, self.id)?
.into_iter()
.map(|m| json!(m.to_activity(conn).ok()))
.collect::<Vec<serde_json::Value>>();
let mut tags_json = Tag::for_post(conn, self.id)?
.into_iter()
.map(|t| json!(t.to_activity().ok()))
.collect::<Vec<serde_json::Value>>();
mentions_json.append(&mut tags_json);
2018-06-20 22:58:11 +02:00
2018-05-19 00:04:30 +02:00
let mut article = Article::default();
2019-03-20 17:56:17 +01:00
article.object_props.set_name_string(self.title.clone())?;
article.object_props.set_id_string(self.ap_url.clone())?;
let mut authors = self
.get_authors(conn)?
.into_iter()
.map(|x| Id::new(x.ap_url))
.collect::<Vec<Id>>();
authors.push(self.get_blog(conn)?.into_id()); // add the blog URL here too
article
.object_props
.set_attributed_to_link_vec::<Id>(authors)?;
article
.object_props
.set_content_string(self.content.get().clone())?;
2019-03-20 17:56:17 +01:00
article.ap_object_props.set_source_object(Source {
content: self.source.clone(),
media_type: String::from("text/markdown"),
})?;
article
.object_props
.set_published_utctime(Utc.from_utc_datetime(&self.creation_date))?;
article
.object_props
.set_summary_string(self.subtitle.clone())?;
article.object_props.tag = Some(json!(mentions_json));
2018-10-31 10:40:20 +01:00
if let Some(media_id) = self.cover_id {
let media = Media::get(conn, media_id)?;
2018-10-31 10:40:20 +01:00
let mut cover = Image::default();
cover.object_props.set_url_string(media.url()?)?;
2018-10-31 10:40:20 +01:00
if media.sensitive {
cover
.object_props
.set_summary_string(media.content_warning.unwrap_or_default())?;
2018-10-31 10:40:20 +01:00
}
2019-03-20 17:56:17 +01:00
cover.object_props.set_content_string(media.alt_text)?;
cover
.object_props
2019-03-20 17:56:17 +01:00
.set_attributed_to_link_vec(vec![User::get(conn, media.owner_id)?.into_id()])?;
article.object_props.set_icon_object(cover)?;
2018-10-31 10:40:20 +01:00
}
2019-03-20 17:56:17 +01:00
article.object_props.set_url_string(self.ap_url.clone())?;
article
.object_props
.set_to_link_vec::<Id>(to.into_iter().map(Id::new).collect())?;
article
.object_props
.set_cc_link_vec::<Id>(cc.into_iter().map(Id::new).collect())?;
let mut license = Licensed::default();
license.set_license_string(self.license.clone())?;
Ok(LicensedArticle::new(article, license))
2018-05-19 00:04:30 +02:00
}
2022-03-06 16:15:36 +01:00
pub fn to_activity07(&self, conn: &Connection) -> Result<LicensedArticle07> {
let cc = self.get_receivers_urls(conn)?;
let to = vec![PUBLIC_VISIBILITY.to_string()];
let mut mentions_json = Mention::list_for_post(conn, self.id)?
.into_iter()
.map(|m| json!(m.to_activity07(conn).ok()))
.collect::<Vec<serde_json::Value>>();
let mut tags_json = Tag::for_post(conn, self.id)?
.into_iter()
.map(|t| json!(t.to_activity07().ok()))
.collect::<Vec<serde_json::Value>>();
mentions_json.append(&mut tags_json);
let mut article = ApObject::new(Article07::new());
article.set_name(self.title.clone());
article.set_id(self.ap_url.parse::<IriString>()?);
let mut authors = self
.get_authors(conn)?
.into_iter()
.filter_map(|x| x.ap_url.parse::<IriString>().ok())
.collect::<Vec<IriString>>();
authors.push(self.get_blog(conn)?.ap_url.parse::<IriString>()?); // add the blog URL here too
article.set_many_attributed_tos(authors);
article.set_content(self.content.get().clone());
let source = SourceProperty {
2022-03-06 16:33:59 +01:00
source: Source {
content: self.source.clone(),
media_type: String::from("text/markdown"),
},
2022-03-06 16:15:36 +01:00
};
article.set_published(
OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos().into())
.expect("OffsetDateTime"),
);
article.set_summary(&*self.subtitle);
2022-03-20 22:38:39 +01:00
article.set_many_tags(
mentions_json
.iter()
.filter_map(|mention_json| AnyBase::from_arbitrary_json(mention_json).ok()),
);
2022-03-06 16:15:36 +01:00
if let Some(media_id) = self.cover_id {
let media = Media::get(conn, media_id)?;
let mut cover = Image07::new();
cover.set_url(media.url()?);
if media.sensitive {
cover.set_summary(media.content_warning.unwrap_or_default());
}
cover.set_content(media.alt_text);
cover.set_many_attributed_tos(vec![User::get(conn, media.owner_id)?
.ap_url
.parse::<IriString>()?]);
article.set_icon(cover.into_any_base()?);
}
article.set_url(self.ap_url.parse::<IriString>()?);
article.set_many_tos(
to.into_iter()
.filter_map(|to| to.parse::<IriString>().ok())
.collect::<Vec<IriString>>(),
);
article.set_many_ccs(
cc.into_iter()
.filter_map(|cc| cc.parse::<IriString>().ok())
.collect::<Vec<IriString>>(),
);
let license = Licensed07 {
license: Some(self.license.clone()),
2022-03-06 16:15:36 +01:00
};
Ok(LicensedArticle07::new(article, license, source))
}
pub fn create_activity(&self, conn: &Connection) -> Result<Create> {
let article = self.to_activity(conn)?;
2018-05-19 00:04:30 +02:00
let mut act = Create::default();
act.object_props
.set_id_string(format!("{}/activity", self.ap_url))?;
act.object_props
2019-03-20 17:56:17 +01:00
.set_to_link_vec::<Id>(article.object.object_props.to_link_vec()?)?;
act.object_props
2019-03-20 17:56:17 +01:00
.set_cc_link_vec::<Id>(article.object.object_props.cc_link_vec()?)?;
act.create_props
.set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?;
2019-03-20 17:56:17 +01:00
act.create_props.set_object_object(article)?;
Ok(act)
2018-05-19 00:04:30 +02:00
}
2022-03-21 01:34:33 +01:00
pub fn create_activity07(&self, conn: &Connection) -> Result<Create07> {
let article = self.to_activity07(conn)?;
let to = article.to().ok_or(Error::MissingApProperty)?.clone();
let cc = article.cc().ok_or(Error::MissingApProperty)?.clone();
let mut act = Create07::new(
self.get_authors(conn)?[0].ap_url.parse::<IriString>()?,
Base::retract(article)?.into_generic()?,
);
act.set_id(format!("{}/activity", self.ap_url).parse::<IriString>()?);
act.set_many_tos(to);
act.set_many_ccs(cc);
Ok(act)
}
pub fn update_activity(&self, conn: &Connection) -> Result<Update> {
let article = self.to_activity(conn)?;
2018-09-06 23:39:22 +02:00
let mut act = Update::default();
2019-03-20 17:56:17 +01:00
act.object_props.set_id_string(format!(
"{}/update-{}",
self.ap_url,
Utc::now().timestamp()
))?;
act.object_props
2019-03-20 17:56:17 +01:00
.set_to_link_vec::<Id>(article.object.object_props.to_link_vec()?)?;
act.object_props
2019-03-20 17:56:17 +01:00
.set_cc_link_vec::<Id>(article.object.object_props.cc_link_vec()?)?;
act.update_props
.set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?;
2019-03-20 17:56:17 +01:00
act.update_props.set_object_object(article)?;
Ok(act)
2018-09-06 23:39:22 +02:00
}
2022-03-21 01:42:00 +01:00
pub fn update_activity07(&self, conn: &Connection) -> Result<Update07> {
let article = self.to_activity07(conn)?;
let to = article.to().ok_or(Error::MissingApProperty)?.clone();
let cc = article.cc().ok_or(Error::MissingApProperty)?.clone();
let mut act = Update07::new(
self.get_authors(conn)?[0].ap_url.parse::<IriString>()?,
Base::retract(article)?.into_generic()?,
);
act.set_id(
format!("{}/update-{}", self.ap_url, Utc::now().timestamp()).parse::<IriString>()?,
);
act.set_many_tos(to);
act.set_many_ccs(cc);
Ok(act)
}
pub fn update_mentions(&self, conn: &Connection, mentions: Vec<link::Mention>) -> Result<()> {
let mentions = mentions
.into_iter()
.map(|m| {
(
m.link_props
.href_string()
.ok()
.and_then(|ap_url| User::find_by_ap_url(conn, &ap_url).ok())
.map(|u| u.id),
m,
)
})
2021-03-27 19:46:37 +01:00
.filter_map(|(id, m)| id.map(|id| (m, id)))
.collect::<Vec<_>>();
2018-10-28 10:05:02 +01:00
2021-11-27 23:53:13 +01:00
let old_mentions = Mention::list_for_post(conn, self.id)?;
let old_user_mentioned = old_mentions
.iter()
.map(|m| m.mentioned_id)
.collect::<HashSet<_>>();
for (m, id) in &mentions {
2021-11-27 23:53:13 +01:00
if !old_user_mentioned.contains(id) {
Mention::from_activity(&*conn, m, self.id, true, true)?;
2018-10-28 10:05:02 +01:00
}
}
let new_mentions = mentions
.into_iter()
.map(|(_m, id)| id)
.collect::<HashSet<_>>();
for m in old_mentions
.iter()
.filter(|m| !new_mentions.contains(&m.mentioned_id))
{
2021-11-27 23:53:13 +01:00
m.delete(conn)?;
2018-10-28 10:05:02 +01:00
}
Ok(())
2018-10-28 10:05:02 +01:00
}
2022-03-21 01:54:52 +01:00
pub fn update_mentions07(
&self,
conn: &Connection,
mentions: Vec<link07::Mention>,
) -> Result<()> {
let mentions = mentions
.into_iter()
.map(|m| {
(
m.href()
.and_then(|ap_url| User::find_by_ap_url(conn, ap_url.as_ref()).ok())
.map(|u| u.id),
m,
)
})
.filter_map(|(id, m)| id.map(|id| (m, id)))
.collect::<Vec<_>>();
let old_mentions = Mention::list_for_post(conn, self.id)?;
let old_user_mentioned = old_mentions
.iter()
.map(|m| m.mentioned_id)
.collect::<HashSet<_>>();
for (m, id) in &mentions {
if !old_user_mentioned.contains(id) {
Mention::from_activity07(&*conn, m, self.id, true, true)?;
}
}
let new_mentions = mentions
.into_iter()
.map(|(_m, id)| id)
.collect::<HashSet<_>>();
for m in old_mentions
.iter()
.filter(|m| !new_mentions.contains(&m.mentioned_id))
{
m.delete(conn)?;
}
Ok(())
}
pub fn update_tags(&self, conn: &Connection, tags: Vec<Hashtag>) -> Result<()> {
let tags_name = tags
.iter()
.filter_map(|t| t.name_string().ok())
.collect::<HashSet<_>>();
let old_tags = Tag::for_post(&*conn, self.id)?;
let old_tags_name = old_tags
.iter()
.filter_map(|tag| {
if !tag.is_hashtag {
Some(tag.tag.clone())
} else {
None
}
})
.collect::<HashSet<_>>();
for t in tags {
if !t
.name_string()
.map(|n| old_tags_name.contains(&n))
.unwrap_or(true)
{
Tag::from_activity(conn, &t, self.id, false)?;
}
}
for ot in old_tags.iter().filter(|t| !t.is_hashtag) {
if !tags_name.contains(&ot.tag) {
ot.delete(conn)?;
}
}
Ok(())
2018-10-28 10:05:02 +01:00
}
2022-03-21 02:08:00 +01:00
pub fn update_tags07(&self, conn: &Connection, tags: Vec<Hashtag07>) -> Result<()> {
let tags_name = tags
.iter()
.filter_map(|t| t.name.as_ref().map(|name| name.as_str().to_string()))
.collect::<HashSet<_>>();
let old_tags = Tag::for_post(&*conn, self.id)?;
let old_tags_name = old_tags
.iter()
.filter_map(|tag| {
if !tag.is_hashtag {
Some(tag.tag.clone())
} else {
None
}
})
.collect::<HashSet<_>>();
for t in tags {
if !t
.name
.as_ref()
.map(|n| old_tags_name.contains(n.as_str()))
.unwrap_or(true)
{
Tag::from_activity07(conn, &t, self.id, false)?;
}
}
for ot in old_tags.iter().filter(|t| !t.is_hashtag) {
if !tags_name.contains(&ot.tag) {
ot.delete(conn)?;
}
}
Ok(())
}
pub fn update_hashtags(&self, conn: &Connection, tags: Vec<Hashtag>) -> Result<()> {
let tags_name = tags
.iter()
.filter_map(|t| t.name_string().ok())
.collect::<HashSet<_>>();
let old_tags = Tag::for_post(&*conn, self.id)?;
let old_tags_name = old_tags
.iter()
.filter_map(|tag| {
if tag.is_hashtag {
Some(tag.tag.clone())
} else {
None
}
})
.collect::<HashSet<_>>();
for t in tags {
if !t
.name_string()
.map(|n| old_tags_name.contains(&n))
.unwrap_or(true)
{
Tag::from_activity(conn, &t, self.id, true)?;
}
}
for ot in old_tags.into_iter().filter(|t| t.is_hashtag) {
if !tags_name.contains(&ot.tag) {
ot.delete(conn)?;
}
}
Ok(())
2018-10-28 10:05:02 +01:00
}
2022-03-21 02:11:35 +01:00
pub fn update_hashtags07(&self, conn: &Connection, tags: Vec<Hashtag07>) -> Result<()> {
let tags_name = tags
.iter()
.filter_map(|t| t.name.as_ref().map(|name| name.as_str().to_string()))
.collect::<HashSet<_>>();
let old_tags = Tag::for_post(&*conn, self.id)?;
let old_tags_name = old_tags
.iter()
.filter_map(|tag| {
if tag.is_hashtag {
Some(tag.tag.clone())
} else {
None
}
})
.collect::<HashSet<_>>();
for t in tags {
if !t
.name
.as_ref()
.map(|n| old_tags_name.contains(n.as_str()))
.unwrap_or(true)
{
Tag::from_activity07(conn, &t, self.id, true)?;
}
}
for ot in old_tags.into_iter().filter(|t| t.is_hashtag) {
if !tags_name.contains(&ot.tag) {
ot.delete(conn)?;
}
}
Ok(())
}
pub fn url(&self, conn: &Connection) -> Result<String> {
let blog = self.get_blog(conn)?;
Ok(format!("/~/{}/{}", blog.fqn, self.slug))
}
pub fn cover_url(&self, conn: &Connection) -> Option<String> {
2019-03-20 17:56:17 +01:00
self.cover_id
.and_then(|i| Media::get(conn, i).ok())
.and_then(|c| c.url().ok())
}
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
2019-04-17 19:31:47 +02:00
pub fn build_delete(&self, conn: &Connection) -> Result<Delete> {
let mut act = Delete::default();
act.delete_props
.set_actor_link(self.get_authors(conn)?[0].clone().into_id())?;
let mut tombstone = Tombstone::default();
tombstone.object_props.set_id_string(self.ap_url.clone())?;
act.delete_props.set_object_object(tombstone)?;
act.object_props
.set_id_string(format!("{}#delete", self.ap_url))?;
act.object_props
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?;
Ok(act)
}
2022-03-21 02:32:12 +01:00
pub fn build_delete07(&self, conn: &Connection) -> Result<Delete07> {
let mut tombstone = Tombstone07::new();
tombstone.set_id(self.ap_url.parse()?);
let mut act = Delete07::new(
self.get_authors(conn)?[0]
.clone()
.into_id()
.parse::<IriString>()?,
Base::retract(tombstone)?.into_generic()?,
);
act.set_id(format!("{}#delete", self.ap_url).parse()?);
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
Ok(act)
}
fn publish_published(&self) {
POST_CHAN.tell(
Publish {
msg: PostPublished(Arc::new(self.clone())),
topic: "post.published".into(),
},
None,
)
}
fn publish_updated(&self) {
POST_CHAN.tell(
Publish {
msg: PostUpdated(Arc::new(self.clone())),
topic: "post.updated".into(),
},
None,
)
}
fn publish_deleted(&self) {
POST_CHAN.tell(
Publish {
msg: PostDeleted(Arc::new(self.clone())),
topic: "post.deleted".into(),
},
None,
)
}
2018-05-19 00:04:30 +02:00
}
2022-05-02 09:07:08 +02:00
impl FromId<DbConn> for Post {
2022-03-21 16:20:53 +01:00
type Error = Error;
type Object = LicensedArticle07;
fn from_db07(conn: &DbConn, id: &str) -> Result<Self> {
Self::find_by_ap_url(conn, id)
}
fn from_activity07(conn: &DbConn, article: LicensedArticle07) -> Result<Self> {
let license = article.ext_one.license.unwrap_or_default();
2022-03-21 16:20:53 +01:00
let source = article.ext_two.source.content;
let article = article.inner;
let (blog, authors) = article
2022-05-01 06:00:04 +02:00
.ap_object_ref()
2022-03-21 16:20:53 +01:00
.attributed_to()
2022-05-01 06:00:04 +02:00
.ok_or(Error::MissingApProperty)?
2022-03-21 16:20:53 +01:00
.iter()
.fold((None, vec![]), |(blog, mut authors), link| {
2022-05-01 06:00:04 +02:00
if let Some(url) = link.id() {
2022-05-02 12:24:36 +02:00
match User::from_id(conn, url.as_str(), None, CONFIG.proxy()) {
2022-05-01 06:00:04 +02:00
Ok(u) => {
authors.push(u);
(blog, authors)
}
Err(_) => (
blog.or_else(|| {
2022-05-02 12:24:36 +02:00
Blog::from_id(conn, url.as_str(), None, CONFIG.proxy()).ok()
2022-05-01 06:00:04 +02:00
}),
authors,
),
2022-03-21 16:20:53 +01:00
}
2022-05-01 06:00:04 +02:00
} else {
// logically, url possible to be an object without id proprty like {"type":"Person", "name":"Sally"} but we ignore the case
(blog, authors)
2022-03-21 16:20:53 +01:00
}
});
let cover = article.icon().and_then(|icon| {
icon.iter().next().and_then(|img| {
let image = img.to_owned().extend::<Image07, ImageType>().ok()??;
2022-03-21 16:20:53 +01:00
Media::from_activity07(conn, &image).ok().map(|m| m.id)
})
});
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());
2022-03-21 16:20:53 +01:00
let ap_url = article
.url()
.and_then(|url| url.to_as_uri().or(id))
2022-03-21 16:20:53 +01:00
.ok_or(Error::MissingApProperty)?;
let post = Post::from_db07(conn, &ap_url)
.and_then(|mut post| {
let mut updated = false;
let slug = Self::slug(&title);
let content = SafeString::new(
&article
.content()
.and_then(|content| content.to_as_string())
.ok_or(Error::MissingApProperty)?,
);
let subtitle = article
.summary()
.and_then(|summary| summary.to_as_string())
.ok_or(Error::MissingApProperty)?;
if post.slug != slug {
post.slug = slug.to_string();
updated = true;
}
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.clone(); // FIXME: Don't clone
2022-03-21 16:20:53 +01:00
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
.content()
.and_then(|content| content.to_as_string())
.ok_or(Error::MissingApProperty)?,
),
published: true,
license,
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
ap_url,
creation_date: article.published().map(|published| {
let timestamp_secs = published.unix_timestamp();
let timestamp_nanos = published.unix_timestamp_nanos()
- (timestamp_secs as i128) * 1000i128 * 1000i128 * 1000i128;
NaiveDateTime::from_timestamp(timestamp_secs, timestamp_nanos as u32)
}),
subtitle: article
.summary()
.and_then(|summary| summary.to_as_string())
.ok_or(Error::MissingApProperty)?,
2022-04-23 15:49:13 +02:00
source,
2022-03-21 16:20:53 +01:00
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(tags) = article.tag() {
for tag in tags.iter() {
tag.clone()
.extend::<link07::Mention, MentionType>() // FIXME: Don't clone
2022-03-21 16:20:53 +01:00
.map(|mention| {
mention.map(|m| Mention::from_activity07(conn, &m, post.id, true, true))
})
.ok();
tag.clone()
.extend::<Hashtag07, HashtagType07>() // FIXME: Don't clone
2022-04-23 15:49:13 +02:00
.map(|hashtag| {
hashtag.and_then(|t| {
let tag_name = t.name.clone()?.as_str().to_string();
Tag::from_activity07(conn, &t, post.id, hashtags.remove(&tag_name)).ok()
2022-04-23 15:49:13 +02:00
})
2022-03-21 16:20:53 +01:00
})
.ok();
}
}
Timeline::add_to_all_timelines(conn, &post, Kind::Original)?;
Ok(post)
}
fn get_sender07() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to get local instance user")
}
}
2022-05-02 10:43:03 +02:00
impl AsObject<User, Create07, &DbConn> for Post {
2022-04-03 12:22:30 +02:00
type Error = Error;
type Output = Self;
fn activity07(self, _conn: &DbConn, _actor: User, _id: &str) -> Result<Self::Output> {
// TODO: check that _actor is actually one of the author?
Ok(self)
}
}
2022-05-02 10:43:03 +02:00
impl AsObject<User, Delete07, &DbConn> for Post {
2022-04-03 12:22:30 +02:00
type Error = Error;
type Output = ();
fn activity07(self, conn: &DbConn, actor: User, _id: &str) -> Result<Self::Output> {
let can_delete = self
.get_authors(conn)?
2019-03-20 17:56:17 +01:00
.into_iter()
.any(|a| actor.id == a.id);
if can_delete {
self.delete(conn).map(|_| ())
} else {
Err(Error::Unauthorized)
}
}
}
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
2019-04-17 19:31:47 +02:00
pub struct PostUpdate {
pub ap_url: String,
pub title: Option<String>,
pub subtitle: Option<String>,
pub content: Option<String>,
pub cover: Option<i32>,
pub source: Option<String>,
pub license: Option<String>,
pub tags: Option<serde_json::Value>,
}
2022-05-02 09:07:08 +02:00
impl FromId<DbConn> for PostUpdate {
2022-05-01 02:56:03 +02:00
type Error = Error;
type Object = LicensedArticle07;
fn from_db07(_: &DbConn, _: &str) -> Result<Self> {
// Always fail because we always want to deserialize the AP object
Err(Error::NotFound)
}
fn from_activity07(conn: &DbConn, updated: Self::Object) -> Result<Self> {
let mut post_update = PostUpdate {
ap_url: updated
.ap_object_ref()
.id_unchecked()
.ok_or(Error::MissingApProperty)?
.to_string(),
title: updated
.ap_object_ref()
.name()
.and_then(|name| name.to_as_string()),
subtitle: updated
.ap_object_ref()
.summary()
.and_then(|summary| summary.to_as_string()),
content: updated
.ap_object_ref()
.content()
.and_then(|content| content.to_as_string()),
cover: None,
source: None,
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::<Image07, ImageType>()
.map(|img| img.and_then(|img| Media::from_activity07(conn, &img).ok()))
.ok()
})
.and_then(|m| m.map(|m| m.id))
});
post_update.source = Some(updated.ext_two.source.content);
post_update.license = updated.ext_one.license;
2022-05-01 02:56:03 +02:00
Ok(post_update)
}
fn get_sender07() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
2022-05-02 10:43:03 +02:00
impl AsObject<User, Update07, &DbConn> for PostUpdate {
2022-05-01 02:56:16 +02:00
type Error = Error;
type Output = ();
fn activity07(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
let mut post =
2022-05-02 12:24:36 +02:00
Post::from_id(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?;
2022-05-01 02:56:16 +02:00
if !post.is_author(conn, actor.id)? {
// TODO: maybe the author was added in the meantime
return Err(Error::Unauthorized);
}
if let Some(title) = self.title {
post.slug = Post::slug(&title).to_string();
post.title = title;
}
if let Some(content) = self.content {
post.content = SafeString::new(&content);
}
if let Some(subtitle) = self.subtitle {
post.subtitle = subtitle;
}
post.cover_id = self.cover;
if let Some(source) = self.source {
post.source = source;
}
if let Some(license) = self.license {
post.license = license;
}
let mut txt_hashtags = md_to_html(&post.source, None, false, None)
.2
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
2019-04-17 19:31:47 +02:00
.into_iter()
.collect::<HashSet<_>>();
if let Some(serde_json::Value::Array(mention_tags)) = self.tags {
let mut mentions = vec![];
let mut tags = vec![];
let mut hashtags = vec![];
for tag in mention_tags {
serde_json::from_value::<link::Mention>(tag.clone())
.map(|m| mentions.push(m))
.ok();
serde_json::from_value::<Hashtag>(tag.clone())
.map_err(Error::from)
.and_then(|t| {
let tag_name = t.name_string()?;
if txt_hashtags.remove(&tag_name) {
hashtags.push(t);
} else {
tags.push(t);
}
Ok(())
})
.ok();
}
post.update_mentions(conn, mentions)?;
post.update_tags(conn, tags)?;
post.update_hashtags(conn, hashtags)?;
}
post.update(conn)?;
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
2019-04-17 19:31:47 +02:00
Ok(())
}
}
2018-05-19 00:04:30 +02:00
impl IntoId for Post {
fn into_id(self) -> Id {
Id::new(self.ap_url)
2018-05-19 00:04:30 +02:00
}
2018-04-23 15:41:43 +02:00
}
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
2019-04-17 19:31:47 +02:00
2021-01-06 19:39:49 +01:00
#[derive(Clone, Debug)]
pub enum PostEvent {
PostPublished(Arc<Post>),
PostUpdated(Arc<Post>),
PostDeleted(Arc<Post>),
2021-01-06 19:39:49 +01:00
}
impl From<PostEvent> for Arc<Post> {
2021-01-06 19:39:49 +01:00
fn from(event: PostEvent) -> Self {
use PostEvent::*;
match event {
PostPublished(post) => post,
PostUpdated(post) => post,
PostDeleted(post) => post,
}
}
}
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
2019-04-17 19:31:47 +02:00
#[cfg(test)]
mod tests {
use super::*;
use crate::inbox::{inbox, tests::fill_database, InboxResult};
use crate::mentions::{Mention, NewMention};
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
2019-04-17 19:31:47 +02:00
use crate::safe_string::SafeString;
2022-01-09 04:28:22 +01:00
use crate::tests::{db, format_datetime};
use assert_json_diff::assert_json_eq;
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
2019-04-17 19:31:47 +02:00
use diesel::Connection;
use serde_json::{json, to_value};
fn prepare_activity(conn: &DbConn) -> (Post, Mention, Vec<Post>, Vec<User>, Vec<Blog>) {
let (posts, users, blogs) = fill_database(conn);
let post = &posts[0];
let mentioned = &users[1];
let mention = Mention::insert(
&conn,
NewMention {
mentioned_id: mentioned.id,
post_id: Some(post.id),
comment_id: None,
},
)
.unwrap();
(post.to_owned(), mention.to_owned(), posts, users, blogs)
}
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
2019-04-17 19:31:47 +02:00
// creates a post, get it's Create activity, delete the post,
// "send" the Create to the inbox, and check it works
#[test]
fn self_federation() {
2021-01-30 15:15:07 +01:00
let conn = &db();
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
2019-04-17 19:31:47 +02:00
conn.test_transaction::<_, (), _>(|| {
2021-01-30 15:15:07 +01:00
let (_, users, blogs) = fill_database(&conn);
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
2019-04-17 19:31:47 +02:00
let post = Post::insert(
2021-01-30 15:15:07 +01:00
&conn,
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
2019-04-17 19:31:47 +02:00
NewPost {
blog_id: blogs[0].id,
slug: "yo".into(),
title: "Yo".into(),
content: SafeString::new("Hello"),
published: true,
license: "WTFPL".to_string(),
creation_date: None,
ap_url: String::new(), // automatically updated when inserting
subtitle: "Testing".into(),
source: "Hello".into(),
cover_id: None,
},
)
.unwrap();
PostAuthor::insert(
2021-01-30 15:15:07 +01:00
&conn,
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
2019-04-17 19:31:47 +02:00
NewPostAuthor {
post_id: post.id,
author_id: users[0].id,
},
)
.unwrap();
2021-01-30 15:15:07 +01:00
let create = post.create_activity(&conn).unwrap();
post.delete(&conn).unwrap();
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
2019-04-17 19:31:47 +02:00
2021-01-30 15:15:07 +01:00
match inbox(&conn, serde_json::to_value(create).unwrap()).unwrap() {
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
2019-04-17 19:31:47 +02:00
InboxResult::Post(p) => {
2021-01-30 15:15:07 +01:00
assert!(p.is_author(&conn, users[0].id).unwrap());
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
2019-04-17 19:31:47 +02:00
assert_eq!(p.source, "Hello".to_owned());
assert_eq!(p.blog_id, blogs[0].id);
assert_eq!(p.content, SafeString::new("Hello"));
assert_eq!(p.subtitle, "Testing".to_owned());
2022-04-03 12:25:32 +02:00
assert_eq!(p.title, "Yo".to_owned());
}
_ => panic!("Unexpected result"),
};
Ok(())
});
}
// creates a post, get it's Create activity, delete the post,
// "send" the Create to the inbox, and check it works
#[test]
fn self_federation07() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (_, users, blogs) = fill_database(&conn);
let post = Post::insert(
&conn,
NewPost {
blog_id: blogs[0].id,
slug: "yo".into(),
title: "Yo".into(),
content: SafeString::new("Hello"),
published: true,
license: "WTFPL".to_string(),
creation_date: None,
ap_url: String::new(), // automatically updated when inserting
subtitle: "Testing".into(),
source: "Hello".into(),
cover_id: None,
},
)
.unwrap();
PostAuthor::insert(
&conn,
NewPostAuthor {
post_id: post.id,
author_id: users[0].id,
},
)
.unwrap();
let create = post.create_activity07(&conn).unwrap();
post.delete(&conn).unwrap();
match inbox(&conn, serde_json::to_value(create).unwrap()).unwrap() {
InboxResult::Post(p) => {
assert!(p.is_author(&conn, users[0].id).unwrap());
assert_eq!(p.source, "Hello".to_owned());
assert_eq!(p.blog_id, blogs[0].id);
assert_eq!(p.content, SafeString::new("Hello"));
assert_eq!(p.subtitle, "Testing".to_owned());
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
2019-04-17 19:31:47 +02:00
assert_eq!(p.title, "Yo".to_owned());
}
_ => panic!("Unexpected result"),
};
Ok(())
});
}
#[test]
fn licensed_article_serde() {
let mut article = Article::default();
article.object_props.set_id_string("Yo".into()).unwrap();
let mut license = Licensed::default();
license.set_license_string("WTFPL".into()).unwrap();
let full_article = LicensedArticle::new(article, license);
let json = serde_json::to_value(full_article).unwrap();
let article_from_json: LicensedArticle = serde_json::from_value(json).unwrap();
assert_eq!(
"Yo",
&article_from_json.object.object_props.id_string().unwrap()
);
assert_eq!(
"WTFPL",
&article_from_json.custom_props.license_string().unwrap()
);
}
#[test]
fn licensed_article_deserialization() {
let json = json!({
"type": "Article",
"id": "https://plu.me/~/Blog/my-article",
"attributedTo": ["https://plu.me/@/Admin", "https://plu.me/~/Blog"],
"content": "Hello.",
"name": "My Article",
"summary": "Bye.",
"source": {
"content": "Hello.",
"mediaType": "text/markdown"
},
"published": "2014-12-12T12:12:12Z",
"to": [plume_common::activity_pub::PUBLIC_VISIBILITY]
});
let article: LicensedArticle = serde_json::from_value(json).unwrap();
assert_eq!(
"https://plu.me/~/Blog/my-article",
&article.object.object_props.id_string().unwrap()
);
}
#[test]
fn to_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn);
let act = post.to_activity(&conn)?;
let expected = json!({
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"],
2022-03-20 22:38:28 +01:00
"cc": [],
"content": "Hello",
"id": "https://plu.me/~/BlogName/testing",
"license": "WTFPL",
"name": "Testing",
"published": format_datetime(&post.creation_date),
"source": {
2022-05-01 05:59:48 +02:00
"content": "Hello",
2022-03-20 22:38:28 +01:00
"mediaType": "text/markdown"
},
2022-05-01 05:59:48 +02:00
"summary": "Bye",
2022-03-20 22:38:28 +01:00
"tag": [
{
"href": "https://plu.me/@/user/",
"name": "@user",
"type": "Mention"
}
],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Article",
"url": "https://plu.me/~/BlogName/testing"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn to_activity07() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn);
let act = post.to_activity07(&conn)?;
let expected = json!({
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"],
"cc": [],
"content": "Hello",
"id": "https://plu.me/~/BlogName/testing",
"license": "WTFPL",
"name": "Testing",
"published": format_datetime(&post.creation_date),
"source": {
2022-05-01 05:59:48 +02:00
"content": "Hello",
"mediaType": "text/markdown"
},
2022-05-01 05:59:48 +02:00
"summary": "Bye",
"tag": [
{
"href": "https://plu.me/@/user/",
"name": "@user",
"type": "Mention"
}
],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Article",
"url": "https://plu.me/~/BlogName/testing"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn create_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn);
let act = post.create_activity(&conn)?;
2022-03-21 01:34:42 +01:00
let expected = json!({
"actor": "https://plu.me/@/admin/",
"cc": [],
"id": "https://plu.me/~/BlogName/testing/activity",
"object": {
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"],
"cc": [],
"content": "Hello",
"id": "https://plu.me/~/BlogName/testing",
"license": "WTFPL",
"name": "Testing",
"published": format_datetime(&post.creation_date),
"source": {
2022-05-01 05:59:48 +02:00
"content": "Hello",
2022-03-21 01:34:42 +01:00
"mediaType": "text/markdown"
},
2022-05-01 05:59:48 +02:00
"summary": "Bye",
2022-03-21 01:34:42 +01:00
"tag": [
{
"href": "https://plu.me/@/user/",
"name": "@user",
"type": "Mention"
}
],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Article",
"url": "https://plu.me/~/BlogName/testing"
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Create"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn create_activity07() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn);
let act = post.create_activity07(&conn)?;
let expected = json!({
"actor": "https://plu.me/@/admin/",
"cc": [],
"id": "https://plu.me/~/BlogName/testing/activity",
"object": {
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"],
"cc": [],
"content": "Hello",
"id": "https://plu.me/~/BlogName/testing",
"license": "WTFPL",
"name": "Testing",
"published": format_datetime(&post.creation_date),
"source": {
2022-05-01 05:59:48 +02:00
"content": "Hello",
"mediaType": "text/markdown"
},
2022-05-01 05:59:48 +02:00
"summary": "Bye",
"tag": [
{
"href": "https://plu.me/@/user/",
"name": "@user",
"type": "Mention"
}
],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Article",
"url": "https://plu.me/~/BlogName/testing"
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Create"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn update_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn);
let act = post.update_activity(&conn)?;
let expected = json!({
"actor": "https://plu.me/@/admin/",
"cc": [],
"id": "https://plu.me/~/BlogName/testing/update-",
"object": {
2022-03-21 01:42:06 +01:00
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"],
"cc": [],
"content": "Hello",
"id": "https://plu.me/~/BlogName/testing",
"license": "WTFPL",
"name": "Testing",
"published": format_datetime(&post.creation_date),
"source": {
2022-05-01 05:59:48 +02:00
"content": "Hello",
2022-03-21 01:42:06 +01:00
"mediaType": "text/markdown"
},
2022-05-01 05:59:48 +02:00
"summary": "Bye",
2022-03-21 01:42:06 +01:00
"tag": [
{
"href": "https://plu.me/@/user/",
"name": "@user",
"type": "Mention"
}
],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Article",
"url": "https://plu.me/~/BlogName/testing"
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Update"
});
let actual = to_value(act)?;
let id = actual["id"].to_string();
let (id_pre, id_post) = id.rsplit_once("-").unwrap();
assert_eq!(post.ap_url, "https://plu.me/~/BlogName/testing");
assert_eq!(
id_pre,
to_value("\"https://plu.me/~/BlogName/testing/update")
.unwrap()
.as_str()
.unwrap()
);
assert_eq!(id_post.len(), 11);
assert_eq!(
id_post.matches(char::is_numeric).collect::<String>().len(),
10
);
for (key, value) in actual.as_object().unwrap().into_iter() {
if key == "id" {
continue;
}
2022-05-01 05:59:48 +02:00
assert_json_eq!(value, expected.get(key).unwrap());
2022-03-21 01:42:06 +01:00
}
Ok(())
});
}
#[test]
fn update_activity07() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn);
let act = post.update_activity07(&conn)?;
let expected = json!({
"actor": "https://plu.me/@/admin/",
"cc": [],
"id": "https://plu.me/~/BlogName/testing/update-",
"object": {
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"],
"cc": [],
"content": "Hello",
"id": "https://plu.me/~/BlogName/testing",
"license": "WTFPL",
"name": "Testing",
"published": format_datetime(&post.creation_date),
"source": {
2022-05-01 05:59:48 +02:00
"content": "Hello",
"mediaType": "text/markdown"
},
2022-05-01 05:59:48 +02:00
"summary": "Bye",
"tag": [
{
"href": "https://plu.me/@/user/",
"name": "@user",
"type": "Mention"
}
],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Article",
"url": "https://plu.me/~/BlogName/testing"
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Update"
});
let actual = to_value(act)?;
let id = actual["id"].to_string();
let (id_pre, id_post) = id.rsplit_once("-").unwrap();
assert_eq!(post.ap_url, "https://plu.me/~/BlogName/testing");
assert_eq!(
id_pre,
to_value("\"https://plu.me/~/BlogName/testing/update")
.unwrap()
.as_str()
.unwrap()
);
assert_eq!(id_post.len(), 11);
assert_eq!(
id_post.matches(char::is_numeric).collect::<String>().len(),
10
);
for (key, value) in actual.as_object().unwrap().into_iter() {
if key == "id" {
continue;
}
2022-05-01 05:59:48 +02:00
assert_json_eq!(value, expected.get(key).unwrap());
}
Ok(())
});
}
2022-03-21 02:24:18 +01:00
#[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(())
});
}
2022-03-21 02:32:29 +01:00
#[test]
fn build_delete07() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn);
let act = post.build_delete07(&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(())
});
}
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
2019-04-17 19:31:47 +02:00
}