Compare commits

...

72 Commits

Author SHA1 Message Date
Mina Galić b596e77f03 remove redundant use statements 2020-05-25 20:00:57 +02:00
Mina Galić 41f97b01f0 unroll filter_map() to easier .await 2020-05-25 20:00:28 +02:00
Mina Galić a508a4150c remove redundant use statements 2020-05-25 19:18:24 +02:00
Mina Galić 25c40adf20 yet another dubious clippy warnings fix re returns 2020-05-25 19:17:32 +02:00
Mina Galić 7490567a21 fix warnings about unused doc comments
we do this by making the macro parse and generate doc comments
2020-05-25 15:44:32 +02:00
Mina Galić 492bbb1ba6 make clippy happy with a weird quirk wrt return
not happy about this lint or rust behaviour where the return statement
must not have a `;`
2020-05-25 15:23:55 +02:00
Mina Galić cf3708e1c6 make clippy happy by removing unused imports 2020-05-25 13:40:35 +02:00
Mina Galić df442002c2 replace .map().map_err() with a match
so we can .await without worries.
plus, it changes nothing about the code, other than making the intent
clearer at first sight.
2020-05-24 22:16:42 +02:00
Mina Galić 07036b5fad upgrade validator: it now uses types! in macros!! 2020-05-24 22:03:26 +02:00
Mina Galić 0726375859 add another async (and correctly convert followers_count) 2020-05-24 21:03:59 +02:00
Mina Galić cb1c260692 remove an experiment of disabling Send… it makes no sense 2020-05-24 21:03:02 +02:00
Mina Galić de6bfca084 removed a few unused imports 2020-05-24 21:01:16 +02:00
Mina Galić 7aabb9661e upgrade webfinger everywhere, and implement async 2020-05-24 20:27:39 +02:00
Mina Galić 18bb413011 add async/.await until all our errors are the same:
that our Connection is not Send-safe.
2020-05-24 19:40:02 +02:00
Mina Galić d2881ee3f7 add async/.await until all our errors are the same: that our Connection is not Send-safe. 2020-05-23 23:32:10 +02:00
Mina Galić 850b3c1337 add async/.await until all our errors are the same:
that our Connection is not Send-safe.
once we get there, we can start thinking about restructing the way we
pass along our connection, or consider using #[database].
2020-05-22 21:41:33 +02:00
Igor Galić 44ebce516c fix some, break some compiling by adding async/await in front of it
i forgot that we can't `Send` diesel connections over threads, so this
is going to require some rethinking.
2020-05-16 12:06:58 +02:00
Igor Galić 3c830ab0ce move towards using #[rocket::async_trait]
this also upgrades some dependencies
some of that fixes stuff, others breaks stuff.
2020-05-15 22:38:21 +02:00
Igor Galić 097d0ea9ce make plume-models async (again) 2020-05-06 22:33:10 +02:00
Igor Galić 6fe16c9f84 upgrade and use futures… then block_on .await in a trait? 2020-05-06 22:32:59 +02:00
Igor Galić 43cb9f700c update webfinger
as requested in https://github.com/Plume-org/webfinger/issues/8
and provided in https://github.com/Plume-org/webfinger/pull/9

```
meena@76ix ~/s/a/plume (go/async) [101]> cargo update --package webfinger
    Updating crates.io index
    Updating git repository `https://github.com/Plume-org/webfinger`
    Removing bytes v0.4.12
    Removing cookie v0.12.0
    Removing cookie_store v0.7.0
    Removing crossbeam-deque v0.7.2
    Removing crossbeam-epoch v0.8.0
    Removing crossbeam-queue v0.1.2
    Removing crossbeam-utils v0.7.0
    Removing h2 v0.1.26
    Removing http v0.1.21
    Removing http-body v0.1.0
    Removing hyper v0.12.35
    Removing hyper-tls v0.3.2
    Removing publicsuffix v1.5.4
    Removing reqwest v0.9.24
    Removing serde_urlencoded v0.5.5
    Removing string v0.2.1
    Removing tokio v0.1.22
    Removing tokio-buf v0.1.1
    Removing tokio-current-thread v0.1.6
    Removing tokio-executor v0.1.9
    Removing tokio-io v0.1.12
    Removing tokio-reactor v0.1.11
    Removing tokio-sync v0.1.7
    Removing tokio-tcp v0.1.3
    Removing tokio-threadpool v0.1.17
    Removing tokio-timer v0.2.12
    Removing try_from v0.3.2
    Removing want v0.2.0
      Adding webfinger v0.5.0 (https://github.com/Plume-org/webfinger?rev=update-deps#cdaab95e)
    Removing webfinger v0.5.0
meena@76ix ~/s/a/plume (go/async)>
```
2020-05-06 22:30:33 +02:00
Igor Galić 2c285b9aca start async-ifying routes
template utils and error routes
2020-05-06 22:29:36 +02:00
Igor Galić e4bb73d22e cargo clippy 2020-05-06 22:29:36 +02:00
Igor Galić e9c7259ffb cargo fmt 2020-05-06 22:29:35 +02:00
Igor Galić be8c67ee9a move reqwest client out of thread spawning
this way, we only spawn one, and reuse that.
2020-05-06 22:29:34 +02:00
Igor Galić 65b2c38c29 .await? result from read_to_string() 2020-05-06 22:29:33 +02:00
Jeb Rosen 8aa99cea35 move signature outside the spawning
this allows us to actually move stuff into the async block
and we can drop the 'static life-time.
2020-05-06 22:29:32 +02:00
Igor Galić a010025074 asyncify reqwest calls (again?) 2020-05-06 22:29:32 +02:00
Igor Galić 82088596a8 asyncify from_activity calls (i.e.: block_on()) 2020-05-06 22:29:31 +02:00
Igor Galić 87ce3a7b51 asyncify plume-models: media upload is now async
including the use of tokio!
2020-05-06 22:29:27 +02:00
Igor Galić 3472a58299 move ClientBuilder into thread, since we cannot Copy it 2020-05-06 22:26:37 +02:00
Igor Galić a3f165f9f4 Use blocking reqwest API in defer
defer, or, trait functions such as it in general(?) cannot be async (yet)
2020-05-06 22:26:32 +02:00
Igor Galić 25c5da1a7c add tokio (0.2) as dependency to further async-ify our FromData code
i'm using this opportunity to also update reqwest (0.10), but it's
turning out to be a little trickier, as it requires more modern async
setup, and that appears to need a lot of thinking…
2020-05-06 22:23:35 +02:00
Igor Galić 022e037eea when using macros!() we need to import the things that they use 2020-05-06 22:20:49 +02:00
Igor Galić 45c335e17b "manually" create ETag and Cache-Control headers 2020-05-06 22:20:48 +02:00
Igor Galić b51551973a start fixing tests in plume-models 2020-05-06 22:20:47 +02:00
Igor Galić 59e5c49aa8 convert plume-models to all async
where sensible! note that State has no asynchronous work to do, so
wrapping it up in async makes no sense.
2020-05-06 22:20:47 +02:00
Igor Galić ce119ffe50 start making PlumeRocket async 2020-05-06 22:20:46 +02:00
Igor Galić 944f8c42fa plume-models: convert api-tokens. use DbConn::from_request() directly
there doesn't seem to be a request.guard_async (yet?)
2020-05-06 22:17:25 +02:00
Igor Galić 909f677bdd plume-models: convert admin & api-tokens to async
n.b.: I do *not* like the error handling in api_tokens 😒
2020-05-06 22:17:24 +02:00
Igor Galić fd9764ff17 plume-common: also make requests async 2020-05-06 22:17:24 +02:00
Igor Galić 75722abc9e rocket does not need decl_macro anymore 2020-05-06 22:17:23 +02:00
Igor Galić ec9b699c6e convert plume-common to rocket async
it only took 3 hours of @jebrosen's patient help.
2020-05-06 22:17:22 +02:00
Igor Galić bb5c2b69a7 update rocket* everywhere!
and run cargo update
2020-05-06 22:17:17 +02:00
Igor Galić e52944e477 update rocket*; which gets us stuck in dependency conflicts 2020-05-06 21:01:25 +02:00
Igor Galić 928470610e remove csrf for now, so we can update the rest 2020-05-06 21:01:18 +02:00
KITAITI Makoto 3be842c653 Make database connections configurable by environment variables (#768)
* Make env DB_MAX_SIZE and DB_MIN_IDLE recognizable

* Make database max size and minimum idle configurable

* Restore file permission

* Fail fast
2020-05-06 19:27:59 +02:00
KITAITI Makoto dabe904642 Escape expressions in @Html (#767) 2020-05-05 23:48:29 +02:00
KITAITI Makoto 180e34b07c Fix Atom feed datetime format (#766) 2020-05-04 23:46:59 +02:00
KITAITI Makoto 847d6f7fac Fix Atom feed (#764)
* Percent-encode URI segments in ap_url

* Fix invalid Atoms

* Remove redundant clone according to cargo clippy

* Revert "Percent-encode URI segments in ap_url"

This reverts commit 8253aa79bd.
ActivityPub(JSON-LD) accepts URI.
See https://github.com/Plume-org/Plume/pull/764#issuecomment-623105734

* Use absolute IRI for IDs in Atom

* Reuse formatted string

* Use parent element's creation date for Atom updated if there's no post

* Remove uncecessary UTC conversion

* Extract routes::build_atom_feed function
2020-05-04 15:28:52 +02:00
KITAITI Makoto 6a3f210dfc Keep atmarks and hashes in link (#765)
* [REFACTORING]Use DocumentContext during parsing Markdown

* Add test case for hash in link

* Make DocumentContext derive Debug

* Add in_link field in DocumentContext

* Don't make hash words tags in link

* Add test for atmark in link

* Don't make atmark words in link mentions
2020-05-03 14:50:32 +02:00
KITAITI Makoto 73aa301d4a Fix user subscription URI so that plume::routes:interact accept it (#762) 2020-05-02 12:07:45 +02:00
KITAITI Makoto b3d367b174 Use plural form for 0 in French (#760)
See https://github.com/Plume-org/Plume/issues/745
2020-04-30 15:35:12 +02:00
KITAITI Makoto edaccd1a31 Fixes #757 Allow Unicode hashtags (#758)
* Add test for Persian language hashtags

See https://github.com/Plume-org/Plume/issues/757

* Add regex-syntax with unicode-perl feature to dependencies

* Install regex-syntax

* Allow hashtag to use Unicode word characters

* Run cargo fmt
2020-04-29 15:13:31 +02:00
Floréal Toumikian e1bd2eab75 Updated: Cargo.lock (#753) 2020-04-19 09:34:05 +02:00
KITAITI Makoto ac7a05b09a Make blocklist deletion possible (#750) 2020-04-18 11:46:10 +02:00
KITAITI Makoto 71e0a35e06 Use LenientForm instead Form for CSRF protected pages (#751) 2020-04-18 11:45:28 +02:00
KITAITI Makoto c217e5e9b3 Move stylesheets for header from _global.sccss to _header.scss (#749) 2020-04-14 23:32:27 +02:00
KITAITI Makoto 8ba0c17db5 Build app and po files before upload pos to Crowdin (#748) 2020-04-14 20:51:53 +02:00
Floréal Toumikian 4e43c676b4 Fixed: #713: Disply likes and boosts on post cards (#744)
This is a basic and simple way to display them. No interaction possible.

Alos, this patch is not optimised. as everytime a partial post_card is
called, `Post::count_likes()` and `Post::count_reshares()` are called
which means quite a few more quesies are sent to database unless diesel
uses some cache mechanisem.

A way to enhance this this would be to keep a count of likes and reshares
are kept in Post model / table.
2020-04-12 21:29:48 +02:00
KITAITI Makoto b834d1c282 Fix #701 Preferable default theme (#746)
* Make it possible to update users.preferred_theme to NULL

* Make preferred them null when 'Default theme' is selected

* Run cargo fmt
2020-04-12 17:36:00 +02:00
Kenneth Steimel 506fe9955d Fixed #734 , passowrds are now hidden on the remote_interact pages (#741)
* Fixed #734 , passowrds are now hidden on the remote_interact pages

* Update templates/remote_interact_base.rs.html

Co-Authored-By: Floréal Toumikian <floreal+github@nimukaito.net>

Co-authored-by: Floréal Toumikian <floreal+github@nimukaito.net>
2020-04-09 20:55:22 +02:00
KemoNine 02c528cae4 Bump llvm -> 9.0.0 (fixes arm builds) (#737) 2020-01-29 21:37:54 +01:00
Christopher James Halse Rogers 2a58835f92 Snapcraft updates (#731)
* snap: Update rust compiler

* snap: Fix the build.

We now need libclang

* snap: Fix the non-x86 builds.

The goddamned environment variable is SNAPCRAFT_ARCH_TRIPLE*T*

* snap: Mark as stable.

The snap works fine; we don't need to warn people away
2020-01-22 14:10:49 +02:00
Ana Gelez 5f8d6b8e0e Rust 2018! (#726) 2020-01-21 07:02:03 +01:00
fdb-hiroshima 3663bffe5c Update compiler (#725)
* update compiler to recent nightly

* cargo fmt

* fix clippy

but ructe code is still complaining

* update circleci and rocket_csrf

* fix last clippy problems

* cargo fmt

* fix build issue

was caused be improper handling of recursive template (comment) by old ructe, which is fixed on newer version
2020-01-19 12:52:32 +01:00
KemoNine 72464fb428 Add missing dep on clang needed for crate builds (#724) 2020-01-15 10:03:52 +01:00
Wilfried OLLIVIER 23049b638c WIP: Fix RSS issues (#720)
* Add id field to RSS feed

* Add published field to RSS feed
2020-01-13 19:30:32 -05:00
Violet White f3c05dae62 Email blocklisting (#718)
* Interface complete for the email blacklisting

* Everything seems to work

* Neutralize language

* fix clippy warnings

* Add missing spaces

* Added matching test

* Correct primary key datatype for postgresql

* Address review comments

* Add placeholder when empty. Fix missing 'i'
2020-01-12 19:41:35 +01:00
Violet White e6bdeb7c4b Fix #699 (#719) 2020-01-12 13:24:41 +01:00
Violet White 458baf5f78 Syntax highlighting (#691)
* Syntax highlighting mostly... Exists.

* Add dependency to dockerfile

* Handle non-existent languages better

* Make the default a bit nicer

* Improve highlighting. Clean up function

* Add dark theme, add the comment scope to the allowed classes

* update build env

* Address review comments

* Use find_syntax_by_token which produces the desired behavior

* Change flat_map into flatten
(commit cargo.lock)
2019-12-30 14:35:27 +01:00
Ana Gelez 597778fd2f release Alpha 4 (#709)
* Update version numbers

* Update translations again

* Translations?

* This time translations should be here
2019-12-22 15:04:09 +01:00
212 changed files with 16048 additions and 14325 deletions
+3 -2
View File
@@ -10,7 +10,7 @@ executors:
type: boolean
default: false
docker:
- image: plumeorg/plume-buildenv:v0.0.7
- image: plumeorg/plume-buildenv:v0.0.9
- image: <<#parameters.postgres>>circleci/postgres:9.6-alpine<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
environment:
POSTGRES_USER: postgres
@@ -82,7 +82,7 @@ commands:
type: string
steps:
- run: zip -0 ccov.zip `find . -name 'plume*.gc*' -o -name 'plm*.gc*'`
- run: grcov ccov.zip -s . -t lcov --llvm --branch --ignore-not-existing --ignore-dir '/*' -o lcov.info
- run: grcov ccov.zip -s . -t lcov --llvm --branch --ignore-not-existing --ignore '/*' -o lcov.info
- run: bash <(curl -s https://codecov.io/bash) -f lcov.info -F <<parameters.type>>
- run: find . -name 'plume*.gc*' -delete -o -name 'plm*.gc*' -delete
- run: rm ccov.zip lcov.info
@@ -226,6 +226,7 @@ jobs:
steps:
- restore_env:
cache: none
- run: cargo build
- run: crowdin upload -b master
workflows:
+2 -2
View File
@@ -4,11 +4,11 @@ ENV PATH="/root/.cargo/bin:${PATH}"
#install native/circleci/build dependancies
RUN apt update &&\
apt install -y --no-install-recommends git ssh tar gzip ca-certificates default-jre&&\
apt install -y --no-install-recommends binutils-dev build-essential cmake curl gcc gettext git libcurl4-openssl-dev libdw-dev libelf-dev libiberty-dev libpq-dev libsqlite3-dev libssl-dev make openssl pkg-config postgresql postgresql-contrib python zlib1g-dev python3-pip zip unzip &&\
apt install -y --no-install-recommends binutils-dev build-essential cmake curl gcc gettext git libcurl4-openssl-dev libdw-dev libelf-dev libiberty-dev libpq-dev libsqlite3-dev libssl-dev make openssl pkg-config postgresql postgresql-contrib python zlib1g-dev python3-pip zip unzip libclang-dev&&\
rm -rf /var/lib/apt/lists/*
#install and configure rust
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2019-03-23 -y &&\
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2020-01-15 -y &&\
rustup component add rustfmt clippy &&\
rustup component add rust-std --target wasm32-unknown-unknown
+1
View File
@@ -18,3 +18,4 @@ tags.*
search_index
.buildconfig
__pycache__
.vscode/
Generated
+2149 -1964
View File
File diff suppressed because it is too large Load Diff
+16 -12
View File
@@ -1,12 +1,14 @@
[package]
authors = ["Plume contributors"]
name = "plume"
version = "0.3.0"
version = "0.4.0"
repository = "https://github.com/Plume-org/Plume"
edition = "2018"
[dependencies]
activitypub = "0.1.3"
askama_escape = "0.1"
async-trait = "*"
atom_syndication = "0.6"
clap = "2.33"
colored = "1.8"
@@ -19,19 +21,19 @@ heck = "0.3.0"
lettre = "0.9.2"
lettre_email = "0.9.2"
num_cpus = "1.10"
rocket = "0.4.0"
rocket_contrib = { version = "0.4.0", features = ["json"] }
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "async" }
rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", rev = "async" , features = ["json"] }
rpassword = "4.0"
runtime-fmt = "0.3.0"
scheduled-thread-pool = "0.2.2"
serde = "1.0"
serde_json = "1.0"
serde_qs = "0.5"
shrinkwraprs = "0.2.1"
validator = "0.8"
validator_derive = "0.8"
webfinger = "0.4.1"
syntect = "3.3"
tokio = "0.2"
validator = "0.10"
validator_derive = "0.10"
webfinger = { git = "https://github.com/Plume-org/webfinger", rev = "4e8f12810c4a7ba7a07bbcb722cd265fdff512b6", features = ["async"] }
[[bin]]
name = "plume"
@@ -63,12 +65,14 @@ path = "plume-common"
[dependencies.plume-models]
path = "plume-models"
[dependencies.rocket_csrf]
git = "https://github.com/fdb-hiroshima/rocket_csrf"
rev = "4a72ea2ec716cb0b26188fb00bccf2ef7d1e031c"
[dependencies.rocket_i18n]
git = "https://github.com/Plume-org/rocket_i18n"
branch = "go-async"
default-features = false
features = ["rocket"]
[build-dependencies]
ructe = "0.6.2"
ructe = "0.9.0"
rsass = "0.9"
[features]
+2 -1
View File
@@ -10,7 +10,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
make \
openssl \
libssl-dev
libssl-dev \
clang
WORKDIR /scratch
COPY script/wasm-deps.sh .
@@ -17,3 +17,12 @@ $text-color: $white;
$primary: $purple;
$primary-text-color: $white; // text color on primary background (buttons for instance)
$success-color: $blue;
//Code Highlighting
$code-keyword-color: #f79ac1;
$code-source-color: #a6f0ab;
$code-constant-color: #dfec84;
$code-operator-color: #eddf95;
$code-string-color: #f2ae60;
$code-comment-color: #a3b4f9;
+47 -88
View File
@@ -190,7 +190,28 @@ p.error {
background: $gray;
text-overflow: ellipsis;
text-overflow: ellipsis;
footer.authors {
div {
float: left;
margin-right: 0.25em;
}
.likes { color: $red; }
.reshares { color: $primary; }
span.likes, span.resahres {
font-family: "Route159",serif;
font-size: 1em;
}
svg.feather {
width: 0.85em;
height: 0.85em;
}
}
> * {
margin: 20px;
@@ -469,93 +490,6 @@ figure {
/// Small screens
@media screen and (max-width: 600px) {
@keyframes menuOpening {
from {
transform: scaleX(0);
transform-origin: left;
opacity: 0;
}
to {
transform: scaleX(1);
transform-origin: left;
opacity: 1;
}
}
body > header {
flex-direction: column;
nav#menu {
display: inline-flex;
z-index: 21;
}
#content {
display: none;
appearance: none;
text-align: center;
z-index: 20;
}
}
body > header:focus-within #content, #content.show {
position: fixed;
display: flex;
flex-direction: column;
justify-content: flex-start;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
animation: 0.2s menuOpening;
&::before {
content: "";
position: absolute;
transform: skewX(-10deg);
top: 0;
left: -20%;
width: 100%;
height: 100%;
z-index: -10;
background: $primary;
}
> nav {
flex-direction: column;
align-items: flex-start;
a {
display: flex;
flex-direction: row;
align-items: center;
margin: 0;
padding: 1rem 1.5rem;
color: $background;
font-size: 1.4em;
font-weight: 300;
&.title { font-size: 1.8em; }
> *:first-child { width: 3rem; }
> img:first-child { height: 3rem; }
> *:last-child { margin-left: 1rem; }
> nav hr {
display: block;
margin: 0;
width: 100%;
border: solid $background 0.1rem;
}
.mobile-label { display: initial; }
}
}
}
main .article-meta {
> *, .comments {
margin: 0 5%;
@@ -623,3 +557,28 @@ figure {
}
}
}
//highlighting
code {
.constant{
color: $code-constant-color;
}
.string{
color: $code-string-color;
}
.keyword.type,.keyword.control,.type{
color: $code-keyword-color;
}
.keyword.operator{
color: $code-operator-color;
}
.source{
color: $code-source-color;
}
.comment{
color: $code-comment-color;
}
.function{
color:inherit;
}
}
+90
View File
@@ -101,6 +101,96 @@ body > header {
}
}
/// Small screens
@media screen and (max-width: 600px) {
@keyframes menuOpening {
from {
transform: scaleX(0);
transform-origin: left;
opacity: 0;
}
to {
transform: scaleX(1);
transform-origin: left;
opacity: 1;
}
}
body > header {
flex-direction: column;
nav#menu {
display: inline-flex;
z-index: 21;
}
#content {
display: none;
appearance: none;
text-align: center;
z-index: 20;
}
}
body > header:focus-within #content, #content.show {
position: fixed;
display: flex;
flex-direction: column;
justify-content: flex-start;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
animation: 0.2s menuOpening;
&::before {
content: "";
position: absolute;
transform: skewX(-10deg);
top: 0;
left: -20%;
width: 100%;
height: 100%;
z-index: -10;
background: $primary;
}
> nav {
flex-direction: column;
align-items: flex-start;
a {
display: flex;
flex-direction: row;
align-items: center;
margin: 0;
padding: 1rem 1.5rem;
color: $background;
font-size: 1.4em;
font-weight: 300;
&.title { font-size: 1.8em; }
> *:first-child { width: 3rem; }
> img:first-child { height: 3rem; }
> *:last-child { margin-left: 1rem; }
> nav hr {
display: block;
margin: 0;
width: 100%;
border: solid $background 0.1rem;
}
.mobile-label { display: initial; }
}
}
}
}
/* Only enable label animations on large screens */
@media screen and (min-width: 600px) {
header nav a {
+8
View File
@@ -27,3 +27,11 @@ $margin: 0 $horizontal-margin;
$route159: "Route159", serif;
$playfair: "Playfair Display", serif;
$lora: "Lora", serif;
//Code Highlighting
$code-keyword-color: #45244a;
$code-source-color: #4c588c;
$code-constant-color: scale-color(magenta,$lightness:-5%);
$code-operator-color: scale-color($code-source-color,$lightness:-5%);
$code-string-color: #8a571c;
$code-comment-color: #1c4c8a;
+1 -3
View File
@@ -1,8 +1,6 @@
extern crate rsass;
extern crate ructe;
use ructe::Ructe;
use std::process::{Command, Stdio};
use std::{env, ffi::OsStr, fs::*, io::Write, path::*};
use std::{ffi::OsStr, fs::*, io::Write, path::*};
fn compute_static_hash() -> String {
//"find static/ -type f ! -path 'static/media/*' | sort | xargs stat -c'%n %Y' | openssl dgst -r"
@@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
drop table email_blocklist;
@@ -0,0 +1,6 @@
-- Your SQL goes here
CREATE TABLE email_blocklist(id SERIAL PRIMARY KEY,
email_address TEXT UNIQUE,
note TEXT,
notify_user BOOLEAN DEFAULT FALSE,
notification_text TEXT);
@@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
drop table email_blocklist;
@@ -0,0 +1,6 @@
-- Your SQL goes here
CREATE TABLE email_blocklist(id INTEGER PRIMARY KEY,
email_address TEXT UNIQUE,
note TEXT,
notify_user BOOLEAN DEFAULT FALSE,
notification_text TEXT);
+2 -1
View File
@@ -1,7 +1,8 @@
[package]
name = "plume-api"
version = "0.3.0"
version = "0.4.0"
authors = ["Plume contributors"]
edition = "2018"
[dependencies]
serde = "1.0"
-1
View File
@@ -1,4 +1,3 @@
extern crate serde;
#[macro_use]
extern crate serde_derive;
+2 -1
View File
@@ -1,7 +1,8 @@
[package]
name = "plume-cli"
version = "0.3.0"
version = "0.4.0"
authors = ["Plume contributors"]
edition = "2018"
[[bin]]
name = "plm"
+1 -5
View File
@@ -1,8 +1,4 @@
extern crate clap;
extern crate diesel;
extern crate dotenv;
extern crate plume_models;
extern crate rpassword;
use dotenv;
use clap::App;
use diesel::Connection;
+13 -6
View File
@@ -1,25 +1,28 @@
[package]
name = "plume-common"
version = "0.3.0"
version = "0.4.0"
authors = ["Plume contributors"]
edition = "2018"
[dependencies]
activitypub = "0.1.1"
activitystreams-derive = "0.1.1"
activitystreams-derive = "0.2"
activitystreams-traits = "0.1.0"
array_tool = "1.0"
base64 = "0.10"
futures-util = "*"
heck = "0.3.0"
hex = "0.3"
hyper = "0.12.33"
hyper = "0.13"
openssl = "0.10.22"
rocket = "0.4.0"
reqwest = "0.9"
rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "async" }
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
shrinkwraprs = "0.2.1"
tokio = "0.1.22"
syntect = "3.3"
tokio = "0.2"
regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] }
[dependencies.chrono]
features = ["serde"]
@@ -28,3 +31,7 @@ version = "0.4"
[dependencies.pulldown-cmark]
default-features = false
version = "0.2.0"
[dependencies.reqwest]
features = ["json", "blocking"]
version = "0.10"
+4 -3
View File
@@ -279,8 +279,9 @@ pub trait FromId<C>: Sized {
/// Dereferences an ID
fn deref(id: &str) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
reqwest::ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
// Use blocking reqwest API here, since defer cannot be async (yet)
reqwest::blocking::Client::builder()
.connect_timeout(std::time::Duration::from_secs(5))
.build()
.map_err(|_| (None, InboxError::DerefError.into()))?
.get(id)
@@ -296,7 +297,7 @@ pub trait FromId<C>: Sized {
)
.send()
.map_err(|_| (None, InboxError::DerefError))
.and_then(|mut r| {
.and_then(|r| {
let json: serde_json::Value = r
.json()
.map_err(|_| (None, InboxError::InvalidObject(None)))?;
+42 -37
View File
@@ -1,14 +1,12 @@
use activitypub::{Activity, Link, Object};
use array_tool::vec::Uniq;
use reqwest::r#async::ClientBuilder;
use reqwest::ClientBuilder;
use rocket::{
http::Status,
request::{FromRequest, Request},
response::{Responder, Response},
response::{Responder, Response, Result},
Outcome,
};
use serde_json;
use tokio::prelude::*;
use self::sign::Signable;
@@ -62,39 +60,45 @@ impl<T> ActivityStream<T> {
ActivityStream(t)
}
}
impl<'r, O: Object> Responder<'r> for ActivityStream<O> {
fn respond_to(self, request: &Request) -> Result<Response<'r>, Status> {
#[rocket::async_trait]
impl<'r, O: Object + Send + 'r> Responder<'r> for ActivityStream<O> {
async fn respond_to(self, request: &'r Request<'_>) -> Result<'r> {
let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?;
json["@context"] = context();
serde_json::to_string(&json).respond_to(request).map(|r| {
Response::build_from(r)
let result = serde_json::to_string(&json).map_err(rocket::response::Debug);
match result.respond_to(request).await {
Ok(r) => Response::build_from(r)
.raw_header("Content-Type", "application/activity+json")
.finalize()
})
.ok(),
Err(e) => Err(e),
}
}
}
#[derive(Clone)]
pub struct ApRequest;
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for ApRequest {
type Error = ();
fn from_request(request: &'a Request<'r>) -> Outcome<Self, (Status, Self::Error), ()> {
async fn from_request(request: &'a Request<'r>) -> Outcome<Self, (Status, Self::Error), ()> {
request
.headers()
.get_one("Accept")
.map(|header| {
header
.split(',')
.map(|ct| match ct.trim() {
// bool for Forward: true if found a valid Content-Type for Plume first (HTML), false otherwise
"application/ld+json; profile=\"https://w3.org/ns/activitystreams\""
| "application/ld+json;profile=\"https://w3.org/ns/activitystreams\""
| "application/activity+json"
| "application/ld+json" => Outcome::Success(ApRequest),
"text/html" => Outcome::Forward(true),
_ => Outcome::Forward(false),
.map(|ct| {
match ct.trim() {
// bool for Forward: true if found a valid Content-Type for Plume first (HTML),
// false otherwise
"application/ld+json; profile=\"https://w3.org/ns/activitystreams\""
| "application/ld+json;profile=\"https://w3.org/ns/activitystreams\""
| "application/activity+json"
| "application/ld+json" => Outcome::Success(ApRequest),
"text/html" => Outcome::Forward(true),
_ => Outcome::Forward(false),
}
})
.fold(Outcome::Forward(false), |out, ct| {
if out.clone().forwarded().unwrap_or_else(|| out.is_success()) {
@@ -130,36 +134,38 @@ where
.sign(sender)
.expect("activity_pub::broadcast: signature error");
let mut rt = tokio::runtime::current_thread::Runtime::new()
.expect("Error while initializing tokio runtime for federation");
let client = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(5))
let rt = tokio::runtime::Builder::new()
.threaded_scheduler()
.build()
.expect("Can't build client");
.expect("Error while initializing tokio runtime for federation");
for inbox in boxes {
let body = signed.to_string();
let mut headers = request::headers();
headers.insert("Digest", request::Digest::digest(&body));
rt.spawn(
let sig = request::signature(sender, &headers)
.expect("activity_pub::broadcast: request signature error");
let client = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(5))
.build()
.expect("Can't build client");
rt.spawn(async move {
client
.post(&inbox)
.headers(headers.clone())
.header(
"Signature",
request::signature(sender, &headers)
.expect("activity_pub::broadcast: request signature error"),
)
.header("Signature", sig)
.body(body)
.send()
.and_then(|r| r.into_body().concat2())
.await
.unwrap()
.text()
.await
.map(move |response| {
println!("Successfully sent activity to inbox ({})", inbox);
println!("Response: \"{:?}\"\n", response)
})
.map_err(|e| println!("Error while sending to inbox ({:?})", e)),
);
.map_err(|e| println!("Error while sending to inbox ({:?})", e))
});
}
rt.run().unwrap();
}
#[derive(Shrinkwrap, Clone, Serialize, Deserialize)]
@@ -203,8 +209,7 @@ pub struct PublicKey {
pub public_key_pem: Option<serde_json::Value>,
}
#[derive(Clone, Debug, Default, UnitString)]
#[activitystreams(Hashtag)]
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct HashtagType;
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
+2 -3
View File
@@ -1,12 +1,11 @@
use base64;
use chrono::{offset::Utc, DateTime};
use openssl::hash::{Hasher, MessageDigest};
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, DATE, USER_AGENT};
use std::ops::Deref;
use std::time::SystemTime;
use activity_pub::sign::Signer;
use activity_pub::{ap_accept_header, AP_CONTENT_TYPE};
use crate::activity_pub::sign::Signer;
use crate::activity_pub::{ap_accept_header, AP_CONTENT_TYPE};
const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION"));
+1 -4
View File
@@ -1,10 +1,7 @@
use super::request;
use base64;
use chrono::{naive::NaiveDateTime, DateTime, Duration, Utc};
use hex;
use openssl::{pkey::PKey, rsa::Rsa, sha::sha256};
use rocket::http::HeaderMap;
use serde_json;
/// Returns (public key, private key)
pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
@@ -131,7 +128,7 @@ impl SignatureValidity {
pub fn verify_http_headers<S: Signer + ::std::fmt::Debug>(
sender: &S,
all_headers: &HeaderMap,
all_headers: &HeaderMap<'_>,
data: &request::Digest,
) -> SignatureValidity {
let sig_header = all_headers.get_one("Signature");
+2 -14
View File
@@ -1,26 +1,14 @@
#![feature(custom_attribute, associated_type_defaults)]
#![feature(associated_type_defaults)]
extern crate activitypub;
#[macro_use]
extern crate activitystreams_derive;
extern crate activitystreams_traits;
extern crate array_tool;
extern crate base64;
extern crate chrono;
extern crate heck;
extern crate hex;
extern crate openssl;
extern crate pulldown_cmark;
extern crate reqwest;
extern crate rocket;
extern crate serde;
#[macro_use]
extern crate shrinkwraprs;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate serde_json;
extern crate tokio;
pub mod activity_pub;
pub mod utils;
+97 -18
View File
@@ -1,12 +1,15 @@
use heck::CamelCase;
use openssl::rand::rand_bytes;
use pulldown_cmark::{html, Event, Options, Parser, Tag};
use regex_syntax::is_word_character;
use rocket::{
http::uri::Uri,
response::{Flash, Redirect},
};
use std::borrow::Cow;
use std::collections::HashSet;
use syntect::html::ClassedHTMLGenerator;
use syntect::parsing::SyntaxSet;
/// Generates an hexadecimal representation of 32 bytes of random data
pub fn random_hex() -> String {
@@ -46,7 +49,7 @@ enum State {
Ready,
}
fn to_inline(tag: Tag) -> Tag {
fn to_inline(tag: Tag<'_>) -> Tag<'_> {
match tag {
Tag::Header(_) | Tag::Table(_) | Tag::TableHead | Tag::TableRow | Tag::TableCell => {
Tag::Paragraph
@@ -55,7 +58,54 @@ fn to_inline(tag: Tag) -> Tag {
t => t,
}
}
struct HighlighterContext {
content: Vec<String>,
}
fn highlight_code<'a>(
context: &mut Option<HighlighterContext>,
evt: Event<'a>,
) -> Option<Vec<Event<'a>>> {
match evt {
Event::Start(Tag::CodeBlock(lang)) => {
if lang.is_empty() {
Some(vec![Event::Start(Tag::CodeBlock(lang))])
} else {
*context = Some(HighlighterContext { content: vec![] });
Some(vec![Event::Start(Tag::CodeBlock(lang))])
}
}
Event::End(Tag::CodeBlock(x)) => {
let mut result = vec![];
if let Some(ctx) = context.take() {
let syntax_set = SyntaxSet::load_defaults_newlines();
let syntax = syntax_set.find_syntax_by_token(&x).unwrap_or_else(|| {
syntax_set
.find_syntax_by_name(&x)
.unwrap_or_else(|| syntax_set.find_syntax_plain_text())
});
let mut html = ClassedHTMLGenerator::new(&syntax, &syntax_set);
for line in ctx.content {
html.parse_html_for_line(&line);
}
let q = html.finalize();
result.push(Event::Html(q.into()));
}
result.push(Event::End(Tag::CodeBlock(x)));
*context = None;
Some(result)
}
Event::Text(t) => {
if let Some(mut c) = context.take() {
c.content.push(t.to_string());
*context = Some(c);
Some(vec![])
} else {
Some(vec![Event::Text(t)])
}
}
_ => Some(vec![evt]),
}
}
fn flatten_text<'a>(state: &mut Option<String>, evt: Event<'a>) -> Option<Vec<Event<'a>>> {
let (s, res) = match evt {
Event::Text(txt) => match state.take() {
@@ -97,7 +147,7 @@ fn inline_tags<'a>(
}
}
pub type MediaProcessor<'a> = Box<'a + Fn(i32) -> Option<(String, Option<String>)>>;
pub type MediaProcessor<'a> = Box<dyn 'a + Fn(i32) -> Option<(String, Option<String>)>>;
fn process_image<'a, 'b>(
evt: Event<'a>,
@@ -108,9 +158,7 @@ fn process_image<'a, 'b>(
match evt {
Event::Start(Tag::Image(id, title)) => {
if let Some((url, cw)) = id.parse::<i32>().ok().and_then(processor.as_ref()) {
if inline || cw.is_none() {
Event::Start(Tag::Image(Cow::Owned(url), title))
} else {
if let (Some(cw), false) = (cw, inline) {
// there is a cw, and where are not inline
Event::Html(Cow::Owned(format!(
r#"<label for="postcontent-cw-{id}">
@@ -121,9 +169,11 @@ fn process_image<'a, 'b>(
</span>
<img src="{url}" alt=""#,
id = random_hex(),
cw = cw.unwrap(),
cw = cw,
url = url
)))
} else {
Event::Start(Tag::Image(Cow::Owned(url), title))
}
} else {
Event::Start(Tag::Image(id, title))
@@ -151,6 +201,12 @@ fn process_image<'a, 'b>(
}
}
#[derive(Default, Debug)]
struct DocumentContext {
in_code: bool,
in_link: bool,
}
/// Returns (HTML, mentions, hashtags)
pub fn md_to_html<'a>(
md: &str,
@@ -165,14 +221,32 @@ pub fn md_to_html<'a>(
};
let parser = Parser::new_ext(md, Options::all());
let (parser, mentions, hashtags): (Vec<Event>, Vec<String>, Vec<String>) = parser
let (parser, mentions, hashtags): (Vec<Event<'_>>, Vec<String>, Vec<String>) = parser
// Flatten text because pulldown_cmark break #hashtag in two individual text elements
.scan(None, flatten_text)
.flat_map(IntoIterator::into_iter)
.flatten()
.scan(None, highlight_code)
.flatten()
.map(|evt| process_image(evt, inline, &media_processor))
// Ignore headings, images, and tables if inline = true
.scan((vec![], inline), inline_tags)
.map(|evt| match evt {
.scan(&mut DocumentContext::default(), |ctx, evt| match evt {
Event::Start(Tag::CodeBlock(_)) | Event::Start(Tag::Code) => {
ctx.in_code = true;
Some((vec![evt], vec![], vec![]))
}
Event::End(Tag::CodeBlock(_)) | Event::End(Tag::Code) => {
ctx.in_code = false;
Some((vec![evt], vec![], vec![]))
}
Event::Start(Tag::Link(_, _)) => {
ctx.in_link = true;
Some((vec![evt], vec![], vec![]))
}
Event::End(Tag::Link(_, _)) => {
ctx.in_link = false;
Some((vec![evt], vec![], vec![]))
}
Event::Text(txt) => {
let (evts, _, _, _, new_mentions, new_hashtags) = txt.chars().fold(
(vec![], State::Ready, String::new(), 0, vec![], vec![]),
@@ -188,7 +262,7 @@ pub fn md_to_html<'a>(
text_acc.push(c)
}
let mention = text_acc;
let short_mention = mention.splitn(1, '@').nth(0).unwrap_or("");
let short_mention = mention.splitn(1, '@').next().unwrap_or("");
let link = Tag::Link(
format!("{}@/{}/", base_url, &mention).into(),
short_mention.to_owned().into(),
@@ -210,7 +284,7 @@ pub fn md_to_html<'a>(
}
}
State::Hashtag => {
let char_matches = c.is_alphanumeric() || "-_".contains(c);
let char_matches = c == '-' || is_word_character(c);
if char_matches && (n < (txt.chars().count() - 1)) {
text_acc.push(c);
(events, State::Hashtag, text_acc, n + 1, mentions, hashtags)
@@ -241,7 +315,7 @@ pub fn md_to_html<'a>(
}
}
State::Ready => {
if c == '@' {
if !ctx.in_code && !ctx.in_link && c == '@' {
events.push(Event::Text(text_acc.into()));
(
events,
@@ -251,7 +325,7 @@ pub fn md_to_html<'a>(
mentions,
hashtags,
)
} else if c == '#' {
} else if !ctx.in_code && !ctx.in_link && c == '#' {
events.push(Event::Text(text_acc.into()));
(
events,
@@ -296,9 +370,9 @@ pub fn md_to_html<'a>(
}
},
);
(evts, new_mentions, new_hashtags)
Some((evts, new_mentions, new_hashtags))
}
_ => (vec![evt], vec![], vec![]),
_ => Some((vec![evt], vec![], vec![])),
})
.fold(
(vec![], vec![], vec![]),
@@ -335,9 +409,12 @@ mod tests {
("mention at @end", vec!["end"]),
("between parenthesis (@test)", vec!["test"]),
("with some punctuation @test!", vec!["test"]),
(" @spaces ", vec!["spaces"]),
(" @spaces ", vec!["spaces"]),
("@is_a@mention", vec!["is_a@mention"]),
("not_a@mention", vec![]),
("`@helo`", vec![]),
("```\n@hello\n```", vec![]),
("[@atmark in link](https://example.org/)", vec![]),
];
for (md, mentions) in tests {
@@ -361,8 +438,10 @@ mod tests {
("hashtag at #end", vec!["end"]),
("between parenthesis (#test)", vec!["test"]),
("with some punctuation #test!", vec!["test"]),
(" #spaces ", vec!["spaces"]),
(" #spaces ", vec!["spaces"]),
("not_a#hashtag", vec![]),
("#نرم‌افزار_آزاد", vec!["نرم‌افزار_آزاد"]),
("[#hash in link](https://example.org/)", vec![]),
];
for (md, mentions) in tests {
+3 -2
View File
@@ -1,7 +1,8 @@
[package]
name = "plume-front"
version = "0.3.0"
version = "0.4.0"
authors = ["Plume contributors"]
edition = "2018"
[dependencies]
stdweb = "=0.4.18"
@@ -11,4 +12,4 @@ gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
lazy_static = "1.3"
serde = "1.0"
serde_json = "1.0"
serde_json = "1.0"
+1 -1
View File
@@ -1,3 +1,4 @@
use crate::CATALOG;
use serde::{Deserialize, Serialize};
use serde_json;
use std::sync::Mutex;
@@ -5,7 +6,6 @@ use stdweb::{
unstable::{TryFrom, TryInto},
web::{event::*, html_element::*, *},
};
use CATALOG;
macro_rules! mv {
( $( $var:ident ),* => $exp:expr ) => {
-3
View File
@@ -1,15 +1,12 @@
#![recursion_limit = "128"]
#![feature(decl_macro, proc_macro_hygiene, try_trait)]
extern crate gettext;
#[macro_use]
extern crate gettext_macros;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate stdweb;
extern crate serde;
extern crate serde_json;
use stdweb::web::{event::*, *};
init_i18n!(
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "plume-macro"
version = "0.1.0"
version = "0.4.0"
authors = ["Trinity Pointard <trinity.pointard@insa-rennes.fr>"]
edition = "2018"
description = "Plume procedural macros"
+11 -14
View File
@@ -1,8 +1,7 @@
#![recursion_limit = "128"]
extern crate proc_macro;
#[macro_use]
extern crate quote;
extern crate syn;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
@@ -103,19 +102,17 @@ fn file_to_migration(file: &str) -> TokenStream2 {
acc.push_str(line);
acc.push('\n');
}
} else if line.starts_with("--#!") {
acc.push_str(&line[4..]);
acc.push('\n');
} else if line.starts_with("--") {
continue;
} else {
if line.starts_with("--#!") {
acc.push_str(&line[4..]);
acc.push('\n');
} else if line.starts_with("--") {
continue;
} else {
let func: TokenStream2 = trampoline(TokenStream::from_str(&acc).unwrap().into());
actions.push(quote!(Action::Function(&#func)));
sql = true;
acc = line.to_string();
acc.push('\n');
}
let func: TokenStream2 = trampoline(TokenStream::from_str(&acc).unwrap().into());
actions.push(quote!(Action::Function(&#func)));
sql = true;
acc = line.to_string();
acc.push('\n');
}
}
if !acc.trim().is_empty() {
+21 -8
View File
@@ -1,7 +1,8 @@
[package]
name = "plume-models"
version = "0.3.0"
version = "0.4.0"
authors = ["Plume contributors"]
edition = "2018"
[dependencies]
activitypub = "0.1.1"
@@ -9,25 +10,26 @@ ammonia = "2.1.1"
askama_escape = "0.1"
bcrypt = "0.5"
guid-create = "0.1"
futures = "0.3"
heck = "0.3.0"
itertools = "0.8.0"
lazy_static = "*"
lazy_static = "1.0"
migrations_internals= "1.4.0"
openssl = "0.10.22"
rocket = "0.4.0"
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
reqwest = "0.9"
rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "async" }
scheduled-thread-pool = "0.2.2"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
tantivy = "0.10.1"
tokio = "0.2"
url = "2.1"
walkdir = "2.2"
webfinger = "0.4.1"
webfinger = { git = "https://github.com/Plume-org/webfinger", rev = "4e8f12810c4a7ba7a07bbcb722cd265fdff512b6", features = ["async"] }
whatlang = "0.7.1"
shrinkwraprs = "0.2.1"
shrinkwraprs = "0.3"
diesel-derive-newtype = "0.1.2"
glob = "0.3.0"
[dependencies.chrono]
features = ["serde"]
@@ -46,8 +48,19 @@ path = "../plume-common"
[dependencies.plume-macro]
path = "../plume-macro"
[dependencies.reqwest]
features = ["json", "blocking"]
version = "0.10"
[dependencies.rocket_i18n]
git = "https://github.com/Plume-org/rocket_i18n"
branch = "go-async"
default-features = false
features = ["rocket"]
[dev-dependencies]
diesel_migrations = "1.3.0"
diesel_migrations = "1.4.0"
[features]
postgres = ["diesel/postgres", "plume-macro/postgres" ]
+7 -6
View File
@@ -1,19 +1,19 @@
use crate::users::User;
use rocket::{
http::Status,
request::{self, FromRequest, Request},
Outcome,
};
use users::User;
/// Wrapper around User to use as a request guard on pages reserved to admins.
pub struct Admin(pub User);
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for Admin {
type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<Admin, ()> {
let user = request.guard::<User>()?;
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let user = try_outcome!(User::from_request(request).await);
if user.is_admin() {
Outcome::Success(Admin(user))
} else {
@@ -25,11 +25,12 @@ impl<'a, 'r> FromRequest<'a, 'r> for Admin {
/// Same as `Admin` but for moderators.
pub struct Moderator(pub User);
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for Moderator {
type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<Moderator, ()> {
let user = request.guard::<User>()?;
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let user = try_outcome!(User::from_request(request).await);
if user.is_moderator() {
Outcome::Success(Moderator(user))
} else {
+21 -20
View File
@@ -1,3 +1,4 @@
use crate::{db_conn::DbConn, schema::api_tokens, Error, Result};
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use rocket::{
@@ -6,10 +7,6 @@ use rocket::{
Outcome,
};
use db_conn::DbConn;
use schema::api_tokens;
use {Error, Result};
#[derive(Clone, Queryable)]
pub struct ApiToken {
pub id: i32,
@@ -79,32 +76,36 @@ pub enum TokenError {
DbError,
}
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for ApiToken {
type Error = TokenError;
fn from_request(request: &'a Request<'r>) -> request::Outcome<ApiToken, TokenError> {
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let headers: Vec<_> = request.headers().get("Authorization").collect();
if headers.len() != 1 {
return Outcome::Failure((Status::BadRequest, TokenError::NoHeader));
}
let mut parsed_header = headers[0].split(' ');
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
.guard::<DbConn>()
.map_failure(|_| (Status::InternalServerError, TokenError::DbError))?;
if let Ok(token) = ApiToken::find_by_value(&*conn, val) {
return Outcome::Success(token);
if let Some(auth_type) = parsed_header.next() {
if let Some(val) = parsed_header.next() {
if auth_type == "Bearer" {
if let Outcome::Success(conn) = DbConn::from_request(request).await {
if let Ok(token) = ApiToken::find_by_value(&*conn, val) {
return Outcome::Success(token);
}
} else {
return Outcome::Failure((
Status::InternalServerError,
TokenError::DbError,
));
}
}
} else {
return Outcome::Failure((Status::BadRequest, TokenError::NoValue));
}
} else {
return Outcome::Failure((Status::BadRequest, TokenError::NoType));
}
Outcome::Forward(())
+1 -3
View File
@@ -1,9 +1,7 @@
use crate::{schema::apps, Error, Result};
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use schema::apps;
use {Error, Result};
#[derive(Clone, Queryable, Serialize)]
pub struct App {
pub id: i32,
+139
View File
@@ -0,0 +1,139 @@
use crate::{schema::email_blocklist, Connection, Error, Result};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, TextExpressionMethods};
use glob::Pattern;
#[derive(Clone, Queryable, Identifiable)]
#[table_name = "email_blocklist"]
pub struct BlocklistedEmail {
pub id: i32,
pub email_address: String,
pub note: String,
pub notify_user: bool,
pub notification_text: String,
}
#[derive(Insertable, FromForm)]
#[table_name = "email_blocklist"]
pub struct NewBlocklistedEmail {
pub email_address: String,
pub note: String,
pub notify_user: bool,
pub notification_text: String,
}
impl BlocklistedEmail {
insert!(email_blocklist, NewBlocklistedEmail);
get!(email_blocklist);
find_by!(email_blocklist, find_by_id, id as i32);
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)?;
delete(&be).execute(conn)?;
}
Ok(true)
}
pub fn find_for_domain(conn: &Connection, domain: &str) -> Result<Vec<BlocklistedEmail>> {
let effective = format!("%@{}", domain);
email_blocklist::table
.filter(email_blocklist::email_address.like(effective))
.load::<BlocklistedEmail>(conn)
.map_err(Error::from)
}
pub fn matches_blocklist(conn: &Connection, email: &str) -> Result<Option<BlocklistedEmail>> {
let mut result = email_blocklist::table.load::<BlocklistedEmail>(conn)?;
for i in result.drain(..) {
if let Ok(x) = Pattern::new(&i.email_address) {
if x.matches(email) {
return Ok(Some(i));
}
}
}
Ok(None)
}
pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<BlocklistedEmail>> {
email_blocklist::table
.offset(min.into())
.limit((max - min).into())
.load::<BlocklistedEmail>(conn)
.map_err(Error::from)
}
pub fn count(conn: &Connection) -> Result<i64> {
email_blocklist::table
.count()
.get_result(conn)
.map_err(Error::from)
}
pub fn pattern_errors(pat: &str) -> Option<glob::PatternError> {
let c = Pattern::new(pat);
c.err()
}
pub fn new(
conn: &Connection,
pattern: &str,
note: &str,
show_notification: bool,
notification_text: &str,
) -> Result<BlocklistedEmail> {
let c = NewBlocklistedEmail {
email_address: pattern.to_owned(),
note: note.to_owned(),
notify_user: show_notification,
notification_text: notification_text.to_owned(),
};
BlocklistedEmail::insert(conn, c)
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::{instance::tests as instance_tests, tests::rockets, Connection as Conn};
use diesel::Connection;
pub(crate) fn fill_database(conn: &Conn) -> Vec<BlocklistedEmail> {
instance_tests::fill_database(conn);
let domainblock =
BlocklistedEmail::new(conn, "*@bad-actor.com", "Mean spammers", false, "").unwrap();
let userblock = BlocklistedEmail::new(
conn,
"spammer@lax-administration.com",
"Decent enough domain, but this user is a problem.",
true,
"Stop it please",
)
.unwrap();
vec![domainblock, userblock]
}
#[test]
fn test_match() {
let r = rockets();
let conn = &*r.conn;
conn.test_transaction::<_, (), _>(|| {
let various = fill_database(conn);
let match1 = "user1@bad-actor.com";
let match2 = "spammer@lax-administration.com";
let no_match = "happy-user@lax-administration.com";
assert_eq!(
BlocklistedEmail::matches_blocklist(conn, match1)
.unwrap()
.unwrap()
.id,
various[0].id
);
assert_eq!(
BlocklistedEmail::matches_blocklist(conn, match2)
.unwrap()
.unwrap()
.id,
various[1].id
);
assert_eq!(
BlocklistedEmail::matches_blocklist(conn, no_match)
.unwrap()
.is_none(),
true
);
Ok(())
});
}
}
+1 -3
View File
@@ -1,8 +1,6 @@
use crate::{schema::blog_authors, Error, Result};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use schema::blog_authors;
use {Error, Result};
#[derive(Clone, Queryable, Identifiable)]
pub struct BlogAuthor {
pub id: i32,
+26 -28
View File
@@ -1,3 +1,7 @@
use crate::{
ap_url, instance::*, medias::Media, posts::Post, safe_string::SafeString, schema::blogs,
search::Searcher, users::User, Connection, Error, PlumeRocket, Result, ITEMS_PER_PAGE,
};
use activitypub::{
actor::Group,
collection::{OrderedCollection, OrderedCollectionPage},
@@ -12,22 +16,12 @@ use openssl::{
rsa::Rsa,
sign::{Signer, Verifier},
};
use serde_json;
use url::Url;
use webfinger::*;
use instance::*;
use medias::Media;
use plume_common::activity_pub::{
inbox::{AsActor, FromId},
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
};
use posts::Post;
use safe_string::SafeString;
use schema::blogs;
use search::Searcher;
use users::User;
use {ap_url, Connection, Error, PlumeRocket, Result, ITEMS_PER_PAGE};
use url::Url;
use webfinger::*;
pub type CustomGroup = CustomObject<ApSignature, Group>;
@@ -76,7 +70,8 @@ impl Blog {
insert!(blogs, NewBlog, |inserted, conn| {
let instance = inserted.get_instance(conn)?;
if inserted.outbox_url.is_empty() {
inserted.outbox_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "outbox");
inserted.outbox_url =
instance.compute_box(BLOG_PREFIX, &inserted.actor_id, r#"outbox"#);
}
if inserted.inbox_url.is_empty() {
@@ -106,8 +101,8 @@ impl Blog {
}
pub fn list_authors(&self, conn: &Connection) -> Result<Vec<User>> {
use schema::blog_authors;
use schema::users;
use crate::schema::blog_authors;
use crate::schema::users;
let authors_ids = blog_authors::table
.filter(blog_authors::blog_id.eq(self.id))
.select(blog_authors::author_id);
@@ -118,7 +113,7 @@ impl Blog {
}
pub fn count_authors(&self, conn: &Connection) -> Result<i64> {
use schema::blog_authors;
use crate::schema::blog_authors;
blog_authors::table
.filter(blog_authors::blog_id.eq(self.id))
.count()
@@ -127,7 +122,7 @@ impl Blog {
}
pub fn find_for_author(conn: &Connection, author: &User) -> Result<Vec<Blog>> {
use schema::blog_authors;
use crate::schema::blog_authors;
let author_ids = blog_authors::table
.filter(blog_authors::author_id.eq(author.id))
.select(blog_authors::blog_id);
@@ -137,7 +132,7 @@ impl Blog {
.map_err(Error::from)
}
pub fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<Blog> {
pub async fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<Blog> {
let from_db = blogs::table
.filter(blogs::fqn.eq(fqn))
.first(&*c.conn)
@@ -145,12 +140,13 @@ impl Blog {
if let Some(from_db) = from_db {
Ok(from_db)
} else {
Blog::fetch_from_webfinger(c, fqn)
Blog::fetch_from_webfinger(c, fqn).await
}
}
fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<Blog> {
resolve_with_prefix(Prefix::Group, acct.to_owned(), true)?
async fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<Blog> {
resolve_with_prefix(Prefix::Group, acct.to_owned(), true)
.await?
.links
.into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
@@ -501,14 +497,16 @@ impl NewBlog {
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use blog_authors::*;
use crate::{
blog_authors::*,
instance::tests as instance_tests,
medias::NewMedia,
search::tests::get_searcher,
tests::{db, rockets},
users::tests as usersTests,
Connection as Conn,
};
use diesel::Connection;
use instance::tests as instance_tests;
use medias::NewMedia;
use search::tests::get_searcher;
use tests::{db, rockets};
use users::tests as usersTests;
use Connection as Conn;
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Blog>) {
instance_tests::fill_database(conn);
+1 -5
View File
@@ -1,10 +1,6 @@
use crate::{comments::Comment, schema::comment_seers, users::User, Connection, Error, Result};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use comments::Comment;
use schema::comment_seers;
use users::User;
use {Connection, Error, Result};
#[derive(Queryable, Clone)]
pub struct CommentSeers {
pub id: i32,
+30 -29
View File
@@ -1,3 +1,15 @@
use crate::{
comment_seers::{CommentSeers, NewCommentSeers},
instance::Instance,
medias::Media,
mentions::Mention,
notifications::*,
posts::Post,
safe_string::SafeString,
schema::comments,
users::User,
Connection, Error, PlumeRocket, Result,
};
use activitypub::{
activity::{Create, Delete},
link,
@@ -5,25 +17,15 @@ use activitypub::{
};
use chrono::{self, NaiveDateTime};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use serde_json;
use std::collections::HashSet;
use comment_seers::{CommentSeers, NewCommentSeers};
use instance::Instance;
use medias::Media;
use mentions::Mention;
use notifications::*;
use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId},
Id, IntoId, PUBLIC_VISIBILITY,
use futures::stream::{self, StreamExt};
use plume_common::{
activity_pub::{
inbox::{AsActor, AsObject, FromId},
Id, IntoId, PUBLIC_VISIBILITY,
},
utils,
};
use plume_common::utils;
use posts::Post;
use safe_string::SafeString;
use schema::comments;
use users::User;
use {Connection, Error, PlumeRocket, Result};
use std::collections::HashSet;
#[derive(Queryable, Identifiable, Clone, AsChangeset)]
pub struct Comment {
@@ -77,7 +79,7 @@ impl Comment {
}
pub fn count_local(conn: &Connection) -> Result<i64> {
use schema::users;
use crate::schema::users;
let local_authors = users::table
.filter(users::instance_id.eq(Instance::get_local()?.id))
.select(users::id);
@@ -103,7 +105,7 @@ impl Comment {
.unwrap_or(false)
}
pub fn to_activity(&self, c: &PlumeRocket) -> Result<Note> {
pub async fn to_activity(&self, c: &PlumeRocket) -> Result<Note> {
let author = User::get(&c.conn, self.author_id)?;
let (html, mentions, _hashtags) = utils::md_to_html(
self.content.get().as_ref(),
@@ -127,22 +129,21 @@ impl Comment {
)?))?;
note.object_props
.set_published_string(chrono::Utc::now().to_rfc3339())?;
note.object_props
.set_attributed_to_link(author.clone().into_id())?;
note.object_props.set_to_link_vec(to.clone())?;
note.object_props.set_attributed_to_link(author.into_id())?;
note.object_props.set_to_link_vec(to)?;
note.object_props.set_tag_link_vec(
mentions
.into_iter()
.filter_map(|m| Mention::build_activity(c, &m).ok())
.collect::<Vec<link::Mention>>(),
stream::iter(mentions)
.filter_map(|m| async move { Mention::build_activity(c, &m).await.ok() })
.collect::<Vec<link::Mention>>()
.await,
)?;
Ok(note)
}
pub fn create_activity(&self, c: &PlumeRocket) -> Result<Create> {
pub async fn create_activity(&self, c: &PlumeRocket) -> Result<Create> {
let author = User::get(&c.conn, self.author_id)?;
let note = self.to_activity(c)?;
let note = self.to_activity(c).await?;
let mut act = Create::default();
act.create_props.set_actor_link(author.into_id())?;
act.create_props.set_object_object(note.clone())?;
+10
View File
@@ -11,6 +11,8 @@ pub struct Config {
pub base_url: String,
pub database_url: String,
pub db_name: &'static str,
pub db_max_size: Option<u32>,
pub db_min_idle: Option<u32>,
pub search_index: String,
pub rocket: Result<RocketConfig, RocketError>,
pub logo: LogoConfig,
@@ -193,6 +195,14 @@ lazy_static! {
var("ROCKET_PORT").unwrap_or_else(|_| "7878".to_owned())
)),
db_name: DB_NAME,
db_max_size: var("DB_MAX_SIZE").map_or(None, |s| Some(
s.parse::<u32>()
.expect("Couldn't parse DB_MAX_SIZE into u32")
)),
db_min_idle: var("DB_MIN_IDLE").map_or(None, |s| Some(
s.parse::<u32>()
.expect("Couldn't parse DB_MIN_IDLE into u32")
)),
#[cfg(feature = "postgres")]
database_url: var("DATABASE_URL")
.unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME)),
+7 -9
View File
@@ -1,3 +1,4 @@
use crate::Connection;
use diesel::r2d2::{
ConnectionManager, CustomizeConnection, Error as ConnError, Pool, PooledConnection,
};
@@ -6,12 +7,10 @@ use diesel::{dsl::sql_query, ConnectionError, RunQueryDsl};
use rocket::{
http::Status,
request::{self, FromRequest},
Outcome, Request, State,
Outcome, Request,
};
use std::ops::Deref;
use Connection;
pub type DbPool = Pool<ConnectionManager<Connection>>;
// From rocket documentation
@@ -22,14 +21,14 @@ pub struct DbConn(pub PooledConnection<ConnectionManager<Connection>>);
/// Attempts to retrieve a single connection from the managed database pool. If
/// no pool is currently managed, fails with an `InternalServerError` status. If
/// no connections are available, fails with a `ServiceUnavailable` status.
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for DbConn {
type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let pool = request.guard::<State<DbPool>>()?;
match pool.get() {
Ok(conn) => Outcome::Success(DbConn(conn)),
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())),
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
match DbConn::from_request(request).await {
Outcome::Success(a) => Outcome::Success(a),
_ => Outcome::Failure((Status::ServiceUnavailable, ())),
}
}
}
@@ -73,5 +72,4 @@ pub(crate) mod tests {
Ok(conn.begin_test_transaction().unwrap())
}
}
}
+6 -9
View File
@@ -1,16 +1,15 @@
use crate::{
ap_url, notifications::*, schema::follows, users::User, Connection, Error, PlumeRocket, Result,
CONFIG,
};
use activitypub::activity::{Accept, Follow as FollowAct, Undo};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use notifications::*;
use plume_common::activity_pub::{
broadcast,
inbox::{AsActor, AsObject, FromId},
sign::Signer,
Id, IntoId, PUBLIC_VISIBILITY,
};
use schema::follows;
use users::User;
use {ap_url, Connection, Error, PlumeRocket, Result, CONFIG};
#[derive(Clone, Queryable, Identifiable, Associations, AsChangeset)]
#[belongs_to(User, foreign_key = "following_id")]
@@ -56,8 +55,7 @@ impl Follow {
let target = User::get(conn, self.following_id)?;
let mut act = FollowAct::default();
act.follow_props
.set_actor_link::<Id>(user.clone().into_id())?;
act.follow_props.set_actor_link::<Id>(user.into_id())?;
act.follow_props
.set_object_link::<Id>(target.clone().into_id())?;
act.object_props.set_id_string(self.ap_url.clone())?;
@@ -202,9 +200,8 @@ impl IntoId for Follow {
#[cfg(test)]
mod tests {
use super::*;
use crate::{tests::db, users::tests as user_tests};
use diesel::Connection;
use tests::db;
use users::tests as user_tests;
#[test]
fn test_id() {
+2 -1
View File
@@ -6,10 +6,11 @@ use rocket::{
pub struct Headers<'r>(pub HeaderMap<'r>);
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for Headers<'r> {
type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, ()> {
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, ()> {
let mut headers = HeaderMap::new();
for header in request.headers().clone().into_iter() {
headers.add(header);
-1
View File
@@ -1,5 +1,4 @@
use activitypub::activity::*;
use serde_json;
use crate::{
comments::Comment,
+10 -10
View File
@@ -1,14 +1,15 @@
use crate::{
ap_url,
medias::Media,
safe_string::SafeString,
schema::{instances, users},
users::{Role, User},
Connection, Error, Result,
};
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use std::sync::RwLock;
use ap_url;
use medias::Media;
use plume_common::utils::md_to_html;
use safe_string::SafeString;
use schema::{instances, users};
use users::{Role, User};
use {Connection, Error, Result};
use std::sync::RwLock;
#[derive(Clone, Identifiable, Queryable)]
pub struct Instance {
@@ -242,9 +243,8 @@ impl Instance {
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::{tests::db, Connection as Conn};
use diesel::Connection;
use tests::db;
use Connection as Conn;
pub(crate) fn fill_database(conn: &Conn) -> Vec<(NewInstance, Instance)> {
let res = vec![
+15 -28
View File
@@ -1,41 +1,22 @@
#![feature(try_trait)]
#![feature(never_type)]
#![feature(custom_attribute)]
#![feature(proc_macro_hygiene)]
extern crate activitypub;
extern crate ammonia;
extern crate askama_escape;
extern crate bcrypt;
extern crate chrono;
#[macro_use]
extern crate diesel;
extern crate guid_create;
extern crate heck;
extern crate itertools;
extern crate futures;
#[macro_use]
extern crate lazy_static;
extern crate migrations_internals;
extern crate openssl;
extern crate plume_api;
extern crate plume_common;
#[macro_use]
extern crate plume_macro;
extern crate reqwest;
#[macro_use]
extern crate rocket;
extern crate rocket_i18n;
extern crate scheduled_thread_pool;
extern crate serde;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate serde_json;
#[macro_use]
extern crate tantivy;
extern crate url;
extern crate walkdir;
extern crate webfinger;
extern crate whatlang;
use plume_common::activity_pub::inbox::InboxError;
@@ -53,6 +34,7 @@ pub type Connection = diesel::PgConnection;
/// All the possible errors that can be encoutered in this crate
#[derive(Debug)]
pub enum Error {
Blocklisted(bool, String),
Db(diesel::result::Error),
Inbox(Box<InboxError<Error>>),
InvalidValue,
@@ -94,6 +76,12 @@ impl From<std::option::NoneError> for Error {
}
}
impl From<Error> for std::option::NoneError {
fn from(_: Error) -> Self {
std::option::NoneError
}
}
impl From<url::ParseError> for Error {
fn from(_: url::ParseError) -> Self {
Error::Url
@@ -247,12 +235,13 @@ macro_rules! get {
/// Model::insert(connection, NewModelType::new());
/// ```
macro_rules! insert {
($table:ident, $from:ident) => {
($table:ident, $from:ty) => {
insert!($table, $from, |x, _conn| Ok(x));
};
($table:ident, $from:ident, |$val:ident, $conn:ident | $( $after:tt )+) => {
($table:ident, $from:ty, |$val:ident, $conn:ident | $( $after:tt )+) => {
last!($table);
#[allow(dead_code)]
pub fn insert(conn: &crate::Connection, new: $from) -> Result<Self> {
diesel::insert_into($table::table)
.values(new)
@@ -279,6 +268,7 @@ macro_rules! insert {
/// ```
macro_rules! last {
($table:ident) => {
#[allow(dead_code)]
pub fn last(conn: &crate::Connection) -> Result<Self> {
$table::table
.order_by($table::id.desc())
@@ -298,16 +288,12 @@ pub fn ap_url(url: &str) -> String {
#[cfg(test)]
#[macro_use]
mod tests {
use db_conn;
use crate::{db_conn, migrations::IMPORTED_MIGRATIONS, search, Connection as Conn, CONFIG};
use diesel::r2d2::ConnectionManager;
use migrations::IMPORTED_MIGRATIONS;
use plume_common::utils::random_hex;
use scheduled_thread_pool::ScheduledThreadPool;
use search;
use std::env::temp_dir;
use std::sync::Arc;
use Connection as Conn;
use CONFIG;
#[macro_export]
macro_rules! part_eq {
@@ -351,6 +337,7 @@ mod tests {
pub mod admin;
pub mod api_tokens;
pub mod apps;
pub mod blocklisted_emails;
pub mod blog_authors;
pub mod blogs;
pub mod comment_seers;
+4 -7
View File
@@ -1,17 +1,14 @@
use crate::{
notifications::*, posts::Post, schema::likes, timeline::*, users::User, Connection, Error,
PlumeRocket, Result,
};
use activitypub::activity;
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use notifications::*;
use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId},
Id, IntoId, PUBLIC_VISIBILITY,
};
use posts::Post;
use schema::likes;
use timeline::*;
use users::User;
use {Connection, Error, PlumeRocket, Result};
#[derive(Clone, Queryable, Identifiable)]
pub struct Like {
+69 -30
View File
@@ -1,13 +1,14 @@
use crate::{
blogs::Blog,
schema::{blogs, list_elems, lists, users},
users::User,
Connection, Error, Result,
};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use blogs::Blog;
use schema::{blogs, list_elems, lists, users};
use std::convert::{TryFrom, TryInto};
use users::User;
use {Connection, Error, Result};
/// Represent what a list is supposed to store. Represented in database as an integer
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
/// Represent what a list is supposed to store. Represented in database as an integer
pub enum ListType {
User,
Blog,
@@ -57,7 +58,11 @@ struct NewList<'a> {
}
macro_rules! func {
(@elem User $id:expr, $value:expr) => {
(
$(#[$outer:meta])*
@elem User $id:expr, $value:expr
) => {
$(#[$outer])*
NewListElem {
list_id: $id,
user_id: Some(*$value),
@@ -65,7 +70,11 @@ macro_rules! func {
word: None,
}
};
(@elem Blog $id:expr, $value:expr) => {
(
$(#[$outer:meta])*
@elem Blog $id:expr, $value:expr
) => {
$(#[$outer])*
NewListElem {
list_id: $id,
user_id: None,
@@ -73,7 +82,11 @@ macro_rules! func {
word: None,
}
};
(@elem Word $id:expr, $value:expr) => {
(
$(#[$outer:meta])*
@elem Word $id:expr, $value:expr
) => {
$(#[$outer])*
NewListElem {
list_id: $id,
user_id: None,
@@ -81,7 +94,11 @@ macro_rules! func {
word: Some($value),
}
};
(@elem Prefix $id:expr, $value:expr) => {
(
$(#[$outer:meta])*
@elem Prefix $id:expr, $value:expr
) => {
$(#[$outer])*
NewListElem {
list_id: $id,
user_id: None,
@@ -98,7 +115,11 @@ macro_rules! func {
(@out_type Word) => { String };
(@out_type Prefix) => { String };
(add: $fn:ident, $kind:ident) => {
(
$(#[$outer:meta])*
add: $fn:ident, $kind:ident
) => {
$(#[$outer])*
pub fn $fn(&self, conn: &Connection, vals: &[func!(@in_type $kind)]) -> Result<()> {
if self.kind() != ListType::$kind {
return Err(Error::InvalidValue);
@@ -115,7 +136,11 @@ macro_rules! func {
}
};
(list: $fn:ident, $kind:ident, $table:ident) => {
(
$(#[$outer:meta])*
list: $fn:ident, $kind:ident, $table:ident
) => {
$(#[$outer])*
pub fn $fn(&self, conn: &Connection) -> Result<Vec<func!(@out_type $kind)>> {
if self.kind() != ListType::$kind {
return Err(Error::InvalidValue);
@@ -131,7 +156,11 @@ macro_rules! func {
(set: $fn:ident, $kind:ident, $add:ident) => {
(
$(#[$outer:meta])*
set: $fn:ident, $kind:ident, $add:ident
) => {
$(#[$outer])*
pub fn $fn(&self, conn: &Connection, val: &[func!(@in_type $kind)]) -> Result<()> {
if self.kind() != ListType::$kind {
return Err(Error::InvalidValue);
@@ -164,7 +193,7 @@ impl List {
last!(lists);
get!(lists);
fn insert(conn: &Connection, val: NewList) -> Result<Self> {
fn insert(conn: &Connection, val: NewList<'_>) -> Result<Self> {
diesel::insert_into(lists::table)
.values(val)
.execute(conn)?;
@@ -245,23 +274,35 @@ impl List {
private::ListElem::prefix_in_list(conn, self, word)
}
/// Insert new users in a list
func! {add: add_users, User}
func! {
/// Insert new users in a list
add: add_users, User
}
/// Insert new blogs in a list
func! {add: add_blogs, Blog}
func! {
/// Insert new blogs in a list
add: add_blogs, Blog
}
/// Insert new words in a list
func! {add: add_words, Word}
func! {
/// Insert new words in a list
add: add_words, Word
}
/// Insert new prefixes in a list
func! {add: add_prefixes, Prefix}
func! {
/// Insert new prefixes in a list
add: add_prefixes, Prefix
}
/// Get all users in the list
func! {list: list_users, User, users}
func! {
/// Get all users in the list
list: list_users, User, users
}
/// Get all blogs in the list
func! {list: list_blogs, Blog, blogs}
func! {
/// Get all blogs in the list
list: list_blogs, Blog, blogs
}
/// Get all words in the list
pub fn list_words(&self, conn: &Connection) -> Result<Vec<String>> {
@@ -309,7 +350,7 @@ mod private {
};
impl ListElem {
insert!(list_elems, NewListElem);
insert!(list_elems, NewListElem<'_>);
pub fn user_in_list(conn: &Connection, list: &List, user: i32) -> Result<bool> {
dsl::select(dsl::exists(
@@ -359,9 +400,8 @@ mod private {
#[cfg(test)]
mod tests {
use super::*;
use blogs::tests as blog_tests;
use crate::{blogs::tests as blog_tests, tests::db};
use diesel::Connection;
use tests::db;
#[test]
fn list_type() {
@@ -551,5 +591,4 @@ mod tests {
Ok(())
});
}
}
+18 -21
View File
@@ -1,20 +1,17 @@
use crate::{
ap_url, instance::Instance, safe_string::SafeString, schema::medias, users::User, Connection,
Error, PlumeRocket, Result,
};
use activitypub::object::Image;
use askama_escape::escape;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use guid_create::GUID;
use reqwest;
use std::{fs, path::Path};
use plume_common::{
activity_pub::{inbox::FromId, Id},
utils::MediaProcessor,
};
use instance::Instance;
use safe_string::SafeString;
use schema::medias;
use users::User;
use {ap_url, Connection, Error, PlumeRocket, Result};
use std::{fs, path::Path};
use tokio::prelude::*;
#[derive(Clone, Identifiable, Queryable)]
pub struct Media {
@@ -117,15 +114,19 @@ impl Media {
Ok(match self.category() {
MediaCategory::Image => SafeString::trusted(&format!(
r#"<img src="{}" alt="{}" title="{}">"#,
url, escape(&self.alt_text), escape(&self.alt_text)
url,
escape(&self.alt_text),
escape(&self.alt_text)
)),
MediaCategory::Audio => SafeString::trusted(&format!(
r#"<div class="media-preview audio"></div><audio src="{}" title="{}" controls></audio>"#,
url, escape(&self.alt_text)
url,
escape(&self.alt_text)
)),
MediaCategory::Video => SafeString::trusted(&format!(
r#"<video src="{}" title="{}" controls></video>"#,
url, escape(&self.alt_text)
url,
escape(&self.alt_text)
)),
MediaCategory::Unknown => SafeString::trusted(&format!(
r#"<a href="{}" class="media-preview unknown"></a>"#,
@@ -196,7 +197,7 @@ impl Media {
}
// TODO: merge with save_remote?
pub fn from_activity(c: &PlumeRocket, image: &Image) -> Result<Media> {
pub async fn from_activity(c: &PlumeRocket, image: &Image) -> Result<Media> {
let conn = &*c.conn;
let remote_url = image.object_props.url_string().ok()?;
let ext = remote_url
@@ -210,11 +211,9 @@ impl Media {
ext
));
let mut dest = fs::File::create(path.clone()).ok()?;
reqwest::get(remote_url.as_str())
.ok()?
.copy_to(&mut dest)
.ok()?;
let mut dest = tokio::fs::File::create(path.clone()).await?;
let contents = reqwest::get(remote_url.as_str()).await?.bytes().await?;
dest.write_all(&contents).await?;
Media::insert(
conn,
@@ -259,13 +258,11 @@ impl Media {
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::{tests::db, users::tests as usersTests, Connection as Conn};
use diesel::Connection;
use std::env::{current_dir, set_current_dir};
use std::fs;
use std::path::Path;
use tests::db;
use users::tests as usersTests;
use Connection as Conn;
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Media>) {
let mut wd = current_dir().unwrap().to_path_buf();
+6 -10
View File
@@ -1,14 +1,10 @@
use crate::{
comments::Comment, notifications::*, posts::Post, schema::mentions, users::User, Connection,
Error, PlumeRocket, Result,
};
use activitypub::link;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use comments::Comment;
use notifications::*;
use plume_common::activity_pub::inbox::AsActor;
use posts::Post;
use schema::mentions;
use users::User;
use PlumeRocket;
use {Connection, Error, Result};
#[derive(Clone, Queryable, Identifiable)]
pub struct Mention {
@@ -56,8 +52,8 @@ impl Mention {
}
}
pub fn build_activity(c: &PlumeRocket, ment: &str) -> Result<link::Mention> {
let user = User::find_by_fqn(c, ment)?;
pub async fn build_activity(c: &PlumeRocket, ment: &str) -> Result<link::Mention> {
let user = User::find_by_fqn(c, ment).await?;
let mut mention = link::Mention::default();
mention.link_props.set_href_string(user.ap_url)?;
mention.link_props.set_name_string(format!("@{}", ment))?;
+2 -6
View File
@@ -1,16 +1,12 @@
use Connection;
use Error;
use Result;
use crate::{Connection, Error, Result};
use diesel::connection::{Connection as Conn, SimpleConnection};
use migrations_internals::{setup_database, MigrationConnection};
use std::path::Path;
#[allow(dead_code)] //variants might not be constructed if not required by current migrations
enum Action {
Sql(&'static str),
Function(&'static Fn(&Connection, &Path) -> Result<()>),
Function(&'static dyn Fn(&Connection, &Path) -> Result<()>),
}
impl Action {
+11 -10
View File
@@ -1,16 +1,17 @@
use crate::{
comments::Comment,
follows::Follow,
likes::Like,
mentions::Mention,
posts::Post,
reshares::Reshare,
schema::{follows, notifications},
users::User,
Connection, Error, Result,
};
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, JoinOnDsl, QueryDsl, RunQueryDsl};
use comments::Comment;
use follows::Follow;
use likes::Like;
use mentions::Mention;
use posts::Post;
use reshares::Reshare;
use schema::{follows, notifications};
use users::User;
use {Connection, Error, Result};
pub mod notification_kind {
pub const COMMENT: &str = "COMMENT";
pub const FOLLOW: &str = "FOLLOW";
+2 -4
View File
@@ -1,7 +1,6 @@
use crate::{schema::password_reset_requests, Connection, Error, Result};
use chrono::{offset::Utc, Duration, NaiveDateTime};
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use schema::password_reset_requests;
use {Connection, Error, Result};
#[derive(Clone, Identifiable, Queryable)]
pub struct PasswordResetRequest {
@@ -75,9 +74,8 @@ impl PasswordResetRequest {
#[cfg(test)]
mod tests {
use super::*;
use crate::{tests::db, users::tests as user_tests};
use diesel::Connection;
use tests::db;
use users::tests as user_tests;
#[test]
fn test_insert_and_find_password_reset_request() {
+32 -20
View File
@@ -2,9 +2,7 @@ pub use self::module::PlumeRocket;
#[cfg(not(test))]
mod module {
use crate::db_conn::DbConn;
use crate::search;
use crate::users;
use crate::{db_conn::DbConn, search, users};
use rocket::{
request::{self, FlashMessage, FromRequest, Request},
Outcome, State,
@@ -22,20 +20,35 @@ mod module {
pub flash_msg: Option<(String, String)>,
}
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for PlumeRocket {
type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<PlumeRocket, ()> {
let conn = request.guard::<DbConn>()?;
let intl = request.guard::<rocket_i18n::I18n>()?;
let user = request.guard::<users::User>().succeeded();
let worker = request.guard::<State<Arc<ScheduledThreadPool>>>()?;
let searcher = request.guard::<State<Arc<search::Searcher>>>()?;
let flash_msg = request.guard::<FlashMessage>().succeeded();
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let conn = DbConn::from_request(request).await.succeeded().unwrap();
let intl = rocket_i18n::I18n::from_request(request)
.await
.succeeded()
.unwrap();
let user = users::User::from_request(request)
.await
.succeeded()
.unwrap();
let worker = request
.guard::<State<'_, Arc<ScheduledThreadPool>>>()
.await
.succeeded()
.unwrap();
let searcher = request
.guard::<State<'_, Arc<search::Searcher>>>()
.await
.succeeded()
.unwrap();
let flash_msg = request.guard::<FlashMessage<'_, '_>>().await.succeeded();
Outcome::Success(PlumeRocket {
conn,
intl,
user,
user: Some(user),
flash_msg: flash_msg.map(|f| (f.name().into(), f.msg().into())),
worker: worker.clone(),
searcher: searcher.clone(),
@@ -46,9 +59,7 @@ mod module {
#[cfg(test)]
mod module {
use crate::db_conn::DbConn;
use crate::search;
use crate::users;
use crate::{db_conn::DbConn, search, users};
use rocket::{
request::{self, FromRequest, Request},
Outcome, State,
@@ -64,17 +75,18 @@ mod module {
pub worker: Arc<ScheduledThreadPool>,
}
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for PlumeRocket {
type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<PlumeRocket, ()> {
let conn = request.guard::<DbConn>()?;
let user = request.guard::<users::User>().succeeded();
let worker = request.guard::<State<Arc<ScheduledThreadPool>>>()?;
let searcher = request.guard::<State<Arc<search::Searcher>>>()?;
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let conn = try_outcome!(DbConn::from_request(request).await);
let user = try_outcome!(users::User::from_request(request).await);
let worker = try_outcome!(request.guard::<'_, State<'_, Arc<ScheduledThreadPool>>>());
let searcher = try_outcome!(request.guard::<'_, State<'_, Arc<search::Searcher>>>());
Outcome::Success(PlumeRocket {
conn,
user,
user: Some(user),
worker: worker.clone(),
searcher: searcher.clone(),
})
+1 -5
View File
@@ -1,10 +1,6 @@
use crate::{posts::Post, schema::post_authors, users::User, Error, Result};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use posts::Post;
use schema::post_authors;
use users::User;
use {Error, Result};
#[derive(Clone, Queryable, Identifiable, Associations)]
#[belongs_to(Post)]
#[belongs_to(User, foreign_key = "author_id")]
+38 -41
View File
@@ -1,3 +1,8 @@
use crate::{
ap_url, blogs::Blog, instance::Instance, medias::Media, mentions::Mention, post_authors::*,
safe_string::SafeString, schema::posts, search::Searcher, tags::*, timeline::*, users::User,
Connection, Error, PlumeRocket, Result, CONFIG,
};
use activitypub::{
activity::{Create, Delete, Update},
link,
@@ -7,13 +12,6 @@ use activitypub::{
use chrono::{NaiveDateTime, TimeZone, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use heck::{CamelCase, KebabCase};
use serde_json;
use std::collections::HashSet;
use blogs::Blog;
use instance::Instance;
use medias::Media;
use mentions::Mention;
use plume_common::{
activity_pub::{
inbox::{AsObject, FromId},
@@ -21,14 +19,8 @@ use plume_common::{
},
utils::md_to_html,
};
use post_authors::*;
use safe_string::SafeString;
use schema::posts;
use search::Searcher;
use tags::*;
use timeline::*;
use users::User;
use {ap_url, Connection, Error, PlumeRocket, Result, CONFIG};
use std::collections::HashSet;
use tokio::runtime::Runtime;
pub type LicensedArticle = CustomObject<Licensed, Article>;
@@ -111,7 +103,7 @@ impl Post {
tag: String,
(min, max): (i32, i32),
) -> Result<Vec<Post>> {
use schema::tags;
use crate::schema::tags;
let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id);
posts::table
@@ -125,7 +117,7 @@ impl Post {
}
pub fn count_for_tag(conn: &Connection, tag: String) -> Result<i64> {
use schema::tags;
use crate::schema::tags;
let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id);
posts::table
.filter(posts::id.eq_any(ids))
@@ -139,8 +131,8 @@ impl Post {
}
pub fn count_local(conn: &Connection) -> Result<i64> {
use schema::post_authors;
use schema::users;
use crate::schema::post_authors;
use crate::schema::users;
let local_authors = users::table
.filter(users::instance_id.eq(Instance::get_local()?.id))
.select(users::id);
@@ -188,7 +180,7 @@ impl Post {
author: &User,
limit: i64,
) -> Result<Vec<Post>> {
use schema::post_authors;
use crate::schema::post_authors;
let posts = PostAuthor::belonging_to(author).select(post_authors::post_id);
posts::table
@@ -239,7 +231,7 @@ impl Post {
}
pub fn drafts_by_author(conn: &Connection, author: &User) -> Result<Vec<Post>> {
use schema::post_authors;
use crate::schema::post_authors;
let posts = PostAuthor::belonging_to(author).select(post_authors::post_id);
posts::table
@@ -251,8 +243,8 @@ impl Post {
}
pub fn get_authors(&self, conn: &Connection) -> Result<Vec<User>> {
use schema::post_authors;
use schema::users;
use crate::schema::post_authors;
use crate::schema::users;
let author_list = PostAuthor::belonging_to(self).select(post_authors::author_id);
users::table
.filter(users::id.eq_any(author_list))
@@ -261,7 +253,7 @@ impl Post {
}
pub fn is_author(&self, conn: &Connection, author_id: i32) -> Result<bool> {
use schema::post_authors;
use crate::schema::post_authors;
Ok(PostAuthor::belonging_to(self)
.filter(post_authors::author_id.eq(author_id))
.count()
@@ -270,7 +262,7 @@ impl Post {
}
pub fn get_blog(&self, conn: &Connection) -> Result<Blog> {
use schema::blogs;
use crate::schema::blogs;
blogs::table
.filter(blogs::id.eq(self.blog_id))
.first(conn)
@@ -278,7 +270,7 @@ impl Post {
}
pub fn count_likes(&self, conn: &Connection) -> Result<i64> {
use schema::likes;
use crate::schema::likes;
likes::table
.filter(likes::post_id.eq(self.id))
.count()
@@ -287,7 +279,7 @@ impl Post {
}
pub fn count_reshares(&self, conn: &Connection) -> Result<i64> {
use schema::reshares;
use crate::schema::reshares;
reshares::table
.filter(reshares::post_id.eq(self.id))
.count()
@@ -587,11 +579,11 @@ impl FromId<PlumeRocket> for Post {
}
});
let cover = article
.object_props
.icon_object::<Image>()
.ok()
.and_then(|img| Media::from_activity(&c, &img).ok().map(|m| m.id));
let image = article.object_props.icon_object::<Image>().ok().unwrap();
let mut r = Runtime::new().unwrap();
let cover =
Some(r.block_on(async { Media::from_activity(&c, &image).await.ok().unwrap().id }));
let title = article.object_props.name_string()?;
let post = Post::insert(
@@ -632,7 +624,7 @@ impl FromId<PlumeRocket> for Post {
.into_iter()
.map(|s| s.to_camel_case())
.collect::<HashSet<_>>();
if let Some(serde_json::Value::Array(tags)) = article.object_props.tag.clone() {
if let Some(serde_json::Value::Array(tags)) = article.object_props.tag {
for tag in tags {
serde_json::from_value::<link::Mention>(tag.clone())
.map(|m| Mention::from_activity(conn, &m, post.id, true, true))
@@ -707,17 +699,22 @@ impl FromId<PlumeRocket> for PostUpdate {
}
fn from_activity(c: &PlumeRocket, updated: LicensedArticle) -> Result<Self> {
let image = updated
.object
.object_props
.icon_object::<Image>()
.ok()
.unwrap();
let mut r = Runtime::new().unwrap();
let cover =
Some(r.block_on(async { Media::from_activity(&c, &image).await.ok().unwrap().id }));
Ok(PostUpdate {
ap_url: updated.object.object_props.id_string()?,
title: updated.object.object_props.name_string().ok(),
subtitle: updated.object.object_props.summary_string().ok(),
content: updated.object.object_props.content_string().ok(),
cover: updated
.object
.object_props
.icon_object::<Image>()
.ok()
.and_then(|img| Media::from_activity(&c, &img).ok().map(|m| m.id)),
cover,
source: updated
.object
.ap_object_props
@@ -725,7 +722,7 @@ impl FromId<PlumeRocket> for PostUpdate {
.ok()
.map(|x| x.content),
license: updated.custom_props.license_string().ok(),
tags: updated.object.object_props.tag.clone(),
tags: updated.object.object_props.tag,
})
}
}
@@ -806,7 +803,7 @@ impl AsObject<User, Update, &PlumeRocket> for PostUpdate {
impl IntoId for Post {
fn into_id(self) -> Id {
Id::new(self.ap_url.clone())
Id::new(self.ap_url)
}
}
+4 -7
View File
@@ -1,17 +1,14 @@
use crate::{
notifications::*, posts::Post, schema::reshares, timeline::*, users::User, Connection, Error,
PlumeRocket, Result,
};
use activitypub::activity::{Announce, Undo};
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use notifications::*;
use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId},
Id, IntoId, PUBLIC_VISIBILITY,
};
use posts::Post;
use schema::reshares;
use timeline::*;
use users::User;
use {Connection, Error, PlumeRocket, Result};
#[derive(Clone, Queryable, Identifiable)]
pub struct Reshare {
+36 -5
View File
@@ -31,10 +31,41 @@ lazy_static! {
.add_tag_attributes("label", ["for"].iter())
.add_tag_attributes("input", ["type", "checked"].iter())
.add_allowed_classes("input", ["cw-checkbox"].iter())
.add_allowed_classes(
"span",
[
"cw-container",
"cw-text",
//Scope classes for the syntax highlighting.
"attribute-name",
"comment",
"constant",
"control",
"declaration",
"entity",
"function",
"invalid",
"keyword",
"language",
"modifier",
"name",
"numeric",
"operator",
"parameter",
"punctuation",
"source",
"storage",
"string",
"support",
"tag",
"type",
"variable",
]
.iter(),
)
// Related to https://github.com/Plume-org/Plume/issues/637
.add_allowed_classes("sup", ["footnote-reference", "footnote-definition-label"].iter())
.add_allowed_classes("div", ["footnote-definition"].iter())
.add_allowed_classes("span", ["cw-container", "cw-text"].iter())
.attribute_filter(|elem, att, val| match (elem, att) {
("input", "type") => Some("checkbox".into()),
("input", "checked") => Some("checked".into()),
@@ -51,7 +82,7 @@ lazy_static! {
};
}
fn url_add_prefix(url: &str) -> Option<Cow<str>> {
fn url_add_prefix(url: &str) -> Option<Cow<'_, str>> {
if url.starts_with('#') && !url.starts_with("#postcontent-") {
//if start with an #
let mut new_url = "#postcontent-".to_owned(); //change to valid id
@@ -108,7 +139,7 @@ struct SafeStringVisitor;
impl<'de> Visitor<'de> for SafeStringVisitor {
type Value = SafeString;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a string")
}
@@ -150,7 +181,7 @@ where
DB: diesel::backend::Backend,
str: ToSql<diesel::sql_types::Text, DB>,
{
fn to_sql<W: Write>(&self, out: &mut Output<W, DB>) -> serialize::Result {
fn to_sql<W: Write>(&self, out: &mut Output<'_, W, DB>) -> serialize::Result {
str::to_sql(&self.value, out)
}
}
@@ -162,7 +193,7 @@ impl Borrow<str> for SafeString {
}
impl Display for SafeString {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.value)
}
}
+9
View File
@@ -73,6 +73,15 @@ table! {
user_id -> Int4,
}
}
table! {
email_blocklist(id){
id -> Int4,
email_address -> VarChar,
note -> Text,
notify_user -> Bool,
notification_text -> Text,
}
}
table! {
follows (id) {
+8 -6
View File
@@ -8,15 +8,17 @@ pub use self::searcher::*;
pub(crate) mod tests {
use super::{Query, Searcher};
use diesel::Connection;
use plume_common::utils::random_hex;
use std::env::temp_dir;
use std::str::FromStr;
use blogs::tests::fill_database;
use plume_common::utils::random_hex;
use post_authors::*;
use posts::{NewPost, Post};
use safe_string::SafeString;
use tests::db;
use crate::{
blogs::tests::fill_database,
post_authors::*,
posts::{NewPost, Post},
safe_string::SafeString,
tests::db,
};
pub(crate) fn get_searcher() -> Searcher {
let dir = temp_dir().join(&format!("plume-test-{}", random_hex()));
+3 -3
View File
@@ -1,5 +1,5 @@
use crate::search::searcher::Searcher;
use chrono::{naive::NaiveDate, offset::Utc, Datelike};
use search::searcher::Searcher;
use std::{cmp, ops::Bound};
use tantivy::{query::*, schema::*, Term};
@@ -153,7 +153,7 @@ impl PlumeQuery {
/// Convert this Query to a Tantivy Query
pub fn into_query(self) -> BooleanQuery {
let mut result: Vec<(Occur, Box<Query>)> = Vec::new();
let mut result: Vec<(Occur, Box<dyn Query>)> = Vec::new();
gen_to_query!(self, result; normal: title, subtitle, content, tag;
oneoff: instance, author, blog, lang, license);
@@ -279,7 +279,7 @@ impl PlumeQuery {
}
// map a token and it's field to a query
fn token_to_query(token: &str, field_name: &str) -> Box<Query> {
fn token_to_query(token: &str, field_name: &str) -> Box<dyn Query> {
let token = token.to_lowercase();
let token = token.as_str();
let field = Searcher::schema().get_field(field_name).unwrap();
+11 -13
View File
@@ -1,9 +1,11 @@
use instance::Instance;
use posts::Post;
use schema::posts;
use tags::Tag;
use Connection;
use crate::{
instance::Instance,
posts::Post,
schema::posts,
search::{query::PlumeQuery, tokenizer},
tags::Tag,
Connection, Result,
};
use chrono::Datelike;
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use itertools::Itertools;
@@ -14,10 +16,6 @@ use tantivy::{
};
use whatlang::{detect as detect_lang, Lang};
use super::tokenizer;
use search::query::PlumeQuery;
use Result;
#[derive(Debug)]
pub enum SearcherError {
IndexCreationError,
@@ -72,7 +70,7 @@ impl Searcher {
schema_builder.build()
}
pub fn create(path: &AsRef<Path>) -> Result<Self> {
pub fn create(path: &dyn AsRef<Path>) -> Result<Self> {
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser);
let content_tokenizer = SimpleTokenizer
@@ -111,7 +109,7 @@ impl Searcher {
})
}
pub fn open(path: &AsRef<Path>) -> Result<Self> {
pub fn open(path: &dyn AsRef<Path>) -> Result<Self> {
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser);
let content_tokenizer = SimpleTokenizer
@@ -175,7 +173,7 @@ impl Searcher {
post_id => i64::from(post.id),
author => post.get_authors(conn)?.into_iter().map(|u| u.fqn).join(" "),
creation_date => i64::from(post.creation_date.num_days_from_ce()),
instance => Instance::get(conn, post.get_blog(conn)?.instance_id)?.public_domain.clone(),
instance => Instance::get(conn, post.get_blog(conn)?.instance_id)?.public_domain,
tag => Tag::for_post(conn, post.id)?.into_iter().map(|t| t.tag).join(" "),
blog_name => post.get_blog(conn)?.title,
content => post.content.get().clone(),
+1 -4
View File
@@ -1,9 +1,6 @@
use crate::{ap_url, instance::Instance, schema::tags, Connection, Error, Result};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use instance::Instance;
use plume_common::activity_pub::Hashtag;
use schema::tags;
use {ap_url, Connection, Error, Result};
#[derive(Clone, Identifiable, Queryable)]
pub struct Tag {
+23 -22
View File
@@ -1,16 +1,16 @@
use crate::{
lists::List,
posts::Post,
schema::{posts, timeline, timeline_definition},
Connection, Error, PlumeRocket, Result,
};
use diesel::{self, BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl};
use lists::List;
use posts::Post;
use schema::{posts, timeline, timeline_definition};
use std::ops::Deref;
use {Connection, Error, PlumeRocket, Result};
pub(crate) mod query;
use self::query::{QueryError, TimelineQuery};
pub use self::query::Kind;
use self::query::{QueryError, TimelineQuery};
#[derive(Clone, Debug, PartialEq, Queryable, Identifiable, AsChangeset)]
#[table_name = "timeline_definition"]
@@ -119,7 +119,7 @@ impl Timeline {
}
})
{
Err(err)?;
return Err(err);
}
}
Self::insert(
@@ -157,7 +157,7 @@ impl Timeline {
}
})
{
Err(err)?;
return Err(err);
}
}
Self::insert(
@@ -208,7 +208,7 @@ impl Timeline {
.map_err(Error::from)
}
pub fn add_to_all_timelines(rocket: &PlumeRocket, post: &Post, kind: Kind) -> Result<()> {
pub fn add_to_all_timelines(rocket: &PlumeRocket, post: &Post, kind: Kind<'_>) -> Result<()> {
let timelines = timeline_definition::table
.load::<Self>(rocket.conn.deref())
.map_err(Error::from)?;
@@ -231,7 +231,7 @@ impl Timeline {
Ok(())
}
pub fn matches(&self, rocket: &PlumeRocket, post: &Post, kind: Kind) -> Result<bool> {
pub fn matches(&self, rocket: &PlumeRocket, post: &Post, kind: Kind<'_>) -> Result<bool> {
let query = TimelineQuery::parse(&self.query)?;
query.matches(rocket, self, post, kind)
}
@@ -240,16 +240,18 @@ impl Timeline {
#[cfg(test)]
mod tests {
use super::*;
use blogs::tests as blogTests;
use crate::{
blogs::tests as blogTests,
follows::*,
lists::ListType,
post_authors::{NewPostAuthor, PostAuthor},
posts::NewPost,
safe_string::SafeString,
tags::Tag,
tests::{db, rockets},
users::tests as userTests,
};
use diesel::Connection;
use follows::*;
use lists::ListType;
use post_authors::{NewPostAuthor, PostAuthor};
use posts::NewPost;
use safe_string::SafeString;
use tags::Tag;
use tests::{db, rockets};
use users::tests as userTests;
#[test]
fn test_timeline() {
@@ -495,8 +497,7 @@ mod tests {
),
published: true,
license: "GPL".to_string(),
source: "Actually, GNU+Linux, GNU×Linux, or GNU¿Linux are better."
.to_string(),
source: "Actually, GNU+Linux, GNU×Linux, or GNU¿Linux are better.".to_string(),
ap_url: "".to_string(),
creation_date: None,
subtitle: "".to_string(),
+59 -30
View File
@@ -1,15 +1,17 @@
use blogs::Blog;
use lists::{self, ListType};
use crate::{
blogs::Blog,
lists::{self, ListType},
posts::Post,
tags::Tag,
timeline::Timeline,
users::User,
PlumeRocket, Result,
};
use futures::stream::{self, StreamExt};
use plume_common::activity_pub::inbox::AsActor;
use posts::Post;
use tags::Tag;
use users::User;
use tokio::runtime::Runtime;
use whatlang::{self, Lang};
use {PlumeRocket, Result};
use super::Timeline;
#[derive(Debug, Clone, PartialEq)]
pub enum QueryError {
SyntaxError(usize, usize, String),
@@ -65,7 +67,7 @@ impl<'a> Token<'a> {
}
}
fn get_error<T>(&self, token: Token) -> QueryResult<T> {
fn get_error<T>(&self, token: Token<'_>) -> QueryResult<T> {
let (b, e) = self.get_pos();
let message = format!(
"Syntax Error: Expected {}, got {}",
@@ -79,7 +81,7 @@ impl<'a> Token<'a> {
impl<'a> ToString for Token<'a> {
fn to_string(&self) -> String {
if let Token::Word(0, 0, v) = self {
return v.to_string();
return (*v).to_string();
}
format!(
"'{}'",
@@ -127,7 +129,7 @@ macro_rules! gen_tokenizer {
}
}
fn lex(stream: &str) -> Vec<Token> {
fn lex(stream: &str) -> Vec<Token<'_>> {
stream
.chars()
.chain(" ".chars()) // force a last whitespace to empty scan's state
@@ -163,7 +165,7 @@ impl<'a> TQ<'a> {
rocket: &PlumeRocket,
timeline: &Timeline,
post: &Post,
kind: Kind,
kind: Kind<'_>,
) -> Result<bool> {
match self {
TQ::Or(inner) => inner.iter().try_fold(false, |s, e| {
@@ -181,7 +183,7 @@ impl<'a> TQ<'a> {
TQ::Or(inner) => inner.iter().flat_map(TQ::list_used_lists).collect(),
TQ::And(inner) => inner.iter().flat_map(TQ::list_used_lists).collect(),
TQ::Arg(Arg::In(typ, List::List(name)), _) => vec![(
name.to_string(),
(*name).to_string(),
match typ {
WithList::Blog => ListType::Blog,
WithList::Author { .. } => ListType::User,
@@ -208,7 +210,7 @@ impl<'a> Arg<'a> {
rocket: &PlumeRocket,
timeline: &Timeline,
post: &Post,
kind: Kind,
kind: Kind<'_>,
) -> Result<bool> {
match self {
Arg::In(t, l) => t.matches(rocket, timeline, post, l, kind),
@@ -233,8 +235,8 @@ impl WithList {
rocket: &PlumeRocket,
timeline: &Timeline,
post: &Post,
list: &List,
kind: Kind,
list: &List<'_>,
kind: Kind<'_>,
) -> Result<bool> {
match list {
List::List(name) => {
@@ -290,19 +292,38 @@ impl WithList {
(_, _) => Err(QueryError::RuntimeError(format!(
"The list '{}' is of the wrong type for this usage",
name
)))?,
))
.into()),
}
}
List::Array(list) => match self {
WithList::Blog => Ok(list
.iter()
.filter_map(|b| Blog::find_by_fqn(rocket, b).ok())
.any(|b| b.id == post.blog_id)),
WithList::Blog => {
let mut rt = Runtime::new().unwrap();
rt.block_on(async move {
Ok(stream::iter(list)
.filter_map(|b| async move {
Some(Blog::find_by_fqn(rocket, b).await.ok().unwrap())
})
.collect::<Vec<_>>()
.await
.into_iter()
.any(|b| b.id == post.blog_id))
})
}
WithList::Author { boosts, likes } => match kind {
Kind::Original => Ok(list
.iter()
.filter_map(|a| User::find_by_fqn(rocket, a).ok())
.any(|a| post.is_author(&rocket.conn, a.id).unwrap_or(false))),
Kind::Original => {
let mut rt = Runtime::new().unwrap();
rt.block_on(async move {
Ok(stream::iter(list)
.filter_map(|a| async move {
Some(User::find_by_fqn(rocket, a).await.ok().unwrap())
})
.collect::<Vec<_>>()
.await
.into_iter()
.any(|a| post.is_author(&rocket.conn, a.id).unwrap_or(false)))
})
}
Kind::Reshare(u) => {
if *boosts {
Ok(list.iter().any(|user| &u.fqn == user))
@@ -373,7 +394,7 @@ impl Bool {
rocket: &PlumeRocket,
timeline: &Timeline,
post: &Post,
kind: Kind,
kind: Kind<'_>,
) -> Result<bool> {
match self {
Bool::Followed { boosts, likes } => {
@@ -644,7 +665,7 @@ impl<'a> TimelineQuery<'a> {
rocket: &PlumeRocket,
timeline: &Timeline,
post: &Post,
kind: Kind,
kind: Kind<'_>,
) -> Result<bool> {
self.0.matches(rocket, timeline, post, kind)
}
@@ -811,9 +832,17 @@ mod tests {
);
let expect_keyword = TimelineQuery::parse(r#"not_a_field contains something"#).unwrap_err();
assert_eq!(expect_keyword, QueryError::SyntaxError(0, 11, "Syntax Error: Expected one of 'blog', \
assert_eq!(
expect_keyword,
QueryError::SyntaxError(
0,
11,
"Syntax Error: Expected one of 'blog', \
'author', 'license', 'tags', 'lang', 'title', 'subtitle', 'content', 'followed', 'has_cover', \
'local' or 'all', got 'not_a_field'".to_owned()));
'local' or 'all', got 'not_a_field'"
.to_owned()
)
);
let expect_bracket_or_comma = TimelineQuery::parse(r#"lang in [en ["#).unwrap_err();
assert_eq!(
+119 -112
View File
@@ -1,3 +1,9 @@
use crate::{
ap_url, blocklisted_emails::BlocklistedEmail, blogs::Blog, db_conn::DbConn, follows::Follow,
instance::*, medias::Media, notifications::Notification, post_authors::PostAuthor, posts::Post,
safe_string::SafeString, schema::users, search::Searcher, timeline::Timeline, Connection,
Error, PlumeRocket, Result, ITEMS_PER_PAGE,
};
use activitypub::{
activity::Delete,
actor::Person,
@@ -5,7 +11,6 @@ use activitypub::{
object::{Image, Tombstone},
Activity, CustomObject, Endpoint,
};
use bcrypt;
use chrono::{NaiveDateTime, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl};
use openssl::{
@@ -14,13 +19,15 @@ use openssl::{
rsa::Rsa,
sign,
};
use plume_common::activity_pub::{
ap_accept_header,
inbox::{AsActor, AsObject, FromId},
sign::{gen_keypair, Signer},
ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY,
use plume_common::{
activity_pub::{
ap_accept_header,
inbox::{AsActor, AsObject, FromId},
sign::{gen_keypair, Signer},
ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY,
},
utils,
};
use plume_common::utils;
use reqwest::{
header::{HeaderValue, ACCEPT},
ClientBuilder,
@@ -29,7 +36,6 @@ use rocket::{
outcome::IntoOutcome,
request::{self, FromRequest, Request},
};
use serde_json;
use std::{
cmp::PartialEq,
hash::{Hash, Hasher},
@@ -37,20 +43,6 @@ use std::{
use url::Url;
use webfinger::*;
use blogs::Blog;
use db_conn::DbConn;
use follows::Follow;
use instance::*;
use medias::Media;
use notifications::Notification;
use post_authors::PostAuthor;
use posts::Post;
use safe_string::SafeString;
use schema::users;
use search::Searcher;
use timeline::Timeline;
use {ap_url, Connection, Error, PlumeRocket, Result, ITEMS_PER_PAGE};
pub type CustomPerson = CustomObject<ApSignature, Person>;
pub enum Role {
@@ -60,6 +52,7 @@ pub enum Role {
}
#[derive(Queryable, Identifiable, Clone, Debug, AsChangeset)]
#[changeset_options(treat_none_as_null = "true")]
pub struct User {
pub id: i32,
pub username: String,
@@ -136,7 +129,7 @@ impl User {
}
pub fn delete(&self, conn: &Connection, searcher: &Searcher) -> Result<()> {
use schema::post_authors;
use crate::schema::post_authors;
for blog in Blog::find_for_author(conn, self)?
.iter()
@@ -196,7 +189,7 @@ impl User {
.map_err(Error::from)
}
pub fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<User> {
pub async fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<User> {
let from_db = users::table
.filter(users::fqn.eq(fqn))
.first(&*c.conn)
@@ -204,12 +197,13 @@ impl User {
if let Some(from_db) = from_db {
Ok(from_db)
} else {
User::fetch_from_webfinger(c, fqn)
User::fetch_from_webfinger(c, fqn).await
}
}
fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<User> {
let link = resolve(acct.to_owned(), true)?
async fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<User> {
let link = resolve(acct.to_owned(), true)
.await?
.links
.into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
@@ -217,8 +211,9 @@ impl User {
User::from_id(c, link.href.as_ref()?, None).map_err(|(_, e)| e)
}
pub fn fetch_remote_interact_uri(acct: &str) -> Result<String> {
resolve(acct.to_owned(), true)?
pub async fn fetch_remote_interact_uri(acct: &str) -> Result<String> {
resolve(acct.to_owned(), true)
.await?
.links
.into_iter()
.find(|l| l.rel == "http://ostatus.org/schema/1.0/subscribe")
@@ -226,9 +221,9 @@ impl User {
.ok_or(Error::Webfinger)
}
fn fetch(url: &str) -> Result<CustomPerson> {
let mut res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
async fn fetch(url: &str) -> Result<CustomPerson> {
let res = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(5))
.build()?
.get(url)
.header(
@@ -240,8 +235,9 @@ impl User {
.join(", "),
)?,
)
.send()?;
let text = &res.text()?;
.send()
.await?;
let text = &res.text().await?;
// without this workaround, publicKey is not correctly deserialized
let ap_sign = serde_json::from_str::<ApSignature>(text)?;
let mut json = serde_json::from_str::<CustomPerson>(text)?;
@@ -249,48 +245,48 @@ impl User {
Ok(json)
}
pub fn fetch_from_url(c: &PlumeRocket, url: &str) -> Result<User> {
User::fetch(url).and_then(|json| User::from_activity(c, json))
pub async fn fetch_from_url(c: &PlumeRocket, url: &str) -> Result<User> {
let json = User::fetch(url).await?;
User::from_activity(c, json)
}
pub fn refetch(&self, conn: &Connection) -> Result<()> {
User::fetch(&self.ap_url.clone()).and_then(|json| {
let avatar = Media::save_remote(
conn,
json.object
.object_props
.icon_image()?
.object_props
.url_string()?,
&self,
)
.ok();
pub async fn refetch(&self, conn: &Connection) -> Result<()> {
let json = User::fetch(&self.ap_url.clone()).await?;
let avatar = Media::save_remote(
conn,
json.object
.object_props
.icon_image()?
.object_props
.url_string()?,
&self,
)
.ok();
diesel::update(self)
.set((
users::username.eq(json.object.ap_actor_props.preferred_username_string()?),
users::display_name.eq(json.object.object_props.name_string()?),
users::outbox_url.eq(json.object.ap_actor_props.outbox_string()?),
users::inbox_url.eq(json.object.ap_actor_props.inbox_string()?),
users::summary.eq(SafeString::new(
&json
.object
.object_props
.summary_string()
.unwrap_or_default(),
)),
users::followers_endpoint.eq(json.object.ap_actor_props.followers_string()?),
users::avatar_id.eq(avatar.map(|a| a.id)),
users::last_fetched_date.eq(Utc::now().naive_utc()),
users::public_key.eq(json
.custom_props
.public_key_publickey()?
.public_key_pem_string()?),
))
.execute(conn)
.map(|_| ())
.map_err(Error::from)
})
diesel::update(self)
.set((
users::username.eq(json.object.ap_actor_props.preferred_username_string()?),
users::display_name.eq(json.object.object_props.name_string()?),
users::outbox_url.eq(json.object.ap_actor_props.outbox_string()?),
users::inbox_url.eq(json.object.ap_actor_props.inbox_string()?),
users::summary.eq(SafeString::new(
&json
.object
.object_props
.summary_string()
.unwrap_or_default(),
)),
users::followers_endpoint.eq(json.object.ap_actor_props.followers_string()?),
users::avatar_id.eq(avatar.map(|a| a.id)),
users::last_fetched_date.eq(Utc::now().naive_utc()),
users::public_key.eq(json
.custom_props
.public_key_publickey()?
.public_key_pem_string()?),
))
.execute(conn)
.map(|_| ())
.map_err(Error::from)
}
pub fn hash_pass(pass: &str) -> Result<String> {
@@ -361,9 +357,10 @@ impl User {
.set_part_of_link(Id::new(&self.outbox_url))?;
Ok(ActivityStream::new(coll))
}
fn fetch_outbox_page<T: Activity>(&self, url: &str) -> Result<(Vec<T>, Option<String>)> {
let mut res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
async fn fetch_outbox_page<T: Activity>(&self, url: &str) -> Result<(Vec<T>, Option<String>)> {
let res = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(5))
.build()?
.get(url)
.header(
@@ -375,8 +372,9 @@ impl User {
.join(", "),
)?,
)
.send()?;
let text = &res.text()?;
.send()
.await?;
let text = &res.text().await?;
let json: serde_json::Value = serde_json::from_str(text)?;
let items = json["items"]
.as_array()
@@ -391,9 +389,9 @@ impl User {
};
Ok((items, next))
}
pub fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> {
let mut res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
pub async fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> {
let res = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(5))
.build()?
.get(&self.outbox_url[..])
.header(
@@ -405,13 +403,14 @@ impl User {
.join(", "),
)?,
)
.send()?;
let text = &res.text()?;
.send()
.await?;
let text = &res.text().await?;
let json: serde_json::Value = serde_json::from_str(text)?;
if let Some(first) = json.get("first") {
let mut items: Vec<T> = Vec::new();
let mut next = first.as_str().unwrap().to_owned();
while let Ok((mut page, nxt)) = self.fetch_outbox_page(&next) {
while let Ok((mut page, nxt)) = self.fetch_outbox_page(&next).await {
if page.is_empty() {
break;
}
@@ -436,9 +435,9 @@ impl User {
}
}
pub fn fetch_followers_ids(&self) -> Result<Vec<String>> {
let mut res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
pub async fn fetch_followers_ids(&self) -> Result<Vec<String>> {
let res = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(5))
.build()?
.get(&self.followers_endpoint[..])
.header(
@@ -450,8 +449,9 @@ impl User {
.join(", "),
)?,
)
.send()?;
let text = &res.text()?;
.send()
.await?;
let text = &res.text().await?;
let json: serde_json::Value = serde_json::from_str(text)?;
Ok(json["items"]
.as_array()
@@ -461,8 +461,8 @@ impl User {
.collect::<Vec<String>>())
}
fn get_activities_count(&self, conn: &Connection) -> i64 {
use schema::post_authors;
use schema::posts;
use crate::schema::post_authors;
use crate::schema::posts;
let posts_by_self = PostAuthor::belonging_to(self).select(post_authors::post_id);
posts::table
.filter(posts::published.eq(true))
@@ -476,8 +476,8 @@ impl User {
conn: &Connection,
(min, max): (i32, i32),
) -> Result<Vec<serde_json::Value>> {
use schema::post_authors;
use schema::posts;
use crate::schema::post_authors;
use crate::schema::posts;
let posts_by_self = PostAuthor::belonging_to(self).select(post_authors::post_id);
let posts = posts::table
.filter(posts::published.eq(true))
@@ -497,7 +497,7 @@ impl User {
}
pub fn get_followers(&self, conn: &Connection) -> Result<Vec<User>> {
use schema::follows;
use crate::schema::follows;
let follows = Follow::belonging_to(self).select(follows::follower_id);
users::table
.filter(users::id.eq_any(follows))
@@ -506,7 +506,7 @@ impl User {
}
pub fn count_followers(&self, conn: &Connection) -> Result<i64> {
use schema::follows;
use crate::schema::follows;
let follows = Follow::belonging_to(self).select(follows::follower_id);
users::table
.filter(users::id.eq_any(follows))
@@ -520,7 +520,7 @@ impl User {
conn: &Connection,
(min, max): (i32, i32),
) -> Result<Vec<User>> {
use schema::follows;
use crate::schema::follows;
let follows = Follow::belonging_to(self).select(follows::follower_id);
users::table
.filter(users::id.eq_any(follows))
@@ -531,7 +531,7 @@ impl User {
}
pub fn get_followed(&self, conn: &Connection) -> Result<Vec<User>> {
use schema::follows::dsl::*;
use crate::schema::follows::dsl::*;
let f = follows.filter(follower_id.eq(self.id)).select(following_id);
users::table
.filter(users::id.eq_any(f))
@@ -540,7 +540,7 @@ impl User {
}
pub fn count_followed(&self, conn: &Connection) -> Result<i64> {
use schema::follows;
use crate::schema::follows;
follows::table
.filter(follows::follower_id.eq(self.id))
.count()
@@ -553,7 +553,7 @@ impl User {
conn: &Connection,
(min, max): (i32, i32),
) -> Result<Vec<User>> {
use schema::follows;
use crate::schema::follows;
let follows = follows::table
.filter(follows::follower_id.eq(self.id))
.select(follows::following_id)
@@ -566,7 +566,7 @@ impl User {
}
pub fn is_followed_by(&self, conn: &Connection, other_id: i32) -> Result<bool> {
use schema::follows;
use crate::schema::follows;
follows::table
.filter(follows::follower_id.eq(other_id))
.filter(follows::following_id.eq(self.id))
@@ -577,7 +577,7 @@ impl User {
}
pub fn is_following(&self, conn: &Connection, other_id: i32) -> Result<bool> {
use schema::follows;
use crate::schema::follows;
follows::table
.filter(follows::follower_id.eq(self.id))
.filter(follows::following_id.eq(other_id))
@@ -588,7 +588,7 @@ impl User {
}
pub fn has_liked(&self, conn: &Connection, post: &Post) -> Result<bool> {
use schema::likes;
use crate::schema::likes;
likes::table
.filter(likes::post_id.eq(post.id))
.filter(likes::user_id.eq(self.id))
@@ -599,7 +599,7 @@ impl User {
}
pub fn has_reshared(&self, conn: &Connection, post: &Post) -> Result<bool> {
use schema::reshares;
use crate::schema::reshares;
reshares::table
.filter(reshares::post_id.eq(post.id))
.filter(reshares::user_id.eq(self.id))
@@ -610,7 +610,7 @@ impl User {
}
pub fn is_author_in(&self, conn: &Connection, blog: &Blog) -> Result<bool> {
use schema::blog_authors;
use crate::schema::blog_authors;
blog_authors::table
.filter(blog_authors::author_id.eq(self.id))
.filter(blog_authors::blog_id.eq(blog.id))
@@ -765,7 +765,7 @@ impl User {
mime_type: None,
href: None,
template: Some(format!(
"https://{}/remote_interact?{{uri}}",
"https://{}/remote_interact?target={{uri}}",
self.get_instance(conn)?.public_domain
)),
},
@@ -794,11 +794,12 @@ impl User {
}
}
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for User {
type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<User, ()> {
let conn = request.guard::<DbConn>()?;
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let conn = try_outcome!(DbConn::from_request(request).await);
request
.cookies()
.get_private(AUTH_COOKIE)
@@ -810,7 +811,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for User {
impl IntoId for User {
fn into_id(self) -> Id {
Id::new(self.ap_url.clone())
Id::new(self.ap_url)
}
}
@@ -992,6 +993,10 @@ impl NewUser {
) -> Result<User> {
let (pub_key, priv_key) = gen_keypair();
let instance = Instance::get_local()?;
let blocklisted = BlocklistedEmail::matches_blocklist(conn, &email)?;
if let Some(x) = blocklisted {
return Err(Error::Blocklisted(x.notify_user, x.notification_text));
}
let res = User::insert(
conn,
@@ -1026,11 +1031,13 @@ impl NewUser {
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::{
instance::{tests as instance_tests, Instance},
search::tests::get_searcher,
tests::{db, rockets},
Connection as Conn,
};
use diesel::Connection;
use instance::{tests as instance_tests, Instance};
use search::tests::get_searcher;
use tests::{db, rockets};
use Connection as Conn;
pub(crate) fn fill_database(conn: &Conn) -> Vec<User> {
instance_tests::fill_database(conn);
-4
View File
@@ -1,7 +1,3 @@
extern crate diesel;
extern crate plume_common;
extern crate plume_models;
use diesel::Connection;
use plume_common::utils::random_hex;
use plume_models::migrations::IMPORTED_MIGRATIONS;
+13 -14
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 19:54\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:06\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Afrikaans\n"
"Language: af_ZA\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-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: af\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:147
msgid "Subtitle or summary"
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr ""
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr ""
+18 -20
View File
@@ -3,57 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 19:54\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:06\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Arabic\n"
"Language: ar_SA\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
"X-Generator: crowdin.com\n"
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: ar\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
msgstr "فتح محرر النصوص الغني"
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr "العنوان"
# plume-front/src/editor.rs:147
#, fuzzy
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr "العنوان الثانوي أو الملخص"
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr ""
msgstr "اكتب مقالك هنا. ماركداون مُدَعَّم."
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr ""
msgstr "يتبقا {} حرفا تقريبا"
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr "الوسوم"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr "الرخصة"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr "الغلاف"
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr ""
msgstr "هذه مسودة"
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr "نشر كتابا"
+22 -22
View File
@@ -3,55 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 18:15\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:06\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Bulgarian\n"
"Language: bg_BG\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-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: bg\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
msgstr "Отворете редактора с богат текст"
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr ""
msgstr "Заглавие"
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr ""
msgstr "Подзаглавие или резюме"
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr ""
msgstr "Напишете статията си тук. Поддържа се Markdown."
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr ""
msgstr "Остават {} знака вляво"
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr ""
msgstr "Етикети"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr ""
msgstr "Лиценз"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr ""
msgstr "Основно изображение"
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr ""
msgstr "Това е проект"
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr ""
msgstr "Публикувай"
+22 -22
View File
@@ -3,55 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 19:55\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:06\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Catalan\n"
"Language: ca_ES\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-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: ca\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
msgstr "Obre leditor de text enriquit"
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr ""
msgstr "Títol"
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr ""
msgstr "Subtítol o resum"
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr ""
msgstr "Escriviu el vostre article ací. Podeu fer servir el Markdown."
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr ""
msgstr "Queden uns {} caràcters"
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr ""
msgstr "Etiquetes"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr ""
msgstr "Llicència"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr ""
msgstr "Coberta"
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr ""
msgstr "Açò és un esborrany"
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr ""
msgstr "Publica"
+14 -15
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 18:15\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:06\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Czech\n"
"Language: cs_CZ\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
"X-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: cs\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr "Otevřít editor formátovaného textu"
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr "Nadpis"
# plume-front/src/editor.rs:147
#, fuzzy
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr "Podnadpis, nebo shrnutí"
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr "Napište sem svůj článek. Markdown je podporován."
msgstr "Sem napište svůj článek. Markdown je podporován."
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr "Zbývá kolem {} znaků"
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr "Tagy"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr "Licence"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr "Titulka"
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr "Tohle je koncept"
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr "Zveřejnit"
+13 -14
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 18:15\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:06\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Danish\n"
"Language: da_DK\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-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: da\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:147
msgid "Subtitle or summary"
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr ""
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr ""
+17 -18
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 18:14\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: German\n"
"Language: de_DE\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-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: de\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
msgstr " Rich Text Editor (RTE) öffnen"
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr "Titel"
# plume-front/src/editor.rs:147
#, fuzzy
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr "Untertitel oder Übersicht"
msgstr "Untertitel oder Zusammenfassung"
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr "Schreibe deinen Artikel hier. Markdown ist unterstützt."
msgstr "Schreiben deinen Artikel hier. Markdown wird unterstützt."
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr "Ungefähr {} Zeichen übrig"
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr "Tags"
msgstr "Schlagwörter"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr "Lizenz"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr "Einband"
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr "Dies ist ein Entwurf"
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr "Veröffentlichen"
+13 -14
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 18:14\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Greek\n"
"Language: el_GR\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-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: el\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:147
msgid "Subtitle or summary"
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr ""
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr ""
+12 -12
View File
@@ -3,24 +3,23 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 18:15\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: English\n"
"Language: en_US\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-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: en\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr ""
@@ -28,30 +27,31 @@ msgstr ""
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr ""
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr ""
+14 -15
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 18:15\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Esperanto\n"
"Language: eo_UY\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-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: eo\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr "Malfermi la riĉan redaktilon"
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr "Titolo"
# plume-front/src/editor.rs:147
#, fuzzy
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr "Subtitolo aŭ resumo"
msgstr ""
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr "Verku vian artikolon ĉi tie. Markdown estas subtenita."
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr "Proksimume {} signoj restantaj"
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr "Etikedoj"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr "Permesilo"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr "Kovro"
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr "Malfinias"
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr "Eldoni"
+15 -16
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 18:15\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Spanish\n"
"Language: es_ES\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-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: es-ES\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr "Abrir el editor de texto enriquecido"
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr "Título"
# plume-front/src/editor.rs:147
#, fuzzy
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr "Subtítulo o resumen"
msgstr ""
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr "Escriba su artículo aquí. Puede utilizar Markdown."
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr "Quedan unos {} caracteres"
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr "Etiquetas"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr "Licencia"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr "Cobertura"
msgstr "Cubierta"
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr "Esto es un borrador"
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr "Publicar"
+22 -23
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 19:55\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Persian\n"
"Language: fa_IR\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-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: fa\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
msgstr "باز کردن ویرایش‌گر غنی"
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr ""
msgstr "عنوان"
# plume-front/src/editor.rs:147
msgid "Subtitle or summary"
msgstr ""
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr "زیرعنوان، یا چکیده"
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr ""
msgstr "مقاله را اینجا بنویسید. از مارک‌داون پشتیبانی می‌شود."
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr ""
msgstr "نزدیک به {} نویسه باقی مانده است"
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr ""
msgstr "برچسب‌ها"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr ""
msgstr "پروانه"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr ""
msgstr "این، یک پیش‌نویس است"
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr ""
msgstr "انتشار"
+23 -24
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 18:14\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Finnish\n"
"Language: fi_FI\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-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: fi\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
msgstr "Avaa edistynyt tekstieditori"
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr ""
msgstr "Otsikko"
# plume-front/src/editor.rs:147
msgid "Subtitle or summary"
msgstr ""
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr "Alaotsikko tai tiivistelmä"
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr ""
msgstr "Kirjoita artikkelisi tähän. Markdown -kuvauskieli on tuettu."
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr ""
msgstr "%{count} merkkiä jäljellä"
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr ""
msgstr "Tagit"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr ""
msgstr "Lisenssi"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr ""
msgstr "Kansi"
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr ""
msgstr "Tämä on luonnos"
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr ""
msgstr "Julkaise"
+14 -15
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 18:14\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: French\n"
"Language: fr_FR\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-Generator: crowdin.com\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: fr\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr "Ouvrir l'éditeur de texte avancé"
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr "Titre"
# plume-front/src/editor.rs:147
#, fuzzy
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr "Sous-titre ou résumé"
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr "Écrivez votre article ici. Vous pouvez utiliser du Markdown."
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr "Environ {} caractères restant"
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr "Étiquettes"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr "Licence"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr "Illustration"
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr "Ceci est un brouillon"
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr "Publier"
+14 -15
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 18:14\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Galician\n"
"Language: gl_ES\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-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: gl\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr "Abra o editor de texto enriquecido"
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr "Título"
# plume-front/src/editor.rs:147
#, fuzzy
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr "Subtítulo ou resumo"
msgstr "Subtítulo, ou resumo"
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr "Escriba aquí o seu artigo: pode utilizar Markdown."
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr "Dispón de {} caracteres restantes"
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr "Etiquetas"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr "Licenza"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr "Portada"
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr "Este é un borrador"
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr "Publicar"
+13 -14
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 18:14\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Hebrew\n"
"Language: he_IL\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n"
"X-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: he\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:147
msgid "Subtitle or summary"
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr ""
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr ""
+14 -15
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 18:14\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Hindi\n"
"Language: hi_IN\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-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: hi\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr "शीर्षक"
# plume-front/src/editor.rs:147
#, fuzzy
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr "उपशीर्षक या सारांश"
msgstr ""
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr "अपना आर्टिकल या लेख यहाँ लिखें. Markdown उपलब्ध है."
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr "लगभग {} अक्षर बाकी हैं"
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr "टैग्स"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr "लाइसेंस"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr "पब्लिश करें"
+15 -17
View File
@@ -3,57 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 18:15\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:06\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Croatian\n"
"Language: hr_HR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: crowdin.com\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: hr\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr "Naslov"
# plume-front/src/editor.rs:147
#, fuzzy
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr "Podnaslov ili sažetak"
msgstr ""
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr "Tagovi"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr "Licenca"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr "Objavi"
+13 -14
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 18:15\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Hungarian\n"
"Language: hu_HU\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-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: hu\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:147
msgid "Subtitle or summary"
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr ""
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr ""
+14 -15
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 18:15\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Italian\n"
"Language: it_IT\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-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: it\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr "Apri il compositore di testo avanzato"
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr "Titolo"
# plume-front/src/editor.rs:147
#, fuzzy
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr "Sottotitolo o sommario"
msgstr "Sottotitolo, o sommario"
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr "Scrivi qui il tuo articolo. È supportato il Markdown."
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr "Circa {} caratteri rimasti"
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr "Etichette"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr "Licenza"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr "Copertina"
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr "Questa è una bozza"
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr "Pubblica"
+14 -15
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 18:15\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Japanese\n"
"Language: ja_JP\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-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: ja\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr "リッチテキストエディターを開く"
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr "タイトル"
# plume-front/src/editor.rs:147
#, fuzzy
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr "サブタイトルまたは概要"
msgstr "サブタイトルまたは概要"
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr "投稿をここに書きます。Markdown がサポートされています。"
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr "残り約 {} 文字"
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr "タグ"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr "ライセンス"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr "カバー"
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr "これは下書きです"
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr "公開"
+13 -14
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 18:15\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Korean\n"
"Language: ko_KR\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-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: ko\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:147
msgid "Subtitle or summary"
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr ""
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr ""
+13 -14
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 18:15\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:06\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Dutch\n"
"Language: nl_NL\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-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: nl\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:147
msgid "Subtitle or summary"
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr ""
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr ""
+13 -14
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 19:55\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Norwegian\n"
"Language: no_NO\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-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: no\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:147
msgid "Subtitle or summary"
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr ""
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr ""
+15 -17
View File
@@ -3,57 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 19:55\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Polish\n"
"Language: pl_PL\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n"
"%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n"
"%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
"X-Generator: crowdin.com\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: pl\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr "Otwórz edytor tekstu sformatowanego"
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr "Tytuł"
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr ""
msgstr "Podtytuł, lub podsumowanie"
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr "Napisz swój artykuł tutaj. Markdown jest obsługiwany."
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr "Pozostało w okolicy {} znaków"
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr "Tagi"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr "Licencja"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr "Okładka"
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr "To jest szkic"
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr "Opublikuj"
msgstr "Publikuj"
+19 -20
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-18 07:05\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"Language-Team: Portuguese\n"
"Language: pt_PT\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Portuguese, Brazilian\n"
"Language: pt_BR\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-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: pt-PT\n"
"X-Crowdin-Language: pt-BR\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
msgstr "Abrir o editor de rich text"
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr "Título"
# plume-front/src/editor.rs:147
#, fuzzy
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr "Legenda ou resumo"
msgstr "Subtítulo ou resumo"
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr "Escreva seu artigo aqui. Markdown pode ser usado."
msgstr "Escreva seu artigo aqui. Markdown é suportado."
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr "Cerca de {} caracteres restantes"
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr "Tags"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr "Licença"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr "Capa"
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr "Isso é um rascunho"
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr "Publicar"
+15 -17
View File
@@ -3,57 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 19:55\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Romanian\n"
"Language: ro_RO\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n"
"%100<20)) ? 1 : 2);\n"
"X-Generator: crowdin.com\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: ro\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr "Deschide editorul de text"
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr "Titlu"
# plume-front/src/editor.rs:147
#, fuzzy
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr "Subtitlu sau rezumat"
msgstr ""
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr "Scrie articolul tău aici. Markdown este acceptat."
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr "În apropiere de {} caractere rămase"
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr "Etichete"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr "Licenţă"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr "Coperta"
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr "Aceasta este o ciornă"
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr "Publică"
+13 -15
View File
@@ -3,26 +3,23 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 19:55\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 "
"&& n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 "
"&& n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
"X-Generator: crowdin.com\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: ru\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr ""
@@ -30,30 +27,31 @@ msgstr ""
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr ""
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr ""
+13 -14
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 18:15\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Slovak\n"
"Language: sk_SK\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
"X-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: sk\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr "Otvor editor formátovaného textu"
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr "Nadpis"
# plume-front/src/editor.rs:147
#, fuzzy
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr "Zhrnutie, alebo podnadpis"
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr "Tu napíš svoj článok. Markdown je podporovaný."
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr "Zostáva asi {} znakov"
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr "Štítky"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr "Licencia"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr "Obálka"
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr "Toto je koncept"
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr "Zverejniť"
+13 -14
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 19:55\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Slovenian\n"
"Language: sl_SI\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0);\n"
"X-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: sl\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr "Naslov"
# plume-front/src/editor.rs:147
msgid "Subtitle or summary"
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr "Oznake"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr "Licenca"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr "To je osnutek"
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr "Objavi"
+22 -23
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 20:49\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Serbian (Latin)\n"
"Language: sr_CS\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: crowdin.com\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: sr-CS\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
msgstr "Otvori uređivač sa stilizacijom"
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr "Naslov"
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr ""
msgstr "Podnaslov, ili sažetak"
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr ""
msgstr "Napišite vaš članak ovde. Na raspolaganju vam je Markdown."
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr ""
msgstr "Preostalo oko {} znakova"
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr ""
msgstr "Markeri"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr ""
msgstr "Licenca"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr ""
msgstr "Naslovna strana"
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr ""
msgstr "Ovo je nacrt"
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr ""
msgstr "Objavi"
+14 -15
View File
@@ -3,56 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-04-17 18:15\n"
"Last-Translator: Ana Gelez (AnaGelez)\n"
"PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Swedish\n"
"Language: sv_SE\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-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Language: sv-SE\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
# plume-front/src/editor.rs:114
# plume-front/src/editor.rs:115
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:143
# plume-front/src/editor.rs:145
msgid "Title"
msgstr "Titel"
# plume-front/src/editor.rs:147
#, fuzzy
# plume-front/src/editor.rs:149
msgid "Subtitle, or summary"
msgstr "Undertitel eller sammanfattning"
msgstr ""
# plume-front/src/editor.rs:154
# plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported."
msgstr "Skriv din artikel här. Markdown stöds."
# plume-front/src/editor.rs:165
# plume-front/src/editor.rs:167
msgid "Around {} characters left"
msgstr "Ungefär {} karaktärer kvar"
# plume-front/src/editor.rs:228
# plume-front/src/editor.rs:243
msgid "Tags"
msgstr "Taggar"
# plume-front/src/editor.rs:229
# plume-front/src/editor.rs:244
msgid "License"
msgstr "Licens"
# plume-front/src/editor.rs:232
# plume-front/src/editor.rs:247
msgid "Cover"
msgstr "Omslag"
# plume-front/src/editor.rs:252
# plume-front/src/editor.rs:267
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:259
# plume-front/src/editor.rs:274
msgid "Publish"
msgstr "Publicera"

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