Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c4abcff81 | |||
| d242e6df11 | |||
| f395c1046c | |||
| df7adaf0e0 | |||
| 768f126f1d | |||
| 169469816d | |||
| 064dd79eef | |||
| df47cddb87 | |||
| f67ce93d94 | |||
| 38ece9b5a6 | |||
| 5e46922ed0 | |||
| 6072351840 | |||
| 444a4673f4 | |||
| ed30284386 | |||
| bf1673dda1 | |||
| ff0c82efc1 | |||
| da6757c55c | |||
| 60270121dc | |||
| fdc7da0edf | |||
| 2dedcdbc53 | |||
| b172a80e35 | |||
| c5f6b88b1d | |||
| 6cd8bd89b2 | |||
| cdc919e308 | |||
| f635dcf6c3 | |||
| 7139119b8f | |||
| 203da23cf2 | |||
| f73fba583a | |||
| fe110b5d8a | |||
| cc0df4ecb2 | |||
| 9cee38ae6a | |||
| 8e7f789969 | |||
| 92fbd174eb | |||
| 6253adf768 | |||
| a0aef50674 | |||
| 8e6b1ab86e | |||
| 1c34ac38f7 | |||
| 468e663344 | |||
| 0645f7e253 | |||
| f94b0c79c5 | |||
| b09b51c74b | |||
| e6747de998 | |||
| 2746e088ae | |||
| 3a4c2f2cf9 | |||
| 351c01f71c | |||
| 65ae51b7e5 | |||
| 92bbeeb45e | |||
| 6bcc4f0ab0 | |||
| fb60236a54 | |||
| 4f7c20fc26 | |||
| 54c6d21fc5 | |||
| c031804464 | |||
| 5289fe872a |
Generated
+342
-133
File diff suppressed because it is too large
Load Diff
+7
-5
@@ -8,6 +8,7 @@ repository = "https://github.com/Plume-org/Plume"
|
||||
activitypub = "0.1.3"
|
||||
askama_escape = "0.1"
|
||||
atom_syndication = "0.6"
|
||||
clap = "2.33"
|
||||
colored = "1.8"
|
||||
dotenv = "0.14"
|
||||
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
||||
@@ -17,20 +18,21 @@ guid-create = "0.1"
|
||||
heck = "0.3.0"
|
||||
lettre = { git = "https://github.com/lettre/lettre", rev = "c988b1760ad8179d9e7f3fb8594d2b86cf2a0a49" }
|
||||
lettre_email = { git = "https://github.com/lettre/lettre", rev = "c988b1760ad8179d9e7f3fb8594d2b86cf2a0a49" }
|
||||
num_cpus = "1.0"
|
||||
num_cpus = "1.10"
|
||||
reqwest = "0.9"
|
||||
rocket = "0.4.0"
|
||||
rocket_contrib = { version = "0.4.0", features = ["json"] }
|
||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||
rpassword = "3.0"
|
||||
rpassword = "4.0"
|
||||
runtime-fmt = "0.3.0"
|
||||
scheduled-thread-pool = "0.2.0"
|
||||
scheduled-thread-pool = "0.2.2"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_qs = "0.4"
|
||||
serde_qs = "0.5"
|
||||
shrinkwraprs = "0.2.1"
|
||||
validator = "0.8"
|
||||
validator_derive = "0.8"
|
||||
webfinger = "0.3.1"
|
||||
webfinger = "0.4.1"
|
||||
|
||||
[[bin]]
|
||||
name = "plume"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- undo the adding of custom_domain column to blogs table.
|
||||
ALTER TABLE blogs DROP COLUMN custom_domain;
|
||||
@@ -0,0 +1,2 @@
|
||||
--- Adding custom domain to Blog as an optional field
|
||||
ALTER TABLE blogs ADD COLUMN custom_domain VARCHAR DEFAULT NULL UNIQUE;
|
||||
@@ -0,0 +1,56 @@
|
||||
-- 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;
|
||||
@@ -0,0 +1,57 @@
|
||||
-- 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;
|
||||
@@ -10,7 +10,7 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
clap = "2.33"
|
||||
dotenv = "0.14"
|
||||
rpassword = "3.0"
|
||||
rpassword = "4.0"
|
||||
|
||||
[dependencies.diesel]
|
||||
features = ["r2d2", "chrono"]
|
||||
|
||||
@@ -25,7 +25,11 @@ fn main() {
|
||||
.subcommand(users::command());
|
||||
let matches = app.clone().get_matches();
|
||||
|
||||
dotenv::dotenv().expect("error while reading .env");
|
||||
match dotenv::dotenv() {
|
||||
Ok(path) => println!("Configuration read from {}", path.display()),
|
||||
Err(ref e) if e.not_found() => eprintln!("no .env was found"),
|
||||
e => e.map(|_| ()).unwrap(),
|
||||
}
|
||||
let conn = Conn::establish(CONFIG.database_url.as_str());
|
||||
let _ = conn.as_ref().map(|conn| Instance::cache_local(conn));
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@ authors = ["Plume contributors"]
|
||||
|
||||
[dependencies]
|
||||
activitypub = "0.1.1"
|
||||
activitystreams-derive = "0.1.0"
|
||||
activitystreams-derive = "0.1.1"
|
||||
activitystreams-traits = "0.1.0"
|
||||
array_tool = "1.0"
|
||||
base64 = "0.10"
|
||||
heck = "0.3.0"
|
||||
hex = "0.3"
|
||||
hyper = "0.12.28"
|
||||
hyper = "0.12.33"
|
||||
openssl = "0.10.22"
|
||||
rocket = "0.4.0"
|
||||
reqwest = "0.9"
|
||||
@@ -19,7 +19,7 @@ serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
shrinkwraprs = "0.2.1"
|
||||
tokio = "0.1.21"
|
||||
tokio = "0.1.22"
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
|
||||
@@ -4,8 +4,8 @@ version = "0.3.0"
|
||||
authors = ["Plume contributors"]
|
||||
|
||||
[dependencies]
|
||||
stdweb = "=0.4.14"
|
||||
stdweb-internal-runtime = "=0.1.3"
|
||||
stdweb = "=0.4.18"
|
||||
stdweb-internal-runtime = "=0.1.4"
|
||||
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
||||
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||
|
||||
@@ -7,7 +7,7 @@ authors = ["Plume contributors"]
|
||||
activitypub = "0.1.1"
|
||||
ammonia = "2.1.1"
|
||||
askama_escape = "0.1"
|
||||
bcrypt = "0.4"
|
||||
bcrypt = "0.5"
|
||||
guid-create = "0.1"
|
||||
heck = "0.3.0"
|
||||
itertools = "0.8.0"
|
||||
@@ -17,15 +17,14 @@ openssl = "0.10.22"
|
||||
rocket = "0.4.0"
|
||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||
reqwest = "0.9"
|
||||
scheduled-thread-pool = "0.2.0"
|
||||
scheduled-thread-pool = "0.2.2"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
tantivy = "0.9.1"
|
||||
url = "1.7"
|
||||
webfinger = "0.3.1"
|
||||
tantivy = "0.10.1"
|
||||
url = "2.1"
|
||||
webfinger = "0.4.1"
|
||||
whatlang = "0.7.1"
|
||||
shrinkwraprs = "0.2.1"
|
||||
diesel-derive-newtype = "0.1.2"
|
||||
|
||||
[dependencies.chrono]
|
||||
|
||||
+121
-1
@@ -7,6 +7,11 @@ use openssl::{
|
||||
rsa::Rsa,
|
||||
sign::{Signer, Verifier},
|
||||
};
|
||||
use rocket::{
|
||||
http::RawStr,
|
||||
outcome::IntoOutcome,
|
||||
request::{self, FromFormValue, FromRequest, Request},
|
||||
};
|
||||
use serde_json;
|
||||
use url::Url;
|
||||
use webfinger::*;
|
||||
@@ -21,11 +26,70 @@ use posts::Post;
|
||||
use safe_string::SafeString;
|
||||
use schema::blogs;
|
||||
use search::Searcher;
|
||||
use std::default::Default;
|
||||
use std::fmt::{self, Display};
|
||||
use std::ops::Deref;
|
||||
use std::sync::RwLock;
|
||||
use users::User;
|
||||
use {Connection, Error, PlumeRocket, Result};
|
||||
|
||||
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)]
|
||||
#[changeset_options(treat_none_as_null = "true")]
|
||||
pub struct Blog {
|
||||
@@ -44,6 +108,7 @@ pub struct Blog {
|
||||
pub summary_html: SafeString,
|
||||
pub icon_id: Option<i32>,
|
||||
pub banner_id: Option<i32>,
|
||||
pub custom_domain: Option<Host>,
|
||||
}
|
||||
|
||||
#[derive(Default, Insertable)]
|
||||
@@ -61,10 +126,15 @@ pub struct NewBlog {
|
||||
pub summary_html: SafeString,
|
||||
pub icon_id: Option<i32>,
|
||||
pub banner_id: Option<i32>,
|
||||
pub custom_domain: Option<Host>,
|
||||
}
|
||||
|
||||
const BLOG_PREFIX: &str = "~";
|
||||
|
||||
lazy_static! {
|
||||
static ref CUSTOM_DOMAINS: RwLock<Vec<String>> = RwLock::new(vec![]);
|
||||
}
|
||||
|
||||
impl Blog {
|
||||
insert!(blogs, NewBlog, |inserted, conn| {
|
||||
let instance = inserted.get_instance(conn)?;
|
||||
@@ -144,8 +214,15 @@ 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> {
|
||||
resolve(acct.to_owned(), true)?
|
||||
resolve_with_prefix(Prefix::Group, acct.to_owned(), true)?
|
||||
.links
|
||||
.into_iter()
|
||||
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
|
||||
@@ -269,6 +346,19 @@ 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 {
|
||||
self.icon_id
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
|
||||
@@ -290,6 +380,23 @@ impl Blog {
|
||||
.map(|_| ())
|
||||
.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 {
|
||||
@@ -392,6 +499,7 @@ impl FromId<PlumeRocket> for Blog {
|
||||
.summary_string()
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
custom_domain: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -441,6 +549,7 @@ impl NewBlog {
|
||||
title: String,
|
||||
summary: String,
|
||||
instance_id: i32,
|
||||
custom_domain: Option<Host>,
|
||||
) -> Result<NewBlog> {
|
||||
let (pub_key, priv_key) = sign::gen_keypair();
|
||||
Ok(NewBlog {
|
||||
@@ -450,6 +559,7 @@ impl NewBlog {
|
||||
instance_id,
|
||||
public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?,
|
||||
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
|
||||
custom_domain,
|
||||
..NewBlog::default()
|
||||
})
|
||||
}
|
||||
@@ -477,6 +587,7 @@ pub(crate) mod tests {
|
||||
"Blog name".to_owned(),
|
||||
"This is a small blog".to_owned(),
|
||||
Instance::get_local().unwrap().id,
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -488,6 +599,7 @@ pub(crate) mod tests {
|
||||
"My blog".to_owned(),
|
||||
"Welcome to my blog".to_owned(),
|
||||
Instance::get_local().unwrap().id,
|
||||
Some(Host::new("blog.myname.me")),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -499,6 +611,7 @@ pub(crate) mod tests {
|
||||
"Why I like Plume".to_owned(),
|
||||
"In this blog I will explay you why I like Plume so much".to_owned(),
|
||||
Instance::get_local().unwrap().id,
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -559,6 +672,7 @@ pub(crate) mod tests {
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local().unwrap().id,
|
||||
Some(Host::new("some.blog.com")),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -587,6 +701,7 @@ pub(crate) mod tests {
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local().unwrap().id,
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -598,6 +713,7 @@ pub(crate) mod tests {
|
||||
"Blog".to_owned(),
|
||||
"I've named my blog Blog".to_owned(),
|
||||
Instance::get_local().unwrap().id,
|
||||
Some(Host::new("named.example.blog")),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -690,6 +806,7 @@ pub(crate) mod tests {
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local().unwrap().id,
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -714,6 +831,7 @@ pub(crate) mod tests {
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local().unwrap().id,
|
||||
Some(Host::new("some.blog.com")),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -752,6 +870,7 @@ pub(crate) mod tests {
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local().unwrap().id,
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -763,6 +882,7 @@ pub(crate) mod tests {
|
||||
"Blog".to_owned(),
|
||||
"I've named my blog Blog".to_owned(),
|
||||
Instance::get_local().unwrap().id,
|
||||
Some(Host::new("my.blog.com")),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
|
||||
@@ -10,6 +10,8 @@ extern crate bcrypt;
|
||||
extern crate chrono;
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
#[macro_use]
|
||||
extern crate diesel_derive_newtype;
|
||||
extern crate guid_create;
|
||||
extern crate heck;
|
||||
extern crate itertools;
|
||||
|
||||
+77
-76
@@ -1,17 +1,17 @@
|
||||
table! {
|
||||
api_tokens (id) {
|
||||
id -> Int4,
|
||||
id -> Integer,
|
||||
creation_date -> Timestamp,
|
||||
value -> Text,
|
||||
scopes -> Text,
|
||||
app_id -> Int4,
|
||||
user_id -> Int4,
|
||||
app_id -> Integer,
|
||||
user_id -> Integer,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
apps (id) {
|
||||
id -> Int4,
|
||||
id -> Integer,
|
||||
name -> Text,
|
||||
client_id -> Text,
|
||||
client_secret -> Text,
|
||||
@@ -23,70 +23,71 @@ table! {
|
||||
|
||||
table! {
|
||||
blog_authors (id) {
|
||||
id -> Int4,
|
||||
blog_id -> Int4,
|
||||
author_id -> Int4,
|
||||
id -> Integer,
|
||||
blog_id -> Integer,
|
||||
author_id -> Integer,
|
||||
is_owner -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
blogs (id) {
|
||||
id -> Int4,
|
||||
actor_id -> Varchar,
|
||||
title -> Varchar,
|
||||
id -> Integer,
|
||||
actor_id -> Text,
|
||||
title -> Text,
|
||||
summary -> Text,
|
||||
outbox_url -> Varchar,
|
||||
inbox_url -> Varchar,
|
||||
instance_id -> Int4,
|
||||
outbox_url -> Text,
|
||||
inbox_url -> Text,
|
||||
instance_id -> Integer,
|
||||
creation_date -> Timestamp,
|
||||
ap_url -> Text,
|
||||
private_key -> Nullable<Text>,
|
||||
public_key -> Text,
|
||||
fqn -> Text,
|
||||
summary_html -> Text,
|
||||
icon_id -> Nullable<Int4>,
|
||||
banner_id -> Nullable<Int4>,
|
||||
icon_id -> Nullable<Integer>,
|
||||
banner_id -> Nullable<Integer>,
|
||||
custom_domain -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
comment_seers (id) {
|
||||
id -> Integer,
|
||||
comment_id -> Integer,
|
||||
user_id -> Integer,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
comments (id) {
|
||||
id -> Int4,
|
||||
id -> Integer,
|
||||
content -> Text,
|
||||
in_response_to_id -> Nullable<Int4>,
|
||||
post_id -> Int4,
|
||||
author_id -> Int4,
|
||||
in_response_to_id -> Nullable<Integer>,
|
||||
post_id -> Integer,
|
||||
author_id -> Integer,
|
||||
creation_date -> Timestamp,
|
||||
ap_url -> Nullable<Varchar>,
|
||||
ap_url -> Nullable<Text>,
|
||||
sensitive -> Bool,
|
||||
spoiler_text -> Text,
|
||||
public_visibility -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
comment_seers (id) {
|
||||
id -> Int4,
|
||||
comment_id -> Int4,
|
||||
user_id -> Int4,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
follows (id) {
|
||||
id -> Int4,
|
||||
follower_id -> Int4,
|
||||
following_id -> Int4,
|
||||
id -> Integer,
|
||||
follower_id -> Integer,
|
||||
following_id -> Integer,
|
||||
ap_url -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
instances (id) {
|
||||
id -> Int4,
|
||||
public_domain -> Varchar,
|
||||
name -> Varchar,
|
||||
id -> Integer,
|
||||
public_domain -> Text,
|
||||
name -> Text,
|
||||
local -> Bool,
|
||||
blocked -> Bool,
|
||||
creation_date -> Timestamp,
|
||||
@@ -94,50 +95,50 @@ table! {
|
||||
short_description -> Text,
|
||||
long_description -> Text,
|
||||
default_license -> Text,
|
||||
long_description_html -> Varchar,
|
||||
short_description_html -> Varchar,
|
||||
long_description_html -> Text,
|
||||
short_description_html -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
likes (id) {
|
||||
id -> Int4,
|
||||
user_id -> Int4,
|
||||
post_id -> Int4,
|
||||
id -> Integer,
|
||||
user_id -> Integer,
|
||||
post_id -> Integer,
|
||||
creation_date -> Timestamp,
|
||||
ap_url -> Varchar,
|
||||
ap_url -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
medias (id) {
|
||||
id -> Int4,
|
||||
id -> Integer,
|
||||
file_path -> Text,
|
||||
alt_text -> Text,
|
||||
is_remote -> Bool,
|
||||
remote_url -> Nullable<Text>,
|
||||
sensitive -> Bool,
|
||||
content_warning -> Nullable<Text>,
|
||||
owner_id -> Int4,
|
||||
owner_id -> Integer,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
mentions (id) {
|
||||
id -> Int4,
|
||||
mentioned_id -> Int4,
|
||||
post_id -> Nullable<Int4>,
|
||||
comment_id -> Nullable<Int4>,
|
||||
id -> Integer,
|
||||
mentioned_id -> Integer,
|
||||
post_id -> Nullable<Integer>,
|
||||
comment_id -> Nullable<Integer>,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
notifications (id) {
|
||||
id -> Int4,
|
||||
user_id -> Int4,
|
||||
id -> Integer,
|
||||
user_id -> Integer,
|
||||
creation_date -> Timestamp,
|
||||
kind -> Varchar,
|
||||
object_id -> Int4,
|
||||
kind -> Text,
|
||||
object_id -> Integer,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,67 +153,67 @@ table! {
|
||||
|
||||
table! {
|
||||
post_authors (id) {
|
||||
id -> Int4,
|
||||
post_id -> Int4,
|
||||
author_id -> Int4,
|
||||
id -> Integer,
|
||||
post_id -> Integer,
|
||||
author_id -> Integer,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
posts (id) {
|
||||
id -> Int4,
|
||||
blog_id -> Int4,
|
||||
slug -> Varchar,
|
||||
title -> Varchar,
|
||||
id -> Integer,
|
||||
blog_id -> Integer,
|
||||
slug -> Text,
|
||||
title -> Text,
|
||||
content -> Text,
|
||||
published -> Bool,
|
||||
license -> Varchar,
|
||||
license -> Text,
|
||||
creation_date -> Timestamp,
|
||||
ap_url -> Varchar,
|
||||
ap_url -> Text,
|
||||
subtitle -> Text,
|
||||
source -> Text,
|
||||
cover_id -> Nullable<Int4>,
|
||||
cover_id -> Nullable<Integer>,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
reshares (id) {
|
||||
id -> Int4,
|
||||
user_id -> Int4,
|
||||
post_id -> Int4,
|
||||
ap_url -> Varchar,
|
||||
id -> Integer,
|
||||
user_id -> Integer,
|
||||
post_id -> Integer,
|
||||
ap_url -> Text,
|
||||
creation_date -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
tags (id) {
|
||||
id -> Int4,
|
||||
id -> Integer,
|
||||
tag -> Text,
|
||||
is_hashtag -> Bool,
|
||||
post_id -> Int4,
|
||||
post_id -> Integer,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
users (id) {
|
||||
id -> Int4,
|
||||
username -> Varchar,
|
||||
display_name -> Varchar,
|
||||
outbox_url -> Varchar,
|
||||
inbox_url -> Varchar,
|
||||
id -> Integer,
|
||||
username -> Text,
|
||||
display_name -> Text,
|
||||
outbox_url -> Text,
|
||||
inbox_url -> Text,
|
||||
is_admin -> Bool,
|
||||
summary -> Text,
|
||||
email -> Nullable<Text>,
|
||||
hashed_password -> Nullable<Text>,
|
||||
instance_id -> Int4,
|
||||
instance_id -> Integer,
|
||||
creation_date -> Timestamp,
|
||||
ap_url -> Text,
|
||||
private_key -> Nullable<Text>,
|
||||
public_key -> Text,
|
||||
shared_inbox_url -> Nullable<Varchar>,
|
||||
followers_endpoint -> Varchar,
|
||||
avatar_id -> Nullable<Int4>,
|
||||
shared_inbox_url -> Nullable<Text>,
|
||||
followers_endpoint -> Text,
|
||||
avatar_id -> Nullable<Integer>,
|
||||
last_fetched_date -> Timestamp,
|
||||
fqn -> Text,
|
||||
summary_html -> Text,
|
||||
@@ -248,8 +249,8 @@ allow_tables_to_appear_in_same_query!(
|
||||
apps,
|
||||
blog_authors,
|
||||
blogs,
|
||||
comments,
|
||||
comment_seers,
|
||||
comments,
|
||||
follows,
|
||||
instances,
|
||||
likes,
|
||||
|
||||
@@ -707,7 +707,7 @@ impl User {
|
||||
mime_type: None,
|
||||
href: None,
|
||||
template: Some(format!(
|
||||
"{}/remote_interact?{{uri}}",
|
||||
"https://{}/remote_interact?{{uri}}",
|
||||
self.get_instance(conn)?.public_domain
|
||||
)),
|
||||
},
|
||||
|
||||
+468
-498
File diff suppressed because it is too large
Load Diff
+434
-461
File diff suppressed because it is too large
Load Diff
+446
-476
File diff suppressed because it is too large
Load Diff
+468
-498
File diff suppressed because it is too large
Load Diff
+472
-502
File diff suppressed because it is too large
Load Diff
+411
-438
File diff suppressed because it is too large
Load Diff
+416
-443
File diff suppressed because it is too large
Load Diff
+466
-496
File diff suppressed because it is too large
Load Diff
+486
-516
File diff suppressed because it is too large
Load Diff
+471
-501
File diff suppressed because it is too large
Load Diff
+447
-474
File diff suppressed because it is too large
Load Diff
+436
-463
File diff suppressed because it is too large
Load Diff
+465
-495
File diff suppressed because it is too large
Load Diff
+475
-505
File diff suppressed because it is too large
Load Diff
+488
-518
File diff suppressed because it is too large
Load Diff
+467
-497
File diff suppressed because it is too large
Load Diff
+406
-426
File diff suppressed because it is too large
Load Diff
+462
-492
File diff suppressed because it is too large
Load Diff
+436
-463
File diff suppressed because it is too large
Load Diff
+430
-460
File diff suppressed because it is too large
Load Diff
+469
-499
File diff suppressed because it is too large
Load Diff
+414
-441
File diff suppressed because it is too large
Load Diff
+411
-438
File diff suppressed because it is too large
Load Diff
+65
-3
@@ -5,6 +5,7 @@ extern crate activitypub;
|
||||
extern crate askama_escape;
|
||||
extern crate atom_syndication;
|
||||
extern crate chrono;
|
||||
extern crate clap;
|
||||
extern crate colored;
|
||||
extern crate ctrlc;
|
||||
extern crate diesel;
|
||||
@@ -21,6 +22,7 @@ extern crate num_cpus;
|
||||
extern crate plume_api;
|
||||
extern crate plume_common;
|
||||
extern crate plume_models;
|
||||
extern crate reqwest;
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
extern crate rocket_contrib;
|
||||
@@ -38,19 +40,24 @@ extern crate validator;
|
||||
extern crate validator_derive;
|
||||
extern crate webfinger;
|
||||
|
||||
use clap::App;
|
||||
use diesel::r2d2::ConnectionManager;
|
||||
use plume_models::{
|
||||
blogs::Blog,
|
||||
blogs::Host,
|
||||
db_conn::{DbPool, PragmaForeignKey},
|
||||
instance::Instance,
|
||||
migrations::IMPORTED_MIGRATIONS,
|
||||
search::{Searcher as UnmanagedSearcher, SearcherError},
|
||||
Connection, Error, CONFIG,
|
||||
};
|
||||
use rocket::{fairing::AdHoc, http::ext::IntoOwned, http::uri::Origin};
|
||||
use rocket_csrf::CsrfFairingBuilder;
|
||||
use scheduled_thread_pool::ScheduledThreadPool;
|
||||
use std::collections::HashMap;
|
||||
use std::process::exit;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
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
|
||||
@@ -73,7 +80,11 @@ compile_i18n!();
|
||||
|
||||
/// Initializes a database pool.
|
||||
fn init_pool() -> Option<DbPool> {
|
||||
dotenv::dotenv().unwrap();
|
||||
match dotenv::dotenv() {
|
||||
Ok(path) => println!("Configuration read from {}", path.display()),
|
||||
Err(ref e) if e.not_found() => eprintln!("no .env was found"),
|
||||
e => e.map(|_| ()).unwrap(),
|
||||
}
|
||||
|
||||
let manager = ConnectionManager::<Connection>::new(CONFIG.database_url.as_str());
|
||||
let pool = DbPool::builder()
|
||||
@@ -81,10 +92,24 @@ fn init_pool() -> Option<DbPool> {
|
||||
.build(manager)
|
||||
.ok()?;
|
||||
Instance::cache_local(&pool.get().unwrap());
|
||||
Blog::cache_custom_domains(&pool.get().unwrap());
|
||||
Some(pool)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new("Plume")
|
||||
.bin_name("plume")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.about("Plume backend server")
|
||||
.after_help(
|
||||
r#"
|
||||
The plume command should be run inside the directory
|
||||
containing the `.env` configuration file and `static` directory.
|
||||
See https://docs.joinplu.me/installation/config
|
||||
and https://docs.joinplu.me/installation/init for more info.
|
||||
"#,
|
||||
)
|
||||
.get_matches();
|
||||
let dbpool = init_pool().expect("main: database pool initialization error");
|
||||
if IMPORTED_MIGRATIONS
|
||||
.is_pending(&dbpool.get().unwrap())
|
||||
@@ -156,7 +181,42 @@ Then try to restart Plume
|
||||
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())
|
||||
.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(
|
||||
"/",
|
||||
routes![
|
||||
@@ -269,6 +329,7 @@ Then try to restart Plume
|
||||
.manage(dbpool)
|
||||
.manage(Arc::new(workpool))
|
||||
.manage(searcher)
|
||||
.manage(Mutex::new(valid_domains))
|
||||
.manage(include_i18n!())
|
||||
.attach(
|
||||
CsrfFairingBuilder::new()
|
||||
@@ -295,7 +356,8 @@ Then try to restart Plume
|
||||
])
|
||||
.finalize()
|
||||
.expect("main: csrf fairing creation error"),
|
||||
);
|
||||
)
|
||||
.attach(custom_domain_fairing);
|
||||
|
||||
#[cfg(feature = "test")]
|
||||
let rocket = rocket.mount("/test", routes![test_routes::health,]);
|
||||
|
||||
+174
-12
@@ -2,11 +2,14 @@ use activitypub::collection::OrderedCollection;
|
||||
use atom_syndication::{Entry, FeedBuilder};
|
||||
use diesel::SaveChangesDsl;
|
||||
use rocket::{
|
||||
http::ContentType,
|
||||
http::{ContentType, Status},
|
||||
request::LenientForm,
|
||||
response::{content::Content, Flash, Redirect},
|
||||
State,
|
||||
};
|
||||
use rocket_i18n::I18n;
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
use validator::{Validate, ValidationError, ValidationErrors};
|
||||
|
||||
@@ -16,14 +19,17 @@ use plume_models::{
|
||||
blog_authors::*, blogs::*, instance::Instance, medias::*, posts::Post, safe_string::SafeString,
|
||||
users::User, Connection, PlumeRocket,
|
||||
};
|
||||
use reqwest::Client;
|
||||
use routes::{errors::ErrorPage, Page, RespondOrRedirect};
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
|
||||
#[get("/~/<name>?<page>", rank = 2)]
|
||||
pub fn details(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
fn detail_guts(
|
||||
blog: Blog,
|
||||
page: Option<Page>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<RespondOrRedirect, ErrorPage> {
|
||||
let page = page.unwrap_or_default();
|
||||
let conn = &*rockets.conn;
|
||||
let blog = Blog::find_by_fqn(&rockets, &name)?;
|
||||
let posts = Post::blog_page(conn, &blog, page.limits())?;
|
||||
let articles_count = Post::count_for_blog(conn, &blog)?;
|
||||
let authors = &blog.list_authors(conn)?;
|
||||
@@ -35,7 +41,43 @@ pub fn details(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result
|
||||
page.0,
|
||||
Page::total(articles_count as i32),
|
||||
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)]
|
||||
@@ -45,7 +87,7 @@ pub fn activity_details(
|
||||
_ap: ApRequest,
|
||||
) -> Option<ActivityStream<CustomGroup>> {
|
||||
let blog = Blog::find_by_fqn(&rockets, &name).ok()?;
|
||||
Some(ActivityStream::new(blog.to_activity(&*rockets.conn).ok()?))
|
||||
activity_detail_guts(blog, rockets, _ap)
|
||||
}
|
||||
|
||||
#[get("/blogs/new")]
|
||||
@@ -57,6 +99,76 @@ 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)]
|
||||
pub fn new_auth(i18n: I18n) -> Flash<Redirect> {
|
||||
utils::requires_login(
|
||||
@@ -72,6 +184,7 @@ pub fn new_auth(i18n: I18n) -> Flash<Redirect> {
|
||||
pub struct NewBlogForm {
|
||||
#[validate(custom(function = "valid_slug", message = "Invalid name"))]
|
||||
pub title: String,
|
||||
pub custom_domain: String,
|
||||
}
|
||||
|
||||
fn valid_slug(title: &str) -> Result<(), ValidationError> {
|
||||
@@ -83,13 +196,43 @@ 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>")]
|
||||
pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> RespondOrRedirect {
|
||||
pub fn create(
|
||||
form: LenientForm<NewBlogForm>,
|
||||
rockets: PlumeRocket,
|
||||
valid_domains: State<Mutex<HashMap<String, Instant>>>,
|
||||
) -> RespondOrRedirect {
|
||||
let slug = utils::make_actor_id(&form.title);
|
||||
let conn = &*rockets.conn;
|
||||
let intl = &rockets.intl.catalog;
|
||||
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() {
|
||||
Ok(_) => ValidationErrors::new(),
|
||||
Err(e) => e,
|
||||
@@ -121,6 +264,7 @@ pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> RespondOr
|
||||
Instance::get_local()
|
||||
.expect("blog::create: instance error")
|
||||
.id,
|
||||
custom_domain,
|
||||
)
|
||||
.expect("blog::create: new local error"),
|
||||
)
|
||||
@@ -136,11 +280,19 @@ pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> RespondOr
|
||||
)
|
||||
.expect("blog::create: author error");
|
||||
|
||||
Flash::success(
|
||||
Redirect::to(uri!(details: name = slug.clone(), page = _)),
|
||||
&i18n!(intl, "Your blog was successfully created!"),
|
||||
)
|
||||
.into()
|
||||
if dns_ok {
|
||||
Flash::success(
|
||||
Redirect::to(uri!(details: name = slug.clone(), page = _)),
|
||||
&i18n!(intl, "Your blog was successfully created!"),
|
||||
)
|
||||
.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")]
|
||||
@@ -181,6 +333,7 @@ pub struct EditForm {
|
||||
pub summary: String,
|
||||
pub icon: Option<i32>,
|
||||
pub banner: Option<i32>,
|
||||
pub custom_domain: String,
|
||||
}
|
||||
|
||||
#[get("/~/<name>/edit")]
|
||||
@@ -198,6 +351,10 @@ pub fn edit(name: String, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
.clone()
|
||||
.expect("blogs::edit: User was None while it shouldn't");
|
||||
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(
|
||||
&rockets.to_context(),
|
||||
&blog,
|
||||
@@ -207,6 +364,7 @@ pub fn edit(name: String, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
summary: blog.summary.clone(),
|
||||
icon: blog.icon_id,
|
||||
banner: blog.banner_id,
|
||||
custom_domain: custom_domain,
|
||||
},
|
||||
ValidationErrors::default()
|
||||
)))
|
||||
@@ -318,6 +476,10 @@ pub fn update(
|
||||
);
|
||||
blog.icon_id = form.icon;
|
||||
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)
|
||||
.expect("Couldn't save blog changes");
|
||||
Ok(Flash::success(
|
||||
|
||||
+21
-4
@@ -1,6 +1,6 @@
|
||||
use plume_models::{Error, PlumeRocket};
|
||||
use plume_models::{instance::Instance, Error, PlumeRocket};
|
||||
use rocket::{
|
||||
response::{self, Responder},
|
||||
response::{self, Redirect, Responder},
|
||||
Request,
|
||||
};
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
@@ -29,9 +29,26 @@ impl<'r> Responder<'r> for ErrorPage {
|
||||
}
|
||||
|
||||
#[catch(404)]
|
||||
pub fn not_found(req: &Request) -> Ructe {
|
||||
pub fn not_found(req: &Request) -> Result<Ructe, Redirect> {
|
||||
let rockets = req.guard::<PlumeRocket>().unwrap();
|
||||
render!(errors::not_found(&rockets.to_context()))
|
||||
if req
|
||||
.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)]
|
||||
|
||||
+8
-1
@@ -10,6 +10,7 @@ use rocket::{
|
||||
response::{Flash, NamedFile, Redirect},
|
||||
Outcome,
|
||||
};
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use template_utils::Ructe;
|
||||
|
||||
@@ -52,9 +53,15 @@ impl From<Flash<Redirect>> for RespondOrRedirect {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Shrinkwrap, Copy, Clone, UriDisplayQuery)]
|
||||
#[derive(Debug, Shrinkwrap, Copy, Clone, UriDisplayQuery)]
|
||||
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 {
|
||||
type Error = &'v RawStr;
|
||||
fn from_form_value(form_value: &'v RawStr) -> Result<Page, &'v RawStr> {
|
||||
|
||||
+85
-21
@@ -31,28 +31,14 @@ use routes::{
|
||||
};
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
|
||||
#[get("/~/<blog>/<slug>?<responding_to>", rank = 4)]
|
||||
pub fn details(
|
||||
blog: String,
|
||||
slug: String,
|
||||
fn detail_guts(
|
||||
blog: &Blog,
|
||||
post: &Post,
|
||||
responding_to: Option<i32>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Ructe, ErrorPage> {
|
||||
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.")
|
||||
)));
|
||||
}
|
||||
|
||||
let comments = CommentTree::from_post(&*conn, &post, user.as_ref())?;
|
||||
|
||||
@@ -61,7 +47,7 @@ pub fn details(
|
||||
Ok(render!(posts::details(
|
||||
&rockets.to_context(),
|
||||
post.clone(),
|
||||
blog,
|
||||
blog.clone(),
|
||||
&NewCommentForm {
|
||||
warning: previous.clone().map(|p| p.spoiler_text).unwrap_or_default(),
|
||||
content: previous.clone().and_then(|p| Some(format!(
|
||||
@@ -94,7 +80,85 @@ pub fn details(
|
||||
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),
|
||||
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)]
|
||||
|
||||
+21
-2
@@ -49,8 +49,7 @@ macro_rules! param_to_query {
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/search?<query..>")]
|
||||
pub fn search(query: Option<Form<SearchQuery>>, rockets: PlumeRocket) -> Ructe {
|
||||
fn search_guts(query: Option<Form<SearchQuery>>, rockets: PlumeRocket) -> Ructe {
|
||||
let conn = &*rockets.conn;
|
||||
let query = query.map(Form::into_inner).unwrap_or_default();
|
||||
let page = query.page.unwrap_or_default();
|
||||
@@ -83,3 +82,23 @@ pub fn search(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)
|
||||
}
|
||||
}
|
||||
|
||||
+11
-11
@@ -48,14 +48,16 @@ impl Resolver<PlumeRocket> for WebfingerResolver {
|
||||
CONFIG.base_url.as_str()
|
||||
}
|
||||
|
||||
fn find(acct: String, ctx: PlumeRocket) -> Result<Webfinger, ResolverError> {
|
||||
User::find_by_fqn(&ctx, &acct)
|
||||
.and_then(|usr| usr.webfinger(&*ctx.conn))
|
||||
.or_else(|_| {
|
||||
Blog::find_by_fqn(&ctx, &acct)
|
||||
.and_then(|blog| blog.webfinger(&*ctx.conn))
|
||||
.or(Err(ResolverError::NotFound))
|
||||
})
|
||||
fn find(prefix: Prefix, acct: String, ctx: PlumeRocket) -> Result<Webfinger, ResolverError> {
|
||||
match prefix {
|
||||
Prefix::Acct => User::find_by_fqn(&ctx, &acct)
|
||||
.and_then(|usr| usr.webfinger(&*ctx.conn))
|
||||
.or(Err(ResolverError::NotFound)),
|
||||
Prefix::Group => Blog::find_by_fqn(&ctx, &acct)
|
||||
.and_then(|blog| blog.webfinger(&*ctx.conn))
|
||||
.or(Err(ResolverError::NotFound)),
|
||||
Prefix::Custom(_) => Err(ResolverError::NotFound),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,9 +74,7 @@ pub fn webfinger(resource: String, rockets: PlumeRocket) -> Content<String> {
|
||||
"Invalid resource. Make sure to request an acct: URI"
|
||||
}
|
||||
ResolverError::NotFound => "Requested resource was not found",
|
||||
ResolverError::WrongInstance => {
|
||||
"This is not the instance of the requested resource"
|
||||
}
|
||||
ResolverError::WrongDomain => "This is not the instance of the requested resource",
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
+108
-8
@@ -1,7 +1,10 @@
|
||||
use plume_models::{notifications::*, users::User, Connection, PlumeRocket};
|
||||
|
||||
use rocket::http::hyper::header::{ETag, EntityTag};
|
||||
use rocket::http::{Method, Status};
|
||||
use rocket::http::{
|
||||
uri::{FromUriParam, Query},
|
||||
Method, Status,
|
||||
};
|
||||
use rocket::request::Request;
|
||||
use rocket::response::{self, content::Html as HtmlCt, Responder, Response};
|
||||
use rocket_i18n::Catalog;
|
||||
@@ -13,6 +16,16 @@ pub use askama_escape::escape;
|
||||
|
||||
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 (
|
||||
&'a Connection,
|
||||
&'a Catalog,
|
||||
@@ -146,7 +159,7 @@ pub fn avatar(
|
||||
pub fn tabs(links: &[(&str, String, bool)]) -> Html<String> {
|
||||
let mut res = String::from(r#"<div class="tabs">"#);
|
||||
for (url, title, selected) in links {
|
||||
res.push_str(r#"<a href=""#);
|
||||
res.push_str(r#"<a dir="auto" href=""#);
|
||||
res.push_str(url);
|
||||
if *selected {
|
||||
res.push_str(r#"" class="selected">"#);
|
||||
@@ -176,7 +189,7 @@ pub fn paginate_param(
|
||||
p
|
||||
})
|
||||
.unwrap_or_default();
|
||||
res.push_str(r#"<div class="pagination">"#);
|
||||
res.push_str(r#"<div class="pagination" dir="auto">"#);
|
||||
if page != 1 {
|
||||
res.push_str(
|
||||
format!(
|
||||
@@ -239,13 +252,13 @@ macro_rules! input {
|
||||
|
||||
Html(format!(
|
||||
r#"
|
||||
<label for="{name}">
|
||||
<label for="{name}" dir="auto">
|
||||
{label}
|
||||
{optional}
|
||||
{details}
|
||||
</label>
|
||||
{error}
|
||||
<input type="{kind}" id="{name}" name="{name}" value="{val}" {props}/>
|
||||
<input type="{kind}" id="{name}" name="{name}" value="{val}" {props} dir="auto"/>
|
||||
"#,
|
||||
name = stringify!($name),
|
||||
label = i18n!(cat, $label),
|
||||
@@ -264,7 +277,7 @@ macro_rules! input {
|
||||
$err.errors().get(stringify!($name))
|
||||
{
|
||||
format!(
|
||||
r#"<p class="error">{}</p>"#,
|
||||
r#"<p class="error" dir="auto">{}</p>"#,
|
||||
errs[0]
|
||||
.message
|
||||
.clone()
|
||||
@@ -332,8 +345,8 @@ macro_rules! input {
|
||||
let cat = $catalog;
|
||||
Html(format!(
|
||||
r#"
|
||||
<label for="{name}">{label}</label>
|
||||
<input type="{kind}" id="{name}" name="{name}" {props}/>
|
||||
<label for="{name}" dir="auto">{label}</label>
|
||||
<input type="{kind}" id="{name}" name="{name}" {props} dir="auto"/>
|
||||
"#,
|
||||
name = stringify!($name),
|
||||
label = i18n!(cat, $label),
|
||||
@@ -342,3 +355,90 @@ 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)*)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
<meta content="120" property="og:image:width" />
|
||||
<meta content="120" property="og:image:height" />
|
||||
<meta content="summary" property="twitter:card" />
|
||||
<meta content="'@Instance::get_local().unwrap().name" property="og:site_name" />
|
||||
<meta content="@blog.ap_url" property="og:url" />
|
||||
<meta content="@Instance::get_local().unwrap().name" property="og:site_name" />
|
||||
<meta content="@blog.url()" property="og:url" />
|
||||
<meta content="@blog.fqn" property="profile:username" />
|
||||
<meta content="@blog.title" property="og:title" />
|
||||
<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='@blog.ap_url' rel='alternate' type='application/activity+json'>
|
||||
}, {
|
||||
<a href="@uri!(blogs::details: name = &blog.fqn, page = _)">@blog.title</a>
|
||||
<a href="@url!(custom_domain = blog.custom_domain, blogs::details: common=[page = None], normal=[name = &blog.fqn])" dir="auto">@blog.title</a>
|
||||
}, {
|
||||
<div class="hidden">
|
||||
@for author in authors {
|
||||
@@ -41,38 +41,38 @@
|
||||
}
|
||||
<div class="h-card">
|
||||
<div class="user">
|
||||
<div class="flex wrap">
|
||||
<div class="flex wrap" dir="auto">
|
||||
<div class="avatar medium" style="background-image: url('@blog.icon_url(ctx.0)');" aria-label="@i18n!(ctx.1, "{}'s icon"; &blog.title)"></div>
|
||||
<img class="hidden u-photo" src="@blog.icon_url(ctx.0)"/>
|
||||
|
||||
<h1 class="grow flex vertical">
|
||||
<span class="p-name">@blog.title</span>
|
||||
<small>~@blog.fqn</small>
|
||||
<small dir="auto">~@blog.fqn</small>
|
||||
</h1>
|
||||
|
||||
@if ctx.2.clone().and_then(|u| u.is_author_in(ctx.0, &blog).ok()).unwrap_or(false) {
|
||||
<a href="@uri!(posts::new: blog = &blog.fqn)" class="button">@i18n!(ctx.1, "New article")</a>
|
||||
<a href="@uri!(blogs::edit: name = &blog.fqn)" class="button">@i18n!(ctx.1, "Edit")</a>
|
||||
<a href="@uri!(posts::new: blog = &blog.fqn)" class="button" dir="auto">@i18n!(ctx.1, "New article")</a>
|
||||
<a href="@uri!(blogs::edit: name = &blog.fqn)" class="button" dir="auto">@i18n!(ctx.1, "Edit")</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<main class="user-summary">
|
||||
<main class="user-summary" dir="auto">
|
||||
<p>
|
||||
@i18n!(ctx.1, "There's one author on this blog: ", "There are {0} authors on this blog: "; authors.len())
|
||||
@for (i, author) in authors.iter().enumerate() {@if i >= 1 {, }
|
||||
<a class="author p-author" href="@uri!(user::details: name = &author.fqn)">@author.name()</a>}
|
||||
<a class="author p-author" href="@uri!(user::details: name = &author.fqn)" dir="auto">@author.name()</a>}
|
||||
</p>
|
||||
@Html(blog.summary_html.clone())
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2>
|
||||
<h2 dir="auto">
|
||||
@i18n!(ctx.1, "Latest articles")
|
||||
<small><a href="@uri!(blogs::atom_feed: name = &blog.fqn)" title="Atom feed">@icon!("rss")</a></small>
|
||||
</h2>
|
||||
@if posts.is_empty() {
|
||||
<p>@i18n!(ctx.1, "No posts to see here yet.")</p>
|
||||
<p dir="auto">@i18n!(ctx.1, "No posts to see here yet.")</p>
|
||||
}
|
||||
<div class="cards">
|
||||
@for article in posts {
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
<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>
|
||||
|
||||
@input!(ctx.1, custom_domain (optional text), "Custom Domain", form, errors, "")
|
||||
|
||||
<p>
|
||||
@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>
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
@(ctx: BaseContext, form: &NewBlogForm, errors: ValidationErrors)
|
||||
|
||||
@:base(ctx, i18n!(ctx.1, "New Blog"), {}, {}, {
|
||||
<h1>@i18n!(ctx.1, "Create a blog")</h1>
|
||||
<h1 dir="auto">@i18n!(ctx.1, "Create a blog")</h1>
|
||||
<form method="post" action="@uri!(blogs::create)">
|
||||
@input!(ctx.1, title (text), "Title", form, errors, "required minlength=\"1\"")
|
||||
<input type="submit" value="@i18n!(ctx.1, "Create blog")"/>
|
||||
@input!(ctx.1, title (text), "Title", form, errors.clone(), "required minlength=\"1\"")
|
||||
<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>
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="comment u-comment h-cite" id="comment-@comm.id">
|
||||
<main class="content">
|
||||
<header>
|
||||
<a class="author u-author h-card" href="@uri!(user::details: name = &author.fqn)">
|
||||
<a class="author u-author h-card" href="@uri!(user::details: name = &author.fqn)" dir="auto">
|
||||
@avatar(ctx.0, &author, Size::Small, true, ctx.1)
|
||||
<span class="display-name p-name">@author.name()</span>
|
||||
<small>@author.fqn</small>
|
||||
@@ -27,7 +27,7 @@
|
||||
<div class="text p-content">
|
||||
@if comm.sensitive {
|
||||
<details>
|
||||
<summary>@comm.spoiler_text</summary>
|
||||
<summary dir="auto">@comm.spoiler_text</summary>
|
||||
}
|
||||
@Html(&comm.content)
|
||||
@if comm.sensitive {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
@if !articles.is_empty() {
|
||||
<div class="h-feed">
|
||||
<h2><span class="p-name">@title</span> — <a href="@link">@i18n!(ctx.1, "View all")</a></h2>
|
||||
<h2 dir="auto"><span class="p-name">@title</span> — <a href="@link">@i18n!(ctx.1, "View all")</a></h2>
|
||||
<div class="cards">
|
||||
@for article in articles {
|
||||
@:post_card(ctx, article)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
@(ctx: BaseContext, id: &str, title: String, optional: bool, medias: Vec<Media>, selected: Option<i32>)
|
||||
|
||||
<label for="@id">
|
||||
<label for="@id" dir="auto">
|
||||
@title
|
||||
@if optional {
|
||||
<small>@i18n!(ctx.1, "Optional")</small>
|
||||
@@ -13,7 +13,7 @@
|
||||
<option value="none" @if selected.is_none() { selected }>@i18n!(ctx.1, "None")</option>
|
||||
@for media in medias {
|
||||
@if media.category() == MediaCategory::Image {
|
||||
<option value="@media.id" @if selected.map(|c| c == media.id).unwrap_or(false) { selected }>
|
||||
<option value="@media.id" @if selected.map(|c| c == media.id).unwrap_or(false) { selected } dir="auto">
|
||||
@if !media.alt_text.is_empty() {
|
||||
@media.alt_text
|
||||
} else {
|
||||
@@ -22,4 +22,4 @@
|
||||
</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
</select>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
@(ctx: BaseContext, instance: Instance, n_users: i64, n_articles: i64)
|
||||
|
||||
<section class="split">
|
||||
<section class="split" dir="auto">
|
||||
<div class="presentation card">
|
||||
<h2>@i18n!(ctx.1, "What is Plume?")</h2>
|
||||
<main>
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
@if article.cover_id.is_some() {
|
||||
<div class="cover" style="background-image: url('@Html(article.cover_url(ctx.0).unwrap_or_default())')"></div>
|
||||
}
|
||||
<h3 class="p-name">
|
||||
<a class="u-url" href="@uri!(posts::details: blog = article.get_blog(ctx.0).unwrap().fqn, slug = &article.slug, responding_to = _)">
|
||||
<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])">
|
||||
@article.title
|
||||
</a>
|
||||
</h3>
|
||||
<main>
|
||||
<p class="p-summary">@article.subtitle</p>
|
||||
<p class="p-summary" dir="auto">@article.subtitle</p>
|
||||
</main>
|
||||
<footer class="authors">
|
||||
@Html(i18n!(ctx.1, "By {0}"; format!(
|
||||
@@ -25,10 +25,9 @@
|
||||
@if article.published {
|
||||
⋅ <span class="dt-published" datetime="@article.creation_date.format("%F %T")">@article.creation_date.format("%B %e, %Y")</span>
|
||||
}
|
||||
⋅ <a href="@uri!(blogs::details: name = &article.get_blog(ctx.0).unwrap().fqn, page = _)">@article.get_blog(ctx.0).unwrap().title</a>
|
||||
⋅ <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>
|
||||
@if !article.published {
|
||||
⋅ @i18n!(ctx.1, "Draft")
|
||||
}
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
@if article.cover_id.is_some() {
|
||||
<meta property="og:image" content="@Html(article.cover_url(ctx.0).unwrap_or_default())"/>
|
||||
}
|
||||
<meta property="og:url" content="@uri!(posts::details: blog = &blog.fqn, slug = &article.slug, responding_to = _)"/>
|
||||
<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:description" content="@article.subtitle"/>
|
||||
}, {
|
||||
<a href="@uri!(blogs::details: name = &blog.fqn, page = _)">@blog.title</a>
|
||||
<a href="@url!(custom_domain = &blog.custom_domain, blogs::details: common=[page = None], normal=[name = &blog.fqn])">@blog.title</a>
|
||||
}, {
|
||||
<div class="h-entry">
|
||||
<header
|
||||
@@ -28,8 +28,8 @@
|
||||
@if article.cover_id.is_some() { style="background-image: url('@article.cover_url(ctx.0).unwrap_or_default()'" }
|
||||
>
|
||||
<div>
|
||||
<h1 class="article p-name">@article.title</h1>
|
||||
<div class="article-info">
|
||||
<h1 class="article p-name" dir="auto">@article.title</h1>
|
||||
<div class="article-info" dir="auto">
|
||||
<span class="author">
|
||||
@Html(i18n!(ctx.1, "Written by {0}"; format!("<a href=\"{}\">{}</a>",
|
||||
uri!(user::details: name = &author.fqn),
|
||||
@@ -38,7 +38,7 @@
|
||||
—
|
||||
<span class="date dt-published" datetime="@article.creation_date.format("%F %T")">@article.creation_date.format("%B %e, %Y")</span><a class="u-url" href="@article.ap_url"></a>
|
||||
</div>
|
||||
<h2 class="article p-summary">@article.subtitle</h2>
|
||||
<h2 class="article p-summary" dir="auto">@article.subtitle</h2>
|
||||
</div>
|
||||
@if article.cover_id.is_some() {
|
||||
<div class="shadow"></div>
|
||||
@@ -46,21 +46,21 @@
|
||||
}
|
||||
</header>
|
||||
|
||||
<article class="e-content">
|
||||
<article class="e-content" dir="auto">
|
||||
@Html(&article.content)
|
||||
</article>
|
||||
<div class="article-meta">
|
||||
<section class="split">
|
||||
<ul class="tags">
|
||||
<ul class="tags" dir="auto">
|
||||
@for tag in tags {
|
||||
@if !tag.is_hashtag {
|
||||
<li><a class="p-category" href="@uri!(tags::tag: name = &tag.tag, page = _)">@tag.tag</a></li>
|
||||
<li><a class="p-category" href="@uri!(tags::tag: name = &tag.tag, page = None)">@tag.tag</a></li>
|
||||
} else {
|
||||
<span class="hidden p-category">@tag.tag</span>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
<p class="right">
|
||||
<p class="right" dir="auto">
|
||||
@if article.license.is_empty() {
|
||||
@i18n!(ctx.1, "All rights reserved."; &article.license)
|
||||
} else {
|
||||
@@ -116,7 +116,7 @@
|
||||
</section>
|
||||
}
|
||||
<section class="banner">
|
||||
<div class="flex p-author h-card user">
|
||||
<div class="flex p-author h-card user" dir="auto">
|
||||
@avatar(ctx.0, &author, Size::Medium, true, ctx.1)
|
||||
<div class="grow">
|
||||
<h2 class="p-name">
|
||||
@@ -132,7 +132,7 @@
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
<section class="comments">
|
||||
<section class="comments" dir="auto">
|
||||
<h2>@i18n!(ctx.1, "Comments")</h2>
|
||||
|
||||
@if ctx.2.is_some() {
|
||||
@@ -143,7 +143,7 @@
|
||||
@if let Some(ref prev) = previous_comment {
|
||||
<input type="hidden" name="responding_to" value="@prev.id"/>
|
||||
}
|
||||
<textarea id="plume-editor" name="content">@comment_form.content</textarea>
|
||||
<textarea id="plume-editor" name="content" dir="auto">@comment_form.content</textarea>
|
||||
<input type="submit" value="@i18n!(ctx.1, "Submit comment")" />
|
||||
</form>
|
||||
}
|
||||
@@ -153,7 +153,7 @@
|
||||
@:comment(ctx, &comm, Some(&article.ap_url), &blog.fqn, &article.slug)
|
||||
}
|
||||
} else {
|
||||
<p class="center">@i18n!(ctx.1, "No comments yet. Be the first to react!")</p>
|
||||
<p class="center" dir="auto">@i18n!(ctx.1, "No comments yet. Be the first to react!")</p>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
@(ctx: BaseContext, title: String, blog: Blog, editing: bool, form: &NewPostForm, is_draft: bool, article: Option<Post>, errors: ValidationErrors, medias: Vec<Media>, content_len: u64)
|
||||
|
||||
@:base(ctx, title.clone(), {}, {}, {
|
||||
<h1 id="plume-editor-title">@title</h1>
|
||||
<div id="plume-editor" style="display: none;">
|
||||
<h1 id="plume-editor-title" dir="auto">@title</h1>
|
||||
<div id="plume-editor" style="display: none;" dir="auto">
|
||||
<header>
|
||||
<button id="publish" class="button">@i18n!(ctx.1, "Publish")</button>
|
||||
<p id="char-count">@content_len</p>
|
||||
@@ -32,10 +32,10 @@
|
||||
@format!(r#"<p class="error">{}</p>"#, errs[0].message.clone().unwrap_or_else(|| Cow::from("Unknown error")))
|
||||
}
|
||||
|
||||
<label for="editor-content">@i18n!(ctx.1, "Content")<small>@i18n!(ctx.1, "Markdown syntax is supported")</small></label>
|
||||
<textarea id="editor-content" name="content" rows="20">@Html(&form.content)</textarea>
|
||||
<small id="editor-left">@content_len</small>
|
||||
<p>
|
||||
<label for="editor-content" dir="auto">@i18n!(ctx.1, "Content")<small>@i18n!(ctx.1, "Markdown syntax is supported")</small></label>
|
||||
<textarea id="editor-content" name="content" rows="20" dir="auto">@Html(&form.content)</textarea>
|
||||
<small id="editor-left" dir="auto">@content_len</small>
|
||||
<p dir="auto">
|
||||
@i18n!(ctx.1, "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them.")
|
||||
<a href="@uri!(medias::new)">@i18n!(ctx.1, "Upload media")</a>
|
||||
</p>
|
||||
@@ -47,7 +47,7 @@
|
||||
@:image_select(ctx, "cover", i18n!(ctx.1, "Illustration"), true, medias, form.cover)
|
||||
|
||||
@if is_draft {
|
||||
<label for="draft">
|
||||
<label for="draft" dir="auto">
|
||||
<input type="checkbox" name="draft" id="draft" checked>
|
||||
@i18n!(ctx.1, "This is a draft, don't publish it yet.")
|
||||
</label>
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
@(ctx: BaseContext, post: Post, login_form: LoginForm, login_errs: ValidationErrors, remote_form: RemoteForm, remote_errs: ValidationErrors)
|
||||
|
||||
@:remote_interact_base(ctx, i18n!(ctx.1, "Interact with {}"; post.title.clone()), i18n!(ctx.1, "Log in to interact"), i18n!(ctx.1, "Enter your full username to interact"), {
|
||||
<h1>@i18n!(ctx.1, "Interact with {}"; post.title.clone())</h1>
|
||||
<h1 dir="auto">@i18n!(ctx.1, "Interact with {}"; post.title.clone())</h1>
|
||||
@:post_card(ctx, post)
|
||||
}, login_form, login_errs, remote_form, remote_errs)
|
||||
|
||||
Reference in New Issue
Block a user