Rewrite article publication with the REST API
- Add a default App and ApiToken for each user, that is used by the front-end - Add an API route to update an article (CSRF had to be disabled because of a bug in rocket_csrf) - Use AJAX to publish and edit articles in the new editor, instead of weird hacks with HTML forms
This commit is contained in:
parent
4142e73018
commit
cc998e7c61
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1965,6 +1965,8 @@ dependencies = [
|
||||
"gettext-macros 0.4.0 (git+https://github.com/Plume-org/gettext-macros/?rev=a7c605f7edd6bfbfbfe7778026bfefd88d82db10)",
|
||||
"gettext-utils 0.1.0 (git+https://github.com/Plume-org/gettext-macros/?rev=a7c605f7edd6bfbfbfe7778026bfefd88d82db10)",
|
||||
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"plume-api 0.3.0",
|
||||
"serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"stdweb 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"stdweb-internal-runtime 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
DELETE FROM apps WHERE name = 'Plume web interface';
|
35
migrations/postgres/2019-08-03-131154_default_app/up.sql
Normal file
35
migrations/postgres/2019-08-03-131154_default_app/up.sql
Normal file
@ -0,0 +1,35 @@
|
||||
-- Your SQL goes here
|
||||
--#!|conn: &Connection, path: &Path| {
|
||||
--#! use plume_common::utils::random_hex;
|
||||
--#!
|
||||
--#! let client_id = random_hex();
|
||||
--#! let client_secret = random_hex();
|
||||
--#! let app = crate::apps::App::insert(
|
||||
--#! &*conn,
|
||||
--#! crate::apps::NewApp {
|
||||
--#! name: "Plume web interface".into(),
|
||||
--#! client_id,
|
||||
--#! client_secret,
|
||||
--#! redirect_uri: None,
|
||||
--#! website: Some("https://joinplu.me".into()),
|
||||
--#! },
|
||||
--#! ).unwrap();
|
||||
--#!
|
||||
--#! for i in 0..=(crate::users::User::count_local(conn).unwrap() as i32 / 20) {
|
||||
--#! if let Ok(page) = crate::users::User::get_local_page(conn, (i * 20, (i + 1) * 20)) {
|
||||
--#! for user in page {
|
||||
--#! crate::api_tokens::ApiToken::insert(
|
||||
--#! conn,
|
||||
--#! crate::api_tokens::NewApiToken {
|
||||
--#! app_id: app.id,
|
||||
--#! user_id: user.id,
|
||||
--#! value: random_hex(),
|
||||
--#! scopes: "read+write".into(),
|
||||
--#! },
|
||||
--#! ).unwrap();
|
||||
--#! }
|
||||
--#! }
|
||||
--#! }
|
||||
--#!
|
||||
--#! Ok(())
|
||||
--#!}
|
2
migrations/sqlite/2019-08-03-210305_default_app/down.sql
Normal file
2
migrations/sqlite/2019-08-03-210305_default_app/down.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
DELETE FROM apps WHERE name = 'Plume web interface';
|
35
migrations/sqlite/2019-08-03-210305_default_app/up.sql
Normal file
35
migrations/sqlite/2019-08-03-210305_default_app/up.sql
Normal file
@ -0,0 +1,35 @@
|
||||
-- Your SQL goes here
|
||||
--#!|conn: &Connection, path: &Path| {
|
||||
--#! use plume_common::utils::random_hex;
|
||||
--#!
|
||||
--#! let client_id = random_hex();
|
||||
--#! let client_secret = random_hex();
|
||||
--#! let app = crate::apps::App::insert(
|
||||
--#! &*conn,
|
||||
--#! crate::apps::NewApp {
|
||||
--#! name: "Plume web interface".into(),
|
||||
--#! client_id,
|
||||
--#! client_secret,
|
||||
--#! redirect_uri: None,
|
||||
--#! website: Some("https://joinplu.me".into()),
|
||||
--#! },
|
||||
--#! ).unwrap();
|
||||
--#!
|
||||
--#! for i in 0..=(crate::users::User::count_local(conn).unwrap() as i32 / 20) {
|
||||
--#! if let Ok(page) = crate::users::User::get_local_page(conn, (i * 20, (i + 1) * 20)) {
|
||||
--#! for user in page {
|
||||
--#! crate::api_tokens::ApiToken::insert(
|
||||
--#! conn,
|
||||
--#! crate::api_tokens::NewApiToken {
|
||||
--#! app_id: app.id,
|
||||
--#! user_id: user.id,
|
||||
--#! value: random_hex(),
|
||||
--#! scopes: "read+write".into(),
|
||||
--#! },
|
||||
--#! ).unwrap();
|
||||
--#! }
|
||||
--#! }
|
||||
--#! }
|
||||
--#!
|
||||
--#! Ok(())
|
||||
--#!}
|
@ -28,4 +28,5 @@ pub struct PostData {
|
||||
pub license: String,
|
||||
pub tags: Vec<String>,
|
||||
pub cover_id: Option<i32>,
|
||||
pub url: String,
|
||||
}
|
||||
|
@ -10,3 +10,5 @@ gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699f
|
||||
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||
lazy_static = "1.3"
|
||||
plume-api = { path = "../plume-api" }
|
||||
serde_json = "1.0"
|
||||
|
@ -16,17 +16,14 @@ macro_rules! mv {
|
||||
fn get_elt_value(id: &'static str) -> String {
|
||||
let elt = document().get_element_by_id(id).unwrap();
|
||||
let inp: Result<InputElement, _> = elt.clone().try_into();
|
||||
let select: Result<SelectElement, _> = elt.clone().try_into();
|
||||
let textarea: Result<TextAreaElement, _> = elt.try_into();
|
||||
inp.map(|i| i.raw_value())
|
||||
.unwrap_or_else(|_| textarea.unwrap().value())
|
||||
}
|
||||
|
||||
fn set_value<S: AsRef<str>>(id: &'static str, val: S) {
|
||||
let elt = document().get_element_by_id(id).unwrap();
|
||||
let inp: Result<InputElement, _> = elt.clone().try_into();
|
||||
let textarea: Result<TextAreaElement, _> = elt.try_into();
|
||||
inp.map(|i| i.set_raw_value(val.as_ref()))
|
||||
.unwrap_or_else(|_| textarea.unwrap().set_value(val.as_ref()))
|
||||
let res = inp.map(|i| i.raw_value()).unwrap_or_else(|_| {
|
||||
textarea
|
||||
.map(|t| t.value())
|
||||
.unwrap_or_else(|_| select.unwrap().value().unwrap_or_default())
|
||||
});
|
||||
res
|
||||
}
|
||||
|
||||
fn no_return(evt: KeyDownEvent) {
|
||||
@ -163,12 +160,102 @@ fn init_editor() -> Result<(), EditorError> {
|
||||
};
|
||||
});
|
||||
|
||||
document()
|
||||
.get_element_by_id("confirm-publish")?
|
||||
.add_event_listener(|_: ClickEvent| {
|
||||
save(false);
|
||||
});
|
||||
|
||||
document()
|
||||
.get_element_by_id("save-draft")?
|
||||
.add_event_listener(|_: ClickEvent| {
|
||||
save(true);
|
||||
});
|
||||
|
||||
show_errors();
|
||||
setup_close_button();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save(is_draft: bool) {
|
||||
let req = XmlHttpRequest::new();
|
||||
if bool::try_from(js! { return window.editing }).unwrap_or(false) {
|
||||
req.open(
|
||||
"PUT",
|
||||
&format!(
|
||||
"/api/v1/posts/{}",
|
||||
i32::try_from(js! { return window.post_id }).unwrap()
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
req.open("POST", "/api/v1/posts").unwrap();
|
||||
}
|
||||
req.set_request_header("Accept", "application/json")
|
||||
.unwrap();
|
||||
req.set_request_header("Content-Type", "application/json")
|
||||
.unwrap();
|
||||
req.set_request_header(
|
||||
"Authorization",
|
||||
&format!(
|
||||
"Bearer {}",
|
||||
String::try_from(js! { return window.api_token }).unwrap()
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
let req_clone = req.clone();
|
||||
req.add_event_listener(move |_: ProgressLoadEvent| {
|
||||
if let Ok(Some(res)) = req_clone.response_text() {
|
||||
serde_json::from_str(&res)
|
||||
.map(|res: plume_api::posts::PostData| {
|
||||
let url = res.url;
|
||||
js! {
|
||||
window.location.href = @{url};
|
||||
};
|
||||
})
|
||||
.map_err(|_| {
|
||||
let json: serde_json::Value = serde_json::from_str(&res).unwrap();
|
||||
window().alert(&format!(
|
||||
"Error: {}",
|
||||
json["error"].as_str().unwrap_or_default()
|
||||
));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
let data = plume_api::posts::NewPostData {
|
||||
title: HtmlElement::try_from(document().get_element_by_id("editor-title").unwrap())
|
||||
.unwrap()
|
||||
.inner_text(),
|
||||
subtitle: document()
|
||||
.get_element_by_id("editor-subtitle")
|
||||
.map(|s| HtmlElement::try_from(s).unwrap().inner_text()),
|
||||
source: HtmlElement::try_from(
|
||||
document()
|
||||
.get_element_by_id("editor-default-paragraph")
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
.inner_text(),
|
||||
author: String::new(), // it is ignored anyway (TODO: remove it ??)
|
||||
blog_id: i32::try_from(js! { return window.blog_id }).ok(),
|
||||
published: Some(!is_draft),
|
||||
creation_date: None,
|
||||
license: Some(get_elt_value("license")),
|
||||
tags: Some(
|
||||
get_elt_value("tags")
|
||||
.split(',')
|
||||
.map(|t| t.trim().to_string())
|
||||
.filter(|t| !t.is_empty())
|
||||
.collect(),
|
||||
),
|
||||
cover_id: get_elt_value("cover").parse().ok(),
|
||||
};
|
||||
let json = serde_json::to_string(&data).unwrap();
|
||||
req.send_with_string(&json).unwrap();
|
||||
}
|
||||
|
||||
fn setup_close_button() {
|
||||
if let Some(button) = document().get_element_by_id("close-editor") {
|
||||
button.add_event_listener(|_: ClickEvent| {
|
||||
@ -201,108 +288,6 @@ fn show_errors() {
|
||||
}
|
||||
}
|
||||
|
||||
fn init_popup(
|
||||
title: &HtmlElement,
|
||||
subtitle: &HtmlElement,
|
||||
content: &HtmlElement,
|
||||
old_ed: &Element,
|
||||
) -> Result<Element, EditorError> {
|
||||
let popup = document().create_element("div")?;
|
||||
popup.class_list().add("popup")?;
|
||||
popup.set_attribute("id", "publish-popup")?;
|
||||
|
||||
let tags = get_elt_value("tags")
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
let license = get_elt_value("license");
|
||||
make_input(&i18n!(CATALOG, "Tags"), "popup-tags", &popup).set_raw_value(&tags.join(", "));
|
||||
make_input(&i18n!(CATALOG, "License"), "popup-license", &popup).set_raw_value(&license);
|
||||
|
||||
let cover_label = document().create_element("label")?;
|
||||
cover_label.append_child(&document().create_text_node(&i18n!(CATALOG, "Cover")));
|
||||
cover_label.set_attribute("for", "cover")?;
|
||||
let cover = document().get_element_by_id("cover")?;
|
||||
cover.parent_element()?.remove_child(&cover).ok();
|
||||
popup.append_child(&cover_label);
|
||||
popup.append_child(&cover);
|
||||
|
||||
if let Some(draft_checkbox) = document().get_element_by_id("draft") {
|
||||
let draft_label = document().create_element("label")?;
|
||||
draft_label.set_attribute("for", "popup-draft")?;
|
||||
|
||||
let draft = document().create_element("input").unwrap();
|
||||
js! {
|
||||
@{&draft}.id = "popup-draft";
|
||||
@{&draft}.name = "popup-draft";
|
||||
@{&draft}.type = "checkbox";
|
||||
@{&draft}.checked = @{&draft_checkbox}.checked;
|
||||
};
|
||||
|
||||
draft_label.append_child(&draft);
|
||||
draft_label.append_child(&document().create_text_node(&i18n!(CATALOG, "This is a draft")));
|
||||
popup.append_child(&draft_label);
|
||||
}
|
||||
|
||||
let button = document().create_element("input")?;
|
||||
js! {
|
||||
@{&button}.type = "submit";
|
||||
@{&button}.value = @{i18n!(CATALOG, "Publish")};
|
||||
};
|
||||
button.append_child(&document().create_text_node(&i18n!(CATALOG, "Publish")));
|
||||
button.add_event_listener(
|
||||
mv!(title, subtitle, content, old_ed => move |_: ClickEvent| {
|
||||
title.focus(); // Remove the placeholder before publishing
|
||||
set_value("title", title.inner_text());
|
||||
subtitle.focus();
|
||||
set_value("subtitle", subtitle.inner_text());
|
||||
content.focus();
|
||||
set_value("editor-content", content.child_nodes().iter().fold(String::new(), |md, ch| {
|
||||
let to_append = match ch.node_type() {
|
||||
NodeType::Element => {
|
||||
if js!{ return @{&ch}.tagName; } == "DIV" {
|
||||
(js!{ return @{&ch}.innerHTML; }).try_into().unwrap_or_default()
|
||||
} else {
|
||||
(js!{ return @{&ch}.outerHTML; }).try_into().unwrap_or_default()
|
||||
}
|
||||
},
|
||||
NodeType::Text => ch.node_value().unwrap_or_default(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
format!("{}\n\n{}", md, to_append)
|
||||
}));
|
||||
set_value("tags", get_elt_value("popup-tags"));
|
||||
if let Some(draft) = document().get_element_by_id("popup-draft") {
|
||||
js!{
|
||||
document.getElementById("draft").checked = @{draft}.checked;
|
||||
};
|
||||
}
|
||||
let cover = document().get_element_by_id("cover").unwrap();
|
||||
cover.parent_element().unwrap().remove_child(&cover).ok();
|
||||
old_ed.append_child(&cover);
|
||||
set_value("license", get_elt_value("popup-license"));
|
||||
js! {
|
||||
@{&old_ed}.submit();
|
||||
};
|
||||
}),
|
||||
);
|
||||
popup.append_child(&button);
|
||||
|
||||
document().body()?.append_child(&popup);
|
||||
Ok(popup)
|
||||
}
|
||||
|
||||
fn init_popup_bg() -> Result<Element, EditorError> {
|
||||
let bg = document().create_element("div")?;
|
||||
bg.class_list().add("popup-bg")?;
|
||||
bg.set_attribute("id", "popup-bg")?;
|
||||
|
||||
document().body()?.append_child(&bg);
|
||||
bg.add_event_listener(|_: ClickEvent| close_popup());
|
||||
Ok(bg)
|
||||
}
|
||||
|
||||
fn chars_left(selector: &str, content: &Element) -> Option<i32> {
|
||||
match document().query_selector(selector) {
|
||||
Ok(Some(form)) => HtmlElement::try_from(form).ok().and_then(|form| {
|
||||
@ -329,42 +314,3 @@ fn chars_left(selector: &str, content: &Element) -> Option<i32> {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn close_popup() {
|
||||
let hide = |x: Element| x.class_list().remove("show");
|
||||
document().get_element_by_id("publish-popup").map(hide);
|
||||
document().get_element_by_id("popup-bg").map(hide);
|
||||
}
|
||||
|
||||
fn make_input(label_text: &str, name: &'static str, form: &Element) -> InputElement {
|
||||
let label = document().create_element("label").unwrap();
|
||||
label.append_child(&document().create_text_node(label_text));
|
||||
label.set_attribute("for", name).unwrap();
|
||||
|
||||
let inp: InputElement = document()
|
||||
.create_element("input")
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
inp.set_attribute("name", name).unwrap();
|
||||
inp.set_attribute("id", name).unwrap();
|
||||
|
||||
form.append_child(&label);
|
||||
form.append_child(&inp);
|
||||
inp
|
||||
}
|
||||
|
||||
fn make_editable(tag: &'static str) -> Element {
|
||||
let elt = document()
|
||||
.create_element(tag)
|
||||
.expect("Couldn't create editable element");
|
||||
elt.set_attribute("contenteditable", "true")
|
||||
.expect("Couldn't make the element editable");
|
||||
elt
|
||||
}
|
||||
|
||||
fn clear_children(elt: &HtmlElement) {
|
||||
for child in elt.child_nodes() {
|
||||
elt.remove_child(&child).unwrap();
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ extern crate gettext_macros;
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate stdweb;
|
||||
extern crate serde_json;
|
||||
|
||||
use stdweb::web::{event::*, *};
|
||||
|
||||
|
@ -44,6 +44,18 @@ impl ApiToken {
|
||||
get!(api_tokens);
|
||||
insert!(api_tokens, NewApiToken);
|
||||
find_by!(api_tokens, find_by_value, value as &str);
|
||||
find_by!(
|
||||
api_tokens,
|
||||
find_by_app_and_user,
|
||||
app_id as i32,
|
||||
user_id as i32
|
||||
);
|
||||
|
||||
/// The token for Plume's front-end
|
||||
pub fn web_token(conn: &crate::Connection, user_id: i32) -> Result<ApiToken> {
|
||||
let app = crate::apps::App::find_by_name(conn, "Plume web interface")?;
|
||||
Self::find_by_app_and_user(conn, app.id, user_id)
|
||||
}
|
||||
|
||||
pub fn can(&self, what: &'static str, scope: &'static str) -> bool {
|
||||
let full_scope = what.to_owned() + ":" + scope;
|
||||
|
@ -29,4 +29,5 @@ impl App {
|
||||
get!(apps);
|
||||
insert!(apps, NewApp);
|
||||
find_by!(apps, find_by_client_id, client_id as &str);
|
||||
find_by!(apps, find_by_name, name as &str);
|
||||
}
|
||||
|
@ -64,6 +64,7 @@ pub enum Error {
|
||||
Signature,
|
||||
Unauthorized,
|
||||
Url,
|
||||
Validation(String),
|
||||
Webfinger,
|
||||
Expired,
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ impl<'r> Responder<'r> for ApiError {
|
||||
"error": "You are not authorized to access this resource"
|
||||
}))
|
||||
.respond_to(req),
|
||||
Error::Validation(msg) => Json(json!({ "error": msg })).respond_to(req),
|
||||
_ => Json(json!({
|
||||
"error": "Server error"
|
||||
}))
|
||||
|
157
src/api/posts.rs
157
src/api/posts.rs
@ -1,6 +1,7 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use heck::{CamelCase, KebabCase};
|
||||
use rocket_contrib::json::Json;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::api::{authorization::*, Api};
|
||||
use plume_api::posts::*;
|
||||
@ -44,6 +45,7 @@ pub fn get(id: i32, auth: Option<Authorization<Read, Post>>, conn: DbConn) -> Ap
|
||||
published: post.published,
|
||||
license: post.license,
|
||||
cover_id: post.cover_id,
|
||||
url: post.ap_url,
|
||||
}))
|
||||
}
|
||||
|
||||
@ -91,6 +93,7 @@ pub fn list(
|
||||
published: p.published,
|
||||
license: p.license,
|
||||
cover_id: p.cover_id,
|
||||
url: p.ap_url,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
@ -114,6 +117,20 @@ pub fn create(
|
||||
NaiveDateTime::parse_from_str(format!("{} 00:00:00", d).as_ref(), "%Y-%m-%d %H:%M:%S").ok()
|
||||
});
|
||||
|
||||
if slug.as_str() == "new" {
|
||||
return Err(
|
||||
Error::Validation("Sorry, but your article can't have this title.".into()).into(),
|
||||
);
|
||||
}
|
||||
|
||||
if payload.title.is_empty() {
|
||||
return Err(Error::Validation("You have to give your article a title.".into()).into());
|
||||
}
|
||||
|
||||
if payload.source.is_empty() {
|
||||
return Err(Error::Validation("Your article can't be empty.".into()).into());
|
||||
}
|
||||
|
||||
let domain = &Instance::get_local()?.public_domain;
|
||||
let (content, mentions, hashtags) = md_to_html(
|
||||
&payload.source,
|
||||
@ -131,6 +148,10 @@ pub fn create(
|
||||
}
|
||||
})?;
|
||||
|
||||
if !author.is_author_in(conn, &Blog::get(conn, blog)?)? {
|
||||
return Err(Error::Unauthorized.into());
|
||||
}
|
||||
|
||||
if Post::find_by_slug(conn, slug, blog).is_ok() {
|
||||
return Err(Error::InvalidValue.into());
|
||||
}
|
||||
@ -166,11 +187,19 @@ pub fn create(
|
||||
)?;
|
||||
|
||||
if let Some(ref tags) = payload.tags {
|
||||
let tags = tags
|
||||
.iter()
|
||||
.map(|t| t.to_camel_case())
|
||||
.filter(|t| !t.is_empty())
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.filter_map(|t| Tag::build_activity(t).ok());
|
||||
|
||||
for tag in tags {
|
||||
Tag::insert(
|
||||
conn,
|
||||
NewTag {
|
||||
tag: tag.to_string(),
|
||||
tag: tag.name_string().unwrap(),
|
||||
is_hashtag: false,
|
||||
post_id: post.id,
|
||||
},
|
||||
@ -211,7 +240,6 @@ pub fn create(
|
||||
.into_iter()
|
||||
.map(|t| t.tag)
|
||||
.collect(),
|
||||
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
subtitle: post.subtitle,
|
||||
@ -221,9 +249,132 @@ pub fn create(
|
||||
published: post.published,
|
||||
license: post.license,
|
||||
cover_id: post.cover_id,
|
||||
url: post.ap_url,
|
||||
}))
|
||||
}
|
||||
|
||||
#[put("/posts/<id>", data = "<payload>")]
|
||||
pub fn update(
|
||||
id: i32,
|
||||
auth: Authorization<Write, Post>,
|
||||
payload: Json<NewPostData>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Api<PostData> {
|
||||
let conn = &*rockets.conn;
|
||||
let mut post = Post::get(&*conn, id)?;
|
||||
let author = User::get(conn, auth.0.user_id)?;
|
||||
let b = post.get_blog(&*conn)?;
|
||||
|
||||
let new_slug = if !post.published {
|
||||
payload.title.to_string().to_kebab_case()
|
||||
} else {
|
||||
post.slug.clone()
|
||||
};
|
||||
|
||||
if new_slug != post.slug && Post::find_by_slug(&*conn, &new_slug, b.id).is_ok() {
|
||||
return Err(Error::Validation("A post with the same title already exists.".into()).into());
|
||||
}
|
||||
|
||||
if !author.is_author_in(&*conn, &b)? {
|
||||
Err(Error::Unauthorized.into())
|
||||
} else {
|
||||
let (content, mentions, hashtags) = md_to_html(
|
||||
&payload.source,
|
||||
Some(&Instance::get_local()?.public_domain),
|
||||
false,
|
||||
Some(Media::get_media_processor(
|
||||
&conn,
|
||||
b.list_authors(&conn)?.iter().collect(),
|
||||
)),
|
||||
);
|
||||
|
||||
// update publication date if when this article is no longer a draft
|
||||
let newly_published = if !post.published && payload.published.unwrap_or(post.published) {
|
||||
post.published = true;
|
||||
post.creation_date = Utc::now().naive_utc();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
post.slug = new_slug.clone();
|
||||
post.title = payload.title.clone();
|
||||
post.subtitle = payload.subtitle.clone().unwrap_or_default();
|
||||
post.content = SafeString::new(&content);
|
||||
post.source = payload.source.clone();
|
||||
post.license = payload.license.clone().unwrap_or_default();
|
||||
post.cover_id = payload.cover_id;
|
||||
post.update(&*conn, &rockets.searcher)?;
|
||||
|
||||
if post.published {
|
||||
post.update_mentions(
|
||||
&conn,
|
||||
mentions
|
||||
.into_iter()
|
||||
.filter_map(|m| Mention::build_activity(&rockets, &m).ok())
|
||||
.collect(),
|
||||
)?;
|
||||
}
|
||||
|
||||
let tags = payload
|
||||
.tags
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|t| t.trim().to_camel_case())
|
||||
.filter(|t| !t.is_empty())
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.filter_map(|t| Tag::build_activity(t).ok())
|
||||
.collect::<Vec<_>>();
|
||||
post.update_tags(&conn, tags)?;
|
||||
|
||||
let hashtags = hashtags
|
||||
.into_iter()
|
||||
.map(|h| h.to_camel_case())
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.filter_map(|t| Tag::build_activity(t).ok())
|
||||
.collect::<Vec<_>>();
|
||||
post.update_hashtags(&conn, hashtags)?;
|
||||
|
||||
if post.published {
|
||||
if newly_published {
|
||||
let act = post.create_activity(&conn)?;
|
||||
let dest = User::one_by_instance(&*conn)?;
|
||||
rockets
|
||||
.worker
|
||||
.execute(move || broadcast(&author, act, dest));
|
||||
} else {
|
||||
let act = post.update_activity(&*conn)?;
|
||||
let dest = User::one_by_instance(&*conn)?;
|
||||
rockets
|
||||
.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,
|
||||
url: post.ap_url,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[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)?;
|
||||
|
@ -275,6 +275,7 @@ Then try to restart Plume
|
||||
api::posts::get,
|
||||
api::posts::list,
|
||||
api::posts::create,
|
||||
api::posts::update,
|
||||
api::posts::delete,
|
||||
],
|
||||
)
|
||||
@ -289,7 +290,7 @@ Then try to restart Plume
|
||||
.manage(Arc::new(workpool))
|
||||
.manage(searcher)
|
||||
.manage(include_i18n!())
|
||||
.attach(
|
||||
/*.attach(
|
||||
CsrfFairingBuilder::new()
|
||||
.set_default_target(
|
||||
"/csrf-violation?target=<uri>".to_owned(),
|
||||
@ -314,7 +315,7 @@ Then try to restart Plume
|
||||
])
|
||||
.finalize()
|
||||
.expect("main: csrf fairing creation error"),
|
||||
);
|
||||
)*/;
|
||||
|
||||
#[cfg(feature = "test")]
|
||||
let rocket = rocket.mount("/test", routes![test_routes::health,]);
|
||||
|
@ -13,6 +13,7 @@ use validator::{Validate, ValidationError, ValidationErrors};
|
||||
use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest};
|
||||
use plume_common::utils;
|
||||
use plume_models::{
|
||||
api_tokens::ApiToken,
|
||||
blogs::*,
|
||||
comments::{Comment, CommentTree},
|
||||
inbox::inbox,
|
||||
@ -156,7 +157,8 @@ pub fn new(blog: String, cl: ContentLen, rockets: PlumeRocket) -> Result<Ructe,
|
||||
None,
|
||||
ValidationErrors::default(),
|
||||
medias,
|
||||
cl.0
|
||||
cl.0,
|
||||
ApiToken::web_token(&*conn, user.id)?.value
|
||||
)))
|
||||
}
|
||||
|
||||
@ -210,7 +212,8 @@ pub fn edit(
|
||||
Some(post),
|
||||
ValidationErrors::default(),
|
||||
medias,
|
||||
cl.0
|
||||
cl.0,
|
||||
ApiToken::web_token(&*conn, user.id)?.value
|
||||
)))
|
||||
}
|
||||
|
||||
@ -366,7 +369,10 @@ pub fn update(
|
||||
Some(post),
|
||||
errors.clone(),
|
||||
medias.clone(),
|
||||
cl.0
|
||||
cl.0,
|
||||
ApiToken::web_token(&*conn, user.id)
|
||||
.expect("The default API token cannot be retrieved")
|
||||
.value
|
||||
))
|
||||
.into()
|
||||
}
|
||||
@ -550,7 +556,8 @@ pub fn create(
|
||||
None,
|
||||
errors.clone(),
|
||||
medias,
|
||||
cl.0
|
||||
cl.0,
|
||||
ApiToken::web_token(&*conn, user.id)?.value
|
||||
))
|
||||
.into())
|
||||
}
|
||||
|
@ -9,9 +9,19 @@
|
||||
@use routes::posts::NewPostForm;
|
||||
@use routes::*;
|
||||
|
||||
@(ctx: BaseContext, title: String, blog: Blog, editing: bool, form: &NewPostForm, is_draft: bool, article: Option<Post>, errors: ValidationErrors, medias: Vec<Media>, content_len: u64)
|
||||
@(ctx: BaseContext, title: String, blog: Blog, editing: bool, form: &NewPostForm, is_draft: bool, article: Option<Post>, errors: ValidationErrors, medias: Vec<Media>, content_len: u64, api_token: String)
|
||||
|
||||
@:base(ctx, title.clone(), {}, {}, {
|
||||
<script>
|
||||
window.blog_id = @blog.id
|
||||
window.api_token = '@api_token'
|
||||
@if editing {
|
||||
window.editing = true
|
||||
window.post_id = @article.clone().unwrap().id
|
||||
} else {
|
||||
window.editing = false
|
||||
}
|
||||
</script>
|
||||
<div id="plume-editor" style="display: none;" dir="auto">
|
||||
<header>
|
||||
<a href="#" id="close-editor">@i18n!(ctx.1, "Classic editor (any changes will be lost)")</a>
|
||||
@ -34,7 +44,7 @@
|
||||
<a href="#" id="cancel-publish">@i18n!(ctx.1, "Cancel")</a>
|
||||
<p>@i18n!(ctx.1, "You are about to publish this article in {}"; &blog.title)</p>
|
||||
<button id="confirm-publish" class="button">@i18n!(ctx.1, "Confirm publication")</button>
|
||||
<button id="draft" class="button secondary">@i18n!(ctx.1, "Save as draft")</button>
|
||||
<button id="save-draft" class="button secondary">@i18n!(ctx.1, "Save as draft")</button>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user