Merge branch 'main' into better-caching

This commit is contained in:
KitaitiMakoto 2020-12-06 17:31:53 +00:00
commit d44c034f6a
43 changed files with 1727 additions and 928 deletions

View File

@ -10,7 +10,7 @@ executors:
type: boolean type: boolean
default: false default: false
docker: docker:
- image: plumeorg/plume-buildenv:v0.0.9 - image: plumeorg/plume-buildenv:v0.2.0
- image: <<#parameters.postgres>>circleci/postgres:9.6-alpine<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>> - image: <<#parameters.postgres>>circleci/postgres:9.6-alpine<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
environment: environment:
POSTGRES_USER: postgres POSTGRES_USER: postgres

View File

@ -1,4 +1,4 @@
FROM debian:stretch-20190326 FROM debian:buster-20201117
ENV PATH="/root/.cargo/bin:${PATH}" ENV PATH="/root/.cargo/bin:${PATH}"
#install native/circleci/build dependancies #install native/circleci/build dependancies

View File

@ -45,3 +45,12 @@ ROCKET_ADDRESS=127.0.0.1
#PLUME_LOGO_192=icons/trwnh/paragraphs/plumeParagraphs192.png #PLUME_LOGO_192=icons/trwnh/paragraphs/plumeParagraphs192.png
#PLUME_LOGO_256=icons/trwnh/paragraphs/plumeParagraphs256.png #PLUME_LOGO_256=icons/trwnh/paragraphs/plumeParagraphs256.png
#PLUME_LOGO_512=icons/trwnh/paragraphs/plumeParagraphs512.png #PLUME_LOGO_512=icons/trwnh/paragraphs/plumeParagraphs512.png
## LDAP CONFIG ##
# the object that will be bound is "${USER_NAME_ATTR}=${username},${BASE_DN}"
#LDAP_ADDR=ldap://127.0.0.1:1389
#LDAP_BASE_DN="ou=users,dc=your-org,dc=eu"
#LDAP_USER_NAME_ATTR=cn
#LDAP_USER_MAIL_ATTR=mail
#LDAP_TLS=false

1
.gitignore vendored
View File

@ -18,3 +18,4 @@ tags.*
search_index search_index
.buildconfig .buildconfig
__pycache__ __pycache__
.vscode/

158
CHANGELOG.md Normal file
View File

@ -0,0 +1,158 @@
# Changelog
<!-- next-header -->
## [Unreleased] - No release date
## [0.5.0] - 2020-06-21
### Added
- Email blocklisting (#718)
- Syntax highlighting (#691)
- Persian localization (#782)
- Switchable tokenizer - enables Japanese full-text search (#776)
- Make database connections configurable by environment variables (#768)
### Changed
- Display likes and boost on post cards (#744)
- Rust 2018 (#726)
- Bump to LLVM to 9.0.0 to fix ARM builds (#737)
- Remove dependency on runtime-fmt (#773)
- Drop the -alpha suffix in release names, it is implied that Plume is not stable yet because of the 0 major version (Plume 1.0.0 will be the first stable release).
### Fixed
- Fix parsing of mentions inside a Markdown code block (be430c6)
- Fix RSS issues (#720)
- Fix Atom feed (#764)
- Fix default theme (#746)
- Fix shown password on remote interact pages (#741)
- Allow unicode hashtags (#757)
- Fix French grammar for for 0 (#760)
- Don't show boosts and likes for "all" and "local" in timelines (#781)
- Fix liking and boosting posts on remote instances (#762)
## [0.4.0] - 2019-12-23
### Added
- Add support for generic timeline (#525)
- Federate user deletion (#551)
- import migrations and don't require diesel_cli for admins (#555)
- Cache local instance (#572)
- Initial RTL support #575 (#577)
- Confirm deletion of blog (#602)
- Make a distinction between moderators and admins (#619)
- Theming (#624)
- Add clap to plume in order to print help and version (#631)
- Add Snapcraft metadata and install/maintenance hooks (#666)
- Add environmental variable to control path of media (#683)
- Add autosaving to the editor (#688)
- CI: Upload artifacts to pull request deploy environment (#539)
- CI: Upload artifact of wasm binary (#571)
### Changed
- Update follow_remote.rs.html grammar (#548)
- Add some feedback when performing some actions (#552)
- Theme update (#553)
- Remove the new index lock tantivy uses (#556)
- Reduce reqwest timeout to 5s (#557)
- Improve notification management (#561)
- Fix occurrences of 'have been' to 'has been' (#578) + Direct follow-up to #578 (#603)
- Store password reset requests in database (#610)
- Use futures and tokio to send activities (#620)
- Don't ignore dotenv errors (#630)
- Replace the input! macro with an Input builder (#646)
- Update default license (#659)
- Paginate the outbox responses. Fixes #669 (#681)
- Use the "classic" editor by default (#697)
- Fix issue #705 (#708)
- Make comments in styleshhets a bit clearer (#545)
- Rewrite circleci config (#558)
- Use openssl instead of sha256sum for build.rs (#568)
- Update dependencies (#574)
- Refactor code to use Shrinkwraprs and diesel-derive-newtype (#598)
- Add enum containing all successful route returns (#614)
- Update dependencies which depended on nix -- fixes arm32 builds (#615)
- Update some documents (#616)
- Update dependencies (#643)
- Make the comment syntax consistent across all CSS (#487)
### Fixed
- Remove r (#535)
- Fix certain improper rendering of forms (#560)
- make hashtags work in profile summary (#562)
- Fix some federation issue (#573)
- Prevent comment form submit button distortion on iOS (#592)
- Update textarea overflow to scroll (#609)
- Fix arm builds (#612)
- Fix theme caching (#647)
- Fix issue #642, frontend not in English if the user language does not exist (#648)
- Don't index drafts (#656)
- Fill entirely user on creation (#657)
- Delete notification on user deletion (#658)
- Order media so that latest added are top (#660)
- Fix logo URL (#664)
- Snap: Ensure cargo-web doesn't erroneously adopt our workspace. (#667)
- Snap: Another fix for building (#668)
- Snap: Fix build for non-Tier-1 Rust platforms (#672)
- Don't split sentences for translations (#677)
- Escape href quotation marks (#678)
- Re-add empty strings in translation (#682)
- Make the search index creation during migration respect SEARCH_INDEX (#689)
- Fix the navigation menu not opening on touch (#690)
- Make search items optional (#693)
- Various snap fixes (#698)
- Fix #637 : Markdown footnotes (#700)
- Fix lettre (#706)
- CI: Fix Crowdin upload (#576)
### Removed
- Remove the Canapi dependency (#540)
- Remove use of Rust in migrations (#704)
## [0.3.0] - 2019-04-19
### Added
- Cover for articles (#299, #387)
- Password reset (#448)
- New editor (#293, #458, #482, #483, #486, #530)
- Search (#324, #375, #445)
- Edit blogs (#460, #494, #497)
- Hashtags in articles (#283, #295)
- API endpoints (#245, #285, #307)
- A bunch of new translations! (#479, #501, #506, #510, #512, #514)
### Changed
- Federation improvements (#216, #217, #357, #364, #399, #443, #446, #455, #502, #519)
- Improved build process (#281, #374, #392, #402, #489, #498, #503, #511, #513, #515, #528)
### Fixes
- UI usability fixes (#370, #386, #401, #417, #418, #444, #452, #480, #516, #518, #522, #532)
## [0.2.0] - 2018-09-12
### Added
- Article publishing, or save as a draft
- Like, or boost an article
- Basic Markdown editor
- Federated commenting system
- User account creation
- Limited federation on other platforms and subscribing to users
- Ability to create multiple blogs
<!-- next-url -->
[Unreleased]: https://github.com/Plume-org/Plume/compare/0.5.0...HEAD
[0.5.0]: https://github.com/Plume-org/Plume/compare/0.4.0-alpha-4...0.5.0
[0.4.0]: https://github.com/Plume-org/Plume/compare/0.3.0-alpha-2...0.4.0-alpha-4
[0.3.0]: https://github.com/Plume-org/Plume/compare/0.2.0-alpha-1...0.3.0-alpha-2
[0.2.0]: https://github.com/Plume-org/Plume/releases/tag/0.2.0-alpha-1

1952
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
[package] [package]
authors = ["Plume contributors"] authors = ["Plume contributors"]
name = "plume" name = "plume"
version = "0.4.0" version = "0.5.0"
repository = "https://github.com/Plume-org/Plume" repository = "https://github.com/Plume-org/Plume"
edition = "2018" edition = "2018"
@ -20,8 +20,8 @@ heck = "0.3.0"
lettre = "0.9.2" lettre = "0.9.2"
lettre_email = "0.9.2" lettre_email = "0.9.2"
num_cpus = "1.10" num_cpus = "1.10"
rocket = "0.4.2" rocket = "0.4.5"
rocket_contrib = { version = "0.4.2", features = ["json"] } rocket_contrib = { version = "0.4.5", 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"
scheduled-thread-pool = "0.2.2" scheduled-thread-pool = "0.2.2"

View File

@ -1,4 +1,4 @@
FROM rust:1-stretch as builder FROM rust:1-buster as builder
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \ ca-certificates \
@ -28,7 +28,7 @@ RUN cargo install --path ./ --force --no-default-features --features postgres
RUN cargo install --path plume-cli --force --no-default-features --features postgres RUN cargo install --path plume-cli --force --no-default-features --features postgres
RUN cargo clean RUN cargo clean
FROM debian:stretch-slim FROM debian:buster-slim
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \ ca-certificates \

View File

@ -1,4 +1,4 @@
FROM rust:1-stretch FROM rust:1-buster
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \ ca-certificates \

View File

@ -40,7 +40,7 @@ main header.article {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: end; justify-content: flex-end;
h1, .article-info { h1, .article-info {
text-align: center; text-align: center;
@ -490,3 +490,30 @@ input:checked ~ .cw-container > .cw-text {
display: inline; display: inline;
} }
} }
// Small screens
@media screen and (max-width: 600px) {
#plume-editor header {
flex-direction: column-reverse;
button {
flex: 0 0 0;
}
}
.popup {
top: 10vh;
bottom: 10vh;
left: 1vw;
right: 1vw;
}
main article {
margin: 2.5em .5em;
max-width: none;
}
main .article-meta > *, main .article-meta .comments, main .article-meta > .banner > * {
margin: 0 5%;
}
}

View File

@ -490,6 +490,10 @@ figure {
/// Small screens /// Small screens
@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {
body > main > *, .h-feed > * {
margin: 1em;
}
main .article-meta { main .article-meta {
> *, .comments { > *, .comments {
margin: 0 5%; margin: 0 5%;
@ -535,7 +539,7 @@ figure {
margin: 0; margin: 0;
& > * { & > * {
max-width: 100%; max-width: 100% !important;
} }
} }

View File

@ -205,6 +205,7 @@ body > header {
position: absolute; position: absolute;
left: 50%; left: 50%;
transform: translate(-50%, 0); transform: translate(-50%, 0);
transform: translateZ(0);
opacity: 0; opacity: 0;
font-size: 0.9em; font-size: 0.9em;
white-space: nowrap; white-space: nowrap;
@ -221,3 +222,93 @@ 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; }
}
}
}
}

View File

@ -1,5 +1,5 @@
"project_identifier": "plume" "project_id": 352097
"api_key_env": CROWDIN_API_KEY "api_token_env": "CROWDIN_API_KEY"
preserve_hierarchy: true preserve_hierarchy: true
files: files:
- source: /po/plume/plume.pot - source: /po/plume/plume.pot

View File

@ -1,6 +1,6 @@
[package] [package]
name = "plume-api" name = "plume-api"
version = "0.4.0" version = "0.5.0"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018" edition = "2018"

1
plume-api/release.toml Normal file
View File

@ -0,0 +1 @@
pre-release-replacements = []

View File

@ -1,6 +1,6 @@
[package] [package]
name = "plume-cli" name = "plume-cli"
version = "0.4.0" version = "0.5.0"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018" edition = "2018"

1
plume-cli/release.toml Normal file
View File

@ -0,0 +1 @@
pre-release-replacements = []

View File

@ -132,7 +132,7 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
role, role,
&bio, &bio,
email, email,
User::hash_pass(&password).expect("Couldn't hash password"), Some(User::hash_pass(&password).expect("Couldn't hash password")),
) )
.expect("Couldn't save new user"); .expect("Couldn't save new user");
} }

View File

@ -1,6 +1,6 @@
[package] [package]
name = "plume-common" name = "plume-common"
version = "0.4.0" version = "0.5.0"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018" edition = "2018"
@ -14,7 +14,7 @@ heck = "0.3.0"
hex = "0.3" hex = "0.3"
hyper = "0.12.33" hyper = "0.12.33"
openssl = "0.10.22" openssl = "0.10.22"
rocket = "0.4.0" rocket = "0.4.5"
reqwest = "0.9" reqwest = "0.9"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"

View File

@ -0,0 +1 @@
pre-release-replacements = []

View File

@ -294,8 +294,7 @@ pub fn md_to_html<'a>(
} }
let hashtag = text_acc; let hashtag = text_acc;
let link = Tag::Link( let link = Tag::Link(
format!("{}tag/{}", base_url, &hashtag.to_camel_case()) format!("{}tag/{}", base_url, &hashtag).into(),
.into(),
hashtag.to_owned().into(), hashtag.to_owned().into(),
); );

View File

@ -1,6 +1,6 @@
[package] [package]
name = "plume-front" name = "plume-front"
version = "0.4.0" version = "0.5.0"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018" edition = "2018"

1
plume-front/release.toml Normal file
View File

@ -0,0 +1 @@
pre-release-replacements = []

View File

@ -269,7 +269,13 @@ pub fn init() -> Result<(), EditorError> {
let editor_button = document().create_element("a")?; let editor_button = document().create_element("a")?;
js! { @{&editor_button}.href = "#"; } js! { @{&editor_button}.href = "#"; }
editor_button.add_event_listener(|_: ClickEvent| { editor_button.add_event_listener(|_: ClickEvent| {
window().local_storage().remove("basic-editor"); if window()
.local_storage()
.insert("basic-editor", "false")
.is_err()
{
console!(log, "Failed to write into local storage");
}
window().history().go(0).ok(); // refresh window().history().go(0).ok(); // refresh
}); });
editor_button.append_child( editor_button.append_child(

View File

@ -1,6 +1,6 @@
[package] [package]
name = "plume-macro" name = "plume-macro"
version = "0.4.0" version = "0.5.0"
authors = ["Trinity Pointard <trinity.pointard@insa-rennes.fr>"] authors = ["Trinity Pointard <trinity.pointard@insa-rennes.fr>"]
edition = "2018" edition = "2018"
description = "Plume procedural macros" description = "Plume procedural macros"

1
plume-macro/release.toml Normal file
View File

@ -0,0 +1 @@
pre-release-replacements = []

View File

@ -1,6 +1,6 @@
[package] [package]
name = "plume-models" name = "plume-models"
version = "0.4.0" version = "0.5.0"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018" edition = "2018"
@ -13,9 +13,10 @@ guid-create = "0.1"
heck = "0.3.0" heck = "0.3.0"
itertools = "0.8.0" itertools = "0.8.0"
lazy_static = "1.0" lazy_static = "1.0"
ldap3 = "0.7.1"
migrations_internals= "1.4.0" migrations_internals= "1.4.0"
openssl = "0.10.22" openssl = "0.10.22"
rocket = "0.4.0" rocket = "0.4.5"
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" } rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
reqwest = "0.9" reqwest = "0.9"
scheduled-thread-pool = "0.2.2" scheduled-thread-pool = "0.2.2"
@ -30,7 +31,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 } lindera-tantivy = { version = "0.1.3", optional = true }
[dependencies.chrono] [dependencies.chrono]
features = ["serde"] features = ["serde"]

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
pre-release-replacements = []

View File

@ -20,6 +20,7 @@ pub struct Config {
pub logo: LogoConfig, pub logo: LogoConfig,
pub default_theme: String, pub default_theme: String,
pub media_directory: String, pub media_directory: String,
pub ldap: Option<LdapConfig>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -240,6 +241,42 @@ impl SearchTokenizerConfig {
} }
} }
pub struct LdapConfig {
pub addr: String,
pub base_dn: String,
pub tls: bool,
pub user_name_attr: String,
pub mail_attr: String,
}
fn get_ldap_config() -> Option<LdapConfig> {
let addr = var("LDAP_ADDR").ok();
let base_dn = var("LDAP_BASE_DN").ok();
match (addr, base_dn) {
(Some(addr), Some(base_dn)) => {
let tls = var("LDAP_TLS").unwrap_or_else(|_| "false".to_owned());
let tls = match tls.as_ref() {
"1" | "true" | "TRUE" => true,
"0" | "false" | "FALSE" => false,
_ => panic!("Invalid LDAP configuration : tls"),
};
let user_name_attr = var("LDAP_USER_NAME_ATTR").unwrap_or_else(|_| "cn".to_owned());
let mail_attr = var("LDAP_USER_MAIL_ATTR").unwrap_or_else(|_| "mail".to_owned());
Some(LdapConfig {
addr,
base_dn,
tls,
user_name_attr,
mail_attr,
})
}
(None, None) => None,
(_, _) => {
panic!("Invalid LDAP configuration : both LDAP_ADDR and LDAP_BASE_DN must be set")
}
}
}
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!(
@ -267,5 +304,6 @@ lazy_static! {
default_theme: var("DEFAULT_THEME").unwrap_or_else(|_| "default-light".to_owned()), default_theme: var("DEFAULT_THEME").unwrap_or_else(|_| "default-light".to_owned()),
media_directory: var("MEDIA_UPLOAD_DIRECTORY") media_directory: var("MEDIA_UPLOAD_DIRECTORY")
.unwrap_or_else(|_| "static/media".to_owned()), .unwrap_or_else(|_| "static/media".to_owned()),
ldap: get_ldap_config(),
}; };
} }

View File

@ -11,7 +11,7 @@ use activitypub::{
}; };
use chrono::{NaiveDateTime, TimeZone, Utc}; use chrono::{NaiveDateTime, TimeZone, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use heck::{CamelCase, KebabCase}; use heck::KebabCase;
use plume_common::{ use plume_common::{
activity_pub::{ activity_pub::{
inbox::{AsObject, FromId}, inbox::{AsObject, FromId},
@ -622,7 +622,6 @@ impl FromId<PlumeRocket> for Post {
let mut hashtags = md_to_html(&post.source, None, false, None) let mut hashtags = md_to_html(&post.source, None, false, None)
.2 .2
.into_iter() .into_iter()
.map(|s| s.to_camel_case())
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
if let Some(serde_json::Value::Array(tags)) = article.object_props.tag { if let Some(serde_json::Value::Array(tags)) = article.object_props.tag {
for tag in tags { for tag in tags {
@ -762,7 +761,6 @@ impl AsObject<User, Update, &PlumeRocket> for PostUpdate {
let mut txt_hashtags = md_to_html(&post.source, None, false, None) let mut txt_hashtags = md_to_html(&post.source, None, false, None)
.2 .2
.into_iter() .into_iter()
.map(|s| s.to_camel_case())
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
if let Some(serde_json::Value::Array(mention_tags)) = self.tags { if let Some(serde_json::Value::Array(mention_tags)) = self.tags {
let mut mentions = vec![]; let mut mentions = vec![];

View File

@ -5,10 +5,10 @@ use crate::{
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, io, path::Path, sync::Mutex};
use tantivy::{ use tantivy::{
collector::TopDocs, directory::MmapDirectory, schema::*, Index, IndexReader, IndexWriter, collector::TopDocs, directory::MmapDirectory, schema::*, Index, IndexReader, IndexWriter,
ReloadPolicy, Term, ReloadPolicy, TantivyError, Term,
}; };
use whatlang::{detect as detect_lang, Lang}; use whatlang::{detect as detect_lang, Lang};
@ -18,6 +18,7 @@ pub enum SearcherError {
WriteLockAcquisitionError, WriteLockAcquisitionError,
IndexOpeningError, IndexOpeningError,
IndexEditionError, IndexEditionError,
InvalidIndexDataError,
} }
pub struct Searcher { pub struct Searcher {
@ -135,7 +136,19 @@ impl Searcher {
.reader_builder() .reader_builder()
.reload_policy(ReloadPolicy::Manual) .reload_policy(ReloadPolicy::Manual)
.try_into() .try_into()
.map_err(|_| SearcherError::IndexCreationError)?, .map_err(|e| {
if let TantivyError::IOError(err) = e {
let err: io::Error = err.into();
if err.kind() == io::ErrorKind::InvalidData {
// Search index was created in older Tantivy format.
SearcherError::InvalidIndexDataError
} else {
SearcherError::IndexCreationError
}
} else {
SearcherError::IndexCreationError
}
})?,
index, index,
}) })
} }

View File

@ -1,8 +1,8 @@
use crate::{ use crate::{
ap_url, blocklisted_emails::BlocklistedEmail, blogs::Blog, db_conn::DbConn, follows::Follow, ap_url, blocklisted_emails::BlocklistedEmail, blogs::Blog, config::CONFIG, db_conn::DbConn,
instance::*, medias::Media, notifications::Notification, post_authors::PostAuthor, posts::Post, follows::Follow, instance::*, medias::Media, notifications::Notification,
safe_string::SafeString, schema::users, search::Searcher, timeline::Timeline, Connection, post_authors::PostAuthor, posts::Post, safe_string::SafeString, schema::users,
Error, PlumeRocket, Result, ITEMS_PER_PAGE, search::Searcher, timeline::Timeline, Connection, Error, PlumeRocket, Result, ITEMS_PER_PAGE,
}; };
use activitypub::{ use activitypub::{
activity::Delete, activity::Delete,
@ -14,6 +14,7 @@ use activitypub::{
use bcrypt; use bcrypt;
use chrono::{NaiveDateTime, Utc}; use chrono::{NaiveDateTime, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl}; use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl};
use ldap3::{LdapConn, Scope, SearchEntry};
use openssl::{ use openssl::{
hash::MessageDigest, hash::MessageDigest,
pkey::{PKey, Private}, pkey::{PKey, Private},
@ -292,11 +293,116 @@ impl User {
bcrypt::hash(pass, 10).map_err(Error::from) bcrypt::hash(pass, 10).map_err(Error::from)
} }
pub fn auth(&self, pass: &str) -> bool { fn ldap_register(conn: &Connection, name: &str, password: &str) -> Result<User> {
self.hashed_password if CONFIG.ldap.is_none() {
.clone() return Err(Error::NotFound);
.map(|hashed| bcrypt::verify(pass, hashed.as_ref()).unwrap_or(false)) }
.unwrap_or(false) let ldap = CONFIG.ldap.as_ref().unwrap();
let mut ldap_conn = LdapConn::new(&ldap.addr).map_err(|_| Error::NotFound)?;
let ldap_name = format!("{}={},{}", ldap.user_name_attr, name, ldap.base_dn);
let bind = ldap_conn
.simple_bind(&ldap_name, password)
.map_err(|_| Error::NotFound)?;
if bind.success().is_err() {
return Err(Error::NotFound);
}
let search = ldap_conn
.search(
&ldap_name,
Scope::Base,
"(|(objectClass=person)(objectClass=user))",
vec![&ldap.mail_attr],
)
.map_err(|_| Error::NotFound)?
.success()
.map_err(|_| Error::NotFound)?;
for entry in search.0 {
let entry = SearchEntry::construct(entry);
let email = entry.attrs.get("mail").and_then(|vec| vec.first());
if let Some(email) = email {
let _ = ldap_conn.unbind();
return NewUser::new_local(
conn,
name.to_owned(),
name.to_owned(),
Role::Normal,
"",
email.to_owned(),
None,
);
}
}
let _ = ldap_conn.unbind();
Err(Error::NotFound)
}
fn ldap_login(&self, password: &str) -> bool {
if let Some(ldap) = CONFIG.ldap.as_ref() {
let mut conn = if let Ok(conn) = LdapConn::new(&ldap.addr) {
conn
} else {
return false;
};
let name = format!(
"{}={},{}",
ldap.user_name_attr, &self.username, ldap.base_dn
);
if let Ok(bind) = conn.simple_bind(&name, password) {
bind.success().is_ok()
} else {
false
}
} else {
false
}
}
pub fn login(conn: &Connection, ident: &str, password: &str) -> Result<User> {
let local_id = Instance::get_local()?.id;
let user = match User::find_by_email(conn, ident) {
Ok(user) => Ok(user),
_ => User::find_by_name(conn, ident, local_id),
}
.and_then(|u| {
if u.instance_id == local_id {
Ok(u)
} else {
Err(Error::NotFound)
}
});
match user {
Ok(user) if user.hashed_password.is_some() => {
if bcrypt::verify(password, user.hashed_password.as_ref().unwrap()).unwrap_or(false)
{
Ok(user)
} else {
Err(Error::NotFound)
}
}
Ok(user) => {
if user.ldap_login(password) {
Ok(user)
} else {
Err(Error::NotFound)
}
}
e => {
if let Ok(user) = User::ldap_register(conn, ident, password) {
return Ok(user);
}
// if no user was found, and we were unable to auto-register from ldap
// fake-verify a password, and return an error.
let other = User::get(&*conn, 1)
.expect("No user is registered")
.hashed_password;
other.map(|pass| bcrypt::verify(password, &pass));
e
}
}
} }
pub fn reset_password(&self, conn: &Connection, pass: &str) -> Result<()> { pub fn reset_password(&self, conn: &Connection, pass: &str) -> Result<()> {
@ -983,7 +1089,7 @@ impl NewUser {
role: Role, role: Role,
summary: &str, summary: &str,
email: String, email: String,
password: String, password: Option<String>,
) -> Result<User> { ) -> Result<User> {
let (pub_key, priv_key) = gen_keypair(); let (pub_key, priv_key) = gen_keypair();
let instance = Instance::get_local()?; let instance = Instance::get_local()?;
@ -1001,7 +1107,7 @@ impl NewUser {
summary: summary.to_owned(), summary: summary.to_owned(),
summary_html: SafeString::new(&utils::md_to_html(&summary, None, false, None).0), summary_html: SafeString::new(&utils::md_to_html(&summary, None, false, None).0),
email: Some(email), email: Some(email),
hashed_password: Some(password), hashed_password: password,
instance_id: instance.id, instance_id: instance.id,
public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?, public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?,
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?), private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
@ -1043,7 +1149,7 @@ pub(crate) mod tests {
Role::Admin, Role::Admin,
"Hello there, I'm the admin", "Hello there, I'm the admin",
"admin@example.com".to_owned(), "admin@example.com".to_owned(),
"invalid_admin_password".to_owned(), Some("invalid_admin_password".to_owned()),
) )
.unwrap(); .unwrap();
let user = NewUser::new_local( let user = NewUser::new_local(
@ -1053,7 +1159,7 @@ pub(crate) mod tests {
Role::Normal, Role::Normal,
"Hello there, I'm no one", "Hello there, I'm no one",
"user@example.com".to_owned(), "user@example.com".to_owned(),
"invalid_user_password".to_owned(), Some("invalid_user_password".to_owned()),
) )
.unwrap(); .unwrap();
let other = NewUser::new_local( let other = NewUser::new_local(
@ -1063,7 +1169,7 @@ pub(crate) mod tests {
Role::Normal, Role::Normal,
"Hello there, I'm someone else", "Hello there, I'm someone else",
"other@example.com".to_owned(), "other@example.com".to_owned(),
"invalid_other_password".to_owned(), Some("invalid_other_password".to_owned()),
) )
.unwrap(); .unwrap();
vec![admin, user, other] vec![admin, user, other]
@ -1082,7 +1188,7 @@ pub(crate) mod tests {
Role::Normal, Role::Normal,
"Hello I'm a test", "Hello I'm a test",
"test@example.com".to_owned(), "test@example.com".to_owned(),
User::hash_pass("test_password").unwrap(), Some(User::hash_pass("test_password").unwrap()),
) )
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@ -1165,12 +1271,15 @@ pub(crate) mod tests {
Role::Normal, Role::Normal,
"Hello I'm a test", "Hello I'm a test",
"test@example.com".to_owned(), "test@example.com".to_owned(),
User::hash_pass("test_password").unwrap(), Some(User::hash_pass("test_password").unwrap()),
) )
.unwrap(); .unwrap();
assert!(test_user.auth("test_password")); assert_eq!(
assert!(!test_user.auth("other_password")); User::login(conn, "test", "test_password").unwrap().id,
test_user.id
);
assert!(User::login(conn, "test", "other_password").is_err());
Ok(()) Ok(())
}); });
} }

17
release.toml Normal file
View File

@ -0,0 +1,17 @@
# we don't have a crate yet, so
disable-publish = true
# change when we all have gpg keys
sign-commit = false
dev-version-ext = 'dev'
# update all crates in plume at once:
consolidate-commits = true
pre-release-hook = ["crowdin", "pull"]
pre-release-replacements = [
{file="CHANGELOG.md", search="Unreleased", replace="[{{version}}]"},
{file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1},
{file="CHANGELOG.md", search="No release date", replace="{{date}}"},
{file="CHANGELOG.md", search="<!-- next-header -->", replace="<!-- next-header -->\n\n## [Unreleased] - ReleaseDate", exactly=1},
{file="CHANGELOG.md", search="<!-- next-url -->", replace="<!-- next-url -->\n[Unreleased]: https://github.com/Plume-org/Plume/compare/{{tag_name}}...HEAD", exactly=1},
]

View File

@ -62,8 +62,7 @@ pub fn oauth(
let conn = &*rockets.conn; let conn = &*rockets.conn;
let app = App::find_by_client_id(conn, &query.client_id)?; let app = App::find_by_client_id(conn, &query.client_id)?;
if app.client_secret == query.client_secret { if app.client_secret == query.client_secret {
if let Ok(user) = User::find_by_fqn(&rockets, &query.username) { if let Ok(user) = User::login(conn, &query.username, &query.password) {
if user.auth(&query.password) {
let token = ApiToken::insert( let token = ApiToken::insert(
conn, conn,
NewApiToken { NewApiToken {
@ -81,15 +80,6 @@ pub fn oauth(
"error": "Invalid credentials" "error": "Invalid credentials"
}))) })))
} }
} else {
// Making fake password verification to avoid different
// response times that would make it possible to know
// if a username is registered or not.
User::get(conn, 1)?.auth(&query.password);
Ok(Json(json!({
"error": "Invalid credentials"
})))
}
} else { } else {
Ok(Json(json!({ Ok(Json(json!({
"error": "Invalid client_secret" "error": "Invalid client_secret"

View File

@ -1,5 +1,5 @@
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use heck::{CamelCase, KebabCase}; use heck::KebabCase;
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
use crate::api::{authorization::*, Api}; use crate::api::{authorization::*, Api};
@ -181,7 +181,7 @@ pub fn create(
Tag::insert( Tag::insert(
conn, conn,
NewTag { NewTag {
tag: hashtag.to_camel_case(), tag: hashtag,
is_hashtag: true, is_hashtag: true,
post_id: post.id, post_id: post.id,
}, },

27
src/main.rs Normal file → Executable file
View File

@ -10,6 +10,7 @@ extern crate serde_json;
#[macro_use] #[macro_use]
extern crate validator_derive; extern crate validator_derive;
use chrono::Utc;
use clap::App; use clap::App;
use diesel::r2d2::ConnectionManager; use diesel::r2d2::ConnectionManager;
use plume_models::{ use plume_models::{
@ -21,6 +22,8 @@ use plume_models::{
}; };
use rocket_csrf::CsrfFairingBuilder; use rocket_csrf::CsrfFairingBuilder;
use scheduled_thread_pool::ScheduledThreadPool; use scheduled_thread_pool::ScheduledThreadPool;
use std::fs;
use std::path::Path;
use std::process::exit; use std::process::exit;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::Duration; use std::time::Duration;
@ -98,8 +101,30 @@ 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
let mut open_searcher =
UnmanagedSearcher::open(&CONFIG.search_index, &CONFIG.search_tokenizers);
if let Err(Error::Search(SearcherError::InvalidIndexDataError)) = open_searcher {
if UnmanagedSearcher::create(&CONFIG.search_index, &CONFIG.search_tokenizers).is_err() {
let current_path = Path::new(&CONFIG.search_index);
let backup_path = format!("{}.{}", &current_path.display(), Utc::now().timestamp());
let backup_path = Path::new(&backup_path);
fs::rename(current_path, backup_path)
.expect("main: error on backing up search index directory for recreating");
if UnmanagedSearcher::create(&CONFIG.search_index, &CONFIG.search_tokenizers).is_ok() {
if fs::remove_dir_all(backup_path).is_err() {
eprintln!(
"error on removing backup directory: {}. it remains",
backup_path.display()
);
}
} else {
panic!("main: error on recreating search index in new index format. remove search index and run `plm search init` manually");
}
}
open_searcher = UnmanagedSearcher::open(&CONFIG.search_index, &CONFIG.search_tokenizers);
}
#[allow(clippy::match_wild_err_arm)] #[allow(clippy::match_wild_err_arm)]
let searcher = match UnmanagedSearcher::open(&CONFIG.search_index, &CONFIG.search_tokenizers) { let searcher = match open_searcher {
Err(Error::Search(e)) => match e { Err(Error::Search(e)) => match e {
SearcherError::WriteLockAcquisitionError => panic!( SearcherError::WriteLockAcquisitionError => panic!(
r#" r#"

View File

@ -210,12 +210,20 @@ pub fn add_email_blocklist(
form: LenientForm<NewBlocklistedEmail>, form: LenientForm<NewBlocklistedEmail>,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<Flash<Redirect>, ErrorPage> { ) -> Result<Flash<Redirect>, ErrorPage> {
BlocklistedEmail::insert(&*rockets.conn, form.0)?; let result = BlocklistedEmail::insert(&*rockets.conn, form.0);
if let Err(Error::Db(_)) = result {
Ok(Flash::error(
Redirect::to(uri!(admin_email_blocklist: page = None)),
i18n!(rockets.intl.catalog, "Email already blocked"),
))
} else {
Ok(Flash::success( Ok(Flash::success(
Redirect::to(uri!(admin_email_blocklist: page = None)), Redirect::to(uri!(admin_email_blocklist: page = None)),
i18n!(rockets.intl.catalog, "Email Blocked"), i18n!(rockets.intl.catalog, "Email Blocked"),
)) ))
} }
}
#[get("/admin/emails?<page>")] #[get("/admin/emails?<page>")]
pub fn admin_email_blocklist( pub fn admin_email_blocklist(
_mod: Moderator, _mod: Moderator,

View File

@ -1,5 +1,5 @@
use chrono::Utc; use chrono::Utc;
use heck::{CamelCase, KebabCase}; use heck::KebabCase;
use rocket::request::LenientForm; use rocket::request::LenientForm;
use rocket::response::{Flash, Redirect}; use rocket::response::{Flash, Redirect};
use rocket_i18n::I18n; use rocket_i18n::I18n;
@ -314,18 +314,17 @@ pub fn update(
let tags = form let tags = form
.tags .tags
.split(',') .split(',')
.map(|t| t.trim().to_camel_case()) .map(|t| t.trim())
.filter(|t| !t.is_empty()) .filter(|t| !t.is_empty())
.collect::<HashSet<_>>() .collect::<HashSet<_>>()
.into_iter() .into_iter()
.filter_map(|t| Tag::build_activity(t).ok()) .filter_map(|t| Tag::build_activity(t.to_string()).ok())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
post.update_tags(&conn, tags) post.update_tags(&conn, tags)
.expect("post::update: tags error"); .expect("post::update: tags error");
let hashtags = hashtags let hashtags = hashtags
.into_iter() .into_iter()
.map(|h| h.to_camel_case())
.collect::<HashSet<_>>() .collect::<HashSet<_>>()
.into_iter() .into_iter()
.filter_map(|t| Tag::build_activity(t).ok()) .filter_map(|t| Tag::build_activity(t).ok())
@ -489,14 +488,14 @@ pub fn create(
let tags = form let tags = form
.tags .tags
.split(',') .split(',')
.map(|t| t.trim().to_camel_case()) .map(|t| t.trim())
.filter(|t| !t.is_empty()) .filter(|t| !t.is_empty())
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
for tag in tags { for tag in tags {
Tag::insert( Tag::insert(
&*conn, &*conn,
NewTag { NewTag {
tag, tag: tag.to_string(),
is_hashtag: false, is_hashtag: false,
post_id: post.id, post_id: post.id,
}, },
@ -507,7 +506,7 @@ pub fn create(
Tag::insert( Tag::insert(
&*conn, &*conn,
NewTag { NewTag {
tag: hashtag.to_camel_case(), tag: hashtag,
is_hashtag: true, is_hashtag: true,
post_id: post.id, post_id: post.id,
}, },

View File

@ -48,38 +48,19 @@ pub fn create(
rockets: PlumeRocket, rockets: PlumeRocket,
) -> RespondOrRedirect { ) -> RespondOrRedirect {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let user = User::find_by_email(&*conn, &form.email_or_name)
.or_else(|_| User::find_by_fqn(&rockets, &form.email_or_name));
let mut errors = match form.validate() { let mut errors = match form.validate() {
Ok(_) => ValidationErrors::new(), Ok(_) => ValidationErrors::new(),
Err(e) => e, Err(e) => e,
}; };
let user = User::login(conn, &form.email_or_name, &form.password);
let user_id = if let Ok(user) = user { let user_id = if let Ok(user) = user {
if !user.auth(&form.password) {
let mut err = ValidationError::new("invalid_login");
err.message = Some(Cow::from("Invalid username, or password"));
errors.add("email_or_name", err);
String::new()
} else {
user.id.to_string() user.id.to_string()
}
} else { } else {
// Fake password verification, only to avoid different login times
// that could be used to see if an email adress is registered or not
User::get(&*conn, 1)
.map(|u| u.auth(&form.password))
.expect("No user is registered");
let mut err = ValidationError::new("invalid_login"); let mut err = ValidationError::new("invalid_login");
err.message = Some(Cow::from("Invalid username, or password")); err.message = Some(Cow::from("Invalid username, or password"));
errors.add("email_or_name", err); errors.add("email_or_name", err);
String::new()
};
if !errors.is_empty() {
return render!(session::login(&rockets.to_context(), None, &*form, errors)).into(); return render!(session::login(&rockets.to_context(), None, &*form, errors)).into();
} };
cookies.add_private( cookies.add_private(
Cookie::build(AUTH_COOKIE, user_id) Cookie::build(AUTH_COOKIE, user_id)

View File

@ -541,7 +541,7 @@ pub fn create(
Role::Normal, Role::Normal,
"", "",
form.email.to_string(), form.email.to_string(),
User::hash_pass(&form.password).map_err(to_validation)?, Some(User::hash_pass(&form.password).map_err(to_validation)?),
).map_err(to_validation)?; ).map_err(to_validation)?;
Ok(Flash::success( Ok(Flash::success(
Redirect::to(uri!(super::session::new: m = _)), Redirect::to(uri!(super::session::new: m = _)),

View File

@ -10,9 +10,6 @@
@:base(ctx, tl.name.clone(), {}, {}, { @:base(ctx, tl.name.clone(), {}, {}, {
<section class="flex wrap" dir="auto"> <section class="flex wrap" dir="auto">
<h1 class="grow">@i18n_timeline_name(ctx.1, &tl.name)</h1> <h1 class="grow">@i18n_timeline_name(ctx.1, &tl.name)</h1>
@if ctx.clone().2.map(|u| (u.is_admin() && tl.user_id.is_none()) || Some(u.id) == tl.user_id).unwrap_or(false) {
<a href="@uri!(timelines::edit: _id = tl.id)" class="button inline-block">@i18n!(ctx.1, "Edit")</a>
}
</section> </section>
@tabs(&vec![(format!("{}", uri!(instance::index)), i18n!(ctx.1, "Latest articles"), false)] @tabs(&vec![(format!("{}", uri!(instance::index)), i18n!(ctx.1, "Latest articles"), false)]