Compare commits

..

1 Commits

Author SHA1 Message Date
Trinity Pointard 6ef8ace025 attempt to do non anonymous ldap connect 2021-02-21 15:34:44 +01:00
176 changed files with 9577 additions and 10863 deletions
+1 -1
View File
@@ -63,7 +63,7 @@ commands:
type: boolean type: boolean
default: false default: false
steps: steps:
- run: cargo clippy <<^parameters.no_feature>>--no-default-features --features="${FEATURES}"<</parameters.no_feature>> --release -p <<parameters.package>> -- -D warnings -A clippy::needless_borrow - run: cargo clippy <<^parameters.no_feature>>--no-default-features --features="${FEATURES}"<</parameters.no_feature>> --release -p <<parameters.package>> -- -D warnings
run_with_coverage: run_with_coverage:
description: run command with environment for coverage description: run command with environment for coverage
+1 -1
View File
@@ -11,7 +11,7 @@ RUN apt update &&\
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
#install and configure rust #install and configure rust
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2021-11-27 -y &&\ RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2021-01-15 -y &&\
rustup component add rustfmt clippy &&\ rustup component add rustfmt clippy &&\
rustup component add rust-std --target wasm32-unknown-unknown rustup component add rust-std --target wasm32-unknown-unknown
+1 -1
View File
@@ -16,7 +16,7 @@ DATABASE_URL=postgres://plume:plume@localhost/plume
BASE_URL=plu.me BASE_URL=plu.me
# Log level for each crate # Log level for each crate
RUST_LOG=info RUST_LOG=warn,html5ever=warn,hyper=warn,tantivy=warn
# The secret key for private cookies and CSRF protection # The secret key for private cookies and CSRF protection
# You can generate one with `openssl rand -base64 32` # You can generate one with `openssl rand -base64 32`
@@ -1,30 +0,0 @@
name: cd
on:
push:
branches:
- 'main'
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
tags: plumeorg/plume:latest
-36
View File
@@ -1,36 +0,0 @@
name: cd
on:
push:
tags:
- '*.*.*'
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: plumeorg/plume
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
+2 -36
View File
@@ -4,26 +4,6 @@
## [Unreleased] - ReleaseDate ## [Unreleased] - ReleaseDate
## [[0.7.1]] - 2022-01-12
### Added
- Introduce environment variable `MAIL_PORT` (#980)
- Introduce email sign-up feature (#636, #1002)
### Changed
- Some styling improvements (#976, #977, #978)
- Respond with error status code when error (#1002)
### Fiexed
- Fix comment link (#974)
- Fix a bug that prevents posting articles (#975)
- Fix a bug that notification page doesn't show (#981)
## [[0.7.0]] - 2022-01-02
### Added ### Added
- Allow `dir` attributes for LtoR text in RtoL document (#860) - Allow `dir` attributes for LtoR text in RtoL document (#860)
@@ -31,31 +11,19 @@
- Proxy support (#829) - Proxy support (#829)
- Riker a actor system library (#870) - Riker a actor system library (#870)
- (request-target) and Host header in HTTP Signature (#872) - (request-target) and Host header in HTTP Signature (#872)
- Default log levels for RUST_LOG (#885, #886, #919)
### Changed ### Changed
- Upgrade some dependent crates (#858) - Upgrade some dependent crates (#858)
- Use tracing crate (#868) - Use tracing crate (#868)
- Update Rust version to nightly-2021-11-27 (#961) - Update Rust version to nightly-2021-01-15 (#878)
- Upgrade Tantivy to 0.13.3 and lindera-tantivy to 0.7.1 (#878) - Upgrade Tantivy to 0.13.3 and lindera-tantivy to 0.7.1 (#878)
- Run searcher on actor system (#870) - Run searcher on actor system (#870)
- Extract a function to calculate posts' ap_url and share it with some places (#918)
- Use article title as its slug instead of capitalizing and inserting hyphens (#920)
- Sign GET requests to other instances (#957)
### Fixed ### Fixed
- Percent-encode URI for remote_interact (#866, #857) - Percent-encode URI for remote_interact (#866, #857)
- Menu animation not opening on iOS (#876, #897) - Menu animation not opening on iOS (#876, #897)
- Make actors subscribe to channel once (#913)
- Upsert posts and media instead of trying to insert and fail (#912)
- Update post's ActivityPub id when published by update (#915)
- Calculate media URI properly even when MEDIA_UPLOAD_DIRECTORY configured (#916)
- Prevent duplicated posts in 'all' timeline (#917)
- Draw side line for blockquote on start (#933)
- Fix URIs of posts on Mastodon (#947)
- Place edit link proper position (#956, #963, #964)
## [[0.6.0]] - 2020-12-29 ## [[0.6.0]] - 2020-12-29
@@ -236,9 +204,7 @@
- Ability to create multiple blogs - Ability to create multiple blogs
<!-- next-url --> <!-- next-url -->
[Unreleased]: https://github.com/Plume-org/Plume/compare/0.7.1...HEAD [Unreleased]: https://github.com/Plume-org/Plume/compare/0.6.0...HEAD
[[0.7.1]]: https://github.com/Plume-org/Plume/compare/0.7.0...0.7.1
[[0.7.0]]: https://github.com/Plume-org/Plume/compare/0.6.0...0.7.0
[[0.6.0]]: https://github.com/Plume-org/Plume/compare/0.5.0...0.6.0 [[0.6.0]]: https://github.com/Plume-org/Plume/compare/0.5.0...0.6.0
[0.5.0]: https://github.com/Plume-org/Plume/compare/0.4.0-alpha-4...0.5.0 [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.4.0]: https://github.com/Plume-org/Plume/compare/0.3.0-alpha-2...0.4.0-alpha-4
Generated
+1005 -1235
View File
File diff suppressed because it is too large Load Diff
+13 -8
View File
@@ -1,32 +1,37 @@
[package] [package]
authors = ["Plume contributors"] authors = ["Plume contributors"]
name = "plume" name = "plume"
version = "0.7.1" version = "0.6.1-dev"
repository = "https://github.com/Plume-org/Plume" repository = "https://github.com/Plume-org/Plume"
edition = "2018" edition = "2018"
[dependencies] [dependencies]
activitypub = "0.1.3" activitypub = "0.1.3"
askama_escape = "0.1"
atom_syndication = "0.6" atom_syndication = "0.6"
clap = "2.33" clap = "2.33"
dotenv = "0.15.0" dotenv = "0.15.0"
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" } gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" } gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" } gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
guid-create = "0.2" guid-create = "0.1"
heck = "0.3.0"
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.6" rocket = "0.4.6"
rocket_contrib = { version = "0.4.5", 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"
scheduled-thread-pool = "0.2.2" scheduled-thread-pool = "0.2.2"
serde = "1.0" serde = "1.0"
serde_json = "1.0.70" serde_json = "1.0"
shrinkwraprs = "0.3.0" shrinkwraprs = "0.2.1"
validator = { version = "0.14", features = ["derive"] } validator = "0.8"
validator_derive = "0.8"
webfinger = "0.4.1" webfinger = "0.4.1"
tracing = "0.1.22" tracing = "0.1.22"
tracing-subscriber = "0.3.5" tracing-subscriber = "0.2.15"
riker = "0.4.2" riker = "0.4.2"
[[bin]] [[bin]]
@@ -48,7 +53,7 @@ version = "1.4.5"
[dependencies.multipart] [dependencies.multipart]
default-features = false default-features = false
features = ["server"] features = ["server"]
version = "0.18" version = "0.16"
[dependencies.plume-api] [dependencies.plume-api]
path = "plume-api" path = "plume-api"
@@ -65,7 +70,7 @@ rev = "29910f2829e7e590a540da3804336577b48c7b31"
[build-dependencies] [build-dependencies]
ructe = "0.13.0" ructe = "0.13.0"
rsass = "0.23" rsass = "0.9"
[features] [features]
default = ["postgres"] default = ["postgres"]
+1 -1
View File
@@ -30,7 +30,7 @@ A lot of features are still missing, but what is already here should be quite st
- **Media management**: you can upload pictures to illustrate your articles, but also audio files if you host a podcast, and manage them all from Plume. - **Media management**: you can upload pictures to illustrate your articles, but also audio files if you host a podcast, and manage them all from Plume.
- **Federation**: Plume is part of a network of interconnected websites called the Fediverse. Each of these websites (often called *instances*) have their own - **Federation**: Plume is part of a network of interconnected websites called the Fediverse. Each of these websites (often called *instances*) have their own
rules and thematics, but they can all communicate with each other. rules and thematics, but they can all communicate with each other.
- **Collaborative writing**: invite other people to your blogs, and write articles together. (Not implemented yet, but will be in 1.0) - **Collaborative writing**: invite other people to your blogs, and write articles together.
## Get involved ## Get involved
+1 -8
View File
@@ -98,7 +98,7 @@ main article {
} }
blockquote { blockquote {
border-inline-start: 5px solid $gray; border-left: 5px solid $gray;
margin: 1em auto; margin: 1em auto;
padding: 0em 2em; padding: 0em 2em;
} }
@@ -516,11 +516,4 @@ input:checked ~ .cw-container > .cw-text {
main .article-meta > *, main .article-meta .comments, main .article-meta > .banner > * { main .article-meta > *, main .article-meta .comments, main .article-meta > .banner > * {
margin: 0 5%; margin: 0 5%;
} }
.bottom-bar {
align-items: center;
& > div:nth-child(2) {
margin: 0;
}
}
} }
+15 -22
View File
@@ -219,23 +219,15 @@ p.error {
margin: 20px; margin: 20px;
} }
.cover-link {
margin: 0;
&:hover {
opacity: 0.9;
}
}
.cover { .cover {
min-height: 10em; min-height: 10em;
background-position: center; background-position: center;
background-size: cover; background-size: cover;
margin: 0px; margin: 0px;
}
header { &:hover {
display: flex; opacity: 0.9;
}
} }
h3 { h3 {
@@ -244,14 +236,9 @@ p.error {
font-family: $playfair; font-family: $playfair;
font-size: 1.75em; font-size: 1.75em;
font-weight: normal; font-weight: normal;
line-height: 1.10; line-height: 1.75;
display: inline-block;
position: relative;
a { a {
display: block; display: block;
width: 100%;
height: 100%;
padding-block-start: 0.5em;
transition: color 0.1s ease-in; transition: color 0.1s ease-in;
color: $text-color; color: $text-color;
@@ -260,8 +247,7 @@ p.error {
} }
.controls { .controls {
flex-shrink: 0; float: right;
text-align: end;
.button { .button {
margin-top: 0; margin-top: 0;
@@ -275,7 +261,7 @@ p.error {
font-family: $lora; font-family: $lora;
font-size: 1em; font-size: 1em;
line-height: 1.25; line-height: 1.25;
text-align: initial; text-align: left;
overflow: hidden; overflow: hidden;
} }
} }
@@ -479,10 +465,9 @@ figure {
/// Avatars /// Avatars
.avatar { .avatar {
background-position: center !important; background-position: center;
background-size: cover; background-size: cover;
border-radius: 100%; border-radius: 100%;
flex-shrink: 0;
&.small { &.small {
width: 50px; width: 50px;
@@ -576,6 +561,14 @@ figure {
} }
} }
.bottom-bar {
flex-direction: column;
align-items: center;
& > div {
margin: 0;
}
}
main .article-meta .comments .comment { main .article-meta .comments .comment {
header { header {
flex-direction: column; flex-direction: column;
+6 -12
View File
@@ -41,9 +41,9 @@ fn main() {
.expect("compile templates"); .expect("compile templates");
compile_themes().expect("Theme compilation error"); compile_themes().expect("Theme compilation error");
recursive_copy(&Path::new("assets").join("icons"), Path::new("static")) recursive_copy(&Path::new("assets").join("icons"), &Path::new("static"))
.expect("Couldn't copy icons"); .expect("Couldn't copy icons");
recursive_copy(&Path::new("assets").join("images"), Path::new("static")) recursive_copy(&Path::new("assets").join("images"), &Path::new("static"))
.expect("Couldn't copy images"); .expect("Couldn't copy images");
create_dir_all(&Path::new("static").join("media")).expect("Couldn't init media directory"); create_dir_all(&Path::new("static").join("media")).expect("Couldn't init media directory");
@@ -97,12 +97,12 @@ fn compile_theme(path: &Path, out_dir: &Path) -> std::io::Result<()> {
.components() .components()
.skip_while(|c| *c != Component::Normal(OsStr::new("themes"))) .skip_while(|c| *c != Component::Normal(OsStr::new("themes")))
.skip(1) .skip(1)
.map(|c| { .filter_map(|c| {
c.as_os_str() c.as_os_str()
.to_str() .to_str()
.unwrap_or_default() .unwrap_or_default()
.split_once('.') .splitn(2, '.')
.map_or(c.as_os_str().to_str().unwrap_or_default(), |x| x.0) .next()
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("-"); .join("-");
@@ -120,13 +120,7 @@ fn compile_theme(path: &Path, out_dir: &Path) -> std::io::Result<()> {
// compile the .scss/.sass file // compile the .scss/.sass file
let mut out = File::create(out.join("theme.css"))?; let mut out = File::create(out.join("theme.css"))?;
out.write_all( out.write_all(
&rsass::compile_scss_path( &rsass::compile_scss_file(path, rsass::OutputStyle::Compressed)
path,
rsass::output::Format {
style: rsass::output::Style::Compressed,
..rsass::output::Format::default()
},
)
.expect("SCSS compilation error"), .expect("SCSS compilation error"),
)?; )?;
@@ -1 +0,0 @@
DROP INDEX medias_index_file_path;
@@ -1 +0,0 @@
CREATE INDEX medias_index_file_path ON medias (file_path);
@@ -1 +0,0 @@
DROP TABLE email_signups;
@@ -1,9 +0,0 @@
CREATE TABLE email_signups (
id SERIAL PRIMARY KEY,
email VARCHAR NOT NULL,
token VARCHAR NOT NULL,
expiration_date TIMESTAMP NOT NULL
);
CREATE INDEX email_signups_token ON email_signups (token);
CREATE UNIQUE INDEX email_signups_token_requests_email ON email_signups (email);
@@ -1 +0,0 @@
DROP INDEX medias_index_file_path;
@@ -1 +0,0 @@
CREATE INDEX medias_index_file_path ON medias (file_path);
@@ -1 +0,0 @@
DROP TABLE email_signups;
@@ -1,9 +0,0 @@
CREATE TABLE email_signups (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
email VARCHAR NOT NULL,
token VARCHAR NOT NULL,
expiration_date TIMESTAMP NOT NULL
);
CREATE INDEX email_signups_token ON email_signups (token);
CREATE UNIQUE INDEX email_signups_token_requests_email ON email_signups (email);
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "plume-api" name = "plume-api"
version = "0.7.1" version = "0.6.1-dev"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018" edition = "2018"
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "plume-cli" name = "plume-cli"
version = "0.7.1" version = "0.6.1-dev"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018" edition = "2018"
@@ -10,7 +10,7 @@ path = "src/main.rs"
[dependencies] [dependencies]
clap = "2.33" clap = "2.33"
dotenv = "0.15" dotenv = "0.14"
rpassword = "5.0.0" rpassword = "5.0.0"
[dependencies.diesel] [dependencies.diesel]
-2
View File
@@ -68,6 +68,4 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
}, },
) )
.expect("Couldn't save instance"); .expect("Couldn't save instance");
Instance::cache_local(conn);
Instance::create_local_instance_user(conn).expect("Couldn't save local instance user");
} }
+1 -1
View File
@@ -25,7 +25,7 @@ fn main() {
e => e.map(|_| ()).unwrap(), e => e.map(|_| ()).unwrap(),
} }
let conn = Conn::establish(CONFIG.database_url.as_str()); let conn = Conn::establish(CONFIG.database_url.as_str());
let _ = conn.as_ref().map(Instance::cache_local); let _ = conn.as_ref().map(|conn| Instance::cache_local(conn));
match matches.subcommand() { match matches.subcommand() {
("instance", Some(args)) => { ("instance", Some(args)) => {
+6 -11
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "plume-common" name = "plume-common"
version = "0.7.1" version = "0.6.1-dev"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018" edition = "2018"
@@ -9,21 +9,21 @@ activitypub = "0.1.1"
activitystreams-derive = "0.1.1" activitystreams-derive = "0.1.1"
activitystreams-traits = "0.1.0" activitystreams-traits = "0.1.0"
array_tool = "1.0" array_tool = "1.0"
base64 = "0.13" base64 = "0.10"
heck = "0.4.0" heck = "0.3.0"
hex = "0.4" hex = "0.3"
hyper = "0.12.33"
openssl = "0.10.22" openssl = "0.10.22"
rocket = "0.4.6" rocket = "0.4.6"
reqwest = { version = "0.9", features = ["socks"] } reqwest = { version = "0.9", features = ["socks"] }
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
serde_json = "1.0.70" serde_json = "1.0"
shrinkwraprs = "0.3.0" shrinkwraprs = "0.3.0"
syntect = "4.5.0" syntect = "4.5.0"
tokio = "0.1.22" tokio = "0.1.22"
regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] } regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] }
tracing = "0.1.22" tracing = "0.1.22"
askama_escape = "0.10.2"
[dependencies.chrono] [dependencies.chrono]
features = ["serde"] features = ["serde"]
@@ -33,8 +33,3 @@ version = "0.4"
default-features = false default-features = false
git = "https://git.joinplu.me/Plume/pulldown-cmark" git = "https://git.joinplu.me/Plume/pulldown-cmark"
branch = "bidi-plume" branch = "bidi-plume"
[dev-dependencies]
once_cell = "1.5.2"
[features]
+22 -165
View File
@@ -1,8 +1,6 @@
use reqwest; use reqwest::header::{HeaderValue, ACCEPT};
use std::fmt::Debug; use std::fmt::Debug;
use super::{request, sign::Signer};
/// Represents an ActivityPub inbox. /// Represents an ActivityPub inbox.
/// ///
/// It routes an incoming Activity through the registered handlers. /// It routes an incoming Activity through the registered handlers.
@@ -12,50 +10,7 @@ use super::{request, sign::Signer};
/// ```rust /// ```rust
/// # extern crate activitypub; /// # extern crate activitypub;
/// # use activitypub::{actor::Person, activity::{Announce, Create}, object::Note}; /// # use activitypub::{actor::Person, activity::{Announce, Create}, object::Note};
/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
/// # use once_cell::sync::Lazy;
/// # use plume_common::activity_pub::inbox::*; /// # use plume_common::activity_pub::inbox::*;
/// # use plume_common::activity_pub::sign::{gen_keypair, Error as SignError, Result as SignResult, Signer};
/// #
/// # static MY_SIGNER: Lazy<MySigner> = Lazy::new(|| MySigner::new());
/// #
/// # struct MySigner {
/// # public_key: String,
/// # private_key: String,
/// # }
/// #
/// # impl MySigner {
/// # fn new() -> Self {
/// # let (pub_key, priv_key) = gen_keypair();
/// # Self {
/// # public_key: String::from_utf8(pub_key).unwrap(),
/// # private_key: String::from_utf8(priv_key).unwrap(),
/// # }
/// # }
/// # }
/// #
/// # impl Signer for MySigner {
/// # fn get_key_id(&self) -> String {
/// # "mysigner".into()
/// # }
/// #
/// # fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> {
/// # let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
/// # .unwrap();
/// # let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
/// # signer.update(to_sign.as_bytes()).unwrap();
/// # signer.sign_to_vec().map_err(|_| SignError())
/// # }
/// #
/// # fn verify(&self, data: &str, signature: &[u8]) -> SignResult<bool> {
/// # let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
/// # .unwrap();
/// # let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
/// # verifier.update(data.as_bytes()).unwrap();
/// # verifier.verify(&signature).map_err(|_| SignError())
/// # }
/// # }
/// #
/// # struct User; /// # struct User;
/// # impl FromId<()> for User { /// # impl FromId<()> for User {
/// # type Error = (); /// # type Error = ();
@@ -68,10 +23,6 @@ use super::{request, sign::Signer};
/// # fn from_activity(_: &(), obj: Person) -> Result<Self, Self::Error> { /// # fn from_activity(_: &(), obj: Person) -> Result<Self, Self::Error> {
/// # Ok(User) /// # Ok(User)
/// # } /// # }
/// #
/// # fn get_sender() -> &'static dyn Signer {
/// # &*MY_SIGNER
/// # }
/// # } /// # }
/// # impl AsActor<&()> for User { /// # impl AsActor<&()> for User {
/// # fn get_inbox_url(&self) -> String { /// # fn get_inbox_url(&self) -> String {
@@ -91,10 +42,6 @@ use super::{request, sign::Signer};
/// # fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> { /// # fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> {
/// # Ok(Message) /// # Ok(Message)
/// # } /// # }
/// #
/// # fn get_sender() -> &'static dyn Signer {
/// # &*MY_SIGNER
/// # }
/// # } /// # }
/// # impl AsObject<User, Create, &()> for Message { /// # impl AsObject<User, Create, &()> for Message {
/// # type Error = (); /// # type Error = ();
@@ -260,7 +207,7 @@ where
}; };
// Handle the activity // Handle the activity
match obj.activity(ctx, actor, act_id) { match obj.activity(ctx, actor, &act_id) {
Ok(res) => Inbox::Handled(res.into()), Ok(res) => Inbox::Handled(res.into()),
Err(e) => Inbox::Failed(e), Err(e) => Inbox::Failed(e),
} }
@@ -364,7 +311,26 @@ pub trait FromId<C>: Sized {
id: &str, id: &str,
proxy: Option<reqwest::Proxy>, proxy: Option<reqwest::Proxy>,
) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> { ) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
request::get(id, Self::get_sender(), proxy) if let Some(proxy) = proxy {
reqwest::ClientBuilder::new().proxy(proxy)
} else {
reqwest::ClientBuilder::new()
}
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()
.map_err(|_| (None, InboxError::DerefError.into()))?
.get(id)
.header(
ACCEPT,
HeaderValue::from_str(
&super::ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)
.map_err(|_| (None, InboxError::DerefError.into()))?,
)
.send()
.map_err(|_| (None, InboxError::DerefError)) .map_err(|_| (None, InboxError::DerefError))
.and_then(|mut r| { .and_then(|mut r| {
let json: serde_json::Value = r let json: serde_json::Value = r
@@ -381,8 +347,6 @@ pub trait FromId<C>: Sized {
/// Tries to find a `Self` with a given ID (`id`), using `ctx` (a database) /// Tries to find a `Self` with a given ID (`id`), using `ctx` (a database)
fn from_db(ctx: &C, id: &str) -> Result<Self, Self::Error>; fn from_db(ctx: &C, id: &str) -> Result<Self, Self::Error>;
fn get_sender() -> &'static dyn Signer;
} }
/// Should be implemented by anything representing an ActivityPub actor. /// Should be implemented by anything representing an ActivityPub actor.
@@ -421,49 +385,6 @@ pub trait AsActor<C> {
/// # extern crate activitypub; /// # extern crate activitypub;
/// # use activitypub::{activity::Create, actor::Person, object::Note}; /// # use activitypub::{activity::Create, actor::Person, object::Note};
/// # use plume_common::activity_pub::inbox::{AsActor, AsObject, FromId}; /// # use plume_common::activity_pub::inbox::{AsActor, AsObject, FromId};
/// # use plume_common::activity_pub::sign::{gen_keypair, Error as SignError, Result as SignResult, Signer};
/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
/// # use once_cell::sync::Lazy;
/// #
/// # static MY_SIGNER: Lazy<MySigner> = Lazy::new(|| MySigner::new());
/// #
/// # struct MySigner {
/// # public_key: String,
/// # private_key: String,
/// # }
/// #
/// # impl MySigner {
/// # fn new() -> Self {
/// # let (pub_key, priv_key) = gen_keypair();
/// # Self {
/// # public_key: String::from_utf8(pub_key).unwrap(),
/// # private_key: String::from_utf8(priv_key).unwrap(),
/// # }
/// # }
/// # }
/// #
/// # impl Signer for MySigner {
/// # fn get_key_id(&self) -> String {
/// # "mysigner".into()
/// # }
/// #
/// # fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> {
/// # let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
/// # .unwrap();
/// # let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
/// # signer.update(to_sign.as_bytes()).unwrap();
/// # signer.sign_to_vec().map_err(|_| SignError())
/// # }
/// #
/// # fn verify(&self, data: &str, signature: &[u8]) -> SignResult<bool> {
/// # let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
/// # .unwrap();
/// # let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
/// # verifier.update(data.as_bytes()).unwrap();
/// # verifier.verify(&signature).map_err(|_| SignError())
/// # }
/// # }
/// #
/// # struct Account; /// # struct Account;
/// # impl FromId<()> for Account { /// # impl FromId<()> for Account {
/// # type Error = (); /// # type Error = ();
@@ -476,10 +397,6 @@ pub trait AsActor<C> {
/// # fn from_activity(_: &(), obj: Person) -> Result<Self, Self::Error> { /// # fn from_activity(_: &(), obj: Person) -> Result<Self, Self::Error> {
/// # Ok(Account) /// # Ok(Account)
/// # } /// # }
/// #
/// # fn get_sender() -> &'static dyn Signer {
/// # &*MY_SIGNER
/// # }
/// # } /// # }
/// # impl AsActor<()> for Account { /// # impl AsActor<()> for Account {
/// # fn get_inbox_url(&self) -> String { /// # fn get_inbox_url(&self) -> String {
@@ -503,10 +420,6 @@ pub trait AsActor<C> {
/// fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> { /// fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> {
/// Ok(Message { text: obj.object_props.content_string().map_err(|_| ())? }) /// Ok(Message { text: obj.object_props.content_string().map_err(|_| ())? })
/// } /// }
///
/// fn get_sender() -> &'static dyn Signer {
/// &*MY_SIGNER
/// }
/// } /// }
/// ///
/// impl AsObject<Account, Create, ()> for Message { /// impl AsObject<Account, Create, ()> for Message {
@@ -546,51 +459,7 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::activity_pub::sign::{
gen_keypair, Error as SignError, Result as SignResult, Signer,
};
use activitypub::{activity::*, actor::Person, object::Note}; use activitypub::{activity::*, actor::Person, object::Note};
use once_cell::sync::Lazy;
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
static MY_SIGNER: Lazy<MySigner> = Lazy::new(|| MySigner::new());
struct MySigner {
public_key: String,
private_key: String,
}
impl MySigner {
fn new() -> Self {
let (pub_key, priv_key) = gen_keypair();
Self {
public_key: String::from_utf8(pub_key).unwrap(),
private_key: String::from_utf8(priv_key).unwrap(),
}
}
}
impl Signer for MySigner {
fn get_key_id(&self) -> String {
"mysigner".into()
}
fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> {
let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
.unwrap();
let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
signer.update(to_sign.as_bytes()).unwrap();
signer.sign_to_vec().map_err(|_| SignError())
}
fn verify(&self, data: &str, signature: &[u8]) -> SignResult<bool> {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
.unwrap();
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
verifier.update(data.as_bytes()).unwrap();
verifier.verify(&signature).map_err(|_| SignError())
}
}
struct MyActor; struct MyActor;
impl FromId<()> for MyActor { impl FromId<()> for MyActor {
@@ -604,10 +473,6 @@ mod tests {
fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> { fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> {
Ok(MyActor) Ok(MyActor)
} }
fn get_sender() -> &'static dyn Signer {
&*MY_SIGNER
}
} }
impl AsActor<&()> for MyActor { impl AsActor<&()> for MyActor {
@@ -632,10 +497,6 @@ mod tests {
fn from_activity(_: &(), _obj: Note) -> Result<Self, Self::Error> { fn from_activity(_: &(), _obj: Note) -> Result<Self, Self::Error> {
Ok(MyObject) Ok(MyObject)
} }
fn get_sender() -> &'static dyn Signer {
&*MY_SIGNER
}
} }
impl AsObject<MyActor, Create, &()> for MyObject { impl AsObject<MyActor, Create, &()> for MyObject {
type Error = (); type Error = ();
@@ -740,10 +601,6 @@ mod tests {
fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> { fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> {
Err(()) Err(())
} }
fn get_sender() -> &'static dyn Signer {
&*MY_SIGNER
}
} }
impl AsActor<&()> for FailingActor { impl AsActor<&()> for FailingActor {
fn get_inbox_url(&self) -> String { fn get_inbox_url(&self) -> String {
+1 -1
View File
@@ -145,7 +145,7 @@ where
warn!("Inbox doesn't have host: {:?}", &inbox); warn!("Inbox doesn't have host: {:?}", &inbox);
continue; continue;
}; };
let host_header_value = HeaderValue::from_str(url.host_str().expect("Unreachable")); let host_header_value = HeaderValue::from_str(&url.host_str().expect("Unreachable"));
if host_header_value.is_err() { if host_header_value.is_err() {
warn!("Header value is invalid: {:?}", url.host_str()); warn!("Header value is invalid: {:?}", url.host_str());
continue; continue;
+9 -55
View File
@@ -1,11 +1,6 @@
use chrono::{offset::Utc, DateTime}; use chrono::{offset::Utc, DateTime};
use openssl::hash::{Hasher, MessageDigest}; use openssl::hash::{Hasher, MessageDigest};
use reqwest::{ use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, DATE, USER_AGENT};
header::{
HeaderMap, HeaderValue, InvalidHeaderValue, ACCEPT, CONTENT_TYPE, DATE, HOST, USER_AGENT,
},
ClientBuilder, Proxy, Response, Url, UrlError,
};
use std::ops::Deref; use std::ops::Deref;
use std::time::SystemTime; use std::time::SystemTime;
use tracing::warn; use tracing::warn;
@@ -18,24 +13,6 @@ const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION"));
#[derive(Debug)] #[derive(Debug)]
pub struct Error(); pub struct Error();
impl From<UrlError> for Error {
fn from(_err: UrlError) -> Self {
Error()
}
}
impl From<InvalidHeaderValue> for Error {
fn from(_err: InvalidHeaderValue) -> Self {
Error()
}
}
impl From<reqwest::Error> for Error {
fn from(_err: reqwest::Error) -> Self {
Error()
}
}
pub struct Digest(String); pub struct Digest(String);
impl Digest { impl Digest {
@@ -141,8 +118,8 @@ type Path<'a> = &'a str;
type Query<'a> = &'a str; type Query<'a> = &'a str;
type RequestTarget<'a> = (Method<'a>, Path<'a>, Option<Query<'a>>); type RequestTarget<'a> = (Method<'a>, Path<'a>, Option<Query<'a>>);
pub fn signature( pub fn signature<S: Signer>(
signer: &dyn Signer, signer: &S,
headers: &HeaderMap, headers: &HeaderMap,
request_target: RequestTarget, request_target: RequestTarget,
) -> Result<HeaderValue, Error> { ) -> Result<HeaderValue, Error> {
@@ -187,35 +164,10 @@ pub fn signature(
)).map_err(|_| Error()) )).map_err(|_| Error())
} }
pub fn get(url_str: &str, sender: &dyn Signer, proxy: Option<Proxy>) -> Result<Response, Error> {
let mut headers = headers();
let url = Url::parse(url_str)?;
if !url.has_host() {
return Err(Error());
}
let host_header_value = HeaderValue::from_str(url.host_str().expect("Unreachable"))?;
headers.insert(HOST, host_header_value);
if let Some(proxy) = proxy {
ClientBuilder::new().proxy(proxy)
} else {
ClientBuilder::new()
}
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()?
.get(url_str)
.headers(headers.clone())
.header(
"Signature",
signature(sender, &headers, ("get", url.path(), url.query()))?,
)
.send()
.map_err(|_| Error())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::signature; use super::{signature, Error};
use crate::activity_pub::sign::{gen_keypair, Error, Result, Signer}; use crate::activity_pub::sign::{gen_keypair, Signer};
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa}; use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
use reqwest::header::HeaderMap; use reqwest::header::HeaderMap;
@@ -235,11 +187,13 @@ mod tests {
} }
impl Signer for MySigner { impl Signer for MySigner {
type Error = Error;
fn get_key_id(&self) -> String { fn get_key_id(&self) -> String {
"mysigner".into() "mysigner".into()
} }
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> { fn sign(&self, to_sign: &str) -> Result<Vec<u8>, Self::Error> {
let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap()) let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
.unwrap(); .unwrap();
let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap(); let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
@@ -247,7 +201,7 @@ mod tests {
signer.sign_to_vec().map_err(|_| Error()) signer.sign_to_vec().map_err(|_| Error())
} }
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> { fn verify(&self, data: &str, signature: &[u8]) -> Result<bool, Self::Error> {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap()) let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
.unwrap(); .unwrap();
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap(); let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
+7 -12
View File
@@ -19,25 +19,20 @@ pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
#[derive(Debug)] #[derive(Debug)]
pub struct Error(); pub struct Error();
pub type Result<T> = std::result::Result<T, Error>;
impl From<openssl::error::ErrorStack> for Error {
fn from(_: openssl::error::ErrorStack) -> Self {
Self()
}
}
pub trait Signer { pub trait Signer {
type Error;
fn get_key_id(&self) -> String; fn get_key_id(&self) -> String;
/// Sign some data with the signer keypair /// Sign some data with the signer keypair
fn sign(&self, to_sign: &str) -> Result<Vec<u8>>; fn sign(&self, to_sign: &str) -> Result<Vec<u8>, Self::Error>;
/// Verify if the signature is valid /// Verify if the signature is valid
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool>; fn verify(&self, data: &str, signature: &[u8]) -> Result<bool, Self::Error>;
} }
pub trait Signable { pub trait Signable {
fn sign<T>(&mut self, creator: &T) -> Result<&mut Self> fn sign<T>(&mut self, creator: &T) -> Result<&mut Self, Error>
where where
T: Signer; T: Signer;
fn verify<T>(self, creator: &T) -> bool fn verify<T>(self, creator: &T) -> bool
@@ -51,7 +46,7 @@ pub trait Signable {
} }
impl Signable for serde_json::Value { impl Signable for serde_json::Value {
fn sign<T: Signer>(&mut self, creator: &T) -> Result<&mut serde_json::Value> { fn sign<T: Signer>(&mut self, creator: &T) -> Result<&mut serde_json::Value, Error> {
let creation_date = Utc::now().to_rfc3339(); let creation_date = Utc::now().to_rfc3339();
let mut options = json!({ let mut options = json!({
"type": "RsaSignature2017", "type": "RsaSignature2017",
@@ -187,7 +182,7 @@ pub fn verify_http_headers<S: Signer + ::std::fmt::Debug>(
} }
let digest = all_headers.get_one("digest").unwrap_or(""); let digest = all_headers.get_one("digest").unwrap_or("");
let digest = request::Digest::from_header(digest); let digest = request::Digest::from_header(digest);
if !digest.map(|d| d.verify_header(data)).unwrap_or(false) { if !digest.map(|d| d.verify_header(&data)).unwrap_or(false) {
// signature was valid, but body content does not match its digest // signature was valid, but body content does not match its digest
return SignatureValidity::Invalid; return SignatureValidity::Invalid;
} }
+20 -74
View File
@@ -1,8 +1,11 @@
use heck::ToUpperCamelCase; use heck::CamelCase;
use openssl::rand::rand_bytes; use openssl::rand::rand_bytes;
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag}; use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag};
use regex_syntax::is_word_character; use regex_syntax::is_word_character;
use rocket::http::uri::Uri; use rocket::{
http::uri::Uri,
response::{Flash, Redirect},
};
use std::collections::HashSet; use std::collections::HashSet;
use syntect::html::{ClassStyle, ClassedHTMLGenerator}; use syntect::html::{ClassStyle, ClassedHTMLGenerator};
use syntect::parsing::SyntaxSet; use syntect::parsing::SyntaxSet;
@@ -18,63 +21,23 @@ pub fn random_hex() -> String {
/// Remove non alphanumeric characters and CamelCase a string /// Remove non alphanumeric characters and CamelCase a string
pub fn make_actor_id(name: &str) -> String { pub fn make_actor_id(name: &str) -> String {
name.to_upper_camel_case() name.to_camel_case()
.chars() .chars()
.filter(|c| c.is_alphanumeric()) .filter(|c| c.is_alphanumeric())
.collect() .collect()
} }
/** /**
* Percent-encode characters which are not allowed in IRI path segments. * Redirects to the login page with a given message.
* *
* Intended to be used for generating Post ap_url. * Note that the message should be translated before passed to this function.
*/ */
pub fn iri_percent_encode_seg(segment: &str) -> String { pub fn requires_login<T: Into<Uri<'static>>>(message: &str, url: T) -> Flash<Redirect> {
segment.chars().map(iri_percent_encode_seg_char).collect() Flash::new(
} Redirect::to(format!("/login?m={}", Uri::percent_encode(message))),
"callback",
pub fn iri_percent_encode_seg_char(c: char) -> String { url.into().to_string(),
if c.is_alphanumeric() { )
c.to_string()
} else {
match c {
'-'
| '.'
| '_'
| '~'
| '\u{A0}'..='\u{D7FF}'
| '\u{20000}'..='\u{2FFFD}'
| '\u{30000}'..='\u{3FFFD}'
| '\u{40000}'..='\u{4FFFD}'
| '\u{50000}'..='\u{5FFFD}'
| '\u{60000}'..='\u{6FFFD}'
| '\u{70000}'..='\u{7FFFD}'
| '\u{80000}'..='\u{8FFFD}'
| '\u{90000}'..='\u{9FFFD}'
| '\u{A0000}'..='\u{AFFFD}'
| '\u{B0000}'..='\u{BFFFD}'
| '\u{C0000}'..='\u{CFFFD}'
| '\u{D0000}'..='\u{DFFFD}'
| '\u{E0000}'..='\u{EFFFD}'
| '!'
| '$'
| '&'
| '\''
| '('
| ')'
| '*'
| '+'
| ','
| ';'
| '='
| ':'
| '@' => c.to_string(),
_ => {
let s = c.to_string();
Uri::percent_encode(&s).to_string()
}
}
}
} }
#[derive(Debug)] #[derive(Debug)]
@@ -125,13 +88,13 @@ fn highlight_code<'a>(
unreachable!(); unreachable!();
}; };
let syntax_set = SyntaxSet::load_defaults_newlines(); let syntax_set = SyntaxSet::load_defaults_newlines();
let syntax = syntax_set.find_syntax_by_token(lang).unwrap_or_else(|| { let syntax = syntax_set.find_syntax_by_token(&lang).unwrap_or_else(|| {
syntax_set syntax_set
.find_syntax_by_name(lang) .find_syntax_by_name(&lang)
.unwrap_or_else(|| syntax_set.find_syntax_plain_text()) .unwrap_or_else(|| syntax_set.find_syntax_plain_text())
}); });
let mut html = ClassedHTMLGenerator::new_with_class_style( let mut html = ClassedHTMLGenerator::new_with_class_style(
syntax, &syntax,
&syntax_set, &syntax_set,
ClassStyle::Spaced, ClassStyle::Spaced,
); );
@@ -318,15 +281,16 @@ pub fn md_to_html<'a>(
text_acc.push(c) text_acc.push(c)
} }
let mention = text_acc; let mention = text_acc;
let short_mention = mention.splitn(1, '@').next().unwrap_or("");
let link = Tag::Link( let link = Tag::Link(
LinkType::Inline, LinkType::Inline,
format!("{}@/{}/", base_url, &mention).into(), format!("{}@/{}/", base_url, &mention).into(),
mention.clone().into(), short_mention.to_owned().into(),
); );
mentions.push(mention.clone()); mentions.push(mention.clone());
events.push(Event::Start(link.clone())); events.push(Event::Start(link.clone()));
events.push(Event::Text(format!("@{}", &mention).into())); events.push(Event::Text(format!("@{}", &short_mention).into()));
events.push(Event::End(link)); events.push(Event::End(link));
( (
@@ -450,10 +414,6 @@ pub fn md_to_html<'a>(
(buf, mentions.collect(), hashtags.collect()) (buf, mentions.collect(), hashtags.collect())
} }
pub fn escape(string: &str) -> askama_escape::Escaped<askama_escape::Html> {
askama_escape::escape(string, askama_escape::Html)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -516,20 +476,6 @@ mod tests {
} }
} }
#[test]
fn test_iri_percent_encode_seg() {
assert_eq!(
&iri_percent_encode_seg("including whitespace"),
"including%20whitespace"
);
assert_eq!(&iri_percent_encode_seg("%20"), "%2520");
assert_eq!(&iri_percent_encode_seg("é"), "é");
assert_eq!(
&iri_percent_encode_seg("空白入り 日本語"),
"空白入り%20日本語"
);
}
#[test] #[test]
fn test_inline() { fn test_inline() {
assert_eq!( assert_eq!(
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "plume-front" name = "plume-front"
version = "0.7.1" version = "0.6.1-dev"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018" edition = "2018"
+14 -20
View File
@@ -54,6 +54,11 @@ pub enum EditorError {
DOMError, DOMError,
} }
impl From<std::option::NoneError> for EditorError {
fn from(_: std::option::NoneError) -> Self {
EditorError::NoneError
}
}
const AUTOSAVE_DEBOUNCE_TIME: i32 = 5000; const AUTOSAVE_DEBOUNCE_TIME: i32 = 5000;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct AutosaveInformation { struct AutosaveInformation {
@@ -193,7 +198,7 @@ fn clear_autosave() {
.unwrap() .unwrap()
.remove_item(&get_autosave_id()) .remove_item(&get_autosave_id())
.unwrap(); .unwrap();
console::log_1(&format!("Saved to {}", &get_autosave_id()).into()); console::log_1(&&format!("Saved to {}", &get_autosave_id()).into());
} }
type TimeoutHandle = i32; type TimeoutHandle = i32;
lazy_static! { lazy_static! {
@@ -361,9 +366,7 @@ fn init_editor() -> Result<(), EditorError> {
return Ok(()); return Ok(());
} }
let old_ed = old_ed.unwrap(); let old_ed = old_ed.unwrap();
let old_title = document() let old_title = document().get_element_by_id("plume-editor-title")?;
.get_element_by_id("plume-editor-title")
.ok_or(EditorError::NoneError)?;
old_ed old_ed
.dyn_ref::<HtmlElement>() .dyn_ref::<HtmlElement>()
.unwrap() .unwrap()
@@ -431,8 +434,7 @@ fn init_editor() -> Result<(), EditorError> {
bg.class_list().add_1("show").unwrap(); bg.class_list().add_1("show").unwrap();
})) as Box<dyn FnMut(MouseEvent)>); })) as Box<dyn FnMut(MouseEvent)>);
document() document()
.get_element_by_id("publish") .get_element_by_id("publish")?
.ok_or(EditorError::NoneError)?
.add_event_listener_with_callback("click", show_popup.as_ref().unchecked_ref()) .add_event_listener_with_callback("click", show_popup.as_ref().unchecked_ref())
.map_err(|_| EditorError::DOMError)?; .map_err(|_| EditorError::DOMError)?;
show_popup.forget(); show_popup.forget();
@@ -526,14 +528,8 @@ fn init_popup(
cover_label cover_label
.set_attribute("for", "cover") .set_attribute("for", "cover")
.map_err(|_| EditorError::DOMError)?; .map_err(|_| EditorError::DOMError)?;
let cover = document let cover = document.get_element_by_id("cover")?;
.get_element_by_id("cover") cover.parent_element()?.remove_child(&cover).ok();
.ok_or(EditorError::NoneError)?;
cover
.parent_element()
.ok_or(EditorError::NoneError)?
.remove_child(&cover)
.ok();
popup popup
.append_child(&cover_label) .append_child(&cover_label)
.map_err(|_| EditorError::DOMError)?; .map_err(|_| EditorError::DOMError)?;
@@ -558,7 +554,7 @@ fn init_popup(
draft.set_checked(draft_checkbox.checked()); draft.set_checked(draft_checkbox.checked());
draft_label draft_label
.append_child(draft) .append_child(&draft)
.map_err(|_| EditorError::DOMError)?; .map_err(|_| EditorError::DOMError)?;
draft_label draft_label
.append_child(&document.create_text_node(&i18n!(CATALOG, "This is a draft"))) .append_child(&document.create_text_node(&i18n!(CATALOG, "This is a draft")))
@@ -624,12 +620,11 @@ fn init_popup(
.map_err(|_| EditorError::DOMError)?; .map_err(|_| EditorError::DOMError)?;
callback.forget(); callback.forget();
popup popup
.append_child(button) .append_child(&button)
.map_err(|_| EditorError::DOMError)?; .map_err(|_| EditorError::DOMError)?;
document document
.body() .body()?
.ok_or(EditorError::NoneError)?
.append_child(&popup) .append_child(&popup)
.map_err(|_| EditorError::DOMError)?; .map_err(|_| EditorError::DOMError)?;
Ok(popup) Ok(popup)
@@ -646,8 +641,7 @@ fn init_popup_bg() -> Result<Element, EditorError> {
.map_err(|_| EditorError::DOMError)?; .map_err(|_| EditorError::DOMError)?;
document() document()
.body() .body()?
.ok_or(EditorError::NoneError)?
.append_child(&bg) .append_child(&bg)
.map_err(|_| EditorError::DOMError)?; .map_err(|_| EditorError::DOMError)?;
let callback = Closure::wrap(Box::new(|_| close_popup()) as Box<dyn FnMut(MouseEvent)>); let callback = Closure::wrap(Box::new(|_| close_popup()) as Box<dyn FnMut(MouseEvent)>);
+3 -3
View File
@@ -1,5 +1,5 @@
#![recursion_limit = "128"] #![recursion_limit = "128"]
#![feature(decl_macro, proc_macro_hygiene)] #![feature(decl_macro, proc_macro_hygiene, try_trait)]
#[macro_use] #[macro_use]
extern crate gettext_macros; extern crate gettext_macros;
@@ -61,7 +61,7 @@ lazy_static! {
static ref CATALOG: gettext::Catalog = { static ref CATALOG: gettext::Catalog = {
let catalogs = include_i18n!(); let catalogs = include_i18n!();
let lang = window().unwrap().navigator().language().unwrap(); let lang = window().unwrap().navigator().language().unwrap();
let lang = lang.split_once('-').map_or("en", |x| x.0); let lang = lang.splitn(2, '-').next().unwrap_or("en");
let english_position = catalogs let english_position = catalogs
.iter() .iter()
@@ -85,7 +85,7 @@ pub fn main() -> Result<(), JsValue> {
menu(); menu();
search(); search();
editor::init() editor::init()
.map_err(|e| console::error_1(&format!("Editor error: {:?}", e).into())) .map_err(|e| console::error_1(&&format!("Editor error: {:?}", e).into()))
.ok(); .ok();
Ok(()) Ok(())
} }
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "plume-macro" name = "plume-macro"
version = "0.7.1" version = "0.6.1-dev"
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"
+2 -2
View File
@@ -58,7 +58,7 @@ pub fn import_migrations(input: TokenStream) -> TokenStream {
(name, up_sql, down_sql) (name, up_sql, down_sql)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let migrations_name = migrations.iter().map(|m| &m.0); let migrations_name = migrations.iter().map(|m| &m.0).collect::<Vec<_>>();
let migrations_up = migrations let migrations_up = migrations
.iter() .iter()
.map(|m| m.1.as_str()) .map(|m| m.1.as_str())
@@ -103,7 +103,7 @@ fn file_to_migration(file: &str) -> TokenStream2 {
acc.push('\n'); acc.push('\n');
} }
} else if let Some(acc_str) = line.strip_prefix("--#!") { } else if let Some(acc_str) = line.strip_prefix("--#!") {
acc.push_str(acc_str); acc.push_str(&acc_str);
acc.push('\n'); acc.push('\n');
} else if line.starts_with("--") { } else if line.starts_with("--") {
continue; continue;
+11 -11
View File
@@ -1,17 +1,19 @@
[package] [package]
name = "plume-models" name = "plume-models"
version = "0.7.1" version = "0.6.1-dev"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
activitypub = "0.1.1" activitypub = "0.1.1"
ammonia = "3.1.2" ammonia = "2.1.1"
bcrypt = "0.10.1" askama_escape = "0.1"
guid-create = "0.2" bcrypt = "0.5"
itertools = "0.10.3" guid-create = "0.1"
heck = "0.3.0"
itertools = "0.8.0"
lazy_static = "1.0" lazy_static = "1.0"
ldap3 = "0.9.3" 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.6" rocket = "0.4.6"
@@ -20,21 +22,19 @@ reqwest = "0.9"
scheduled-thread-pool = "0.2.2" scheduled-thread-pool = "0.2.2"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
serde_json = "1.0.70" serde_json = "1.0"
tantivy = "0.13.3" tantivy = "0.13.3"
url = "2.1" url = "2.1"
walkdir = "2.2" walkdir = "2.2"
webfinger = "0.4.1" webfinger = "0.4.1"
whatlang = "0.13.0" whatlang = "0.11.1"
shrinkwraprs = "0.3.0" 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.7.1", optional = true } lindera-tantivy = { version = "0.7.1", optional = true }
tracing = "0.1.22" tracing = "0.1.22"
riker = "0.4.2" riker = "0.4.2"
once_cell = "1.5.2" once_cell = "1.5.2"
lettre = "0.9.6"
native-tls = "0.2.8"
[dependencies.chrono] [dependencies.chrono]
features = ["serde"] features = ["serde"]
+2 -6
View File
@@ -86,15 +86,11 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApiToken {
} }
let mut parsed_header = headers[0].split(' '); let mut parsed_header = headers[0].split(' ');
let auth_type = parsed_header let auth_type = parsed_header.next().map_or_else(
.next()
.map_or_else::<rocket::Outcome<&str, _, ()>, _, _>(
|| Outcome::Failure((Status::BadRequest, TokenError::NoType)), || Outcome::Failure((Status::BadRequest, TokenError::NoType)),
Outcome::Success, Outcome::Success,
)?; )?;
let val = parsed_header let val = parsed_header.next().map_or_else(
.next()
.map_or_else::<rocket::Outcome<&str, _, ()>, _, _>(
|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)), || Outcome::Failure((Status::BadRequest, TokenError::NoValue)),
Outcome::Success, Outcome::Success,
)?; )?;
+1 -1
View File
@@ -28,7 +28,7 @@ impl BlocklistedEmail {
pub fn delete_entries(conn: &Connection, ids: Vec<i32>) -> Result<bool> { pub fn delete_entries(conn: &Connection, ids: Vec<i32>) -> Result<bool> {
use diesel::delete; use diesel::delete;
for i in ids { for i in ids {
let be: BlocklistedEmail = BlocklistedEmail::find_by_id(conn, i)?; let be: BlocklistedEmail = BlocklistedEmail::find_by_id(&conn, i)?;
delete(&be).execute(conn)?; delete(&be).execute(conn)?;
} }
Ok(true) Ok(true)
+13 -26
View File
@@ -149,15 +149,7 @@ impl Blog {
.into_iter() .into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json"))) .find(|l| l.mime_type == Some(String::from("application/activity+json")))
.ok_or(Error::Webfinger) .ok_or(Error::Webfinger)
.and_then(|l| { .and_then(|l| Blog::from_id(conn, &l.href?, None, CONFIG.proxy()).map_err(|(_, e)| e))
Blog::from_id(
conn,
&l.href.ok_or(Error::MissingApProperty)?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)
})
} }
pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> { pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> {
@@ -244,7 +236,7 @@ impl Blog {
(min, max): (i32, i32), (min, max): (i32, i32),
) -> Result<ActivityStream<OrderedCollectionPage>> { ) -> Result<ActivityStream<OrderedCollectionPage>> {
let mut coll = OrderedCollectionPage::default(); let mut coll = OrderedCollectionPage::default();
let acts = self.get_activity_page(conn, (min, max)); let acts = self.get_activity_page(&conn, (min, max));
//This still doesn't do anything because the outbox //This still doesn't do anything because the outbox
//doesn't do anything yet //doesn't do anything yet
coll.collection_page_props.set_next_link(Id::new(&format!( coll.collection_page_props.set_next_link(Id::new(&format!(
@@ -273,10 +265,7 @@ impl Blog {
pub fn get_keypair(&self) -> Result<PKey<Private>> { pub fn get_keypair(&self) -> Result<PKey<Private>> {
PKey::from_rsa(Rsa::private_key_from_pem( PKey::from_rsa(Rsa::private_key_from_pem(
self.private_key self.private_key.clone()?.as_ref(),
.clone()
.ok_or(Error::MissingApProperty)?
.as_ref(),
)?) )?)
.map_err(Error::from) .map_err(Error::from)
} }
@@ -329,7 +318,7 @@ impl Blog {
} }
pub fn delete(&self, conn: &Connection) -> Result<()> { pub fn delete(&self, conn: &Connection) -> Result<()> {
for post in Post::get_for_blog(conn, self)? { for post in Post::get_for_blog(conn, &self)? {
post.delete(conn)?; post.delete(conn)?;
} }
diesel::delete(self) diesel::delete(self)
@@ -350,12 +339,12 @@ impl FromId<DbConn> for Blog {
type Object = CustomGroup; type Object = CustomGroup;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> { fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
Self::find_by_ap_url(conn, id) Self::find_by_ap_url(&conn, id)
} }
fn from_activity(conn: &DbConn, acct: CustomGroup) -> Result<Self> { fn from_activity(conn: &DbConn, acct: CustomGroup) -> Result<Self> {
let url = Url::parse(&acct.object.object_props.id_string()?)?; let url = Url::parse(&acct.object.object_props.id_string()?)?;
let inst = url.host_str().ok_or(Error::Url)?; let inst = url.host_str()?;
let instance = Instance::find_by_domain(conn, inst).or_else(|_| { let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
Instance::insert( Instance::insert(
conn, conn,
@@ -443,10 +432,6 @@ impl FromId<DbConn> for Blog {
}, },
) )
} }
fn get_sender() -> &'static dyn sign::Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
} }
impl AsActor<&PlumeRocket> for Blog { impl AsActor<&PlumeRocket> for Blog {
@@ -466,22 +451,24 @@ impl AsActor<&PlumeRocket> for Blog {
} }
impl sign::Signer for Blog { impl sign::Signer for Blog {
type Error = Error;
fn get_key_id(&self) -> String { fn get_key_id(&self) -> String {
format!("{}#main-key", self.ap_url) format!("{}#main-key", self.ap_url)
} }
fn sign(&self, to_sign: &str) -> sign::Result<Vec<u8>> { fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
let key = self.get_keypair().map_err(|_| sign::Error())?; let key = self.get_keypair()?;
let mut signer = Signer::new(MessageDigest::sha256(), &key)?; let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
signer.update(to_sign.as_bytes())?; signer.update(to_sign.as_bytes())?;
signer.sign_to_vec().map_err(sign::Error::from) signer.sign_to_vec().map_err(Error::from)
} }
fn verify(&self, data: &str, signature: &[u8]) -> sign::Result<bool> { fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?; let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?; let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?;
verifier.update(data.as_bytes())?; verifier.update(data.as_bytes())?;
verifier.verify(signature).map_err(sign::Error::from) verifier.verify(&signature).map_err(Error::from)
} }
} }
+10 -23
View File
@@ -21,7 +21,6 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use plume_common::{ use plume_common::{
activity_pub::{ activity_pub::{
inbox::{AsActor, AsObject, FromId}, inbox::{AsActor, AsObject, FromId},
sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY, Id, IntoId, PUBLIC_VISIBILITY,
}, },
utils, utils,
@@ -142,20 +141,18 @@ impl Comment {
} }
pub fn create_activity(&self, conn: &DbConn) -> Result<Create> { pub fn create_activity(&self, conn: &DbConn) -> Result<Create> {
let author = User::get(conn, self.author_id)?; let author = User::get(&conn, self.author_id)?;
let note = self.to_activity(conn)?; let note = self.to_activity(conn)?;
let mut act = Create::default(); let mut act = Create::default();
act.create_props.set_actor_link(author.into_id())?; act.create_props.set_actor_link(author.into_id())?;
act.create_props.set_object_object(note.clone())?; act.create_props.set_object_object(note.clone())?;
act.object_props.set_id_string(format!( act.object_props
"{}/activity", .set_id_string(format!("{}/activity", self.ap_url.clone()?,))?;
self.ap_url.clone().ok_or(Error::MissingApProperty)?,
))?;
act.object_props act.object_props
.set_to_link_vec(note.object_props.to_link_vec::<Id>()?)?; .set_to_link_vec(note.object_props.to_link_vec::<Id>()?)?;
act.object_props act.object_props
.set_cc_link_vec(vec![Id::new(self.get_author(conn)?.followers_endpoint)])?; .set_cc_link_vec(vec![Id::new(self.get_author(&conn)?.followers_endpoint)])?;
Ok(act) Ok(act)
} }
@@ -185,9 +182,7 @@ impl Comment {
.set_actor_link(self.get_author(conn)?.into_id())?; .set_actor_link(self.get_author(conn)?.into_id())?;
let mut tombstone = Tombstone::default(); let mut tombstone = Tombstone::default();
tombstone tombstone.object_props.set_id_string(self.ap_url.clone()?)?;
.object_props
.set_id_string(self.ap_url.clone().ok_or(Error::MissingApProperty)?)?;
act.delete_props.set_object_object(tombstone)?; act.delete_props.set_object_object(tombstone)?;
act.object_props act.object_props
@@ -209,13 +204,7 @@ impl FromId<DbConn> for Comment {
fn from_activity(conn: &DbConn, note: Note) -> Result<Self> { fn from_activity(conn: &DbConn, note: Note) -> Result<Self> {
let comm = { let comm = {
let previous_url = note let previous_url = note.object_props.in_reply_to.as_ref()?.as_str()?;
.object_props
.in_reply_to
.as_ref()
.ok_or(Error::MissingApProperty)?
.as_str()
.ok_or(Error::MissingApProperty)?;
let previous_comment = Comment::find_by_ap_url(conn, previous_url); let previous_comment = Comment::find_by_ap_url(conn, previous_url);
let is_public = |v: &Option<serde_json::Value>| match v let is_public = |v: &Option<serde_json::Value>| match v
@@ -266,7 +255,9 @@ impl FromId<DbConn> for Comment {
.and_then(|m| { .and_then(|m| {
let author = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0]; let author = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0];
let not_author = m.link_props.href_string()? != author.ap_url.clone(); let not_author = m.link_props.href_string()? != author.ap_url.clone();
Mention::from_activity(conn, &m, comm.id, false, not_author) Ok(Mention::from_activity(
conn, &m, comm.id, false, not_author,
)?)
}) })
.ok(); .ok();
} }
@@ -329,10 +320,6 @@ impl FromId<DbConn> for Comment {
comm.notify(conn)?; comm.notify(conn)?;
Ok(comm) Ok(comm)
} }
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
} }
impl AsObject<User, Create, &DbConn> for Comment { impl AsObject<User, Create, &DbConn> for Comment {
@@ -361,7 +348,7 @@ impl AsObject<User, Delete, &DbConn> for Comment {
m.delete(conn)?; m.delete(conn)?;
} }
for n in Notification::find_for_comment(conn, &self)? { for n in Notification::find_for_comment(&conn, &self)? {
n.delete(&**conn)?; n.delete(&**conn)?;
} }
+26 -45
View File
@@ -1,6 +1,4 @@
use crate::search::TokenizerKind as SearchTokenizer; use crate::search::TokenizerKind as SearchTokenizer;
use crate::signups::Strategy as SignupStrategy;
use crate::smtp::{SMTP_PORT, SUBMISSIONS_PORT, SUBMISSION_PORT};
use rocket::config::Limits; use rocket::config::Limits;
use rocket::Config as RocketConfig; use rocket::Config as RocketConfig;
use std::collections::HashSet; use std::collections::HashSet;
@@ -17,14 +15,12 @@ pub struct Config {
pub db_name: &'static str, pub db_name: &'static str,
pub db_max_size: Option<u32>, pub db_max_size: Option<u32>,
pub db_min_idle: Option<u32>, pub db_min_idle: Option<u32>,
pub signup: SignupStrategy,
pub search_index: String, pub search_index: String,
pub search_tokenizers: SearchTokenizerConfig, pub search_tokenizers: SearchTokenizerConfig,
pub rocket: Result<RocketConfig, InvalidRocketConfig>, pub rocket: Result<RocketConfig, RocketError>,
pub logo: LogoConfig, pub logo: LogoConfig,
pub default_theme: String, pub default_theme: String,
pub media_directory: String, pub media_directory: String,
pub mail: Option<MailConfig>,
pub ldap: Option<LdapConfig>, pub ldap: Option<LdapConfig>,
pub proxy: Option<ProxyConfig>, pub proxy: Option<ProxyConfig>,
} }
@@ -35,21 +31,21 @@ impl Config {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum InvalidRocketConfig { pub enum RocketError {
Env, InvalidEnv,
Address, InvalidAddress,
SecretKey, InvalidSecretKey,
} }
fn get_rocket_config() -> Result<RocketConfig, InvalidRocketConfig> { fn get_rocket_config() -> Result<RocketConfig, RocketError> {
let mut c = RocketConfig::active().map_err(|_| InvalidRocketConfig::Env)?; let mut c = RocketConfig::active().map_err(|_| RocketError::InvalidEnv)?;
let address = var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned()); let address = var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned());
let port = var("ROCKET_PORT") let port = var("ROCKET_PORT")
.ok() .ok()
.map(|s| s.parse::<u16>().unwrap()) .map(|s| s.parse::<u16>().unwrap())
.unwrap_or(7878); .unwrap_or(7878);
let secret_key = var("ROCKET_SECRET_KEY").map_err(|_| InvalidRocketConfig::SecretKey)?; let secret_key = var("ROCKET_SECRET_KEY").map_err(|_| RocketError::InvalidSecretKey)?;
let form_size = var("FORM_SIZE") let form_size = var("FORM_SIZE")
.unwrap_or_else(|_| "128".to_owned()) .unwrap_or_else(|_| "128".to_owned())
.parse::<u64>() .parse::<u64>()
@@ -60,10 +56,10 @@ fn get_rocket_config() -> Result<RocketConfig, InvalidRocketConfig> {
.unwrap(); .unwrap();
c.set_address(address) c.set_address(address)
.map_err(|_| InvalidRocketConfig::Address)?; .map_err(|_| RocketError::InvalidAddress)?;
c.set_port(port); c.set_port(port);
c.set_secret_key(secret_key) c.set_secret_key(secret_key)
.map_err(|_| InvalidRocketConfig::SecretKey)?; .map_err(|_| RocketError::InvalidSecretKey)?;
c.set_limits( c.set_limits(
Limits::new() Limits::new()
@@ -159,7 +155,7 @@ impl Default for LogoConfig {
.ok() .ok()
.or_else(|| custom_main.clone()); .or_else(|| custom_main.clone());
let other = if let Some(main) = custom_main.clone() { let other = if let Some(main) = custom_main.clone() {
let ext = |path: &str| match path.rsplit_once('.').map(|x| x.1) { let ext = |path: &str| match path.rsplitn(2, '.').next() {
Some("png") => Some("image/png".to_owned()), Some("png") => Some("image/png".to_owned()),
Some("jpg") | Some("jpeg") => Some("image/jpeg".to_owned()), Some("jpg") | Some("jpeg") => Some("image/jpeg".to_owned()),
Some("svg") => Some("image/svg+xml".to_owned()), Some("svg") => Some("image/svg+xml".to_owned()),
@@ -168,8 +164,11 @@ impl Default for LogoConfig {
}; };
let mut custom_icons = env::vars() let mut custom_icons = env::vars()
.filter_map(|(var, val)| { .filter_map(|(var, val)| {
var.strip_prefix("PLUME_LOGO_") if let Some(size) = var.strip_prefix("PLUME_LOGO_") {
.map(|size| (size.to_owned(), val)) Some((size.to_owned(), val))
} else {
None
}
}) })
.filter_map(|(var, val)| var.parse::<u64>().ok().map(|var| (var, val))) .filter_map(|(var, val)| var.parse::<u64>().ok().map(|var| (var, val)))
.map(|(dim, src)| Icon { .map(|(dim, src)| Icon {
@@ -249,37 +248,13 @@ impl SearchTokenizerConfig {
} }
} }
pub struct MailConfig {
pub server: String,
pub port: u16,
pub helo_name: String,
pub username: String,
pub password: String,
}
fn get_mail_config() -> Option<MailConfig> {
Some(MailConfig {
server: env::var("MAIL_SERVER").ok()?,
port: env::var("MAIL_PORT").map_or(SUBMISSIONS_PORT, |port| match port.as_str() {
"smtp" => SMTP_PORT,
"submissions" => SUBMISSIONS_PORT,
"submission" => SUBMISSION_PORT,
number => number
.parse()
.expect(r#"MAIL_PORT must be "smtp", "submissions", "submission" or an integer."#),
}),
helo_name: env::var("MAIL_HELO_NAME").unwrap_or_else(|_| "localhost".to_owned()),
username: env::var("MAIL_USER").ok()?,
password: env::var("MAIL_PASSWORD").ok()?,
})
}
pub struct LdapConfig { pub struct LdapConfig {
pub addr: String, pub addr: String,
pub base_dn: String, pub base_dn: String,
pub tls: bool, pub tls: bool,
pub user_name_attr: String, pub user_name_attr: String,
pub mail_attr: String, pub mail_attr: String,
pub user: Option<(String, String)>,
} }
fn get_ldap_config() -> Option<LdapConfig> { fn get_ldap_config() -> Option<LdapConfig> {
@@ -295,16 +270,24 @@ fn get_ldap_config() -> Option<LdapConfig> {
}; };
let user_name_attr = var("LDAP_USER_NAME_ATTR").unwrap_or_else(|_| "cn".to_owned()); 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()); let mail_attr = var("LDAP_USER_MAIL_ATTR").unwrap_or_else(|_| "mail".to_owned());
let user = var("LDAP_USER").ok();
let password = var("LDAP_PASSWORD").ok();
let user = match (user, password) {
(Some(user), Some(password)) => Some((user, password)),
(None, None) => None,
_ => panic!("Invalid LDAP configuration both or neither of LDAP_USER and LDAP_PASSWORD must be set")
};
Some(LdapConfig { Some(LdapConfig {
addr, addr,
base_dn, base_dn,
tls, tls,
user_name_attr, user_name_attr,
mail_attr, mail_attr,
user
}) })
} }
(None, None) => None, (None, None) => None,
(_, _) => { _ => {
panic!("Invalid LDAP configuration : both LDAP_ADDR and LDAP_BASE_DN must be set") panic!("Invalid LDAP configuration : both LDAP_ADDR and LDAP_BASE_DN must be set")
} }
} }
@@ -364,7 +347,6 @@ lazy_static! {
s.parse::<u32>() s.parse::<u32>()
.expect("Couldn't parse DB_MIN_IDLE into u32") .expect("Couldn't parse DB_MIN_IDLE into u32")
)), )),
signup: var("SIGNUP").map_or(SignupStrategy::default(), |s| s.parse().unwrap()),
#[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)),
@@ -377,7 +359,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()),
mail: get_mail_config(),
ldap: get_ldap_config(), ldap: get_ldap_config(),
proxy: get_proxy_config(), proxy: get_proxy_config(),
}; };
-143
View File
@@ -1,143 +0,0 @@
use crate::{
db_conn::DbConn,
schema::email_signups,
users::{NewUser, Role, User},
Error, Result,
};
use chrono::{offset::Utc, Duration, NaiveDateTime};
use diesel::{
Connection as _, ExpressionMethods, Identifiable, Insertable, QueryDsl, Queryable, RunQueryDsl,
};
use plume_common::utils::random_hex;
use std::ops::Deref;
const TOKEN_VALIDITY_HOURS: i64 = 2;
#[repr(transparent)]
pub struct Token(String);
impl From<String> for Token {
fn from(string: String) -> Self {
Token(string)
}
}
impl From<Token> for String {
fn from(token: Token) -> Self {
token.0
}
}
impl Deref for Token {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Token {
fn generate() -> Self {
Self(random_hex())
}
}
#[derive(Identifiable, Queryable)]
pub struct EmailSignup {
pub id: i32,
pub email: String,
pub token: String,
pub expiration_date: NaiveDateTime,
}
#[derive(Insertable)]
#[table_name = "email_signups"]
pub struct NewEmailSignup<'a> {
pub email: &'a str,
pub token: &'a str,
pub expiration_date: NaiveDateTime,
}
impl EmailSignup {
pub fn start(conn: &DbConn, email: &str) -> Result<Token> {
conn.transaction(|| {
Self::ensure_user_not_exist_by_email(conn, email)?;
let _rows = Self::delete_existings_by_email(conn, email)?;
let token = Token::generate();
let expiration_date = Utc::now()
.naive_utc()
.checked_add_signed(Duration::hours(TOKEN_VALIDITY_HOURS))
.expect("could not calculate expiration date");
let new_signup = NewEmailSignup {
email,
token: &token,
expiration_date,
};
let _rows = diesel::insert_into(email_signups::table)
.values(new_signup)
.execute(&**conn)?;
Ok(token)
})
}
pub fn find_by_token(conn: &DbConn, token: Token) -> Result<Self> {
let signup = email_signups::table
.filter(email_signups::token.eq(token.as_str()))
.first::<Self>(&**conn)
.map_err(Error::from)?;
Ok(signup)
}
pub fn confirm(&self, conn: &DbConn) -> Result<()> {
conn.transaction(|| {
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
if self.expired() {
Self::delete_existings_by_email(conn, &self.email)?;
return Err(Error::Expired);
}
Ok(())
})
}
pub fn complete(&self, conn: &DbConn, username: String, password: String) -> Result<User> {
conn.transaction(|| {
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
let user = NewUser::new_local(
conn,
username,
"".to_string(),
Role::Normal,
"",
self.email.clone(),
Some(User::hash_pass(&password)?),
)?;
self.delete(conn)?;
Ok(user)
})
}
fn delete(&self, conn: &DbConn) -> Result<()> {
let _rows = diesel::delete(self).execute(&**conn).map_err(Error::from)?;
Ok(())
}
fn ensure_user_not_exist_by_email(conn: &DbConn, email: &str) -> Result<()> {
if User::email_used(conn, email)? {
let _rows = Self::delete_existings_by_email(conn, email)?;
return Err(Error::UserAlreadyExists);
}
Ok(())
}
fn delete_existings_by_email(conn: &DbConn, email: &str) -> Result<usize> {
let existing_signups = email_signups::table.filter(email_signups::email.eq(email));
diesel::delete(existing_signups)
.execute(&**conn)
.map_err(Error::from)
}
fn expired(&self) -> bool {
self.expiration_date < Utc::now().naive_utc()
}
}
+3 -7
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
ap_url, db_conn::DbConn, instance::Instance, notifications::*, schema::follows, users::User, ap_url, db_conn::DbConn, notifications::*, schema::follows, users::User, Connection, Error,
Connection, Error, Result, CONFIG, Result, CONFIG,
}; };
use activitypub::activity::{Accept, Follow as FollowAct, Undo}; use activitypub::activity::{Accept, Follow as FollowAct, Undo};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
@@ -183,10 +183,6 @@ impl FromId<DbConn> for Follow {
.map_err(|(_, e)| e)?; .map_err(|(_, e)| e)?;
Follow::accept_follow(conn, &actor, &target, follow, actor.id, target.id) Follow::accept_follow(conn, &actor, &target, follow, actor.id, target.id)
} }
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
} }
impl AsObject<User, Undo, &DbConn> for Follow { impl AsObject<User, Undo, &DbConn> for Follow {
@@ -199,7 +195,7 @@ impl AsObject<User, Undo, &DbConn> for Follow {
diesel::delete(&self).execute(&**conn)?; diesel::delete(&self).execute(&**conn)?;
// delete associated notification if any // delete associated notification if any
if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) { if let Ok(notif) = Notification::find(&conn, notification_kind::FOLLOW, self.id) {
diesel::delete(&notif).execute(&**conn)?; diesel::delete(&notif).execute(&**conn)?;
} }
+2 -43
View File
@@ -3,12 +3,11 @@ use crate::{
medias::Media, medias::Media,
safe_string::SafeString, safe_string::SafeString,
schema::{instances, users}, schema::{instances, users},
users::{NewUser, Role, User}, users::{Role, User},
Connection, Error, Result, Connection, Error, Result,
}; };
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, result::Error::NotFound, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use once_cell::sync::OnceCell;
use plume_common::utils::md_to_html; use plume_common::utils::md_to_html;
use std::sync::RwLock; use std::sync::RwLock;
@@ -46,9 +45,6 @@ lazy_static! {
static ref LOCAL_INSTANCE: RwLock<Option<Instance>> = RwLock::new(None); static ref LOCAL_INSTANCE: RwLock<Option<Instance>> = RwLock::new(None);
} }
const LOCAL_INSTANCE_USERNAME: &str = "__instance__";
static LOCAL_INSTANCE_USER: OnceCell<User> = OnceCell::new();
impl Instance { impl Instance {
pub fn set_local(self) { pub fn set_local(self) {
LOCAL_INSTANCE.write().unwrap().replace(self); LOCAL_INSTANCE.write().unwrap().replace(self);
@@ -80,42 +76,6 @@ impl Instance {
.map_err(Error::from) .map_err(Error::from)
} }
pub fn create_local_instance_user(conn: &Connection) -> Result<User> {
let instance = Instance::get_local()?;
let email = format!("{}@{}", LOCAL_INSTANCE_USERNAME, &instance.public_domain);
NewUser::new_local(
conn,
LOCAL_INSTANCE_USERNAME.into(),
instance.public_domain,
Role::Instance,
"Local instance",
email,
None,
)
}
pub fn get_local_instance_user() -> Option<&'static User> {
LOCAL_INSTANCE_USER.get()
}
pub fn get_local_instance_user_uncached(conn: &Connection) -> Result<User> {
users::table
.filter(users::role.eq(3))
.first(conn)
.or_else(|err| match err {
NotFound => Self::create_local_instance_user(conn),
_ => Err(Error::Db(err)),
})
}
pub fn cache_local_instance_user(conn: &Connection) {
let _ = LOCAL_INSTANCE_USER.get_or_init(|| {
Self::get_local_instance_user_uncached(conn)
.or_else(|_| Self::create_local_instance_user(conn))
.expect("Failed to cache local instance user")
});
}
pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<Instance>> { pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<Instance>> {
instances::table instances::table
.order(instances::public_domain.asc()) .order(instances::public_domain.asc())
@@ -344,7 +304,6 @@ pub(crate) mod tests {
}) })
.collect(); .collect();
Instance::cache_local(conn); Instance::cache_local(conn);
Instance::cache_local_instance_user(conn);
res res
} }
Regular → Executable
+8 -45
View File
@@ -1,3 +1,4 @@
#![feature(try_trait)]
#![feature(never_type)] #![feature(never_type)]
#![feature(proc_macro_hygiene)] #![feature(proc_macro_hygiene)]
#![feature(box_patterns)] #![feature(box_patterns)]
@@ -16,10 +17,8 @@ extern crate serde_json;
#[macro_use] #[macro_use]
extern crate tantivy; extern crate tantivy;
pub use lettre;
pub use lettre::smtp;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use plume_common::activity_pub::{inbox::InboxError, request, sign}; use plume_common::activity_pub::inbox::InboxError;
use posts::PostEvent; use posts::PostEvent;
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder}; use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
use users::UserEvent; use users::UserEvent;
@@ -67,7 +66,6 @@ pub enum Error {
Url, Url,
Webfinger, Webfinger,
Expired, Expired,
UserAlreadyExists,
} }
impl From<bcrypt::BcryptError> for Error { impl From<bcrypt::BcryptError> for Error {
@@ -82,18 +80,18 @@ impl From<openssl::error::ErrorStack> for Error {
} }
} }
impl From<sign::Error> for Error {
fn from(_: sign::Error) -> Self {
Error::Signature
}
}
impl From<diesel::result::Error> for Error { impl From<diesel::result::Error> for Error {
fn from(err: diesel::result::Error) -> Self { fn from(err: diesel::result::Error) -> Self {
Error::Db(err) Error::Db(err)
} }
} }
impl From<std::option::NoneError> for Error {
fn from(_: std::option::NoneError) -> Self {
Error::NotFound
}
}
impl From<url::ParseError> for Error { impl From<url::ParseError> for Error {
fn from(_: url::ParseError) -> Self { fn from(_: url::ParseError) -> Self {
Error::Url Error::Url
@@ -160,12 +158,6 @@ impl From<InboxError<Error>> for Error {
} }
} }
impl From<request::Error> for Error {
fn from(_err: request::Error) -> Error {
Error::Request
}
}
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
/// Adds a function to a model, that returns the first /// Adds a function to a model, that returns the first
@@ -303,33 +295,6 @@ pub fn ap_url(url: &str) -> String {
format!("https://{}", url) format!("https://{}", url)
} }
pub trait SmtpNewWithAddr {
fn new_with_addr(
addr: (&str, u16),
) -> std::result::Result<smtp::SmtpClient, smtp::error::Error>;
}
impl SmtpNewWithAddr for smtp::SmtpClient {
// Stolen from lettre::smtp::SmtpClient::new_simple()
fn new_with_addr(addr: (&str, u16)) -> std::result::Result<Self, smtp::error::Error> {
use native_tls::TlsConnector;
use smtp::{
client::net::{ClientTlsParameters, DEFAULT_TLS_PROTOCOLS},
ClientSecurity, SmtpClient,
};
let (domain, port) = addr;
let mut tls_builder = TlsConnector::builder();
tls_builder.min_protocol_version(Some(DEFAULT_TLS_PROTOCOLS[0]));
let tls_parameters =
ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap());
SmtpClient::new((domain, port), ClientSecurity::Wrapper(tls_parameters))
}
}
#[cfg(test)] #[cfg(test)]
#[macro_use] #[macro_use]
mod tests { mod tests {
@@ -377,7 +342,6 @@ pub mod blogs;
pub mod comment_seers; pub mod comment_seers;
pub mod comments; pub mod comments;
pub mod db_conn; pub mod db_conn;
pub mod email_signups;
pub mod follows; pub mod follows;
pub mod headers; pub mod headers;
pub mod inbox; pub mod inbox;
@@ -398,7 +362,6 @@ pub mod safe_string;
#[allow(unused_imports)] #[allow(unused_imports)]
pub mod schema; pub mod schema;
pub mod search; pub mod search;
pub mod signups;
pub mod tags; pub mod tags;
pub mod timeline; pub mod timeline;
pub mod users; pub mod users;
+3 -8
View File
@@ -1,13 +1,12 @@
use crate::{ use crate::{
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*, db_conn::DbConn, notifications::*, posts::Post, schema::likes, timeline::*, users::User,
users::User, Connection, Error, Result, CONFIG, Connection, Error, Result, CONFIG,
}; };
use activitypub::activity; use activitypub::activity;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{ use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId}, inbox::{AsActor, AsObject, FromId},
sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY, Id, IntoId, PUBLIC_VISIBILITY,
}; };
@@ -138,10 +137,6 @@ impl FromId<DbConn> for Like {
res.notify(conn)?; res.notify(conn)?;
Ok(res) Ok(res)
} }
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
} }
impl AsObject<User, activity::Undo, &DbConn> for Like { impl AsObject<User, activity::Undo, &DbConn> for Like {
@@ -153,7 +148,7 @@ impl AsObject<User, activity::Undo, &DbConn> for Like {
diesel::delete(&self).execute(&**conn)?; diesel::delete(&self).execute(&**conn)?;
// delete associated notification if any // delete associated notification if any
if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) { if let Ok(notif) = Notification::find(&conn, notification_kind::LIKE, self.id) {
diesel::delete(&notif).execute(&**conn)?; diesel::delete(&notif).execute(&**conn)?;
} }
Ok(()) Ok(())
+1 -3
View File
@@ -143,7 +143,6 @@ macro_rules! func {
} }
} }
#[allow(dead_code)]
#[derive(Clone, Queryable, Identifiable)] #[derive(Clone, Queryable, Identifiable)]
struct ListElem { struct ListElem {
pub id: i32, pub id: i32,
@@ -286,8 +285,7 @@ impl List {
.select(list_elems::word) .select(list_elems::word)
.load::<Option<String>>(conn) .load::<Option<String>>(conn)
.map_err(Error::from) .map_err(Error::from)
// .map(|r| r.into_iter().filter_map(|o| o).collect::<Vec<String>>()) .map(|r| r.into_iter().filter_map(|o| o).collect::<Vec<String>>())
.map(|r| r.into_iter().flatten().collect::<Vec<String>>())
} }
pub fn clear(&self, conn: &Connection) -> Result<()> { pub fn clear(&self, conn: &Connection) -> Result<()> {
+30 -72
View File
@@ -3,22 +3,23 @@ use crate::{
users::User, Connection, Error, Result, CONFIG, users::User, Connection, Error, Result, CONFIG,
}; };
use activitypub::object::Image; use activitypub::object::Image;
use askama_escape::escape;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use guid_create::GUID; use guid_create::GUID;
use plume_common::{ use plume_common::{
activity_pub::{inbox::FromId, request, Id}, activity_pub::{inbox::FromId, Id},
utils::{escape, MediaProcessor}, utils::MediaProcessor,
}; };
use std::{ use std::{
fs::{self, DirBuilder}, fs::{self, DirBuilder},
path::{self, Path, PathBuf}, path::{Path, PathBuf},
}; };
use tracing::warn; use tracing::warn;
use url::Url; use url::Url;
const REMOTE_MEDIA_DIRECTORY: &str = "remote"; const REMOTE_MEDIA_DIRECTORY: &str = "remote";
#[derive(Clone, Identifiable, Queryable, AsChangeset)] #[derive(Clone, Identifiable, Queryable)]
pub struct Media { pub struct Media {
pub id: i32, pub id: i32,
pub file_path: String, pub file_path: String,
@@ -64,7 +65,6 @@ impl MediaCategory {
impl Media { impl Media {
insert!(medias, NewMedia); insert!(medias, NewMedia);
get!(medias); get!(medias);
find_by!(medias, find_by_file_path, file_path as &str);
pub fn for_user(conn: &Connection, owner: i32) -> Result<Vec<Media>> { pub fn for_user(conn: &Connection, owner: i32) -> Result<Vec<Media>> {
medias::table medias::table
@@ -103,8 +103,8 @@ impl Media {
pub fn category(&self) -> MediaCategory { pub fn category(&self) -> MediaCategory {
match &*self match &*self
.file_path .file_path
.rsplit_once('.') .rsplitn(2, '.')
.map(|x| x.1) .next()
.expect("Media::category: extension error") .expect("Media::category: extension error")
.to_lowercase() .to_lowercase()
{ {
@@ -155,15 +155,12 @@ impl Media {
if self.is_remote { if self.is_remote {
Ok(self.remote_url.clone().unwrap_or_default()) Ok(self.remote_url.clone().unwrap_or_default())
} else { } else {
let file_path = self.file_path.replace(path::MAIN_SEPARATOR, "/").replacen( let p = Path::new(&self.file_path);
&CONFIG.media_directory, let filename: String = p.file_name().unwrap().to_str().unwrap().to_owned();
"static/media",
1,
); // "static/media" from plume::routs::plume_media_files()
Ok(ap_url(&format!( Ok(ap_url(&format!(
"{}/{}", "{}/static/media/{}",
Instance::get_local()?.public_domain, Instance::get_local()?.public_domain,
&file_path &filename
))) )))
} }
} }
@@ -207,69 +204,32 @@ impl Media {
// TODO: merge with save_remote? // TODO: merge with save_remote?
pub fn from_activity(conn: &DbConn, image: &Image) -> Result<Media> { pub fn from_activity(conn: &DbConn, image: &Image) -> Result<Media> {
let remote_url = image let remote_url = image.object_props.url_string().ok()?;
.object_props
.url_string()
.or(Err(Error::MissingApProperty))?;
let path = determine_mirror_file_path(&remote_url); let path = determine_mirror_file_path(&remote_url);
let parent = path.parent().ok_or(Error::InvalidValue)?; let parent = path.parent()?;
if !parent.is_dir() { if !parent.is_dir() {
DirBuilder::new().recursive(true).create(parent)?; DirBuilder::new().recursive(true).create(parent)?;
} }
let mut dest = fs::File::create(path.clone())?; let mut dest = fs::File::create(path.clone()).ok()?;
// TODO: conditional GET // TODO: conditional GET
request::get( if let Some(proxy) = CONFIG.proxy() {
remote_url.as_str(), reqwest::ClientBuilder::new().proxy(proxy.clone()).build()?
User::get_sender(), } else {
CONFIG.proxy().cloned(), reqwest::Client::new()
)? }
.copy_to(&mut dest)?; .get(remote_url.as_str())
.send()
.ok()?
.copy_to(&mut dest)
.ok()?;
Media::find_by_file_path(conn, path.to_str().ok_or(Error::InvalidValue)?) // TODO: upsert
.and_then(|mut media| {
let mut updated = false;
let alt_text = image
.object_props
.content_string()
.or(Err(Error::NotFound))?;
let sensitive = image.object_props.summary_string().is_ok();
let content_warning = image.object_props.summary_string().ok();
if media.alt_text != alt_text {
media.alt_text = alt_text;
updated = true;
}
if media.is_remote {
media.is_remote = false;
updated = true;
}
if media.remote_url.is_some() {
media.remote_url = None;
updated = true;
}
if media.sensitive != sensitive {
media.sensitive = sensitive;
updated = true;
}
if media.content_warning != content_warning {
media.content_warning = content_warning;
updated = true;
}
if updated {
diesel::update(&media).set(&media).execute(&**conn)?;
}
Ok(media)
})
.or_else(|_| {
Media::insert( Media::insert(
conn, conn,
NewMedia { NewMedia {
file_path: path.to_str().ok_or(Error::InvalidValue)?.to_string(), file_path: path.to_str()?.to_string(),
alt_text: image alt_text: image.object_props.content_string().ok()?,
.object_props
.content_string()
.or(Err(Error::NotFound))?,
is_remote: false, is_remote: false,
remote_url: None, remote_url: None,
sensitive: image.object_props.summary_string().is_ok(), sensitive: image.object_props.summary_string().is_ok(),
@@ -279,10 +239,9 @@ impl Media {
image image
.object_props .object_props
.attributed_to_link_vec::<Id>() .attributed_to_link_vec::<Id>()
.or(Err(Error::NotFound))? .ok()?
.into_iter() .into_iter()
.next() .next()?
.ok_or(Error::NotFound)?
.as_ref(), .as_ref(),
None, None,
CONFIG.proxy(), CONFIG.proxy(),
@@ -291,7 +250,6 @@ impl Media {
.id, .id,
}, },
) )
})
} }
pub fn get_media_processor<'a>(conn: &'a Connection, user: Vec<&User>) -> MediaProcessor<'a> { pub fn get_media_processor<'a>(conn: &'a Connection, user: Vec<&User>) -> MediaProcessor<'a> {
@@ -330,7 +288,7 @@ fn determine_mirror_file_path(url: &str) -> PathBuf {
.next() .next()
.map(ToOwned::to_owned) .map(ToOwned::to_owned)
.unwrap_or_else(|| String::from("png")); .unwrap_or_else(|| String::from("png"));
file_path.push(format!("{}.{}", GUID::rand(), ext)); file_path.push(format!("{}.{}", GUID::rand().to_string(), ext));
}); });
file_path file_path
} }
+2 -6
View File
@@ -47,11 +47,7 @@ impl Mention {
pub fn get_user(&self, conn: &Connection) -> Result<User> { pub fn get_user(&self, conn: &Connection) -> Result<User> {
match self.get_post(conn) { match self.get_post(conn) {
Ok(p) => Ok(p Ok(p) => Ok(p.get_authors(conn)?.into_iter().next()?),
.get_authors(conn)?
.into_iter()
.next()
.ok_or(Error::NotFound)?),
Err(_) => self.get_comment(conn).and_then(|c| c.get_author(conn)), Err(_) => self.get_comment(conn).and_then(|c| c.get_author(conn)),
} }
} }
@@ -81,7 +77,7 @@ impl Mention {
in_post: bool, in_post: bool,
notify: bool, notify: bool,
) -> Result<Self> { ) -> Result<Self> {
let ap_url = ment.link_props.href_string().or(Err(Error::NotFound))?; let ap_url = ment.link_props.href_string().ok()?;
let mentioned = User::find_by_ap_url(conn, &ap_url)?; let mentioned = User::find_by_ap_url(conn, &ap_url)?;
if in_post { if in_post {
+1 -2
View File
@@ -105,8 +105,7 @@ impl ImportedMigrations {
pub fn rerun_last_migration(&self, conn: &Connection, path: &Path) -> Result<()> { pub fn rerun_last_migration(&self, conn: &Connection, path: &Path) -> Result<()> {
let latest_migration = conn.latest_run_migration_version()?; let latest_migration = conn.latest_run_migration_version()?;
let id = latest_migration let id = latest_migration
.and_then(|m| self.0.binary_search_by_key(&m.as_str(), |m| m.name).ok()) .and_then(|m| self.0.binary_search_by_key(&m.as_str(), |m| m.name).ok())?;
.ok_or(Error::NotFound)?;
let migration = &self.0[id]; let migration = &self.0[id];
conn.transaction(|| { conn.transaction(|| {
migration.revert(conn, path)?; migration.revert(conn, path)?;
+1 -1
View File
@@ -61,7 +61,7 @@ impl PasswordResetRequest {
} }
pub fn find_and_delete_by_token(conn: &Connection, token: &str) -> Result<Self> { pub fn find_and_delete_by_token(conn: &Connection, token: &str) -> Result<Self> {
let request = Self::find_by_token(conn, token)?; let request = Self::find_by_token(&conn, &token)?;
let filter = let filter =
password_reset_requests::table.filter(password_reset_requests::id.eq(request.id)); password_reset_requests::table.filter(password_reset_requests::id.eq(request.id));
+38 -100
View File
@@ -10,15 +10,15 @@ use activitypub::{
CustomObject, CustomObject,
}; };
use chrono::{NaiveDateTime, TimeZone, Utc}; use chrono::{NaiveDateTime, TimeZone, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use heck::KebabCase;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use plume_common::{ use plume_common::{
activity_pub::{ activity_pub::{
inbox::{AsActor, AsObject, FromId}, inbox::{AsObject, FromId},
sign::Signer,
Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILITY, Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILITY,
}, },
utils::{iri_percent_encode_seg, md_to_html}, utils::md_to_html,
}; };
use riker::actors::{Publish, Tell}; use riker::actors::{Publish, Tell};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
@@ -67,15 +67,20 @@ impl Post {
find_by!(posts, find_by_ap_url, ap_url as &str); find_by!(posts, find_by_ap_url, ap_url as &str);
last!(posts); last!(posts);
pub fn insert(conn: &Connection, mut new: NewPost) -> Result<Self> { pub fn insert(conn: &Connection, new: NewPost) -> Result<Self> {
if new.ap_url.is_empty() {
let blog = Blog::get(conn, new.blog_id)?;
new.ap_url = Self::ap_url(blog, &new.slug);
}
diesel::insert_into(posts::table) diesel::insert_into(posts::table)
.values(new) .values(new)
.execute(conn)?; .execute(conn)?;
let post = Self::last(conn)?; let mut post = Self::last(conn)?;
if post.ap_url.is_empty() {
post.ap_url = ap_url(&format!(
"{}/~/{}/{}/",
CONFIG.base_url,
post.get_blog(conn)?.fqn,
post.slug
));
let _: Post = post.save_changes(conn)?;
}
if post.published { if post.published {
post.publish_published(); post.publish_published();
@@ -89,16 +94,13 @@ impl Post {
let post = Self::get(conn, self.id)?; let post = Self::get(conn, self.id)?;
// TODO: Call publish_published() when newly published // TODO: Call publish_published() when newly published
if post.published { if post.published {
let blog = post.get_blog(conn);
if blog.is_ok() && blog.unwrap().is_local() {
self.publish_updated(); self.publish_updated();
} }
}
Ok(post) Ok(post)
} }
pub fn delete(&self, conn: &Connection) -> Result<()> { pub fn delete(&self, conn: &Connection) -> Result<()> {
for m in Mention::list_for_post(conn, self.id)? { for m in Mention::list_for_post(&conn, self.id)? {
m.delete(conn)?; m.delete(conn)?;
} }
diesel::delete(self).execute(conn)?; diesel::delete(self).execute(conn)?;
@@ -249,20 +251,6 @@ impl Post {
.map_err(Error::from) .map_err(Error::from)
} }
pub fn ap_url(blog: Blog, slug: &str) -> String {
ap_url(&format!(
"{}/~/{}/{}/",
CONFIG.base_url,
blog.fqn,
iri_percent_encode_seg(slug)
))
}
// It's better to calc slug in insert and update
pub fn slug(title: &str) -> &str {
title
}
pub fn get_authors(&self, conn: &Connection) -> Result<Vec<User>> { pub fn get_authors(&self, conn: &Connection) -> Result<Vec<User>> {
use crate::schema::post_authors; use crate::schema::post_authors;
use crate::schema::users; use crate::schema::users;
@@ -455,17 +443,23 @@ impl Post {
m, m,
) )
}) })
.filter_map(|(id, m)| id.map(|id| (m, id))) .filter_map(|(id, m)| {
if let Some(id) = id {
Some((m, id))
} else {
None
}
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let old_mentions = Mention::list_for_post(conn, self.id)?; let old_mentions = Mention::list_for_post(&conn, self.id)?;
let old_user_mentioned = old_mentions let old_user_mentioned = old_mentions
.iter() .iter()
.map(|m| m.mentioned_id) .map(|m| m.mentioned_id)
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
for (m, id) in &mentions { for (m, id) in &mentions {
if !old_user_mentioned.contains(id) { if !old_user_mentioned.contains(&id) {
Mention::from_activity(&*conn, m, self.id, true, true)?; Mention::from_activity(&*conn, &m, self.id, true, true)?;
} }
} }
@@ -477,7 +471,7 @@ impl Post {
.iter() .iter()
.filter(|m| !new_mentions.contains(&m.mentioned_id)) .filter(|m| !new_mentions.contains(&m.mentioned_id))
{ {
m.delete(conn)?; m.delete(&conn)?;
} }
Ok(()) Ok(())
} }
@@ -650,72 +644,28 @@ impl FromId<DbConn> for Post {
.and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id)); .and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id));
let title = article.object_props.name_string()?; let title = article.object_props.name_string()?;
let ap_url = article // TODO: upsert
.object_props let post = Post::insert(
.url_string()
.or_else(|_| article.object_props.id_string())?;
let post = Post::from_db(conn, &ap_url)
.and_then(|mut post| {
let mut updated = false;
let slug = Self::slug(&title);
let content = SafeString::new(&article.object_props.content_string()?);
let subtitle = article.object_props.summary_string()?;
let source = article.ap_object_props.source_object::<Source>()?.content;
if post.slug != slug {
post.slug = slug.to_string();
updated = true;
}
if post.title != title {
post.title = title.clone();
updated = true;
}
if post.content != content {
post.content = content;
updated = true;
}
if post.license != license {
post.license = license.clone();
updated = true;
}
if post.subtitle != subtitle {
post.subtitle = subtitle;
updated = true;
}
if post.source != source {
post.source = source;
updated = true;
}
if post.cover_id != cover {
post.cover_id = cover;
updated = true;
}
if updated {
post.update(conn)?;
}
Ok(post)
})
.or_else(|_| {
Post::insert(
conn, conn,
NewPost { NewPost {
blog_id: blog.ok_or(Error::NotFound)?.id, blog_id: blog?.id,
slug: Self::slug(&title).to_string(), slug: title.to_kebab_case(),
title, title,
content: SafeString::new(&article.object_props.content_string()?), content: SafeString::new(&article.object_props.content_string()?),
published: true, published: true,
license, license,
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields // FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
ap_url, ap_url: article
.object_props
.url_string()
.or_else(|_| article.object_props.id_string())?,
creation_date: Some(article.object_props.published_utctime()?.naive_utc()), creation_date: Some(article.object_props.published_utctime()?.naive_utc()),
subtitle: article.object_props.summary_string()?, subtitle: article.object_props.summary_string()?,
source: article.ap_object_props.source_object::<Source>()?.content, source: article.ap_object_props.source_object::<Source>()?.content,
cover_id: cover, cover_id: cover,
}, },
) )?;
.and_then(|post| {
for author in authors { for author in authors {
PostAuthor::insert( PostAuthor::insert(
conn, conn,
@@ -726,10 +676,6 @@ impl FromId<DbConn> for Post {
)?; )?;
} }
Ok(post)
})
})?;
// save mentions and tags // save mentions and tags
let mut hashtags = md_to_html(&post.source, None, false, None) let mut hashtags = md_to_html(&post.source, None, false, None)
.2 .2
@@ -760,10 +706,6 @@ impl FromId<DbConn> for Post {
Ok(post) Ok(post)
} }
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
} }
impl AsObject<User, Create, &DbConn> for Post { impl AsObject<User, Create, &DbConn> for Post {
@@ -835,10 +777,6 @@ impl FromId<DbConn> for PostUpdate {
tags: updated.object.object_props.tag, tags: updated.object.object_props.tag,
}) })
} }
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
} }
impl AsObject<User, Update, &DbConn> for PostUpdate { impl AsObject<User, Update, &DbConn> for PostUpdate {
@@ -855,7 +793,7 @@ impl AsObject<User, Update, &DbConn> for PostUpdate {
} }
if let Some(title) = self.title { if let Some(title) = self.title {
post.slug = Post::slug(&title).to_string(); post.slug = title.to_kebab_case();
post.title = title; post.title = title;
} }
+11 -9
View File
@@ -17,23 +17,25 @@ pub struct RemoteFetchActor {
impl RemoteFetchActor { impl RemoteFetchActor {
pub fn init(conn: DbPool) { pub fn init(conn: DbPool) {
let actor = ACTOR_SYS ACTOR_SYS
.actor_of_args::<RemoteFetchActor, _>("remote-fetch", conn) .actor_of_args::<RemoteFetchActor, _>("remote-fetch", conn)
.expect("Failed to initialize remote fetch actor"); .expect("Failed to initialize remote fetch actor");
USER_CHAN.tell(
Subscribe {
actor: Box::new(actor),
topic: "*".into(),
},
None,
)
} }
} }
impl Actor for RemoteFetchActor { impl Actor for RemoteFetchActor {
type Msg = UserEvent; type Msg = UserEvent;
fn pre_start(&mut self, ctx: &Context<Self::Msg>) {
USER_CHAN.tell(
Subscribe {
actor: Box::new(ctx.myself()),
topic: "*".into(),
},
None,
)
}
fn recv(&mut self, _ctx: &Context<Self::Msg>, msg: Self::Msg, _sender: Sender) { fn recv(&mut self, _ctx: &Context<Self::Msg>, msg: Self::Msg, _sender: Sender) {
use UserEvent::*; use UserEvent::*;
+3 -8
View File
@@ -1,13 +1,12 @@
use crate::{ use crate::{
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::reshares, db_conn::DbConn, notifications::*, posts::Post, schema::reshares, timeline::*, users::User,
timeline::*, users::User, Connection, Error, Result, CONFIG, Connection, Error, Result, CONFIG,
}; };
use activitypub::activity::{Announce, Undo}; use activitypub::activity::{Announce, Undo};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{ use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId}, inbox::{AsActor, AsObject, FromId},
sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY, Id, IntoId, PUBLIC_VISIBILITY,
}; };
@@ -163,10 +162,6 @@ impl FromId<DbConn> for Reshare {
res.notify(conn)?; res.notify(conn)?;
Ok(res) Ok(res)
} }
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
} }
impl AsObject<User, Undo, &DbConn> for Reshare { impl AsObject<User, Undo, &DbConn> for Reshare {
@@ -178,7 +173,7 @@ impl AsObject<User, Undo, &DbConn> for Reshare {
diesel::delete(&self).execute(&**conn)?; diesel::delete(&self).execute(&**conn)?;
// delete associated notification if any // delete associated notification if any
if let Ok(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) { if let Ok(notif) = Notification::find(&conn, notification_kind::RESHARE, self.id) {
diesel::delete(&notif).execute(&**conn)?; diesel::delete(&notif).execute(&**conn)?;
} }
+2 -2
View File
@@ -102,7 +102,7 @@ pub struct SafeString {
impl SafeString { impl SafeString {
pub fn new(value: &str) -> Self { pub fn new(value: &str) -> Self {
SafeString { SafeString {
value: CLEAN.clean(value).to_string(), value: CLEAN.clean(&value).to_string(),
} }
} }
@@ -156,7 +156,7 @@ impl<'de> Deserialize<'de> for SafeString {
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
deserializer.deserialize_string(SafeStringVisitor) Ok(deserializer.deserialize_string(SafeStringVisitor)?)
} }
} }
-11
View File
@@ -73,7 +73,6 @@ table! {
user_id -> Int4, user_id -> Int4,
} }
} }
table! { table! {
email_blocklist(id){ email_blocklist(id){
id -> Int4, id -> Int4,
@@ -84,15 +83,6 @@ table! {
} }
} }
table! {
email_signups (id) {
id -> Int4,
email -> Varchar,
token -> Varchar,
expiration_date -> Timestamp,
}
}
table! { table! {
follows (id) { follows (id) {
id -> Int4, id -> Int4,
@@ -316,7 +306,6 @@ allow_tables_to_appear_in_same_query!(
blogs, blogs,
comments, comments,
comment_seers, comment_seers,
email_signups,
follows, follows,
instances, instances,
likes, likes,
+11 -9
View File
@@ -13,23 +13,25 @@ pub struct SearchActor {
impl SearchActor { impl SearchActor {
pub fn init(searcher: Arc<Searcher>, conn: DbPool) { pub fn init(searcher: Arc<Searcher>, conn: DbPool) {
let actor = ACTOR_SYS ACTOR_SYS
.actor_of_args::<SearchActor, _>("search", (searcher, conn)) .actor_of_args::<SearchActor, _>("search", (searcher, conn))
.expect("Failed to initialize searcher actor"); .expect("Failed to initialize searcher actor");
POST_CHAN.tell(
Subscribe {
actor: Box::new(actor),
topic: "*".into(),
},
None,
)
} }
} }
impl Actor for SearchActor { impl Actor for SearchActor {
type Msg = PostEvent; type Msg = PostEvent;
fn pre_start(&mut self, ctx: &Context<Self::Msg>) {
POST_CHAN.tell(
Subscribe {
actor: Box::new(ctx.myself()),
topic: "*".into(),
},
None,
)
}
fn recv(&mut self, _ctx: &Context<Self::Msg>, msg: Self::Msg, _sender: Sender) { fn recv(&mut self, _ctx: &Context<Self::Msg>, msg: Self::Msg, _sender: Sender) {
use PostEvent::*; use PostEvent::*;
+2 -2
View File
@@ -148,7 +148,7 @@ impl PlumeQuery {
/// Parse a query string into this Query /// Parse a query string into this Query
pub fn parse_query(&mut self, query: &str) -> &mut Self { pub fn parse_query(&mut self, query: &str) -> &mut Self {
self.from_str_req(query.trim()) self.from_str_req(&query.trim())
} }
/// Convert this Query to a Tantivy Query /// Convert this Query to a Tantivy Query
@@ -360,7 +360,7 @@ impl std::str::FromStr for PlumeQuery {
fn from_str(query: &str) -> Result<PlumeQuery, !> { fn from_str(query: &str) -> Result<PlumeQuery, !> {
let mut res: PlumeQuery = Default::default(); let mut res: PlumeQuery = Default::default();
res.from_str_req(query.trim()); res.from_str_req(&query.trim());
Ok(res) Ok(res)
} }
} }
-72
View File
@@ -1,72 +0,0 @@
use crate::CONFIG;
use rocket::request::{FromRequest, Outcome, Request};
use std::fmt;
use std::str::FromStr;
pub enum Strategy {
Password,
Email,
}
impl Default for Strategy {
fn default() -> Self {
Self::Password
}
}
impl FromStr for Strategy {
type Err = StrategyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use self::Strategy::*;
match s {
"password" => Ok(Password),
"email" => Ok(Email),
s => Err(StrategyError::Unsupported(s.to_string())),
}
}
}
#[derive(Debug)]
pub enum StrategyError {
Unsupported(String),
}
impl fmt::Display for StrategyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use self::StrategyError::*;
match self {
// FIXME: Calc option strings from enum
Unsupported(s) => write!(f, "Unsupported strategy: {}. Choose password or email", s),
}
}
}
impl std::error::Error for StrategyError {}
pub struct Password();
pub struct Email();
impl<'a, 'r> FromRequest<'a, 'r> for Password {
type Error = ();
fn from_request(_request: &'a Request<'r>) -> Outcome<Self, ()> {
match matches!(CONFIG.signup, Strategy::Password) {
true => Outcome::Success(Self()),
false => Outcome::Forward(()),
}
}
}
impl<'a, 'r> FromRequest<'a, 'r> for Email {
type Error = ();
fn from_request(_request: &'a Request<'r>) -> Outcome<Self, ()> {
match matches!(CONFIG.signup, Strategy::Email) {
true => Outcome::Success(Self()),
false => Outcome::Forward(()),
}
}
}
-13
View File
@@ -223,9 +223,6 @@ impl Timeline {
} }
pub fn add_post(&self, conn: &Connection, post: &Post) -> Result<()> { pub fn add_post(&self, conn: &Connection, post: &Post) -> Result<()> {
if self.includes_post(conn, post)? {
return Ok(());
}
diesel::insert_into(timeline::table) diesel::insert_into(timeline::table)
.values(TimelineEntry { .values(TimelineEntry {
post_id: post.id, post_id: post.id,
@@ -239,16 +236,6 @@ impl Timeline {
let query = TimelineQuery::parse(&self.query)?; let query = TimelineQuery::parse(&self.query)?;
query.matches(conn, self, post, kind) query.matches(conn, self, post, kind)
} }
fn includes_post(&self, conn: &Connection, post: &Post) -> Result<bool> {
diesel::dsl::select(diesel::dsl::exists(
timeline::table
.filter(timeline::timeline_id.eq(self.id))
.filter(timeline::post_id.eq(post.id)),
))
.get_result(conn)
.map_err(Error::from)
}
} }
#[cfg(test)] #[cfg(test)]
+23 -41
View File
@@ -18,6 +18,12 @@ pub enum QueryError {
RuntimeError(String), RuntimeError(String),
} }
impl From<std::option::NoneError> for QueryError {
fn from(_: std::option::NoneError) -> Self {
QueryError::UnexpectedEndOfQuery
}
}
pub type QueryResult<T> = std::result::Result<T, QueryError>; pub type QueryResult<T> = std::result::Result<T, QueryError>;
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
@@ -233,7 +239,7 @@ impl WithList {
) -> Result<bool> { ) -> Result<bool> {
match list { match list {
List::List(name) => { List::List(name) => {
let list = lists::List::find_for_user_by_name(conn, timeline.user_id, name)?; let list = lists::List::find_for_user_by_name(conn, timeline.user_id, &name)?;
match (self, list.kind()) { match (self, list.kind()) {
(WithList::Blog, ListType::Blog) => list.contains_blog(conn, post.blog_id), (WithList::Blog, ListType::Blog) => list.contains_blog(conn, post.blog_id),
(WithList::Author { boosts, likes }, ListType::User) => match kind { (WithList::Author { boosts, likes }, ListType::User) => match kind {
@@ -408,7 +414,7 @@ enum List<'a> {
fn parse_s<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> { fn parse_s<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> {
let mut res = Vec::new(); let mut res = Vec::new();
let (left, token) = parse_a(stream)?; let (left, token) = parse_a(&stream)?;
res.push(token); res.push(token);
stream = left; stream = left;
while !stream.is_empty() { while !stream.is_empty() {
@@ -430,7 +436,7 @@ fn parse_s<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
fn parse_a<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> { fn parse_a<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> {
let mut res = Vec::new(); let mut res = Vec::new();
let (left, token) = parse_b(stream)?; let (left, token) = parse_b(&stream)?;
res.push(token); res.push(token);
stream = left; stream = left;
while !stream.is_empty() { while !stream.is_empty() {
@@ -457,7 +463,7 @@ fn parse_b<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<
match left.get(0) { match left.get(0) {
Some(Token::RParent(_)) => Ok((&left[1..], token)), Some(Token::RParent(_)) => Ok((&left[1..], token)),
Some(t) => t.get_error(Token::RParent(0)), Some(t) => t.get_error(Token::RParent(0)),
None => Err(QueryError::UnexpectedEndOfQuery), None => None?,
} }
} }
_ => parse_c(stream), _ => parse_c(stream),
@@ -478,13 +484,9 @@ fn parse_c<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<
} }
fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Arg<'a>)> { fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Arg<'a>)> {
match stream match stream.get(0).map(Token::get_text)? {
.get(0)
.map(Token::get_text)
.ok_or(QueryError::UnexpectedEndOfQuery)?
{
s @ "blog" | s @ "author" | s @ "license" | s @ "tags" | s @ "lang" => { s @ "blog" | s @ "author" | s @ "license" | s @ "tags" | s @ "lang" => {
match stream.get(1).ok_or(QueryError::UnexpectedEndOfQuery)? { match stream.get(1)? {
Token::Word(_, _, r#in) if r#in == &"in" => { Token::Word(_, _, r#in) if r#in == &"in" => {
let (mut left, list) = parse_l(&stream[2..])?; let (mut left, list) = parse_l(&stream[2..])?;
let kind = match s { let kind = match s {
@@ -496,12 +498,7 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
if *clude != "include" && *clude != "exclude" { if *clude != "include" && *clude != "exclude" {
break; break;
} }
match ( match (*clude, left.get(1).map(Token::get_text)?) {
*clude,
left.get(1)
.map(Token::get_text)
.ok_or(QueryError::UnexpectedEndOfQuery)?,
) {
("include", "reshares") | ("include", "reshare") => { ("include", "reshares") | ("include", "reshare") => {
boosts = true boosts = true
} }
@@ -532,10 +529,7 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
t => t.get_error(Token::Word(0, 0, "'in'")), t => t.get_error(Token::Word(0, 0, "'in'")),
} }
} }
s @ "title" | s @ "subtitle" | s @ "content" => match ( s @ "title" | s @ "subtitle" | s @ "content" => match (stream.get(1)?, stream.get(2)?) {
stream.get(1).ok_or(QueryError::UnexpectedEndOfQuery)?,
stream.get(2).ok_or(QueryError::UnexpectedEndOfQuery)?,
) {
(Token::Word(_, _, contains), Token::Word(_, _, w)) if contains == &"contains" => Ok(( (Token::Word(_, _, contains), Token::Word(_, _, w)) if contains == &"contains" => Ok((
&stream[3..], &stream[3..],
Arg::Contains( Arg::Contains(
@@ -561,13 +555,7 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
if *clude != "include" && *clude != "exclude" { if *clude != "include" && *clude != "exclude" {
break; break;
} }
match ( match (*clude, stream.get(2).map(Token::get_text)?) {
*clude,
stream
.get(2)
.map(Token::get_text)
.ok_or(QueryError::UnexpectedEndOfQuery)?,
) {
("include", "reshares") | ("include", "reshare") => boosts = true, ("include", "reshares") | ("include", "reshare") => boosts = true,
("exclude", "reshares") | ("exclude", "reshare") => boosts = false, ("exclude", "reshares") | ("exclude", "reshare") => boosts = false,
("include", "likes") | ("include", "like") => likes = true, ("include", "likes") | ("include", "like") => likes = true,
@@ -589,10 +577,7 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
"all" => Ok((&stream[1..], Arg::Boolean(Bool::All))), "all" => Ok((&stream[1..], Arg::Boolean(Bool::All))),
_ => unreachable!(), _ => unreachable!(),
}, },
_ => stream _ => stream.get(0)?.get_error(Token::Word(
.get(0)
.ok_or(QueryError::UnexpectedEndOfQuery)?
.get_error(Token::Word(
0, 0,
0, 0,
"one of 'blog', 'author', 'license', 'tags', 'lang', \ "one of 'blog', 'author', 'license', 'tags', 'lang', \
@@ -602,10 +587,10 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
} }
fn parse_l<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], List<'a>)> { fn parse_l<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], List<'a>)> {
match stream.get(0).ok_or(QueryError::UnexpectedEndOfQuery)? { match stream.get(0)? {
Token::LBracket(_) => { Token::LBracket(_) => {
let (left, list) = parse_m(&stream[1..])?; let (left, list) = parse_m(&stream[1..])?;
match left.get(0).ok_or(QueryError::UnexpectedEndOfQuery)? { match left.get(0)? {
Token::RBracket(_) => Ok((&left[1..], List::Array(list))), Token::RBracket(_) => Ok((&left[1..], List::Array(list))),
t => t.get_error(Token::Word(0, 0, "one of ']' or ','")), t => t.get_error(Token::Word(0, 0, "one of ']' or ','")),
} }
@@ -616,20 +601,17 @@ fn parse_l<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Lis
} }
fn parse_m<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Vec<&'a str>)> { fn parse_m<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Vec<&'a str>)> {
let mut res: Vec<&str> = vec![ let mut res: Vec<&str> = Vec::new();
match stream.get(0).ok_or(QueryError::UnexpectedEndOfQuery)? { res.push(match stream.get(0)? {
Token::Word(_, _, w) => w, Token::Word(_, _, w) => w,
t => return t.get_error(Token::Word(0, 0, "any word")), t => return t.get_error(Token::Word(0, 0, "any word")),
}, });
];
stream = &stream[1..]; stream = &stream[1..];
while let Token::Comma(_) = stream[0] { while let Token::Comma(_) = stream[0] {
res.push( res.push(match stream.get(1)? {
match stream.get(1).ok_or(QueryError::UnexpectedEndOfQuery)? {
Token::Word(_, _, w) => w, Token::Word(_, _, w) => w,
t => return t.get_error(Token::Word(0, 0, "any word")), t => return t.get_error(Token::Word(0, 0, "any word")),
}, });
);
stream = &stream[2..]; stream = &stream[2..];
} }
+104 -58
View File
@@ -22,13 +22,17 @@ use openssl::{
}; };
use plume_common::{ use plume_common::{
activity_pub::{ activity_pub::{
ap_accept_header,
inbox::{AsActor, AsObject, FromId}, inbox::{AsActor, AsObject, FromId},
request::get, sign::{gen_keypair, Signer},
sign::{gen_keypair, Error as SignError, Result as SignResult, Signer},
ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY, ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY,
}, },
utils, utils,
}; };
use reqwest::{
header::{HeaderValue, ACCEPT},
ClientBuilder,
};
use riker::actors::{Publish, Tell}; use riker::actors::{Publish, Tell};
use rocket::{ use rocket::{
outcome::IntoOutcome, outcome::IntoOutcome,
@@ -48,7 +52,6 @@ pub enum Role {
Admin = 0, Admin = 0,
Moderator = 1, Moderator = 1,
Normal = 2, Normal = 2,
Instance = 3,
} }
#[derive(Queryable, Identifiable, Clone, Debug, AsChangeset)] #[derive(Queryable, Identifiable, Clone, Debug, AsChangeset)]
@@ -75,7 +78,6 @@ pub struct User {
pub summary_html: SafeString, pub summary_html: SafeString,
/// 0 = admin /// 0 = admin
/// 1 = moderator /// 1 = moderator
/// 3 = local instance
/// anything else = normal user /// anything else = normal user
pub role: i32, pub role: i32,
pub preferred_theme: Option<String>, pub preferred_theme: Option<String>,
@@ -202,35 +204,13 @@ impl User {
} }
} }
/**
* TODO: Should create user record with normalized(lowercased) email
*/
pub fn email_used(conn: &DbConn, email: &str) -> Result<bool> {
use diesel::dsl::{exists, select};
select(exists(
users::table
.filter(users::instance_id.eq(Instance::get_local()?.id))
.filter(users::email.eq(email))
.or_filter(users::email.eq(email.to_ascii_lowercase())),
))
.get_result(&**conn)
.map_err(Error::from)
}
fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result<User> { fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result<User> {
let link = resolve(acct.to_owned(), true)? let link = resolve(acct.to_owned(), true)?
.links .links
.into_iter() .into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json"))) .find(|l| l.mime_type == Some(String::from("application/activity+json")))
.ok_or(Error::Webfinger)?; .ok_or(Error::Webfinger)?;
User::from_id( User::from_id(conn, link.href.as_ref()?, None, CONFIG.proxy()).map_err(|(_, e)| e)
conn,
link.href.as_ref().ok_or(Error::Webfinger)?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)
} }
pub fn fetch_remote_interact_uri(acct: &str) -> Result<String> { pub fn fetch_remote_interact_uri(acct: &str) -> Result<String> {
@@ -243,7 +223,20 @@ impl User {
} }
fn fetch(url: &str) -> Result<CustomPerson> { fn fetch(url: &str) -> Result<CustomPerson> {
let mut res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?; let mut res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()?
.get(url)
.header(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
)
.send()?;
let text = &res.text()?; let text = &res.text()?;
// without this workaround, publicKey is not correctly deserialized // without this workaround, publicKey is not correctly deserialized
let ap_sign = serde_json::from_str::<ApSignature>(text)?; let ap_sign = serde_json::from_str::<ApSignature>(text)?;
@@ -262,10 +255,10 @@ impl User {
conn, conn,
json.object json.object
.object_props .object_props
.icon_image()? // FIXME: Fails when icon is not set .icon_image()?
.object_props .object_props
.url_string()?, .url_string()?,
self, &self,
) )
.ok(); .ok();
@@ -300,6 +293,21 @@ impl User {
bcrypt::hash(pass, 10).map_err(Error::from) bcrypt::hash(pass, 10).map_err(Error::from)
} }
fn ldap_preconn(ldap_conn: &mut LdapConn) -> Result<()> {
let ldap = CONFIG.ldap.as_ref().unwrap();
if let Some((user, password)) = ldap.user.as_ref() {
let bind = ldap_conn
.simple_bind(user, password)
.map_err(|_| Error::NotFound)?;
if bind.success().is_err() {
return Err(Error::NotFound);
}
}
Ok(())
}
fn ldap_register(conn: &Connection, name: &str, password: &str) -> Result<User> { fn ldap_register(conn: &Connection, name: &str, password: &str) -> Result<User> {
if CONFIG.ldap.is_none() { if CONFIG.ldap.is_none() {
return Err(Error::NotFound); return Err(Error::NotFound);
@@ -307,6 +315,9 @@ impl User {
let ldap = CONFIG.ldap.as_ref().unwrap(); let ldap = CONFIG.ldap.as_ref().unwrap();
let mut ldap_conn = LdapConn::new(&ldap.addr).map_err(|_| Error::NotFound)?; let mut ldap_conn = LdapConn::new(&ldap.addr).map_err(|_| Error::NotFound)?;
User::ldap_preconn(&mut ldap_conn)?;
let ldap_name = format!("{}={},{}", ldap.user_name_attr, name, ldap.base_dn); let ldap_name = format!("{}={},{}", ldap.user_name_attr, name, ldap.base_dn);
let bind = ldap_conn let bind = ldap_conn
.simple_bind(&ldap_name, password) .simple_bind(&ldap_name, password)
@@ -353,6 +364,9 @@ impl User {
} else { } else {
return false; return false;
}; };
if User::ldap_preconn(&mut conn).is_err() {
return false;
}
let name = format!( let name = format!(
"{}={},{}", "{}={},{}",
ldap.user_name_attr, &self.username, ldap.base_dn ldap.user_name_attr, &self.username, ldap.base_dn
@@ -434,12 +448,12 @@ impl User {
let last = &format!( let last = &format!(
"{}?page={}", "{}?page={}",
&self.outbox_url, &self.outbox_url,
self.get_activities_count(conn) / i64::from(ITEMS_PER_PAGE) + 1 self.get_activities_count(&conn) / i64::from(ITEMS_PER_PAGE) + 1
); );
coll.collection_props.set_first_link(Id::new(first))?; coll.collection_props.set_first_link(Id::new(first))?;
coll.collection_props.set_last_link(Id::new(last))?; coll.collection_props.set_last_link(Id::new(last))?;
coll.collection_props coll.collection_props
.set_total_items_u64(self.get_activities_count(conn) as u64)?; .set_total_items_u64(self.get_activities_count(&conn) as u64)?;
Ok(ActivityStream::new(coll)) Ok(ActivityStream::new(coll))
} }
pub fn outbox_page( pub fn outbox_page(
@@ -448,7 +462,7 @@ impl User {
(min, max): (i32, i32), (min, max): (i32, i32),
) -> Result<ActivityStream<OrderedCollectionPage>> { ) -> Result<ActivityStream<OrderedCollectionPage>> {
let acts = self.get_activities_page(conn, (min, max))?; let acts = self.get_activities_page(conn, (min, max))?;
let n_acts = self.get_activities_count(conn); let n_acts = self.get_activities_count(&conn);
let mut coll = OrderedCollectionPage::default(); let mut coll = OrderedCollectionPage::default();
if n_acts - i64::from(min) >= i64::from(ITEMS_PER_PAGE) { if n_acts - i64::from(min) >= i64::from(ITEMS_PER_PAGE) {
coll.collection_page_props.set_next_link(Id::new(&format!( coll.collection_page_props.set_next_link(Id::new(&format!(
@@ -470,7 +484,20 @@ impl User {
Ok(ActivityStream::new(coll)) Ok(ActivityStream::new(coll))
} }
fn fetch_outbox_page<T: Activity>(&self, url: &str) -> Result<(Vec<T>, Option<String>)> { fn fetch_outbox_page<T: Activity>(&self, url: &str) -> Result<(Vec<T>, Option<String>)> {
let mut res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?; let mut res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()?
.get(url)
.header(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
)
.send()?;
let text = &res.text()?; let text = &res.text()?;
let json: serde_json::Value = serde_json::from_str(text)?; let json: serde_json::Value = serde_json::from_str(text)?;
let items = json["items"] let items = json["items"]
@@ -480,15 +507,27 @@ impl User {
.filter_map(|j| serde_json::from_value(j.clone()).ok()) .filter_map(|j| serde_json::from_value(j.clone()).ok())
.collect::<Vec<T>>(); .collect::<Vec<T>>();
let next = json.get("next").map(|x| x.as_str().unwrap().to_owned()); let next = match json.get("next") {
Some(x) => Some(x.as_str().unwrap().to_owned()),
None => None,
};
Ok((items, next)) Ok((items, next))
} }
pub fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> { pub fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> {
let mut res = get( let mut res = ClientBuilder::new()
&self.outbox_url[..], .connect_timeout(Some(std::time::Duration::from_secs(5)))
Self::get_sender(), .build()?
CONFIG.proxy().cloned(), .get(&self.outbox_url[..])
)?; .header(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
)
.send()?;
let text = &res.text()?; let text = &res.text()?;
let json: serde_json::Value = serde_json::from_str(text)?; let json: serde_json::Value = serde_json::from_str(text)?;
if let Some(first) = json.get("first") { if let Some(first) = json.get("first") {
@@ -498,7 +537,7 @@ impl User {
if page.is_empty() { if page.is_empty() {
break; break;
} }
items.append(&mut page); items.extend(page.drain(..));
if let Some(n) = nxt { if let Some(n) = nxt {
if n == next { if n == next {
break; break;
@@ -520,11 +559,20 @@ impl User {
} }
pub fn fetch_followers_ids(&self) -> Result<Vec<String>> { pub fn fetch_followers_ids(&self) -> Result<Vec<String>> {
let mut res = get( let mut res = ClientBuilder::new()
&self.followers_endpoint[..], .connect_timeout(Some(std::time::Duration::from_secs(5)))
Self::get_sender(), .build()?
CONFIG.proxy().cloned(), .get(&self.followers_endpoint[..])
)?; .header(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
)
.send()?;
let text = &res.text()?; let text = &res.text()?;
let json: serde_json::Value = serde_json::from_str(text)?; let json: serde_json::Value = serde_json::from_str(text)?;
Ok(json["items"] Ok(json["items"]
@@ -696,7 +744,7 @@ impl User {
pub fn get_keypair(&self) -> Result<PKey<Private>> { pub fn get_keypair(&self) -> Result<PKey<Private>> {
PKey::from_rsa(Rsa::private_key_from_pem( PKey::from_rsa(Rsa::private_key_from_pem(
self.private_key.clone().ok_or(Error::Signature)?.as_ref(), self.private_key.clone()?.as_ref(),
)?) )?)
.map_err(Error::from) .map_err(Error::from)
} }
@@ -919,7 +967,7 @@ impl FromId<DbConn> for User {
fn from_activity(conn: &DbConn, acct: CustomPerson) -> Result<Self> { fn from_activity(conn: &DbConn, acct: CustomPerson) -> Result<Self> {
let url = Url::parse(&acct.object.object_props.id_string()?)?; let url = Url::parse(&acct.object.object_props.id_string()?)?;
let inst = url.host_str().ok_or(Error::Url)?; let inst = url.host_str()?;
let instance = Instance::find_by_domain(conn, inst).or_else(|_| { let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
Instance::insert( Instance::insert(
conn, conn,
@@ -1007,10 +1055,6 @@ impl FromId<DbConn> for User {
Ok(user) Ok(user)
} }
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
} }
impl AsActor<&DbConn> for User { impl AsActor<&DbConn> for User {
@@ -1043,22 +1087,24 @@ impl AsObject<User, Delete, &DbConn> for User {
} }
impl Signer for User { impl Signer for User {
type Error = Error;
fn get_key_id(&self) -> String { fn get_key_id(&self) -> String {
format!("{}#main-key", self.ap_url) format!("{}#main-key", self.ap_url)
} }
fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> { fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
let key = self.get_keypair().map_err(|_| SignError())?; let key = self.get_keypair()?;
let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?; let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?;
signer.update(to_sign.as_bytes())?; signer.update(to_sign.as_bytes())?;
signer.sign_to_vec().map_err(SignError::from) signer.sign_to_vec().map_err(Error::from)
} }
fn verify(&self, data: &str, signature: &[u8]) -> SignResult<bool> { fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?; let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?; let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?;
verifier.update(data.as_bytes())?; verifier.update(data.as_bytes())?;
verifier.verify(signature).map_err(SignError::from) verifier.verify(&signature).map_err(Error::from)
} }
} }
@@ -1099,7 +1145,7 @@ impl NewUser {
display_name, display_name,
role: role as i32, role: role as i32,
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: password, hashed_password: password,
instance_id: instance.id, instance_id: instance.id,
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Afrikaans\n" "Language-Team: Afrikaans\n"
"Language: af_ZA\n" "Language: af_ZA\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Arabic\n" "Language-Team: Arabic\n"
"Language: ar_SA\n" "Language: ar_SA\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "فتح محرر النصوص الغني" msgstr "فتح محرر النصوص الغني"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "العنوان" msgstr "العنوان"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "العنوان الثانوي أو الملخص" msgstr "العنوان الثانوي أو الملخص"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "اكتب مقالك هنا. ماركداون مُدَعَّم." msgstr "اكتب مقالك هنا. ماركداون مُدَعَّم."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "يتبقا {} حرفا تقريبا" msgstr "يتبقا {} حرفا تقريبا"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "الوسوم" msgstr "الوسوم"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "الرخصة" msgstr "الرخصة"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "الغلاف" msgstr "الغلاف"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "هذه مسودة" msgstr "هذه مسودة"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "نشر كتابا" msgstr "نشر كتابا"
+13 -13
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Bulgarian\n" "Language-Team: Bulgarian\n"
"Language: bg_BG\n" "Language: bg_BG\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "Искате ли да активирате локално автоматично запаметяване, последно редактирано в {}?" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Отворете редактора с богат текст" msgstr "Отворете редактора с богат текст"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Заглавие" msgstr "Заглавие"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Подзаглавие или резюме" msgstr "Подзаглавие или резюме"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Напишете статията си тук. Поддържа се Markdown." msgstr "Напишете статията си тук. Поддържа се Markdown."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Остават {} знака вляво" msgstr "Остават {} знака вляво"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Етикети" msgstr "Етикети"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Лиценз" msgstr "Лиценз"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Основно изображение" msgstr "Основно изображение"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Това е проект" msgstr "Това е проект"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Публикувай" msgstr "Публикувай"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Catalan\n" "Language-Team: Catalan\n"
"Language: ca_ES\n" "Language: ca_ES\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Obre leditor de text enriquit" msgstr "Obre leditor de text enriquit"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Títol" msgstr "Títol"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Subtítol o resum" msgstr "Subtítol o resum"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Escriviu el vostre article ací. Podeu fer servir el Markdown." msgstr "Escriviu el vostre article ací. Podeu fer servir el Markdown."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Queden uns {} caràcters" msgstr "Queden uns {} caràcters"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Etiquetes" msgstr "Etiquetes"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Llicència" msgstr "Llicència"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Coberta" msgstr "Coberta"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Açò és un esborrany" msgstr "Açò és un esborrany"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Publica" msgstr "Publica"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Czech\n" "Language-Team: Czech\n"
"Language: cs_CZ\n" "Language: cs_CZ\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Otevřít editor formátovaného textu" msgstr "Otevřít editor formátovaného textu"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Nadpis" msgstr "Nadpis"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Podnadpis, nebo shrnutí" msgstr "Podnadpis, nebo shrnutí"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Sem napište svůj článek. Markdown je podporován." msgstr "Sem napište svůj článek. Markdown je podporován."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Zbývá kolem {} znaků" msgstr "Zbývá kolem {} znaků"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Tagy" msgstr "Tagy"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licence" msgstr "Licence"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Titulka" msgstr "Titulka"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Tohle je koncept" msgstr "Tohle je koncept"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Zveřejnit" msgstr "Zveřejnit"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Danish\n" "Language-Team: Danish\n"
"Language: da_DK\n" "Language: da_DK\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+13 -13
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: German\n" "Language-Team: German\n"
"Language: de_DE\n" "Language: de_DE\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "Möchten Sie die lokale automatische Speicherung laden, die zuletzt um {} bearbeitet wurde?" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr " Rich Text Editor (RTE) öffnen" msgstr " Rich Text Editor (RTE) öffnen"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Titel" msgstr "Titel"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Untertitel oder Zusammenfassung" msgstr "Untertitel oder Zusammenfassung"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Schreiben deinen Artikel hier. Markdown wird unterstützt." msgstr "Schreiben deinen Artikel hier. Markdown wird unterstützt."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Ungefähr {} Zeichen übrig" msgstr "Ungefähr {} Zeichen übrig"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Schlagwörter" msgstr "Schlagwörter"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Lizenz" msgstr "Lizenz"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Einband" msgstr "Einband"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Dies ist ein Entwurf" msgstr "Dies ist ein Entwurf"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Veröffentlichen" msgstr "Veröffentlichen"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Greek\n" "Language-Team: Greek\n"
"Language: el_GR\n" "Language: el_GR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: English\n" "Language-Team: English\n"
"Language: en_US\n" "Language: en_US\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Esperanto\n" "Language-Team: Esperanto\n"
"Language: eo_UY\n" "Language: eo_UY\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Malfermi la riĉan redaktilon" msgstr "Malfermi la riĉan redaktilon"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Titolo" msgstr "Titolo"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Verku vian artikolon ĉi tie. Markdown estas subtenita." msgstr "Verku vian artikolon ĉi tie. Markdown estas subtenita."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Proksimume {} signoj restantaj" msgstr "Proksimume {} signoj restantaj"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Etikedoj" msgstr "Etikedoj"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Permesilo" msgstr "Permesilo"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Kovro" msgstr "Kovro"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Malfinias" msgstr "Malfinias"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Eldoni" msgstr "Eldoni"
+13 -13
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Spanish\n" "Language-Team: Spanish\n"
"Language: es_ES\n" "Language: es_ES\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "¿Quieres cargar el guardado automático local editado por última vez en {}?" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Abrir el editor de texto enriquecido" msgstr "Abrir el editor de texto enriquecido"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Título" msgstr "Título"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Subtítulo, o resumen" msgstr "Subtítulo, o resumen"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Escriba su artículo aquí. Puede utilizar Markdown." msgstr "Escriba su artículo aquí. Puede utilizar Markdown."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Quedan unos {} caracteres" msgstr "Quedan unos {} caracteres"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Etiquetas" msgstr "Etiquetas"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licencia" msgstr "Licencia"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Cubierta" msgstr "Cubierta"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Esto es un borrador" msgstr "Esto es un borrador"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Publicar" msgstr "Publicar"
+15 -15
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Persian\n" "Language-Team: Persian\n"
"Language: fa_IR\n" "Language: fa_IR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "آیا می‌خواهید نسخهٔ ذخیره شدهٔ خودکار محلّی از آخرین ویرایش در {} را بار کنید؟" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "باز کردن ویرایش‌گر غنی" msgstr "باز کردن ویرایش‌گر غنی"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "عنوان" msgstr "عنوان"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "زیرعنوان، یا چکیده" msgstr "زیرعنوان، یا چکیده"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "مقاله‌تان را اینجا بنویسید. از مارک‌داون پشتیبانی می‌شود." msgstr "مقاله را اینجا بنویسید. از مارک‌داون پشتیبانی می‌شود."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "نزدیک به {} حرف باقی مانده است" msgstr "نزدیک به {} حرف باقی مانده است"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "برچسب‌ها" msgstr "برچسب‌ها"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "پروانه" msgstr "پروانه"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "جلد" msgstr "تصویر شاخص"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "این، یک پیش‌نویس است" msgstr "این، یک پیش‌نویس است"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "انتشار" msgstr "انتشار"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Finnish\n" "Language-Team: Finnish\n"
"Language: fi_FI\n" "Language: fi_FI\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Avaa edistynyt tekstieditori" msgstr "Avaa edistynyt tekstieditori"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Otsikko" msgstr "Otsikko"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Alaotsikko tai tiivistelmä" msgstr "Alaotsikko tai tiivistelmä"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Kirjoita artikkelisi tähän. Markdown -kuvauskieli on tuettu." msgstr "Kirjoita artikkelisi tähän. Markdown -kuvauskieli on tuettu."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "%{count} merkkiä jäljellä" msgstr "%{count} merkkiä jäljellä"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Tagit" msgstr "Tagit"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Lisenssi" msgstr "Lisenssi"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Kansi" msgstr "Kansi"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Tämä on luonnos" msgstr "Tämä on luonnos"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Julkaise" msgstr "Julkaise"
+13 -13
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: French\n" "Language-Team: French\n"
"Language: fr_FR\n" "Language: fr_FR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "Voulez vous charger la sauvegarde automatique locale, éditée la dernière fois à {}?" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Ouvrir l'éditeur de texte avancé" msgstr "Ouvrir l'éditeur de texte avancé"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Titre" msgstr "Titre"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Sous-titre ou résumé" msgstr "Sous-titre ou résumé"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Écrivez votre article ici. Vous pouvez utiliser du Markdown." msgstr "Écrivez votre article ici. Vous pouvez utiliser du Markdown."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Environ {} caractères restant" msgstr "Environ {} caractères restant"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Étiquettes" msgstr "Étiquettes"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licence" msgstr "Licence"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Illustration" msgstr "Illustration"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Ceci est un brouillon" msgstr "Ceci est un brouillon"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Publier" msgstr "Publier"
+13 -13
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Galician\n" "Language-Team: Galician\n"
"Language: gl_ES\n" "Language: gl_ES\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "Queres cargar a última copia gardada editada o {}?" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Abre o editor de texto enriquecido" msgstr "Abre o editor de texto enriquecido"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Título" msgstr "Título"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Subtítulo, ou resumo" msgstr "Subtítulo, ou resumo"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Escribe aquí o teu artigo: podes utilizar Markdown." msgstr "Escribe aquí o teu artigo: podes utilizar Markdown."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Dispós de {} caracteres" msgstr "Dispós de {} caracteres"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Etiquetas" msgstr "Etiquetas"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licenza" msgstr "Licenza"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Portada" msgstr "Portada"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Este é un borrador" msgstr "Este é un borrador"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Publicar" msgstr "Publicar"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Hebrew\n" "Language-Team: Hebrew\n"
"Language: he_IL\n" "Language: he_IL\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Hindi\n" "Language-Team: Hindi\n"
"Language: hi_IN\n" "Language: hi_IN\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "शीर्षक" msgstr "शीर्षक"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "अपना आर्टिकल या लेख यहाँ लिखें. Markdown उपलब्ध है." msgstr "अपना आर्टिकल या लेख यहाँ लिखें. Markdown उपलब्ध है."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "लगभग {} अक्षर बाकी हैं" msgstr "लगभग {} अक्षर बाकी हैं"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "टैग्स" msgstr "टैग्स"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "लाइसेंस" msgstr "लाइसेंस"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "पब्लिश करें" msgstr "पब्लिश करें"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Croatian\n" "Language-Team: Croatian\n"
"Language: hr_HR\n" "Language: hr_HR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Naslov" msgstr "Naslov"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Tagovi" msgstr "Tagovi"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licenca" msgstr "Licenca"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Objavi" msgstr "Objavi"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Hungarian\n" "Language-Team: Hungarian\n"
"Language: hu_HU\n" "Language: hu_HU\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Italian\n" "Language-Team: Italian\n"
"Language: it_IT\n" "Language: it_IT\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Apri il compositore di testo avanzato" msgstr "Apri il compositore di testo avanzato"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Titolo" msgstr "Titolo"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Sottotitolo, o sommario" msgstr "Sottotitolo, o sommario"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Scrivi qui il tuo articolo. È supportato il Markdown." msgstr "Scrivi qui il tuo articolo. È supportato il Markdown."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Circa {} caratteri rimasti" msgstr "Circa {} caratteri rimasti"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Etichette" msgstr "Etichette"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licenza" msgstr "Licenza"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Copertina" msgstr "Copertina"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Questa è una bozza" msgstr "Questa è una bozza"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Pubblica" msgstr "Pubblica"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Japanese\n" "Language-Team: Japanese\n"
"Language: ja_JP\n" "Language: ja_JP\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "リッチテキストエディターを開く" msgstr "リッチテキストエディターを開く"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "タイトル" msgstr "タイトル"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "サブタイトル、または概要" msgstr "サブタイトル、または概要"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "投稿をここに書きます。Markdown がサポートされています。" msgstr "投稿をここに書きます。Markdown がサポートされています。"
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "残り約 {} 文字" msgstr "残り約 {} 文字"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "タグ" msgstr "タグ"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "ライセンス" msgstr "ライセンス"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "カバー" msgstr "カバー"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "これは下書きです" msgstr "これは下書きです"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "公開" msgstr "公開"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Korean\n" "Language-Team: Korean\n"
"Language: ko_KR\n" "Language: ko_KR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+13 -13
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Dutch\n" "Language-Team: Dutch\n"
"Language: nl_NL\n" "Language: nl_NL\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "Wilt u de lokale auto-opslaan laden? Laatst bewerkt om: {}" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Open de rich-text editor" msgstr "Open de rich-text editor"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Titel" msgstr "Titel"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Ondertitel of samenvatting" msgstr "Ondertitel of samenvatting"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Schrijf hier je artikel. Markdown wordt ondersteund." msgstr "Schrijf hier je artikel. Markdown wordt ondersteund."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Ongeveer {} tekens over" msgstr "Ongeveer {} tekens over"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Tags" msgstr "Tags"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licentie" msgstr "Licentie"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Hoofdafbeelding" msgstr "Hoofdafbeelding"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Dit is een concept" msgstr "Dit is een concept"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Publiceren" msgstr "Publiceren"
+14 -14
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Norwegian\n" "Language-Team: Norwegian\n"
"Language: no_NO\n" "Language: no_NO\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Lisens" msgstr ""
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Publiser" msgstr ""
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Polish\n" "Language-Team: Polish\n"
"Language: pl_PL\n" "Language: pl_PL\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Otwórz edytor tekstu sformatowanego" msgstr "Otwórz edytor tekstu sformatowanego"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Tytuł" msgstr "Tytuł"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Podtytuł, lub podsumowanie" msgstr "Podtytuł, lub podsumowanie"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Napisz swój artykuł tutaj. Markdown jest obsługiwany." msgstr "Napisz swój artykuł tutaj. Markdown jest obsługiwany."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Pozostało w okolicy {} znaków" msgstr "Pozostało w okolicy {} znaków"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Tagi" msgstr "Tagi"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licencja" msgstr "Licencja"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Okładka" msgstr "Okładka"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "To jest szkic" msgstr "To jest szkic"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Publikuj" msgstr "Publikuj"
+11 -11
View File
@@ -12,46 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:188
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:281
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:314
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:318
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:325
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:336
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:413
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:414
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:417
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:437
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:444
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+13 -13
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Portuguese, Brazilian\n" "Language-Team: Portuguese, Brazilian\n"
"Language: pt_BR\n" "Language: pt_BR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "Você quer carregar o último conteúdo salvo localmente editado em {}?" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Abrir o editor de rich text" msgstr "Abrir o editor de rich text"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Título" msgstr "Título"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Subtítulo ou resumo" msgstr "Subtítulo ou resumo"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Escreva seu artigo aqui. Markdown é suportado." msgstr "Escreva seu artigo aqui. Markdown é suportado."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Cerca de {} caracteres restantes" msgstr "Cerca de {} caracteres restantes"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Tags" msgstr "Tags"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licença" msgstr "Licença"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Capa" msgstr "Capa"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Isso é um rascunho" msgstr "Isso é um rascunho"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Publicar" msgstr "Publicar"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Romanian\n" "Language-Team: Romanian\n"
"Language: ro_RO\n" "Language: ro_RO\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Deschide editorul de text" msgstr "Deschide editorul de text"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Titlu" msgstr "Titlu"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Scrie articolul tău aici. Markdown este acceptat." msgstr "Scrie articolul tău aici. Markdown este acceptat."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "În apropiere de {} caractere rămase" msgstr "În apropiere de {} caractere rămase"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Etichete" msgstr "Etichete"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licenţă" msgstr "Licenţă"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Coperta" msgstr "Coperta"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Aceasta este o ciornă" msgstr "Aceasta este o ciornă"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Publică" msgstr "Publică"
+16 -16
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Russian\n" "Language-Team: Russian\n"
"Language: ru_RU\n" "Language: ru_RU\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "Хотите ли вы загрузить локальное автосохранение, сделанное в {}?" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Открыть в визуальном редакторе" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Заголовок" msgstr "Заголовок"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Подзаголовок или резюме" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Пишите свою статью здесь. Markdown поддерживается." msgstr "Пишите свою статью здесь. Markdown поддерживается."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Осталось около {} символов" msgstr "Осталось около {} символов"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Теги" msgstr "Теги"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Лицензия" msgstr ""
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Обложка" msgstr "Обложка"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Это черновик" msgstr "Это черновик"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Опубликовать" msgstr "Опубликовать"
+18 -18
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Santali\n" "Language-Team: Santali\n"
"Language: sat_IN\n" "Language: sat_IN\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "ᱟᱢ ᱪᱮᱫ ᱢᱟᱲᱟᱝ {} ᱨᱮ ᱥᱟᱯᱲᱟᱣ ᱟᱠᱟᱱ ᱞᱚᱠᱟᱞ ᱚᱴᱚᱥᱮᱣ ᱞᱟᱫᱮ ᱥᱟᱱᱟᱢ ᱠᱟᱱᱟ ᱥᱮ?" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "ᱨᱤᱪ ᱚᱞ ᱥᱟᱯᱟᱣᱤᱡ ᱠᱷᱩᱞᱟᱹᱭ ᱢᱮ" msgstr "ᱨᱤᱪ ᱚᱞ ᱥᱟᱯᱟᱣᱤᱡ ᱠᱷᱩᱞᱟᱹᱭ ᱢᱮ"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "ᱴᱭᱴᱚᱞ" msgstr "ᱴᱭᱴᱚᱞ"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "ᱥᱟᱹᱵᱴᱟᱭᱴᱟᱹᱞ, ᱟᱨ ᱵᱟᱝ ᱥᱟᱹᱢᱢᱟᱨᱭ" msgstr "ᱥᱟᱹᱵᱴᱟᱭᱴᱟᱹᱞ, ᱟᱨ ᱵᱟᱝ ᱥᱟᱹᱢᱢᱟᱨᱭ"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "ᱟᱢᱟᱜ ᱚᱱᱚᱞ ᱱᱚᱰᱮ ᱚᱞ ᱛᱟᱢ ᱾ ᱪᱤᱱᱦᱟᱹ ᱥᱟᱯᱚᱴ ᱜᱮᱭᱟ ᱾" msgstr "ᱟᱢᱟᱜ ᱚᱱᱚᱞ ᱱᱚᱰᱮ ᱚᱞ ᱛᱟᱢ ᱾ ᱪᱤᱱᱦᱟᱹ ᱥᱟᱯᱚᱴ ᱜᱮᱭᱟ ᱾"
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "ᱡᱷᱚᱛᱚ ᱨᱮ {} ᱡᱤᱱᱤᱥ ᱵᱟᱧᱪᱟᱣᱠᱟᱱᱟ" msgstr "ᱡᱷᱚᱛᱚ ᱨᱮ {} ᱡᱤᱱᱤᱥ ᱵᱟᱧᱪᱟᱣᱠᱟᱱᱟ"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "ᱴᱮᱜᱥ" msgstr "ᱴᱮᱜᱥ"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "ᱞᱚᱭᱥᱮᱱᱥ" msgstr "ᱞᱚᱭᱥᱮᱱᱥ"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "ᱢᱚᱭᱞᱟᱹᱴ" msgstr "ᱠᱚᱵᱷᱚᱨ"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "ᱱᱚᱶᱟ ᱫᱚ ᱰᱨᱟᱯᱷᱼᱴ ᱠᱟᱱᱟ" msgstr "ᱱᱚᱶᱟ ᱫᱚ ᱰᱨᱟᱯᱷᱼᱴ ᱠᱟᱱᱟ"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "ᱯᱟᱨᱥᱟᱞ" msgstr "ᱯᱟᱨᱥᱟᱞ"
+14 -14
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Sinhala\n" "Language-Team: Sinhala\n"
"Language: si_LK\n" "Language: si_LK\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "මාතෘකාව" msgstr ""
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "බලපත්‍රය" msgstr ""
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+13 -13
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Slovak\n" "Language-Team: Slovak\n"
"Language: sk_SK\n" "Language: sk_SK\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "Chceš načítať automaticky uloženú zálohu, s poslednou úpravou {}?" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Otvor editor formátovaného textu" msgstr "Otvor editor formátovaného textu"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Nadpis" msgstr "Nadpis"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Zhrnutie, alebo podnadpis" msgstr "Zhrnutie, alebo podnadpis"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Tu napíš svoj článok. Markdown je podporovaný." msgstr "Tu napíš svoj článok. Markdown je podporovaný."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Zostáva asi {} znakov" msgstr "Zostáva asi {} znakov"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Štítky" msgstr "Štítky"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licencia" msgstr "Licencia"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Obálka" msgstr "Obálka"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Toto je koncept" msgstr "Toto je koncept"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Zverejniť" msgstr "Zverejniť"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Slovenian\n" "Language-Team: Slovenian\n"
"Language: sl_SI\n" "Language: sl_SI\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Naslov" msgstr "Naslov"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Oznake" msgstr "Oznake"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licenca" msgstr "Licenca"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "To je osnutek" msgstr "To je osnutek"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Objavi" msgstr "Objavi"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Serbian (Latin)\n" "Language-Team: Serbian (Latin)\n"
"Language: sr_CS\n" "Language: sr_CS\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Otvori uređivač sa stilizacijom" msgstr "Otvori uređivač sa stilizacijom"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Naslov" msgstr "Naslov"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Podnaslov, ili sažetak" msgstr "Podnaslov, ili sažetak"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Napišite vaš članak ovde. Na raspolaganju vam je Markdown." msgstr "Napišite vaš članak ovde. Na raspolaganju vam je Markdown."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Preostalo oko {} znakova" msgstr "Preostalo oko {} znakova"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Markeri" msgstr "Markeri"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licenca" msgstr "Licenca"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Naslovna strana" msgstr "Naslovna strana"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Ovo je nacrt" msgstr "Ovo je nacrt"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Objavi" msgstr "Objavi"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Swedish\n" "Language-Team: Swedish\n"
"Language: sv_SE\n" "Language: sv_SE\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Titel" msgstr "Titel"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Skriv din artikel här. Markdown stöds." msgstr "Skriv din artikel här. Markdown stöds."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Ungefär {} karaktärer kvar" msgstr "Ungefär {} karaktärer kvar"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Taggar" msgstr "Taggar"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licens" msgstr "Licens"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Omslag" msgstr "Omslag"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Publicera" msgstr "Publicera"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Turkish\n" "Language-Team: Turkish\n"
"Language: tr_TR\n" "Language: tr_TR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Zengin metin editörünü (RTE) aç" msgstr "Zengin metin editörünü (RTE) aç"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Başlık" msgstr "Başlık"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Alt başlık, veya açıklama" msgstr "Alt başlık, veya açıklama"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Makaleni buraya yaz. Markdown kullanabilirsin." msgstr "Makaleni buraya yaz. Markdown kullanabilirsin."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Yaklaşık {} karakter kaldı" msgstr "Yaklaşık {} karakter kaldı"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Etiketler" msgstr "Etiketler"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Lisans" msgstr "Lisans"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Kapak" msgstr "Kapak"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Bu bir taslaktır" msgstr "Bu bir taslaktır"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Yayınla" msgstr "Yayınla"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Ukrainian\n" "Language-Team: Ukrainian\n"
"Language: uk_UA\n" "Language: uk_UA\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
-63
View File
@@ -1,63 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Urdu (Pakistan)\n"
"Language: ur_PK\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: ur-PK\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:385
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:518
msgid "License"
msgstr ""
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr ""

Some files were not shown because too many files have changed in this diff Show More