Remove Canapi (#540)

* Remove Canapi

It added more complexity than it helped.

* Fail if there are many blog, but none was specified

* cargo fmt
This commit is contained in:
Baptiste Gelez 2019-04-28 22:17:21 +01:00 committed by GitHub
parent 787eb7f399
commit ec57f1e687
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 298 additions and 435 deletions

12
Cargo.lock generated
View file

@ -314,14 +314,6 @@ dependencies = [
"iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "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]] [[package]]
name = "cc" name = "cc"
version = "1.0.30" version = "1.0.30"
@ -1782,7 +1774,6 @@ dependencies = [
"activitypub 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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)", "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)", "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)", "ctrlc 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1821,7 +1812,6 @@ dependencies = [
name = "plume-api" name = "plume-api"
version = "0.3.0" version = "0.3.0"
dependencies = [ 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 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)", "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)", "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)", "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)", "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)", "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 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)", "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 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 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 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 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 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" "checksum cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11d43355396e872eefb45ce6342e4374ed7bc2b3a502d1b28e36d6e23c05d1f4"

View file

@ -8,7 +8,6 @@ repository = "https://github.com/Plume-org/Plume"
activitypub = "0.1.3" activitypub = "0.1.3"
askama_escape = "0.1" askama_escape = "0.1"
atom_syndication = "0.6" atom_syndication = "0.6"
canapi = "0.2"
colored = "1.7" colored = "1.7"
dotenv = "0.13" dotenv = "0.13"
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" } gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }

View file

@ -4,6 +4,5 @@ version = "0.3.0"
authors = ["Plume contributors"] authors = ["Plume contributors"]
[dependencies] [dependencies]
canapi = "0.2"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"

View file

@ -1,13 +1,6 @@
use canapi::Endpoint; #[derive(Clone, Serialize, Deserialize)]
pub struct NewAppData {
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct AppEndpoint {
pub id: Option<i32>,
pub name: String, pub name: String,
pub website: Option<String>, pub website: Option<String>,
pub redirect_uri: Option<String>, pub redirect_uri: Option<String>,
pub client_id: Option<String>,
pub client_secret: Option<String>,
} }
api!("/api/v1/apps" => AppEndpoint);

View file

@ -1,24 +1,6 @@
extern crate canapi;
extern crate serde; extern crate serde;
#[macro_use] #[macro_use]
extern crate serde_derive; 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 apps;
pub mod posts; pub mod posts;
#[derive(Default)]
pub struct Api {
pub posts: posts::PostEndpoint,
}

View file

@ -1,13 +1,11 @@
use canapi::Endpoint;
#[derive(Clone, Default, Serialize, Deserialize)] #[derive(Clone, Default, Serialize, Deserialize)]
pub struct PostEndpoint { pub struct NewPostData {
pub id: Option<i32>, pub title: String,
pub title: Option<String>,
pub subtitle: Option<String>, pub subtitle: Option<String>,
pub content: Option<String>, pub source: String,
pub source: Option<String>, pub author: String,
pub author: Option<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<i32>, pub blog_id: Option<i32>,
pub published: Option<bool>, pub published: Option<bool>,
pub creation_date: Option<String>, pub creation_date: Option<String>,
@ -16,4 +14,18 @@ pub struct PostEndpoint {
pub cover_id: Option<i32>, pub cover_id: Option<i32>,
} }
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<String>,
pub authors: Vec<String>,
pub blog_id: i32,
pub published: bool,
pub creation_date: String,
pub license: String,
pub tags: Vec<String>,
pub cover_id: Option<i32>,
}

View file

@ -8,7 +8,6 @@ activitypub = "0.1.1"
ammonia = "2.0.0" ammonia = "2.0.0"
askama_escape = "0.1" askama_escape = "0.1"
bcrypt = "0.2" bcrypt = "0.2"
canapi = "0.2"
guid-create = "0.1" guid-create = "0.1"
heck = "0.3.0" heck = "0.3.0"
itertools = "0.8.0" itertools = "0.8.0"

View file

@ -1,13 +1,10 @@
use canapi::{Error as ApiError, Provider};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_api::apps::AppEndpoint;
use plume_common::utils::random_hex;
use schema::apps; use schema::apps;
use {ApiResult, Connection, Error, Result}; use {Error, Result};
#[derive(Clone, Queryable)] #[derive(Clone, Queryable, Serialize)]
pub struct App { pub struct App {
pub id: i32, pub id: i32,
pub name: String, pub name: String,
@ -28,52 +25,6 @@ pub struct NewApp {
pub website: Option<String>, pub website: Option<String>,
} }
impl Provider<Connection> for App {
type Data = AppEndpoint;
fn get(_conn: &Connection, _id: i32) -> ApiResult<AppEndpoint> {
unimplemented!()
}
fn list(_conn: &Connection, _query: AppEndpoint) -> Vec<AppEndpoint> {
unimplemented!()
}
fn create(conn: &Connection, data: AppEndpoint) -> ApiResult<AppEndpoint> {
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<AppEndpoint> {
unimplemented!()
}
fn delete(_conn: &Connection, _id: i32) {
unimplemented!()
}
}
impl App { impl App {
get!(apps); get!(apps);
insert!(apps, NewApp); insert!(apps, NewApp);

View file

@ -6,7 +6,6 @@ extern crate activitypub;
extern crate ammonia; extern crate ammonia;
extern crate askama_escape; extern crate askama_escape;
extern crate bcrypt; extern crate bcrypt;
extern crate canapi;
extern crate chrono; extern crate chrono;
#[macro_use] #[macro_use]
extern crate diesel; extern crate diesel;
@ -154,8 +153,6 @@ impl From<InboxError<Error>> for Error {
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
pub type ApiResult<T> = std::result::Result<T, canapi::Error>;
/// Adds a function to a model, that returns the first /// Adds a function to a model, that returns the first
/// matching row for a given list of fields. /// matching row for a given list of fields.
/// ///

View file

@ -4,7 +4,6 @@ use activitypub::{
object::{Article, Image, Tombstone}, object::{Article, Image, Tombstone},
CustomObject, CustomObject,
}; };
use canapi::{Error as ApiError, Provider};
use chrono::{NaiveDateTime, TimeZone, Utc}; use chrono::{NaiveDateTime, TimeZone, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use heck::{CamelCase, KebabCase}; use heck::{CamelCase, KebabCase};
@ -15,10 +14,8 @@ use blogs::Blog;
use instance::Instance; use instance::Instance;
use medias::Media; use medias::Media;
use mentions::Mention; use mentions::Mention;
use plume_api::posts::PostEndpoint;
use plume_common::{ use plume_common::{
activity_pub::{ activity_pub::{
broadcast,
inbox::{AsObject, FromId}, inbox::{AsObject, FromId},
Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILITY, Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILITY,
}, },
@ -30,7 +27,7 @@ use schema::posts;
use search::Searcher; use search::Searcher;
use tags::*; use tags::*;
use users::User; use users::User;
use {ap_url, ApiResult, Connection, Error, PlumeRocket, Result, CONFIG}; use {ap_url, Connection, Error, PlumeRocket, Result, CONFIG};
pub type LicensedArticle = CustomObject<Licensed, Article>; pub type LicensedArticle = CustomObject<Licensed, Article>;
@ -67,282 +64,6 @@ pub struct NewPost {
pub cover_id: Option<i32>, pub cover_id: Option<i32>,
} }
impl Provider<PlumeRocket> for Post {
type Data = PostEndpoint;
fn get(rockets: &PlumeRocket, id: i32) -> ApiResult<PostEndpoint> {
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<PostEndpoint> {
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::<Post>(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<PostEndpoint> {
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<PostEndpoint> {
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 { impl Post {
get!(posts); get!(posts);
find_by!(posts, find_by_slug, slug as &str, blog_id as i32); find_by!(posts, find_by_slug, slug as &str, blog_id as i32);
@ -441,6 +162,26 @@ impl Post {
.map_err(Error::from) .map_err(Error::from)
} }
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)
}
pub fn get_recents(conn: &Connection, limit: i64) -> Result<Vec<Post>> { pub fn get_recents(conn: &Connection, limit: i64) -> Result<Vec<Post>> {
posts::table posts::table
.order(posts::creation_date.desc()) .order(posts::creation_date.desc())

View file

@ -1,12 +1,24 @@
use canapi::Provider;
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
use serde_json;
use plume_api::apps::AppEndpoint; use crate::api::Api;
use plume_models::{apps::App, db_conn::DbConn, Connection}; use plume_api::apps::NewAppData;
use plume_common::utils::random_hex;
use plume_models::{apps::*, db_conn::DbConn};
#[post("/apps", data = "<data>")] #[post("/apps", data = "<data>")]
pub fn create(conn: DbConn, data: Json<AppEndpoint>) -> Json<serde_json::Value> { pub fn create(conn: DbConn, data: Json<NewAppData>) -> Api<App> {
let post = <App as Provider<Connection>>::create(&*conn, (*data).clone()).ok(); let client_id = random_hex();
Json(json!(post)) 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))
} }

View file

@ -9,6 +9,8 @@ use serde_json;
use plume_common::utils::random_hex; use plume_common::utils::random_hex;
use plume_models::{api_tokens::*, apps::App, users::User, Error, PlumeRocket}; use plume_models::{api_tokens::*, apps::App, users::User, Error, PlumeRocket};
type Api<T> = Result<Json<T>, ApiError>;
#[derive(Debug)] #[derive(Debug)]
pub struct ApiError(Error); pub struct ApiError(Error);
@ -18,6 +20,12 @@ impl From<Error> for ApiError {
} }
} }
impl From<std::option::NoneError> for ApiError {
fn from(err: std::option::NoneError) -> ApiError {
ApiError(err.into())
}
}
impl<'r> Responder<'r> for ApiError { impl<'r> Responder<'r> for ApiError {
fn respond_to(self, req: &Request) -> response::Result<'r> { fn respond_to(self, req: &Request) -> response::Result<'r> {
match self.0 { match self.0 {

View file

@ -1,54 +1,236 @@
use canapi::{Error as ApiError, Provider}; use chrono::NaiveDateTime;
use rocket::http::uri::Origin; use heck::{CamelCase, KebabCase};
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
use serde_json;
use serde_qs;
use api::authorization::*; use crate::api::{authorization::*, Api};
use plume_api::posts::PostEndpoint; use plume_api::posts::*;
use plume_models::{posts::Post, users::User, PlumeRocket}; 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/<id>")] #[get("/posts/<id>")]
pub fn get( pub fn get(id: i32, auth: Option<Authorization<Read, Post>>, conn: DbConn) -> Api<PostData> {
id: i32, let user = auth.and_then(|a| User::get(&conn, a.0.user_id).ok());
auth: Option<Authorization<Read, Post>>, let post = Post::get(&conn, id)?;
mut rockets: PlumeRocket,
) -> Json<serde_json::Value> { if !post.published
rockets.user = auth.and_then(|a| User::get(&*rockets.conn, a.0.user_id).ok()); && !user
let post = <Post as Provider<PlumeRocket>>::get(&rockets, id).ok(); .and_then(|u| post.is_author(&conn, u.id).ok())
Json(json!(post)) .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?<title>&<subtitle>&<content>")]
pub fn list( pub fn list(
uri: &Origin, title: Option<String>,
subtitle: Option<String>,
content: Option<String>,
auth: Option<Authorization<Read, Post>>, auth: Option<Authorization<Read, Post>>,
mut rockets: PlumeRocket, conn: DbConn,
) -> Json<serde_json::Value> { ) -> Api<Vec<PostData>> {
rockets.user = auth.and_then(|a| User::get(&*rockets.conn, a.0.user_id).ok()); let user = auth.and_then(|a| User::get(&conn, a.0.user_id).ok());
let query: PostEndpoint = let user_id = user.map(|u| u.id);
serde_qs::from_str(uri.query().unwrap_or("")).expect("api::list: invalid query error");
let post = <Post as Provider<PlumeRocket>>::list(&rockets, query); Ok(Json(
Json(json!(post)) 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>")] #[post("/posts", data = "<payload>")]
pub fn create( pub fn create(
auth: Authorization<Write, Post>, auth: Authorization<Write, Post>,
payload: Json<PostEndpoint>, payload: Json<NewPostData>,
mut rockets: PlumeRocket, rockets: PlumeRocket,
) -> Json<serde_json::Value> { ) -> Api<PostData> {
rockets.user = User::get(&*rockets.conn, auth.0.user_id).ok(); let conn = &*rockets.conn;
let new_post = <Post as Provider<PlumeRocket>>::create(&rockets, (*payload).clone()); let search = &rockets.searcher;
Json(new_post.map(|p| json!(p)).unwrap_or_else(|e| { let worker = &rockets.worker;
json!({
"error": "Invalid data, couldn't create new post", let author = User::get(conn, auth.0.user_id)?;
"details": match e {
ApiError::Fetch(msg) => msg, let slug = &payload.title.clone().to_kebab_case();
ApiError::SerDe(msg) => msg, let date = payload.creation_date.clone().and_then(|d| {
ApiError::NotFound(msg) => msg, NaiveDateTime::parse_from_str(format!("{} 00:00:00", d).as_ref(), "%Y-%m-%d %H:%M:%S").ok()
ApiError::Authorization(msg) => msg, });
}
}) 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(()))
}

View file

@ -1,10 +1,9 @@
#![allow(clippy::too_many_arguments)] #![allow(clippy::too_many_arguments)]
#![feature(decl_macro, proc_macro_hygiene)] #![feature(decl_macro, proc_macro_hygiene, try_trait)]
extern crate activitypub; extern crate activitypub;
extern crate askama_escape; extern crate askama_escape;
extern crate atom_syndication; extern crate atom_syndication;
extern crate canapi;
extern crate chrono; extern crate chrono;
extern crate colored; extern crate colored;
extern crate ctrlc; extern crate ctrlc;
@ -102,8 +101,8 @@ Then try to restart Plume.
SearcherError::IndexOpeningError => panic!( SearcherError::IndexOpeningError => panic!(
r#" r#"
Plume was unable to open the search index. If you created the index 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 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 to set SEARCH_INDEX accordingly. If you did not yet create the search
index, run this command: index, run this command:
plm search init plm search init
@ -237,6 +236,7 @@ Then try to restart Plume
api::posts::get, api::posts::get,
api::posts::list, api::posts::list,
api::posts::create, api::posts::create,
api::posts::delete,
], ],
) )
.register(catchers![ .register(catchers![