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:
@@ -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.
|
||||
///
|
||||
|
||||
+21
-280
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user