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