Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6405ba57ee | |||
| d99b42582d | |||
| 92a386277b | |||
| 297d9fcf40 | |||
| ef70cb93e6 | |||
| efb76a3c17 | |||
| 197f0d7ecd | |||
| 3be842c653 | |||
| dabe904642 | |||
| 180e34b07c | |||
| 847d6f7fac | |||
| 6a3f210dfc | |||
| 73aa301d4a | |||
| b3d367b174 | |||
| edaccd1a31 | |||
| e1bd2eab75 | |||
| ac7a05b09a | |||
| 71e0a35e06 | |||
| c217e5e9b3 | |||
| 8ba0c17db5 | |||
| 4e43c676b4 | |||
| b834d1c282 | |||
| 506fe9955d | |||
| 02c528cae4 |
@@ -226,6 +226,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- restore_env:
|
- restore_env:
|
||||||
cache: none
|
cache: none
|
||||||
|
- run: cargo build
|
||||||
- run: crowdin upload -b master
|
- run: crowdin upload -b master
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
|
|||||||
Generated
+1754
-1383
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -24,7 +24,6 @@ rocket = "0.4.2"
|
|||||||
rocket_contrib = { version = "0.4.2", features = ["json"] }
|
rocket_contrib = { version = "0.4.2", features = ["json"] }
|
||||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||||
rpassword = "4.0"
|
rpassword = "4.0"
|
||||||
runtime-fmt = "0.4.0"
|
|
||||||
scheduled-thread-pool = "0.2.2"
|
scheduled-thread-pool = "0.2.2"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
@@ -79,6 +78,7 @@ postgres = ["plume-models/postgres", "diesel/postgres"]
|
|||||||
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
|
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
|
||||||
debug-mailer = []
|
debug-mailer = []
|
||||||
test = []
|
test = []
|
||||||
|
search-lindera = ["plume-models/search-lindera"]
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["plume-api", "plume-cli", "plume-models", "plume-common", "plume-front", "plume-macro"]
|
members = ["plume-api", "plume-cli", "plume-models", "plume-common", "plume-front", "plume-macro"]
|
||||||
|
|||||||
@@ -192,6 +192,27 @@ p.error {
|
|||||||
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
footer.authors {
|
||||||
|
div {
|
||||||
|
float: left;
|
||||||
|
margin-right: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.likes { color: $red; }
|
||||||
|
.reshares { color: $primary; }
|
||||||
|
|
||||||
|
span.likes, span.resahres {
|
||||||
|
font-family: "Route159",serif;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg.feather {
|
||||||
|
width: 0.85em;
|
||||||
|
height: 0.85em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
}
|
}
|
||||||
@@ -469,93 +490,6 @@ figure {
|
|||||||
|
|
||||||
/// Small screens
|
/// Small screens
|
||||||
@media screen and (max-width: 600px) {
|
@media screen and (max-width: 600px) {
|
||||||
@keyframes menuOpening {
|
|
||||||
from {
|
|
||||||
transform: scaleX(0);
|
|
||||||
transform-origin: left;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: scaleX(1);
|
|
||||||
transform-origin: left;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body > header {
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
nav#menu {
|
|
||||||
display: inline-flex;
|
|
||||||
z-index: 21;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content {
|
|
||||||
display: none;
|
|
||||||
appearance: none;
|
|
||||||
text-align: center;
|
|
||||||
z-index: 20;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body > header:focus-within #content, #content.show {
|
|
||||||
position: fixed;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
animation: 0.2s menuOpening;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
transform: skewX(-10deg);
|
|
||||||
top: 0;
|
|
||||||
left: -20%;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
z-index: -10;
|
|
||||||
|
|
||||||
background: $primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
> nav {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
a {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0;
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
color: $background;
|
|
||||||
font-size: 1.4em;
|
|
||||||
font-weight: 300;
|
|
||||||
|
|
||||||
&.title { font-size: 1.8em; }
|
|
||||||
|
|
||||||
> *:first-child { width: 3rem; }
|
|
||||||
> img:first-child { height: 3rem; }
|
|
||||||
> *:last-child { margin-left: 1rem; }
|
|
||||||
> nav hr {
|
|
||||||
display: block;
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
border: solid $background 0.1rem;
|
|
||||||
}
|
|
||||||
.mobile-label { display: initial; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main .article-meta {
|
main .article-meta {
|
||||||
> *, .comments {
|
> *, .comments {
|
||||||
margin: 0 5%;
|
margin: 0 5%;
|
||||||
|
|||||||
@@ -101,6 +101,96 @@ body > header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Small screens
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
@keyframes menuOpening {
|
||||||
|
from {
|
||||||
|
transform: scaleX(0);
|
||||||
|
transform-origin: left;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scaleX(1);
|
||||||
|
transform-origin: left;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body > header {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
nav#menu {
|
||||||
|
display: inline-flex;
|
||||||
|
z-index: 21;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
display: none;
|
||||||
|
appearance: none;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body > header:focus-within #content, #content.show {
|
||||||
|
position: fixed;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
animation: 0.2s menuOpening;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
transform: skewX(-10deg);
|
||||||
|
top: 0;
|
||||||
|
left: -20%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
z-index: -10;
|
||||||
|
|
||||||
|
background: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
> nav {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
color: $background;
|
||||||
|
font-size: 1.4em;
|
||||||
|
font-weight: 300;
|
||||||
|
|
||||||
|
&.title { font-size: 1.8em; }
|
||||||
|
|
||||||
|
> *:first-child { width: 3rem; }
|
||||||
|
> img:first-child { height: 3rem; }
|
||||||
|
> *:last-child { margin-left: 1rem; }
|
||||||
|
> nav hr {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
border: solid $background 0.1rem;
|
||||||
|
}
|
||||||
|
.mobile-label { display: initial; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Only enable label animations on large screens */
|
/* Only enable label animations on large screens */
|
||||||
@media screen and (min-width: 600px) {
|
@media screen and (min-width: 600px) {
|
||||||
header nav a {
|
header nav a {
|
||||||
|
|||||||
@@ -23,3 +23,4 @@ path = "../plume-models"
|
|||||||
[features]
|
[features]
|
||||||
postgres = ["plume-models/postgres", "diesel/postgres"]
|
postgres = ["plume-models/postgres", "diesel/postgres"]
|
||||||
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
|
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
|
||||||
|
search-lindera = ["plume-models/search-lindera"]
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ fn init<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
if can_do || force {
|
if can_do || force {
|
||||||
let searcher = Searcher::create(&path).unwrap();
|
let searcher = Searcher::create(&path, &CONFIG.search_tokenizers).unwrap();
|
||||||
refill(args, conn, Some(searcher));
|
refill(args, conn, Some(searcher));
|
||||||
} else {
|
} else {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
@@ -98,7 +98,8 @@ fn refill<'a>(args: &ArgMatches<'a>, conn: &Connection, searcher: Option<Searche
|
|||||||
Some(path) => Path::new(path).join("search_index"),
|
Some(path) => Path::new(path).join("search_index"),
|
||||||
None => Path::new(&CONFIG.search_index).to_path_buf(),
|
None => Path::new(&CONFIG.search_index).to_path_buf(),
|
||||||
};
|
};
|
||||||
let searcher = searcher.unwrap_or_else(|| Searcher::open(&path).unwrap());
|
let searcher =
|
||||||
|
searcher.unwrap_or_else(|| Searcher::open(&path, &CONFIG.search_tokenizers).unwrap());
|
||||||
|
|
||||||
searcher.fill(conn).expect("Couldn't import post");
|
searcher.fill(conn).expect("Couldn't import post");
|
||||||
println!("Commiting result");
|
println!("Commiting result");
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ serde_json = "1.0"
|
|||||||
shrinkwraprs = "0.2.1"
|
shrinkwraprs = "0.2.1"
|
||||||
syntect = "3.3"
|
syntect = "3.3"
|
||||||
tokio = "0.1.22"
|
tokio = "0.1.22"
|
||||||
|
regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] }
|
||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use heck::CamelCase;
|
use heck::CamelCase;
|
||||||
use openssl::rand::rand_bytes;
|
use openssl::rand::rand_bytes;
|
||||||
use pulldown_cmark::{html, Event, Options, Parser, Tag};
|
use pulldown_cmark::{html, Event, Options, Parser, Tag};
|
||||||
|
use regex_syntax::is_word_character;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::uri::Uri,
|
http::uri::Uri,
|
||||||
response::{Flash, Redirect},
|
response::{Flash, Redirect},
|
||||||
@@ -200,6 +201,12 @@ fn process_image<'a, 'b>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
struct DocumentContext {
|
||||||
|
in_code: bool,
|
||||||
|
in_link: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns (HTML, mentions, hashtags)
|
/// Returns (HTML, mentions, hashtags)
|
||||||
pub fn md_to_html<'a>(
|
pub fn md_to_html<'a>(
|
||||||
md: &str,
|
md: &str,
|
||||||
@@ -223,13 +230,21 @@ pub fn md_to_html<'a>(
|
|||||||
.map(|evt| process_image(evt, inline, &media_processor))
|
.map(|evt| process_image(evt, inline, &media_processor))
|
||||||
// Ignore headings, images, and tables if inline = true
|
// Ignore headings, images, and tables if inline = true
|
||||||
.scan((vec![], inline), inline_tags)
|
.scan((vec![], inline), inline_tags)
|
||||||
.scan(false, |in_code, evt| match evt {
|
.scan(&mut DocumentContext::default(), |ctx, evt| match evt {
|
||||||
Event::Start(Tag::CodeBlock(_)) | Event::Start(Tag::Code) => {
|
Event::Start(Tag::CodeBlock(_)) | Event::Start(Tag::Code) => {
|
||||||
*in_code = true;
|
ctx.in_code = true;
|
||||||
Some((vec![evt], vec![], vec![]))
|
Some((vec![evt], vec![], vec![]))
|
||||||
}
|
}
|
||||||
Event::End(Tag::CodeBlock(_)) | Event::End(Tag::Code) => {
|
Event::End(Tag::CodeBlock(_)) | Event::End(Tag::Code) => {
|
||||||
*in_code = false;
|
ctx.in_code = false;
|
||||||
|
Some((vec![evt], vec![], vec![]))
|
||||||
|
}
|
||||||
|
Event::Start(Tag::Link(_, _)) => {
|
||||||
|
ctx.in_link = true;
|
||||||
|
Some((vec![evt], vec![], vec![]))
|
||||||
|
}
|
||||||
|
Event::End(Tag::Link(_, _)) => {
|
||||||
|
ctx.in_link = false;
|
||||||
Some((vec![evt], vec![], vec![]))
|
Some((vec![evt], vec![], vec![]))
|
||||||
}
|
}
|
||||||
Event::Text(txt) => {
|
Event::Text(txt) => {
|
||||||
@@ -269,7 +284,7 @@ pub fn md_to_html<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
State::Hashtag => {
|
State::Hashtag => {
|
||||||
let char_matches = c.is_alphanumeric() || "-_".contains(c);
|
let char_matches = c == '-' || is_word_character(c);
|
||||||
if char_matches && (n < (txt.chars().count() - 1)) {
|
if char_matches && (n < (txt.chars().count() - 1)) {
|
||||||
text_acc.push(c);
|
text_acc.push(c);
|
||||||
(events, State::Hashtag, text_acc, n + 1, mentions, hashtags)
|
(events, State::Hashtag, text_acc, n + 1, mentions, hashtags)
|
||||||
@@ -300,7 +315,7 @@ pub fn md_to_html<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
State::Ready => {
|
State::Ready => {
|
||||||
if !*in_code && c == '@' {
|
if !ctx.in_code && !ctx.in_link && c == '@' {
|
||||||
events.push(Event::Text(text_acc.into()));
|
events.push(Event::Text(text_acc.into()));
|
||||||
(
|
(
|
||||||
events,
|
events,
|
||||||
@@ -310,7 +325,7 @@ pub fn md_to_html<'a>(
|
|||||||
mentions,
|
mentions,
|
||||||
hashtags,
|
hashtags,
|
||||||
)
|
)
|
||||||
} else if !*in_code && c == '#' {
|
} else if !ctx.in_code && !ctx.in_link && c == '#' {
|
||||||
events.push(Event::Text(text_acc.into()));
|
events.push(Event::Text(text_acc.into()));
|
||||||
(
|
(
|
||||||
events,
|
events,
|
||||||
@@ -399,6 +414,7 @@ mod tests {
|
|||||||
("not_a@mention", vec![]),
|
("not_a@mention", vec![]),
|
||||||
("`@helo`", vec![]),
|
("`@helo`", vec![]),
|
||||||
("```\n@hello\n```", vec![]),
|
("```\n@hello\n```", vec![]),
|
||||||
|
("[@atmark in link](https://example.org/)", vec![]),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (md, mentions) in tests {
|
for (md, mentions) in tests {
|
||||||
@@ -424,6 +440,9 @@ mod tests {
|
|||||||
("with some punctuation #test!", vec!["test"]),
|
("with some punctuation #test!", vec!["test"]),
|
||||||
(" #spaces ", vec!["spaces"]),
|
(" #spaces ", vec!["spaces"]),
|
||||||
("not_a#hashtag", vec![]),
|
("not_a#hashtag", vec![]),
|
||||||
|
("#نرمافزار_آزاد", vec!["نرمافزار_آزاد"]),
|
||||||
|
("[#hash in link](https://example.org/)", vec![]),
|
||||||
|
("#zwsp\u{200b}inhash", vec!["zwsp"]),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (md, mentions) in tests {
|
for (md, mentions) in tests {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ init_i18n!(
|
|||||||
en,
|
en,
|
||||||
eo,
|
eo,
|
||||||
es,
|
es,
|
||||||
|
fa,
|
||||||
fr,
|
fr,
|
||||||
gl,
|
gl,
|
||||||
hi,
|
hi,
|
||||||
@@ -45,6 +46,9 @@ lazy_static! {
|
|||||||
let lang = js! { return navigator.language }.into_string().unwrap();
|
let lang = js! { return navigator.language }.into_string().unwrap();
|
||||||
let lang = lang.splitn(2, '-').next().unwrap_or("en");
|
let lang = lang.splitn(2, '-').next().unwrap_or("en");
|
||||||
|
|
||||||
|
// Force a language (Add .env setting)
|
||||||
|
//let lang = "fa"; ???
|
||||||
|
|
||||||
let english_position = catalogs
|
let english_position = catalogs
|
||||||
.iter()
|
.iter()
|
||||||
.position(|(language_code, _)| *language_code == "en")
|
.position(|(language_code, _)| *language_code == "en")
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ scheduled-thread-pool = "0.2.2"
|
|||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tantivy = "0.10.1"
|
tantivy = "0.12.0"
|
||||||
url = "2.1"
|
url = "2.1"
|
||||||
walkdir = "2.2"
|
walkdir = "2.2"
|
||||||
webfinger = "0.4.1"
|
webfinger = "0.4.1"
|
||||||
@@ -30,6 +30,7 @@ whatlang = "0.7.1"
|
|||||||
shrinkwraprs = "0.2.1"
|
shrinkwraprs = "0.2.1"
|
||||||
diesel-derive-newtype = "0.1.2"
|
diesel-derive-newtype = "0.1.2"
|
||||||
glob = "0.3.0"
|
glob = "0.3.0"
|
||||||
|
lindera-tantivy = { version = "0.1.2", optional = true }
|
||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
@@ -54,3 +55,4 @@ diesel_migrations = "1.3.0"
|
|||||||
[features]
|
[features]
|
||||||
postgres = ["diesel/postgres", "plume-macro/postgres" ]
|
postgres = ["diesel/postgres", "plume-macro/postgres" ]
|
||||||
sqlite = ["diesel/sqlite", "plume-macro/sqlite" ]
|
sqlite = ["diesel/sqlite", "plume-macro/sqlite" ]
|
||||||
|
search-lindera = ["lindera-tantivy"]
|
||||||
|
|||||||
@@ -498,6 +498,7 @@ pub(crate) mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
blog_authors::*,
|
blog_authors::*,
|
||||||
|
config::CONFIG,
|
||||||
instance::tests as instance_tests,
|
instance::tests as instance_tests,
|
||||||
medias::NewMedia,
|
medias::NewMedia,
|
||||||
search::tests::get_searcher,
|
search::tests::get_searcher,
|
||||||
@@ -767,7 +768,9 @@ pub(crate) mod tests {
|
|||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let (_, blogs) = fill_database(conn);
|
let (_, blogs) = fill_database(conn);
|
||||||
|
|
||||||
blogs[0].delete(conn, &get_searcher()).unwrap();
|
blogs[0]
|
||||||
|
.delete(conn, &get_searcher(&CONFIG.search_tokenizers))
|
||||||
|
.unwrap();
|
||||||
assert!(Blog::get(conn, blogs[0].id).is_err());
|
assert!(Blog::get(conn, blogs[0].id).is_err());
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -777,7 +780,7 @@ pub(crate) mod tests {
|
|||||||
fn delete_via_user() {
|
fn delete_via_user() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let searcher = get_searcher();
|
let searcher = get_searcher(&CONFIG.search_tokenizers);
|
||||||
let (user, _) = fill_database(conn);
|
let (user, _) = fill_database(conn);
|
||||||
|
|
||||||
let b1 = Blog::insert(
|
let b1 = Blog::insert(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::search::TokenizerKind as SearchTokenizer;
|
||||||
use rocket::config::Limits;
|
use rocket::config::Limits;
|
||||||
use rocket::Config as RocketConfig;
|
use rocket::Config as RocketConfig;
|
||||||
use std::env::{self, var};
|
use std::env::{self, var};
|
||||||
@@ -11,7 +12,10 @@ pub struct Config {
|
|||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
pub database_url: String,
|
pub database_url: String,
|
||||||
pub db_name: &'static str,
|
pub db_name: &'static str,
|
||||||
|
pub db_max_size: Option<u32>,
|
||||||
|
pub db_min_idle: Option<u32>,
|
||||||
pub search_index: String,
|
pub search_index: String,
|
||||||
|
pub search_tokenizers: SearchTokenizerConfig,
|
||||||
pub rocket: Result<RocketConfig, RocketError>,
|
pub rocket: Result<RocketConfig, RocketError>,
|
||||||
pub logo: LogoConfig,
|
pub logo: LogoConfig,
|
||||||
pub default_theme: String,
|
pub default_theme: String,
|
||||||
@@ -186,6 +190,56 @@ impl Default for LogoConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct SearchTokenizerConfig {
|
||||||
|
pub tag_tokenizer: SearchTokenizer,
|
||||||
|
pub content_tokenizer: SearchTokenizer,
|
||||||
|
pub property_tokenizer: SearchTokenizer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchTokenizerConfig {
|
||||||
|
pub fn init() -> Self {
|
||||||
|
use SearchTokenizer::*;
|
||||||
|
|
||||||
|
match var("SEARCH_LANG").ok().as_deref() {
|
||||||
|
Some("ja") => {
|
||||||
|
#[cfg(not(feature = "search-lindera"))]
|
||||||
|
panic!("You need build Plume with search-lindera feature, or execute it with SEARCH_TAG_TOKENIZER=ngram and SEARCH_CONTENT_TOKENIZER=ngram to enable Japanese search feature");
|
||||||
|
#[cfg(feature = "search-lindera")]
|
||||||
|
Self {
|
||||||
|
tag_tokenizer: Self::determine_tokenizer("SEARCH_TAG_TOKENIZER", Lindera),
|
||||||
|
content_tokenizer: Self::determine_tokenizer(
|
||||||
|
"SEARCH_CONTENT_TOKENIZER",
|
||||||
|
Lindera,
|
||||||
|
),
|
||||||
|
property_tokenizer: Ngram,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Self {
|
||||||
|
tag_tokenizer: Self::determine_tokenizer("SEARCH_TAG_TOKENIZER", Whitespace),
|
||||||
|
content_tokenizer: Self::determine_tokenizer("SEARCH_CONTENT_TOKENIZER", Simple),
|
||||||
|
property_tokenizer: Ngram,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn determine_tokenizer(var_name: &str, default: SearchTokenizer) -> SearchTokenizer {
|
||||||
|
use SearchTokenizer::*;
|
||||||
|
|
||||||
|
match var(var_name).ok().as_deref() {
|
||||||
|
Some("simple") => Simple,
|
||||||
|
Some("ngram") => Ngram,
|
||||||
|
Some("whitespace") => Whitespace,
|
||||||
|
Some("lindera") => {
|
||||||
|
#[cfg(not(feature = "search-lindera"))]
|
||||||
|
panic!("You need build Plume with search-lindera feature to use Lindera tokenizer");
|
||||||
|
#[cfg(feature = "search-lindera")]
|
||||||
|
Lindera
|
||||||
|
}
|
||||||
|
_ => default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref CONFIG: Config = Config {
|
pub static ref CONFIG: Config = Config {
|
||||||
base_url: var("BASE_URL").unwrap_or_else(|_| format!(
|
base_url: var("BASE_URL").unwrap_or_else(|_| format!(
|
||||||
@@ -193,12 +247,21 @@ lazy_static! {
|
|||||||
var("ROCKET_PORT").unwrap_or_else(|_| "7878".to_owned())
|
var("ROCKET_PORT").unwrap_or_else(|_| "7878".to_owned())
|
||||||
)),
|
)),
|
||||||
db_name: DB_NAME,
|
db_name: DB_NAME,
|
||||||
|
db_max_size: var("DB_MAX_SIZE").map_or(None, |s| Some(
|
||||||
|
s.parse::<u32>()
|
||||||
|
.expect("Couldn't parse DB_MAX_SIZE into u32")
|
||||||
|
)),
|
||||||
|
db_min_idle: var("DB_MIN_IDLE").map_or(None, |s| Some(
|
||||||
|
s.parse::<u32>()
|
||||||
|
.expect("Couldn't parse DB_MIN_IDLE into u32")
|
||||||
|
)),
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
database_url: var("DATABASE_URL")
|
database_url: var("DATABASE_URL")
|
||||||
.unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME)),
|
.unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME)),
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
database_url: var("DATABASE_URL").unwrap_or_else(|_| format!("{}.sqlite", DB_NAME)),
|
database_url: var("DATABASE_URL").unwrap_or_else(|_| format!("{}.sqlite", DB_NAME)),
|
||||||
search_index: var("SEARCH_INDEX").unwrap_or_else(|_| "search_index".to_owned()),
|
search_index: var("SEARCH_INDEX").unwrap_or_else(|_| "search_index".to_owned()),
|
||||||
|
search_tokenizers: SearchTokenizerConfig::init(),
|
||||||
rocket: get_rocket_config(),
|
rocket: get_rocket_config(),
|
||||||
logo: LogoConfig::default(),
|
logo: LogoConfig::default(),
|
||||||
default_theme: var("DEFAULT_THEME").unwrap_or_else(|_| "default-light".to_owned()),
|
default_theme: var("DEFAULT_THEME").unwrap_or_else(|_| "default-light".to_owned()),
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ mod tests {
|
|||||||
pub fn rockets() -> super::PlumeRocket {
|
pub fn rockets() -> super::PlumeRocket {
|
||||||
super::PlumeRocket {
|
super::PlumeRocket {
|
||||||
conn: db_conn::DbConn((*DB_POOL).get().unwrap()),
|
conn: db_conn::DbConn((*DB_POOL).get().unwrap()),
|
||||||
searcher: Arc::new(search::tests::get_searcher()),
|
searcher: Arc::new(search::tests::get_searcher(&CONFIG.search_tokenizers)),
|
||||||
worker: Arc::new(ScheduledThreadPool::new(2)),
|
worker: Arc::new(ScheduledThreadPool::new(2)),
|
||||||
user: None,
|
user: None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ mod searcher;
|
|||||||
mod tokenizer;
|
mod tokenizer;
|
||||||
pub use self::query::PlumeQuery as Query;
|
pub use self::query::PlumeQuery as Query;
|
||||||
pub use self::searcher::*;
|
pub use self::searcher::*;
|
||||||
|
pub use self::tokenizer::TokenizerKind;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use super::{Query, Searcher};
|
use super::{Query, Searcher, TokenizerKind};
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
use plume_common::utils::random_hex;
|
use plume_common::utils::random_hex;
|
||||||
use std::env::temp_dir;
|
use std::env::temp_dir;
|
||||||
@@ -14,18 +15,20 @@ pub(crate) mod tests {
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
blogs::tests::fill_database,
|
blogs::tests::fill_database,
|
||||||
|
config::SearchTokenizerConfig,
|
||||||
post_authors::*,
|
post_authors::*,
|
||||||
posts::{NewPost, Post},
|
posts::{NewPost, Post},
|
||||||
safe_string::SafeString,
|
safe_string::SafeString,
|
||||||
tests::db,
|
tests::db,
|
||||||
|
CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) fn get_searcher() -> Searcher {
|
pub(crate) fn get_searcher(tokenizers: &SearchTokenizerConfig) -> Searcher {
|
||||||
let dir = temp_dir().join(&format!("plume-test-{}", random_hex()));
|
let dir = temp_dir().join(&format!("plume-test-{}", random_hex()));
|
||||||
if dir.exists() {
|
if dir.exists() {
|
||||||
Searcher::open(&dir)
|
Searcher::open(&dir, tokenizers)
|
||||||
} else {
|
} else {
|
||||||
Searcher::create(&dir)
|
Searcher::create(&dir, tokenizers)
|
||||||
}
|
}
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
@@ -100,27 +103,27 @@ pub(crate) mod tests {
|
|||||||
fn open() {
|
fn open() {
|
||||||
let dir = temp_dir().join(format!("plume-test-{}", random_hex()));
|
let dir = temp_dir().join(format!("plume-test-{}", random_hex()));
|
||||||
{
|
{
|
||||||
Searcher::create(&dir).unwrap();
|
Searcher::create(&dir, &CONFIG.search_tokenizers).unwrap();
|
||||||
}
|
}
|
||||||
Searcher::open(&dir).unwrap();
|
Searcher::open(&dir, &CONFIG.search_tokenizers).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn create() {
|
fn create() {
|
||||||
let dir = temp_dir().join(format!("plume-test-{}", random_hex()));
|
let dir = temp_dir().join(format!("plume-test-{}", random_hex()));
|
||||||
|
|
||||||
assert!(Searcher::open(&dir).is_err());
|
assert!(Searcher::open(&dir, &CONFIG.search_tokenizers).is_err());
|
||||||
{
|
{
|
||||||
Searcher::create(&dir).unwrap();
|
Searcher::create(&dir, &CONFIG.search_tokenizers).unwrap();
|
||||||
}
|
}
|
||||||
Searcher::open(&dir).unwrap(); //verify it's well created
|
Searcher::open(&dir, &CONFIG.search_tokenizers).unwrap(); //verify it's well created
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn search() {
|
fn search() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let searcher = get_searcher();
|
let searcher = get_searcher(&CONFIG.search_tokenizers);
|
||||||
let blog = &fill_database(conn).1[0];
|
let blog = &fill_database(conn).1[0];
|
||||||
let author = &blog.list_authors(conn).unwrap()[0];
|
let author = &blog.list_authors(conn).unwrap()[0];
|
||||||
|
|
||||||
@@ -180,10 +183,69 @@ pub(crate) mod tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "search-lindera")]
|
||||||
|
#[test]
|
||||||
|
fn search_japanese() {
|
||||||
|
let conn = &db();
|
||||||
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
|
let tokenizers = SearchTokenizerConfig {
|
||||||
|
tag_tokenizer: TokenizerKind::Lindera,
|
||||||
|
content_tokenizer: TokenizerKind::Lindera,
|
||||||
|
property_tokenizer: TokenizerKind::Ngram,
|
||||||
|
};
|
||||||
|
let searcher = get_searcher(&tokenizers);
|
||||||
|
let blog = &fill_database(conn).1[0];
|
||||||
|
|
||||||
|
let title = random_hex()[..8].to_owned();
|
||||||
|
|
||||||
|
let post = Post::insert(
|
||||||
|
conn,
|
||||||
|
NewPost {
|
||||||
|
blog_id: blog.id,
|
||||||
|
slug: title.clone(),
|
||||||
|
title: title.clone(),
|
||||||
|
content: SafeString::new("ブログエンジンPlumeです。"),
|
||||||
|
published: true,
|
||||||
|
license: "CC-BY-SA".to_owned(),
|
||||||
|
ap_url: "".to_owned(),
|
||||||
|
creation_date: None,
|
||||||
|
subtitle: "".to_owned(),
|
||||||
|
source: "".to_owned(),
|
||||||
|
cover_id: None,
|
||||||
|
},
|
||||||
|
&searcher,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
searcher.commit();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
searcher.search_document(conn, Query::from_str("ブログエンジン").unwrap(), (0, 1))
|
||||||
|
[0]
|
||||||
|
.id,
|
||||||
|
post.id
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
searcher.search_document(conn, Query::from_str("Plume").unwrap(), (0, 1))[0].id,
|
||||||
|
post.id
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
searcher.search_document(conn, Query::from_str("です").unwrap(), (0, 1))[0].id,
|
||||||
|
post.id
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
searcher.search_document(conn, Query::from_str("。").unwrap(), (0, 1))[0].id,
|
||||||
|
post.id
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn drop_writer() {
|
fn drop_writer() {
|
||||||
let searcher = get_searcher();
|
let searcher = get_searcher(&CONFIG.search_tokenizers);
|
||||||
searcher.drop_writer();
|
searcher.drop_writer();
|
||||||
get_searcher();
|
get_searcher(&CONFIG.search_tokenizers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
instance::Instance,
|
config::SearchTokenizerConfig, instance::Instance, posts::Post, schema::posts,
|
||||||
posts::Post,
|
search::query::PlumeQuery, tags::Tag, Connection, Result,
|
||||||
schema::posts,
|
|
||||||
search::{query::PlumeQuery, tokenizer},
|
|
||||||
tags::Tag,
|
|
||||||
Connection, Result,
|
|
||||||
};
|
};
|
||||||
use chrono::Datelike;
|
use chrono::Datelike;
|
||||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use std::{cmp, fs::create_dir_all, path::Path, sync::Mutex};
|
use std::{cmp, fs::create_dir_all, path::Path, sync::Mutex};
|
||||||
use tantivy::{
|
use tantivy::{
|
||||||
collector::TopDocs, directory::MmapDirectory, schema::*, tokenizer::*, Index, IndexReader,
|
collector::TopDocs, directory::MmapDirectory, schema::*, Index, IndexReader, IndexWriter,
|
||||||
IndexWriter, ReloadPolicy, Term,
|
ReloadPolicy, Term,
|
||||||
};
|
};
|
||||||
use whatlang::{detect as detect_lang, Lang};
|
use whatlang::{detect as detect_lang, Lang};
|
||||||
|
|
||||||
@@ -34,7 +30,7 @@ impl Searcher {
|
|||||||
pub fn schema() -> Schema {
|
pub fn schema() -> Schema {
|
||||||
let tag_indexing = TextOptions::default().set_indexing_options(
|
let tag_indexing = TextOptions::default().set_indexing_options(
|
||||||
TextFieldIndexing::default()
|
TextFieldIndexing::default()
|
||||||
.set_tokenizer("whitespace_tokenizer")
|
.set_tokenizer("tag_tokenizer")
|
||||||
.set_index_option(IndexRecordOption::Basic),
|
.set_index_option(IndexRecordOption::Basic),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -70,15 +66,7 @@ impl Searcher {
|
|||||||
schema_builder.build()
|
schema_builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create(path: &dyn AsRef<Path>) -> Result<Self> {
|
pub fn create(path: &dyn AsRef<Path>, tokenizers: &SearchTokenizerConfig) -> Result<Self> {
|
||||||
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser);
|
|
||||||
|
|
||||||
let content_tokenizer = SimpleTokenizer
|
|
||||||
.filter(RemoveLongFilter::limit(40))
|
|
||||||
.filter(LowerCaser);
|
|
||||||
|
|
||||||
let property_tokenizer = NgramTokenizer::new(2, 8, false).filter(LowerCaser);
|
|
||||||
|
|
||||||
let schema = Self::schema();
|
let schema = Self::schema();
|
||||||
|
|
||||||
create_dir_all(path).map_err(|_| SearcherError::IndexCreationError)?;
|
create_dir_all(path).map_err(|_| SearcherError::IndexCreationError)?;
|
||||||
@@ -90,9 +78,9 @@ impl Searcher {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let tokenizer_manager = index.tokenizers();
|
let tokenizer_manager = index.tokenizers();
|
||||||
tokenizer_manager.register("whitespace_tokenizer", whitespace_tokenizer);
|
tokenizer_manager.register("tag_tokenizer", tokenizers.tag_tokenizer);
|
||||||
tokenizer_manager.register("content_tokenizer", content_tokenizer);
|
tokenizer_manager.register("content_tokenizer", tokenizers.content_tokenizer);
|
||||||
tokenizer_manager.register("property_tokenizer", property_tokenizer);
|
tokenizer_manager.register("property_tokenizer", tokenizers.property_tokenizer);
|
||||||
} //to please the borrow checker
|
} //to please the borrow checker
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
writer: Mutex::new(Some(
|
writer: Mutex::new(Some(
|
||||||
@@ -109,31 +97,38 @@ impl Searcher {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open(path: &dyn AsRef<Path>) -> Result<Self> {
|
pub fn open(path: &dyn AsRef<Path>, tokenizers: &SearchTokenizerConfig) -> Result<Self> {
|
||||||
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser);
|
let mut index =
|
||||||
|
|
||||||
let content_tokenizer = SimpleTokenizer
|
|
||||||
.filter(RemoveLongFilter::limit(40))
|
|
||||||
.filter(LowerCaser);
|
|
||||||
|
|
||||||
let property_tokenizer = NgramTokenizer::new(2, 8, false).filter(LowerCaser);
|
|
||||||
|
|
||||||
let index =
|
|
||||||
Index::open(MmapDirectory::open(path).map_err(|_| SearcherError::IndexOpeningError)?)
|
Index::open(MmapDirectory::open(path).map_err(|_| SearcherError::IndexOpeningError)?)
|
||||||
.map_err(|_| SearcherError::IndexOpeningError)?;
|
.map_err(|_| SearcherError::IndexOpeningError)?;
|
||||||
|
|
||||||
{
|
{
|
||||||
let tokenizer_manager = index.tokenizers();
|
let tokenizer_manager = index.tokenizers();
|
||||||
tokenizer_manager.register("whitespace_tokenizer", whitespace_tokenizer);
|
tokenizer_manager.register("tag_tokenizer", tokenizers.tag_tokenizer);
|
||||||
tokenizer_manager.register("content_tokenizer", content_tokenizer);
|
tokenizer_manager.register("content_tokenizer", tokenizers.content_tokenizer);
|
||||||
tokenizer_manager.register("property_tokenizer", property_tokenizer);
|
tokenizer_manager.register("property_tokenizer", tokenizers.property_tokenizer);
|
||||||
} //to please the borrow checker
|
} //to please the borrow checker
|
||||||
let mut writer = index
|
let writer = index
|
||||||
.writer(50_000_000)
|
.writer(50_000_000)
|
||||||
.map_err(|_| SearcherError::WriteLockAcquisitionError)?;
|
.map_err(|_| SearcherError::WriteLockAcquisitionError)?;
|
||||||
writer
|
|
||||||
.garbage_collect_files()
|
// Since Tantivy v0.12.0, IndexWriter::garbage_collect_files() returns Future.
|
||||||
|
// To avoid conflict with Plume async project, we don't introduce async now.
|
||||||
|
// After async is introduced to Plume, we can use garbage_collect_files() again.
|
||||||
|
// Algorithm stolen from Tantivy's SegmentUpdater::list_files()
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
let mut files: HashSet<PathBuf> = index
|
||||||
|
.list_all_segment_metas()
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|segment_meta| segment_meta.list_files())
|
||||||
|
.collect();
|
||||||
|
files.insert(Path::new("meta.json").to_path_buf());
|
||||||
|
index
|
||||||
|
.directory_mut()
|
||||||
|
.garbage_collect(|| files)
|
||||||
.map_err(|_| SearcherError::IndexEditionError)?;
|
.map_err(|_| SearcherError::IndexEditionError)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
writer: Mutex::new(Some(writer)),
|
writer: Mutex::new(Some(writer)),
|
||||||
reader: index
|
reader: index
|
||||||
|
|||||||
@@ -1,5 +1,34 @@
|
|||||||
|
#[cfg(feature = "search-lindera")]
|
||||||
|
use lindera_tantivy::tokenizer::LinderaTokenizer;
|
||||||
use std::str::CharIndices;
|
use std::str::CharIndices;
|
||||||
use tantivy::tokenizer::{Token, TokenStream, Tokenizer};
|
use tantivy::tokenizer::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum TokenizerKind {
|
||||||
|
Simple,
|
||||||
|
Ngram,
|
||||||
|
Whitespace,
|
||||||
|
#[cfg(feature = "search-lindera")]
|
||||||
|
Lindera,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TokenizerKind> for TextAnalyzer {
|
||||||
|
fn from(tokenizer: TokenizerKind) -> TextAnalyzer {
|
||||||
|
use TokenizerKind::*;
|
||||||
|
|
||||||
|
match tokenizer {
|
||||||
|
Simple => TextAnalyzer::from(SimpleTokenizer)
|
||||||
|
.filter(RemoveLongFilter::limit(40))
|
||||||
|
.filter(LowerCaser),
|
||||||
|
Ngram => TextAnalyzer::from(NgramTokenizer::new(2, 8, false)).filter(LowerCaser),
|
||||||
|
Whitespace => TextAnalyzer::from(WhitespaceTokenizer).filter(LowerCaser),
|
||||||
|
#[cfg(feature = "search-lindera")]
|
||||||
|
Lindera => {
|
||||||
|
TextAnalyzer::from(LinderaTokenizer::new("decompose", "")).filter(LowerCaser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Tokenize the text by splitting on whitespaces. Pretty much a copy of Tantivy's SimpleTokenizer,
|
/// Tokenize the text by splitting on whitespaces. Pretty much a copy of Tantivy's SimpleTokenizer,
|
||||||
/// but not splitting on punctuation
|
/// but not splitting on punctuation
|
||||||
@@ -12,15 +41,13 @@ pub struct WhitespaceTokenStream<'a> {
|
|||||||
token: Token,
|
token: Token,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Tokenizer<'a> for WhitespaceTokenizer {
|
impl Tokenizer for WhitespaceTokenizer {
|
||||||
type TokenStreamImpl = WhitespaceTokenStream<'a>;
|
fn token_stream<'a>(&self, text: &'a str) -> BoxTokenStream<'a> {
|
||||||
|
BoxTokenStream::from(WhitespaceTokenStream {
|
||||||
fn token_stream(&self, text: &'a str) -> Self::TokenStreamImpl {
|
|
||||||
WhitespaceTokenStream {
|
|
||||||
text,
|
text,
|
||||||
chars: text.char_indices(),
|
chars: text.char_indices(),
|
||||||
token: Token::default(),
|
token: Token::default(),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<'a> WhitespaceTokenStream<'a> {
|
impl<'a> WhitespaceTokenStream<'a> {
|
||||||
|
|||||||
@@ -406,8 +406,8 @@ impl Bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Bool::HasCover => Ok(post.cover_id.is_some()),
|
Bool::HasCover => Ok(post.cover_id.is_some()),
|
||||||
Bool::Local => Ok(post.get_blog(&rocket.conn)?.is_local()),
|
Bool::Local => Ok(post.get_blog(&rocket.conn)?.is_local() && kind == Kind::Original),
|
||||||
Bool::All => Ok(true),
|
Bool::All => Ok(kind == Kind::Original),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ pub enum Role {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Identifiable, Clone, Debug, AsChangeset)]
|
#[derive(Queryable, Identifiable, Clone, Debug, AsChangeset)]
|
||||||
|
#[changeset_options(treat_none_as_null = "true")]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
@@ -759,7 +760,7 @@ impl User {
|
|||||||
mime_type: None,
|
mime_type: None,
|
||||||
href: None,
|
href: None,
|
||||||
template: Some(format!(
|
template: Some(format!(
|
||||||
"https://{}/remote_interact?{{uri}}",
|
"https://{}/remote_interact?target={{uri}}",
|
||||||
self.get_instance(conn)?.public_domain
|
self.get_instance(conn)?.public_domain
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
@@ -1025,6 +1026,7 @@ impl NewUser {
|
|||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
config::CONFIG,
|
||||||
instance::{tests as instance_tests, Instance},
|
instance::{tests as instance_tests, Instance},
|
||||||
search::tests::get_searcher,
|
search::tests::get_searcher,
|
||||||
tests::{db, rockets},
|
tests::{db, rockets},
|
||||||
@@ -1121,7 +1123,9 @@ pub(crate) mod tests {
|
|||||||
let inserted = fill_database(conn);
|
let inserted = fill_database(conn);
|
||||||
|
|
||||||
assert!(User::get(conn, inserted[0].id).is_ok());
|
assert!(User::get(conn, inserted[0].id).is_ok());
|
||||||
inserted[0].delete(conn, &get_searcher()).unwrap();
|
inserted[0]
|
||||||
|
.delete(conn, &get_searcher(&CONFIG.search_tokenizers))
|
||||||
|
.unwrap();
|
||||||
assert!(User::get(conn, inserted[0].id).is_err());
|
assert!(User::get(conn, inserted[0].id).is_err());
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ msgstr ""
|
|||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
"X-Crowdin-Project: plume\n"
|
"X-Crowdin-Project: plume\n"
|
||||||
"X-Crowdin-Language: fr\n"
|
"X-Crowdin-Language: fr\n"
|
||||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ msgstr ""
|
|||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
"X-Crowdin-Project: plume\n"
|
"X-Crowdin-Project: plume\n"
|
||||||
"X-Crowdin-Language: fr\n"
|
"X-Crowdin-Language: fr\n"
|
||||||
"X-Crowdin-File: /master/po/plume/plume.pot\n"
|
"X-Crowdin-File: /master/po/plume/plume.pot\n"
|
||||||
|
|||||||
+2
-2
@@ -24,9 +24,9 @@ if [ $ARCH == "aarch64" -o $ARCH == "armv71" ] ; then
|
|||||||
apt-get install -y --no-install-recommends build-essential subversion ninja-build cmake
|
apt-get install -y --no-install-recommends build-essential subversion ninja-build cmake
|
||||||
mkdir -p /scratch/src
|
mkdir -p /scratch/src
|
||||||
cd /scratch/src
|
cd /scratch/src
|
||||||
svn co http://llvm.org/svn/llvm-project/llvm/tags/RELEASE_800/final/ llvm
|
svn co http://llvm.org/svn/llvm-project/llvm/tags/RELEASE_900/final/ llvm
|
||||||
cd /scratch/src/llvm/tools
|
cd /scratch/src/llvm/tools
|
||||||
svn co http://llvm.org/svn/llvm-project/lld/tags/RELEASE_800/final/ lld
|
svn co http://llvm.org/svn/llvm-project/lld/tags/RELEASE_900/final/ lld
|
||||||
mkdir -p /scratch/build/arm
|
mkdir -p /scratch/build/arm
|
||||||
cd /scratch/build/arm
|
cd /scratch/build/arm
|
||||||
if [ "$ARCH" == "aarch64" ] ; then
|
if [ "$ARCH" == "aarch64" ] ; then
|
||||||
|
|||||||
+9
-7
@@ -6,8 +6,6 @@ extern crate gettext_macros;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate rocket;
|
extern crate rocket;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate runtime_fmt;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate validator_derive;
|
extern crate validator_derive;
|
||||||
@@ -28,7 +26,8 @@ use std::sync::{Arc, Mutex};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
init_i18n!(
|
init_i18n!(
|
||||||
"plume", ar, bg, ca, cs, de, en, eo, es, fr, gl, hi, hr, it, ja, nb, pl, pt, ro, ru, sr, sk, sv
|
"plume", ar, bg, ca, cs, de, en, eo, es, fa, fr, gl, hi, hr, it, ja, nb, pl, pt, ro, ru, sr,
|
||||||
|
sk, sv
|
||||||
);
|
);
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
@@ -55,10 +54,13 @@ fn init_pool() -> Option<DbPool> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let manager = ConnectionManager::<Connection>::new(CONFIG.database_url.as_str());
|
let manager = ConnectionManager::<Connection>::new(CONFIG.database_url.as_str());
|
||||||
let pool = DbPool::builder()
|
let mut builder = DbPool::builder()
|
||||||
.connection_customizer(Box::new(PragmaForeignKey))
|
.connection_customizer(Box::new(PragmaForeignKey))
|
||||||
.build(manager)
|
.min_idle(CONFIG.db_min_idle);
|
||||||
.ok()?;
|
if let Some(max_size) = CONFIG.db_max_size {
|
||||||
|
builder = builder.max_size(max_size);
|
||||||
|
};
|
||||||
|
let pool = builder.build(manager).ok()?;
|
||||||
Instance::cache_local(&pool.get().unwrap());
|
Instance::cache_local(&pool.get().unwrap());
|
||||||
Some(pool)
|
Some(pool)
|
||||||
}
|
}
|
||||||
@@ -97,7 +99,7 @@ Then try to restart Plume.
|
|||||||
let workpool = ScheduledThreadPool::with_name("worker {}", num_cpus::get());
|
let workpool = ScheduledThreadPool::with_name("worker {}", num_cpus::get());
|
||||||
// we want a fast exit here, so
|
// we want a fast exit here, so
|
||||||
#[allow(clippy::match_wild_err_arm)]
|
#[allow(clippy::match_wild_err_arm)]
|
||||||
let searcher = match UnmanagedSearcher::open(&CONFIG.search_index) {
|
let searcher = match UnmanagedSearcher::open(&CONFIG.search_index, &CONFIG.search_tokenizers) {
|
||||||
Err(Error::Search(e)) => match e {
|
Err(Error::Search(e)) => match e {
|
||||||
SearcherError::WriteLockAcquisitionError => panic!(
|
SearcherError::WriteLockAcquisitionError => panic!(
|
||||||
r#"
|
r#"
|
||||||
|
|||||||
+6
-14
@@ -1,5 +1,4 @@
|
|||||||
use activitypub::collection::{OrderedCollection, OrderedCollectionPage};
|
use activitypub::collection::{OrderedCollection, OrderedCollectionPage};
|
||||||
use atom_syndication::{Entry, FeedBuilder};
|
|
||||||
use diesel::SaveChangesDsl;
|
use diesel::SaveChangesDsl;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::ContentType,
|
http::ContentType,
|
||||||
@@ -361,20 +360,13 @@ pub fn outbox_page(
|
|||||||
pub fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>> {
|
pub fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>> {
|
||||||
let blog = Blog::find_by_fqn(&rockets, &name).ok()?;
|
let blog = Blog::find_by_fqn(&rockets, &name).ok()?;
|
||||||
let conn = &*rockets.conn;
|
let conn = &*rockets.conn;
|
||||||
let feed = FeedBuilder::default()
|
let entries = Post::get_recents_for_blog(&*conn, &blog, 15).ok()?;
|
||||||
.title(blog.title.clone())
|
let uri = Instance::get_local()
|
||||||
.id(Instance::get_local()
|
|
||||||
.ok()?
|
.ok()?
|
||||||
.compute_box("~", &name, "atom.xml"))
|
.compute_box("~", &name, "atom.xml");
|
||||||
.entries(
|
let title = &blog.title;
|
||||||
Post::get_recents_for_blog(&*conn, &blog, 15)
|
let default_updated = &blog.creation_date;
|
||||||
.ok()?
|
let feed = super::build_atom_feed(entries, &uri, title, default_updated, conn);
|
||||||
.into_iter()
|
|
||||||
.map(|p| super::post_to_atom(p, &*conn))
|
|
||||||
.collect::<Vec<Entry>>(),
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
.ok()?;
|
|
||||||
Some(Content(
|
Some(Content(
|
||||||
ContentType::new("application", "atom+xml"),
|
ContentType::new("application", "atom+xml"),
|
||||||
feed.to_string(),
|
feed.to_string(),
|
||||||
|
|||||||
Regular → Executable
+42
-5
@@ -1,6 +1,9 @@
|
|||||||
#![warn(clippy::too_many_arguments)]
|
#![warn(clippy::too_many_arguments)]
|
||||||
use crate::template_utils::Ructe;
|
use crate::template_utils::Ructe;
|
||||||
use atom_syndication::{ContentBuilder, Entry, EntryBuilder, LinkBuilder, Person, PersonBuilder};
|
use atom_syndication::{
|
||||||
|
ContentBuilder, Entry, EntryBuilder, Feed, FeedBuilder, LinkBuilder, Person, PersonBuilder,
|
||||||
|
};
|
||||||
|
use chrono::naive::NaiveDateTime;
|
||||||
use plume_models::{posts::Post, Connection, CONFIG, ITEMS_PER_PAGE};
|
use plume_models::{posts::Post, Connection, CONFIG, ITEMS_PER_PAGE};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::{
|
http::{
|
||||||
@@ -115,13 +118,46 @@ pub struct RemoteForm {
|
|||||||
pub remote: String,
|
pub remote: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn post_to_atom(post: Post, conn: &Connection) -> Entry {
|
pub fn build_atom_feed(
|
||||||
|
entries: Vec<Post>,
|
||||||
|
uri: &str,
|
||||||
|
title: &str,
|
||||||
|
default_updated: &NaiveDateTime,
|
||||||
|
conn: &Connection,
|
||||||
|
) -> Feed {
|
||||||
|
let updated = if entries.is_empty() {
|
||||||
|
default_updated
|
||||||
|
} else {
|
||||||
|
&entries[0].creation_date
|
||||||
|
};
|
||||||
|
|
||||||
|
FeedBuilder::default()
|
||||||
|
.title(title)
|
||||||
|
.id(uri)
|
||||||
|
.updated(updated.format("%Y-%m-%dT%H:%M:%SZ").to_string())
|
||||||
|
.entries(
|
||||||
|
entries
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| post_to_atom(p, conn))
|
||||||
|
.collect::<Vec<Entry>>(),
|
||||||
|
)
|
||||||
|
.links(vec![LinkBuilder::default()
|
||||||
|
.href(uri)
|
||||||
|
.rel("self")
|
||||||
|
.mime_type("application/atom+xml".to_string())
|
||||||
|
.build()
|
||||||
|
.expect("Atom feed: link error")])
|
||||||
|
.build()
|
||||||
|
.expect("user::atom_feed: Error building Atom feed")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn post_to_atom(post: Post, conn: &Connection) -> Entry {
|
||||||
|
let formatted_creation_date = post.creation_date.format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
||||||
EntryBuilder::default()
|
EntryBuilder::default()
|
||||||
.title(format!("<![CDATA[{}]]>", post.title))
|
.title(format!("<![CDATA[{}]]>", post.title))
|
||||||
.content(
|
.content(
|
||||||
ContentBuilder::default()
|
ContentBuilder::default()
|
||||||
.value(format!("<![CDATA[{}]]>", *post.content.get()))
|
.value(format!("<![CDATA[{}]]>", *post.content.get()))
|
||||||
.src(post.ap_url.clone())
|
|
||||||
.content_type("html".to_string())
|
.content_type("html".to_string())
|
||||||
.build()
|
.build()
|
||||||
.expect("Atom feed: content error"),
|
.expect("Atom feed: content error"),
|
||||||
@@ -141,8 +177,9 @@ pub fn post_to_atom(post: Post, conn: &Connection) -> Entry {
|
|||||||
)
|
)
|
||||||
// Using RFC 4287 format, see https://tools.ietf.org/html/rfc4287#section-3.3 for dates
|
// Using RFC 4287 format, see https://tools.ietf.org/html/rfc4287#section-3.3 for dates
|
||||||
// eg: 2003-12-13T18:30:02Z (Z is here because there is no timezone support with the NaiveDateTime crate)
|
// eg: 2003-12-13T18:30:02Z (Z is here because there is no timezone support with the NaiveDateTime crate)
|
||||||
.published(post.creation_date.format("%Y-%m-%dT%H:%M:%SZ").to_string())
|
.published(formatted_creation_date.clone())
|
||||||
.id(post.id.to_string())
|
.updated(formatted_creation_date)
|
||||||
|
.id(post.ap_url.clone())
|
||||||
.links(vec![LinkBuilder::default()
|
.links(vec![LinkBuilder::default()
|
||||||
.href(post.ap_url)
|
.href(post.ap_url)
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
+1
-1
@@ -643,7 +643,7 @@ pub fn remote_interact_post(
|
|||||||
.and_then(|blog| Post::find_by_slug(&rockets.conn, &slug, blog.id))?;
|
.and_then(|blog| Post::find_by_slug(&rockets.conn, &slug, blog.id))?;
|
||||||
if let Some(uri) = User::fetch_remote_interact_uri(&remote.remote)
|
if let Some(uri) = User::fetch_remote_interact_uri(&remote.remote)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|uri| rt_format!(uri, uri = target.ap_url).ok())
|
.map(|uri| uri.replace("{uri}", &target.ap_url))
|
||||||
{
|
{
|
||||||
Ok(Redirect::to(uri).into())
|
Ok(Redirect::to(uri).into())
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use lettre::Transport;
|
|||||||
use rocket::http::ext::IntoOwned;
|
use rocket::http::ext::IntoOwned;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::{uri::Uri, Cookie, Cookies, SameSite},
|
http::{uri::Uri, Cookie, Cookies, SameSite},
|
||||||
request::{Form, LenientForm},
|
request::LenientForm,
|
||||||
response::{Flash, Redirect},
|
response::{Flash, Redirect},
|
||||||
State,
|
State,
|
||||||
};
|
};
|
||||||
@@ -159,7 +159,7 @@ pub struct ResetForm {
|
|||||||
#[post("/password-reset", data = "<form>")]
|
#[post("/password-reset", data = "<form>")]
|
||||||
pub fn password_reset_request(
|
pub fn password_reset_request(
|
||||||
mail: State<'_, Arc<Mutex<Mailer>>>,
|
mail: State<'_, Arc<Mutex<Mailer>>>,
|
||||||
form: Form<ResetForm>,
|
form: LenientForm<ResetForm>,
|
||||||
rockets: PlumeRocket,
|
rockets: PlumeRocket,
|
||||||
) -> Ructe {
|
) -> Ructe {
|
||||||
if User::find_by_email(&*rockets.conn, &form.email).is_ok() {
|
if User::find_by_email(&*rockets.conn, &form.email).is_ok() {
|
||||||
@@ -216,7 +216,7 @@ fn passwords_match(form: &NewPasswordForm) -> Result<(), ValidationError> {
|
|||||||
#[post("/password-reset/<token>", data = "<form>")]
|
#[post("/password-reset/<token>", data = "<form>")]
|
||||||
pub fn password_reset(
|
pub fn password_reset(
|
||||||
token: String,
|
token: String,
|
||||||
form: Form<NewPasswordForm>,
|
form: LenientForm<NewPasswordForm>,
|
||||||
rockets: PlumeRocket,
|
rockets: PlumeRocket,
|
||||||
) -> Result<Flash<Redirect>, Ructe> {
|
) -> Result<Flash<Redirect>, Ructe> {
|
||||||
form.validate()
|
form.validate()
|
||||||
|
|||||||
+15
-21
@@ -2,7 +2,6 @@ use activitypub::{
|
|||||||
activity::Create,
|
activity::Create,
|
||||||
collection::{OrderedCollection, OrderedCollectionPage},
|
collection::{OrderedCollection, OrderedCollectionPage},
|
||||||
};
|
};
|
||||||
use atom_syndication::{Entry, FeedBuilder};
|
|
||||||
use diesel::SaveChangesDsl;
|
use diesel::SaveChangesDsl;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::{ContentType, Cookies},
|
http::{ContentType, Cookies},
|
||||||
@@ -202,15 +201,14 @@ pub fn follow_not_connected(
|
|||||||
if let Some(uri) = User::fetch_remote_interact_uri(&remote_form)
|
if let Some(uri) = User::fetch_remote_interact_uri(&remote_form)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|uri| {
|
.and_then(|uri| {
|
||||||
rt_format!(
|
Some(uri.replace(
|
||||||
uri,
|
"{uri}",
|
||||||
uri = format!(
|
&format!(
|
||||||
"{}@{}",
|
"{}@{}",
|
||||||
target.fqn,
|
target.fqn,
|
||||||
target.get_instance(&rockets.conn).ok()?.public_domain
|
target.get_instance(&rockets.conn).ok()?.public_domain
|
||||||
)
|
),
|
||||||
)
|
))
|
||||||
.ok()
|
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
Ok(Redirect::to(uri).into())
|
Ok(Redirect::to(uri).into())
|
||||||
@@ -399,7 +397,10 @@ pub fn update(
|
|||||||
)
|
)
|
||||||
.0,
|
.0,
|
||||||
);
|
);
|
||||||
user.preferred_theme = form.theme.clone();
|
user.preferred_theme = form
|
||||||
|
.theme
|
||||||
|
.clone()
|
||||||
|
.and_then(|t| if &t == "" { None } else { Some(t) });
|
||||||
user.hide_custom_css = form.hide_custom_css;
|
user.hide_custom_css = form.hide_custom_css;
|
||||||
let _: User = user.save_changes(&*conn).map_err(Error::from)?;
|
let _: User = user.save_changes(&*conn).map_err(Error::from)?;
|
||||||
|
|
||||||
@@ -616,20 +617,13 @@ pub fn ap_followers(
|
|||||||
pub fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>> {
|
pub fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>> {
|
||||||
let conn = &*rockets.conn;
|
let conn = &*rockets.conn;
|
||||||
let author = User::find_by_fqn(&rockets, &name).ok()?;
|
let author = User::find_by_fqn(&rockets, &name).ok()?;
|
||||||
let feed = FeedBuilder::default()
|
let entries = Post::get_recents_for_author(conn, &author, 15).ok()?;
|
||||||
.title(author.display_name.clone())
|
let uri = Instance::get_local()
|
||||||
.id(Instance::get_local()
|
|
||||||
.unwrap()
|
|
||||||
.compute_box("@", &name, "atom.xml"))
|
|
||||||
.entries(
|
|
||||||
Post::get_recents_for_author(conn, &author, 15)
|
|
||||||
.ok()?
|
.ok()?
|
||||||
.into_iter()
|
.compute_box("@", &name, "atom.xml");
|
||||||
.map(|p| super::post_to_atom(p, conn))
|
let title = &author.display_name;
|
||||||
.collect::<Vec<Entry>>(),
|
let default_updated = &author.creation_date;
|
||||||
)
|
let feed = super::build_atom_feed(entries, &uri, title, default_updated, conn);
|
||||||
.build()
|
|
||||||
.expect("user::atom_feed: Error building Atom feed");
|
|
||||||
Some(Content(
|
Some(Content(
|
||||||
ContentType::new("application", "atom+xml"),
|
ContentType::new("application", "atom+xml"),
|
||||||
feed.to_string(),
|
feed.to_string(),
|
||||||
|
|||||||
@@ -32,9 +32,9 @@
|
|||||||
<form method="post" action="@uri!(instance::delete_email_blocklist)">
|
<form method="post" action="@uri!(instance::delete_email_blocklist)">
|
||||||
<header>
|
<header>
|
||||||
@if emails.is_empty() {
|
@if emails.is_empty() {
|
||||||
<input type="submit" class="destructive" value='@i18n!(ctx.1, "Delete selected emails")'>
|
|
||||||
} else {
|
|
||||||
<p class="center" >@i18n!(ctx.1, "There are no blocked emails on your instance")</p>
|
<p class="center" >@i18n!(ctx.1, "There are no blocked emails on your instance")</p>
|
||||||
|
} else {
|
||||||
|
<input type="submit" class="destructive" value='@i18n!(ctx.1, "Delete selected emails")'>
|
||||||
}
|
}
|
||||||
</header>
|
</header>
|
||||||
<div class="list">
|
<div class="list">
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
<p class="p-summary" dir="auto">@article.subtitle</p>
|
<p class="p-summary" dir="auto">@article.subtitle</p>
|
||||||
</main>
|
</main>
|
||||||
<footer class="authors">
|
<footer class="authors">
|
||||||
|
<div>
|
||||||
@Html(i18n!(ctx.1, "By {0}"; format!(
|
@Html(i18n!(ctx.1, "By {0}"; format!(
|
||||||
"<a class=\"p-author h-card\" href=\"{}\">{}</a>",
|
"<a class=\"p-author h-card\" href=\"{}\">{}</a>",
|
||||||
uri!(user::details: name = &article.get_authors(ctx.0).unwrap_or_default()[0].fqn),
|
uri!(user::details: name = &article.get_authors(ctx.0).unwrap_or_default()[0].fqn),
|
||||||
@@ -26,8 +27,20 @@
|
|||||||
⋅ <span class="dt-published" datetime="@article.creation_date.format("%F %T")">@article.creation_date.format("%B %e, %Y")</span>
|
⋅ <span class="dt-published" datetime="@article.creation_date.format("%F %T")">@article.creation_date.format("%B %e, %Y")</span>
|
||||||
}
|
}
|
||||||
⋅ <a href="@uri!(blogs::details: name = &article.get_blog(ctx.0).unwrap().fqn, page = _)">@article.get_blog(ctx.0).unwrap().title</a>
|
⋅ <a href="@uri!(blogs::details: name = &article.get_blog(ctx.0).unwrap().fqn, page = _)">@article.get_blog(ctx.0).unwrap().title</a>
|
||||||
|
⋅
|
||||||
|
</div>
|
||||||
@if !article.published {
|
@if !article.published {
|
||||||
⋅ @i18n!(ctx.1, "Draft")
|
<div>⋅ @i18n!(ctx.1, "Draft")</div>
|
||||||
|
} else {
|
||||||
|
<div>
|
||||||
|
<span class="likes" aria-label="@i18n!(ctx.1, "One like", "{0} likes"; article.count_likes(ctx.0).unwrap_or_default())" title="@i18n!(ctx.1, "One like", "{0} likes"; article.count_likes(ctx.0).unwrap_or_default())">
|
||||||
|
@icon!("heart") @article.count_likes(ctx.0).unwrap_or_default()
|
||||||
|
</span>
|
||||||
|
⋅
|
||||||
|
<span class="reshares" aria-label="@i18n!(ctx.1, "One like", "{0} boost"; article.count_reshares(ctx.0).unwrap_or_default())" title="@i18n!(ctx.1, "One boost", "{0} boosts"; article.count_reshares(ctx.0).unwrap_or_default())">
|
||||||
|
@icon!("repeat") @article.count_reshares(ctx.0).unwrap_or_default()
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
<div class="article-info" dir="auto">
|
<div class="article-info" dir="auto">
|
||||||
<span class="author">
|
<span class="author">
|
||||||
@Html(i18n!(ctx.1, "Written by {0}"; format!("<a href=\"{}\">{}</a>",
|
@Html(i18n!(ctx.1, "Written by {0}"; format!("<a href=\"{}\">{}</a>",
|
||||||
uri!(user::details: name = &author.fqn),
|
escape(&uri!(user::details: name = &author.fqn).to_string()),
|
||||||
escape(&author.name()))))
|
escape(&author.name()))))
|
||||||
</span>
|
</span>
|
||||||
—
|
—
|
||||||
@@ -103,8 +103,8 @@
|
|||||||
</section>
|
</section>
|
||||||
} else {
|
} else {
|
||||||
<p class="center">@Html(i18n!(ctx.1, "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article";
|
<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='{}'>", uri!(session::new: m = _)), "</a>",
|
format!("<a href='{}'>", escape(&uri!(session::new: m = _).to_string())), "</a>",
|
||||||
format!("<a href='{}'>", uri!(posts::remote_interact: blog_name = &blog.fqn, slug = &article.slug)), "</a>"
|
format!("<a href='{}'>", escape(&uri!(posts::remote_interact: blog_name = &blog.fqn, slug = &article.slug).to_string())), "</a>"
|
||||||
))
|
))
|
||||||
</p>
|
</p>
|
||||||
<section class="actions">
|
<section class="actions">
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
.default(login_form.password)
|
.default(login_form.password)
|
||||||
.error(&login_errs)
|
.error(&login_errs)
|
||||||
.set_prop("minlength", 1)
|
.set_prop("minlength", 1)
|
||||||
|
.input_type("password")
|
||||||
.html(ctx.1))
|
.html(ctx.1))
|
||||||
<input type="submit" value="@i18n!(ctx.1, "Log in")" />
|
<input type="submit" value="@i18n!(ctx.1, "Log in")" />
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user