Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2fe705a712 | |||
| 3a448e9e17 | |||
| 6e4def4cc5 | |||
| 34b8fd83c1 | |||
| d5774078e0 | |||
| a3623412f9 | |||
| 1ed60537cf | |||
| 037d670fb7 | |||
| 8b817d50c5 | |||
| fa48060a94 | |||
| b41e982daf | |||
| 2fcb449ed8 | |||
| 218bc54a5f | |||
| 64f0333497 | |||
| 8aa7a5780d | |||
| 17c398bcee | |||
| 76f1455372 | |||
| 1bcad6d7cd | |||
| 9a58d9bcb7 | |||
| c2dcac4413 | |||
| 394273e866 | |||
| 1bcc70c174 |
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
@@ -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
-30
@@ -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,18 +11,15 @@
|
||||
- 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
|
||||
|
||||
@@ -53,9 +30,6 @@
|
||||
- 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 +210,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
+533
-608
File diff suppressed because it is too large
Load Diff
+14
-10
@@ -1,32 +1,36 @@
|
||||
[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"
|
||||
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 = "=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 +52,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 +69,7 @@ rev = "29910f2829e7e590a540da3804336577b48c7b31"
|
||||
|
||||
[build-dependencies]
|
||||
ructe = "0.13.0"
|
||||
rsass = "0.23"
|
||||
rsass = "0.9"
|
||||
|
||||
[features]
|
||||
default = ["postgres"]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE instances DROP COLUMN private_key;
|
||||
ALTER TABLE instances DROP COLUMN public_key;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE instances ADD COLUMN private_key TEXT;
|
||||
ALTER TABLE instances ADD COLUMN public_key TEXT;
|
||||
@@ -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);
|
||||
@@ -0,0 +1,30 @@
|
||||
CREATE TABLE instances_old (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
public_domain VARCHAR NOT NULL,
|
||||
name VARCHAR NOT NULL,
|
||||
local BOOLEAN NOT NULL DEFAULT 'f',
|
||||
blocked BOOLEAN NOT NULL DEFAULT 'f',
|
||||
creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
open_registrations BOOLEAN NOT NULL DEFAULT 't',
|
||||
short_description TEXT NOT NULL DEFAULT '',
|
||||
long_description TEXT NOT NULL DEFAULT '',
|
||||
default_license TEXT NOT NULL DEFAULT 'CC-0',
|
||||
long_description_html VARCHAR NOT NULL DEFAULT '',
|
||||
short_description_html VARCHAR NOT NULL DEFAULT ''
|
||||
);
|
||||
INSERT INTO instances_old SELECT
|
||||
id,
|
||||
public_domain,
|
||||
name,
|
||||
local,
|
||||
blocked,
|
||||
creation_date,
|
||||
open_registrations,
|
||||
short_description,
|
||||
long_description,
|
||||
default_license,
|
||||
long_description_html,
|
||||
short_description_html
|
||||
FROM instances;
|
||||
DROP TABLE instances;
|
||||
ALTER TABLE instances_old RENAME TO instances;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE instances ADD COLUMN private_key TEXT;
|
||||
ALTER TABLE instances ADD COLUMN public_key TEXT;
|
||||
@@ -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,6 +1,6 @@
|
||||
[package]
|
||||
name = "plume-api"
|
||||
version = "0.7.1"
|
||||
version = "0.6.1-dev"
|
||||
authors = ["Plume contributors"]
|
||||
edition = "2018"
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
|
||||
use plume_models::{instance::*, safe_string::SafeString, Connection};
|
||||
use plume_models::{instance::NewInstance, Connection};
|
||||
use std::env;
|
||||
|
||||
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
||||
@@ -53,21 +53,5 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||
.unwrap_or_else(|| String::from("CC-BY-SA"));
|
||||
let open_reg = !args.is_present("private");
|
||||
|
||||
Instance::insert(
|
||||
conn,
|
||||
NewInstance {
|
||||
public_domain: domain,
|
||||
name,
|
||||
local: true,
|
||||
long_description: SafeString::new(""),
|
||||
short_description: SafeString::new(""),
|
||||
default_license: license,
|
||||
open_registrations: open_reg,
|
||||
short_description_html: String::new(),
|
||||
long_description_html: String::new(),
|
||||
},
|
||||
)
|
||||
.expect("Couldn't save instance");
|
||||
Instance::cache_local(conn);
|
||||
Instance::create_local_instance_user(conn).expect("Couldn't save local instance user");
|
||||
NewInstance::new_local(conn, domain, name, open_reg, license).expect("Couldn't save instance");
|
||||
}
|
||||
|
||||
@@ -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)) => {
|
||||
|
||||
+7
-12
@@ -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"
|
||||
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]
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use reqwest;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use super::{request, sign::Signer};
|
||||
use reqwest::{
|
||||
header::{HeaderValue, HOST},
|
||||
Url,
|
||||
};
|
||||
|
||||
/// Represents an ActivityPub inbox.
|
||||
///
|
||||
@@ -13,49 +16,7 @@ use super::{request, sign::Signer};
|
||||
/// # 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())
|
||||
/// # }
|
||||
/// # }
|
||||
/// #
|
||||
/// # use plume_common::activity_pub::{inbox::*, sign::{gen_keypair, Error as SignatureError, Result as SignatureResult, Signer}};
|
||||
/// # struct User;
|
||||
/// # impl FromId<()> for User {
|
||||
/// # type Error = ();
|
||||
@@ -68,10 +29,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 +48,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 = ();
|
||||
@@ -112,6 +65,42 @@ use super::{request, sign::Signer};
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// # }
|
||||
/// # 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) -> SignatureResult<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(|_| SignatureError())
|
||||
/// # }
|
||||
/// #
|
||||
/// # fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<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(|_| SignatureError())
|
||||
/// # }
|
||||
/// # }
|
||||
/// #
|
||||
/// # let mut act = Create::default();
|
||||
/// # act.object_props.set_id_string(String::from("https://test.ap/activity")).unwrap();
|
||||
@@ -122,8 +111,9 @@ use super::{request, sign::Signer};
|
||||
/// # let activity_json = serde_json::to_value(act).unwrap();
|
||||
/// #
|
||||
/// # let conn = ();
|
||||
/// # let sender = MySigner::new();
|
||||
/// #
|
||||
/// let result: Result<(), ()> = Inbox::handle(&conn, activity_json)
|
||||
/// let result: Result<(), ()> = Inbox::handle(&conn, &sender, activity_json)
|
||||
/// .with::<User, Announce, Message>(None)
|
||||
/// .with::<User, Create, Message>(None)
|
||||
/// .done();
|
||||
@@ -137,9 +127,10 @@ where
|
||||
/// # Structure
|
||||
///
|
||||
/// - the context to be passed to each handler.
|
||||
/// - the sender actor to sign request
|
||||
/// - the activity
|
||||
/// - the reason it has not been handled yet
|
||||
NotHandled(&'a C, serde_json::Value, InboxError<E>),
|
||||
NotHandled(&'a C, &'a dyn Signer, serde_json::Value, InboxError<E>),
|
||||
|
||||
/// A matching handler have been found but failed
|
||||
///
|
||||
@@ -192,8 +183,12 @@ where
|
||||
///
|
||||
/// - `ctx`: the context to pass to each handler
|
||||
/// - `json`: the JSON representation of the incoming activity
|
||||
pub fn handle(ctx: &'a C, json: serde_json::Value) -> Inbox<'a, C, E, R> {
|
||||
Inbox::NotHandled(ctx, json, InboxError::NoMatch)
|
||||
pub fn handle(
|
||||
ctx: &'a C,
|
||||
sender: &'a dyn Signer,
|
||||
json: serde_json::Value,
|
||||
) -> Inbox<'a, C, E, R> {
|
||||
Inbox::NotHandled(ctx, sender, json, InboxError::NoMatch)
|
||||
}
|
||||
|
||||
/// Registers an handler on this Inbox.
|
||||
@@ -204,27 +199,30 @@ where
|
||||
M: AsObject<A, V, &'a C, Error = E> + FromId<C, Error = E>,
|
||||
M::Output: Into<R>,
|
||||
{
|
||||
if let Inbox::NotHandled(ctx, mut act, e) = self {
|
||||
if let Inbox::NotHandled(ctx, sender, mut act, e) = self {
|
||||
if serde_json::from_value::<V>(act.clone()).is_ok() {
|
||||
let act_clone = act.clone();
|
||||
let act_id = match act_clone["id"].as_str() {
|
||||
Some(x) => x,
|
||||
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidID),
|
||||
None => return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidID),
|
||||
};
|
||||
|
||||
// Get the actor ID
|
||||
let actor_id = match get_id(act["actor"].clone()) {
|
||||
Some(x) => x,
|
||||
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(None)),
|
||||
None => {
|
||||
return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidActor(None))
|
||||
}
|
||||
};
|
||||
|
||||
if Self::is_spoofed_activity(&actor_id, &act) {
|
||||
return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(None));
|
||||
return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidObject(None));
|
||||
}
|
||||
|
||||
// Transform this actor to a model (see FromId for details about the from_id function)
|
||||
let actor = match A::from_id(
|
||||
ctx,
|
||||
sender,
|
||||
&actor_id,
|
||||
serde_json::from_value(act["actor"].clone()).ok(),
|
||||
proxy,
|
||||
@@ -235,17 +233,25 @@ where
|
||||
if let Some(json) = json {
|
||||
act["actor"] = json;
|
||||
}
|
||||
return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(Some(e)));
|
||||
return Inbox::NotHandled(
|
||||
ctx,
|
||||
sender,
|
||||
act,
|
||||
InboxError::InvalidActor(Some(e)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Same logic for "object"
|
||||
let obj_id = match get_id(act["object"].clone()) {
|
||||
Some(x) => x,
|
||||
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(None)),
|
||||
None => {
|
||||
return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidObject(None))
|
||||
}
|
||||
};
|
||||
let obj = match M::from_id(
|
||||
ctx,
|
||||
sender,
|
||||
&obj_id,
|
||||
serde_json::from_value(act["object"].clone()).ok(),
|
||||
proxy,
|
||||
@@ -255,19 +261,24 @@ where
|
||||
if let Some(json) = json {
|
||||
act["object"] = json;
|
||||
}
|
||||
return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(Some(e)));
|
||||
return Inbox::NotHandled(
|
||||
ctx,
|
||||
sender,
|
||||
act,
|
||||
InboxError::InvalidObject(Some(e)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 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),
|
||||
}
|
||||
} else {
|
||||
// If the Activity type is not matching the expected one for
|
||||
// this handler, try with the next one.
|
||||
Inbox::NotHandled(ctx, act, e)
|
||||
Inbox::NotHandled(ctx, sender, act, e)
|
||||
}
|
||||
} else {
|
||||
self
|
||||
@@ -278,7 +289,7 @@ where
|
||||
pub fn done(self) -> Result<R, E> {
|
||||
match self {
|
||||
Inbox::Handled(res) => Ok(res),
|
||||
Inbox::NotHandled(_, _, err) => Err(E::from(err)),
|
||||
Inbox::NotHandled(_, _, _, err) => Err(E::from(err)),
|
||||
Inbox::Failed(err) => Err(err),
|
||||
}
|
||||
}
|
||||
@@ -345,6 +356,7 @@ pub trait FromId<C>: Sized {
|
||||
/// If absent, the ID will be dereferenced.
|
||||
fn from_id(
|
||||
ctx: &C,
|
||||
sender: &dyn Signer,
|
||||
id: &str,
|
||||
object: Option<Self::Object>,
|
||||
proxy: Option<&reqwest::Proxy>,
|
||||
@@ -353,7 +365,7 @@ pub trait FromId<C>: Sized {
|
||||
Ok(x) => Ok(x),
|
||||
_ => match object {
|
||||
Some(o) => Self::from_activity(ctx, o).map_err(|e| (None, e)),
|
||||
None => Self::from_activity(ctx, Self::deref(id, proxy.cloned())?)
|
||||
None => Self::from_activity(ctx, Self::deref(id, sender, proxy.cloned())?)
|
||||
.map_err(|e| (None, e)),
|
||||
},
|
||||
}
|
||||
@@ -362,18 +374,42 @@ pub trait FromId<C>: Sized {
|
||||
/// Dereferences an ID
|
||||
fn deref(
|
||||
id: &str,
|
||||
sender: &dyn Signer,
|
||||
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()))
|
||||
let mut headers = request::headers();
|
||||
let url = Url::parse(&id).map_err(|_| (None, InboxError::InvalidID.into()))?;
|
||||
if !url.has_host() {
|
||||
return Err((None, InboxError::InvalidID.into()));
|
||||
}
|
||||
let host_header_value = HeaderValue::from_str(&url.host_str().expect("Unreachable"))
|
||||
.map_err(|_| (None, InboxError::DerefError.into()))?;
|
||||
headers.insert(HOST, host_header_value);
|
||||
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)
|
||||
.headers(headers.clone())
|
||||
.header(
|
||||
"Signature",
|
||||
request::signature(sender, &headers, ("get", url.path(), url.query()))
|
||||
.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 +417,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 +455,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 +467,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 +490,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 {
|
||||
@@ -545,53 +528,11 @@ where
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::sign::{gen_keypair, Error as SignatureError, Result as SignatureResult};
|
||||
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 {
|
||||
type Error = ();
|
||||
@@ -604,10 +545,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 +569,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 = ();
|
||||
@@ -696,10 +629,47 @@ mod tests {
|
||||
act
|
||||
}
|
||||
|
||||
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) -> SignatureResult<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(|_| SignatureError())
|
||||
}
|
||||
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<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(|_| SignatureError())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inbox_basic() {
|
||||
let act = serde_json::to_value(build_create()).unwrap();
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act)
|
||||
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act)
|
||||
.with::<MyActor, Create, MyObject>(None)
|
||||
.done();
|
||||
assert!(res.is_ok());
|
||||
@@ -708,7 +678,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_inbox_multi_handlers() {
|
||||
let act = serde_json::to_value(build_create()).unwrap();
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act)
|
||||
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act)
|
||||
.with::<MyActor, Announce, MyObject>(None)
|
||||
.with::<MyActor, Delete, MyObject>(None)
|
||||
.with::<MyActor, Create, MyObject>(None)
|
||||
@@ -721,7 +691,7 @@ mod tests {
|
||||
fn test_inbox_failure() {
|
||||
let act = serde_json::to_value(build_create()).unwrap();
|
||||
// Create is not handled by this inbox
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act)
|
||||
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act)
|
||||
.with::<MyActor, Announce, MyObject>(None)
|
||||
.with::<MyActor, Like, MyObject>(None)
|
||||
.done();
|
||||
@@ -740,10 +710,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 {
|
||||
@@ -774,12 +740,12 @@ mod tests {
|
||||
fn test_inbox_actor_failure() {
|
||||
let act = serde_json::to_value(build_create()).unwrap();
|
||||
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
|
||||
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act.clone())
|
||||
.with::<FailingActor, Create, MyObject>(None)
|
||||
.done();
|
||||
assert!(res.is_err());
|
||||
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
|
||||
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act.clone())
|
||||
.with::<FailingActor, Create, MyObject>(None)
|
||||
.with::<MyActor, Create, MyObject>(None)
|
||||
.done();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
@@ -187,35 +164,12 @@ 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 crate::activity_pub::sign::{
|
||||
gen_keypair, Error as SignatureError, Result as SignatureResult, Signer,
|
||||
};
|
||||
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
|
||||
use reqwest::header::HeaderMap;
|
||||
|
||||
@@ -239,20 +193,20 @@ mod tests {
|
||||
"mysigner".into()
|
||||
}
|
||||
|
||||
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
|
||||
fn sign(&self, to_sign: &str) -> SignatureResult<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(|_| Error())
|
||||
signer.sign_to_vec().map_err(|_| SignatureError())
|
||||
}
|
||||
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<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(|_| Error())
|
||||
verifier.verify(&signature).map_err(|_| SignatureError())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +187,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;
|
||||
}
|
||||
|
||||
+25
-12
@@ -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,7 +21,7 @@ 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()
|
||||
@@ -77,6 +80,19 @@ pub fn iri_percent_encode_seg_char(c: char) -> 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)]
|
||||
enum State {
|
||||
Mention,
|
||||
@@ -125,13 +141,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 +334,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 +467,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::*;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "plume-front"
|
||||
version = "0.7.1"
|
||||
version = "0.6.1-dev"
|
||||
authors = ["Plume contributors"]
|
||||
edition = "2018"
|
||||
|
||||
|
||||
+14
-20
@@ -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)>);
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
+10
-11
@@ -1,40 +1,39 @@
|
||||
[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"
|
||||
ammonia = "2.1.1"
|
||||
askama_escape = "0.1"
|
||||
bcrypt = "0.10.1"
|
||||
guid-create = "0.2"
|
||||
itertools = "0.10.3"
|
||||
guid-create = "0.1"
|
||||
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"
|
||||
rocket = "=0.4.6"
|
||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||
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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
+32
-21
@@ -18,7 +18,8 @@ use openssl::{
|
||||
};
|
||||
use plume_common::activity_pub::{
|
||||
inbox::{AsActor, FromId},
|
||||
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
|
||||
sign::{self, Error as SignatureError, Result as SignatureResult},
|
||||
ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
|
||||
};
|
||||
use url::Url;
|
||||
use webfinger::*;
|
||||
@@ -152,7 +153,8 @@ impl Blog {
|
||||
.and_then(|l| {
|
||||
Blog::from_id(
|
||||
conn,
|
||||
&l.href.ok_or(Error::MissingApProperty)?,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&l.href?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
@@ -244,7 +246,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 +275,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 +328,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 +349,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,
|
||||
@@ -370,6 +369,8 @@ impl FromId<DbConn> for Blog {
|
||||
open_registrations: true,
|
||||
short_description_html: String::new(),
|
||||
long_description_html: String::new(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
)
|
||||
})?;
|
||||
@@ -383,7 +384,14 @@ impl FromId<DbConn> for Blog {
|
||||
Media::save_remote(
|
||||
conn,
|
||||
icon.object_props.url_string().ok()?,
|
||||
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
|
||||
&User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&owner,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.ok()?,
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
@@ -399,7 +407,14 @@ impl FromId<DbConn> for Blog {
|
||||
Media::save_remote(
|
||||
conn,
|
||||
banner.object_props.url_string().ok()?,
|
||||
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
|
||||
&User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&owner,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.ok()?,
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
@@ -443,10 +458,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 {
|
||||
@@ -470,18 +481,18 @@ impl sign::Signer for Blog {
|
||||
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) -> SignatureResult<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(SignatureError::from)
|
||||
}
|
||||
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> sign::Result<bool> {
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<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(SignatureError::from)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -247,6 +236,7 @@ impl FromId<DbConn> for Comment {
|
||||
})?,
|
||||
author_id: User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
¬e.object_props.attributed_to_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
@@ -305,7 +295,13 @@ impl FromId<DbConn> for Comment {
|
||||
.collect::<HashSet<_>>() // remove duplicates (don't do a query more than once)
|
||||
.into_iter()
|
||||
.map(|v| {
|
||||
if let Ok(user) = User::from_id(conn, &v, None, CONFIG.proxy()) {
|
||||
if let Ok(user) = User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&v,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
) {
|
||||
vec![user]
|
||||
} else {
|
||||
vec![] // TODO try to fetch collection
|
||||
@@ -329,10 +325,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 +353,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)?;
|
||||
}
|
||||
|
||||
|
||||
+11
-42
@@ -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()),
|
||||
@@ -249,31 +245,6 @@ 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,
|
||||
@@ -364,7 +335,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 +347,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(),
|
||||
};
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -168,6 +168,7 @@ impl FromId<DbConn> for Follow {
|
||||
fn from_activity(conn: &DbConn, follow: FollowAct) -> Result<Self> {
|
||||
let actor = User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&follow.follow_props.actor_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
@@ -176,6 +177,7 @@ impl FromId<DbConn> for Follow {
|
||||
|
||||
let target = User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&follow.follow_props.object_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
@@ -183,10 +185,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 +197,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(¬if).execute(&**conn)?;
|
||||
}
|
||||
|
||||
|
||||
+21
-15
@@ -3,7 +3,9 @@ use activitypub::activity::*;
|
||||
use crate::{
|
||||
comments::Comment,
|
||||
db_conn::DbConn,
|
||||
follows, likes,
|
||||
follows,
|
||||
instance::Instance,
|
||||
likes,
|
||||
posts::{Post, PostUpdate},
|
||||
reshares::Reshare,
|
||||
users::User,
|
||||
@@ -47,20 +49,24 @@ impl_into_inbox_result! {
|
||||
}
|
||||
|
||||
pub fn inbox(conn: &DbConn, act: serde_json::Value) -> Result<InboxResult, Error> {
|
||||
Inbox::handle(conn, act)
|
||||
.with::<User, Announce, Post>(CONFIG.proxy())
|
||||
.with::<User, Create, Comment>(CONFIG.proxy())
|
||||
.with::<User, Create, Post>(CONFIG.proxy())
|
||||
.with::<User, Delete, Comment>(CONFIG.proxy())
|
||||
.with::<User, Delete, Post>(CONFIG.proxy())
|
||||
.with::<User, Delete, User>(CONFIG.proxy())
|
||||
.with::<User, Follow, User>(CONFIG.proxy())
|
||||
.with::<User, Like, Post>(CONFIG.proxy())
|
||||
.with::<User, Undo, Reshare>(CONFIG.proxy())
|
||||
.with::<User, Undo, follows::Follow>(CONFIG.proxy())
|
||||
.with::<User, Undo, likes::Like>(CONFIG.proxy())
|
||||
.with::<User, Update, PostUpdate>(CONFIG.proxy())
|
||||
.done()
|
||||
Inbox::handle(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
act,
|
||||
)
|
||||
.with::<User, Announce, Post>(CONFIG.proxy())
|
||||
.with::<User, Create, Comment>(CONFIG.proxy())
|
||||
.with::<User, Create, Post>(CONFIG.proxy())
|
||||
.with::<User, Delete, Comment>(CONFIG.proxy())
|
||||
.with::<User, Delete, Post>(CONFIG.proxy())
|
||||
.with::<User, Delete, User>(CONFIG.proxy())
|
||||
.with::<User, Follow, User>(CONFIG.proxy())
|
||||
.with::<User, Like, Post>(CONFIG.proxy())
|
||||
.with::<User, Undo, Reshare>(CONFIG.proxy())
|
||||
.with::<User, Undo, follows::Follow>(CONFIG.proxy())
|
||||
.with::<User, Undo, likes::Like>(CONFIG.proxy())
|
||||
.with::<User, Update, PostUpdate>(CONFIG.proxy())
|
||||
.done()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+144
-44
@@ -3,14 +3,29 @@ use crate::{
|
||||
medias::Media,
|
||||
safe_string::SafeString,
|
||||
schema::{instances, users},
|
||||
users::{NewUser, Role, User},
|
||||
users::{Role, User},
|
||||
Connection, Error, Result,
|
||||
};
|
||||
use activitypub::{actor::Service, CustomObject};
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::{self, result::Error::NotFound, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use once_cell::sync::OnceCell;
|
||||
use plume_common::utils::md_to_html;
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use openssl::{
|
||||
hash::MessageDigest,
|
||||
pkey::{PKey, Private},
|
||||
rsa::Rsa,
|
||||
sign,
|
||||
};
|
||||
use plume_common::{
|
||||
activity_pub::{
|
||||
sign::{gen_keypair, Error as SignatureError, Result as SignatureResult, Signer},
|
||||
ApSignature, PublicKey,
|
||||
},
|
||||
utils::md_to_html,
|
||||
};
|
||||
use std::sync::RwLock;
|
||||
use tracing::warn;
|
||||
|
||||
pub type CustomService = CustomObject<ApSignature, Service>;
|
||||
|
||||
#[derive(Clone, Identifiable, Queryable)]
|
||||
pub struct Instance {
|
||||
@@ -26,6 +41,8 @@ pub struct Instance {
|
||||
pub default_license: String,
|
||||
pub long_description_html: SafeString,
|
||||
pub short_description_html: SafeString,
|
||||
pub private_key: Option<String>,
|
||||
pub public_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Insertable)]
|
||||
@@ -40,15 +57,14 @@ pub struct NewInstance {
|
||||
pub default_license: String,
|
||||
pub long_description_html: String,
|
||||
pub short_description_html: String,
|
||||
pub private_key: Option<String>,
|
||||
pub public_key: Option<String>,
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -73,6 +89,13 @@ impl Instance {
|
||||
*LOCAL_INSTANCE.write().unwrap() = Instance::get_local_uncached(conn).ok();
|
||||
}
|
||||
|
||||
pub fn get_locals(conn: &Connection) -> Result<Vec<Instance>> {
|
||||
instances::table
|
||||
.filter(instances::local.eq(true))
|
||||
.load::<Instance>(conn)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn get_remotes(conn: &Connection) -> Result<Vec<Instance>> {
|
||||
instances::table
|
||||
.filter(instances::local.eq(false))
|
||||
@@ -80,42 +103,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())
|
||||
@@ -278,6 +265,112 @@ impl Instance {
|
||||
})
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn set_keypair(&self, conn: &Connection) -> Result<()> {
|
||||
let (pub_key, priv_key) = gen_keypair();
|
||||
let private_key = String::from_utf8(priv_key).or(Err(Error::Signature))?;
|
||||
let public_key = String::from_utf8(pub_key).or(Err(Error::Signature))?;
|
||||
diesel::update(self)
|
||||
.set((
|
||||
instances::private_key.eq(Some(private_key)),
|
||||
instances::public_key.eq(Some(public_key)),
|
||||
))
|
||||
.execute(conn)
|
||||
.and(Ok(()))
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn get_keypair(&self) -> Result<PKey<Private>> {
|
||||
PKey::from_rsa(Rsa::private_key_from_pem(
|
||||
self.private_key.clone()?.as_ref(),
|
||||
)?)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// This is experimental and might change in the future.
|
||||
/// Currently "!" sign is used but it's not decided.
|
||||
pub fn ap_url(&self) -> String {
|
||||
ap_url(&format!(
|
||||
"{}/!/{}",
|
||||
Self::get_local().unwrap().public_domain,
|
||||
self.public_domain
|
||||
))
|
||||
}
|
||||
|
||||
pub fn to_activity(&self) -> Result<CustomService> {
|
||||
let mut actor = Service::default();
|
||||
let id = self.ap_url();
|
||||
actor.object_props.set_id_string(id.clone())?;
|
||||
actor.object_props.set_name_string(self.name.clone())?;
|
||||
|
||||
let mut ap_signature = ApSignature::default();
|
||||
if self.local {
|
||||
if let Some(pub_key) = self.public_key.clone() {
|
||||
let mut public_key = PublicKey::default();
|
||||
public_key.set_id_string(format!("{}#main-key", id))?;
|
||||
public_key.set_owner_string(id)?;
|
||||
public_key.set_public_key_pem_string(pub_key)?;
|
||||
ap_signature.set_public_key_publickey(public_key)?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(CustomService::new(actor, ap_signature))
|
||||
}
|
||||
}
|
||||
|
||||
impl NewInstance {
|
||||
pub fn new_local(
|
||||
conn: &Connection,
|
||||
public_domain: String,
|
||||
name: String,
|
||||
open_registrations: bool,
|
||||
default_license: String,
|
||||
) -> Result<Instance> {
|
||||
let (pub_key, priv_key) = gen_keypair();
|
||||
|
||||
Instance::insert(
|
||||
conn,
|
||||
NewInstance {
|
||||
public_domain,
|
||||
name,
|
||||
local: true,
|
||||
open_registrations,
|
||||
short_description: SafeString::new(""),
|
||||
long_description: SafeString::new(""),
|
||||
default_license,
|
||||
long_description_html: String::new(),
|
||||
short_description_html: String::new(),
|
||||
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
|
||||
public_key: Some(String::from_utf8(pub_key).or(Err(Error::Signature))?),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Signer for Instance {
|
||||
fn get_key_id(&self) -> String {
|
||||
format!("{}#main-key", self.ap_url())
|
||||
}
|
||||
|
||||
fn sign(&self, to_sign: &str) -> SignatureResult<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(SignatureError::from)
|
||||
}
|
||||
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<bool> {
|
||||
if self.public_key.is_none() {
|
||||
warn!("missing public key for {}", self.public_domain);
|
||||
return Err(SignatureError());
|
||||
}
|
||||
let key = PKey::from_rsa(Rsa::public_key_from_pem(
|
||||
self.public_key.clone().unwrap().as_ref(),
|
||||
)?)?;
|
||||
let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?;
|
||||
verifier.update(data.as_bytes())?;
|
||||
verifier.verify(&signature).map_err(SignatureError::from)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -299,6 +392,8 @@ pub(crate) mod tests {
|
||||
name: "My instance".to_string(),
|
||||
open_registrations: true,
|
||||
public_domain: "plu.me".to_string(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
NewInstance {
|
||||
default_license: "WTFPL".to_string(),
|
||||
@@ -310,6 +405,8 @@ pub(crate) mod tests {
|
||||
name: "An instance".to_string(),
|
||||
open_registrations: true,
|
||||
public_domain: "1plu.me".to_string(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
NewInstance {
|
||||
default_license: "CC-0".to_string(),
|
||||
@@ -321,6 +418,8 @@ pub(crate) mod tests {
|
||||
name: "Someone instance".to_string(),
|
||||
open_registrations: false,
|
||||
public_domain: "2plu.me".to_string(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
NewInstance {
|
||||
default_license: "CC-0-BY-SA".to_string(),
|
||||
@@ -332,6 +431,8 @@ pub(crate) mod tests {
|
||||
name: "Nice day".to_string(),
|
||||
open_registrations: true,
|
||||
public_domain: "3plu.me".to_string(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
@@ -344,7 +445,6 @@ pub(crate) mod tests {
|
||||
})
|
||||
.collect();
|
||||
Instance::cache_local(conn);
|
||||
Instance::cache_local_instance_user(conn);
|
||||
res
|
||||
}
|
||||
|
||||
|
||||
Regular → Executable
+21
-33
@@ -1,3 +1,4 @@
|
||||
#![feature(try_trait)]
|
||||
#![feature(never_type)]
|
||||
#![feature(proc_macro_hygiene)]
|
||||
#![feature(box_patterns)]
|
||||
@@ -16,10 +17,10 @@ extern crate serde_json;
|
||||
#[macro_use]
|
||||
extern crate tantivy;
|
||||
|
||||
pub use lettre;
|
||||
pub use lettre::smtp;
|
||||
use db_conn::DbPool;
|
||||
use instance::Instance;
|
||||
use once_cell::sync::Lazy;
|
||||
use plume_common::activity_pub::{inbox::InboxError, request, sign};
|
||||
use plume_common::activity_pub::{inbox::InboxError, sign};
|
||||
use posts::PostEvent;
|
||||
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
|
||||
use users::UserEvent;
|
||||
@@ -67,7 +68,6 @@ pub enum Error {
|
||||
Url,
|
||||
Webfinger,
|
||||
Expired,
|
||||
UserAlreadyExists,
|
||||
}
|
||||
|
||||
impl From<bcrypt::BcryptError> for Error {
|
||||
@@ -94,6 +94,12 @@ impl From<diesel::result::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
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,14 +166,14 @@ 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>;
|
||||
|
||||
impl From<Error> for sign::Error {
|
||||
fn from(_: Error) -> Self {
|
||||
Self()
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Adds a function to a model, that returns the first
|
||||
/// matching row for a given list of fields.
|
||||
///
|
||||
@@ -303,31 +309,15 @@ 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>;
|
||||
pub fn migrate_data(dbpool: &DbPool) -> Result<()> {
|
||||
ensure_local_instance_keys(&dbpool.get().unwrap())
|
||||
}
|
||||
|
||||
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))
|
||||
fn ensure_local_instance_keys(conn: &Connection) -> Result<()> {
|
||||
for instance in Instance::get_locals(conn)? {
|
||||
instance.set_keypair(conn)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -377,7 +367,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 +387,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;
|
||||
|
||||
@@ -7,7 +7,6 @@ use chrono::NaiveDateTime;
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use plume_common::activity_pub::{
|
||||
inbox::{AsActor, AsObject, FromId},
|
||||
sign::Signer,
|
||||
Id, IntoId, PUBLIC_VISIBILITY,
|
||||
};
|
||||
|
||||
@@ -118,6 +117,7 @@ impl FromId<DbConn> for Like {
|
||||
NewLike {
|
||||
post_id: Post::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&act.like_props.object_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
@@ -126,6 +126,7 @@ impl FromId<DbConn> for Like {
|
||||
.id,
|
||||
user_id: User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&act.like_props.actor_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
@@ -138,10 +139,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 +150,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(¬if).execute(&**conn)?;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -143,7 +143,6 @@ macro_rules! func {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Queryable, Identifiable)]
|
||||
struct ListElem {
|
||||
pub id: i32,
|
||||
|
||||
+26
-30
@@ -3,11 +3,12 @@ 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},
|
||||
@@ -103,8 +104,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()
|
||||
{
|
||||
@@ -207,33 +208,31 @@ 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)?)
|
||||
Media::find_by_file_path(conn, &path.to_str()?)
|
||||
.and_then(|mut media| {
|
||||
let mut updated = false;
|
||||
|
||||
let alt_text = image
|
||||
.object_props
|
||||
.content_string()
|
||||
.or(Err(Error::NotFound))?;
|
||||
let alt_text = image.object_props.content_string().ok()?;
|
||||
let sensitive = image.object_props.summary_string().is_ok();
|
||||
let content_warning = image.object_props.summary_string().ok();
|
||||
if media.alt_text != alt_text {
|
||||
@@ -265,24 +264,21 @@ impl Media {
|
||||
Media::insert(
|
||||
conn,
|
||||
NewMedia {
|
||||
file_path: path.to_str().ok_or(Error::InvalidValue)?.to_string(),
|
||||
alt_text: image
|
||||
.object_props
|
||||
.content_string()
|
||||
.or(Err(Error::NotFound))?,
|
||||
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,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
image
|
||||
.object_props
|
||||
.attributed_to_link_vec::<Id>()
|
||||
.or(Err(Error::NotFound))?
|
||||
.ok()?
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(Error::NotFound)?
|
||||
.next()?
|
||||
.as_ref(),
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
@@ -330,7 +326,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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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));
|
||||
|
||||
+38
-26
@@ -10,12 +10,11 @@ use activitypub::{
|
||||
CustomObject,
|
||||
};
|
||||
use chrono::{NaiveDateTime, TimeZone, Utc};
|
||||
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||
use once_cell::sync::Lazy;
|
||||
use plume_common::{
|
||||
activity_pub::{
|
||||
inbox::{AsActor, AsObject, FromId},
|
||||
sign::Signer,
|
||||
Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILITY,
|
||||
},
|
||||
utils::{iri_percent_encode_seg, md_to_html},
|
||||
@@ -67,15 +66,15 @@ 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 = Self::ap_url(post.get_blog(conn)?, &post.slug);
|
||||
let _: Post = post.save_changes(conn)?;
|
||||
}
|
||||
|
||||
if post.published {
|
||||
post.publish_published();
|
||||
@@ -98,7 +97,7 @@ impl 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)?;
|
||||
@@ -458,14 +457,14 @@ impl Post {
|
||||
.filter_map(|(id, m)| id.map(|id| (m, id)))
|
||||
.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 +476,7 @@ impl Post {
|
||||
.iter()
|
||||
.filter(|m| !new_mentions.contains(&m.mentioned_id))
|
||||
{
|
||||
m.delete(conn)?;
|
||||
m.delete(&conn)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -631,13 +630,28 @@ impl FromId<DbConn> for Post {
|
||||
.into_iter()
|
||||
.fold((None, vec![]), |(blog, mut authors), link| {
|
||||
let url = link;
|
||||
match User::from_id(conn, &url, None, CONFIG.proxy()) {
|
||||
match User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&url,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
) {
|
||||
Ok(u) => {
|
||||
authors.push(u);
|
||||
(blog, authors)
|
||||
}
|
||||
Err(_) => (
|
||||
blog.or_else(|| Blog::from_id(conn, &url, None, CONFIG.proxy()).ok()),
|
||||
blog.or_else(|| {
|
||||
Blog::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&url,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.ok()
|
||||
}),
|
||||
authors,
|
||||
),
|
||||
}
|
||||
@@ -701,7 +715,7 @@ impl FromId<DbConn> for Post {
|
||||
Post::insert(
|
||||
conn,
|
||||
NewPost {
|
||||
blog_id: blog.ok_or(Error::NotFound)?.id,
|
||||
blog_id: blog?.id,
|
||||
slug: Self::slug(&title).to_string(),
|
||||
title,
|
||||
content: SafeString::new(&article.object_props.content_string()?),
|
||||
@@ -760,10 +774,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 +845,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 {
|
||||
@@ -846,8 +852,14 @@ impl AsObject<User, Update, &DbConn> for PostUpdate {
|
||||
type Output = ();
|
||||
|
||||
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
||||
let mut post =
|
||||
Post::from_id(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?;
|
||||
let mut post = Post::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&self.ap_url,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.map_err(|(_, e)| e)?;
|
||||
|
||||
if !post.is_author(conn, actor.id)? {
|
||||
// TODO: maybe the author was added in the meantime
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
db_conn::{DbConn, DbPool},
|
||||
follows,
|
||||
instance::Instance,
|
||||
posts::{LicensedArticle, Post},
|
||||
users::{User, UserEvent},
|
||||
ACTOR_SYS, CONFIG, USER_CHAN,
|
||||
@@ -89,7 +90,13 @@ fn fetch_and_cache_followers(user: &Arc<User>, conn: &DbConn) {
|
||||
match follower_ids {
|
||||
Ok(user_ids) => {
|
||||
for user_id in user_ids {
|
||||
let follower = User::from_id(conn, &user_id, None, CONFIG.proxy());
|
||||
let follower = User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&user_id,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
);
|
||||
match follower {
|
||||
Ok(follower) => {
|
||||
let inserted = follows::Follow::insert(
|
||||
|
||||
@@ -7,7 +7,6 @@ use chrono::NaiveDateTime;
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use plume_common::activity_pub::{
|
||||
inbox::{AsActor, AsObject, FromId},
|
||||
sign::Signer,
|
||||
Id, IntoId, PUBLIC_VISIBILITY,
|
||||
};
|
||||
|
||||
@@ -143,6 +142,7 @@ impl FromId<DbConn> for Reshare {
|
||||
NewReshare {
|
||||
post_id: Post::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&act.announce_props.object_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
@@ -151,6 +151,7 @@ impl FromId<DbConn> for Reshare {
|
||||
.id,
|
||||
user_id: User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&act.announce_props.actor_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
@@ -163,10 +164,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 +175,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(¬if).execute(&**conn)?;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -116,6 +106,8 @@ table! {
|
||||
default_license -> Text,
|
||||
long_description_html -> Varchar,
|
||||
short_description_html -> Varchar,
|
||||
private_key -> Nullable<Text>,
|
||||
public_key -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,7 +308,6 @@ allow_tables_to_appear_in_same_query!(
|
||||
blogs,
|
||||
comments,
|
||||
comment_seers,
|
||||
email_signups,
|
||||
follows,
|
||||
instances,
|
||||
likes,
|
||||
|
||||
@@ -164,6 +164,8 @@ mod tests {
|
||||
name: random_hex().to_string(),
|
||||
open_registrations: true,
|
||||
public_domain: random_hex().to_string(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,16 @@ 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![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..];
|
||||
}
|
||||
|
||||
|
||||
+80
-51
@@ -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, Error as SignatureError, Result as SignatureResult, 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,22 +204,6 @@ 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
|
||||
@@ -226,7 +212,8 @@ impl User {
|
||||
.ok_or(Error::Webfinger)?;
|
||||
User::from_id(
|
||||
conn,
|
||||
link.href.as_ref().ok_or(Error::Webfinger)?,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
link.href.as_ref()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
@@ -243,7 +230,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 +262,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();
|
||||
|
||||
@@ -434,12 +434,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 +448,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 +470,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"]
|
||||
@@ -484,11 +497,20 @@ impl User {
|
||||
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 +520,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 +542,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 +727,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 +950,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,
|
||||
@@ -934,6 +965,8 @@ impl FromId<DbConn> for User {
|
||||
open_registrations: true,
|
||||
short_description_html: String::new(),
|
||||
long_description_html: String::new(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
)
|
||||
})?;
|
||||
@@ -1007,10 +1040,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 {
|
||||
@@ -1047,18 +1076,18 @@ impl Signer for User {
|
||||
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) -> SignatureResult<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(SignatureError::from)
|
||||
}
|
||||
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> SignResult<bool> {
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<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(SignatureError::from)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1099,7 +1128,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
@@ -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
@@ -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
@@ -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
@@ -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 l’editor 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 ""
|
||||
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
+12
-12
@@ -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: Vietnamese\n"
|
||||
"Language: vi_VN\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 "Văn bản của tôi"
|
||||
|
||||
# plume-front/src/editor.rs:385
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "Tiêu Châu"
|
||||
|
||||
# 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 ""
|
||||
|
||||
|
||||
+30
-30
@@ -3,61 +3,61 @@ 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: Chinese Traditional\n"
|
||||
"Language: zh_TW\n"
|
||||
"Language-Team: Chinese Simplified\n"
|
||||
"Language: zh_CN\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Crowdin-Project: plume\n"
|
||||
"X-Crowdin-Project-ID: 352097\n"
|
||||
"X-Crowdin-Language: zh-TW\n"
|
||||
"X-Crowdin-Language: zh-CN\n"
|
||||
"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:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "打开富文本编辑器"
|
||||
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "标题"
|
||||
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr "副标题或摘要"
|
||||
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "開啟 RTF 編輯器"
|
||||
|
||||
# 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 "在這裡寫下您的文章。支援 Markdown 語法。"
|
||||
msgstr "在这里写下您的文章。支持 Markdown 语法。"
|
||||
|
||||
# plume-front/src/editor.rs:407
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "大約還可輸入 {} 字符"
|
||||
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 "封面"
|
||||
|
||||
# plume-front/src/editor.rs:564
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "這是草稿"
|
||||
msgstr "这是一个草稿"
|
||||
|
||||
# plume-front/src/editor.rs:575
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "發布"
|
||||
msgstr "发布"
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user