Compare commits
38 Commits
main
...
blog-title
Author | SHA1 | Date | |
---|---|---|---|
|
9bd8f5272f | ||
|
39cd4f830d | ||
|
cd9cb311c7 | ||
|
83ed168f9c | ||
|
83c628d490 | ||
|
badff3f3cb | ||
|
ba00e36884 | ||
|
5ee84427bf | ||
|
f203dddae5 | ||
|
ba1eac9482 | ||
|
3dad83b179 | ||
|
4eab51b159 | ||
|
abf0b28fd4 | ||
|
115b5b31a4 | ||
|
5a03fd7340 | ||
|
e75449410f | ||
|
c9bb31b8f5 | ||
|
0c2eaf0f1b | ||
|
71824aa524 | ||
|
fc848a8d53 | ||
|
53cdd8198b | ||
|
08f4dac3d3 | ||
|
18a9ed5504 | ||
|
631359c3f7 | ||
|
3111fa0735 | ||
|
890c9a0da4 | ||
|
22b03710be | ||
|
e3609f7863 | ||
|
0714d2d010 | ||
|
5bd084eff7 | ||
|
f369fa9b25 | ||
|
8afcc1511e | ||
|
ce89faef84 | ||
|
e18b6e78f2 | ||
|
31e817385d | ||
|
af7ed450e2 | ||
|
55a5a64b1a | ||
|
08b7d100fd |
8
Cargo.lock
generated
8
Cargo.lock
generated
@ -1763,6 +1763,12 @@ dependencies = [
|
||||
"ahash 0.7.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
@ -3348,6 +3354,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"shrinkwraprs",
|
||||
"syntect",
|
||||
"thiserror",
|
||||
"tokio 1.24.1",
|
||||
"tracing",
|
||||
"url 2.3.1",
|
||||
@ -3393,6 +3400,7 @@ dependencies = [
|
||||
"diesel_migrations",
|
||||
"glob",
|
||||
"guid-create",
|
||||
"heck",
|
||||
"itertools 0.10.5",
|
||||
"lazy_static",
|
||||
"ldap3",
|
||||
|
@ -0,0 +1,3 @@
|
||||
ALTER TABLE users DROP CONSTRAINT users_fqn;
|
||||
ALTER TABLE blogs DROP CONSTRAINT blogs_actor_id;
|
||||
ALTER TABLE blogs DROP CONSTRAINT blogs_fqn;
|
@ -0,0 +1,3 @@
|
||||
ALTER TABLE users ADD CONSTRAINT users_fqn UNIQUE (fqn);
|
||||
ALTER TABLE blogs ADD CONSTRAINT blogs_actor_id UNIQUE (actor_id);
|
||||
ALTER TABLE blogs ADD CONSTRAINT blogs_fqn UNIQUE (fqn);
|
@ -0,0 +1,3 @@
|
||||
DROP INDEX users_fqn;
|
||||
DROP INDEX blogs_actor_id;
|
||||
DROP INDEX blogs_fqn;
|
@ -0,0 +1,3 @@
|
||||
CREATE UNIQUE INDEX users_fqn ON users (fqn);
|
||||
CREATE UNIQUE INDEX blogs_actor_id ON blogs (actor_id);
|
||||
CREATE UNIQUE INDEX blogs_fqn ON blogs (fqn);
|
@ -25,6 +25,7 @@ url = "2.2.2"
|
||||
flume = "0.10.13"
|
||||
tokio = { version = "1.19.2", features = ["full"] }
|
||||
futures = "0.3.25"
|
||||
thiserror = "1.0.38"
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
|
@ -18,6 +18,11 @@ use rocket::{
|
||||
response::{Responder, Response},
|
||||
Outcome,
|
||||
};
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
fmt,
|
||||
str::FromStr,
|
||||
};
|
||||
use tokio::{
|
||||
runtime,
|
||||
time::{sleep, Duration},
|
||||
@ -241,6 +246,97 @@ pub trait IntoId {
|
||||
fn into_id(self) -> Id;
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum PreferredUsernameError {
|
||||
#[error("preferredUsername must be longer than 2 characters")]
|
||||
TooShort,
|
||||
#[error("Invaliad character at {character:?}: {position:?}")]
|
||||
InvalidCharacter { character: char, position: usize },
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Shrinkwrap, PartialEq, Eq, Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct PreferredUsername(String);
|
||||
|
||||
// Mastodon allows only /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i for `preferredUsername`
|
||||
impl PreferredUsername {
|
||||
fn validate(name: &str) -> std::result::Result<(), PreferredUsernameError> {
|
||||
let len = name.len();
|
||||
if len < 3 {
|
||||
return Err(PreferredUsernameError::TooShort);
|
||||
}
|
||||
match name.chars().enumerate().find(|(pos, c)| {
|
||||
if pos == &0 || pos == &(len - 1) {
|
||||
c != &'_' && !c.is_ascii_alphanumeric()
|
||||
} else {
|
||||
match c {
|
||||
'_' | '\\' | '.' | '-' => false,
|
||||
_ => !c.is_ascii_alphanumeric(),
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Some((pos, c)) => Err(PreferredUsernameError::InvalidCharacter {
|
||||
character: c,
|
||||
position: pos,
|
||||
}),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// The given string must be match against /\A[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?\z/i in Ruby's RegExp which is required by Mastodon.
|
||||
pub unsafe fn new_unchecked(name: String) -> Self {
|
||||
Self(name)
|
||||
}
|
||||
|
||||
pub fn new(name: String) -> std::result::Result<Self, PreferredUsernameError> {
|
||||
Self::validate(&name).map(|_| unsafe { Self::new_unchecked(name) })
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PreferredUsername {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for PreferredUsername {
|
||||
type Error = PreferredUsernameError;
|
||||
|
||||
fn try_from(name: String) -> std::result::Result<Self, Self::Error> {
|
||||
Self::new(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for PreferredUsername {
|
||||
type Error = PreferredUsernameError;
|
||||
|
||||
fn try_from(name: &str) -> std::result::Result<Self, Self::Error> {
|
||||
Self::new(name.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PreferredUsername> for String {
|
||||
fn from(preferred_username: PreferredUsername) -> Self {
|
||||
preferred_username.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for PreferredUsername {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PreferredUsername {
|
||||
type Err = PreferredUsernameError;
|
||||
|
||||
fn from_str(name: &str) -> std::result::Result<Self, Self::Err> {
|
||||
name.try_into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApSignature {
|
||||
@ -524,6 +620,35 @@ mod tests {
|
||||
use assert_json_diff::assert_json_eq;
|
||||
use serde_json::{from_str, json, to_value};
|
||||
|
||||
#[test]
|
||||
fn preferred_username() {
|
||||
assert!(PreferredUsername::new("".into()).is_err());
|
||||
assert!(PreferredUsername::new("a".into()).is_err());
|
||||
assert!(PreferredUsername::new("ab".into()).is_err());
|
||||
assert_eq!(
|
||||
"abc",
|
||||
PreferredUsername::new("abc".into()).unwrap().as_str()
|
||||
);
|
||||
assert_eq!(
|
||||
"abcd",
|
||||
PreferredUsername::new("abcd".into()).unwrap().as_str()
|
||||
);
|
||||
assert!(PreferredUsername::new("abc-".into()).is_err());
|
||||
assert!(PreferredUsername::new("日本語".into()).is_err());
|
||||
assert_eq!("abc", "abc".parse::<PreferredUsername>().unwrap().as_str());
|
||||
assert!("abc-".parse::<PreferredUsername>().is_err());
|
||||
assert_eq!(
|
||||
PreferredUsername::new("admin".into()).unwrap(),
|
||||
PreferredUsername("admin".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefferred_username_to_string() {
|
||||
let pu = PreferredUsername::new("admin".into()).unwrap();
|
||||
assert_eq!("admin".to_string(), pu.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn se_ap_signature() {
|
||||
let ap_signature = ApSignature {
|
||||
|
@ -1,7 +1,7 @@
|
||||
use activitystreams::iri_string::percent_encode::PercentEncodedForIri;
|
||||
use openssl::rand::rand_bytes;
|
||||
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag};
|
||||
use regex_syntax::is_word_character;
|
||||
use rocket::http::uri::Uri;
|
||||
use std::collections::HashSet;
|
||||
use syntect::html::{ClassStyle, ClassedHTMLGenerator};
|
||||
use syntect::parsing::SyntaxSet;
|
||||
@ -21,51 +21,7 @@ pub fn random_hex() -> String {
|
||||
* Intended to be used for generating Post ap_url.
|
||||
*/
|
||||
pub fn iri_percent_encode_seg(segment: &str) -> String {
|
||||
segment.chars().map(iri_percent_encode_seg_char).collect()
|
||||
}
|
||||
|
||||
pub fn iri_percent_encode_seg_char(c: char) -> String {
|
||||
if c.is_alphanumeric() {
|
||||
c.to_string()
|
||||
} else {
|
||||
match c {
|
||||
'-'
|
||||
| '.'
|
||||
| '_'
|
||||
| '~'
|
||||
| '\u{A0}'..='\u{D7FF}'
|
||||
| '\u{20000}'..='\u{2FFFD}'
|
||||
| '\u{30000}'..='\u{3FFFD}'
|
||||
| '\u{40000}'..='\u{4FFFD}'
|
||||
| '\u{50000}'..='\u{5FFFD}'
|
||||
| '\u{60000}'..='\u{6FFFD}'
|
||||
| '\u{70000}'..='\u{7FFFD}'
|
||||
| '\u{80000}'..='\u{8FFFD}'
|
||||
| '\u{90000}'..='\u{9FFFD}'
|
||||
| '\u{A0000}'..='\u{AFFFD}'
|
||||
| '\u{B0000}'..='\u{BFFFD}'
|
||||
| '\u{C0000}'..='\u{CFFFD}'
|
||||
| '\u{D0000}'..='\u{DFFFD}'
|
||||
| '\u{E0000}'..='\u{EFFFD}'
|
||||
| '!'
|
||||
| '$'
|
||||
| '&'
|
||||
| '\''
|
||||
| '('
|
||||
| ')'
|
||||
| '*'
|
||||
| '+'
|
||||
| ','
|
||||
| ';'
|
||||
| '='
|
||||
| ':'
|
||||
| '@' => c.to_string(),
|
||||
_ => {
|
||||
let s = c.to_string();
|
||||
Uri::percent_encode(&s).to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
PercentEncodedForIri::from_path_segment(segment).to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -35,6 +35,7 @@ once_cell = "1.12.0"
|
||||
lettre = "0.9.6"
|
||||
native-tls = "0.2.10"
|
||||
activitystreams = "=0.7.0-alpha.20"
|
||||
heck = "0.4.0"
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
db_conn::DbConn, instance::*, medias::Media, posts::Post, safe_string::SafeString,
|
||||
schema::blogs, users::User, Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE,
|
||||
schema::blogs, users::User, Connection, Error, Fqn, PlumeRocket, Result, CONFIG,
|
||||
ITEMS_PER_PAGE,
|
||||
};
|
||||
use activitystreams::{
|
||||
actor::{ApActor, ApActorExt, AsApActor, Group},
|
||||
@ -42,14 +43,14 @@ pub struct Blog {
|
||||
pub ap_url: String,
|
||||
pub private_key: Option<String>,
|
||||
pub public_key: String,
|
||||
pub fqn: String,
|
||||
pub fqn: Fqn,
|
||||
pub summary_html: SafeString,
|
||||
pub icon_id: Option<i32>,
|
||||
pub banner_id: Option<i32>,
|
||||
pub theme: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Insertable)]
|
||||
#[derive(Insertable)]
|
||||
#[table_name = "blogs"]
|
||||
pub struct NewBlog {
|
||||
pub actor_id: String,
|
||||
@ -61,6 +62,7 @@ pub struct NewBlog {
|
||||
pub ap_url: String,
|
||||
pub private_key: Option<String>,
|
||||
pub public_key: String,
|
||||
pub fqn: Fqn,
|
||||
pub summary_html: SafeString,
|
||||
pub icon_id: Option<i32>,
|
||||
pub banner_id: Option<i32>,
|
||||
@ -84,15 +86,13 @@ impl Blog {
|
||||
inserted.ap_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "");
|
||||
}
|
||||
|
||||
if inserted.fqn.is_empty() {
|
||||
if inserted.fqn.to_string().is_empty() {
|
||||
// This might not enough for some titles such as all-Japanese title,
|
||||
// but better than doing nothing.
|
||||
if instance.local {
|
||||
inserted.fqn = iri_percent_encode_seg(&inserted.actor_id);
|
||||
inserted.fqn = Fqn::make_local(&inserted.title)?;
|
||||
} else {
|
||||
inserted.fqn = format!(
|
||||
"{}@{}",
|
||||
iri_percent_encode_seg(&inserted.actor_id),
|
||||
instance.public_domain
|
||||
);
|
||||
inserted.fqn = Fqn::make_remote(&inserted.title, instance.public_domain)?;
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,8 +102,8 @@ impl Blog {
|
||||
find_by!(blogs, find_by_ap_url, ap_url as &str);
|
||||
find_by!(blogs, find_by_name, actor_id as &str, instance_id as i32);
|
||||
|
||||
pub fn slug(title: &str) -> &str {
|
||||
title
|
||||
pub fn slug(title: &str) -> String {
|
||||
iri_percent_encode_seg(title)
|
||||
}
|
||||
|
||||
pub fn get_instance(&self, conn: &Connection) -> Result<Instance> {
|
||||
@ -173,7 +173,7 @@ impl Blog {
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> {
|
||||
let mut blog = ApActor::new(self.inbox_url.parse()?, Group::new());
|
||||
blog.set_preferred_username(iri_percent_encode_seg(&self.actor_id));
|
||||
blog.set_preferred_username(self.fqn.to_string());
|
||||
blog.set_name(self.title.clone());
|
||||
blog.set_outbox(self.outbox_url.parse()?);
|
||||
blog.set_summary(self.summary_html.to_string());
|
||||
@ -398,22 +398,18 @@ impl FromId<DbConn> for Blog {
|
||||
)
|
||||
};
|
||||
|
||||
let mut new_blog = NewBlog {
|
||||
actor_id: name.to_string(),
|
||||
outbox_url,
|
||||
inbox_url,
|
||||
public_key: acct.ext_one.public_key.public_key_pem.to_string(),
|
||||
private_key: None,
|
||||
theme: None,
|
||||
..NewBlog::default()
|
||||
};
|
||||
|
||||
let object = ApObject::new(acct.inner);
|
||||
new_blog.title = object
|
||||
let actor_id = iri_percent_encode_seg(
|
||||
&acct
|
||||
.name()
|
||||
.and_then(|name| name.to_as_string())
|
||||
.unwrap_or(name);
|
||||
new_blog.summary_html = SafeString::new(
|
||||
.ok_or(Error::MissingApProperty)?,
|
||||
);
|
||||
let object = ApObject::new(acct.inner);
|
||||
let title = object
|
||||
.name()
|
||||
.and_then(|name| name.to_as_string())
|
||||
.unwrap_or(name.clone());
|
||||
let summary_html = SafeString::new(
|
||||
&object
|
||||
.summary()
|
||||
.and_then(|summary| summary.to_as_string())
|
||||
@ -435,7 +431,6 @@ impl FromId<DbConn> for Blog {
|
||||
})
|
||||
})
|
||||
.map(|m| m.id);
|
||||
new_blog.icon_id = icon_id;
|
||||
|
||||
let banner_id = object
|
||||
.image()
|
||||
@ -452,13 +447,12 @@ impl FromId<DbConn> for Blog {
|
||||
})
|
||||
})
|
||||
.map(|m| m.id);
|
||||
new_blog.banner_id = banner_id;
|
||||
|
||||
new_blog.summary = acct.ext_two.source.content;
|
||||
let summary = acct.ext_two.source.content;
|
||||
|
||||
let any_base = AnyBase::from_extended(object)?;
|
||||
let id = any_base.id().ok_or(Error::MissingApProperty)?;
|
||||
new_blog.ap_url = id.to_string();
|
||||
let ap_url = id.to_string();
|
||||
|
||||
let inst = id
|
||||
.authority_components()
|
||||
@ -482,7 +476,29 @@ impl FromId<DbConn> for Blog {
|
||||
},
|
||||
)
|
||||
})?;
|
||||
new_blog.instance_id = instance.id;
|
||||
let instance_id = instance.id;
|
||||
let fqn = if instance.local {
|
||||
Fqn::new_local(name)?
|
||||
} else {
|
||||
Fqn::new_remote(name, instance.public_domain)?
|
||||
};
|
||||
|
||||
let new_blog = NewBlog {
|
||||
actor_id,
|
||||
outbox_url,
|
||||
inbox_url,
|
||||
fqn,
|
||||
public_key: acct.ext_one.public_key.public_key_pem.to_string(),
|
||||
private_key: None,
|
||||
theme: None,
|
||||
title,
|
||||
summary,
|
||||
ap_url,
|
||||
summary_html,
|
||||
icon_id,
|
||||
banner_id,
|
||||
instance_id,
|
||||
};
|
||||
|
||||
Blog::insert(conn, new_blog)
|
||||
}
|
||||
@ -538,12 +554,19 @@ impl NewBlog {
|
||||
let (pub_key, priv_key) = sign::gen_keypair();
|
||||
Ok(NewBlog {
|
||||
actor_id,
|
||||
fqn: Fqn::make_local(&title)?,
|
||||
title,
|
||||
summary,
|
||||
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))?),
|
||||
..NewBlog::default()
|
||||
outbox_url: Default::default(),
|
||||
inbox_url: Default::default(),
|
||||
ap_url: Default::default(),
|
||||
summary_html: Default::default(),
|
||||
icon_id: Default::default(),
|
||||
banner_id: Default::default(),
|
||||
theme: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -565,8 +588,8 @@ pub(crate) mod tests {
|
||||
let mut blog1 = Blog::insert(
|
||||
conn,
|
||||
NewBlog::new_local(
|
||||
"BlogName".to_owned(),
|
||||
"Blog name".to_owned(),
|
||||
"Blog%20Name".to_owned(),
|
||||
"Blog Name".to_owned(),
|
||||
"This is a small blog".to_owned(),
|
||||
Instance::get_local().unwrap().id,
|
||||
)
|
||||
@ -576,7 +599,7 @@ pub(crate) mod tests {
|
||||
let blog2 = Blog::insert(
|
||||
conn,
|
||||
NewBlog::new_local(
|
||||
"MyBlog".to_owned(),
|
||||
"My%20Blog".to_owned(),
|
||||
"My blog".to_owned(),
|
||||
"Welcome to my blog".to_owned(),
|
||||
Instance::get_local().unwrap().id,
|
||||
@ -587,7 +610,7 @@ pub(crate) mod tests {
|
||||
let blog3 = Blog::insert(
|
||||
conn,
|
||||
NewBlog::new_local(
|
||||
"WhyILikePlume".to_owned(),
|
||||
"Why%20I%20Like%20Plume".to_owned(),
|
||||
"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,
|
||||
@ -682,7 +705,7 @@ pub(crate) mod tests {
|
||||
let blog = Blog::insert(
|
||||
conn,
|
||||
NewBlog::new_local(
|
||||
"SomeName".to_owned(),
|
||||
"Some%20Name".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local().unwrap().id,
|
||||
@ -709,7 +732,7 @@ pub(crate) mod tests {
|
||||
let b1 = Blog::insert(
|
||||
conn,
|
||||
NewBlog::new_local(
|
||||
"SomeName".to_owned(),
|
||||
"Some%20Name".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local().unwrap().id,
|
||||
@ -810,7 +833,7 @@ pub(crate) mod tests {
|
||||
let blog = Blog::insert(
|
||||
conn,
|
||||
NewBlog::new_local(
|
||||
"SomeName".to_owned(),
|
||||
"Some%20Name".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local().unwrap().id,
|
||||
@ -830,10 +853,10 @@ pub(crate) mod tests {
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
fill_database(conn);
|
||||
|
||||
let blog = Blog::insert(
|
||||
let _ = Blog::insert(
|
||||
conn,
|
||||
NewBlog::new_local(
|
||||
"SomeName".to_owned(),
|
||||
"Some%20Name".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local().unwrap().id,
|
||||
@ -842,7 +865,6 @@ pub(crate) mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(blog.fqn, "SomeName");
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@ -868,7 +890,7 @@ pub(crate) mod tests {
|
||||
let b1 = Blog::insert(
|
||||
conn,
|
||||
NewBlog::new_local(
|
||||
"SomeName".to_owned(),
|
||||
"Some%20Name".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local().unwrap().id,
|
||||
@ -968,6 +990,7 @@ pub(crate) mod tests {
|
||||
let _: Blog = blogs[0].save_changes(&**conn).unwrap();
|
||||
let ap_repr = blogs[0].to_activity(conn).unwrap();
|
||||
blogs[0].delete(conn).unwrap();
|
||||
eprintln!("{:#?}", &ap_repr);
|
||||
let blog = Blog::from_activity(conn, ap_repr).unwrap();
|
||||
|
||||
assert_eq!(blog.actor_id, blogs[0].actor_id);
|
||||
@ -1001,19 +1024,19 @@ pub(crate) mod tests {
|
||||
"type": "Image",
|
||||
"url": "https://plu.me/aaa.png"
|
||||
},
|
||||
"id": "https://plu.me/~/BlogName/",
|
||||
"id": "https://plu.me/~/Blog%20Name/",
|
||||
"image": {
|
||||
"attributedTo": "https://plu.me/@/admin/",
|
||||
"type": "Image",
|
||||
"url": "https://plu.me/bbb.png"
|
||||
},
|
||||
"inbox": "https://plu.me/~/BlogName/inbox",
|
||||
"name": "Blog name",
|
||||
"outbox": "https://plu.me/~/BlogName/outbox",
|
||||
"inbox": "https://plu.me/~/Blog%20Name/inbox",
|
||||
"name": "Blog Name",
|
||||
"outbox": "https://plu.me/~/Blog%20Name/outbox",
|
||||
"preferredUsername": "BlogName",
|
||||
"publicKey": {
|
||||
"id": "https://plu.me/~/BlogName/#main-key",
|
||||
"owner": "https://plu.me/~/BlogName/",
|
||||
"id": "https://plu.me/~/Blog%20Name/#main-key",
|
||||
"owner": "https://plu.me/~/Blog%20Name/",
|
||||
"publicKeyPem": blog.public_key
|
||||
},
|
||||
"source": {
|
||||
@ -1041,8 +1064,8 @@ pub(crate) mod tests {
|
||||
let expected = json!({
|
||||
"items": [],
|
||||
"totalItems": 0,
|
||||
"first": "https://plu.me/~/BlogName/outbox?page=1",
|
||||
"last": "https://plu.me/~/BlogName/outbox?page=0",
|
||||
"first": "https://plu.me/~/Blog%20Name/outbox?page=1",
|
||||
"last": "https://plu.me/~/Blog%20Name/outbox?page=0",
|
||||
"type": "OrderedCollection"
|
||||
});
|
||||
|
||||
@ -1061,8 +1084,8 @@ pub(crate) mod tests {
|
||||
let act = blog.outbox_collection_page(conn, (33, 36))?;
|
||||
|
||||
let expected = json!({
|
||||
"next": "https://plu.me/~/BlogName/outbox?page=3",
|
||||
"prev": "https://plu.me/~/BlogName/outbox?page=1",
|
||||
"next": "https://plu.me/~/Blog%20Name/outbox?page=3",
|
||||
"prev": "https://plu.me/~/Blog%20Name/outbox?page=1",
|
||||
"items": [],
|
||||
"type": "OrderedCollectionPage"
|
||||
});
|
||||
|
@ -463,13 +463,13 @@ mod tests {
|
||||
assert_json_eq!(to_value(&act).unwrap(), json!({
|
||||
"actor": "https://plu.me/@/admin/",
|
||||
"cc": ["https://plu.me/@/admin/followers"],
|
||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}/activity", original_comm.id),
|
||||
"id": format!("https://plu.me/~/Blog%20Name/testing/comment/{}/activity", original_comm.id),
|
||||
"object": {
|
||||
"attributedTo": "https://plu.me/@/admin/",
|
||||
"content": r###"<p dir="auto">My comment, mentioning to <a href="https://plu.me/@/user/" title="user">@user</a></p>
|
||||
"###,
|
||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", original_comm.id),
|
||||
"inReplyTo": "https://plu.me/~/BlogName/testing",
|
||||
"id": format!("https://plu.me/~/Blog%20Name/testing/comment/{}", original_comm.id),
|
||||
"inReplyTo": "https://plu.me/~/Blog%20Name/testing",
|
||||
"published": format_datetime(&original_comm.creation_date),
|
||||
"summary": "My CW",
|
||||
"tag": [
|
||||
@ -505,12 +505,12 @@ mod tests {
|
||||
assert_json_eq!(to_value(&reply_act).unwrap(), json!({
|
||||
"actor": "https://plu.me/@/user/",
|
||||
"cc": ["https://plu.me/@/user/followers"],
|
||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}/activity", reply.id),
|
||||
"id": format!("https://plu.me/~/Blog%20Name/testing/comment/{}/activity", reply.id),
|
||||
"object": {
|
||||
"attributedTo": "https://plu.me/@/user/",
|
||||
"content": "",
|
||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", reply.id),
|
||||
"inReplyTo": format!("https://plu.me/~/BlogName/testing/comment/{}", original_comm.id),
|
||||
"id": format!("https://plu.me/~/Blog%20Name/testing/comment/{}", reply.id),
|
||||
"inReplyTo": format!("https://plu.me/~/Blog%20Name/testing/comment/{}", original_comm.id),
|
||||
"published": format_datetime(&reply.creation_date),
|
||||
"summary": "",
|
||||
"tag": [],
|
||||
@ -554,8 +554,8 @@ mod tests {
|
||||
"attributedTo": "https://plu.me/@/admin/",
|
||||
"content": r###"<p dir="auto">My comment, mentioning to <a href="https://plu.me/@/user/" title="user">@user</a></p>
|
||||
"###,
|
||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", comment.id),
|
||||
"inReplyTo": "https://plu.me/~/BlogName/testing",
|
||||
"id": format!("https://plu.me/~/Blog%20Name/testing/comment/{}", comment.id),
|
||||
"inReplyTo": "https://plu.me/~/Blog%20Name/testing",
|
||||
"published": format_datetime(&comment.creation_date),
|
||||
"summary": "My CW",
|
||||
"tag": [
|
||||
@ -584,9 +584,9 @@ mod tests {
|
||||
|
||||
let expected = json!({
|
||||
"actor": "https://plu.me/@/admin/",
|
||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}#delete", comment.id),
|
||||
"id": format!("https://plu.me/~/Blog%20Name/testing/comment/{}#delete", comment.id),
|
||||
"object": {
|
||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", comment.id),
|
||||
"id": format!("https://plu.me/~/Blog%20Name/testing/comment/{}", comment.id),
|
||||
"type": "Tombstone"
|
||||
},
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
|
@ -41,7 +41,7 @@ pub enum InvalidRocketConfig {
|
||||
SecretKey,
|
||||
}
|
||||
|
||||
fn get_rocket_config() -> Result<RocketConfig, InvalidRocketConfig> {
|
||||
pub fn get_rocket_config() -> Result<RocketConfig, InvalidRocketConfig> {
|
||||
let mut c = RocketConfig::active().map_err(|_| InvalidRocketConfig::Env)?;
|
||||
|
||||
let address = var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned());
|
||||
|
@ -268,7 +268,7 @@ pub(crate) mod tests {
|
||||
"actor": users[0].ap_url,
|
||||
"object": {
|
||||
"type": "Article",
|
||||
"id": "https://plu.me/~/BlogName/testing",
|
||||
"id": "https://plu.me/~/Blog%20Name/testing",
|
||||
"attributedTo": [users[0].ap_url, blogs[0].ap_url],
|
||||
"content": "Hello.",
|
||||
"name": "My Article",
|
||||
|
@ -9,7 +9,7 @@ use crate::{
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::{self, result::Error::NotFound, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use once_cell::sync::OnceCell;
|
||||
use plume_common::utils::{iri_percent_encode_seg, md_to_html};
|
||||
use plume_common::utils::md_to_html;
|
||||
use std::sync::RwLock;
|
||||
|
||||
#[derive(Clone, Identifiable, Queryable)]
|
||||
@ -173,8 +173,8 @@ impl Instance {
|
||||
"{instance}/{prefix}/{name}/{box_name}",
|
||||
instance = self.public_domain,
|
||||
prefix = prefix,
|
||||
name = iri_percent_encode_seg(name),
|
||||
box_name = iri_percent_encode_seg(box_name)
|
||||
name = name,
|
||||
box_name = box_name
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -17,12 +17,18 @@ extern crate serde_json;
|
||||
extern crate tantivy;
|
||||
|
||||
use activitystreams::iri_string;
|
||||
use diesel::backend::Backend;
|
||||
use diesel::sql_types::Text;
|
||||
use diesel::types::{FromSql, ToSql};
|
||||
use heck::ToUpperCamelCase;
|
||||
pub use lettre;
|
||||
pub use lettre::smtp;
|
||||
use once_cell::sync::Lazy;
|
||||
use plume_common::activity_pub::{inbox::InboxError, request, sign};
|
||||
use plume_common::activity_pub::PreferredUsernameError;
|
||||
use plume_common::activity_pub::{inbox::InboxError, request, sign, PreferredUsername};
|
||||
use posts::PostEvent;
|
||||
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
|
||||
use std::{fmt, io::Write, string::ToString};
|
||||
use users::UserEvent;
|
||||
|
||||
#[cfg(not(any(feature = "sqlite", feature = "postgres")))]
|
||||
@ -170,6 +176,13 @@ impl From<request::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PreferredUsernameError> for Error {
|
||||
fn from(err: PreferredUsernameError) -> Error {
|
||||
tracing::trace!("{:?}", err);
|
||||
Error::InvalidValue
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Adds a function to a model, that returns the first
|
||||
@ -301,7 +314,7 @@ macro_rules! last {
|
||||
}
|
||||
|
||||
mod config;
|
||||
pub use config::CONFIG;
|
||||
pub use config::{get_rocket_config, Config, SearchTokenizerConfig, CONFIG};
|
||||
|
||||
pub fn ap_url(url: &str) -> String {
|
||||
format!("https://{}", url)
|
||||
@ -334,10 +347,97 @@ impl SmtpNewWithAddr for smtp::SmtpClient {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(AsExpression, PartialEq, Eq, Clone, FromSqlRow, Debug)]
|
||||
#[sql_type = "Text"]
|
||||
pub enum Fqn {
|
||||
Local(PreferredUsername),
|
||||
Remote(PreferredUsername, String),
|
||||
}
|
||||
|
||||
impl Fqn {
|
||||
pub fn new_local(username: String) -> Result<Self> {
|
||||
Ok(Self::Local(
|
||||
PreferredUsername::new(username)?,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn new_remote(username: String, domain: String) -> Result<Self> {
|
||||
Ok(Self::Remote(
|
||||
PreferredUsername::new(username)?,
|
||||
domain,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn make_local_string(base: &str) -> String {
|
||||
base.to_upper_camel_case()
|
||||
.chars()
|
||||
.filter(|c| c.is_ascii_alphanumeric())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn make_local(base: &str) -> Result<Self> {
|
||||
Self::new_local(Self::make_local_string(base))
|
||||
}
|
||||
|
||||
pub fn make_remote(base: &str, domain: String) -> Result<Self> {
|
||||
Self::new_remote(Self::make_local_string(base), domain)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Fqn> for String {
|
||||
fn from(fqn: &Fqn) -> Self {
|
||||
match fqn {
|
||||
Fqn::Local(username) => username.to_string(),
|
||||
Fqn::Remote(username, domain) => format!("{}@{}", username, domain),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Fqn {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
String::from(self).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<DB> ToSql<Text, DB> for Fqn
|
||||
where
|
||||
DB: diesel::backend::Backend,
|
||||
{
|
||||
fn to_sql<W: Write>(
|
||||
&self,
|
||||
out: &mut diesel::serialize::Output<W, DB>,
|
||||
) -> diesel::serialize::Result {
|
||||
let fqn = match self {
|
||||
Self::Local(username) => username.to_string(),
|
||||
Self::Remote(username, domain) => format!("{}@{}", username, domain),
|
||||
};
|
||||
ToSql::<Text, DB>::to_sql::<W>(&fqn, out)
|
||||
}
|
||||
}
|
||||
|
||||
impl<DB> FromSql<Text, DB> for Fqn
|
||||
where
|
||||
DB: diesel::backend::Backend,
|
||||
String: FromSql<Text, DB>,
|
||||
{
|
||||
/// We use PreferredUsername::new_unchecked() because, even if bytes is invalid as `preferredUsername`,
|
||||
/// we need return some value.
|
||||
fn from_sql(bytes: Option<&<DB as Backend>::RawValue>) -> diesel::deserialize::Result<Self> {
|
||||
let value = <String as FromSql<Text, DB>>::from_sql(bytes)?;
|
||||
Ok(match value.rsplit_once('@') {
|
||||
None => Self::Local(unsafe { PreferredUsername::new_unchecked(value) }),
|
||||
Some((username, domain)) => Self::Remote(
|
||||
unsafe { PreferredUsername::new_unchecked(username.into()) },
|
||||
domain.into(),
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
mod tests {
|
||||
use crate::{db_conn, migrations::IMPORTED_MIGRATIONS, Connection as Conn, CONFIG};
|
||||
use crate::{db_conn, migrations::IMPORTED_MIGRATIONS, Connection as Conn, CONFIG, Fqn};
|
||||
use chrono::{naive::NaiveDateTime, Datelike, Timelike};
|
||||
use diesel::r2d2::ConnectionManager;
|
||||
use plume_common::utils::random_hex;
|
||||
@ -398,6 +498,20 @@ mod tests {
|
||||
dt.second()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fqn() {
|
||||
assert_eq!(
|
||||
Fqn::new_local("admin".to_string()).unwrap(),
|
||||
Fqn::new_local("admin".to_string()).unwrap()
|
||||
);
|
||||
assert!(Fqn::new_local("admin".to_string()).is_ok());
|
||||
let fqn = Fqn::new_local("admin".to_string()).unwrap();
|
||||
assert_eq!("admin".to_string(), String::from(&fqn));
|
||||
let fqn = Fqn::new_local("admin".to_string()).unwrap();
|
||||
assert_eq!("admin".to_string(), ToString::to_string(&fqn));
|
||||
assert_eq!("admin".to_string(), fqn.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
pub mod admin;
|
||||
|
@ -205,8 +205,8 @@ mod tests {
|
||||
let expected = json!({
|
||||
"actor": "https://plu.me/@/admin/",
|
||||
"cc": ["https://plu.me/@/admin/followers"],
|
||||
"id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing",
|
||||
"object": "https://plu.me/~/BlogName/testing",
|
||||
"id": "https://plu.me/@/admin/like/https://plu.me/~/Blog%20Name/testing",
|
||||
"object": "https://plu.me/~/Blog%20Name/testing",
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Like",
|
||||
});
|
||||
@ -229,12 +229,12 @@ mod tests {
|
||||
let expected = json!({
|
||||
"actor": "https://plu.me/@/admin/",
|
||||
"cc": ["https://plu.me/@/admin/followers"],
|
||||
"id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing#delete",
|
||||
"id": "https://plu.me/@/admin/like/https://plu.me/~/Blog%20Name/testing#delete",
|
||||
"object": {
|
||||
"actor": "https://plu.me/@/admin/",
|
||||
"cc": ["https://plu.me/@/admin/followers"],
|
||||
"id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing",
|
||||
"object": "https://plu.me/~/BlogName/testing",
|
||||
"id": "https://plu.me/@/admin/like/https://plu.me/~/Blog%20Name/testing",
|
||||
"object": "https://plu.me/~/Blog%20Name/testing",
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Like",
|
||||
},
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
ap_url, blogs::Blog, db_conn::DbConn, instance::Instance, medias::Media, mentions::Mention,
|
||||
post_authors::*, safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User,
|
||||
Connection, Error, PostEvent::*, Result, CONFIG, POST_CHAN,
|
||||
Connection, Error, Fqn, PostEvent::*, Result, CONFIG, POST_CHAN,
|
||||
};
|
||||
use activitystreams::{
|
||||
activity::{Create, Delete, Update},
|
||||
@ -28,7 +28,7 @@ use riker::actors::{Publish, Tell};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
static BLOG_FQN_CACHE: Lazy<Mutex<HashMap<i32, String>>> = Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
static BLOG_FQN_CACHE: Lazy<Mutex<HashMap<i32, Fqn>>> = Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
#[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)]
|
||||
#[changeset_options(treat_none_as_null = "true")]
|
||||
@ -255,7 +255,7 @@ impl Post {
|
||||
ap_url(&format!(
|
||||
"{}/~/{}/{}/",
|
||||
CONFIG.base_url,
|
||||
iri_percent_encode_seg(&blog.fqn),
|
||||
&blog.fqn,
|
||||
iri_percent_encode_seg(slug)
|
||||
))
|
||||
}
|
||||
@ -298,9 +298,9 @@ impl Post {
|
||||
/// This caches query result. The best way to cache query result is holding it in `Post`s field
|
||||
/// but Diesel doesn't allow it currently.
|
||||
/// If sometime Diesel allow it, this method should be removed.
|
||||
pub fn get_blog_fqn(&self, conn: &Connection) -> String {
|
||||
pub fn get_blog_fqn(&self, conn: &Connection) -> Fqn {
|
||||
if let Some(blog_fqn) = BLOG_FQN_CACHE.lock().unwrap().get(&self.blog_id) {
|
||||
return blog_fqn.to_string();
|
||||
return blog_fqn.to_owned();
|
||||
}
|
||||
let blog_fqn = self.get_blog(conn).unwrap().fqn;
|
||||
BLOG_FQN_CACHE
|
||||
@ -1109,10 +1109,10 @@ mod tests {
|
||||
let act = post.to_activity(&conn)?;
|
||||
|
||||
let expected = json!({
|
||||
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"],
|
||||
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/Blog%20Name/"],
|
||||
"cc": [],
|
||||
"content": "Hello",
|
||||
"id": "https://plu.me/~/BlogName/testing",
|
||||
"id": "https://plu.me/~/Blog%20Name/testing",
|
||||
"license": "WTFPL",
|
||||
"name": "Testing",
|
||||
"published": format_datetime(&post.creation_date),
|
||||
@ -1130,7 +1130,7 @@ mod tests {
|
||||
],
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Article",
|
||||
"url": "https://plu.me/~/BlogName/testing"
|
||||
"url": "https://plu.me/~/Blog%20Name/testing"
|
||||
});
|
||||
|
||||
assert_json_eq!(to_value(act)?, expected);
|
||||
@ -1149,12 +1149,12 @@ mod tests {
|
||||
let expected = json!({
|
||||
"actor": "https://plu.me/@/admin/",
|
||||
"cc": [],
|
||||
"id": "https://plu.me/~/BlogName/testing/activity",
|
||||
"id": "https://plu.me/~/Blog%20Name/testing/activity",
|
||||
"object": {
|
||||
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"],
|
||||
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/Blog%20Name/"],
|
||||
"cc": [],
|
||||
"content": "Hello",
|
||||
"id": "https://plu.me/~/BlogName/testing",
|
||||
"id": "https://plu.me/~/Blog%20Name/testing",
|
||||
"license": "WTFPL",
|
||||
"name": "Testing",
|
||||
"published": format_datetime(&post.creation_date),
|
||||
@ -1172,7 +1172,7 @@ mod tests {
|
||||
],
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Article",
|
||||
"url": "https://plu.me/~/BlogName/testing"
|
||||
"url": "https://plu.me/~/Blog%20Name/testing"
|
||||
},
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Create"
|
||||
@ -1194,12 +1194,12 @@ mod tests {
|
||||
let expected = json!({
|
||||
"actor": "https://plu.me/@/admin/",
|
||||
"cc": [],
|
||||
"id": "https://plu.me/~/BlogName/testing/update-",
|
||||
"id": "https://plu.me/~/Blog%20Name/testing/update-",
|
||||
"object": {
|
||||
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"],
|
||||
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/Blog%20Name/"],
|
||||
"cc": [],
|
||||
"content": "Hello",
|
||||
"id": "https://plu.me/~/BlogName/testing",
|
||||
"id": "https://plu.me/~/Blog%20Name/testing",
|
||||
"license": "WTFPL",
|
||||
"name": "Testing",
|
||||
"published": format_datetime(&post.creation_date),
|
||||
@ -1217,7 +1217,7 @@ mod tests {
|
||||
],
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Article",
|
||||
"url": "https://plu.me/~/BlogName/testing"
|
||||
"url": "https://plu.me/~/Blog%20Name/testing"
|
||||
},
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Update"
|
||||
@ -1226,10 +1226,10 @@ mod tests {
|
||||
|
||||
let id = actual["id"].to_string();
|
||||
let (id_pre, id_post) = id.rsplit_once('-').unwrap();
|
||||
assert_eq!(post.ap_url, "https://plu.me/~/BlogName/testing");
|
||||
assert_eq!(post.ap_url, "https://plu.me/~/Blog%20Name/testing");
|
||||
assert_eq!(
|
||||
id_pre,
|
||||
to_value("\"https://plu.me/~/BlogName/testing/update")
|
||||
to_value("\"https://plu.me/~/Blog%20Name/testing/update")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap()
|
||||
@ -1259,9 +1259,9 @@ mod tests {
|
||||
|
||||
let expected = json!({
|
||||
"actor": "https://plu.me/@/admin/",
|
||||
"id": "https://plu.me/~/BlogName/testing#delete",
|
||||
"id": "https://plu.me/~/Blog%20Name/testing#delete",
|
||||
"object": {
|
||||
"id": "https://plu.me/~/BlogName/testing",
|
||||
"id": "https://plu.me/~/Blog%20Name/testing",
|
||||
"type": "Tombstone"
|
||||
},
|
||||
"to": [
|
||||
|
@ -235,8 +235,8 @@ mod test {
|
||||
let expected = json!({
|
||||
"actor": "https://plu.me/@/admin/",
|
||||
"cc": ["https://plu.me/@/admin/followers"],
|
||||
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing",
|
||||
"object": "https://plu.me/~/BlogName/testing",
|
||||
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/Blog%20Name/testing",
|
||||
"object": "https://plu.me/~/Blog%20Name/testing",
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Announce",
|
||||
});
|
||||
@ -259,12 +259,12 @@ mod test {
|
||||
let expected = json!({
|
||||
"actor": "https://plu.me/@/admin/",
|
||||
"cc": ["https://plu.me/@/admin/followers"],
|
||||
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing#delete",
|
||||
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/Blog%20Name/testing#delete",
|
||||
"object": {
|
||||
"actor": "https://plu.me/@/admin/",
|
||||
"cc": ["https://plu.me/@/admin/followers"],
|
||||
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing",
|
||||
"object": "https://plu.me/~/BlogName/testing",
|
||||
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/Blog%20Name/testing",
|
||||
"object": "https://plu.me/~/Blog%20Name/testing",
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Announce"
|
||||
},
|
||||
|
@ -51,6 +51,14 @@ table! {
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
comment_seers (id) {
|
||||
id -> Int4,
|
||||
comment_id -> Int4,
|
||||
user_id -> Int4,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
comments (id) {
|
||||
id -> Int4,
|
||||
@ -66,14 +74,6 @@ table! {
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
comment_seers (id) {
|
||||
id -> Int4,
|
||||
comment_id -> Int4,
|
||||
user_id -> Int4,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
email_blocklist (id) {
|
||||
id -> Int4,
|
||||
@ -314,8 +314,8 @@ allow_tables_to_appear_in_same_query!(
|
||||
apps,
|
||||
blog_authors,
|
||||
blogs,
|
||||
comments,
|
||||
comment_seers,
|
||||
comments,
|
||||
email_blocklist,
|
||||
email_signups,
|
||||
follows,
|
||||
|
@ -77,6 +77,7 @@ impl ActorFactoryArgs<(Arc<Searcher>, DbPool)> for SearchActor {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::diesel::Connection;
|
||||
use crate::Fqn;
|
||||
use crate::{
|
||||
blog_authors::{BlogAuthor, NewBlogAuthor},
|
||||
blogs::{Blog, NewBlog},
|
||||
@ -190,13 +191,22 @@ mod tests {
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let title = random_hex();
|
||||
let blog = NewBlog {
|
||||
instance_id: instance.id,
|
||||
actor_id: random_hex(),
|
||||
ap_url: random_hex(),
|
||||
inbox_url: random_hex(),
|
||||
outbox_url: random_hex(),
|
||||
..Default::default()
|
||||
fqn: Fqn::make_local(&title).unwrap(),
|
||||
title,
|
||||
summary: Default::default(),
|
||||
summary_html: Default::default(),
|
||||
private_key: Default::default(),
|
||||
public_key: Default::default(),
|
||||
icon_id: Default::default(),
|
||||
banner_id: Default::default(),
|
||||
theme: Default::default(),
|
||||
};
|
||||
let blog = Blog::insert(conn, blog).unwrap();
|
||||
BlogAuthor::insert(
|
||||
|
@ -761,7 +761,7 @@ impl User {
|
||||
actor.set_url(ap_url.clone());
|
||||
actor.set_inbox(self.inbox_url.parse()?);
|
||||
actor.set_outbox(self.outbox_url.parse()?);
|
||||
actor.set_preferred_username(self.username.clone());
|
||||
actor.set_preferred_username(&self.fqn);
|
||||
actor.set_followers(self.followers_endpoint.parse()?);
|
||||
|
||||
if let Some(shared_inbox_url) = self.shared_inbox_url.clone() {
|
||||
|
63
src/main.rs
63
src/main.rs
@ -16,7 +16,7 @@ use plume_models::{
|
||||
migrations::IMPORTED_MIGRATIONS,
|
||||
remote_fetch_actor::RemoteFetchActor,
|
||||
search::{actor::SearchActor, Searcher as UnmanagedSearcher},
|
||||
Connection, CONFIG,
|
||||
Config, Connection, CONFIG,
|
||||
};
|
||||
use rocket_csrf::CsrfFairingBuilder;
|
||||
use scheduled_thread_pool::ScheduledThreadPool;
|
||||
@ -47,12 +47,12 @@ include!(concat!(env!("OUT_DIR"), "/templates.rs"));
|
||||
compile_i18n!();
|
||||
|
||||
/// Initializes a database pool.
|
||||
fn init_pool() -> Option<DbPool> {
|
||||
fn init_pool(config: &Config) -> Option<DbPool> {
|
||||
let manager = ConnectionManager::<Connection>::new(CONFIG.database_url.as_str());
|
||||
let mut builder = DbPool::builder()
|
||||
.connection_customizer(Box::new(PragmaForeignKey))
|
||||
.min_idle(CONFIG.db_min_idle);
|
||||
if let Some(max_size) = CONFIG.db_max_size {
|
||||
.min_idle(config.db_min_idle);
|
||||
if let Some(max_size) = config.db_max_size {
|
||||
builder = builder.max_size(max_size);
|
||||
};
|
||||
let pool = builder.build(manager).ok()?;
|
||||
@ -63,28 +63,8 @@ fn init_pool() -> Option<DbPool> {
|
||||
Some(pool)
|
||||
}
|
||||
|
||||
pub(crate) fn init_rocket() -> rocket::Rocket {
|
||||
match dotenv::dotenv() {
|
||||
Ok(path) => eprintln!("Configuration read from {}", path.display()),
|
||||
Err(ref e) if e.not_found() => eprintln!("no .env was found"),
|
||||
e => e.map(|_| ()).unwrap(),
|
||||
}
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
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");
|
||||
pub(crate) fn init_rocket(config: &Config) -> rocket::Rocket {
|
||||
let dbpool = init_pool(config).expect("main: database pool initialization error");
|
||||
if IMPORTED_MIGRATIONS
|
||||
.is_pending(&dbpool.get().unwrap())
|
||||
.unwrap_or(true)
|
||||
@ -104,8 +84,8 @@ Then try to restart Plume.
|
||||
let workpool = ScheduledThreadPool::with_name("worker {}", num_cpus::get());
|
||||
// we want a fast exit here, so
|
||||
let searcher = Arc::new(UnmanagedSearcher::open_or_recreate(
|
||||
&CONFIG.search_index,
|
||||
&CONFIG.search_tokenizers,
|
||||
&config.search_index,
|
||||
&config.search_tokenizers,
|
||||
));
|
||||
RemoteFetchActor::init(dbpool.clone());
|
||||
SearchActor::init(searcher.clone(), dbpool.clone());
|
||||
@ -125,12 +105,12 @@ Then try to restart Plume.
|
||||
.expect("Error setting Ctrl-c handler");
|
||||
|
||||
let mail = mail::init();
|
||||
if mail.is_none() && CONFIG.rocket.as_ref().unwrap().environment.is_prod() {
|
||||
if mail.is_none() && config.rocket.as_ref().unwrap().environment.is_prod() {
|
||||
warn!("Warning: the email server is not configured (or not completely).");
|
||||
warn!("Please refer to the documentation to see how to configure it.");
|
||||
}
|
||||
|
||||
rocket::custom(CONFIG.rocket.clone().unwrap())
|
||||
rocket::custom(config.rocket.clone().unwrap())
|
||||
.mount(
|
||||
"/",
|
||||
routes![
|
||||
@ -280,7 +260,28 @@ Then try to restart Plume.
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let rocket = init_rocket();
|
||||
match dotenv::dotenv() {
|
||||
Ok(path) => eprintln!("Configuration read from {}", path.display()),
|
||||
Err(ref e) if e.not_found() => eprintln!("no .env was found"),
|
||||
e => e.map(|_| ()).unwrap(),
|
||||
}
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
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 rocket = init_rocket(&CONFIG);
|
||||
|
||||
#[cfg(feature = "test")]
|
||||
let rocket = rocket.mount("/test", routes![test_routes::health,]);
|
||||
|
@ -1,4 +1,7 @@
|
||||
use activitystreams::collection::{OrderedCollection, OrderedCollectionPage};
|
||||
use activitystreams::{
|
||||
collection::{OrderedCollection, OrderedCollectionPage},
|
||||
iri_string::{spec::IriSpec, validate::iri_reference},
|
||||
};
|
||||
use diesel::SaveChangesDsl;
|
||||
use rocket::{
|
||||
http::ContentType,
|
||||
@ -80,7 +83,7 @@ pub struct NewBlogForm {
|
||||
|
||||
fn valid_slug(title: &str) -> Result<(), ValidationError> {
|
||||
let slug = Blog::slug(title);
|
||||
if slug.is_empty() {
|
||||
if slug.is_empty() || iri_reference::<IriSpec>(&slug).is_err() {
|
||||
Err(ValidationError::new("empty_slug"))
|
||||
} else {
|
||||
Ok(())
|
||||
@ -101,7 +104,7 @@ pub fn create(
|
||||
Ok(_) => ValidationErrors::new(),
|
||||
Err(e) => e,
|
||||
};
|
||||
if Blog::find_by_fqn(&conn, slug).is_ok() {
|
||||
if Blog::find_by_fqn(&conn, &slug).is_ok() {
|
||||
errors.add(
|
||||
"title",
|
||||
ValidationError {
|
||||
@ -122,7 +125,7 @@ pub fn create(
|
||||
let blog = Blog::insert(
|
||||
&conn,
|
||||
NewBlog::new_local(
|
||||
slug.into(),
|
||||
slug.clone(),
|
||||
form.title.to_string(),
|
||||
String::from(""),
|
||||
Instance::get_local()
|
||||
@ -144,7 +147,7 @@ pub fn create(
|
||||
.expect("blog::create: author error");
|
||||
|
||||
Flash::success(
|
||||
Redirect::to(uri!(details: name = slug, page = _)),
|
||||
Redirect::to(uri!(details: name = &slug, page = _)),
|
||||
&i18n!(intl, "Your blog was successfully created!"),
|
||||
)
|
||||
.into()
|
||||
@ -379,6 +382,8 @@ pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::env::var;
|
||||
|
||||
use super::valid_slug;
|
||||
use crate::init_rocket;
|
||||
use diesel::Connection;
|
||||
@ -387,23 +392,92 @@ mod tests {
|
||||
blog_authors::{BlogAuthor, NewBlogAuthor},
|
||||
blogs::{Blog, NewBlog},
|
||||
db_conn::{DbConn, DbPool},
|
||||
get_rocket_config,
|
||||
instance::{Instance, NewInstance},
|
||||
post_authors::{NewPostAuthor, PostAuthor},
|
||||
posts::{NewPost, Post},
|
||||
safe_string::SafeString,
|
||||
search::Searcher,
|
||||
users::{NewUser, User, AUTH_COOKIE},
|
||||
Connection as Conn, CONFIG,
|
||||
Config, Fqn, SearchTokenizerConfig,
|
||||
};
|
||||
use rocket::{
|
||||
http::{Cookie, Cookies, SameSite},
|
||||
http::{ContentType, Cookie, Cookies, SameSite, Status},
|
||||
local::{Client, LocalRequest},
|
||||
};
|
||||
|
||||
type Models = (Instance, User, Blog, Post);
|
||||
|
||||
fn setup() -> (Client, Models) {
|
||||
dotenv::from_path(".env.test").unwrap();
|
||||
let config = Config {
|
||||
base_url: var("BASE_URL").unwrap(),
|
||||
db_name: "plume",
|
||||
db_max_size: None,
|
||||
db_min_idle: None,
|
||||
signup: Default::default(),
|
||||
database_url: var("DATABASE_URL").unwrap(),
|
||||
search_index: format!("/tmp/plume_test-{}", random_hex()),
|
||||
search_tokenizers: SearchTokenizerConfig::init(),
|
||||
rocket: get_rocket_config(),
|
||||
logo: Default::default(),
|
||||
default_theme: Default::default(),
|
||||
media_directory: format!("/tmp/plume_test-{}", random_hex()),
|
||||
mail: None,
|
||||
ldap: None,
|
||||
proxy: None,
|
||||
};
|
||||
let _ = Searcher::create(&config.search_index, &config.search_tokenizers).unwrap();
|
||||
let rocket = init_rocket(&config);
|
||||
let client = Client::new(rocket).expect("valid rocket instance");
|
||||
let dbpool = client.rocket().state::<DbPool>().unwrap();
|
||||
let conn = &DbConn(dbpool.get().unwrap());
|
||||
|
||||
(client, create_models(conn))
|
||||
}
|
||||
|
||||
fn teardown((client, (instance, user, _blog, _post)): (&Client, Models)) {
|
||||
let rocket = client.rocket();
|
||||
|
||||
let dbpool = rocket.state::<DbPool>().unwrap();
|
||||
let conn = &DbConn(dbpool.get().unwrap());
|
||||
|
||||
user.delete(conn).unwrap();
|
||||
let _ = diesel::delete(&instance);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_link_within_post_card() {
|
||||
let conn = Conn::establish(CONFIG.database_url.as_str()).unwrap();
|
||||
let (client, (instance, user, blog, post)) = setup();
|
||||
|
||||
let blog_path = uri!(super::activity_details: name = &blog.fqn.to_string()).to_string();
|
||||
let edit_link = uri!(
|
||||
super::super::posts::edit: blog = &blog.fqn.to_string(),
|
||||
slug = &post.slug
|
||||
)
|
||||
.to_string();
|
||||
|
||||
let mut response = client.get(&blog_path).dispatch();
|
||||
let body = response.body_string().unwrap();
|
||||
let body_not_contain_edit_link = !body.contains(&edit_link);
|
||||
|
||||
let request = client.get(&blog_path);
|
||||
login(&request, &user);
|
||||
let mut response = request.dispatch();
|
||||
let body = response.body_string().unwrap();
|
||||
eprintln!("{:?}", &blog.fqn);
|
||||
let body_contains_edit_lnk = body.contains(&edit_link);
|
||||
|
||||
teardown((&client, (instance, user, blog, post)));
|
||||
|
||||
assert!(body_not_contain_edit_link);
|
||||
assert!(body_contains_edit_lnk);
|
||||
}
|
||||
|
||||
fn create_models(conn: &DbConn) -> Models {
|
||||
Instance::find_by_domain(conn, "example.org").unwrap_or_else(|_| {
|
||||
Instance::insert(
|
||||
&conn,
|
||||
conn,
|
||||
NewInstance {
|
||||
public_domain: "example.org".to_string(),
|
||||
name: "Plume".to_string(),
|
||||
@ -416,33 +490,9 @@ mod tests {
|
||||
long_description_html: String::new(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let rocket = init_rocket();
|
||||
let client = Client::new(rocket).expect("valid rocket instance");
|
||||
let dbpool = client.rocket().state::<DbPool>().unwrap();
|
||||
let conn = &DbConn(dbpool.get().unwrap());
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let (_instance, user, blog, post) = create_models(conn);
|
||||
|
||||
let blog_path = uri!(super::activity_details: name = &blog.fqn).to_string();
|
||||
let edit_link = uri!(
|
||||
super::super::posts::edit: blog = &blog.fqn,
|
||||
slug = &post.slug
|
||||
)
|
||||
.to_string();
|
||||
|
||||
let mut response = client.get(&blog_path).dispatch();
|
||||
let body = response.body_string().unwrap();
|
||||
assert!(!body.contains(&edit_link));
|
||||
|
||||
let request = client.get(&blog_path);
|
||||
login(&request, &user);
|
||||
let mut response = request.dispatch();
|
||||
let body = response.body_string().unwrap();
|
||||
assert!(body.contains(&edit_link));
|
||||
}
|
||||
|
||||
fn create_models(conn: &DbConn) -> (Instance, User, Blog, Post) {
|
||||
conn.transaction::<(Instance, User, Blog, Post), diesel::result::Error, _>(|| {
|
||||
let instance = Instance::get_local().unwrap_or_else(|_| {
|
||||
let instance = Instance::insert(
|
||||
@ -470,16 +520,26 @@ mod tests {
|
||||
inbox_url: random_hex(),
|
||||
outbox_url: random_hex(),
|
||||
followers_endpoint: random_hex(),
|
||||
fqn: random_hex(),
|
||||
..Default::default()
|
||||
};
|
||||
let user = User::insert(conn, user).unwrap();
|
||||
let title = random_hex();
|
||||
let blog = NewBlog {
|
||||
instance_id: instance.id,
|
||||
fqn: Fqn::make_local(&title).unwrap(),
|
||||
title,
|
||||
actor_id: random_hex(),
|
||||
ap_url: random_hex(),
|
||||
inbox_url: random_hex(),
|
||||
outbox_url: random_hex(),
|
||||
..Default::default()
|
||||
summary: Default::default(),
|
||||
summary_html: Default::default(),
|
||||
public_key: Default::default(),
|
||||
private_key: Default::default(),
|
||||
icon_id: Default::default(),
|
||||
banner_id: Default::default(),
|
||||
theme: Default::default(),
|
||||
};
|
||||
let blog = Blog::insert(conn, blog).unwrap();
|
||||
BlogAuthor::insert(
|
||||
@ -535,4 +595,38 @@ mod tests {
|
||||
assert!(valid_slug("Blog Title").is_ok());
|
||||
assert!(valid_slug("ブログ タイトル").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_blog_with_same_title_twice() {
|
||||
let (client, (instance, user, blog, post)) = setup();
|
||||
|
||||
let new_path = uri!(super::new).to_string();
|
||||
let request = client.get(new_path);
|
||||
login(&request, &user);
|
||||
let mut response = request.dispatch();
|
||||
let body = response.body_string().unwrap();
|
||||
let prefix = r#"<input type="hidden" name="csrf-token" value=""#;
|
||||
let pos = body.find(prefix).unwrap();
|
||||
let token = body[pos + prefix.len()..pos + prefix.len() + 123].to_string();
|
||||
|
||||
let create_path = uri!(super::create).to_string();
|
||||
let response = client
|
||||
.post(&create_path)
|
||||
.body(format!("title=My%20Blog&csrf-token={}", &token))
|
||||
.header(ContentType::Form)
|
||||
.dispatch();
|
||||
let first_attempt = response;
|
||||
|
||||
let response = client
|
||||
.post(&create_path)
|
||||
.body(format!("title=My%20Blog&csrf-token={}", &token))
|
||||
.header(ContentType::Form)
|
||||
.dispatch();
|
||||
let second_attempt = response;
|
||||
|
||||
teardown((&client, (instance, user, blog, post)));
|
||||
|
||||
assert_eq!(first_attempt.status(), Status::SeeOther);
|
||||
assert_eq!(second_attempt.status(), Status::SeeOther);
|
||||
}
|
||||
}
|
||||
|
@ -407,7 +407,7 @@ pub fn interact(conn: DbConn, user: Option<User>, target: String) -> Option<Redi
|
||||
|
||||
if let Ok(post) = Post::from_id(&conn, &target, None, CONFIG.proxy()) {
|
||||
return Some(Redirect::to(uri!(
|
||||
super::posts::details: blog = post.get_blog(&conn).expect("Can't retrieve blog").fqn,
|
||||
super::posts::details: blog = post.get_blog(&conn).expect("Can't retrieve blog").fqn.to_string(),
|
||||
slug = &post.slug,
|
||||
responding_to = _
|
||||
)));
|
||||
@ -418,7 +418,7 @@ pub fn interact(conn: DbConn, user: Option<User>, target: String) -> Option<Redi
|
||||
let post = comment.get_post(&conn).expect("Can't retrieve post");
|
||||
return Some(Redirect::to(uri!(
|
||||
super::posts::details: blog =
|
||||
post.get_blog(&conn).expect("Can't retrieve blog").fqn,
|
||||
post.get_blog(&conn).expect("Can't retrieve blog").fqn.to_string(),
|
||||
slug = &post.slug,
|
||||
responding_to = comment.id
|
||||
)));
|
||||
|
@ -22,7 +22,7 @@
|
||||
<meta content="@blog.summary_html" property="og:description" />
|
||||
<meta content="@blog.icon_url(ctx.0)" property="og:image" />
|
||||
|
||||
<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.to_string(), "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='canonical'>
|
||||
@if !ctx.2.clone().map(|u| u.hide_custom_css).unwrap_or(false) {
|
||||
@ -31,7 +31,7 @@
|
||||
}
|
||||
}
|
||||
}, {
|
||||
<a href="@uri!(blogs::details: name = &blog.fqn, page = _)" dir="auto">@blog.title</a>
|
||||
<a href="@uri!(blogs::details: name = &blog.fqn.to_string(), page = _)" dir="auto">@blog.title</a>
|
||||
}, {
|
||||
<div class="hidden">
|
||||
@for author in authors {
|
||||
@ -58,8 +58,8 @@
|
||||
</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" 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>
|
||||
<a href="@uri!(posts::new: blog = &blog.fqn.to_string())" class="button" dir="auto">@i18n!(ctx.1, "New article")</a>
|
||||
<a href="@uri!(blogs::edit: name = &blog.fqn.to_string())" class="button" dir="auto">@i18n!(ctx.1, "Edit")</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -76,7 +76,7 @@
|
||||
<section>
|
||||
<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>
|
||||
<small><a href="@uri!(blogs::atom_feed: name = &blog.fqn.to_string())" title="Atom feed">@icon!("rss")</a></small>
|
||||
</h2>
|
||||
@if posts.is_empty() {
|
||||
<p dir="auto">@i18n!(ctx.1, "No posts to see here yet.")</p>
|
||||
|
@ -12,10 +12,10 @@
|
||||
@(ctx: BaseContext, blog: &Blog, medias: Vec<Media>, form: &EditForm, errors: ValidationErrors)
|
||||
|
||||
@:base(ctx, i18n!(ctx.1, "Edit \"{}\""; &blog.title), {}, {
|
||||
<a href="@uri!(blogs::details: name = &blog.fqn, page = _)">@blog.title</a>
|
||||
<a href="@uri!(blogs::details: name = &blog.fqn.to_string(), page = _)">@blog.title</a>
|
||||
}, {
|
||||
<h1>@i18n!(ctx.1, "Edit \"{}\""; &blog.title)</h1>
|
||||
<form method="post" action="@uri!(blogs::update: name = &blog.fqn)">
|
||||
<form method="post" action="@uri!(blogs::update: name = &blog.fqn.to_string())">
|
||||
<!-- Rocket hack to use various HTTP methods -->
|
||||
<input type=hidden name="_method" value="put">
|
||||
|
||||
@ -53,7 +53,7 @@
|
||||
|
||||
<h2>@i18n!(ctx.1, "Danger zone")</h2>
|
||||
<p>@i18n!(ctx.1, "Be very careful, any action taken here can't be reversed.")</p>
|
||||
<form method="post" action="@uri!(blogs::delete: name = &blog.fqn)" onsubmit="return confirm('@i18n!(ctx.1, "Are you sure that you want to permanently delete this blog?")')">
|
||||
<form method="post" action="@uri!(blogs::delete: name = &blog.fqn.to_string())" onsubmit="return confirm('@i18n!(ctx.1, "Are you sure that you want to permanently delete this blog?")')">
|
||||
<input type="submit" class="inline-block button destructive" value="@i18n!(ctx.1, "Permanently delete this blog")">
|
||||
</form>
|
||||
})
|
||||
|
@ -6,19 +6,19 @@
|
||||
|
||||
<div class="card h-entry">
|
||||
@if article.cover_id.is_some() {
|
||||
<a class="cover-link" href="@uri!(posts::details: blog = article.get_blog_fqn(ctx.0), slug = &article.slug, responding_to = _)">
|
||||
<a class="cover-link" href="@uri!(posts::details: blog = article.get_blog_fqn(ctx.0).to_string(), slug = &article.slug, responding_to = _)">
|
||||
<div class="cover" style="background-image: url('@Html(article.cover_url(ctx.0).unwrap_or_default())')"></div>
|
||||
</a>
|
||||
}
|
||||
<header dir="auto">
|
||||
<h3 class="p-name">
|
||||
<a class="u-url" href="@uri!(posts::details: blog = article.get_blog_fqn(ctx.0), slug = &article.slug, responding_to = _)">
|
||||
<a class="u-url" href="@uri!(posts::details: blog = article.get_blog_fqn(ctx.0).to_string(), slug = &article.slug, responding_to = _)">
|
||||
@article.title
|
||||
</a>
|
||||
</h3>
|
||||
@if ctx.2.clone().and_then(|u| article.is_author(ctx.0, u.id).ok()).unwrap_or(false) {
|
||||
<div class="controls">
|
||||
<a class="button" href="@uri!(posts::edit: blog = &article.get_blog_fqn(ctx.0), slug = &article.slug)">@i18n!(ctx.1, "Edit")</a>
|
||||
<a class="button" href="@uri!(posts::edit: blog = &article.get_blog_fqn(ctx.0).to_string(), slug = &article.slug)">@i18n!(ctx.1, "Edit")</a>
|
||||
</div>
|
||||
}
|
||||
</header>
|
||||
@ -35,7 +35,7 @@
|
||||
@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_fqn(ctx.0), page = _)">@article.get_blog(ctx.0).unwrap().title</a>
|
||||
⋅ <a href="@uri!(blogs::details: name = &article.get_blog_fqn(ctx.0).to_string(), page = _)">@article.get_blog(ctx.0).unwrap().title</a>
|
||||
⋅
|
||||
</div>
|
||||
@if !article.published {
|
||||
|
@ -18,7 +18,7 @@
|
||||
@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="@uri!(posts::details: blog = &blog.fqn.to_string(), slug = &article.slug, responding_to = _)"/>
|
||||
<meta property="og:description" content="@article.subtitle"/>
|
||||
<link rel="canonical" href="@article.ap_url"/>
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
}
|
||||
}
|
||||
}, {
|
||||
<a href="@uri!(blogs::details: name = &blog.fqn, page = _)">@blog.title</a>
|
||||
<a href="@uri!(blogs::details: name = &blog.fqn.to_string(), page = _)">@blog.title</a>
|
||||
}, {
|
||||
<div class="h-entry">
|
||||
<header
|
||||
@ -78,7 +78,7 @@
|
||||
</section>
|
||||
@if ctx.2.is_some() {
|
||||
<section class="actions">
|
||||
<form id="likes" class="likes" action="@uri!(likes::create: blog = &blog.fqn, slug = &article.slug)#likes" method="POST">
|
||||
<form id="likes" class="likes" action="@uri!(likes::create: blog = &blog.fqn.to_string(), slug = &article.slug)#likes" method="POST">
|
||||
<p aria-label="@i18n!(ctx.1, "One like", "{0} likes"; n_likes)" title="@i18n!(ctx.1, "One like", "{0} likes"; n_likes)">
|
||||
@n_likes
|
||||
</p>
|
||||
@ -89,7 +89,7 @@
|
||||
<button type="submit" class="action">@icon!("heart") @i18n!(ctx.1, "Add yours")</button>
|
||||
}
|
||||
</form>
|
||||
<form id="reshares" class="reshares" action="@uri!(reshares::create: blog = &blog.fqn, slug = &article.slug)#reshares" method="POST">
|
||||
<form id="reshares" class="reshares" action="@uri!(reshares::create: blog = &blog.fqn.to_string(), slug = &article.slug)#reshares" method="POST">
|
||||
<p aria-label="@i18n!(ctx.1, "One boost", "{0} boosts"; n_reshares)" title="@i18n!(ctx.1, "One boost", "{0} boosts"; n_reshares)">
|
||||
@n_reshares
|
||||
</p>
|
||||
@ -104,7 +104,7 @@
|
||||
} else {
|
||||
<p class="center">@Html(i18n!(ctx.1, "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article";
|
||||
format!("<a href='{}'>", escape(&uri!(session::new: m = _).to_string())), "</a>",
|
||||
format!("<a href='{}'>", escape(&uri!(posts::remote_interact: blog_name = &blog.fqn, slug = &article.slug).to_string())), "</a>"
|
||||
format!("<a href='{}'>", escape(&uri!(posts::remote_interact: blog_name = &blog.fqn.to_string(), slug = &article.slug).to_string())), "</a>"
|
||||
))
|
||||
</p>
|
||||
<section class="actions">
|
||||
@ -112,14 +112,14 @@
|
||||
<p aria-label="@i18n!(ctx.1, "One like", "{0} likes"; n_likes)" title="@i18n!(ctx.1, "One like", "{0} likes"; n_likes)">
|
||||
@n_likes
|
||||
</p>
|
||||
<a href="@uri!(posts::remote_interact: blog_name = &blog.fqn, slug = &article.slug)" class="action">@icon!("heart") @i18n!(ctx.1, "Add yours")</a>
|
||||
<a href="@uri!(posts::remote_interact: blog_name = &blog.fqn.to_string(), slug = &article.slug)" class="action">@icon!("heart") @i18n!(ctx.1, "Add yours")</a>
|
||||
</div>
|
||||
|
||||
<div id="reshares" class="reshares">
|
||||
<p aria-label="@i18n!(ctx.1, "One boost", "{0} boost"; n_reshares)" title="@i18n!(ctx.1, "One boost", "{0} boosts"; n_reshares)">
|
||||
@n_reshares
|
||||
</p>
|
||||
<a href="@uri!(posts::remote_interact: blog_name = &blog.fqn, slug = &article.slug)" class="action">@icon!("repeat") @i18n!(ctx.1, "Boost")</a>
|
||||
<a href="@uri!(posts::remote_interact: blog_name = &blog.fqn.to_string(), slug = &article.slug)" class="action">@icon!("repeat") @i18n!(ctx.1, "Boost")</a>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
@ -144,7 +144,7 @@
|
||||
<h2>@i18n!(ctx.1, "Comments")</h2>
|
||||
|
||||
@if ctx.2.is_some() {
|
||||
<form method="post" action="@uri!(comments::create: blog_name = &blog.fqn, slug = &article.slug)#comments">
|
||||
<form method="post" action="@uri!(comments::create: blog_name = &blog.fqn.to_string(), slug = &article.slug)#comments">
|
||||
@(Input::new("warning", i18n!(ctx.1, "Content warning"))
|
||||
.default(&comment_form.warning)
|
||||
.error(&comment_errors)
|
||||
@ -162,7 +162,7 @@
|
||||
|
||||
@if !comments.is_empty() {
|
||||
@for comm in comments {
|
||||
@:comment(ctx, &comm, Some(&article.ap_url), &blog.fqn, &article.slug)
|
||||
@:comment(ctx, &comm, Some(&article.ap_url), &blog.fqn.to_string(), &article.slug)
|
||||
}
|
||||
} else {
|
||||
<p class="center" dir="auto">@i18n!(ctx.1, "No comments yet. Be the first to react!")</p>
|
||||
@ -173,7 +173,7 @@
|
||||
@if ctx.2.clone().and_then(|u| article.is_author(ctx.0, u.id).ok()).unwrap_or(false) {
|
||||
<aside class="bottom-bar">
|
||||
<div>
|
||||
<form class="inline" method="post" action="@uri!(posts::delete: blog_name = &blog.fqn, slug = &article.slug)">
|
||||
<form class="inline" method="post" action="@uri!(posts::delete: blog_name = &blog.fqn.to_string(), slug = &article.slug)">
|
||||
<input class="button destructive" onclick="return confirm('@i18n!(ctx.1, "Are you sure?")')" type="submit" value="@i18n!(ctx.1, "Delete")">
|
||||
</form>
|
||||
</div>
|
||||
@ -186,9 +186,9 @@
|
||||
</div>
|
||||
<div>
|
||||
@if !article.published {
|
||||
<a class="button secondary" href="@uri!(posts::edit: blog = &blog.fqn, slug = &article.slug)">@i18n!(ctx.1, "Publish")</a>
|
||||
<a class="button secondary" href="@uri!(posts::edit: blog = &blog.fqn.to_string(), slug = &article.slug)">@i18n!(ctx.1, "Publish")</a>
|
||||
}
|
||||
<a class="button" href="@uri!(posts::edit: blog = &blog.fqn, slug = &article.slug)">@i18n!(ctx.1, "Edit")</a>
|
||||
<a class="button" href="@uri!(posts::edit: blog = &blog.fqn.to_string(), slug = &article.slug)">@i18n!(ctx.1, "Edit")</a>
|
||||
</div>
|
||||
</aside>
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user