Compare commits
342 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ef8ace025 | |||
| c26f2623a8 | |||
| 451c3b5c88 | |||
| af5674074d | |||
| 28380021a7 | |||
| 37cbf537bb | |||
| 9345a1b7a9 | |||
| d18f1d6d1d | |||
| 621258481d | |||
| ce285bac17 | |||
| a7e45bee11 | |||
| 73e075e216 | |||
| b7c7b6da9f | |||
| 703328601c | |||
| b2e7664339 | |||
| ed22c6b103 | |||
| 94ccbd6190 | |||
| 77edc4849f | |||
| 07da97f434 | |||
| 89dd6be7dd | |||
| 60c1a4264e | |||
| c4c177c530 | |||
| 4a5bffbd57 | |||
| 49b2296015 | |||
| 0ef4a06565 | |||
| 16b1337d67 | |||
| 6599179174 | |||
| f2411f4607 | |||
| 2c3d429b8e | |||
| 7562cf1e75 | |||
| de1c3f3245 | |||
| 5d38c3ad70 | |||
| b55c2ff2f0 | |||
| 056d68804a | |||
| 347bc653d8 | |||
| 6fcece3bc0 | |||
| e119c5bde9 | |||
| 1bc2749e1b | |||
| 58bb07d11d | |||
| affe24b9c6 | |||
| 0bcf063b05 | |||
| 24ecb15119 | |||
| 208c515d70 | |||
| d0ada7fc53 | |||
| d86bd898cb | |||
| db581a955b | |||
| 0b8b1bf25f | |||
| faab5f6b3b | |||
| 6387a1334b | |||
| 10be055381 | |||
| 24c9c7b5a1 | |||
| ac48744aca | |||
| f2d2ecb247 | |||
| 1d46bf8d50 | |||
| 4353f2a92d | |||
| 1b9aeae53c | |||
| 540b9af873 | |||
| d08d2c6958 | |||
| 348259a14b | |||
| e68ca1151d | |||
| 3c15f5a82f | |||
| 5ba823990a | |||
| c82c38fe52 | |||
| e7ab80d5c8 | |||
| 61b68c3a42 | |||
| 6401cb6fb7 | |||
| 9d37408535 | |||
| ded64e1cf0 | |||
| 3b766fc427 | |||
| ad285898f6 | |||
| 78be49d57d | |||
| 25e52788c5 | |||
| 5d25a64f13 | |||
| 0dc89fbf23 | |||
| 94ed0eec2a | |||
| d70e585b64 | |||
| 8f55e50c6e | |||
| c113c99e1d | |||
| 191239584b | |||
| bd1caaf5da | |||
| 76f7b5e7ac | |||
| afe449b951 | |||
| 29f0712ea1 | |||
| d091c24f79 | |||
| f9e7d2bb23 | |||
| dbf36ddf05 | |||
| 8b00853e88 | |||
| 84a3d53075 | |||
| 7967dd91c2 | |||
| b5eb2628f9 | |||
| 4ef6561cc6 | |||
| 7f54f418a8 | |||
| 8f90ea18bf | |||
| e12528aae4 | |||
| 042a1fd2b0 | |||
| be99fe5f99 | |||
| 4a212e5f9e | |||
| c253bdee54 | |||
| 46fc030df4 | |||
| 02277b1ace | |||
| ac507d1d42 | |||
| 5148349098 | |||
| e817941cc5 | |||
| 06962514fe | |||
| c3c8f6804b | |||
| 6dd730030d | |||
| 6c1dd6608d | |||
| 42fb0cf825 | |||
| 2483303138 | |||
| 47e67d9b64 | |||
| 1d042e1bb2 | |||
| 31418b1079 | |||
| 68d79bfa9c | |||
| a46b718937 | |||
| 5570494a37 | |||
| a0cd9dd6bd | |||
| cd5895d560 | |||
| fc8ee1c3bc | |||
| 2a8cc5f3ba | |||
| 82524c9dca | |||
| 864379fb81 | |||
| aae2073146 | |||
| 69963689f9 | |||
| 6ad3359609 | |||
| 6a2ba2260d | |||
| ccbf2124cb | |||
| a733ece26e | |||
| 16624fd742 | |||
| e18be6ec95 | |||
| 1bfed0294f | |||
| d4d32bcc11 | |||
| 0a148721de | |||
| cfe097edf4 | |||
| 578768d7df | |||
| 996dc309f7 | |||
| 09d9164a1c | |||
| 43f32d30cb | |||
| b1d7b3969d | |||
| 89fa208bec | |||
| a11205324b | |||
| 7d17b12000 | |||
| 885dcf91fe | |||
| 192c0f8efd | |||
| 35e71b0f8a | |||
| 1a24ba44b2 | |||
| 7c00d4a88a | |||
| 476310a217 | |||
| 26ffde726f | |||
| 3fd89e6b48 | |||
| be939cf169 | |||
| 4a15bef34f | |||
| 6eee4b0b65 | |||
| f720dcbe9a | |||
| 33fceddc08 | |||
| dfe83aafc5 | |||
| 0775c8f3f9 | |||
| 4cdc1a3655 | |||
| bbd415c392 | |||
| a236f33269 | |||
| 9b593c76e4 | |||
| 012c8d2026 | |||
| cb05571339 | |||
| 64e4550dea | |||
| ec8c17482d | |||
| f167aad0fb | |||
| 24aac22b89 | |||
| 615a5fe346 | |||
| a6699a7319 | |||
| 0f408bc738 | |||
| 44a7953c20 | |||
| 21069ea18e | |||
| 1e1fd49c8c | |||
| 84621d1d27 | |||
| ba4476bf5a | |||
| f87bc455b7 | |||
| 76ec0d2b13 | |||
| 4d547082e0 | |||
| b9b9af6c44 | |||
| 4a4534d6f3 | |||
| 894c4fee2c | |||
| 27a33c833d | |||
| b52d8e4e26 | |||
| b059fb48fc | |||
| f33b057961 | |||
| 663ca4f530 | |||
| 5892c0b088 | |||
| 044a24eac8 | |||
| b93ebedaf3 | |||
| 8539bdaf72 | |||
| c1abd3d6f9 | |||
| becb40544c | |||
| ee96d2b754 | |||
| 30093d3a9f | |||
| 29f2239e3f | |||
| 7f1b33a567 | |||
| d0dd23a1af | |||
| f8774223d4 | |||
| bca870f1f8 | |||
| 64d38c88e8 | |||
| 3ac94b3e43 | |||
| 254eef6a2a | |||
| f1255f477f | |||
| 6c7b95c1b9 | |||
| e7d17caf9d | |||
| 71dd80f125 | |||
| ed4761e841 | |||
| fe05f9a58e | |||
| 51e5a401a5 | |||
| f1990b782d | |||
| 4e3ed0f4c1 | |||
| fdb58215c0 | |||
| 47c8711933 | |||
| 20a099c16f | |||
| 3824e4ba25 | |||
| b163fc8423 | |||
| b50f723a4d | |||
| b778933c64 | |||
| 12b97ce713 | |||
| 4025a45d52 | |||
| 8a6d832513 | |||
| 0b0b3a158f | |||
| 6b745ca8f0 | |||
| 1f0cb7e05c | |||
| c59821945d | |||
| 1a723b78b7 | |||
| e7ff7e3263 | |||
| 3a6c1aeeef | |||
| 567e011a66 | |||
| eaf63e0b17 | |||
| 63b31a9154 | |||
| 5885cb5b09 | |||
| f5ca464b16 | |||
| 1c40d5d3bc | |||
| 7e1abc527f | |||
| 39b2ef72fd | |||
| 17b081a21f | |||
| c482b1b552 | |||
| 8b16b9aaa4 | |||
| 11acc4172c | |||
| 6187c62cb3 | |||
| d6b5b9a721 | |||
| 80675f85f3 | |||
| 07a6be424f | |||
| 8cc56d8d81 | |||
| f8cabcc904 | |||
| d6a946f5b9 | |||
| 2a5a2ec4d2 | |||
| 106e9417d3 | |||
| 2660efec16 | |||
| 1eaa5a6e31 | |||
| b2c26f03e8 | |||
| a75cdaa870 | |||
| 2047355d46 | |||
| 17bbcd1a4b | |||
| 5c8170a97d | |||
| beaeaf743a | |||
| fe641620a6 | |||
| 3e1a6e61ad | |||
| 45b46838ae | |||
| 46cdde8687 | |||
| 6b0dfb729c | |||
| 75a4d1abf1 | |||
| 984c0cda6b | |||
| 902612a470 | |||
| e311d4d423 | |||
| aa33c8e7d3 | |||
| 60e6d49319 | |||
| 6dbd9b76fb | |||
| e3877d3022 | |||
| 4c11df65dc | |||
| f8cfb8cacf | |||
| 9a7e10b560 | |||
| e131330175 | |||
| b9066cb7d0 | |||
| 4c4d9fafd5 | |||
| bbdf2a8293 | |||
| 0d3e1a233f | |||
| 712bc2afbe | |||
| 2eadb80435 | |||
| de05b9e176 | |||
| 5bf1e81f0b | |||
| 55abbff9b8 | |||
| 5f89401a7c | |||
| 7bd3c1b01e | |||
| 961806247c | |||
| ac7ffae121 | |||
| 5321f3cf18 | |||
| 5099a00c6b | |||
| d44c034f6a | |||
| 963e742dcf | |||
| 50462204e8 | |||
| 174ed738bd | |||
| f8d098f353 | |||
| f5b18dffaf | |||
| 002d1f1720 | |||
| 23a5634035 | |||
| 5cd8ae9106 | |||
| 350697f89a | |||
| 569979374f | |||
| 3c666080a9 | |||
| 4e6d7b70cf | |||
| a59f167771 | |||
| f28ab19dc0 | |||
| 6dd4080d65 | |||
| eac76e6806 | |||
| 0cd26dfbf4 | |||
| 3de009713d | |||
| 69bcb01715 | |||
| 9ec2d93f50 | |||
| 4da9b24cb1 | |||
| f40634aa97 | |||
| 83282e0736 | |||
| 43fde90387 | |||
| ccba8163c9 | |||
| 8975b0f9e9 | |||
| d626f3366d | |||
| b24f195e10 | |||
| 003dcf861a | |||
| 5d8efa77a3 | |||
| fb6d49cc6e | |||
| 48776939f4 | |||
| 104b6c1d0c | |||
| 3881927550 | |||
| 0bec13edc0 | |||
| 191d6da486 | |||
| 9aaff1a484 | |||
| 36fd55a7e4 | |||
| fc474bf8d1 | |||
| a050deb557 | |||
| 6de9a1f1c8 | |||
| 5e30bede40 | |||
| 5cca66b346 | |||
| 484659fde2 | |||
| 5fc827c1c9 | |||
| 50753b3a65 | |||
| c5d03d300b | |||
| 010eac6c4a | |||
| a107d35492 | |||
| f9beb2383b | |||
| 7e78cffcaa | |||
| 719fec0828 | |||
| 90fdade566 |
@@ -0,0 +1,10 @@
|
||||
[target.wasm32-unknown-unknown]
|
||||
# required for clippy
|
||||
rustflags = [
|
||||
"--cfg", "web_sys_unstable_apis",
|
||||
]
|
||||
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
rustflags = [
|
||||
"--cfg", "web_sys_unstable_apis",
|
||||
]
|
||||
+13
-9
@@ -10,7 +10,7 @@ executors:
|
||||
type: boolean
|
||||
default: false
|
||||
docker:
|
||||
- image: plumeorg/plume-buildenv:v0.0.9
|
||||
- image: plumeorg/plume-buildenv:v0.4.0
|
||||
- image: <<#parameters.postgres>>circleci/postgres:9.6-alpine<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
@@ -21,6 +21,7 @@ executors:
|
||||
RUST_TEST_THREADS: 1
|
||||
FEATURES: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
|
||||
DATABASE_URL: <<#parameters.postgres>>postgres://postgres@localhost/plume<</parameters.postgres>><<^parameters.postgres>>plume.sqlite<</parameters.postgres>>
|
||||
ROCKET_SECRET_KEY: VN5xV1DN7XdpATadOCYcuGeR/dV0hHfgx9mx9TarLdM=
|
||||
|
||||
|
||||
commands:
|
||||
@@ -71,7 +72,7 @@ commands:
|
||||
type: string
|
||||
steps:
|
||||
- run: |
|
||||
export RUSTFLAGS="-Zprofile -Zfewer-names -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zno-landing-pads -Clink-arg=-Xlinker -Clink-arg=--no-keep-memory -Clink-arg=-Xlinker -Clink-arg=--reduce-memory-overheads"
|
||||
export RUSTFLAGS="-Zprofile -Zfewer-names -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Clink-arg=-Xlinker -Clink-arg=--no-keep-memory -Clink-arg=-Xlinker -Clink-arg=--reduce-memory-overheads"
|
||||
export CARGO_INCREMENTAL=0
|
||||
<< parameters.cmd >>
|
||||
|
||||
@@ -99,7 +100,7 @@ commands:
|
||||
steps:
|
||||
- run: |
|
||||
cmd="cargo build <<#parameters.release>>--release<</parameters.release>> --no-default-features --features="${FEATURES}" -p <<parameters.package>> -j"
|
||||
for i in 36 4 2 1 1; do
|
||||
for i in 16 4 2 1 1; do
|
||||
$cmd $i && exit 0
|
||||
done
|
||||
exit 1
|
||||
@@ -143,11 +144,14 @@ jobs:
|
||||
cache: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
|
||||
- run_with_coverage:
|
||||
cmd: |
|
||||
cargo build -p plume-cli --no-default-features --features=${FEATURES} -j 4
|
||||
./target/debug/plm migration run
|
||||
./target/debug/plm search init
|
||||
cmd="cargo test --all --exclude plume-front --exclude plume-macro --no-run --no-default-features --features=${FEATURES} -j"
|
||||
for i in 36 4 2 1 1; do
|
||||
for i in 16 4 2 1 1; do
|
||||
$cmd $i && break
|
||||
done
|
||||
cargo test --all --exclude plume-front --exclude plume-macro --no-default-features --features="${FEATURES}" -j1 -- --test-threads=1
|
||||
cargo test --all --exclude plume-front --exclude plume-macro --no-default-features --features="${FEATURES}" -j1
|
||||
- upload_coverage:
|
||||
type: unit
|
||||
- cache:
|
||||
@@ -164,18 +168,18 @@ jobs:
|
||||
steps:
|
||||
- restore_env:
|
||||
cache: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
|
||||
- run: cargo web deploy -p plume-front
|
||||
- run: wasm-pack build --target web --release plume-front
|
||||
- run_with_coverage:
|
||||
cmd: |
|
||||
cmd="cargo install --debug --no-default-features --features="${FEATURES}",test --force --path . -j"
|
||||
for i in 36 4 2 1 1; do
|
||||
for i in 16 4 2 1 1; do
|
||||
$cmd $i && exit 0
|
||||
done
|
||||
exit 1
|
||||
- run_with_coverage:
|
||||
cmd: |
|
||||
cmd="cargo install --debug --no-default-features --features="${FEATURES}" --force --path plume-cli -j"
|
||||
for i in 36 4 2 1 1; do
|
||||
for i in 16 4 2 1 1; do
|
||||
$cmd $i && exit 0
|
||||
done
|
||||
exit 1
|
||||
@@ -199,7 +203,7 @@ jobs:
|
||||
steps:
|
||||
- restore_env:
|
||||
cache: release-<<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
|
||||
- run: cargo web deploy -p plume-front --release
|
||||
- run: wasm-pack build --target web --release plume-front
|
||||
- build:
|
||||
package: plume
|
||||
release: true
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
localhost:443 {
|
||||
proxy / localhost:7878 {
|
||||
transparent
|
||||
}
|
||||
tls self_signed
|
||||
localhost {
|
||||
reverse_proxy localhost:7878
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
FROM debian:stretch-20190326
|
||||
FROM debian:buster-20210208
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
#install native/circleci/build dependancies
|
||||
RUN apt update &&\
|
||||
apt install -y --no-install-recommends git ssh tar gzip ca-certificates default-jre&&\
|
||||
apt install -y --no-install-recommends binutils-dev build-essential cmake curl gcc gettext git libcurl4-openssl-dev libdw-dev libelf-dev libiberty-dev libpq-dev libsqlite3-dev libssl-dev make openssl pkg-config postgresql postgresql-contrib python zlib1g-dev python3-pip zip unzip libclang-dev&&\
|
||||
echo "deb [trusted=yes] https://apt.fury.io/caddy/ /" \
|
||||
| tee -a /etc/apt/sources.list.d/caddy-fury.list &&\
|
||||
apt update &&\
|
||||
apt install -y --no-install-recommends binutils-dev build-essential cmake curl gcc gettext git libcurl4-openssl-dev libdw-dev libelf-dev libiberty-dev libpq-dev libsqlite3-dev libssl-dev make openssl pkg-config postgresql postgresql-contrib python zlib1g-dev python3-pip zip unzip libclang-dev clang caddy&&\
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
#install and configure rust
|
||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2020-01-15 -y &&\
|
||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2021-01-15 -y &&\
|
||||
rustup component add rustfmt clippy &&\
|
||||
rustup component add rust-std --target wasm32-unknown-unknown
|
||||
|
||||
#compile some deps
|
||||
RUN cargo install cargo-web &&\
|
||||
RUN cargo install wasm-pack &&\
|
||||
cargo install grcov &&\
|
||||
strip /root/.cargo/bin/* &&\
|
||||
rm -fr ~/.cargo/registry
|
||||
@@ -24,8 +27,7 @@ COPY cargo_config /root/.cargo/config
|
||||
#install selenium for front end tests
|
||||
RUN pip3 install selenium
|
||||
|
||||
#install and configure caddy
|
||||
RUN curl https://getcaddy.com | bash -s personal
|
||||
#configure caddy
|
||||
COPY Caddyfile /Caddyfile
|
||||
|
||||
#install crowdin
|
||||
|
||||
+12
@@ -15,6 +15,9 @@ DATABASE_URL=postgres://plume:plume@localhost/plume
|
||||
# The domain of your instance
|
||||
BASE_URL=plu.me
|
||||
|
||||
# Log level for each crate
|
||||
RUST_LOG=warn,html5ever=warn,hyper=warn,tantivy=warn
|
||||
|
||||
# The secret key for private cookies and CSRF protection
|
||||
# You can generate one with `openssl rand -base64 32`
|
||||
ROCKET_SECRET_KEY=
|
||||
@@ -45,3 +48,12 @@ ROCKET_ADDRESS=127.0.0.1
|
||||
#PLUME_LOGO_192=icons/trwnh/paragraphs/plumeParagraphs192.png
|
||||
#PLUME_LOGO_256=icons/trwnh/paragraphs/plumeParagraphs256.png
|
||||
#PLUME_LOGO_512=icons/trwnh/paragraphs/plumeParagraphs512.png
|
||||
|
||||
## LDAP CONFIG ##
|
||||
# the object that will be bound is "${USER_NAME_ATTR}=${username},${BASE_DN}"
|
||||
#LDAP_ADDR=ldap://127.0.0.1:1389
|
||||
#LDAP_BASE_DN="ou=users,dc=your-org,dc=eu"
|
||||
#LDAP_USER_NAME_ATTR=cn
|
||||
#LDAP_USER_MAIL_ATTR=mail
|
||||
#LDAP_TLS=false
|
||||
|
||||
|
||||
@@ -7,6 +7,16 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
We would appreciated if you report a bug at our Gitea instance's issue page:
|
||||
https://git.joinplu.me/Plume/Plume/issues
|
||||
You can login to the Gitea with your GitHub account.
|
||||
|
||||
We welcome to receive bug reports here, GitHub, too.
|
||||
-->
|
||||
|
||||
|
||||
|
||||
<!-- Describe your bug, explaining how to reproduce it, and what was expected -->
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,15 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
We would appreciated if you request a feature at our Gitea instance's issue page:
|
||||
https://git.joinplu.me/Plume/Plume/issues
|
||||
You can login to the Gitea with your GitHub account.
|
||||
|
||||
We welcome to receive feature requests here, GitHub, too.
|
||||
-->
|
||||
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<!--
|
||||
We would appreciated if you report a bug at our Gitea instance's pull request page:
|
||||
https://git.joinplu.me/Plume/Plume/pulls
|
||||
You can login to the Gitea with your GitHub account.
|
||||
|
||||
We welcome to receive pull requests here, GitHub, too.
|
||||
-->
|
||||
@@ -18,3 +18,5 @@ tags.*
|
||||
search_index
|
||||
.buildconfig
|
||||
__pycache__
|
||||
.vscode/
|
||||
*-journal
|
||||
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
# Changelog
|
||||
|
||||
<!-- next-header -->
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
### Added
|
||||
|
||||
- Allow `dir` attributes for LtoR text in RtoL document (#860)
|
||||
- More translation languages (#862)
|
||||
- Proxy support (#829)
|
||||
- Riker a actor system library (#870)
|
||||
- (request-target) and Host header in HTTP Signature (#872)
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade some dependent crates (#858)
|
||||
- Use tracing crate (#868)
|
||||
- Update Rust version to nightly-2021-01-15 (#878)
|
||||
- Upgrade Tantivy to 0.13.3 and lindera-tantivy to 0.7.1 (#878)
|
||||
- Run searcher on actor system (#870)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Percent-encode URI for remote_interact (#866, #857)
|
||||
- Menu animation not opening on iOS (#876, #897)
|
||||
|
||||
## [[0.6.0]] - 2020-12-29
|
||||
|
||||
### Added
|
||||
|
||||
- Vazir font for better support of languages written in Arabic script (#787)
|
||||
- Login via LDAP (#826)
|
||||
- cargo-release (#835)
|
||||
- Care about weak ETag header for better caching (#840)
|
||||
- Support for right to left languages in post content (#853)
|
||||
|
||||
### Changed
|
||||
|
||||
- Bump Docker base images to Buster flavor (#797)
|
||||
- Upgrade Rocket to 0.4.5 (#800)
|
||||
- Keep tags as-is (#832)
|
||||
- Update Docker image for testing (#838)
|
||||
- Update Dockerfile.dev (#841)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Recreate search index if its format is outdated (#802)
|
||||
- Make it possible to switch to rich text editor (#808)
|
||||
- Fix margins for the mobile devices (#817)
|
||||
- GPU acceleration for the mobile menu (#818)
|
||||
- Natural title position for RtoL languages (#825)
|
||||
- Remove link to unimplemented page (#827)
|
||||
- Fix displaying not found page when submitting a duplicated blocklist email (#831)
|
||||
|
||||
### Security
|
||||
|
||||
- Validate spoofing of activity
|
||||
|
||||
## [0.5.0] - 2020-06-21
|
||||
|
||||
### Added
|
||||
|
||||
- Email blocklisting (#718)
|
||||
- Syntax highlighting (#691)
|
||||
- Persian localization (#782)
|
||||
- Switchable tokenizer - enables Japanese full-text search (#776)
|
||||
- Make database connections configurable by environment variables (#768)
|
||||
|
||||
### Changed
|
||||
|
||||
- Display likes and boost on post cards (#744)
|
||||
- Rust 2018 (#726)
|
||||
- Bump to LLVM to 9.0.0 to fix ARM builds (#737)
|
||||
- Remove dependency on runtime-fmt (#773)
|
||||
- Drop the -alpha suffix in release names, it is implied that Plume is not stable yet because of the 0 major version (Plume 1.0.0 will be the first stable release).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix parsing of mentions inside a Markdown code block (be430c6)
|
||||
- Fix RSS issues (#720)
|
||||
- Fix Atom feed (#764)
|
||||
- Fix default theme (#746)
|
||||
- Fix shown password on remote interact pages (#741)
|
||||
- Allow unicode hashtags (#757)
|
||||
- Fix French grammar for for 0 (#760)
|
||||
- Don't show boosts and likes for "all" and "local" in timelines (#781)
|
||||
- Fix liking and boosting posts on remote instances (#762)
|
||||
|
||||
## [0.4.0] - 2019-12-23
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for generic timeline (#525)
|
||||
- Federate user deletion (#551)
|
||||
- import migrations and don't require diesel_cli for admins (#555)
|
||||
- Cache local instance (#572)
|
||||
- Initial RTL support #575 (#577)
|
||||
- Confirm deletion of blog (#602)
|
||||
- Make a distinction between moderators and admins (#619)
|
||||
- Theming (#624)
|
||||
- Add clap to plume in order to print help and version (#631)
|
||||
- Add Snapcraft metadata and install/maintenance hooks (#666)
|
||||
- Add environmental variable to control path of media (#683)
|
||||
- Add autosaving to the editor (#688)
|
||||
- CI: Upload artifacts to pull request deploy environment (#539)
|
||||
- CI: Upload artifact of wasm binary (#571)
|
||||
|
||||
### Changed
|
||||
|
||||
- Update follow_remote.rs.html grammar (#548)
|
||||
- Add some feedback when performing some actions (#552)
|
||||
- Theme update (#553)
|
||||
- Remove the new index lock tantivy uses (#556)
|
||||
- Reduce reqwest timeout to 5s (#557)
|
||||
- Improve notification management (#561)
|
||||
- Fix occurrences of 'have been' to 'has been' (#578) + Direct follow-up to #578 (#603)
|
||||
- Store password reset requests in database (#610)
|
||||
- Use futures and tokio to send activities (#620)
|
||||
- Don't ignore dotenv errors (#630)
|
||||
- Replace the input! macro with an Input builder (#646)
|
||||
- Update default license (#659)
|
||||
- Paginate the outbox responses. Fixes #669 (#681)
|
||||
- Use the "classic" editor by default (#697)
|
||||
- Fix issue #705 (#708)
|
||||
- Make comments in styleshhets a bit clearer (#545)
|
||||
- Rewrite circleci config (#558)
|
||||
- Use openssl instead of sha256sum for build.rs (#568)
|
||||
- Update dependencies (#574)
|
||||
- Refactor code to use Shrinkwraprs and diesel-derive-newtype (#598)
|
||||
- Add enum containing all successful route returns (#614)
|
||||
- Update dependencies which depended on nix -- fixes arm32 builds (#615)
|
||||
- Update some documents (#616)
|
||||
- Update dependencies (#643)
|
||||
- Make the comment syntax consistent across all CSS (#487)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Remove r (#535)
|
||||
- Fix certain improper rendering of forms (#560)
|
||||
- make hashtags work in profile summary (#562)
|
||||
- Fix some federation issue (#573)
|
||||
- Prevent comment form submit button distortion on iOS (#592)
|
||||
- Update textarea overflow to scroll (#609)
|
||||
- Fix arm builds (#612)
|
||||
- Fix theme caching (#647)
|
||||
- Fix issue #642, frontend not in English if the user language does not exist (#648)
|
||||
- Don't index drafts (#656)
|
||||
- Fill entirely user on creation (#657)
|
||||
- Delete notification on user deletion (#658)
|
||||
- Order media so that latest added are top (#660)
|
||||
- Fix logo URL (#664)
|
||||
- Snap: Ensure cargo-web doesn't erroneously adopt our workspace. (#667)
|
||||
- Snap: Another fix for building (#668)
|
||||
- Snap: Fix build for non-Tier-1 Rust platforms (#672)
|
||||
- Don't split sentences for translations (#677)
|
||||
- Escape href quotation marks (#678)
|
||||
- Re-add empty strings in translation (#682)
|
||||
- Make the search index creation during migration respect SEARCH_INDEX (#689)
|
||||
- Fix the navigation menu not opening on touch (#690)
|
||||
- Make search items optional (#693)
|
||||
- Various snap fixes (#698)
|
||||
- Fix #637 : Markdown footnotes (#700)
|
||||
- Fix lettre (#706)
|
||||
- CI: Fix Crowdin upload (#576)
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove the Canapi dependency (#540)
|
||||
- Remove use of Rust in migrations (#704)
|
||||
|
||||
## [0.3.0] - 2019-04-19
|
||||
|
||||
### Added
|
||||
|
||||
- Cover for articles (#299, #387)
|
||||
- Password reset (#448)
|
||||
- New editor (#293, #458, #482, #483, #486, #530)
|
||||
- Search (#324, #375, #445)
|
||||
- Edit blogs (#460, #494, #497)
|
||||
- Hashtags in articles (#283, #295)
|
||||
- API endpoints (#245, #285, #307)
|
||||
- A bunch of new translations! (#479, #501, #506, #510, #512, #514)
|
||||
|
||||
### Changed
|
||||
|
||||
- Federation improvements (#216, #217, #357, #364, #399, #443, #446, #455, #502, #519)
|
||||
- Improved build process (#281, #374, #392, #402, #489, #498, #503, #511, #513, #515, #528)
|
||||
|
||||
### Fixes
|
||||
|
||||
- UI usability fixes (#370, #386, #401, #417, #418, #444, #452, #480, #516, #518, #522, #532)
|
||||
|
||||
## [0.2.0] - 2018-09-12
|
||||
|
||||
### Added
|
||||
|
||||
- Article publishing, or save as a draft
|
||||
- Like, or boost an article
|
||||
- Basic Markdown editor
|
||||
- Federated commenting system
|
||||
- User account creation
|
||||
- Limited federation on other platforms and subscribing to users
|
||||
- Ability to create multiple blogs
|
||||
|
||||
<!-- next-url -->
|
||||
[Unreleased]: https://github.com/Plume-org/Plume/compare/0.6.0...HEAD
|
||||
[[0.6.0]]: https://github.com/Plume-org/Plume/compare/0.5.0...0.6.0
|
||||
[0.5.0]: https://github.com/Plume-org/Plume/compare/0.4.0-alpha-4...0.5.0
|
||||
[0.4.0]: https://github.com/Plume-org/Plume/compare/0.3.0-alpha-2...0.4.0-alpha-4
|
||||
[0.3.0]: https://github.com/Plume-org/Plume/compare/0.2.0-alpha-1...0.3.0-alpha-2
|
||||
[0.2.0]: https://github.com/Plume-org/Plume/releases/tag/0.2.0-alpha-1
|
||||
Generated
+2695
-2109
File diff suppressed because it is too large
Load Diff
+9
-9
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
authors = ["Plume contributors"]
|
||||
name = "plume"
|
||||
version = "0.4.0"
|
||||
version = "0.6.1-dev"
|
||||
repository = "https://github.com/Plume-org/Plume"
|
||||
edition = "2018"
|
||||
|
||||
@@ -10,8 +10,7 @@ activitypub = "0.1.3"
|
||||
askama_escape = "0.1"
|
||||
atom_syndication = "0.6"
|
||||
clap = "2.33"
|
||||
colored = "1.8"
|
||||
dotenv = "0.14"
|
||||
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" }
|
||||
@@ -20,19 +19,20 @@ heck = "0.3.0"
|
||||
lettre = "0.9.2"
|
||||
lettre_email = "0.9.2"
|
||||
num_cpus = "1.10"
|
||||
rocket = "0.4.2"
|
||||
rocket_contrib = { version = "0.4.2", features = ["json"] }
|
||||
rocket = "0.4.6"
|
||||
rocket_contrib = { version = "0.4.5", features = ["json"] }
|
||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||
rpassword = "4.0"
|
||||
scheduled-thread-pool = "0.2.2"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_qs = "0.5"
|
||||
shrinkwraprs = "0.2.1"
|
||||
syntect = "3.3"
|
||||
validator = "0.8"
|
||||
validator_derive = "0.8"
|
||||
webfinger = "0.4.1"
|
||||
tracing = "0.1.22"
|
||||
tracing-subscriber = "0.2.15"
|
||||
riker = "0.4.2"
|
||||
|
||||
[[bin]]
|
||||
name = "plume"
|
||||
@@ -48,7 +48,7 @@ version = "3.1.2"
|
||||
|
||||
[dependencies.diesel]
|
||||
features = ["r2d2", "chrono"]
|
||||
version = "*"
|
||||
version = "1.4.5"
|
||||
|
||||
[dependencies.multipart]
|
||||
default-features = false
|
||||
@@ -69,7 +69,7 @@ git = "https://github.com/fdb-hiroshima/rocket_csrf"
|
||||
rev = "29910f2829e7e590a540da3804336577b48c7b31"
|
||||
|
||||
[build-dependencies]
|
||||
ructe = "0.9.0"
|
||||
ructe = "0.13.0"
|
||||
rsass = "0.9"
|
||||
|
||||
[features]
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
FROM rust:1-stretch as builder
|
||||
FROM rust:1-buster as builder
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
@@ -19,7 +19,7 @@ RUN chmod a+x ./wasm-deps.sh && sleep 1 && ./wasm-deps.sh
|
||||
|
||||
WORKDIR /app
|
||||
COPY Cargo.toml Cargo.lock rust-toolchain ./
|
||||
RUN cargo install cargo-web
|
||||
RUN cargo install wasm-pack
|
||||
|
||||
COPY . .
|
||||
|
||||
@@ -28,7 +28,7 @@ RUN cargo install --path ./ --force --no-default-features --features postgres
|
||||
RUN cargo install --path plume-cli --force --no-default-features --features postgres
|
||||
RUN cargo clean
|
||||
|
||||
FROM debian:stretch-slim
|
||||
FROM debian:buster-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
|
||||
+4
-3
@@ -1,4 +1,4 @@
|
||||
FROM rust:1-stretch
|
||||
FROM rust:1-buster
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
@@ -10,7 +10,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
make \
|
||||
openssl \
|
||||
libssl-dev
|
||||
libssl-dev\
|
||||
clang
|
||||
|
||||
WORKDIR /scratch
|
||||
COPY script/wasm-deps.sh .
|
||||
@@ -19,7 +20,7 @@ RUN chmod a+x ./wasm-deps.sh && sleep 1 && ./wasm-deps.sh
|
||||
WORKDIR /app
|
||||
COPY Cargo.toml Cargo.lock rust-toolchain ./
|
||||
RUN cargo install diesel_cli --no-default-features --features postgres --version '=1.3.0'
|
||||
RUN cargo install cargo-web
|
||||
RUN cargo install wasm-pack
|
||||
|
||||
COPY . .
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ rules and thematics, but they can all communicate with each other.
|
||||
|
||||
## Get involved
|
||||
|
||||
If you want to have regular news about the project, the best place is probably [our blog](https://fediverse.blog/~/PlumeDev), or our Matrix room: [`#plume:disroot.org`](https://riot.im/app/#/room/#plume:disroot.org).
|
||||
If you want to have regular news about the project, the best place is probably [our blog](https://fediverse.blog/~/PlumeDev), or our Matrix room: [`#plume-blog:matrix.org`](https://matrix.to/#/#plume-blog:matrix.org).
|
||||
|
||||
If you want to contribute more, a good first step is to read [our contribution guides](https://docs.joinplu.me/contribute). We accept all kind of contribution:
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
* {
|
||||
font-family: monospace;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ main header.article {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: end;
|
||||
justify-content: flex-end;
|
||||
|
||||
h1, .article-info {
|
||||
text-align: center;
|
||||
@@ -64,37 +64,37 @@ main header.article {
|
||||
}
|
||||
|
||||
main .article-info {
|
||||
margin: 0 auto 3em;
|
||||
font-size: 0.95em;
|
||||
font-weight: 400;
|
||||
margin: 0 auto 3em;
|
||||
font-size: 0.95em;
|
||||
font-weight: 400;
|
||||
|
||||
.author, .author a {
|
||||
font-weight: 600;
|
||||
}
|
||||
.author, .author a {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
/* The article itself */
|
||||
main article {
|
||||
max-width: $article-width;
|
||||
margin: 2.5em auto;
|
||||
font-family: $lora;
|
||||
font-size: 1.2em;
|
||||
line-height: 1.7;
|
||||
margin: 2.5em auto;
|
||||
font-family: $lora;
|
||||
font-size: 1.2em;
|
||||
line-height: 1.7;
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin: 3em auto;
|
||||
max-width: 100%;
|
||||
img {
|
||||
display: block;
|
||||
margin: 3em auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 1em;
|
||||
background: $gray;
|
||||
overflow: auto;
|
||||
padding: 1em;
|
||||
background: $gray;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
@@ -126,7 +126,7 @@ main .article-meta {
|
||||
|
||||
> p {
|
||||
margin: 2em $horizontal-margin;
|
||||
font-size: 0.9em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Article Tags */
|
||||
@@ -157,15 +157,15 @@ main .article-meta {
|
||||
/* Likes & Boosts */
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.likes, .reshares {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0.5em 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0.5em 0;
|
||||
|
||||
p {
|
||||
font-size: 1.5em;
|
||||
@@ -175,34 +175,34 @@ main .article-meta {
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
color: $text-color;
|
||||
border: none;
|
||||
font-size: 1.1em;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
color: $text-color;
|
||||
border: none;
|
||||
font-size: 1.1em;
|
||||
cursor: pointer;
|
||||
|
||||
svg.feather {
|
||||
transition: background 0.1s ease-in;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
svg.feather {
|
||||
transition: background 0.1s ease-in;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
margin: 0.5em 0;
|
||||
width: 2.5em;
|
||||
height: 2.5em;
|
||||
margin: 0.5em 0;
|
||||
width: 2.5em;
|
||||
height: 2.5em;
|
||||
|
||||
border-radius: 50%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&.reshared, &.liked {
|
||||
svg.feather {
|
||||
color: $background;
|
||||
font-weight: 900;
|
||||
color: $background;
|
||||
font-weight: 900;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,14 +213,14 @@ main .article-meta {
|
||||
|
||||
.action svg.feather {
|
||||
padding: 0.7em;
|
||||
box-sizing: border-box;
|
||||
color: $red;
|
||||
fill: none;
|
||||
border: solid $red thin;
|
||||
box-sizing: border-box;
|
||||
color: $red;
|
||||
fill: none;
|
||||
border: solid $red thin;
|
||||
}
|
||||
|
||||
.action:hover svg.feather {
|
||||
background: transparentize($red, 0.85);
|
||||
background: transparentize($red, 0.85);
|
||||
}
|
||||
|
||||
.action.liked svg.feather {
|
||||
@@ -238,22 +238,22 @@ main .article-meta {
|
||||
|
||||
.action svg.feather {
|
||||
padding: 0.7em;
|
||||
box-sizing: border-box;
|
||||
color: $primary;
|
||||
border: solid $primary thin;
|
||||
font-weight: 600;
|
||||
box-sizing: border-box;
|
||||
color: $primary;
|
||||
border: solid $primary thin;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action:hover svg.feather {
|
||||
background: transparentize($primary, 0.85);
|
||||
background: transparentize($primary, 0.85);
|
||||
}
|
||||
|
||||
.action.reshared svg.feather {
|
||||
background: $primary;
|
||||
}
|
||||
.action.reshared:hover svg.feather {
|
||||
background: transparentize($primary, 0.75)
|
||||
color: $primary;
|
||||
background: transparentize($primary, 0.75)
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,9 +262,9 @@ main .article-meta {
|
||||
margin: 0 $horizontal-margin;
|
||||
|
||||
h2 {
|
||||
color: $primary;
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
color: $primary;
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
summary {
|
||||
@@ -279,16 +279,16 @@ main .article-meta {
|
||||
|
||||
// Respond & delete comment buttons
|
||||
a.button, form.inline, form.inline input {
|
||||
padding: 0;
|
||||
background: none;
|
||||
color: $text-color;
|
||||
margin-right: 2em;
|
||||
font-family: $route159;
|
||||
padding: 0;
|
||||
background: none;
|
||||
color: $text-color;
|
||||
margin-right: 2em;
|
||||
font-family: $route159;
|
||||
font-weight: normal;
|
||||
|
||||
&::before {
|
||||
color: $primary;
|
||||
padding-right: 0.5em;
|
||||
&::before {
|
||||
color: $primary;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
&:hover { color: $primary; }
|
||||
@@ -296,8 +296,8 @@ main .article-meta {
|
||||
|
||||
.comment {
|
||||
margin: 1em 0;
|
||||
font-size: 1em;
|
||||
border: none;
|
||||
font-size: 1em;
|
||||
border: none;
|
||||
|
||||
.content {
|
||||
background: $gray;
|
||||
@@ -328,36 +328,36 @@ main .article-meta {
|
||||
color: transparentize($text-color, 0.6);
|
||||
}
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
.author {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
|
||||
* {
|
||||
transition: all 0.1s ease-in;
|
||||
}
|
||||
* {
|
||||
transition: all 0.1s ease-in;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
color: $text-color;
|
||||
.display-name {
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.display-name { color: $primary; }
|
||||
small { opacity: 1; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > .comment {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.text {
|
||||
padding: 1.25em 0;
|
||||
font-family: $lora;
|
||||
font-size: 1.1em;
|
||||
line-height: 1.4;
|
||||
text-align: left;
|
||||
.text {
|
||||
padding: 1.25em 0;
|
||||
font-family: $lora;
|
||||
font-size: 1.1em;
|
||||
line-height: 1.4;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -490,3 +490,30 @@ input:checked ~ .cw-container > .cw-text {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
// Small screens
|
||||
@media screen and (max-width: 600px) {
|
||||
#plume-editor header {
|
||||
flex-direction: column-reverse;
|
||||
|
||||
button {
|
||||
flex: 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.popup {
|
||||
top: 10vh;
|
||||
bottom: 10vh;
|
||||
left: 1vw;
|
||||
right: 1vw;
|
||||
}
|
||||
|
||||
main article {
|
||||
margin: 2.5em .5em;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
main .article-meta > *, main .article-meta .comments, main .article-meta > .banner > * {
|
||||
margin: 0 5%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
label {
|
||||
display: block;
|
||||
margin: 2em auto .5em;
|
||||
font-size: 1.2em;
|
||||
display: block;
|
||||
margin: 2em auto .5em;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
input, textarea, select {
|
||||
transition: all 0.1s ease-in;
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
padding: 1em;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.1s ease-in;
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
padding: 1em;
|
||||
box-sizing: border-box;
|
||||
-webkit-appearance: textarea;
|
||||
|
||||
background: $form-input-background;
|
||||
color: $text-color;
|
||||
border: solid $form-input-border thin;
|
||||
background: $form-input-background;
|
||||
color: $text-color;
|
||||
border: solid $form-input-border thin;
|
||||
|
||||
font-size: 1.2em;
|
||||
font-weight: 400;
|
||||
font-size: 1.2em;
|
||||
font-weight: 400;
|
||||
|
||||
&:focus {
|
||||
border-color: $primary;
|
||||
}
|
||||
&:focus {
|
||||
border-color: $primary;
|
||||
}
|
||||
}
|
||||
form input[type="submit"] {
|
||||
margin: 2em auto;
|
||||
@@ -29,18 +29,18 @@ form input[type="submit"] {
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
resize: vertical;
|
||||
overflow-y: scroll;
|
||||
font-family: $lora;
|
||||
font-size: 1.1em;
|
||||
line-height: 1.5;
|
||||
font-family: $lora;
|
||||
font-size: 1.1em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
display: inline;
|
||||
margin: initial;
|
||||
min-width: initial;
|
||||
width: initial;
|
||||
display: inline;
|
||||
margin: initial;
|
||||
min-width: initial;
|
||||
width: initial;
|
||||
-webkit-appearance: checkbox;
|
||||
}
|
||||
|
||||
@@ -71,31 +71,31 @@ form.inline {
|
||||
}
|
||||
|
||||
.button, .button:visited, input[type="submit"], input[type="submit"].button {
|
||||
transition: all 0.1s ease-in;
|
||||
display: inline-block;
|
||||
transition: all 0.1s ease-in;
|
||||
display: inline-block;
|
||||
-webkit-appearance: none;
|
||||
|
||||
margin: 0.5em auto;
|
||||
padding: 0.75em 1em;
|
||||
margin: 0.5em auto;
|
||||
padding: 0.75em 1em;
|
||||
|
||||
background: $primary;
|
||||
color: $primary-text-color;
|
||||
background: $primary;
|
||||
color: $primary-text-color;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: transparentize($primary, 0.1);
|
||||
}
|
||||
&:hover {
|
||||
background: transparentize($primary, 0.1);
|
||||
}
|
||||
|
||||
&.destructive {
|
||||
background: $red;
|
||||
&.destructive {
|
||||
background: $red;
|
||||
|
||||
&:hover {
|
||||
background: transparentize($red, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: $gray;
|
||||
@@ -115,20 +115,20 @@ input[type="submit"] {
|
||||
form.new-post {
|
||||
max-width: 60em;
|
||||
.title {
|
||||
margin: 0 auto;
|
||||
padding: 0.75em 0;
|
||||
margin: 0 auto;
|
||||
padding: 0.75em 0;
|
||||
|
||||
background: none;
|
||||
border: none;
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
font-family: $playfair;
|
||||
font-size: 2em;
|
||||
text-align: left;
|
||||
font-family: $playfair;
|
||||
font-size: 2em;
|
||||
text-align: left;
|
||||
}
|
||||
textarea {
|
||||
min-height: 20em;
|
||||
overflow-y: scroll;
|
||||
resize: none;
|
||||
min-height: 20em;
|
||||
overflow-y: scroll;
|
||||
resize: none;
|
||||
-webkit-appearance: textarea;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,43 +6,43 @@ html {
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: $background;
|
||||
color: $text-color;
|
||||
font-family: $route159;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: $background;
|
||||
color: $text-color;
|
||||
font-family: $route159;
|
||||
|
||||
::selection {
|
||||
background: transparentize($primary, 0.7);
|
||||
}
|
||||
::-moz-selection {
|
||||
::-moz-selection {
|
||||
background: transparentize($primary, 0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a, a:visited {
|
||||
color: $primary;
|
||||
text-decoration: none;
|
||||
color: $primary;
|
||||
text-decoration: none;
|
||||
}
|
||||
a::selection {
|
||||
color: $background;
|
||||
color: $background;
|
||||
}
|
||||
a::-moz-selection {
|
||||
color: $background;
|
||||
color: $background;
|
||||
}
|
||||
small {
|
||||
margin-left: 1em;
|
||||
color: transparentize($text-color, 0.6);
|
||||
font-size: 0.75em;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
margin-left: 1em;
|
||||
color: transparentize($text-color, 0.6);
|
||||
font-size: 0.75em;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
opacity: 0.6;
|
||||
padding: 5em;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
opacity: 0.6;
|
||||
padding: 5em;
|
||||
}
|
||||
|
||||
.right {
|
||||
@@ -53,28 +53,28 @@ small {
|
||||
}
|
||||
|
||||
.spaced {
|
||||
margin: 4rem 0;
|
||||
margin: 4rem 0;
|
||||
}
|
||||
|
||||
.banner {
|
||||
background: $gray;
|
||||
padding-top: 2em;
|
||||
padding-bottom: 1em;
|
||||
margin: 3em 0px;
|
||||
background: $gray;
|
||||
padding-top: 2em;
|
||||
padding-bottom: 1em;
|
||||
margin: 3em 0px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
appearance: none;
|
||||
display: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
/* Main */
|
||||
body > main > *, .h-feed > * {
|
||||
margin: 1em $horizontal-margin;
|
||||
margin: 1em $horizontal-margin;
|
||||
}
|
||||
|
||||
body > main > .h-entry, .h-feed {
|
||||
margin: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body > main {
|
||||
@@ -98,18 +98,18 @@ main {
|
||||
margin-top: 1em;
|
||||
|
||||
&.article {
|
||||
margin: 1em auto 0.5em;
|
||||
font-family: $playfair;
|
||||
font-size: 2.5em;
|
||||
font-weight: normal;
|
||||
margin: 1em auto 0.5em;
|
||||
font-family: $playfair;
|
||||
font-size: 2.5em;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.75em;
|
||||
font-weight: 300;
|
||||
font-size: 1.75em;
|
||||
font-weight: 300;
|
||||
|
||||
&.article {
|
||||
&.article {
|
||||
font-size: 1.25em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
@@ -139,15 +139,15 @@ main {
|
||||
|
||||
/* Errors */
|
||||
p.error {
|
||||
color: $red;
|
||||
font-weight: bold;
|
||||
color: $red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* User page */
|
||||
.user h1 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
@@ -156,14 +156,14 @@ p.error {
|
||||
}
|
||||
|
||||
.badge {
|
||||
margin-right: 1em;
|
||||
padding: 0.35em 1em;
|
||||
margin-right: 1em;
|
||||
padding: 0.35em 1em;
|
||||
|
||||
background: $background;
|
||||
color: $primary;
|
||||
border: 1px solid $primary;
|
||||
background: $background;
|
||||
color: $primary;
|
||||
border: 1px solid $primary;
|
||||
|
||||
font-size: 1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.user-summary {
|
||||
@@ -172,23 +172,25 @@ p.error {
|
||||
|
||||
/* Cards */
|
||||
.cards {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 5%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 5%;
|
||||
margin: 1rem 0 5rem;
|
||||
}
|
||||
.card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
min-width: 20em;
|
||||
min-height: 20em;
|
||||
margin: 1em;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
|
||||
background: $gray;
|
||||
min-width: 20em;
|
||||
min-height: 20em;
|
||||
margin: 1em;
|
||||
box-sizing: border-box;
|
||||
|
||||
background: $gray;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -213,38 +215,54 @@ p.error {
|
||||
}
|
||||
|
||||
|
||||
> * {
|
||||
margin: 20px;
|
||||
}
|
||||
> * {
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.cover {
|
||||
.cover {
|
||||
min-height: 10em;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
margin: 0px;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0.75em 20px;
|
||||
font-family: $playfair;
|
||||
font-size: 1.75em;
|
||||
font-weight: normal;
|
||||
a {
|
||||
transition: color 0.1s ease-in;
|
||||
color: $text-color;
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
font-family: $playfair;
|
||||
font-size: 1.75em;
|
||||
font-weight: normal;
|
||||
line-height: 1.75;
|
||||
a {
|
||||
display: block;
|
||||
transition: color 0.1s ease-in;
|
||||
color: $text-color;
|
||||
|
||||
&:hover { color: $primary; }
|
||||
}
|
||||
&:hover { color: $primary; }
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
float: right;
|
||||
|
||||
.button {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
flex: 1;
|
||||
|
||||
font-family: $lora;
|
||||
font-size: 1em;
|
||||
line-height: 1.25;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
font-family: $lora;
|
||||
font-size: 1em;
|
||||
line-height: 1.25;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,15 +304,15 @@ p.error {
|
||||
|
||||
/* Stats */
|
||||
.stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin: 2em;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin: 2em;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
p {
|
||||
@@ -490,6 +508,10 @@ figure {
|
||||
|
||||
/// Small screens
|
||||
@media screen and (max-width: 600px) {
|
||||
body > main > *, .h-feed > * {
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
main .article-meta {
|
||||
> *, .comments {
|
||||
margin: 0 5%;
|
||||
@@ -535,7 +557,7 @@ figure {
|
||||
margin: 0;
|
||||
|
||||
& > * {
|
||||
max-width: 100%;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ body > header {
|
||||
|
||||
#content {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: space-between;
|
||||
align-content: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
nav#menu {
|
||||
@@ -19,44 +19,44 @@ body > header {
|
||||
|
||||
a {
|
||||
transform: skewX(15deg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.4em;
|
||||
height: 1.4em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: $gray;
|
||||
font-size: 1.33em;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.4em;
|
||||
height: 1.4em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: $gray;
|
||||
font-size: 1.33em;
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
hr {
|
||||
height: 100%;
|
||||
width: 0.2em;
|
||||
background: $primary;
|
||||
border: none;
|
||||
transform: skewX(-15deg);
|
||||
hr {
|
||||
height: 100%;
|
||||
width: 0.2em;
|
||||
background: $primary;
|
||||
border: none;
|
||||
transform: skewX(-15deg);
|
||||
}
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
align-self: stretch;
|
||||
margin: 0;
|
||||
padding: 0 2em;
|
||||
font-size: 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
align-self: stretch;
|
||||
margin: 0;
|
||||
padding: 0 2em;
|
||||
font-size: 1em;
|
||||
|
||||
i { font-size: 1.2em; }
|
||||
i { font-size: 1.2em; }
|
||||
|
||||
&.title {
|
||||
margin: 0;
|
||||
&.title {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
padding: 0.5em 1em;
|
||||
font-size: 1.75em;
|
||||
@@ -70,7 +70,7 @@ body > header {
|
||||
margin: 0;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,18 @@ body > header {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes menuOpening {
|
||||
from {
|
||||
-webkit-transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
-webkit-transform: scaleX(1);
|
||||
transform-origin: left;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
body > header {
|
||||
flex-direction: column;
|
||||
@@ -132,7 +144,7 @@ body > header {
|
||||
}
|
||||
}
|
||||
|
||||
body > header:focus-within #content, #content.show {
|
||||
body > header:focus-within #content, .show + #content {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -193,31 +205,133 @@ body > header {
|
||||
|
||||
/* Only enable label animations on large screens */
|
||||
@media screen and (min-width: 600px) {
|
||||
header nav a {
|
||||
i {
|
||||
transition: all 0.2s ease;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mobile-label {
|
||||
transition: all 0.2s ease;
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
opacity: 0;
|
||||
font-size: 0.9em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
img + .mobile-label { display: none; }
|
||||
|
||||
&:hover {
|
||||
i { margin-bottom: 0.75em; }
|
||||
.mobile-label {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 80%);
|
||||
}
|
||||
header nav a {
|
||||
i {
|
||||
transition: all 0.2s ease;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-label {
|
||||
transition: all 0.2s ease;
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateZ(0);
|
||||
opacity: 0;
|
||||
font-size: 0.9em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
img + .mobile-label { display: none; }
|
||||
|
||||
&:hover {
|
||||
i { margin-bottom: 0.75em; }
|
||||
.mobile-label {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 80%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Small screens
|
||||
@media screen and (max-width: 600px) {
|
||||
@keyframes menuOpening {
|
||||
from {
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scaleX(1);
|
||||
transform-origin: left;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes menuOpening {
|
||||
from {
|
||||
-webkit-transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
-webkit-transform: scaleX(1);
|
||||
transform-origin: left;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
body > header {
|
||||
flex-direction: column;
|
||||
|
||||
nav#menu {
|
||||
display: inline-flex;
|
||||
z-index: 21;
|
||||
}
|
||||
|
||||
#content {
|
||||
display: none;
|
||||
appearance: none;
|
||||
text-align: center;
|
||||
z-index: 20;
|
||||
}
|
||||
}
|
||||
|
||||
body > header:focus-within #content, .show + #content {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
animation: 0.2s menuOpening;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
transform: skewX(-10deg);
|
||||
top: 0;
|
||||
left: -20%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
z-index: -10;
|
||||
|
||||
background: $primary;
|
||||
}
|
||||
|
||||
> nav {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: 1rem 1.5rem;
|
||||
color: $background;
|
||||
font-size: 1.4em;
|
||||
font-weight: 300;
|
||||
|
||||
&.title { font-size: 1.8em; }
|
||||
|
||||
> *:first-child { width: 3rem; }
|
||||
> img:first-child { height: 3rem; }
|
||||
> *:last-child { margin-left: 1rem; }
|
||||
> nav hr {
|
||||
display: block;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
border: solid $background 0.1rem;
|
||||
}
|
||||
.mobile-label { display: initial; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use rsass;
|
||||
|
||||
use ructe::Ructe;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::{ffi::OsStr, fs::*, io::Write, path::*};
|
||||
@@ -50,19 +48,13 @@ fn main() {
|
||||
create_dir_all(&Path::new("static").join("media")).expect("Couldn't init media directory");
|
||||
|
||||
let cache_id = &compute_static_hash()[..8];
|
||||
println!("cargo:rerun-if-changed=target/deploy/plume-front.wasm");
|
||||
copy("target/deploy/plume-front.wasm", "static/plume-front.wasm")
|
||||
.and_then(|_| read_to_string("target/deploy/plume-front.js"))
|
||||
.and_then(|js| {
|
||||
write(
|
||||
"static/plume-front.js",
|
||||
js.replace(
|
||||
"\"plume-front.wasm\"",
|
||||
&format!("\"/static/cached/{}/plume-front.wasm\"", cache_id),
|
||||
),
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
println!("cargo:rerun-if-changed=plume-front/pkg/plume_front_bg.wasm");
|
||||
copy(
|
||||
"plume-front/pkg/plume_front_bg.wasm",
|
||||
"static/plume_front_bg.wasm",
|
||||
)
|
||||
.and_then(|_| copy("plume-front/pkg/plume_front.js", "static/plume_front.js"))
|
||||
.ok();
|
||||
|
||||
println!("cargo:rustc-env=CACHE_ID={}", cache_id)
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
"project_identifier": "plume"
|
||||
"api_key_env": CROWDIN_API_KEY
|
||||
"project_id": 352097
|
||||
"api_token_env": "CROWDIN_API_KEY"
|
||||
preserve_hierarchy: true
|
||||
files:
|
||||
- source: /po/plume/plume.pot
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "plume-api"
|
||||
version = "0.4.0"
|
||||
version = "0.6.1-dev"
|
||||
authors = ["Plume contributors"]
|
||||
edition = "2018"
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
pre-release-hook = ["cargo", "fmt"]
|
||||
pre-release-replacements = []
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "plume-cli"
|
||||
version = "0.4.0"
|
||||
version = "0.6.1-dev"
|
||||
authors = ["Plume contributors"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -11,11 +11,11 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
clap = "2.33"
|
||||
dotenv = "0.14"
|
||||
rpassword = "4.0"
|
||||
rpassword = "5.0.0"
|
||||
|
||||
[dependencies.diesel]
|
||||
features = ["r2d2", "chrono"]
|
||||
version = "*"
|
||||
version = "1.4.5"
|
||||
|
||||
[dependencies.plume-models]
|
||||
path = "../plume-models"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
pre-release-hook = ["cargo", "fmt"]
|
||||
pre-release-replacements = []
|
||||
@@ -1,5 +1,3 @@
|
||||
use dotenv;
|
||||
|
||||
use clap::App;
|
||||
use diesel::Connection;
|
||||
use plume_models::{instance::Instance, Connection as Conn, CONFIG};
|
||||
|
||||
@@ -106,7 +106,7 @@ fn refill<'a>(args: &ArgMatches<'a>, conn: &Connection, searcher: Option<Searche
|
||||
searcher.commit();
|
||||
}
|
||||
|
||||
fn unlock<'a>(args: &ArgMatches<'a>) {
|
||||
fn unlock(args: &ArgMatches) {
|
||||
let path = match args.value_of("path") {
|
||||
None => Path::new(&CONFIG.search_index),
|
||||
Some(x) => Path::new(x),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
|
||||
use plume_models::{instance::Instance, users::*, Connection};
|
||||
use rpassword;
|
||||
use std::io::{self, Write};
|
||||
|
||||
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
||||
@@ -132,7 +131,7 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||
role,
|
||||
&bio,
|
||||
email,
|
||||
User::hash_pass(&password).expect("Couldn't hash password"),
|
||||
Some(User::hash_pass(&password).expect("Couldn't hash password")),
|
||||
)
|
||||
.expect("Couldn't save new user");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "plume-common"
|
||||
version = "0.4.0"
|
||||
version = "0.6.1-dev"
|
||||
authors = ["Plume contributors"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -14,15 +14,16 @@ heck = "0.3.0"
|
||||
hex = "0.3"
|
||||
hyper = "0.12.33"
|
||||
openssl = "0.10.22"
|
||||
rocket = "0.4.0"
|
||||
reqwest = "0.9"
|
||||
rocket = "0.4.6"
|
||||
reqwest = { version = "0.9", features = ["socks"] }
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
shrinkwraprs = "0.2.1"
|
||||
syntect = "3.3"
|
||||
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"
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
@@ -30,4 +31,5 @@ version = "0.4"
|
||||
|
||||
[dependencies.pulldown-cmark]
|
||||
default-features = false
|
||||
version = "0.2.0"
|
||||
git = "https://git.joinplu.me/Plume/pulldown-cmark"
|
||||
branch = "bidi-plume"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
pre-release-hook = ["cargo", "fmt"]
|
||||
pre-release-replacements = []
|
||||
@@ -71,8 +71,8 @@ use std::fmt::Debug;
|
||||
/// # let conn = ();
|
||||
/// #
|
||||
/// let result: Result<(), ()> = Inbox::handle(&conn, activity_json)
|
||||
/// .with::<User, Announce, Message>()
|
||||
/// .with::<User, Create, Message>()
|
||||
/// .with::<User, Announce, Message>(None)
|
||||
/// .with::<User, Create, Message>(None)
|
||||
/// .done();
|
||||
/// ```
|
||||
pub enum Inbox<'a, C, E, R>
|
||||
@@ -144,7 +144,7 @@ where
|
||||
}
|
||||
|
||||
/// Registers an handler on this Inbox.
|
||||
pub fn with<A, V, M>(self) -> Inbox<'a, C, E, R>
|
||||
pub fn with<A, V, M>(self, proxy: Option<&reqwest::Proxy>) -> Inbox<'a, C, E, R>
|
||||
where
|
||||
A: AsActor<&'a C> + FromId<C, Error = E>,
|
||||
V: activitypub::Activity,
|
||||
@@ -164,11 +164,17 @@ where
|
||||
Some(x) => x,
|
||||
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(None)),
|
||||
};
|
||||
|
||||
if Self::is_spoofed_activity(&actor_id, &act) {
|
||||
return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(None));
|
||||
}
|
||||
|
||||
// Transform this actor to a model (see FromId for details about the from_id function)
|
||||
let actor = match A::from_id(
|
||||
ctx,
|
||||
&actor_id,
|
||||
serde_json::from_value(act["actor"].clone()).ok(),
|
||||
proxy,
|
||||
) {
|
||||
Ok(a) => a,
|
||||
// If the actor was not found, go to the next handler
|
||||
@@ -189,6 +195,7 @@ where
|
||||
ctx,
|
||||
&obj_id,
|
||||
serde_json::from_value(act["object"].clone()).ok(),
|
||||
proxy,
|
||||
) {
|
||||
Ok(o) => o,
|
||||
Err((json, e)) => {
|
||||
@@ -222,6 +229,26 @@ where
|
||||
Inbox::Failed(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_spoofed_activity(actor_id: &str, act: &serde_json::Value) -> bool {
|
||||
use serde_json::Value::{Array, Object, String};
|
||||
|
||||
let attributed_to = act["object"].get("attributedTo");
|
||||
if attributed_to.is_none() {
|
||||
return false;
|
||||
}
|
||||
let attributed_to = attributed_to.unwrap();
|
||||
match attributed_to {
|
||||
Array(v) => v.iter().all(|i| match i {
|
||||
String(s) => s != actor_id,
|
||||
Object(obj) => obj.get("id").map_or(true, |s| s != actor_id),
|
||||
_ => false,
|
||||
}),
|
||||
String(s) => s != actor_id,
|
||||
Object(obj) => obj.get("id").map_or(true, |s| s != actor_id),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the ActivityPub ID of a JSON value.
|
||||
@@ -267,43 +294,52 @@ pub trait FromId<C>: Sized {
|
||||
ctx: &C,
|
||||
id: &str,
|
||||
object: Option<Self::Object>,
|
||||
proxy: Option<&reqwest::Proxy>,
|
||||
) -> Result<Self, (Option<serde_json::Value>, Self::Error)> {
|
||||
match Self::from_db(ctx, id) {
|
||||
Ok(x) => Ok(x),
|
||||
_ => match object {
|
||||
Some(o) => Self::from_activity(ctx, o).map_err(|e| (None, e)),
|
||||
None => Self::from_activity(ctx, Self::deref(id)?).map_err(|e| (None, e)),
|
||||
None => Self::from_activity(ctx, Self::deref(id, proxy.cloned())?)
|
||||
.map_err(|e| (None, e)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Dereferences an ID
|
||||
fn deref(id: &str) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
|
||||
reqwest::ClientBuilder::new()
|
||||
.connect_timeout(Some(std::time::Duration::from_secs(5)))
|
||||
.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()))?,
|
||||
fn deref(
|
||||
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(", "),
|
||||
)
|
||||
.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()))
|
||||
.map_err(|_| (None, InboxError::DerefError.into()))?,
|
||||
)
|
||||
.send()
|
||||
.map_err(|_| (None, InboxError::DerefError))
|
||||
.and_then(|mut r| {
|
||||
let json: serde_json::Value = r
|
||||
.json()
|
||||
.map_err(|_| (None, InboxError::InvalidObject(None)))?;
|
||||
serde_json::from_value(json.clone())
|
||||
.map_err(|_| (Some(json), InboxError::InvalidObject(None)))
|
||||
})
|
||||
.map_err(|(json, e)| (json, e.into()))
|
||||
}
|
||||
|
||||
/// Builds a `Self` from its ActivityPub representation
|
||||
@@ -525,7 +561,7 @@ mod tests {
|
||||
fn test_inbox_basic() {
|
||||
let act = serde_json::to_value(build_create()).unwrap();
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act)
|
||||
.with::<MyActor, Create, MyObject>()
|
||||
.with::<MyActor, Create, MyObject>(None)
|
||||
.done();
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
@@ -534,10 +570,10 @@ mod tests {
|
||||
fn test_inbox_multi_handlers() {
|
||||
let act = serde_json::to_value(build_create()).unwrap();
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act)
|
||||
.with::<MyActor, Announce, MyObject>()
|
||||
.with::<MyActor, Delete, MyObject>()
|
||||
.with::<MyActor, Create, MyObject>()
|
||||
.with::<MyActor, Like, MyObject>()
|
||||
.with::<MyActor, Announce, MyObject>(None)
|
||||
.with::<MyActor, Delete, MyObject>(None)
|
||||
.with::<MyActor, Create, MyObject>(None)
|
||||
.with::<MyActor, Like, MyObject>(None)
|
||||
.done();
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
@@ -547,8 +583,8 @@ mod tests {
|
||||
let act = serde_json::to_value(build_create()).unwrap();
|
||||
// Create is not handled by this inbox
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act)
|
||||
.with::<MyActor, Announce, MyObject>()
|
||||
.with::<MyActor, Like, MyObject>()
|
||||
.with::<MyActor, Announce, MyObject>(None)
|
||||
.with::<MyActor, Like, MyObject>(None)
|
||||
.done();
|
||||
assert!(res.is_err());
|
||||
}
|
||||
@@ -596,13 +632,13 @@ mod tests {
|
||||
let act = serde_json::to_value(build_create()).unwrap();
|
||||
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
|
||||
.with::<FailingActor, Create, MyObject>()
|
||||
.with::<FailingActor, Create, MyObject>(None)
|
||||
.done();
|
||||
assert!(res.is_err());
|
||||
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
|
||||
.with::<FailingActor, Create, MyObject>()
|
||||
.with::<MyActor, Create, MyObject>()
|
||||
.with::<FailingActor, Create, MyObject>(None)
|
||||
.with::<MyActor, Create, MyObject>(None)
|
||||
.done();
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use activitypub::{Activity, Link, Object};
|
||||
use array_tool::vec::Uniq;
|
||||
use reqwest::r#async::ClientBuilder;
|
||||
use reqwest::{header::HeaderValue, r#async::ClientBuilder, Url};
|
||||
use rocket::{
|
||||
http::Status,
|
||||
request::{FromRequest, Request},
|
||||
response::{Responder, Response},
|
||||
Outcome,
|
||||
};
|
||||
use serde_json;
|
||||
use tokio::prelude::*;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use self::sign::Signable;
|
||||
|
||||
@@ -108,7 +108,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApRequest {
|
||||
.unwrap_or(Outcome::Forward(()))
|
||||
}
|
||||
}
|
||||
pub fn broadcast<S, A, T, C>(sender: &S, act: A, to: Vec<T>)
|
||||
pub fn broadcast<S, A, T, C>(sender: &S, act: A, to: Vec<T>, proxy: Option<reqwest::Proxy>)
|
||||
where
|
||||
S: sign::Signer,
|
||||
A: Activity,
|
||||
@@ -132,31 +132,54 @@ where
|
||||
|
||||
let mut rt = tokio::runtime::current_thread::Runtime::new()
|
||||
.expect("Error while initializing tokio runtime for federation");
|
||||
let client = ClientBuilder::new()
|
||||
.connect_timeout(std::time::Duration::from_secs(5))
|
||||
.build()
|
||||
.expect("Can't build client");
|
||||
for inbox in boxes {
|
||||
let body = signed.to_string();
|
||||
let mut headers = request::headers();
|
||||
let url = Url::parse(&inbox);
|
||||
if url.is_err() {
|
||||
warn!("Inbox is invalid URL: {:?}", &inbox);
|
||||
continue;
|
||||
}
|
||||
let url = url.unwrap();
|
||||
if !url.has_host() {
|
||||
warn!("Inbox doesn't have host: {:?}", &inbox);
|
||||
continue;
|
||||
};
|
||||
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;
|
||||
}
|
||||
headers.insert("Host", host_header_value.unwrap());
|
||||
headers.insert("Digest", request::Digest::digest(&body));
|
||||
rt.spawn(
|
||||
client
|
||||
.post(&inbox)
|
||||
.headers(headers.clone())
|
||||
.header(
|
||||
"Signature",
|
||||
request::signature(sender, &headers)
|
||||
.expect("activity_pub::broadcast: request signature error"),
|
||||
)
|
||||
.body(body)
|
||||
.send()
|
||||
.and_then(|r| r.into_body().concat2())
|
||||
.map(move |response| {
|
||||
println!("Successfully sent activity to inbox ({})", inbox);
|
||||
println!("Response: \"{:?}\"\n", response)
|
||||
})
|
||||
.map_err(|e| println!("Error while sending to inbox ({:?})", e)),
|
||||
if let Some(proxy) = proxy.clone() {
|
||||
ClientBuilder::new().proxy(proxy)
|
||||
} else {
|
||||
ClientBuilder::new()
|
||||
}
|
||||
.connect_timeout(std::time::Duration::from_secs(5))
|
||||
.build()
|
||||
.expect("Can't build client")
|
||||
.post(&inbox)
|
||||
.headers(headers.clone())
|
||||
.header(
|
||||
"Signature",
|
||||
request::signature(sender, &headers, ("post", url.path(), url.query()))
|
||||
.expect("activity_pub::broadcast: request signature error"),
|
||||
)
|
||||
.body(body)
|
||||
.send()
|
||||
.and_then(move |r| {
|
||||
if r.status().is_success() {
|
||||
debug!("Successfully sent activity to inbox ({})", &inbox);
|
||||
} else {
|
||||
warn!("Error while sending to inbox ({:?})", &r)
|
||||
}
|
||||
r.into_body().concat2()
|
||||
})
|
||||
.map(move |response| debug!("Response: \"{:?}\"\n", response))
|
||||
.map_err(|e| warn!("Error while sending to inbox ({:?})", e)),
|
||||
);
|
||||
}
|
||||
rt.run().unwrap();
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
use base64;
|
||||
use chrono::{offset::Utc, DateTime};
|
||||
use openssl::hash::{Hasher, MessageDigest};
|
||||
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, DATE, USER_AGENT};
|
||||
use std::ops::Deref;
|
||||
use std::time::SystemTime;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::activity_pub::sign::Signer;
|
||||
use crate::activity_pub::{ap_accept_header, AP_CONTENT_TYPE};
|
||||
|
||||
const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Error();
|
||||
|
||||
pub struct Digest(String);
|
||||
|
||||
impl Digest {
|
||||
@@ -62,16 +65,16 @@ impl Digest {
|
||||
base64::decode(&self.0[pos..]).expect("Digest::value: invalid encoding error")
|
||||
}
|
||||
|
||||
pub fn from_header(dig: &str) -> Result<Self, ()> {
|
||||
pub fn from_header(dig: &str) -> Result<Self, Error> {
|
||||
if let Some(pos) = dig.find('=') {
|
||||
let pos = pos + 1;
|
||||
if base64::decode(&dig[pos..]).is_ok() {
|
||||
Ok(Digest(dig.to_owned()))
|
||||
} else {
|
||||
Err(())
|
||||
Err(Error())
|
||||
}
|
||||
} else {
|
||||
Err(())
|
||||
Err(Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,27 +113,47 @@ pub fn headers() -> HeaderMap {
|
||||
headers
|
||||
}
|
||||
|
||||
pub fn signature<S: Signer>(signer: &S, headers: &HeaderMap) -> Result<HeaderValue, ()> {
|
||||
let signed_string = headers
|
||||
type Method<'a> = &'a str;
|
||||
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,
|
||||
headers: &HeaderMap,
|
||||
request_target: RequestTarget,
|
||||
) -> Result<HeaderValue, Error> {
|
||||
let (method, path, query) = request_target;
|
||||
let origin_form = if let Some(query) = query {
|
||||
format!("{}?{}", path, query)
|
||||
} else {
|
||||
path.to_string()
|
||||
};
|
||||
|
||||
let mut headers_vec = Vec::with_capacity(headers.len());
|
||||
for (h, v) in headers.iter() {
|
||||
let v = v.to_str();
|
||||
if v.is_err() {
|
||||
warn!("invalid header error: {:?}", v.unwrap_err());
|
||||
return Err(Error());
|
||||
}
|
||||
headers_vec.push((h.as_str().to_lowercase(), v.expect("Unreachable")));
|
||||
}
|
||||
let request_target = format!("{} {}", method.to_lowercase(), origin_form);
|
||||
headers_vec.push(("(request-target)".to_string(), &request_target));
|
||||
|
||||
let signed_string = headers_vec
|
||||
.iter()
|
||||
.map(|(h, v)| {
|
||||
format!(
|
||||
"{}: {}",
|
||||
h.as_str().to_lowercase(),
|
||||
v.to_str()
|
||||
.expect("request::signature: invalid header error")
|
||||
)
|
||||
})
|
||||
.map(|(h, v)| format!("{}: {}", h, v))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
let signed_headers = headers
|
||||
let signed_headers = headers_vec
|
||||
.iter()
|
||||
.map(|(h, _)| h.as_str())
|
||||
.map(|(h, _)| h.as_ref())
|
||||
.collect::<Vec<&str>>()
|
||||
.join(" ")
|
||||
.to_lowercase();
|
||||
.join(" ");
|
||||
|
||||
let data = signer.sign(&signed_string).map_err(|_| ())?;
|
||||
let data = signer.sign(&signed_string).map_err(|_| Error())?;
|
||||
let sign = base64::encode(&data);
|
||||
|
||||
HeaderValue::from_str(&format!(
|
||||
@@ -138,5 +161,63 @@ pub fn signature<S: Signer>(signer: &S, headers: &HeaderMap) -> Result<HeaderVal
|
||||
key_id = signer.get_key_id(),
|
||||
signed_headers = signed_headers,
|
||||
signature = sign
|
||||
)).map_err(|_| ())
|
||||
)).map_err(|_| Error())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{signature, Error};
|
||||
use crate::activity_pub::sign::{gen_keypair, Signer};
|
||||
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
|
||||
use reqwest::header::HeaderMap;
|
||||
|
||||
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 {
|
||||
type Error = Error;
|
||||
|
||||
fn get_key_id(&self) -> String {
|
||||
"mysigner".into()
|
||||
}
|
||||
|
||||
fn sign(&self, to_sign: &str) -> Result<Vec<u8>, Self::Error> {
|
||||
let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
|
||||
.unwrap();
|
||||
let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
|
||||
signer.update(to_sign.as_bytes()).unwrap();
|
||||
signer.sign_to_vec().map_err(|_| Error())
|
||||
}
|
||||
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool, Self::Error> {
|
||||
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
|
||||
.unwrap();
|
||||
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
|
||||
verifier.update(data.as_bytes()).unwrap();
|
||||
verifier.verify(&signature).map_err(|_| Error())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_signature_request_target() {
|
||||
let signer = MySigner::new();
|
||||
let headers = HeaderMap::new();
|
||||
let result = signature(&signer, &headers, ("post", "/inbox", None)).unwrap();
|
||||
let fields: Vec<&str> = result.to_str().unwrap().split(",").collect();
|
||||
assert_eq!(r#"headers="(request-target)""#, fields[2]);
|
||||
let sign = &fields[3][11..(fields[3].len() - 1)];
|
||||
assert!(signer.verify("post /inbox", sign.as_bytes()).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
use super::request;
|
||||
use base64;
|
||||
use chrono::{naive::NaiveDateTime, DateTime, Duration, Utc};
|
||||
use hex;
|
||||
use openssl::{pkey::PKey, rsa::Rsa, sha::sha256};
|
||||
use rocket::http::HeaderMap;
|
||||
use serde_json;
|
||||
|
||||
/// Returns (public key, private key)
|
||||
pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
|
||||
@@ -20,6 +17,9 @@ pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Error();
|
||||
|
||||
pub trait Signer {
|
||||
type Error;
|
||||
|
||||
@@ -32,7 +32,7 @@ pub trait Signer {
|
||||
}
|
||||
|
||||
pub trait Signable {
|
||||
fn sign<T>(&mut self, creator: &T) -> Result<&mut Self, ()>
|
||||
fn sign<T>(&mut self, creator: &T) -> Result<&mut Self, Error>
|
||||
where
|
||||
T: Signer;
|
||||
fn verify<T>(self, creator: &T) -> bool
|
||||
@@ -46,7 +46,7 @@ pub trait Signable {
|
||||
}
|
||||
|
||||
impl Signable for serde_json::Value {
|
||||
fn sign<T: Signer>(&mut self, creator: &T) -> Result<&mut serde_json::Value, ()> {
|
||||
fn sign<T: Signer>(&mut self, creator: &T) -> Result<&mut serde_json::Value, Error> {
|
||||
let creation_date = Utc::now().to_rfc3339();
|
||||
let mut options = json!({
|
||||
"type": "RsaSignature2017",
|
||||
@@ -64,7 +64,7 @@ impl Signable for serde_json::Value {
|
||||
let document_hash = Self::hash(&self.to_string());
|
||||
let to_be_signed = options_hash + &document_hash;
|
||||
|
||||
let signature = base64::encode(&creator.sign(&to_be_signed).map_err(|_| ())?);
|
||||
let signature = base64::encode(&creator.sign(&to_be_signed).map_err(|_| Error())?);
|
||||
|
||||
options["signatureValue"] = serde_json::Value::String(signature);
|
||||
self["signature"] = options;
|
||||
|
||||
Regular → Executable
-3
@@ -2,9 +2,6 @@
|
||||
|
||||
#[macro_use]
|
||||
extern crate activitystreams_derive;
|
||||
use activitystreams_traits;
|
||||
|
||||
use serde;
|
||||
#[macro_use]
|
||||
extern crate shrinkwraprs;
|
||||
#[macro_use]
|
||||
|
||||
+60
-40
@@ -1,14 +1,13 @@
|
||||
use heck::CamelCase;
|
||||
use openssl::rand::rand_bytes;
|
||||
use pulldown_cmark::{html, Event, Options, Parser, Tag};
|
||||
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 std::borrow::Cow;
|
||||
use std::collections::HashSet;
|
||||
use syntect::html::ClassedHTMLGenerator;
|
||||
use syntect::html::{ClassStyle, ClassedHTMLGenerator};
|
||||
use syntect::parsing::SyntaxSet;
|
||||
|
||||
/// Generates an hexadecimal representation of 32 bytes of random data
|
||||
@@ -51,46 +50,61 @@ enum State {
|
||||
|
||||
fn to_inline(tag: Tag<'_>) -> Tag<'_> {
|
||||
match tag {
|
||||
Tag::Header(_) | Tag::Table(_) | Tag::TableHead | Tag::TableRow | Tag::TableCell => {
|
||||
Tag::Heading(_) | Tag::Table(_) | Tag::TableHead | Tag::TableRow | Tag::TableCell => {
|
||||
Tag::Paragraph
|
||||
}
|
||||
Tag::Image(url, title) => Tag::Link(url, title),
|
||||
Tag::Image(typ, url, title) => Tag::Link(typ, url, title),
|
||||
t => t,
|
||||
}
|
||||
}
|
||||
struct HighlighterContext {
|
||||
content: Vec<String>,
|
||||
}
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn highlight_code<'a>(
|
||||
context: &mut Option<HighlighterContext>,
|
||||
evt: Event<'a>,
|
||||
) -> Option<Vec<Event<'a>>> {
|
||||
match evt {
|
||||
Event::Start(Tag::CodeBlock(lang)) => {
|
||||
if lang.is_empty() {
|
||||
Some(vec![Event::Start(Tag::CodeBlock(lang))])
|
||||
} else {
|
||||
*context = Some(HighlighterContext { content: vec![] });
|
||||
Some(vec![Event::Start(Tag::CodeBlock(lang))])
|
||||
Event::Start(Tag::CodeBlock(kind)) => {
|
||||
match &kind {
|
||||
CodeBlockKind::Fenced(lang) if !lang.is_empty() => {
|
||||
*context = Some(HighlighterContext { content: vec![] });
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Some(vec![Event::Start(Tag::CodeBlock(kind))])
|
||||
}
|
||||
Event::End(Tag::CodeBlock(x)) => {
|
||||
Event::End(Tag::CodeBlock(kind)) => {
|
||||
let mut result = vec![];
|
||||
if let Some(ctx) = context.take() {
|
||||
let lang = if let CodeBlockKind::Fenced(lang) = &kind {
|
||||
if lang.is_empty() {
|
||||
unreachable!();
|
||||
} else {
|
||||
lang
|
||||
}
|
||||
} else {
|
||||
unreachable!();
|
||||
};
|
||||
let syntax_set = SyntaxSet::load_defaults_newlines();
|
||||
let syntax = syntax_set.find_syntax_by_token(&x).unwrap_or_else(|| {
|
||||
let syntax = syntax_set.find_syntax_by_token(&lang).unwrap_or_else(|| {
|
||||
syntax_set
|
||||
.find_syntax_by_name(&x)
|
||||
.find_syntax_by_name(&lang)
|
||||
.unwrap_or_else(|| syntax_set.find_syntax_plain_text())
|
||||
});
|
||||
let mut html = ClassedHTMLGenerator::new(&syntax, &syntax_set);
|
||||
let mut html = ClassedHTMLGenerator::new_with_class_style(
|
||||
&syntax,
|
||||
&syntax_set,
|
||||
ClassStyle::Spaced,
|
||||
);
|
||||
for line in ctx.content {
|
||||
html.parse_html_for_line(&line);
|
||||
html.parse_html_for_line_which_includes_newline(&line);
|
||||
}
|
||||
let q = html.finalize();
|
||||
result.push(Event::Html(q.into()));
|
||||
}
|
||||
result.push(Event::End(Tag::CodeBlock(x)));
|
||||
result.push(Event::End(Tag::CodeBlock(kind)));
|
||||
*context = None;
|
||||
Some(result)
|
||||
}
|
||||
@@ -106,6 +120,7 @@ fn highlight_code<'a>(
|
||||
_ => Some(vec![evt]),
|
||||
}
|
||||
}
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn flatten_text<'a>(state: &mut Option<String>, evt: Event<'a>) -> Option<Vec<Event<'a>>> {
|
||||
let (s, res) = match evt {
|
||||
Event::Text(txt) => match state.take() {
|
||||
@@ -113,10 +128,10 @@ fn flatten_text<'a>(state: &mut Option<String>, evt: Event<'a>) -> Option<Vec<Ev
|
||||
prev_txt.push_str(&txt);
|
||||
(Some(prev_txt), vec![])
|
||||
}
|
||||
None => (Some(txt.into_owned()), vec![]),
|
||||
None => (Some(txt.into_string()), vec![]),
|
||||
},
|
||||
e => match state.take() {
|
||||
Some(prev) => (None, vec![Event::Text(Cow::Owned(prev)), e]),
|
||||
Some(prev) => (None, vec![Event::Text(CowStr::Boxed(prev.into())), e]),
|
||||
None => (None, vec![e]),
|
||||
},
|
||||
};
|
||||
@@ -124,6 +139,7 @@ fn flatten_text<'a>(state: &mut Option<String>, evt: Event<'a>) -> Option<Vec<Ev
|
||||
Some(res)
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn inline_tags<'a>(
|
||||
(state, inline): &mut (Vec<Tag<'a>>, bool),
|
||||
evt: Event<'a>,
|
||||
@@ -156,42 +172,45 @@ fn process_image<'a, 'b>(
|
||||
) -> Event<'a> {
|
||||
if let Some(ref processor) = *processor {
|
||||
match evt {
|
||||
Event::Start(Tag::Image(id, title)) => {
|
||||
Event::Start(Tag::Image(typ, id, title)) => {
|
||||
if let Some((url, cw)) = id.parse::<i32>().ok().and_then(processor.as_ref()) {
|
||||
if let (Some(cw), false) = (cw, inline) {
|
||||
// there is a cw, and where are not inline
|
||||
Event::Html(Cow::Owned(format!(
|
||||
r#"<label for="postcontent-cw-{id}">
|
||||
Event::Html(CowStr::Boxed(
|
||||
format!(
|
||||
r#"<label for="postcontent-cw-{id}">
|
||||
<input type="checkbox" id="postcontent-cw-{id}" checked="checked" class="cw-checkbox">
|
||||
<span class="cw-container">
|
||||
<span class="cw-text">
|
||||
{cw}
|
||||
</span>
|
||||
<img src="{url}" alt=""#,
|
||||
id = random_hex(),
|
||||
cw = cw,
|
||||
url = url
|
||||
)))
|
||||
id = random_hex(),
|
||||
cw = cw,
|
||||
url = url
|
||||
)
|
||||
.into(),
|
||||
))
|
||||
} else {
|
||||
Event::Start(Tag::Image(Cow::Owned(url), title))
|
||||
Event::Start(Tag::Image(typ, CowStr::Boxed(url.into()), title))
|
||||
}
|
||||
} else {
|
||||
Event::Start(Tag::Image(id, title))
|
||||
Event::Start(Tag::Image(typ, id, title))
|
||||
}
|
||||
}
|
||||
Event::End(Tag::Image(id, title)) => {
|
||||
Event::End(Tag::Image(typ, id, title)) => {
|
||||
if let Some((url, cw)) = id.parse::<i32>().ok().and_then(processor.as_ref()) {
|
||||
if inline || cw.is_none() {
|
||||
Event::End(Tag::Image(Cow::Owned(url), title))
|
||||
Event::End(Tag::Image(typ, CowStr::Boxed(url.into()), title))
|
||||
} else {
|
||||
Event::Html(Cow::Borrowed(
|
||||
Event::Html(CowStr::Borrowed(
|
||||
r#""/>
|
||||
</span>
|
||||
</label>"#,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Event::End(Tag::Image(id, title))
|
||||
Event::End(Tag::Image(typ, id, title))
|
||||
}
|
||||
}
|
||||
e => e,
|
||||
@@ -231,19 +250,19 @@ pub fn md_to_html<'a>(
|
||||
// Ignore headings, images, and tables if inline = true
|
||||
.scan((vec![], inline), inline_tags)
|
||||
.scan(&mut DocumentContext::default(), |ctx, evt| match evt {
|
||||
Event::Start(Tag::CodeBlock(_)) | Event::Start(Tag::Code) => {
|
||||
Event::Start(Tag::CodeBlock(_)) => {
|
||||
ctx.in_code = true;
|
||||
Some((vec![evt], vec![], vec![]))
|
||||
}
|
||||
Event::End(Tag::CodeBlock(_)) | Event::End(Tag::Code) => {
|
||||
Event::End(Tag::CodeBlock(_)) => {
|
||||
ctx.in_code = false;
|
||||
Some((vec![evt], vec![], vec![]))
|
||||
}
|
||||
Event::Start(Tag::Link(_, _)) => {
|
||||
Event::Start(Tag::Link(_, _, _)) => {
|
||||
ctx.in_link = true;
|
||||
Some((vec![evt], vec![], vec![]))
|
||||
}
|
||||
Event::End(Tag::Link(_, _)) => {
|
||||
Event::End(Tag::Link(_, _, _)) => {
|
||||
ctx.in_link = false;
|
||||
Some((vec![evt], vec![], vec![]))
|
||||
}
|
||||
@@ -264,6 +283,7 @@ pub fn md_to_html<'a>(
|
||||
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(),
|
||||
);
|
||||
@@ -294,8 +314,8 @@ pub fn md_to_html<'a>(
|
||||
}
|
||||
let hashtag = text_acc;
|
||||
let link = Tag::Link(
|
||||
format!("{}tag/{}", base_url, &hashtag.to_camel_case())
|
||||
.into(),
|
||||
LinkType::Inline,
|
||||
format!("{}tag/{}", base_url, &hashtag).into(),
|
||||
hashtag.to_owned().into(),
|
||||
);
|
||||
|
||||
@@ -460,11 +480,11 @@ mod tests {
|
||||
fn test_inline() {
|
||||
assert_eq!(
|
||||
md_to_html("# Hello", None, false, None).0,
|
||||
String::from("<h1>Hello</h1>\n")
|
||||
String::from("<h1 dir=\"auto\">Hello</h1>\n")
|
||||
);
|
||||
assert_eq!(
|
||||
md_to_html("# Hello", None, true, None).0,
|
||||
String::from("<p>Hello</p>\n")
|
||||
String::from("<p dir=\"auto\">Hello</p>\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+40
-3
@@ -1,15 +1,52 @@
|
||||
[package]
|
||||
name = "plume-front"
|
||||
version = "0.4.0"
|
||||
version = "0.6.1-dev"
|
||||
authors = ["Plume contributors"]
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
stdweb = "=0.4.18"
|
||||
stdweb-internal-runtime = "=0.1.4"
|
||||
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" }
|
||||
lazy_static = "1.3"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
wasm-bindgen = "0.2.70"
|
||||
js-sys = "0.3.47"
|
||||
serde_derive = "1.0.123"
|
||||
console_error_panic_hook = "0.1.6"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.47"
|
||||
features = [
|
||||
'console',
|
||||
'ClipboardEvent',
|
||||
'CssStyleDeclaration',
|
||||
'DataTransfer',
|
||||
'Document',
|
||||
'DomStringMap',
|
||||
'DomTokenList',
|
||||
'Element',
|
||||
'EventTarget',
|
||||
'FocusEvent',
|
||||
'History',
|
||||
'HtmlAnchorElement',
|
||||
'HtmlDocument',
|
||||
'HtmlFormElement',
|
||||
'HtmlInputElement',
|
||||
'HtmlSelectElement',
|
||||
'HtmlTextAreaElement',
|
||||
'KeyboardEvent',
|
||||
'Storage',
|
||||
'Location',
|
||||
'MouseEvent',
|
||||
'Navigator',
|
||||
'Node',
|
||||
'NodeList',
|
||||
'Text',
|
||||
'TouchEvent',
|
||||
'Window'
|
||||
]
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
pre-release-hook = ["cargo", "fmt"]
|
||||
pre-release-replacements = []
|
||||
+430
-264
@@ -1,10 +1,12 @@
|
||||
use crate::CATALOG;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use std::sync::Mutex;
|
||||
use stdweb::{
|
||||
unstable::{TryFrom, TryInto},
|
||||
web::{event::*, html_element::*, *},
|
||||
use crate::{document, CATALOG};
|
||||
use js_sys::{encode_uri_component, Date, RegExp};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::{convert::TryInto, sync::Mutex};
|
||||
use wasm_bindgen::{prelude::*, JsCast, JsValue};
|
||||
use web_sys::{
|
||||
console, window, ClipboardEvent, Element, Event, FocusEvent, HtmlAnchorElement, HtmlDocument,
|
||||
HtmlElement, HtmlFormElement, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement,
|
||||
KeyboardEvent, MouseEvent, Node,
|
||||
};
|
||||
|
||||
macro_rules! mv {
|
||||
@@ -18,30 +20,29 @@ macro_rules! mv {
|
||||
|
||||
fn get_elt_value(id: &'static str) -> String {
|
||||
let elt = document().get_element_by_id(id).unwrap();
|
||||
let inp: Result<InputElement, _> = elt.clone().try_into();
|
||||
let textarea: Result<TextAreaElement, _> = elt.clone().try_into();
|
||||
let select: Result<SelectElement, _> = elt.try_into();
|
||||
inp.map(|i| i.raw_value()).unwrap_or_else(|_| {
|
||||
let inp: Option<&HtmlInputElement> = elt.dyn_ref();
|
||||
let textarea: Option<&HtmlTextAreaElement> = elt.dyn_ref();
|
||||
let select: Option<&HtmlSelectElement> = elt.dyn_ref();
|
||||
inp.map(|i| i.value()).unwrap_or_else(|| {
|
||||
textarea
|
||||
.map(|t| t.value())
|
||||
.unwrap_or_else(|_| select.unwrap().raw_value())
|
||||
.unwrap_or_else(|| select.unwrap().value())
|
||||
})
|
||||
}
|
||||
|
||||
fn set_value<S: AsRef<str>>(id: &'static str, val: S) {
|
||||
let elt = document().get_element_by_id(id).unwrap();
|
||||
let inp: Result<InputElement, _> = elt.clone().try_into();
|
||||
let textarea: Result<TextAreaElement, _> = elt.clone().try_into();
|
||||
let select: Result<SelectElement, _> = elt.try_into();
|
||||
inp.map(|i| i.set_raw_value(val.as_ref()))
|
||||
.unwrap_or_else(|_| {
|
||||
textarea
|
||||
.map(|t| t.set_value(val.as_ref()))
|
||||
.unwrap_or_else(|_| select.unwrap().set_raw_value(val.as_ref()))
|
||||
})
|
||||
let inp: Option<&HtmlInputElement> = elt.dyn_ref();
|
||||
let textarea: Option<&HtmlTextAreaElement> = elt.dyn_ref();
|
||||
let select: Option<&HtmlSelectElement> = elt.dyn_ref();
|
||||
inp.map(|i| i.set_value(val.as_ref())).unwrap_or_else(|| {
|
||||
textarea
|
||||
.map(|t| t.set_value(val.as_ref()))
|
||||
.unwrap_or_else(|| select.unwrap().set_value(val.as_ref()))
|
||||
})
|
||||
}
|
||||
|
||||
fn no_return(evt: KeyDownEvent) {
|
||||
fn no_return(evt: KeyboardEvent) {
|
||||
if evt.key() == "Enter" {
|
||||
evt.prevent_default();
|
||||
}
|
||||
@@ -51,7 +52,6 @@ fn no_return(evt: KeyDownEvent) {
|
||||
pub enum EditorError {
|
||||
NoneError,
|
||||
DOMError,
|
||||
TypeError,
|
||||
}
|
||||
|
||||
impl From<std::option::NoneError> for EditorError {
|
||||
@@ -59,22 +59,7 @@ impl From<std::option::NoneError> for EditorError {
|
||||
EditorError::NoneError
|
||||
}
|
||||
}
|
||||
impl From<stdweb::web::error::InvalidCharacterError> for EditorError {
|
||||
fn from(_: stdweb::web::error::InvalidCharacterError) -> Self {
|
||||
EditorError::DOMError
|
||||
}
|
||||
}
|
||||
impl From<stdweb::private::TODO> for EditorError {
|
||||
fn from(_: stdweb::private::TODO) -> Self {
|
||||
EditorError::DOMError
|
||||
}
|
||||
}
|
||||
impl From<stdweb::private::ConversionError> for EditorError {
|
||||
fn from(_: stdweb::private::ConversionError) -> Self {
|
||||
EditorError::TypeError
|
||||
}
|
||||
}
|
||||
const AUTOSAVE_DEBOUNCE_TIME: u32 = 5000;
|
||||
const AUTOSAVE_DEBOUNCE_TIME: i32 = 5000;
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct AutosaveInformation {
|
||||
contents: String,
|
||||
@@ -85,10 +70,16 @@ struct AutosaveInformation {
|
||||
tags: String,
|
||||
title: String,
|
||||
}
|
||||
js_serializable!(AutosaveInformation);
|
||||
fn is_basic_editor() -> bool {
|
||||
if let Some(basic_editor) = window().local_storage().get("basic-editor") {
|
||||
basic_editor == "true"
|
||||
if let Some(basic_editor) = window()
|
||||
.unwrap()
|
||||
.local_storage()
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.get("basic-editor")
|
||||
.unwrap()
|
||||
{
|
||||
&basic_editor == "true"
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -97,65 +88,58 @@ fn get_title() -> String {
|
||||
if is_basic_editor() {
|
||||
get_elt_value("title")
|
||||
} else {
|
||||
let title_field = HtmlElement::try_from(
|
||||
document()
|
||||
.query_selector("#plume-editor > h1")
|
||||
.ok()
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
)
|
||||
.ok()
|
||||
.unwrap();
|
||||
title_field.inner_text()
|
||||
document()
|
||||
.query_selector("#plume-editor > h1")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_ref::<HtmlElement>()
|
||||
.unwrap()
|
||||
.inner_text()
|
||||
}
|
||||
}
|
||||
fn get_autosave_id() -> String {
|
||||
format!(
|
||||
"editor_contents={}",
|
||||
window().location().unwrap().pathname().unwrap()
|
||||
window().unwrap().location().pathname().unwrap()
|
||||
)
|
||||
}
|
||||
fn get_editor_contents() -> String {
|
||||
if is_basic_editor() {
|
||||
get_elt_value("editor-content")
|
||||
} else {
|
||||
let editor =
|
||||
HtmlElement::try_from(document().query_selector("article").ok().unwrap().unwrap())
|
||||
.ok()
|
||||
.unwrap();
|
||||
editor.child_nodes().iter().fold(String::new(), |md, ch| {
|
||||
let editor = document().query_selector("article").unwrap().unwrap();
|
||||
let child_nodes = editor.child_nodes();
|
||||
let mut md = String::new();
|
||||
for i in 0..child_nodes.length() {
|
||||
let ch = child_nodes.get(i).unwrap();
|
||||
let to_append = match ch.node_type() {
|
||||
NodeType::Element => {
|
||||
if js! { return @{&ch}.tagName; } == "DIV" {
|
||||
(js! { return @{&ch}.innerHTML; })
|
||||
.try_into()
|
||||
.unwrap_or_default()
|
||||
Node::ELEMENT_NODE => {
|
||||
let elt = ch.dyn_ref::<Element>().unwrap();
|
||||
if elt.tag_name() == "DIV" {
|
||||
elt.inner_html()
|
||||
} else {
|
||||
(js! { return @{&ch}.outerHTML; })
|
||||
.try_into()
|
||||
.unwrap_or_default()
|
||||
elt.outer_html()
|
||||
}
|
||||
}
|
||||
NodeType::Text => ch.node_value().unwrap_or_default(),
|
||||
Node::TEXT_NODE => ch.node_value().unwrap_or_default(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
format!("{}\n\n{}", md, to_append)
|
||||
})
|
||||
md = format!("{}\n\n{}", md, to_append);
|
||||
}
|
||||
md
|
||||
}
|
||||
}
|
||||
fn get_subtitle() -> String {
|
||||
if is_basic_editor() {
|
||||
get_elt_value("subtitle")
|
||||
} else {
|
||||
let subtitle_element = HtmlElement::try_from(
|
||||
document()
|
||||
.query_selector("#plume-editor > h2")
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
)
|
||||
.ok()
|
||||
.unwrap();
|
||||
subtitle_element.inner_text()
|
||||
document()
|
||||
.query_selector("#plume-editor > h2")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_ref::<HtmlElement>()
|
||||
.unwrap()
|
||||
.inner_text()
|
||||
}
|
||||
}
|
||||
fn autosave() {
|
||||
@@ -170,27 +154,31 @@ fn autosave() {
|
||||
};
|
||||
let id = get_autosave_id();
|
||||
match window()
|
||||
.unwrap()
|
||||
.local_storage()
|
||||
.insert(&id, &serde_json::to_string(&info).unwrap())
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.set(&id, &serde_json::to_string(&info).unwrap())
|
||||
{
|
||||
Ok(_) => {}
|
||||
_ => console!(log, "Autosave failed D:"),
|
||||
_ => console::log_1(&"Autosave failed D:".into()),
|
||||
}
|
||||
}
|
||||
//This is only necessary until we go to stdweb 4.20 at least
|
||||
fn confirm(message: &str) -> bool {
|
||||
let result: bool = js! {return confirm(@{message});} == true;
|
||||
result
|
||||
}
|
||||
fn load_autosave() {
|
||||
if let Some(autosave_str) = window().local_storage().get(&get_autosave_id()) {
|
||||
if let Ok(Some(autosave_str)) = window()
|
||||
.unwrap()
|
||||
.local_storage()
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.get(&get_autosave_id())
|
||||
{
|
||||
let autosave_info: AutosaveInformation = serde_json::from_str(&autosave_str).ok().unwrap();
|
||||
let message = i18n!(
|
||||
CATALOG,
|
||||
"Do you want to load the local autosave last edited at {}?";
|
||||
Date::from_time(autosave_info.last_saved).to_date_string()
|
||||
Date::new(&JsValue::from_f64(autosave_info.last_saved)).to_date_string().as_string().unwrap()
|
||||
);
|
||||
if confirm(&message) {
|
||||
if let Ok(true) = window().unwrap().confirm_with_message(&message) {
|
||||
set_value("editor-content", &autosave_info.contents);
|
||||
set_value("title", &autosave_info.title);
|
||||
set_value("subtitle", &autosave_info.subtitle);
|
||||
@@ -203,18 +191,33 @@ fn load_autosave() {
|
||||
}
|
||||
}
|
||||
fn clear_autosave() {
|
||||
window().local_storage().remove(&get_autosave_id());
|
||||
console!(log, &format!("Saved to {}", &get_autosave_id()));
|
||||
window()
|
||||
.unwrap()
|
||||
.local_storage()
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.remove_item(&get_autosave_id())
|
||||
.unwrap();
|
||||
console::log_1(&&format!("Saved to {}", &get_autosave_id()).into());
|
||||
}
|
||||
type TimeoutHandle = i32;
|
||||
lazy_static! {
|
||||
static ref AUTOSAVE_TIMEOUT: Mutex<Option<TimeoutHandle>> = Mutex::new(None);
|
||||
}
|
||||
fn autosave_debounce() {
|
||||
let window = window().unwrap();
|
||||
let timeout = &mut AUTOSAVE_TIMEOUT.lock().unwrap();
|
||||
if let Some(timeout) = timeout.take() {
|
||||
timeout.clear();
|
||||
window.clear_timeout_with_handle(timeout);
|
||||
}
|
||||
**timeout = Some(window().set_clearable_timeout(autosave, AUTOSAVE_DEBOUNCE_TIME));
|
||||
let callback = Closure::once(autosave);
|
||||
**timeout = window
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
callback.as_ref().unchecked_ref(),
|
||||
AUTOSAVE_DEBOUNCE_TIME,
|
||||
)
|
||||
.ok();
|
||||
callback.forget();
|
||||
}
|
||||
fn init_widget(
|
||||
parent: &Element,
|
||||
@@ -223,19 +226,33 @@ fn init_widget(
|
||||
content: String,
|
||||
disable_return: bool,
|
||||
) -> Result<HtmlElement, EditorError> {
|
||||
let widget = placeholder(make_editable(tag).try_into()?, &placeholder_text);
|
||||
let widget = placeholder(
|
||||
make_editable(tag).dyn_into::<HtmlElement>().unwrap(),
|
||||
&placeholder_text,
|
||||
);
|
||||
if !content.is_empty() {
|
||||
widget.dataset().insert("edited", "true")?;
|
||||
widget
|
||||
.dataset()
|
||||
.set("edited", "true")
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
}
|
||||
widget.append_child(&document().create_text_node(&content));
|
||||
widget
|
||||
.append_child(&document().create_text_node(&content))
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
if disable_return {
|
||||
widget.add_event_listener(no_return);
|
||||
let callback = Closure::wrap(Box::new(no_return) as Box<dyn FnMut(KeyboardEvent)>);
|
||||
widget
|
||||
.add_event_listener_with_callback("keydown", callback.as_ref().unchecked_ref())
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
callback.forget();
|
||||
}
|
||||
|
||||
parent.append_child(&widget);
|
||||
parent
|
||||
.append_child(&widget)
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
// We need to do that to make sure the placeholder is correctly rendered
|
||||
widget.focus();
|
||||
widget.blur();
|
||||
widget.focus().map_err(|_| EditorError::DOMError)?;
|
||||
widget.blur().map_err(|_| EditorError::DOMError)?;
|
||||
|
||||
filter_paste(&widget);
|
||||
|
||||
@@ -244,42 +261,88 @@ fn init_widget(
|
||||
|
||||
fn filter_paste(elt: &HtmlElement) {
|
||||
// Only insert text when pasting something
|
||||
js! {
|
||||
@{&elt}.addEventListener("paste", function (evt) {
|
||||
evt.preventDefault();
|
||||
document.execCommand("insertText", false, evt.clipboardData.getData("text"));
|
||||
});
|
||||
};
|
||||
let insert_text = Closure::wrap(Box::new(|evt: ClipboardEvent| {
|
||||
evt.prevent_default();
|
||||
if let Some(data) = evt.clipboard_data() {
|
||||
if let Ok(data) = data.get_data("text") {
|
||||
document()
|
||||
.dyn_ref::<HtmlDocument>()
|
||||
.unwrap()
|
||||
.exec_command_with_show_ui_and_value("insertText", false, &data)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}) as Box<dyn FnMut(ClipboardEvent)>);
|
||||
elt.add_event_listener_with_callback("paste", insert_text.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
insert_text.forget();
|
||||
}
|
||||
|
||||
pub fn init() -> Result<(), EditorError> {
|
||||
if let Some(ed) = document().get_element_by_id("plume-fallback-editor") {
|
||||
load_autosave();
|
||||
ed.add_event_listener(|_: SubmitEvent| clear_autosave());
|
||||
let callback = Closure::wrap(Box::new(|_| clear_autosave()) as Box<dyn FnMut(Event)>);
|
||||
ed.add_event_listener_with_callback("submit", callback.as_ref().unchecked_ref())
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
callback.forget();
|
||||
}
|
||||
// Check if the user wants to use the basic editor
|
||||
if window()
|
||||
.unwrap()
|
||||
.local_storage()
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.get("basic-editor")
|
||||
.map(|x| x == "true")
|
||||
.map(|x| x.is_some() && x.unwrap() == "true")
|
||||
.unwrap_or(true)
|
||||
{
|
||||
if let Some(editor) = document().get_element_by_id("plume-fallback-editor") {
|
||||
if let Ok(Some(title_label)) = document().query_selector("label[for=title]") {
|
||||
let editor_button = document().create_element("a")?;
|
||||
js! { @{&editor_button}.href = "#"; }
|
||||
editor_button.add_event_listener(|_: ClickEvent| {
|
||||
window().local_storage().remove("basic-editor");
|
||||
window().history().go(0).ok(); // refresh
|
||||
});
|
||||
editor_button.append_child(
|
||||
&document().create_text_node(&i18n!(CATALOG, "Open the rich text editor")),
|
||||
let editor_button = document()
|
||||
.create_element("a")
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
editor_button
|
||||
.dyn_ref::<HtmlAnchorElement>()
|
||||
.unwrap()
|
||||
.set_href("#");
|
||||
let disable_basic_editor = Closure::wrap(Box::new(|_| {
|
||||
let window = window().unwrap();
|
||||
if window
|
||||
.local_storage()
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.set("basic-editor", "false")
|
||||
.is_err()
|
||||
{
|
||||
console::log_1(&"Failed to write into local storage".into());
|
||||
}
|
||||
window.history().unwrap().go_with_delta(0).ok(); // refresh
|
||||
})
|
||||
as Box<dyn FnMut(MouseEvent)>);
|
||||
editor_button
|
||||
.add_event_listener_with_callback(
|
||||
"click",
|
||||
disable_basic_editor.as_ref().unchecked_ref(),
|
||||
)
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
disable_basic_editor.forget();
|
||||
editor_button
|
||||
.append_child(
|
||||
&document().create_text_node(&i18n!(CATALOG, "Open the rich text editor")),
|
||||
)
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
editor
|
||||
.insert_before(&editor_button, Some(&title_label))
|
||||
.ok();
|
||||
let callback = Closure::wrap(
|
||||
Box::new(|_| autosave_debounce()) as Box<dyn FnMut(KeyboardEvent)>
|
||||
);
|
||||
editor.insert_before(&editor_button, &title_label).ok();
|
||||
document()
|
||||
.get_element_by_id("editor-content")
|
||||
.unwrap()
|
||||
.add_event_listener(|_: KeyDownEvent| autosave_debounce());
|
||||
.add_event_listener_with_callback("keydown", callback.as_ref().unchecked_ref())
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
callback.forget();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,14 +355,30 @@ pub fn init() -> Result<(), EditorError> {
|
||||
fn init_editor() -> Result<(), EditorError> {
|
||||
if let Some(ed) = document().get_element_by_id("plume-editor") {
|
||||
// Show the editor
|
||||
js! { @{&ed}.style.display = "block"; };
|
||||
ed.dyn_ref::<HtmlElement>()
|
||||
.unwrap()
|
||||
.style()
|
||||
.set_property("display", "block")
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
// And hide the HTML-only fallback
|
||||
let old_ed = document().get_element_by_id("plume-fallback-editor")?;
|
||||
let old_ed = document().get_element_by_id("plume-fallback-editor");
|
||||
if old_ed.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let old_ed = old_ed.unwrap();
|
||||
let old_title = document().get_element_by_id("plume-editor-title")?;
|
||||
js! {
|
||||
@{&old_ed}.style.display = "none";
|
||||
@{&old_title}.style.display = "none";
|
||||
};
|
||||
old_ed
|
||||
.dyn_ref::<HtmlElement>()
|
||||
.unwrap()
|
||||
.style()
|
||||
.set_property("display", "none")
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
old_title
|
||||
.dyn_ref::<HtmlElement>()
|
||||
.unwrap()
|
||||
.style()
|
||||
.set_property("display", "none")
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
|
||||
// Get content from the old editor (when editing an article for instance)
|
||||
let title_val = get_elt_value("title");
|
||||
@@ -321,35 +400,44 @@ fn init_editor() -> Result<(), EditorError> {
|
||||
content_val.clone(),
|
||||
false,
|
||||
)?;
|
||||
js! { @{&content}.innerHTML = @{content_val}; };
|
||||
content.set_inner_html(&content_val);
|
||||
|
||||
// character counter
|
||||
content.add_event_listener(mv!(content => move |_: KeyDownEvent| {
|
||||
window().set_timeout(mv!(content => move || {
|
||||
let character_counter = Closure::wrap(Box::new(mv!(content => move |_| {
|
||||
let update_char_count = Closure::wrap(Box::new(mv!(content => move || {
|
||||
if let Some(e) = document().get_element_by_id("char-count") {
|
||||
let count = chars_left("#plume-fallback-editor", &content).unwrap_or_default();
|
||||
let text = i18n!(CATALOG, "Around {} characters left"; count);
|
||||
HtmlElement::try_from(e).map(|e| {
|
||||
js!{@{e}.innerText = @{text}};
|
||||
}).ok();
|
||||
e.dyn_ref::<HtmlElement>().map(|e| {
|
||||
e.set_inner_text(&text);
|
||||
}).unwrap();
|
||||
};
|
||||
}), 0);
|
||||
})) as Box<dyn FnMut()>);
|
||||
window().unwrap().set_timeout_with_callback_and_timeout_and_arguments(update_char_count.as_ref().unchecked_ref(), 0, &js_sys::Array::new()).unwrap();
|
||||
update_char_count.forget();
|
||||
autosave_debounce();
|
||||
}));
|
||||
})) as Box<dyn FnMut(KeyboardEvent)>);
|
||||
content
|
||||
.add_event_listener_with_callback("keydown", character_counter.as_ref().unchecked_ref())
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
character_counter.forget();
|
||||
|
||||
document().get_element_by_id("publish")?.add_event_listener(
|
||||
mv!(title, subtitle, content, old_ed => move |_: ClickEvent| {
|
||||
let popup = document().get_element_by_id("publish-popup").or_else(||
|
||||
init_popup(&title, &subtitle, &content, &old_ed).ok()
|
||||
).unwrap();
|
||||
let bg = document().get_element_by_id("popup-bg").or_else(||
|
||||
init_popup_bg().ok()
|
||||
).unwrap();
|
||||
let show_popup = Closure::wrap(Box::new(mv!(title, subtitle, content, old_ed => move |_| {
|
||||
let popup = document().get_element_by_id("publish-popup").or_else(||
|
||||
init_popup(&title, &subtitle, &content, &old_ed).ok()
|
||||
).unwrap();
|
||||
let bg = document().get_element_by_id("popup-bg").or_else(||
|
||||
init_popup_bg().ok()
|
||||
).unwrap();
|
||||
|
||||
popup.class_list().add("show").unwrap();
|
||||
bg.class_list().add("show").unwrap();
|
||||
}),
|
||||
);
|
||||
popup.class_list().add_1("show").unwrap();
|
||||
bg.class_list().add_1("show").unwrap();
|
||||
})) as Box<dyn FnMut(MouseEvent)>);
|
||||
document()
|
||||
.get_element_by_id("publish")?
|
||||
.add_event_listener_with_callback("click", show_popup.as_ref().unchecked_ref())
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
show_popup.forget();
|
||||
|
||||
show_errors();
|
||||
setup_close_button();
|
||||
@@ -359,32 +447,47 @@ fn init_editor() -> Result<(), EditorError> {
|
||||
|
||||
fn setup_close_button() {
|
||||
if let Some(button) = document().get_element_by_id("close-editor") {
|
||||
button.add_event_listener(|_: ClickEvent| {
|
||||
let close_editor = Closure::wrap(Box::new(|_| {
|
||||
window()
|
||||
.unwrap()
|
||||
.local_storage()
|
||||
.insert("basic-editor", "true")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.set("basic-editor", "true")
|
||||
.unwrap();
|
||||
window().history().go(0).unwrap(); // Refresh the page
|
||||
});
|
||||
window()
|
||||
.unwrap()
|
||||
.history()
|
||||
.unwrap()
|
||||
.go_with_delta(0)
|
||||
.unwrap(); // Refresh the page
|
||||
}) as Box<dyn FnMut(MouseEvent)>);
|
||||
button
|
||||
.add_event_listener_with_callback("click", close_editor.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
close_editor.forget();
|
||||
}
|
||||
}
|
||||
|
||||
fn show_errors() {
|
||||
if let Ok(Some(header)) = document().query_selector("header") {
|
||||
let list = document().create_element("header").unwrap();
|
||||
list.class_list().add("messages").unwrap();
|
||||
for error in document().query_selector_all("p.error").unwrap() {
|
||||
let document = document();
|
||||
if let Ok(Some(header)) = document.query_selector("header") {
|
||||
let list = document.create_element("header").unwrap();
|
||||
list.class_list().add_1("messages").unwrap();
|
||||
let errors = document.query_selector_all("p.error").unwrap();
|
||||
for i in 0..errors.length() {
|
||||
let error = errors.get(i).unwrap();
|
||||
error
|
||||
.parent_element()
|
||||
.unwrap()
|
||||
.remove_child(&error)
|
||||
.unwrap();
|
||||
list.append_child(&error);
|
||||
let _ = list.append_child(&error);
|
||||
}
|
||||
header
|
||||
.parent_element()
|
||||
.unwrap()
|
||||
.insert_before(&list, &header.next_sibling().unwrap())
|
||||
.insert_before(&list, header.next_sibling().as_ref())
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -395,9 +498,17 @@ fn init_popup(
|
||||
content: &HtmlElement,
|
||||
old_ed: &Element,
|
||||
) -> Result<Element, EditorError> {
|
||||
let popup = document().create_element("div")?;
|
||||
popup.class_list().add("popup")?;
|
||||
popup.set_attribute("id", "publish-popup")?;
|
||||
let document = document();
|
||||
let popup = document
|
||||
.create_element("div")
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
popup
|
||||
.class_list()
|
||||
.add_1("popup")
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
popup
|
||||
.set_attribute("id", "publish-popup")
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
|
||||
let tags = get_elt_value("tags")
|
||||
.split(',')
|
||||
@@ -405,112 +516,157 @@ fn init_popup(
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
let license = get_elt_value("license");
|
||||
make_input(&i18n!(CATALOG, "Tags"), "popup-tags", &popup).set_raw_value(&tags.join(", "));
|
||||
make_input(&i18n!(CATALOG, "License"), "popup-license", &popup).set_raw_value(&license);
|
||||
make_input(&i18n!(CATALOG, "Tags"), "popup-tags", &popup).set_value(&tags.join(", "));
|
||||
make_input(&i18n!(CATALOG, "License"), "popup-license", &popup).set_value(&license);
|
||||
|
||||
let cover_label = document().create_element("label")?;
|
||||
cover_label.append_child(&document().create_text_node(&i18n!(CATALOG, "Cover")));
|
||||
cover_label.set_attribute("for", "cover")?;
|
||||
let cover = document().get_element_by_id("cover")?;
|
||||
let cover_label = document
|
||||
.create_element("label")
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
cover_label
|
||||
.append_child(&document.create_text_node(&i18n!(CATALOG, "Cover")))
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
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();
|
||||
popup.append_child(&cover_label);
|
||||
popup.append_child(&cover);
|
||||
popup
|
||||
.append_child(&cover_label)
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
popup
|
||||
.append_child(&cover)
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
|
||||
if let Some(draft_checkbox) = document().get_element_by_id("draft") {
|
||||
let draft_label = document().create_element("label")?;
|
||||
draft_label.set_attribute("for", "popup-draft")?;
|
||||
if let Some(draft_checkbox) = document.get_element_by_id("draft") {
|
||||
let draft_checkbox = draft_checkbox.dyn_ref::<HtmlInputElement>().unwrap();
|
||||
let draft_label = document
|
||||
.create_element("label")
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
draft_label
|
||||
.set_attribute("for", "popup-draft")
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
|
||||
let draft = document().create_element("input").unwrap();
|
||||
js! {
|
||||
@{&draft}.id = "popup-draft";
|
||||
@{&draft}.name = "popup-draft";
|
||||
@{&draft}.type = "checkbox";
|
||||
@{&draft}.checked = @{&draft_checkbox}.checked;
|
||||
};
|
||||
let draft = document.create_element("input").unwrap();
|
||||
draft.set_id("popup-draft");
|
||||
let draft = draft.dyn_ref::<HtmlInputElement>().unwrap();
|
||||
draft.set_name("popup-draft");
|
||||
draft.set_type("checkbox");
|
||||
draft.set_checked(draft_checkbox.checked());
|
||||
|
||||
draft_label.append_child(&draft);
|
||||
draft_label.append_child(&document().create_text_node(&i18n!(CATALOG, "This is a draft")));
|
||||
popup.append_child(&draft_label);
|
||||
draft_label
|
||||
.append_child(&draft)
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
draft_label
|
||||
.append_child(&document.create_text_node(&i18n!(CATALOG, "This is a draft")))
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
popup
|
||||
.append_child(&draft_label)
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
}
|
||||
|
||||
let button = document().create_element("input")?;
|
||||
js! {
|
||||
@{&button}.type = "submit";
|
||||
@{&button}.value = @{i18n!(CATALOG, "Publish")};
|
||||
};
|
||||
button.append_child(&document().create_text_node(&i18n!(CATALOG, "Publish")));
|
||||
button.add_event_listener(
|
||||
mv!(title, subtitle, content, old_ed => move |_: ClickEvent| {
|
||||
title.focus(); // Remove the placeholder before publishing
|
||||
set_value("title", title.inner_text());
|
||||
subtitle.focus();
|
||||
set_value("subtitle", subtitle.inner_text());
|
||||
content.focus();
|
||||
set_value("editor-content", content.child_nodes().iter().fold(String::new(), |md, ch| {
|
||||
let to_append = match ch.node_type() {
|
||||
NodeType::Element => {
|
||||
if js!{ return @{&ch}.tagName; } == "DIV" {
|
||||
(js!{ return @{&ch}.innerHTML; }).try_into().unwrap_or_default()
|
||||
} else {
|
||||
(js!{ return @{&ch}.outerHTML; }).try_into().unwrap_or_default()
|
||||
}
|
||||
},
|
||||
NodeType::Text => ch.node_value().unwrap_or_default(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
format!("{}\n\n{}", md, to_append)
|
||||
}));
|
||||
set_value("tags", get_elt_value("popup-tags"));
|
||||
if let Some(draft) = document().get_element_by_id("popup-draft") {
|
||||
js!{
|
||||
document.getElementById("draft").checked = @{draft}.checked;
|
||||
};
|
||||
}
|
||||
let cover = document().get_element_by_id("cover").unwrap();
|
||||
cover.parent_element().unwrap().remove_child(&cover).ok();
|
||||
old_ed.append_child(&cover);
|
||||
set_value("license", get_elt_value("popup-license"));
|
||||
clear_autosave();
|
||||
js! {
|
||||
@{&old_ed}.submit();
|
||||
let button = document
|
||||
.create_element("input")
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
button
|
||||
.append_child(&document.create_text_node(&i18n!(CATALOG, "Publish")))
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
let button = button.dyn_ref::<HtmlInputElement>().unwrap();
|
||||
button.set_type("submit");
|
||||
button.set_value(&i18n!(CATALOG, "Publish"));
|
||||
let callback = Closure::wrap(Box::new(mv!(title, subtitle, content, old_ed => move |_| {
|
||||
let document = self::document();
|
||||
title.focus().unwrap(); // Remove the placeholder before publishing
|
||||
set_value("title", title.inner_text());
|
||||
subtitle.focus().unwrap();
|
||||
set_value("subtitle", subtitle.inner_text());
|
||||
content.focus().unwrap();
|
||||
let mut md = String::new();
|
||||
let child_nodes = content.child_nodes();
|
||||
for i in 0..child_nodes.length() {
|
||||
let ch = child_nodes.get(i).unwrap();
|
||||
let to_append = match ch.node_type() {
|
||||
Node::ELEMENT_NODE => {
|
||||
let ch = ch.dyn_ref::<Element>().unwrap();
|
||||
if ch.tag_name() == "DIV" {
|
||||
ch.inner_html()
|
||||
} else {
|
||||
ch.outer_html()
|
||||
}
|
||||
},
|
||||
Node::TEXT_NODE => ch.node_value().unwrap_or_default(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
}),
|
||||
);
|
||||
popup.append_child(&button);
|
||||
md = format!("{}\n\n{}", md, to_append);
|
||||
}
|
||||
set_value("editor-content", md);
|
||||
set_value("tags", get_elt_value("popup-tags"));
|
||||
if let Some(draft) = document.get_element_by_id("popup-draft") {
|
||||
if let Some(draft_checkbox) = document.get_element_by_id("draft") {
|
||||
let draft_checkbox = draft_checkbox.dyn_ref::<HtmlInputElement>().unwrap();
|
||||
let draft = draft.dyn_ref::<HtmlInputElement>().unwrap();
|
||||
draft_checkbox.set_checked(draft.checked());
|
||||
}
|
||||
}
|
||||
let cover = document.get_element_by_id("cover").unwrap();
|
||||
cover.parent_element().unwrap().remove_child(&cover).ok();
|
||||
old_ed.append_child(&cover).unwrap();
|
||||
set_value("license", get_elt_value("popup-license"));
|
||||
clear_autosave();
|
||||
let old_ed = old_ed.dyn_ref::<HtmlFormElement>().unwrap();
|
||||
old_ed.submit().unwrap();
|
||||
})) as Box<dyn FnMut(MouseEvent)>);
|
||||
button
|
||||
.add_event_listener_with_callback("click", callback.as_ref().unchecked_ref())
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
callback.forget();
|
||||
popup
|
||||
.append_child(&button)
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
|
||||
document().body()?.append_child(&popup);
|
||||
document
|
||||
.body()?
|
||||
.append_child(&popup)
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
Ok(popup)
|
||||
}
|
||||
|
||||
fn init_popup_bg() -> Result<Element, EditorError> {
|
||||
let bg = document().create_element("div")?;
|
||||
bg.class_list().add("popup-bg")?;
|
||||
bg.set_attribute("id", "popup-bg")?;
|
||||
let bg = document()
|
||||
.create_element("div")
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
bg.class_list()
|
||||
.add_1("popup-bg")
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
bg.set_attribute("id", "popup-bg")
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
|
||||
document().body()?.append_child(&bg);
|
||||
bg.add_event_listener(|_: ClickEvent| close_popup());
|
||||
document()
|
||||
.body()?
|
||||
.append_child(&bg)
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
let callback = Closure::wrap(Box::new(|_| close_popup()) as Box<dyn FnMut(MouseEvent)>);
|
||||
bg.add_event_listener_with_callback("click", callback.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
callback.forget();
|
||||
Ok(bg)
|
||||
}
|
||||
|
||||
fn chars_left(selector: &str, content: &HtmlElement) -> Option<i32> {
|
||||
match document().query_selector(selector) {
|
||||
Ok(Some(form)) => HtmlElement::try_from(form).ok().and_then(|form| {
|
||||
Ok(Some(form)) => form.dyn_ref::<HtmlElement>().and_then(|form| {
|
||||
if let Some(len) = form
|
||||
.get_attribute("content-size")
|
||||
.and_then(|s| s.parse::<i32>().ok())
|
||||
{
|
||||
(js! {
|
||||
let x = encodeURIComponent(@{content}.innerHTML)
|
||||
.replace(/%20/g, "+")
|
||||
.replace(/%0A/g, "%0D%0A")
|
||||
.replace(new RegExp("[!'*()]", "g"), "XXX") // replace exceptions of encodeURIComponent with placeholder
|
||||
.length + 2;
|
||||
console.log(x);
|
||||
return x;
|
||||
})
|
||||
.try_into()
|
||||
.map(|c: i32| len - c)
|
||||
.ok()
|
||||
(encode_uri_component(&content.inner_html())
|
||||
.replace("%20", "+")
|
||||
.replace("%0A", "%0D0A")
|
||||
.replace_by_pattern(&RegExp::new("[!'*()]", "g"), "XXX")
|
||||
.length()
|
||||
+ 2_u32)
|
||||
.try_into()
|
||||
.map(|c: i32| len - c)
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -520,26 +676,26 @@ fn chars_left(selector: &str, content: &HtmlElement) -> Option<i32> {
|
||||
}
|
||||
|
||||
fn close_popup() {
|
||||
let hide = |x: Element| x.class_list().remove("show");
|
||||
let hide = |x: Element| x.class_list().remove_1("show");
|
||||
document().get_element_by_id("publish-popup").map(hide);
|
||||
document().get_element_by_id("popup-bg").map(hide);
|
||||
}
|
||||
|
||||
fn make_input(label_text: &str, name: &'static str, form: &Element) -> InputElement {
|
||||
let label = document().create_element("label").unwrap();
|
||||
label.append_child(&document().create_text_node(label_text));
|
||||
fn make_input(label_text: &str, name: &'static str, form: &Element) -> HtmlInputElement {
|
||||
let document = document();
|
||||
let label = document.create_element("label").unwrap();
|
||||
label
|
||||
.append_child(&document.create_text_node(label_text))
|
||||
.unwrap();
|
||||
label.set_attribute("for", name).unwrap();
|
||||
|
||||
let inp: InputElement = document()
|
||||
.create_element("input")
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
let inp = document.create_element("input").unwrap();
|
||||
let inp = inp.dyn_into::<HtmlInputElement>().unwrap();
|
||||
inp.set_attribute("name", name).unwrap();
|
||||
inp.set_attribute("id", name).unwrap();
|
||||
|
||||
form.append_child(&label);
|
||||
form.append_child(&inp);
|
||||
form.append_child(&label).unwrap();
|
||||
form.append_child(&inp).unwrap();
|
||||
inp
|
||||
}
|
||||
|
||||
@@ -553,36 +709,46 @@ fn make_editable(tag: &'static str) -> Element {
|
||||
}
|
||||
|
||||
fn placeholder(elt: HtmlElement, text: &str) -> HtmlElement {
|
||||
elt.dataset().insert("placeholder", text).unwrap();
|
||||
elt.dataset().insert("edited", "false").unwrap();
|
||||
elt.dataset().set("placeholder", text).unwrap();
|
||||
elt.dataset().set("edited", "false").unwrap();
|
||||
|
||||
elt.add_event_listener(mv!(elt => move |_: FocusEvent| {
|
||||
let callback = Closure::wrap(Box::new(mv!(elt => move |_: FocusEvent| {
|
||||
if elt.dataset().get("edited").unwrap().as_str() != "true" {
|
||||
clear_children(&elt);
|
||||
}
|
||||
}));
|
||||
elt.add_event_listener(mv!(elt => move |_: BlurEvent| {
|
||||
})) as Box<dyn FnMut(FocusEvent)>);
|
||||
elt.add_event_listener_with_callback("focus", callback.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
callback.forget();
|
||||
let callback = Closure::wrap(Box::new(mv!(elt => move |_: Event| {
|
||||
if elt.dataset().get("edited").unwrap().as_str() != "true" {
|
||||
clear_children(&elt);
|
||||
|
||||
let ph = document().create_element("span").expect("Couldn't create placeholder");
|
||||
ph.class_list().add("placeholder").expect("Couldn't add class");
|
||||
ph.append_child(&document().create_text_node(&elt.dataset().get("placeholder").unwrap_or_default()));
|
||||
elt.append_child(&ph);
|
||||
ph.class_list().add_1("placeholder").expect("Couldn't add class");
|
||||
ph.append_child(&document().create_text_node(&elt.dataset().get("placeholder").unwrap_or_default())).unwrap();
|
||||
elt.append_child(&ph).unwrap();
|
||||
}
|
||||
}));
|
||||
elt.add_event_listener(mv!(elt => move |_: KeyUpEvent| {
|
||||
elt.dataset().insert("edited", if elt.inner_text().trim_matches('\n').is_empty() {
|
||||
})) as Box<dyn FnMut(Event)>);
|
||||
elt.add_event_listener_with_callback("blur", callback.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
callback.forget();
|
||||
let callback = Closure::wrap(Box::new(mv!(elt => move |_: KeyboardEvent| {
|
||||
elt.dataset().set("edited", if elt.inner_text().trim_matches('\n').is_empty() {
|
||||
"false"
|
||||
} else {
|
||||
"true"
|
||||
}).expect("Couldn't update edition state");
|
||||
}));
|
||||
})) as Box<dyn FnMut(KeyboardEvent)>);
|
||||
elt.add_event_listener_with_callback("keyup", callback.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
callback.forget();
|
||||
elt
|
||||
}
|
||||
|
||||
fn clear_children(elt: &HtmlElement) {
|
||||
for child in elt.child_nodes() {
|
||||
elt.remove_child(&child).unwrap();
|
||||
let child_nodes = elt.child_nodes();
|
||||
for _ in 0..child_nodes.length() {
|
||||
elt.remove_child(&child_nodes.get(0).unwrap()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+174
@@ -0,0 +1,174 @@
|
||||
#![recursion_limit = "128"]
|
||||
#![feature(decl_macro, proc_macro_hygiene, try_trait)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate gettext_macros;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
use wasm_bindgen::{prelude::*, JsCast};
|
||||
use web_sys::{console, window, Document, Element, Event, HtmlInputElement, TouchEvent};
|
||||
|
||||
init_i18n!(
|
||||
"plume-front",
|
||||
af,
|
||||
ar,
|
||||
bg,
|
||||
ca,
|
||||
cs,
|
||||
cy,
|
||||
da,
|
||||
de,
|
||||
el,
|
||||
en,
|
||||
eo,
|
||||
es,
|
||||
fa,
|
||||
fi,
|
||||
fr,
|
||||
gl,
|
||||
he,
|
||||
hi,
|
||||
hr,
|
||||
hu,
|
||||
it,
|
||||
ja,
|
||||
ko,
|
||||
nb,
|
||||
nl,
|
||||
no,
|
||||
pl,
|
||||
pt,
|
||||
ro,
|
||||
ru,
|
||||
sat,
|
||||
si,
|
||||
sk,
|
||||
sl,
|
||||
sr,
|
||||
sv,
|
||||
tr,
|
||||
uk,
|
||||
vi,
|
||||
zh
|
||||
);
|
||||
|
||||
mod editor;
|
||||
|
||||
compile_i18n!();
|
||||
|
||||
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 english_position = catalogs
|
||||
.iter()
|
||||
.position(|(language_code, _)| *language_code == "en")
|
||||
.unwrap();
|
||||
catalogs
|
||||
.iter()
|
||||
.find(|(l, _)| l == &lang)
|
||||
.unwrap_or(&catalogs[english_position])
|
||||
.clone()
|
||||
.1
|
||||
};
|
||||
}
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn main() -> Result<(), JsValue> {
|
||||
extern crate console_error_panic_hook;
|
||||
use std::panic;
|
||||
panic::set_hook(Box::new(console_error_panic_hook::hook));
|
||||
|
||||
menu();
|
||||
search();
|
||||
editor::init()
|
||||
.map_err(|e| console::error_1(&&format!("Editor error: {:?}", e).into()))
|
||||
.ok();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Toggle menu on mobile devices
|
||||
///
|
||||
/// It should normally be working fine even without this code
|
||||
/// But :focus-within is not yet supported by Webkit/Blink
|
||||
fn menu() {
|
||||
let document = document();
|
||||
if let Ok(Some(button)) = document.query_selector("#menu a") {
|
||||
if let Some(menu) = document.get_element_by_id("content") {
|
||||
let show_menu = Closure::wrap(Box::new(|_: TouchEvent| {
|
||||
self::document()
|
||||
.get_element_by_id("menu")
|
||||
.map(|menu| {
|
||||
menu.set_attribute("aria-expanded", "true")
|
||||
.map(|_| menu.class_list().add_1("show"))
|
||||
})
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
}) as Box<dyn FnMut(TouchEvent)>);
|
||||
button
|
||||
.add_event_listener_with_callback("touchend", show_menu.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
show_menu.forget();
|
||||
|
||||
let close_menu = Closure::wrap(Box::new(|evt: TouchEvent| {
|
||||
if evt
|
||||
.target()
|
||||
.unwrap()
|
||||
.dyn_ref::<Element>()
|
||||
.unwrap()
|
||||
.closest("a")
|
||||
.unwrap()
|
||||
.is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
self::document()
|
||||
.get_element_by_id("menu")
|
||||
.map(|menu| {
|
||||
menu.set_attribute("aria-expanded", "false")
|
||||
.map(|_| menu.class_list().remove_1("show"))
|
||||
})
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
}) as Box<dyn FnMut(TouchEvent)>);
|
||||
menu.add_event_listener_with_callback("touchend", close_menu.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
close_menu.forget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear the URL of the search page before submitting request
|
||||
fn search() {
|
||||
if let Some(form) = document().get_element_by_id("form") {
|
||||
let normalize_query = Closure::wrap(Box::new(|_: Event| {
|
||||
document()
|
||||
.query_selector_all("#form input")
|
||||
.map(|inputs| {
|
||||
for i in 0..inputs.length() {
|
||||
let input = inputs.get(i).unwrap();
|
||||
let input = input.dyn_ref::<HtmlInputElement>().unwrap();
|
||||
if input.name().is_empty() {
|
||||
input.set_name(&input.dyn_ref::<Element>().unwrap().id());
|
||||
}
|
||||
if !input.name().is_empty() && input.value().is_empty() {
|
||||
input.set_name("");
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}) as Box<dyn FnMut(Event)>);
|
||||
form.add_event_listener_with_callback("submit", normalize_query.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
normalize_query.forget();
|
||||
}
|
||||
}
|
||||
|
||||
fn document() -> Document {
|
||||
window().unwrap().document().unwrap()
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
#![recursion_limit = "128"]
|
||||
#![feature(decl_macro, proc_macro_hygiene, try_trait)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate gettext_macros;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate stdweb;
|
||||
use stdweb::web::{event::*, *};
|
||||
|
||||
init_i18n!(
|
||||
"plume-front",
|
||||
ar,
|
||||
bg,
|
||||
ca,
|
||||
cs,
|
||||
de,
|
||||
en,
|
||||
eo,
|
||||
es,
|
||||
fa,
|
||||
fr,
|
||||
gl,
|
||||
hi,
|
||||
hr,
|
||||
it,
|
||||
ja,
|
||||
nb,
|
||||
pl,
|
||||
pt,
|
||||
ro,
|
||||
ru,
|
||||
sr,
|
||||
sk,
|
||||
sv
|
||||
);
|
||||
|
||||
mod editor;
|
||||
|
||||
compile_i18n!();
|
||||
|
||||
lazy_static! {
|
||||
static ref CATALOG: gettext::Catalog = {
|
||||
let catalogs = include_i18n!();
|
||||
let lang = js! { return navigator.language }.into_string().unwrap();
|
||||
let lang = lang.splitn(2, '-').next().unwrap_or("en");
|
||||
|
||||
let english_position = catalogs
|
||||
.iter()
|
||||
.position(|(language_code, _)| *language_code == "en")
|
||||
.unwrap();
|
||||
catalogs
|
||||
.iter()
|
||||
.find(|(l, _)| l == &lang)
|
||||
.unwrap_or(&catalogs[english_position])
|
||||
.clone()
|
||||
.1
|
||||
};
|
||||
}
|
||||
|
||||
fn main() {
|
||||
menu();
|
||||
search();
|
||||
editor::init()
|
||||
.map_err(|e| console!(error, format!("Editor error: {:?}", e)))
|
||||
.ok();
|
||||
}
|
||||
|
||||
/// Toggle menu on mobile devices
|
||||
///
|
||||
/// It should normally be working fine even without this code
|
||||
/// But :focus-within is not yet supported by Webkit/Blink
|
||||
fn menu() {
|
||||
if let Some(button) = document().get_element_by_id("menu") {
|
||||
if let Some(menu) = document().get_element_by_id("content") {
|
||||
button.add_event_listener(|_: TouchEnd| {
|
||||
document()
|
||||
.get_element_by_id("menu")
|
||||
.map(|menu| menu.class_list().add("show"));
|
||||
});
|
||||
menu.add_event_listener(|_: TouchEnd| {
|
||||
document()
|
||||
.get_element_by_id("menu")
|
||||
.map(|menu| menu.class_list().remove("show"));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear the URL of the search page before submitting request
|
||||
fn search() {
|
||||
if let Some(form) = document().get_element_by_id("form") {
|
||||
form.add_event_listener(|_: SubmitEvent| {
|
||||
document()
|
||||
.query_selector_all("#form input")
|
||||
.map(|inputs| {
|
||||
for input in inputs {
|
||||
js! {
|
||||
if (@{&input}.name === "") {
|
||||
@{&input}.name = @{&input}.id
|
||||
}
|
||||
if (@{&input}.name && !@{&input}.value) {
|
||||
@{&input}.name = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "plume-macro"
|
||||
version = "0.4.0"
|
||||
version = "0.6.1-dev"
|
||||
authors = ["Trinity Pointard <trinity.pointard@insa-rennes.fr>"]
|
||||
edition = "2018"
|
||||
description = "Plume procedural macros"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
pre-release-hook = ["cargo", "fmt"]
|
||||
pre-release-replacements = []
|
||||
Regular → Executable
+4
-4
@@ -89,12 +89,12 @@ fn file_to_migration(file: &str) -> TokenStream2 {
|
||||
let mut actions = vec![];
|
||||
for line in file.lines() {
|
||||
if sql {
|
||||
if line.starts_with("--#!") {
|
||||
if let Some(acc_str) = line.strip_prefix("--#!") {
|
||||
if !acc.trim().is_empty() {
|
||||
actions.push(quote!(Action::Sql(#acc)));
|
||||
}
|
||||
sql = false;
|
||||
acc = line[4..].to_string();
|
||||
acc = acc_str.to_string();
|
||||
acc.push('\n');
|
||||
} else if line.starts_with("--") {
|
||||
continue;
|
||||
@@ -102,8 +102,8 @@ fn file_to_migration(file: &str) -> TokenStream2 {
|
||||
acc.push_str(line);
|
||||
acc.push('\n');
|
||||
}
|
||||
} else if line.starts_with("--#!") {
|
||||
acc.push_str(&line[4..]);
|
||||
} else if let Some(acc_str) = line.strip_prefix("--#!") {
|
||||
acc.push_str(&acc_str);
|
||||
acc.push('\n');
|
||||
} else if line.starts_with("--") {
|
||||
continue;
|
||||
|
||||
+10
-6
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "plume-models"
|
||||
version = "0.4.0"
|
||||
version = "0.6.1-dev"
|
||||
authors = ["Plume contributors"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -13,24 +13,28 @@ guid-create = "0.1"
|
||||
heck = "0.3.0"
|
||||
itertools = "0.8.0"
|
||||
lazy_static = "1.0"
|
||||
ldap3 = "0.7.1"
|
||||
migrations_internals= "1.4.0"
|
||||
openssl = "0.10.22"
|
||||
rocket = "0.4.0"
|
||||
rocket = "0.4.6"
|
||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||
reqwest = "0.9"
|
||||
scheduled-thread-pool = "0.2.2"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
tantivy = "0.12.0"
|
||||
tantivy = "0.13.3"
|
||||
url = "2.1"
|
||||
walkdir = "2.2"
|
||||
webfinger = "0.4.1"
|
||||
whatlang = "0.7.1"
|
||||
whatlang = "0.11.1"
|
||||
shrinkwraprs = "0.2.1"
|
||||
diesel-derive-newtype = "0.1.2"
|
||||
glob = "0.3.0"
|
||||
lindera-tantivy = { version = "0.1.2", optional = true }
|
||||
lindera-tantivy = { version = "0.7.1", optional = true }
|
||||
tracing = "0.1.22"
|
||||
riker = "0.4.2"
|
||||
once_cell = "1.5.2"
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
@@ -38,7 +42,7 @@ version = "0.4"
|
||||
|
||||
[dependencies.diesel]
|
||||
features = ["r2d2", "chrono"]
|
||||
version = "1.4.2"
|
||||
version = "1.4.5"
|
||||
|
||||
[dependencies.plume-api]
|
||||
path = "../plume-api"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
pre-release-hook = ["cargo", "fmt"]
|
||||
pre-release-replacements = []
|
||||
@@ -87,7 +87,7 @@ impl BlocklistedEmail {
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::{instance::tests as instance_tests, tests::rockets, Connection as Conn};
|
||||
use crate::{instance::tests as instance_tests, tests::db, Connection as Conn};
|
||||
use diesel::Connection;
|
||||
|
||||
pub(crate) fn fill_database(conn: &Conn) -> Vec<BlocklistedEmail> {
|
||||
@@ -106,29 +106,28 @@ pub(crate) mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn test_match() {
|
||||
let r = rockets();
|
||||
let conn = &*r.conn;
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let various = fill_database(conn);
|
||||
let various = fill_database(&conn);
|
||||
let match1 = "user1@bad-actor.com";
|
||||
let match2 = "spammer@lax-administration.com";
|
||||
let no_match = "happy-user@lax-administration.com";
|
||||
assert_eq!(
|
||||
BlocklistedEmail::matches_blocklist(conn, match1)
|
||||
BlocklistedEmail::matches_blocklist(&conn, match1)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.id,
|
||||
various[0].id
|
||||
);
|
||||
assert_eq!(
|
||||
BlocklistedEmail::matches_blocklist(conn, match2)
|
||||
BlocklistedEmail::matches_blocklist(&conn, match2)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.id,
|
||||
various[1].id
|
||||
);
|
||||
assert_eq!(
|
||||
BlocklistedEmail::matches_blocklist(conn, no_match)
|
||||
BlocklistedEmail::matches_blocklist(&conn, no_match)
|
||||
.unwrap()
|
||||
.is_none(),
|
||||
true
|
||||
|
||||
+77
-89
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
ap_url, instance::*, medias::Media, posts::Post, safe_string::SafeString, schema::blogs,
|
||||
search::Searcher, users::User, Connection, Error, PlumeRocket, Result, ITEMS_PER_PAGE,
|
||||
ap_url, db_conn::DbConn, instance::*, medias::Media, posts::Post, safe_string::SafeString,
|
||||
schema::blogs, users::User, Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE,
|
||||
};
|
||||
use activitypub::{
|
||||
actor::Group,
|
||||
@@ -20,7 +20,6 @@ use plume_common::activity_pub::{
|
||||
inbox::{AsActor, FromId},
|
||||
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
|
||||
};
|
||||
use serde_json;
|
||||
use url::Url;
|
||||
use webfinger::*;
|
||||
|
||||
@@ -132,25 +131,25 @@ impl Blog {
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<Blog> {
|
||||
pub fn find_by_fqn(conn: &DbConn, fqn: &str) -> Result<Blog> {
|
||||
let from_db = blogs::table
|
||||
.filter(blogs::fqn.eq(fqn))
|
||||
.first(&*c.conn)
|
||||
.first(&**conn)
|
||||
.optional()?;
|
||||
if let Some(from_db) = from_db {
|
||||
Ok(from_db)
|
||||
} else {
|
||||
Blog::fetch_from_webfinger(c, fqn)
|
||||
Blog::fetch_from_webfinger(conn, fqn)
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<Blog> {
|
||||
fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result<Blog> {
|
||||
resolve_with_prefix(Prefix::Group, acct.to_owned(), true)?
|
||||
.links
|
||||
.into_iter()
|
||||
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
|
||||
.ok_or(Error::Webfinger)
|
||||
.and_then(|l| Blog::from_id(c, &l.href?, None).map_err(|(_, e)| e))
|
||||
.and_then(|l| Blog::from_id(conn, &l.href?, None, CONFIG.proxy()).map_err(|(_, e)| e))
|
||||
}
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> {
|
||||
@@ -217,16 +216,16 @@ impl Blog {
|
||||
|
||||
pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> {
|
||||
let mut coll = OrderedCollection::default();
|
||||
coll.collection_props.items = serde_json::to_value(self.get_activities(conn)?)?;
|
||||
coll.collection_props.items = serde_json::to_value(self.get_activities(conn))?;
|
||||
coll.collection_props
|
||||
.set_total_items_u64(self.get_activities(conn)?.len() as u64)?;
|
||||
.set_total_items_u64(self.get_activities(conn).len() as u64)?;
|
||||
coll.collection_props
|
||||
.set_first_link(Id::new(ap_url(&format!("{}?page=1", &self.outbox_url))))?;
|
||||
coll.collection_props
|
||||
.set_last_link(Id::new(ap_url(&format!(
|
||||
"{}?page={}",
|
||||
&self.outbox_url,
|
||||
(self.get_activities(conn)?.len() as u64 + ITEMS_PER_PAGE as u64 - 1) as u64
|
||||
(self.get_activities(conn).len() as u64 + ITEMS_PER_PAGE as u64 - 1) as u64
|
||||
/ ITEMS_PER_PAGE as u64
|
||||
))))?;
|
||||
Ok(ActivityStream::new(coll))
|
||||
@@ -237,7 +236,7 @@ impl Blog {
|
||||
(min, max): (i32, i32),
|
||||
) -> Result<ActivityStream<OrderedCollectionPage>> {
|
||||
let mut coll = OrderedCollectionPage::default();
|
||||
let acts = self.get_activity_page(&conn, (min, max))?;
|
||||
let acts = self.get_activity_page(&conn, (min, max));
|
||||
//This still doesn't do anything because the outbox
|
||||
//doesn't do anything yet
|
||||
coll.collection_page_props.set_next_link(Id::new(&format!(
|
||||
@@ -253,15 +252,15 @@ impl Blog {
|
||||
coll.collection_props.items = serde_json::to_value(acts)?;
|
||||
Ok(ActivityStream::new(coll))
|
||||
}
|
||||
fn get_activities(&self, _conn: &Connection) -> Result<Vec<serde_json::Value>> {
|
||||
Ok(vec![])
|
||||
fn get_activities(&self, _conn: &Connection) -> Vec<serde_json::Value> {
|
||||
vec![]
|
||||
}
|
||||
fn get_activity_page(
|
||||
&self,
|
||||
_conn: &Connection,
|
||||
(_min, _max): (i32, i32),
|
||||
) -> Result<Vec<serde_json::Value>> {
|
||||
Ok(vec![])
|
||||
) -> Vec<serde_json::Value> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
pub fn get_keypair(&self) -> Result<PKey<Private>> {
|
||||
@@ -318,9 +317,9 @@ impl Blog {
|
||||
.and_then(|c| c.url().ok())
|
||||
}
|
||||
|
||||
pub fn delete(&self, conn: &Connection, searcher: &Searcher) -> Result<()> {
|
||||
pub fn delete(&self, conn: &Connection) -> Result<()> {
|
||||
for post in Post::get_for_blog(conn, &self)? {
|
||||
post.delete(conn, searcher)?;
|
||||
post.delete(conn)?;
|
||||
}
|
||||
diesel::delete(self)
|
||||
.execute(conn)
|
||||
@@ -335,20 +334,20 @@ impl IntoId for Blog {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromId<PlumeRocket> for Blog {
|
||||
impl FromId<DbConn> for Blog {
|
||||
type Error = Error;
|
||||
type Object = CustomGroup;
|
||||
|
||||
fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
|
||||
Self::find_by_ap_url(&c.conn, id)
|
||||
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
||||
Self::find_by_ap_url(&conn, id)
|
||||
}
|
||||
|
||||
fn from_activity(c: &PlumeRocket, acct: CustomGroup) -> Result<Self> {
|
||||
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 instance = Instance::find_by_domain(&c.conn, inst).or_else(|_| {
|
||||
let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
|
||||
Instance::insert(
|
||||
&c.conn,
|
||||
conn,
|
||||
NewInstance {
|
||||
public_domain: inst.to_owned(),
|
||||
name: inst.to_owned(),
|
||||
@@ -371,9 +370,9 @@ impl FromId<PlumeRocket> for Blog {
|
||||
.and_then(|icon| {
|
||||
let owner = icon.object_props.attributed_to_link::<Id>().ok()?;
|
||||
Media::save_remote(
|
||||
&c.conn,
|
||||
conn,
|
||||
icon.object_props.url_string().ok()?,
|
||||
&User::from_id(c, &owner, None).ok()?,
|
||||
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
@@ -387,9 +386,9 @@ impl FromId<PlumeRocket> for Blog {
|
||||
.and_then(|banner| {
|
||||
let owner = banner.object_props.attributed_to_link::<Id>().ok()?;
|
||||
Media::save_remote(
|
||||
&c.conn,
|
||||
conn,
|
||||
banner.object_props.url_string().ok()?,
|
||||
&User::from_id(c, &owner, None).ok()?,
|
||||
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
@@ -401,7 +400,7 @@ impl FromId<PlumeRocket> for Blog {
|
||||
}
|
||||
|
||||
Blog::insert(
|
||||
&c.conn,
|
||||
conn,
|
||||
NewBlog {
|
||||
actor_id: name.clone(),
|
||||
title: acct.object.object_props.name_string().unwrap_or(name),
|
||||
@@ -497,14 +496,8 @@ impl NewBlog {
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
blog_authors::*,
|
||||
config::CONFIG,
|
||||
instance::tests as instance_tests,
|
||||
medias::NewMedia,
|
||||
search::tests::get_searcher,
|
||||
tests::{db, rockets},
|
||||
users::tests as usersTests,
|
||||
Connection as Conn,
|
||||
blog_authors::*, instance::tests as instance_tests, medias::NewMedia, tests::db,
|
||||
users::tests as usersTests, Connection as Conn,
|
||||
};
|
||||
use diesel::Connection;
|
||||
|
||||
@@ -591,10 +584,10 @@ pub(crate) mod tests {
|
||||
fn get_instance() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
fill_database(conn);
|
||||
fill_database(&conn);
|
||||
|
||||
let blog = Blog::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlog::new_local(
|
||||
"SomeName".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
@@ -606,7 +599,7 @@ pub(crate) mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
blog.get_instance(conn).unwrap().id,
|
||||
blog.get_instance(&conn).unwrap().id,
|
||||
Instance::get_local().unwrap().id
|
||||
);
|
||||
// TODO add tests for remote instance
|
||||
@@ -618,10 +611,10 @@ pub(crate) mod tests {
|
||||
fn authors() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (user, _) = fill_database(conn);
|
||||
let (user, _) = fill_database(&conn);
|
||||
|
||||
let b1 = Blog::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlog::new_local(
|
||||
"SomeName".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
@@ -632,7 +625,7 @@ pub(crate) mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
let b2 = Blog::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlog::new_local(
|
||||
"Blog".to_owned(),
|
||||
"Blog".to_owned(),
|
||||
@@ -645,7 +638,7 @@ pub(crate) mod tests {
|
||||
let blog = vec![b1, b2];
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlogAuthor {
|
||||
blog_id: blog[0].id,
|
||||
author_id: user[0].id,
|
||||
@@ -655,7 +648,7 @@ pub(crate) mod tests {
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlogAuthor {
|
||||
blog_id: blog[0].id,
|
||||
author_id: user[1].id,
|
||||
@@ -665,7 +658,7 @@ pub(crate) mod tests {
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlogAuthor {
|
||||
blog_id: blog[1].id,
|
||||
author_id: user[0].id,
|
||||
@@ -675,39 +668,39 @@ pub(crate) mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert!(blog[0]
|
||||
.list_authors(conn)
|
||||
.list_authors(&conn)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[0].id));
|
||||
assert!(blog[0]
|
||||
.list_authors(conn)
|
||||
.list_authors(&conn)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[1].id));
|
||||
assert!(blog[1]
|
||||
.list_authors(conn)
|
||||
.list_authors(&conn)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[0].id));
|
||||
assert!(!blog[1]
|
||||
.list_authors(conn)
|
||||
.list_authors(&conn)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[1].id));
|
||||
|
||||
assert!(Blog::find_for_author(conn, &user[0])
|
||||
assert!(Blog::find_for_author(&conn, &user[0])
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[0].id));
|
||||
assert!(Blog::find_for_author(conn, &user[1])
|
||||
assert!(Blog::find_for_author(&conn, &user[1])
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[0].id));
|
||||
assert!(Blog::find_for_author(conn, &user[0])
|
||||
assert!(Blog::find_for_author(&conn, &user[0])
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[1].id));
|
||||
assert!(!Blog::find_for_author(conn, &user[1])
|
||||
assert!(!Blog::find_for_author(&conn, &user[1])
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[1].id));
|
||||
@@ -717,13 +710,12 @@ pub(crate) mod tests {
|
||||
|
||||
#[test]
|
||||
fn find_local() {
|
||||
let r = rockets();
|
||||
let conn = &*r.conn;
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
fill_database(conn);
|
||||
fill_database(&conn);
|
||||
|
||||
let blog = Blog::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlog::new_local(
|
||||
"SomeName".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
@@ -734,7 +726,7 @@ pub(crate) mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(Blog::find_by_fqn(&r, "SomeName").unwrap().id, blog.id);
|
||||
assert_eq!(Blog::find_by_fqn(&conn, "SomeName").unwrap().id, blog.id);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -743,10 +735,10 @@ pub(crate) mod tests {
|
||||
fn get_fqn() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
fill_database(conn);
|
||||
fill_database(&conn);
|
||||
|
||||
let blog = Blog::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlog::new_local(
|
||||
"SomeName".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
@@ -766,12 +758,10 @@ pub(crate) mod tests {
|
||||
fn delete() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (_, blogs) = fill_database(conn);
|
||||
let (_, blogs) = fill_database(&conn);
|
||||
|
||||
blogs[0]
|
||||
.delete(conn, &get_searcher(&CONFIG.search_tokenizers))
|
||||
.unwrap();
|
||||
assert!(Blog::get(conn, blogs[0].id).is_err());
|
||||
blogs[0].delete(&conn).unwrap();
|
||||
assert!(Blog::get(&conn, blogs[0].id).is_err());
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -780,11 +770,10 @@ pub(crate) mod tests {
|
||||
fn delete_via_user() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let searcher = get_searcher(&CONFIG.search_tokenizers);
|
||||
let (user, _) = fill_database(conn);
|
||||
let (user, _) = fill_database(&conn);
|
||||
|
||||
let b1 = Blog::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlog::new_local(
|
||||
"SomeName".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
@@ -795,7 +784,7 @@ pub(crate) mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
let b2 = Blog::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlog::new_local(
|
||||
"Blog".to_owned(),
|
||||
"Blog".to_owned(),
|
||||
@@ -808,7 +797,7 @@ pub(crate) mod tests {
|
||||
let blog = vec![b1, b2];
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlogAuthor {
|
||||
blog_id: blog[0].id,
|
||||
author_id: user[0].id,
|
||||
@@ -818,7 +807,7 @@ pub(crate) mod tests {
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlogAuthor {
|
||||
blog_id: blog[0].id,
|
||||
author_id: user[1].id,
|
||||
@@ -828,7 +817,7 @@ pub(crate) mod tests {
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlogAuthor {
|
||||
blog_id: blog[1].id,
|
||||
author_id: user[0].id,
|
||||
@@ -837,24 +826,23 @@ pub(crate) mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
user[0].delete(conn, &searcher).unwrap();
|
||||
assert!(Blog::get(conn, blog[0].id).is_ok());
|
||||
assert!(Blog::get(conn, blog[1].id).is_err());
|
||||
user[1].delete(conn, &searcher).unwrap();
|
||||
assert!(Blog::get(conn, blog[0].id).is_err());
|
||||
user[0].delete(&conn).unwrap();
|
||||
assert!(Blog::get(&conn, blog[0].id).is_ok());
|
||||
assert!(Blog::get(&conn, blog[1].id).is_err());
|
||||
user[1].delete(&conn).unwrap();
|
||||
assert!(Blog::get(&conn, blog[0].id).is_err());
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_federation() {
|
||||
let r = rockets();
|
||||
let conn = &*r.conn;
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (users, mut blogs) = fill_database(conn);
|
||||
let (users, mut blogs) = fill_database(&conn);
|
||||
blogs[0].icon_id = Some(
|
||||
Media::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewMedia {
|
||||
file_path: "aaa.png".into(),
|
||||
alt_text: String::new(),
|
||||
@@ -870,7 +858,7 @@ pub(crate) mod tests {
|
||||
);
|
||||
blogs[0].banner_id = Some(
|
||||
Media::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewMedia {
|
||||
file_path: "bbb.png".into(),
|
||||
alt_text: String::new(),
|
||||
@@ -884,11 +872,11 @@ pub(crate) mod tests {
|
||||
.unwrap()
|
||||
.id,
|
||||
);
|
||||
let _: Blog = blogs[0].save_changes(conn).unwrap();
|
||||
let _: Blog = blogs[0].save_changes(&**conn).unwrap();
|
||||
|
||||
let ap_repr = blogs[0].to_activity(conn).unwrap();
|
||||
blogs[0].delete(conn, &*r.searcher).unwrap();
|
||||
let blog = Blog::from_activity(&r, ap_repr).unwrap();
|
||||
let ap_repr = blogs[0].to_activity(&conn).unwrap();
|
||||
blogs[0].delete(&conn).unwrap();
|
||||
let blog = Blog::from_activity(&conn, ap_repr).unwrap();
|
||||
|
||||
assert_eq!(blog.actor_id, blogs[0].actor_id);
|
||||
assert_eq!(blog.title, blogs[0].title);
|
||||
@@ -900,8 +888,8 @@ pub(crate) mod tests {
|
||||
assert_eq!(blog.public_key, blogs[0].public_key);
|
||||
assert_eq!(blog.fqn, blogs[0].fqn);
|
||||
assert_eq!(blog.summary_html, blogs[0].summary_html);
|
||||
assert_eq!(blog.icon_url(conn), blogs[0].icon_url(conn));
|
||||
assert_eq!(blog.banner_url(conn), blogs[0].banner_url(conn));
|
||||
assert_eq!(blog.icon_url(&conn), blogs[0].icon_url(&conn));
|
||||
assert_eq!(blog.banner_url(&conn), blogs[0].banner_url(&conn));
|
||||
|
||||
Ok(())
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
comment_seers::{CommentSeers, NewCommentSeers},
|
||||
db_conn::DbConn,
|
||||
instance::Instance,
|
||||
medias::Media,
|
||||
mentions::Mention,
|
||||
@@ -8,7 +9,7 @@ use crate::{
|
||||
safe_string::SafeString,
|
||||
schema::comments,
|
||||
users::User,
|
||||
Connection, Error, PlumeRocket, Result,
|
||||
Connection, Error, Result, CONFIG,
|
||||
};
|
||||
use activitypub::{
|
||||
activity::{Create, Delete},
|
||||
@@ -24,7 +25,6 @@ use plume_common::{
|
||||
},
|
||||
utils,
|
||||
};
|
||||
use serde_json;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Queryable, Identifiable, Clone, AsChangeset)]
|
||||
@@ -105,13 +105,13 @@ impl Comment {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn to_activity(&self, c: &PlumeRocket) -> Result<Note> {
|
||||
let author = User::get(&c.conn, self.author_id)?;
|
||||
pub fn to_activity(&self, conn: &DbConn) -> Result<Note> {
|
||||
let author = User::get(conn, self.author_id)?;
|
||||
let (html, mentions, _hashtags) = utils::md_to_html(
|
||||
self.content.get().as_ref(),
|
||||
Some(&Instance::get_local()?.public_domain),
|
||||
true,
|
||||
Some(Media::get_media_processor(&c.conn, vec![&author])),
|
||||
Some(Media::get_media_processor(conn, vec![&author])),
|
||||
);
|
||||
|
||||
let mut note = Note::default();
|
||||
@@ -124,8 +124,8 @@ impl Comment {
|
||||
note.object_props.set_content_string(html)?;
|
||||
note.object_props
|
||||
.set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else(
|
||||
|| Ok(Post::get(&c.conn, self.post_id)?.ap_url),
|
||||
|id| Ok(Comment::get(&c.conn, id)?.ap_url.unwrap_or_default()) as Result<String>,
|
||||
|| Ok(Post::get(conn, self.post_id)?.ap_url),
|
||||
|id| Ok(Comment::get(conn, id)?.ap_url.unwrap_or_default()) as Result<String>,
|
||||
)?))?;
|
||||
note.object_props
|
||||
.set_published_string(chrono::Utc::now().to_rfc3339())?;
|
||||
@@ -134,16 +134,16 @@ impl Comment {
|
||||
note.object_props.set_tag_link_vec(
|
||||
mentions
|
||||
.into_iter()
|
||||
.filter_map(|m| Mention::build_activity(c, &m).ok())
|
||||
.filter_map(|m| Mention::build_activity(conn, &m).ok())
|
||||
.collect::<Vec<link::Mention>>(),
|
||||
)?;
|
||||
Ok(note)
|
||||
}
|
||||
|
||||
pub fn create_activity(&self, c: &PlumeRocket) -> Result<Create> {
|
||||
let author = User::get(&c.conn, self.author_id)?;
|
||||
pub fn create_activity(&self, conn: &DbConn) -> Result<Create> {
|
||||
let author = User::get(&conn, self.author_id)?;
|
||||
|
||||
let note = self.to_activity(c)?;
|
||||
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())?;
|
||||
@@ -152,7 +152,7 @@ impl Comment {
|
||||
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(&c.conn)?.followers_endpoint)])?;
|
||||
.set_cc_link_vec(vec![Id::new(self.get_author(&conn)?.followers_endpoint)])?;
|
||||
Ok(act)
|
||||
}
|
||||
|
||||
@@ -194,16 +194,15 @@ impl Comment {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromId<PlumeRocket> for Comment {
|
||||
impl FromId<DbConn> for Comment {
|
||||
type Error = Error;
|
||||
type Object = Note;
|
||||
|
||||
fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
|
||||
Self::find_by_ap_url(&c.conn, id)
|
||||
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
||||
Self::find_by_ap_url(conn, id)
|
||||
}
|
||||
|
||||
fn from_activity(c: &PlumeRocket, note: Note) -> Result<Self> {
|
||||
let conn = &*c.conn;
|
||||
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_comment = Comment::find_by_ap_url(conn, previous_url);
|
||||
@@ -236,9 +235,10 @@ impl FromId<PlumeRocket> for Comment {
|
||||
Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result<i32>
|
||||
})?,
|
||||
author_id: User::from_id(
|
||||
c,
|
||||
conn,
|
||||
¬e.object_props.attributed_to_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
@@ -296,7 +296,7 @@ impl FromId<PlumeRocket> for Comment {
|
||||
.collect::<HashSet<_>>() // remove duplicates (don't do a query more than once)
|
||||
.into_iter()
|
||||
.map(|v| {
|
||||
if let Ok(user) = User::from_id(c, &v, None) {
|
||||
if let Ok(user) = User::from_id(conn, &v, None, CONFIG.proxy()) {
|
||||
vec![user]
|
||||
} else {
|
||||
vec![] // TODO try to fetch collection
|
||||
@@ -322,41 +322,41 @@ impl FromId<PlumeRocket> for Comment {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, Create, &PlumeRocket> for Comment {
|
||||
impl AsObject<User, Create, &DbConn> for Comment {
|
||||
type Error = Error;
|
||||
type Output = Self;
|
||||
|
||||
fn activity(self, _c: &PlumeRocket, _actor: User, _id: &str) -> Result<Self> {
|
||||
fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result<Self> {
|
||||
// The actual creation takes place in the FromId impl
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, Delete, &PlumeRocket> for Comment {
|
||||
impl AsObject<User, Delete, &DbConn> for Comment {
|
||||
type Error = Error;
|
||||
type Output = ();
|
||||
|
||||
fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
|
||||
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
||||
if self.author_id != actor.id {
|
||||
return Err(Error::Unauthorized);
|
||||
}
|
||||
|
||||
for m in Mention::list_for_comment(&c.conn, self.id)? {
|
||||
for n in Notification::find_for_mention(&c.conn, &m)? {
|
||||
n.delete(&c.conn)?;
|
||||
for m in Mention::list_for_comment(conn, self.id)? {
|
||||
for n in Notification::find_for_mention(conn, &m)? {
|
||||
n.delete(conn)?;
|
||||
}
|
||||
m.delete(&c.conn)?;
|
||||
m.delete(conn)?;
|
||||
}
|
||||
|
||||
for n in Notification::find_for_comment(&c.conn, &self)? {
|
||||
n.delete(&c.conn)?;
|
||||
for n in Notification::find_for_comment(&conn, &self)? {
|
||||
n.delete(&**conn)?;
|
||||
}
|
||||
|
||||
diesel::update(comments::table)
|
||||
.filter(comments::in_response_to_id.eq(self.id))
|
||||
.set(comments::in_response_to_id.eq(self.in_response_to_id))
|
||||
.execute(&*c.conn)?;
|
||||
diesel::delete(&self).execute(&*c.conn)?;
|
||||
.execute(&**conn)?;
|
||||
diesel::delete(&self).execute(&**conn)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -392,17 +392,16 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::inbox::{inbox, tests::fill_database, InboxResult};
|
||||
use crate::safe_string::SafeString;
|
||||
use crate::tests::rockets;
|
||||
use crate::tests::db;
|
||||
use diesel::Connection;
|
||||
|
||||
// creates a post, get it's Create activity, delete the post,
|
||||
// "send" the Create to the inbox, and check it works
|
||||
#[test]
|
||||
fn self_federation() {
|
||||
let r = rockets();
|
||||
let conn = &*r.conn;
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (posts, users, _) = fill_database(&r);
|
||||
let (posts, users, _) = fill_database(&conn);
|
||||
|
||||
let original_comm = Comment::insert(
|
||||
conn,
|
||||
@@ -418,14 +417,14 @@ mod tests {
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let act = original_comm.create_activity(&r).unwrap();
|
||||
let act = original_comm.create_activity(&conn).unwrap();
|
||||
inbox(
|
||||
&r,
|
||||
serde_json::to_value(original_comm.build_delete(conn).unwrap()).unwrap(),
|
||||
&conn,
|
||||
serde_json::to_value(original_comm.build_delete(&conn).unwrap()).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
match inbox(&r, serde_json::to_value(act).unwrap()).unwrap() {
|
||||
match inbox(&conn, serde_json::to_value(act).unwrap()).unwrap() {
|
||||
InboxResult::Commented(c) => {
|
||||
// TODO: one is HTML, the other markdown: assert_eq!(c.content, original_comm.content);
|
||||
assert_eq!(c.in_response_to_id, original_comm.in_response_to_id);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::search::TokenizerKind as SearchTokenizer;
|
||||
use rocket::config::Limits;
|
||||
use rocket::Config as RocketConfig;
|
||||
use std::collections::HashSet;
|
||||
use std::env::{self, var};
|
||||
|
||||
#[cfg(not(test))]
|
||||
@@ -20,6 +21,13 @@ pub struct Config {
|
||||
pub logo: LogoConfig,
|
||||
pub default_theme: String,
|
||||
pub media_directory: String,
|
||||
pub ldap: Option<LdapConfig>,
|
||||
pub proxy: Option<ProxyConfig>,
|
||||
}
|
||||
impl Config {
|
||||
pub fn proxy(&self) -> Option<&reqwest::Proxy> {
|
||||
self.proxy.as_ref().map(|p| &p.proxy)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -156,8 +164,8 @@ impl Default for LogoConfig {
|
||||
};
|
||||
let mut custom_icons = env::vars()
|
||||
.filter_map(|(var, val)| {
|
||||
if var.starts_with("PLUME_LOGO_") {
|
||||
Some((var[11..].to_owned(), val))
|
||||
if let Some(size) = var.strip_prefix("PLUME_LOGO_") {
|
||||
Some((size.to_owned(), val))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -240,6 +248,90 @@ impl SearchTokenizerConfig {
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
let addr = var("LDAP_ADDR").ok();
|
||||
let base_dn = var("LDAP_BASE_DN").ok();
|
||||
match (addr, base_dn) {
|
||||
(Some(addr), Some(base_dn)) => {
|
||||
let tls = var("LDAP_TLS").unwrap_or_else(|_| "false".to_owned());
|
||||
let tls = match tls.as_ref() {
|
||||
"1" | "true" | "TRUE" => true,
|
||||
"0" | "false" | "FALSE" => false,
|
||||
_ => panic!("Invalid LDAP configuration : tls"),
|
||||
};
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProxyConfig {
|
||||
pub url: reqwest::Url,
|
||||
pub only_domains: Option<HashSet<String>>,
|
||||
pub proxy: reqwest::Proxy,
|
||||
}
|
||||
|
||||
fn get_proxy_config() -> Option<ProxyConfig> {
|
||||
let url: reqwest::Url = var("PROXY_URL").ok()?.parse().expect("Invalid PROXY_URL");
|
||||
let proxy_url = url.clone();
|
||||
let only_domains: Option<HashSet<String>> = var("PROXY_DOMAINS")
|
||||
.ok()
|
||||
.map(|ods| ods.split(',').map(str::to_owned).collect());
|
||||
let proxy = if let Some(ref only_domains) = only_domains {
|
||||
let only_domains = only_domains.clone();
|
||||
reqwest::Proxy::custom(move |url| {
|
||||
if let Some(domain) = url.domain() {
|
||||
if only_domains.contains(domain)
|
||||
|| only_domains
|
||||
.iter()
|
||||
.any(|target| domain.ends_with(&format!(".{}", target)))
|
||||
{
|
||||
Some(proxy_url.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
} else {
|
||||
reqwest::Proxy::all(proxy_url).expect("Invalid PROXY_URL")
|
||||
};
|
||||
Some(ProxyConfig {
|
||||
url,
|
||||
only_domains,
|
||||
proxy,
|
||||
})
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref CONFIG: Config = Config {
|
||||
base_url: var("BASE_URL").unwrap_or_else(|_| format!(
|
||||
@@ -267,5 +359,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()),
|
||||
ldap: get_ldap_config(),
|
||||
proxy: get_proxy_config(),
|
||||
};
|
||||
}
|
||||
|
||||
+36
-21
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
ap_url, notifications::*, schema::follows, users::User, Connection, Error, PlumeRocket, Result,
|
||||
CONFIG,
|
||||
ap_url, db_conn::DbConn, notifications::*, schema::follows, users::User, Connection, Error,
|
||||
Result, CONFIG,
|
||||
};
|
||||
use activitypub::activity::{Accept, Follow as FollowAct, Undo};
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||
@@ -116,7 +116,12 @@ impl Follow {
|
||||
.accept_props
|
||||
.set_actor_link::<Id>(target.clone().into_id())?;
|
||||
accept.accept_props.set_object_object(follow)?;
|
||||
broadcast(&*target, accept, vec![from.clone()]);
|
||||
broadcast(
|
||||
&*target,
|
||||
accept,
|
||||
vec![from.clone()],
|
||||
CONFIG.proxy().cloned(),
|
||||
);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -136,11 +141,11 @@ impl Follow {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, FollowAct, &PlumeRocket> for User {
|
||||
impl AsObject<User, FollowAct, &DbConn> for User {
|
||||
type Error = Error;
|
||||
type Output = Follow;
|
||||
|
||||
fn activity(self, c: &PlumeRocket, actor: User, id: &str) -> Result<Follow> {
|
||||
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Follow> {
|
||||
// Mastodon (at least) requires the full Follow object when accepting it,
|
||||
// so we rebuilt it here
|
||||
let mut follow = FollowAct::default();
|
||||
@@ -148,40 +153,50 @@ impl AsObject<User, FollowAct, &PlumeRocket> for User {
|
||||
follow
|
||||
.follow_props
|
||||
.set_actor_link::<Id>(actor.clone().into_id())?;
|
||||
Follow::accept_follow(&c.conn, &actor, &self, follow, actor.id, self.id)
|
||||
Follow::accept_follow(conn, &actor, &self, follow, actor.id, self.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromId<PlumeRocket> for Follow {
|
||||
impl FromId<DbConn> for Follow {
|
||||
type Error = Error;
|
||||
type Object = FollowAct;
|
||||
|
||||
fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
|
||||
Follow::find_by_ap_url(&c.conn, id)
|
||||
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
||||
Follow::find_by_ap_url(conn, id)
|
||||
}
|
||||
|
||||
fn from_activity(c: &PlumeRocket, follow: FollowAct) -> Result<Self> {
|
||||
let actor =
|
||||
User::from_id(c, &follow.follow_props.actor_link::<Id>()?, None).map_err(|(_, e)| e)?;
|
||||
fn from_activity(conn: &DbConn, follow: FollowAct) -> Result<Self> {
|
||||
let actor = User::from_id(
|
||||
conn,
|
||||
&follow.follow_props.actor_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.map_err(|(_, e)| e)?;
|
||||
|
||||
let target = User::from_id(c, &follow.follow_props.object_link::<Id>()?, None)
|
||||
.map_err(|(_, e)| e)?;
|
||||
Follow::accept_follow(&c.conn, &actor, &target, follow, actor.id, target.id)
|
||||
let target = User::from_id(
|
||||
conn,
|
||||
&follow.follow_props.object_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.map_err(|(_, e)| e)?;
|
||||
Follow::accept_follow(conn, &actor, &target, follow, actor.id, target.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, Undo, &PlumeRocket> for Follow {
|
||||
impl AsObject<User, Undo, &DbConn> for Follow {
|
||||
type Error = Error;
|
||||
type Output = ();
|
||||
|
||||
fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
|
||||
let conn = &*c.conn;
|
||||
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
||||
let conn = conn;
|
||||
if self.follower_id == actor.id {
|
||||
diesel::delete(&self).execute(conn)?;
|
||||
diesel::delete(&self).execute(&**conn)?;
|
||||
|
||||
// delete associated notification if any
|
||||
if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
|
||||
diesel::delete(¬if).execute(conn)?;
|
||||
if let Ok(notif) = Notification::find(&conn, notification_kind::FOLLOW, self.id) {
|
||||
diesel::delete(¬if).execute(&**conn)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+267
-84
@@ -1,13 +1,13 @@
|
||||
use activitypub::activity::*;
|
||||
use serde_json;
|
||||
|
||||
use crate::{
|
||||
comments::Comment,
|
||||
db_conn::DbConn,
|
||||
follows, likes,
|
||||
posts::{Post, PostUpdate},
|
||||
reshares::Reshare,
|
||||
users::User,
|
||||
Error, PlumeRocket,
|
||||
Error, CONFIG,
|
||||
};
|
||||
use plume_common::activity_pub::inbox::Inbox;
|
||||
|
||||
@@ -46,20 +46,20 @@ impl_into_inbox_result! {
|
||||
Reshare => Reshared
|
||||
}
|
||||
|
||||
pub fn inbox(ctx: &PlumeRocket, act: serde_json::Value) -> Result<InboxResult, Error> {
|
||||
Inbox::handle(ctx, act)
|
||||
.with::<User, Announce, Post>()
|
||||
.with::<User, Create, Comment>()
|
||||
.with::<User, Create, Post>()
|
||||
.with::<User, Delete, Comment>()
|
||||
.with::<User, Delete, Post>()
|
||||
.with::<User, Delete, User>()
|
||||
.with::<User, Follow, User>()
|
||||
.with::<User, Like, Post>()
|
||||
.with::<User, Undo, Reshare>()
|
||||
.with::<User, Undo, follows::Follow>()
|
||||
.with::<User, Undo, likes::Like>()
|
||||
.with::<User, Update, PostUpdate>()
|
||||
pub fn inbox(conn: &DbConn, act: serde_json::Value) -> Result<InboxResult, Error> {
|
||||
Inbox::handle(conn, act)
|
||||
.with::<User, Announce, Post>(CONFIG.proxy())
|
||||
.with::<User, Create, Comment>(CONFIG.proxy())
|
||||
.with::<User, Create, Post>(CONFIG.proxy())
|
||||
.with::<User, Delete, Comment>(CONFIG.proxy())
|
||||
.with::<User, Delete, Post>(CONFIG.proxy())
|
||||
.with::<User, Delete, User>(CONFIG.proxy())
|
||||
.with::<User, Follow, User>(CONFIG.proxy())
|
||||
.with::<User, Like, Post>(CONFIG.proxy())
|
||||
.with::<User, Undo, Reshare>(CONFIG.proxy())
|
||||
.with::<User, Undo, follows::Follow>(CONFIG.proxy())
|
||||
.with::<User, Undo, likes::Like>(CONFIG.proxy())
|
||||
.with::<User, Update, PostUpdate>(CONFIG.proxy())
|
||||
.done()
|
||||
}
|
||||
|
||||
@@ -67,13 +67,13 @@ pub fn inbox(ctx: &PlumeRocket, act: serde_json::Value) -> Result<InboxResult, E
|
||||
pub(crate) mod tests {
|
||||
use super::InboxResult;
|
||||
use crate::blogs::tests::fill_database as blog_fill_db;
|
||||
use crate::db_conn::DbConn;
|
||||
use crate::safe_string::SafeString;
|
||||
use crate::tests::rockets;
|
||||
use crate::PlumeRocket;
|
||||
use crate::tests::db;
|
||||
use diesel::Connection;
|
||||
|
||||
pub fn fill_database(
|
||||
rockets: &PlumeRocket,
|
||||
conn: &DbConn,
|
||||
) -> (
|
||||
Vec<crate::posts::Post>,
|
||||
Vec<crate::users::User>,
|
||||
@@ -82,9 +82,9 @@ pub(crate) mod tests {
|
||||
use crate::post_authors::*;
|
||||
use crate::posts::*;
|
||||
|
||||
let (users, blogs) = blog_fill_db(&rockets.conn);
|
||||
let (users, blogs) = blog_fill_db(&conn);
|
||||
let post = Post::insert(
|
||||
&rockets.conn,
|
||||
&conn,
|
||||
NewPost {
|
||||
blog_id: blogs[0].id,
|
||||
slug: "testing".to_owned(),
|
||||
@@ -98,12 +98,11 @@ pub(crate) mod tests {
|
||||
source: String::new(),
|
||||
cover_id: None,
|
||||
},
|
||||
&rockets.searcher,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
PostAuthor::insert(
|
||||
&rockets.conn,
|
||||
&conn,
|
||||
NewPostAuthor {
|
||||
post_id: post.id,
|
||||
author_id: users[0].id,
|
||||
@@ -116,10 +115,9 @@ pub(crate) mod tests {
|
||||
|
||||
#[test]
|
||||
fn announce_post() {
|
||||
let r = rockets();
|
||||
let conn = &*r.conn;
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (posts, users, _) = fill_database(&r);
|
||||
let (posts, users, _) = fill_database(&conn);
|
||||
let act = json!({
|
||||
"id": "https://plu.me/announce/1",
|
||||
"actor": users[0].ap_url,
|
||||
@@ -127,7 +125,7 @@ pub(crate) mod tests {
|
||||
"type": "Announce",
|
||||
});
|
||||
|
||||
match super::inbox(&r, act).unwrap() {
|
||||
match super::inbox(&conn, act).unwrap() {
|
||||
super::InboxResult::Reshared(r) => {
|
||||
assert_eq!(r.post_id, posts[0].id);
|
||||
assert_eq!(r.user_id, users[0].id);
|
||||
@@ -141,10 +139,9 @@ pub(crate) mod tests {
|
||||
|
||||
#[test]
|
||||
fn create_comment() {
|
||||
let r = rockets();
|
||||
let conn = &*r.conn;
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (posts, users, _) = fill_database(&r);
|
||||
let (posts, users, _) = fill_database(&conn);
|
||||
let act = json!({
|
||||
"id": "https://plu.me/comment/1/activity",
|
||||
"actor": users[0].ap_url,
|
||||
@@ -159,7 +156,7 @@ pub(crate) mod tests {
|
||||
"type": "Create",
|
||||
});
|
||||
|
||||
match super::inbox(&r, act).unwrap() {
|
||||
match super::inbox(&conn, act).unwrap() {
|
||||
super::InboxResult::Commented(c) => {
|
||||
assert_eq!(c.author_id, users[0].id);
|
||||
assert_eq!(c.post_id, posts[0].id);
|
||||
@@ -174,11 +171,98 @@ pub(crate) mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_post() {
|
||||
let r = rockets();
|
||||
let conn = &*r.conn;
|
||||
fn spoof_comment() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (_, users, blogs) = fill_database(&r);
|
||||
let (posts, users, _) = fill_database(&conn);
|
||||
let act = json!({
|
||||
"id": "https://plu.me/comment/1/activity",
|
||||
"actor": users[0].ap_url,
|
||||
"object": {
|
||||
"type": "Note",
|
||||
"id": "https://plu.me/comment/1",
|
||||
"attributedTo": users[1].ap_url,
|
||||
"inReplyTo": posts[0].ap_url,
|
||||
"content": "Hello.",
|
||||
"to": [plume_common::activity_pub::PUBLIC_VISIBILITY]
|
||||
},
|
||||
"type": "Create",
|
||||
});
|
||||
|
||||
assert!(matches!(
|
||||
super::inbox(&conn, act.clone()),
|
||||
Err(super::Error::Inbox(
|
||||
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
||||
))
|
||||
));
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spoof_comment_by_object_with_id() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (posts, users, _) = fill_database(&conn);
|
||||
let act = json!({
|
||||
"id": "https://plu.me/comment/1/activity",
|
||||
"actor": users[0].ap_url,
|
||||
"object": {
|
||||
"type": "Note",
|
||||
"id": "https://plu.me/comment/1",
|
||||
"attributedTo": {
|
||||
"id": users[1].ap_url
|
||||
},
|
||||
"inReplyTo": posts[0].ap_url,
|
||||
"content": "Hello.",
|
||||
"to": [plume_common::activity_pub::PUBLIC_VISIBILITY]
|
||||
},
|
||||
"type": "Create",
|
||||
});
|
||||
|
||||
assert!(matches!(
|
||||
super::inbox(&conn, act.clone()),
|
||||
Err(super::Error::Inbox(
|
||||
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
||||
))
|
||||
));
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
#[test]
|
||||
fn spoof_comment_by_object_without_id() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (posts, users, _) = fill_database(&conn);
|
||||
let act = json!({
|
||||
"id": "https://plu.me/comment/1/activity",
|
||||
"actor": users[0].ap_url,
|
||||
"object": {
|
||||
"type": "Note",
|
||||
"id": "https://plu.me/comment/1",
|
||||
"attributedTo": {},
|
||||
"inReplyTo": posts[0].ap_url,
|
||||
"content": "Hello.",
|
||||
"to": [plume_common::activity_pub::PUBLIC_VISIBILITY]
|
||||
},
|
||||
"type": "Create",
|
||||
});
|
||||
|
||||
assert!(matches!(
|
||||
super::inbox(&conn, act.clone()),
|
||||
Err(super::Error::Inbox(
|
||||
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
||||
))
|
||||
));
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_post() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (_, users, blogs) = fill_database(&conn);
|
||||
let act = json!({
|
||||
"id": "https://plu.me/comment/1/activity",
|
||||
"actor": users[0].ap_url,
|
||||
@@ -199,9 +283,9 @@ pub(crate) mod tests {
|
||||
"type": "Create",
|
||||
});
|
||||
|
||||
match super::inbox(&r, act).unwrap() {
|
||||
match super::inbox(&conn, act).unwrap() {
|
||||
super::InboxResult::Post(p) => {
|
||||
assert!(p.is_author(conn, users[0].id).unwrap());
|
||||
assert!(p.is_author(&conn, users[0].id).unwrap());
|
||||
assert_eq!(p.source, "Hello.".to_owned());
|
||||
assert_eq!(p.blog_id, blogs[0].id);
|
||||
assert_eq!(p.content, SafeString::new("Hello."));
|
||||
@@ -214,16 +298,123 @@ pub(crate) mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spoof_post() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (_, users, blogs) = fill_database(&conn);
|
||||
let act = json!({
|
||||
"id": "https://plu.me/comment/1/activity",
|
||||
"actor": users[0].ap_url,
|
||||
"object": {
|
||||
"type": "Article",
|
||||
"id": "https://plu.me/~/Blog/my-article",
|
||||
"attributedTo": [users[1].ap_url, blogs[0].ap_url],
|
||||
"content": "Hello.",
|
||||
"name": "My Article",
|
||||
"summary": "Bye.",
|
||||
"source": {
|
||||
"content": "Hello.",
|
||||
"mediaType": "text/markdown"
|
||||
},
|
||||
"published": "2014-12-12T12:12:12Z",
|
||||
"to": [plume_common::activity_pub::PUBLIC_VISIBILITY]
|
||||
},
|
||||
"type": "Create",
|
||||
});
|
||||
|
||||
assert!(matches!(
|
||||
super::inbox(&conn, act.clone()),
|
||||
Err(super::Error::Inbox(
|
||||
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
||||
))
|
||||
));
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spoof_post_by_object_with_id() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (_, users, blogs) = fill_database(&conn);
|
||||
let act = json!({
|
||||
"id": "https://plu.me/comment/1/activity",
|
||||
"actor": users[0].ap_url,
|
||||
"object": {
|
||||
"type": "Article",
|
||||
"id": "https://plu.me/~/Blog/my-article",
|
||||
"attributedTo": [
|
||||
{"id": users[1].ap_url},
|
||||
blogs[0].ap_url
|
||||
],
|
||||
"content": "Hello.",
|
||||
"name": "My Article",
|
||||
"summary": "Bye.",
|
||||
"source": {
|
||||
"content": "Hello.",
|
||||
"mediaType": "text/markdown"
|
||||
},
|
||||
"published": "2014-12-12T12:12:12Z",
|
||||
"to": [plume_common::activity_pub::PUBLIC_VISIBILITY]
|
||||
},
|
||||
"type": "Create",
|
||||
});
|
||||
|
||||
assert!(matches!(
|
||||
super::inbox(&conn, act.clone()),
|
||||
Err(super::Error::Inbox(
|
||||
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
||||
))
|
||||
));
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spoof_post_by_object_without_id() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (_, users, blogs) = fill_database(&conn);
|
||||
let act = json!({
|
||||
"id": "https://plu.me/comment/1/activity",
|
||||
"actor": users[0].ap_url,
|
||||
"object": {
|
||||
"type": "Article",
|
||||
"id": "https://plu.me/~/Blog/my-article",
|
||||
"attributedTo": [{}, blogs[0].ap_url],
|
||||
"content": "Hello.",
|
||||
"name": "My Article",
|
||||
"summary": "Bye.",
|
||||
"source": {
|
||||
"content": "Hello.",
|
||||
"mediaType": "text/markdown"
|
||||
},
|
||||
"published": "2014-12-12T12:12:12Z",
|
||||
"to": [plume_common::activity_pub::PUBLIC_VISIBILITY]
|
||||
},
|
||||
"type": "Create",
|
||||
});
|
||||
|
||||
assert!(matches!(
|
||||
super::inbox(&conn, act.clone()),
|
||||
Err(super::Error::Inbox(
|
||||
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
||||
))
|
||||
));
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_comment() {
|
||||
use crate::comments::*;
|
||||
|
||||
let r = rockets();
|
||||
let conn = &*r.conn;
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (posts, users, _) = fill_database(&r);
|
||||
let (posts, users, _) = fill_database(&conn);
|
||||
Comment::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewComment {
|
||||
content: SafeString::new("My comment"),
|
||||
in_response_to_id: None,
|
||||
@@ -243,7 +434,7 @@ pub(crate) mod tests {
|
||||
"object": "https://plu.me/comment/1",
|
||||
"type": "Delete",
|
||||
});
|
||||
assert!(super::inbox(&r, fail_act).is_err());
|
||||
assert!(super::inbox(&conn, fail_act).is_err());
|
||||
|
||||
let ok_act = json!({
|
||||
"id": "https://plu.me/comment/1/delete",
|
||||
@@ -251,17 +442,16 @@ pub(crate) mod tests {
|
||||
"object": "https://plu.me/comment/1",
|
||||
"type": "Delete",
|
||||
});
|
||||
assert!(super::inbox(&r, ok_act).is_ok());
|
||||
assert!(super::inbox(&conn, ok_act).is_ok());
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_post() {
|
||||
let r = rockets();
|
||||
let conn = &*r.conn;
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (posts, users, _) = fill_database(&r);
|
||||
let (posts, users, _) = fill_database(&conn);
|
||||
|
||||
let fail_act = json!({
|
||||
"id": "https://plu.me/comment/1/delete",
|
||||
@@ -269,7 +459,7 @@ pub(crate) mod tests {
|
||||
"object": posts[0].ap_url,
|
||||
"type": "Delete",
|
||||
});
|
||||
assert!(super::inbox(&r, fail_act).is_err());
|
||||
assert!(super::inbox(&conn, fail_act).is_err());
|
||||
|
||||
let ok_act = json!({
|
||||
"id": "https://plu.me/comment/1/delete",
|
||||
@@ -277,17 +467,16 @@ pub(crate) mod tests {
|
||||
"object": posts[0].ap_url,
|
||||
"type": "Delete",
|
||||
});
|
||||
assert!(super::inbox(&r, ok_act).is_ok());
|
||||
assert!(super::inbox(&conn, ok_act).is_ok());
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_user() {
|
||||
let r = rockets();
|
||||
let conn = &*r.conn;
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (_, users, _) = fill_database(&r);
|
||||
let (_, users, _) = fill_database(&conn);
|
||||
|
||||
let fail_act = json!({
|
||||
"id": "https://plu.me/@/Admin#delete",
|
||||
@@ -295,7 +484,7 @@ pub(crate) mod tests {
|
||||
"object": users[0].ap_url,
|
||||
"type": "Delete",
|
||||
});
|
||||
assert!(super::inbox(&r, fail_act).is_err());
|
||||
assert!(super::inbox(&conn, fail_act).is_err());
|
||||
|
||||
let ok_act = json!({
|
||||
"id": "https://plu.me/@/Admin#delete",
|
||||
@@ -303,8 +492,8 @@ pub(crate) mod tests {
|
||||
"object": users[0].ap_url,
|
||||
"type": "Delete",
|
||||
});
|
||||
assert!(super::inbox(&r, ok_act).is_ok());
|
||||
assert!(crate::users::User::get(conn, users[0].id).is_err());
|
||||
assert!(super::inbox(&conn, ok_act).is_ok());
|
||||
assert!(crate::users::User::get(&conn, users[0].id).is_err());
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@@ -312,10 +501,9 @@ pub(crate) mod tests {
|
||||
|
||||
#[test]
|
||||
fn follow() {
|
||||
let r = rockets();
|
||||
let conn = &*r.conn;
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (_, users, _) = fill_database(&r);
|
||||
let (_, users, _) = fill_database(&conn);
|
||||
|
||||
let act = json!({
|
||||
"id": "https://plu.me/follow/1",
|
||||
@@ -323,7 +511,7 @@ pub(crate) mod tests {
|
||||
"object": users[1].ap_url,
|
||||
"type": "Follow",
|
||||
});
|
||||
match super::inbox(&r, act).unwrap() {
|
||||
match super::inbox(&conn, act).unwrap() {
|
||||
InboxResult::Followed(f) => {
|
||||
assert_eq!(f.follower_id, users[0].id);
|
||||
assert_eq!(f.following_id, users[1].id);
|
||||
@@ -337,10 +525,9 @@ pub(crate) mod tests {
|
||||
|
||||
#[test]
|
||||
fn like() {
|
||||
let r = rockets();
|
||||
let conn = &*r.conn;
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (posts, users, _) = fill_database(&r);
|
||||
let (posts, users, _) = fill_database(&conn);
|
||||
|
||||
let act = json!({
|
||||
"id": "https://plu.me/like/1",
|
||||
@@ -348,7 +535,7 @@ pub(crate) mod tests {
|
||||
"object": posts[0].ap_url,
|
||||
"type": "Like",
|
||||
});
|
||||
match super::inbox(&r, act).unwrap() {
|
||||
match super::inbox(&conn, act).unwrap() {
|
||||
InboxResult::Liked(l) => {
|
||||
assert_eq!(l.user_id, users[1].id);
|
||||
assert_eq!(l.post_id, posts[0].id);
|
||||
@@ -364,13 +551,12 @@ pub(crate) mod tests {
|
||||
fn undo_reshare() {
|
||||
use crate::reshares::*;
|
||||
|
||||
let r = rockets();
|
||||
let conn = &*r.conn;
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (posts, users, _) = fill_database(&r);
|
||||
let (posts, users, _) = fill_database(&conn);
|
||||
|
||||
let announce = Reshare::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewReshare {
|
||||
post_id: posts[0].id,
|
||||
user_id: users[1].id,
|
||||
@@ -385,7 +571,7 @@ pub(crate) mod tests {
|
||||
"object": announce.ap_url,
|
||||
"type": "Undo",
|
||||
});
|
||||
assert!(super::inbox(&r, fail_act).is_err());
|
||||
assert!(super::inbox(&conn, fail_act).is_err());
|
||||
|
||||
let ok_act = json!({
|
||||
"id": "https://plu.me/undo/1",
|
||||
@@ -393,7 +579,7 @@ pub(crate) mod tests {
|
||||
"object": announce.ap_url,
|
||||
"type": "Undo",
|
||||
});
|
||||
assert!(super::inbox(&r, ok_act).is_ok());
|
||||
assert!(super::inbox(&conn, ok_act).is_ok());
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
@@ -402,13 +588,12 @@ pub(crate) mod tests {
|
||||
fn undo_follow() {
|
||||
use crate::follows::*;
|
||||
|
||||
let r = rockets();
|
||||
let conn = &*r.conn;
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (_, users, _) = fill_database(&r);
|
||||
let (_, users, _) = fill_database(&conn);
|
||||
|
||||
let follow = Follow::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewFollow {
|
||||
follower_id: users[0].id,
|
||||
following_id: users[1].id,
|
||||
@@ -423,7 +608,7 @@ pub(crate) mod tests {
|
||||
"object": follow.ap_url,
|
||||
"type": "Undo",
|
||||
});
|
||||
assert!(super::inbox(&r, fail_act).is_err());
|
||||
assert!(super::inbox(&conn, fail_act).is_err());
|
||||
|
||||
let ok_act = json!({
|
||||
"id": "https://plu.me/undo/1",
|
||||
@@ -431,7 +616,7 @@ pub(crate) mod tests {
|
||||
"object": follow.ap_url,
|
||||
"type": "Undo",
|
||||
});
|
||||
assert!(super::inbox(&r, ok_act).is_ok());
|
||||
assert!(super::inbox(&conn, ok_act).is_ok());
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
@@ -440,13 +625,12 @@ pub(crate) mod tests {
|
||||
fn undo_like() {
|
||||
use crate::likes::*;
|
||||
|
||||
let r = rockets();
|
||||
let conn = &*r.conn;
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (posts, users, _) = fill_database(&r);
|
||||
let (posts, users, _) = fill_database(&conn);
|
||||
|
||||
let like = Like::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewLike {
|
||||
post_id: posts[0].id,
|
||||
user_id: users[1].id,
|
||||
@@ -461,7 +645,7 @@ pub(crate) mod tests {
|
||||
"object": like.ap_url,
|
||||
"type": "Undo",
|
||||
});
|
||||
assert!(super::inbox(&r, fail_act).is_err());
|
||||
assert!(super::inbox(&conn, fail_act).is_err());
|
||||
|
||||
let ok_act = json!({
|
||||
"id": "https://plu.me/undo/1",
|
||||
@@ -469,17 +653,16 @@ pub(crate) mod tests {
|
||||
"object": like.ap_url,
|
||||
"type": "Undo",
|
||||
});
|
||||
assert!(super::inbox(&r, ok_act).is_ok());
|
||||
assert!(super::inbox(&conn, ok_act).is_ok());
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_post() {
|
||||
let r = rockets();
|
||||
let conn = &*r.conn;
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (posts, users, _) = fill_database(&r);
|
||||
let (posts, users, _) = fill_database(&conn);
|
||||
|
||||
let act = json!({
|
||||
"id": "https://plu.me/update/1",
|
||||
@@ -498,7 +681,7 @@ pub(crate) mod tests {
|
||||
"type": "Update",
|
||||
});
|
||||
|
||||
super::inbox(&r, act).unwrap();
|
||||
super::inbox(&conn, act).unwrap();
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -247,6 +247,7 @@ pub(crate) mod tests {
|
||||
use diesel::Connection;
|
||||
|
||||
pub(crate) fn fill_database(conn: &Conn) -> Vec<(NewInstance, Instance)> {
|
||||
diesel::delete(instances::table).execute(conn).unwrap();
|
||||
let res = vec");
|
||||
assert_eq!(
|
||||
inst.short_description_html,
|
||||
SafeString::new("<p><a href=\"#link\">short</a></p>\n")
|
||||
SafeString::new("<p dir=\"auto\"><a href=\"#link\">short</a></p>\n")
|
||||
);
|
||||
assert_eq!(inst.default_license, "CC-BY-SAO".to_owned());
|
||||
Ok(())
|
||||
|
||||
Regular → Executable
+20
-13
@@ -1,6 +1,7 @@
|
||||
#![feature(try_trait)]
|
||||
#![feature(never_type)]
|
||||
#![feature(proc_macro_hygiene)]
|
||||
#![feature(box_patterns)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
@@ -10,14 +11,17 @@ extern crate lazy_static;
|
||||
extern crate plume_macro;
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
#[macro_use]
|
||||
extern crate tantivy;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use plume_common::activity_pub::inbox::InboxError;
|
||||
use posts::PostEvent;
|
||||
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
|
||||
use users::UserEvent;
|
||||
|
||||
#[cfg(not(any(feature = "sqlite", feature = "postgres")))]
|
||||
compile_error!("Either feature \"sqlite\" or \"postgres\" must be enabled for this crate.");
|
||||
@@ -30,6 +34,19 @@ pub type Connection = diesel::SqliteConnection;
|
||||
#[cfg(all(not(feature = "sqlite"), feature = "postgres"))]
|
||||
pub type Connection = diesel::PgConnection;
|
||||
|
||||
pub(crate) static ACTOR_SYS: Lazy<ActorSystem> = Lazy::new(|| {
|
||||
SystemBuilder::new()
|
||||
.name("plume")
|
||||
.create()
|
||||
.expect("Failed to create actor system")
|
||||
});
|
||||
|
||||
pub(crate) static USER_CHAN: Lazy<ChannelRef<UserEvent>> =
|
||||
Lazy::new(|| channel("user_events", &*ACTOR_SYS).expect("Failed to create user channel"));
|
||||
|
||||
pub(crate) static POST_CHAN: Lazy<ChannelRef<PostEvent>> =
|
||||
Lazy::new(|| channel("post_events", &*ACTOR_SYS).expect("Failed to create post channel"));
|
||||
|
||||
/// All the possible errors that can be encoutered in this crate
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
@@ -281,12 +298,10 @@ pub fn ap_url(url: &str) -> String {
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
mod tests {
|
||||
use crate::{db_conn, migrations::IMPORTED_MIGRATIONS, search, Connection as Conn, CONFIG};
|
||||
use crate::{db_conn, migrations::IMPORTED_MIGRATIONS, Connection as Conn, CONFIG};
|
||||
use diesel::r2d2::ConnectionManager;
|
||||
use plume_common::utils::random_hex;
|
||||
use scheduled_thread_pool::ScheduledThreadPool;
|
||||
use std::env::temp_dir;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! part_eq {
|
||||
@@ -316,15 +331,6 @@ mod tests {
|
||||
pool
|
||||
};
|
||||
}
|
||||
|
||||
pub fn rockets() -> super::PlumeRocket {
|
||||
super::PlumeRocket {
|
||||
conn: db_conn::DbConn((*DB_POOL).get().unwrap()),
|
||||
searcher: Arc::new(search::tests::get_searcher(&CONFIG.search_tokenizers)),
|
||||
worker: Arc::new(ScheduledThreadPool::new(2)),
|
||||
user: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod admin;
|
||||
@@ -350,6 +356,7 @@ pub mod password_reset_requests;
|
||||
pub mod plume_rocket;
|
||||
pub mod post_authors;
|
||||
pub mod posts;
|
||||
pub mod remote_fetch_actor;
|
||||
pub mod reshares;
|
||||
pub mod safe_string;
|
||||
#[allow(unused_imports)]
|
||||
|
||||
+34
-25
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
notifications::*, posts::Post, schema::likes, timeline::*, users::User, Connection, Error,
|
||||
PlumeRocket, Result,
|
||||
db_conn::DbConn, notifications::*, posts::Post, schema::likes, timeline::*, users::User,
|
||||
Connection, Error, Result, CONFIG,
|
||||
};
|
||||
use activitypub::activity;
|
||||
use chrono::NaiveDateTime;
|
||||
@@ -83,64 +83,73 @@ impl Like {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, activity::Like, &PlumeRocket> for Post {
|
||||
impl AsObject<User, activity::Like, &DbConn> for Post {
|
||||
type Error = Error;
|
||||
type Output = Like;
|
||||
|
||||
fn activity(self, c: &PlumeRocket, actor: User, id: &str) -> Result<Like> {
|
||||
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Like> {
|
||||
let res = Like::insert(
|
||||
&c.conn,
|
||||
conn,
|
||||
NewLike {
|
||||
post_id: self.id,
|
||||
user_id: actor.id,
|
||||
ap_url: id.to_string(),
|
||||
},
|
||||
)?;
|
||||
res.notify(&c.conn)?;
|
||||
res.notify(conn)?;
|
||||
|
||||
Timeline::add_to_all_timelines(c, &self, Kind::Like(&actor))?;
|
||||
Timeline::add_to_all_timelines(conn, &self, Kind::Like(&actor))?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromId<PlumeRocket> for Like {
|
||||
impl FromId<DbConn> for Like {
|
||||
type Error = Error;
|
||||
type Object = activity::Like;
|
||||
|
||||
fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
|
||||
Like::find_by_ap_url(&c.conn, id)
|
||||
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
||||
Like::find_by_ap_url(conn, id)
|
||||
}
|
||||
|
||||
fn from_activity(c: &PlumeRocket, act: activity::Like) -> Result<Self> {
|
||||
fn from_activity(conn: &DbConn, act: activity::Like) -> Result<Self> {
|
||||
let res = Like::insert(
|
||||
&c.conn,
|
||||
conn,
|
||||
NewLike {
|
||||
post_id: Post::from_id(c, &act.like_props.object_link::<Id>()?, None)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
user_id: User::from_id(c, &act.like_props.actor_link::<Id>()?, None)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
post_id: Post::from_id(
|
||||
conn,
|
||||
&act.like_props.object_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
user_id: User::from_id(
|
||||
conn,
|
||||
&act.like_props.actor_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
ap_url: act.object_props.id_string()?,
|
||||
},
|
||||
)?;
|
||||
res.notify(&c.conn)?;
|
||||
res.notify(conn)?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, activity::Undo, &PlumeRocket> for Like {
|
||||
impl AsObject<User, activity::Undo, &DbConn> for Like {
|
||||
type Error = Error;
|
||||
type Output = ();
|
||||
|
||||
fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
|
||||
let conn = &*c.conn;
|
||||
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
||||
if actor.id == self.user_id {
|
||||
diesel::delete(&self).execute(conn)?;
|
||||
diesel::delete(&self).execute(&**conn)?;
|
||||
|
||||
// delete associated notification if any
|
||||
if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) {
|
||||
diesel::delete(¬if).execute(conn)?;
|
||||
if let Ok(notif) = Notification::find(&conn, notification_kind::LIKE, self.id) {
|
||||
diesel::delete(¬if).execute(&**conn)?;
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
|
||||
@@ -30,9 +30,9 @@ impl TryFrom<i32> for ListType {
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<i32> for ListType {
|
||||
fn into(self) -> i32 {
|
||||
match self {
|
||||
impl From<ListType> for i32 {
|
||||
fn from(list_type: ListType) -> Self {
|
||||
match list_type {
|
||||
ListType::User => 0,
|
||||
ListType::Blog => 1,
|
||||
ListType::Word => 2,
|
||||
@@ -246,22 +246,22 @@ impl List {
|
||||
private::ListElem::prefix_in_list(conn, self, word)
|
||||
}
|
||||
|
||||
/// Insert new users in a list
|
||||
// Insert new users in a list
|
||||
func! {add: add_users, User}
|
||||
|
||||
/// Insert new blogs in a list
|
||||
// Insert new blogs in a list
|
||||
func! {add: add_blogs, Blog}
|
||||
|
||||
/// Insert new words in a list
|
||||
// Insert new words in a list
|
||||
func! {add: add_words, Word}
|
||||
|
||||
/// Insert new prefixes in a list
|
||||
// Insert new prefixes in a list
|
||||
func! {add: add_prefixes, Prefix}
|
||||
|
||||
/// Get all users in the list
|
||||
// Get all users in the list
|
||||
func! {list: list_users, User, users}
|
||||
|
||||
/// Get all blogs in the list
|
||||
// Get all blogs in the list
|
||||
func! {list: list_blogs, Blog, blogs}
|
||||
|
||||
/// Get all words in the list
|
||||
|
||||
+57
-21
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
ap_url, instance::Instance, safe_string::SafeString, schema::medias, users::User, Connection,
|
||||
Error, PlumeRocket, Result,
|
||||
ap_url, db_conn::DbConn, instance::Instance, safe_string::SafeString, schema::medias,
|
||||
users::User, Connection, Error, Result, CONFIG,
|
||||
};
|
||||
use activitypub::object::Image;
|
||||
use askama_escape::escape;
|
||||
@@ -10,8 +10,14 @@ use plume_common::{
|
||||
activity_pub::{inbox::FromId, Id},
|
||||
utils::MediaProcessor,
|
||||
};
|
||||
use reqwest;
|
||||
use std::{fs, path::Path};
|
||||
use std::{
|
||||
fs::{self, DirBuilder},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tracing::warn;
|
||||
use url::Url;
|
||||
|
||||
const REMOTE_MEDIA_DIRECTORY: &str = "remote";
|
||||
|
||||
#[derive(Clone, Identifiable, Queryable)]
|
||||
pub struct Media {
|
||||
@@ -197,26 +203,28 @@ impl Media {
|
||||
}
|
||||
|
||||
// TODO: merge with save_remote?
|
||||
pub fn from_activity(c: &PlumeRocket, image: &Image) -> Result<Media> {
|
||||
let conn = &*c.conn;
|
||||
pub fn from_activity(conn: &DbConn, image: &Image) -> Result<Media> {
|
||||
let remote_url = image.object_props.url_string().ok()?;
|
||||
let ext = remote_url
|
||||
.rsplit('.')
|
||||
.next()
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_else(|| String::from("png"));
|
||||
let path = Path::new(&super::CONFIG.media_directory).join(format!(
|
||||
"{}.{}",
|
||||
GUID::rand().to_string(),
|
||||
ext
|
||||
));
|
||||
let path = determine_mirror_file_path(&remote_url);
|
||||
let parent = path.parent()?;
|
||||
if !parent.is_dir() {
|
||||
DirBuilder::new().recursive(true).create(parent)?;
|
||||
}
|
||||
|
||||
let mut dest = fs::File::create(path.clone()).ok()?;
|
||||
reqwest::get(remote_url.as_str())
|
||||
.ok()?
|
||||
.copy_to(&mut dest)
|
||||
.ok()?;
|
||||
// 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()?;
|
||||
|
||||
// TODO: upsert
|
||||
Media::insert(
|
||||
conn,
|
||||
NewMedia {
|
||||
@@ -227,7 +235,7 @@ impl Media {
|
||||
sensitive: image.object_props.summary_string().is_ok(),
|
||||
content_warning: image.object_props.summary_string().ok(),
|
||||
owner_id: User::from_id(
|
||||
c,
|
||||
conn,
|
||||
image
|
||||
.object_props
|
||||
.attributed_to_link_vec::<Id>()
|
||||
@@ -236,6 +244,7 @@ impl Media {
|
||||
.next()?
|
||||
.as_ref(),
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
@@ -257,6 +266,33 @@ impl Media {
|
||||
}
|
||||
}
|
||||
|
||||
fn determine_mirror_file_path(url: &str) -> PathBuf {
|
||||
let mut file_path = Path::new(&super::CONFIG.media_directory).join(REMOTE_MEDIA_DIRECTORY);
|
||||
Url::parse(url)
|
||||
.map(|url| {
|
||||
if !url.has_host() {
|
||||
return;
|
||||
}
|
||||
file_path.push(url.host_str().unwrap());
|
||||
for segment in url.path_segments().expect("FIXME") {
|
||||
file_path.push(segment);
|
||||
}
|
||||
// TODO: handle query
|
||||
// HINT: Use characters which must be percent-encoded in path as separator between path and query
|
||||
// HINT: handle extension
|
||||
})
|
||||
.unwrap_or_else(|err| {
|
||||
warn!("Failed to parse url: {} {}", &url, err);
|
||||
let ext = url
|
||||
.rsplit('.')
|
||||
.next()
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_else(|| String::from("png"));
|
||||
file_path.push(format!("{}.{}", GUID::rand().to_string(), ext));
|
||||
});
|
||||
file_path
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
comments::Comment, notifications::*, posts::Post, schema::mentions, users::User, Connection,
|
||||
Error, PlumeRocket, Result,
|
||||
comments::Comment, db_conn::DbConn, notifications::*, posts::Post, schema::mentions,
|
||||
users::User, Connection, Error, Result,
|
||||
};
|
||||
use activitypub::link;
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
@@ -52,8 +52,8 @@ impl Mention {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_activity(c: &PlumeRocket, ment: &str) -> Result<link::Mention> {
|
||||
let user = User::find_by_fqn(c, ment)?;
|
||||
pub fn build_activity(conn: &DbConn, ment: &str) -> Result<link::Mention> {
|
||||
let user = User::find_by_fqn(conn, ment)?;
|
||||
let mut mention = link::Mention::default();
|
||||
mention.link_props.set_href_string(user.ap_url)?;
|
||||
mention.link_props.set_name_string(format!("@{}", ment))?;
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::{Connection, Error, Result};
|
||||
use diesel::connection::{Connection as Conn, SimpleConnection};
|
||||
use migrations_internals::{setup_database, MigrationConnection};
|
||||
use std::path::Path;
|
||||
use tracing::info;
|
||||
|
||||
#[allow(dead_code)] //variants might not be constructed if not required by current migrations
|
||||
enum Action {
|
||||
@@ -26,7 +27,7 @@ struct ComplexMigration {
|
||||
|
||||
impl ComplexMigration {
|
||||
fn run(&self, conn: &Connection, path: &Path) -> Result<()> {
|
||||
println!("Running migration {}", self.name);
|
||||
info!("Running migration {}", self.name);
|
||||
for step in self.up {
|
||||
step.run(conn, path)?
|
||||
}
|
||||
@@ -34,7 +35,7 @@ impl ComplexMigration {
|
||||
}
|
||||
|
||||
fn revert(&self, conn: &Connection, path: &Path) -> Result<()> {
|
||||
println!("Reverting migration {}", self.name);
|
||||
info!("Reverting migration {}", self.name);
|
||||
for step in self.down {
|
||||
step.run(conn, path)?
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ pub use self::module::PlumeRocket;
|
||||
|
||||
#[cfg(not(test))]
|
||||
mod module {
|
||||
use crate::{db_conn::DbConn, search, users};
|
||||
use crate::{search, users};
|
||||
use rocket::{
|
||||
request::{self, FlashMessage, FromRequest, Request},
|
||||
Outcome, State,
|
||||
@@ -12,7 +12,6 @@ mod module {
|
||||
|
||||
/// Common context needed by most routes and operations on models
|
||||
pub struct PlumeRocket {
|
||||
pub conn: DbConn,
|
||||
pub intl: rocket_i18n::I18n,
|
||||
pub user: Option<users::User>,
|
||||
pub searcher: Arc<search::Searcher>,
|
||||
@@ -24,14 +23,12 @@ mod module {
|
||||
type Error = ();
|
||||
|
||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<PlumeRocket, ()> {
|
||||
let conn = request.guard::<DbConn>()?;
|
||||
let intl = request.guard::<rocket_i18n::I18n>()?;
|
||||
let user = request.guard::<users::User>().succeeded();
|
||||
let worker = request.guard::<'_, State<'_, Arc<ScheduledThreadPool>>>()?;
|
||||
let searcher = request.guard::<'_, State<'_, Arc<search::Searcher>>>()?;
|
||||
let flash_msg = request.guard::<FlashMessage<'_, '_>>().succeeded();
|
||||
Outcome::Success(PlumeRocket {
|
||||
conn,
|
||||
intl,
|
||||
user,
|
||||
flash_msg: flash_msg.map(|f| (f.name().into(), f.msg().into())),
|
||||
@@ -44,7 +41,7 @@ mod module {
|
||||
|
||||
#[cfg(test)]
|
||||
mod module {
|
||||
use crate::{db_conn::DbConn, search, users};
|
||||
use crate::{search, users};
|
||||
use rocket::{
|
||||
request::{self, FromRequest, Request},
|
||||
Outcome, State,
|
||||
@@ -54,7 +51,6 @@ mod module {
|
||||
|
||||
/// Common context needed by most routes and operations on models
|
||||
pub struct PlumeRocket {
|
||||
pub conn: DbConn,
|
||||
pub user: Option<users::User>,
|
||||
pub searcher: Arc<search::Searcher>,
|
||||
pub worker: Arc<ScheduledThreadPool>,
|
||||
@@ -64,12 +60,10 @@ mod module {
|
||||
type Error = ();
|
||||
|
||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<PlumeRocket, ()> {
|
||||
let conn = request.guard::<DbConn>()?;
|
||||
let user = request.guard::<users::User>().succeeded();
|
||||
let worker = request.guard::<'_, State<'_, Arc<ScheduledThreadPool>>>()?;
|
||||
let searcher = request.guard::<'_, State<'_, Arc<search::Searcher>>>()?;
|
||||
Outcome::Success(PlumeRocket {
|
||||
conn,
|
||||
user,
|
||||
worker: worker.clone(),
|
||||
searcher: searcher.clone(),
|
||||
|
||||
+135
-63
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
ap_url, blogs::Blog, instance::Instance, medias::Media, mentions::Mention, post_authors::*,
|
||||
safe_string::SafeString, schema::posts, search::Searcher, tags::*, timeline::*, users::User,
|
||||
Connection, Error, PlumeRocket, Result, CONFIG,
|
||||
ap_url, blogs::Blog, db_conn::DbConn, instance::Instance, medias::Media, mentions::Mention,
|
||||
post_authors::*, safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User,
|
||||
Connection, Error, PostEvent::*, Result, CONFIG, POST_CHAN,
|
||||
};
|
||||
use activitypub::{
|
||||
activity::{Create, Delete, Update},
|
||||
@@ -11,7 +11,8 @@ use activitypub::{
|
||||
};
|
||||
use chrono::{NaiveDateTime, TimeZone, Utc};
|
||||
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||
use heck::{CamelCase, KebabCase};
|
||||
use heck::KebabCase;
|
||||
use once_cell::sync::Lazy;
|
||||
use plume_common::{
|
||||
activity_pub::{
|
||||
inbox::{AsObject, FromId},
|
||||
@@ -19,12 +20,15 @@ use plume_common::{
|
||||
},
|
||||
utils::md_to_html,
|
||||
};
|
||||
use serde_json;
|
||||
use std::collections::HashSet;
|
||||
use riker::actors::{Publish, Tell};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub type LicensedArticle = CustomObject<Licensed, Article>;
|
||||
|
||||
#[derive(Queryable, Identifiable, Clone, AsChangeset)]
|
||||
static BLOG_FQN_CACHE: Lazy<Mutex<HashMap<i32, String>>> = Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
#[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)]
|
||||
#[changeset_options(treat_none_as_null = "true")]
|
||||
pub struct Post {
|
||||
pub id: i32,
|
||||
@@ -63,7 +67,7 @@ impl Post {
|
||||
find_by!(posts, find_by_ap_url, ap_url as &str);
|
||||
|
||||
last!(posts);
|
||||
pub fn insert(conn: &Connection, new: NewPost, searcher: &Searcher) -> Result<Self> {
|
||||
pub fn insert(conn: &Connection, new: NewPost) -> Result<Self> {
|
||||
diesel::insert_into(posts::table)
|
||||
.values(new)
|
||||
.execute(conn)?;
|
||||
@@ -78,23 +82,29 @@ impl Post {
|
||||
let _: Post = post.save_changes(conn)?;
|
||||
}
|
||||
|
||||
searcher.add_document(conn, &post)?;
|
||||
if post.published {
|
||||
post.publish_published();
|
||||
}
|
||||
|
||||
Ok(post)
|
||||
}
|
||||
|
||||
pub fn update(&self, conn: &Connection, searcher: &Searcher) -> Result<Self> {
|
||||
pub fn update(&self, conn: &Connection) -> Result<Self> {
|
||||
diesel::update(self).set(self).execute(conn)?;
|
||||
let post = Self::get(conn, self.id)?;
|
||||
searcher.update_document(conn, &post)?;
|
||||
// TODO: Call publish_published() when newly published
|
||||
if post.published {
|
||||
self.publish_updated();
|
||||
}
|
||||
Ok(post)
|
||||
}
|
||||
|
||||
pub fn delete(&self, conn: &Connection, searcher: &Searcher) -> Result<()> {
|
||||
pub fn delete(&self, conn: &Connection) -> Result<()> {
|
||||
for m in Mention::list_for_post(&conn, self.id)? {
|
||||
m.delete(conn)?;
|
||||
}
|
||||
diesel::delete(self).execute(conn)?;
|
||||
searcher.delete_document(self);
|
||||
self.publish_deleted();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -124,8 +134,7 @@ impl Post {
|
||||
.filter(posts::published.eq(true))
|
||||
.count()
|
||||
.load(conn)?
|
||||
.iter()
|
||||
.next()
|
||||
.get(0)
|
||||
.cloned()
|
||||
.ok_or(Error::NotFound)
|
||||
}
|
||||
@@ -269,6 +278,24 @@ impl Post {
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// This method exists for use in templates to reduce database access.
|
||||
/// This should not be used for other purpose.
|
||||
///
|
||||
/// This caches query result. The best way to cache query result is holding it in `Post`s field
|
||||
/// but Diesel doesn't allow it currently.
|
||||
/// If sometime Diesel allow it, this method should be removed.
|
||||
pub fn get_blog_fqn(&self, conn: &Connection) -> String {
|
||||
if let Some(blog_fqn) = BLOG_FQN_CACHE.lock().unwrap().get(&self.blog_id) {
|
||||
return blog_fqn.to_string();
|
||||
}
|
||||
let blog_fqn = self.get_blog(conn).unwrap().fqn;
|
||||
BLOG_FQN_CACHE
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(self.blog_id, blog_fqn.clone());
|
||||
blog_fqn
|
||||
}
|
||||
|
||||
pub fn count_likes(&self, conn: &Connection) -> Result<i64> {
|
||||
use crate::schema::likes;
|
||||
likes::table
|
||||
@@ -288,17 +315,16 @@ impl Post {
|
||||
}
|
||||
|
||||
pub fn get_receivers_urls(&self, conn: &Connection) -> Result<Vec<String>> {
|
||||
let followers = self
|
||||
Ok(self
|
||||
.get_authors(conn)?
|
||||
.into_iter()
|
||||
.filter_map(|a| a.get_followers(conn).ok())
|
||||
.collect::<Vec<Vec<User>>>();
|
||||
Ok(followers.into_iter().fold(vec![], |mut acc, f| {
|
||||
for x in f {
|
||||
acc.push(x.ap_url);
|
||||
}
|
||||
acc
|
||||
}))
|
||||
.fold(vec![], |mut acc, f| {
|
||||
for x in f {
|
||||
acc.push(x.ap_url);
|
||||
}
|
||||
acc
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<LicensedArticle> {
|
||||
@@ -548,19 +574,48 @@ impl Post {
|
||||
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?;
|
||||
Ok(act)
|
||||
}
|
||||
|
||||
fn publish_published(&self) {
|
||||
POST_CHAN.tell(
|
||||
Publish {
|
||||
msg: PostPublished(Arc::new(self.clone())),
|
||||
topic: "post.published".into(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn publish_updated(&self) {
|
||||
POST_CHAN.tell(
|
||||
Publish {
|
||||
msg: PostUpdated(Arc::new(self.clone())),
|
||||
topic: "post.updated".into(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn publish_deleted(&self) {
|
||||
POST_CHAN.tell(
|
||||
Publish {
|
||||
msg: PostDeleted(Arc::new(self.clone())),
|
||||
topic: "post.deleted".into(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromId<PlumeRocket> for Post {
|
||||
impl FromId<DbConn> for Post {
|
||||
type Error = Error;
|
||||
type Object = LicensedArticle;
|
||||
|
||||
fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
|
||||
Self::find_by_ap_url(&c.conn, id)
|
||||
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
||||
Self::find_by_ap_url(conn, id)
|
||||
}
|
||||
|
||||
fn from_activity(c: &PlumeRocket, article: LicensedArticle) -> Result<Self> {
|
||||
let conn = &*c.conn;
|
||||
let searcher = &c.searcher;
|
||||
fn from_activity(conn: &DbConn, article: LicensedArticle) -> Result<Self> {
|
||||
let conn = conn;
|
||||
let license = article.custom_props.license_string().unwrap_or_default();
|
||||
let article = article.object;
|
||||
|
||||
@@ -570,12 +625,15 @@ impl FromId<PlumeRocket> for Post {
|
||||
.into_iter()
|
||||
.fold((None, vec![]), |(blog, mut authors), link| {
|
||||
let url = link;
|
||||
match User::from_id(&c, &url, None) {
|
||||
match User::from_id(conn, &url, None, CONFIG.proxy()) {
|
||||
Ok(u) => {
|
||||
authors.push(u);
|
||||
(blog, authors)
|
||||
}
|
||||
Err(_) => (blog.or_else(|| Blog::from_id(&c, &url, None).ok()), authors),
|
||||
Err(_) => (
|
||||
blog.or_else(|| Blog::from_id(conn, &url, None, CONFIG.proxy()).ok()),
|
||||
authors,
|
||||
),
|
||||
}
|
||||
});
|
||||
|
||||
@@ -583,9 +641,10 @@ impl FromId<PlumeRocket> for Post {
|
||||
.object_props
|
||||
.icon_object::<Image>()
|
||||
.ok()
|
||||
.and_then(|img| Media::from_activity(&c, &img).ok().map(|m| m.id));
|
||||
.and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id));
|
||||
|
||||
let title = article.object_props.name_string()?;
|
||||
// TODO: upsert
|
||||
let post = Post::insert(
|
||||
conn,
|
||||
NewPost {
|
||||
@@ -605,7 +664,6 @@ impl FromId<PlumeRocket> for Post {
|
||||
source: article.ap_object_props.source_object::<Source>()?.content,
|
||||
cover_id: cover,
|
||||
},
|
||||
searcher,
|
||||
)?;
|
||||
|
||||
for author in authors {
|
||||
@@ -622,7 +680,6 @@ impl FromId<PlumeRocket> for Post {
|
||||
let mut hashtags = md_to_html(&post.source, None, false, None)
|
||||
.2
|
||||
.into_iter()
|
||||
.map(|s| s.to_camel_case())
|
||||
.collect::<HashSet<_>>();
|
||||
if let Some(serde_json::Value::Array(tags)) = article.object_props.tag {
|
||||
for tag in tags {
|
||||
@@ -645,33 +702,33 @@ impl FromId<PlumeRocket> for Post {
|
||||
}
|
||||
}
|
||||
|
||||
Timeline::add_to_all_timelines(c, &post, Kind::Original)?;
|
||||
Timeline::add_to_all_timelines(conn, &post, Kind::Original)?;
|
||||
|
||||
Ok(post)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, Create, &PlumeRocket> for Post {
|
||||
impl AsObject<User, Create, &DbConn> for Post {
|
||||
type Error = Error;
|
||||
type Output = Post;
|
||||
|
||||
fn activity(self, _c: &PlumeRocket, _actor: User, _id: &str) -> Result<Post> {
|
||||
fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result<Post> {
|
||||
// TODO: check that _actor is actually one of the author?
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, Delete, &PlumeRocket> for Post {
|
||||
impl AsObject<User, Delete, &DbConn> for Post {
|
||||
type Error = Error;
|
||||
type Output = ();
|
||||
|
||||
fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
|
||||
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
||||
let can_delete = self
|
||||
.get_authors(&c.conn)?
|
||||
.get_authors(conn)?
|
||||
.into_iter()
|
||||
.any(|a| actor.id == a.id);
|
||||
if can_delete {
|
||||
self.delete(&c.conn, &c.searcher).map(|_| ())
|
||||
self.delete(conn).map(|_| ())
|
||||
} else {
|
||||
Err(Error::Unauthorized)
|
||||
}
|
||||
@@ -689,16 +746,16 @@ pub struct PostUpdate {
|
||||
pub tags: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl FromId<PlumeRocket> for PostUpdate {
|
||||
impl FromId<DbConn> for PostUpdate {
|
||||
type Error = Error;
|
||||
type Object = LicensedArticle;
|
||||
|
||||
fn from_db(_: &PlumeRocket, _: &str) -> Result<Self> {
|
||||
fn from_db(_: &DbConn, _: &str) -> Result<Self> {
|
||||
// Always fail because we always want to deserialize the AP object
|
||||
Err(Error::NotFound)
|
||||
}
|
||||
|
||||
fn from_activity(c: &PlumeRocket, updated: LicensedArticle) -> Result<Self> {
|
||||
fn from_activity(conn: &DbConn, updated: LicensedArticle) -> Result<Self> {
|
||||
Ok(PostUpdate {
|
||||
ap_url: updated.object.object_props.id_string()?,
|
||||
title: updated.object.object_props.name_string().ok(),
|
||||
@@ -709,7 +766,7 @@ impl FromId<PlumeRocket> for PostUpdate {
|
||||
.object_props
|
||||
.icon_object::<Image>()
|
||||
.ok()
|
||||
.and_then(|img| Media::from_activity(&c, &img).ok().map(|m| m.id)),
|
||||
.and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id)),
|
||||
source: updated
|
||||
.object
|
||||
.ap_object_props
|
||||
@@ -722,14 +779,13 @@ impl FromId<PlumeRocket> for PostUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, Update, &PlumeRocket> for PostUpdate {
|
||||
impl AsObject<User, Update, &DbConn> for PostUpdate {
|
||||
type Error = Error;
|
||||
type Output = ();
|
||||
|
||||
fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
|
||||
let conn = &*c.conn;
|
||||
let searcher = &c.searcher;
|
||||
let mut post = Post::from_id(c, &self.ap_url, None).map_err(|(_, e)| e)?;
|
||||
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
||||
let mut post =
|
||||
Post::from_id(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?;
|
||||
|
||||
if !post.is_author(conn, actor.id)? {
|
||||
// TODO: maybe the author was added in the meantime
|
||||
@@ -762,7 +818,6 @@ impl AsObject<User, Update, &PlumeRocket> for PostUpdate {
|
||||
let mut txt_hashtags = md_to_html(&post.source, None, false, None)
|
||||
.2
|
||||
.into_iter()
|
||||
.map(|s| s.to_camel_case())
|
||||
.collect::<HashSet<_>>();
|
||||
if let Some(serde_json::Value::Array(mention_tags)) = self.tags {
|
||||
let mut mentions = vec![];
|
||||
@@ -791,7 +846,7 @@ impl AsObject<User, Update, &PlumeRocket> for PostUpdate {
|
||||
post.update_hashtags(conn, hashtags)?;
|
||||
}
|
||||
|
||||
post.update(conn, searcher)?;
|
||||
post.update(conn)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -802,24 +857,42 @@ impl IntoId for Post {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PostEvent {
|
||||
PostPublished(Arc<Post>),
|
||||
PostUpdated(Arc<Post>),
|
||||
PostDeleted(Arc<Post>),
|
||||
}
|
||||
|
||||
impl From<PostEvent> for Arc<Post> {
|
||||
fn from(event: PostEvent) -> Self {
|
||||
use PostEvent::*;
|
||||
|
||||
match event {
|
||||
PostPublished(post) => post,
|
||||
PostUpdated(post) => post,
|
||||
PostDeleted(post) => post,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::inbox::{inbox, tests::fill_database, InboxResult};
|
||||
use crate::safe_string::SafeString;
|
||||
use crate::tests::rockets;
|
||||
use crate::tests::db;
|
||||
use diesel::Connection;
|
||||
|
||||
// creates a post, get it's Create activity, delete the post,
|
||||
// "send" the Create to the inbox, and check it works
|
||||
#[test]
|
||||
fn self_federation() {
|
||||
let r = rockets();
|
||||
let conn = &*r.conn;
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (_, users, blogs) = fill_database(&r);
|
||||
let (_, users, blogs) = fill_database(&conn);
|
||||
let post = Post::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewPost {
|
||||
blog_id: blogs[0].id,
|
||||
slug: "yo".into(),
|
||||
@@ -833,23 +906,22 @@ mod tests {
|
||||
source: "Hello".into(),
|
||||
cover_id: None,
|
||||
},
|
||||
&r.searcher,
|
||||
)
|
||||
.unwrap();
|
||||
PostAuthor::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewPostAuthor {
|
||||
post_id: post.id,
|
||||
author_id: users[0].id,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let create = post.create_activity(conn).unwrap();
|
||||
post.delete(conn, &r.searcher).unwrap();
|
||||
let create = post.create_activity(&conn).unwrap();
|
||||
post.delete(&conn).unwrap();
|
||||
|
||||
match inbox(&r, serde_json::to_value(create).unwrap()).unwrap() {
|
||||
match inbox(&conn, serde_json::to_value(create).unwrap()).unwrap() {
|
||||
InboxResult::Post(p) => {
|
||||
assert!(p.is_author(conn, users[0].id).unwrap());
|
||||
assert!(p.is_author(&conn, users[0].id).unwrap());
|
||||
assert_eq!(p.source, "Hello".to_owned());
|
||||
assert_eq!(p.blog_id, blogs[0].id);
|
||||
assert_eq!(p.content, SafeString::new("Hello"));
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
use crate::{
|
||||
db_conn::{DbConn, DbPool},
|
||||
follows,
|
||||
posts::{LicensedArticle, Post},
|
||||
users::{User, UserEvent},
|
||||
ACTOR_SYS, CONFIG, USER_CHAN,
|
||||
};
|
||||
use activitypub::activity::Create;
|
||||
use plume_common::activity_pub::inbox::FromId;
|
||||
use riker::actors::{Actor, ActorFactoryArgs, ActorRefFactory, Context, Sender, Subscribe, Tell};
|
||||
use std::sync::Arc;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
pub struct RemoteFetchActor {
|
||||
conn: DbPool,
|
||||
}
|
||||
|
||||
impl RemoteFetchActor {
|
||||
pub fn init(conn: DbPool) {
|
||||
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()),
|
||||
topic: "*".into(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn recv(&mut self, _ctx: &Context<Self::Msg>, msg: Self::Msg, _sender: Sender) {
|
||||
use UserEvent::*;
|
||||
|
||||
match msg {
|
||||
RemoteUserFound(user) => match self.conn.get() {
|
||||
Ok(conn) => {
|
||||
let conn = DbConn(conn);
|
||||
// Don't call these functions in parallel
|
||||
// for the case database connections limit is too small
|
||||
fetch_and_cache_articles(&user, &conn);
|
||||
fetch_and_cache_followers(&user, &conn);
|
||||
if user.needs_update() {
|
||||
fetch_and_cache_user(&user, &conn);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
error!("Failed to get database connection");
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ActorFactoryArgs<DbPool> for RemoteFetchActor {
|
||||
fn create_args(conn: DbPool) -> Self {
|
||||
Self { conn }
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_and_cache_articles(user: &Arc<User>, conn: &DbConn) {
|
||||
let create_acts = user.fetch_outbox::<Create>();
|
||||
match create_acts {
|
||||
Ok(create_acts) => {
|
||||
for create_act in create_acts {
|
||||
match create_act.create_props.object_object::<LicensedArticle>() {
|
||||
Ok(article) => {
|
||||
Post::from_activity(conn, article)
|
||||
.expect("Article from remote user couldn't be saved");
|
||||
info!("Fetched article from remote user");
|
||||
}
|
||||
Err(e) => warn!("Error while fetching articles in background: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to fetch outboxes: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_and_cache_followers(user: &Arc<User>, conn: &DbConn) {
|
||||
let follower_ids = user.fetch_followers_ids();
|
||||
match follower_ids {
|
||||
Ok(user_ids) => {
|
||||
for user_id in user_ids {
|
||||
let follower = User::from_id(conn, &user_id, None, CONFIG.proxy());
|
||||
match follower {
|
||||
Ok(follower) => {
|
||||
let inserted = follows::Follow::insert(
|
||||
conn,
|
||||
follows::NewFollow {
|
||||
follower_id: follower.id,
|
||||
following_id: user.id,
|
||||
ap_url: String::new(),
|
||||
},
|
||||
);
|
||||
if inserted.is_err() {
|
||||
error!("Couldn't save follower for remote user: {:?}", user_id);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Couldn't fetch follower: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to fetch follower: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_and_cache_user(user: &Arc<User>, conn: &DbConn) {
|
||||
if user.refetch(conn).is_err() {
|
||||
error!("Couldn't update user info: {:?}", user);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
notifications::*, posts::Post, schema::reshares, timeline::*, users::User, Connection, Error,
|
||||
PlumeRocket, Result,
|
||||
db_conn::DbConn, notifications::*, posts::Post, schema::reshares, timeline::*, users::User,
|
||||
Connection, Error, Result, CONFIG,
|
||||
};
|
||||
use activitypub::activity::{Announce, Undo};
|
||||
use chrono::NaiveDateTime;
|
||||
@@ -107,12 +107,12 @@ impl Reshare {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, Announce, &PlumeRocket> for Post {
|
||||
impl AsObject<User, Announce, &DbConn> for Post {
|
||||
type Error = Error;
|
||||
type Output = Reshare;
|
||||
|
||||
fn activity(self, c: &PlumeRocket, actor: User, id: &str) -> Result<Reshare> {
|
||||
let conn = &*c.conn;
|
||||
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Reshare> {
|
||||
let conn = conn;
|
||||
let reshare = Reshare::insert(
|
||||
conn,
|
||||
NewReshare {
|
||||
@@ -123,49 +123,58 @@ impl AsObject<User, Announce, &PlumeRocket> for Post {
|
||||
)?;
|
||||
reshare.notify(conn)?;
|
||||
|
||||
Timeline::add_to_all_timelines(c, &self, Kind::Reshare(&actor))?;
|
||||
Timeline::add_to_all_timelines(conn, &self, Kind::Reshare(&actor))?;
|
||||
Ok(reshare)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromId<PlumeRocket> for Reshare {
|
||||
impl FromId<DbConn> for Reshare {
|
||||
type Error = Error;
|
||||
type Object = Announce;
|
||||
|
||||
fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
|
||||
Reshare::find_by_ap_url(&c.conn, id)
|
||||
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
||||
Reshare::find_by_ap_url(conn, id)
|
||||
}
|
||||
|
||||
fn from_activity(c: &PlumeRocket, act: Announce) -> Result<Self> {
|
||||
fn from_activity(conn: &DbConn, act: Announce) -> Result<Self> {
|
||||
let res = Reshare::insert(
|
||||
&c.conn,
|
||||
conn,
|
||||
NewReshare {
|
||||
post_id: Post::from_id(c, &act.announce_props.object_link::<Id>()?, None)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
user_id: User::from_id(c, &act.announce_props.actor_link::<Id>()?, None)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
post_id: Post::from_id(
|
||||
conn,
|
||||
&act.announce_props.object_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
user_id: User::from_id(
|
||||
conn,
|
||||
&act.announce_props.actor_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
ap_url: act.object_props.id_string()?,
|
||||
},
|
||||
)?;
|
||||
res.notify(&c.conn)?;
|
||||
res.notify(conn)?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, Undo, &PlumeRocket> for Reshare {
|
||||
impl AsObject<User, Undo, &DbConn> for Reshare {
|
||||
type Error = Error;
|
||||
type Output = ();
|
||||
|
||||
fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
|
||||
let conn = &*c.conn;
|
||||
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
||||
if actor.id == self.user_id {
|
||||
diesel::delete(&self).execute(conn)?;
|
||||
diesel::delete(&self).execute(&**conn)?;
|
||||
|
||||
// delete associated notification if any
|
||||
if let Ok(notif) = Notification::find(&conn, notification_kind::RESHARE, self.id) {
|
||||
diesel::delete(¬if).execute(conn)?;
|
||||
diesel::delete(¬if).execute(&**conn)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -11,14 +11,13 @@ use std::{
|
||||
borrow::{Borrow, Cow},
|
||||
fmt::{self, Display},
|
||||
io::Write,
|
||||
iter,
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
static ref CLEAN: Builder<'static> = {
|
||||
let mut b = Builder::new();
|
||||
b.add_generic_attributes(iter::once("id"))
|
||||
b.add_generic_attributes(&["id", "dir"])
|
||||
.add_tags(&["iframe", "video", "audio", "label", "input"])
|
||||
.id_prefix(Some("postcontent-"))
|
||||
.url_relative(UrlRelative::Custom(Box::new(url_add_prefix)))
|
||||
@@ -82,6 +81,7 @@ lazy_static! {
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn url_add_prefix(url: &str) -> Option<Cow<'_, str>> {
|
||||
if url.starts_with('#') && !url.starts_with("#postcontent-") {
|
||||
//if start with an #
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
use super::Searcher;
|
||||
use crate::{db_conn::DbPool, posts::PostEvent, ACTOR_SYS, POST_CHAN};
|
||||
use riker::actors::{Actor, ActorFactoryArgs, ActorRefFactory, Context, Sender, Subscribe, Tell};
|
||||
use std::sync::Arc;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use tracing::error;
|
||||
|
||||
pub struct SearchActor {
|
||||
searcher: Arc<Searcher>,
|
||||
conn: DbPool,
|
||||
}
|
||||
|
||||
impl SearchActor {
|
||||
pub fn init(searcher: Arc<Searcher>, conn: DbPool) {
|
||||
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()),
|
||||
topic: "*".into(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn recv(&mut self, _ctx: &Context<Self::Msg>, msg: Self::Msg, _sender: Sender) {
|
||||
use PostEvent::*;
|
||||
|
||||
// Wait for transaction commited
|
||||
sleep(Duration::from_millis(500));
|
||||
|
||||
match msg {
|
||||
PostPublished(post) => {
|
||||
let conn = self.conn.get();
|
||||
match conn {
|
||||
Ok(conn) => {
|
||||
self.searcher
|
||||
.add_document(&conn, &post)
|
||||
.unwrap_or_else(|e| error!("{:?}", e));
|
||||
}
|
||||
_ => {
|
||||
error!("Failed to get database connection");
|
||||
}
|
||||
}
|
||||
}
|
||||
PostUpdated(post) => {
|
||||
let conn = self.conn.get();
|
||||
match conn {
|
||||
Ok(_) => {
|
||||
self.searcher
|
||||
.update_document(&conn.unwrap(), &post)
|
||||
.unwrap_or_else(|e| error!("{:?}", e));
|
||||
}
|
||||
_ => {
|
||||
error!("Failed to get database connection");
|
||||
}
|
||||
}
|
||||
}
|
||||
PostDeleted(post) => self.searcher.delete_document(&post),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ActorFactoryArgs<(Arc<Searcher>, DbPool)> for SearchActor {
|
||||
fn create_args((searcher, conn): (Arc<Searcher>, DbPool)) -> Self {
|
||||
Self { searcher, conn }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::diesel::Connection;
|
||||
use crate::{
|
||||
blog_authors::{BlogAuthor, NewBlogAuthor},
|
||||
blogs::{Blog, NewBlog},
|
||||
db_conn::{DbPool, PragmaForeignKey},
|
||||
instance::{Instance, NewInstance},
|
||||
post_authors::{NewPostAuthor, PostAuthor},
|
||||
posts::{NewPost, Post},
|
||||
safe_string::SafeString,
|
||||
search::{actor::SearchActor, tests::get_searcher, Query},
|
||||
users::{NewUser, User},
|
||||
Connection as Conn, CONFIG,
|
||||
};
|
||||
use diesel::r2d2::ConnectionManager;
|
||||
use plume_common::utils::random_hex;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn post_updated() {
|
||||
// Need to commit so that searcher on another thread retrieve records.
|
||||
// So, build DbPool instead of using DB_POOL for testing.
|
||||
let manager = ConnectionManager::<Conn>::new(CONFIG.database_url.as_str());
|
||||
let db_pool = DbPool::builder()
|
||||
.connection_customizer(Box::new(PragmaForeignKey))
|
||||
.build(manager)
|
||||
.unwrap();
|
||||
|
||||
let searcher = Arc::new(get_searcher(&CONFIG.search_tokenizers));
|
||||
SearchActor::init(searcher.clone(), db_pool.clone());
|
||||
let conn = db_pool.clone().get().unwrap();
|
||||
|
||||
let title = random_hex()[..8].to_owned();
|
||||
let (_instance, _user, blog) = fill_database(&conn);
|
||||
let author = &blog.list_authors(&conn).unwrap()[0];
|
||||
|
||||
let post = Post::insert(
|
||||
&conn,
|
||||
NewPost {
|
||||
blog_id: blog.id,
|
||||
slug: title.clone(),
|
||||
title: title.clone(),
|
||||
content: SafeString::new(""),
|
||||
published: true,
|
||||
license: "CC-BY-SA".to_owned(),
|
||||
ap_url: "".to_owned(),
|
||||
creation_date: None,
|
||||
subtitle: "".to_owned(),
|
||||
source: "".to_owned(),
|
||||
cover_id: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
PostAuthor::insert(
|
||||
&conn,
|
||||
NewPostAuthor {
|
||||
post_id: post.id,
|
||||
author_id: author.id,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let post_id = post.id;
|
||||
|
||||
// Wait for searcher on another thread add document asynchronously
|
||||
sleep(Duration::from_millis(700));
|
||||
searcher.commit();
|
||||
assert_eq!(
|
||||
searcher.search_document(&conn, Query::from_str(&title).unwrap(), (0, 1))[0].id,
|
||||
post_id
|
||||
);
|
||||
}
|
||||
|
||||
fn fill_database(conn: &Conn) -> (Instance, User, Blog) {
|
||||
conn.transaction::<(Instance, User, Blog), diesel::result::Error, _>(|| {
|
||||
let instance = Instance::insert(
|
||||
conn,
|
||||
NewInstance {
|
||||
default_license: "CC-0-BY-SA".to_string(),
|
||||
local: false,
|
||||
long_description: SafeString::new("Good morning"),
|
||||
long_description_html: "<p>Good morning</p>".to_string(),
|
||||
short_description: SafeString::new("Hello"),
|
||||
short_description_html: "<p>Hello</p>".to_string(),
|
||||
name: random_hex().to_string(),
|
||||
open_registrations: true,
|
||||
public_domain: random_hex().to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let user = User::insert(
|
||||
conn,
|
||||
NewUser {
|
||||
username: random_hex().to_string(),
|
||||
display_name: random_hex().to_string(),
|
||||
outbox_url: random_hex().to_string(),
|
||||
inbox_url: random_hex().to_string(),
|
||||
summary: "".to_string(),
|
||||
email: None,
|
||||
hashed_password: None,
|
||||
instance_id: instance.id,
|
||||
ap_url: random_hex().to_string(),
|
||||
private_key: None,
|
||||
public_key: "".to_string(),
|
||||
shared_inbox_url: None,
|
||||
followers_endpoint: random_hex().to_string(),
|
||||
avatar_id: None,
|
||||
summary_html: SafeString::new(""),
|
||||
role: 0,
|
||||
fqn: random_hex().to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let mut blog = NewBlog::default();
|
||||
blog.instance_id = instance.id;
|
||||
blog.actor_id = random_hex().to_string();
|
||||
blog.ap_url = random_hex().to_string();
|
||||
blog.inbox_url = random_hex().to_string();
|
||||
blog.outbox_url = random_hex().to_string();
|
||||
let blog = Blog::insert(conn, blog).unwrap();
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
NewBlogAuthor {
|
||||
blog_id: blog.id,
|
||||
author_id: user.id,
|
||||
is_owner: true,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Ok((instance, user, blog))
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod actor;
|
||||
mod query;
|
||||
mod searcher;
|
||||
mod tokenizer;
|
||||
@@ -7,12 +8,7 @@ pub use self::tokenizer::TokenizerKind;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::{Query, Searcher, TokenizerKind};
|
||||
use diesel::Connection;
|
||||
use plume_common::utils::random_hex;
|
||||
use std::env::temp_dir;
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::{Query, Searcher};
|
||||
use crate::{
|
||||
blogs::tests::fill_database,
|
||||
config::SearchTokenizerConfig,
|
||||
@@ -22,6 +18,10 @@ pub(crate) mod tests {
|
||||
tests::db,
|
||||
CONFIG,
|
||||
};
|
||||
use diesel::Connection;
|
||||
use plume_common::utils::random_hex;
|
||||
use std::env::temp_dir;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub(crate) fn get_searcher(tokenizers: &SearchTokenizerConfig) -> Searcher {
|
||||
let dir = temp_dir().join(&format!("plume-test-{}", random_hex()));
|
||||
@@ -144,7 +144,6 @@ pub(crate) mod tests {
|
||||
source: "".to_owned(),
|
||||
cover_id: None,
|
||||
},
|
||||
&searcher,
|
||||
)
|
||||
.unwrap();
|
||||
PostAuthor::insert(
|
||||
@@ -155,7 +154,7 @@ pub(crate) mod tests {
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
searcher.add_document(&conn, &post).unwrap();
|
||||
searcher.commit();
|
||||
assert_eq!(
|
||||
searcher.search_document(conn, Query::from_str(&title).unwrap(), (0, 1))[0].id,
|
||||
@@ -164,7 +163,8 @@ pub(crate) mod tests {
|
||||
|
||||
let newtitle = random_hex()[..8].to_owned();
|
||||
post.title = newtitle.clone();
|
||||
post.update(conn, &searcher).unwrap();
|
||||
post.update(conn).unwrap();
|
||||
searcher.update_document(conn, &post).unwrap();
|
||||
searcher.commit();
|
||||
assert_eq!(
|
||||
searcher.search_document(conn, Query::from_str(&newtitle).unwrap(), (0, 1))[0].id,
|
||||
@@ -174,7 +174,7 @@ pub(crate) mod tests {
|
||||
.search_document(conn, Query::from_str(&title).unwrap(), (0, 1))
|
||||
.is_empty());
|
||||
|
||||
post.delete(conn, &searcher).unwrap();
|
||||
searcher.delete_document(&post);
|
||||
searcher.commit();
|
||||
assert!(searcher
|
||||
.search_document(conn, Query::from_str(&newtitle).unwrap(), (0, 1))
|
||||
@@ -213,7 +213,6 @@ pub(crate) mod tests {
|
||||
source: "".to_owned(),
|
||||
cover_id: None,
|
||||
},
|
||||
&searcher,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
use crate::{
|
||||
config::SearchTokenizerConfig, instance::Instance, posts::Post, schema::posts,
|
||||
search::query::PlumeQuery, tags::Tag, Connection, Result,
|
||||
search::query::PlumeQuery, tags::Tag, Connection, Error, Result,
|
||||
};
|
||||
use chrono::Datelike;
|
||||
use chrono::{Datelike, Utc};
|
||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use itertools::Itertools;
|
||||
use std::{cmp, fs::create_dir_all, path::Path, sync::Mutex};
|
||||
use std::fs;
|
||||
use std::{cmp, fs::create_dir_all, io, path::Path, sync::Mutex};
|
||||
use tantivy::{
|
||||
collector::TopDocs, directory::MmapDirectory, schema::*, Index, IndexReader, IndexWriter,
|
||||
ReloadPolicy, Term,
|
||||
ReloadPolicy, TantivyError, Term,
|
||||
};
|
||||
use tracing::warn;
|
||||
use whatlang::{detect as detect_lang, Lang};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -18,6 +20,7 @@ pub enum SearcherError {
|
||||
WriteLockAcquisitionError,
|
||||
IndexOpeningError,
|
||||
IndexEditionError,
|
||||
InvalidIndexDataError,
|
||||
}
|
||||
|
||||
pub struct Searcher {
|
||||
@@ -66,6 +69,58 @@ impl Searcher {
|
||||
schema_builder.build()
|
||||
}
|
||||
|
||||
pub fn open_or_recreate(path: &dyn AsRef<Path>, tokenizers: &SearchTokenizerConfig) -> Self {
|
||||
let mut open_searcher = Self::open(path, tokenizers);
|
||||
if let Err(Error::Search(SearcherError::InvalidIndexDataError)) = open_searcher {
|
||||
if Self::create(path, tokenizers).is_err() {
|
||||
let backup_path = format!("{}.{}", path.as_ref().display(), Utc::now().timestamp());
|
||||
let backup_path = Path::new(&backup_path);
|
||||
fs::rename(path, backup_path)
|
||||
.expect("main: error on backing up search index directory for recreating");
|
||||
if Self::create(path, tokenizers).is_ok() {
|
||||
if fs::remove_dir_all(backup_path).is_err() {
|
||||
warn!(
|
||||
"error on removing backup directory: {}. it remains",
|
||||
backup_path.display()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
panic!("main: error on recreating search index in new index format. remove search index and run `plm search init` manually");
|
||||
}
|
||||
}
|
||||
open_searcher = Self::open(path, tokenizers);
|
||||
}
|
||||
match open_searcher {
|
||||
Ok(s) => s,
|
||||
Err(Error::Search(e)) => match e {
|
||||
SearcherError::WriteLockAcquisitionError => panic!(
|
||||
r#"
|
||||
Your search index is locked. Plume can't start. To fix this issue
|
||||
make sure no other Plume instance is started, and run:
|
||||
|
||||
plm search unlock
|
||||
|
||||
Then try to restart Plume.
|
||||
"#
|
||||
),
|
||||
SearcherError::IndexOpeningError => panic!(
|
||||
r#"
|
||||
Plume was unable to open the search index. If you created the index
|
||||
before, make sure to run Plume in the same directory it was created in, or
|
||||
to set SEARCH_INDEX accordingly. If you did not yet create the search
|
||||
index, run this command:
|
||||
|
||||
plm search init
|
||||
|
||||
Then try to restart Plume
|
||||
"#
|
||||
),
|
||||
e => Err(e).unwrap(),
|
||||
},
|
||||
_ => panic!("Unexpected error while opening search index"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(path: &dyn AsRef<Path>, tokenizers: &SearchTokenizerConfig) -> Result<Self> {
|
||||
let schema = Self::schema();
|
||||
|
||||
@@ -135,7 +190,19 @@ impl Searcher {
|
||||
.reader_builder()
|
||||
.reload_policy(ReloadPolicy::Manual)
|
||||
.try_into()
|
||||
.map_err(|_| SearcherError::IndexCreationError)?,
|
||||
.map_err(|e| {
|
||||
if let TantivyError::IOError(err) = e {
|
||||
let err: io::Error = err.into();
|
||||
if err.kind() == io::ErrorKind::InvalidData {
|
||||
// Search index was created in older Tantivy format.
|
||||
SearcherError::InvalidIndexDataError
|
||||
} else {
|
||||
SearcherError::IndexCreationError
|
||||
}
|
||||
} else {
|
||||
SearcherError::IndexCreationError
|
||||
}
|
||||
})?,
|
||||
index,
|
||||
})
|
||||
}
|
||||
|
||||
+134
-135
@@ -1,8 +1,9 @@
|
||||
use crate::{
|
||||
db_conn::DbConn,
|
||||
lists::List,
|
||||
posts::Post,
|
||||
schema::{posts, timeline, timeline_definition},
|
||||
Connection, Error, PlumeRocket, Result,
|
||||
Connection, Error, Result,
|
||||
};
|
||||
use diesel::{self, BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use std::ops::Deref;
|
||||
@@ -208,14 +209,14 @@ impl Timeline {
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn add_to_all_timelines(rocket: &PlumeRocket, post: &Post, kind: Kind<'_>) -> Result<()> {
|
||||
pub fn add_to_all_timelines(conn: &DbConn, post: &Post, kind: Kind<'_>) -> Result<()> {
|
||||
let timelines = timeline_definition::table
|
||||
.load::<Self>(rocket.conn.deref())
|
||||
.load::<Self>(conn.deref())
|
||||
.map_err(Error::from)?;
|
||||
|
||||
for t in timelines {
|
||||
if t.matches(rocket, post, kind)? {
|
||||
t.add_post(&rocket.conn, post)?;
|
||||
if t.matches(conn, post, kind)? {
|
||||
t.add_post(conn, post)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -231,9 +232,9 @@ impl Timeline {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn matches(&self, rocket: &PlumeRocket, post: &Post, kind: Kind<'_>) -> Result<bool> {
|
||||
pub fn matches(&self, conn: &DbConn, post: &Post, kind: Kind<'_>) -> Result<bool> {
|
||||
let query = TimelineQuery::parse(&self.query)?;
|
||||
query.matches(rocket, self, post, kind)
|
||||
query.matches(conn, self, post, kind)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,7 +249,7 @@ mod tests {
|
||||
posts::NewPost,
|
||||
safe_string::SafeString,
|
||||
tags::Tag,
|
||||
tests::{db, rockets},
|
||||
tests::db,
|
||||
users::tests as userTests,
|
||||
};
|
||||
use diesel::Connection;
|
||||
@@ -257,63 +258,69 @@ mod tests {
|
||||
fn test_timeline() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let users = userTests::fill_database(conn);
|
||||
let users = userTests::fill_database(&conn);
|
||||
|
||||
let mut tl1_u1 = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"my timeline".to_owned(),
|
||||
"all".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
List::new(conn, "languages I speak", Some(&users[1]), ListType::Prefix).unwrap();
|
||||
List::new(
|
||||
&conn,
|
||||
"languages I speak",
|
||||
Some(&users[1]),
|
||||
ListType::Prefix,
|
||||
)
|
||||
.unwrap();
|
||||
let tl2_u1 = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"another timeline".to_owned(),
|
||||
"followed".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
let tl1_u2 = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[1].id,
|
||||
"english posts".to_owned(),
|
||||
"lang in \"languages I speak\"".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
let tl1_instance = Timeline::new_for_instance(
|
||||
conn,
|
||||
&conn,
|
||||
"english posts".to_owned(),
|
||||
"license in [cc]".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(tl1_u1, Timeline::get(conn, tl1_u1.id).unwrap());
|
||||
assert_eq!(tl1_u1, Timeline::get(&conn, tl1_u1.id).unwrap());
|
||||
assert_eq!(
|
||||
tl2_u1,
|
||||
Timeline::find_for_user_by_name(conn, Some(users[0].id), "another timeline")
|
||||
Timeline::find_for_user_by_name(&conn, Some(users[0].id), "another timeline")
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
tl1_instance,
|
||||
Timeline::find_for_user_by_name(conn, None, "english posts").unwrap()
|
||||
Timeline::find_for_user_by_name(&conn, None, "english posts").unwrap()
|
||||
);
|
||||
|
||||
let tl_u1 = Timeline::list_for_user(conn, Some(users[0].id)).unwrap();
|
||||
let tl_u1 = Timeline::list_for_user(&conn, Some(users[0].id)).unwrap();
|
||||
assert_eq!(3, tl_u1.len()); // it is not 2 because there is a "Your feed" tl created for each user automatically
|
||||
assert!(tl_u1.iter().fold(false, |res, tl| { res || *tl == tl1_u1 }));
|
||||
assert!(tl_u1.iter().fold(false, |res, tl| { res || *tl == tl2_u1 }));
|
||||
|
||||
let tl_instance = Timeline::list_for_user(conn, None).unwrap();
|
||||
let tl_instance = Timeline::list_for_user(&conn, None).unwrap();
|
||||
assert_eq!(3, tl_instance.len()); // there are also the local and federated feed by default
|
||||
assert!(tl_instance
|
||||
.iter()
|
||||
.fold(false, |res, tl| { res || *tl == tl1_instance }));
|
||||
|
||||
tl1_u1.name = "My Super TL".to_owned();
|
||||
let new_tl1_u2 = tl1_u2.update(conn).unwrap();
|
||||
let new_tl1_u2 = tl1_u2.update(&conn).unwrap();
|
||||
|
||||
let tl_u2 = Timeline::list_for_user(conn, Some(users[1].id)).unwrap();
|
||||
let tl_u2 = Timeline::list_for_user(&conn, Some(users[1].id)).unwrap();
|
||||
assert_eq!(2, tl_u2.len()); // same here
|
||||
assert!(tl_u2
|
||||
.iter()
|
||||
@@ -327,48 +334,48 @@ mod tests {
|
||||
fn test_timeline_creation_error() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let users = userTests::fill_database(conn);
|
||||
let users = userTests::fill_database(&conn);
|
||||
|
||||
assert!(Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"my timeline".to_owned(),
|
||||
"invalid keyword".to_owned(),
|
||||
)
|
||||
.is_err());
|
||||
assert!(Timeline::new_for_instance(
|
||||
conn,
|
||||
&conn,
|
||||
"my timeline".to_owned(),
|
||||
"invalid keyword".to_owned(),
|
||||
)
|
||||
.is_err());
|
||||
|
||||
assert!(Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"my timeline".to_owned(),
|
||||
"author in non_existant_list".to_owned(),
|
||||
)
|
||||
.is_err());
|
||||
assert!(Timeline::new_for_instance(
|
||||
conn,
|
||||
&conn,
|
||||
"my timeline".to_owned(),
|
||||
"lang in dont-exist".to_owned(),
|
||||
)
|
||||
.is_err());
|
||||
|
||||
List::new(conn, "friends", Some(&users[0]), ListType::User).unwrap();
|
||||
List::new(conn, "idk", None, ListType::Blog).unwrap();
|
||||
List::new(&conn, "friends", Some(&users[0]), ListType::User).unwrap();
|
||||
List::new(&conn, "idk", None, ListType::Blog).unwrap();
|
||||
|
||||
assert!(Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"my timeline".to_owned(),
|
||||
"blog in friends".to_owned(),
|
||||
)
|
||||
.is_err());
|
||||
assert!(Timeline::new_for_instance(
|
||||
conn,
|
||||
&conn,
|
||||
"my timeline".to_owned(),
|
||||
"not author in idk".to_owned(),
|
||||
)
|
||||
@@ -380,13 +387,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_simple_match() {
|
||||
let r = &rockets();
|
||||
let conn = &r.conn;
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (users, blogs) = blogTests::fill_database(conn);
|
||||
let (users, blogs) = blogTests::fill_database(&conn);
|
||||
|
||||
let gnu_tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"GNU timeline".to_owned(),
|
||||
"license in [AGPL, LGPL, GPL]".to_owned(),
|
||||
@@ -394,7 +400,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let gnu_post = Post::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewPost {
|
||||
blog_id: blogs[0].id,
|
||||
slug: "slug".to_string(),
|
||||
@@ -408,13 +414,12 @@ mod tests {
|
||||
source: "you must say GNU/Linux, not Linux!!!".to_string(),
|
||||
cover_id: None,
|
||||
},
|
||||
&r.searcher,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(gnu_tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
||||
assert!(gnu_tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
|
||||
let non_free_post = Post::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewPost {
|
||||
blog_id: blogs[0].id,
|
||||
slug: "slug2".to_string(),
|
||||
@@ -428,10 +433,11 @@ mod tests {
|
||||
source: "so is Microsoft".to_string(),
|
||||
cover_id: None,
|
||||
},
|
||||
&r.searcher,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!gnu_tl.matches(r, &non_free_post, Kind::Original).unwrap());
|
||||
assert!(!gnu_tl
|
||||
.matches(&conn, &non_free_post, Kind::Original)
|
||||
.unwrap());
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@@ -439,12 +445,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_complex_match() {
|
||||
let r = &rockets();
|
||||
let conn = &r.conn;
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (users, blogs) = blogTests::fill_database(conn);
|
||||
let (users, blogs) = blogTests::fill_database(&conn);
|
||||
Follow::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewFollow {
|
||||
follower_id: users[0].id,
|
||||
following_id: users[1].id,
|
||||
@@ -454,11 +459,11 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let fav_blogs_list =
|
||||
List::new(conn, "fav_blogs", Some(&users[0]), ListType::Blog).unwrap();
|
||||
fav_blogs_list.add_blogs(conn, &[blogs[0].id]).unwrap();
|
||||
List::new(&conn, "fav_blogs", Some(&users[0]), ListType::Blog).unwrap();
|
||||
fav_blogs_list.add_blogs(&conn, &[blogs[0].id]).unwrap();
|
||||
|
||||
let my_tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"My timeline".to_owned(),
|
||||
"blog in fav_blogs and not has_cover or local and followed exclude likes"
|
||||
@@ -467,7 +472,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let post = Post::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewPost {
|
||||
blog_id: blogs[0].id,
|
||||
slug: "about-linux".to_string(),
|
||||
@@ -481,13 +486,12 @@ mod tests {
|
||||
subtitle: "".to_string(),
|
||||
cover_id: None,
|
||||
},
|
||||
&r.searcher,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(my_tl.matches(r, &post, Kind::Original).unwrap()); // matches because of "blog in fav_blogs" (and there is no cover)
|
||||
assert!(my_tl.matches(&conn, &post, Kind::Original).unwrap()); // matches because of "blog in fav_blogs" (and there is no cover)
|
||||
|
||||
let post = Post::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewPost {
|
||||
blog_id: blogs[1].id,
|
||||
slug: "about-linux-2".to_string(),
|
||||
@@ -503,10 +507,9 @@ mod tests {
|
||||
subtitle: "".to_string(),
|
||||
cover_id: None,
|
||||
},
|
||||
&r.searcher,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!my_tl.matches(r, &post, Kind::Like(&users[1])).unwrap());
|
||||
assert!(!my_tl.matches(&conn, &post, Kind::Like(&users[1])).unwrap());
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@@ -514,20 +517,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_add_to_all_timelines() {
|
||||
let r = &rockets();
|
||||
let conn = &r.conn;
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (users, blogs) = blogTests::fill_database(conn);
|
||||
let (users, blogs) = blogTests::fill_database(&conn);
|
||||
|
||||
let gnu_tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"GNU timeline".to_owned(),
|
||||
"license in [AGPL, LGPL, GPL]".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
let non_gnu_tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"Stallman disapproved timeline".to_owned(),
|
||||
"not license in [AGPL, LGPL, GPL]".to_owned(),
|
||||
@@ -535,7 +537,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let gnu_post = Post::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewPost {
|
||||
blog_id: blogs[0].id,
|
||||
slug: "slug".to_string(),
|
||||
@@ -549,12 +551,11 @@ mod tests {
|
||||
source: "you must say GNU/Linux, not Linux!!!".to_string(),
|
||||
cover_id: None,
|
||||
},
|
||||
&r.searcher,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let non_free_post = Post::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewPost {
|
||||
blog_id: blogs[0].id,
|
||||
slug: "slug2".to_string(),
|
||||
@@ -568,17 +569,16 @@ mod tests {
|
||||
source: "so is Microsoft".to_string(),
|
||||
cover_id: None,
|
||||
},
|
||||
&r.searcher,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Timeline::add_to_all_timelines(r, &gnu_post, Kind::Original).unwrap();
|
||||
Timeline::add_to_all_timelines(r, &non_free_post, Kind::Original).unwrap();
|
||||
Timeline::add_to_all_timelines(&conn, &gnu_post, Kind::Original).unwrap();
|
||||
Timeline::add_to_all_timelines(&conn, &non_free_post, Kind::Original).unwrap();
|
||||
|
||||
let res = gnu_tl.get_latest(conn, 2).unwrap();
|
||||
let res = gnu_tl.get_latest(&conn, 2).unwrap();
|
||||
assert_eq!(res.len(), 1);
|
||||
assert_eq!(res[0].id, gnu_post.id);
|
||||
let res = non_gnu_tl.get_latest(conn, 2).unwrap();
|
||||
let res = non_gnu_tl.get_latest(&conn, 2).unwrap();
|
||||
assert_eq!(res.len(), 1);
|
||||
assert_eq!(res[0].id, non_free_post.id);
|
||||
|
||||
@@ -588,13 +588,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_matches_lists_direct() {
|
||||
let r = &rockets();
|
||||
let conn = &r.conn;
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (users, blogs) = blogTests::fill_database(conn);
|
||||
let (users, blogs) = blogTests::fill_database(&conn);
|
||||
|
||||
let gnu_post = Post::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewPost {
|
||||
blog_id: blogs[0].id,
|
||||
slug: "slug".to_string(),
|
||||
@@ -608,65 +607,66 @@ mod tests {
|
||||
source: "you must say GNU/Linux, not Linux!!!".to_string(),
|
||||
cover_id: None,
|
||||
},
|
||||
&r.searcher,
|
||||
)
|
||||
.unwrap();
|
||||
gnu_post
|
||||
.update_tags(conn, vec![Tag::build_activity("free".to_owned()).unwrap()])
|
||||
.update_tags(&conn, vec![Tag::build_activity("free".to_owned()).unwrap()])
|
||||
.unwrap();
|
||||
PostAuthor::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewPostAuthor {
|
||||
post_id: gnu_post.id,
|
||||
author_id: blogs[0].list_authors(conn).unwrap()[0].id,
|
||||
author_id: blogs[0].list_authors(&conn).unwrap()[0].id,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"blog timeline".to_owned(),
|
||||
format!("blog in [{}]", blogs[0].fqn),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"blog timeline".to_owned(),
|
||||
"blog in [no_one@nowhere]".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"author timeline".to_owned(),
|
||||
format!(
|
||||
"author in [{}]",
|
||||
blogs[0].list_authors(conn).unwrap()[0].fqn
|
||||
blogs[0].list_authors(&conn).unwrap()[0].fqn
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"author timeline".to_owned(),
|
||||
format!("author in [{}]", users[2].fqn),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
||||
assert!(tl.matches(r, &gnu_post, Kind::Reshare(&users[2])).unwrap());
|
||||
assert!(!tl.matches(r, &gnu_post, Kind::Like(&users[2])).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
assert!(tl
|
||||
.matches(&conn, &gnu_post, Kind::Reshare(&users[2]))
|
||||
.unwrap());
|
||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Like(&users[2])).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"author timeline".to_owned(),
|
||||
format!(
|
||||
@@ -675,48 +675,50 @@ mod tests {
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
||||
assert!(!tl.matches(r, &gnu_post, Kind::Reshare(&users[2])).unwrap());
|
||||
assert!(tl.matches(r, &gnu_post, Kind::Like(&users[2])).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
assert!(!tl
|
||||
.matches(&conn, &gnu_post, Kind::Reshare(&users[2]))
|
||||
.unwrap());
|
||||
assert!(tl.matches(&conn, &gnu_post, Kind::Like(&users[2])).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"tag timeline".to_owned(),
|
||||
"tags in [free]".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"tag timeline".to_owned(),
|
||||
"tags in [private]".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"english timeline".to_owned(),
|
||||
"lang in [en]".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"franco-italian timeline".to_owned(),
|
||||
"lang in [fr, it]".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@@ -726,12 +728,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_matches_lists_saved() {
|
||||
let r = &rockets();
|
||||
let conn = &r.conn;
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (users, blogs) = blogTests::fill_database(conn);
|
||||
let (users, blogs) = blogTests::fill_database(&conn);
|
||||
|
||||
let gnu_post = Post::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewPost {
|
||||
blog_id: blogs[0].id,
|
||||
slug: "slug".to_string(),
|
||||
@@ -745,11 +747,10 @@ mod tests {
|
||||
source: "you must say GNU/Linux, not Linux!!!".to_string(),
|
||||
cover_id: None,
|
||||
},
|
||||
&r.searcher,
|
||||
)
|
||||
.unwrap();
|
||||
gnu_post.update_tags(conn, vec![Tag::build_activity("free".to_owned()).unwrap()]).unwrap();
|
||||
PostAuthor::insert(conn, NewPostAuthor {post_id: gnu_post.id, author_id: blogs[0].list_authors(conn).unwrap()[0].id}).unwrap();
|
||||
gnu_post.update_tags(&conn, vec![Tag::build_activity("free".to_owned()).unwrap()]).unwrap();
|
||||
PostAuthor::insert(&conn, NewPostAuthor {post_id: gnu_post.id, author_id: blogs[0].list_authors(&conn).unwrap()[0].id}).unwrap();
|
||||
|
||||
unimplemented!();
|
||||
|
||||
@@ -759,13 +760,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_matches_keyword() {
|
||||
let r = &rockets();
|
||||
let conn = &r.conn;
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (users, blogs) = blogTests::fill_database(conn);
|
||||
let (users, blogs) = blogTests::fill_database(&conn);
|
||||
|
||||
let gnu_post = Post::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewPost {
|
||||
blog_id: blogs[0].id,
|
||||
slug: "slug".to_string(),
|
||||
@@ -779,66 +779,65 @@ mod tests {
|
||||
source: "you must say GNU/Linux, not Linux!!!".to_string(),
|
||||
cover_id: None,
|
||||
},
|
||||
&r.searcher,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"Linux title".to_owned(),
|
||||
"title contains Linux".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"Microsoft title".to_owned(),
|
||||
"title contains Microsoft".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"Linux subtitle".to_owned(),
|
||||
"subtitle contains Stallman".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"Microsoft subtitle".to_owned(),
|
||||
"subtitle contains Nadella".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"Linux content".to_owned(),
|
||||
"content contains Linux".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"Microsoft content".to_owned(),
|
||||
"subtitle contains Windows".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use crate::{
|
||||
blogs::Blog,
|
||||
db_conn::DbConn,
|
||||
lists::{self, ListType},
|
||||
posts::Post,
|
||||
tags::Tag,
|
||||
timeline::Timeline,
|
||||
users::User,
|
||||
PlumeRocket, Result,
|
||||
Result,
|
||||
};
|
||||
use plume_common::activity_pub::inbox::AsActor;
|
||||
use whatlang::{self, Lang};
|
||||
@@ -160,19 +161,19 @@ enum TQ<'a> {
|
||||
impl<'a> TQ<'a> {
|
||||
fn matches(
|
||||
&self,
|
||||
rocket: &PlumeRocket,
|
||||
conn: &DbConn,
|
||||
timeline: &Timeline,
|
||||
post: &Post,
|
||||
kind: Kind<'_>,
|
||||
) -> Result<bool> {
|
||||
match self {
|
||||
TQ::Or(inner) => inner.iter().try_fold(false, |s, e| {
|
||||
e.matches(rocket, timeline, post, kind).map(|r| s || r)
|
||||
e.matches(conn, timeline, post, kind).map(|r| s || r)
|
||||
}),
|
||||
TQ::And(inner) => inner.iter().try_fold(true, |s, e| {
|
||||
e.matches(rocket, timeline, post, kind).map(|r| s && r)
|
||||
e.matches(conn, timeline, post, kind).map(|r| s && r)
|
||||
}),
|
||||
TQ::Arg(inner, invert) => Ok(inner.matches(rocket, timeline, post, kind)? ^ invert),
|
||||
TQ::Arg(inner, invert) => Ok(inner.matches(conn, timeline, post, kind)? ^ invert),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,15 +206,15 @@ enum Arg<'a> {
|
||||
impl<'a> Arg<'a> {
|
||||
pub fn matches(
|
||||
&self,
|
||||
rocket: &PlumeRocket,
|
||||
conn: &DbConn,
|
||||
timeline: &Timeline,
|
||||
post: &Post,
|
||||
kind: Kind<'_>,
|
||||
) -> Result<bool> {
|
||||
match self {
|
||||
Arg::In(t, l) => t.matches(rocket, timeline, post, l, kind),
|
||||
Arg::In(t, l) => t.matches(conn, timeline, post, l, kind),
|
||||
Arg::Contains(t, v) => t.matches(post, v),
|
||||
Arg::Boolean(t) => t.matches(rocket, timeline, post, kind),
|
||||
Arg::Boolean(t) => t.matches(conn, timeline, post, kind),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,7 +231,7 @@ enum WithList {
|
||||
impl WithList {
|
||||
pub fn matches(
|
||||
&self,
|
||||
rocket: &PlumeRocket,
|
||||
conn: &DbConn,
|
||||
timeline: &Timeline,
|
||||
post: &Post,
|
||||
list: &List<'_>,
|
||||
@@ -238,39 +239,34 @@ impl WithList {
|
||||
) -> Result<bool> {
|
||||
match list {
|
||||
List::List(name) => {
|
||||
let list =
|
||||
lists::List::find_for_user_by_name(&rocket.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(&rocket.conn, post.blog_id)
|
||||
}
|
||||
(WithList::Blog, ListType::Blog) => list.contains_blog(conn, post.blog_id),
|
||||
(WithList::Author { boosts, likes }, ListType::User) => match kind {
|
||||
Kind::Original => Ok(list
|
||||
.list_users(&rocket.conn)?
|
||||
.list_users(conn)?
|
||||
.iter()
|
||||
.any(|a| post.is_author(&rocket.conn, a.id).unwrap_or(false))),
|
||||
.any(|a| post.is_author(conn, a.id).unwrap_or(false))),
|
||||
Kind::Reshare(u) => {
|
||||
if *boosts {
|
||||
list.contains_user(&rocket.conn, u.id)
|
||||
list.contains_user(conn, u.id)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
Kind::Like(u) => {
|
||||
if *likes {
|
||||
list.contains_user(&rocket.conn, u.id)
|
||||
list.contains_user(conn, u.id)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
(WithList::License, ListType::Word) => {
|
||||
list.contains_word(&rocket.conn, &post.license)
|
||||
}
|
||||
(WithList::License, ListType::Word) => list.contains_word(conn, &post.license),
|
||||
(WithList::Tags, ListType::Word) => {
|
||||
let tags = Tag::for_post(&rocket.conn, post.id)?;
|
||||
let tags = Tag::for_post(conn, post.id)?;
|
||||
Ok(list
|
||||
.list_words(&rocket.conn)?
|
||||
.list_words(conn)?
|
||||
.iter()
|
||||
.any(|s| tags.iter().any(|t| s == &t.tag)))
|
||||
}
|
||||
@@ -285,7 +281,7 @@ impl WithList {
|
||||
})
|
||||
.unwrap_or(Lang::Eng)
|
||||
.name();
|
||||
list.contains_prefix(&rocket.conn, lang)
|
||||
list.contains_prefix(conn, lang)
|
||||
}
|
||||
(_, _) => Err(QueryError::RuntimeError(format!(
|
||||
"The list '{}' is of the wrong type for this usage",
|
||||
@@ -297,13 +293,13 @@ impl WithList {
|
||||
List::Array(list) => match self {
|
||||
WithList::Blog => Ok(list
|
||||
.iter()
|
||||
.filter_map(|b| Blog::find_by_fqn(rocket, b).ok())
|
||||
.filter_map(|b| Blog::find_by_fqn(conn, b).ok())
|
||||
.any(|b| b.id == post.blog_id)),
|
||||
WithList::Author { boosts, likes } => match kind {
|
||||
Kind::Original => Ok(list
|
||||
.iter()
|
||||
.filter_map(|a| User::find_by_fqn(rocket, a).ok())
|
||||
.any(|a| post.is_author(&rocket.conn, a.id).unwrap_or(false))),
|
||||
.filter_map(|a| User::find_by_fqn(&*conn, a).ok())
|
||||
.any(|a| post.is_author(conn, a.id).unwrap_or(false))),
|
||||
Kind::Reshare(u) => {
|
||||
if *boosts {
|
||||
Ok(list.iter().any(|user| &u.fqn == user))
|
||||
@@ -321,7 +317,7 @@ impl WithList {
|
||||
},
|
||||
WithList::License => Ok(list.iter().any(|s| s == &post.license)),
|
||||
WithList::Tags => {
|
||||
let tags = Tag::for_post(&rocket.conn, post.id)?;
|
||||
let tags = Tag::for_post(conn, post.id)?;
|
||||
Ok(list.iter().any(|s| tags.iter().any(|t| s == &t.tag)))
|
||||
}
|
||||
WithList::Lang => {
|
||||
@@ -371,7 +367,7 @@ enum Bool {
|
||||
impl Bool {
|
||||
pub fn matches(
|
||||
&self,
|
||||
rocket: &PlumeRocket,
|
||||
conn: &DbConn,
|
||||
timeline: &Timeline,
|
||||
post: &Post,
|
||||
kind: Kind<'_>,
|
||||
@@ -384,21 +380,19 @@ impl Bool {
|
||||
let user = timeline.user_id.unwrap();
|
||||
match kind {
|
||||
Kind::Original => post
|
||||
.get_authors(&rocket.conn)?
|
||||
.get_authors(conn)?
|
||||
.iter()
|
||||
.try_fold(false, |s, a| {
|
||||
a.is_followed_by(&rocket.conn, user).map(|r| s || r)
|
||||
}),
|
||||
.try_fold(false, |s, a| a.is_followed_by(conn, user).map(|r| s || r)),
|
||||
Kind::Reshare(u) => {
|
||||
if *boosts {
|
||||
u.is_followed_by(&rocket.conn, user)
|
||||
u.is_followed_by(conn, user)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
Kind::Like(u) => {
|
||||
if *likes {
|
||||
u.is_followed_by(&rocket.conn, user)
|
||||
u.is_followed_by(conn, user)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
@@ -406,7 +400,7 @@ impl Bool {
|
||||
}
|
||||
}
|
||||
Bool::HasCover => Ok(post.cover_id.is_some()),
|
||||
Bool::Local => Ok(post.get_blog(&rocket.conn)?.is_local() && kind == Kind::Original),
|
||||
Bool::Local => Ok(post.get_blog(conn)?.is_local() && kind == Kind::Original),
|
||||
Bool::All => Ok(kind == Kind::Original),
|
||||
}
|
||||
}
|
||||
@@ -642,12 +636,12 @@ impl<'a> TimelineQuery<'a> {
|
||||
|
||||
pub fn matches(
|
||||
&self,
|
||||
rocket: &PlumeRocket,
|
||||
conn: &DbConn,
|
||||
timeline: &Timeline,
|
||||
post: &Post,
|
||||
kind: Kind<'_>,
|
||||
) -> Result<bool> {
|
||||
self.0.matches(rocket, timeline, post, kind)
|
||||
self.0.matches(conn, timeline, post, kind)
|
||||
}
|
||||
|
||||
pub fn list_used_lists(&self) -> Vec<(String, ListType)> {
|
||||
|
||||
+244
-86
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
ap_url, blocklisted_emails::BlocklistedEmail, blogs::Blog, db_conn::DbConn, follows::Follow,
|
||||
instance::*, medias::Media, notifications::Notification, post_authors::PostAuthor, posts::Post,
|
||||
safe_string::SafeString, schema::users, search::Searcher, timeline::Timeline, Connection,
|
||||
Error, PlumeRocket, Result, ITEMS_PER_PAGE,
|
||||
safe_string::SafeString, schema::users, timeline::Timeline, Connection, Error, Result,
|
||||
UserEvent::*, CONFIG, ITEMS_PER_PAGE, USER_CHAN,
|
||||
};
|
||||
use activitypub::{
|
||||
activity::Delete,
|
||||
@@ -11,9 +11,9 @@ use activitypub::{
|
||||
object::{Image, Tombstone},
|
||||
Activity, CustomObject, Endpoint,
|
||||
};
|
||||
use bcrypt;
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl};
|
||||
use ldap3::{LdapConn, Scope, SearchEntry};
|
||||
use openssl::{
|
||||
hash::MessageDigest,
|
||||
pkey::{PKey, Private},
|
||||
@@ -33,14 +33,15 @@ use reqwest::{
|
||||
header::{HeaderValue, ACCEPT},
|
||||
ClientBuilder,
|
||||
};
|
||||
use riker::actors::{Publish, Tell};
|
||||
use rocket::{
|
||||
outcome::IntoOutcome,
|
||||
request::{self, FromRequest, Request},
|
||||
};
|
||||
use serde_json;
|
||||
use std::{
|
||||
cmp::PartialEq,
|
||||
hash::{Hash, Hasher},
|
||||
sync::Arc,
|
||||
};
|
||||
use url::Url;
|
||||
use webfinger::*;
|
||||
@@ -130,14 +131,14 @@ impl User {
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn delete(&self, conn: &Connection, searcher: &Searcher) -> Result<()> {
|
||||
pub fn delete(&self, conn: &Connection) -> Result<()> {
|
||||
use crate::schema::post_authors;
|
||||
|
||||
for blog in Blog::find_for_author(conn, self)?
|
||||
.iter()
|
||||
.filter(|b| b.count_authors(conn).map(|c| c <= 1).unwrap_or(false))
|
||||
{
|
||||
blog.delete(conn, searcher)?;
|
||||
blog.delete(conn)?;
|
||||
}
|
||||
// delete the posts if they is the only author
|
||||
let all_their_posts_ids: Vec<i32> = post_authors::table
|
||||
@@ -157,7 +158,7 @@ impl User {
|
||||
.unwrap_or(&0)
|
||||
> &0;
|
||||
if !has_other_authors {
|
||||
Post::get(conn, post_id)?.delete(conn, searcher)?;
|
||||
Post::get(conn, post_id)?.delete(conn)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,29 +188,29 @@ impl User {
|
||||
users::table
|
||||
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
||||
.count()
|
||||
.get_result(conn)
|
||||
.get_result(&*conn)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<User> {
|
||||
pub fn find_by_fqn(conn: &DbConn, fqn: &str) -> Result<User> {
|
||||
let from_db = users::table
|
||||
.filter(users::fqn.eq(fqn))
|
||||
.first(&*c.conn)
|
||||
.first(&**conn)
|
||||
.optional()?;
|
||||
if let Some(from_db) = from_db {
|
||||
Ok(from_db)
|
||||
} else {
|
||||
User::fetch_from_webfinger(c, fqn)
|
||||
User::fetch_from_webfinger(conn, fqn)
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<User> {
|
||||
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(c, link.href.as_ref()?, None).map_err(|(_, e)| e)
|
||||
User::from_id(conn, link.href.as_ref()?, None, CONFIG.proxy()).map_err(|(_, e)| e)
|
||||
}
|
||||
|
||||
pub fn fetch_remote_interact_uri(acct: &str) -> Result<String> {
|
||||
@@ -244,8 +245,8 @@ impl User {
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
pub fn fetch_from_url(c: &PlumeRocket, url: &str) -> Result<User> {
|
||||
User::fetch(url).and_then(|json| User::from_activity(c, json))
|
||||
pub fn fetch_from_url(conn: &DbConn, url: &str) -> Result<User> {
|
||||
User::fetch(url).and_then(|json| User::from_activity(conn, json))
|
||||
}
|
||||
|
||||
pub fn refetch(&self, conn: &Connection) -> Result<()> {
|
||||
@@ -292,11 +293,137 @@ impl User {
|
||||
bcrypt::hash(pass, 10).map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn auth(&self, pass: &str) -> bool {
|
||||
self.hashed_password
|
||||
.clone()
|
||||
.map(|hashed| bcrypt::verify(pass, hashed.as_ref()).unwrap_or(false))
|
||||
.unwrap_or(false)
|
||||
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);
|
||||
}
|
||||
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)
|
||||
.map_err(|_| Error::NotFound)?;
|
||||
|
||||
if bind.success().is_err() {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
|
||||
let search = ldap_conn
|
||||
.search(
|
||||
&ldap_name,
|
||||
Scope::Base,
|
||||
"(|(objectClass=person)(objectClass=user))",
|
||||
vec![&ldap.mail_attr],
|
||||
)
|
||||
.map_err(|_| Error::NotFound)?
|
||||
.success()
|
||||
.map_err(|_| Error::NotFound)?;
|
||||
for entry in search.0 {
|
||||
let entry = SearchEntry::construct(entry);
|
||||
let email = entry.attrs.get("mail").and_then(|vec| vec.first());
|
||||
if let Some(email) = email {
|
||||
let _ = ldap_conn.unbind();
|
||||
return NewUser::new_local(
|
||||
conn,
|
||||
name.to_owned(),
|
||||
name.to_owned(),
|
||||
Role::Normal,
|
||||
"",
|
||||
email.to_owned(),
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
let _ = ldap_conn.unbind();
|
||||
Err(Error::NotFound)
|
||||
}
|
||||
|
||||
fn ldap_login(&self, password: &str) -> bool {
|
||||
if let Some(ldap) = CONFIG.ldap.as_ref() {
|
||||
let mut conn = if let Ok(conn) = LdapConn::new(&ldap.addr) {
|
||||
conn
|
||||
} 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
|
||||
);
|
||||
if let Ok(bind) = conn.simple_bind(&name, password) {
|
||||
bind.success().is_ok()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn login(conn: &Connection, ident: &str, password: &str) -> Result<User> {
|
||||
let local_id = Instance::get_local()?.id;
|
||||
let user = match User::find_by_email(conn, ident) {
|
||||
Ok(user) => Ok(user),
|
||||
_ => User::find_by_name(conn, ident, local_id),
|
||||
}
|
||||
.and_then(|u| {
|
||||
if u.instance_id == local_id {
|
||||
Ok(u)
|
||||
} else {
|
||||
Err(Error::NotFound)
|
||||
}
|
||||
});
|
||||
|
||||
match user {
|
||||
Ok(user) if user.hashed_password.is_some() => {
|
||||
if bcrypt::verify(password, user.hashed_password.as_ref().unwrap()).unwrap_or(false)
|
||||
{
|
||||
Ok(user)
|
||||
} else {
|
||||
Err(Error::NotFound)
|
||||
}
|
||||
}
|
||||
Ok(user) => {
|
||||
if user.ldap_login(password) {
|
||||
Ok(user)
|
||||
} else {
|
||||
Err(Error::NotFound)
|
||||
}
|
||||
}
|
||||
e => {
|
||||
if let Ok(user) = User::ldap_register(conn, ident, password) {
|
||||
return Ok(user);
|
||||
}
|
||||
// if no user was found, and we were unable to auto-register from ldap
|
||||
// fake-verify a password, and return an error.
|
||||
let other = User::get(&*conn, 1)
|
||||
.expect("No user is registered")
|
||||
.hashed_password;
|
||||
other.map(|pass| bcrypt::verify(password, &pass));
|
||||
e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_password(&self, conn: &Connection, pass: &str) -> Result<()> {
|
||||
@@ -726,11 +853,7 @@ impl User {
|
||||
|
||||
pub fn webfinger(&self, conn: &Connection) -> Result<Webfinger> {
|
||||
Ok(Webfinger {
|
||||
subject: format!(
|
||||
"acct:{}@{}",
|
||||
self.username,
|
||||
self.get_instance(conn)?.public_domain
|
||||
),
|
||||
subject: format!("acct:{}", self.acct_authority(conn)?),
|
||||
aliases: vec![self.ap_url.clone()],
|
||||
links: vec![
|
||||
Link {
|
||||
@@ -768,6 +891,14 @@ impl User {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn acct_authority(&self, conn: &Connection) -> Result<String> {
|
||||
Ok(format!(
|
||||
"{}@{}",
|
||||
self.username,
|
||||
self.get_instance(conn)?.public_domain
|
||||
))
|
||||
}
|
||||
|
||||
pub fn set_avatar(&self, conn: &Connection, id: i32) -> Result<()> {
|
||||
diesel::update(self)
|
||||
.set(users::avatar_id.eq(id))
|
||||
@@ -787,6 +918,21 @@ impl User {
|
||||
self.fqn.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remote_user_found(&self) {
|
||||
tracing::trace!("{:?}", self);
|
||||
self.publish_remote_user_found();
|
||||
}
|
||||
|
||||
fn publish_remote_user_found(&self) {
|
||||
USER_CHAN.tell(
|
||||
Publish {
|
||||
msg: RemoteUserFound(Arc::new(self.clone())),
|
||||
topic: "user.remote_user_found".into(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'r> FromRequest<'a, 'r> for User {
|
||||
@@ -811,20 +957,20 @@ impl IntoId for User {
|
||||
|
||||
impl Eq for User {}
|
||||
|
||||
impl FromId<PlumeRocket> for User {
|
||||
impl FromId<DbConn> for User {
|
||||
type Error = Error;
|
||||
type Object = CustomPerson;
|
||||
|
||||
fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
|
||||
Self::find_by_ap_url(&c.conn, id)
|
||||
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
||||
Self::find_by_ap_url(conn, id)
|
||||
}
|
||||
|
||||
fn from_activity(c: &PlumeRocket, acct: CustomPerson) -> Result<Self> {
|
||||
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 instance = Instance::find_by_domain(&c.conn, inst).or_else(|_| {
|
||||
let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
|
||||
Instance::insert(
|
||||
&c.conn,
|
||||
conn,
|
||||
NewInstance {
|
||||
name: inst.to_owned(),
|
||||
public_domain: inst.to_owned(),
|
||||
@@ -853,7 +999,7 @@ impl FromId<PlumeRocket> for User {
|
||||
};
|
||||
|
||||
let user = User::insert(
|
||||
&c.conn,
|
||||
conn,
|
||||
NewUser {
|
||||
display_name: acct
|
||||
.object
|
||||
@@ -899,10 +1045,10 @@ impl FromId<PlumeRocket> for User {
|
||||
|
||||
if let Ok(icon) = acct.object.object_props.icon_image() {
|
||||
if let Ok(url) = icon.object_props.url_string() {
|
||||
let avatar = Media::save_remote(&c.conn, url, &user);
|
||||
let avatar = Media::save_remote(conn, url, &user);
|
||||
|
||||
if let Ok(avatar) = avatar {
|
||||
user.set_avatar(&c.conn, avatar.id)?;
|
||||
user.set_avatar(conn, avatar.id)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -911,7 +1057,7 @@ impl FromId<PlumeRocket> for User {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsActor<&PlumeRocket> for User {
|
||||
impl AsActor<&DbConn> for User {
|
||||
fn get_inbox_url(&self) -> String {
|
||||
self.inbox_url.clone()
|
||||
}
|
||||
@@ -927,13 +1073,13 @@ impl AsActor<&PlumeRocket> for User {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, Delete, &PlumeRocket> for User {
|
||||
impl AsObject<User, Delete, &DbConn> for User {
|
||||
type Error = Error;
|
||||
type Output = ();
|
||||
|
||||
fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
|
||||
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
||||
if self.id == actor.id {
|
||||
self.delete(&c.conn, &c.searcher).map(|_| ())
|
||||
self.delete(conn).map(|_| ())
|
||||
} else {
|
||||
Err(Error::Unauthorized)
|
||||
}
|
||||
@@ -983,7 +1129,7 @@ impl NewUser {
|
||||
role: Role,
|
||||
summary: &str,
|
||||
email: String,
|
||||
password: String,
|
||||
password: Option<String>,
|
||||
) -> Result<User> {
|
||||
let (pub_key, priv_key) = gen_keypair();
|
||||
let instance = Instance::get_local()?;
|
||||
@@ -1001,7 +1147,7 @@ impl NewUser {
|
||||
summary: summary.to_owned(),
|
||||
summary_html: SafeString::new(&utils::md_to_html(&summary, None, false, None).0),
|
||||
email: Some(email),
|
||||
hashed_password: Some(password),
|
||||
hashed_password: password,
|
||||
instance_id: instance.id,
|
||||
public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?,
|
||||
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
|
||||
@@ -1022,14 +1168,27 @@ impl NewUser {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum UserEvent {
|
||||
RemoteUserFound(Arc<User>),
|
||||
}
|
||||
|
||||
impl From<UserEvent> for Arc<User> {
|
||||
fn from(event: UserEvent) -> Self {
|
||||
use UserEvent::*;
|
||||
|
||||
match event {
|
||||
RemoteUserFound(user) => user,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
config::CONFIG,
|
||||
instance::{tests as instance_tests, Instance},
|
||||
search::tests::get_searcher,
|
||||
tests::{db, rockets},
|
||||
tests::db,
|
||||
Connection as Conn,
|
||||
};
|
||||
use diesel::Connection;
|
||||
@@ -1043,7 +1202,7 @@ pub(crate) mod tests {
|
||||
Role::Admin,
|
||||
"Hello there, I'm the admin",
|
||||
"admin@example.com".to_owned(),
|
||||
"invalid_admin_password".to_owned(),
|
||||
Some("invalid_admin_password".to_owned()),
|
||||
)
|
||||
.unwrap();
|
||||
let user = NewUser::new_local(
|
||||
@@ -1053,7 +1212,7 @@ pub(crate) mod tests {
|
||||
Role::Normal,
|
||||
"Hello there, I'm no one",
|
||||
"user@example.com".to_owned(),
|
||||
"invalid_user_password".to_owned(),
|
||||
Some("invalid_user_password".to_owned()),
|
||||
)
|
||||
.unwrap();
|
||||
let other = NewUser::new_local(
|
||||
@@ -1063,7 +1222,7 @@ pub(crate) mod tests {
|
||||
Role::Normal,
|
||||
"Hello there, I'm someone else",
|
||||
"other@example.com".to_owned(),
|
||||
"invalid_other_password".to_owned(),
|
||||
Some("invalid_other_password".to_owned()),
|
||||
)
|
||||
.unwrap();
|
||||
vec![admin, user, other]
|
||||
@@ -1071,38 +1230,37 @@ pub(crate) mod tests {
|
||||
|
||||
#[test]
|
||||
fn find_by() {
|
||||
let r = rockets();
|
||||
let conn = &*r.conn;
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
fill_database(conn);
|
||||
fill_database(&conn);
|
||||
let test_user = NewUser::new_local(
|
||||
conn,
|
||||
&conn,
|
||||
"test".to_owned(),
|
||||
"test user".to_owned(),
|
||||
Role::Normal,
|
||||
"Hello I'm a test",
|
||||
"test@example.com".to_owned(),
|
||||
User::hash_pass("test_password").unwrap(),
|
||||
Some(User::hash_pass("test_password").unwrap()),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
test_user.id,
|
||||
User::find_by_name(conn, "test", Instance::get_local().unwrap().id)
|
||||
User::find_by_name(&conn, "test", Instance::get_local().unwrap().id)
|
||||
.unwrap()
|
||||
.id
|
||||
);
|
||||
assert_eq!(
|
||||
test_user.id,
|
||||
User::find_by_fqn(&r, &test_user.fqn).unwrap().id
|
||||
User::find_by_fqn(&conn, &test_user.fqn).unwrap().id
|
||||
);
|
||||
assert_eq!(
|
||||
test_user.id,
|
||||
User::find_by_email(conn, "test@example.com").unwrap().id
|
||||
User::find_by_email(&conn, "test@example.com").unwrap().id
|
||||
);
|
||||
assert_eq!(
|
||||
test_user.id,
|
||||
User::find_by_ap_url(
|
||||
conn,
|
||||
&conn,
|
||||
&format!(
|
||||
"https://{}/@/{}/",
|
||||
Instance::get_local().unwrap().public_domain,
|
||||
@@ -1120,13 +1278,11 @@ pub(crate) mod tests {
|
||||
fn delete() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let inserted = fill_database(conn);
|
||||
let inserted = fill_database(&conn);
|
||||
|
||||
assert!(User::get(conn, inserted[0].id).is_ok());
|
||||
inserted[0]
|
||||
.delete(conn, &get_searcher(&CONFIG.search_tokenizers))
|
||||
.unwrap();
|
||||
assert!(User::get(conn, inserted[0].id).is_err());
|
||||
assert!(User::get(&conn, inserted[0].id).is_ok());
|
||||
inserted[0].delete(&conn).unwrap();
|
||||
assert!(User::get(&conn, inserted[0].id).is_err());
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
@@ -1135,20 +1291,20 @@ pub(crate) mod tests {
|
||||
fn admin() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let inserted = fill_database(conn);
|
||||
let inserted = fill_database(&conn);
|
||||
let local_inst = Instance::get_local().unwrap();
|
||||
let mut i = 0;
|
||||
while local_inst.has_admin(conn).unwrap() {
|
||||
while local_inst.has_admin(&conn).unwrap() {
|
||||
assert!(i < 100); //prevent from looping indefinitelly
|
||||
local_inst
|
||||
.main_admin(conn)
|
||||
.main_admin(&conn)
|
||||
.unwrap()
|
||||
.set_role(conn, Role::Normal)
|
||||
.set_role(&conn, Role::Normal)
|
||||
.unwrap();
|
||||
i += 1;
|
||||
}
|
||||
inserted[0].set_role(conn, Role::Admin).unwrap();
|
||||
assert_eq!(inserted[0].id, local_inst.main_admin(conn).unwrap().id);
|
||||
inserted[0].set_role(&conn, Role::Admin).unwrap();
|
||||
assert_eq!(inserted[0].id, local_inst.main_admin(&conn).unwrap().id);
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
@@ -1157,20 +1313,23 @@ pub(crate) mod tests {
|
||||
fn auth() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
fill_database(conn);
|
||||
fill_database(&conn);
|
||||
let test_user = NewUser::new_local(
|
||||
conn,
|
||||
&conn,
|
||||
"test".to_owned(),
|
||||
"test user".to_owned(),
|
||||
Role::Normal,
|
||||
"Hello I'm a test",
|
||||
"test@example.com".to_owned(),
|
||||
User::hash_pass("test_password").unwrap(),
|
||||
Some(User::hash_pass("test_password").unwrap()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(test_user.auth("test_password"));
|
||||
assert!(!test_user.auth("other_password"));
|
||||
assert_eq!(
|
||||
User::login(&conn, "test", "test_password").unwrap().id,
|
||||
test_user.id
|
||||
);
|
||||
assert!(User::login(&conn, "test", "other_password").is_err());
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
@@ -1179,26 +1338,26 @@ pub(crate) mod tests {
|
||||
fn get_local_page() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
fill_database(conn);
|
||||
fill_database(&conn);
|
||||
|
||||
let page = User::get_local_page(conn, (0, 2)).unwrap();
|
||||
let page = User::get_local_page(&conn, (0, 2)).unwrap();
|
||||
assert_eq!(page.len(), 2);
|
||||
assert!(page[0].username <= page[1].username);
|
||||
|
||||
let mut last_username = User::get_local_page(conn, (0, 1)).unwrap()[0]
|
||||
let mut last_username = User::get_local_page(&conn, (0, 1)).unwrap()[0]
|
||||
.username
|
||||
.clone();
|
||||
for i in 1..User::count_local(conn).unwrap() as i32 {
|
||||
let page = User::get_local_page(conn, (i, i + 1)).unwrap();
|
||||
for i in 1..User::count_local(&conn).unwrap() as i32 {
|
||||
let page = User::get_local_page(&conn, (i, i + 1)).unwrap();
|
||||
assert_eq!(page.len(), 1);
|
||||
assert!(last_username <= page[0].username);
|
||||
last_username = page[0].username.clone();
|
||||
}
|
||||
assert_eq!(
|
||||
User::get_local_page(conn, (0, User::count_local(conn).unwrap() as i32 + 10))
|
||||
User::get_local_page(&conn, (0, User::count_local(&conn).unwrap() as i32 + 10))
|
||||
.unwrap()
|
||||
.len() as i64,
|
||||
User::count_local(conn).unwrap()
|
||||
User::count_local(&conn).unwrap()
|
||||
);
|
||||
Ok(())
|
||||
});
|
||||
@@ -1206,14 +1365,13 @@ pub(crate) mod tests {
|
||||
|
||||
#[test]
|
||||
fn self_federation() {
|
||||
let r = rockets();
|
||||
let conn = &*r.conn;
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let users = fill_database(conn);
|
||||
let users = fill_database(&conn);
|
||||
|
||||
let ap_repr = users[0].to_activity(conn).unwrap();
|
||||
users[0].delete(conn, &*r.searcher).unwrap();
|
||||
let user = User::from_activity(&r, ap_repr).unwrap();
|
||||
let ap_repr = users[0].to_activity(&conn).unwrap();
|
||||
users[0].delete(&conn).unwrap();
|
||||
let user = User::from_activity(&conn, ap_repr).unwrap();
|
||||
|
||||
assert_eq!(user.username, users[0].username);
|
||||
assert_eq!(user.display_name, users[0].display_name);
|
||||
@@ -1224,7 +1382,7 @@ pub(crate) mod tests {
|
||||
assert_eq!(user.public_key, users[0].public_key);
|
||||
assert_eq!(user.shared_inbox_url, users[0].shared_inbox_url);
|
||||
assert_eq!(user.followers_endpoint, users[0].followers_endpoint);
|
||||
assert_eq!(user.avatar_url(conn), users[0].avatar_url(conn));
|
||||
assert_eq!(user.avatar_url(&conn), users[0].avatar_url(&conn));
|
||||
assert_eq!(user.fqn, users[0].fqn);
|
||||
assert_eq!(user.summary_html, users[0].summary_html);
|
||||
Ok(())
|
||||
|
||||
+18
-12
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:06\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Afrikaans\n"
|
||||
"Language: af_ZA\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"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: af\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+18
-12
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:06\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Arabic\n"
|
||||
"Language: ar_SA\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
|
||||
"X-Crowdin-Project: plume\n"
|
||||
"X-Crowdin-Project-ID: 352097\n"
|
||||
"X-Crowdin-Language: ar\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "فتح محرر النصوص الغني"
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "العنوان"
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr "العنوان الثانوي أو الملخص"
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "اكتب مقالك هنا. ماركداون مُدَعَّم."
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "يتبقا {} حرفا تقريبا"
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "الوسوم"
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "الرخصة"
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr "الغلاف"
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "هذه مسودة"
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "نشر كتابا"
|
||||
|
||||
|
||||
+18
-12
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:06\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Bulgarian\n"
|
||||
"Language: bg_BG\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"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: bg\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "Отворете редактора с богат текст"
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "Заглавие"
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr "Подзаглавие или резюме"
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "Напишете статията си тук. Поддържа се Markdown."
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "Остават {} знака вляво"
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "Етикети"
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "Лиценз"
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr "Основно изображение"
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "Това е проект"
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "Публикувай"
|
||||
|
||||
|
||||
+18
-12
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:06\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Catalan\n"
|
||||
"Language: ca_ES\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"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: ca\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "Obre l’editor de text enriquit"
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "Títol"
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr "Subtítol o resum"
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "Escriviu el vostre article ací. Podeu fer servir el Markdown."
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "Queden uns {} caràcters"
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetes"
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "Llicència"
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr "Coberta"
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "Açò és un esborrany"
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "Publica"
|
||||
|
||||
|
||||
+18
-12
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:06\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Czech\n"
|
||||
"Language: cs_CZ\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
|
||||
"X-Crowdin-Project: plume\n"
|
||||
"X-Crowdin-Project-ID: 352097\n"
|
||||
"X-Crowdin-Language: cs\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "Otevřít editor formátovaného textu"
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "Nadpis"
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr "Podnadpis, nebo shrnutí"
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "Sem napište svůj článek. Markdown je podporován."
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "Zbývá kolem {} znaků"
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "Tagy"
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "Licence"
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr "Titulka"
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "Tohle je koncept"
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "Zveřejnit"
|
||||
|
||||
|
||||
@@ -10,12 +10,17 @@ msgstr ""
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=6; plural=(n == 0) ? 0 : ((n == 1) ? 1 : ((n == 2) ? 2 : ((n == 3) ? 3 : ((n == 6) ? 4 : 5))));\n"
|
||||
"Plural-Forms: nplurals=6; plural=(n == 0) ? 0 : ((n == 1) ? 1 : ((n == 2) ? "
|
||||
"2 : ((n == 3) ? 3 : ((n == 6) ? 4 : 5))));\n"
|
||||
"X-Generator: crowdin.com\n"
|
||||
"X-Crowdin-Project: plume\n"
|
||||
"X-Crowdin-Language: cy\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:114
|
||||
msgid "Open the rich text editor"
|
||||
msgstr ""
|
||||
@@ -24,8 +29,8 @@ msgstr ""
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:147
|
||||
msgid "Subtitle or summary"
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:154
|
||||
@@ -55,4 +60,3 @@ msgstr ""
|
||||
# plume-front/src/editor.rs:259
|
||||
msgid "Publish"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+18
-12
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:06\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Danish\n"
|
||||
"Language: da_DK\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"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: da\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+18
-12
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:07\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Language: de_DE\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"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: de\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr " Rich Text Editor (RTE) öffnen"
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "Titel"
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr "Untertitel oder Zusammenfassung"
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "Schreiben deinen Artikel hier. Markdown wird unterstützt."
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "Ungefähr {} Zeichen übrig"
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "Schlagwörter"
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "Lizenz"
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr "Einband"
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "Dies ist ein Entwurf"
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "Veröffentlichen"
|
||||
|
||||
|
||||
+18
-12
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:07\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Greek\n"
|
||||
"Language: el_GR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"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: el\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+18
-12
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:07\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
"Language: en_US\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"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: en\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+18
-12
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:07\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Esperanto\n"
|
||||
"Language: eo_UY\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"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: eo\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "Malfermi la riĉan redaktilon"
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "Titolo"
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "Verku vian artikolon ĉi tie. Markdown estas subtenita."
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "Proksimume {} signoj restantaj"
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "Etikedoj"
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "Permesilo"
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr "Kovro"
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "Malfinias"
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "Eldoni"
|
||||
|
||||
|
||||
+19
-13
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:07\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:56\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Spanish\n"
|
||||
"Language: es_ES\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"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: es-ES\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "Abrir el editor de texto enriquecido"
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "Título"
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr ""
|
||||
msgstr "Subtítulo, o resumen"
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "Escriba su artículo aquí. Puede utilizar Markdown."
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "Quedan unos {} caracteres"
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetas"
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "Licencia"
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr "Cubierta"
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "Esto es un borrador"
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "Publicar"
|
||||
|
||||
|
||||
+20
-14
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:07\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:56\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Persian\n"
|
||||
"Language: fa_IR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"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: fa\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "باز کردن ویرایشگر غنی"
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "عنوان"
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr "زیرعنوان، یا چکیده"
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "مقاله را اینجا بنویسید. از مارکداون پشتیبانی میشود."
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "نزدیک به {} نویسه باقی مانده است"
|
||||
msgstr "نزدیک به {} حرف باقی مانده است"
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "برچسبها"
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "پروانه"
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr ""
|
||||
msgstr "تصویر شاخص"
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "این، یک پیشنویس است"
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "انتشار"
|
||||
|
||||
|
||||
+18
-12
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:07\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Finnish\n"
|
||||
"Language: fi_FI\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"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: fi\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "Avaa edistynyt tekstieditori"
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "Otsikko"
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr "Alaotsikko tai tiivistelmä"
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "Kirjoita artikkelisi tähän. Markdown -kuvauskieli on tuettu."
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "%{count} merkkiä jäljellä"
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "Tagit"
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "Lisenssi"
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr "Kansi"
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "Tämä on luonnos"
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "Julkaise"
|
||||
|
||||
|
||||
+19
-13
@@ -3,55 +3,61 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:07\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Language: fr_FR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
"X-Crowdin-Project: plume\n"
|
||||
"X-Crowdin-Project-ID: 352097\n"
|
||||
"X-Crowdin-Language: fr\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "Ouvrir l'éditeur de texte avancé"
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "Titre"
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr "Sous-titre ou résumé"
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "Écrivez votre article ici. Vous pouvez utiliser du Markdown."
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "Environ {} caractères restant"
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "Étiquettes"
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "Licence"
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr "Illustration"
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "Ceci est un brouillon"
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "Publier"
|
||||
|
||||
|
||||
+21
-15
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:07\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Galician\n"
|
||||
"Language: gl_ES\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"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: gl\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "Abra o editor de texto enriquecido"
|
||||
msgstr "Abre o editor de texto enriquecido"
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "Título"
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr "Subtítulo, ou resumo"
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "Escriba aquí o seu artigo: pode utilizar Markdown."
|
||||
msgstr "Escribe aquí o teu artigo: podes utilizar Markdown."
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "Dispón de {} caracteres restantes"
|
||||
msgstr "Dispós de {} caracteres"
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetas"
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "Licenza"
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr "Portada"
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "Este é un borrador"
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "Publicar"
|
||||
|
||||
|
||||
+18
-12
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:07\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Hebrew\n"
|
||||
"Language: he_IL\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n"
|
||||
"X-Crowdin-Project: plume\n"
|
||||
"X-Crowdin-Project-ID: 352097\n"
|
||||
"X-Crowdin-Language: he\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+18
-12
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:07\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Hindi\n"
|
||||
"Language: hi_IN\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"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: hi\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "शीर्षक"
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "अपना आर्टिकल या लेख यहाँ लिखें. Markdown उपलब्ध है."
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "लगभग {} अक्षर बाकी हैं"
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "टैग्स"
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "लाइसेंस"
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "पब्लिश करें"
|
||||
|
||||
|
||||
+18
-12
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:06\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Croatian\n"
|
||||
"Language: hr_HR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
"X-Crowdin-Project: plume\n"
|
||||
"X-Crowdin-Project-ID: 352097\n"
|
||||
"X-Crowdin-Language: hr\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "Naslov"
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "Tagovi"
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "Licenca"
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "Objavi"
|
||||
|
||||
|
||||
+18
-12
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:07\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Hungarian\n"
|
||||
"Language: hu_HU\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"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: hu\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+18
-12
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:07\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Italian\n"
|
||||
"Language: it_IT\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"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: it\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "Apri il compositore di testo avanzato"
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "Titolo"
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr "Sottotitolo, o sommario"
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "Scrivi qui il tuo articolo. È supportato il Markdown."
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "Circa {} caratteri rimasti"
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "Etichette"
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "Licenza"
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr "Copertina"
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "Questa è una bozza"
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "Pubblica"
|
||||
|
||||
|
||||
+18
-12
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:07\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Japanese\n"
|
||||
"Language: ja_JP\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Crowdin-Project: plume\n"
|
||||
"X-Crowdin-Project-ID: 352097\n"
|
||||
"X-Crowdin-Language: ja\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "リッチテキストエディターを開く"
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "タイトル"
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr "サブタイトル、または概要"
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "投稿をここに書きます。Markdown がサポートされています。"
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "残り約 {} 文字"
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "タグ"
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "ライセンス"
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr "カバー"
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "これは下書きです"
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "公開"
|
||||
|
||||
|
||||
+18
-12
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:07\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:56\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Korean\n"
|
||||
"Language: ko_KR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Crowdin-Project: plume\n"
|
||||
"X-Crowdin-Project-ID: 352097\n"
|
||||
"X-Crowdin-Language: ko\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
msgid "Open the rich text editor"
|
||||
msgstr ""
|
||||
|
||||
+28
-22
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:06\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Dutch\n"
|
||||
"Language: nl_NL\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"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: nl\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr ""
|
||||
msgstr "Open de rich-text editor"
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
msgstr "Titel"
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr ""
|
||||
msgstr "Ondertitel of samenvatting"
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr ""
|
||||
msgstr "Schrijf hier je artikel. Markdown wordt ondersteund."
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr ""
|
||||
msgstr "Ongeveer {} tekens over"
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
msgstr "Tags"
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr ""
|
||||
msgstr "Licentie"
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr ""
|
||||
msgstr "Hoofdafbeelding"
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr ""
|
||||
msgstr "Dit is een concept"
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr ""
|
||||
msgstr "Publiceren"
|
||||
|
||||
|
||||
+18
-12
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:07\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:56\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Norwegian\n"
|
||||
"Language: no_NO\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"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: no\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+18
-12
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:07\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:56\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Polish\n"
|
||||
"Language: pl_PL\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
||||
"X-Crowdin-Project: plume\n"
|
||||
"X-Crowdin-Project-ID: 352097\n"
|
||||
"X-Crowdin-Language: pl\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "Otwórz edytor tekstu sformatowanego"
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "Tytuł"
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr "Podtytuł, lub podsumowanie"
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "Napisz swój artykuł tutaj. Markdown jest obsługiwany."
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "Pozostało w okolicy {} znaków"
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "Tagi"
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "Licencja"
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr "Okładka"
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "To jest szkic"
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "Publikuj"
|
||||
|
||||
|
||||
@@ -12,42 +12,46 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:188
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:281
|
||||
msgid "Open the rich text editor"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:314
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:318
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:325
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:336
|
||||
msgid "Around {} characters left"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:413
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "License"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:417
|
||||
msgid "Cover"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:437
|
||||
msgid "This is a draft"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:444
|
||||
msgid "Publish"
|
||||
msgstr ""
|
||||
|
||||
+18
-12
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2019-12-16 21:07\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:56\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Portuguese, Brazilian\n"
|
||||
"Language: pt_BR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12,46 +12,52 @@ msgstr ""
|
||||
"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: pt-BR\n"
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:115
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "Abrir o editor de rich text"
|
||||
|
||||
# plume-front/src/editor.rs:145
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "Título"
|
||||
|
||||
# plume-front/src/editor.rs:149
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr "Subtítulo ou resumo"
|
||||
|
||||
# plume-front/src/editor.rs:156
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "Escreva seu artigo aqui. Markdown é suportado."
|
||||
|
||||
# plume-front/src/editor.rs:167
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "Cerca de {} caracteres restantes"
|
||||
|
||||
# plume-front/src/editor.rs:243
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "Tags"
|
||||
|
||||
# plume-front/src/editor.rs:244
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "Licença"
|
||||
|
||||
# plume-front/src/editor.rs:247
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr "Capa"
|
||||
|
||||
# plume-front/src/editor.rs:267
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "Isso é um rascunho"
|
||||
|
||||
# plume-front/src/editor.rs:274
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "Publicar"
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user