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:
parent
787eb7f399
commit
ec57f1e687
14 changed files with 298 additions and 435 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -4,6 +4,5 @@ version = "0.3.0"
|
|||
authors = ["Plume contributors"]
|
||||
|
||||
[dependencies]
|
||||
canapi = "0.2"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
|
|
|
@ -1,13 +1,6 @@
|
|||
use canapi::Endpoint;
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
pub struct AppEndpoint {
|
||||
pub id: Option<i32>,
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct NewAppData {
|
||||
pub name: String,
|
||||
pub website: Option<String>,
|
||||
pub redirect_uri: Option<String>,
|
||||
pub client_id: Option<String>,
|
||||
pub client_secret: Option<String>,
|
||||
}
|
||||
|
||||
api!("/api/v1/apps" => AppEndpoint);
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
use canapi::Endpoint;
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
pub struct PostEndpoint {
|
||||
pub id: Option<i32>,
|
||||
pub title: Option<String>,
|
||||
pub struct NewPostData {
|
||||
pub title: String,
|
||||
pub subtitle: Option<String>,
|
||||
pub content: Option<String>,
|
||||
pub source: Option<String>,
|
||||
pub author: Option<String>,
|
||||
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<i32>,
|
||||
pub published: Option<bool>,
|
||||
pub creation_date: Option<String>,
|
||||
|
@ -16,4 +14,18 @@ pub struct PostEndpoint {
|
|||
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>,
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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<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 {
|
||||
get!(apps);
|
||||
insert!(apps, NewApp);
|
||||
|
|
|
@ -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<InboxError<Error>> for 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
|
||||
/// matching row for a given list of fields.
|
||||
///
|
||||
|
|
|
@ -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<Licensed, Article>;
|
||||
|
||||
|
@ -67,282 +64,6 @@ pub struct NewPost {
|
|||
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 {
|
||||
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<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>> {
|
||||
posts::table
|
||||
.order(posts::creation_date.desc())
|
||||
|
|
|
@ -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 = "<data>")]
|
||||
pub fn create(conn: DbConn, data: Json<AppEndpoint>) -> Json<serde_json::Value> {
|
||||
let post = <App as Provider<Connection>>::create(&*conn, (*data).clone()).ok();
|
||||
Json(json!(post))
|
||||
pub fn create(conn: DbConn, data: Json<NewAppData>) -> Api<App> {
|
||||
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))
|
||||
}
|
||||
|
|
|
@ -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<T> = Result<Json<T>, ApiError>;
|
||||
|
||||
#[derive(Debug)]
|
||||
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 {
|
||||
fn respond_to(self, req: &Request) -> response::Result<'r> {
|
||||
match self.0 {
|
||||
|
|
260
src/api/posts.rs
260
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/<id>")]
|
||||
pub fn get(
|
||||
id: i32,
|
||||
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 post = <Post as Provider<PlumeRocket>>::get(&rockets, id).ok();
|
||||
Json(json!(post))
|
||||
pub fn get(id: i32, auth: Option<Authorization<Read, Post>>, conn: DbConn) -> Api<PostData> {
|
||||
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?<title>&<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(()))
|
||||
}
|
||||
|
|
|
@ -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![
|
||||
|
|
Loading…
Reference in a new issue