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
default: false
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:
description: run command with environment for coverage
+1 -1
View File
@@ -11,7 +11,7 @@ RUN apt update &&\
rm -rf /var/lib/apt/lists/*
#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 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
# 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
# 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
## [[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
- Allow `dir` attributes for LtoR text in RtoL document (#860)
@@ -31,31 +11,19 @@
- Proxy support (#829)
- Riker a actor system library (#870)
- (request-target) and Host header in HTTP Signature (#872)
- Default log levels for RUST_LOG (#885, #886, #919)
### Changed
- Upgrade some dependent crates (#858)
- 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)
- 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
- Percent-encode URI for remote_interact (#866, #857)
- 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
@@ -236,9 +204,7 @@
- Ability to create multiple blogs
<!-- next-url -->
[Unreleased]: https://github.com/Plume-org/Plume/compare/0.7.1...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
[Unreleased]: https://github.com/Plume-org/Plume/compare/0.6.0...HEAD
[[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.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]
authors = ["Plume contributors"]
name = "plume"
version = "0.7.1"
version = "0.6.1-dev"
repository = "https://github.com/Plume-org/Plume"
edition = "2018"
[dependencies]
activitypub = "0.1.3"
askama_escape = "0.1"
atom_syndication = "0.6"
clap = "2.33"
dotenv = "0.15.0"
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
guid-create = "0.2"
guid-create = "0.1"
heck = "0.3.0"
lettre = "0.9.2"
lettre_email = "0.9.2"
num_cpus = "1.10"
rocket = "0.4.6"
rocket_contrib = { version = "0.4.5", features = ["json"] }
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
rpassword = "4.0"
scheduled-thread-pool = "0.2.2"
serde = "1.0"
serde_json = "1.0.70"
shrinkwraprs = "0.3.0"
validator = { version = "0.14", features = ["derive"] }
serde_json = "1.0"
shrinkwraprs = "0.2.1"
validator = "0.8"
validator_derive = "0.8"
webfinger = "0.4.1"
tracing = "0.1.22"
tracing-subscriber = "0.3.5"
tracing-subscriber = "0.2.15"
riker = "0.4.2"
[[bin]]
@@ -48,7 +53,7 @@ version = "1.4.5"
[dependencies.multipart]
default-features = false
features = ["server"]
version = "0.18"
version = "0.16"
[dependencies.plume-api]
path = "plume-api"
@@ -65,7 +70,7 @@ rev = "29910f2829e7e590a540da3804336577b48c7b31"
[build-dependencies]
ructe = "0.13.0"
rsass = "0.23"
rsass = "0.9"
[features]
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.
- **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.
- **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
+1 -8
View File
@@ -98,7 +98,7 @@ main article {
}
blockquote {
border-inline-start: 5px solid $gray;
border-left: 5px solid $gray;
margin: 1em auto;
padding: 0em 2em;
}
@@ -516,11 +516,4 @@ input:checked ~ .cw-container > .cw-text {
main .article-meta > *, main .article-meta .comments, main .article-meta > .banner > * {
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;
}
.cover-link {
margin: 0;
&:hover {
opacity: 0.9;
}
}
.cover {
min-height: 10em;
background-position: center;
background-size: cover;
margin: 0px;
}
header {
display: flex;
&:hover {
opacity: 0.9;
}
}
h3 {
@@ -244,14 +236,9 @@ p.error {
font-family: $playfair;
font-size: 1.75em;
font-weight: normal;
line-height: 1.10;
display: inline-block;
position: relative;
line-height: 1.75;
a {
display: block;
width: 100%;
height: 100%;
padding-block-start: 0.5em;
transition: color 0.1s ease-in;
color: $text-color;
@@ -260,8 +247,7 @@ p.error {
}
.controls {
flex-shrink: 0;
text-align: end;
float: right;
.button {
margin-top: 0;
@@ -275,7 +261,7 @@ p.error {
font-family: $lora;
font-size: 1em;
line-height: 1.25;
text-align: initial;
text-align: left;
overflow: hidden;
}
}
@@ -479,10 +465,9 @@ figure {
/// Avatars
.avatar {
background-position: center !important;
background-position: center;
background-size: cover;
border-radius: 100%;
flex-shrink: 0;
&.small {
width: 50px;
@@ -576,6 +561,14 @@ figure {
}
}
.bottom-bar {
flex-direction: column;
align-items: center;
& > div {
margin: 0;
}
}
main .article-meta .comments .comment {
header {
flex-direction: column;
+7 -13
View File
@@ -41,9 +41,9 @@ fn main() {
.expect("compile templates");
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");
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");
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()
.skip_while(|c| *c != Component::Normal(OsStr::new("themes")))
.skip(1)
.map(|c| {
.filter_map(|c| {
c.as_os_str()
.to_str()
.unwrap_or_default()
.split_once('.')
.map_or(c.as_os_str().to_str().unwrap_or_default(), |x| x.0)
.splitn(2, '.')
.next()
})
.collect::<Vec<_>>()
.join("-");
@@ -120,14 +120,8 @@ fn compile_theme(path: &Path, out_dir: &Path) -> std::io::Result<()> {
// compile the .scss/.sass file
let mut out = File::create(out.join("theme.css"))?;
out.write_all(
&rsass::compile_scss_path(
path,
rsass::output::Format {
style: rsass::output::Style::Compressed,
..rsass::output::Format::default()
},
)
.expect("SCSS compilation error"),
&rsass::compile_scss_file(path, rsass::OutputStyle::Compressed)
.expect("SCSS compilation error"),
)?;
Ok(())
@@ -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]
name = "plume-api"
version = "0.7.1"
version = "0.6.1-dev"
authors = ["Plume contributors"]
edition = "2018"
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "plume-cli"
version = "0.7.1"
version = "0.6.1-dev"
authors = ["Plume contributors"]
edition = "2018"
@@ -10,7 +10,7 @@ path = "src/main.rs"
[dependencies]
clap = "2.33"
dotenv = "0.15"
dotenv = "0.14"
rpassword = "5.0.0"
[dependencies.diesel]
-2
View File
@@ -68,6 +68,4 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
},
)
.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(),
}
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() {
("instance", Some(args)) => {
+6 -11
View File
@@ -1,6 +1,6 @@
[package]
name = "plume-common"
version = "0.7.1"
version = "0.6.1-dev"
authors = ["Plume contributors"]
edition = "2018"
@@ -9,21 +9,21 @@ activitypub = "0.1.1"
activitystreams-derive = "0.1.1"
activitystreams-traits = "0.1.0"
array_tool = "1.0"
base64 = "0.13"
heck = "0.4.0"
hex = "0.4"
base64 = "0.10"
heck = "0.3.0"
hex = "0.3"
hyper = "0.12.33"
openssl = "0.10.22"
rocket = "0.4.6"
reqwest = { version = "0.9", features = ["socks"] }
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0.70"
serde_json = "1.0"
shrinkwraprs = "0.3.0"
syntect = "4.5.0"
tokio = "0.1.22"
regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] }
tracing = "0.1.22"
askama_escape = "0.10.2"
[dependencies.chrono]
features = ["serde"]
@@ -33,8 +33,3 @@ version = "0.4"
default-features = false
git = "https://git.joinplu.me/Plume/pulldown-cmark"
branch = "bidi-plume"
[dev-dependencies]
once_cell = "1.5.2"
[features]
+31 -174
View File
@@ -1,8 +1,6 @@
use reqwest;
use reqwest::header::{HeaderValue, ACCEPT};
use std::fmt::Debug;
use super::{request, sign::Signer};
/// Represents an ActivityPub inbox.
///
/// It routes an incoming Activity through the registered handlers.
@@ -12,50 +10,7 @@ use super::{request, sign::Signer};
/// ```rust
/// # extern crate activitypub;
/// # 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::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;
/// # impl FromId<()> for User {
/// # type Error = ();
@@ -68,10 +23,6 @@ use super::{request, sign::Signer};
/// # fn from_activity(_: &(), obj: Person) -> Result<Self, Self::Error> {
/// # Ok(User)
/// # }
/// #
/// # fn get_sender() -> &'static dyn Signer {
/// # &*MY_SIGNER
/// # }
/// # }
/// # impl AsActor<&()> for User {
/// # fn get_inbox_url(&self) -> String {
@@ -91,10 +42,6 @@ use super::{request, sign::Signer};
/// # fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> {
/// # Ok(Message)
/// # }
/// #
/// # fn get_sender() -> &'static dyn Signer {
/// # &*MY_SIGNER
/// # }
/// # }
/// # impl AsObject<User, Create, &()> for Message {
/// # type Error = ();
@@ -260,7 +207,7 @@ where
};
// Handle the activity
match obj.activity(ctx, actor, act_id) {
match obj.activity(ctx, actor, &act_id) {
Ok(res) => Inbox::Handled(res.into()),
Err(e) => Inbox::Failed(e),
}
@@ -364,16 +311,35 @@ pub trait FromId<C>: Sized {
id: &str,
proxy: Option<reqwest::Proxy>,
) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
request::get(id, Self::get_sender(), proxy)
.map_err(|_| (None, InboxError::DerefError))
.and_then(|mut r| {
let json: serde_json::Value = r
.json()
.map_err(|_| (None, InboxError::InvalidObject(None)))?;
serde_json::from_value(json.clone())
.map_err(|_| (Some(json), InboxError::InvalidObject(None)))
})
.map_err(|(json, e)| (json, e.into()))
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))
.and_then(|mut r| {
let json: serde_json::Value = r
.json()
.map_err(|_| (None, InboxError::InvalidObject(None)))?;
serde_json::from_value(json.clone())
.map_err(|_| (Some(json), InboxError::InvalidObject(None)))
})
.map_err(|(json, e)| (json, e.into()))
}
/// Builds a `Self` from its ActivityPub representation
@@ -381,8 +347,6 @@ pub trait FromId<C>: Sized {
/// 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 get_sender() -> &'static dyn Signer;
}
/// Should be implemented by anything representing an ActivityPub actor.
@@ -421,49 +385,6 @@ pub trait AsActor<C> {
/// # extern crate activitypub;
/// # use activitypub::{activity::Create, actor::Person, object::Note};
/// # 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;
/// # impl FromId<()> for Account {
/// # type Error = ();
@@ -476,10 +397,6 @@ pub trait AsActor<C> {
/// # fn from_activity(_: &(), obj: Person) -> Result<Self, Self::Error> {
/// # Ok(Account)
/// # }
/// #
/// # fn get_sender() -> &'static dyn Signer {
/// # &*MY_SIGNER
/// # }
/// # }
/// # impl AsActor<()> for Account {
/// # fn get_inbox_url(&self) -> String {
@@ -503,10 +420,6 @@ pub trait AsActor<C> {
/// fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> {
/// Ok(Message { text: obj.object_props.content_string().map_err(|_| ())? })
/// }
///
/// fn get_sender() -> &'static dyn Signer {
/// &*MY_SIGNER
/// }
/// }
///
/// impl AsObject<Account, Create, ()> for Message {
@@ -546,51 +459,7 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::activity_pub::sign::{
gen_keypair, Error as SignError, Result as SignResult, Signer,
};
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;
impl FromId<()> for MyActor {
@@ -604,10 +473,6 @@ mod tests {
fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> {
Ok(MyActor)
}
fn get_sender() -> &'static dyn Signer {
&*MY_SIGNER
}
}
impl AsActor<&()> for MyActor {
@@ -632,10 +497,6 @@ mod tests {
fn from_activity(_: &(), _obj: Note) -> Result<Self, Self::Error> {
Ok(MyObject)
}
fn get_sender() -> &'static dyn Signer {
&*MY_SIGNER
}
}
impl AsObject<MyActor, Create, &()> for MyObject {
type Error = ();
@@ -740,10 +601,6 @@ mod tests {
fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> {
Err(())
}
fn get_sender() -> &'static dyn Signer {
&*MY_SIGNER
}
}
impl AsActor<&()> for FailingActor {
fn get_inbox_url(&self) -> String {
+1 -1
View File
@@ -145,7 +145,7 @@ where
warn!("Inbox doesn't have host: {:?}", &inbox);
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() {
warn!("Header value is invalid: {:?}", url.host_str());
continue;
+9 -55
View File
@@ -1,11 +1,6 @@
use chrono::{offset::Utc, DateTime};
use openssl::hash::{Hasher, MessageDigest};
use reqwest::{
header::{
HeaderMap, HeaderValue, InvalidHeaderValue, ACCEPT, CONTENT_TYPE, DATE, HOST, USER_AGENT,
},
ClientBuilder, Proxy, Response, Url, UrlError,
};
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, DATE, USER_AGENT};
use std::ops::Deref;
use std::time::SystemTime;
use tracing::warn;
@@ -18,24 +13,6 @@ const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION"));
#[derive(Debug)]
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);
impl Digest {
@@ -141,8 +118,8 @@ type Path<'a> = &'a str;
type Query<'a> = &'a str;
type RequestTarget<'a> = (Method<'a>, Path<'a>, Option<Query<'a>>);
pub fn signature(
signer: &dyn Signer,
pub fn signature<S: Signer>(
signer: &S,
headers: &HeaderMap,
request_target: RequestTarget,
) -> Result<HeaderValue, Error> {
@@ -187,35 +164,10 @@ pub fn signature(
)).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)]
mod tests {
use super::signature;
use crate::activity_pub::sign::{gen_keypair, Error, Result, Signer};
use super::{signature, Error};
use crate::activity_pub::sign::{gen_keypair, Signer};
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
use reqwest::header::HeaderMap;
@@ -235,11 +187,13 @@ mod tests {
}
impl Signer for MySigner {
type Error = Error;
fn get_key_id(&self) -> String {
"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())
.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())
}
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())
.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)]
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 {
type Error;
fn get_key_id(&self) -> String;
/// 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
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool>;
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool, Self::Error>;
}
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
T: Signer;
fn verify<T>(self, creator: &T) -> bool
@@ -51,7 +46,7 @@ pub trait Signable {
}
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 mut options = json!({
"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 = 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
return SignatureValidity::Invalid;
}
+22 -76
View File
@@ -1,8 +1,11 @@
use heck::ToUpperCamelCase;
use heck::CamelCase;
use openssl::rand::rand_bytes;
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag};
use regex_syntax::is_word_character;
use rocket::http::uri::Uri;
use rocket::{
http::uri::Uri,
response::{Flash, Redirect},
};
use std::collections::HashSet;
use syntect::html::{ClassStyle, ClassedHTMLGenerator};
use syntect::parsing::SyntaxSet;
@@ -18,63 +21,23 @@ pub fn random_hex() -> String {
/// Remove non alphanumeric characters and CamelCase a string
pub fn make_actor_id(name: &str) -> String {
name.to_upper_camel_case()
name.to_camel_case()
.chars()
.filter(|c| c.is_alphanumeric())
.collect()
}
/**
* Percent-encode characters which are not allowed in IRI path segments.
*
* Intended to be used for generating Post ap_url.
*/
pub fn iri_percent_encode_seg(segment: &str) -> String {
segment.chars().map(iri_percent_encode_seg_char).collect()
}
pub fn iri_percent_encode_seg_char(c: char) -> String {
if c.is_alphanumeric() {
c.to_string()
} else {
match c {
'-'
| '.'
| '_'
| '~'
| '\u{A0}'..='\u{D7FF}'
| '\u{20000}'..='\u{2FFFD}'
| '\u{30000}'..='\u{3FFFD}'
| '\u{40000}'..='\u{4FFFD}'
| '\u{50000}'..='\u{5FFFD}'
| '\u{60000}'..='\u{6FFFD}'
| '\u{70000}'..='\u{7FFFD}'
| '\u{80000}'..='\u{8FFFD}'
| '\u{90000}'..='\u{9FFFD}'
| '\u{A0000}'..='\u{AFFFD}'
| '\u{B0000}'..='\u{BFFFD}'
| '\u{C0000}'..='\u{CFFFD}'
| '\u{D0000}'..='\u{DFFFD}'
| '\u{E0000}'..='\u{EFFFD}'
| '!'
| '$'
| '&'
| '\''
| '('
| ')'
| '*'
| '+'
| ','
| ';'
| '='
| ':'
| '@' => c.to_string(),
_ => {
let s = c.to_string();
Uri::percent_encode(&s).to_string()
}
}
}
* Redirects to the login page with a given message.
*
* Note that the message should be translated before passed to this function.
*/
pub fn requires_login<T: Into<Uri<'static>>>(message: &str, url: T) -> Flash<Redirect> {
Flash::new(
Redirect::to(format!("/login?m={}", Uri::percent_encode(message))),
"callback",
url.into().to_string(),
)
}
#[derive(Debug)]
@@ -125,13 +88,13 @@ fn highlight_code<'a>(
unreachable!();
};
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
.find_syntax_by_name(lang)
.find_syntax_by_name(&lang)
.unwrap_or_else(|| syntax_set.find_syntax_plain_text())
});
let mut html = ClassedHTMLGenerator::new_with_class_style(
syntax,
&syntax,
&syntax_set,
ClassStyle::Spaced,
);
@@ -318,15 +281,16 @@ pub fn md_to_html<'a>(
text_acc.push(c)
}
let mention = text_acc;
let short_mention = mention.splitn(1, '@').next().unwrap_or("");
let link = Tag::Link(
LinkType::Inline,
format!("{}@/{}/", base_url, &mention).into(),
mention.clone().into(),
short_mention.to_owned().into(),
);
mentions.push(mention.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));
(
@@ -450,10 +414,6 @@ pub fn md_to_html<'a>(
(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)]
mod tests {
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]
fn test_inline() {
assert_eq!(
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "plume-front"
version = "0.7.1"
version = "0.6.1-dev"
authors = ["Plume contributors"]
edition = "2018"
+14 -20
View File
@@ -54,6 +54,11 @@ pub enum EditorError {
DOMError,
}
impl From<std::option::NoneError> for EditorError {
fn from(_: std::option::NoneError) -> Self {
EditorError::NoneError
}
}
const AUTOSAVE_DEBOUNCE_TIME: i32 = 5000;
#[derive(Serialize, Deserialize)]
struct AutosaveInformation {
@@ -193,7 +198,7 @@ fn clear_autosave() {
.unwrap()
.remove_item(&get_autosave_id())
.unwrap();
console::log_1(&format!("Saved to {}", &get_autosave_id()).into());
console::log_1(&&format!("Saved to {}", &get_autosave_id()).into());
}
type TimeoutHandle = i32;
lazy_static! {
@@ -361,9 +366,7 @@ fn init_editor() -> Result<(), EditorError> {
return Ok(());
}
let old_ed = old_ed.unwrap();
let old_title = document()
.get_element_by_id("plume-editor-title")
.ok_or(EditorError::NoneError)?;
let old_title = document().get_element_by_id("plume-editor-title")?;
old_ed
.dyn_ref::<HtmlElement>()
.unwrap()
@@ -431,8 +434,7 @@ fn init_editor() -> Result<(), EditorError> {
bg.class_list().add_1("show").unwrap();
})) as Box<dyn FnMut(MouseEvent)>);
document()
.get_element_by_id("publish")
.ok_or(EditorError::NoneError)?
.get_element_by_id("publish")?
.add_event_listener_with_callback("click", show_popup.as_ref().unchecked_ref())
.map_err(|_| EditorError::DOMError)?;
show_popup.forget();
@@ -526,14 +528,8 @@ fn init_popup(
cover_label
.set_attribute("for", "cover")
.map_err(|_| EditorError::DOMError)?;
let cover = document
.get_element_by_id("cover")
.ok_or(EditorError::NoneError)?;
cover
.parent_element()
.ok_or(EditorError::NoneError)?
.remove_child(&cover)
.ok();
let cover = document.get_element_by_id("cover")?;
cover.parent_element()?.remove_child(&cover).ok();
popup
.append_child(&cover_label)
.map_err(|_| EditorError::DOMError)?;
@@ -558,7 +554,7 @@ fn init_popup(
draft.set_checked(draft_checkbox.checked());
draft_label
.append_child(draft)
.append_child(&draft)
.map_err(|_| EditorError::DOMError)?;
draft_label
.append_child(&document.create_text_node(&i18n!(CATALOG, "This is a draft")))
@@ -624,12 +620,11 @@ fn init_popup(
.map_err(|_| EditorError::DOMError)?;
callback.forget();
popup
.append_child(button)
.append_child(&button)
.map_err(|_| EditorError::DOMError)?;
document
.body()
.ok_or(EditorError::NoneError)?
.body()?
.append_child(&popup)
.map_err(|_| EditorError::DOMError)?;
Ok(popup)
@@ -646,8 +641,7 @@ fn init_popup_bg() -> Result<Element, EditorError> {
.map_err(|_| EditorError::DOMError)?;
document()
.body()
.ok_or(EditorError::NoneError)?
.body()?
.append_child(&bg)
.map_err(|_| EditorError::DOMError)?;
let callback = Closure::wrap(Box::new(|_| close_popup()) as Box<dyn FnMut(MouseEvent)>);
+3 -3
View File
@@ -1,5 +1,5 @@
#![recursion_limit = "128"]
#![feature(decl_macro, proc_macro_hygiene)]
#![feature(decl_macro, proc_macro_hygiene, try_trait)]
#[macro_use]
extern crate gettext_macros;
@@ -61,7 +61,7 @@ lazy_static! {
static ref CATALOG: gettext::Catalog = {
let catalogs = include_i18n!();
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
.iter()
@@ -85,7 +85,7 @@ pub fn main() -> Result<(), JsValue> {
menu();
search();
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(())
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "plume-macro"
version = "0.7.1"
version = "0.6.1-dev"
authors = ["Trinity Pointard <trinity.pointard@insa-rennes.fr>"]
edition = "2018"
description = "Plume procedural macros"
+2 -2
View File
@@ -58,7 +58,7 @@ pub fn import_migrations(input: TokenStream) -> TokenStream {
(name, up_sql, down_sql)
})
.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
.iter()
.map(|m| m.1.as_str())
@@ -103,7 +103,7 @@ fn file_to_migration(file: &str) -> TokenStream2 {
acc.push('\n');
}
} else if let Some(acc_str) = line.strip_prefix("--#!") {
acc.push_str(acc_str);
acc.push_str(&acc_str);
acc.push('\n');
} else if line.starts_with("--") {
continue;
+11 -11
View File
@@ -1,17 +1,19 @@
[package]
name = "plume-models"
version = "0.7.1"
version = "0.6.1-dev"
authors = ["Plume contributors"]
edition = "2018"
[dependencies]
activitypub = "0.1.1"
ammonia = "3.1.2"
bcrypt = "0.10.1"
guid-create = "0.2"
itertools = "0.10.3"
ammonia = "2.1.1"
askama_escape = "0.1"
bcrypt = "0.5"
guid-create = "0.1"
heck = "0.3.0"
itertools = "0.8.0"
lazy_static = "1.0"
ldap3 = "0.9.3"
ldap3 = "0.7.1"
migrations_internals= "1.4.0"
openssl = "0.10.22"
rocket = "0.4.6"
@@ -20,21 +22,19 @@ reqwest = "0.9"
scheduled-thread-pool = "0.2.2"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0.70"
serde_json = "1.0"
tantivy = "0.13.3"
url = "2.1"
walkdir = "2.2"
webfinger = "0.4.1"
whatlang = "0.13.0"
shrinkwraprs = "0.3.0"
whatlang = "0.11.1"
shrinkwraprs = "0.2.1"
diesel-derive-newtype = "0.1.2"
glob = "0.3.0"
lindera-tantivy = { version = "0.7.1", optional = true }
tracing = "0.1.22"
riker = "0.4.2"
once_cell = "1.5.2"
lettre = "0.9.6"
native-tls = "0.2.8"
[dependencies.chrono]
features = ["serde"]
+8 -12
View File
@@ -86,18 +86,14 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApiToken {
}
let mut parsed_header = headers[0].split(' ');
let auth_type = parsed_header
.next()
.map_or_else::<rocket::Outcome<&str, _, ()>, _, _>(
|| Outcome::Failure((Status::BadRequest, TokenError::NoType)),
Outcome::Success,
)?;
let val = parsed_header
.next()
.map_or_else::<rocket::Outcome<&str, _, ()>, _, _>(
|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)),
Outcome::Success,
)?;
let auth_type = parsed_header.next().map_or_else(
|| Outcome::Failure((Status::BadRequest, TokenError::NoType)),
Outcome::Success,
)?;
let val = parsed_header.next().map_or_else(
|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)),
Outcome::Success,
)?;
if auth_type == "Bearer" {
let conn = request
+1 -1
View File
@@ -28,7 +28,7 @@ impl BlocklistedEmail {
pub fn delete_entries(conn: &Connection, ids: Vec<i32>) -> Result<bool> {
use diesel::delete;
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)?;
}
Ok(true)
+13 -26
View File
@@ -149,15 +149,7 @@ impl Blog {
.into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
.ok_or(Error::Webfinger)
.and_then(|l| {
Blog::from_id(
conn,
&l.href.ok_or(Error::MissingApProperty)?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)
})
.and_then(|l| Blog::from_id(conn, &l.href?, None, CONFIG.proxy()).map_err(|(_, e)| e))
}
pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> {
@@ -244,7 +236,7 @@ impl Blog {
(min, max): (i32, i32),
) -> Result<ActivityStream<OrderedCollectionPage>> {
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
//doesn't do anything yet
coll.collection_page_props.set_next_link(Id::new(&format!(
@@ -273,10 +265,7 @@ impl Blog {
pub fn get_keypair(&self) -> Result<PKey<Private>> {
PKey::from_rsa(Rsa::private_key_from_pem(
self.private_key
.clone()
.ok_or(Error::MissingApProperty)?
.as_ref(),
self.private_key.clone()?.as_ref(),
)?)
.map_err(Error::from)
}
@@ -329,7 +318,7 @@ impl Blog {
}
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)?;
}
diesel::delete(self)
@@ -350,12 +339,12 @@ impl FromId<DbConn> for Blog {
type Object = CustomGroup;
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> {
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(|_| {
Instance::insert(
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 {
@@ -466,22 +451,24 @@ impl AsActor<&PlumeRocket> for Blog {
}
impl sign::Signer for Blog {
type Error = Error;
fn get_key_id(&self) -> String {
format!("{}#main-key", self.ap_url)
}
fn sign(&self, to_sign: &str) -> sign::Result<Vec<u8>> {
let key = self.get_keypair().map_err(|_| sign::Error())?;
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
let key = self.get_keypair()?;
let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
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 mut verifier = Verifier::new(MessageDigest::sha256(), &key)?;
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::{
activity_pub::{
inbox::{AsActor, AsObject, FromId},
sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY,
},
utils,
@@ -142,20 +141,18 @@ impl Comment {
}
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 mut act = Create::default();
act.create_props.set_actor_link(author.into_id())?;
act.create_props.set_object_object(note.clone())?;
act.object_props.set_id_string(format!(
"{}/activity",
self.ap_url.clone().ok_or(Error::MissingApProperty)?,
))?;
act.object_props
.set_id_string(format!("{}/activity", self.ap_url.clone()?,))?;
act.object_props
.set_to_link_vec(note.object_props.to_link_vec::<Id>()?)?;
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)
}
@@ -185,9 +182,7 @@ impl Comment {
.set_actor_link(self.get_author(conn)?.into_id())?;
let mut tombstone = Tombstone::default();
tombstone
.object_props
.set_id_string(self.ap_url.clone().ok_or(Error::MissingApProperty)?)?;
tombstone.object_props.set_id_string(self.ap_url.clone()?)?;
act.delete_props.set_object_object(tombstone)?;
act.object_props
@@ -209,13 +204,7 @@ impl FromId<DbConn> for Comment {
fn from_activity(conn: &DbConn, note: Note) -> Result<Self> {
let comm = {
let previous_url = note
.object_props
.in_reply_to
.as_ref()
.ok_or(Error::MissingApProperty)?
.as_str()
.ok_or(Error::MissingApProperty)?;
let previous_url = note.object_props.in_reply_to.as_ref()?.as_str()?;
let previous_comment = Comment::find_by_ap_url(conn, previous_url);
let is_public = |v: &Option<serde_json::Value>| match v
@@ -266,7 +255,9 @@ impl FromId<DbConn> for Comment {
.and_then(|m| {
let author = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0];
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();
}
@@ -329,10 +320,6 @@ impl FromId<DbConn> for Comment {
comm.notify(conn)?;
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 {
@@ -361,7 +348,7 @@ impl AsObject<User, Delete, &DbConn> for Comment {
m.delete(conn)?;
}
for n in Notification::find_for_comment(conn, &self)? {
for n in Notification::find_for_comment(&conn, &self)? {
n.delete(&**conn)?;
}
+26 -45
View File
@@ -1,6 +1,4 @@
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 as RocketConfig;
use std::collections::HashSet;
@@ -17,14 +15,12 @@ pub struct Config {
pub db_name: &'static str,
pub db_max_size: Option<u32>,
pub db_min_idle: Option<u32>,
pub signup: SignupStrategy,
pub search_index: String,
pub search_tokenizers: SearchTokenizerConfig,
pub rocket: Result<RocketConfig, InvalidRocketConfig>,
pub rocket: Result<RocketConfig, RocketError>,
pub logo: LogoConfig,
pub default_theme: String,
pub media_directory: String,
pub mail: Option<MailConfig>,
pub ldap: Option<LdapConfig>,
pub proxy: Option<ProxyConfig>,
}
@@ -35,21 +31,21 @@ impl Config {
}
#[derive(Debug, Clone)]
pub enum InvalidRocketConfig {
Env,
Address,
SecretKey,
pub enum RocketError {
InvalidEnv,
InvalidAddress,
InvalidSecretKey,
}
fn get_rocket_config() -> Result<RocketConfig, InvalidRocketConfig> {
let mut c = RocketConfig::active().map_err(|_| InvalidRocketConfig::Env)?;
fn get_rocket_config() -> Result<RocketConfig, RocketError> {
let mut c = RocketConfig::active().map_err(|_| RocketError::InvalidEnv)?;
let address = var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned());
let port = var("ROCKET_PORT")
.ok()
.map(|s| s.parse::<u16>().unwrap())
.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")
.unwrap_or_else(|_| "128".to_owned())
.parse::<u64>()
@@ -60,10 +56,10 @@ fn get_rocket_config() -> Result<RocketConfig, InvalidRocketConfig> {
.unwrap();
c.set_address(address)
.map_err(|_| InvalidRocketConfig::Address)?;
.map_err(|_| RocketError::InvalidAddress)?;
c.set_port(port);
c.set_secret_key(secret_key)
.map_err(|_| InvalidRocketConfig::SecretKey)?;
.map_err(|_| RocketError::InvalidSecretKey)?;
c.set_limits(
Limits::new()
@@ -159,7 +155,7 @@ impl Default for LogoConfig {
.ok()
.or_else(|| 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("jpg") | Some("jpeg") => Some("image/jpeg".to_owned()),
Some("svg") => Some("image/svg+xml".to_owned()),
@@ -168,8 +164,11 @@ impl Default for LogoConfig {
};
let mut custom_icons = env::vars()
.filter_map(|(var, val)| {
var.strip_prefix("PLUME_LOGO_")
.map(|size| (size.to_owned(), val))
if let Some(size) = var.strip_prefix("PLUME_LOGO_") {
Some((size.to_owned(), val))
} else {
None
}
})
.filter_map(|(var, val)| var.parse::<u64>().ok().map(|var| (var, val)))
.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 addr: String,
pub base_dn: String,
pub tls: bool,
pub user_name_attr: String,
pub mail_attr: String,
pub user: Option<(String, String)>,
}
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 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 {
addr,
base_dn,
tls,
user_name_attr,
mail_attr,
user
})
}
(None, None) => None,
(_, _) => {
_ => {
panic!("Invalid LDAP configuration : both LDAP_ADDR and LDAP_BASE_DN must be set")
}
}
@@ -364,7 +347,6 @@ lazy_static! {
s.parse::<u32>()
.expect("Couldn't parse DB_MIN_IDLE into u32")
)),
signup: var("SIGNUP").map_or(SignupStrategy::default(), |s| s.parse().unwrap()),
#[cfg(feature = "postgres")]
database_url: var("DATABASE_URL")
.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()),
media_directory: var("MEDIA_UPLOAD_DIRECTORY")
.unwrap_or_else(|_| "static/media".to_owned()),
mail: get_mail_config(),
ldap: get_ldap_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::{
ap_url, db_conn::DbConn, instance::Instance, notifications::*, schema::follows, users::User,
Connection, Error, Result, CONFIG,
ap_url, db_conn::DbConn, notifications::*, schema::follows, users::User, Connection, Error,
Result, CONFIG,
};
use activitypub::activity::{Accept, Follow as FollowAct, Undo};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
@@ -183,10 +183,6 @@ impl FromId<DbConn> for Follow {
.map_err(|(_, e)| e)?;
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 {
@@ -199,7 +195,7 @@ impl AsObject<User, Undo, &DbConn> for Follow {
diesel::delete(&self).execute(&**conn)?;
// 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)?;
}
+2 -43
View File
@@ -3,12 +3,11 @@ use crate::{
medias::Media,
safe_string::SafeString,
schema::{instances, users},
users::{NewUser, Role, User},
users::{Role, User},
Connection, Error, Result,
};
use chrono::NaiveDateTime;
use diesel::{self, result::Error::NotFound, ExpressionMethods, QueryDsl, RunQueryDsl};
use once_cell::sync::OnceCell;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::utils::md_to_html;
use std::sync::RwLock;
@@ -46,9 +45,6 @@ lazy_static! {
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 {
pub fn set_local(self) {
LOCAL_INSTANCE.write().unwrap().replace(self);
@@ -80,42 +76,6 @@ impl Instance {
.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>> {
instances::table
.order(instances::public_domain.asc())
@@ -344,7 +304,6 @@ pub(crate) mod tests {
})
.collect();
Instance::cache_local(conn);
Instance::cache_local_instance_user(conn);
res
}
Regular → Executable
+8 -45
View File
@@ -1,3 +1,4 @@
#![feature(try_trait)]
#![feature(never_type)]
#![feature(proc_macro_hygiene)]
#![feature(box_patterns)]
@@ -16,10 +17,8 @@ extern crate serde_json;
#[macro_use]
extern crate tantivy;
pub use lettre;
pub use lettre::smtp;
use once_cell::sync::Lazy;
use plume_common::activity_pub::{inbox::InboxError, request, sign};
use plume_common::activity_pub::inbox::InboxError;
use posts::PostEvent;
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
use users::UserEvent;
@@ -67,7 +66,6 @@ pub enum Error {
Url,
Webfinger,
Expired,
UserAlreadyExists,
}
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 {
fn from(err: diesel::result::Error) -> Self {
Error::Db(err)
}
}
impl From<std::option::NoneError> for Error {
fn from(_: std::option::NoneError) -> Self {
Error::NotFound
}
}
impl From<url::ParseError> for Error {
fn from(_: url::ParseError) -> Self {
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>;
/// Adds a function to a model, that returns the first
@@ -303,33 +295,6 @@ pub fn ap_url(url: &str) -> String {
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)]
#[macro_use]
mod tests {
@@ -377,7 +342,6 @@ pub mod blogs;
pub mod comment_seers;
pub mod comments;
pub mod db_conn;
pub mod email_signups;
pub mod follows;
pub mod headers;
pub mod inbox;
@@ -398,7 +362,6 @@ pub mod safe_string;
#[allow(unused_imports)]
pub mod schema;
pub mod search;
pub mod signups;
pub mod tags;
pub mod timeline;
pub mod users;
+3 -8
View File
@@ -1,13 +1,12 @@
use crate::{
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*,
users::User, Connection, Error, Result, CONFIG,
db_conn::DbConn, notifications::*, posts::Post, schema::likes, timeline::*, users::User,
Connection, Error, Result, CONFIG,
};
use activitypub::activity;
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId},
sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY,
};
@@ -138,10 +137,6 @@ impl FromId<DbConn> for Like {
res.notify(conn)?;
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 {
@@ -153,7 +148,7 @@ impl AsObject<User, activity::Undo, &DbConn> for Like {
diesel::delete(&self).execute(&**conn)?;
// 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)?;
}
Ok(())
+1 -3
View File
@@ -143,7 +143,6 @@ macro_rules! func {
}
}
#[allow(dead_code)]
#[derive(Clone, Queryable, Identifiable)]
struct ListElem {
pub id: i32,
@@ -286,8 +285,7 @@ impl List {
.select(list_elems::word)
.load::<Option<String>>(conn)
.map_err(Error::from)
// .map(|r| r.into_iter().filter_map(|o| o).collect::<Vec<String>>())
.map(|r| r.into_iter().flatten().collect::<Vec<String>>())
.map(|r| r.into_iter().filter_map(|o| o).collect::<Vec<String>>())
}
pub fn clear(&self, conn: &Connection) -> Result<()> {
+49 -91
View File
@@ -3,22 +3,23 @@ use crate::{
users::User, Connection, Error, Result, CONFIG,
};
use activitypub::object::Image;
use askama_escape::escape;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use guid_create::GUID;
use plume_common::{
activity_pub::{inbox::FromId, request, Id},
utils::{escape, MediaProcessor},
activity_pub::{inbox::FromId, Id},
utils::MediaProcessor,
};
use std::{
fs::{self, DirBuilder},
path::{self, Path, PathBuf},
path::{Path, PathBuf},
};
use tracing::warn;
use url::Url;
const REMOTE_MEDIA_DIRECTORY: &str = "remote";
#[derive(Clone, Identifiable, Queryable, AsChangeset)]
#[derive(Clone, Identifiable, Queryable)]
pub struct Media {
pub id: i32,
pub file_path: String,
@@ -64,7 +65,6 @@ impl MediaCategory {
impl Media {
insert!(medias, NewMedia);
get!(medias);
find_by!(medias, find_by_file_path, file_path as &str);
pub fn for_user(conn: &Connection, owner: i32) -> Result<Vec<Media>> {
medias::table
@@ -103,8 +103,8 @@ impl Media {
pub fn category(&self) -> MediaCategory {
match &*self
.file_path
.rsplit_once('.')
.map(|x| x.1)
.rsplitn(2, '.')
.next()
.expect("Media::category: extension error")
.to_lowercase()
{
@@ -155,15 +155,12 @@ impl Media {
if self.is_remote {
Ok(self.remote_url.clone().unwrap_or_default())
} else {
let file_path = self.file_path.replace(path::MAIN_SEPARATOR, "/").replacen(
&CONFIG.media_directory,
"static/media",
1,
); // "static/media" from plume::routs::plume_media_files()
let p = Path::new(&self.file_path);
let filename: String = p.file_name().unwrap().to_str().unwrap().to_owned();
Ok(ap_url(&format!(
"{}/{}",
"{}/static/media/{}",
Instance::get_local()?.public_domain,
&file_path
&filename
)))
}
}
@@ -207,91 +204,52 @@ impl Media {
// TODO: merge with save_remote?
pub fn from_activity(conn: &DbConn, image: &Image) -> Result<Media> {
let remote_url = image
.object_props
.url_string()
.or(Err(Error::MissingApProperty))?;
let remote_url = image.object_props.url_string().ok()?;
let path = determine_mirror_file_path(&remote_url);
let parent = path.parent().ok_or(Error::InvalidValue)?;
let parent = path.parent()?;
if !parent.is_dir() {
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
request::get(
remote_url.as_str(),
User::get_sender(),
CONFIG.proxy().cloned(),
)?
.copy_to(&mut dest)?;
if let Some(proxy) = CONFIG.proxy() {
reqwest::ClientBuilder::new().proxy(proxy.clone()).build()?
} else {
reqwest::Client::new()
}
.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)?)
.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(
// TODO: upsert
Media::insert(
conn,
NewMedia {
file_path: path.to_str()?.to_string(),
alt_text: image.object_props.content_string().ok()?,
is_remote: false,
remote_url: None,
sensitive: image.object_props.summary_string().is_ok(),
content_warning: image.object_props.summary_string().ok(),
owner_id: User::from_id(
conn,
NewMedia {
file_path: path.to_str().ok_or(Error::InvalidValue)?.to_string(),
alt_text: image
.object_props
.content_string()
.or(Err(Error::NotFound))?,
is_remote: false,
remote_url: None,
sensitive: image.object_props.summary_string().is_ok(),
content_warning: image.object_props.summary_string().ok(),
owner_id: User::from_id(
conn,
image
.object_props
.attributed_to_link_vec::<Id>()
.or(Err(Error::NotFound))?
.into_iter()
.next()
.ok_or(Error::NotFound)?
.as_ref(),
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?
.id,
},
image
.object_props
.attributed_to_link_vec::<Id>()
.ok()?
.into_iter()
.next()?
.as_ref(),
None,
CONFIG.proxy(),
)
})
.map_err(|(_, e)| e)?
.id,
},
)
}
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()
.map(ToOwned::to_owned)
.unwrap_or_else(|| String::from("png"));
file_path.push(format!("{}.{}", GUID::rand(), ext));
file_path.push(format!("{}.{}", GUID::rand().to_string(), ext));
});
file_path
}
+2 -6
View File
@@ -47,11 +47,7 @@ impl Mention {
pub fn get_user(&self, conn: &Connection) -> Result<User> {
match self.get_post(conn) {
Ok(p) => Ok(p
.get_authors(conn)?
.into_iter()
.next()
.ok_or(Error::NotFound)?),
Ok(p) => Ok(p.get_authors(conn)?.into_iter().next()?),
Err(_) => self.get_comment(conn).and_then(|c| c.get_author(conn)),
}
}
@@ -81,7 +77,7 @@ impl Mention {
in_post: bool,
notify: bool,
) -> 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)?;
if in_post {
+1 -2
View File
@@ -105,8 +105,7 @@ impl ImportedMigrations {
pub fn rerun_last_migration(&self, conn: &Connection, path: &Path) -> Result<()> {
let latest_migration = conn.latest_run_migration_version()?;
let id = latest_migration
.and_then(|m| self.0.binary_search_by_key(&m.as_str(), |m| m.name).ok())
.ok_or(Error::NotFound)?;
.and_then(|m| self.0.binary_search_by_key(&m.as_str(), |m| m.name).ok())?;
let migration = &self.0[id];
conn.transaction(|| {
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> {
let request = Self::find_by_token(conn, token)?;
let request = Self::find_by_token(&conn, &token)?;
let filter =
password_reset_requests::table.filter(password_reset_requests::id.eq(request.id));
+59 -121
View File
@@ -10,15 +10,15 @@ use activitypub::{
CustomObject,
};
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 plume_common::{
activity_pub::{
inbox::{AsActor, AsObject, FromId},
sign::Signer,
inbox::{AsObject, FromId},
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 std::collections::{HashMap, HashSet};
@@ -67,15 +67,20 @@ impl Post {
find_by!(posts, find_by_ap_url, ap_url as &str);
last!(posts);
pub fn insert(conn: &Connection, mut 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);
}
pub fn insert(conn: &Connection, new: NewPost) -> Result<Self> {
diesel::insert_into(posts::table)
.values(new)
.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 {
post.publish_published();
@@ -89,16 +94,13 @@ impl Post {
let post = Self::get(conn, self.id)?;
// TODO: Call publish_published() when newly 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)
}
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)?;
}
diesel::delete(self).execute(conn)?;
@@ -249,20 +251,6 @@ impl Post {
.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>> {
use crate::schema::post_authors;
use crate::schema::users;
@@ -455,17 +443,23 @@ impl Post {
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<_>>();
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
.iter()
.map(|m| m.mentioned_id)
.collect::<HashSet<_>>();
for (m, id) in &mentions {
if !old_user_mentioned.contains(id) {
Mention::from_activity(&*conn, m, self.id, true, true)?;
if !old_user_mentioned.contains(&id) {
Mention::from_activity(&*conn, &m, self.id, true, true)?;
}
}
@@ -477,7 +471,7 @@ impl Post {
.iter()
.filter(|m| !new_mentions.contains(&m.mentioned_id))
{
m.delete(conn)?;
m.delete(&conn)?;
}
Ok(())
}
@@ -650,85 +644,37 @@ impl FromId<DbConn> for Post {
.and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id));
let title = article.object_props.name_string()?;
let ap_url = article
.object_props
.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;
// TODO: upsert
let post = Post::insert(
conn,
NewPost {
blog_id: blog?.id,
slug: title.to_kebab_case(),
title,
content: SafeString::new(&article.object_props.content_string()?),
published: true,
license,
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
ap_url: article
.object_props
.url_string()
.or_else(|_| article.object_props.id_string())?,
creation_date: Some(article.object_props.published_utctime()?.naive_utc()),
subtitle: article.object_props.summary_string()?,
source: article.ap_object_props.source_object::<Source>()?.content,
cover_id: cover,
},
)?;
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,
NewPost {
blog_id: blog.ok_or(Error::NotFound)?.id,
slug: Self::slug(&title).to_string(),
title,
content: SafeString::new(&article.object_props.content_string()?),
published: true,
license,
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
ap_url,
creation_date: Some(article.object_props.published_utctime()?.naive_utc()),
subtitle: article.object_props.summary_string()?,
source: article.ap_object_props.source_object::<Source>()?.content,
cover_id: cover,
},
)
.and_then(|post| {
for author in authors {
PostAuthor::insert(
conn,
NewPostAuthor {
post_id: post.id,
author_id: author.id,
},
)?;
}
Ok(post)
})
})?;
for author in authors {
PostAuthor::insert(
conn,
NewPostAuthor {
post_id: post.id,
author_id: author.id,
},
)?;
}
// save mentions and tags
let mut hashtags = md_to_html(&post.source, None, false, None)
@@ -760,10 +706,6 @@ impl FromId<DbConn> for 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 {
@@ -835,10 +777,6 @@ impl FromId<DbConn> for PostUpdate {
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 {
@@ -855,7 +793,7 @@ impl AsObject<User, Update, &DbConn> for PostUpdate {
}
if let Some(title) = self.title {
post.slug = Post::slug(&title).to_string();
post.slug = title.to_kebab_case();
post.title = title;
}
+11 -9
View File
@@ -17,23 +17,25 @@ pub struct RemoteFetchActor {
impl RemoteFetchActor {
pub fn init(conn: DbPool) {
let actor = ACTOR_SYS
ACTOR_SYS
.actor_of_args::<RemoteFetchActor, _>("remote-fetch", conn)
.expect("Failed to initialize remote fetch actor");
USER_CHAN.tell(
Subscribe {
actor: Box::new(actor),
topic: "*".into(),
},
None,
)
}
}
impl Actor for RemoteFetchActor {
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) {
use UserEvent::*;
+3 -8
View File
@@ -1,13 +1,12 @@
use crate::{
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::reshares,
timeline::*, users::User, Connection, Error, Result, CONFIG,
db_conn::DbConn, notifications::*, posts::Post, schema::reshares, timeline::*, users::User,
Connection, Error, Result, CONFIG,
};
use activitypub::activity::{Announce, Undo};
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId},
sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY,
};
@@ -163,10 +162,6 @@ impl FromId<DbConn> for Reshare {
res.notify(conn)?;
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 {
@@ -178,7 +173,7 @@ impl AsObject<User, Undo, &DbConn> for Reshare {
diesel::delete(&self).execute(&**conn)?;
// 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)?;
}
+2 -2
View File
@@ -102,7 +102,7 @@ pub struct SafeString {
impl SafeString {
pub fn new(value: &str) -> Self {
SafeString {
value: CLEAN.clean(value).to_string(),
value: CLEAN.clean(&value).to_string(),
}
}
@@ -156,7 +156,7 @@ impl<'de> Deserialize<'de> for SafeString {
where
D: Deserializer<'de>,
{
deserializer.deserialize_string(SafeStringVisitor)
Ok(deserializer.deserialize_string(SafeStringVisitor)?)
}
}
-11
View File
@@ -73,7 +73,6 @@ table! {
user_id -> Int4,
}
}
table! {
email_blocklist(id){
id -> Int4,
@@ -84,15 +83,6 @@ table! {
}
}
table! {
email_signups (id) {
id -> Int4,
email -> Varchar,
token -> Varchar,
expiration_date -> Timestamp,
}
}
table! {
follows (id) {
id -> Int4,
@@ -316,7 +306,6 @@ allow_tables_to_appear_in_same_query!(
blogs,
comments,
comment_seers,
email_signups,
follows,
instances,
likes,
+11 -9
View File
@@ -13,23 +13,25 @@ pub struct SearchActor {
impl SearchActor {
pub fn init(searcher: Arc<Searcher>, conn: DbPool) {
let actor = ACTOR_SYS
ACTOR_SYS
.actor_of_args::<SearchActor, _>("search", (searcher, conn))
.expect("Failed to initialize searcher actor");
POST_CHAN.tell(
Subscribe {
actor: Box::new(actor),
topic: "*".into(),
},
None,
)
}
}
impl Actor for SearchActor {
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) {
use PostEvent::*;
+2 -2
View File
@@ -148,7 +148,7 @@ impl PlumeQuery {
/// Parse a query string into this Query
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
@@ -360,7 +360,7 @@ impl std::str::FromStr for PlumeQuery {
fn from_str(query: &str) -> Result<PlumeQuery, !> {
let mut res: PlumeQuery = Default::default();
res.from_str_req(query.trim());
res.from_str_req(&query.trim());
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<()> {
if self.includes_post(conn, post)? {
return Ok(());
}
diesel::insert_into(timeline::table)
.values(TimelineEntry {
post_id: post.id,
@@ -239,16 +236,6 @@ impl Timeline {
let query = TimelineQuery::parse(&self.query)?;
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)]
+31 -49
View File
@@ -18,6 +18,12 @@ pub enum QueryError {
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>;
#[derive(Debug, Clone, Copy, PartialEq)]
@@ -233,7 +239,7 @@ impl WithList {
) -> Result<bool> {
match list {
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()) {
(WithList::Blog, ListType::Blog) => list.contains_blog(conn, post.blog_id),
(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>)> {
let mut res = Vec::new();
let (left, token) = parse_a(stream)?;
let (left, token) = parse_a(&stream)?;
res.push(token);
stream = left;
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>)> {
let mut res = Vec::new();
let (left, token) = parse_b(stream)?;
let (left, token) = parse_b(&stream)?;
res.push(token);
stream = left;
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) {
Some(Token::RParent(_)) => Ok((&left[1..], token)),
Some(t) => t.get_error(Token::RParent(0)),
None => Err(QueryError::UnexpectedEndOfQuery),
None => None?,
}
}
_ => 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>)> {
match stream
.get(0)
.map(Token::get_text)
.ok_or(QueryError::UnexpectedEndOfQuery)?
{
match stream.get(0).map(Token::get_text)? {
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" => {
let (mut left, list) = parse_l(&stream[2..])?;
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" {
break;
}
match (
*clude,
left.get(1)
.map(Token::get_text)
.ok_or(QueryError::UnexpectedEndOfQuery)?,
) {
match (*clude, left.get(1).map(Token::get_text)?) {
("include", "reshares") | ("include", "reshare") => {
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'")),
}
}
s @ "title" | s @ "subtitle" | s @ "content" => match (
stream.get(1).ok_or(QueryError::UnexpectedEndOfQuery)?,
stream.get(2).ok_or(QueryError::UnexpectedEndOfQuery)?,
) {
s @ "title" | s @ "subtitle" | s @ "content" => match (stream.get(1)?, stream.get(2)?) {
(Token::Word(_, _, contains), Token::Word(_, _, w)) if contains == &"contains" => Ok((
&stream[3..],
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" {
break;
}
match (
*clude,
stream
.get(2)
.map(Token::get_text)
.ok_or(QueryError::UnexpectedEndOfQuery)?,
) {
match (*clude, stream.get(2).map(Token::get_text)?) {
("include", "reshares") | ("include", "reshare") => boosts = true,
("exclude", "reshares") | ("exclude", "reshare") => boosts = false,
("include", "likes") | ("include", "like") => likes = true,
@@ -589,23 +577,20 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
"all" => Ok((&stream[1..], Arg::Boolean(Bool::All))),
_ => unreachable!(),
},
_ => stream
.get(0)
.ok_or(QueryError::UnexpectedEndOfQuery)?
.get_error(Token::Word(
0,
0,
"one of 'blog', 'author', 'license', 'tags', 'lang', \
_ => stream.get(0)?.get_error(Token::Word(
0,
0,
"one of 'blog', 'author', 'license', 'tags', 'lang', \
'title', 'subtitle', 'content', 'followed', 'has_cover', 'local' or 'all'",
)),
)),
}
}
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(_) => {
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))),
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>)> {
let mut res: Vec<&str> = vec![
match stream.get(0).ok_or(QueryError::UnexpectedEndOfQuery)? {
Token::Word(_, _, w) => w,
t => return t.get_error(Token::Word(0, 0, "any word")),
},
];
let mut res: Vec<&str> = Vec::new();
res.push(match stream.get(0)? {
Token::Word(_, _, w) => w,
t => return t.get_error(Token::Word(0, 0, "any word")),
});
stream = &stream[1..];
while let Token::Comma(_) = stream[0] {
res.push(
match stream.get(1).ok_or(QueryError::UnexpectedEndOfQuery)? {
Token::Word(_, _, w) => w,
t => return t.get_error(Token::Word(0, 0, "any word")),
},
);
res.push(match stream.get(1)? {
Token::Word(_, _, w) => w,
t => return t.get_error(Token::Word(0, 0, "any word")),
});
stream = &stream[2..];
}
+104 -58
View File
@@ -22,13 +22,17 @@ use openssl::{
};
use plume_common::{
activity_pub::{
ap_accept_header,
inbox::{AsActor, AsObject, FromId},
request::get,
sign::{gen_keypair, Error as SignError, Result as SignResult, Signer},
sign::{gen_keypair, Signer},
ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY,
},
utils,
};
use reqwest::{
header::{HeaderValue, ACCEPT},
ClientBuilder,
};
use riker::actors::{Publish, Tell};
use rocket::{
outcome::IntoOutcome,
@@ -48,7 +52,6 @@ pub enum Role {
Admin = 0,
Moderator = 1,
Normal = 2,
Instance = 3,
}
#[derive(Queryable, Identifiable, Clone, Debug, AsChangeset)]
@@ -75,7 +78,6 @@ pub struct User {
pub summary_html: SafeString,
/// 0 = admin
/// 1 = moderator
/// 3 = local instance
/// anything else = normal user
pub role: i32,
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> {
let link = resolve(acct.to_owned(), true)?
.links
.into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
.ok_or(Error::Webfinger)?;
User::from_id(
conn,
link.href.as_ref().ok_or(Error::Webfinger)?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)
User::from_id(conn, link.href.as_ref()?, None, CONFIG.proxy()).map_err(|(_, e)| e)
}
pub fn fetch_remote_interact_uri(acct: &str) -> Result<String> {
@@ -243,7 +223,20 @@ impl User {
}
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()?;
// without this workaround, publicKey is not correctly deserialized
let ap_sign = serde_json::from_str::<ApSignature>(text)?;
@@ -262,10 +255,10 @@ impl User {
conn,
json.object
.object_props
.icon_image()? // FIXME: Fails when icon is not set
.icon_image()?
.object_props
.url_string()?,
self,
&self,
)
.ok();
@@ -300,6 +293,21 @@ impl User {
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> {
if CONFIG.ldap.is_none() {
return Err(Error::NotFound);
@@ -307,6 +315,9 @@ impl User {
let ldap = CONFIG.ldap.as_ref().unwrap();
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 bind = ldap_conn
.simple_bind(&ldap_name, password)
@@ -353,6 +364,9 @@ impl User {
} else {
return false;
};
if User::ldap_preconn(&mut conn).is_err() {
return false;
}
let name = format!(
"{}={},{}",
ldap.user_name_attr, &self.username, ldap.base_dn
@@ -434,12 +448,12 @@ impl User {
let last = &format!(
"{}?page={}",
&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_last_link(Id::new(last))?;
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))
}
pub fn outbox_page(
@@ -448,7 +462,7 @@ impl User {
(min, max): (i32, i32),
) -> Result<ActivityStream<OrderedCollectionPage>> {
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();
if n_acts - i64::from(min) >= i64::from(ITEMS_PER_PAGE) {
coll.collection_page_props.set_next_link(Id::new(&format!(
@@ -470,7 +484,20 @@ impl User {
Ok(ActivityStream::new(coll))
}
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 json: serde_json::Value = serde_json::from_str(text)?;
let items = json["items"]
@@ -480,15 +507,27 @@ impl User {
.filter_map(|j| serde_json::from_value(j.clone()).ok())
.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))
}
pub fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> {
let mut res = get(
&self.outbox_url[..],
Self::get_sender(),
CONFIG.proxy().cloned(),
)?;
let mut res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()?
.get(&self.outbox_url[..])
.header(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
)
.send()?;
let text = &res.text()?;
let json: serde_json::Value = serde_json::from_str(text)?;
if let Some(first) = json.get("first") {
@@ -498,7 +537,7 @@ impl User {
if page.is_empty() {
break;
}
items.append(&mut page);
items.extend(page.drain(..));
if let Some(n) = nxt {
if n == next {
break;
@@ -520,11 +559,20 @@ impl User {
}
pub fn fetch_followers_ids(&self) -> Result<Vec<String>> {
let mut res = get(
&self.followers_endpoint[..],
Self::get_sender(),
CONFIG.proxy().cloned(),
)?;
let mut res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()?
.get(&self.followers_endpoint[..])
.header(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
)
.send()?;
let text = &res.text()?;
let json: serde_json::Value = serde_json::from_str(text)?;
Ok(json["items"]
@@ -696,7 +744,7 @@ impl User {
pub fn get_keypair(&self) -> Result<PKey<Private>> {
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)
}
@@ -919,7 +967,7 @@ impl FromId<DbConn> for User {
fn from_activity(conn: &DbConn, acct: CustomPerson) -> Result<Self> {
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(|_| {
Instance::insert(
conn,
@@ -1007,10 +1055,6 @@ impl FromId<DbConn> for 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 {
@@ -1043,22 +1087,24 @@ impl AsObject<User, Delete, &DbConn> for User {
}
impl Signer for User {
type Error = Error;
fn get_key_id(&self) -> String {
format!("{}#main-key", self.ap_url)
}
fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> {
let key = self.get_keypair().map_err(|_| SignError())?;
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
let key = self.get_keypair()?;
let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?;
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 mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?;
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,
role: role as i32,
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),
hashed_password: password,
instance_id: instance.id,
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n"
"Language-Team: Afrikaans\n"
"Language: af_ZA\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:407
# plume-front/src/editor.rs:337
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr ""
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr ""
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n"
"Language-Team: Arabic\n"
"Language: ar_SA\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr "فتح محرر النصوص الغني"
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "العنوان"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr "العنوان الثانوي أو الملخص"
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
msgstr "اكتب مقالك هنا. ماركداون مُدَعَّم."
# plume-front/src/editor.rs:407
# plume-front/src/editor.rs:337
msgid "Around {} characters left"
msgstr "يتبقا {} حرفا تقريبا"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "الوسوم"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "الرخصة"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "الغلاف"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "هذه مسودة"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "نشر كتابا"
+13 -13
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n"
"Language-Team: Bulgarian\n"
"Language: bg_BG\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr "Искате ли да активирате локално автоматично запаметяване, последно редактирано в {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr "Отворете редактора с богат текст"
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "Заглавие"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr "Подзаглавие или резюме"
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
msgstr "Напишете статията си тук. Поддържа се Markdown."
# plume-front/src/editor.rs:407
# plume-front/src/editor.rs:337
msgid "Around {} characters left"
msgstr "Остават {} знака вляво"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "Етикети"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Лиценз"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "Основно изображение"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "Това е проект"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Публикувай"
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n"
"Language-Team: Catalan\n"
"Language: ca_ES\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr "Obre leditor de text enriquit"
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "Títol"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
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."
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"
msgstr "Queden uns {} caràcters"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "Etiquetes"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Llicència"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "Coberta"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "Açò és un esborrany"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Publica"
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n"
"Language-Team: Czech\n"
"Language: cs_CZ\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr "Otevřít editor formátovaného textu"
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "Nadpis"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr "Podnadpis, nebo shrnutí"
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
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"
msgstr "Zbývá kolem {} znaků"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "Tagy"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Licence"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "Titulka"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "Tohle je koncept"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Zveřejnit"
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n"
"Language-Team: Danish\n"
"Language: da_DK\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:407
# plume-front/src/editor.rs:337
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr ""
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr ""
+13 -13
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
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"
msgstr " Rich Text Editor (RTE) öffnen"
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "Titel"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr "Untertitel oder Zusammenfassung"
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
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"
msgstr "Ungefähr {} Zeichen übrig"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "Schlagwörter"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Lizenz"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "Einband"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "Dies ist ein Entwurf"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Veröffentlichen"
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n"
"Language-Team: Greek\n"
"Language: el_GR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:407
# plume-front/src/editor.rs:337
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr ""
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr ""
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:407
# plume-front/src/editor.rs:337
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr ""
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr ""
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n"
"Language-Team: Esperanto\n"
"Language: eo_UY\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr "Malfermi la riĉan redaktilon"
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "Titolo"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
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"
msgstr "Proksimume {} signoj restantaj"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "Etikedoj"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Permesilo"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "Kovro"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "Malfinias"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Eldoni"
+13 -13
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
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"
msgstr "Abrir el editor de texto enriquecido"
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "Título"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
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."
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"
msgstr "Quedan unos {} caracteres"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "Etiquetas"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Licencia"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "Cubierta"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "Esto es un borrador"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Publicar"
+15 -15
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n"
"Language-Team: Persian\n"
"Language: fa_IR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr "آیا می‌خواهید نسخهٔ ذخیره شدهٔ خودکار محلّی از آخرین ویرایش در {} را بار کنید؟"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr "باز کردن ویرایش‌گر غنی"
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "عنوان"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr "زیرعنوان، یا چکیده"
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
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"
msgstr "نزدیک به {} حرف باقی مانده است"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "برچسب‌ها"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "پروانه"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "جلد"
msgstr "تصویر شاخص"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "این، یک پیش‌نویس است"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "انتشار"
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n"
"Language-Team: Finnish\n"
"Language: fi_FI\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr "Avaa edistynyt tekstieditori"
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "Otsikko"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr "Alaotsikko tai tiivistelmä"
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
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"
msgstr "%{count} merkkiä jäljellä"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "Tagit"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Lisenssi"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "Kansi"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "Tämä on luonnos"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Julkaise"
+13 -13
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
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"
msgstr "Ouvrir l'éditeur de texte avancé"
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "Titre"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
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."
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"
msgstr "Environ {} caractères restant"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "Étiquettes"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Licence"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "Illustration"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "Ceci est un brouillon"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Publier"
+13 -13
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n"
"Language-Team: Galician\n"
"Language: gl_ES\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
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"
msgstr "Abre o editor de texto enriquecido"
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "Título"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
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."
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"
msgstr "Dispós de {} caracteres"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "Etiquetas"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Licenza"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "Portada"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "Este é un borrador"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Publicar"
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n"
"Language-Team: Hebrew\n"
"Language: he_IL\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:407
# plume-front/src/editor.rs:337
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr ""
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr ""
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n"
"Language-Team: Hindi\n"
"Language: hi_IN\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "शीर्षक"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
msgstr "अपना आर्टिकल या लेख यहाँ लिखें. Markdown उपलब्ध है."
# plume-front/src/editor.rs:407
# plume-front/src/editor.rs:337
msgid "Around {} characters left"
msgstr "लगभग {} अक्षर बाकी हैं"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "टैग्स"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "लाइसेंस"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "पब्लिश करें"
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n"
"Language-Team: Croatian\n"
"Language: hr_HR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "Naslov"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:407
# plume-front/src/editor.rs:337
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "Tagovi"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Licenca"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Objavi"
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n"
"Language-Team: Hungarian\n"
"Language: hu_HU\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:407
# plume-front/src/editor.rs:337
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr ""
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr ""
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr "Apri il compositore di testo avanzato"
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "Titolo"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr "Sottotitolo, o sommario"
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
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"
msgstr "Circa {} caratteri rimasti"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "Etichette"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Licenza"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "Copertina"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "Questa è una bozza"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Pubblica"
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"Language: ja_JP\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr "リッチテキストエディターを開く"
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "タイトル"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr "サブタイトル、または概要"
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
msgstr "投稿をここに書きます。Markdown がサポートされています。"
# plume-front/src/editor.rs:407
# plume-front/src/editor.rs:337
msgid "Around {} characters left"
msgstr "残り約 {} 文字"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "タグ"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "ライセンス"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "カバー"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "これは下書きです"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "公開"
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n"
"Language-Team: Korean\n"
"Language: ko_KR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:407
# plume-front/src/editor.rs:337
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr ""
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr ""
+13 -13
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
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"
msgstr "Open de rich-text editor"
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "Titel"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr "Ondertitel of samenvatting"
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
msgstr "Schrijf hier je artikel. Markdown wordt ondersteund."
# plume-front/src/editor.rs:407
# plume-front/src/editor.rs:337
msgid "Around {} characters left"
msgstr "Ongeveer {} tekens over"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "Tags"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Licentie"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "Hoofdafbeelding"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "Dit is een concept"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Publiceren"
+14 -14
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n"
"Language-Team: Norwegian\n"
"Language: no_NO\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:407
# plume-front/src/editor.rs:337
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Lisens"
msgstr ""
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Publiser"
msgstr ""
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n"
"Language-Team: Polish\n"
"Language: pl_PL\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr "Otwórz edytor tekstu sformatowanego"
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "Tytuł"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr "Podtytuł, lub podsumowanie"
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
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"
msgstr "Pozostało w okolicy {} znaków"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "Tagi"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Licencja"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "Okładka"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "To jest szkic"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Publikuj"
+11 -11
View File
@@ -12,46 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:281
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:314
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:318
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:325
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:407
# plume-front/src/editor.rs:336
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:413
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:414
msgid "License"
msgstr ""
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:417
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:437
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:444
msgid "Publish"
msgstr ""
+13 -13
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n"
"Language-Team: Portuguese, Brazilian\n"
"Language: pt_BR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
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"
msgstr "Abrir o editor de rich text"
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "Título"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
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."
msgstr "Escreva seu artigo aqui. Markdown é suportado."
# plume-front/src/editor.rs:407
# plume-front/src/editor.rs:337
msgid "Around {} characters left"
msgstr "Cerca de {} caracteres restantes"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "Tags"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Licença"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "Capa"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "Isso é um rascunho"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Publicar"
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n"
"Language-Team: Romanian\n"
"Language: ro_RO\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr "Deschide editorul de text"
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "Titlu"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
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"
msgstr "În apropiere de {} caractere rămase"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "Etichete"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Licenţă"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "Coperta"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "Aceasta este o ciornă"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Publică"
+16 -16
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr "Хотите ли вы загрузить локальное автосохранение, сделанное в {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr "Открыть в визуальном редакторе"
msgstr ""
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "Заголовок"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
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."
msgstr "Пишите свою статью здесь. Markdown поддерживается."
# plume-front/src/editor.rs:407
# plume-front/src/editor.rs:337
msgid "Around {} characters left"
msgstr "Осталось около {} символов"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "Теги"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Лицензия"
msgstr ""
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "Обложка"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "Это черновик"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Опубликовать"
+18 -18
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n"
"Language-Team: Santali\n"
"Language: sat_IN\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr "ᱟᱢ ᱪᱮᱫ ᱢᱟᱲᱟᱝ {} ᱨᱮ ᱥᱟᱯᱲᱟᱣ ᱟᱠᱟᱱ ᱞᱚᱠᱟᱞ ᱚᱴᱚᱥᱮᱣ ᱞᱟᱫᱮ ᱥᱟᱱᱟᱢ ᱠᱟᱱᱟ ᱥᱮ?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr "ᱨᱤᱪ ᱚᱞ ᱥᱟᱯᱟᱣᱤᱡ ᱠᱷᱩᱞᱟᱹᱭ ᱢᱮ"
msgstr "ᱨᱤᱪ ᱚᱞ ᱥᱟᱯᱟᱣᱤᱡ ᱠᱷᱩᱞᱟᱹᱭ ᱢᱮ"
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "ᱴᱭᱴᱚᱞ"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr "ᱥᱟᱹᱵᱴᱟᱭᱴᱟᱹᱞ, ᱟᱨ ᱵᱟᱝ ᱥᱟᱹᱢᱢᱟᱨᱭ"
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
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"
msgstr "ᱡᱷᱚᱛᱚ ᱨᱮ {} ᱡᱤᱱᱤᱥ ᱵᱟᱧᱪᱟᱣᱠᱟᱱᱟ"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "ᱴᱮᱜᱥ"
msgstr "ᱴᱮᱜᱥ"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "ᱞᱚᱭᱥᱮᱱᱥ"
msgstr "ᱞᱚᱭᱥᱮᱱᱥ"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "ᱢᱚᱭᱞᱟᱹᱴ"
msgstr "ᱠᱚᱵᱷᱚᱨ"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "ᱱᱚᱶᱟ ᱫᱚ ᱰᱨᱟᱯᱷᱼᱴ ᱠᱟᱱᱟ"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "ᱯᱟᱨᱥᱟᱞ"
+14 -14
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n"
"Language-Team: Sinhala\n"
"Language: si_LK\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "මාතෘකාව"
msgstr ""
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:407
# plume-front/src/editor.rs:337
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "බලපත්‍රය"
msgstr ""
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr ""
+13 -13
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n"
"Language-Team: Slovak\n"
"Language: sk_SK\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
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"
msgstr "Otvor editor formátovaného textu"
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "Nadpis"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr "Zhrnutie, alebo podnadpis"
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
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"
msgstr "Zostáva asi {} znakov"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "Štítky"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Licencia"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "Obálka"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "Toto je koncept"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Zverejniť"
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Language: sl_SI\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "Naslov"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:407
# plume-front/src/editor.rs:337
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "Oznake"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Licenca"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "To je osnutek"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Objavi"
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n"
"Language-Team: Serbian (Latin)\n"
"Language: sr_CS\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr "Otvori uređivač sa stilizacijom"
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "Naslov"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
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."
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"
msgstr "Preostalo oko {} znakova"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "Markeri"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Licenca"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "Naslovna strana"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "Ovo je nacrt"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Objavi"
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Language: sv_SE\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "Titel"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
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"
msgstr "Ungefär {} karaktärer kvar"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "Taggar"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Licens"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "Omslag"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Publicera"
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr "Zengin metin editörünü (RTE) aç"
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr "Başlık"
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
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."
msgstr "Makaleni buraya yaz. Markdown kullanabilirsin."
# plume-front/src/editor.rs:407
# plume-front/src/editor.rs:337
msgid "Around {} characters left"
msgstr "Yaklaşık {} karakter kaldı"
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr "Etiketler"
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr "Lisans"
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr "Kapak"
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr "Bu bir taslaktır"
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
msgstr "Yayınla"
+12 -12
View File
@@ -3,7 +3,7 @@ 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"
"PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 {}?"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:282
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:385
# plume-front/src/editor.rs:315
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:389
# plume-front/src/editor.rs:319
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:396
# plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:407
# plume-front/src/editor.rs:337
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:517
# plume-front/src/editor.rs:414
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:518
# plume-front/src/editor.rs:415
msgid "License"
msgstr ""
# plume-front/src/editor.rs:524
# plume-front/src/editor.rs:418
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:564
# plume-front/src/editor.rs:438
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:575
# plume-front/src/editor.rs:445
msgid "Publish"
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