From ec57f1e6874133524eeb9ca08bef588923180b30 Mon Sep 17 00:00:00 2001 From: Baptiste Gelez Date: Sun, 28 Apr 2019 22:17:21 +0100 Subject: [PATCH] Remove Canapi (#540) * Remove Canapi It added more complexity than it helped. * Fail if there are many blog, but none was specified * cargo fmt --- Cargo.lock | 12 -- Cargo.toml | 1 - plume-api/Cargo.toml | 1 - plume-api/src/apps.rs | 11 +- plume-api/src/lib.rs | 18 --- plume-api/src/posts.rs | 30 ++-- plume-models/Cargo.toml | 1 - plume-models/src/apps.rs | 53 +------ plume-models/src/lib.rs | 3 - plume-models/src/posts.rs | 301 +++----------------------------------- src/api/apps.rs | 26 +++- src/api/mod.rs | 8 + src/api/posts.rs | 260 +++++++++++++++++++++++++++----- src/main.rs | 8 +- 14 files changed, 298 insertions(+), 435 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 037deb82..0d0c1f5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -314,14 +314,6 @@ dependencies = [ "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "canapi" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "cc" version = "1.0.30" @@ -1782,7 +1774,6 @@ dependencies = [ "activitypub 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "askama_escape 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "atom_syndication 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "canapi 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "colored 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "ctrlc 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1821,7 +1812,6 @@ dependencies = [ name = "plume-api" version = "0.3.0" dependencies = [ - "canapi 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1878,7 +1868,6 @@ dependencies = [ "ammonia 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "askama_escape 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "bcrypt 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "canapi 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "diesel 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "diesel_migrations 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3296,7 +3285,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum byteorder 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "96c8b41881888cc08af32d47ac4edd52bc7fa27fef774be47a92443756451304" "checksum byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a019b10a2a7cdeb292db131fc8113e57ea2a908f6e7894b0c3c671893b65dbeb" "checksum bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "40ade3d27603c2cb345eb0912aec461a6dec7e06a4ae48589904e808335c7afa" -"checksum canapi 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aab4d6d1edcef8bf19b851b7730d3d1a90373c06321a49a984baebe0989c962c" "checksum cc 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)" = "d01c69d08ff207f231f07196e30f84c70f1c815b04f980f8b7b01ff01f05eb92" "checksum census 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "641317709904ba3c1ad137cb5d88ec9d8c03c07de087b2cff5e84ec565c7e299" "checksum cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11d43355396e872eefb45ce6342e4374ed7bc2b3a502d1b28e36d6e23c05d1f4" diff --git a/Cargo.toml b/Cargo.toml index 0926cbcb..98bd8053 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,6 @@ repository = "https://github.com/Plume-org/Plume" activitypub = "0.1.3" askama_escape = "0.1" atom_syndication = "0.6" -canapi = "0.2" colored = "1.7" dotenv = "0.13" gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" } diff --git a/plume-api/Cargo.toml b/plume-api/Cargo.toml index d75466ac..60058a2c 100644 --- a/plume-api/Cargo.toml +++ b/plume-api/Cargo.toml @@ -4,6 +4,5 @@ version = "0.3.0" authors = ["Plume contributors"] [dependencies] -canapi = "0.2" serde = "1.0" serde_derive = "1.0" diff --git a/plume-api/src/apps.rs b/plume-api/src/apps.rs index 5cd8576c..9e0610d2 100644 --- a/plume-api/src/apps.rs +++ b/plume-api/src/apps.rs @@ -1,13 +1,6 @@ -use canapi::Endpoint; - -#[derive(Clone, Default, Serialize, Deserialize)] -pub struct AppEndpoint { - pub id: Option, +#[derive(Clone, Serialize, Deserialize)] +pub struct NewAppData { pub name: String, pub website: Option, pub redirect_uri: Option, - pub client_id: Option, - pub client_secret: Option, } - -api!("/api/v1/apps" => AppEndpoint); diff --git a/plume-api/src/lib.rs b/plume-api/src/lib.rs index 129950c2..158b4261 100644 --- a/plume-api/src/lib.rs +++ b/plume-api/src/lib.rs @@ -1,24 +1,6 @@ -extern crate canapi; extern crate serde; #[macro_use] extern crate serde_derive; -macro_rules! api { - ($url:expr => $ep:ty) => { - impl Endpoint for $ep { - type Id = i32; - - fn endpoint() -> &'static str { - $url - } - } - }; -} - pub mod apps; pub mod posts; - -#[derive(Default)] -pub struct Api { - pub posts: posts::PostEndpoint, -} diff --git a/plume-api/src/posts.rs b/plume-api/src/posts.rs index f9897756..57b7cf29 100644 --- a/plume-api/src/posts.rs +++ b/plume-api/src/posts.rs @@ -1,13 +1,11 @@ -use canapi::Endpoint; - #[derive(Clone, Default, Serialize, Deserialize)] -pub struct PostEndpoint { - pub id: Option, - pub title: Option, +pub struct NewPostData { + pub title: String, pub subtitle: Option, - pub content: Option, - pub source: Option, - pub author: Option, + pub source: String, + pub author: String, + // If None, and that there is only one blog, it will be choosen automatically. + // If there are more than one blog, the request will fail. pub blog_id: Option, pub published: Option, pub creation_date: Option, @@ -16,4 +14,18 @@ pub struct PostEndpoint { pub cover_id: Option, } -api!("/api/v1/posts" => PostEndpoint); +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct PostData { + pub id: i32, + pub title: String, + pub subtitle: String, + pub content: String, + pub source: Option, + pub authors: Vec, + pub blog_id: i32, + pub published: bool, + pub creation_date: String, + pub license: String, + pub tags: Vec, + pub cover_id: Option, +} diff --git a/plume-models/Cargo.toml b/plume-models/Cargo.toml index 54a43d99..4f96e1b4 100644 --- a/plume-models/Cargo.toml +++ b/plume-models/Cargo.toml @@ -8,7 +8,6 @@ activitypub = "0.1.1" ammonia = "2.0.0" askama_escape = "0.1" bcrypt = "0.2" -canapi = "0.2" guid-create = "0.1" heck = "0.3.0" itertools = "0.8.0" diff --git a/plume-models/src/apps.rs b/plume-models/src/apps.rs index f245c0a7..4b24a88e 100644 --- a/plume-models/src/apps.rs +++ b/plume-models/src/apps.rs @@ -1,13 +1,10 @@ -use canapi::{Error as ApiError, Provider}; use chrono::NaiveDateTime; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; -use plume_api::apps::AppEndpoint; -use plume_common::utils::random_hex; use schema::apps; -use {ApiResult, Connection, Error, Result}; +use {Error, Result}; -#[derive(Clone, Queryable)] +#[derive(Clone, Queryable, Serialize)] pub struct App { pub id: i32, pub name: String, @@ -28,52 +25,6 @@ pub struct NewApp { pub website: Option, } -impl Provider for App { - type Data = AppEndpoint; - - fn get(_conn: &Connection, _id: i32) -> ApiResult { - unimplemented!() - } - - fn list(_conn: &Connection, _query: AppEndpoint) -> Vec { - unimplemented!() - } - - fn create(conn: &Connection, data: AppEndpoint) -> ApiResult { - let client_id = random_hex(); - - let client_secret = random_hex(); - let app = App::insert( - conn, - NewApp { - name: data.name, - client_id, - client_secret, - redirect_uri: data.redirect_uri, - website: data.website, - }, - ) - .map_err(|_| ApiError::NotFound("Couldn't register app".into()))?; - - Ok(AppEndpoint { - id: Some(app.id), - name: app.name, - client_id: Some(app.client_id), - client_secret: Some(app.client_secret), - redirect_uri: app.redirect_uri, - website: app.website, - }) - } - - fn update(_conn: &Connection, _id: i32, _new_data: AppEndpoint) -> ApiResult { - unimplemented!() - } - - fn delete(_conn: &Connection, _id: i32) { - unimplemented!() - } -} - impl App { get!(apps); insert!(apps, NewApp); diff --git a/plume-models/src/lib.rs b/plume-models/src/lib.rs index 2e77d276..5dd1a865 100644 --- a/plume-models/src/lib.rs +++ b/plume-models/src/lib.rs @@ -6,7 +6,6 @@ extern crate activitypub; extern crate ammonia; extern crate askama_escape; extern crate bcrypt; -extern crate canapi; extern crate chrono; #[macro_use] extern crate diesel; @@ -154,8 +153,6 @@ impl From> for Error { pub type Result = std::result::Result; -pub type ApiResult = std::result::Result; - /// Adds a function to a model, that returns the first /// matching row for a given list of fields. /// diff --git a/plume-models/src/posts.rs b/plume-models/src/posts.rs index 3cc302b3..356f584d 100644 --- a/plume-models/src/posts.rs +++ b/plume-models/src/posts.rs @@ -4,7 +4,6 @@ use activitypub::{ object::{Article, Image, Tombstone}, CustomObject, }; -use canapi::{Error as ApiError, Provider}; use chrono::{NaiveDateTime, TimeZone, Utc}; use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; use heck::{CamelCase, KebabCase}; @@ -15,10 +14,8 @@ use blogs::Blog; use instance::Instance; use medias::Media; use mentions::Mention; -use plume_api::posts::PostEndpoint; use plume_common::{ activity_pub::{ - broadcast, inbox::{AsObject, FromId}, Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILITY, }, @@ -30,7 +27,7 @@ use schema::posts; use search::Searcher; use tags::*; use users::User; -use {ap_url, ApiResult, Connection, Error, PlumeRocket, Result, CONFIG}; +use {ap_url, Connection, Error, PlumeRocket, Result, CONFIG}; pub type LicensedArticle = CustomObject; @@ -67,282 +64,6 @@ pub struct NewPost { pub cover_id: Option, } -impl Provider for Post { - type Data = PostEndpoint; - - fn get(rockets: &PlumeRocket, id: i32) -> ApiResult { - let conn = &*rockets.conn; - if let Ok(post) = Post::get(conn, id) { - if !post.published - && !rockets - .user - .as_ref() - .and_then(|u| post.is_author(conn, u.id).ok()) - .unwrap_or(false) - { - return Err(ApiError::Authorization( - "You are not authorized to access this post yet.".to_string(), - )); - } - Ok(PostEndpoint { - id: Some(post.id), - title: Some(post.title.clone()), - subtitle: Some(post.subtitle.clone()), - content: Some(post.content.get().clone()), - source: Some(post.source.clone()), - author: Some( - post.get_authors(conn) - .map_err(|_| ApiError::NotFound("Authors not found".into()))?[0] - .username - .clone(), - ), - blog_id: Some(post.blog_id), - published: Some(post.published), - creation_date: Some(post.creation_date.format("%Y-%m-%d").to_string()), - license: Some(post.license.clone()), - tags: Some( - Tag::for_post(conn, post.id) - .map_err(|_| ApiError::NotFound("Tags not found".into()))? - .into_iter() - .map(|t| t.tag) - .collect(), - ), - cover_id: post.cover_id, - }) - } else { - Err(ApiError::NotFound("Request post was not found".to_string())) - } - } - - fn list(rockets: &PlumeRocket, filter: PostEndpoint) -> Vec { - let conn = &*rockets.conn; - let mut query = posts::table.into_boxed(); - if let Some(title) = filter.title { - query = query.filter(posts::title.eq(title)); - } - if let Some(subtitle) = filter.subtitle { - query = query.filter(posts::subtitle.eq(subtitle)); - } - if let Some(content) = filter.content { - query = query.filter(posts::content.eq(content)); - } - - query - .get_results::(conn) - .map(|ps| { - ps.into_iter() - .filter(|p| { - p.published - || rockets - .user - .as_ref() - .and_then(|u| p.is_author(conn, u.id).ok()) - .unwrap_or(false) - }) - .map(|p| PostEndpoint { - id: Some(p.id), - title: Some(p.title.clone()), - subtitle: Some(p.subtitle.clone()), - content: Some(p.content.get().clone()), - source: Some(p.source.clone()), - author: Some(p.get_authors(conn).unwrap_or_default()[0].username.clone()), - blog_id: Some(p.blog_id), - published: Some(p.published), - creation_date: Some(p.creation_date.format("%Y-%m-%d").to_string()), - license: Some(p.license.clone()), - tags: Some( - Tag::for_post(conn, p.id) - .unwrap_or_else(|_| vec![]) - .into_iter() - .map(|t| t.tag) - .collect(), - ), - cover_id: p.cover_id, - }) - .collect() - }) - .unwrap_or_else(|_| vec![]) - } - - fn update( - _rockets: &PlumeRocket, - _id: i32, - _new_data: PostEndpoint, - ) -> ApiResult { - unimplemented!() - } - - fn delete(rockets: &PlumeRocket, id: i32) { - let conn = &*rockets.conn; - let user_id = rockets - .user - .as_ref() - .expect("Post as Provider::delete: not authenticated") - .id; - if let Ok(post) = Post::get(conn, id) { - if post.is_author(conn, user_id).unwrap_or(false) { - post.delete(conn, &rockets.searcher) - .expect("Post as Provider::delete: delete error"); - } - } - } - - fn create(rockets: &PlumeRocket, query: PostEndpoint) -> ApiResult { - let conn = &*rockets.conn; - let search = &rockets.searcher; - let worker = &rockets.worker; - if rockets.user.is_none() { - return Err(ApiError::Authorization( - "You are not authorized to create new articles.".to_string(), - )); - } - - let title = query.title.clone().expect("No title for new post in API"); - let slug = query.title.unwrap().to_kebab_case(); - - let date = query.creation_date.clone().and_then(|d| { - NaiveDateTime::parse_from_str(format!("{} 00:00:00", d).as_ref(), "%Y-%m-%d %H:%M:%S") - .ok() - }); - - let domain = &Instance::get_local(&conn) - .map_err(|_| ApiError::NotFound("posts::update: Error getting local instance".into()))? - .public_domain; - let author = rockets - .user - .clone() - .ok_or_else(|| ApiError::NotFound("Author not found".into()))?; - - let (content, mentions, hashtags) = md_to_html( - query.source.clone().unwrap_or_default().clone().as_ref(), - domain, - false, - Some(Media::get_media_processor(conn, vec![&author])), - ); - - let blog = match query.blog_id { - Some(x) => x, - None => { - Blog::find_for_author(conn, &author) - .map_err(|_| ApiError::NotFound("No default blog".into()))?[0] - .id - } - }; - - if Post::find_by_slug(conn, &slug, blog).is_ok() { - // Not an actual authorization problem, but we have nothing better for now… - // TODO: add another error variant to canapi and add it there - return Err(ApiError::Authorization( - "A post with the same slug already exists".to_string(), - )); - } - - let post = Post::insert( - conn, - NewPost { - blog_id: blog, - slug, - title, - content: SafeString::new(content.as_ref()), - published: query.published.unwrap_or(true), - license: query.license.unwrap_or_else(|| { - Instance::get_local(conn) - .map(|i| i.default_license) - .unwrap_or_else(|_| String::from("CC-BY-SA")) - }), - creation_date: date, - ap_url: String::new(), - subtitle: query.subtitle.unwrap_or_default(), - source: query.source.expect("Post API::create: no source error"), - cover_id: query.cover_id, - }, - search, - ) - .map_err(|_| ApiError::NotFound("Creation error".into()))?; - - PostAuthor::insert( - conn, - NewPostAuthor { - author_id: author.id, - post_id: post.id, - }, - ) - .map_err(|_| ApiError::NotFound("Error saving authors".into()))?; - - if let Some(tags) = query.tags { - for tag in tags { - Tag::insert( - conn, - NewTag { - tag, - is_hashtag: false, - post_id: post.id, - }, - ) - .map_err(|_| ApiError::NotFound("Error saving tags".into()))?; - } - } - for hashtag in hashtags { - Tag::insert( - conn, - NewTag { - tag: hashtag.to_camel_case(), - is_hashtag: true, - post_id: post.id, - }, - ) - .map_err(|_| ApiError::NotFound("Error saving hashtags".into()))?; - } - - if post.published { - for m in mentions.into_iter() { - Mention::from_activity( - &*conn, - &Mention::build_activity(&rockets, &m) - .map_err(|_| ApiError::NotFound("Couldn't build mentions".into()))?, - post.id, - true, - true, - ) - .map_err(|_| ApiError::NotFound("Error saving mentions".into()))?; - } - - let act = post - .create_activity(&*conn) - .map_err(|_| ApiError::NotFound("Couldn't create activity".into()))?; - let dest = User::one_by_instance(&*conn) - .map_err(|_| ApiError::NotFound("Couldn't list remote instances".into()))?; - worker.execute(move || broadcast(&author, act, dest)); - } - - Ok(PostEndpoint { - id: Some(post.id), - title: Some(post.title.clone()), - subtitle: Some(post.subtitle.clone()), - content: Some(post.content.get().clone()), - source: Some(post.source.clone()), - author: Some( - post.get_authors(conn) - .map_err(|_| ApiError::NotFound("No authors".into()))?[0] - .username - .clone(), - ), - blog_id: Some(post.blog_id), - published: Some(post.published), - creation_date: Some(post.creation_date.format("%Y-%m-%d").to_string()), - license: Some(post.license.clone()), - tags: Some( - Tag::for_post(conn, post.id) - .map_err(|_| ApiError::NotFound("Tags not found".into()))? - .into_iter() - .map(|t| t.tag) - .collect(), - ), - cover_id: post.cover_id, - }) - } -} - impl Post { get!(posts); find_by!(posts, find_by_slug, slug as &str, blog_id as i32); @@ -441,6 +162,26 @@ impl Post { .map_err(Error::from) } + pub fn list_filtered( + conn: &Connection, + title: Option, + subtitle: Option, + content: Option, + ) -> Result> { + 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::(conn).map_err(Error::from) + } + pub fn get_recents(conn: &Connection, limit: i64) -> Result> { posts::table .order(posts::creation_date.desc()) diff --git a/src/api/apps.rs b/src/api/apps.rs index 3b24fd9c..7bbc2d7a 100644 --- a/src/api/apps.rs +++ b/src/api/apps.rs @@ -1,12 +1,24 @@ -use canapi::Provider; use rocket_contrib::json::Json; -use serde_json; -use plume_api::apps::AppEndpoint; -use plume_models::{apps::App, db_conn::DbConn, Connection}; +use crate::api::Api; +use plume_api::apps::NewAppData; +use plume_common::utils::random_hex; +use plume_models::{apps::*, db_conn::DbConn}; #[post("/apps", data = "")] -pub fn create(conn: DbConn, data: Json) -> Json { - let post = >::create(&*conn, (*data).clone()).ok(); - Json(json!(post)) +pub fn create(conn: DbConn, data: Json) -> Api { + let client_id = random_hex(); + let client_secret = random_hex(); + let app = App::insert( + &*conn, + NewApp { + name: data.name.clone(), + client_id, + client_secret, + redirect_uri: data.redirect_uri.clone(), + website: data.website.clone(), + }, + )?; + + Ok(Json(app)) } diff --git a/src/api/mod.rs b/src/api/mod.rs index 410fdaa4..c320e081 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -9,6 +9,8 @@ use serde_json; use plume_common::utils::random_hex; use plume_models::{api_tokens::*, apps::App, users::User, Error, PlumeRocket}; +type Api = Result, ApiError>; + #[derive(Debug)] pub struct ApiError(Error); @@ -18,6 +20,12 @@ impl From for ApiError { } } +impl From for ApiError { + fn from(err: std::option::NoneError) -> ApiError { + ApiError(err.into()) + } +} + impl<'r> Responder<'r> for ApiError { fn respond_to(self, req: &Request) -> response::Result<'r> { match self.0 { diff --git a/src/api/posts.rs b/src/api/posts.rs index 9a9d1e80..fd97c210 100644 --- a/src/api/posts.rs +++ b/src/api/posts.rs @@ -1,54 +1,236 @@ -use canapi::{Error as ApiError, Provider}; -use rocket::http::uri::Origin; +use chrono::NaiveDateTime; +use heck::{CamelCase, KebabCase}; use rocket_contrib::json::Json; -use serde_json; -use serde_qs; -use api::authorization::*; -use plume_api::posts::PostEndpoint; -use plume_models::{posts::Post, users::User, PlumeRocket}; +use crate::api::{authorization::*, Api}; +use plume_api::posts::*; +use plume_common::{activity_pub::broadcast, utils::md_to_html}; +use plume_models::{ + blogs::Blog, db_conn::DbConn, instance::Instance, medias::Media, mentions::*, post_authors::*, + posts::*, safe_string::SafeString, tags::*, users::User, Error, PlumeRocket, +}; #[get("/posts/")] -pub fn get( - id: i32, - auth: Option>, - mut rockets: PlumeRocket, -) -> Json { - rockets.user = auth.and_then(|a| User::get(&*rockets.conn, a.0.user_id).ok()); - let post = >::get(&rockets, id).ok(); - Json(json!(post)) +pub fn get(id: i32, auth: Option>, conn: DbConn) -> Api { + let user = auth.and_then(|a| User::get(&conn, a.0.user_id).ok()); + let post = Post::get(&conn, id)?; + + if !post.published + && !user + .and_then(|u| post.is_author(&conn, u.id).ok()) + .unwrap_or(false) + { + return Err(Error::Unauthorized.into()); + } + + Ok(Json(PostData { + authors: post + .get_authors(&conn)? + .into_iter() + .map(|a| a.username) + .collect(), + creation_date: post.creation_date.format("%Y-%m-%d").to_string(), + tags: Tag::for_post(&conn, post.id)? + .into_iter() + .map(|t| t.tag) + .collect(), + + id: post.id, + title: post.title, + subtitle: post.subtitle, + content: post.content.to_string(), + source: Some(post.source), + blog_id: post.blog_id, + published: post.published, + license: post.license, + cover_id: post.cover_id, + })) } -#[get("/posts")] +#[get("/posts?&<subtitle>&<content>")] pub fn list( - uri: &Origin, + title: Option<String>, + subtitle: Option<String>, + content: Option<String>, auth: Option<Authorization<Read, Post>>, - mut rockets: PlumeRocket, -) -> Json<serde_json::Value> { - rockets.user = auth.and_then(|a| User::get(&*rockets.conn, a.0.user_id).ok()); - let query: PostEndpoint = - serde_qs::from_str(uri.query().unwrap_or("")).expect("api::list: invalid query error"); - let post = <Post as Provider<PlumeRocket>>::list(&rockets, query); - Json(json!(post)) + conn: DbConn, +) -> Api<Vec<PostData>> { + let user = auth.and_then(|a| User::get(&conn, a.0.user_id).ok()); + let user_id = user.map(|u| u.id); + + Ok(Json( + Post::list_filtered(&conn, title, subtitle, content)? + .into_iter() + .filter(|p| { + p.published + || user_id + .and_then(|u| p.is_author(&conn, u).ok()) + .unwrap_or(false) + }) + .filter_map(|p| { + Some(PostData { + authors: p + .get_authors(&conn) + .ok()? + .into_iter() + .map(|a| a.username) + .collect(), + creation_date: p.creation_date.format("%Y-%m-%d").to_string(), + tags: Tag::for_post(&conn, p.id) + .ok()? + .into_iter() + .map(|t| t.tag) + .collect(), + + id: p.id, + title: p.title, + subtitle: p.subtitle, + content: p.content.to_string(), + source: Some(p.source), + blog_id: p.blog_id, + published: p.published, + license: p.license, + cover_id: p.cover_id, + }) + }) + .collect(), + )) } #[post("/posts", data = "<payload>")] pub fn create( auth: Authorization<Write, Post>, - payload: Json<PostEndpoint>, - mut rockets: PlumeRocket, -) -> Json<serde_json::Value> { - rockets.user = User::get(&*rockets.conn, auth.0.user_id).ok(); - let new_post = <Post as Provider<PlumeRocket>>::create(&rockets, (*payload).clone()); - Json(new_post.map(|p| json!(p)).unwrap_or_else(|e| { - json!({ - "error": "Invalid data, couldn't create new post", - "details": match e { - ApiError::Fetch(msg) => msg, - ApiError::SerDe(msg) => msg, - ApiError::NotFound(msg) => msg, - ApiError::Authorization(msg) => msg, - } - }) + payload: Json<NewPostData>, + rockets: PlumeRocket, +) -> Api<PostData> { + let conn = &*rockets.conn; + let search = &rockets.searcher; + let worker = &rockets.worker; + + let author = User::get(conn, auth.0.user_id)?; + + let slug = &payload.title.clone().to_kebab_case(); + let date = payload.creation_date.clone().and_then(|d| { + NaiveDateTime::parse_from_str(format!("{} 00:00:00", d).as_ref(), "%Y-%m-%d %H:%M:%S").ok() + }); + + let domain = &Instance::get_local(conn)?.public_domain; + let (content, mentions, hashtags) = md_to_html( + &payload.source, + domain, + false, + Some(Media::get_media_processor(conn, vec![&author])), + ); + + let blog = payload.blog_id.or_else(|| { + let blogs = Blog::find_for_author(conn, &author).ok()?; + if blogs.len() == 1 { + Some(blogs[0].id) + } else { + None + } + })?; + + if Post::find_by_slug(conn, slug, blog).is_ok() { + return Err(Error::InvalidValue.into()); + } + + let post = Post::insert( + conn, + NewPost { + blog_id: blog, + slug: slug.to_string(), + title: payload.title.clone(), + content: SafeString::new(content.as_ref()), + published: payload.published.unwrap_or(true), + license: payload.license.clone().unwrap_or_else(|| { + Instance::get_local(conn) + .map(|i| i.default_license) + .unwrap_or_else(|_| String::from("CC-BY-SA")) + }), + creation_date: date, + ap_url: String::new(), + subtitle: payload.subtitle.clone().unwrap_or_default(), + source: payload.source.clone(), + cover_id: payload.cover_id, + }, + search, + )?; + + PostAuthor::insert( + conn, + NewPostAuthor { + author_id: author.id, + post_id: post.id, + }, + )?; + + if let Some(ref tags) = payload.tags { + for tag in tags { + Tag::insert( + conn, + NewTag { + tag: tag.to_string(), + is_hashtag: false, + post_id: post.id, + }, + )?; + } + } + for hashtag in hashtags { + Tag::insert( + conn, + NewTag { + tag: hashtag.to_camel_case(), + is_hashtag: true, + post_id: post.id, + }, + )?; + } + + if post.published { + for m in mentions.into_iter() { + Mention::from_activity( + &*conn, + &Mention::build_activity(&rockets, &m)?, + post.id, + true, + true, + )?; + } + + let act = post.create_activity(&*conn)?; + let dest = User::one_by_instance(&*conn)?; + worker.execute(move || broadcast(&author, act, dest)); + } + + Ok(Json(PostData { + authors: post.get_authors(conn)?.into_iter().map(|a| a.fqn).collect(), + creation_date: post.creation_date.format("%Y-%m-%d").to_string(), + tags: Tag::for_post(conn, post.id)? + .into_iter() + .map(|t| t.tag) + .collect(), + + id: post.id, + title: post.title, + subtitle: post.subtitle, + content: post.content.to_string(), + source: Some(post.source), + blog_id: post.blog_id, + published: post.published, + license: post.license, + cover_id: post.cover_id, })) } + +#[delete("/posts/<id>")] +pub fn delete(auth: Authorization<Write, Post>, rockets: PlumeRocket, id: i32) -> Api<()> { + let author = User::get(&*rockets.conn, auth.0.user_id)?; + if let Ok(post) = Post::get(&*rockets.conn, id) { + if post.is_author(&*rockets.conn, author.id).unwrap_or(false) { + post.delete(&*rockets.conn, &rockets.searcher)?; + } + } + Ok(Json(())) +} diff --git a/src/main.rs b/src/main.rs index 1ca01880..d34ff111 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,9 @@ #![allow(clippy::too_many_arguments)] -#![feature(decl_macro, proc_macro_hygiene)] +#![feature(decl_macro, proc_macro_hygiene, try_trait)] extern crate activitypub; extern crate askama_escape; extern crate atom_syndication; -extern crate canapi; extern crate chrono; extern crate colored; extern crate ctrlc; @@ -102,8 +101,8 @@ Then try to restart Plume. SearcherError::IndexOpeningError => panic!( r#" Plume was unable to open the search index. If you created the index -before, make sure to run Plume in the same directory it was created in, or -to set SEARCH_INDEX accordingly. If you did not yet create the search +before, make sure to run Plume in the same directory it was created in, or +to set SEARCH_INDEX accordingly. If you did not yet create the search index, run this command: plm search init @@ -237,6 +236,7 @@ Then try to restart Plume api::posts::get, api::posts::list, api::posts::create, + api::posts::delete, ], ) .register(catchers![