Compare commits
343 Commits
0.5.0
...
ldap-non-anon
| 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 | |||
| 28576c1fa3 |
@@ -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
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
docker:
|
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>>
|
- image: <<#parameters.postgres>>circleci/postgres:9.6-alpine<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
@@ -21,6 +21,7 @@ executors:
|
|||||||
RUST_TEST_THREADS: 1
|
RUST_TEST_THREADS: 1
|
||||||
FEATURES: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
|
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>>
|
DATABASE_URL: <<#parameters.postgres>>postgres://postgres@localhost/plume<</parameters.postgres>><<^parameters.postgres>>plume.sqlite<</parameters.postgres>>
|
||||||
|
ROCKET_SECRET_KEY: VN5xV1DN7XdpATadOCYcuGeR/dV0hHfgx9mx9TarLdM=
|
||||||
|
|
||||||
|
|
||||||
commands:
|
commands:
|
||||||
@@ -71,7 +72,7 @@ commands:
|
|||||||
type: string
|
type: string
|
||||||
steps:
|
steps:
|
||||||
- run: |
|
- 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
|
export CARGO_INCREMENTAL=0
|
||||||
<< parameters.cmd >>
|
<< parameters.cmd >>
|
||||||
|
|
||||||
@@ -99,7 +100,7 @@ commands:
|
|||||||
steps:
|
steps:
|
||||||
- run: |
|
- run: |
|
||||||
cmd="cargo build <<#parameters.release>>--release<</parameters.release>> --no-default-features --features="${FEATURES}" -p <<parameters.package>> -j"
|
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
|
$cmd $i && exit 0
|
||||||
done
|
done
|
||||||
exit 1
|
exit 1
|
||||||
@@ -143,11 +144,14 @@ jobs:
|
|||||||
cache: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
|
cache: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
|
||||||
- run_with_coverage:
|
- run_with_coverage:
|
||||||
cmd: |
|
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"
|
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
|
$cmd $i && break
|
||||||
done
|
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:
|
- upload_coverage:
|
||||||
type: unit
|
type: unit
|
||||||
- cache:
|
- cache:
|
||||||
@@ -164,18 +168,18 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- restore_env:
|
- restore_env:
|
||||||
cache: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
|
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:
|
- run_with_coverage:
|
||||||
cmd: |
|
cmd: |
|
||||||
cmd="cargo install --debug --no-default-features --features="${FEATURES}",test --force --path . -j"
|
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
|
$cmd $i && exit 0
|
||||||
done
|
done
|
||||||
exit 1
|
exit 1
|
||||||
- run_with_coverage:
|
- run_with_coverage:
|
||||||
cmd: |
|
cmd: |
|
||||||
cmd="cargo install --debug --no-default-features --features="${FEATURES}" --force --path plume-cli -j"
|
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
|
$cmd $i && exit 0
|
||||||
done
|
done
|
||||||
exit 1
|
exit 1
|
||||||
@@ -199,7 +203,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- restore_env:
|
- restore_env:
|
||||||
cache: release-<<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
|
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:
|
- build:
|
||||||
package: plume
|
package: plume
|
||||||
release: true
|
release: true
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
localhost:443 {
|
localhost {
|
||||||
proxy / localhost:7878 {
|
reverse_proxy localhost:7878
|
||||||
transparent
|
|
||||||
}
|
|
||||||
tls self_signed
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
FROM debian:stretch-20190326
|
FROM debian:buster-20210208
|
||||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||||
|
|
||||||
#install native/circleci/build dependancies
|
#install native/circleci/build dependancies
|
||||||
RUN apt update &&\
|
RUN apt update &&\
|
||||||
apt install -y --no-install-recommends git ssh tar gzip ca-certificates default-jre&&\
|
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/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
#install and configure rust
|
#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 rustfmt clippy &&\
|
||||||
rustup component add rust-std --target wasm32-unknown-unknown
|
rustup component add rust-std --target wasm32-unknown-unknown
|
||||||
|
|
||||||
#compile some deps
|
#compile some deps
|
||||||
RUN cargo install cargo-web &&\
|
RUN cargo install wasm-pack &&\
|
||||||
cargo install grcov &&\
|
cargo install grcov &&\
|
||||||
strip /root/.cargo/bin/* &&\
|
strip /root/.cargo/bin/* &&\
|
||||||
rm -fr ~/.cargo/registry
|
rm -fr ~/.cargo/registry
|
||||||
@@ -24,8 +27,7 @@ COPY cargo_config /root/.cargo/config
|
|||||||
#install selenium for front end tests
|
#install selenium for front end tests
|
||||||
RUN pip3 install selenium
|
RUN pip3 install selenium
|
||||||
|
|
||||||
#install and configure caddy
|
#configure caddy
|
||||||
RUN curl https://getcaddy.com | bash -s personal
|
|
||||||
COPY Caddyfile /Caddyfile
|
COPY Caddyfile /Caddyfile
|
||||||
|
|
||||||
#install crowdin
|
#install crowdin
|
||||||
|
|||||||
+12
@@ -15,6 +15,9 @@ DATABASE_URL=postgres://plume:plume@localhost/plume
|
|||||||
# The domain of your instance
|
# The domain of your instance
|
||||||
BASE_URL=plu.me
|
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
|
# The secret key for private cookies and CSRF protection
|
||||||
# You can generate one with `openssl rand -base64 32`
|
# You can generate one with `openssl rand -base64 32`
|
||||||
ROCKET_SECRET_KEY=
|
ROCKET_SECRET_KEY=
|
||||||
@@ -45,3 +48,12 @@ ROCKET_ADDRESS=127.0.0.1
|
|||||||
#PLUME_LOGO_192=icons/trwnh/paragraphs/plumeParagraphs192.png
|
#PLUME_LOGO_192=icons/trwnh/paragraphs/plumeParagraphs192.png
|
||||||
#PLUME_LOGO_256=icons/trwnh/paragraphs/plumeParagraphs256.png
|
#PLUME_LOGO_256=icons/trwnh/paragraphs/plumeParagraphs256.png
|
||||||
#PLUME_LOGO_512=icons/trwnh/paragraphs/plumeParagraphs512.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 -->
|
<!-- 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.**
|
**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 [...]
|
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
|
search_index
|
||||||
.buildconfig
|
.buildconfig
|
||||||
__pycache__
|
__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]
|
[package]
|
||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
name = "plume"
|
name = "plume"
|
||||||
version = "0.4.0"
|
version = "0.6.1-dev"
|
||||||
repository = "https://github.com/Plume-org/Plume"
|
repository = "https://github.com/Plume-org/Plume"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
@@ -10,8 +10,7 @@ activitypub = "0.1.3"
|
|||||||
askama_escape = "0.1"
|
askama_escape = "0.1"
|
||||||
atom_syndication = "0.6"
|
atom_syndication = "0.6"
|
||||||
clap = "2.33"
|
clap = "2.33"
|
||||||
colored = "1.8"
|
dotenv = "0.15.0"
|
||||||
dotenv = "0.14"
|
|
||||||
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
||||||
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||||
gettext-utils = { 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 = "0.9.2"
|
||||||
lettre_email = "0.9.2"
|
lettre_email = "0.9.2"
|
||||||
num_cpus = "1.10"
|
num_cpus = "1.10"
|
||||||
rocket = "0.4.2"
|
rocket = "0.4.6"
|
||||||
rocket_contrib = { version = "0.4.2", features = ["json"] }
|
rocket_contrib = { version = "0.4.5", features = ["json"] }
|
||||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||||
rpassword = "4.0"
|
rpassword = "4.0"
|
||||||
scheduled-thread-pool = "0.2.2"
|
scheduled-thread-pool = "0.2.2"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_qs = "0.5"
|
|
||||||
shrinkwraprs = "0.2.1"
|
shrinkwraprs = "0.2.1"
|
||||||
syntect = "3.3"
|
|
||||||
validator = "0.8"
|
validator = "0.8"
|
||||||
validator_derive = "0.8"
|
validator_derive = "0.8"
|
||||||
webfinger = "0.4.1"
|
webfinger = "0.4.1"
|
||||||
|
tracing = "0.1.22"
|
||||||
|
tracing-subscriber = "0.2.15"
|
||||||
|
riker = "0.4.2"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "plume"
|
name = "plume"
|
||||||
@@ -48,7 +48,7 @@ version = "3.1.2"
|
|||||||
|
|
||||||
[dependencies.diesel]
|
[dependencies.diesel]
|
||||||
features = ["r2d2", "chrono"]
|
features = ["r2d2", "chrono"]
|
||||||
version = "*"
|
version = "1.4.5"
|
||||||
|
|
||||||
[dependencies.multipart]
|
[dependencies.multipart]
|
||||||
default-features = false
|
default-features = false
|
||||||
@@ -69,7 +69,7 @@ git = "https://github.com/fdb-hiroshima/rocket_csrf"
|
|||||||
rev = "29910f2829e7e590a540da3804336577b48c7b31"
|
rev = "29910f2829e7e590a540da3804336577b48c7b31"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
ructe = "0.9.0"
|
ructe = "0.13.0"
|
||||||
rsass = "0.9"
|
rsass = "0.9"
|
||||||
|
|
||||||
[features]
|
[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 \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
@@ -19,7 +19,7 @@ RUN chmod a+x ./wasm-deps.sh && sleep 1 && ./wasm-deps.sh
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY Cargo.toml Cargo.lock rust-toolchain ./
|
COPY Cargo.toml Cargo.lock rust-toolchain ./
|
||||||
RUN cargo install cargo-web
|
RUN cargo install wasm-pack
|
||||||
|
|
||||||
COPY . .
|
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 install --path plume-cli --force --no-default-features --features postgres
|
||||||
RUN cargo clean
|
RUN cargo clean
|
||||||
|
|
||||||
FROM debian:stretch-slim
|
FROM debian:buster-slim
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
ca-certificates \
|
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 \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
@@ -10,7 +10,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
gcc \
|
gcc \
|
||||||
make \
|
make \
|
||||||
openssl \
|
openssl \
|
||||||
libssl-dev
|
libssl-dev\
|
||||||
|
clang
|
||||||
|
|
||||||
WORKDIR /scratch
|
WORKDIR /scratch
|
||||||
COPY script/wasm-deps.sh .
|
COPY script/wasm-deps.sh .
|
||||||
@@ -19,7 +20,7 @@ RUN chmod a+x ./wasm-deps.sh && sleep 1 && ./wasm-deps.sh
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY Cargo.toml Cargo.lock rust-toolchain ./
|
COPY Cargo.toml Cargo.lock rust-toolchain ./
|
||||||
RUN cargo install diesel_cli --no-default-features --features postgres --version '=1.3.0'
|
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 . .
|
COPY . .
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ rules and thematics, but they can all communicate with each other.
|
|||||||
|
|
||||||
## Get involved
|
## 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:
|
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:
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ main header.article {
|
|||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
h1, .article-info {
|
h1, .article-info {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -490,3 +490,30 @@ input:checked ~ .cw-container > .cw-text {
|
|||||||
display: inline;
|
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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -183,6 +183,8 @@ p.error {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
min-width: 20em;
|
min-width: 20em;
|
||||||
min-height: 20em;
|
min-height: 20em;
|
||||||
margin: 1em;
|
margin: 1em;
|
||||||
@@ -222,14 +224,21 @@ p.error {
|
|||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin: 0.75em 20px;
|
flex-grow: 1;
|
||||||
|
margin: 0;
|
||||||
font-family: $playfair;
|
font-family: $playfair;
|
||||||
font-size: 1.75em;
|
font-size: 1.75em;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
line-height: 1.75;
|
||||||
a {
|
a {
|
||||||
|
display: block;
|
||||||
transition: color 0.1s ease-in;
|
transition: color 0.1s ease-in;
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
|
|
||||||
@@ -237,6 +246,15 @@ p.error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
float: right;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
@@ -490,6 +508,10 @@ figure {
|
|||||||
|
|
||||||
/// Small screens
|
/// Small screens
|
||||||
@media screen and (max-width: 600px) {
|
@media screen and (max-width: 600px) {
|
||||||
|
body > main > *, .h-feed > * {
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
main .article-meta {
|
main .article-meta {
|
||||||
> *, .comments {
|
> *, .comments {
|
||||||
margin: 0 5%;
|
margin: 0 5%;
|
||||||
@@ -535,7 +557,7 @@ figure {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
max-width: 100%;
|
max-width: 100% !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,18 @@ body > header {
|
|||||||
opacity: 1;
|
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 {
|
body > header {
|
||||||
flex-direction: column;
|
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;
|
position: fixed;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -204,7 +216,7 @@ body > header {
|
|||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, 0);
|
transform: translateZ(0);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -221,3 +233,105 @@ body > header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Small screens
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
@keyframes menuOpening {
|
||||||
|
from {
|
||||||
|
transform: scaleX(0);
|
||||||
|
transform-origin: left;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scaleX(1);
|
||||||
|
transform-origin: left;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-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,10 +1,10 @@
|
|||||||
/* Color Scheme */
|
/* Color Scheme */
|
||||||
$gray: #F3F3F3;
|
$gray: #f3f3f3;
|
||||||
$black: #242424;
|
$black: #242424;
|
||||||
$white: #F8F8F8;
|
$white: #f8f8f8;
|
||||||
$purple: #7765E3;
|
$purple: #7765e3;
|
||||||
$lightpurple: #c2bbee;
|
$lightpurple: #c2bbee;
|
||||||
$red: #E92F2F;
|
$red: #e92f2f;
|
||||||
$yellow: #ffe347;
|
$yellow: #ffe347;
|
||||||
$green: #23f0c7;
|
$green: #23f0c7;
|
||||||
|
|
||||||
@@ -24,14 +24,14 @@ $margin: 0 $horizontal-margin;
|
|||||||
|
|
||||||
/* Fonts */
|
/* Fonts */
|
||||||
|
|
||||||
$route159: "Route159", serif;
|
$route159: "Shabnam", "Route159", serif;
|
||||||
$playfair: "Playfair Display", serif;
|
$playfair: "Vazir", "Playfair Display", serif;
|
||||||
$lora: "Lora", serif;
|
$lora: "Vazir", "Lora", serif;
|
||||||
|
|
||||||
//Code Highlighting
|
//Code Highlighting
|
||||||
$code-keyword-color: #45244a;
|
$code-keyword-color: #45244a;
|
||||||
$code-source-color: #4c588c;
|
$code-source-color: #4c588c;
|
||||||
$code-constant-color: scale-color(magenta,$lightness:-5%);
|
$code-constant-color: scale-color(magenta, $lightness: -5%);
|
||||||
$code-operator-color: scale-color($code-source-color,$lightness:-5%);
|
$code-operator-color: scale-color($code-source-color, $lightness: -5%);
|
||||||
$code-string-color: #8a571c;
|
$code-string-color: #8a571c;
|
||||||
$code-comment-color: #1c4c8a;
|
$code-comment-color: #1c4c8a;
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
/* color palette: https://coolors.co/23f0c7-ef767a-7765e3-6457a6-ffe347 */
|
/* color palette: https://coolors.co/23f0c7-ef767a-7765e3-6457a6-ffe347 */
|
||||||
|
|
||||||
@import url('./feather.css');
|
@import url("./feather.css");
|
||||||
@import url('./fonts/Route159/Route159.css');
|
@import url("./fonts/Route159/Route159.css");
|
||||||
@import url('./fonts/Lora/Lora.css');
|
@import url("./fonts/Lora/Lora.css");
|
||||||
@import url('./fonts/Playfair_Display/PlayfairDisplay.css');
|
@import url("./fonts/Playfair_Display/PlayfairDisplay.css");
|
||||||
|
@import url("./fonts/Vazir_WOL/Vazir_WOL.css");
|
||||||
|
@import url("./fonts/Shabnam_WOL/Shabnam_WOL.css");
|
||||||
|
|
||||||
@import 'dark_variables';
|
@import "dark_variables";
|
||||||
@import 'global';
|
@import "global";
|
||||||
@import 'header';
|
@import "header";
|
||||||
@import 'article';
|
@import "article";
|
||||||
@import 'forms';
|
@import "forms";
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
Copyright (c) 2015, Saber Rastikerdar (saber.rastikerdar@gmail.com),
|
||||||
|
Glyphs and data from Roboto font are licensed under the Apache License, Version 2.0.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,49 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: Shabnam;
|
||||||
|
src: url("Shabnam-WOL.eot");
|
||||||
|
src: url("Shabnam-WOL.eot?#iefix") format("embedded-opentype"),
|
||||||
|
url("Shabnam-WOL.woff2") format("woff2"),
|
||||||
|
url("Shabnam-WOL.woff") format("woff"),
|
||||||
|
url("Shabnam-WOL.ttf") format("truetype");
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Shabnam;
|
||||||
|
src: url("Shabnam-Bold-WOL.eot");
|
||||||
|
src: url("Shabnam-Bold-WOL.eot?#iefix") format("embedded-opentype"),
|
||||||
|
url("Shabnam-Bold-WOL.woff2") format("woff2"),
|
||||||
|
url("Shabnam-Bold-WOL.woff") format("woff"),
|
||||||
|
url("Shabnam-Bold-WOL.ttf") format("truetype");
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Shabnam;
|
||||||
|
src: url("Shabnam-Thin-WOL.eot");
|
||||||
|
src: url("Shabnam-Thin-WOL.eot?#iefix") format("embedded-opentype"),
|
||||||
|
url("Shabnam-Thin-WOL.woff2") format("woff2"),
|
||||||
|
url("Shabnam-Thin-WOL.woff") format("woff"),
|
||||||
|
url("Shabnam-Thin-WOL.ttf") format("truetype");
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Shabnam;
|
||||||
|
src: url("Shabnam-Light-WOL.eot");
|
||||||
|
src: url("Shabnam-Light-WOL.eot?#iefix") format("embedded-opentype"),
|
||||||
|
url("Shabnam-Light-WOL.woff2") format("woff2"),
|
||||||
|
url("Shabnam-Light-WOL.woff") format("woff"),
|
||||||
|
url("Shabnam-Light-WOL.ttf") format("truetype");
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Shabnam;
|
||||||
|
src: url("Shabnam-Medium-WOL.eot");
|
||||||
|
src: url("Shabnam-Medium-WOL.eot?#iefix") format("embedded-opentype"),
|
||||||
|
url("Shabnam-Medium-WOL.woff2") format("woff2"),
|
||||||
|
url("Shabnam-Medium-WOL.woff") format("woff"),
|
||||||
|
url("Shabnam-Medium-WOL.ttf") format("truetype");
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
Changes by Saber Rastikerdar (saber.rastikerdar@gmail.com) are in public domain.
|
||||||
|
Glyphs and data from Roboto font are licensed under the Apache License, Version 2.0.
|
||||||
|
|
||||||
|
Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
|
||||||
|
|
||||||
|
Bitstream Vera Fonts Copyright
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is
|
||||||
|
a trademark of Bitstream, Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of the fonts accompanying this license ("Fonts") and associated
|
||||||
|
documentation files (the "Font Software"), to reproduce and distribute the
|
||||||
|
Font Software, including without limitation the rights to use, copy, merge,
|
||||||
|
publish, distribute, and/or sell copies of the Font Software, and to permit
|
||||||
|
persons to whom the Font Software is furnished to do so, subject to the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
|
The above copyright and trademark notices and this permission notice shall
|
||||||
|
be included in all copies of one or more of the Font Software typefaces.
|
||||||
|
|
||||||
|
The Font Software may be modified, altered, or added to, and in particular
|
||||||
|
the designs of glyphs or characters in the Fonts may be modified and
|
||||||
|
additional glyphs or characters may be added to the Fonts, only if the fonts
|
||||||
|
are renamed to names not containing either the words "Bitstream" or the word
|
||||||
|
"Vera".
|
||||||
|
|
||||||
|
This License becomes null and void to the extent applicable to Fonts or Font
|
||||||
|
Software that has been modified and is distributed under the "Bitstream
|
||||||
|
Vera" names.
|
||||||
|
|
||||||
|
The Font Software may be sold as part of a larger software package but no
|
||||||
|
copy of one or more of the Font Software typefaces may be sold by itself.
|
||||||
|
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
|
||||||
|
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
|
||||||
|
FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
|
||||||
|
ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
|
||||||
|
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||||
|
THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
|
||||||
|
FONT SOFTWARE.
|
||||||
|
|
||||||
|
Except as contained in this notice, the names of Gnome, the Gnome
|
||||||
|
Foundation, and Bitstream Inc., shall not be used in advertising or
|
||||||
|
otherwise to promote the sale, use or other dealings in this Font Software
|
||||||
|
without prior written authorization from the Gnome Foundation or Bitstream
|
||||||
|
Inc., respectively. For further information, contact: fonts at gnome dot
|
||||||
|
org.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,65 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: Vazir;
|
||||||
|
src: url('Vazir-WOL.eot');
|
||||||
|
src: url('Vazir-WOL.eot?#iefix') format('embedded-opentype'),
|
||||||
|
url('Vazir-WOL.woff2') format('woff2'),
|
||||||
|
url('Vazir-WOL.woff') format('woff'),
|
||||||
|
url('Vazir-WOL.ttf') format('truetype');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Vazir;
|
||||||
|
src: url('Vazir-Bold-WOL.eot');
|
||||||
|
src: url('Vazir-Bold-WOL.eot?#iefix') format('embedded-opentype'),
|
||||||
|
url('Vazir-Bold-WOL.woff2') format('woff2'),
|
||||||
|
url('Vazir-Bold-WOL.woff') format('woff'),
|
||||||
|
url('Vazir-Bold-WOL.ttf') format('truetype');
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Vazir;
|
||||||
|
src: url('Vazir-Black-WOL.eot');
|
||||||
|
src: url('Vazir-Black-WOL.eot?#iefix') format('embedded-opentype'),
|
||||||
|
url('Vazir-Black-WOL.woff2') format('woff2'),
|
||||||
|
url('Vazir-Black-WOL.woff') format('woff'),
|
||||||
|
url('Vazir-Black-WOL.ttf') format('truetype');
|
||||||
|
font-weight: 900;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Vazir;
|
||||||
|
src: url('Vazir-Medium-WOL.eot');
|
||||||
|
src: url('Vazir-Medium-WOL.eot?#iefix') format('embedded-opentype'),
|
||||||
|
url('Vazir-Medium-WOL.woff2') format('woff2'),
|
||||||
|
url('Vazir-Medium-WOL.woff') format('woff'),
|
||||||
|
url('Vazir-Medium-WOL.ttf') format('truetype');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Vazir;
|
||||||
|
src: url('Vazir-Light-WOL.eot');
|
||||||
|
src: url('Vazir-Light-WOL.eot?#iefix') format('embedded-opentype'),
|
||||||
|
url('Vazir-Light-WOL.woff2') format('woff2'),
|
||||||
|
url('Vazir-Light-WOL.woff') format('woff'),
|
||||||
|
url('Vazir-Light-WOL.ttf') format('truetype');
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Vazir;
|
||||||
|
src: url('Vazir-Thin-WOL.eot');
|
||||||
|
src: url('Vazir-Thin-WOL.eot?#iefix') format('embedded-opentype'),
|
||||||
|
url('Vazir-Thin-WOL.woff2') format('woff2'),
|
||||||
|
url('Vazir-Thin-WOL.woff') format('woff'),
|
||||||
|
url('Vazir-Thin-WOL.ttf') format('truetype');
|
||||||
|
font-weight: 100;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
/* color palette: https://coolors.co/23f0c7-ef767a-7765e3-6457a6-ffe347 */
|
/* color palette: https://coolors.co/23f0c7-ef767a-7765e3-6457a6-ffe347 */
|
||||||
|
|
||||||
@import url('./feather.css');
|
@import url("./feather.css");
|
||||||
@import url('./fonts/Route159/Route159.css');
|
@import url("./fonts/Route159/Route159.css");
|
||||||
@import url('./fonts/Lora/Lora.css');
|
@import url("./fonts/Lora/Lora.css");
|
||||||
@import url('./fonts/Playfair_Display/PlayfairDisplay.css');
|
@import url("./fonts/Playfair_Display/PlayfairDisplay.css");
|
||||||
|
@import url("./fonts/Vazir_WOL/Vazir_WOL.css");
|
||||||
|
@import url("./fonts/Shabnam_WOL/Shabnam_WOL.css");
|
||||||
|
|
||||||
@import 'variables';
|
@import "variables";
|
||||||
@import 'global';
|
@import "global";
|
||||||
@import 'header';
|
@import "header";
|
||||||
@import 'article';
|
@import "article";
|
||||||
@import 'forms';
|
@import "forms";
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
use rsass;
|
|
||||||
|
|
||||||
use ructe::Ructe;
|
use ructe::Ructe;
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
use std::{ffi::OsStr, fs::*, io::Write, path::*};
|
use std::{ffi::OsStr, fs::*, io::Write, path::*};
|
||||||
@@ -50,18 +48,12 @@ fn main() {
|
|||||||
create_dir_all(&Path::new("static").join("media")).expect("Couldn't init media directory");
|
create_dir_all(&Path::new("static").join("media")).expect("Couldn't init media directory");
|
||||||
|
|
||||||
let cache_id = &compute_static_hash()[..8];
|
let cache_id = &compute_static_hash()[..8];
|
||||||
println!("cargo:rerun-if-changed=target/deploy/plume-front.wasm");
|
println!("cargo:rerun-if-changed=plume-front/pkg/plume_front_bg.wasm");
|
||||||
copy("target/deploy/plume-front.wasm", "static/plume-front.wasm")
|
copy(
|
||||||
.and_then(|_| read_to_string("target/deploy/plume-front.js"))
|
"plume-front/pkg/plume_front_bg.wasm",
|
||||||
.and_then(|js| {
|
"static/plume_front_bg.wasm",
|
||||||
write(
|
|
||||||
"static/plume-front.js",
|
|
||||||
js.replace(
|
|
||||||
"\"plume-front.wasm\"",
|
|
||||||
&format!("\"/static/cached/{}/plume-front.wasm\"", cache_id),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
})
|
.and_then(|_| copy("plume-front/pkg/plume_front.js", "static/plume_front.js"))
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
println!("cargo:rustc-env=CACHE_ID={}", cache_id)
|
println!("cargo:rustc-env=CACHE_ID={}", cache_id)
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
"project_identifier": "plume"
|
"project_id": 352097
|
||||||
"api_key_env": CROWDIN_API_KEY
|
"api_token_env": "CROWDIN_API_KEY"
|
||||||
preserve_hierarchy: true
|
preserve_hierarchy: true
|
||||||
files:
|
files:
|
||||||
- source: /po/plume/plume.pot
|
- source: /po/plume/plume.pot
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "plume-api"
|
name = "plume-api"
|
||||||
version = "0.4.0"
|
version = "0.6.1-dev"
|
||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pre-release-hook = ["cargo", "fmt"]
|
||||||
|
pre-release-replacements = []
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "plume-cli"
|
name = "plume-cli"
|
||||||
version = "0.4.0"
|
version = "0.6.1-dev"
|
||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
@@ -11,11 +11,11 @@ path = "src/main.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
clap = "2.33"
|
clap = "2.33"
|
||||||
dotenv = "0.14"
|
dotenv = "0.14"
|
||||||
rpassword = "4.0"
|
rpassword = "5.0.0"
|
||||||
|
|
||||||
[dependencies.diesel]
|
[dependencies.diesel]
|
||||||
features = ["r2d2", "chrono"]
|
features = ["r2d2", "chrono"]
|
||||||
version = "*"
|
version = "1.4.5"
|
||||||
|
|
||||||
[dependencies.plume-models]
|
[dependencies.plume-models]
|
||||||
path = "../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 clap::App;
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
use plume_models::{instance::Instance, Connection as Conn, CONFIG};
|
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();
|
searcher.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unlock<'a>(args: &ArgMatches<'a>) {
|
fn unlock(args: &ArgMatches) {
|
||||||
let path = match args.value_of("path") {
|
let path = match args.value_of("path") {
|
||||||
None => Path::new(&CONFIG.search_index),
|
None => Path::new(&CONFIG.search_index),
|
||||||
Some(x) => Path::new(x),
|
Some(x) => Path::new(x),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||||
|
|
||||||
use plume_models::{instance::Instance, users::*, Connection};
|
use plume_models::{instance::Instance, users::*, Connection};
|
||||||
use rpassword;
|
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
|
|
||||||
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
||||||
@@ -132,7 +131,7 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
|||||||
role,
|
role,
|
||||||
&bio,
|
&bio,
|
||||||
email,
|
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");
|
.expect("Couldn't save new user");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "plume-common"
|
name = "plume-common"
|
||||||
version = "0.4.0"
|
version = "0.6.1-dev"
|
||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
@@ -14,15 +14,16 @@ heck = "0.3.0"
|
|||||||
hex = "0.3"
|
hex = "0.3"
|
||||||
hyper = "0.12.33"
|
hyper = "0.12.33"
|
||||||
openssl = "0.10.22"
|
openssl = "0.10.22"
|
||||||
rocket = "0.4.0"
|
rocket = "0.4.6"
|
||||||
reqwest = "0.9"
|
reqwest = { version = "0.9", features = ["socks"] }
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
shrinkwraprs = "0.2.1"
|
shrinkwraprs = "0.3.0"
|
||||||
syntect = "3.3"
|
syntect = "4.5.0"
|
||||||
tokio = "0.1.22"
|
tokio = "0.1.22"
|
||||||
regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] }
|
regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] }
|
||||||
|
tracing = "0.1.22"
|
||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
@@ -30,4 +31,5 @@ version = "0.4"
|
|||||||
|
|
||||||
[dependencies.pulldown-cmark]
|
[dependencies.pulldown-cmark]
|
||||||
default-features = false
|
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 conn = ();
|
||||||
/// #
|
/// #
|
||||||
/// let result: Result<(), ()> = Inbox::handle(&conn, activity_json)
|
/// let result: Result<(), ()> = Inbox::handle(&conn, activity_json)
|
||||||
/// .with::<User, Announce, Message>()
|
/// .with::<User, Announce, Message>(None)
|
||||||
/// .with::<User, Create, Message>()
|
/// .with::<User, Create, Message>(None)
|
||||||
/// .done();
|
/// .done();
|
||||||
/// ```
|
/// ```
|
||||||
pub enum Inbox<'a, C, E, R>
|
pub enum Inbox<'a, C, E, R>
|
||||||
@@ -144,7 +144,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Registers an handler on this Inbox.
|
/// 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
|
where
|
||||||
A: AsActor<&'a C> + FromId<C, Error = E>,
|
A: AsActor<&'a C> + FromId<C, Error = E>,
|
||||||
V: activitypub::Activity,
|
V: activitypub::Activity,
|
||||||
@@ -164,11 +164,17 @@ where
|
|||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(None)),
|
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)
|
// Transform this actor to a model (see FromId for details about the from_id function)
|
||||||
let actor = match A::from_id(
|
let actor = match A::from_id(
|
||||||
ctx,
|
ctx,
|
||||||
&actor_id,
|
&actor_id,
|
||||||
serde_json::from_value(act["actor"].clone()).ok(),
|
serde_json::from_value(act["actor"].clone()).ok(),
|
||||||
|
proxy,
|
||||||
) {
|
) {
|
||||||
Ok(a) => a,
|
Ok(a) => a,
|
||||||
// If the actor was not found, go to the next handler
|
// If the actor was not found, go to the next handler
|
||||||
@@ -189,6 +195,7 @@ where
|
|||||||
ctx,
|
ctx,
|
||||||
&obj_id,
|
&obj_id,
|
||||||
serde_json::from_value(act["object"].clone()).ok(),
|
serde_json::from_value(act["object"].clone()).ok(),
|
||||||
|
proxy,
|
||||||
) {
|
) {
|
||||||
Ok(o) => o,
|
Ok(o) => o,
|
||||||
Err((json, e)) => {
|
Err((json, e)) => {
|
||||||
@@ -222,6 +229,26 @@ where
|
|||||||
Inbox::Failed(err) => Err(err),
|
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.
|
/// Get the ActivityPub ID of a JSON value.
|
||||||
@@ -267,19 +294,28 @@ pub trait FromId<C>: Sized {
|
|||||||
ctx: &C,
|
ctx: &C,
|
||||||
id: &str,
|
id: &str,
|
||||||
object: Option<Self::Object>,
|
object: Option<Self::Object>,
|
||||||
|
proxy: Option<&reqwest::Proxy>,
|
||||||
) -> Result<Self, (Option<serde_json::Value>, Self::Error)> {
|
) -> Result<Self, (Option<serde_json::Value>, Self::Error)> {
|
||||||
match Self::from_db(ctx, id) {
|
match Self::from_db(ctx, id) {
|
||||||
Ok(x) => Ok(x),
|
Ok(x) => Ok(x),
|
||||||
_ => match object {
|
_ => match object {
|
||||||
Some(o) => Self::from_activity(ctx, o).map_err(|e| (None, e)),
|
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
|
/// Dereferences an ID
|
||||||
fn deref(id: &str) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
|
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()
|
reqwest::ClientBuilder::new()
|
||||||
|
}
|
||||||
.connect_timeout(Some(std::time::Duration::from_secs(5)))
|
.connect_timeout(Some(std::time::Duration::from_secs(5)))
|
||||||
.build()
|
.build()
|
||||||
.map_err(|_| (None, InboxError::DerefError.into()))?
|
.map_err(|_| (None, InboxError::DerefError.into()))?
|
||||||
@@ -525,7 +561,7 @@ mod tests {
|
|||||||
fn test_inbox_basic() {
|
fn test_inbox_basic() {
|
||||||
let act = serde_json::to_value(build_create()).unwrap();
|
let act = serde_json::to_value(build_create()).unwrap();
|
||||||
let res: Result<(), ()> = Inbox::handle(&(), act)
|
let res: Result<(), ()> = Inbox::handle(&(), act)
|
||||||
.with::<MyActor, Create, MyObject>()
|
.with::<MyActor, Create, MyObject>(None)
|
||||||
.done();
|
.done();
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
}
|
}
|
||||||
@@ -534,10 +570,10 @@ mod tests {
|
|||||||
fn test_inbox_multi_handlers() {
|
fn test_inbox_multi_handlers() {
|
||||||
let act = serde_json::to_value(build_create()).unwrap();
|
let act = serde_json::to_value(build_create()).unwrap();
|
||||||
let res: Result<(), ()> = Inbox::handle(&(), act)
|
let res: Result<(), ()> = Inbox::handle(&(), act)
|
||||||
.with::<MyActor, Announce, MyObject>()
|
.with::<MyActor, Announce, MyObject>(None)
|
||||||
.with::<MyActor, Delete, MyObject>()
|
.with::<MyActor, Delete, MyObject>(None)
|
||||||
.with::<MyActor, Create, MyObject>()
|
.with::<MyActor, Create, MyObject>(None)
|
||||||
.with::<MyActor, Like, MyObject>()
|
.with::<MyActor, Like, MyObject>(None)
|
||||||
.done();
|
.done();
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
}
|
}
|
||||||
@@ -547,8 +583,8 @@ mod tests {
|
|||||||
let act = serde_json::to_value(build_create()).unwrap();
|
let act = serde_json::to_value(build_create()).unwrap();
|
||||||
// Create is not handled by this inbox
|
// Create is not handled by this inbox
|
||||||
let res: Result<(), ()> = Inbox::handle(&(), act)
|
let res: Result<(), ()> = Inbox::handle(&(), act)
|
||||||
.with::<MyActor, Announce, MyObject>()
|
.with::<MyActor, Announce, MyObject>(None)
|
||||||
.with::<MyActor, Like, MyObject>()
|
.with::<MyActor, Like, MyObject>(None)
|
||||||
.done();
|
.done();
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
}
|
}
|
||||||
@@ -596,13 +632,13 @@ mod tests {
|
|||||||
let act = serde_json::to_value(build_create()).unwrap();
|
let act = serde_json::to_value(build_create()).unwrap();
|
||||||
|
|
||||||
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
|
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
|
||||||
.with::<FailingActor, Create, MyObject>()
|
.with::<FailingActor, Create, MyObject>(None)
|
||||||
.done();
|
.done();
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
|
|
||||||
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
|
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
|
||||||
.with::<FailingActor, Create, MyObject>()
|
.with::<FailingActor, Create, MyObject>(None)
|
||||||
.with::<MyActor, Create, MyObject>()
|
.with::<MyActor, Create, MyObject>(None)
|
||||||
.done();
|
.done();
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
use activitypub::{Activity, Link, Object};
|
use activitypub::{Activity, Link, Object};
|
||||||
use array_tool::vec::Uniq;
|
use array_tool::vec::Uniq;
|
||||||
use reqwest::r#async::ClientBuilder;
|
use reqwest::{header::HeaderValue, r#async::ClientBuilder, Url};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::Status,
|
http::Status,
|
||||||
request::{FromRequest, Request},
|
request::{FromRequest, Request},
|
||||||
response::{Responder, Response},
|
response::{Responder, Response},
|
||||||
Outcome,
|
Outcome,
|
||||||
};
|
};
|
||||||
use serde_json;
|
|
||||||
use tokio::prelude::*;
|
use tokio::prelude::*;
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
use self::sign::Signable;
|
use self::sign::Signable;
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApRequest {
|
|||||||
.unwrap_or(Outcome::Forward(()))
|
.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
|
where
|
||||||
S: sign::Signer,
|
S: sign::Signer,
|
||||||
A: Activity,
|
A: Activity,
|
||||||
@@ -132,31 +132,54 @@ where
|
|||||||
|
|
||||||
let mut rt = tokio::runtime::current_thread::Runtime::new()
|
let mut rt = tokio::runtime::current_thread::Runtime::new()
|
||||||
.expect("Error while initializing tokio runtime for federation");
|
.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 {
|
for inbox in boxes {
|
||||||
let body = signed.to_string();
|
let body = signed.to_string();
|
||||||
let mut headers = request::headers();
|
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));
|
headers.insert("Digest", request::Digest::digest(&body));
|
||||||
rt.spawn(
|
rt.spawn(
|
||||||
client
|
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)
|
.post(&inbox)
|
||||||
.headers(headers.clone())
|
.headers(headers.clone())
|
||||||
.header(
|
.header(
|
||||||
"Signature",
|
"Signature",
|
||||||
request::signature(sender, &headers)
|
request::signature(sender, &headers, ("post", url.path(), url.query()))
|
||||||
.expect("activity_pub::broadcast: request signature error"),
|
.expect("activity_pub::broadcast: request signature error"),
|
||||||
)
|
)
|
||||||
.body(body)
|
.body(body)
|
||||||
.send()
|
.send()
|
||||||
.and_then(|r| r.into_body().concat2())
|
.and_then(move |r| {
|
||||||
.map(move |response| {
|
if r.status().is_success() {
|
||||||
println!("Successfully sent activity to inbox ({})", inbox);
|
debug!("Successfully sent activity to inbox ({})", &inbox);
|
||||||
println!("Response: \"{:?}\"\n", response)
|
} else {
|
||||||
|
warn!("Error while sending to inbox ({:?})", &r)
|
||||||
|
}
|
||||||
|
r.into_body().concat2()
|
||||||
})
|
})
|
||||||
.map_err(|e| println!("Error while sending to inbox ({:?})", e)),
|
.map(move |response| debug!("Response: \"{:?}\"\n", response))
|
||||||
|
.map_err(|e| warn!("Error while sending to inbox ({:?})", e)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
rt.run().unwrap();
|
rt.run().unwrap();
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
use base64;
|
|
||||||
use chrono::{offset::Utc, DateTime};
|
use chrono::{offset::Utc, DateTime};
|
||||||
use openssl::hash::{Hasher, MessageDigest};
|
use openssl::hash::{Hasher, MessageDigest};
|
||||||
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, DATE, USER_AGENT};
|
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, DATE, USER_AGENT};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::activity_pub::sign::Signer;
|
use crate::activity_pub::sign::Signer;
|
||||||
use crate::activity_pub::{ap_accept_header, AP_CONTENT_TYPE};
|
use crate::activity_pub::{ap_accept_header, AP_CONTENT_TYPE};
|
||||||
|
|
||||||
const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION"));
|
const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Error();
|
||||||
|
|
||||||
pub struct Digest(String);
|
pub struct Digest(String);
|
||||||
|
|
||||||
impl Digest {
|
impl Digest {
|
||||||
@@ -62,16 +65,16 @@ impl Digest {
|
|||||||
base64::decode(&self.0[pos..]).expect("Digest::value: invalid encoding error")
|
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('=') {
|
if let Some(pos) = dig.find('=') {
|
||||||
let pos = pos + 1;
|
let pos = pos + 1;
|
||||||
if base64::decode(&dig[pos..]).is_ok() {
|
if base64::decode(&dig[pos..]).is_ok() {
|
||||||
Ok(Digest(dig.to_owned()))
|
Ok(Digest(dig.to_owned()))
|
||||||
} else {
|
} else {
|
||||||
Err(())
|
Err(Error())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(())
|
Err(Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,27 +113,47 @@ pub fn headers() -> HeaderMap {
|
|||||||
headers
|
headers
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn signature<S: Signer>(signer: &S, headers: &HeaderMap) -> Result<HeaderValue, ()> {
|
type Method<'a> = &'a str;
|
||||||
let signed_string = headers
|
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()
|
.iter()
|
||||||
.map(|(h, v)| {
|
.map(|(h, v)| format!("{}: {}", h, v))
|
||||||
format!(
|
|
||||||
"{}: {}",
|
|
||||||
h.as_str().to_lowercase(),
|
|
||||||
v.to_str()
|
|
||||||
.expect("request::signature: invalid header error")
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join("\n");
|
.join("\n");
|
||||||
let signed_headers = headers
|
let signed_headers = headers_vec
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(h, _)| h.as_str())
|
.map(|(h, _)| h.as_ref())
|
||||||
.collect::<Vec<&str>>()
|
.collect::<Vec<&str>>()
|
||||||
.join(" ")
|
.join(" ");
|
||||||
.to_lowercase();
|
|
||||||
|
|
||||||
let data = signer.sign(&signed_string).map_err(|_| ())?;
|
let data = signer.sign(&signed_string).map_err(|_| Error())?;
|
||||||
let sign = base64::encode(&data);
|
let sign = base64::encode(&data);
|
||||||
|
|
||||||
HeaderValue::from_str(&format!(
|
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(),
|
key_id = signer.get_key_id(),
|
||||||
signed_headers = signed_headers,
|
signed_headers = signed_headers,
|
||||||
signature = sign
|
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 super::request;
|
||||||
use base64;
|
|
||||||
use chrono::{naive::NaiveDateTime, DateTime, Duration, Utc};
|
use chrono::{naive::NaiveDateTime, DateTime, Duration, Utc};
|
||||||
use hex;
|
|
||||||
use openssl::{pkey::PKey, rsa::Rsa, sha::sha256};
|
use openssl::{pkey::PKey, rsa::Rsa, sha::sha256};
|
||||||
use rocket::http::HeaderMap;
|
use rocket::http::HeaderMap;
|
||||||
use serde_json;
|
|
||||||
|
|
||||||
/// Returns (public key, private key)
|
/// Returns (public key, private key)
|
||||||
pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
|
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 {
|
pub trait Signer {
|
||||||
type Error;
|
type Error;
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ pub trait Signer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait Signable {
|
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
|
where
|
||||||
T: Signer;
|
T: Signer;
|
||||||
fn verify<T>(self, creator: &T) -> bool
|
fn verify<T>(self, creator: &T) -> bool
|
||||||
@@ -46,7 +46,7 @@ pub trait Signable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Signable for serde_json::Value {
|
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 creation_date = Utc::now().to_rfc3339();
|
||||||
let mut options = json!({
|
let mut options = json!({
|
||||||
"type": "RsaSignature2017",
|
"type": "RsaSignature2017",
|
||||||
@@ -64,7 +64,7 @@ impl Signable for serde_json::Value {
|
|||||||
let document_hash = Self::hash(&self.to_string());
|
let document_hash = Self::hash(&self.to_string());
|
||||||
let to_be_signed = options_hash + &document_hash;
|
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);
|
options["signatureValue"] = serde_json::Value::String(signature);
|
||||||
self["signature"] = options;
|
self["signature"] = options;
|
||||||
|
|||||||
Regular → Executable
-3
@@ -2,9 +2,6 @@
|
|||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate activitystreams_derive;
|
extern crate activitystreams_derive;
|
||||||
use activitystreams_traits;
|
|
||||||
|
|
||||||
use serde;
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate shrinkwraprs;
|
extern crate shrinkwraprs;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
|||||||
+55
-35
@@ -1,14 +1,13 @@
|
|||||||
use heck::CamelCase;
|
use heck::CamelCase;
|
||||||
use openssl::rand::rand_bytes;
|
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 regex_syntax::is_word_character;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::uri::Uri,
|
http::uri::Uri,
|
||||||
response::{Flash, Redirect},
|
response::{Flash, Redirect},
|
||||||
};
|
};
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use syntect::html::ClassedHTMLGenerator;
|
use syntect::html::{ClassStyle, ClassedHTMLGenerator};
|
||||||
use syntect::parsing::SyntaxSet;
|
use syntect::parsing::SyntaxSet;
|
||||||
|
|
||||||
/// Generates an hexadecimal representation of 32 bytes of random data
|
/// Generates an hexadecimal representation of 32 bytes of random data
|
||||||
@@ -51,46 +50,61 @@ enum State {
|
|||||||
|
|
||||||
fn to_inline(tag: Tag<'_>) -> Tag<'_> {
|
fn to_inline(tag: Tag<'_>) -> Tag<'_> {
|
||||||
match 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::Paragraph
|
||||||
}
|
}
|
||||||
Tag::Image(url, title) => Tag::Link(url, title),
|
Tag::Image(typ, url, title) => Tag::Link(typ, url, title),
|
||||||
t => t,
|
t => t,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
struct HighlighterContext {
|
struct HighlighterContext {
|
||||||
content: Vec<String>,
|
content: Vec<String>,
|
||||||
}
|
}
|
||||||
|
#[allow(clippy::unnecessary_wraps)]
|
||||||
fn highlight_code<'a>(
|
fn highlight_code<'a>(
|
||||||
context: &mut Option<HighlighterContext>,
|
context: &mut Option<HighlighterContext>,
|
||||||
evt: Event<'a>,
|
evt: Event<'a>,
|
||||||
) -> Option<Vec<Event<'a>>> {
|
) -> Option<Vec<Event<'a>>> {
|
||||||
match evt {
|
match evt {
|
||||||
Event::Start(Tag::CodeBlock(lang)) => {
|
Event::Start(Tag::CodeBlock(kind)) => {
|
||||||
if lang.is_empty() {
|
match &kind {
|
||||||
Some(vec![Event::Start(Tag::CodeBlock(lang))])
|
CodeBlockKind::Fenced(lang) if !lang.is_empty() => {
|
||||||
} else {
|
|
||||||
*context = Some(HighlighterContext { content: vec![] });
|
*context = Some(HighlighterContext { content: vec![] });
|
||||||
Some(vec![Event::Start(Tag::CodeBlock(lang))])
|
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
Event::End(Tag::CodeBlock(x)) => {
|
Some(vec![Event::Start(Tag::CodeBlock(kind))])
|
||||||
|
}
|
||||||
|
Event::End(Tag::CodeBlock(kind)) => {
|
||||||
let mut result = vec![];
|
let mut result = vec![];
|
||||||
if let Some(ctx) = context.take() {
|
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_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
|
syntax_set
|
||||||
.find_syntax_by_name(&x)
|
.find_syntax_by_name(&lang)
|
||||||
.unwrap_or_else(|| syntax_set.find_syntax_plain_text())
|
.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 {
|
for line in ctx.content {
|
||||||
html.parse_html_for_line(&line);
|
html.parse_html_for_line_which_includes_newline(&line);
|
||||||
}
|
}
|
||||||
let q = html.finalize();
|
let q = html.finalize();
|
||||||
result.push(Event::Html(q.into()));
|
result.push(Event::Html(q.into()));
|
||||||
}
|
}
|
||||||
result.push(Event::End(Tag::CodeBlock(x)));
|
result.push(Event::End(Tag::CodeBlock(kind)));
|
||||||
*context = None;
|
*context = None;
|
||||||
Some(result)
|
Some(result)
|
||||||
}
|
}
|
||||||
@@ -106,6 +120,7 @@ fn highlight_code<'a>(
|
|||||||
_ => Some(vec![evt]),
|
_ => Some(vec![evt]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[allow(clippy::unnecessary_wraps)]
|
||||||
fn flatten_text<'a>(state: &mut Option<String>, evt: Event<'a>) -> Option<Vec<Event<'a>>> {
|
fn flatten_text<'a>(state: &mut Option<String>, evt: Event<'a>) -> Option<Vec<Event<'a>>> {
|
||||||
let (s, res) = match evt {
|
let (s, res) = match evt {
|
||||||
Event::Text(txt) => match state.take() {
|
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);
|
prev_txt.push_str(&txt);
|
||||||
(Some(prev_txt), vec![])
|
(Some(prev_txt), vec![])
|
||||||
}
|
}
|
||||||
None => (Some(txt.into_owned()), vec![]),
|
None => (Some(txt.into_string()), vec![]),
|
||||||
},
|
},
|
||||||
e => match state.take() {
|
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]),
|
None => (None, vec![e]),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -124,6 +139,7 @@ fn flatten_text<'a>(state: &mut Option<String>, evt: Event<'a>) -> Option<Vec<Ev
|
|||||||
Some(res)
|
Some(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unnecessary_wraps)]
|
||||||
fn inline_tags<'a>(
|
fn inline_tags<'a>(
|
||||||
(state, inline): &mut (Vec<Tag<'a>>, bool),
|
(state, inline): &mut (Vec<Tag<'a>>, bool),
|
||||||
evt: Event<'a>,
|
evt: Event<'a>,
|
||||||
@@ -156,11 +172,12 @@ fn process_image<'a, 'b>(
|
|||||||
) -> Event<'a> {
|
) -> Event<'a> {
|
||||||
if let Some(ref processor) = *processor {
|
if let Some(ref processor) = *processor {
|
||||||
match evt {
|
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((url, cw)) = id.parse::<i32>().ok().and_then(processor.as_ref()) {
|
||||||
if let (Some(cw), false) = (cw, inline) {
|
if let (Some(cw), false) = (cw, inline) {
|
||||||
// there is a cw, and where are not inline
|
// there is a cw, and where are not inline
|
||||||
Event::Html(Cow::Owned(format!(
|
Event::Html(CowStr::Boxed(
|
||||||
|
format!(
|
||||||
r#"<label for="postcontent-cw-{id}">
|
r#"<label for="postcontent-cw-{id}">
|
||||||
<input type="checkbox" id="postcontent-cw-{id}" checked="checked" class="cw-checkbox">
|
<input type="checkbox" id="postcontent-cw-{id}" checked="checked" class="cw-checkbox">
|
||||||
<span class="cw-container">
|
<span class="cw-container">
|
||||||
@@ -171,27 +188,29 @@ fn process_image<'a, 'b>(
|
|||||||
id = random_hex(),
|
id = random_hex(),
|
||||||
cw = cw,
|
cw = cw,
|
||||||
url = url
|
url = url
|
||||||
)))
|
)
|
||||||
|
.into(),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
Event::Start(Tag::Image(Cow::Owned(url), title))
|
Event::Start(Tag::Image(typ, CowStr::Boxed(url.into()), title))
|
||||||
}
|
}
|
||||||
} else {
|
} 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 let Some((url, cw)) = id.parse::<i32>().ok().and_then(processor.as_ref()) {
|
||||||
if inline || cw.is_none() {
|
if inline || cw.is_none() {
|
||||||
Event::End(Tag::Image(Cow::Owned(url), title))
|
Event::End(Tag::Image(typ, CowStr::Boxed(url.into()), title))
|
||||||
} else {
|
} else {
|
||||||
Event::Html(Cow::Borrowed(
|
Event::Html(CowStr::Borrowed(
|
||||||
r#""/>
|
r#""/>
|
||||||
</span>
|
</span>
|
||||||
</label>"#,
|
</label>"#,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Event::End(Tag::Image(id, title))
|
Event::End(Tag::Image(typ, id, title))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
e => e,
|
e => e,
|
||||||
@@ -231,19 +250,19 @@ pub fn md_to_html<'a>(
|
|||||||
// Ignore headings, images, and tables if inline = true
|
// Ignore headings, images, and tables if inline = true
|
||||||
.scan((vec![], inline), inline_tags)
|
.scan((vec![], inline), inline_tags)
|
||||||
.scan(&mut DocumentContext::default(), |ctx, evt| match evt {
|
.scan(&mut DocumentContext::default(), |ctx, evt| match evt {
|
||||||
Event::Start(Tag::CodeBlock(_)) | Event::Start(Tag::Code) => {
|
Event::Start(Tag::CodeBlock(_)) => {
|
||||||
ctx.in_code = true;
|
ctx.in_code = true;
|
||||||
Some((vec![evt], vec![], vec![]))
|
Some((vec![evt], vec![], vec![]))
|
||||||
}
|
}
|
||||||
Event::End(Tag::CodeBlock(_)) | Event::End(Tag::Code) => {
|
Event::End(Tag::CodeBlock(_)) => {
|
||||||
ctx.in_code = false;
|
ctx.in_code = false;
|
||||||
Some((vec![evt], vec![], vec![]))
|
Some((vec![evt], vec![], vec![]))
|
||||||
}
|
}
|
||||||
Event::Start(Tag::Link(_, _)) => {
|
Event::Start(Tag::Link(_, _, _)) => {
|
||||||
ctx.in_link = true;
|
ctx.in_link = true;
|
||||||
Some((vec![evt], vec![], vec![]))
|
Some((vec![evt], vec![], vec![]))
|
||||||
}
|
}
|
||||||
Event::End(Tag::Link(_, _)) => {
|
Event::End(Tag::Link(_, _, _)) => {
|
||||||
ctx.in_link = false;
|
ctx.in_link = false;
|
||||||
Some((vec![evt], vec![], vec![]))
|
Some((vec![evt], vec![], vec![]))
|
||||||
}
|
}
|
||||||
@@ -264,6 +283,7 @@ pub fn md_to_html<'a>(
|
|||||||
let mention = text_acc;
|
let mention = text_acc;
|
||||||
let short_mention = mention.splitn(1, '@').next().unwrap_or("");
|
let short_mention = mention.splitn(1, '@').next().unwrap_or("");
|
||||||
let link = Tag::Link(
|
let link = Tag::Link(
|
||||||
|
LinkType::Inline,
|
||||||
format!("{}@/{}/", base_url, &mention).into(),
|
format!("{}@/{}/", base_url, &mention).into(),
|
||||||
short_mention.to_owned().into(),
|
short_mention.to_owned().into(),
|
||||||
);
|
);
|
||||||
@@ -294,8 +314,8 @@ pub fn md_to_html<'a>(
|
|||||||
}
|
}
|
||||||
let hashtag = text_acc;
|
let hashtag = text_acc;
|
||||||
let link = Tag::Link(
|
let link = Tag::Link(
|
||||||
format!("{}tag/{}", base_url, &hashtag.to_camel_case())
|
LinkType::Inline,
|
||||||
.into(),
|
format!("{}tag/{}", base_url, &hashtag).into(),
|
||||||
hashtag.to_owned().into(),
|
hashtag.to_owned().into(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -460,11 +480,11 @@ mod tests {
|
|||||||
fn test_inline() {
|
fn test_inline() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
md_to_html("# Hello", None, false, None).0,
|
md_to_html("# Hello", None, false, None).0,
|
||||||
String::from("<h1>Hello</h1>\n")
|
String::from("<h1 dir=\"auto\">Hello</h1>\n")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
md_to_html("# Hello", None, true, None).0,
|
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]
|
[package]
|
||||||
name = "plume-front"
|
name = "plume-front"
|
||||||
version = "0.4.0"
|
version = "0.6.1-dev"
|
||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
stdweb = "=0.4.18"
|
|
||||||
stdweb-internal-runtime = "=0.1.4"
|
|
||||||
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
||||||
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||||
gettext-utils = { 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"
|
lazy_static = "1.3"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "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 = []
|
||||||
+400
-234
@@ -1,10 +1,12 @@
|
|||||||
use crate::CATALOG;
|
use crate::{document, CATALOG};
|
||||||
use serde::{Deserialize, Serialize};
|
use js_sys::{encode_uri_component, Date, RegExp};
|
||||||
use serde_json;
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use std::sync::Mutex;
|
use std::{convert::TryInto, sync::Mutex};
|
||||||
use stdweb::{
|
use wasm_bindgen::{prelude::*, JsCast, JsValue};
|
||||||
unstable::{TryFrom, TryInto},
|
use web_sys::{
|
||||||
web::{event::*, html_element::*, *},
|
console, window, ClipboardEvent, Element, Event, FocusEvent, HtmlAnchorElement, HtmlDocument,
|
||||||
|
HtmlElement, HtmlFormElement, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement,
|
||||||
|
KeyboardEvent, MouseEvent, Node,
|
||||||
};
|
};
|
||||||
|
|
||||||
macro_rules! mv {
|
macro_rules! mv {
|
||||||
@@ -18,30 +20,29 @@ macro_rules! mv {
|
|||||||
|
|
||||||
fn get_elt_value(id: &'static str) -> String {
|
fn get_elt_value(id: &'static str) -> String {
|
||||||
let elt = document().get_element_by_id(id).unwrap();
|
let elt = document().get_element_by_id(id).unwrap();
|
||||||
let inp: Result<InputElement, _> = elt.clone().try_into();
|
let inp: Option<&HtmlInputElement> = elt.dyn_ref();
|
||||||
let textarea: Result<TextAreaElement, _> = elt.clone().try_into();
|
let textarea: Option<&HtmlTextAreaElement> = elt.dyn_ref();
|
||||||
let select: Result<SelectElement, _> = elt.try_into();
|
let select: Option<&HtmlSelectElement> = elt.dyn_ref();
|
||||||
inp.map(|i| i.raw_value()).unwrap_or_else(|_| {
|
inp.map(|i| i.value()).unwrap_or_else(|| {
|
||||||
textarea
|
textarea
|
||||||
.map(|t| t.value())
|
.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) {
|
fn set_value<S: AsRef<str>>(id: &'static str, val: S) {
|
||||||
let elt = document().get_element_by_id(id).unwrap();
|
let elt = document().get_element_by_id(id).unwrap();
|
||||||
let inp: Result<InputElement, _> = elt.clone().try_into();
|
let inp: Option<&HtmlInputElement> = elt.dyn_ref();
|
||||||
let textarea: Result<TextAreaElement, _> = elt.clone().try_into();
|
let textarea: Option<&HtmlTextAreaElement> = elt.dyn_ref();
|
||||||
let select: Result<SelectElement, _> = elt.try_into();
|
let select: Option<&HtmlSelectElement> = elt.dyn_ref();
|
||||||
inp.map(|i| i.set_raw_value(val.as_ref()))
|
inp.map(|i| i.set_value(val.as_ref())).unwrap_or_else(|| {
|
||||||
.unwrap_or_else(|_| {
|
|
||||||
textarea
|
textarea
|
||||||
.map(|t| t.set_value(val.as_ref()))
|
.map(|t| t.set_value(val.as_ref()))
|
||||||
.unwrap_or_else(|_| select.unwrap().set_raw_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" {
|
if evt.key() == "Enter" {
|
||||||
evt.prevent_default();
|
evt.prevent_default();
|
||||||
}
|
}
|
||||||
@@ -51,7 +52,6 @@ fn no_return(evt: KeyDownEvent) {
|
|||||||
pub enum EditorError {
|
pub enum EditorError {
|
||||||
NoneError,
|
NoneError,
|
||||||
DOMError,
|
DOMError,
|
||||||
TypeError,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<std::option::NoneError> for EditorError {
|
impl From<std::option::NoneError> for EditorError {
|
||||||
@@ -59,22 +59,7 @@ impl From<std::option::NoneError> for EditorError {
|
|||||||
EditorError::NoneError
|
EditorError::NoneError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl From<stdweb::web::error::InvalidCharacterError> for EditorError {
|
const AUTOSAVE_DEBOUNCE_TIME: i32 = 5000;
|
||||||
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;
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct AutosaveInformation {
|
struct AutosaveInformation {
|
||||||
contents: String,
|
contents: String,
|
||||||
@@ -85,10 +70,16 @@ struct AutosaveInformation {
|
|||||||
tags: String,
|
tags: String,
|
||||||
title: String,
|
title: String,
|
||||||
}
|
}
|
||||||
js_serializable!(AutosaveInformation);
|
|
||||||
fn is_basic_editor() -> bool {
|
fn is_basic_editor() -> bool {
|
||||||
if let Some(basic_editor) = window().local_storage().get("basic-editor") {
|
if let Some(basic_editor) = window()
|
||||||
basic_editor == "true"
|
.unwrap()
|
||||||
|
.local_storage()
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.get("basic-editor")
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
&basic_editor == "true"
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -97,65 +88,58 @@ fn get_title() -> String {
|
|||||||
if is_basic_editor() {
|
if is_basic_editor() {
|
||||||
get_elt_value("title")
|
get_elt_value("title")
|
||||||
} else {
|
} else {
|
||||||
let title_field = HtmlElement::try_from(
|
|
||||||
document()
|
document()
|
||||||
.query_selector("#plume-editor > h1")
|
.query_selector("#plume-editor > h1")
|
||||||
.ok()
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap(),
|
.unwrap()
|
||||||
)
|
.dyn_ref::<HtmlElement>()
|
||||||
.ok()
|
.unwrap()
|
||||||
.unwrap();
|
.inner_text()
|
||||||
title_field.inner_text()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn get_autosave_id() -> String {
|
fn get_autosave_id() -> String {
|
||||||
format!(
|
format!(
|
||||||
"editor_contents={}",
|
"editor_contents={}",
|
||||||
window().location().unwrap().pathname().unwrap()
|
window().unwrap().location().pathname().unwrap()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
fn get_editor_contents() -> String {
|
fn get_editor_contents() -> String {
|
||||||
if is_basic_editor() {
|
if is_basic_editor() {
|
||||||
get_elt_value("editor-content")
|
get_elt_value("editor-content")
|
||||||
} else {
|
} else {
|
||||||
let editor =
|
let editor = document().query_selector("article").unwrap().unwrap();
|
||||||
HtmlElement::try_from(document().query_selector("article").ok().unwrap().unwrap())
|
let child_nodes = editor.child_nodes();
|
||||||
.ok()
|
let mut md = String::new();
|
||||||
.unwrap();
|
for i in 0..child_nodes.length() {
|
||||||
editor.child_nodes().iter().fold(String::new(), |md, ch| {
|
let ch = child_nodes.get(i).unwrap();
|
||||||
let to_append = match ch.node_type() {
|
let to_append = match ch.node_type() {
|
||||||
NodeType::Element => {
|
Node::ELEMENT_NODE => {
|
||||||
if js! { return @{&ch}.tagName; } == "DIV" {
|
let elt = ch.dyn_ref::<Element>().unwrap();
|
||||||
(js! { return @{&ch}.innerHTML; })
|
if elt.tag_name() == "DIV" {
|
||||||
.try_into()
|
elt.inner_html()
|
||||||
.unwrap_or_default()
|
|
||||||
} else {
|
} else {
|
||||||
(js! { return @{&ch}.outerHTML; })
|
elt.outer_html()
|
||||||
.try_into()
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NodeType::Text => ch.node_value().unwrap_or_default(),
|
Node::TEXT_NODE => ch.node_value().unwrap_or_default(),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
format!("{}\n\n{}", md, to_append)
|
md = format!("{}\n\n{}", md, to_append);
|
||||||
})
|
}
|
||||||
|
md
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn get_subtitle() -> String {
|
fn get_subtitle() -> String {
|
||||||
if is_basic_editor() {
|
if is_basic_editor() {
|
||||||
get_elt_value("subtitle")
|
get_elt_value("subtitle")
|
||||||
} else {
|
} else {
|
||||||
let subtitle_element = HtmlElement::try_from(
|
|
||||||
document()
|
document()
|
||||||
.query_selector("#plume-editor > h2")
|
.query_selector("#plume-editor > h2")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap(),
|
.unwrap()
|
||||||
)
|
.dyn_ref::<HtmlElement>()
|
||||||
.ok()
|
.unwrap()
|
||||||
.unwrap();
|
.inner_text()
|
||||||
subtitle_element.inner_text()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn autosave() {
|
fn autosave() {
|
||||||
@@ -170,27 +154,31 @@ fn autosave() {
|
|||||||
};
|
};
|
||||||
let id = get_autosave_id();
|
let id = get_autosave_id();
|
||||||
match window()
|
match window()
|
||||||
|
.unwrap()
|
||||||
.local_storage()
|
.local_storage()
|
||||||
.insert(&id, &serde_json::to_string(&info).unwrap())
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.set(&id, &serde_json::to_string(&info).unwrap())
|
||||||
{
|
{
|
||||||
Ok(_) => {}
|
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() {
|
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 autosave_info: AutosaveInformation = serde_json::from_str(&autosave_str).ok().unwrap();
|
||||||
let message = i18n!(
|
let message = i18n!(
|
||||||
CATALOG,
|
CATALOG,
|
||||||
"Do you want to load the local autosave last edited at {}?";
|
"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("editor-content", &autosave_info.contents);
|
||||||
set_value("title", &autosave_info.title);
|
set_value("title", &autosave_info.title);
|
||||||
set_value("subtitle", &autosave_info.subtitle);
|
set_value("subtitle", &autosave_info.subtitle);
|
||||||
@@ -203,18 +191,33 @@ fn load_autosave() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn clear_autosave() {
|
fn clear_autosave() {
|
||||||
window().local_storage().remove(&get_autosave_id());
|
window()
|
||||||
console!(log, &format!("Saved to {}", &get_autosave_id()));
|
.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! {
|
lazy_static! {
|
||||||
static ref AUTOSAVE_TIMEOUT: Mutex<Option<TimeoutHandle>> = Mutex::new(None);
|
static ref AUTOSAVE_TIMEOUT: Mutex<Option<TimeoutHandle>> = Mutex::new(None);
|
||||||
}
|
}
|
||||||
fn autosave_debounce() {
|
fn autosave_debounce() {
|
||||||
|
let window = window().unwrap();
|
||||||
let timeout = &mut AUTOSAVE_TIMEOUT.lock().unwrap();
|
let timeout = &mut AUTOSAVE_TIMEOUT.lock().unwrap();
|
||||||
if let Some(timeout) = timeout.take() {
|
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(
|
fn init_widget(
|
||||||
parent: &Element,
|
parent: &Element,
|
||||||
@@ -223,19 +226,33 @@ fn init_widget(
|
|||||||
content: String,
|
content: String,
|
||||||
disable_return: bool,
|
disable_return: bool,
|
||||||
) -> Result<HtmlElement, EditorError> {
|
) -> 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() {
|
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 {
|
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
|
// We need to do that to make sure the placeholder is correctly rendered
|
||||||
widget.focus();
|
widget.focus().map_err(|_| EditorError::DOMError)?;
|
||||||
widget.blur();
|
widget.blur().map_err(|_| EditorError::DOMError)?;
|
||||||
|
|
||||||
filter_paste(&widget);
|
filter_paste(&widget);
|
||||||
|
|
||||||
@@ -244,42 +261,88 @@ fn init_widget(
|
|||||||
|
|
||||||
fn filter_paste(elt: &HtmlElement) {
|
fn filter_paste(elt: &HtmlElement) {
|
||||||
// Only insert text when pasting something
|
// Only insert text when pasting something
|
||||||
js! {
|
let insert_text = Closure::wrap(Box::new(|evt: ClipboardEvent| {
|
||||||
@{&elt}.addEventListener("paste", function (evt) {
|
evt.prevent_default();
|
||||||
evt.preventDefault();
|
if let Some(data) = evt.clipboard_data() {
|
||||||
document.execCommand("insertText", false, evt.clipboardData.getData("text"));
|
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> {
|
pub fn init() -> Result<(), EditorError> {
|
||||||
if let Some(ed) = document().get_element_by_id("plume-fallback-editor") {
|
if let Some(ed) = document().get_element_by_id("plume-fallback-editor") {
|
||||||
load_autosave();
|
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
|
// Check if the user wants to use the basic editor
|
||||||
if window()
|
if window()
|
||||||
|
.unwrap()
|
||||||
.local_storage()
|
.local_storage()
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
.get("basic-editor")
|
.get("basic-editor")
|
||||||
.map(|x| x == "true")
|
.map(|x| x.is_some() && x.unwrap() == "true")
|
||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
{
|
{
|
||||||
if let Some(editor) = document().get_element_by_id("plume-fallback-editor") {
|
if let Some(editor) = document().get_element_by_id("plume-fallback-editor") {
|
||||||
if let Ok(Some(title_label)) = document().query_selector("label[for=title]") {
|
if let Ok(Some(title_label)) = document().query_selector("label[for=title]") {
|
||||||
let editor_button = document().create_element("a")?;
|
let editor_button = document()
|
||||||
js! { @{&editor_button}.href = "#"; }
|
.create_element("a")
|
||||||
editor_button.add_event_listener(|_: ClickEvent| {
|
.map_err(|_| EditorError::DOMError)?;
|
||||||
window().local_storage().remove("basic-editor");
|
editor_button
|
||||||
window().history().go(0).ok(); // refresh
|
.dyn_ref::<HtmlAnchorElement>()
|
||||||
});
|
.unwrap()
|
||||||
editor_button.append_child(
|
.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")),
|
&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()
|
document()
|
||||||
.get_element_by_id("editor-content")
|
.get_element_by_id("editor-content")
|
||||||
.unwrap()
|
.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> {
|
fn init_editor() -> Result<(), EditorError> {
|
||||||
if let Some(ed) = document().get_element_by_id("plume-editor") {
|
if let Some(ed) = document().get_element_by_id("plume-editor") {
|
||||||
// Show the 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
|
// 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")?;
|
let old_title = document().get_element_by_id("plume-editor-title")?;
|
||||||
js! {
|
old_ed
|
||||||
@{&old_ed}.style.display = "none";
|
.dyn_ref::<HtmlElement>()
|
||||||
@{&old_title}.style.display = "none";
|
.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)
|
// Get content from the old editor (when editing an article for instance)
|
||||||
let title_val = get_elt_value("title");
|
let title_val = get_elt_value("title");
|
||||||
@@ -321,24 +400,29 @@ fn init_editor() -> Result<(), EditorError> {
|
|||||||
content_val.clone(),
|
content_val.clone(),
|
||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
js! { @{&content}.innerHTML = @{content_val}; };
|
content.set_inner_html(&content_val);
|
||||||
|
|
||||||
// character counter
|
// character counter
|
||||||
content.add_event_listener(mv!(content => move |_: KeyDownEvent| {
|
let character_counter = Closure::wrap(Box::new(mv!(content => move |_| {
|
||||||
window().set_timeout(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") {
|
if let Some(e) = document().get_element_by_id("char-count") {
|
||||||
let count = chars_left("#plume-fallback-editor", &content).unwrap_or_default();
|
let count = chars_left("#plume-fallback-editor", &content).unwrap_or_default();
|
||||||
let text = i18n!(CATALOG, "Around {} characters left"; count);
|
let text = i18n!(CATALOG, "Around {} characters left"; count);
|
||||||
HtmlElement::try_from(e).map(|e| {
|
e.dyn_ref::<HtmlElement>().map(|e| {
|
||||||
js!{@{e}.innerText = @{text}};
|
e.set_inner_text(&text);
|
||||||
}).ok();
|
}).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();
|
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(
|
let show_popup = Closure::wrap(Box::new(mv!(title, subtitle, content, old_ed => move |_| {
|
||||||
mv!(title, subtitle, content, old_ed => move |_: ClickEvent| {
|
|
||||||
let popup = document().get_element_by_id("publish-popup").or_else(||
|
let popup = document().get_element_by_id("publish-popup").or_else(||
|
||||||
init_popup(&title, &subtitle, &content, &old_ed).ok()
|
init_popup(&title, &subtitle, &content, &old_ed).ok()
|
||||||
).unwrap();
|
).unwrap();
|
||||||
@@ -346,10 +430,14 @@ fn init_editor() -> Result<(), EditorError> {
|
|||||||
init_popup_bg().ok()
|
init_popup_bg().ok()
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
popup.class_list().add("show").unwrap();
|
popup.class_list().add_1("show").unwrap();
|
||||||
bg.class_list().add("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();
|
show_errors();
|
||||||
setup_close_button();
|
setup_close_button();
|
||||||
@@ -359,32 +447,47 @@ fn init_editor() -> Result<(), EditorError> {
|
|||||||
|
|
||||||
fn setup_close_button() {
|
fn setup_close_button() {
|
||||||
if let Some(button) = document().get_element_by_id("close-editor") {
|
if let Some(button) = document().get_element_by_id("close-editor") {
|
||||||
button.add_event_listener(|_: ClickEvent| {
|
let close_editor = Closure::wrap(Box::new(|_| {
|
||||||
window()
|
window()
|
||||||
|
.unwrap()
|
||||||
.local_storage()
|
.local_storage()
|
||||||
.insert("basic-editor", "true")
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.set("basic-editor", "true")
|
||||||
.unwrap();
|
.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() {
|
fn show_errors() {
|
||||||
if let Ok(Some(header)) = document().query_selector("header") {
|
let document = document();
|
||||||
let list = document().create_element("header").unwrap();
|
if let Ok(Some(header)) = document.query_selector("header") {
|
||||||
list.class_list().add("messages").unwrap();
|
let list = document.create_element("header").unwrap();
|
||||||
for error in document().query_selector_all("p.error").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
|
error
|
||||||
.parent_element()
|
.parent_element()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.remove_child(&error)
|
.remove_child(&error)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
list.append_child(&error);
|
let _ = list.append_child(&error);
|
||||||
}
|
}
|
||||||
header
|
header
|
||||||
.parent_element()
|
.parent_element()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.insert_before(&list, &header.next_sibling().unwrap())
|
.insert_before(&list, header.next_sibling().as_ref())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -395,9 +498,17 @@ fn init_popup(
|
|||||||
content: &HtmlElement,
|
content: &HtmlElement,
|
||||||
old_ed: &Element,
|
old_ed: &Element,
|
||||||
) -> Result<Element, EditorError> {
|
) -> Result<Element, EditorError> {
|
||||||
let popup = document().create_element("div")?;
|
let document = document();
|
||||||
popup.class_list().add("popup")?;
|
let popup = document
|
||||||
popup.set_attribute("id", "publish-popup")?;
|
.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")
|
let tags = get_elt_value("tags")
|
||||||
.split(',')
|
.split(',')
|
||||||
@@ -405,109 +516,154 @@ fn init_popup(
|
|||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let license = get_elt_value("license");
|
let license = get_elt_value("license");
|
||||||
make_input(&i18n!(CATALOG, "Tags"), "popup-tags", &popup).set_raw_value(&tags.join(", "));
|
make_input(&i18n!(CATALOG, "Tags"), "popup-tags", &popup).set_value(&tags.join(", "));
|
||||||
make_input(&i18n!(CATALOG, "License"), "popup-license", &popup).set_raw_value(&license);
|
make_input(&i18n!(CATALOG, "License"), "popup-license", &popup).set_value(&license);
|
||||||
|
|
||||||
let cover_label = document().create_element("label")?;
|
let cover_label = document
|
||||||
cover_label.append_child(&document().create_text_node(&i18n!(CATALOG, "Cover")));
|
.create_element("label")
|
||||||
cover_label.set_attribute("for", "cover")?;
|
.map_err(|_| EditorError::DOMError)?;
|
||||||
let cover = document().get_element_by_id("cover")?;
|
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();
|
cover.parent_element()?.remove_child(&cover).ok();
|
||||||
popup.append_child(&cover_label);
|
popup
|
||||||
popup.append_child(&cover);
|
.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") {
|
if let Some(draft_checkbox) = document.get_element_by_id("draft") {
|
||||||
let draft_label = document().create_element("label")?;
|
let draft_checkbox = draft_checkbox.dyn_ref::<HtmlInputElement>().unwrap();
|
||||||
draft_label.set_attribute("for", "popup-draft")?;
|
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();
|
let draft = document.create_element("input").unwrap();
|
||||||
js! {
|
draft.set_id("popup-draft");
|
||||||
@{&draft}.id = "popup-draft";
|
let draft = draft.dyn_ref::<HtmlInputElement>().unwrap();
|
||||||
@{&draft}.name = "popup-draft";
|
draft.set_name("popup-draft");
|
||||||
@{&draft}.type = "checkbox";
|
draft.set_type("checkbox");
|
||||||
@{&draft}.checked = @{&draft_checkbox}.checked;
|
draft.set_checked(draft_checkbox.checked());
|
||||||
};
|
|
||||||
|
|
||||||
draft_label.append_child(&draft);
|
draft_label
|
||||||
draft_label.append_child(&document().create_text_node(&i18n!(CATALOG, "This is a draft")));
|
.append_child(&draft)
|
||||||
popup.append_child(&draft_label);
|
.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")?;
|
let button = document
|
||||||
js! {
|
.create_element("input")
|
||||||
@{&button}.type = "submit";
|
.map_err(|_| EditorError::DOMError)?;
|
||||||
@{&button}.value = @{i18n!(CATALOG, "Publish")};
|
button
|
||||||
};
|
.append_child(&document.create_text_node(&i18n!(CATALOG, "Publish")))
|
||||||
button.append_child(&document().create_text_node(&i18n!(CATALOG, "Publish")));
|
.map_err(|_| EditorError::DOMError)?;
|
||||||
button.add_event_listener(
|
let button = button.dyn_ref::<HtmlInputElement>().unwrap();
|
||||||
mv!(title, subtitle, content, old_ed => move |_: ClickEvent| {
|
button.set_type("submit");
|
||||||
title.focus(); // Remove the placeholder before publishing
|
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());
|
set_value("title", title.inner_text());
|
||||||
subtitle.focus();
|
subtitle.focus().unwrap();
|
||||||
set_value("subtitle", subtitle.inner_text());
|
set_value("subtitle", subtitle.inner_text());
|
||||||
content.focus();
|
content.focus().unwrap();
|
||||||
set_value("editor-content", content.child_nodes().iter().fold(String::new(), |md, ch| {
|
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() {
|
let to_append = match ch.node_type() {
|
||||||
NodeType::Element => {
|
Node::ELEMENT_NODE => {
|
||||||
if js!{ return @{&ch}.tagName; } == "DIV" {
|
let ch = ch.dyn_ref::<Element>().unwrap();
|
||||||
(js!{ return @{&ch}.innerHTML; }).try_into().unwrap_or_default()
|
if ch.tag_name() == "DIV" {
|
||||||
|
ch.inner_html()
|
||||||
} else {
|
} else {
|
||||||
(js!{ return @{&ch}.outerHTML; }).try_into().unwrap_or_default()
|
ch.outer_html()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
NodeType::Text => ch.node_value().unwrap_or_default(),
|
Node::TEXT_NODE => ch.node_value().unwrap_or_default(),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
format!("{}\n\n{}", md, to_append)
|
md = 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();
|
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();
|
cover.parent_element().unwrap().remove_child(&cover).ok();
|
||||||
old_ed.append_child(&cover);
|
old_ed.append_child(&cover).unwrap();
|
||||||
set_value("license", get_elt_value("popup-license"));
|
set_value("license", get_elt_value("popup-license"));
|
||||||
clear_autosave();
|
clear_autosave();
|
||||||
js! {
|
let old_ed = old_ed.dyn_ref::<HtmlFormElement>().unwrap();
|
||||||
@{&old_ed}.submit();
|
old_ed.submit().unwrap();
|
||||||
};
|
})) as Box<dyn FnMut(MouseEvent)>);
|
||||||
}),
|
button
|
||||||
);
|
.add_event_listener_with_callback("click", callback.as_ref().unchecked_ref())
|
||||||
popup.append_child(&button);
|
.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)
|
Ok(popup)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_popup_bg() -> Result<Element, EditorError> {
|
fn init_popup_bg() -> Result<Element, EditorError> {
|
||||||
let bg = document().create_element("div")?;
|
let bg = document()
|
||||||
bg.class_list().add("popup-bg")?;
|
.create_element("div")
|
||||||
bg.set_attribute("id", "popup-bg")?;
|
.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);
|
document()
|
||||||
bg.add_event_listener(|_: ClickEvent| close_popup());
|
.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)
|
Ok(bg)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn chars_left(selector: &str, content: &HtmlElement) -> Option<i32> {
|
fn chars_left(selector: &str, content: &HtmlElement) -> Option<i32> {
|
||||||
match document().query_selector(selector) {
|
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
|
if let Some(len) = form
|
||||||
.get_attribute("content-size")
|
.get_attribute("content-size")
|
||||||
.and_then(|s| s.parse::<i32>().ok())
|
.and_then(|s| s.parse::<i32>().ok())
|
||||||
{
|
{
|
||||||
(js! {
|
(encode_uri_component(&content.inner_html())
|
||||||
let x = encodeURIComponent(@{content}.innerHTML)
|
.replace("%20", "+")
|
||||||
.replace(/%20/g, "+")
|
.replace("%0A", "%0D0A")
|
||||||
.replace(/%0A/g, "%0D%0A")
|
.replace_by_pattern(&RegExp::new("[!'*()]", "g"), "XXX")
|
||||||
.replace(new RegExp("[!'*()]", "g"), "XXX") // replace exceptions of encodeURIComponent with placeholder
|
.length()
|
||||||
.length + 2;
|
+ 2_u32)
|
||||||
console.log(x);
|
|
||||||
return x;
|
|
||||||
})
|
|
||||||
.try_into()
|
.try_into()
|
||||||
.map(|c: i32| len - c)
|
.map(|c: i32| len - c)
|
||||||
.ok()
|
.ok()
|
||||||
@@ -520,26 +676,26 @@ fn chars_left(selector: &str, content: &HtmlElement) -> Option<i32> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn close_popup() {
|
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("publish-popup").map(hide);
|
||||||
document().get_element_by_id("popup-bg").map(hide);
|
document().get_element_by_id("popup-bg").map(hide);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_input(label_text: &str, name: &'static str, form: &Element) -> InputElement {
|
fn make_input(label_text: &str, name: &'static str, form: &Element) -> HtmlInputElement {
|
||||||
let label = document().create_element("label").unwrap();
|
let document = document();
|
||||||
label.append_child(&document().create_text_node(label_text));
|
let label = document.create_element("label").unwrap();
|
||||||
|
label
|
||||||
|
.append_child(&document.create_text_node(label_text))
|
||||||
|
.unwrap();
|
||||||
label.set_attribute("for", name).unwrap();
|
label.set_attribute("for", name).unwrap();
|
||||||
|
|
||||||
let inp: InputElement = document()
|
let inp = document.create_element("input").unwrap();
|
||||||
.create_element("input")
|
let inp = inp.dyn_into::<HtmlInputElement>().unwrap();
|
||||||
.unwrap()
|
|
||||||
.try_into()
|
|
||||||
.unwrap();
|
|
||||||
inp.set_attribute("name", name).unwrap();
|
inp.set_attribute("name", name).unwrap();
|
||||||
inp.set_attribute("id", name).unwrap();
|
inp.set_attribute("id", name).unwrap();
|
||||||
|
|
||||||
form.append_child(&label);
|
form.append_child(&label).unwrap();
|
||||||
form.append_child(&inp);
|
form.append_child(&inp).unwrap();
|
||||||
inp
|
inp
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,36 +709,46 @@ fn make_editable(tag: &'static str) -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn placeholder(elt: HtmlElement, text: &str) -> HtmlElement {
|
fn placeholder(elt: HtmlElement, text: &str) -> HtmlElement {
|
||||||
elt.dataset().insert("placeholder", text).unwrap();
|
elt.dataset().set("placeholder", text).unwrap();
|
||||||
elt.dataset().insert("edited", "false").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" {
|
if elt.dataset().get("edited").unwrap().as_str() != "true" {
|
||||||
clear_children(&elt);
|
clear_children(&elt);
|
||||||
}
|
}
|
||||||
}));
|
})) as Box<dyn FnMut(FocusEvent)>);
|
||||||
elt.add_event_listener(mv!(elt => move |_: BlurEvent| {
|
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" {
|
if elt.dataset().get("edited").unwrap().as_str() != "true" {
|
||||||
clear_children(&elt);
|
clear_children(&elt);
|
||||||
|
|
||||||
let ph = document().create_element("span").expect("Couldn't create placeholder");
|
let ph = document().create_element("span").expect("Couldn't create placeholder");
|
||||||
ph.class_list().add("placeholder").expect("Couldn't add class");
|
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()));
|
ph.append_child(&document().create_text_node(&elt.dataset().get("placeholder").unwrap_or_default())).unwrap();
|
||||||
elt.append_child(&ph);
|
elt.append_child(&ph).unwrap();
|
||||||
}
|
}
|
||||||
}));
|
})) as Box<dyn FnMut(Event)>);
|
||||||
elt.add_event_listener(mv!(elt => move |_: KeyUpEvent| {
|
elt.add_event_listener_with_callback("blur", callback.as_ref().unchecked_ref())
|
||||||
elt.dataset().insert("edited", if elt.inner_text().trim_matches('\n').is_empty() {
|
.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"
|
"false"
|
||||||
} else {
|
} else {
|
||||||
"true"
|
"true"
|
||||||
}).expect("Couldn't update edition state");
|
}).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
|
elt
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_children(elt: &HtmlElement) {
|
fn clear_children(elt: &HtmlElement) {
|
||||||
for child in elt.child_nodes() {
|
let child_nodes = elt.child_nodes();
|
||||||
elt.remove_child(&child).unwrap();
|
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]
|
[package]
|
||||||
name = "plume-macro"
|
name = "plume-macro"
|
||||||
version = "0.4.0"
|
version = "0.6.1-dev"
|
||||||
authors = ["Trinity Pointard <trinity.pointard@insa-rennes.fr>"]
|
authors = ["Trinity Pointard <trinity.pointard@insa-rennes.fr>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "Plume procedural macros"
|
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![];
|
let mut actions = vec![];
|
||||||
for line in file.lines() {
|
for line in file.lines() {
|
||||||
if sql {
|
if sql {
|
||||||
if line.starts_with("--#!") {
|
if let Some(acc_str) = line.strip_prefix("--#!") {
|
||||||
if !acc.trim().is_empty() {
|
if !acc.trim().is_empty() {
|
||||||
actions.push(quote!(Action::Sql(#acc)));
|
actions.push(quote!(Action::Sql(#acc)));
|
||||||
}
|
}
|
||||||
sql = false;
|
sql = false;
|
||||||
acc = line[4..].to_string();
|
acc = acc_str.to_string();
|
||||||
acc.push('\n');
|
acc.push('\n');
|
||||||
} else if line.starts_with("--") {
|
} else if line.starts_with("--") {
|
||||||
continue;
|
continue;
|
||||||
@@ -102,8 +102,8 @@ fn file_to_migration(file: &str) -> TokenStream2 {
|
|||||||
acc.push_str(line);
|
acc.push_str(line);
|
||||||
acc.push('\n');
|
acc.push('\n');
|
||||||
}
|
}
|
||||||
} else if line.starts_with("--#!") {
|
} else if let Some(acc_str) = line.strip_prefix("--#!") {
|
||||||
acc.push_str(&line[4..]);
|
acc.push_str(&acc_str);
|
||||||
acc.push('\n');
|
acc.push('\n');
|
||||||
} else if line.starts_with("--") {
|
} else if line.starts_with("--") {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
+10
-6
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "plume-models"
|
name = "plume-models"
|
||||||
version = "0.4.0"
|
version = "0.6.1-dev"
|
||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
@@ -13,24 +13,28 @@ guid-create = "0.1"
|
|||||||
heck = "0.3.0"
|
heck = "0.3.0"
|
||||||
itertools = "0.8.0"
|
itertools = "0.8.0"
|
||||||
lazy_static = "1.0"
|
lazy_static = "1.0"
|
||||||
|
ldap3 = "0.7.1"
|
||||||
migrations_internals= "1.4.0"
|
migrations_internals= "1.4.0"
|
||||||
openssl = "0.10.22"
|
openssl = "0.10.22"
|
||||||
rocket = "0.4.0"
|
rocket = "0.4.6"
|
||||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||||
reqwest = "0.9"
|
reqwest = "0.9"
|
||||||
scheduled-thread-pool = "0.2.2"
|
scheduled-thread-pool = "0.2.2"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tantivy = "0.12.0"
|
tantivy = "0.13.3"
|
||||||
url = "2.1"
|
url = "2.1"
|
||||||
walkdir = "2.2"
|
walkdir = "2.2"
|
||||||
webfinger = "0.4.1"
|
webfinger = "0.4.1"
|
||||||
whatlang = "0.7.1"
|
whatlang = "0.11.1"
|
||||||
shrinkwraprs = "0.2.1"
|
shrinkwraprs = "0.2.1"
|
||||||
diesel-derive-newtype = "0.1.2"
|
diesel-derive-newtype = "0.1.2"
|
||||||
glob = "0.3.0"
|
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]
|
[dependencies.chrono]
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
@@ -38,7 +42,7 @@ version = "0.4"
|
|||||||
|
|
||||||
[dependencies.diesel]
|
[dependencies.diesel]
|
||||||
features = ["r2d2", "chrono"]
|
features = ["r2d2", "chrono"]
|
||||||
version = "1.4.2"
|
version = "1.4.5"
|
||||||
|
|
||||||
[dependencies.plume-api]
|
[dependencies.plume-api]
|
||||||
path = "../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)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use super::*;
|
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;
|
use diesel::Connection;
|
||||||
|
|
||||||
pub(crate) fn fill_database(conn: &Conn) -> Vec<BlocklistedEmail> {
|
pub(crate) fn fill_database(conn: &Conn) -> Vec<BlocklistedEmail> {
|
||||||
@@ -106,29 +106,28 @@ pub(crate) mod tests {
|
|||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn test_match() {
|
fn test_match() {
|
||||||
let r = rockets();
|
let conn = db();
|
||||||
let conn = &*r.conn;
|
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let various = fill_database(conn);
|
let various = fill_database(&conn);
|
||||||
let match1 = "user1@bad-actor.com";
|
let match1 = "user1@bad-actor.com";
|
||||||
let match2 = "spammer@lax-administration.com";
|
let match2 = "spammer@lax-administration.com";
|
||||||
let no_match = "happy-user@lax-administration.com";
|
let no_match = "happy-user@lax-administration.com";
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
BlocklistedEmail::matches_blocklist(conn, match1)
|
BlocklistedEmail::matches_blocklist(&conn, match1)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.id,
|
.id,
|
||||||
various[0].id
|
various[0].id
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
BlocklistedEmail::matches_blocklist(conn, match2)
|
BlocklistedEmail::matches_blocklist(&conn, match2)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.id,
|
.id,
|
||||||
various[1].id
|
various[1].id
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
BlocklistedEmail::matches_blocklist(conn, no_match)
|
BlocklistedEmail::matches_blocklist(&conn, no_match)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.is_none(),
|
.is_none(),
|
||||||
true
|
true
|
||||||
|
|||||||
+77
-89
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
ap_url, instance::*, medias::Media, posts::Post, safe_string::SafeString, schema::blogs,
|
ap_url, db_conn::DbConn, instance::*, medias::Media, posts::Post, safe_string::SafeString,
|
||||||
search::Searcher, users::User, Connection, Error, PlumeRocket, Result, ITEMS_PER_PAGE,
|
schema::blogs, users::User, Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE,
|
||||||
};
|
};
|
||||||
use activitypub::{
|
use activitypub::{
|
||||||
actor::Group,
|
actor::Group,
|
||||||
@@ -20,7 +20,6 @@ use plume_common::activity_pub::{
|
|||||||
inbox::{AsActor, FromId},
|
inbox::{AsActor, FromId},
|
||||||
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
|
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
|
||||||
};
|
};
|
||||||
use serde_json;
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use webfinger::*;
|
use webfinger::*;
|
||||||
|
|
||||||
@@ -132,25 +131,25 @@ impl Blog {
|
|||||||
.map_err(Error::from)
|
.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
|
let from_db = blogs::table
|
||||||
.filter(blogs::fqn.eq(fqn))
|
.filter(blogs::fqn.eq(fqn))
|
||||||
.first(&*c.conn)
|
.first(&**conn)
|
||||||
.optional()?;
|
.optional()?;
|
||||||
if let Some(from_db) = from_db {
|
if let Some(from_db) = from_db {
|
||||||
Ok(from_db)
|
Ok(from_db)
|
||||||
} else {
|
} 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)?
|
resolve_with_prefix(Prefix::Group, acct.to_owned(), true)?
|
||||||
.links
|
.links
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
|
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
|
||||||
.ok_or(Error::Webfinger)
|
.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> {
|
pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> {
|
||||||
@@ -217,16 +216,16 @@ impl Blog {
|
|||||||
|
|
||||||
pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> {
|
pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> {
|
||||||
let mut coll = OrderedCollection::default();
|
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
|
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
|
coll.collection_props
|
||||||
.set_first_link(Id::new(ap_url(&format!("{}?page=1", &self.outbox_url))))?;
|
.set_first_link(Id::new(ap_url(&format!("{}?page=1", &self.outbox_url))))?;
|
||||||
coll.collection_props
|
coll.collection_props
|
||||||
.set_last_link(Id::new(ap_url(&format!(
|
.set_last_link(Id::new(ap_url(&format!(
|
||||||
"{}?page={}",
|
"{}?page={}",
|
||||||
&self.outbox_url,
|
&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
|
/ ITEMS_PER_PAGE as u64
|
||||||
))))?;
|
))))?;
|
||||||
Ok(ActivityStream::new(coll))
|
Ok(ActivityStream::new(coll))
|
||||||
@@ -237,7 +236,7 @@ impl Blog {
|
|||||||
(min, max): (i32, i32),
|
(min, max): (i32, i32),
|
||||||
) -> Result<ActivityStream<OrderedCollectionPage>> {
|
) -> Result<ActivityStream<OrderedCollectionPage>> {
|
||||||
let mut coll = OrderedCollectionPage::default();
|
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
|
//This still doesn't do anything because the outbox
|
||||||
//doesn't do anything yet
|
//doesn't do anything yet
|
||||||
coll.collection_page_props.set_next_link(Id::new(&format!(
|
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)?;
|
coll.collection_props.items = serde_json::to_value(acts)?;
|
||||||
Ok(ActivityStream::new(coll))
|
Ok(ActivityStream::new(coll))
|
||||||
}
|
}
|
||||||
fn get_activities(&self, _conn: &Connection) -> Result<Vec<serde_json::Value>> {
|
fn get_activities(&self, _conn: &Connection) -> Vec<serde_json::Value> {
|
||||||
Ok(vec![])
|
vec![]
|
||||||
}
|
}
|
||||||
fn get_activity_page(
|
fn get_activity_page(
|
||||||
&self,
|
&self,
|
||||||
_conn: &Connection,
|
_conn: &Connection,
|
||||||
(_min, _max): (i32, i32),
|
(_min, _max): (i32, i32),
|
||||||
) -> Result<Vec<serde_json::Value>> {
|
) -> Vec<serde_json::Value> {
|
||||||
Ok(vec![])
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_keypair(&self) -> Result<PKey<Private>> {
|
pub fn get_keypair(&self) -> Result<PKey<Private>> {
|
||||||
@@ -318,9 +317,9 @@ impl Blog {
|
|||||||
.and_then(|c| c.url().ok())
|
.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)? {
|
for post in Post::get_for_blog(conn, &self)? {
|
||||||
post.delete(conn, searcher)?;
|
post.delete(conn)?;
|
||||||
}
|
}
|
||||||
diesel::delete(self)
|
diesel::delete(self)
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
@@ -335,20 +334,20 @@ impl IntoId for Blog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromId<PlumeRocket> for Blog {
|
impl FromId<DbConn> for Blog {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = CustomGroup;
|
type Object = CustomGroup;
|
||||||
|
|
||||||
fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
|
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
||||||
Self::find_by_ap_url(&c.conn, id)
|
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 url = Url::parse(&acct.object.object_props.id_string()?)?;
|
||||||
let inst = url.host_str()?;
|
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(
|
Instance::insert(
|
||||||
&c.conn,
|
conn,
|
||||||
NewInstance {
|
NewInstance {
|
||||||
public_domain: inst.to_owned(),
|
public_domain: inst.to_owned(),
|
||||||
name: inst.to_owned(),
|
name: inst.to_owned(),
|
||||||
@@ -371,9 +370,9 @@ impl FromId<PlumeRocket> for Blog {
|
|||||||
.and_then(|icon| {
|
.and_then(|icon| {
|
||||||
let owner = icon.object_props.attributed_to_link::<Id>().ok()?;
|
let owner = icon.object_props.attributed_to_link::<Id>().ok()?;
|
||||||
Media::save_remote(
|
Media::save_remote(
|
||||||
&c.conn,
|
conn,
|
||||||
icon.object_props.url_string().ok()?,
|
icon.object_props.url_string().ok()?,
|
||||||
&User::from_id(c, &owner, None).ok()?,
|
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
|
||||||
)
|
)
|
||||||
.ok()
|
.ok()
|
||||||
})
|
})
|
||||||
@@ -387,9 +386,9 @@ impl FromId<PlumeRocket> for Blog {
|
|||||||
.and_then(|banner| {
|
.and_then(|banner| {
|
||||||
let owner = banner.object_props.attributed_to_link::<Id>().ok()?;
|
let owner = banner.object_props.attributed_to_link::<Id>().ok()?;
|
||||||
Media::save_remote(
|
Media::save_remote(
|
||||||
&c.conn,
|
conn,
|
||||||
banner.object_props.url_string().ok()?,
|
banner.object_props.url_string().ok()?,
|
||||||
&User::from_id(c, &owner, None).ok()?,
|
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
|
||||||
)
|
)
|
||||||
.ok()
|
.ok()
|
||||||
})
|
})
|
||||||
@@ -401,7 +400,7 @@ impl FromId<PlumeRocket> for Blog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Blog::insert(
|
Blog::insert(
|
||||||
&c.conn,
|
conn,
|
||||||
NewBlog {
|
NewBlog {
|
||||||
actor_id: name.clone(),
|
actor_id: name.clone(),
|
||||||
title: acct.object.object_props.name_string().unwrap_or(name),
|
title: acct.object.object_props.name_string().unwrap_or(name),
|
||||||
@@ -497,14 +496,8 @@ impl NewBlog {
|
|||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
blog_authors::*,
|
blog_authors::*, instance::tests as instance_tests, medias::NewMedia, tests::db,
|
||||||
config::CONFIG,
|
users::tests as usersTests, Connection as Conn,
|
||||||
instance::tests as instance_tests,
|
|
||||||
medias::NewMedia,
|
|
||||||
search::tests::get_searcher,
|
|
||||||
tests::{db, rockets},
|
|
||||||
users::tests as usersTests,
|
|
||||||
Connection as Conn,
|
|
||||||
};
|
};
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
|
|
||||||
@@ -591,10 +584,10 @@ pub(crate) mod tests {
|
|||||||
fn get_instance() {
|
fn get_instance() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
fill_database(conn);
|
fill_database(&conn);
|
||||||
|
|
||||||
let blog = Blog::insert(
|
let blog = Blog::insert(
|
||||||
conn,
|
&conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"SomeName".to_owned(),
|
"SomeName".to_owned(),
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
@@ -606,7 +599,7 @@ pub(crate) mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
blog.get_instance(conn).unwrap().id,
|
blog.get_instance(&conn).unwrap().id,
|
||||||
Instance::get_local().unwrap().id
|
Instance::get_local().unwrap().id
|
||||||
);
|
);
|
||||||
// TODO add tests for remote instance
|
// TODO add tests for remote instance
|
||||||
@@ -618,10 +611,10 @@ pub(crate) mod tests {
|
|||||||
fn authors() {
|
fn authors() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let (user, _) = fill_database(conn);
|
let (user, _) = fill_database(&conn);
|
||||||
|
|
||||||
let b1 = Blog::insert(
|
let b1 = Blog::insert(
|
||||||
conn,
|
&conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"SomeName".to_owned(),
|
"SomeName".to_owned(),
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
@@ -632,7 +625,7 @@ pub(crate) mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let b2 = Blog::insert(
|
let b2 = Blog::insert(
|
||||||
conn,
|
&conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"Blog".to_owned(),
|
"Blog".to_owned(),
|
||||||
"Blog".to_owned(),
|
"Blog".to_owned(),
|
||||||
@@ -645,7 +638,7 @@ pub(crate) mod tests {
|
|||||||
let blog = vec![b1, b2];
|
let blog = vec![b1, b2];
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
conn,
|
&conn,
|
||||||
NewBlogAuthor {
|
NewBlogAuthor {
|
||||||
blog_id: blog[0].id,
|
blog_id: blog[0].id,
|
||||||
author_id: user[0].id,
|
author_id: user[0].id,
|
||||||
@@ -655,7 +648,7 @@ pub(crate) mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
conn,
|
&conn,
|
||||||
NewBlogAuthor {
|
NewBlogAuthor {
|
||||||
blog_id: blog[0].id,
|
blog_id: blog[0].id,
|
||||||
author_id: user[1].id,
|
author_id: user[1].id,
|
||||||
@@ -665,7 +658,7 @@ pub(crate) mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
conn,
|
&conn,
|
||||||
NewBlogAuthor {
|
NewBlogAuthor {
|
||||||
blog_id: blog[1].id,
|
blog_id: blog[1].id,
|
||||||
author_id: user[0].id,
|
author_id: user[0].id,
|
||||||
@@ -675,39 +668,39 @@ pub(crate) mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(blog[0]
|
assert!(blog[0]
|
||||||
.list_authors(conn)
|
.list_authors(&conn)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|a| a.id == user[0].id));
|
.any(|a| a.id == user[0].id));
|
||||||
assert!(blog[0]
|
assert!(blog[0]
|
||||||
.list_authors(conn)
|
.list_authors(&conn)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|a| a.id == user[1].id));
|
.any(|a| a.id == user[1].id));
|
||||||
assert!(blog[1]
|
assert!(blog[1]
|
||||||
.list_authors(conn)
|
.list_authors(&conn)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|a| a.id == user[0].id));
|
.any(|a| a.id == user[0].id));
|
||||||
assert!(!blog[1]
|
assert!(!blog[1]
|
||||||
.list_authors(conn)
|
.list_authors(&conn)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|a| a.id == user[1].id));
|
.any(|a| a.id == user[1].id));
|
||||||
|
|
||||||
assert!(Blog::find_for_author(conn, &user[0])
|
assert!(Blog::find_for_author(&conn, &user[0])
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|b| b.id == blog[0].id));
|
.any(|b| b.id == blog[0].id));
|
||||||
assert!(Blog::find_for_author(conn, &user[1])
|
assert!(Blog::find_for_author(&conn, &user[1])
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|b| b.id == blog[0].id));
|
.any(|b| b.id == blog[0].id));
|
||||||
assert!(Blog::find_for_author(conn, &user[0])
|
assert!(Blog::find_for_author(&conn, &user[0])
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|b| b.id == blog[1].id));
|
.any(|b| b.id == blog[1].id));
|
||||||
assert!(!Blog::find_for_author(conn, &user[1])
|
assert!(!Blog::find_for_author(&conn, &user[1])
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|b| b.id == blog[1].id));
|
.any(|b| b.id == blog[1].id));
|
||||||
@@ -717,13 +710,12 @@ pub(crate) mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn find_local() {
|
fn find_local() {
|
||||||
let r = rockets();
|
let conn = &db();
|
||||||
let conn = &*r.conn;
|
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
fill_database(conn);
|
fill_database(&conn);
|
||||||
|
|
||||||
let blog = Blog::insert(
|
let blog = Blog::insert(
|
||||||
conn,
|
&conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"SomeName".to_owned(),
|
"SomeName".to_owned(),
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
@@ -734,7 +726,7 @@ pub(crate) mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.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(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -743,10 +735,10 @@ pub(crate) mod tests {
|
|||||||
fn get_fqn() {
|
fn get_fqn() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
fill_database(conn);
|
fill_database(&conn);
|
||||||
|
|
||||||
let blog = Blog::insert(
|
let blog = Blog::insert(
|
||||||
conn,
|
&conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"SomeName".to_owned(),
|
"SomeName".to_owned(),
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
@@ -766,12 +758,10 @@ pub(crate) mod tests {
|
|||||||
fn delete() {
|
fn delete() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let (_, blogs) = fill_database(conn);
|
let (_, blogs) = fill_database(&conn);
|
||||||
|
|
||||||
blogs[0]
|
blogs[0].delete(&conn).unwrap();
|
||||||
.delete(conn, &get_searcher(&CONFIG.search_tokenizers))
|
assert!(Blog::get(&conn, blogs[0].id).is_err());
|
||||||
.unwrap();
|
|
||||||
assert!(Blog::get(conn, blogs[0].id).is_err());
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -780,11 +770,10 @@ pub(crate) mod tests {
|
|||||||
fn delete_via_user() {
|
fn delete_via_user() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let searcher = get_searcher(&CONFIG.search_tokenizers);
|
let (user, _) = fill_database(&conn);
|
||||||
let (user, _) = fill_database(conn);
|
|
||||||
|
|
||||||
let b1 = Blog::insert(
|
let b1 = Blog::insert(
|
||||||
conn,
|
&conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"SomeName".to_owned(),
|
"SomeName".to_owned(),
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
@@ -795,7 +784,7 @@ pub(crate) mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let b2 = Blog::insert(
|
let b2 = Blog::insert(
|
||||||
conn,
|
&conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"Blog".to_owned(),
|
"Blog".to_owned(),
|
||||||
"Blog".to_owned(),
|
"Blog".to_owned(),
|
||||||
@@ -808,7 +797,7 @@ pub(crate) mod tests {
|
|||||||
let blog = vec![b1, b2];
|
let blog = vec![b1, b2];
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
conn,
|
&conn,
|
||||||
NewBlogAuthor {
|
NewBlogAuthor {
|
||||||
blog_id: blog[0].id,
|
blog_id: blog[0].id,
|
||||||
author_id: user[0].id,
|
author_id: user[0].id,
|
||||||
@@ -818,7 +807,7 @@ pub(crate) mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
conn,
|
&conn,
|
||||||
NewBlogAuthor {
|
NewBlogAuthor {
|
||||||
blog_id: blog[0].id,
|
blog_id: blog[0].id,
|
||||||
author_id: user[1].id,
|
author_id: user[1].id,
|
||||||
@@ -828,7 +817,7 @@ pub(crate) mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
conn,
|
&conn,
|
||||||
NewBlogAuthor {
|
NewBlogAuthor {
|
||||||
blog_id: blog[1].id,
|
blog_id: blog[1].id,
|
||||||
author_id: user[0].id,
|
author_id: user[0].id,
|
||||||
@@ -837,24 +826,23 @@ pub(crate) mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
user[0].delete(conn, &searcher).unwrap();
|
user[0].delete(&conn).unwrap();
|
||||||
assert!(Blog::get(conn, blog[0].id).is_ok());
|
assert!(Blog::get(&conn, blog[0].id).is_ok());
|
||||||
assert!(Blog::get(conn, blog[1].id).is_err());
|
assert!(Blog::get(&conn, blog[1].id).is_err());
|
||||||
user[1].delete(conn, &searcher).unwrap();
|
user[1].delete(&conn).unwrap();
|
||||||
assert!(Blog::get(conn, blog[0].id).is_err());
|
assert!(Blog::get(&conn, blog[0].id).is_err());
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn self_federation() {
|
fn self_federation() {
|
||||||
let r = rockets();
|
let conn = &db();
|
||||||
let conn = &*r.conn;
|
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let (users, mut blogs) = fill_database(conn);
|
let (users, mut blogs) = fill_database(&conn);
|
||||||
blogs[0].icon_id = Some(
|
blogs[0].icon_id = Some(
|
||||||
Media::insert(
|
Media::insert(
|
||||||
conn,
|
&conn,
|
||||||
NewMedia {
|
NewMedia {
|
||||||
file_path: "aaa.png".into(),
|
file_path: "aaa.png".into(),
|
||||||
alt_text: String::new(),
|
alt_text: String::new(),
|
||||||
@@ -870,7 +858,7 @@ pub(crate) mod tests {
|
|||||||
);
|
);
|
||||||
blogs[0].banner_id = Some(
|
blogs[0].banner_id = Some(
|
||||||
Media::insert(
|
Media::insert(
|
||||||
conn,
|
&conn,
|
||||||
NewMedia {
|
NewMedia {
|
||||||
file_path: "bbb.png".into(),
|
file_path: "bbb.png".into(),
|
||||||
alt_text: String::new(),
|
alt_text: String::new(),
|
||||||
@@ -884,11 +872,11 @@ pub(crate) mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.id,
|
.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();
|
let ap_repr = blogs[0].to_activity(&conn).unwrap();
|
||||||
blogs[0].delete(conn, &*r.searcher).unwrap();
|
blogs[0].delete(&conn).unwrap();
|
||||||
let blog = Blog::from_activity(&r, ap_repr).unwrap();
|
let blog = Blog::from_activity(&conn, ap_repr).unwrap();
|
||||||
|
|
||||||
assert_eq!(blog.actor_id, blogs[0].actor_id);
|
assert_eq!(blog.actor_id, blogs[0].actor_id);
|
||||||
assert_eq!(blog.title, blogs[0].title);
|
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.public_key, blogs[0].public_key);
|
||||||
assert_eq!(blog.fqn, blogs[0].fqn);
|
assert_eq!(blog.fqn, blogs[0].fqn);
|
||||||
assert_eq!(blog.summary_html, blogs[0].summary_html);
|
assert_eq!(blog.summary_html, blogs[0].summary_html);
|
||||||
assert_eq!(blog.icon_url(conn), blogs[0].icon_url(conn));
|
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.banner_url(&conn), blogs[0].banner_url(&conn));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
comment_seers::{CommentSeers, NewCommentSeers},
|
comment_seers::{CommentSeers, NewCommentSeers},
|
||||||
|
db_conn::DbConn,
|
||||||
instance::Instance,
|
instance::Instance,
|
||||||
medias::Media,
|
medias::Media,
|
||||||
mentions::Mention,
|
mentions::Mention,
|
||||||
@@ -8,7 +9,7 @@ use crate::{
|
|||||||
safe_string::SafeString,
|
safe_string::SafeString,
|
||||||
schema::comments,
|
schema::comments,
|
||||||
users::User,
|
users::User,
|
||||||
Connection, Error, PlumeRocket, Result,
|
Connection, Error, Result, CONFIG,
|
||||||
};
|
};
|
||||||
use activitypub::{
|
use activitypub::{
|
||||||
activity::{Create, Delete},
|
activity::{Create, Delete},
|
||||||
@@ -24,7 +25,6 @@ use plume_common::{
|
|||||||
},
|
},
|
||||||
utils,
|
utils,
|
||||||
};
|
};
|
||||||
use serde_json;
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
#[derive(Queryable, Identifiable, Clone, AsChangeset)]
|
#[derive(Queryable, Identifiable, Clone, AsChangeset)]
|
||||||
@@ -105,13 +105,13 @@ impl Comment {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_activity(&self, c: &PlumeRocket) -> Result<Note> {
|
pub fn to_activity(&self, conn: &DbConn) -> Result<Note> {
|
||||||
let author = User::get(&c.conn, self.author_id)?;
|
let author = User::get(conn, self.author_id)?;
|
||||||
let (html, mentions, _hashtags) = utils::md_to_html(
|
let (html, mentions, _hashtags) = utils::md_to_html(
|
||||||
self.content.get().as_ref(),
|
self.content.get().as_ref(),
|
||||||
Some(&Instance::get_local()?.public_domain),
|
Some(&Instance::get_local()?.public_domain),
|
||||||
true,
|
true,
|
||||||
Some(Media::get_media_processor(&c.conn, vec![&author])),
|
Some(Media::get_media_processor(conn, vec![&author])),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut note = Note::default();
|
let mut note = Note::default();
|
||||||
@@ -124,8 +124,8 @@ impl Comment {
|
|||||||
note.object_props.set_content_string(html)?;
|
note.object_props.set_content_string(html)?;
|
||||||
note.object_props
|
note.object_props
|
||||||
.set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else(
|
.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),
|
|| Ok(Post::get(conn, self.post_id)?.ap_url),
|
||||||
|id| Ok(Comment::get(&c.conn, id)?.ap_url.unwrap_or_default()) as Result<String>,
|
|id| Ok(Comment::get(conn, id)?.ap_url.unwrap_or_default()) as Result<String>,
|
||||||
)?))?;
|
)?))?;
|
||||||
note.object_props
|
note.object_props
|
||||||
.set_published_string(chrono::Utc::now().to_rfc3339())?;
|
.set_published_string(chrono::Utc::now().to_rfc3339())?;
|
||||||
@@ -134,16 +134,16 @@ impl Comment {
|
|||||||
note.object_props.set_tag_link_vec(
|
note.object_props.set_tag_link_vec(
|
||||||
mentions
|
mentions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|m| Mention::build_activity(c, &m).ok())
|
.filter_map(|m| Mention::build_activity(conn, &m).ok())
|
||||||
.collect::<Vec<link::Mention>>(),
|
.collect::<Vec<link::Mention>>(),
|
||||||
)?;
|
)?;
|
||||||
Ok(note)
|
Ok(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_activity(&self, c: &PlumeRocket) -> Result<Create> {
|
pub fn create_activity(&self, conn: &DbConn) -> Result<Create> {
|
||||||
let author = User::get(&c.conn, self.author_id)?;
|
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();
|
let mut act = Create::default();
|
||||||
act.create_props.set_actor_link(author.into_id())?;
|
act.create_props.set_actor_link(author.into_id())?;
|
||||||
act.create_props.set_object_object(note.clone())?;
|
act.create_props.set_object_object(note.clone())?;
|
||||||
@@ -152,7 +152,7 @@ impl Comment {
|
|||||||
act.object_props
|
act.object_props
|
||||||
.set_to_link_vec(note.object_props.to_link_vec::<Id>()?)?;
|
.set_to_link_vec(note.object_props.to_link_vec::<Id>()?)?;
|
||||||
act.object_props
|
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)
|
Ok(act)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,16 +194,15 @@ impl Comment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromId<PlumeRocket> for Comment {
|
impl FromId<DbConn> for Comment {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = Note;
|
type Object = Note;
|
||||||
|
|
||||||
fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
|
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
||||||
Self::find_by_ap_url(&c.conn, id)
|
Self::find_by_ap_url(conn, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_activity(c: &PlumeRocket, note: Note) -> Result<Self> {
|
fn from_activity(conn: &DbConn, note: Note) -> Result<Self> {
|
||||||
let conn = &*c.conn;
|
|
||||||
let comm = {
|
let comm = {
|
||||||
let previous_url = note.object_props.in_reply_to.as_ref()?.as_str()?;
|
let previous_url = note.object_props.in_reply_to.as_ref()?.as_str()?;
|
||||||
let previous_comment = Comment::find_by_ap_url(conn, previous_url);
|
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>
|
Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result<i32>
|
||||||
})?,
|
})?,
|
||||||
author_id: User::from_id(
|
author_id: User::from_id(
|
||||||
c,
|
conn,
|
||||||
¬e.object_props.attributed_to_link::<Id>()?,
|
¬e.object_props.attributed_to_link::<Id>()?,
|
||||||
None,
|
None,
|
||||||
|
CONFIG.proxy(),
|
||||||
)
|
)
|
||||||
.map_err(|(_, e)| e)?
|
.map_err(|(_, e)| e)?
|
||||||
.id,
|
.id,
|
||||||
@@ -296,7 +296,7 @@ impl FromId<PlumeRocket> for Comment {
|
|||||||
.collect::<HashSet<_>>() // remove duplicates (don't do a query more than once)
|
.collect::<HashSet<_>>() // remove duplicates (don't do a query more than once)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|v| {
|
.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]
|
vec![user]
|
||||||
} else {
|
} else {
|
||||||
vec![] // TODO try to fetch collection
|
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 Error = Error;
|
||||||
type Output = Self;
|
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
|
// The actual creation takes place in the FromId impl
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Delete, &PlumeRocket> for Comment {
|
impl AsObject<User, Delete, &DbConn> for Comment {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = ();
|
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 {
|
if self.author_id != actor.id {
|
||||||
return Err(Error::Unauthorized);
|
return Err(Error::Unauthorized);
|
||||||
}
|
}
|
||||||
|
|
||||||
for m in Mention::list_for_comment(&c.conn, self.id)? {
|
for m in Mention::list_for_comment(conn, self.id)? {
|
||||||
for n in Notification::find_for_mention(&c.conn, &m)? {
|
for n in Notification::find_for_mention(conn, &m)? {
|
||||||
n.delete(&c.conn)?;
|
n.delete(conn)?;
|
||||||
}
|
}
|
||||||
m.delete(&c.conn)?;
|
m.delete(conn)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for n in Notification::find_for_comment(&c.conn, &self)? {
|
for n in Notification::find_for_comment(&conn, &self)? {
|
||||||
n.delete(&c.conn)?;
|
n.delete(&**conn)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::update(comments::table)
|
diesel::update(comments::table)
|
||||||
.filter(comments::in_response_to_id.eq(self.id))
|
.filter(comments::in_response_to_id.eq(self.id))
|
||||||
.set(comments::in_response_to_id.eq(self.in_response_to_id))
|
.set(comments::in_response_to_id.eq(self.in_response_to_id))
|
||||||
.execute(&*c.conn)?;
|
.execute(&**conn)?;
|
||||||
diesel::delete(&self).execute(&*c.conn)?;
|
diesel::delete(&self).execute(&**conn)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,17 +392,16 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::inbox::{inbox, tests::fill_database, InboxResult};
|
use crate::inbox::{inbox, tests::fill_database, InboxResult};
|
||||||
use crate::safe_string::SafeString;
|
use crate::safe_string::SafeString;
|
||||||
use crate::tests::rockets;
|
use crate::tests::db;
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
|
|
||||||
// creates a post, get it's Create activity, delete the post,
|
// creates a post, get it's Create activity, delete the post,
|
||||||
// "send" the Create to the inbox, and check it works
|
// "send" the Create to the inbox, and check it works
|
||||||
#[test]
|
#[test]
|
||||||
fn self_federation() {
|
fn self_federation() {
|
||||||
let r = rockets();
|
let conn = &db();
|
||||||
let conn = &*r.conn;
|
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let (posts, users, _) = fill_database(&r);
|
let (posts, users, _) = fill_database(&conn);
|
||||||
|
|
||||||
let original_comm = Comment::insert(
|
let original_comm = Comment::insert(
|
||||||
conn,
|
conn,
|
||||||
@@ -418,14 +417,14 @@ mod tests {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let act = original_comm.create_activity(&r).unwrap();
|
let act = original_comm.create_activity(&conn).unwrap();
|
||||||
inbox(
|
inbox(
|
||||||
&r,
|
&conn,
|
||||||
serde_json::to_value(original_comm.build_delete(conn).unwrap()).unwrap(),
|
serde_json::to_value(original_comm.build_delete(&conn).unwrap()).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) => {
|
InboxResult::Commented(c) => {
|
||||||
// TODO: one is HTML, the other markdown: assert_eq!(c.content, original_comm.content);
|
// 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);
|
assert_eq!(c.in_response_to_id, original_comm.in_response_to_id);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::search::TokenizerKind as SearchTokenizer;
|
use crate::search::TokenizerKind as SearchTokenizer;
|
||||||
use rocket::config::Limits;
|
use rocket::config::Limits;
|
||||||
use rocket::Config as RocketConfig;
|
use rocket::Config as RocketConfig;
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::env::{self, var};
|
use std::env::{self, var};
|
||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
@@ -20,6 +21,13 @@ pub struct Config {
|
|||||||
pub logo: LogoConfig,
|
pub logo: LogoConfig,
|
||||||
pub default_theme: String,
|
pub default_theme: String,
|
||||||
pub media_directory: 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)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -156,8 +164,8 @@ impl Default for LogoConfig {
|
|||||||
};
|
};
|
||||||
let mut custom_icons = env::vars()
|
let mut custom_icons = env::vars()
|
||||||
.filter_map(|(var, val)| {
|
.filter_map(|(var, val)| {
|
||||||
if var.starts_with("PLUME_LOGO_") {
|
if let Some(size) = var.strip_prefix("PLUME_LOGO_") {
|
||||||
Some((var[11..].to_owned(), val))
|
Some((size.to_owned(), val))
|
||||||
} else {
|
} else {
|
||||||
None
|
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! {
|
lazy_static! {
|
||||||
pub static ref CONFIG: Config = Config {
|
pub static ref CONFIG: Config = Config {
|
||||||
base_url: var("BASE_URL").unwrap_or_else(|_| format!(
|
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()),
|
default_theme: var("DEFAULT_THEME").unwrap_or_else(|_| "default-light".to_owned()),
|
||||||
media_directory: var("MEDIA_UPLOAD_DIRECTORY")
|
media_directory: var("MEDIA_UPLOAD_DIRECTORY")
|
||||||
.unwrap_or_else(|_| "static/media".to_owned()),
|
.unwrap_or_else(|_| "static/media".to_owned()),
|
||||||
|
ldap: get_ldap_config(),
|
||||||
|
proxy: get_proxy_config(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user