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;
@ -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![