Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39edca5edc | |||
| bce806ac63 | |||
| 3669a0097d | |||
| cc998e7c61 | |||
| 4142e73018 | |||
| 5d03331f0c | |||
| 3198f30515 |
Generated
+158
-353
File diff suppressed because it is too large
Load Diff
+6
-7
@@ -18,17 +18,16 @@ guid-create = "0.1"
|
|||||||
heck = "0.3.0"
|
heck = "0.3.0"
|
||||||
lettre = { git = "https://github.com/lettre/lettre", rev = "c988b1760ad8179d9e7f3fb8594d2b86cf2a0a49" }
|
lettre = { git = "https://github.com/lettre/lettre", rev = "c988b1760ad8179d9e7f3fb8594d2b86cf2a0a49" }
|
||||||
lettre_email = { git = "https://github.com/lettre/lettre", rev = "c988b1760ad8179d9e7f3fb8594d2b86cf2a0a49" }
|
lettre_email = { git = "https://github.com/lettre/lettre", rev = "c988b1760ad8179d9e7f3fb8594d2b86cf2a0a49" }
|
||||||
num_cpus = "1.10"
|
num_cpus = "1.0"
|
||||||
reqwest = "0.9"
|
|
||||||
rocket = "0.4.0"
|
rocket = "0.4.0"
|
||||||
rocket_contrib = { version = "0.4.0", features = ["json"] }
|
rocket_contrib = { version = "0.4.0", features = ["json"] }
|
||||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||||
rpassword = "4.0"
|
rpassword = "3.0"
|
||||||
runtime-fmt = "0.3.0"
|
runtime-fmt = "0.3.0"
|
||||||
scheduled-thread-pool = "0.2.2"
|
scheduled-thread-pool = "0.2.0"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_qs = "0.5"
|
serde_qs = "0.4"
|
||||||
shrinkwraprs = "0.2.1"
|
shrinkwraprs = "0.2.1"
|
||||||
validator = "0.8"
|
validator = "0.8"
|
||||||
validator_derive = "0.8"
|
validator_derive = "0.8"
|
||||||
@@ -65,8 +64,8 @@ path = "plume-common"
|
|||||||
path = "plume-models"
|
path = "plume-models"
|
||||||
|
|
||||||
[dependencies.rocket_csrf]
|
[dependencies.rocket_csrf]
|
||||||
git = "https://github.com/fdb-hiroshima/rocket_csrf"
|
git = "https://github.com/Plume-org/rocket_csrf"
|
||||||
rev = "4a72ea2ec716cb0b26188fb00bccf2ef7d1e031c"
|
rev = "89ecb380266234f858c651354216bf5bf3cc09b2"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
ructe = "0.6.2"
|
ructe = "0.6.2"
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
-- undo the adding of custom_domain column to blogs table.
|
|
||||||
ALTER TABLE blogs DROP COLUMN custom_domain;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
--- Adding custom domain to Blog as an optional field
|
|
||||||
ALTER TABLE blogs ADD COLUMN custom_domain VARCHAR DEFAULT NULL UNIQUE;
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
DELETE FROM apps WHERE name = 'Plume web interface';
|
||||||
@@ -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(())
|
||||||
|
--#!}
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
-- undo the adding of "custom_domain" to blogs
|
|
||||||
CREATE TABLE IF NOT EXISTS "blogs_drop_custom_domain" (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
actor_id VARCHAR NOT NULL,
|
|
||||||
title VARCHAR NOT NULL,
|
|
||||||
summary TEXT NOT NULL DEFAULT '',
|
|
||||||
outbox_url VARCHAR NOT NULL UNIQUE,
|
|
||||||
inbox_url VARCHAR NOT NULL UNIQUE,
|
|
||||||
instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL,
|
|
||||||
creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
ap_url text not null default '' UNIQUE,
|
|
||||||
private_key TEXT,
|
|
||||||
public_key TEXT NOT NULL DEFAULT '',
|
|
||||||
fqn TEXT NOT NULL DEFAULT '',
|
|
||||||
summary_html TEXT NOT NULL DEFAULT '',
|
|
||||||
icon_id INTEGER REFERENCES medias(id) ON DELETE SET NULL DEFAULT NULL,
|
|
||||||
banner_id INTEGER REFERENCES medias(id) ON DELETE SET NULL DEFAULT NULL,
|
|
||||||
CONSTRAINT blog_unique UNIQUE (actor_id, instance_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO blogs_drop_custom_domain (
|
|
||||||
id,
|
|
||||||
actor_id,
|
|
||||||
title,
|
|
||||||
summary,
|
|
||||||
outbox_url,
|
|
||||||
inbox_url,
|
|
||||||
instance_id,
|
|
||||||
creation_date,
|
|
||||||
ap_url,
|
|
||||||
private_key,
|
|
||||||
public_key,
|
|
||||||
fqn,
|
|
||||||
summary_html,
|
|
||||||
icon_id,
|
|
||||||
banner_id
|
|
||||||
) SELECT
|
|
||||||
id,
|
|
||||||
actor_id,
|
|
||||||
title,
|
|
||||||
summary,
|
|
||||||
outbox_url,
|
|
||||||
inbox_url,
|
|
||||||
instance_id,
|
|
||||||
creation_date,
|
|
||||||
ap_url,
|
|
||||||
private_key,
|
|
||||||
public_key,
|
|
||||||
fqn,
|
|
||||||
summary_html,
|
|
||||||
icon_id,
|
|
||||||
banner_id
|
|
||||||
FROM blogs;
|
|
||||||
|
|
||||||
DROP TABLE blogs;
|
|
||||||
ALTER TABLE "blogs_drop_custom_domain" RENAME to blogs;
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
-- add custom_domain to blogs
|
|
||||||
CREATE TABLE IF NOT EXISTS "blogs_add_custom_domain" (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
actor_id VARCHAR NOT NULL,
|
|
||||||
title VARCHAR NOT NULL,
|
|
||||||
summary TEXT NOT NULL DEFAULT '',
|
|
||||||
outbox_url VARCHAR NOT NULL UNIQUE,
|
|
||||||
inbox_url VARCHAR NOT NULL UNIQUE,
|
|
||||||
instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL,
|
|
||||||
creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
ap_url text not null default '' UNIQUE,
|
|
||||||
private_key TEXT,
|
|
||||||
public_key TEXT NOT NULL DEFAULT '',
|
|
||||||
fqn TEXT NOT NULL DEFAULT '',
|
|
||||||
summary_html TEXT NOT NULL DEFAULT '',
|
|
||||||
icon_id INTEGER REFERENCES medias(id) ON DELETE SET NULL DEFAULT NULL,
|
|
||||||
banner_id INTEGER REFERENCES medias(id) ON DELETE SET NULL DEFAULT NULL,
|
|
||||||
custom_domain text default NULL UNIQUE,
|
|
||||||
CONSTRAINT blog_unique UNIQUE (actor_id, instance_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO blogs_add_custom_domain (
|
|
||||||
id,
|
|
||||||
actor_id,
|
|
||||||
title,
|
|
||||||
summary,
|
|
||||||
outbox_url,
|
|
||||||
inbox_url,
|
|
||||||
instance_id,
|
|
||||||
creation_date,
|
|
||||||
ap_url,
|
|
||||||
private_key,
|
|
||||||
public_key,
|
|
||||||
fqn,
|
|
||||||
summary_html,
|
|
||||||
icon_id,
|
|
||||||
banner_id
|
|
||||||
) SELECT
|
|
||||||
id,
|
|
||||||
actor_id,
|
|
||||||
title,
|
|
||||||
summary,
|
|
||||||
outbox_url,
|
|
||||||
inbox_url,
|
|
||||||
instance_id,
|
|
||||||
creation_date,
|
|
||||||
ap_url,
|
|
||||||
private_key,
|
|
||||||
public_key,
|
|
||||||
fqn,
|
|
||||||
summary_html,
|
|
||||||
icon_id,
|
|
||||||
banner_id
|
|
||||||
FROM blogs;
|
|
||||||
|
|
||||||
DROP TABLE blogs;
|
|
||||||
ALTER TABLE "blogs_add_custom_domain" RENAME to blogs;
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
DELETE FROM apps WHERE name = 'Plume web interface';
|
||||||
@@ -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 license: String,
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
pub cover_id: Option<i32>,
|
pub cover_id: Option<i32>,
|
||||||
|
pub url: String,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ path = "src/main.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
clap = "2.33"
|
clap = "2.33"
|
||||||
dotenv = "0.14"
|
dotenv = "0.14"
|
||||||
rpassword = "4.0"
|
rpassword = "3.0"
|
||||||
|
|
||||||
[dependencies.diesel]
|
[dependencies.diesel]
|
||||||
features = ["r2d2", "chrono"]
|
features = ["r2d2", "chrono"]
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ authors = ["Plume contributors"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
activitypub = "0.1.1"
|
activitypub = "0.1.1"
|
||||||
activitystreams-derive = "0.1.1"
|
activitystreams-derive = "0.1.0"
|
||||||
activitystreams-traits = "0.1.0"
|
activitystreams-traits = "0.1.0"
|
||||||
array_tool = "1.0"
|
array_tool = "1.0"
|
||||||
base64 = "0.10"
|
base64 = "0.10"
|
||||||
heck = "0.3.0"
|
heck = "0.3.0"
|
||||||
hex = "0.3"
|
hex = "0.3"
|
||||||
hyper = "0.12.33"
|
hyper = "0.12.28"
|
||||||
openssl = "0.10.22"
|
openssl = "0.10.22"
|
||||||
rocket = "0.4.0"
|
rocket = "0.4.0"
|
||||||
reqwest = "0.9"
|
reqwest = "0.9"
|
||||||
@@ -19,7 +19,7 @@ serde = "1.0"
|
|||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
shrinkwraprs = "0.2.1"
|
shrinkwraprs = "0.2.1"
|
||||||
tokio = "0.1.22"
|
tokio = "0.1.21"
|
||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
|
|||||||
@@ -4,9 +4,15 @@ version = "0.3.0"
|
|||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
stdweb = "=0.4.18"
|
stdweb = "=0.4.14"
|
||||||
stdweb-internal-runtime = "=0.1.4"
|
stdweb-internal-runtime = "=0.1.3"
|
||||||
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
||||||
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||||
gettext-utils = { 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"
|
lazy_static = "1.3"
|
||||||
|
plume-api = { path = "../plume-api" }
|
||||||
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
[dependencies.pulldown-cmark]
|
||||||
|
default-features = false
|
||||||
|
version = "0.5"
|
||||||
|
|||||||
+417
-285
@@ -1,32 +1,212 @@
|
|||||||
|
use pulldown_cmark::{Event, Options, Parser, Tag};
|
||||||
use stdweb::{
|
use stdweb::{
|
||||||
unstable::{TryFrom, TryInto},
|
unstable::{TryFrom, TryInto},
|
||||||
web::{event::*, html_element::*, *},
|
web::{event::*, html_element::*, *},
|
||||||
};
|
};
|
||||||
use CATALOG;
|
use CATALOG;
|
||||||
|
|
||||||
macro_rules! mv {
|
fn from_md(md: &str) {
|
||||||
( $( $var:ident ),* => $exp:expr ) => {
|
let md_parser = Parser::new_ext(md, Options::all());
|
||||||
{
|
md_parser.fold(
|
||||||
$( let $var = $var.clone(); )*
|
document().get_element_by_id("editor-main").unwrap(),
|
||||||
$exp
|
|last_elt, event| {
|
||||||
|
match event {
|
||||||
|
Event::Start(tag) => {
|
||||||
|
let new = match tag {
|
||||||
|
Tag::Paragraph => document().create_element("p").unwrap(),
|
||||||
|
Tag::Rule => document().create_element("hr").unwrap(),
|
||||||
|
Tag::Header(level) => {
|
||||||
|
document().create_element(&format!("h{}", level)).unwrap()
|
||||||
|
}
|
||||||
|
Tag::BlockQuote => document().create_element("blockquote").unwrap(),
|
||||||
|
Tag::CodeBlock(code) => {
|
||||||
|
let pre = document().create_element("pre").unwrap();
|
||||||
|
let code_elt = document().create_element("code").unwrap();
|
||||||
|
code_elt.append_child(&document().create_text_node(&code));
|
||||||
|
pre.append_child(&code_elt);
|
||||||
|
pre
|
||||||
|
}
|
||||||
|
Tag::List(None) => document().create_element("ul").unwrap(),
|
||||||
|
Tag::List(Some(_start_index)) => document().create_element("ol").unwrap(), // TODO: handle start_index
|
||||||
|
Tag::Item => document().create_element("li").unwrap(),
|
||||||
|
Tag::FootnoteDefinition(def) => {
|
||||||
|
let note = document().create_element("div").unwrap();
|
||||||
|
note.class_list().add("footnote");
|
||||||
|
note.append_child(&document().create_text_node(&def));
|
||||||
|
note
|
||||||
|
}
|
||||||
|
Tag::HtmlBlock => document().create_element("div").unwrap(),
|
||||||
|
Tag::Table(_alignements) => document().create_element("table").unwrap(), // TODO: handle alignements
|
||||||
|
Tag::TableHead => document().create_element("th").unwrap(),
|
||||||
|
Tag::TableRow => document().create_element("tr").unwrap(),
|
||||||
|
Tag::TableCell => document().create_element("td").unwrap(),
|
||||||
|
Tag::Emphasis => document().create_element("em").unwrap(),
|
||||||
|
Tag::Strong => document().create_element("strong").unwrap(),
|
||||||
|
Tag::Strikethrough => document().create_element("s").unwrap(),
|
||||||
|
Tag::Link(_link_type, url, text) => {
|
||||||
|
let url: &str = &url;
|
||||||
|
let text: &str = &text;
|
||||||
|
let link = document().create_element("a").unwrap();
|
||||||
|
js! {
|
||||||
|
@{&link}.href = @{url};
|
||||||
|
@{&link}.title = @{text};
|
||||||
|
};
|
||||||
|
link
|
||||||
|
}
|
||||||
|
Tag::Image(_link_type, url, text) => {
|
||||||
|
let url: &str = &url;
|
||||||
|
let text: &str = &text;
|
||||||
|
let img = document().create_element("img").unwrap();
|
||||||
|
js! {
|
||||||
|
@{&img}.src = @{url};
|
||||||
|
@{&img}.title = @{text};
|
||||||
|
@{&img}.alt = @{text};
|
||||||
|
};
|
||||||
|
img
|
||||||
|
}
|
||||||
|
};
|
||||||
|
last_elt.append_child(&new);
|
||||||
|
new
|
||||||
|
}
|
||||||
|
Event::End(_) => last_elt.parent_element().unwrap(),
|
||||||
|
Event::Text(text) => {
|
||||||
|
let node = document().create_text_node(&text);
|
||||||
|
last_elt.append_child(&node);
|
||||||
|
last_elt
|
||||||
|
}
|
||||||
|
Event::Code(code) => {
|
||||||
|
let elt = document().create_element("code").unwrap();
|
||||||
|
let content = document().create_text_node(&code);
|
||||||
|
elt.append_child(&content);
|
||||||
|
last_elt.append_child(&elt);
|
||||||
|
last_elt
|
||||||
|
}
|
||||||
|
Event::Html(html) => {
|
||||||
|
// TODO: sanitize it?
|
||||||
|
last_elt.set_attribute("innerHtml", &html);
|
||||||
|
last_elt
|
||||||
|
}
|
||||||
|
Event::InlineHtml(html) => {
|
||||||
|
let elt = document().create_element("span").unwrap();
|
||||||
|
elt.set_attribute("innerHtml", &html);
|
||||||
|
last_elt.append_child(&elt);
|
||||||
|
last_elt
|
||||||
|
}
|
||||||
|
Event::FootnoteReference(reference) => {
|
||||||
|
last_elt // TODO
|
||||||
|
}
|
||||||
|
Event::SoftBreak => {
|
||||||
|
last_elt.append_child(&document().create_element("br").unwrap());
|
||||||
|
last_elt
|
||||||
|
}
|
||||||
|
Event::HardBreak => {
|
||||||
|
last_elt // TODO
|
||||||
|
}
|
||||||
|
Event::TaskListMarker(done) => {
|
||||||
|
last_elt // TODO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
MutationObserver::new(|muts, _obs| {
|
||||||
|
for m in muts {
|
||||||
|
console!(log, "mut!!");
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.observe(
|
||||||
|
&document().get_element_by_id("editor-main").unwrap(),
|
||||||
|
MutationObserverInit {
|
||||||
|
child_list: true,
|
||||||
|
attributes: true,
|
||||||
|
character_data: false,
|
||||||
|
subtree: true,
|
||||||
|
attribute_old_value: true,
|
||||||
|
character_data_old_value: false,
|
||||||
|
attribute_filter: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_md() -> String {
|
||||||
|
let root = document().get_element_by_id("editor-main").unwrap();
|
||||||
|
fold_children(&root).join("")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fold_children(elt: &Element) -> Vec<String> {
|
||||||
|
elt.child_nodes().iter().fold(vec![], |mut blocks, node| {
|
||||||
|
blocks.push(html_to_md(&node));
|
||||||
|
blocks
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn html_to_md(node: &Node) -> String {
|
||||||
|
console!(log, node);
|
||||||
|
if let Ok(elt) = Element::try_from(node.clone()) {
|
||||||
|
console!(log, elt.node_name().to_lowercase());
|
||||||
|
match elt.node_name().to_lowercase().as_ref() {
|
||||||
|
"hr" => "---".into(),
|
||||||
|
"h1" => format!("# {}\n\n", fold_children(&elt).join("")),
|
||||||
|
"h2" => format!("## {}\n\n", fold_children(&elt).join("")),
|
||||||
|
"h3" => format!("### {}\n\n", fold_children(&elt).join("")),
|
||||||
|
"h4" => format!("#### {}\n\n", fold_children(&elt).join("")),
|
||||||
|
"h5" => format!("##### {}\n\n", fold_children(&elt).join("")),
|
||||||
|
"h6" => format!("###### {}\n\n", fold_children(&elt).join("")),
|
||||||
|
"blockquote" => format!("> {}\n\n", fold_children(&elt).join("> ")),
|
||||||
|
"pre" => format!("```\n{}\n```\n\n", node.text_content().unwrap_or_default()),
|
||||||
|
"li" => match elt
|
||||||
|
.parent_element()
|
||||||
|
.unwrap()
|
||||||
|
.node_name()
|
||||||
|
.to_lowercase()
|
||||||
|
.as_ref()
|
||||||
|
{
|
||||||
|
"ol" => format!(
|
||||||
|
"{}. {}\n",
|
||||||
|
elt.parent_element()
|
||||||
|
.unwrap()
|
||||||
|
.child_nodes()
|
||||||
|
.iter()
|
||||||
|
.position(|n| Element::try_from(n).unwrap() == elt)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
fold_children(&elt).join(""),
|
||||||
|
),
|
||||||
|
_ => format!("- {}\n", fold_children(&elt).join("")),
|
||||||
|
},
|
||||||
|
"em" => format!("_{}_", fold_children(&elt).join("")),
|
||||||
|
"strong" => format!("**{}**", fold_children(&elt).join("")),
|
||||||
|
"s" => format!("~~{}~~", fold_children(&elt).join("")),
|
||||||
|
"a" => format!(
|
||||||
|
"[{}]({})",
|
||||||
|
fold_children(&elt).join(""),
|
||||||
|
String::try_from(js! { return @{&elt}.href }).unwrap()
|
||||||
|
),
|
||||||
|
"img" => format!(
|
||||||
|
"",
|
||||||
|
String::try_from(js! { return @{&elt}.alt }).unwrap(),
|
||||||
|
String::try_from(js! { return @{&elt}.src }).unwrap()
|
||||||
|
),
|
||||||
|
other => {
|
||||||
|
console!(log, "Warning: unhandled element:", other);
|
||||||
|
String::new()
|
||||||
|
} // TODO: refs, tables, raw html
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
node.text_content().unwrap_or_default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_elt_value(id: &'static str) -> String {
|
fn get_elt_value(id: &'static str) -> String {
|
||||||
let elt = document().get_element_by_id(id).unwrap();
|
let elt = document().get_element_by_id(id).unwrap();
|
||||||
let inp: Result<InputElement, _> = elt.clone().try_into();
|
let inp: Result<InputElement, _> = elt.clone().try_into();
|
||||||
|
let select: Result<SelectElement, _> = elt.clone().try_into();
|
||||||
let textarea: Result<TextAreaElement, _> = elt.try_into();
|
let textarea: Result<TextAreaElement, _> = elt.try_into();
|
||||||
inp.map(|i| i.raw_value())
|
let res = inp.map(|i| i.raw_value()).unwrap_or_else(|_| {
|
||||||
.unwrap_or_else(|_| textarea.unwrap().value())
|
textarea
|
||||||
}
|
.map(|t| t.value())
|
||||||
|
.unwrap_or_else(|_| select.unwrap().value().unwrap_or_default())
|
||||||
fn set_value<S: AsRef<str>>(id: &'static str, val: S) {
|
});
|
||||||
let elt = document().get_element_by_id(id).unwrap();
|
res
|
||||||
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()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn no_return(evt: KeyDownEvent) {
|
fn no_return(evt: KeyDownEvent) {
|
||||||
@@ -63,33 +243,7 @@ impl From<stdweb::private::ConversionError> for EditorError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_widget(
|
fn filter_paste(elt: &Element) {
|
||||||
parent: &Element,
|
|
||||||
tag: &'static str,
|
|
||||||
placeholder_text: String,
|
|
||||||
content: String,
|
|
||||||
disable_return: bool,
|
|
||||||
) -> Result<HtmlElement, EditorError> {
|
|
||||||
let widget = placeholder(make_editable(tag).try_into()?, &placeholder_text);
|
|
||||||
if !content.is_empty() {
|
|
||||||
widget.dataset().insert("edited", "true")?;
|
|
||||||
}
|
|
||||||
widget.append_child(&document().create_text_node(&content));
|
|
||||||
if disable_return {
|
|
||||||
widget.add_event_listener(no_return);
|
|
||||||
}
|
|
||||||
|
|
||||||
parent.append_child(&widget);
|
|
||||||
// We need to do that to make sure the placeholder is correctly rendered
|
|
||||||
widget.focus();
|
|
||||||
widget.blur();
|
|
||||||
|
|
||||||
filter_paste(&widget);
|
|
||||||
|
|
||||||
Ok(widget)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn filter_paste(elt: &HtmlElement) {
|
|
||||||
// Only insert text when pasting something
|
// Only insert text when pasting something
|
||||||
js! {
|
js! {
|
||||||
@{&elt}.addEventListener("paste", function (evt) {
|
@{&elt}.addEventListener("paste", function (evt) {
|
||||||
@@ -127,64 +281,69 @@ pub fn init() -> Result<(), EditorError> {
|
|||||||
|
|
||||||
fn init_editor() -> Result<(), EditorError> {
|
fn init_editor() -> Result<(), EditorError> {
|
||||||
if let Some(ed) = document().get_element_by_id("plume-editor") {
|
if let Some(ed) = document().get_element_by_id("plume-editor") {
|
||||||
|
document().body()?.set_attribute("id", "editor")?;
|
||||||
|
|
||||||
|
let aside = document().get_element_by_id("plume-editor-aside")?;
|
||||||
|
|
||||||
// Show the editor
|
// Show the editor
|
||||||
js! { @{&ed}.style.display = "block"; };
|
js! {
|
||||||
|
@{&ed}.style.display = "grid";
|
||||||
|
@{&aside}.style.display = "block";
|
||||||
|
};
|
||||||
// And hide the HTML-only fallback
|
// And hide the HTML-only fallback
|
||||||
let old_ed = document().get_element_by_id("plume-fallback-editor")?;
|
let old_ed = document().get_element_by_id("plume-fallback-editor")?;
|
||||||
let old_title = document().get_element_by_id("plume-editor-title")?;
|
|
||||||
js! {
|
js! {
|
||||||
@{&old_ed}.style.display = "none";
|
@{&old_ed}.style.display = "none";
|
||||||
@{&old_title}.style.display = "none";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get content from the old editor (when editing an article for instance)
|
|
||||||
let title_val = get_elt_value("title");
|
|
||||||
let subtitle_val = get_elt_value("subtitle");
|
|
||||||
let content_val = get_elt_value("editor-content");
|
|
||||||
// And pre-fill the new editor with this values
|
// And pre-fill the new editor with this values
|
||||||
let title = init_widget(&ed, "h1", i18n!(CATALOG, "Title"), title_val, true)?;
|
let title = document().get_element_by_id("editor-title")?;
|
||||||
let subtitle = init_widget(
|
let subtitle = document().get_element_by_id("editor-subtitle")?;
|
||||||
&ed,
|
let source = get_elt_value("editor-content");
|
||||||
"h2",
|
|
||||||
i18n!(CATALOG, "Subtitle, or summary"),
|
|
||||||
subtitle_val,
|
|
||||||
true,
|
|
||||||
)?;
|
|
||||||
let content = init_widget(
|
|
||||||
&ed,
|
|
||||||
"article",
|
|
||||||
i18n!(CATALOG, "Write your article here. Markdown is supported."),
|
|
||||||
content_val.clone(),
|
|
||||||
false,
|
|
||||||
)?;
|
|
||||||
js! { @{&content}.innerHTML = @{content_val}; };
|
|
||||||
|
|
||||||
// character counter
|
setup_toolbar();
|
||||||
content.add_event_listener(mv!(content => move |_: KeyDownEvent| {
|
from_md(&source);
|
||||||
window().set_timeout(mv!(content => move || {
|
|
||||||
if let Some(e) = document().get_element_by_id("char-count") {
|
title.add_event_listener(no_return);
|
||||||
let count = chars_left("#plume-fallback-editor", &content).unwrap_or_default();
|
subtitle.add_event_listener(no_return);
|
||||||
let text = i18n!(CATALOG, "Around {} characters left"; count);
|
|
||||||
HtmlElement::try_from(e).map(|e| {
|
filter_paste(&title);
|
||||||
js!{@{e}.innerText = @{text}};
|
filter_paste(&subtitle);
|
||||||
}).ok();
|
// TODO: filter_paste(&content);
|
||||||
|
|
||||||
|
document()
|
||||||
|
.get_element_by_id("publish")?
|
||||||
|
.add_event_listener(|_: ClickEvent| {
|
||||||
|
let publish_page = document().get_element_by_id("publish-page").unwrap();
|
||||||
|
let options_page = document().get_element_by_id("options-page").unwrap();
|
||||||
|
js! {
|
||||||
|
@{&options_page}.style.display = "none";
|
||||||
|
@{&publish_page}.style.display = "flex";
|
||||||
};
|
};
|
||||||
}), 0);
|
});
|
||||||
}));
|
|
||||||
|
|
||||||
document().get_element_by_id("publish")?.add_event_listener(
|
document()
|
||||||
mv!(title, subtitle, content, old_ed => move |_: ClickEvent| {
|
.get_element_by_id("cancel-publish")?
|
||||||
let popup = document().get_element_by_id("publish-popup").or_else(||
|
.add_event_listener(|_: ClickEvent| {
|
||||||
init_popup(&title, &subtitle, &content, &old_ed).ok()
|
let publish_page = document().get_element_by_id("publish-page").unwrap();
|
||||||
).unwrap();
|
let options_page = document().get_element_by_id("options-page").unwrap();
|
||||||
let bg = document().get_element_by_id("popup-bg").or_else(||
|
js! {
|
||||||
init_popup_bg().ok()
|
@{&publish_page}.style.display = "none";
|
||||||
).unwrap();
|
@{&options_page}.style.display = "flex";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
popup.class_list().add("show").unwrap();
|
document()
|
||||||
bg.class_list().add("show").unwrap();
|
.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();
|
show_errors();
|
||||||
setup_close_button();
|
setup_close_button();
|
||||||
@@ -192,6 +351,176 @@ fn init_editor() -> Result<(), EditorError> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn select_style(style: &str) {
|
||||||
|
if let Some(select) = document()
|
||||||
|
.get_element_by_id("toolbar-style")
|
||||||
|
.and_then(|e| SelectElement::try_from(e).ok())
|
||||||
|
{
|
||||||
|
select.set_value(Some(style));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_toolbar() {
|
||||||
|
let toolbar = document().get_element_by_id("editor-toolbar").unwrap();
|
||||||
|
|
||||||
|
// List of styles (headings, quote, code, etc)
|
||||||
|
let style_select =
|
||||||
|
SelectElement::try_from(document().create_element("select").unwrap()).unwrap();
|
||||||
|
let options = vec![
|
||||||
|
("p", i18n!(CATALOG, "Paragraph")),
|
||||||
|
("ul", i18n!(CATALOG, "List")),
|
||||||
|
("ol", i18n!(CATALOG, "Ordered list")),
|
||||||
|
("h1", i18n!(CATALOG, "Heading 1")),
|
||||||
|
("h2", i18n!(CATALOG, "Heading 2")),
|
||||||
|
("h3", i18n!(CATALOG, "Heading 3")),
|
||||||
|
("h4", i18n!(CATALOG, "Heading 4")),
|
||||||
|
("h5", i18n!(CATALOG, "Heading 5")),
|
||||||
|
("h6", i18n!(CATALOG, "Heading 6")),
|
||||||
|
("blockquote", i18n!(CATALOG, "Quote")),
|
||||||
|
("pre", i18n!(CATALOG, "Code")),
|
||||||
|
];
|
||||||
|
for (tag, name) in options.clone() {
|
||||||
|
let opt = document().create_element("option").unwrap();
|
||||||
|
opt.set_attribute("value", tag);
|
||||||
|
opt.append_child(&document().create_text_node(&name));
|
||||||
|
style_select.append_child(&opt)
|
||||||
|
}
|
||||||
|
style_select.set_attribute("id", "toolbar-style");
|
||||||
|
|
||||||
|
let options_clone = options.clone();
|
||||||
|
document().add_event_listener(move |_: SelectionChangeEvent| {
|
||||||
|
let block = std::iter::successors(
|
||||||
|
window().get_selection().and_then(|s| s.anchor_node()),
|
||||||
|
|node| {
|
||||||
|
let t = node.node_name().to_lowercase();
|
||||||
|
if options_clone.iter().any(|(tag, _)| *tag == &t) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
node.parent_node()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.last();
|
||||||
|
|
||||||
|
if let Some(b) = block {
|
||||||
|
select_style(&b.node_name().to_lowercase());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
style_select.add_event_listener(move |_: ChangeEvent| {
|
||||||
|
let block = std::iter::successors(
|
||||||
|
window().get_selection().and_then(|s| s.anchor_node()),
|
||||||
|
|node| {
|
||||||
|
let t = node.node_name().to_lowercase();
|
||||||
|
if options.iter().any(|(tag, _)| *tag == &t) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
node.parent_node()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.last();
|
||||||
|
|
||||||
|
if let Some(block) = block {
|
||||||
|
if let Some(select) = document()
|
||||||
|
.get_element_by_id("toolbar-style")
|
||||||
|
.and_then(|e| SelectElement::try_from(e).ok())
|
||||||
|
{
|
||||||
|
let tag = select.value();
|
||||||
|
|
||||||
|
let new = document().create_element(&tag.unwrap_or_default()).unwrap();
|
||||||
|
for ch in block.child_nodes() {
|
||||||
|
block.remove_child(&ch);
|
||||||
|
new.append_child(&ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
block.parent_node().unwrap().replace_child(&new, &block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bold
|
||||||
|
|
||||||
|
// Italics
|
||||||
|
|
||||||
|
// Insert an image
|
||||||
|
|
||||||
|
toolbar.append_child(&style_select);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console!(log, to_md());
|
||||||
|
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: to_md(),
|
||||||
|
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() {
|
fn setup_close_button() {
|
||||||
if let Some(button) = document().get_element_by_id("close-editor") {
|
if let Some(button) = document().get_element_by_id("close-editor") {
|
||||||
button.add_event_listener(|_: ClickEvent| {
|
button.add_event_listener(|_: ClickEvent| {
|
||||||
@@ -223,200 +552,3 @@ fn show_errors() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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: &HtmlElement) -> Option<i32> {
|
|
||||||
match document().query_selector(selector) {
|
|
||||||
Ok(Some(form)) => HtmlElement::try_from(form).ok().and_then(|form| {
|
|
||||||
if let Some(len) = form
|
|
||||||
.get_attribute("content-size")
|
|
||||||
.and_then(|s| s.parse::<i32>().ok())
|
|
||||||
{
|
|
||||||
(js! {
|
|
||||||
let x = encodeURIComponent(@{content}.innerHTML)
|
|
||||||
.replace(/%20/g, "+")
|
|
||||||
.replace(/%0A/g, "%0D%0A")
|
|
||||||
.replace(new RegExp("[!'*()]", "g"), "XXX") // replace exceptions of encodeURIComponent with placeholder
|
|
||||||
.length + 2;
|
|
||||||
console.log(x);
|
|
||||||
return x;
|
|
||||||
})
|
|
||||||
.try_into()
|
|
||||||
.map(|c: i32| len - c)
|
|
||||||
.ok()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
_ => 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 placeholder(elt: HtmlElement, text: &str) -> HtmlElement {
|
|
||||||
elt.dataset().insert("placeholder", text).unwrap();
|
|
||||||
elt.dataset().insert("edited", "false").unwrap();
|
|
||||||
|
|
||||||
elt.add_event_listener(mv!(elt => move |_: FocusEvent| {
|
|
||||||
if elt.dataset().get("edited").unwrap().as_str() != "true" {
|
|
||||||
clear_children(&elt);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
elt.add_event_listener(mv!(elt => move |_: BlurEvent| {
|
|
||||||
if elt.dataset().get("edited").unwrap().as_str() != "true" {
|
|
||||||
clear_children(&elt);
|
|
||||||
|
|
||||||
let ph = document().create_element("span").expect("Couldn't create placeholder");
|
|
||||||
ph.class_list().add("placeholder").expect("Couldn't add class");
|
|
||||||
ph.append_child(&document().create_text_node(&elt.dataset().get("placeholder").unwrap_or_default()));
|
|
||||||
elt.append_child(&ph);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
elt.add_event_listener(mv!(elt => move |_: KeyUpEvent| {
|
|
||||||
elt.dataset().insert("edited", if elt.inner_text().trim_matches('\n').is_empty() {
|
|
||||||
"false"
|
|
||||||
} else {
|
|
||||||
"true"
|
|
||||||
}).expect("Couldn't update edition state");
|
|
||||||
}));
|
|
||||||
elt
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear_children(elt: &HtmlElement) {
|
|
||||||
for child in elt.child_nodes() {
|
|
||||||
elt.remove_child(&child).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ extern crate gettext;
|
|||||||
extern crate gettext_macros;
|
extern crate gettext_macros;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate lazy_static;
|
extern crate lazy_static;
|
||||||
|
extern crate pulldown_cmark;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate stdweb;
|
extern crate stdweb;
|
||||||
|
extern crate serde_json;
|
||||||
|
|
||||||
use stdweb::web::{event::*, *};
|
use stdweb::web::{event::*, *};
|
||||||
|
|
||||||
@@ -56,6 +58,18 @@ lazy_static! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
std::panic::set_hook(Box::new(|info: &std::panic::PanicInfo| {
|
||||||
|
let mut msg = info.to_string();
|
||||||
|
msg.push_str("\n\nStack:\n\n");
|
||||||
|
let e = error::Error::new("Panicked");
|
||||||
|
let stack = js! { return @{&e}.stack; }
|
||||||
|
.into_string()
|
||||||
|
.unwrap_or_default();
|
||||||
|
msg.push_str(&stack);
|
||||||
|
msg.push_str("\n\n");
|
||||||
|
console!(error, msg);
|
||||||
|
}));
|
||||||
|
|
||||||
menu();
|
menu();
|
||||||
search();
|
search();
|
||||||
editor::init()
|
editor::init()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ authors = ["Plume contributors"]
|
|||||||
activitypub = "0.1.1"
|
activitypub = "0.1.1"
|
||||||
ammonia = "2.1.1"
|
ammonia = "2.1.1"
|
||||||
askama_escape = "0.1"
|
askama_escape = "0.1"
|
||||||
bcrypt = "0.5"
|
bcrypt = "0.4"
|
||||||
guid-create = "0.1"
|
guid-create = "0.1"
|
||||||
heck = "0.3.0"
|
heck = "0.3.0"
|
||||||
itertools = "0.8.0"
|
itertools = "0.8.0"
|
||||||
@@ -17,14 +17,15 @@ openssl = "0.10.22"
|
|||||||
rocket = "0.4.0"
|
rocket = "0.4.0"
|
||||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||||
reqwest = "0.9"
|
reqwest = "0.9"
|
||||||
scheduled-thread-pool = "0.2.2"
|
scheduled-thread-pool = "0.2.0"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tantivy = "0.10.1"
|
tantivy = "0.9.1"
|
||||||
url = "2.1"
|
url = "1.7"
|
||||||
webfinger = "0.4.1"
|
webfinger = "0.4.1"
|
||||||
whatlang = "0.7.1"
|
whatlang = "0.7.1"
|
||||||
|
shrinkwraprs = "0.2.1"
|
||||||
diesel-derive-newtype = "0.1.2"
|
diesel-derive-newtype = "0.1.2"
|
||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
|
|||||||
@@ -44,6 +44,18 @@ impl ApiToken {
|
|||||||
get!(api_tokens);
|
get!(api_tokens);
|
||||||
insert!(api_tokens, NewApiToken);
|
insert!(api_tokens, NewApiToken);
|
||||||
find_by!(api_tokens, find_by_value, value as &str);
|
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 {
|
pub fn can(&self, what: &'static str, scope: &'static str) -> bool {
|
||||||
let full_scope = what.to_owned() + ":" + scope;
|
let full_scope = what.to_owned() + ":" + scope;
|
||||||
|
|||||||
@@ -29,4 +29,5 @@ impl App {
|
|||||||
get!(apps);
|
get!(apps);
|
||||||
insert!(apps, NewApp);
|
insert!(apps, NewApp);
|
||||||
find_by!(apps, find_by_client_id, client_id as &str);
|
find_by!(apps, find_by_client_id, client_id as &str);
|
||||||
|
find_by!(apps, find_by_name, name as &str);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,6 @@ use openssl::{
|
|||||||
rsa::Rsa,
|
rsa::Rsa,
|
||||||
sign::{Signer, Verifier},
|
sign::{Signer, Verifier},
|
||||||
};
|
};
|
||||||
use rocket::{
|
|
||||||
http::RawStr,
|
|
||||||
outcome::IntoOutcome,
|
|
||||||
request::{self, FromFormValue, FromRequest, Request},
|
|
||||||
};
|
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use webfinger::*;
|
use webfinger::*;
|
||||||
@@ -26,70 +21,11 @@ use posts::Post;
|
|||||||
use safe_string::SafeString;
|
use safe_string::SafeString;
|
||||||
use schema::blogs;
|
use schema::blogs;
|
||||||
use search::Searcher;
|
use search::Searcher;
|
||||||
use std::default::Default;
|
|
||||||
use std::fmt::{self, Display};
|
|
||||||
use std::ops::Deref;
|
|
||||||
use std::sync::RwLock;
|
|
||||||
use users::User;
|
use users::User;
|
||||||
use {Connection, Error, PlumeRocket, Result};
|
use {Connection, Error, PlumeRocket, Result};
|
||||||
|
|
||||||
pub type CustomGroup = CustomObject<ApSignature, Group>;
|
pub type CustomGroup = CustomObject<ApSignature, Group>;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DieselNewType)]
|
|
||||||
pub struct Host(String);
|
|
||||||
|
|
||||||
impl Host {
|
|
||||||
pub fn new(host: impl ToString) -> Host {
|
|
||||||
Host(host.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for Host {
|
|
||||||
type Target = str;
|
|
||||||
fn deref(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Host {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsRef<str> for Host {
|
|
||||||
fn as_ref(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for Host {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Host, ()> {
|
|
||||||
request
|
|
||||||
.headers()
|
|
||||||
.get_one("Host")
|
|
||||||
.and_then(|x| {
|
|
||||||
if Blog::list_custom_domains().contains(&x.to_string()) {
|
|
||||||
Some(Host::new(x))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.or_forward(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'v> FromFormValue<'v> for Host {
|
|
||||||
type Error = &'v RawStr;
|
|
||||||
|
|
||||||
fn from_form_value(form_value: &'v RawStr) -> std::result::Result<Host, &'v RawStr> {
|
|
||||||
let val = String::from_form_value(form_value)?;
|
|
||||||
Ok(Host::new(&val))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Queryable, Identifiable, Clone, AsChangeset)]
|
#[derive(Queryable, Identifiable, Clone, AsChangeset)]
|
||||||
#[changeset_options(treat_none_as_null = "true")]
|
#[changeset_options(treat_none_as_null = "true")]
|
||||||
pub struct Blog {
|
pub struct Blog {
|
||||||
@@ -108,7 +44,6 @@ pub struct Blog {
|
|||||||
pub summary_html: SafeString,
|
pub summary_html: SafeString,
|
||||||
pub icon_id: Option<i32>,
|
pub icon_id: Option<i32>,
|
||||||
pub banner_id: Option<i32>,
|
pub banner_id: Option<i32>,
|
||||||
pub custom_domain: Option<Host>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Insertable)]
|
#[derive(Default, Insertable)]
|
||||||
@@ -126,15 +61,10 @@ pub struct NewBlog {
|
|||||||
pub summary_html: SafeString,
|
pub summary_html: SafeString,
|
||||||
pub icon_id: Option<i32>,
|
pub icon_id: Option<i32>,
|
||||||
pub banner_id: Option<i32>,
|
pub banner_id: Option<i32>,
|
||||||
pub custom_domain: Option<Host>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const BLOG_PREFIX: &str = "~";
|
const BLOG_PREFIX: &str = "~";
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref CUSTOM_DOMAINS: RwLock<Vec<String>> = RwLock::new(vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Blog {
|
impl Blog {
|
||||||
insert!(blogs, NewBlog, |inserted, conn| {
|
insert!(blogs, NewBlog, |inserted, conn| {
|
||||||
let instance = inserted.get_instance(conn)?;
|
let instance = inserted.get_instance(conn)?;
|
||||||
@@ -214,13 +144,6 @@ impl Blog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_host(c: &PlumeRocket, host: Host) -> Result<Blog> {
|
|
||||||
blogs::table
|
|
||||||
.filter(blogs::custom_domain.eq(host))
|
|
||||||
.first::<Blog>(&*c.conn)
|
|
||||||
.map_err(|_| Error::NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<Blog> {
|
fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<Blog> {
|
||||||
resolve_with_prefix(Prefix::Group, acct.to_owned(), true)?
|
resolve_with_prefix(Prefix::Group, acct.to_owned(), true)?
|
||||||
.links
|
.links
|
||||||
@@ -346,19 +269,6 @@ impl Blog {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn url(&self) -> String {
|
|
||||||
format!(
|
|
||||||
"https://{}",
|
|
||||||
self.custom_domain
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| Host::new(format!(
|
|
||||||
"{}/~/{}",
|
|
||||||
Instance::get_local().unwrap().public_domain,
|
|
||||||
self.fqn,
|
|
||||||
)))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn icon_url(&self, conn: &Connection) -> String {
|
pub fn icon_url(&self, conn: &Connection) -> String {
|
||||||
self.icon_id
|
self.icon_id
|
||||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
|
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
|
||||||
@@ -380,23 +290,6 @@ impl Blog {
|
|||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_custom_domains() -> Vec<String> {
|
|
||||||
CUSTOM_DOMAINS.read().unwrap().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cache_custom_domains(conn: &Connection) {
|
|
||||||
*CUSTOM_DOMAINS.write().unwrap() = Blog::list_custom_domains_uncached(conn).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list_custom_domains_uncached(conn: &Connection) -> Result<Vec<String>> {
|
|
||||||
blogs::table
|
|
||||||
.filter(blogs::custom_domain.is_not_null())
|
|
||||||
.select(blogs::custom_domain)
|
|
||||||
.load::<Option<String>>(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
.map(|res| res.into_iter().map(Option::unwrap).collect::<Vec<_>>())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoId for Blog {
|
impl IntoId for Blog {
|
||||||
@@ -499,7 +392,6 @@ impl FromId<PlumeRocket> for Blog {
|
|||||||
.summary_string()
|
.summary_string()
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
),
|
),
|
||||||
custom_domain: None,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -549,7 +441,6 @@ impl NewBlog {
|
|||||||
title: String,
|
title: String,
|
||||||
summary: String,
|
summary: String,
|
||||||
instance_id: i32,
|
instance_id: i32,
|
||||||
custom_domain: Option<Host>,
|
|
||||||
) -> Result<NewBlog> {
|
) -> Result<NewBlog> {
|
||||||
let (pub_key, priv_key) = sign::gen_keypair();
|
let (pub_key, priv_key) = sign::gen_keypair();
|
||||||
Ok(NewBlog {
|
Ok(NewBlog {
|
||||||
@@ -559,7 +450,6 @@ impl NewBlog {
|
|||||||
instance_id,
|
instance_id,
|
||||||
public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?,
|
public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?,
|
||||||
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
|
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
|
||||||
custom_domain,
|
|
||||||
..NewBlog::default()
|
..NewBlog::default()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -587,7 +477,6 @@ pub(crate) mod tests {
|
|||||||
"Blog name".to_owned(),
|
"Blog name".to_owned(),
|
||||||
"This is a small blog".to_owned(),
|
"This is a small blog".to_owned(),
|
||||||
Instance::get_local().unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -599,7 +488,6 @@ pub(crate) mod tests {
|
|||||||
"My blog".to_owned(),
|
"My blog".to_owned(),
|
||||||
"Welcome to my blog".to_owned(),
|
"Welcome to my blog".to_owned(),
|
||||||
Instance::get_local().unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
Some(Host::new("blog.myname.me")),
|
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -611,7 +499,6 @@ pub(crate) mod tests {
|
|||||||
"Why I like Plume".to_owned(),
|
"Why I like Plume".to_owned(),
|
||||||
"In this blog I will explay you why I like Plume so much".to_owned(),
|
"In this blog I will explay you why I like Plume so much".to_owned(),
|
||||||
Instance::get_local().unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -672,7 +559,6 @@ pub(crate) mod tests {
|
|||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
"This is some blog".to_owned(),
|
"This is some blog".to_owned(),
|
||||||
Instance::get_local().unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
Some(Host::new("some.blog.com")),
|
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -701,7 +587,6 @@ pub(crate) mod tests {
|
|||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
"This is some blog".to_owned(),
|
"This is some blog".to_owned(),
|
||||||
Instance::get_local().unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -713,7 +598,6 @@ pub(crate) mod tests {
|
|||||||
"Blog".to_owned(),
|
"Blog".to_owned(),
|
||||||
"I've named my blog Blog".to_owned(),
|
"I've named my blog Blog".to_owned(),
|
||||||
Instance::get_local().unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
Some(Host::new("named.example.blog")),
|
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -806,7 +690,6 @@ pub(crate) mod tests {
|
|||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
"This is some blog".to_owned(),
|
"This is some blog".to_owned(),
|
||||||
Instance::get_local().unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -831,7 +714,6 @@ pub(crate) mod tests {
|
|||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
"This is some blog".to_owned(),
|
"This is some blog".to_owned(),
|
||||||
Instance::get_local().unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
Some(Host::new("some.blog.com")),
|
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -870,7 +752,6 @@ pub(crate) mod tests {
|
|||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
"This is some blog".to_owned(),
|
"This is some blog".to_owned(),
|
||||||
Instance::get_local().unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -882,7 +763,6 @@ pub(crate) mod tests {
|
|||||||
"Blog".to_owned(),
|
"Blog".to_owned(),
|
||||||
"I've named my blog Blog".to_owned(),
|
"I've named my blog Blog".to_owned(),
|
||||||
Instance::get_local().unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
Some(Host::new("my.blog.com")),
|
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ extern crate bcrypt;
|
|||||||
extern crate chrono;
|
extern crate chrono;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate diesel;
|
extern crate diesel;
|
||||||
#[macro_use]
|
|
||||||
extern crate diesel_derive_newtype;
|
|
||||||
extern crate guid_create;
|
extern crate guid_create;
|
||||||
extern crate heck;
|
extern crate heck;
|
||||||
extern crate itertools;
|
extern crate itertools;
|
||||||
@@ -66,6 +64,7 @@ pub enum Error {
|
|||||||
Signature,
|
Signature,
|
||||||
Unauthorized,
|
Unauthorized,
|
||||||
Url,
|
Url,
|
||||||
|
Validation(String),
|
||||||
Webfinger,
|
Webfinger,
|
||||||
Expired,
|
Expired,
|
||||||
}
|
}
|
||||||
|
|||||||
+76
-77
@@ -1,17 +1,17 @@
|
|||||||
table! {
|
table! {
|
||||||
api_tokens (id) {
|
api_tokens (id) {
|
||||||
id -> Integer,
|
id -> Int4,
|
||||||
creation_date -> Timestamp,
|
creation_date -> Timestamp,
|
||||||
value -> Text,
|
value -> Text,
|
||||||
scopes -> Text,
|
scopes -> Text,
|
||||||
app_id -> Integer,
|
app_id -> Int4,
|
||||||
user_id -> Integer,
|
user_id -> Int4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
apps (id) {
|
apps (id) {
|
||||||
id -> Integer,
|
id -> Int4,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
client_id -> Text,
|
client_id -> Text,
|
||||||
client_secret -> Text,
|
client_secret -> Text,
|
||||||
@@ -23,71 +23,70 @@ table! {
|
|||||||
|
|
||||||
table! {
|
table! {
|
||||||
blog_authors (id) {
|
blog_authors (id) {
|
||||||
id -> Integer,
|
id -> Int4,
|
||||||
blog_id -> Integer,
|
blog_id -> Int4,
|
||||||
author_id -> Integer,
|
author_id -> Int4,
|
||||||
is_owner -> Bool,
|
is_owner -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
blogs (id) {
|
blogs (id) {
|
||||||
id -> Integer,
|
id -> Int4,
|
||||||
actor_id -> Text,
|
actor_id -> Varchar,
|
||||||
title -> Text,
|
title -> Varchar,
|
||||||
summary -> Text,
|
summary -> Text,
|
||||||
outbox_url -> Text,
|
outbox_url -> Varchar,
|
||||||
inbox_url -> Text,
|
inbox_url -> Varchar,
|
||||||
instance_id -> Integer,
|
instance_id -> Int4,
|
||||||
creation_date -> Timestamp,
|
creation_date -> Timestamp,
|
||||||
ap_url -> Text,
|
ap_url -> Text,
|
||||||
private_key -> Nullable<Text>,
|
private_key -> Nullable<Text>,
|
||||||
public_key -> Text,
|
public_key -> Text,
|
||||||
fqn -> Text,
|
fqn -> Text,
|
||||||
summary_html -> Text,
|
summary_html -> Text,
|
||||||
icon_id -> Nullable<Integer>,
|
icon_id -> Nullable<Int4>,
|
||||||
banner_id -> Nullable<Integer>,
|
banner_id -> Nullable<Int4>,
|
||||||
custom_domain -> Nullable<Text>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
|
||||||
comment_seers (id) {
|
|
||||||
id -> Integer,
|
|
||||||
comment_id -> Integer,
|
|
||||||
user_id -> Integer,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
comments (id) {
|
comments (id) {
|
||||||
id -> Integer,
|
id -> Int4,
|
||||||
content -> Text,
|
content -> Text,
|
||||||
in_response_to_id -> Nullable<Integer>,
|
in_response_to_id -> Nullable<Int4>,
|
||||||
post_id -> Integer,
|
post_id -> Int4,
|
||||||
author_id -> Integer,
|
author_id -> Int4,
|
||||||
creation_date -> Timestamp,
|
creation_date -> Timestamp,
|
||||||
ap_url -> Nullable<Text>,
|
ap_url -> Nullable<Varchar>,
|
||||||
sensitive -> Bool,
|
sensitive -> Bool,
|
||||||
spoiler_text -> Text,
|
spoiler_text -> Text,
|
||||||
public_visibility -> Bool,
|
public_visibility -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
comment_seers (id) {
|
||||||
|
id -> Int4,
|
||||||
|
comment_id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
follows (id) {
|
follows (id) {
|
||||||
id -> Integer,
|
id -> Int4,
|
||||||
follower_id -> Integer,
|
follower_id -> Int4,
|
||||||
following_id -> Integer,
|
following_id -> Int4,
|
||||||
ap_url -> Text,
|
ap_url -> Text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
instances (id) {
|
instances (id) {
|
||||||
id -> Integer,
|
id -> Int4,
|
||||||
public_domain -> Text,
|
public_domain -> Varchar,
|
||||||
name -> Text,
|
name -> Varchar,
|
||||||
local -> Bool,
|
local -> Bool,
|
||||||
blocked -> Bool,
|
blocked -> Bool,
|
||||||
creation_date -> Timestamp,
|
creation_date -> Timestamp,
|
||||||
@@ -95,50 +94,50 @@ table! {
|
|||||||
short_description -> Text,
|
short_description -> Text,
|
||||||
long_description -> Text,
|
long_description -> Text,
|
||||||
default_license -> Text,
|
default_license -> Text,
|
||||||
long_description_html -> Text,
|
long_description_html -> Varchar,
|
||||||
short_description_html -> Text,
|
short_description_html -> Varchar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
likes (id) {
|
likes (id) {
|
||||||
id -> Integer,
|
id -> Int4,
|
||||||
user_id -> Integer,
|
user_id -> Int4,
|
||||||
post_id -> Integer,
|
post_id -> Int4,
|
||||||
creation_date -> Timestamp,
|
creation_date -> Timestamp,
|
||||||
ap_url -> Text,
|
ap_url -> Varchar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
medias (id) {
|
medias (id) {
|
||||||
id -> Integer,
|
id -> Int4,
|
||||||
file_path -> Text,
|
file_path -> Text,
|
||||||
alt_text -> Text,
|
alt_text -> Text,
|
||||||
is_remote -> Bool,
|
is_remote -> Bool,
|
||||||
remote_url -> Nullable<Text>,
|
remote_url -> Nullable<Text>,
|
||||||
sensitive -> Bool,
|
sensitive -> Bool,
|
||||||
content_warning -> Nullable<Text>,
|
content_warning -> Nullable<Text>,
|
||||||
owner_id -> Integer,
|
owner_id -> Int4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
mentions (id) {
|
mentions (id) {
|
||||||
id -> Integer,
|
id -> Int4,
|
||||||
mentioned_id -> Integer,
|
mentioned_id -> Int4,
|
||||||
post_id -> Nullable<Integer>,
|
post_id -> Nullable<Int4>,
|
||||||
comment_id -> Nullable<Integer>,
|
comment_id -> Nullable<Int4>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
notifications (id) {
|
notifications (id) {
|
||||||
id -> Integer,
|
id -> Int4,
|
||||||
user_id -> Integer,
|
user_id -> Int4,
|
||||||
creation_date -> Timestamp,
|
creation_date -> Timestamp,
|
||||||
kind -> Text,
|
kind -> Varchar,
|
||||||
object_id -> Integer,
|
object_id -> Int4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,67 +152,67 @@ table! {
|
|||||||
|
|
||||||
table! {
|
table! {
|
||||||
post_authors (id) {
|
post_authors (id) {
|
||||||
id -> Integer,
|
id -> Int4,
|
||||||
post_id -> Integer,
|
post_id -> Int4,
|
||||||
author_id -> Integer,
|
author_id -> Int4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
posts (id) {
|
posts (id) {
|
||||||
id -> Integer,
|
id -> Int4,
|
||||||
blog_id -> Integer,
|
blog_id -> Int4,
|
||||||
slug -> Text,
|
slug -> Varchar,
|
||||||
title -> Text,
|
title -> Varchar,
|
||||||
content -> Text,
|
content -> Text,
|
||||||
published -> Bool,
|
published -> Bool,
|
||||||
license -> Text,
|
license -> Varchar,
|
||||||
creation_date -> Timestamp,
|
creation_date -> Timestamp,
|
||||||
ap_url -> Text,
|
ap_url -> Varchar,
|
||||||
subtitle -> Text,
|
subtitle -> Text,
|
||||||
source -> Text,
|
source -> Text,
|
||||||
cover_id -> Nullable<Integer>,
|
cover_id -> Nullable<Int4>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
reshares (id) {
|
reshares (id) {
|
||||||
id -> Integer,
|
id -> Int4,
|
||||||
user_id -> Integer,
|
user_id -> Int4,
|
||||||
post_id -> Integer,
|
post_id -> Int4,
|
||||||
ap_url -> Text,
|
ap_url -> Varchar,
|
||||||
creation_date -> Timestamp,
|
creation_date -> Timestamp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
tags (id) {
|
tags (id) {
|
||||||
id -> Integer,
|
id -> Int4,
|
||||||
tag -> Text,
|
tag -> Text,
|
||||||
is_hashtag -> Bool,
|
is_hashtag -> Bool,
|
||||||
post_id -> Integer,
|
post_id -> Int4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
users (id) {
|
users (id) {
|
||||||
id -> Integer,
|
id -> Int4,
|
||||||
username -> Text,
|
username -> Varchar,
|
||||||
display_name -> Text,
|
display_name -> Varchar,
|
||||||
outbox_url -> Text,
|
outbox_url -> Varchar,
|
||||||
inbox_url -> Text,
|
inbox_url -> Varchar,
|
||||||
is_admin -> Bool,
|
is_admin -> Bool,
|
||||||
summary -> Text,
|
summary -> Text,
|
||||||
email -> Nullable<Text>,
|
email -> Nullable<Text>,
|
||||||
hashed_password -> Nullable<Text>,
|
hashed_password -> Nullable<Text>,
|
||||||
instance_id -> Integer,
|
instance_id -> Int4,
|
||||||
creation_date -> Timestamp,
|
creation_date -> Timestamp,
|
||||||
ap_url -> Text,
|
ap_url -> Text,
|
||||||
private_key -> Nullable<Text>,
|
private_key -> Nullable<Text>,
|
||||||
public_key -> Text,
|
public_key -> Text,
|
||||||
shared_inbox_url -> Nullable<Text>,
|
shared_inbox_url -> Nullable<Varchar>,
|
||||||
followers_endpoint -> Text,
|
followers_endpoint -> Varchar,
|
||||||
avatar_id -> Nullable<Integer>,
|
avatar_id -> Nullable<Int4>,
|
||||||
last_fetched_date -> Timestamp,
|
last_fetched_date -> Timestamp,
|
||||||
fqn -> Text,
|
fqn -> Text,
|
||||||
summary_html -> Text,
|
summary_html -> Text,
|
||||||
@@ -249,8 +248,8 @@ allow_tables_to_appear_in_same_query!(
|
|||||||
apps,
|
apps,
|
||||||
blog_authors,
|
blog_authors,
|
||||||
blogs,
|
blogs,
|
||||||
comment_seers,
|
|
||||||
comments,
|
comments,
|
||||||
|
comment_seers,
|
||||||
follows,
|
follows,
|
||||||
instances,
|
instances,
|
||||||
likes,
|
likes,
|
||||||
|
|||||||
+9
-9
@@ -36,39 +36,39 @@ msgstr ""
|
|||||||
msgid "{0}'s avatar"
|
msgid "{0}'s avatar"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
# src/routes/blogs.rs:96
|
# src/routes/blogs.rs:64
|
||||||
msgid "To create a new blog, you need to be logged in"
|
msgid "To create a new blog, you need to be logged in"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
# src/routes/blogs.rs:138
|
# src/routes/blogs.rs:106
|
||||||
msgid "A blog with the same name already exists."
|
msgid "A blog with the same name already exists."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
# src/routes/blogs.rs:173
|
# src/routes/blogs.rs:141
|
||||||
msgid "Your blog was successfully created!"
|
msgid "Your blog was successfully created!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
# src/routes/blogs.rs:195
|
# src/routes/blogs.rs:163
|
||||||
msgid "Your blog was deleted."
|
msgid "Your blog was deleted."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
# src/routes/blogs.rs:202
|
# src/routes/blogs.rs:170
|
||||||
msgid "You are not allowed to delete this blog."
|
msgid "You are not allowed to delete this blog."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
# src/routes/blogs.rs:250
|
# src/routes/blogs.rs:218
|
||||||
msgid "You are not allowed to edit this blog."
|
msgid "You are not allowed to edit this blog."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
# src/routes/blogs.rs:295
|
# src/routes/blogs.rs:263
|
||||||
msgid "You can't use this media as a blog icon."
|
msgid "You can't use this media as a blog icon."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
# src/routes/blogs.rs:313
|
# src/routes/blogs.rs:281
|
||||||
msgid "You can't use this media as a blog banner."
|
msgid "You can't use this media as a blog banner."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
# src/routes/blogs.rs:346
|
# src/routes/blogs.rs:314
|
||||||
msgid "Your blog information have been updated."
|
msgid "Your blog information have been updated."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ impl<'r> Responder<'r> for ApiError {
|
|||||||
"error": "You are not authorized to access this resource"
|
"error": "You are not authorized to access this resource"
|
||||||
}))
|
}))
|
||||||
.respond_to(req),
|
.respond_to(req),
|
||||||
|
Error::Validation(msg) => Json(json!({ "error": msg })).respond_to(req),
|
||||||
_ => Json(json!({
|
_ => Json(json!({
|
||||||
"error": "Server error"
|
"error": "Server error"
|
||||||
}))
|
}))
|
||||||
|
|||||||
+154
-3
@@ -1,6 +1,7 @@
|
|||||||
use chrono::NaiveDateTime;
|
use chrono::{NaiveDateTime, Utc};
|
||||||
use heck::{CamelCase, KebabCase};
|
use heck::{CamelCase, KebabCase};
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use crate::api::{authorization::*, Api};
|
use crate::api::{authorization::*, Api};
|
||||||
use plume_api::posts::*;
|
use plume_api::posts::*;
|
||||||
@@ -44,6 +45,7 @@ pub fn get(id: i32, auth: Option<Authorization<Read, Post>>, conn: DbConn) -> Ap
|
|||||||
published: post.published,
|
published: post.published,
|
||||||
license: post.license,
|
license: post.license,
|
||||||
cover_id: post.cover_id,
|
cover_id: post.cover_id,
|
||||||
|
url: post.ap_url,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +93,7 @@ pub fn list(
|
|||||||
published: p.published,
|
published: p.published,
|
||||||
license: p.license,
|
license: p.license,
|
||||||
cover_id: p.cover_id,
|
cover_id: p.cover_id,
|
||||||
|
url: p.ap_url,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect(),
|
.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()
|
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 domain = &Instance::get_local()?.public_domain;
|
||||||
let (content, mentions, hashtags) = md_to_html(
|
let (content, mentions, hashtags) = md_to_html(
|
||||||
&payload.source,
|
&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() {
|
if Post::find_by_slug(conn, slug, blog).is_ok() {
|
||||||
return Err(Error::InvalidValue.into());
|
return Err(Error::InvalidValue.into());
|
||||||
}
|
}
|
||||||
@@ -166,11 +187,19 @@ pub fn create(
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
if let Some(ref tags) = payload.tags {
|
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 {
|
for tag in tags {
|
||||||
Tag::insert(
|
Tag::insert(
|
||||||
conn,
|
conn,
|
||||||
NewTag {
|
NewTag {
|
||||||
tag: tag.to_string(),
|
tag: tag.name_string().unwrap(),
|
||||||
is_hashtag: false,
|
is_hashtag: false,
|
||||||
post_id: post.id,
|
post_id: post.id,
|
||||||
},
|
},
|
||||||
@@ -211,7 +240,6 @@ pub fn create(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|t| t.tag)
|
.map(|t| t.tag)
|
||||||
.collect(),
|
.collect(),
|
||||||
|
|
||||||
id: post.id,
|
id: post.id,
|
||||||
title: post.title,
|
title: post.title,
|
||||||
subtitle: post.subtitle,
|
subtitle: post.subtitle,
|
||||||
@@ -221,9 +249,132 @@ pub fn create(
|
|||||||
published: post.published,
|
published: post.published,
|
||||||
license: post.license,
|
license: post.license,
|
||||||
cover_id: post.cover_id,
|
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>")]
|
#[delete("/posts/<id>")]
|
||||||
pub fn delete(auth: Authorization<Write, Post>, rockets: PlumeRocket, id: i32) -> Api<()> {
|
pub fn delete(auth: Authorization<Write, Post>, rockets: PlumeRocket, id: i32) -> Api<()> {
|
||||||
let author = User::get(&*rockets.conn, auth.0.user_id)?;
|
let author = User::get(&*rockets.conn, auth.0.user_id)?;
|
||||||
|
|||||||
+6
-52
@@ -22,7 +22,6 @@ extern crate num_cpus;
|
|||||||
extern crate plume_api;
|
extern crate plume_api;
|
||||||
extern crate plume_common;
|
extern crate plume_common;
|
||||||
extern crate plume_models;
|
extern crate plume_models;
|
||||||
extern crate reqwest;
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate rocket;
|
extern crate rocket;
|
||||||
extern crate rocket_contrib;
|
extern crate rocket_contrib;
|
||||||
@@ -43,21 +42,17 @@ extern crate webfinger;
|
|||||||
use clap::App;
|
use clap::App;
|
||||||
use diesel::r2d2::ConnectionManager;
|
use diesel::r2d2::ConnectionManager;
|
||||||
use plume_models::{
|
use plume_models::{
|
||||||
blogs::Blog,
|
|
||||||
blogs::Host,
|
|
||||||
db_conn::{DbPool, PragmaForeignKey},
|
db_conn::{DbPool, PragmaForeignKey},
|
||||||
instance::Instance,
|
instance::Instance,
|
||||||
migrations::IMPORTED_MIGRATIONS,
|
migrations::IMPORTED_MIGRATIONS,
|
||||||
search::{Searcher as UnmanagedSearcher, SearcherError},
|
search::{Searcher as UnmanagedSearcher, SearcherError},
|
||||||
Connection, Error, CONFIG,
|
Connection, Error, CONFIG,
|
||||||
};
|
};
|
||||||
use rocket::{fairing::AdHoc, http::ext::IntoOwned, http::uri::Origin};
|
|
||||||
use rocket_csrf::CsrfFairingBuilder;
|
use rocket_csrf::CsrfFairingBuilder;
|
||||||
use scheduled_thread_pool::ScheduledThreadPool;
|
use scheduled_thread_pool::ScheduledThreadPool;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::Duration;
|
||||||
|
|
||||||
init_i18n!(
|
init_i18n!(
|
||||||
"plume", ar, bg, ca, cs, de, en, eo, es, fr, gl, hi, hr, it, ja, nb, pl, pt, ro, ru, sr, sk, sv
|
"plume", ar, bg, ca, cs, de, en, eo, es, fr, gl, hi, hr, it, ja, nb, pl, pt, ro, ru, sr, sk, sv
|
||||||
@@ -92,7 +87,6 @@ fn init_pool() -> Option<DbPool> {
|
|||||||
.build(manager)
|
.build(manager)
|
||||||
.ok()?;
|
.ok()?;
|
||||||
Instance::cache_local(&pool.get().unwrap());
|
Instance::cache_local(&pool.get().unwrap());
|
||||||
Blog::cache_custom_domains(&pool.get().unwrap());
|
|
||||||
Some(pool)
|
Some(pool)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,42 +175,7 @@ Then try to restart Plume
|
|||||||
println!("Please refer to the documentation to see how to configure it.");
|
println!("Please refer to the documentation to see how to configure it.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let custom_domain_fairing = AdHoc::on_request("Custom Blog Domains", |req, _data| {
|
|
||||||
let host = req.guard::<Host>();
|
|
||||||
if host.is_success()
|
|
||||||
&& req
|
|
||||||
.uri()
|
|
||||||
.segments()
|
|
||||||
.next()
|
|
||||||
.map(|path| path != "static" && path != "api")
|
|
||||||
.unwrap_or(true)
|
|
||||||
{
|
|
||||||
let rewrite_uri = format!("/custom_domains/{}/{}", host.unwrap(), req.uri());
|
|
||||||
let uri = Origin::parse_owned(rewrite_uri).unwrap();
|
|
||||||
let uri = uri.to_normalized().into_owned();
|
|
||||||
req.set_uri(uri);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let valid_domains: HashMap<String, Instant> = HashMap::new();
|
|
||||||
let rocket = rocket::custom(CONFIG.rocket.clone().unwrap())
|
let rocket = rocket::custom(CONFIG.rocket.clone().unwrap())
|
||||||
.mount(
|
|
||||||
"/custom_domains/domain_validation/",
|
|
||||||
routes![routes::blogs::custom::domain_validation,],
|
|
||||||
)
|
|
||||||
.mount(
|
|
||||||
"/domain_validation/",
|
|
||||||
routes![routes::blogs::domain_validation,],
|
|
||||||
)
|
|
||||||
.mount(
|
|
||||||
"/custom_domains/",
|
|
||||||
routes![
|
|
||||||
routes::blogs::custom::details,
|
|
||||||
routes::posts::custom::details,
|
|
||||||
routes::blogs::custom::activity_details,
|
|
||||||
routes::search::custom::search,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.mount(
|
.mount(
|
||||||
"/",
|
"/",
|
||||||
routes![
|
routes![
|
||||||
@@ -316,6 +275,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::update,
|
||||||
api::posts::delete,
|
api::posts::delete,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -329,7 +289,6 @@ Then try to restart Plume
|
|||||||
.manage(dbpool)
|
.manage(dbpool)
|
||||||
.manage(Arc::new(workpool))
|
.manage(Arc::new(workpool))
|
||||||
.manage(searcher)
|
.manage(searcher)
|
||||||
.manage(Mutex::new(valid_domains))
|
|
||||||
.manage(include_i18n!())
|
.manage(include_i18n!())
|
||||||
.attach(
|
.attach(
|
||||||
CsrfFairingBuilder::new()
|
CsrfFairingBuilder::new()
|
||||||
@@ -341,23 +300,18 @@ Then try to restart Plume
|
|||||||
(
|
(
|
||||||
"/inbox".to_owned(),
|
"/inbox".to_owned(),
|
||||||
"/inbox".to_owned(),
|
"/inbox".to_owned(),
|
||||||
rocket::http::Method::Post,
|
Some(rocket::http::Method::Post),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"/@/<name>/inbox".to_owned(),
|
"/@/<name>/inbox".to_owned(),
|
||||||
"/@/<name>/inbox".to_owned(),
|
"/@/<name>/inbox".to_owned(),
|
||||||
rocket::http::Method::Post,
|
Some(rocket::http::Method::Post),
|
||||||
),
|
|
||||||
(
|
|
||||||
"/api/<path..>".to_owned(),
|
|
||||||
"/api/<path..>".to_owned(),
|
|
||||||
rocket::http::Method::Post,
|
|
||||||
),
|
),
|
||||||
|
("/api/<path..>".to_owned(), "/api/<path..>".to_owned(), None),
|
||||||
])
|
])
|
||||||
.finalize()
|
.finalize()
|
||||||
.expect("main: csrf fairing creation error"),
|
.expect("main: csrf fairing creation error"),
|
||||||
)
|
);
|
||||||
.attach(custom_domain_fairing);
|
|
||||||
|
|
||||||
#[cfg(feature = "test")]
|
#[cfg(feature = "test")]
|
||||||
let rocket = rocket.mount("/test", routes![test_routes::health,]);
|
let rocket = rocket.mount("/test", routes![test_routes::health,]);
|
||||||
|
|||||||
+12
-174
@@ -2,14 +2,11 @@ use activitypub::collection::OrderedCollection;
|
|||||||
use atom_syndication::{Entry, FeedBuilder};
|
use atom_syndication::{Entry, FeedBuilder};
|
||||||
use diesel::SaveChangesDsl;
|
use diesel::SaveChangesDsl;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::{ContentType, Status},
|
http::ContentType,
|
||||||
request::LenientForm,
|
request::LenientForm,
|
||||||
response::{content::Content, Flash, Redirect},
|
response::{content::Content, Flash, Redirect},
|
||||||
State,
|
|
||||||
};
|
};
|
||||||
use rocket_i18n::I18n;
|
use rocket_i18n::I18n;
|
||||||
use std::sync::Mutex;
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
use std::{borrow::Cow, collections::HashMap};
|
use std::{borrow::Cow, collections::HashMap};
|
||||||
use validator::{Validate, ValidationError, ValidationErrors};
|
use validator::{Validate, ValidationError, ValidationErrors};
|
||||||
|
|
||||||
@@ -19,17 +16,14 @@ use plume_models::{
|
|||||||
blog_authors::*, blogs::*, instance::Instance, medias::*, posts::Post, safe_string::SafeString,
|
blog_authors::*, blogs::*, instance::Instance, medias::*, posts::Post, safe_string::SafeString,
|
||||||
users::User, Connection, PlumeRocket,
|
users::User, Connection, PlumeRocket,
|
||||||
};
|
};
|
||||||
use reqwest::Client;
|
|
||||||
use routes::{errors::ErrorPage, Page, RespondOrRedirect};
|
use routes::{errors::ErrorPage, Page, RespondOrRedirect};
|
||||||
use template_utils::{IntoContext, Ructe};
|
use template_utils::{IntoContext, Ructe};
|
||||||
|
|
||||||
fn detail_guts(
|
#[get("/~/<name>?<page>", rank = 2)]
|
||||||
blog: Blog,
|
pub fn details(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||||
page: Option<Page>,
|
|
||||||
rockets: PlumeRocket,
|
|
||||||
) -> Result<RespondOrRedirect, ErrorPage> {
|
|
||||||
let page = page.unwrap_or_default();
|
let page = page.unwrap_or_default();
|
||||||
let conn = &*rockets.conn;
|
let conn = &*rockets.conn;
|
||||||
|
let blog = Blog::find_by_fqn(&rockets, &name)?;
|
||||||
let posts = Post::blog_page(conn, &blog, page.limits())?;
|
let posts = Post::blog_page(conn, &blog, page.limits())?;
|
||||||
let articles_count = Post::count_for_blog(conn, &blog)?;
|
let articles_count = Post::count_for_blog(conn, &blog)?;
|
||||||
let authors = &blog.list_authors(conn)?;
|
let authors = &blog.list_authors(conn)?;
|
||||||
@@ -41,43 +35,7 @@ fn detail_guts(
|
|||||||
page.0,
|
page.0,
|
||||||
Page::total(articles_count as i32),
|
Page::total(articles_count as i32),
|
||||||
posts
|
posts
|
||||||
))
|
)))
|
||||||
.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/~/<name>?<page>", rank = 2)]
|
|
||||||
pub fn details(
|
|
||||||
name: String,
|
|
||||||
page: Option<Page>,
|
|
||||||
rockets: PlumeRocket,
|
|
||||||
) -> Result<RespondOrRedirect, ErrorPage> {
|
|
||||||
let blog = Blog::find_by_fqn(&rockets, &name)?;
|
|
||||||
|
|
||||||
// check this first, and return early
|
|
||||||
// doing this prevents partially moving `blog` into the `match (tuple)`,
|
|
||||||
// which makes it impossible to reuse then.
|
|
||||||
if blog.custom_domain == None {
|
|
||||||
return detail_guts(blog, page, rockets);
|
|
||||||
}
|
|
||||||
|
|
||||||
match (blog.custom_domain, page) {
|
|
||||||
(Some(ref custom_domain), Some(ref page)) => {
|
|
||||||
Ok(Redirect::to(format!("https://{}/?page={}", custom_domain, page)).into())
|
|
||||||
}
|
|
||||||
(Some(ref custom_domain), _) => {
|
|
||||||
Ok(Redirect::to(format!("https://{}/", custom_domain)).into())
|
|
||||||
}
|
|
||||||
// we need this match arm, or the match won't compile
|
|
||||||
(None, _) => unreachable!("This code path should have already been handled!"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn activity_detail_guts(
|
|
||||||
blog: Blog,
|
|
||||||
rockets: PlumeRocket,
|
|
||||||
_ap: ApRequest,
|
|
||||||
) -> Option<ActivityStream<CustomGroup>> {
|
|
||||||
Some(ActivityStream::new(blog.to_activity(&*rockets.conn).ok()?))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/~/<name>", rank = 1)]
|
#[get("/~/<name>", rank = 1)]
|
||||||
@@ -87,7 +45,7 @@ pub fn activity_details(
|
|||||||
_ap: ApRequest,
|
_ap: ApRequest,
|
||||||
) -> Option<ActivityStream<CustomGroup>> {
|
) -> Option<ActivityStream<CustomGroup>> {
|
||||||
let blog = Blog::find_by_fqn(&rockets, &name).ok()?;
|
let blog = Blog::find_by_fqn(&rockets, &name).ok()?;
|
||||||
activity_detail_guts(blog, rockets, _ap)
|
Some(ActivityStream::new(blog.to_activity(&*rockets.conn).ok()?))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/blogs/new")]
|
#[get("/blogs/new")]
|
||||||
@@ -99,76 +57,6 @@ pub fn new(rockets: PlumeRocket, _user: User) -> Ructe {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// mounted as /domain_validation/
|
|
||||||
#[get("/<validation_id>")]
|
|
||||||
pub fn domain_validation(
|
|
||||||
validation_id: String,
|
|
||||||
valid_domains: State<Mutex<HashMap<String, Instant>>>,
|
|
||||||
) -> Status {
|
|
||||||
let mutex = valid_domains.inner().lock();
|
|
||||||
let mut validation_map = mutex.unwrap();
|
|
||||||
let validation_getter = validation_map.clone();
|
|
||||||
|
|
||||||
let value = validation_getter.get(&validation_id);
|
|
||||||
if value.is_none() {
|
|
||||||
// validation id not found
|
|
||||||
return Status::NotFound;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we have valid id, now check the time
|
|
||||||
let valid_until = value.unwrap();
|
|
||||||
let now = Instant::now();
|
|
||||||
|
|
||||||
// nope, expired (410: gone)
|
|
||||||
if now.duration_since(*valid_until).as_secs() > 0 {
|
|
||||||
validation_map.remove(&validation_id);
|
|
||||||
// validation expired
|
|
||||||
return Status::Gone;
|
|
||||||
}
|
|
||||||
|
|
||||||
validation_map.remove(&validation_id);
|
|
||||||
Status::Ok
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod custom {
|
|
||||||
use plume_common::activity_pub::{ActivityStream, ApRequest};
|
|
||||||
use plume_models::{blogs::Blog, blogs::CustomGroup, blogs::Host, PlumeRocket};
|
|
||||||
use rocket::{http::Status, State};
|
|
||||||
use routes::{errors::ErrorPage, Page, RespondOrRedirect};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
#[get("/<custom_domain>?<page>", rank = 2)]
|
|
||||||
pub fn details(
|
|
||||||
custom_domain: String,
|
|
||||||
page: Option<Page>,
|
|
||||||
rockets: PlumeRocket,
|
|
||||||
) -> Result<RespondOrRedirect, ErrorPage> {
|
|
||||||
let blog = Blog::find_by_host(&rockets, Host::new(custom_domain))?;
|
|
||||||
super::detail_guts(blog, page, rockets)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/<custom_domain>", rank = 1)]
|
|
||||||
pub fn activity_details(
|
|
||||||
custom_domain: String,
|
|
||||||
rockets: PlumeRocket,
|
|
||||||
_ap: ApRequest,
|
|
||||||
) -> Option<ActivityStream<CustomGroup>> {
|
|
||||||
let blog = Blog::find_by_host(&rockets, Host::new(custom_domain)).ok()?;
|
|
||||||
super::activity_detail_guts(blog, rockets, _ap)
|
|
||||||
}
|
|
||||||
|
|
||||||
// mounted as /custom_domains/domain_validation/
|
|
||||||
#[get("/<validation_id>")]
|
|
||||||
pub fn domain_validation(
|
|
||||||
validation_id: String,
|
|
||||||
valid_domains: State<Mutex<HashMap<String, Instant>>>,
|
|
||||||
) -> Status {
|
|
||||||
super::domain_validation(validation_id, valid_domains)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/blogs/new", rank = 2)]
|
#[get("/blogs/new", rank = 2)]
|
||||||
pub fn new_auth(i18n: I18n) -> Flash<Redirect> {
|
pub fn new_auth(i18n: I18n) -> Flash<Redirect> {
|
||||||
utils::requires_login(
|
utils::requires_login(
|
||||||
@@ -184,7 +72,6 @@ pub fn new_auth(i18n: I18n) -> Flash<Redirect> {
|
|||||||
pub struct NewBlogForm {
|
pub struct NewBlogForm {
|
||||||
#[validate(custom(function = "valid_slug", message = "Invalid name"))]
|
#[validate(custom(function = "valid_slug", message = "Invalid name"))]
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub custom_domain: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn valid_slug(title: &str) -> Result<(), ValidationError> {
|
fn valid_slug(title: &str) -> Result<(), ValidationError> {
|
||||||
@@ -196,43 +83,13 @@ fn valid_slug(title: &str) -> Result<(), ValidationError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn valid_domain(domain: &str, valid_domains: State<Mutex<HashMap<String, Instant>>>) -> bool {
|
|
||||||
let mutex = valid_domains.inner().lock();
|
|
||||||
let mut validation_map = mutex.unwrap();
|
|
||||||
|
|
||||||
let random_id = utils::random_hex();
|
|
||||||
validation_map.insert(
|
|
||||||
random_id.clone(),
|
|
||||||
Instant::now().checked_add(Duration::new(60, 0)).unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let client = Client::new();
|
|
||||||
let validation_uri = format!("https://{}/domain_validation/{}", domain, random_id);
|
|
||||||
|
|
||||||
match client.get(&validation_uri).send() {
|
|
||||||
Ok(resp) => resp.status().is_success(),
|
|
||||||
Err(_) => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/blogs/new", data = "<form>")]
|
#[post("/blogs/new", data = "<form>")]
|
||||||
pub fn create(
|
pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> RespondOrRedirect {
|
||||||
form: LenientForm<NewBlogForm>,
|
|
||||||
rockets: PlumeRocket,
|
|
||||||
valid_domains: State<Mutex<HashMap<String, Instant>>>,
|
|
||||||
) -> RespondOrRedirect {
|
|
||||||
let slug = utils::make_actor_id(&form.title);
|
let slug = utils::make_actor_id(&form.title);
|
||||||
let conn = &*rockets.conn;
|
let conn = &*rockets.conn;
|
||||||
let intl = &rockets.intl.catalog;
|
let intl = &rockets.intl.catalog;
|
||||||
let user = rockets.user.clone().unwrap();
|
let user = rockets.user.clone().unwrap();
|
||||||
|
|
||||||
let (custom_domain, dns_ok) = if form.custom_domain.is_empty() {
|
|
||||||
(None, true)
|
|
||||||
} else {
|
|
||||||
let dns_check = valid_domain(&form.custom_domain.clone(), valid_domains);
|
|
||||||
(Some(Host::new(form.custom_domain.clone())), dns_check)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut errors = match form.validate() {
|
let mut errors = match form.validate() {
|
||||||
Ok(_) => ValidationErrors::new(),
|
Ok(_) => ValidationErrors::new(),
|
||||||
Err(e) => e,
|
Err(e) => e,
|
||||||
@@ -264,7 +121,6 @@ pub fn create(
|
|||||||
Instance::get_local()
|
Instance::get_local()
|
||||||
.expect("blog::create: instance error")
|
.expect("blog::create: instance error")
|
||||||
.id,
|
.id,
|
||||||
custom_domain,
|
|
||||||
)
|
)
|
||||||
.expect("blog::create: new local error"),
|
.expect("blog::create: new local error"),
|
||||||
)
|
)
|
||||||
@@ -280,19 +136,11 @@ pub fn create(
|
|||||||
)
|
)
|
||||||
.expect("blog::create: author error");
|
.expect("blog::create: author error");
|
||||||
|
|
||||||
if dns_ok {
|
Flash::success(
|
||||||
Flash::success(
|
Redirect::to(uri!(details: name = slug.clone(), page = _)),
|
||||||
Redirect::to(uri!(details: name = slug.clone(), page = _)),
|
&i18n!(intl, "Your blog was successfully created!"),
|
||||||
&i18n!(intl, "Your blog was successfully created!"),
|
)
|
||||||
)
|
.into()
|
||||||
.into()
|
|
||||||
} else {
|
|
||||||
Flash::warning(
|
|
||||||
Redirect::to(uri!(details: name = slug.clone(), page = _)),
|
|
||||||
&i18n!(intl, "Your blog was successfully created, but the custom domain seems invalid. Please check it is correct from your blog's settings."),
|
|
||||||
)
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/~/<name>/delete")]
|
#[post("/~/<name>/delete")]
|
||||||
@@ -333,7 +181,6 @@ pub struct EditForm {
|
|||||||
pub summary: String,
|
pub summary: String,
|
||||||
pub icon: Option<i32>,
|
pub icon: Option<i32>,
|
||||||
pub banner: Option<i32>,
|
pub banner: Option<i32>,
|
||||||
pub custom_domain: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/~/<name>/edit")]
|
#[get("/~/<name>/edit")]
|
||||||
@@ -351,10 +198,6 @@ pub fn edit(name: String, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
|||||||
.clone()
|
.clone()
|
||||||
.expect("blogs::edit: User was None while it shouldn't");
|
.expect("blogs::edit: User was None while it shouldn't");
|
||||||
let medias = Media::for_user(conn, user.id).expect("Couldn't list media");
|
let medias = Media::for_user(conn, user.id).expect("Couldn't list media");
|
||||||
let custom_domain = match blog.custom_domain {
|
|
||||||
Some(ref c) => c.to_string(),
|
|
||||||
_ => String::from(""),
|
|
||||||
};
|
|
||||||
Ok(render!(blogs::edit(
|
Ok(render!(blogs::edit(
|
||||||
&rockets.to_context(),
|
&rockets.to_context(),
|
||||||
&blog,
|
&blog,
|
||||||
@@ -364,7 +207,6 @@ pub fn edit(name: String, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
|||||||
summary: blog.summary.clone(),
|
summary: blog.summary.clone(),
|
||||||
icon: blog.icon_id,
|
icon: blog.icon_id,
|
||||||
banner: blog.banner_id,
|
banner: blog.banner_id,
|
||||||
custom_domain: custom_domain,
|
|
||||||
},
|
},
|
||||||
ValidationErrors::default()
|
ValidationErrors::default()
|
||||||
)))
|
)))
|
||||||
@@ -476,10 +318,6 @@ pub fn update(
|
|||||||
);
|
);
|
||||||
blog.icon_id = form.icon;
|
blog.icon_id = form.icon;
|
||||||
blog.banner_id = form.banner;
|
blog.banner_id = form.banner;
|
||||||
if !form.custom_domain.is_empty() {
|
|
||||||
blog.custom_domain = Some(Host::new(form.custom_domain.clone()))
|
|
||||||
}
|
|
||||||
|
|
||||||
blog.save_changes::<Blog>(&*conn)
|
blog.save_changes::<Blog>(&*conn)
|
||||||
.expect("Couldn't save blog changes");
|
.expect("Couldn't save blog changes");
|
||||||
Ok(Flash::success(
|
Ok(Flash::success(
|
||||||
|
|||||||
+4
-21
@@ -1,6 +1,6 @@
|
|||||||
use plume_models::{instance::Instance, Error, PlumeRocket};
|
use plume_models::{Error, PlumeRocket};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
response::{self, Redirect, Responder},
|
response::{self, Responder},
|
||||||
Request,
|
Request,
|
||||||
};
|
};
|
||||||
use template_utils::{IntoContext, Ructe};
|
use template_utils::{IntoContext, Ructe};
|
||||||
@@ -29,26 +29,9 @@ impl<'r> Responder<'r> for ErrorPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[catch(404)]
|
#[catch(404)]
|
||||||
pub fn not_found(req: &Request) -> Result<Ructe, Redirect> {
|
pub fn not_found(req: &Request) -> Ructe {
|
||||||
let rockets = req.guard::<PlumeRocket>().unwrap();
|
let rockets = req.guard::<PlumeRocket>().unwrap();
|
||||||
if req
|
render!(errors::not_found(&rockets.to_context()))
|
||||||
.uri()
|
|
||||||
.segments()
|
|
||||||
.next()
|
|
||||||
.map(|path| path == "custom_domains")
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
let path = req
|
|
||||||
.uri()
|
|
||||||
.segments()
|
|
||||||
.skip(2)
|
|
||||||
.collect::<Vec<&str>>()
|
|
||||||
.join("/");
|
|
||||||
let public_domain = Instance::get_local().unwrap().public_domain;
|
|
||||||
Err(Redirect::to(format!("https://{}/{}", public_domain, path)))
|
|
||||||
} else {
|
|
||||||
Ok(render!(errors::not_found(&rockets.to_context())))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[catch(422)]
|
#[catch(422)]
|
||||||
|
|||||||
+1
-8
@@ -10,7 +10,6 @@ use rocket::{
|
|||||||
response::{Flash, NamedFile, Redirect},
|
response::{Flash, NamedFile, Redirect},
|
||||||
Outcome,
|
Outcome,
|
||||||
};
|
};
|
||||||
use std::fmt;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use template_utils::Ructe;
|
use template_utils::Ructe;
|
||||||
|
|
||||||
@@ -53,15 +52,9 @@ impl From<Flash<Redirect>> for RespondOrRedirect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Shrinkwrap, Copy, Clone, UriDisplayQuery)]
|
#[derive(Shrinkwrap, Copy, Clone, UriDisplayQuery)]
|
||||||
pub struct Page(i32);
|
pub struct Page(i32);
|
||||||
|
|
||||||
impl fmt::Display for Page {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'v> FromFormValue<'v> for Page {
|
impl<'v> FromFormValue<'v> for Page {
|
||||||
type Error = &'v RawStr;
|
type Error = &'v RawStr;
|
||||||
fn from_form_value(form_value: &'v RawStr) -> Result<Page, &'v RawStr> {
|
fn from_form_value(form_value: &'v RawStr) -> Result<Page, &'v RawStr> {
|
||||||
|
|||||||
+32
-89
@@ -13,6 +13,7 @@ use validator::{Validate, ValidationError, ValidationErrors};
|
|||||||
use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest};
|
use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest};
|
||||||
use plume_common::utils;
|
use plume_common::utils;
|
||||||
use plume_models::{
|
use plume_models::{
|
||||||
|
api_tokens::ApiToken,
|
||||||
blogs::*,
|
blogs::*,
|
||||||
comments::{Comment, CommentTree},
|
comments::{Comment, CommentTree},
|
||||||
inbox::inbox,
|
inbox::inbox,
|
||||||
@@ -31,14 +32,28 @@ use routes::{
|
|||||||
};
|
};
|
||||||
use template_utils::{IntoContext, Ructe};
|
use template_utils::{IntoContext, Ructe};
|
||||||
|
|
||||||
fn detail_guts(
|
#[get("/~/<blog>/<slug>?<responding_to>", rank = 4)]
|
||||||
blog: &Blog,
|
pub fn details(
|
||||||
post: &Post,
|
blog: String,
|
||||||
|
slug: String,
|
||||||
responding_to: Option<i32>,
|
responding_to: Option<i32>,
|
||||||
rockets: &PlumeRocket,
|
rockets: PlumeRocket,
|
||||||
) -> Result<RespondOrRedirect, ErrorPage> {
|
) -> Result<Ructe, ErrorPage> {
|
||||||
let conn = &*rockets.conn;
|
let conn = &*rockets.conn;
|
||||||
let user = rockets.user.clone();
|
let user = rockets.user.clone();
|
||||||
|
let blog = Blog::find_by_fqn(&rockets, &blog)?;
|
||||||
|
let post = Post::find_by_slug(&*conn, &slug, blog.id)?;
|
||||||
|
if !(post.published
|
||||||
|
|| post
|
||||||
|
.get_authors(&*conn)?
|
||||||
|
.into_iter()
|
||||||
|
.any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0)))
|
||||||
|
{
|
||||||
|
return Ok(render!(errors::not_authorized(
|
||||||
|
&rockets.to_context(),
|
||||||
|
i18n!(rockets.intl.catalog, "This post isn't published yet.")
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
let comments = CommentTree::from_post(&*conn, &post, user.as_ref())?;
|
let comments = CommentTree::from_post(&*conn, &post, user.as_ref())?;
|
||||||
|
|
||||||
@@ -47,7 +62,7 @@ fn detail_guts(
|
|||||||
Ok(render!(posts::details(
|
Ok(render!(posts::details(
|
||||||
&rockets.to_context(),
|
&rockets.to_context(),
|
||||||
post.clone(),
|
post.clone(),
|
||||||
blog.clone(),
|
blog,
|
||||||
&NewCommentForm {
|
&NewCommentForm {
|
||||||
warning: previous.clone().map(|p| p.spoiler_text).unwrap_or_default(),
|
warning: previous.clone().map(|p| p.spoiler_text).unwrap_or_default(),
|
||||||
content: previous.clone().and_then(|p| Some(format!(
|
content: previous.clone().and_then(|p| Some(format!(
|
||||||
@@ -80,85 +95,7 @@ fn detail_guts(
|
|||||||
user.clone().and_then(|u| u.has_reshared(&*conn, &post).ok()).unwrap_or(false),
|
user.clone().and_then(|u| u.has_reshared(&*conn, &post).ok()).unwrap_or(false),
|
||||||
user.and_then(|u| u.is_following(&*conn, post.get_authors(&*conn).ok()?[0].id).ok()).unwrap_or(false),
|
user.and_then(|u| u.is_following(&*conn, post.get_authors(&*conn).ok()?[0].id).ok()).unwrap_or(false),
|
||||||
post.get_authors(&*conn)?[0].clone()
|
post.get_authors(&*conn)?[0].clone()
|
||||||
)).into())
|
)))
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/~/<blog>/<slug>?<responding_to>", rank = 4)]
|
|
||||||
pub fn details(
|
|
||||||
blog: String,
|
|
||||||
slug: String,
|
|
||||||
responding_to: Option<i32>,
|
|
||||||
rockets: PlumeRocket,
|
|
||||||
) -> Result<RespondOrRedirect, ErrorPage> {
|
|
||||||
let conn = &*rockets.conn;
|
|
||||||
let user = rockets.user.clone();
|
|
||||||
let blog = Blog::find_by_fqn(&rockets, &blog)?;
|
|
||||||
let post = Post::find_by_slug(&*conn, &slug, blog.id)?;
|
|
||||||
|
|
||||||
if !(post.published
|
|
||||||
|| post
|
|
||||||
.get_authors(&*conn)?
|
|
||||||
.into_iter()
|
|
||||||
.any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0)))
|
|
||||||
{
|
|
||||||
return Ok(render!(errors::not_authorized(
|
|
||||||
&rockets.to_context(),
|
|
||||||
i18n!(rockets.intl.catalog, "This post isn't published yet.")
|
|
||||||
))
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// check this first, and return early
|
|
||||||
// doing this prevents partially moving `blog` into the `match (tuple)`,
|
|
||||||
// which makes it impossible to reuse then.
|
|
||||||
if blog.custom_domain == None {
|
|
||||||
return detail_guts(&blog, &post, responding_to, &rockets);
|
|
||||||
}
|
|
||||||
|
|
||||||
match (blog.custom_domain, responding_to) {
|
|
||||||
(Some(ref custom_domain), Some(ref responding_to)) => Ok(Redirect::to(format!(
|
|
||||||
"https://{}/{}?responding_to={}",
|
|
||||||
custom_domain, slug, responding_to
|
|
||||||
))
|
|
||||||
.into()),
|
|
||||||
(Some(ref custom_domain), _) => {
|
|
||||||
Ok(Redirect::to(format!("https://{}/{}", custom_domain, slug)).into())
|
|
||||||
}
|
|
||||||
(None, _) => unreachable!("This code path should have already been handled!"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod custom {
|
|
||||||
use plume_models::{blogs::Blog, blogs::Host, posts::Post, PlumeRocket};
|
|
||||||
use routes::{errors::ErrorPage, RespondOrRedirect};
|
|
||||||
use template_utils::{IntoContext, Ructe};
|
|
||||||
|
|
||||||
#[get("/<custom_domain>/<slug>?<responding_to>", rank = 4)]
|
|
||||||
pub fn details(
|
|
||||||
custom_domain: String,
|
|
||||||
slug: String,
|
|
||||||
responding_to: Option<i32>,
|
|
||||||
rockets: PlumeRocket,
|
|
||||||
) -> Result<RespondOrRedirect, ErrorPage> {
|
|
||||||
let conn = &*rockets.conn;
|
|
||||||
let user = rockets.user.clone();
|
|
||||||
let blog = Blog::find_by_host(&rockets, Host::new(custom_domain))?;
|
|
||||||
let post = Post::find_by_slug(&*conn, &slug, blog.id)?;
|
|
||||||
if !(post.published
|
|
||||||
|| post
|
|
||||||
.get_authors(&*conn)?
|
|
||||||
.into_iter()
|
|
||||||
.any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0)))
|
|
||||||
{
|
|
||||||
return Ok(render!(errors::not_authorized(
|
|
||||||
&rockets.to_context(),
|
|
||||||
i18n!(rockets.intl.catalog, "This post isn't published yet.")
|
|
||||||
))
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
super::detail_guts(&blog, &post, responding_to, &rockets)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/~/<blog>/<slug>", rank = 3)]
|
#[get("/~/<blog>/<slug>", rank = 3)]
|
||||||
@@ -220,7 +157,8 @@ pub fn new(blog: String, cl: ContentLen, rockets: PlumeRocket) -> Result<Ructe,
|
|||||||
None,
|
None,
|
||||||
ValidationErrors::default(),
|
ValidationErrors::default(),
|
||||||
medias,
|
medias,
|
||||||
cl.0
|
cl.0,
|
||||||
|
ApiToken::web_token(&*conn, user.id)?.value
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +212,8 @@ pub fn edit(
|
|||||||
Some(post),
|
Some(post),
|
||||||
ValidationErrors::default(),
|
ValidationErrors::default(),
|
||||||
medias,
|
medias,
|
||||||
cl.0
|
cl.0,
|
||||||
|
ApiToken::web_token(&*conn, user.id)?.value
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,7 +369,10 @@ pub fn update(
|
|||||||
Some(post),
|
Some(post),
|
||||||
errors.clone(),
|
errors.clone(),
|
||||||
medias.clone(),
|
medias.clone(),
|
||||||
cl.0
|
cl.0,
|
||||||
|
ApiToken::web_token(&*conn, user.id)
|
||||||
|
.expect("The default API token cannot be retrieved")
|
||||||
|
.value
|
||||||
))
|
))
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
@@ -614,7 +556,8 @@ pub fn create(
|
|||||||
None,
|
None,
|
||||||
errors.clone(),
|
errors.clone(),
|
||||||
medias,
|
medias,
|
||||||
cl.0
|
cl.0,
|
||||||
|
ApiToken::web_token(&*conn, user.id)?.value
|
||||||
))
|
))
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-21
@@ -49,7 +49,8 @@ macro_rules! param_to_query {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn search_guts(query: Option<Form<SearchQuery>>, rockets: PlumeRocket) -> Ructe {
|
#[get("/search?<query..>")]
|
||||||
|
pub fn search(query: Option<Form<SearchQuery>>, rockets: PlumeRocket) -> Ructe {
|
||||||
let conn = &*rockets.conn;
|
let conn = &*rockets.conn;
|
||||||
let query = query.map(Form::into_inner).unwrap_or_default();
|
let query = query.map(Form::into_inner).unwrap_or_default();
|
||||||
let page = query.page.unwrap_or_default();
|
let page = query.page.unwrap_or_default();
|
||||||
@@ -82,23 +83,3 @@ fn search_guts(query: Option<Form<SearchQuery>>, rockets: PlumeRocket) -> Ructe
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/search?<query..>")]
|
|
||||||
pub fn search(query: Option<Form<SearchQuery>>, rockets: PlumeRocket) -> Ructe {
|
|
||||||
search_guts(query, rockets)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod custom {
|
|
||||||
use plume_models::PlumeRocket;
|
|
||||||
use rocket::request::Form;
|
|
||||||
use template_utils::Ructe;
|
|
||||||
|
|
||||||
#[get("/<_custom_domain>/search?<query..>")]
|
|
||||||
pub fn search(
|
|
||||||
_custom_domain: String,
|
|
||||||
query: Option<Form<super::SearchQuery>>,
|
|
||||||
rockets: PlumeRocket,
|
|
||||||
) -> Ructe {
|
|
||||||
super::search_guts(query, rockets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-101
@@ -1,10 +1,7 @@
|
|||||||
use plume_models::{notifications::*, users::User, Connection, PlumeRocket};
|
use plume_models::{notifications::*, users::User, Connection, PlumeRocket};
|
||||||
|
|
||||||
use rocket::http::hyper::header::{ETag, EntityTag};
|
use rocket::http::hyper::header::{ETag, EntityTag};
|
||||||
use rocket::http::{
|
use rocket::http::{Method, Status};
|
||||||
uri::{FromUriParam, Query},
|
|
||||||
Method, Status,
|
|
||||||
};
|
|
||||||
use rocket::request::Request;
|
use rocket::request::Request;
|
||||||
use rocket::response::{self, content::Html as HtmlCt, Responder, Response};
|
use rocket::response::{self, content::Html as HtmlCt, Responder, Response};
|
||||||
use rocket_i18n::Catalog;
|
use rocket_i18n::Catalog;
|
||||||
@@ -16,16 +13,6 @@ pub use askama_escape::escape;
|
|||||||
|
|
||||||
pub static CACHE_NAME: &str = env!("CACHE_ID");
|
pub static CACHE_NAME: &str = env!("CACHE_ID");
|
||||||
|
|
||||||
pub struct NoValue; // workarround for missing FromUriParam implementation for Option
|
|
||||||
|
|
||||||
impl FromUriParam<Query, NoValue> for Option<i32> {
|
|
||||||
type Target = Option<i32>;
|
|
||||||
|
|
||||||
fn from_uri_param(_: NoValue) -> Self::Target {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type BaseContext<'a> = &'a (
|
pub type BaseContext<'a> = &'a (
|
||||||
&'a Connection,
|
&'a Connection,
|
||||||
&'a Catalog,
|
&'a Catalog,
|
||||||
@@ -355,90 +342,3 @@ macro_rules! input {
|
|||||||
))
|
))
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This macro imitate rocket's uri!, but with support for custom domains
|
|
||||||
///
|
|
||||||
/// It takes one more argument, domain, which must appear first, and must be an Option<&str>
|
|
||||||
/// sample call :
|
|
||||||
/// assuming both take the same parameters
|
|
||||||
/// url!(custom_domain=Some("something.tld"), posts::details: slug = "title", responding_to = _, blog = "blogname"));
|
|
||||||
///
|
|
||||||
/// assuming posts::details take one more parameter than posts::custom::details
|
|
||||||
/// url!(custom_domain=Some("something.tld"), posts::details:
|
|
||||||
/// common=[slug = "title", responding_to = _],
|
|
||||||
/// normal=[blog = "blogname"]));
|
|
||||||
///
|
|
||||||
/// you can also provide custom=[] for custom-domain specific arguments
|
|
||||||
/// custom_domain can be changed to anything, indicating custom domain varname in the custom-domain
|
|
||||||
/// function (most likely custom_domain or _custom_domain)
|
|
||||||
macro_rules! url {
|
|
||||||
($custom_domain:ident=$domain:expr, $module:ident::$route:ident:
|
|
||||||
common=[$($common_args:tt = $common_val:expr),*],
|
|
||||||
normal=[$($normal_args:tt = $normal_val:expr),*],
|
|
||||||
custom=[$($custom_args:tt = $custom_val:expr),*]) => {{
|
|
||||||
let domain: &Option<plume_models::blogs::Host> = &$domain; //for type inference with None
|
|
||||||
$(
|
|
||||||
let $common_args = $common_val;
|
|
||||||
)*
|
|
||||||
if let Some(domain) = domain {
|
|
||||||
$(
|
|
||||||
let $custom_args = $custom_val;
|
|
||||||
)*
|
|
||||||
let origin = uri!(crate::routes::$module::custom::$route:
|
|
||||||
$custom_domain = domain.to_string(),
|
|
||||||
$($common_args = $common_args,)*
|
|
||||||
$($custom_args = $custom_args,)*
|
|
||||||
);
|
|
||||||
let path = origin
|
|
||||||
.segments()
|
|
||||||
.skip(1)// skip is <custom_domain> part
|
|
||||||
.map(|seg| format!("/{}", seg)).collect::<String>();
|
|
||||||
let query = origin.query()
|
|
||||||
.filter(|q| !q.is_empty())
|
|
||||||
.map(|q| format!("?{}", q))
|
|
||||||
.unwrap_or_default();
|
|
||||||
format!("https://{}{}{}", &domain, path, query)
|
|
||||||
} else {
|
|
||||||
$(
|
|
||||||
let $normal_args = $normal_val;
|
|
||||||
)*
|
|
||||||
url!($module::$route:
|
|
||||||
$($common_args = $common_args,)*
|
|
||||||
$($normal_args = $normal_args,)*)
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
($cd:ident=$d:expr, $m:ident::$r:ident:
|
|
||||||
common=[$($tt:tt)*]) => {
|
|
||||||
url!($cd=$d, $m::$r: common=[$($tt)*], normal=[], custom=[])
|
|
||||||
};
|
|
||||||
($cd:ident=$d:expr, $m:ident::$r:ident:
|
|
||||||
normal=[$($tt:tt)*]) => {
|
|
||||||
url!($cd=$d, $m::$r: common=[], normal=[$($tt)*], custom=[])
|
|
||||||
};
|
|
||||||
($cd:ident=$d:expr, $m:ident::$r:ident:
|
|
||||||
custom=[$($tt:tt)*]) => {
|
|
||||||
url!($cd=$d, $m::$r: common=[], normal=[], custom=[$($tt)*])
|
|
||||||
};
|
|
||||||
($cd:ident=$d:expr, $m:ident::$r:ident:
|
|
||||||
common=[$($co:tt)*],
|
|
||||||
normal=[$($no:tt)*]) => {
|
|
||||||
url!($cd=$d, $m::$r: common=[$($co)*], normal=[$($no)*], custom=[])
|
|
||||||
};
|
|
||||||
($cd:ident=$d:expr, $m:ident::$r:ident:
|
|
||||||
common=[$($co:tt)*],
|
|
||||||
custom=[$($cu:tt)*]) => {
|
|
||||||
url!($cd=$d, $m::$r: common=[$($co)*], normal=[], custom=[$($cu)*])
|
|
||||||
};
|
|
||||||
($cd:ident=$d:expr, $m:ident::$r:ident:
|
|
||||||
normal=[$($no:tt)*],
|
|
||||||
custom=[$($cu:tt)*]) => {
|
|
||||||
url!($cd=$d, $m::$r: common=[], normal=[$($no)*], custom=[$($cu)*])
|
|
||||||
};
|
|
||||||
($custom_domain:ident=$domain:expr, $module:ident::$route:ident: $($common_args:tt)*) => {
|
|
||||||
url!($custom_domain=$domain, $module::$route: common=[$($common_args)*])
|
|
||||||
};
|
|
||||||
($module:ident::$route:ident: $($tt:tt)*) => {
|
|
||||||
uri!(crate::routes::$module::$route: $($tt)*)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
+73
-16
@@ -364,32 +364,89 @@ main .article-meta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#plume-editor {
|
#plume-editor {
|
||||||
header {
|
margin: 0;
|
||||||
|
grid: 50px 1fr / 1fr auto 20%;
|
||||||
|
min-height: 80vh;
|
||||||
|
|
||||||
|
& > header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row-reverse;
|
|
||||||
background: transparent;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
button {
|
padding: 0px 20px;
|
||||||
flex: 0 0 10em;
|
border-bottom: 1px solid $purple;
|
||||||
font-size: 1.25em;
|
max-height: 90px;
|
||||||
margin: .5em 0em .5em 1em;
|
background: $background;
|
||||||
|
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
grid-row: 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#edition-area {
|
||||||
|
margin: 0;
|
||||||
|
max-width: none;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#edition-area > * {
|
||||||
|
min-height: 1em;
|
||||||
|
outline: none;
|
||||||
|
margin-left: 20%;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
padding-right: 5%;
|
||||||
|
|
||||||
|
&:empty::before {
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
display: none;
|
||||||
|
color: transparentize($black, 0.6);
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:empty:not(:focus)::before {
|
||||||
|
display: inline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > * {
|
#edition-area > h1 {
|
||||||
min-height: 1em;
|
margin-top: 110px;
|
||||||
outline: none;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder {
|
#editor-title, #editor-subtitle, #editor-main > * {
|
||||||
color: transparentize($black, 0.6);
|
padding-left: 18px;
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
transition: border-left-color 0.1s ease-in;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-left-color: transparentize($black, 0.6);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
article {
|
aside {
|
||||||
max-width: none;
|
background: $gray;
|
||||||
min-height: 80vh;
|
margin: 0;
|
||||||
|
flex: 0 0 15%;
|
||||||
|
padding: 0 1em;
|
||||||
|
grid-row: 1 / 4;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin: 2em 0 .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-size: 1.25em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body#editor {
|
||||||
|
footer {
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
<meta content="120" property="og:image:width" />
|
<meta content="120" property="og:image:width" />
|
||||||
<meta content="120" property="og:image:height" />
|
<meta content="120" property="og:image:height" />
|
||||||
<meta content="summary" property="twitter:card" />
|
<meta content="summary" property="twitter:card" />
|
||||||
<meta content="@Instance::get_local().unwrap().name" property="og:site_name" />
|
<meta content="'@Instance::get_local().unwrap().name" property="og:site_name" />
|
||||||
<meta content="@blog.url()" property="og:url" />
|
<meta content="@blog.ap_url" property="og:url" />
|
||||||
<meta content="@blog.fqn" property="profile:username" />
|
<meta content="@blog.fqn" property="profile:username" />
|
||||||
<meta content="@blog.title" property="og:title" />
|
<meta content="@blog.title" property="og:title" />
|
||||||
<meta content="@blog.summary_html" name="description">
|
<meta content="@blog.summary_html" name="description">
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<link href='@Instance::get_local().unwrap().compute_box("~", &blog.fqn, "atom.xml")' rel='alternate' type='application/atom+xml'>
|
<link href='@Instance::get_local().unwrap().compute_box("~", &blog.fqn, "atom.xml")' rel='alternate' type='application/atom+xml'>
|
||||||
<link href='@blog.ap_url' rel='alternate' type='application/activity+json'>
|
<link href='@blog.ap_url' rel='alternate' type='application/activity+json'>
|
||||||
}, {
|
}, {
|
||||||
<a href="@url!(custom_domain = blog.custom_domain, blogs::details: common=[page = None], normal=[name = &blog.fqn])" dir="auto">@blog.title</a>
|
<a href="@uri!(blogs::details: name = &blog.fqn, page = _)" dir="auto">@blog.title</a>
|
||||||
}, {
|
}, {
|
||||||
<div class="hidden">
|
<div class="hidden">
|
||||||
@for author in authors {
|
@for author in authors {
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
<a href="@uri!(blogs::edit: name = &blog.fqn)" class="button" dir="auto">@i18n!(ctx.1, "Edit")</a>
|
<a href="@uri!(blogs::edit: name = &blog.fqn)" class="button" dir="auto">@i18n!(ctx.1, "Edit")</a>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main class="user-summary" dir="auto">
|
<main class="user-summary" dir="auto">
|
||||||
<p>
|
<p>
|
||||||
@i18n!(ctx.1, "There's one author on this blog: ", "There are {0} authors on this blog: "; authors.len())
|
@i18n!(ctx.1, "There's one author on this blog: ", "There are {0} authors on this blog: "; authors.len())
|
||||||
|
|||||||
@@ -23,8 +23,6 @@
|
|||||||
<label for="summary">@i18n!(ctx.1, "Description")<small>@i18n!(ctx.1, "Markdown syntax is supported")</small></label>
|
<label for="summary">@i18n!(ctx.1, "Description")<small>@i18n!(ctx.1, "Markdown syntax is supported")</small></label>
|
||||||
<textarea id="summary" name="summary" rows="20">@form.summary</textarea>
|
<textarea id="summary" name="summary" rows="20">@form.summary</textarea>
|
||||||
|
|
||||||
@input!(ctx.1, custom_domain (optional text), "Custom Domain", form, errors, "")
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
@i18n!(ctx.1, "You can upload images to your gallery, to use them as blog icons, or banners.")
|
@i18n!(ctx.1, "You can upload images to your gallery, to use them as blog icons, or banners.")
|
||||||
<a href="@uri!(medias::new)">@i18n!(ctx.1, "Upload images")</a>
|
<a href="@uri!(medias::new)">@i18n!(ctx.1, "Upload images")</a>
|
||||||
|
|||||||
@@ -9,10 +9,7 @@
|
|||||||
@:base(ctx, i18n!(ctx.1, "New Blog"), {}, {}, {
|
@:base(ctx, i18n!(ctx.1, "New Blog"), {}, {}, {
|
||||||
<h1 dir="auto">@i18n!(ctx.1, "Create a blog")</h1>
|
<h1 dir="auto">@i18n!(ctx.1, "Create a blog")</h1>
|
||||||
<form method="post" action="@uri!(blogs::create)">
|
<form method="post" action="@uri!(blogs::create)">
|
||||||
@input!(ctx.1, title (text), "Title", form, errors.clone(), "required minlength=\"1\"")
|
@input!(ctx.1, title (text), "Title", form, errors, "required minlength=\"1\"")
|
||||||
<input type="submit" value="@i18n!(ctx.1, "Create blog")" dir="auto"/>
|
<input type="submit" value="@i18n!(ctx.1, "Create blog")" dir="auto"/>
|
||||||
|
|
||||||
@input!(ctx.1, custom_domain (optional text), "Custom Domain", form, errors, "")
|
|
||||||
<input type="submit" value="@i18n!(ctx.1, "Make your blog available under a custom domain")" dir="auto"/>
|
|
||||||
</form>
|
</form>
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="cover" style="background-image: url('@Html(article.cover_url(ctx.0).unwrap_or_default())')"></div>
|
<div class="cover" style="background-image: url('@Html(article.cover_url(ctx.0).unwrap_or_default())')"></div>
|
||||||
}
|
}
|
||||||
<h3 class="p-name" dir="auto">
|
<h3 class="p-name" dir="auto">
|
||||||
<a class="u-url" href="@url!(custom_domain = article.get_blog(ctx.0).unwrap().custom_domain, posts::details: common=[ slug = &article.slug, responding_to = NoValue], normal=[blog = article.get_blog(ctx.0).unwrap().fqn])">
|
<a class="u-url" href="@uri!(posts::details: blog = article.get_blog(ctx.0).unwrap().fqn, slug = &article.slug, responding_to = _)">
|
||||||
@article.title
|
@article.title
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</h3>
|
||||||
@@ -25,9 +25,10 @@
|
|||||||
@if article.published {
|
@if article.published {
|
||||||
⋅ <span class="dt-published" datetime="@article.creation_date.format("%F %T")">@article.creation_date.format("%B %e, %Y")</span>
|
⋅ <span class="dt-published" datetime="@article.creation_date.format("%F %T")">@article.creation_date.format("%B %e, %Y")</span>
|
||||||
}
|
}
|
||||||
⋅ <a href="@url!(custom_domain = article.get_blog(ctx.0).unwrap().custom_domain, blogs::details: common=[page = None], normal=[name = &article.get_blog(ctx.0).unwrap().fqn])">@article.get_blog(ctx.0).unwrap().title</a>
|
⋅ <a href="@uri!(blogs::details: name = &article.get_blog(ctx.0).unwrap().fqn, page = _)">@article.get_blog(ctx.0).unwrap().title</a>
|
||||||
@if !article.published {
|
@if !article.published {
|
||||||
⋅ @i18n!(ctx.1, "Draft")
|
⋅ @i18n!(ctx.1, "Draft")
|
||||||
}
|
}
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,10 @@
|
|||||||
@if article.cover_id.is_some() {
|
@if article.cover_id.is_some() {
|
||||||
<meta property="og:image" content="@Html(article.cover_url(ctx.0).unwrap_or_default())"/>
|
<meta property="og:image" content="@Html(article.cover_url(ctx.0).unwrap_or_default())"/>
|
||||||
}
|
}
|
||||||
<meta property="og:url" content="@url!(custom_domain = blog.custom_domain, posts::details: common=[slug = &article.slug, responding_to = NoValue], normal=[blog = &blog.fqn])"/>
|
<meta property="og:url" content="@uri!(posts::details: blog = &blog.fqn, slug = &article.slug, responding_to = _)"/>
|
||||||
<meta property="og:description" content="@article.subtitle"/>
|
<meta property="og:description" content="@article.subtitle"/>
|
||||||
}, {
|
}, {
|
||||||
<a href="@url!(custom_domain = &blog.custom_domain, blogs::details: common=[page = None], normal=[name = &blog.fqn])">@blog.title</a>
|
<a href="@uri!(blogs::details: name = &blog.fqn, page = _)">@blog.title</a>
|
||||||
}, {
|
}, {
|
||||||
<div class="h-entry">
|
<div class="h-entry">
|
||||||
<header
|
<header
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
<ul class="tags" dir="auto">
|
<ul class="tags" dir="auto">
|
||||||
@for tag in tags {
|
@for tag in tags {
|
||||||
@if !tag.is_hashtag {
|
@if !tag.is_hashtag {
|
||||||
<li><a class="p-category" href="@uri!(tags::tag: name = &tag.tag, page = None)">@tag.tag</a></li>
|
<li><a class="p-category" href="@uri!(tags::tag: name = &tag.tag, page = _)">@tag.tag</a></li>
|
||||||
} else {
|
} else {
|
||||||
<span class="hidden p-category">@tag.tag</span>
|
<span class="hidden p-category">@tag.tag</span>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,22 +9,52 @@
|
|||||||
@use routes::posts::NewPostForm;
|
@use routes::posts::NewPostForm;
|
||||||
@use routes::*;
|
@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(), {}, {}, {
|
@:base(ctx, title.clone(), {}, {}, {
|
||||||
<h1 id="plume-editor-title" dir="auto">@title</h1>
|
<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">
|
<div id="plume-editor" style="display: none;" dir="auto">
|
||||||
<header>
|
<header id="editor-toolbar">
|
||||||
<button id="publish" class="button">@i18n!(ctx.1, "Publish")</button>
|
|
||||||
<p id="char-count">@content_len</p>
|
|
||||||
<a href="#" id="close-editor">@i18n!(ctx.1, "Classic editor (any changes will be lost)")</a>
|
<a href="#" id="close-editor">@i18n!(ctx.1, "Classic editor (any changes will be lost)")</a>
|
||||||
</header>
|
</header>
|
||||||
|
<article id="edition-area">
|
||||||
|
<h1 contenteditable id="editor-title" data-placeholder="@i18n!(ctx.1, "Type your title")">@form.title</h1>
|
||||||
|
<h2 contenteditable id="editor-subtitle" data-placeholder="@i18n!(ctx.1, "Type a subtitle or a summary")">@form.subtitle</h2>
|
||||||
|
<article contenteditable id="editor-main" data-placeholder="@i18n!(ctx.1, "Write your article here. You can use markdown.")"></article>
|
||||||
|
</article>
|
||||||
|
<aside id="plume-editor-aside" style="display: none;">
|
||||||
|
<div id="options-page">
|
||||||
|
<button id="publish" class="button">@i18n!(ctx.1, "Publish")</button>
|
||||||
|
|
||||||
|
@input!(ctx.1, tags (optional text), "Tags, separated by commas", form, errors.clone(), "")
|
||||||
|
@input!(ctx.1, license (optional text), "License", "Leave it empty to reserve all rights", form, errors.clone(), "")
|
||||||
|
@:image_select(ctx, "cover", i18n!(ctx.1, "Illustration"), true, medias.clone(), form.cover)
|
||||||
|
</div>
|
||||||
|
<div id="publish-page" style="display: none">
|
||||||
|
<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="save-draft" class="button secondary">@i18n!(ctx.1, "Save as draft")</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if let Some(article) = article {
|
@if let Some(article) = article {
|
||||||
<form id="plume-fallback-editor" class="new-post" method="post" action="@uri!(posts::update: blog = blog.actor_id, slug = &article.slug)" content-size="@content_len">
|
<form id="plume-fallback-editor" class="new-post" method="post" action="@uri!(posts::update: blog = blog.actor_id, slug = &article.slug)" content-size="@content_len">
|
||||||
} else {
|
} else {
|
||||||
<form id="plume-fallback-editor" class="new-post" method="post" action="@uri!(posts::new: blog = blog.actor_id)" content-size="@content_len">
|
<form id="plume-fallback-editor" class="new-post" method="post" action="@uri!(posts::new: blog = blog.actor_id)" content-size="@content_len">
|
||||||
}
|
}
|
||||||
|
<h1 id="plume-editor-title" dir="auto">@title</h1>
|
||||||
|
|
||||||
@input!(ctx.1, title (text), "Title", form, errors.clone(), "required")
|
@input!(ctx.1, title (text), "Title", form, errors.clone(), "required")
|
||||||
@input!(ctx.1, subtitle (optional text), "Subtitle", form, errors.clone(), "")
|
@input!(ctx.1, subtitle (optional text), "Subtitle", form, errors.clone(), "")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user