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}