From 1536a6d3f3ed056868bf4517dba960b0d2b6510a Mon Sep 17 00:00:00 2001 From: trinity-1686a Date: Fri, 16 Dec 2022 22:51:14 +0100 Subject: [PATCH 1/3] allow timeline manipulation from plm --- plume-cli/src/main.rs | 5 + plume-cli/src/timeline.rs | 254 +++++++++++++++++++++++++++++ plume-models/src/blogs.rs | 16 +- plume-models/src/comments.rs | 25 ++- plume-models/src/follows.rs | 22 +-- plume-models/src/inbox.rs | 2 +- plume-models/src/likes.rs | 22 +-- plume-models/src/medias.rs | 8 +- plume-models/src/mentions.rs | 6 +- plume-models/src/posts.rs | 30 ++-- plume-models/src/reshares.rs | 22 +-- plume-models/src/timeline/mod.rs | 26 ++- plume-models/src/timeline/query.rs | 15 +- plume-models/src/users.rs | 18 +- 14 files changed, 373 insertions(+), 98 deletions(-) create mode 100644 plume-cli/src/timeline.rs diff --git a/plume-cli/src/main.rs b/plume-cli/src/main.rs index 3615c10d..f6b1f48b 100644 --- a/plume-cli/src/main.rs +++ b/plume-cli/src/main.rs @@ -6,6 +6,7 @@ use std::io::{self, prelude::*}; mod instance; mod migration; mod search; +mod timeline; mod users; fn main() { @@ -16,6 +17,7 @@ fn main() { .subcommand(instance::command()) .subcommand(migration::command()) .subcommand(search::command()) + .subcommand(timeline::command()) .subcommand(users::command()); let matches = app.clone().get_matches(); @@ -37,6 +39,9 @@ fn main() { ("search", Some(args)) => { search::run(args, &conn.expect("Couldn't connect to the database.")) } + ("timeline", Some(args)) => { + timeline::run(args, &conn.expect("Couldn't connect to the database.")) + } ("users", Some(args)) => { users::run(args, &conn.expect("Couldn't connect to the database.")) } diff --git a/plume-cli/src/timeline.rs b/plume-cli/src/timeline.rs new file mode 100644 index 00000000..1ad21e4c --- /dev/null +++ b/plume-cli/src/timeline.rs @@ -0,0 +1,254 @@ +use clap::{App, Arg, ArgMatches, SubCommand}; + +use plume_models::{instance::Instance, posts::Post, timeline::*, users::*, Connection}; + +pub fn command<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("timeline") + .about("Manage public timeline") + .subcommand( + SubCommand::with_name("new") + .arg( + Arg::with_name("name") + .short("n") + .long("name") + .takes_value(true) + .help("The name of this timeline"), + ) + .arg( + Arg::with_name("query") + .short("q") + .long("query") + .takes_value(true) + .help("The query posts in this timelines have to match"), + ) + .arg( + Arg::with_name("user") + .short("u") + .long("user") + .takes_value(true) + .help( + "Username of whom this timeline is for. Empty for an instance timeline", + ), + ) + .arg( + Arg::with_name("preload-count") + .short("p") + .long("preload-count") + .takes_value(true) + .help("Number of posts to try to preload in this timeline at its creation"), + ) + .about("Create a new timeline"), + ) + .subcommand( + SubCommand::with_name("delete") + .arg( + Arg::with_name("name") + .short("n") + .long("name") + .takes_value(true) + .help("The name of the timeline to delete"), + ) + .arg( + Arg::with_name("user") + .short("u") + .long("user") + .takes_value(true) + .help( + "Username of whom this timeline was for. Empty for instance timeline", + ), + ) + .arg( + Arg::with_name("yes") + .short("y") + .long("yes") + .help("Confirm the deletion"), + ), + ) + .subcommand( + SubCommand::with_name("edit") + .arg( + Arg::with_name("name") + .short("n") + .long("name") + .takes_value(true) + .help("The name of the timeline to edit"), + ) + .arg( + Arg::with_name("user") + .short("u") + .long("user") + .takes_value(true) + .help("Username of whom this timeline is for. Empty for instance timeline"), + ) + .arg( + Arg::with_name("query") + .short("q") + .long("query") + .takes_value(true) + .help("The query posts in this timelines have to match"), + ), + ) + .subcommand( + SubCommand::with_name("repopulate") + .arg( + Arg::with_name("name") + .short("n") + .long("name") + .takes_value(true) + .help("The name of the timeline to repopulate"), + ) + .arg( + Arg::with_name("user") + .short("u") + .long("user") + .takes_value(true) + .help( + "Username of whom this timeline was for. Empty for instance timeline", + ), + ) + .arg( + Arg::with_name("preload-count") + .short("p") + .long("preload-count") + .takes_value(true) + .help("Number of posts to try to preload in this timeline at its creation"), + ), + ) +} + +pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) { + let conn = conn; + match args.subcommand() { + ("new", Some(x)) => new(x, conn), + ("edit", Some(x)) => edit(x, conn), + ("delete", Some(x)) => delete(x, conn), + ("repopulate", Some(x)) => repopulate(x, conn), + ("", None) => command().print_help().unwrap(), + _ => println!("Unknown subcommand"), + } +} + +fn get_timeline_identifier(args: &ArgMatches<'_>) -> (String, Option) { + let name = args + .value_of("name") + .map(String::from) + .expect("No name provided for the timeline"); + let user = args.value_of("user").map(String::from); + (name, user) +} + +fn get_query(args: &ArgMatches<'_>) -> String { + let query = args + .value_of("query") + .map(String::from) + .expect("No query provided"); + + match TimelineQuery::parse(&query) { + Ok(_) => (), + Err(QueryError::SyntaxError(start, end, message)) => panic!( + "Query parsing error between {} and {}: {}", + start, end, message + ), + Err(QueryError::UnexpectedEndOfQuery) => { + panic!("Query parsing error: unexpected end of query") + } + Err(QueryError::RuntimeError(message)) => panic!("Query parsing error: {}", message), + } + + query +} + +fn get_preload_count(args: &ArgMatches<'_>) -> usize { + args.value_of("preload-count") + .map(|arg| arg.parse().expect("invalid preload-count")) + .unwrap_or(plume_models::ITEMS_PER_PAGE as usize) +} + +fn resolve_user(username: &str, conn: &Connection) -> User { + let instance = Instance::get_local_uncached(conn).expect("Failed to load local instance"); + + User::find_by_name(conn, username, instance.id).expect("User not found") +} + +fn preload(timeline: Timeline, count: usize, conn: &Connection) { + timeline.remove_all_posts(conn).unwrap(); + + if count == 0 { + return; + } + + let mut posts = Vec::with_capacity(count as usize); + for post in Post::list_filtered(conn, None, None, None) + .unwrap() + .into_iter() + .rev() + { + if timeline.matches(conn, &post, Kind::Original).unwrap() { + posts.push(post); + if posts.len() >= count { + break; + } + } + } + + for post in posts.iter().rev() { + timeline.add_post(conn, &post).unwrap(); + } +} + +fn new(args: &ArgMatches<'_>, conn: &Connection) { + let (name, user) = get_timeline_identifier(args); + let query = get_query(args); + let preload_count = get_preload_count(args); + + let user = user.map(|user| resolve_user(&user, conn)); + + let timeline = if let Some(user) = user { + Timeline::new_for_user(conn, user.id, name, query) + } else { + Timeline::new_for_instance(conn, name, query) + } + .expect("Failed to create new timeline"); + + preload(timeline, preload_count, conn); +} + +fn edit(args: &ArgMatches<'_>, conn: &Connection) { + let (name, user) = get_timeline_identifier(args); + let query = get_query(args); + + let user = user.map(|user| resolve_user(&user, conn)); + + let mut timeline = Timeline::find_for_user_by_name(conn, user.map(|u| u.id), &name) + .expect("timeline not found"); + + timeline.query = query; + + timeline.update(conn).expect("Failed to update timeline"); +} + +fn delete(args: &ArgMatches<'_>, conn: &Connection) { + let (name, user) = get_timeline_identifier(args); + + if !args.is_present("yes") { + panic!("Warning, this operation is destructive. Add --yes to confirm you want to do it.") + } + + let user = user.map(|user| resolve_user(&user, conn)); + + let timeline = Timeline::find_for_user_by_name(conn, user.map(|u| u.id), &name) + .expect("timeline not found"); + + timeline.delete(conn).expect("Failed to update timeline"); +} + +fn repopulate(args: &ArgMatches<'_>, conn: &Connection) { + let (name, user) = get_timeline_identifier(args); + let preload_count = get_preload_count(args); + + let user = user.map(|user| resolve_user(&user, conn)); + + let timeline = Timeline::find_for_user_by_name(conn, user.map(|u| u.id), &name) + .expect("timeline not found"); + preload(timeline, preload_count, conn); +} diff --git a/plume-models/src/blogs.rs b/plume-models/src/blogs.rs index fb20be48..0331cbaf 100644 --- a/plume-models/src/blogs.rs +++ b/plume-models/src/blogs.rs @@ -1,6 +1,6 @@ use crate::{ - db_conn::DbConn, instance::*, medias::Media, posts::Post, safe_string::SafeString, - schema::blogs, users::User, Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE, + instance::*, medias::Media, posts::Post, safe_string::SafeString, schema::blogs, users::User, + Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE, }; use activitystreams::{ actor::{ApActor, ApActorExt, AsApActor, Group}, @@ -135,10 +135,10 @@ impl Blog { .map_err(Error::from) } - pub fn find_by_fqn(conn: &DbConn, fqn: &str) -> Result { + pub fn find_by_fqn(conn: &Connection, fqn: &str) -> Result { let from_db = blogs::table .filter(blogs::fqn.eq(fqn)) - .first(&**conn) + .first(conn) .optional()?; if let Some(from_db) = from_db { Ok(from_db) @@ -147,7 +147,7 @@ impl Blog { } } - fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result { + fn fetch_from_webfinger(conn: &Connection, acct: &str) -> Result { resolve_with_prefix(Prefix::Group, acct.to_owned(), true)? .links .into_iter() @@ -365,15 +365,15 @@ impl IntoId for Blog { } } -impl FromId for Blog { +impl FromId for Blog { type Error = Error; type Object = CustomGroup; - fn from_db(conn: &DbConn, id: &str) -> Result { + fn from_db(conn: &Connection, id: &str) -> Result { Self::find_by_ap_url(conn, id) } - fn from_activity(conn: &DbConn, acct: CustomGroup) -> Result { + fn from_activity(conn: &Connection, acct: CustomGroup) -> Result { let (name, outbox_url, inbox_url) = { let actor = acct.ap_actor_ref(); let name = actor diff --git a/plume-models/src/comments.rs b/plume-models/src/comments.rs index d928b9a3..6c133dd2 100644 --- a/plume-models/src/comments.rs +++ b/plume-models/src/comments.rs @@ -1,6 +1,5 @@ use crate::{ comment_seers::{CommentSeers, NewCommentSeers}, - db_conn::DbConn, instance::Instance, medias::Media, mentions::Mention, @@ -111,7 +110,7 @@ impl Comment { .unwrap_or(false) } - pub fn to_activity(&self, conn: &DbConn) -> Result { + pub fn to_activity(&self, conn: &Connection) -> Result { let author = User::get(conn, self.author_id)?; let (html, mentions, _hashtags) = utils::md_to_html( self.content.get().as_ref(), @@ -149,7 +148,7 @@ impl Comment { Ok(note) } - pub fn create_activity(&self, conn: &DbConn) -> Result { + pub fn create_activity(&self, conn: &Connection) -> Result { let author = User::get(conn, self.author_id)?; let note = self.to_activity(conn)?; @@ -217,15 +216,15 @@ impl Comment { } } -impl FromId for Comment { +impl FromId for Comment { type Error = Error; type Object = Note; - fn from_db(conn: &DbConn, id: &str) -> Result { + fn from_db(conn: &Connection, id: &str) -> Result { Self::find_by_ap_url(conn, id) } - fn from_activity(conn: &DbConn, note: Note) -> Result { + fn from_activity(conn: &Connection, note: Note) -> Result { let comm = { let previous_url = note .in_reply_to() @@ -354,21 +353,21 @@ impl FromId for Comment { } } -impl AsObject for Comment { +impl AsObject for Comment { type Error = Error; type Output = Self; - fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result { + fn activity(self, _conn: &Connection, _actor: User, _id: &str) -> Result { // The actual creation takes place in the FromId impl Ok(self) } } -impl AsObject for Comment { +impl AsObject for Comment { type Error = Error; type Output = (); - fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { + fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> { if self.author_id != actor.id { return Err(Error::Unauthorized); } @@ -381,14 +380,14 @@ impl AsObject for Comment { } for n in Notification::find_for_comment(conn, &self)? { - n.delete(&**conn)?; + n.delete(conn)?; } diesel::update(comments::table) .filter(comments::in_response_to_id.eq(self.id)) .set(comments::in_response_to_id.eq(self.in_response_to_id)) - .execute(&**conn)?; - diesel::delete(&self).execute(&**conn)?; + .execute(conn)?; + diesel::delete(&self).execute(conn)?; Ok(()) } } diff --git a/plume-models/src/follows.rs b/plume-models/src/follows.rs index c74b90a1..91624ee3 100644 --- a/plume-models/src/follows.rs +++ b/plume-models/src/follows.rs @@ -1,6 +1,6 @@ use crate::{ - ap_url, db_conn::DbConn, instance::Instance, notifications::*, schema::follows, users::User, - Connection, Error, Result, CONFIG, + ap_url, instance::Instance, notifications::*, schema::follows, users::User, Connection, Error, + Result, CONFIG, }; use activitystreams::{ activity::{Accept, ActorAndObjectRef, Follow as FollowAct, Undo}, @@ -155,11 +155,11 @@ impl Follow { } } -impl AsObject for User { +impl AsObject for User { type Error = Error; type Output = Follow; - fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result { + fn activity(self, conn: &Connection, actor: User, id: &str) -> Result { // Mastodon (at least) requires the full Follow object when accepting it, // so we rebuilt it here let follow = FollowAct::new(actor.ap_url.parse::()?, id.parse::()?); @@ -167,15 +167,15 @@ impl AsObject for User { } } -impl FromId for Follow { +impl FromId for Follow { type Error = Error; type Object = FollowAct; - fn from_db(conn: &DbConn, id: &str) -> Result { + fn from_db(conn: &Connection, id: &str) -> Result { Follow::find_by_ap_url(conn, id) } - fn from_activity(conn: &DbConn, follow: FollowAct) -> Result { + fn from_activity(conn: &Connection, follow: FollowAct) -> Result { let actor = User::from_id( conn, follow @@ -207,18 +207,18 @@ impl FromId for Follow { } } -impl AsObject for Follow { +impl AsObject for Follow { type Error = Error; type Output = (); - fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { + fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> { let conn = conn; if self.follower_id == actor.id { - diesel::delete(&self).execute(&**conn)?; + diesel::delete(&self).execute(conn)?; // delete associated notification if any if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) { - diesel::delete(¬if).execute(&**conn)?; + diesel::delete(¬if).execute(conn)?; } Ok(()) diff --git a/plume-models/src/inbox.rs b/plume-models/src/inbox.rs index fdf4056e..e52b0168 100644 --- a/plume-models/src/inbox.rs +++ b/plume-models/src/inbox.rs @@ -47,7 +47,7 @@ impl_into_inbox_result! { } pub fn inbox(conn: &DbConn, act: serde_json::Value) -> Result { - Inbox::handle(conn, act) + Inbox::handle(&**conn, act) .with::(CONFIG.proxy()) .with::(CONFIG.proxy()) .with::(CONFIG.proxy()) diff --git a/plume-models/src/likes.rs b/plume-models/src/likes.rs index b9dda167..7a517d9c 100644 --- a/plume-models/src/likes.rs +++ b/plume-models/src/likes.rs @@ -1,6 +1,6 @@ use crate::{ - db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*, - users::User, Connection, Error, Result, CONFIG, + instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*, users::User, + Connection, Error, Result, CONFIG, }; use activitystreams::{ activity::{ActorAndObjectRef, Like as LikeAct, Undo}, @@ -85,11 +85,11 @@ impl Like { } } -impl AsObject for Post { +impl AsObject for Post { type Error = Error; type Output = Like; - fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result { + fn activity(self, conn: &Connection, actor: User, id: &str) -> Result { let res = Like::insert( conn, NewLike { @@ -105,15 +105,15 @@ impl AsObject for Post { } } -impl FromId for Like { +impl FromId for Like { type Error = Error; type Object = LikeAct; - fn from_db(conn: &DbConn, id: &str) -> Result { + fn from_db(conn: &Connection, id: &str) -> Result { Like::find_by_ap_url(conn, id) } - fn from_activity(conn: &DbConn, act: LikeAct) -> Result { + fn from_activity(conn: &Connection, act: LikeAct) -> Result { let res = Like::insert( conn, NewLike { @@ -154,17 +154,17 @@ impl FromId for Like { } } -impl AsObject for Like { +impl AsObject for Like { type Error = Error; type Output = (); - fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { + fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> { if actor.id == self.user_id { - diesel::delete(&self).execute(&**conn)?; + diesel::delete(&self).execute(conn)?; // delete associated notification if any if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) { - diesel::delete(¬if).execute(&**conn)?; + diesel::delete(¬if).execute(conn)?; } Ok(()) } else { diff --git a/plume-models/src/medias.rs b/plume-models/src/medias.rs index 05a5278c..078ea913 100644 --- a/plume-models/src/medias.rs +++ b/plume-models/src/medias.rs @@ -1,6 +1,6 @@ use crate::{ - ap_url, db_conn::DbConn, instance::Instance, safe_string::SafeString, schema::medias, - users::User, Connection, Error, Result, CONFIG, + ap_url, instance::Instance, safe_string::SafeString, schema::medias, users::User, Connection, + Error, Result, CONFIG, }; use activitystreams::{object::Image, prelude::*}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; @@ -206,7 +206,7 @@ impl Media { } // TODO: merge with save_remote? - pub fn from_activity(conn: &DbConn, image: &Image) -> Result { + pub fn from_activity(conn: &Connection, image: &Image) -> Result { let remote_url = image .url() .and_then(|url| url.to_as_uri()) @@ -258,7 +258,7 @@ impl Media { updated = true; } if updated { - diesel::update(&media).set(&media).execute(&**conn)?; + diesel::update(&media).set(&media).execute(conn)?; } Ok(media) }) diff --git a/plume-models/src/mentions.rs b/plume-models/src/mentions.rs index dddb4250..372a71ba 100644 --- a/plume-models/src/mentions.rs +++ b/plume-models/src/mentions.rs @@ -1,6 +1,6 @@ use crate::{ - comments::Comment, db_conn::DbConn, notifications::*, posts::Post, schema::mentions, - users::User, Connection, Error, Result, + comments::Comment, notifications::*, posts::Post, schema::mentions, users::User, Connection, + Error, Result, }; use activitystreams::{ base::BaseExt, @@ -60,7 +60,7 @@ impl Mention { } } - pub fn build_activity(conn: &DbConn, ment: &str) -> Result { + pub fn build_activity(conn: &Connection, ment: &str) -> Result { let user = User::find_by_fqn(conn, ment)?; let mut mention = link::Mention::new(); mention.set_href(user.ap_url.parse::()?); diff --git a/plume-models/src/posts.rs b/plume-models/src/posts.rs index ef86ca97..0137dd2b 100644 --- a/plume-models/src/posts.rs +++ b/plume-models/src/posts.rs @@ -1,7 +1,7 @@ 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, + ap_url, blogs::Blog, instance::Instance, medias::Media, mentions::Mention, post_authors::*, + safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User, Connection, Error, + PostEvent::*, Result, CONFIG, POST_CHAN, }; use activitystreams::{ activity::{Create, Delete, Update}, @@ -615,15 +615,15 @@ impl Post { } } -impl FromId for Post { +impl FromId for Post { type Error = Error; type Object = LicensedArticle; - fn from_db(conn: &DbConn, id: &str) -> Result { + fn from_db(conn: &Connection, id: &str) -> Result { Self::find_by_ap_url(conn, id) } - fn from_activity(conn: &DbConn, article: LicensedArticle) -> Result { + fn from_activity(conn: &Connection, article: LicensedArticle) -> Result { let license = article.ext_one.license.unwrap_or_default(); let article = article.inner; @@ -817,21 +817,21 @@ impl FromId for Post { } } -impl AsObject for Post { +impl AsObject for Post { type Error = Error; type Output = Self; - fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result { + fn activity(self, _conn: &Connection, _actor: User, _id: &str) -> Result { // TODO: check that _actor is actually one of the author? Ok(self) } } -impl AsObject for Post { +impl AsObject for Post { type Error = Error; type Output = (); - fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result { + fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result { let can_delete = self .get_authors(conn)? .into_iter() @@ -855,16 +855,16 @@ pub struct PostUpdate { pub tags: Option, } -impl FromId for PostUpdate { +impl FromId for PostUpdate { type Error = Error; type Object = LicensedArticle; - fn from_db(_: &DbConn, _: &str) -> Result { + fn from_db(_: &Connection, _: &str) -> Result { // Always fail because we always want to deserialize the AP object Err(Error::NotFound) } - fn from_activity(conn: &DbConn, updated: Self::Object) -> Result { + fn from_activity(conn: &Connection, updated: Self::Object) -> Result { let mut post_update = PostUpdate { ap_url: updated .ap_object_ref() @@ -919,11 +919,11 @@ impl FromId for PostUpdate { } } -impl AsObject for PostUpdate { +impl AsObject for PostUpdate { type Error = Error; type Output = (); - fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { + fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> { let mut post = Post::from_id(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?; diff --git a/plume-models/src/reshares.rs b/plume-models/src/reshares.rs index aec75796..46ab1760 100644 --- a/plume-models/src/reshares.rs +++ b/plume-models/src/reshares.rs @@ -1,6 +1,6 @@ use crate::{ - db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::reshares, - timeline::*, users::User, Connection, Error, Result, CONFIG, + instance::Instance, notifications::*, posts::Post, schema::reshares, timeline::*, users::User, + Connection, Error, Result, CONFIG, }; use activitystreams::{ activity::{ActorAndObjectRef, Announce, Undo}, @@ -113,11 +113,11 @@ impl Reshare { } } -impl AsObject for Post { +impl AsObject for Post { type Error = Error; type Output = Reshare; - fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result { + fn activity(self, conn: &Connection, actor: User, id: &str) -> Result { let conn = conn; let reshare = Reshare::insert( conn, @@ -134,15 +134,15 @@ impl AsObject for Post { } } -impl FromId for Reshare { +impl FromId for Reshare { type Error = Error; type Object = Announce; - fn from_db(conn: &DbConn, id: &str) -> Result { + fn from_db(conn: &Connection, id: &str) -> Result { Reshare::find_by_ap_url(conn, id) } - fn from_activity(conn: &DbConn, act: Announce) -> Result { + fn from_activity(conn: &Connection, act: Announce) -> Result { let res = Reshare::insert( conn, NewReshare { @@ -183,17 +183,17 @@ impl FromId for Reshare { } } -impl AsObject for Reshare { +impl AsObject for Reshare { type Error = Error; type Output = (); - fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { + fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> { if actor.id == self.user_id { - diesel::delete(&self).execute(&**conn)?; + diesel::delete(&self).execute(conn)?; // delete associated notification if any if let Ok(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) { - diesel::delete(¬if).execute(&**conn)?; + diesel::delete(¬if).execute(conn)?; } Ok(()) diff --git a/plume-models/src/timeline/mod.rs b/plume-models/src/timeline/mod.rs index 2bc87021..8aad5278 100644 --- a/plume-models/src/timeline/mod.rs +++ b/plume-models/src/timeline/mod.rs @@ -1,5 +1,4 @@ use crate::{ - db_conn::DbConn, lists::List, posts::Post, schema::{posts, timeline, timeline_definition}, @@ -12,7 +11,7 @@ use std::ops::Deref; pub(crate) mod query; pub use self::query::Kind; -use self::query::{QueryError, TimelineQuery}; +pub use self::query::{QueryError, TimelineQuery}; #[derive(Clone, Debug, PartialEq, Queryable, Identifiable, AsChangeset)] #[table_name = "timeline_definition"] @@ -220,7 +219,7 @@ impl Timeline { .map_err(Error::from) } - pub fn add_to_all_timelines(conn: &DbConn, post: &Post, kind: Kind<'_>) -> Result<()> { + pub fn add_to_all_timelines(conn: &Connection, post: &Post, kind: Kind<'_>) -> Result<()> { let timelines = timeline_definition::table .load::(conn.deref()) .map_err(Error::from)?; @@ -246,7 +245,26 @@ impl Timeline { Ok(()) } - pub fn matches(&self, conn: &DbConn, post: &Post, kind: Kind<'_>) -> Result { + pub fn remove_post(&self, conn: &Connection, post: &Post) -> Result { + if self.includes_post(conn, post)? { + return Ok(false); + } + diesel::delete( + timeline::table + .filter(timeline::timeline_id.eq(self.id)) + .filter(timeline::post_id.eq(post.id)), + ) + .execute(conn)?; + Ok(true) + } + + pub fn remove_all_posts(&self, conn: &Connection) -> Result { + let count = diesel::delete(timeline::table.filter(timeline::timeline_id.eq(self.id))) + .execute(conn)?; + Ok(count as u64) + } + + pub fn matches(&self, conn: &Connection, post: &Post, kind: Kind<'_>) -> Result { let query = TimelineQuery::parse(&self.query)?; query.matches(conn, self, post, kind) } diff --git a/plume-models/src/timeline/query.rs b/plume-models/src/timeline/query.rs index 00509440..be2ec566 100644 --- a/plume-models/src/timeline/query.rs +++ b/plume-models/src/timeline/query.rs @@ -1,12 +1,11 @@ use crate::{ blogs::Blog, - db_conn::DbConn, lists::{self, ListType}, posts::Post, tags::Tag, timeline::Timeline, users::User, - Result, + Connection, Result, }; use plume_common::activity_pub::inbox::AsActor; use whatlang::{self, Lang}; @@ -155,7 +154,7 @@ enum TQ<'a> { impl<'a> TQ<'a> { fn matches( &self, - conn: &DbConn, + conn: &Connection, timeline: &Timeline, post: &Post, kind: Kind<'_>, @@ -200,7 +199,7 @@ enum Arg<'a> { impl<'a> Arg<'a> { pub fn matches( &self, - conn: &DbConn, + conn: &Connection, timeline: &Timeline, post: &Post, kind: Kind<'_>, @@ -225,7 +224,7 @@ enum WithList { impl WithList { pub fn matches( &self, - conn: &DbConn, + conn: &Connection, timeline: &Timeline, post: &Post, list: &List<'_>, @@ -292,7 +291,7 @@ impl WithList { WithList::Author { boosts, likes } => match kind { Kind::Original => Ok(list .iter() - .filter_map(|a| User::find_by_fqn(&*conn, a).ok()) + .filter_map(|a| User::find_by_fqn(conn, a).ok()) .any(|a| post.is_author(conn, a.id).unwrap_or(false))), Kind::Reshare(u) => { if *boosts { @@ -361,7 +360,7 @@ enum Bool { impl Bool { pub fn matches( &self, - conn: &DbConn, + conn: &Connection, timeline: &Timeline, post: &Post, kind: Kind<'_>, @@ -654,7 +653,7 @@ impl<'a> TimelineQuery<'a> { pub fn matches( &self, - conn: &DbConn, + conn: &Connection, timeline: &Timeline, post: &Post, kind: Kind<'_>, diff --git a/plume-models/src/users.rs b/plume-models/src/users.rs index b33f9bc1..76ee1182 100644 --- a/plume-models/src/users.rs +++ b/plume-models/src/users.rs @@ -191,10 +191,10 @@ impl User { .map_err(Error::from) } - pub fn find_by_fqn(conn: &DbConn, fqn: &str) -> Result { + pub fn find_by_fqn(conn: &Connection, fqn: &str) -> Result { let from_db = users::table .filter(users::fqn.eq(fqn)) - .first(&**conn) + .first(conn) .optional()?; if let Some(from_db) = from_db { Ok(from_db) @@ -219,7 +219,7 @@ impl User { .map_err(Error::from) } - fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result { + fn fetch_from_webfinger(conn: &Connection, acct: &str) -> Result { let link = resolve(acct.to_owned(), true)? .links .into_iter() @@ -944,15 +944,15 @@ impl IntoId for User { impl Eq for User {} -impl FromId for User { +impl FromId for User { type Error = Error; type Object = CustomPerson; - fn from_db(conn: &DbConn, id: &str) -> Result { + fn from_db(conn: &Connection, id: &str) -> Result { Self::find_by_ap_url(conn, id) } - fn from_activity(conn: &DbConn, acct: CustomPerson) -> Result { + fn from_activity(conn: &Connection, acct: CustomPerson) -> Result { let actor = acct.ap_actor_ref(); let username = actor .preferred_username() @@ -1049,7 +1049,7 @@ impl FromId for User { } } -impl AsActor<&DbConn> for User { +impl AsActor<&Connection> for User { fn get_inbox_url(&self) -> String { self.inbox_url.clone() } @@ -1065,11 +1065,11 @@ impl AsActor<&DbConn> for User { } } -impl AsObject for User { +impl AsObject for User { type Error = Error; type Output = (); - fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { + fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> { if self.id == actor.id { self.delete(conn).map(|_| ()) } else { From 771d4325c27f49c1b2d82450e68d47106bfca86d Mon Sep 17 00:00:00 2001 From: trinity-1686a Date: Sat, 17 Dec 2022 17:51:51 +0100 Subject: [PATCH 2/3] add plm command for list management --- plume-cli/src/list.rs | 257 ++++++++++++++++++++++++++++++++++++++ plume-cli/src/main.rs | 3 + plume-cli/src/timeline.rs | 2 +- plume-models/src/lists.rs | 22 ++++ 4 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 plume-cli/src/list.rs diff --git a/plume-cli/src/list.rs b/plume-cli/src/list.rs new file mode 100644 index 00000000..340c974f --- /dev/null +++ b/plume-cli/src/list.rs @@ -0,0 +1,257 @@ +use clap::{App, Arg, ArgMatches, SubCommand}; + +use plume_models::{blogs::Blog, instance::Instance, lists::*, users::User, Connection}; + +pub fn command<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("lists") + .about("Manage lists") + .subcommand( + SubCommand::with_name("new") + .arg( + Arg::with_name("name") + .short("n") + .long("name") + .takes_value(true) + .help("The name of this list"), + ) + .arg( + Arg::with_name("type") + .short("t") + .long("type") + .takes_value(true) + .help("The type of this list"), + ) + .arg( + Arg::with_name("user") + .short("u") + .long("user") + .takes_value(true) + .help("Username of whom this list is for. Empty for an instance list"), + ) + .about("Create a new list"), + ) + .subcommand( + SubCommand::with_name("delete") + .arg( + Arg::with_name("name") + .short("n") + .long("name") + .takes_value(true) + .help("The name of the list to delete"), + ) + .arg( + Arg::with_name("user") + .short("u") + .long("user") + .takes_value(true) + .help("Username of whom this list was for. Empty for instance list"), + ) + .arg( + Arg::with_name("yes") + .short("y") + .long("yes") + .help("Confirm the deletion"), + ), + ) + .subcommand( + SubCommand::with_name("add") + .arg( + Arg::with_name("name") + .short("n") + .long("name") + .takes_value(true) + .help("The name of the list to add an element to"), + ) + .arg( + Arg::with_name("user") + .short("u") + .long("user") + .takes_value(true) + .help("Username of whom this list is for. Empty for instance list"), + ) + .arg( + Arg::with_name("value") + .short("v") + .long("value") + .takes_value(true) + .help("The value to add"), + ), + ) + .subcommand( + SubCommand::with_name("rm") + .arg( + Arg::with_name("name") + .short("n") + .long("name") + .takes_value(true) + .help("The name of the list to remove an element from"), + ) + .arg( + Arg::with_name("user") + .short("u") + .long("user") + .takes_value(true) + .help("Username of whom this list is for. Empty for instance list"), + ) + .arg( + Arg::with_name("value") + .short("v") + .long("value") + .takes_value(true) + .help("The value to remove"), + ), + ) +} + +pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) { + let conn = conn; + match args.subcommand() { + ("new", Some(x)) => new(x, conn), + ("delete", Some(x)) => delete(x, conn), + ("add", Some(x)) => add(x, conn), + ("rm", Some(x)) => rm(x, conn), + ("", None) => command().print_help().unwrap(), + _ => println!("Unknown subcommand"), + } +} + +fn get_list_identifier(args: &ArgMatches<'_>) -> (String, Option) { + let name = args + .value_of("name") + .map(String::from) + .expect("No name provided for the list"); + let user = args.value_of("user").map(String::from); + (name, user) +} + +fn get_list_type(args: &ArgMatches<'_>) -> ListType { + let typ = args + .value_of("type") + .map(String::from) + .expect("No name type for the list"); + match typ.as_str() { + "user" => ListType::User, + "blog" => ListType::Blog, + "word" => ListType::Word, + "prefix" => ListType::Prefix, + _ => panic!("Invalid list type: {}", typ), + } +} + +fn get_value(args: &ArgMatches<'_>) -> String { + args.value_of("value") + .map(String::from) + .expect("No query provided") +} + +fn resolve_user(username: &str, conn: &Connection) -> User { + let instance = Instance::get_local_uncached(conn).expect("Failed to load local instance"); + + User::find_by_name(conn, username, instance.id).expect("User not found") +} + +fn new(args: &ArgMatches<'_>, conn: &Connection) { + let (name, user) = get_list_identifier(args); + let typ = get_list_type(args); + + let user = user.map(|user| resolve_user(&user, conn)); + + List::new(conn, &name, user.as_ref(), typ).expect("failed to create list"); +} + +fn delete(args: &ArgMatches<'_>, conn: &Connection) { + let (name, user) = get_list_identifier(args); + + if !args.is_present("yes") { + panic!("Warning, this operation is destructive. Add --yes to confirm you want to do it.") + } + + let user = user.map(|user| resolve_user(&user, conn)); + + let list = + List::find_for_user_by_name(conn, user.map(|u| u.id), &name).expect("list not found"); + + list.delete(conn).expect("Failed to update list"); +} + +fn add(args: &ArgMatches<'_>, conn: &Connection) { + let (name, user) = get_list_identifier(args); + let value = get_value(args); + + let user = user.map(|user| resolve_user(&user, conn)); + + let list = + List::find_for_user_by_name(conn, user.map(|u| u.id), &name).expect("list not found"); + + match list.kind() { + ListType::Blog => { + let blog_id = Blog::find_by_fqn(conn, &value).expect("unknown blog").id; + if !list.contains_blog(conn, blog_id).unwrap() { + list.add_blogs(conn, &[blog_id]).unwrap(); + } + } + ListType::User => { + let user_id = User::find_by_fqn(conn, &value).expect("unknown user").id; + if !list.contains_user(conn, user_id).unwrap() { + list.add_users(conn, &[user_id]).unwrap(); + } + } + ListType::Word => { + if !list.contains_word(conn, &value).unwrap() { + list.add_words(conn, &[&value]).unwrap(); + } + } + ListType::Prefix => { + if !list.contains_prefix(conn, &value).unwrap() { + list.add_prefixes(conn, &[&value]).unwrap(); + } + } + } +} + +fn rm(args: &ArgMatches<'_>, conn: &Connection) { + let (name, user) = get_list_identifier(args); + let value = get_value(args); + + let user = user.map(|user| resolve_user(&user, conn)); + + let list = + List::find_for_user_by_name(conn, user.map(|u| u.id), &name).expect("list not found"); + + match list.kind() { + ListType::Blog => { + let blog_id = Blog::find_by_fqn(conn, &value).expect("unknown blog").id; + let mut blogs = list.list_blogs(conn).unwrap(); + if let Some(index) = blogs.iter().position(|b| b.id == blog_id) { + blogs.swap_remove(index); + let blogs = blogs.iter().map(|b| b.id).collect::>(); + list.set_blogs(conn, &blogs).unwrap(); + } + } + ListType::User => { + let user_id = User::find_by_fqn(conn, &value).expect("unknown user").id; + let mut users = list.list_users(conn).unwrap(); + if let Some(index) = users.iter().position(|u| u.id == user_id) { + users.swap_remove(index); + let users = users.iter().map(|u| u.id).collect::>(); + list.set_users(conn, &users).unwrap(); + } + } + ListType::Word => { + let mut words = list.list_words(conn).unwrap(); + if let Some(index) = words.iter().position(|w| *w == value) { + words.swap_remove(index); + let words = words.iter().map(String::as_str).collect::>(); + list.set_words(conn, &words).unwrap(); + } + } + ListType::Prefix => { + let mut prefixes = list.list_prefixes(conn).unwrap(); + if let Some(index) = prefixes.iter().position(|p| *p == value) { + prefixes.swap_remove(index); + let prefixes = prefixes.iter().map(String::as_str).collect::>(); + list.set_prefixes(conn, &prefixes).unwrap(); + } + } + } +} diff --git a/plume-cli/src/main.rs b/plume-cli/src/main.rs index f6b1f48b..00ea0307 100644 --- a/plume-cli/src/main.rs +++ b/plume-cli/src/main.rs @@ -4,6 +4,7 @@ use plume_models::{instance::Instance, Connection as Conn, CONFIG}; use std::io::{self, prelude::*}; mod instance; +mod list; mod migration; mod search; mod timeline; @@ -18,6 +19,7 @@ fn main() { .subcommand(migration::command()) .subcommand(search::command()) .subcommand(timeline::command()) + .subcommand(list::command()) .subcommand(users::command()); let matches = app.clone().get_matches(); @@ -42,6 +44,7 @@ fn main() { ("timeline", Some(args)) => { timeline::run(args, &conn.expect("Couldn't connect to the database.")) } + ("lists", Some(args)) => list::run(args, &conn.expect("Couldn't connect to the database.")), ("users", Some(args)) => { users::run(args, &conn.expect("Couldn't connect to the database.")) } diff --git a/plume-cli/src/timeline.rs b/plume-cli/src/timeline.rs index 1ad21e4c..ee78b731 100644 --- a/plume-cli/src/timeline.rs +++ b/plume-cli/src/timeline.rs @@ -192,7 +192,7 @@ fn preload(timeline: Timeline, count: usize, conn: &Connection) { } for post in posts.iter().rev() { - timeline.add_post(conn, &post).unwrap(); + timeline.add_post(conn, post).unwrap(); } } diff --git a/plume-models/src/lists.rs b/plume-models/src/lists.rs index 5b393f64..9dea12d7 100644 --- a/plume-models/src/lists.rs +++ b/plume-models/src/lists.rs @@ -297,6 +297,28 @@ impl List { .map_err(Error::from) } + pub fn delete(&self, conn: &Connection) -> Result<()> { + if let Some(user_id) = self.user_id { + diesel::delete( + lists::table + .filter(lists::user_id.eq(user_id)) + .filter(lists::name.eq(&self.name)), + ) + .execute(conn) + .map(|_| ()) + .map_err(Error::from) + } else { + diesel::delete( + lists::table + .filter(lists::user_id.is_null()) + .filter(lists::name.eq(&self.name)), + ) + .execute(conn) + .map(|_| ()) + .map_err(Error::from) + } + } + func! {set: set_users, User, add_users} func! {set: set_blogs, Blog, add_blogs} func! {set: set_words, Word, add_words} From 35b951967d466b7b38774dc98bc2a3794e0381f3 Mon Sep 17 00:00:00 2001 From: trinity-1686a Date: Sun, 1 Jan 2023 11:03:17 +0100 Subject: [PATCH 3/3] add a few help messages to the cli --- plume-cli/src/list.rs | 13 +++++++++---- plume-cli/src/timeline.rs | 9 ++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/plume-cli/src/list.rs b/plume-cli/src/list.rs index 340c974f..3670879a 100644 --- a/plume-cli/src/list.rs +++ b/plume-cli/src/list.rs @@ -19,7 +19,9 @@ pub fn command<'a, 'b>() -> App<'a, 'b> { .short("t") .long("type") .takes_value(true) - .help("The type of this list"), + .help( + r#"The type of this list (one of "user", "blog", "word" or "prefix")"#, + ), ) .arg( Arg::with_name("user") @@ -51,7 +53,8 @@ pub fn command<'a, 'b>() -> App<'a, 'b> { .short("y") .long("yes") .help("Confirm the deletion"), - ), + ) + .about("Delete a list"), ) .subcommand( SubCommand::with_name("add") @@ -75,7 +78,8 @@ pub fn command<'a, 'b>() -> App<'a, 'b> { .long("value") .takes_value(true) .help("The value to add"), - ), + ) + .about("Add element to a list"), ) .subcommand( SubCommand::with_name("rm") @@ -99,7 +103,8 @@ pub fn command<'a, 'b>() -> App<'a, 'b> { .long("value") .takes_value(true) .help("The value to remove"), - ), + ) + .about("Remove element from list"), ) } diff --git a/plume-cli/src/timeline.rs b/plume-cli/src/timeline.rs index ee78b731..225811ef 100644 --- a/plume-cli/src/timeline.rs +++ b/plume-cli/src/timeline.rs @@ -62,7 +62,8 @@ pub fn command<'a, 'b>() -> App<'a, 'b> { .short("y") .long("yes") .help("Confirm the deletion"), - ), + ) + .about("Delete a timeline"), ) .subcommand( SubCommand::with_name("edit") @@ -86,7 +87,8 @@ pub fn command<'a, 'b>() -> App<'a, 'b> { .long("query") .takes_value(true) .help("The query posts in this timelines have to match"), - ), + ) + .about("Edit the query of a timeline"), ) .subcommand( SubCommand::with_name("repopulate") @@ -112,7 +114,8 @@ pub fn command<'a, 'b>() -> App<'a, 'b> { .long("preload-count") .takes_value(true) .help("Number of posts to try to preload in this timeline at its creation"), - ), + ) + .about("Repopulate a timeline. Run this after modifying a list the timeline depends on."), ) }