Plume/plume-models/src/follows.rs
fdb-hiroshima 006b44f580 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

243 lines
7.5 KiB
Rust

use activitypub::activity::{Accept, Follow as FollowAct, Undo};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use notifications::*;
use plume_common::activity_pub::{
broadcast,
inbox::{AsActor, AsObject, FromId},
sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY,
};
use schema::follows;
use users::User;
use {ap_url, Connection, Error, PlumeRocket, Result, CONFIG};
#[derive(Clone, Queryable, Identifiable, Associations, AsChangeset)]
#[belongs_to(User, foreign_key = "following_id")]
pub struct Follow {
pub id: i32,
pub follower_id: i32,
pub following_id: i32,
pub ap_url: String,
}
#[derive(Insertable)]
#[table_name = "follows"]
pub struct NewFollow {
pub follower_id: i32,
pub following_id: i32,
pub ap_url: String,
}
impl Follow {
insert!(
follows,
NewFollow,
|inserted, conn| if inserted.ap_url.is_empty() {
inserted.ap_url = ap_url(&format!("{}/follows/{}", CONFIG.base_url, inserted.id));
inserted.save_changes(conn).map_err(Error::from)
} else {
Ok(inserted)
}
);
get!(follows);
find_by!(follows, find_by_ap_url, ap_url as &str);
pub fn find(conn: &Connection, from: i32, to: i32) -> Result<Follow> {
follows::table
.filter(follows::follower_id.eq(from))
.filter(follows::following_id.eq(to))
.get_result(conn)
.map_err(Error::from)
}
pub fn to_activity(&self, conn: &Connection) -> Result<FollowAct> {
let user = User::get(conn, self.follower_id)?;
let target = User::get(conn, self.following_id)?;
let mut act = FollowAct::default();
act.follow_props
.set_actor_link::<Id>(user.clone().into_id())?;
act.follow_props
.set_object_link::<Id>(target.clone().into_id())?;
act.object_props.set_id_string(self.ap_url.clone())?;
act.object_props.set_to_link_vec(vec![target.into_id()])?;
act.object_props
.set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
Ok(act)
}
pub fn notify(&self, conn: &Connection) -> Result<()> {
if User::get(conn, self.following_id)?.is_local() {
Notification::insert(
conn,
NewNotification {
kind: notification_kind::FOLLOW.to_string(),
object_id: self.id,
user_id: self.following_id,
},
)?;
}
Ok(())
}
/// from -> The one sending the follow request
/// target -> The target of the request, responding with Accept
pub fn accept_follow<A: Signer + IntoId + Clone, B: Clone + AsActor<T> + IntoId, T>(
conn: &Connection,
from: &B,
target: &A,
follow: FollowAct,
from_id: i32,
target_id: i32,
) -> Result<Follow> {
let res = Follow::insert(
conn,
NewFollow {
follower_id: from_id,
following_id: target_id,
ap_url: follow.object_props.id_string()?,
},
)?;
res.notify(conn)?;
let mut accept = Accept::default();
let accept_id = ap_url(&format!(
"{}/follow/{}/accept",
CONFIG.base_url.as_str(),
&res.id
));
accept.object_props.set_id_string(accept_id)?;
accept
.object_props
.set_to_link_vec(vec![from.clone().into_id()])?;
accept
.object_props
.set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
accept
.accept_props
.set_actor_link::<Id>(target.clone().into_id())?;
accept.accept_props.set_object_object(follow)?;
broadcast(&*target, accept, vec![from.clone()]);
Ok(res)
}
pub fn build_undo(&self, conn: &Connection) -> Result<Undo> {
let mut undo = Undo::default();
undo.undo_props
.set_actor_link(User::get(conn, self.follower_id)?.into_id())?;
undo.object_props
.set_id_string(format!("{}/undo", self.ap_url))?;
undo.undo_props
.set_object_link::<Id>(self.clone().into_id())?;
undo.object_props
.set_to_link_vec(vec![User::get(conn, self.following_id)?.into_id()])?;
undo.object_props
.set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
Ok(undo)
}
}
impl AsObject<User, FollowAct, &PlumeRocket> for User {
type Error = Error;
type Output = Follow;
fn activity(self, c: &PlumeRocket, actor: User, id: &str) -> Result<Follow> {
// Mastodon (at least) requires the full Follow object when accepting it,
// so we rebuilt it here
let mut follow = FollowAct::default();
follow.object_props.set_id_string(id.to_string())?;
follow
.follow_props
.set_actor_link::<Id>(actor.clone().into_id())?;
Follow::accept_follow(&c.conn, &actor, &self, follow, actor.id, self.id)
}
}
impl FromId<PlumeRocket> for Follow {
type Error = Error;
type Object = FollowAct;
fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Follow::find_by_ap_url(&c.conn, id)
}
fn from_activity(c: &PlumeRocket, follow: FollowAct) -> Result<Self> {
let actor =
User::from_id(c, &follow.follow_props.actor_link::<Id>()?, None).map_err(|(_, e)| e)?;
let target = User::from_id(c, &follow.follow_props.object_link::<Id>()?, None)
.map_err(|(_, e)| e)?;
Follow::accept_follow(&c.conn, &actor, &target, follow, actor.id, target.id)
}
}
impl AsObject<User, Undo, &PlumeRocket> for Follow {
type Error = Error;
type Output = ();
fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
let conn = &*c.conn;
if self.follower_id == actor.id {
diesel::delete(&self).execute(conn)?;
// delete associated notification if any
if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
diesel::delete(&notif).execute(conn)?;
}
Ok(())
} else {
Err(Error::Unauthorized)
}
}
}
impl IntoId for Follow {
fn into_id(self) -> Id {
Id::new(self.ap_url)
}
}
#[cfg(test)]
mod tests {
use super::*;
use diesel::Connection;
use tests::db;
use users::tests as user_tests;
#[test]
fn test_id() {
let conn = db();
conn.test_transaction::<_, (), _>(|| {
let users = user_tests::fill_database(&conn);
let follow = Follow::insert(
&conn,
NewFollow {
follower_id: users[0].id,
following_id: users[1].id,
ap_url: String::new(),
},
)
.expect("Couldn't insert new follow");
assert_eq!(
follow.ap_url,
format!("https://{}/follows/{}", CONFIG.base_url, follow.id)
);
let follow = Follow::insert(
&conn,
NewFollow {
follower_id: users[1].id,
following_id: users[0].id,
ap_url: String::from("https://some.url/"),
},
)
.expect("Couldn't insert new follow");
assert_eq!(follow.ap_url, String::from("https://some.url/"));
Ok(())
})
}
}