Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f2273b984c | |||
| 7be397b76e | |||
| d82a7674ce | |||
| 38723a4d20 | |||
| 1fc3b3dd4e | |||
| d2a684650a | |||
| 82659391f9 | |||
| bffce041d7 | |||
| 037674ae13 | |||
| 8c59c822b6 | |||
| 59023e9602 | |||
| ad3a8b92d1 | |||
| c67f65e684 | |||
| d78b3a05ec | |||
| 5579f64dd0 | |||
| 773fbfe7c8 | |||
| 5b50f90d2b | |||
| 90f6ef0380 | |||
| 33619abdfb | |||
| c52aac012c | |||
| c9070930d2 | |||
| 918bda14ec | |||
| 33a0c7dcd3 | |||
| 8f1ab3485e | |||
| 18ae6e26b3 | |||
| 49bb8cb0bc | |||
| ec57f1e687 | |||
| 787eb7f399 | |||
| 85aa0883c8 | |||
| ff92ac0763 | |||
| a20d2df50a | |||
| 29e11f552f |
+227
-284
@@ -1,313 +1,256 @@
|
||||
version: 2.1
|
||||
|
||||
aliases:
|
||||
- &plume-docker
|
||||
image: plumeorg/plume-buildenv:v0.0.5
|
||||
- &defaults
|
||||
executors:
|
||||
default:
|
||||
parameters:
|
||||
postgres:
|
||||
type: boolean
|
||||
default: false
|
||||
selenium:
|
||||
type: boolean
|
||||
default: false
|
||||
docker:
|
||||
- *plume-docker
|
||||
- image: plumeorg/plume-buildenv:v0.0.7
|
||||
- image: <<#parameters.postgres>>circleci/postgres:9.6-alpine<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: plume
|
||||
- image: <<#parameters.selenium>>elgalu/selenium:latest<</parameters.selenium>><<^parameters.selenium>>alpine:latest<</parameters.selenium>>
|
||||
working_directory: ~/projects/Plume
|
||||
- &postgresql
|
||||
docker:
|
||||
- *plume-docker
|
||||
- image: circleci/postgres:9.6-alpine
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: plume
|
||||
working_directory: ~/projects/Plume
|
||||
- &selenium
|
||||
docker:
|
||||
- *plume-docker
|
||||
- image: elgalu/selenium:latest
|
||||
working_directory: ~/projects/Plume
|
||||
- &postgresql_selenium
|
||||
docker:
|
||||
- *plume-docker
|
||||
- image: circleci/postgres:9.6-alpine
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: plume
|
||||
- image: elgalu/selenium:latest
|
||||
working_directory: ~/projects/Plume
|
||||
|
||||
- &attach_workspace
|
||||
attach_workspace:
|
||||
at: ~/projects/Plume/
|
||||
- &persist_to_workspace
|
||||
persist_to_workspace:
|
||||
root: ~/projects/Plume/
|
||||
paths:
|
||||
- ./
|
||||
|
||||
- &env_postgresql
|
||||
environment:
|
||||
MIGRATION_DIRECTORY: migrations/postgres
|
||||
FEATURES: postgres
|
||||
DATABASE_URL: postgres://postgres@localhost/plume
|
||||
RUST_TEST_THREADS: 1
|
||||
- &env_sqlite
|
||||
environment:
|
||||
MIGRATION_DIRECTORY: migrations/sqlite
|
||||
FEATURES: sqlite
|
||||
DATABASE_URL: plume.sqlite3
|
||||
RUST_TEST_THREADS: 1
|
||||
FEATURES: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
|
||||
DATABASE_URL: <<#parameters.postgres>>postgres://postgres@localhost/plume<</parameters.postgres>><<^parameters.postgres>>plume.sqlite<</parameters.postgres>>
|
||||
|
||||
- &restore_cache
|
||||
restore_cache:
|
||||
keys:
|
||||
- v2-plume-notest-{{ checksum "Cargo.lock" }}
|
||||
- v2-plume-notest
|
||||
- &save_cache
|
||||
save_cache:
|
||||
key: v2-plume-notest-{{ checksum "Cargo.lock" }}
|
||||
paths:
|
||||
|
||||
commands:
|
||||
restore_env:
|
||||
description: checkout and pull cache
|
||||
parameters:
|
||||
cache:
|
||||
type: enum
|
||||
default: none
|
||||
enum: ["none", "clippy", "postgres", "sqlite", "release-postgres", "release-sqlite"]
|
||||
steps:
|
||||
- checkout
|
||||
- run: git config --global --remove-section url."ssh://git@github.com"
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v0-<< parameters.cache >>-{{ checksum "Cargo.lock" }}-{{ .Branch }}
|
||||
- v0-<< parameters.cache >>-{{ checksum "Cargo.lock" }}-master
|
||||
|
||||
cache:
|
||||
description: push cache
|
||||
parameters:
|
||||
cache:
|
||||
type: enum
|
||||
enum: ["clippy", "postgres", "sqlite", "release-postgres", "release-sqlite"]
|
||||
steps:
|
||||
- save_cache:
|
||||
key: v0-<< parameters.cache >>-{{ checksum "Cargo.lock" }}-{{ .Branch }}
|
||||
paths:
|
||||
- ~/.cargo/
|
||||
- ./target
|
||||
|
||||
- &restore_cache_web
|
||||
restore_cache:
|
||||
keys:
|
||||
- v2-plume-web-{{ checksum "Cargo.lock" }}
|
||||
- v2-plume-web
|
||||
- &save_cache_web
|
||||
save_cache:
|
||||
key: v2-plume-web-{{ checksum "Cargo.lock" }}
|
||||
paths:
|
||||
- ~/.cargo/
|
||||
- ./target
|
||||
|
||||
- &restore_cache_cli
|
||||
restore_cache:
|
||||
keys:
|
||||
- v2-plume-notest-{{ checksum "Cargo.lock" }}-{{ checksum "/FEATURES" }}-cli
|
||||
- v2-plume-notest-{{ checksum "Cargo.lock" }}-{{ checksum "/FEATURES" }}
|
||||
- v2-plume-notest-{{ checksum "Cargo.lock" }}
|
||||
- v2-plume-notest
|
||||
- &save_cache_cli
|
||||
save_cache:
|
||||
key: v2-plume-notest-{{ checksum "Cargo.lock" }}-{{ checksum "/FEATURES" }}-cli
|
||||
paths:
|
||||
- ~/.cargo/
|
||||
- target
|
||||
|
||||
- &restore_cache_release
|
||||
restore_cache:
|
||||
keys:
|
||||
- v2-plume-release-{{ checksum "Cargo.lock" }}
|
||||
- v2-plume-release
|
||||
- v2-plume-notest-{{ checksum "Cargo.lock" }}
|
||||
- v2-plume-notest
|
||||
- &save_cache_release
|
||||
save_cache:
|
||||
key: v2-plume-release-{{ checksum "Cargo.lock" }}
|
||||
paths:
|
||||
- ~/.cargo
|
||||
- target/release
|
||||
- target/wasm32-unknown-unknown/release
|
||||
|
||||
- &restore_cache_plume_dead_code
|
||||
restore_cache:
|
||||
keys:
|
||||
- v2-plume-test-{{ checksum "/FEATURES" }}-{{ checksum "Cargo.lock" }}-plume
|
||||
- v2-plume-test-{{ checksum "/FEATURES" }}-{{ checksum "Cargo.lock" }}
|
||||
- v2-plume-test-{{ checksum "/FEATURES" }}
|
||||
- v2-plume-notest-{{ checksum "Cargo.lock" }}-{{ checksum "/FEATURES" }}
|
||||
- v2-plume-notest-{{ checksum "Cargo.lock" }}
|
||||
- v2-plume-notest
|
||||
- &save_cache_plume_dead_code
|
||||
save_cache:
|
||||
key: v2-plume-test-{{ checksum "/FEATURES" }}-{{ checksum "Cargo.lock" }}-plume
|
||||
paths:
|
||||
- ~/.cargo/
|
||||
- target
|
||||
|
||||
- &test_cli
|
||||
clippy:
|
||||
description: run cargo clippy
|
||||
parameters:
|
||||
package:
|
||||
type: string
|
||||
default: plume
|
||||
no_feature:
|
||||
type: boolean
|
||||
default: false
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- run:
|
||||
name: Set cache key
|
||||
command: echo "$FEATURES" > /FEATURES
|
||||
- *restore_cache_cli
|
||||
- run:
|
||||
name: clippy
|
||||
command: cargo clippy --no-default-features --features="${FEATURES}" --release -p plume-cli -- -D warnings
|
||||
- *save_cache_cli
|
||||
- &test_unit
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- run:
|
||||
name: Set cache key
|
||||
command: echo "$FEATURES" > /FEATURES
|
||||
- *restore_cache_plume_dead_code
|
||||
- run:
|
||||
name: clippy
|
||||
command: cargo clippy --no-default-features --features="${FEATURES}" --release -- -D warnings
|
||||
- run:
|
||||
name: compile test
|
||||
command: cargo test --no-default-features --features="${FEATURES}" --all --exclude plume-front --no-run || cargo test --no-default-features --features="${FEATURES}" --all --exclude plume-front --no-run
|
||||
- run:
|
||||
name: run test
|
||||
command: ./script/run_unit_test.sh
|
||||
- run:
|
||||
name: upload coverage
|
||||
command: ./script/upload_coverage.sh unit
|
||||
- *save_cache_plume_dead_code
|
||||
- run: cargo clippy <<^parameters.no_feature>>--no-default-features --features="${FEATURES}"<</parameters.no_feature>> --release -p <<parameters.package>> -- -D warnings
|
||||
|
||||
- &test_browser
|
||||
run_with_coverage:
|
||||
description: run command with environment for coverage
|
||||
parameters:
|
||||
cmd:
|
||||
type: string
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- run:
|
||||
name: Set cache key
|
||||
command: echo "$FEATURES" > /FEATURES
|
||||
- *restore_cache_plume_dead_code
|
||||
- run:
|
||||
name: install server
|
||||
command: cargo install --debug --no-default-features --features="${FEATURES}",test --path . --force || cargo install --debug --no-default-features --features="${FEATURES}",test --path . --force
|
||||
- run:
|
||||
name: install plm
|
||||
command: cargo install --debug --no-default-features --features="${FEATURES}" --path plume-cli --force || cargo install --debug --no-default-features --features="${FEATURES}" --path plume-cli --force
|
||||
- run:
|
||||
name: run test
|
||||
command: ./script/run_browser_test.sh
|
||||
environment:
|
||||
BROWSER: firefox
|
||||
- run:
|
||||
name: upload coverage
|
||||
command: ./script/upload_coverage.sh integration
|
||||
- *save_cache_plume_dead_code
|
||||
- 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 CARGO_INCREMENTAL=0
|
||||
<< parameters.cmd >>
|
||||
|
||||
- &release
|
||||
upload_coverage:
|
||||
description: merge coverage files and upload to codecov.io
|
||||
parameters:
|
||||
type:
|
||||
type: string
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- *restore_cache_release
|
||||
- run:
|
||||
name: build frontend
|
||||
command: cargo web deploy -p plume-front --release
|
||||
- run:
|
||||
name: build server
|
||||
command: cargo build --release --no-default-features --features="${FEATURES}" || cargo build --release --no-default-features --features="${FEATURES}"
|
||||
- run:
|
||||
name: build plm
|
||||
command: cargo build --release --no-default-features --features="${FEATURES}" -p plume-cli || cargo build --release --no-default-features --features="${FEATURES}" -p plume-cli
|
||||
- *save_cache_release
|
||||
- run: script/generate_artifact.sh
|
||||
- store_artifacts:
|
||||
path: plume.tar.gz
|
||||
destination: plume.tar.gz
|
||||
- run: zip -0 ccov.zip `find . -name 'plume*.gc*' -o -name 'plm*.gc*'`
|
||||
- run: grcov ccov.zip -s . -t lcov --llvm --branch --ignore-not-existing --ignore-dir '/*' -o lcov.info
|
||||
- run: bash <(curl -s https://codecov.io/bash) -f lcov.info -F <<parameters.type>>
|
||||
- run: find . -name 'plume*.gc*' -delete -o -name 'plm*.gc*' -delete
|
||||
- run: rm ccov.zip lcov.info
|
||||
|
||||
build:
|
||||
description: build a package
|
||||
parameters:
|
||||
package:
|
||||
type: string
|
||||
default: plume
|
||||
release:
|
||||
type: boolean
|
||||
default: false
|
||||
steps:
|
||||
- run: |
|
||||
cmd="cargo build <<#parameters.release>>--release<</parameters.release>> --no-default-features --features="${FEATURES}" -p <<parameters.package>> -j"
|
||||
for i in 36 4 2 1 1; do
|
||||
$cmd $i && exit 0
|
||||
done
|
||||
exit 1
|
||||
|
||||
jobs:
|
||||
download_deps:
|
||||
<<: *defaults
|
||||
cargo fmt:
|
||||
executor:
|
||||
name: default
|
||||
steps:
|
||||
- checkout
|
||||
- *attach_workspace
|
||||
- *restore_cache
|
||||
- run: git config --global --remove-section url."ssh://git@github.com"
|
||||
- run: cargo fetch
|
||||
- *save_cache
|
||||
- *persist_to_workspace
|
||||
- restore_env
|
||||
- run: cargo fmt --all -- --check
|
||||
|
||||
cargo_fmt:
|
||||
<<: *defaults
|
||||
clippy:
|
||||
parameters:
|
||||
postgres:
|
||||
type: boolean
|
||||
executor:
|
||||
name: default
|
||||
postgres: << parameters.postgres >>
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- run: cargo fmt --all -- --check
|
||||
- restore_env:
|
||||
cache: clippy
|
||||
- clippy
|
||||
- clippy:
|
||||
package: plume-cli
|
||||
- clippy:
|
||||
package: plume-front
|
||||
no_feature: true
|
||||
- cache:
|
||||
cache: clippy
|
||||
|
||||
build_web:
|
||||
<<: *defaults
|
||||
unit:
|
||||
parameters:
|
||||
postgres:
|
||||
type: boolean
|
||||
executor:
|
||||
name: default
|
||||
postgres: << parameters.postgres >>
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- *restore_cache_web
|
||||
- run: cargo clippy -p plume-front -- -D warnings
|
||||
- run: cargo web deploy -p plume-front
|
||||
- *save_cache_web
|
||||
- *persist_to_workspace
|
||||
- restore_env:
|
||||
cache: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
|
||||
- run_with_coverage:
|
||||
cmd: |
|
||||
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
|
||||
$cmd $i && break
|
||||
done
|
||||
cargo test --all --exclude plume-front --exclude plume-macro --no-default-features --features="${FEATURES}" -j1 -- --test-threads=1
|
||||
- upload_coverage:
|
||||
type: unit
|
||||
- cache:
|
||||
cache: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
|
||||
|
||||
test_cli_postgresql:
|
||||
<<: *postgresql
|
||||
<<: *env_postgresql
|
||||
<<: *test_cli
|
||||
|
||||
test_cli_sqlite:
|
||||
<<: *defaults
|
||||
<<: *env_sqlite
|
||||
<<: *test_cli
|
||||
|
||||
test_unit_postgresql:
|
||||
<<: *postgresql
|
||||
<<: *env_postgresql
|
||||
<<: *test_unit
|
||||
|
||||
test_unit_sqlite:
|
||||
<<: *defaults
|
||||
<<: *env_sqlite
|
||||
<<: *test_unit
|
||||
|
||||
test_browser_postgresql:
|
||||
<<: *postgresql_selenium
|
||||
<<: *env_postgresql
|
||||
<<: *test_browser
|
||||
|
||||
test_browser_sqlite:
|
||||
<<: *selenium
|
||||
<<: *env_sqlite
|
||||
<<: *test_browser
|
||||
all_ok:
|
||||
docker:
|
||||
- image: alpine:3.7
|
||||
integration:
|
||||
parameters:
|
||||
postgres:
|
||||
type: boolean
|
||||
executor:
|
||||
name: default
|
||||
postgres: << parameters.postgres >>
|
||||
selenium: true
|
||||
steps:
|
||||
- run: /bin/true
|
||||
release_postgresql:
|
||||
<<: *defaults
|
||||
<<: *env_postgresql
|
||||
<<: *release
|
||||
release_sqlite:
|
||||
<<: *defaults
|
||||
<<: *env_sqlite
|
||||
<<: *release
|
||||
- restore_env:
|
||||
cache: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
|
||||
- run: cargo web deploy -p plume-front
|
||||
- run_with_coverage:
|
||||
cmd: |
|
||||
cmd="cargo install --debug --no-default-features --features="${FEATURES}",test --force --path . -j"
|
||||
for i in 36 4 2 1 1; do
|
||||
$cmd $i && exit 0
|
||||
done
|
||||
exit 1
|
||||
- run_with_coverage:
|
||||
cmd: |
|
||||
cmd="cargo install --debug --no-default-features --features="${FEATURES}" --force --path plume-cli -j"
|
||||
for i in 36 4 2 1 1; do
|
||||
$cmd $i && exit 0
|
||||
done
|
||||
exit 1
|
||||
- run:
|
||||
name: run test
|
||||
command: ./script/run_browser_test.sh
|
||||
environment:
|
||||
BROWSER: firefox
|
||||
- upload_coverage:
|
||||
type: integration
|
||||
- cache:
|
||||
cache: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
|
||||
|
||||
release:
|
||||
parameters:
|
||||
postgres:
|
||||
type: boolean
|
||||
executor:
|
||||
name: default
|
||||
postgres: << parameters.postgres >>
|
||||
steps:
|
||||
- restore_env:
|
||||
cache: release-<<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
|
||||
- run: cargo web deploy -p plume-front --release
|
||||
- build:
|
||||
package: plume
|
||||
release: true
|
||||
- build:
|
||||
package: plume-cli
|
||||
release: true
|
||||
- cache:
|
||||
cache: release-<<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
|
||||
- run: ./script/generate_artifact.sh
|
||||
- unless:
|
||||
condition: << parameters.postgres >>
|
||||
steps:
|
||||
- run: ./script/upload_test_environment.sh
|
||||
- store_artifacts:
|
||||
path: plume.tar.gz
|
||||
destination: plume.tar.gz
|
||||
- store_artifacts:
|
||||
path: wasm.tar.gz
|
||||
destination: wasm.tar.gz
|
||||
|
||||
push translations:
|
||||
executor:
|
||||
name: default
|
||||
steps:
|
||||
- restore_env:
|
||||
cache: none
|
||||
- run: crowdin upload -b master
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build_and_test:
|
||||
build and test:
|
||||
jobs:
|
||||
- download_deps
|
||||
- cargo_fmt:
|
||||
requires:
|
||||
- download_deps
|
||||
- build_web:
|
||||
requires:
|
||||
- download_deps
|
||||
- test_cli_postgresql:
|
||||
requires:
|
||||
- download_deps
|
||||
- test_cli_sqlite:
|
||||
requires:
|
||||
- download_deps
|
||||
- test_unit_postgresql:
|
||||
requires:
|
||||
- download_deps
|
||||
- test_unit_sqlite:
|
||||
requires:
|
||||
- download_deps
|
||||
- test_browser_postgresql:
|
||||
requires:
|
||||
- build_web
|
||||
- test_browser_sqlite:
|
||||
requires:
|
||||
- build_web
|
||||
- all_ok:
|
||||
requires:
|
||||
- cargo_fmt
|
||||
- test_cli_postgresql
|
||||
- test_cli_sqlite
|
||||
- test_unit_postgresql
|
||||
- test_unit_sqlite
|
||||
- test_browser_postgresql
|
||||
- test_browser_sqlite
|
||||
- release_postgresql:
|
||||
requires:
|
||||
- all_ok
|
||||
- release_sqlite:
|
||||
requires:
|
||||
- all_ok
|
||||
- cargo fmt
|
||||
- clippy:
|
||||
postgres: false
|
||||
- clippy:
|
||||
postgres: true
|
||||
- unit:
|
||||
postgres: false
|
||||
- unit:
|
||||
postgres: true
|
||||
- integration:
|
||||
postgres: false
|
||||
- integration:
|
||||
postgres: true
|
||||
- release:
|
||||
postgres: false
|
||||
- release:
|
||||
postgres: true
|
||||
- push translations:
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^master/
|
||||
|
||||
@@ -3,8 +3,9 @@ ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
#install native/circleci/build dependancies
|
||||
RUN apt update &&\
|
||||
apt install -y git ssh tar gzip ca-certificates &&\
|
||||
apt install -y 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
|
||||
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 &&\
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
#install and configure rust
|
||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2019-03-23 -y &&\
|
||||
@@ -13,14 +14,10 @@ RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2019-0
|
||||
|
||||
#compile some deps
|
||||
RUN cargo install cargo-web &&\
|
||||
cargo install diesel_cli --no-default-features --features postgres,sqlite --version '=1.3.0' &&\
|
||||
cargo install grcov &&\
|
||||
strip /root/.cargo/bin/* &&\
|
||||
rm -fr ~/.cargo/registry
|
||||
|
||||
#install coverage tools
|
||||
RUN curl -L https://github.com/SimonKagstrom/kcov/archive/master.tar.gz | tar xz &&\
|
||||
mkdir -p kcov-master/build && cd kcov-master/build && cmake .. && make &&\
|
||||
make install && cd ../.. && rm -rf kcov-master
|
||||
|
||||
#set some compilation parametters
|
||||
COPY cargo_config /root/.cargo/config
|
||||
|
||||
@@ -30,3 +27,11 @@ RUN pip3 install selenium
|
||||
#install and configure caddy
|
||||
RUN curl https://getcaddy.com | bash -s personal
|
||||
COPY Caddyfile /Caddyfile
|
||||
|
||||
#install crowdin
|
||||
RUN mkdir /crowdin && cd /crowdin &&\
|
||||
curl -O https://downloads.crowdin.com/cli/v2/crowdin-cli.zip &&\
|
||||
unzip crowdin-cli.zip && rm crowdin-cli.zip &&\
|
||||
cd * && mv crowdin-cli.jar /usr/local/bin && cd && rm -rf /crowdin &&\
|
||||
/bin/echo -e '#!/bin/sh\njava -jar /usr/local/bin/crowdin-cli.jar $@' > /usr/local/bin/crowdin &&\
|
||||
chmod +x /usr/local/bin/crowdin
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
# link dead code for coverage, attempt to reduce linking memory usage to not get killed
|
||||
rustflags = ["-Clink-dead-code", "-Clink-args=-Xlinker --no-keep-memory -Xlinker --reduce-memory-overheads"]
|
||||
rustflags = ["-Clink-args=-Xlinker --no-keep-memory -Xlinker --reduce-memory-overheads"]
|
||||
|
||||
Generated
+752
-540
File diff suppressed because it is too large
Load Diff
+7
-7
@@ -8,9 +8,8 @@ repository = "https://github.com/Plume-org/Plume"
|
||||
activitypub = "0.1.3"
|
||||
askama_escape = "0.1"
|
||||
atom_syndication = "0.6"
|
||||
canapi = "0.2"
|
||||
colored = "1.7"
|
||||
dotenv = "0.13"
|
||||
colored = "1.8"
|
||||
dotenv = "0.14"
|
||||
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
||||
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||
@@ -22,12 +21,13 @@ num_cpus = "1.0"
|
||||
rocket = "0.4.0"
|
||||
rocket_contrib = { version = "0.4.0", features = ["json"] }
|
||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||
rpassword = "2.0"
|
||||
rpassword = "3.0"
|
||||
runtime-fmt = "0.3.0"
|
||||
scheduled-thread-pool = "0.2.0"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_qs = "0.4"
|
||||
shrinkwraprs = "0.2.1"
|
||||
validator = "0.8"
|
||||
validator_derive = "0.8"
|
||||
webfinger = "0.3.1"
|
||||
@@ -42,7 +42,7 @@ version = "0.4"
|
||||
|
||||
[dependencies.ctrlc]
|
||||
features = ["termination"]
|
||||
version = "3.1.1"
|
||||
version = "3.1.2"
|
||||
|
||||
[dependencies.diesel]
|
||||
features = ["r2d2", "chrono"]
|
||||
@@ -67,7 +67,7 @@ git = "https://github.com/fdb-hiroshima/rocket_csrf"
|
||||
rev = "4a72ea2ec716cb0b26188fb00bccf2ef7d1e031c"
|
||||
|
||||
[build-dependencies]
|
||||
ructe = "0.5.6"
|
||||
ructe = "0.6.2"
|
||||
rsass = "0.9"
|
||||
|
||||
[features]
|
||||
@@ -78,4 +78,4 @@ debug-mailer = []
|
||||
test = []
|
||||
|
||||
[workspace]
|
||||
members = ["plume-api", "plume-cli", "plume-models", "plume-common", "plume-front"]
|
||||
members = ["plume-api", "plume-cli", "plume-models", "plume-common", "plume-front", "plume-macro"]
|
||||
|
||||
@@ -18,7 +18,6 @@ RUN chmod a+x ./wasm-deps.sh && sleep 1 && ./wasm-deps.sh
|
||||
|
||||
WORKDIR /app
|
||||
COPY Cargo.toml Cargo.lock rust-toolchain ./
|
||||
RUN cargo install diesel_cli --no-default-features --features postgres --version '=1.3.0'
|
||||
RUN cargo install cargo-web
|
||||
|
||||
COPY . .
|
||||
@@ -40,7 +39,6 @@ WORKDIR /app
|
||||
COPY --from=builder /app /app
|
||||
COPY --from=builder /usr/local/cargo/bin/plm /bin/
|
||||
COPY --from=builder /usr/local/cargo/bin/plume /bin/
|
||||
COPY --from=builder /usr/local/cargo/bin/diesel /bin/
|
||||
|
||||
CMD ["plume"]
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::process::{Command, Stdio};
|
||||
use std::{env, fs::*, io::Write, path::PathBuf};
|
||||
|
||||
fn compute_static_hash() -> String {
|
||||
//"find static/ -type f ! -path 'static/media/*' | sort | xargs stat --printf='%n %Y\n' | sha256sum"
|
||||
//"find static/ -type f ! -path 'static/media/*' | sort | xargs stat -c'%n %Y' | openssl dgst -r"
|
||||
|
||||
let find = Command::new("find")
|
||||
.args(&["static/", "-type", "f", "!", "-path", "static/media/*"])
|
||||
@@ -20,17 +20,19 @@ fn compute_static_hash() -> String {
|
||||
.expect("failed sort command");
|
||||
|
||||
let xargs = Command::new("xargs")
|
||||
.args(&["stat", "--printf='%n %Y\n'"])
|
||||
.args(&["stat", "-c'%n %Y'"])
|
||||
.stdin(sort.stdout.unwrap())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("failed xargs command");
|
||||
|
||||
let sha = Command::new("sha256sum")
|
||||
let mut sha = Command::new("openssl")
|
||||
.args(&["dgst", "-r"])
|
||||
.stdin(xargs.stdout.unwrap())
|
||||
.output()
|
||||
.expect("failed sha256sum command");
|
||||
.expect("failed openssl command");
|
||||
|
||||
sha.stdout.resize(64, 0);
|
||||
String::from_utf8(sha.stdout).unwrap()
|
||||
}
|
||||
|
||||
@@ -39,7 +41,12 @@ fn main() {
|
||||
let in_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("templates");
|
||||
compile_templates(&in_dir, &out_dir).expect("compile templates");
|
||||
|
||||
println!("cargo:rerun-if-changed=static/css");
|
||||
println!("cargo:rerun-if-changed=static/css/_article.scss");
|
||||
println!("cargo:rerun-if-changed=static/css/_forms.scss");
|
||||
println!("cargo:rerun-if-changed=static/css/_global.scss");
|
||||
println!("cargo:rerun-if-changed=static/css/_header.scss");
|
||||
println!("cargo:rerun-if-changed=static/css/_variables.scss");
|
||||
println!("cargo:rerun-if-changed=static/css/main.scss");
|
||||
let mut out = File::create("static/css/main.css").expect("Couldn't create main.css");
|
||||
out.write_all(
|
||||
&rsass::compile_scss_file(
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
"project_identifier": "plume"
|
||||
"api_key_env": CROWDIN_API_KEY
|
||||
preserve_hierarchy: true
|
||||
files:
|
||||
- source: /po/plume/plume.pot
|
||||
translation: /po/plume/%two_letters_code%.po
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
--#!|_conn, path: &Path| {
|
||||
--#! let mut pb = path.to_path_buf();
|
||||
--#! pb.push("search_index");
|
||||
--#! std::fs::remove_dir_all(pb).map_err(Error::from)
|
||||
--#!}
|
||||
@@ -0,0 +1,10 @@
|
||||
-- Your SQL goes here
|
||||
--#!|conn: &Connection, path: &Path| {
|
||||
--#! let mut pb = path.to_path_buf();
|
||||
--#! pb.push("search_index");
|
||||
--#! let searcher = super::search::Searcher::create(&pb)?;
|
||||
--#! searcher.fill(conn)?;
|
||||
--#! searcher.commit();
|
||||
--#! Ok(())
|
||||
--#!}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
--#!|_conn, path: &Path| {
|
||||
--#! let mut pb = path.to_path_buf();
|
||||
--#! pb.push("search_index");
|
||||
--#! std::fs::remove_dir_all(pb).map_err(Error::from)
|
||||
--#!}
|
||||
@@ -0,0 +1,10 @@
|
||||
-- Your SQL goes here
|
||||
--#!|conn: &Connection, path: &Path| {
|
||||
--#! let mut pb = path.to_path_buf();
|
||||
--#! pb.push("search_index");
|
||||
--#! let searcher = super::search::Searcher::create(&pb)?;
|
||||
--#! searcher.fill(conn)?;
|
||||
--#! searcher.commit();
|
||||
--#! Ok(())
|
||||
--#!}
|
||||
|
||||
@@ -4,6 +4,5 @@ version = "0.3.0"
|
||||
authors = ["Plume contributors"]
|
||||
|
||||
[dependencies]
|
||||
canapi = "0.2"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
use canapi::Endpoint;
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
pub struct AppEndpoint {
|
||||
pub id: Option<i32>,
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct NewAppData {
|
||||
pub name: String,
|
||||
pub website: Option<String>,
|
||||
pub redirect_uri: Option<String>,
|
||||
pub client_id: Option<String>,
|
||||
pub client_secret: Option<String>,
|
||||
}
|
||||
|
||||
api!("/api/v1/apps" => AppEndpoint);
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
extern crate canapi;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
|
||||
macro_rules! api {
|
||||
($url:expr => $ep:ty) => {
|
||||
impl Endpoint for $ep {
|
||||
type Id = i32;
|
||||
|
||||
fn endpoint() -> &'static str {
|
||||
$url
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub mod apps;
|
||||
pub mod posts;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Api {
|
||||
pub posts: posts::PostEndpoint,
|
||||
}
|
||||
|
||||
+21
-9
@@ -1,13 +1,11 @@
|
||||
use canapi::Endpoint;
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
pub struct PostEndpoint {
|
||||
pub id: Option<i32>,
|
||||
pub title: Option<String>,
|
||||
pub struct NewPostData {
|
||||
pub title: String,
|
||||
pub subtitle: Option<String>,
|
||||
pub content: Option<String>,
|
||||
pub source: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub source: String,
|
||||
pub author: String,
|
||||
// If None, and that there is only one blog, it will be choosen automatically.
|
||||
// If there are more than one blog, the request will fail.
|
||||
pub blog_id: Option<i32>,
|
||||
pub published: Option<bool>,
|
||||
pub creation_date: Option<String>,
|
||||
@@ -16,4 +14,18 @@ pub struct PostEndpoint {
|
||||
pub cover_id: Option<i32>,
|
||||
}
|
||||
|
||||
api!("/api/v1/posts" => PostEndpoint);
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
pub struct PostData {
|
||||
pub id: i32,
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub content: String,
|
||||
pub source: Option<String>,
|
||||
pub authors: Vec<String>,
|
||||
pub blog_id: i32,
|
||||
pub published: bool,
|
||||
pub creation_date: String,
|
||||
pub license: String,
|
||||
pub tags: Vec<String>,
|
||||
pub cover_id: Option<i32>,
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ name = "plm"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.32"
|
||||
dotenv = "0.13"
|
||||
rpassword = "2.1"
|
||||
clap = "2.33"
|
||||
dotenv = "0.14"
|
||||
rpassword = "3.0"
|
||||
|
||||
[dependencies.diesel]
|
||||
features = ["r2d2", "chrono"]
|
||||
|
||||
+11
-5
@@ -6,10 +6,11 @@ extern crate rpassword;
|
||||
|
||||
use clap::App;
|
||||
use diesel::Connection;
|
||||
use plume_models::{Connection as Conn, CONFIG};
|
||||
use plume_models::{instance::Instance, Connection as Conn, CONFIG};
|
||||
use std::io::{self, prelude::*};
|
||||
|
||||
mod instance;
|
||||
mod migration;
|
||||
mod search;
|
||||
mod users;
|
||||
|
||||
@@ -19,23 +20,28 @@ fn main() {
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.about("Collection of tools to manage your Plume instance.")
|
||||
.subcommand(instance::command())
|
||||
.subcommand(users::command())
|
||||
.subcommand(search::command());
|
||||
.subcommand(migration::command())
|
||||
.subcommand(search::command())
|
||||
.subcommand(users::command());
|
||||
let matches = app.clone().get_matches();
|
||||
|
||||
dotenv::dotenv().ok();
|
||||
let conn = Conn::establish(CONFIG.database_url.as_str());
|
||||
let _ = conn.as_ref().map(|conn| Instance::cache_local(conn));
|
||||
|
||||
match matches.subcommand() {
|
||||
("instance", Some(args)) => {
|
||||
instance::run(args, &conn.expect("Couldn't connect to the database."))
|
||||
}
|
||||
("users", Some(args)) => {
|
||||
users::run(args, &conn.expect("Couldn't connect to the database."))
|
||||
("migration", Some(args)) => {
|
||||
migration::run(args, &conn.expect("Couldn't connect to the database."))
|
||||
}
|
||||
("search", Some(args)) => {
|
||||
search::run(args, &conn.expect("Couldn't connect to the database."))
|
||||
}
|
||||
("users", Some(args)) => {
|
||||
users::run(args, &conn.expect("Couldn't connect to the database."))
|
||||
}
|
||||
_ => app.print_help().expect("Couldn't print help"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
|
||||
use plume_models::{migrations::IMPORTED_MIGRATIONS, Connection};
|
||||
use std::path::Path;
|
||||
|
||||
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("migration")
|
||||
.about("Manage migrations")
|
||||
.subcommand(
|
||||
SubCommand::with_name("run")
|
||||
.arg(
|
||||
Arg::with_name("path")
|
||||
.short("p")
|
||||
.long("path")
|
||||
.takes_value(true)
|
||||
.required(false)
|
||||
.help("Path to Plume's working directory"),
|
||||
)
|
||||
.about("Run migrations"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("redo")
|
||||
.arg(
|
||||
Arg::with_name("path")
|
||||
.short("p")
|
||||
.long("path")
|
||||
.takes_value(true)
|
||||
.required(false)
|
||||
.help("Path to Plume's working directory"),
|
||||
)
|
||||
.about("Rerun latest migration"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||
let conn = conn;
|
||||
match args.subcommand() {
|
||||
("run", Some(x)) => run_(x, conn),
|
||||
("redo", Some(x)) => redo(x, conn),
|
||||
("", None) => command().print_help().unwrap(),
|
||||
_ => println!("Unknown subcommand"),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||
let path = args.value_of("path").unwrap_or(".");
|
||||
|
||||
IMPORTED_MIGRATIONS
|
||||
.run_pending_migrations(conn, Path::new(path))
|
||||
.expect("Failed to run migrations")
|
||||
}
|
||||
|
||||
fn redo<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||
let path = args.value_of("path").unwrap_or(".");
|
||||
|
||||
IMPORTED_MIGRATIONS
|
||||
.rerun_last_migration(conn, Path::new(path))
|
||||
.expect("Failed to rerun migrations")
|
||||
}
|
||||
+6
-17
@@ -1,7 +1,6 @@
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
|
||||
use plume_models::{posts::Post, schema::posts, search::Searcher, Connection, CONFIG};
|
||||
use plume_models::{search::Searcher, Connection, CONFIG};
|
||||
use std::fs::{read_dir, remove_file};
|
||||
use std::io::ErrorKind;
|
||||
use std::path::Path;
|
||||
@@ -98,25 +97,15 @@ fn refill<'a>(args: &ArgMatches<'a>, conn: &Connection, searcher: Option<Searche
|
||||
let path = Path::new(path).join("search_index");
|
||||
let searcher = searcher.unwrap_or_else(|| Searcher::open(&path).unwrap());
|
||||
|
||||
let posts = posts::table
|
||||
.filter(posts::published.eq(true))
|
||||
.load::<Post>(conn)
|
||||
.expect("Post::get_recents: loading error");
|
||||
|
||||
let len = posts.len();
|
||||
for (i, post) in posts.iter().enumerate() {
|
||||
println!("Importing {}/{} : {}", i + 1, len, post.title);
|
||||
searcher
|
||||
.update_document(conn, &post)
|
||||
.expect("Couldn't import post");
|
||||
}
|
||||
searcher.fill(conn).expect("Couldn't import post");
|
||||
println!("Commiting result");
|
||||
searcher.commit();
|
||||
}
|
||||
|
||||
fn unlock<'a>(args: &ArgMatches<'a>) {
|
||||
let path = args.value_of("path").unwrap_or(".");
|
||||
let path = Path::new(path).join("search_index/.tantivy-indexer.lock");
|
||||
|
||||
remove_file(path).unwrap();
|
||||
let meta = Path::new(path).join("search_index/.tantivy-meta.lock");
|
||||
remove_file(meta).unwrap();
|
||||
let writer = Path::new(path).join("search_index/.tantivy-writer.lock");
|
||||
remove_file(writer).unwrap();
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ fn reset_password<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||
let user = User::find_by_name(
|
||||
conn,
|
||||
&username,
|
||||
Instance::get_local(conn)
|
||||
Instance::get_local()
|
||||
.expect("Failed to get local instance")
|
||||
.id,
|
||||
)
|
||||
|
||||
@@ -11,13 +11,14 @@ array_tool = "1.0"
|
||||
base64 = "0.10"
|
||||
heck = "0.3.0"
|
||||
hex = "0.3"
|
||||
hyper = "0.12.20"
|
||||
openssl = "0.10.15"
|
||||
hyper = "0.12.28"
|
||||
openssl = "0.10.22"
|
||||
rocket = "0.4.0"
|
||||
reqwest = "0.9"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
shrinkwraprs = "0.2.1"
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
|
||||
@@ -279,7 +279,10 @@ pub trait FromId<C>: Sized {
|
||||
|
||||
/// Dereferences an ID
|
||||
fn deref(id: &str) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
|
||||
reqwest::Client::new()
|
||||
reqwest::ClientBuilder::new()
|
||||
.connect_timeout(Some(std::time::Duration::from_secs(5)))
|
||||
.build()
|
||||
.map_err(|_| (None, InboxError::DerefError.into()))?
|
||||
.get(id)
|
||||
.header(
|
||||
ACCEPT,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use activitypub::{Activity, Link, Object};
|
||||
use array_tool::vec::Uniq;
|
||||
use reqwest::Client;
|
||||
use reqwest::ClientBuilder;
|
||||
use rocket::{
|
||||
http::Status,
|
||||
request::{FromRequest, Request},
|
||||
@@ -134,16 +134,21 @@ where
|
||||
let body = signed.to_string();
|
||||
let mut headers = request::headers();
|
||||
headers.insert("Digest", request::Digest::digest(&body));
|
||||
let res = Client::new()
|
||||
.post(&inbox)
|
||||
.headers(headers.clone())
|
||||
.header(
|
||||
"Signature",
|
||||
request::signature(sender, &headers)
|
||||
.expect("activity_pub::broadcast: request signature error"),
|
||||
)
|
||||
.body(body)
|
||||
.send();
|
||||
let res = ClientBuilder::new()
|
||||
.connect_timeout(Some(std::time::Duration::from_secs(5)))
|
||||
.build()
|
||||
.and_then(|client| {
|
||||
client
|
||||
.post(&inbox)
|
||||
.headers(headers.clone())
|
||||
.header(
|
||||
"Signature",
|
||||
request::signature(sender, &headers)
|
||||
.expect("activity_pub::broadcast: request signature error"),
|
||||
)
|
||||
.body(body)
|
||||
.send()
|
||||
});
|
||||
match res {
|
||||
Ok(mut r) => {
|
||||
println!("Successfully sent activity to inbox ({})", inbox);
|
||||
@@ -158,18 +163,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Shrinkwrap, Clone, Serialize, Deserialize)]
|
||||
pub struct Id(String);
|
||||
|
||||
impl Id {
|
||||
pub fn new<T: Into<String>>(id: T) -> Id {
|
||||
Id(id.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<String> for Id {
|
||||
fn into(self) -> String {
|
||||
self.0.clone()
|
||||
pub fn new(id: impl ToString) -> Id {
|
||||
Id(id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ extern crate reqwest;
|
||||
extern crate rocket;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate shrinkwraprs;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
|
||||
@@ -154,10 +154,15 @@ fn process_image<'a, 'b>(
|
||||
/// Returns (HTML, mentions, hashtags)
|
||||
pub fn md_to_html<'a>(
|
||||
md: &str,
|
||||
base_url: &str,
|
||||
base_url: Option<&str>,
|
||||
inline: bool,
|
||||
media_processor: Option<MediaProcessor<'a>>,
|
||||
) -> (String, HashSet<String>, HashSet<String>) {
|
||||
let base_url = if let Some(base_url) = base_url {
|
||||
format!("//{}/", base_url)
|
||||
} else {
|
||||
"/".to_owned()
|
||||
};
|
||||
let parser = Parser::new_ext(md, Options::all());
|
||||
|
||||
let (parser, mentions, hashtags): (Vec<Event>, Vec<String>, Vec<String>) = parser
|
||||
@@ -185,7 +190,7 @@ pub fn md_to_html<'a>(
|
||||
let mention = text_acc;
|
||||
let short_mention = mention.splitn(1, '@').nth(0).unwrap_or("");
|
||||
let link = Tag::Link(
|
||||
format!("//{}/@/{}/", base_url, &mention).into(),
|
||||
format!("{}@/{}/", base_url, &mention).into(),
|
||||
short_mention.to_owned().into(),
|
||||
);
|
||||
|
||||
@@ -215,7 +220,7 @@ pub fn md_to_html<'a>(
|
||||
}
|
||||
let hashtag = text_acc;
|
||||
let link = Tag::Link(
|
||||
format!("//{}/tag/{}", base_url, &hashtag.to_camel_case())
|
||||
format!("{}tag/{}", base_url, &hashtag.to_camel_case())
|
||||
.into(),
|
||||
hashtag.to_owned().into(),
|
||||
);
|
||||
@@ -337,7 +342,7 @@ mod tests {
|
||||
|
||||
for (md, mentions) in tests {
|
||||
assert_eq!(
|
||||
md_to_html(md, "", false, None).1,
|
||||
md_to_html(md, None, false, None).1,
|
||||
mentions
|
||||
.into_iter()
|
||||
.map(|s| s.to_string())
|
||||
@@ -362,7 +367,7 @@ mod tests {
|
||||
|
||||
for (md, mentions) in tests {
|
||||
assert_eq!(
|
||||
md_to_html(md, "", false, None).2,
|
||||
md_to_html(md, None, false, None).2,
|
||||
mentions
|
||||
.into_iter()
|
||||
.map(|s| s.to_string())
|
||||
@@ -374,11 +379,11 @@ mod tests {
|
||||
#[test]
|
||||
fn test_inline() {
|
||||
assert_eq!(
|
||||
md_to_html("# Hello", "", false, None).0,
|
||||
md_to_html("# Hello", None, false, None).0,
|
||||
String::from("<h1>Hello</h1>\n")
|
||||
);
|
||||
assert_eq!(
|
||||
md_to_html("# Hello", "", true, None).0,
|
||||
md_to_html("# Hello", None, true, None).0,
|
||||
String::from("<p>Hello</p>\n")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ version = "0.3.0"
|
||||
authors = ["Plume contributors"]
|
||||
|
||||
[dependencies]
|
||||
stdweb = "0.4"
|
||||
stdweb = "=0.4.14"
|
||||
stdweb-internal-runtime = "=0.1.3"
|
||||
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
||||
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "plume-macro"
|
||||
version = "0.1.0"
|
||||
authors = ["Trinity Pointard <trinity.pointard@insa-rennes.fr>"]
|
||||
edition = "2018"
|
||||
description = "Plume procedural macros"
|
||||
license = "AGPLv3"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "0.4"
|
||||
quote = "0.6.12"
|
||||
syn = "0.15.27"
|
||||
|
||||
|
||||
[features]
|
||||
default = []
|
||||
postgres = []
|
||||
sqlite = []
|
||||
@@ -0,0 +1,145 @@
|
||||
#![recursion_limit = "128"]
|
||||
extern crate proc_macro;
|
||||
#[macro_use]
|
||||
extern crate quote;
|
||||
extern crate syn;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use std::fs::{read_dir, File};
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[proc_macro]
|
||||
pub fn import_migrations(input: TokenStream) -> TokenStream {
|
||||
assert!(input.is_empty());
|
||||
let migration_dir = if cfg!(feature = "postgres") {
|
||||
"migrations/postgres"
|
||||
} else if cfg!(feature = "sqlite") {
|
||||
"migrations/sqlite"
|
||||
} else {
|
||||
"migrations"
|
||||
};
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.ancestors()
|
||||
.find(|path| path.join(migration_dir).is_dir() || path.join(".git").exists())
|
||||
.expect("migrations dir not found")
|
||||
.join(migration_dir);
|
||||
let mut files = read_dir(path)
|
||||
.unwrap()
|
||||
.map(|dir| dir.unwrap())
|
||||
.filter(|dir| dir.file_type().unwrap().is_dir())
|
||||
.map(|dir| dir.path())
|
||||
.collect::<Vec<_>>();
|
||||
files.sort_unstable();
|
||||
let migrations = files
|
||||
.into_iter()
|
||||
.map(|path| {
|
||||
let mut up = path.clone();
|
||||
let mut down = path.clone();
|
||||
up.push("up.sql");
|
||||
down.push("down.sql");
|
||||
let mut up_sql = String::new();
|
||||
let mut down_sql = String::new();
|
||||
File::open(up).unwrap().read_to_string(&mut up_sql).unwrap();
|
||||
File::open(down)
|
||||
.unwrap()
|
||||
.read_to_string(&mut down_sql)
|
||||
.unwrap();
|
||||
let name = path
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.chars()
|
||||
.filter(char::is_ascii_digit)
|
||||
.take(14)
|
||||
.collect::<String>();
|
||||
(name, up_sql, down_sql)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let migrations_name = migrations.iter().map(|m| &m.0).collect::<Vec<_>>();
|
||||
let migrations_up = migrations
|
||||
.iter()
|
||||
.map(|m| m.1.as_str())
|
||||
.map(file_to_migration)
|
||||
.collect::<Vec<_>>();
|
||||
let migrations_down = migrations
|
||||
.iter()
|
||||
.map(|m| m.2.as_str())
|
||||
.map(file_to_migration)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
/*
|
||||
enum Action {
|
||||
Sql(&'static str),
|
||||
Function(&'static Fn(&Connection, &Path) -> Result<()>)
|
||||
}*/
|
||||
|
||||
quote!(
|
||||
ImportedMigrations(
|
||||
&[#(ComplexMigration{name: #migrations_name, up: #migrations_up, down: #migrations_down}),*]
|
||||
)
|
||||
).into()
|
||||
}
|
||||
|
||||
fn file_to_migration(file: &str) -> TokenStream2 {
|
||||
let mut sql = true;
|
||||
let mut acc = String::new();
|
||||
let mut actions = vec![];
|
||||
for line in file.lines() {
|
||||
if sql {
|
||||
if line.starts_with("--#!") {
|
||||
if !acc.trim().is_empty() {
|
||||
actions.push(quote!(Action::Sql(#acc)));
|
||||
}
|
||||
sql = false;
|
||||
acc = line[4..].to_string();
|
||||
acc.push('\n');
|
||||
} else if line.starts_with("--") {
|
||||
continue;
|
||||
} else {
|
||||
acc.push_str(line);
|
||||
acc.push('\n');
|
||||
}
|
||||
} else {
|
||||
if line.starts_with("--#!") {
|
||||
acc.push_str(&line[4..]);
|
||||
acc.push('\n');
|
||||
} else if line.starts_with("--") {
|
||||
continue;
|
||||
} else {
|
||||
let func: TokenStream2 = trampoline(TokenStream::from_str(&acc).unwrap().into());
|
||||
actions.push(quote!(Action::Function(&#func)));
|
||||
sql = true;
|
||||
acc = line.to_string();
|
||||
acc.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
if !acc.trim().is_empty() {
|
||||
if sql {
|
||||
actions.push(quote!(Action::Sql(#acc)));
|
||||
} else {
|
||||
let func: TokenStream2 = trampoline(TokenStream::from_str(&acc).unwrap().into());
|
||||
actions.push(quote!(Action::Function(&#func)));
|
||||
}
|
||||
}
|
||||
|
||||
quote!(
|
||||
&[#(#actions),*]
|
||||
)
|
||||
}
|
||||
|
||||
/// Build a trampoline to allow reference to closure from const context
|
||||
fn trampoline(closure: TokenStream2) -> TokenStream2 {
|
||||
quote! {
|
||||
{
|
||||
fn trampoline<'a, 'b>(conn: &'a Connection, path: &'b Path) -> Result<()> {
|
||||
(#closure)(conn, path)
|
||||
}
|
||||
trampoline
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-8
@@ -5,15 +5,15 @@ authors = ["Plume contributors"]
|
||||
|
||||
[dependencies]
|
||||
activitypub = "0.1.1"
|
||||
ammonia = "2.0.0"
|
||||
ammonia = "2.1.1"
|
||||
askama_escape = "0.1"
|
||||
bcrypt = "0.2"
|
||||
canapi = "0.2"
|
||||
bcrypt = "0.4"
|
||||
guid-create = "0.1"
|
||||
heck = "0.3.0"
|
||||
itertools = "0.8.0"
|
||||
lazy_static = "*"
|
||||
openssl = "0.10.15"
|
||||
migrations_internals= "1.4.0"
|
||||
openssl = "0.10.22"
|
||||
rocket = "0.4.0"
|
||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||
reqwest = "0.9"
|
||||
@@ -24,7 +24,9 @@ serde_json = "1.0"
|
||||
tantivy = "0.9.1"
|
||||
url = "1.7"
|
||||
webfinger = "0.3.1"
|
||||
whatlang = "0.7.0"
|
||||
whatlang = "0.7.1"
|
||||
shrinkwraprs = "0.2.1"
|
||||
diesel-derive-newtype = "0.1.2"
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
@@ -32,7 +34,7 @@ version = "0.4"
|
||||
|
||||
[dependencies.diesel]
|
||||
features = ["r2d2", "chrono"]
|
||||
version = "1.3.2"
|
||||
version = "1.4.2"
|
||||
|
||||
[dependencies.plume-api]
|
||||
path = "../plume-api"
|
||||
@@ -40,9 +42,12 @@ path = "../plume-api"
|
||||
[dependencies.plume-common]
|
||||
path = "../plume-common"
|
||||
|
||||
[dependencies.plume-macro]
|
||||
path = "../plume-macro"
|
||||
|
||||
[dev-dependencies]
|
||||
diesel_migrations = "1.3.0"
|
||||
|
||||
[features]
|
||||
postgres = ["diesel/postgres"]
|
||||
sqlite = ["diesel/sqlite"]
|
||||
postgres = ["diesel/postgres", "plume-macro/postgres"]
|
||||
sqlite = ["diesel/sqlite", "plume-macro/sqlite"]
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
use canapi::{Error as ApiError, Provider};
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
|
||||
use plume_api::apps::AppEndpoint;
|
||||
use plume_common::utils::random_hex;
|
||||
use schema::apps;
|
||||
use {ApiResult, Connection, Error, Result};
|
||||
use {Error, Result};
|
||||
|
||||
#[derive(Clone, Queryable)]
|
||||
#[derive(Clone, Queryable, Serialize)]
|
||||
pub struct App {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
@@ -28,52 +25,6 @@ pub struct NewApp {
|
||||
pub website: Option<String>,
|
||||
}
|
||||
|
||||
impl Provider<Connection> for App {
|
||||
type Data = AppEndpoint;
|
||||
|
||||
fn get(_conn: &Connection, _id: i32) -> ApiResult<AppEndpoint> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn list(_conn: &Connection, _query: AppEndpoint) -> Vec<AppEndpoint> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn create(conn: &Connection, data: AppEndpoint) -> ApiResult<AppEndpoint> {
|
||||
let client_id = random_hex();
|
||||
|
||||
let client_secret = random_hex();
|
||||
let app = App::insert(
|
||||
conn,
|
||||
NewApp {
|
||||
name: data.name,
|
||||
client_id,
|
||||
client_secret,
|
||||
redirect_uri: data.redirect_uri,
|
||||
website: data.website,
|
||||
},
|
||||
)
|
||||
.map_err(|_| ApiError::NotFound("Couldn't register app".into()))?;
|
||||
|
||||
Ok(AppEndpoint {
|
||||
id: Some(app.id),
|
||||
name: app.name,
|
||||
client_id: Some(app.client_id),
|
||||
client_secret: Some(app.client_secret),
|
||||
redirect_uri: app.redirect_uri,
|
||||
website: app.website,
|
||||
})
|
||||
}
|
||||
|
||||
fn update(_conn: &Connection, _id: i32, _new_data: AppEndpoint) -> ApiResult<AppEndpoint> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn delete(_conn: &Connection, _id: i32) {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
get!(apps);
|
||||
insert!(apps, NewApp);
|
||||
|
||||
+20
-18
@@ -172,7 +172,7 @@ impl Blog {
|
||||
let mut icon = Image::default();
|
||||
icon.object_props.set_url_string(
|
||||
self.icon_id
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url(conn)).ok())
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
|
||||
.unwrap_or_default(),
|
||||
)?;
|
||||
icon.object_props.set_attributed_to_link(
|
||||
@@ -189,7 +189,7 @@ impl Blog {
|
||||
let mut banner = Image::default();
|
||||
banner.object_props.set_url_string(
|
||||
self.banner_id
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url(conn)).ok())
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
|
||||
.unwrap_or_default(),
|
||||
)?;
|
||||
banner.object_props.set_attributed_to_link(
|
||||
@@ -271,14 +271,14 @@ impl Blog {
|
||||
|
||||
pub fn icon_url(&self, conn: &Connection) -> String {
|
||||
self.icon_id
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url(conn)).ok())
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
|
||||
.unwrap_or_else(|| "/static/default-avatar.png".to_string())
|
||||
}
|
||||
|
||||
pub fn banner_url(&self, conn: &Connection) -> Option<String> {
|
||||
self.banner_id
|
||||
.and_then(|i| Media::get(conn, i).ok())
|
||||
.and_then(|c| c.url(conn).ok())
|
||||
.and_then(|c| c.url().ok())
|
||||
}
|
||||
|
||||
pub fn delete(&self, conn: &Connection, searcher: &Searcher) -> Result<()> {
|
||||
@@ -332,7 +332,7 @@ impl FromId<PlumeRocket> for Blog {
|
||||
.icon_image()
|
||||
.ok()
|
||||
.and_then(|icon| {
|
||||
let owner: String = icon.object_props.attributed_to_link::<Id>().ok()?.into();
|
||||
let owner = icon.object_props.attributed_to_link::<Id>().ok()?;
|
||||
Media::save_remote(
|
||||
&c.conn,
|
||||
icon.object_props.url_string().ok()?,
|
||||
@@ -348,7 +348,7 @@ impl FromId<PlumeRocket> for Blog {
|
||||
.image_image()
|
||||
.ok()
|
||||
.and_then(|banner| {
|
||||
let owner: String = banner.object_props.attributed_to_link::<Id>().ok()?.into();
|
||||
let owner = banner.object_props.attributed_to_link::<Id>().ok()?;
|
||||
Media::save_remote(
|
||||
&c.conn,
|
||||
banner.object_props.url_string().ok()?,
|
||||
@@ -407,7 +407,9 @@ impl AsActor<&PlumeRocket> for Blog {
|
||||
}
|
||||
|
||||
fn is_local(&self) -> bool {
|
||||
self.instance_id == 1 // TODO: this is not always true
|
||||
Instance::get_local()
|
||||
.map(|i| self.instance_id == i.id)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,7 +476,7 @@ pub(crate) mod tests {
|
||||
"BlogName".to_owned(),
|
||||
"Blog name".to_owned(),
|
||||
"This is a small blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
Instance::get_local().unwrap().id,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -485,7 +487,7 @@ pub(crate) mod tests {
|
||||
"MyBlog".to_owned(),
|
||||
"My blog".to_owned(),
|
||||
"Welcome to my blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
Instance::get_local().unwrap().id,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -496,7 +498,7 @@ pub(crate) mod tests {
|
||||
"WhyILikePlume".to_owned(),
|
||||
"Why I like Plume".to_owned(),
|
||||
"In this blog I will explay you why I like Plume so much".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
Instance::get_local().unwrap().id,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -556,7 +558,7 @@ pub(crate) mod tests {
|
||||
"SomeName".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
Instance::get_local().unwrap().id,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -564,7 +566,7 @@ pub(crate) mod tests {
|
||||
|
||||
assert_eq!(
|
||||
blog.get_instance(conn).unwrap().id,
|
||||
Instance::get_local(conn).unwrap().id
|
||||
Instance::get_local().unwrap().id
|
||||
);
|
||||
// TODO add tests for remote instance
|
||||
|
||||
@@ -584,7 +586,7 @@ pub(crate) mod tests {
|
||||
"SomeName".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
Instance::get_local().unwrap().id,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -595,7 +597,7 @@ pub(crate) mod tests {
|
||||
"Blog".to_owned(),
|
||||
"Blog".to_owned(),
|
||||
"I've named my blog Blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
Instance::get_local().unwrap().id,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -687,7 +689,7 @@ pub(crate) mod tests {
|
||||
"SomeName".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
Instance::get_local().unwrap().id,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -711,7 +713,7 @@ pub(crate) mod tests {
|
||||
"SomeName".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
Instance::get_local().unwrap().id,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -749,7 +751,7 @@ pub(crate) mod tests {
|
||||
"SomeName".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
Instance::get_local().unwrap().id,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -760,7 +762,7 @@ pub(crate) mod tests {
|
||||
"Blog".to_owned(),
|
||||
"Blog".to_owned(),
|
||||
"I've named my blog Blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
Instance::get_local().unwrap().id,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ use medias::Media;
|
||||
use mentions::Mention;
|
||||
use notifications::*;
|
||||
use plume_common::activity_pub::{
|
||||
inbox::{AsObject, FromId},
|
||||
inbox::{AsActor, AsObject, FromId},
|
||||
Id, IntoId, PUBLIC_VISIBILITY,
|
||||
};
|
||||
use plume_common::utils;
|
||||
@@ -79,7 +79,7 @@ impl Comment {
|
||||
pub fn count_local(conn: &Connection) -> Result<i64> {
|
||||
use schema::users;
|
||||
let local_authors = users::table
|
||||
.filter(users::instance_id.eq(Instance::get_local(conn)?.id))
|
||||
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
||||
.select(users::id);
|
||||
comments::table
|
||||
.filter(comments::author_id.eq_any(local_authors))
|
||||
@@ -107,7 +107,7 @@ impl Comment {
|
||||
let author = User::get(&c.conn, self.author_id)?;
|
||||
let (html, mentions, _hashtags) = utils::md_to_html(
|
||||
self.content.get().as_ref(),
|
||||
&Instance::get_local(&c.conn)?.public_domain,
|
||||
Some(&Instance::get_local()?.public_domain),
|
||||
true,
|
||||
Some(Media::get_media_processor(&c.conn, vec![&author])),
|
||||
);
|
||||
@@ -157,14 +157,20 @@ impl Comment {
|
||||
|
||||
pub fn notify(&self, conn: &Connection) -> Result<()> {
|
||||
for author in self.get_post(conn)?.get_authors(conn)? {
|
||||
Notification::insert(
|
||||
conn,
|
||||
NewNotification {
|
||||
kind: notification_kind::COMMENT.to_string(),
|
||||
object_id: self.id,
|
||||
user_id: author.id,
|
||||
},
|
||||
)?;
|
||||
if Mention::list_for_comment(conn, self.id)?
|
||||
.iter()
|
||||
.all(|m| m.get_mentioned(conn).map(|u| u != author).unwrap_or(true))
|
||||
&& author.is_local()
|
||||
{
|
||||
Notification::insert(
|
||||
conn,
|
||||
NewNotification {
|
||||
kind: notification_kind::COMMENT.to_string(),
|
||||
object_id: self.id,
|
||||
user_id: author.id,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -230,10 +236,7 @@ impl FromId<PlumeRocket> for Comment {
|
||||
})?,
|
||||
author_id: User::from_id(
|
||||
c,
|
||||
&{
|
||||
let res: String = note.object_props.attributed_to_link::<Id>()?.into();
|
||||
res
|
||||
},
|
||||
¬e.object_props.attributed_to_link::<Id>()?,
|
||||
None,
|
||||
)
|
||||
.map_err(|(_, e)| e)?
|
||||
|
||||
+16
-27
@@ -67,15 +67,18 @@ impl Follow {
|
||||
Ok(act)
|
||||
}
|
||||
|
||||
pub fn notify(&self, conn: &Connection) -> Result<Notification> {
|
||||
Notification::insert(
|
||||
conn,
|
||||
NewNotification {
|
||||
kind: notification_kind::FOLLOW.to_string(),
|
||||
object_id: self.id,
|
||||
user_id: self.following_id,
|
||||
},
|
||||
)
|
||||
pub fn notify(&self, conn: &Connection) -> Result<()> {
|
||||
if User::get(conn, self.following_id)?.is_local() {
|
||||
Notification::insert(
|
||||
conn,
|
||||
NewNotification {
|
||||
kind: notification_kind::FOLLOW.to_string(),
|
||||
object_id: self.id,
|
||||
user_id: self.following_id,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// from -> The one sending the follow request
|
||||
@@ -160,25 +163,11 @@ impl FromId<PlumeRocket> for Follow {
|
||||
}
|
||||
|
||||
fn from_activity(c: &PlumeRocket, follow: FollowAct) -> Result<Self> {
|
||||
let actor = User::from_id(
|
||||
c,
|
||||
&{
|
||||
let res: String = follow.follow_props.actor_link::<Id>()?.into();
|
||||
res
|
||||
},
|
||||
None,
|
||||
)
|
||||
.map_err(|(_, e)| e)?;
|
||||
let actor =
|
||||
User::from_id(c, &follow.follow_props.actor_link::<Id>()?, None).map_err(|(_, e)| e)?;
|
||||
|
||||
let target = User::from_id(
|
||||
c,
|
||||
&{
|
||||
let res: String = follow.follow_props.object_link::<Id>()?.into();
|
||||
res
|
||||
},
|
||||
None,
|
||||
)
|
||||
.map_err(|(_, e)| e)?;
|
||||
let target = User::from_id(c, &follow.follow_props.object_link::<Id>()?, None)
|
||||
.map_err(|(_, e)| e)?;
|
||||
Follow::accept_follow(&c.conn, &actor, &target, follow, actor.id, target.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ pub fn inbox(ctx: &PlumeRocket, act: serde_json::Value) -> Result<InboxResult, E
|
||||
.with::<User, Create, Post>()
|
||||
.with::<User, Delete, Comment>()
|
||||
.with::<User, Delete, Post>()
|
||||
.with::<User, Delete, User>()
|
||||
.with::<User, Follow, User>()
|
||||
.with::<User, Like, Post>()
|
||||
.with::<User, Undo, Reshare>()
|
||||
@@ -286,6 +287,34 @@ pub(crate) mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_user() {
|
||||
let r = rockets();
|
||||
let conn = &*r.conn;
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (_, users, _) = fill_database(&r);
|
||||
|
||||
let fail_act = json!({
|
||||
"id": "https://plu.me/@/Admin#delete",
|
||||
"actor": users[1].ap_url, // Not the same account
|
||||
"object": users[0].ap_url,
|
||||
"type": "Delete",
|
||||
});
|
||||
assert!(super::inbox(&r, fail_act).is_err());
|
||||
|
||||
let ok_act = json!({
|
||||
"id": "https://plu.me/@/Admin#delete",
|
||||
"actor": users[0].ap_url,
|
||||
"object": users[0].ap_url,
|
||||
"type": "Delete",
|
||||
});
|
||||
assert!(super::inbox(&r, ok_act).is_ok());
|
||||
assert!(crate::users::User::get(conn, users[0].id).is_err());
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn follow() {
|
||||
let r = rockets();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use std::iter::Iterator;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use ap_url;
|
||||
use medias::Media;
|
||||
@@ -40,8 +41,24 @@ pub struct NewInstance {
|
||||
pub short_description_html: String,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref LOCAL_INSTANCE: RwLock<Option<Instance>> = RwLock::new(None);
|
||||
}
|
||||
|
||||
impl Instance {
|
||||
pub fn get_local(conn: &Connection) -> Result<Instance> {
|
||||
pub fn set_local(self) {
|
||||
LOCAL_INSTANCE.write().unwrap().replace(self);
|
||||
}
|
||||
|
||||
pub fn get_local() -> Result<Instance> {
|
||||
LOCAL_INSTANCE
|
||||
.read()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.ok_or(Error::NotFound)
|
||||
}
|
||||
|
||||
pub fn get_local_uncached(conn: &Connection) -> Result<Instance> {
|
||||
instances::table
|
||||
.filter(instances::local.eq(true))
|
||||
.limit(1)
|
||||
@@ -51,6 +68,10 @@ impl Instance {
|
||||
.ok_or(Error::NotFound)
|
||||
}
|
||||
|
||||
pub fn cache_local(conn: &Connection) {
|
||||
*LOCAL_INSTANCE.write().unwrap() = Instance::get_local_uncached(conn).ok();
|
||||
}
|
||||
|
||||
pub fn get_remotes(conn: &Connection) -> Result<Vec<Instance>> {
|
||||
instances::table
|
||||
.filter(instances::local.eq(false))
|
||||
@@ -131,17 +152,17 @@ impl Instance {
|
||||
) -> Result<()> {
|
||||
let (sd, _, _) = md_to_html(
|
||||
short_description.as_ref(),
|
||||
&self.public_domain,
|
||||
Some(&self.public_domain),
|
||||
true,
|
||||
Some(Media::get_media_processor(conn, vec![])),
|
||||
);
|
||||
let (ld, _, _) = md_to_html(
|
||||
long_description.as_ref(),
|
||||
&self.public_domain,
|
||||
Some(&self.public_domain),
|
||||
false,
|
||||
Some(Media::get_media_processor(conn, vec![])),
|
||||
);
|
||||
diesel::update(self)
|
||||
let res = diesel::update(self)
|
||||
.set((
|
||||
instances::name.eq(name),
|
||||
instances::open_registrations.eq(open_registrations),
|
||||
@@ -152,7 +173,11 @@ impl Instance {
|
||||
))
|
||||
.execute(conn)
|
||||
.map(|_| ())
|
||||
.map_err(Error::from)
|
||||
.map_err(Error::from);
|
||||
if self.local {
|
||||
Instance::cache_local(conn);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub fn count(conn: &Connection) -> Result<i64> {
|
||||
@@ -171,7 +196,7 @@ pub(crate) mod tests {
|
||||
use Connection as Conn;
|
||||
|
||||
pub(crate) fn fill_database(conn: &Conn) -> Vec<(NewInstance, Instance)> {
|
||||
vec![
|
||||
let res = vec![
|
||||
NewInstance {
|
||||
default_license: "WTFPL".to_string(),
|
||||
local: true,
|
||||
@@ -225,7 +250,9 @@ pub(crate) mod tests {
|
||||
.unwrap_or_else(|_| Instance::insert(conn, inst).unwrap()),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
.collect();
|
||||
Instance::cache_local(conn);
|
||||
res
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -237,7 +264,7 @@ pub(crate) mod tests {
|
||||
.map(|(inserted, _)| inserted)
|
||||
.find(|inst| inst.local)
|
||||
.unwrap();
|
||||
let res = Instance::get_local(conn).unwrap();
|
||||
let res = Instance::get_local().unwrap();
|
||||
|
||||
part_eq!(
|
||||
res,
|
||||
|
||||
+12
-14
@@ -1,12 +1,12 @@
|
||||
#![feature(try_trait)]
|
||||
#![feature(never_type)]
|
||||
#![feature(custom_attribute)]
|
||||
#![feature(proc_macro_hygiene)]
|
||||
|
||||
extern crate activitypub;
|
||||
extern crate ammonia;
|
||||
extern crate askama_escape;
|
||||
extern crate bcrypt;
|
||||
extern crate canapi;
|
||||
extern crate chrono;
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
@@ -15,9 +15,12 @@ extern crate heck;
|
||||
extern crate itertools;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
extern crate migrations_internals;
|
||||
extern crate openssl;
|
||||
extern crate plume_api;
|
||||
extern crate plume_common;
|
||||
#[macro_use]
|
||||
extern crate plume_macro;
|
||||
extern crate reqwest;
|
||||
extern crate rocket;
|
||||
extern crate rocket_i18n;
|
||||
@@ -33,10 +36,6 @@ extern crate url;
|
||||
extern crate webfinger;
|
||||
extern crate whatlang;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate diesel_migrations;
|
||||
|
||||
use plume_common::activity_pub::inbox::InboxError;
|
||||
|
||||
#[cfg(not(any(feature = "sqlite", feature = "postgres")))]
|
||||
@@ -154,8 +153,6 @@ impl From<InboxError<Error>> for Error {
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub type ApiResult<T> = std::result::Result<T, canapi::Error>;
|
||||
|
||||
/// Adds a function to a model, that returns the first
|
||||
/// matching row for a given list of fields.
|
||||
///
|
||||
@@ -305,18 +302,15 @@ mod tests {
|
||||
use diesel::r2d2::ConnectionManager;
|
||||
#[cfg(feature = "sqlite")]
|
||||
use diesel::{dsl::sql_query, RunQueryDsl};
|
||||
use migrations::IMPORTED_MIGRATIONS;
|
||||
use plume_common::utils::random_hex;
|
||||
use scheduled_thread_pool::ScheduledThreadPool;
|
||||
use search;
|
||||
use std::env::temp_dir;
|
||||
use std::sync::Arc;
|
||||
use Connection as Conn;
|
||||
use CONFIG;
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
embed_migrations!("../migrations/sqlite");
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
embed_migrations!("../migrations/postgres");
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! part_eq {
|
||||
( $x:expr, $y:expr, [$( $var:ident ),*] ) => {
|
||||
@@ -338,7 +332,10 @@ mod tests {
|
||||
.connection_customizer(Box::new(db_conn::PragmaForeignKey))
|
||||
.build(ConnectionManager::<Conn>::new(CONFIG.database_url.as_str()))
|
||||
.unwrap();
|
||||
embedded_migrations::run(&*pool.get().unwrap()).expect("Migrations error");
|
||||
let dir = temp_dir().join(format!("plume-test-{}", random_hex()));
|
||||
IMPORTED_MIGRATIONS
|
||||
.run_pending_migrations(&pool.get().unwrap(), &dir)
|
||||
.expect("Migrations error");
|
||||
pool
|
||||
};
|
||||
}
|
||||
@@ -368,6 +365,7 @@ pub mod instance;
|
||||
pub mod likes;
|
||||
pub mod medias;
|
||||
pub mod mentions;
|
||||
pub mod migrations;
|
||||
pub mod notifications;
|
||||
pub mod plume_rocket;
|
||||
pub mod post_authors;
|
||||
|
||||
+17
-29
@@ -4,7 +4,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
|
||||
use notifications::*;
|
||||
use plume_common::activity_pub::{
|
||||
inbox::{AsObject, FromId},
|
||||
inbox::{AsActor, AsObject, FromId},
|
||||
Id, IntoId, PUBLIC_VISIBILITY,
|
||||
};
|
||||
use posts::Post;
|
||||
@@ -54,14 +54,16 @@ impl Like {
|
||||
pub fn notify(&self, conn: &Connection) -> Result<()> {
|
||||
let post = Post::get(conn, self.post_id)?;
|
||||
for author in post.get_authors(conn)? {
|
||||
Notification::insert(
|
||||
conn,
|
||||
NewNotification {
|
||||
kind: notification_kind::LIKE.to_string(),
|
||||
object_id: self.id,
|
||||
user_id: author.id,
|
||||
},
|
||||
)?;
|
||||
if author.is_local() {
|
||||
Notification::insert(
|
||||
conn,
|
||||
NewNotification {
|
||||
kind: notification_kind::LIKE.to_string(),
|
||||
object_id: self.id,
|
||||
user_id: author.id,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -113,26 +115,12 @@ impl FromId<PlumeRocket> for Like {
|
||||
let res = Like::insert(
|
||||
&c.conn,
|
||||
NewLike {
|
||||
post_id: Post::from_id(
|
||||
c,
|
||||
&{
|
||||
let res: String = act.like_props.object_link::<Id>()?.into();
|
||||
res
|
||||
},
|
||||
None,
|
||||
)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
user_id: User::from_id(
|
||||
c,
|
||||
&{
|
||||
let res: String = act.like_props.actor_link::<Id>()?.into();
|
||||
res
|
||||
},
|
||||
None,
|
||||
)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
post_id: Post::from_id(c, &act.like_props.object_link::<Id>()?, None)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
user_id: User::from_id(c, &act.like_props.actor_link::<Id>()?, None)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
ap_url: act.object_props.id_string()?,
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -104,8 +104,8 @@ impl Media {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn html(&self, conn: &Connection) -> Result<SafeString> {
|
||||
let url = self.url(conn)?;
|
||||
pub fn html(&self) -> Result<SafeString> {
|
||||
let url = self.url()?;
|
||||
Ok(match self.category() {
|
||||
MediaCategory::Image => SafeString::trusted(&format!(
|
||||
r#"<img src="{}" alt="{}" title="{}">"#,
|
||||
@@ -126,23 +126,23 @@ impl Media {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn markdown(&self, conn: &Connection) -> Result<SafeString> {
|
||||
pub fn markdown(&self) -> Result<SafeString> {
|
||||
Ok(match self.category() {
|
||||
MediaCategory::Image => {
|
||||
SafeString::new(&format!("", escape(&self.alt_text), self.id))
|
||||
}
|
||||
MediaCategory::Audio | MediaCategory::Video => self.html(conn)?,
|
||||
MediaCategory::Audio | MediaCategory::Video => self.html()?,
|
||||
MediaCategory::Unknown => SafeString::new(""),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn url(&self, conn: &Connection) -> Result<String> {
|
||||
pub fn url(&self) -> Result<String> {
|
||||
if self.is_remote {
|
||||
Ok(self.remote_url.clone().unwrap_or_default())
|
||||
} else {
|
||||
Ok(ap_url(&format!(
|
||||
"{}/{}",
|
||||
Instance::get_local(conn)?.public_domain,
|
||||
Instance::get_local()?.public_domain,
|
||||
self.file_path
|
||||
)))
|
||||
}
|
||||
@@ -237,7 +237,7 @@ impl Media {
|
||||
let media = Media::get(conn, id).ok()?;
|
||||
// if owner is user or check is disabled
|
||||
if uid.contains(&media.owner_id) || uid.is_empty() {
|
||||
Some((media.url(conn).ok()?, media.content_warning))
|
||||
Some((media.url().ok()?, media.content_warning))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
|
||||
use comments::Comment;
|
||||
use notifications::*;
|
||||
use plume_common::activity_pub::inbox::AsActor;
|
||||
use posts::Post;
|
||||
use schema::mentions;
|
||||
use users::User;
|
||||
@@ -129,14 +130,18 @@ impl Mention {
|
||||
|
||||
fn notify(&self, conn: &Connection) -> Result<()> {
|
||||
let m = self.get_mentioned(conn)?;
|
||||
Notification::insert(
|
||||
conn,
|
||||
NewNotification {
|
||||
kind: notification_kind::MENTION.to_string(),
|
||||
object_id: self.id,
|
||||
user_id: m.id,
|
||||
},
|
||||
)
|
||||
.map(|_| ())
|
||||
if m.is_local() {
|
||||
Notification::insert(
|
||||
conn,
|
||||
NewNotification {
|
||||
kind: notification_kind::MENTION.to_string(),
|
||||
object_id: self.id,
|
||||
user_id: m.id,
|
||||
},
|
||||
)
|
||||
.map(|_| ())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
use Connection;
|
||||
use Error;
|
||||
use Result;
|
||||
|
||||
use diesel::connection::{Connection as Conn, SimpleConnection};
|
||||
use migrations_internals::{setup_database, MigrationConnection};
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
#[allow(dead_code)] //variants might not be constructed if not required by current migrations
|
||||
enum Action {
|
||||
Sql(&'static str),
|
||||
Function(&'static Fn(&Connection, &Path) -> Result<()>),
|
||||
}
|
||||
|
||||
impl Action {
|
||||
fn run(&self, conn: &Connection, path: &Path) -> Result<()> {
|
||||
match self {
|
||||
Action::Sql(sql) => conn.batch_execute(sql).map_err(Error::from),
|
||||
Action::Function(f) => f(conn, path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ComplexMigration {
|
||||
name: &'static str,
|
||||
up: &'static [Action],
|
||||
down: &'static [Action],
|
||||
}
|
||||
|
||||
impl ComplexMigration {
|
||||
fn run(&self, conn: &Connection, path: &Path) -> Result<()> {
|
||||
println!("Running migration {}", self.name);
|
||||
for step in self.up {
|
||||
step.run(conn, path)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn revert(&self, conn: &Connection, path: &Path) -> Result<()> {
|
||||
println!("Reverting migration {}", self.name);
|
||||
for step in self.down {
|
||||
step.run(conn, path)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ImportedMigrations(&'static [ComplexMigration]);
|
||||
|
||||
impl ImportedMigrations {
|
||||
pub fn run_pending_migrations(&self, conn: &Connection, path: &Path) -> Result<()> {
|
||||
use diesel::dsl::sql;
|
||||
use diesel::sql_types::Bool;
|
||||
use diesel::{select, RunQueryDsl};
|
||||
#[cfg(feature = "postgres")]
|
||||
let schema_exists: bool = select(sql::<Bool>(
|
||||
"EXISTS \
|
||||
(SELECT 1 \
|
||||
FROM information_schema.tables \
|
||||
WHERE table_name = '__diesel_schema_migrations')",
|
||||
))
|
||||
.get_result(conn)?;
|
||||
#[cfg(feature = "sqlite")]
|
||||
let schema_exists: bool = select(sql::<Bool>(
|
||||
"EXISTS \
|
||||
(SELECT 1 \
|
||||
FROM sqlite_master \
|
||||
WHERE type = 'table' \
|
||||
AND name = '__diesel_schema_migrations')",
|
||||
))
|
||||
.get_result(conn)?;
|
||||
|
||||
if !schema_exists {
|
||||
setup_database(conn)?;
|
||||
}
|
||||
|
||||
let latest_migration = conn.latest_run_migration_version()?;
|
||||
let latest_id = if let Some(migration) = latest_migration {
|
||||
self.0
|
||||
.binary_search_by_key(&migration.as_str(), |mig| mig.name)
|
||||
.map(|id| id + 1)
|
||||
.map_err(|_| Error::NotFound)?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let to_run = &self.0[latest_id..];
|
||||
for migration in to_run {
|
||||
conn.transaction(|| {
|
||||
migration.run(conn, path)?;
|
||||
conn.insert_new_migration(migration.name)
|
||||
.map_err(Error::from)
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_pending(&self, conn: &Connection) -> Result<bool> {
|
||||
let latest_migration = conn.latest_run_migration_version()?;
|
||||
if let Some(migration) = latest_migration {
|
||||
Ok(self.0.last().expect("no migrations found").name != migration)
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rerun_last_migration(&self, conn: &Connection, path: &Path) -> Result<()> {
|
||||
let latest_migration = conn.latest_run_migration_version()?;
|
||||
let id = latest_migration
|
||||
.and_then(|m| self.0.binary_search_by_key(&m.as_str(), |m| m.name).ok())?;
|
||||
let migration = &self.0[id];
|
||||
conn.transaction(|| {
|
||||
migration.revert(conn, path)?;
|
||||
migration.run(conn, path)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub const IMPORTED_MIGRATIONS: ImportedMigrations = {
|
||||
import_migrations! {}
|
||||
};
|
||||
@@ -6,7 +6,7 @@ mod module {
|
||||
use crate::search;
|
||||
use crate::users;
|
||||
use rocket::{
|
||||
request::{self, FromRequest, Request},
|
||||
request::{self, FlashMessage, FromRequest, Request},
|
||||
Outcome, State,
|
||||
};
|
||||
use scheduled_thread_pool::ScheduledThreadPool;
|
||||
@@ -19,6 +19,7 @@ mod module {
|
||||
pub user: Option<users::User>,
|
||||
pub searcher: Arc<search::Searcher>,
|
||||
pub worker: Arc<ScheduledThreadPool>,
|
||||
pub flash_msg: Option<(String, String)>,
|
||||
}
|
||||
|
||||
impl<'a, 'r> FromRequest<'a, 'r> for PlumeRocket {
|
||||
@@ -30,10 +31,12 @@ mod module {
|
||||
let user = request.guard::<users::User>().succeeded();
|
||||
let worker = request.guard::<State<Arc<ScheduledThreadPool>>>()?;
|
||||
let searcher = request.guard::<State<Arc<search::Searcher>>>()?;
|
||||
let flash_msg = request.guard::<FlashMessage>().succeeded();
|
||||
Outcome::Success(PlumeRocket {
|
||||
conn,
|
||||
intl,
|
||||
user,
|
||||
flash_msg: flash_msg.map(|f| (f.name().into(), f.msg().into())),
|
||||
worker: worker.clone(),
|
||||
searcher: searcher.clone(),
|
||||
})
|
||||
|
||||
+28
-287
@@ -4,7 +4,6 @@ use activitypub::{
|
||||
object::{Article, Image, Tombstone},
|
||||
CustomObject,
|
||||
};
|
||||
use canapi::{Error as ApiError, Provider};
|
||||
use chrono::{NaiveDateTime, TimeZone, Utc};
|
||||
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||
use heck::{CamelCase, KebabCase};
|
||||
@@ -15,10 +14,8 @@ use blogs::Blog;
|
||||
use instance::Instance;
|
||||
use medias::Media;
|
||||
use mentions::Mention;
|
||||
use plume_api::posts::PostEndpoint;
|
||||
use plume_common::{
|
||||
activity_pub::{
|
||||
broadcast,
|
||||
inbox::{AsObject, FromId},
|
||||
Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILITY,
|
||||
},
|
||||
@@ -30,7 +27,7 @@ use schema::posts;
|
||||
use search::Searcher;
|
||||
use tags::*;
|
||||
use users::User;
|
||||
use {ap_url, ApiResult, Connection, Error, PlumeRocket, Result, CONFIG};
|
||||
use {ap_url, Connection, Error, PlumeRocket, Result, CONFIG};
|
||||
|
||||
pub type LicensedArticle = CustomObject<Licensed, Article>;
|
||||
|
||||
@@ -67,282 +64,6 @@ pub struct NewPost {
|
||||
pub cover_id: Option<i32>,
|
||||
}
|
||||
|
||||
impl Provider<PlumeRocket> for Post {
|
||||
type Data = PostEndpoint;
|
||||
|
||||
fn get(rockets: &PlumeRocket, id: i32) -> ApiResult<PostEndpoint> {
|
||||
let conn = &*rockets.conn;
|
||||
if let Ok(post) = Post::get(conn, id) {
|
||||
if !post.published
|
||||
&& !rockets
|
||||
.user
|
||||
.as_ref()
|
||||
.and_then(|u| post.is_author(conn, u.id).ok())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err(ApiError::Authorization(
|
||||
"You are not authorized to access this post yet.".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(PostEndpoint {
|
||||
id: Some(post.id),
|
||||
title: Some(post.title.clone()),
|
||||
subtitle: Some(post.subtitle.clone()),
|
||||
content: Some(post.content.get().clone()),
|
||||
source: Some(post.source.clone()),
|
||||
author: Some(
|
||||
post.get_authors(conn)
|
||||
.map_err(|_| ApiError::NotFound("Authors not found".into()))?[0]
|
||||
.username
|
||||
.clone(),
|
||||
),
|
||||
blog_id: Some(post.blog_id),
|
||||
published: Some(post.published),
|
||||
creation_date: Some(post.creation_date.format("%Y-%m-%d").to_string()),
|
||||
license: Some(post.license.clone()),
|
||||
tags: Some(
|
||||
Tag::for_post(conn, post.id)
|
||||
.map_err(|_| ApiError::NotFound("Tags not found".into()))?
|
||||
.into_iter()
|
||||
.map(|t| t.tag)
|
||||
.collect(),
|
||||
),
|
||||
cover_id: post.cover_id,
|
||||
})
|
||||
} else {
|
||||
Err(ApiError::NotFound("Request post was not found".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn list(rockets: &PlumeRocket, filter: PostEndpoint) -> Vec<PostEndpoint> {
|
||||
let conn = &*rockets.conn;
|
||||
let mut query = posts::table.into_boxed();
|
||||
if let Some(title) = filter.title {
|
||||
query = query.filter(posts::title.eq(title));
|
||||
}
|
||||
if let Some(subtitle) = filter.subtitle {
|
||||
query = query.filter(posts::subtitle.eq(subtitle));
|
||||
}
|
||||
if let Some(content) = filter.content {
|
||||
query = query.filter(posts::content.eq(content));
|
||||
}
|
||||
|
||||
query
|
||||
.get_results::<Post>(conn)
|
||||
.map(|ps| {
|
||||
ps.into_iter()
|
||||
.filter(|p| {
|
||||
p.published
|
||||
|| rockets
|
||||
.user
|
||||
.as_ref()
|
||||
.and_then(|u| p.is_author(conn, u.id).ok())
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.map(|p| PostEndpoint {
|
||||
id: Some(p.id),
|
||||
title: Some(p.title.clone()),
|
||||
subtitle: Some(p.subtitle.clone()),
|
||||
content: Some(p.content.get().clone()),
|
||||
source: Some(p.source.clone()),
|
||||
author: Some(p.get_authors(conn).unwrap_or_default()[0].username.clone()),
|
||||
blog_id: Some(p.blog_id),
|
||||
published: Some(p.published),
|
||||
creation_date: Some(p.creation_date.format("%Y-%m-%d").to_string()),
|
||||
license: Some(p.license.clone()),
|
||||
tags: Some(
|
||||
Tag::for_post(conn, p.id)
|
||||
.unwrap_or_else(|_| vec![])
|
||||
.into_iter()
|
||||
.map(|t| t.tag)
|
||||
.collect(),
|
||||
),
|
||||
cover_id: p.cover_id,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|_| vec![])
|
||||
}
|
||||
|
||||
fn update(
|
||||
_rockets: &PlumeRocket,
|
||||
_id: i32,
|
||||
_new_data: PostEndpoint,
|
||||
) -> ApiResult<PostEndpoint> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn delete(rockets: &PlumeRocket, id: i32) {
|
||||
let conn = &*rockets.conn;
|
||||
let user_id = rockets
|
||||
.user
|
||||
.as_ref()
|
||||
.expect("Post as Provider::delete: not authenticated")
|
||||
.id;
|
||||
if let Ok(post) = Post::get(conn, id) {
|
||||
if post.is_author(conn, user_id).unwrap_or(false) {
|
||||
post.delete(conn, &rockets.searcher)
|
||||
.expect("Post as Provider::delete: delete error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create(rockets: &PlumeRocket, query: PostEndpoint) -> ApiResult<PostEndpoint> {
|
||||
let conn = &*rockets.conn;
|
||||
let search = &rockets.searcher;
|
||||
let worker = &rockets.worker;
|
||||
if rockets.user.is_none() {
|
||||
return Err(ApiError::Authorization(
|
||||
"You are not authorized to create new articles.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let title = query.title.clone().expect("No title for new post in API");
|
||||
let slug = query.title.unwrap().to_kebab_case();
|
||||
|
||||
let date = query.creation_date.clone().and_then(|d| {
|
||||
NaiveDateTime::parse_from_str(format!("{} 00:00:00", d).as_ref(), "%Y-%m-%d %H:%M:%S")
|
||||
.ok()
|
||||
});
|
||||
|
||||
let domain = &Instance::get_local(&conn)
|
||||
.map_err(|_| ApiError::NotFound("posts::update: Error getting local instance".into()))?
|
||||
.public_domain;
|
||||
let author = rockets
|
||||
.user
|
||||
.clone()
|
||||
.ok_or_else(|| ApiError::NotFound("Author not found".into()))?;
|
||||
|
||||
let (content, mentions, hashtags) = md_to_html(
|
||||
query.source.clone().unwrap_or_default().clone().as_ref(),
|
||||
domain,
|
||||
false,
|
||||
Some(Media::get_media_processor(conn, vec![&author])),
|
||||
);
|
||||
|
||||
let blog = match query.blog_id {
|
||||
Some(x) => x,
|
||||
None => {
|
||||
Blog::find_for_author(conn, &author)
|
||||
.map_err(|_| ApiError::NotFound("No default blog".into()))?[0]
|
||||
.id
|
||||
}
|
||||
};
|
||||
|
||||
if Post::find_by_slug(conn, &slug, blog).is_ok() {
|
||||
// Not an actual authorization problem, but we have nothing better for now…
|
||||
// TODO: add another error variant to canapi and add it there
|
||||
return Err(ApiError::Authorization(
|
||||
"A post with the same slug already exists".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let post = Post::insert(
|
||||
conn,
|
||||
NewPost {
|
||||
blog_id: blog,
|
||||
slug,
|
||||
title,
|
||||
content: SafeString::new(content.as_ref()),
|
||||
published: query.published.unwrap_or(true),
|
||||
license: query.license.unwrap_or_else(|| {
|
||||
Instance::get_local(conn)
|
||||
.map(|i| i.default_license)
|
||||
.unwrap_or_else(|_| String::from("CC-BY-SA"))
|
||||
}),
|
||||
creation_date: date,
|
||||
ap_url: String::new(),
|
||||
subtitle: query.subtitle.unwrap_or_default(),
|
||||
source: query.source.expect("Post API::create: no source error"),
|
||||
cover_id: query.cover_id,
|
||||
},
|
||||
search,
|
||||
)
|
||||
.map_err(|_| ApiError::NotFound("Creation error".into()))?;
|
||||
|
||||
PostAuthor::insert(
|
||||
conn,
|
||||
NewPostAuthor {
|
||||
author_id: author.id,
|
||||
post_id: post.id,
|
||||
},
|
||||
)
|
||||
.map_err(|_| ApiError::NotFound("Error saving authors".into()))?;
|
||||
|
||||
if let Some(tags) = query.tags {
|
||||
for tag in tags {
|
||||
Tag::insert(
|
||||
conn,
|
||||
NewTag {
|
||||
tag,
|
||||
is_hashtag: false,
|
||||
post_id: post.id,
|
||||
},
|
||||
)
|
||||
.map_err(|_| ApiError::NotFound("Error saving tags".into()))?;
|
||||
}
|
||||
}
|
||||
for hashtag in hashtags {
|
||||
Tag::insert(
|
||||
conn,
|
||||
NewTag {
|
||||
tag: hashtag.to_camel_case(),
|
||||
is_hashtag: true,
|
||||
post_id: post.id,
|
||||
},
|
||||
)
|
||||
.map_err(|_| ApiError::NotFound("Error saving hashtags".into()))?;
|
||||
}
|
||||
|
||||
if post.published {
|
||||
for m in mentions.into_iter() {
|
||||
Mention::from_activity(
|
||||
&*conn,
|
||||
&Mention::build_activity(&rockets, &m)
|
||||
.map_err(|_| ApiError::NotFound("Couldn't build mentions".into()))?,
|
||||
post.id,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.map_err(|_| ApiError::NotFound("Error saving mentions".into()))?;
|
||||
}
|
||||
|
||||
let act = post
|
||||
.create_activity(&*conn)
|
||||
.map_err(|_| ApiError::NotFound("Couldn't create activity".into()))?;
|
||||
let dest = User::one_by_instance(&*conn)
|
||||
.map_err(|_| ApiError::NotFound("Couldn't list remote instances".into()))?;
|
||||
worker.execute(move || broadcast(&author, act, dest));
|
||||
}
|
||||
|
||||
Ok(PostEndpoint {
|
||||
id: Some(post.id),
|
||||
title: Some(post.title.clone()),
|
||||
subtitle: Some(post.subtitle.clone()),
|
||||
content: Some(post.content.get().clone()),
|
||||
source: Some(post.source.clone()),
|
||||
author: Some(
|
||||
post.get_authors(conn)
|
||||
.map_err(|_| ApiError::NotFound("No authors".into()))?[0]
|
||||
.username
|
||||
.clone(),
|
||||
),
|
||||
blog_id: Some(post.blog_id),
|
||||
published: Some(post.published),
|
||||
creation_date: Some(post.creation_date.format("%Y-%m-%d").to_string()),
|
||||
license: Some(post.license.clone()),
|
||||
tags: Some(
|
||||
Tag::for_post(conn, post.id)
|
||||
.map_err(|_| ApiError::NotFound("Tags not found".into()))?
|
||||
.into_iter()
|
||||
.map(|t| t.tag)
|
||||
.collect(),
|
||||
),
|
||||
cover_id: post.cover_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Post {
|
||||
get!(posts);
|
||||
find_by!(posts, find_by_slug, slug as &str, blog_id as i32);
|
||||
@@ -420,7 +141,7 @@ impl Post {
|
||||
use schema::post_authors;
|
||||
use schema::users;
|
||||
let local_authors = users::table
|
||||
.filter(users::instance_id.eq(Instance::get_local(conn)?.id))
|
||||
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
||||
.select(users::id);
|
||||
let local_posts_id = post_authors::table
|
||||
.filter(post_authors::author_id.eq_any(local_authors))
|
||||
@@ -441,6 +162,26 @@ impl Post {
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn list_filtered(
|
||||
conn: &Connection,
|
||||
title: Option<String>,
|
||||
subtitle: Option<String>,
|
||||
content: Option<String>,
|
||||
) -> Result<Vec<Post>> {
|
||||
let mut query = posts::table.into_boxed();
|
||||
if let Some(title) = title {
|
||||
query = query.filter(posts::title.eq(title));
|
||||
}
|
||||
if let Some(subtitle) = subtitle {
|
||||
query = query.filter(posts::subtitle.eq(subtitle));
|
||||
}
|
||||
if let Some(content) = content {
|
||||
query = query.filter(posts::content.eq(content));
|
||||
}
|
||||
|
||||
query.get_results::<Post>(conn).map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn get_recents(conn: &Connection, limit: i64) -> Result<Vec<Post>> {
|
||||
posts::table
|
||||
.order(posts::creation_date.desc())
|
||||
@@ -643,7 +384,7 @@ impl Post {
|
||||
.collect::<Vec<serde_json::Value>>();
|
||||
let mut tags_json = Tag::for_post(conn, self.id)?
|
||||
.into_iter()
|
||||
.map(|t| json!(t.to_activity(conn).ok()))
|
||||
.map(|t| json!(t.to_activity().ok()))
|
||||
.collect::<Vec<serde_json::Value>>();
|
||||
mentions_json.append(&mut tags_json);
|
||||
|
||||
@@ -678,7 +419,7 @@ impl Post {
|
||||
if let Some(media_id) = self.cover_id {
|
||||
let media = Media::get(conn, media_id)?;
|
||||
let mut cover = Image::default();
|
||||
cover.object_props.set_url_string(media.url(conn)?)?;
|
||||
cover.object_props.set_url_string(media.url()?)?;
|
||||
if media.sensitive {
|
||||
cover
|
||||
.object_props
|
||||
@@ -862,7 +603,7 @@ impl Post {
|
||||
pub fn cover_url(&self, conn: &Connection) -> Option<String> {
|
||||
self.cover_id
|
||||
.and_then(|i| Media::get(conn, i).ok())
|
||||
.and_then(|c| c.url(conn).ok())
|
||||
.and_then(|c| c.url().ok())
|
||||
}
|
||||
|
||||
pub fn build_delete(&self, conn: &Connection) -> Result<Delete> {
|
||||
@@ -901,7 +642,7 @@ impl FromId<PlumeRocket> for Post {
|
||||
.attributed_to_link_vec::<Id>()?
|
||||
.into_iter()
|
||||
.fold((None, vec![]), |(blog, mut authors), link| {
|
||||
let url: String = link.into();
|
||||
let url = link;
|
||||
match User::from_id(&c, &url, None) {
|
||||
Ok(u) => {
|
||||
authors.push(u);
|
||||
@@ -951,7 +692,7 @@ impl FromId<PlumeRocket> for Post {
|
||||
}
|
||||
|
||||
// save mentions and tags
|
||||
let mut hashtags = md_to_html(&post.source, "", false, None)
|
||||
let mut hashtags = md_to_html(&post.source, None, false, None)
|
||||
.2
|
||||
.into_iter()
|
||||
.map(|s| s.to_camel_case())
|
||||
@@ -1088,7 +829,7 @@ impl AsObject<User, Update, &PlumeRocket> for PostUpdate {
|
||||
post.license = license;
|
||||
}
|
||||
|
||||
let mut txt_hashtags = md_to_html(&post.source, "", false, None)
|
||||
let mut txt_hashtags = md_to_html(&post.source, None, false, None)
|
||||
.2
|
||||
.into_iter()
|
||||
.map(|s| s.to_camel_case())
|
||||
|
||||
@@ -4,7 +4,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
|
||||
use notifications::*;
|
||||
use plume_common::activity_pub::{
|
||||
inbox::{AsObject, FromId},
|
||||
inbox::{AsActor, AsObject, FromId},
|
||||
Id, IntoId, PUBLIC_VISIBILITY,
|
||||
};
|
||||
use posts::Post;
|
||||
@@ -79,14 +79,16 @@ impl Reshare {
|
||||
pub fn notify(&self, conn: &Connection) -> Result<()> {
|
||||
let post = self.get_post(conn)?;
|
||||
for author in post.get_authors(conn)? {
|
||||
Notification::insert(
|
||||
conn,
|
||||
NewNotification {
|
||||
kind: notification_kind::RESHARE.to_string(),
|
||||
object_id: self.id,
|
||||
user_id: author.id,
|
||||
},
|
||||
)?;
|
||||
if author.is_local() {
|
||||
Notification::insert(
|
||||
conn,
|
||||
NewNotification {
|
||||
kind: notification_kind::RESHARE.to_string(),
|
||||
object_id: self.id,
|
||||
user_id: author.id,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -138,26 +140,12 @@ impl FromId<PlumeRocket> for Reshare {
|
||||
let res = Reshare::insert(
|
||||
&c.conn,
|
||||
NewReshare {
|
||||
post_id: Post::from_id(
|
||||
c,
|
||||
&{
|
||||
let res: String = act.announce_props.object_link::<Id>()?.into();
|
||||
res
|
||||
},
|
||||
None,
|
||||
)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
user_id: User::from_id(
|
||||
c,
|
||||
&{
|
||||
let res: String = act.announce_props.actor_link::<Id>()?.into();
|
||||
res
|
||||
},
|
||||
None,
|
||||
)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
post_id: Post::from_id(c, &act.announce_props.object_link::<Id>()?, None)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
user_id: User::from_id(c, &act.announce_props.actor_link::<Id>()?, None)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
ap_url: act.object_props.id_string()?,
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use instance::Instance;
|
||||
use posts::Post;
|
||||
use schema::posts;
|
||||
use tags::Tag;
|
||||
use Connection;
|
||||
|
||||
use chrono::Datelike;
|
||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use itertools::Itertools;
|
||||
use std::{cmp, fs::create_dir_all, path::Path, sync::Mutex};
|
||||
use tantivy::{
|
||||
@@ -222,6 +224,16 @@ impl Searcher {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn fill(&self, conn: &Connection) -> Result<()> {
|
||||
for post in posts::table
|
||||
.filter(posts::published.eq(true))
|
||||
.load::<Post>(conn)?
|
||||
{
|
||||
self.update_document(conn, &post)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn commit(&self) {
|
||||
let mut writer = self.writer.lock().unwrap();
|
||||
writer.as_mut().unwrap().commit().unwrap();
|
||||
|
||||
@@ -27,11 +27,11 @@ impl Tag {
|
||||
find_by!(tags, find_by_name, tag as &str);
|
||||
list_by!(tags, for_post, post_id as i32);
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<Hashtag> {
|
||||
pub fn to_activity(&self) -> Result<Hashtag> {
|
||||
let mut ht = Hashtag::default();
|
||||
ht.set_href_string(ap_url(&format!(
|
||||
"{}/tag/{}",
|
||||
Instance::get_local(conn)?.public_domain,
|
||||
Instance::get_local()?.public_domain,
|
||||
self.tag
|
||||
)))?;
|
||||
ht.set_name_string(self.tag.clone())?;
|
||||
@@ -54,11 +54,11 @@ impl Tag {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn build_activity(conn: &Connection, tag: String) -> Result<Hashtag> {
|
||||
pub fn build_activity(tag: String) -> Result<Hashtag> {
|
||||
let mut ht = Hashtag::default();
|
||||
ht.set_href_string(ap_url(&format!(
|
||||
"{}/tag/{}",
|
||||
Instance::get_local(conn)?.public_domain,
|
||||
Instance::get_local()?.public_domain,
|
||||
tag
|
||||
)))?;
|
||||
ht.set_name_string(tag)?;
|
||||
|
||||
+66
-18
@@ -1,5 +1,9 @@
|
||||
use activitypub::{
|
||||
actor::Person, collection::OrderedCollection, object::Image, Activity, CustomObject, Endpoint,
|
||||
activity::Delete,
|
||||
actor::Person,
|
||||
collection::OrderedCollection,
|
||||
object::{Image, Tombstone},
|
||||
Activity, CustomObject, Endpoint,
|
||||
};
|
||||
use bcrypt;
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
@@ -12,14 +16,14 @@ use openssl::{
|
||||
};
|
||||
use plume_common::activity_pub::{
|
||||
ap_accept_header,
|
||||
inbox::{AsActor, FromId},
|
||||
inbox::{AsActor, AsObject, FromId},
|
||||
sign::{gen_keypair, Signer},
|
||||
ActivityStream, ApSignature, Id, IntoId, PublicKey,
|
||||
ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY,
|
||||
};
|
||||
use plume_common::utils;
|
||||
use reqwest::{
|
||||
header::{HeaderValue, ACCEPT},
|
||||
Client,
|
||||
ClientBuilder,
|
||||
};
|
||||
use rocket::{
|
||||
outcome::IntoOutcome,
|
||||
@@ -211,7 +215,7 @@ impl User {
|
||||
users::email.eq(email),
|
||||
users::summary_html.eq(utils::md_to_html(
|
||||
&summary,
|
||||
"",
|
||||
None,
|
||||
false,
|
||||
Some(Media::get_media_processor(conn, vec![self])),
|
||||
)
|
||||
@@ -224,7 +228,7 @@ impl User {
|
||||
|
||||
pub fn count_local(conn: &Connection) -> Result<i64> {
|
||||
users::table
|
||||
.filter(users::instance_id.eq(Instance::get_local(conn)?.id))
|
||||
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
||||
.count()
|
||||
.get_result(conn)
|
||||
.map_err(Error::from)
|
||||
@@ -263,7 +267,9 @@ impl User {
|
||||
}
|
||||
|
||||
fn fetch(url: &str) -> Result<CustomPerson> {
|
||||
let mut res = Client::new()
|
||||
let mut res = ClientBuilder::new()
|
||||
.connect_timeout(Some(std::time::Duration::from_secs(5)))
|
||||
.build()?
|
||||
.get(url)
|
||||
.header(
|
||||
ACCEPT,
|
||||
@@ -347,7 +353,7 @@ impl User {
|
||||
|
||||
pub fn get_local_page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<User>> {
|
||||
users::table
|
||||
.filter(users::instance_id.eq(Instance::get_local(conn)?.id))
|
||||
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
||||
.order(users::username.asc())
|
||||
.offset(min.into())
|
||||
.limit((max - min).into())
|
||||
@@ -365,7 +371,9 @@ impl User {
|
||||
}
|
||||
|
||||
pub fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> {
|
||||
let mut res = Client::new()
|
||||
let mut res = ClientBuilder::new()
|
||||
.connect_timeout(Some(std::time::Duration::from_secs(5)))
|
||||
.build()?
|
||||
.get(&self.outbox_url[..])
|
||||
.header(
|
||||
ACCEPT,
|
||||
@@ -388,7 +396,9 @@ impl User {
|
||||
}
|
||||
|
||||
pub fn fetch_followers_ids(&self) -> Result<Vec<String>> {
|
||||
let mut res = Client::new()
|
||||
let mut res = ClientBuilder::new()
|
||||
.connect_timeout(Some(std::time::Duration::from_secs(5)))
|
||||
.build()?
|
||||
.get(&self.followers_endpoint[..])
|
||||
.header(
|
||||
ACCEPT,
|
||||
@@ -624,7 +634,7 @@ impl User {
|
||||
let mut avatar = Image::default();
|
||||
avatar.object_props.set_url_string(
|
||||
self.avatar_id
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url(conn)).ok())
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
|
||||
.unwrap_or_default(),
|
||||
)?;
|
||||
actor.object_props.set_icon_object(avatar)?;
|
||||
@@ -632,9 +642,32 @@ impl User {
|
||||
Ok(CustomPerson::new(actor, ap_signature))
|
||||
}
|
||||
|
||||
pub fn delete_activity(&self, conn: &Connection) -> Result<Delete> {
|
||||
let mut del = Delete::default();
|
||||
|
||||
let mut tombstone = Tombstone::default();
|
||||
tombstone.object_props.set_id_string(self.ap_url.clone())?;
|
||||
|
||||
del.delete_props
|
||||
.set_actor_link(Id::new(self.ap_url.clone()))?;
|
||||
del.delete_props.set_object_object(tombstone)?;
|
||||
del.object_props
|
||||
.set_id_string(format!("{}#delete", self.ap_url))?;
|
||||
del.object_props
|
||||
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?;
|
||||
del.object_props.set_cc_link_vec(
|
||||
self.get_followers(conn)?
|
||||
.into_iter()
|
||||
.map(|f| Id::new(f.ap_url))
|
||||
.collect(),
|
||||
)?;
|
||||
|
||||
Ok(del)
|
||||
}
|
||||
|
||||
pub fn avatar_url(&self, conn: &Connection) -> String {
|
||||
self.avatar_id
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url(conn)).ok())
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
|
||||
.unwrap_or_else(|| "/static/default-avatar.png".to_string())
|
||||
}
|
||||
|
||||
@@ -828,7 +861,22 @@ impl AsActor<&PlumeRocket> for User {
|
||||
}
|
||||
|
||||
fn is_local(&self) -> bool {
|
||||
self.instance_id == 1
|
||||
Instance::get_local()
|
||||
.map(|i| self.instance_id == i.id)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, Delete, &PlumeRocket> for User {
|
||||
type Error = Error;
|
||||
type Output = ();
|
||||
|
||||
fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
|
||||
if self.id == actor.id {
|
||||
self.delete(&c.conn, &c.searcher).map(|_| ())
|
||||
} else {
|
||||
Err(Error::Unauthorized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -885,10 +933,10 @@ impl NewUser {
|
||||
display_name,
|
||||
is_admin,
|
||||
summary: summary.to_owned(),
|
||||
summary_html: SafeString::new(&utils::md_to_html(&summary, "", false, None).0),
|
||||
summary_html: SafeString::new(&utils::md_to_html(&summary, None, false, None).0),
|
||||
email: Some(email),
|
||||
hashed_password: Some(password),
|
||||
instance_id: Instance::get_local(conn)?.id,
|
||||
instance_id: Instance::get_local()?.id,
|
||||
ap_url: String::new(),
|
||||
public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?,
|
||||
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
|
||||
@@ -961,7 +1009,7 @@ pub(crate) mod tests {
|
||||
|
||||
assert_eq!(
|
||||
test_user.id,
|
||||
User::find_by_name(conn, "test", Instance::get_local(conn).unwrap().id)
|
||||
User::find_by_name(conn, "test", Instance::get_local().unwrap().id)
|
||||
.unwrap()
|
||||
.id
|
||||
);
|
||||
@@ -979,7 +1027,7 @@ pub(crate) mod tests {
|
||||
conn,
|
||||
&format!(
|
||||
"https://{}/@/{}/",
|
||||
Instance::get_local(conn).unwrap().public_domain,
|
||||
Instance::get_local().unwrap().public_domain,
|
||||
"test"
|
||||
)
|
||||
)
|
||||
@@ -1010,7 +1058,7 @@ pub(crate) mod tests {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let inserted = fill_database(conn);
|
||||
let local_inst = Instance::get_local(conn).unwrap();
|
||||
let local_inst = Instance::get_local().unwrap();
|
||||
let mut i = 0;
|
||||
while local_inst.has_admin(conn).unwrap() {
|
||||
assert!(i < 100); //prevent from looping indefinitelly
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
extern crate diesel;
|
||||
#[macro_use]
|
||||
extern crate diesel_migrations;
|
||||
|
||||
extern crate plume_common;
|
||||
extern crate plume_models;
|
||||
|
||||
use diesel::Connection;
|
||||
use plume_common::utils::random_hex;
|
||||
use plume_models::migrations::IMPORTED_MIGRATIONS;
|
||||
use plume_models::{Connection as Conn, CONFIG};
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
embed_migrations!("../migrations/sqlite");
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
embed_migrations!("../migrations/postgres");
|
||||
use std::env::temp_dir;
|
||||
|
||||
fn db() -> Conn {
|
||||
let conn =
|
||||
Conn::establish(CONFIG.database_url.as_str()).expect("Couldn't connect to the database");
|
||||
embedded_migrations::run(&conn).expect("Couldn't run migrations");
|
||||
let dir = temp_dir().join(format!("plume-test-{}", random_hex()));
|
||||
IMPORTED_MIGRATIONS
|
||||
.run_pending_migrations(&conn, &dir)
|
||||
.expect("Couldn't run migrations");
|
||||
conn
|
||||
}
|
||||
|
||||
|
||||
+599
-445
File diff suppressed because it is too large
Load Diff
+557
-411
File diff suppressed because it is too large
Load Diff
+575
-426
File diff suppressed because it is too large
Load Diff
+597
-441
File diff suppressed because it is too large
Load Diff
+600
-446
File diff suppressed because it is too large
Load Diff
+526
-384
File diff suppressed because it is too large
Load Diff
+536
-389
File diff suppressed because it is too large
Load Diff
+595
-441
File diff suppressed because it is too large
Load Diff
+619
-465
File diff suppressed because it is too large
Load Diff
+612
-458
File diff suppressed because it is too large
Load Diff
+573
-422
File diff suppressed because it is too large
Load Diff
+565
-419
File diff suppressed because it is too large
Load Diff
+598
-444
File diff suppressed because it is too large
Load Diff
+609
-455
File diff suppressed because it is too large
Load Diff
+608
-471
File diff suppressed because it is too large
Load Diff
+595
-440
File diff suppressed because it is too large
Load Diff
+545
-413
File diff suppressed because it is too large
Load Diff
+590
-439
File diff suppressed because it is too large
Load Diff
+567
-419
File diff suppressed because it is too large
Load Diff
+563
-417
File diff suppressed because it is too large
Load Diff
+598
-442
File diff suppressed because it is too large
Load Diff
+537
-395
File diff suppressed because it is too large
Load Diff
+530
-384
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
mkdir bin
|
||||
cp target/release/{plume,plm} bin
|
||||
cp "$(which diesel)" bin
|
||||
strip -s bin/*
|
||||
tar -cvzf plume.tar.gz bin/ static/ migrations/$FEATURES
|
||||
tar -cvzf plume.tar.gz bin/ static/
|
||||
tar -cvzf wasm.tar.gz static/plume-front.{js,wasm}
|
||||
|
||||
@@ -3,17 +3,12 @@ set -eo pipefail
|
||||
|
||||
export ROCKET_SECRET_KEY="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||
|
||||
mkdir -p "target/cov/plume"
|
||||
mkdir -p "target/cov/plm"
|
||||
plm='kcov --exclude-pattern=/.cargo,/usr/lib --verify target/cov/plm plm'
|
||||
plm migration run
|
||||
plm migration redo
|
||||
plm instance new -d plume-test.local -n plume-test
|
||||
plm users new -n admin -N 'Admin' -e 'email@exemple.com' -p 'password'
|
||||
|
||||
diesel migration run
|
||||
diesel migration redo
|
||||
$plm instance new -d plume-test.local -n plume-test
|
||||
$plm users new -n admin -N 'Admin' -e 'email@exemple.com' -p 'password'
|
||||
$plm search init
|
||||
|
||||
kcov --exclude-pattern=/.cargo,/usr/lib --verify target/cov/plume plume &
|
||||
plume &
|
||||
caddy -conf /Caddyfile &
|
||||
|
||||
until curl http://localhost:7878/test/health -f; do sleep 1; done 2>/dev/null >/dev/null
|
||||
@@ -23,4 +18,4 @@ python3 -m unittest *.py
|
||||
|
||||
kill -SIGINT %1
|
||||
kill -SIGKILL %2
|
||||
wait
|
||||
sleep 15
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
for file in target/debug/*-*[^\.d]; do
|
||||
if [[ -x "$file" ]]
|
||||
then
|
||||
filename=$(basename $file)
|
||||
mkdir -p "target/cov/$filename"
|
||||
kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$filename" "$file"
|
||||
rm $file
|
||||
fi
|
||||
done
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
bash <(curl -s https://codecov.io/bash) -F $1
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
pr_id=$(basename "$CI_PULL_REQUEST")
|
||||
[ -z "$pr_id" ] && exit
|
||||
backend="$FEATURES"
|
||||
password="$JOINPLUME_PASSWORD"
|
||||
|
||||
curl -T plume.tar.gz "https://circleci:$password@joinplu.me/upload_pr/$backend/$pr_id.tar.gz"
|
||||
+19
-7
@@ -1,12 +1,24 @@
|
||||
use canapi::Provider;
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json;
|
||||
|
||||
use plume_api::apps::AppEndpoint;
|
||||
use plume_models::{apps::App, db_conn::DbConn, Connection};
|
||||
use crate::api::Api;
|
||||
use plume_api::apps::NewAppData;
|
||||
use plume_common::utils::random_hex;
|
||||
use plume_models::{apps::*, db_conn::DbConn};
|
||||
|
||||
#[post("/apps", data = "<data>")]
|
||||
pub fn create(conn: DbConn, data: Json<AppEndpoint>) -> Json<serde_json::Value> {
|
||||
let post = <App as Provider<Connection>>::create(&*conn, (*data).clone()).ok();
|
||||
Json(json!(post))
|
||||
pub fn create(conn: DbConn, data: Json<NewAppData>) -> Api<App> {
|
||||
let client_id = random_hex();
|
||||
let client_secret = random_hex();
|
||||
let app = App::insert(
|
||||
&*conn,
|
||||
NewApp {
|
||||
name: data.name.clone(),
|
||||
client_id,
|
||||
client_secret,
|
||||
redirect_uri: data.redirect_uri.clone(),
|
||||
website: data.website.clone(),
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(Json(app))
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ use serde_json;
|
||||
use plume_common::utils::random_hex;
|
||||
use plume_models::{api_tokens::*, apps::App, users::User, Error, PlumeRocket};
|
||||
|
||||
type Api<T> = Result<Json<T>, ApiError>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ApiError(Error);
|
||||
|
||||
@@ -18,6 +20,12 @@ impl From<Error> for ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::option::NoneError> for ApiError {
|
||||
fn from(err: std::option::NoneError) -> ApiError {
|
||||
ApiError(err.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> Responder<'r> for ApiError {
|
||||
fn respond_to(self, req: &Request) -> response::Result<'r> {
|
||||
match self.0 {
|
||||
|
||||
+221
-39
@@ -1,54 +1,236 @@
|
||||
use canapi::{Error as ApiError, Provider};
|
||||
use rocket::http::uri::Origin;
|
||||
use chrono::NaiveDateTime;
|
||||
use heck::{CamelCase, KebabCase};
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json;
|
||||
use serde_qs;
|
||||
|
||||
use api::authorization::*;
|
||||
use plume_api::posts::PostEndpoint;
|
||||
use plume_models::{posts::Post, users::User, PlumeRocket};
|
||||
use crate::api::{authorization::*, Api};
|
||||
use plume_api::posts::*;
|
||||
use plume_common::{activity_pub::broadcast, utils::md_to_html};
|
||||
use plume_models::{
|
||||
blogs::Blog, db_conn::DbConn, instance::Instance, medias::Media, mentions::*, post_authors::*,
|
||||
posts::*, safe_string::SafeString, tags::*, users::User, Error, PlumeRocket,
|
||||
};
|
||||
|
||||
#[get("/posts/<id>")]
|
||||
pub fn get(
|
||||
id: i32,
|
||||
auth: Option<Authorization<Read, Post>>,
|
||||
mut rockets: PlumeRocket,
|
||||
) -> Json<serde_json::Value> {
|
||||
rockets.user = auth.and_then(|a| User::get(&*rockets.conn, a.0.user_id).ok());
|
||||
let post = <Post as Provider<PlumeRocket>>::get(&rockets, id).ok();
|
||||
Json(json!(post))
|
||||
pub fn get(id: i32, auth: Option<Authorization<Read, Post>>, conn: DbConn) -> Api<PostData> {
|
||||
let user = auth.and_then(|a| User::get(&conn, a.0.user_id).ok());
|
||||
let post = Post::get(&conn, id)?;
|
||||
|
||||
if !post.published
|
||||
&& !user
|
||||
.and_then(|u| post.is_author(&conn, u.id).ok())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err(Error::Unauthorized.into());
|
||||
}
|
||||
|
||||
Ok(Json(PostData {
|
||||
authors: post
|
||||
.get_authors(&conn)?
|
||||
.into_iter()
|
||||
.map(|a| a.username)
|
||||
.collect(),
|
||||
creation_date: post.creation_date.format("%Y-%m-%d").to_string(),
|
||||
tags: Tag::for_post(&conn, post.id)?
|
||||
.into_iter()
|
||||
.map(|t| t.tag)
|
||||
.collect(),
|
||||
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
subtitle: post.subtitle,
|
||||
content: post.content.to_string(),
|
||||
source: Some(post.source),
|
||||
blog_id: post.blog_id,
|
||||
published: post.published,
|
||||
license: post.license,
|
||||
cover_id: post.cover_id,
|
||||
}))
|
||||
}
|
||||
|
||||
#[get("/posts")]
|
||||
#[get("/posts?<title>&<subtitle>&<content>")]
|
||||
pub fn list(
|
||||
uri: &Origin,
|
||||
title: Option<String>,
|
||||
subtitle: Option<String>,
|
||||
content: Option<String>,
|
||||
auth: Option<Authorization<Read, Post>>,
|
||||
mut rockets: PlumeRocket,
|
||||
) -> Json<serde_json::Value> {
|
||||
rockets.user = auth.and_then(|a| User::get(&*rockets.conn, a.0.user_id).ok());
|
||||
let query: PostEndpoint =
|
||||
serde_qs::from_str(uri.query().unwrap_or("")).expect("api::list: invalid query error");
|
||||
let post = <Post as Provider<PlumeRocket>>::list(&rockets, query);
|
||||
Json(json!(post))
|
||||
conn: DbConn,
|
||||
) -> Api<Vec<PostData>> {
|
||||
let user = auth.and_then(|a| User::get(&conn, a.0.user_id).ok());
|
||||
let user_id = user.map(|u| u.id);
|
||||
|
||||
Ok(Json(
|
||||
Post::list_filtered(&conn, title, subtitle, content)?
|
||||
.into_iter()
|
||||
.filter(|p| {
|
||||
p.published
|
||||
|| user_id
|
||||
.and_then(|u| p.is_author(&conn, u).ok())
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.filter_map(|p| {
|
||||
Some(PostData {
|
||||
authors: p
|
||||
.get_authors(&conn)
|
||||
.ok()?
|
||||
.into_iter()
|
||||
.map(|a| a.username)
|
||||
.collect(),
|
||||
creation_date: p.creation_date.format("%Y-%m-%d").to_string(),
|
||||
tags: Tag::for_post(&conn, p.id)
|
||||
.ok()?
|
||||
.into_iter()
|
||||
.map(|t| t.tag)
|
||||
.collect(),
|
||||
|
||||
id: p.id,
|
||||
title: p.title,
|
||||
subtitle: p.subtitle,
|
||||
content: p.content.to_string(),
|
||||
source: Some(p.source),
|
||||
blog_id: p.blog_id,
|
||||
published: p.published,
|
||||
license: p.license,
|
||||
cover_id: p.cover_id,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
#[post("/posts", data = "<payload>")]
|
||||
pub fn create(
|
||||
auth: Authorization<Write, Post>,
|
||||
payload: Json<PostEndpoint>,
|
||||
mut rockets: PlumeRocket,
|
||||
) -> Json<serde_json::Value> {
|
||||
rockets.user = User::get(&*rockets.conn, auth.0.user_id).ok();
|
||||
let new_post = <Post as Provider<PlumeRocket>>::create(&rockets, (*payload).clone());
|
||||
Json(new_post.map(|p| json!(p)).unwrap_or_else(|e| {
|
||||
json!({
|
||||
"error": "Invalid data, couldn't create new post",
|
||||
"details": match e {
|
||||
ApiError::Fetch(msg) => msg,
|
||||
ApiError::SerDe(msg) => msg,
|
||||
ApiError::NotFound(msg) => msg,
|
||||
ApiError::Authorization(msg) => msg,
|
||||
}
|
||||
})
|
||||
payload: Json<NewPostData>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Api<PostData> {
|
||||
let conn = &*rockets.conn;
|
||||
let search = &rockets.searcher;
|
||||
let worker = &rockets.worker;
|
||||
|
||||
let author = User::get(conn, auth.0.user_id)?;
|
||||
|
||||
let slug = &payload.title.clone().to_kebab_case();
|
||||
let date = payload.creation_date.clone().and_then(|d| {
|
||||
NaiveDateTime::parse_from_str(format!("{} 00:00:00", d).as_ref(), "%Y-%m-%d %H:%M:%S").ok()
|
||||
});
|
||||
|
||||
let domain = &Instance::get_local()?.public_domain;
|
||||
let (content, mentions, hashtags) = md_to_html(
|
||||
&payload.source,
|
||||
Some(domain),
|
||||
false,
|
||||
Some(Media::get_media_processor(conn, vec![&author])),
|
||||
);
|
||||
|
||||
let blog = payload.blog_id.or_else(|| {
|
||||
let blogs = Blog::find_for_author(conn, &author).ok()?;
|
||||
if blogs.len() == 1 {
|
||||
Some(blogs[0].id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?;
|
||||
|
||||
if Post::find_by_slug(conn, slug, blog).is_ok() {
|
||||
return Err(Error::InvalidValue.into());
|
||||
}
|
||||
|
||||
let post = Post::insert(
|
||||
conn,
|
||||
NewPost {
|
||||
blog_id: blog,
|
||||
slug: slug.to_string(),
|
||||
title: payload.title.clone(),
|
||||
content: SafeString::new(content.as_ref()),
|
||||
published: payload.published.unwrap_or(true),
|
||||
license: payload.license.clone().unwrap_or_else(|| {
|
||||
Instance::get_local()
|
||||
.map(|i| i.default_license)
|
||||
.unwrap_or_else(|_| String::from("CC-BY-SA"))
|
||||
}),
|
||||
creation_date: date,
|
||||
ap_url: String::new(),
|
||||
subtitle: payload.subtitle.clone().unwrap_or_default(),
|
||||
source: payload.source.clone(),
|
||||
cover_id: payload.cover_id,
|
||||
},
|
||||
search,
|
||||
)?;
|
||||
|
||||
PostAuthor::insert(
|
||||
conn,
|
||||
NewPostAuthor {
|
||||
author_id: author.id,
|
||||
post_id: post.id,
|
||||
},
|
||||
)?;
|
||||
|
||||
if let Some(ref tags) = payload.tags {
|
||||
for tag in tags {
|
||||
Tag::insert(
|
||||
conn,
|
||||
NewTag {
|
||||
tag: tag.to_string(),
|
||||
is_hashtag: false,
|
||||
post_id: post.id,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
for hashtag in hashtags {
|
||||
Tag::insert(
|
||||
conn,
|
||||
NewTag {
|
||||
tag: hashtag.to_camel_case(),
|
||||
is_hashtag: true,
|
||||
post_id: post.id,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
if post.published {
|
||||
for m in mentions.into_iter() {
|
||||
Mention::from_activity(
|
||||
&*conn,
|
||||
&Mention::build_activity(&rockets, &m)?,
|
||||
post.id,
|
||||
true,
|
||||
true,
|
||||
)?;
|
||||
}
|
||||
|
||||
let act = post.create_activity(&*conn)?;
|
||||
let dest = User::one_by_instance(&*conn)?;
|
||||
worker.execute(move || broadcast(&author, act, dest));
|
||||
}
|
||||
|
||||
Ok(Json(PostData {
|
||||
authors: post.get_authors(conn)?.into_iter().map(|a| a.fqn).collect(),
|
||||
creation_date: post.creation_date.format("%Y-%m-%d").to_string(),
|
||||
tags: Tag::for_post(conn, post.id)?
|
||||
.into_iter()
|
||||
.map(|t| t.tag)
|
||||
.collect(),
|
||||
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
subtitle: post.subtitle,
|
||||
content: post.content.to_string(),
|
||||
source: Some(post.source),
|
||||
blog_id: post.blog_id,
|
||||
published: post.published,
|
||||
license: post.license,
|
||||
cover_id: post.cover_id,
|
||||
}))
|
||||
}
|
||||
|
||||
#[delete("/posts/<id>")]
|
||||
pub fn delete(auth: Authorization<Write, Post>, rockets: PlumeRocket, id: i32) -> Api<()> {
|
||||
let author = User::get(&*rockets.conn, auth.0.user_id)?;
|
||||
if let Ok(post) = Post::get(&*rockets.conn, id) {
|
||||
if post.is_author(&*rockets.conn, author.id).unwrap_or(false) {
|
||||
post.delete(&*rockets.conn, &rockets.searcher)?;
|
||||
}
|
||||
}
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
+27
-7
@@ -1,10 +1,9 @@
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
#![feature(decl_macro, proc_macro_hygiene)]
|
||||
#![feature(decl_macro, proc_macro_hygiene, try_trait)]
|
||||
|
||||
extern crate activitypub;
|
||||
extern crate askama_escape;
|
||||
extern crate atom_syndication;
|
||||
extern crate canapi;
|
||||
extern crate chrono;
|
||||
extern crate colored;
|
||||
extern crate ctrlc;
|
||||
@@ -42,10 +41,11 @@ extern crate webfinger;
|
||||
use diesel::r2d2::ConnectionManager;
|
||||
use plume_models::{
|
||||
db_conn::{DbPool, PragmaForeignKey},
|
||||
instance::Instance,
|
||||
migrations::IMPORTED_MIGRATIONS,
|
||||
search::{Searcher as UnmanagedSearcher, SearcherError},
|
||||
Connection, Error, CONFIG,
|
||||
};
|
||||
use rocket::State;
|
||||
use rocket_csrf::CsrfFairingBuilder;
|
||||
use scheduled_thread_pool::ScheduledThreadPool;
|
||||
use std::process::exit;
|
||||
@@ -62,6 +62,8 @@ mod mail;
|
||||
#[macro_use]
|
||||
mod template_utils;
|
||||
mod routes;
|
||||
#[macro_use]
|
||||
extern crate shrinkwraprs;
|
||||
#[cfg(feature = "test")]
|
||||
mod test_routes;
|
||||
|
||||
@@ -69,21 +71,37 @@ include!(concat!(env!("OUT_DIR"), "/templates.rs"));
|
||||
|
||||
compile_i18n!();
|
||||
|
||||
type Searcher<'a> = State<'a, Arc<UnmanagedSearcher>>;
|
||||
|
||||
/// Initializes a database pool.
|
||||
fn init_pool() -> Option<DbPool> {
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
let manager = ConnectionManager::<Connection>::new(CONFIG.database_url.as_str());
|
||||
DbPool::builder()
|
||||
let pool = DbPool::builder()
|
||||
.connection_customizer(Box::new(PragmaForeignKey))
|
||||
.build(manager)
|
||||
.ok()
|
||||
.ok()?;
|
||||
Instance::cache_local(&pool.get().unwrap());
|
||||
Some(pool)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let dbpool = init_pool().expect("main: database pool initialization error");
|
||||
if IMPORTED_MIGRATIONS
|
||||
.is_pending(&dbpool.get().unwrap())
|
||||
.unwrap_or(true)
|
||||
{
|
||||
panic!(
|
||||
r#"
|
||||
It appear your database migration does not run the migration required
|
||||
by this version of Plume. To fix this, you can run migrations via
|
||||
this command:
|
||||
|
||||
plm migration run
|
||||
|
||||
Then try to restart Plume.
|
||||
"#
|
||||
)
|
||||
}
|
||||
let workpool = ScheduledThreadPool::with_name("worker {}", num_cpus::get());
|
||||
// we want a fast exit here, so
|
||||
#[allow(clippy::match_wild_err_arm)]
|
||||
@@ -169,6 +187,7 @@ Then try to restart Plume
|
||||
routes::instance::interact,
|
||||
routes::instance::nodeinfo,
|
||||
routes::instance::about,
|
||||
routes::instance::privacy,
|
||||
routes::instance::web_manifest,
|
||||
routes::likes::create,
|
||||
routes::likes::create_auth,
|
||||
@@ -237,6 +256,7 @@ Then try to restart Plume
|
||||
api::posts::get,
|
||||
api::posts::list,
|
||||
api::posts::create,
|
||||
api::posts::delete,
|
||||
],
|
||||
)
|
||||
.register(catchers![
|
||||
|
||||
+44
-40
@@ -17,21 +17,19 @@ use plume_models::{
|
||||
users::User, Connection, PlumeRocket,
|
||||
};
|
||||
use routes::{errors::ErrorPage, Page};
|
||||
use template_utils::Ructe;
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
|
||||
#[get("/~/<name>?<page>", rank = 2)]
|
||||
pub fn details(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
let page = page.unwrap_or_default();
|
||||
let conn = &*rockets.conn;
|
||||
let blog = Blog::find_by_fqn(&rockets, &name)?;
|
||||
let posts = Post::blog_page(&*conn, &blog, page.limits())?;
|
||||
let articles_count = Post::count_for_blog(&*conn, &blog)?;
|
||||
let authors = &blog.list_authors(&*conn)?;
|
||||
let user = rockets.user;
|
||||
let intl = rockets.intl;
|
||||
let posts = Post::blog_page(conn, &blog, page.limits())?;
|
||||
let articles_count = Post::count_for_blog(conn, &blog)?;
|
||||
let authors = &blog.list_authors(conn)?;
|
||||
|
||||
Ok(render!(blogs::details(
|
||||
&(&*conn, &intl.catalog, user.clone()),
|
||||
&rockets.to_context(),
|
||||
blog,
|
||||
authors,
|
||||
page.0,
|
||||
@@ -51,13 +49,9 @@ pub fn activity_details(
|
||||
}
|
||||
|
||||
#[get("/blogs/new")]
|
||||
pub fn new(rockets: PlumeRocket) -> Ructe {
|
||||
let user = rockets.user.unwrap();
|
||||
let intl = rockets.intl;
|
||||
let conn = &*rockets.conn;
|
||||
|
||||
pub fn new(rockets: PlumeRocket, _user: User) -> Ructe {
|
||||
render!(blogs::new(
|
||||
&(&*conn, &intl.catalog, Some(user)),
|
||||
&rockets.to_context(),
|
||||
&NewBlogForm::default(),
|
||||
ValidationErrors::default()
|
||||
))
|
||||
@@ -90,7 +84,10 @@ fn valid_slug(title: &str) -> Result<(), ValidationError> {
|
||||
}
|
||||
|
||||
#[post("/blogs/new", data = "<form>")]
|
||||
pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> Result<Redirect, Ructe> {
|
||||
pub fn create(
|
||||
form: LenientForm<NewBlogForm>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Flash<Redirect>, Ructe> {
|
||||
let slug = utils::make_actor_id(&form.title);
|
||||
let conn = &*rockets.conn;
|
||||
let intl = &rockets.intl.catalog;
|
||||
@@ -121,7 +118,7 @@ pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> Result<Re
|
||||
slug.clone(),
|
||||
form.title.to_string(),
|
||||
String::from(""),
|
||||
Instance::get_local(&*conn)
|
||||
Instance::get_local()
|
||||
.expect("blog::create: instance error")
|
||||
.id,
|
||||
)
|
||||
@@ -139,37 +136,40 @@ pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> Result<Re
|
||||
)
|
||||
.expect("blog::create: author error");
|
||||
|
||||
Ok(Redirect::to(uri!(details: name = slug.clone(), page = _)))
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(details: name = slug.clone(), page = _)),
|
||||
&i18n!(intl, "Your blog was successfully created!"),
|
||||
))
|
||||
} else {
|
||||
Err(render!(blogs::new(
|
||||
&(&*conn, intl, Some(user)),
|
||||
&*form,
|
||||
errors
|
||||
)))
|
||||
Err(render!(blogs::new(&rockets.to_context(), &*form, errors)))
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/~/<name>/delete")]
|
||||
pub fn delete(name: String, rockets: PlumeRocket) -> Result<Redirect, Ructe> {
|
||||
pub fn delete(name: String, rockets: PlumeRocket) -> Result<Flash<Redirect>, Ructe> {
|
||||
let conn = &*rockets.conn;
|
||||
let blog = Blog::find_by_fqn(&rockets, &name).expect("blog::delete: blog not found");
|
||||
let user = rockets.user;
|
||||
let intl = rockets.intl;
|
||||
let searcher = rockets.searcher;
|
||||
|
||||
if user
|
||||
if rockets
|
||||
.user
|
||||
.clone()
|
||||
.and_then(|u| u.is_author_in(&*conn, &blog).ok())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
blog.delete(&conn, &searcher)
|
||||
blog.delete(&conn, &rockets.searcher)
|
||||
.expect("blog::expect: deletion error");
|
||||
Ok(Redirect::to(uri!(super::instance::index)))
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(super::instance::index)),
|
||||
i18n!(rockets.intl.catalog, "Your blog was deleted."),
|
||||
))
|
||||
} else {
|
||||
// TODO actually return 403 error code
|
||||
Err(render!(errors::not_authorized(
|
||||
&(&*conn, &intl.catalog, user),
|
||||
i18n!(intl.catalog, "You are not allowed to delete this blog.")
|
||||
&rockets.to_context(),
|
||||
i18n!(
|
||||
rockets.intl.catalog,
|
||||
"You are not allowed to delete this blog."
|
||||
)
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -190,15 +190,16 @@ pub fn edit(name: String, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
if rockets
|
||||
.user
|
||||
.clone()
|
||||
.and_then(|u| u.is_author_in(&*conn, &blog).ok())
|
||||
.and_then(|u| u.is_author_in(conn, &blog).ok())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let user = rockets
|
||||
.user
|
||||
.clone()
|
||||
.expect("blogs::edit: User was None while it shouldn't");
|
||||
let medias = Media::for_user(&*conn, user.id).expect("Couldn't list media");
|
||||
let medias = Media::for_user(conn, user.id).expect("Couldn't list media");
|
||||
Ok(render!(blogs::edit(
|
||||
&(&*conn, &rockets.intl.catalog, Some(user)),
|
||||
&rockets.to_context(),
|
||||
&blog,
|
||||
medias,
|
||||
&EditForm {
|
||||
@@ -212,7 +213,7 @@ pub fn edit(name: String, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
} else {
|
||||
// TODO actually return 403 error code
|
||||
Ok(render!(errors::not_authorized(
|
||||
&(&*conn, &rockets.intl.catalog, rockets.user),
|
||||
&rockets.to_context(),
|
||||
i18n!(
|
||||
rockets.intl.catalog,
|
||||
"You are not allowed to edit this blog."
|
||||
@@ -235,7 +236,7 @@ pub fn update(
|
||||
name: String,
|
||||
form: LenientForm<EditForm>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Redirect, Ructe> {
|
||||
) -> Result<Flash<Redirect>, Ructe> {
|
||||
let conn = &*rockets.conn;
|
||||
let intl = &rockets.intl.catalog;
|
||||
let mut blog = Blog::find_by_fqn(&rockets, &name).expect("blog::update: blog not found");
|
||||
@@ -292,7 +293,7 @@ pub fn update(
|
||||
blog.summary_html = SafeString::new(
|
||||
&utils::md_to_html(
|
||||
&form.summary,
|
||||
"",
|
||||
None,
|
||||
true,
|
||||
Some(Media::get_media_processor(
|
||||
&conn,
|
||||
@@ -308,12 +309,15 @@ pub fn update(
|
||||
blog.banner_id = form.banner;
|
||||
blog.save_changes::<Blog>(&*conn)
|
||||
.expect("Couldn't save blog changes");
|
||||
Ok(Redirect::to(uri!(details: name = name, page = _)))
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(details: name = name, page = _)),
|
||||
i18n!(intl, "Your blog information have been updated."),
|
||||
))
|
||||
})
|
||||
.map_err(|err| {
|
||||
let medias = Media::for_user(&*conn, user.id).expect("Couldn't list media");
|
||||
render!(blogs::edit(
|
||||
&(&*conn, intl, Some(user)),
|
||||
&rockets.to_context(),
|
||||
&blog,
|
||||
medias,
|
||||
&*form,
|
||||
@@ -323,7 +327,7 @@ pub fn update(
|
||||
} else {
|
||||
// TODO actually return 403 error code
|
||||
Err(render!(errors::not_authorized(
|
||||
&(&*conn, &rockets.intl.catalog, rockets.user),
|
||||
&rockets.to_context(),
|
||||
i18n!(
|
||||
rockets.intl.catalog,
|
||||
"You are not allowed to edit this blog."
|
||||
@@ -344,7 +348,7 @@ pub fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>>
|
||||
let conn = &*rockets.conn;
|
||||
let feed = FeedBuilder::default()
|
||||
.title(blog.title.clone())
|
||||
.id(Instance::get_local(&*conn)
|
||||
.id(Instance::get_local()
|
||||
.ok()?
|
||||
.compute_box("~", &name, "atom.xml"))
|
||||
.entries(
|
||||
|
||||
+23
-12
@@ -1,5 +1,8 @@
|
||||
use activitypub::object::Note;
|
||||
use rocket::{request::LenientForm, response::Redirect};
|
||||
use rocket::{
|
||||
request::LenientForm,
|
||||
response::{Flash, Redirect},
|
||||
};
|
||||
use template_utils::Ructe;
|
||||
use validator::Validate;
|
||||
|
||||
@@ -14,6 +17,7 @@ use plume_models::{
|
||||
posts::Post, safe_string::SafeString, tags::Tag, users::User, Error, PlumeRocket,
|
||||
};
|
||||
use routes::errors::ErrorPage;
|
||||
use template_utils::IntoContext;
|
||||
|
||||
#[derive(Default, FromForm, Debug, Validate)]
|
||||
pub struct NewCommentForm {
|
||||
@@ -30,7 +34,7 @@ pub fn create(
|
||||
form: LenientForm<NewCommentForm>,
|
||||
user: User,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Redirect, Ructe> {
|
||||
) -> Result<Flash<Redirect>, Ructe> {
|
||||
let conn = &*rockets.conn;
|
||||
let blog = Blog::find_by_fqn(&rockets, &blog_name).expect("comments::create: blog error");
|
||||
let post = Post::find_by_slug(&*conn, &slug, blog.id).expect("comments::create: post error");
|
||||
@@ -38,9 +42,11 @@ pub fn create(
|
||||
.map(|_| {
|
||||
let (html, mentions, _hashtags) = utils::md_to_html(
|
||||
form.content.as_ref(),
|
||||
&Instance::get_local(&conn)
|
||||
.expect("comments::create: local instance error")
|
||||
.public_domain,
|
||||
Some(
|
||||
&Instance::get_local()
|
||||
.expect("comments::create: local instance error")
|
||||
.public_domain,
|
||||
),
|
||||
true,
|
||||
Some(Media::get_media_processor(&conn, vec![&user])),
|
||||
);
|
||||
@@ -58,7 +64,6 @@ pub fn create(
|
||||
},
|
||||
)
|
||||
.expect("comments::create: insert error");
|
||||
comm.notify(&*conn).expect("comments::create: notify error");
|
||||
let new_comment = comm
|
||||
.create_activity(&rockets)
|
||||
.expect("comments::create: activity error");
|
||||
@@ -76,6 +81,8 @@ pub fn create(
|
||||
.expect("comments::create: mention save error");
|
||||
}
|
||||
|
||||
comm.notify(&*conn).expect("comments::create: notify error");
|
||||
|
||||
// federate
|
||||
let dest = User::one_by_instance(&*conn).expect("comments::create: dest error");
|
||||
let user_clone = user.clone();
|
||||
@@ -83,8 +90,11 @@ pub fn create(
|
||||
.worker
|
||||
.execute(move || broadcast(&user_clone, new_comment, dest));
|
||||
|
||||
Redirect::to(
|
||||
uri!(super::posts::details: blog = blog_name, slug = slug, responding_to = _),
|
||||
Flash::success(
|
||||
Redirect::to(
|
||||
uri!(super::posts::details: blog = blog_name, slug = slug, responding_to = _),
|
||||
),
|
||||
i18n!(&rockets.intl.catalog, "Your comment has been posted."),
|
||||
)
|
||||
})
|
||||
.map_err(|errors| {
|
||||
@@ -97,7 +107,7 @@ pub fn create(
|
||||
.and_then(|r| Comment::get(&*conn, r).ok());
|
||||
|
||||
render!(posts::details(
|
||||
&(&*conn, &rockets.intl.catalog, Some(user.clone())),
|
||||
&rockets.to_context(),
|
||||
post.clone(),
|
||||
blog,
|
||||
&*form,
|
||||
@@ -134,7 +144,7 @@ pub fn delete(
|
||||
id: i32,
|
||||
user: User,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Redirect, ErrorPage> {
|
||||
) -> Result<Flash<Redirect>, ErrorPage> {
|
||||
if let Ok(comment) = Comment::get(&*rockets.conn, id) {
|
||||
if comment.author_id == user.id {
|
||||
let dest = User::one_by_instance(&*rockets.conn)?;
|
||||
@@ -157,8 +167,9 @@ pub fn delete(
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(Redirect::to(
|
||||
uri!(super::posts::details: blog = blog, slug = slug, responding_to = _),
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _)),
|
||||
i18n!(&rockets.intl.catalog, "Your comment has been deleted."),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
+16
-57
@@ -1,12 +1,9 @@
|
||||
use plume_models::users::User;
|
||||
use plume_models::{db_conn::DbConn, Error};
|
||||
use plume_models::{Error, PlumeRocket};
|
||||
use rocket::{
|
||||
request::FromRequest,
|
||||
response::{self, Responder},
|
||||
Request,
|
||||
};
|
||||
use rocket_i18n::I18n;
|
||||
use template_utils::Ructe;
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ErrorPage(Error);
|
||||
@@ -19,78 +16,40 @@ impl From<Error> for ErrorPage {
|
||||
|
||||
impl<'r> Responder<'r> for ErrorPage {
|
||||
fn respond_to(self, req: &Request) -> response::Result<'r> {
|
||||
let conn = req.guard::<DbConn>().succeeded();
|
||||
let intl = req.guard::<I18n>().succeeded();
|
||||
let user = User::from_request(req).succeeded();
|
||||
let rockets = req.guard::<PlumeRocket>().unwrap();
|
||||
|
||||
match self.0 {
|
||||
Error::NotFound => render!(errors::not_found(&(
|
||||
&*conn.unwrap(),
|
||||
&intl.unwrap().catalog,
|
||||
user
|
||||
)))
|
||||
.respond_to(req),
|
||||
Error::Unauthorized => render!(errors::not_found(&(
|
||||
&*conn.unwrap(),
|
||||
&intl.unwrap().catalog,
|
||||
user
|
||||
)))
|
||||
.respond_to(req),
|
||||
_ => render!(errors::not_found(&(
|
||||
&*conn.unwrap(),
|
||||
&intl.unwrap().catalog,
|
||||
user
|
||||
)))
|
||||
.respond_to(req),
|
||||
Error::NotFound => render!(errors::not_found(&rockets.to_context())).respond_to(req),
|
||||
Error::Unauthorized => {
|
||||
render!(errors::not_found(&rockets.to_context())).respond_to(req)
|
||||
}
|
||||
_ => render!(errors::not_found(&rockets.to_context())).respond_to(req),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[catch(404)]
|
||||
pub fn not_found(req: &Request) -> Ructe {
|
||||
let conn = req.guard::<DbConn>().succeeded();
|
||||
let intl = req.guard::<I18n>().succeeded();
|
||||
let user = User::from_request(req).succeeded();
|
||||
render!(errors::not_found(&(
|
||||
&*conn.unwrap(),
|
||||
&intl.unwrap().catalog,
|
||||
user
|
||||
)))
|
||||
let rockets = req.guard::<PlumeRocket>().unwrap();
|
||||
render!(errors::not_found(&rockets.to_context()))
|
||||
}
|
||||
|
||||
#[catch(422)]
|
||||
pub fn unprocessable_entity(req: &Request) -> Ructe {
|
||||
let conn = req.guard::<DbConn>().succeeded();
|
||||
let intl = req.guard::<I18n>().succeeded();
|
||||
let user = User::from_request(req).succeeded();
|
||||
render!(errors::unprocessable_entity(&(
|
||||
&*conn.unwrap(),
|
||||
&intl.unwrap().catalog,
|
||||
user
|
||||
)))
|
||||
let rockets = req.guard::<PlumeRocket>().unwrap();
|
||||
render!(errors::unprocessable_entity(&rockets.to_context()))
|
||||
}
|
||||
|
||||
#[catch(500)]
|
||||
pub fn server_error(req: &Request) -> Ructe {
|
||||
let conn = req.guard::<DbConn>().succeeded();
|
||||
let intl = req.guard::<I18n>().succeeded();
|
||||
let user = User::from_request(req).succeeded();
|
||||
render!(errors::server_error(&(
|
||||
&*conn.unwrap(),
|
||||
&intl.unwrap().catalog,
|
||||
user
|
||||
)))
|
||||
let rockets = req.guard::<PlumeRocket>().unwrap();
|
||||
render!(errors::server_error(&rockets.to_context()))
|
||||
}
|
||||
|
||||
#[post("/csrf-violation?<target>")]
|
||||
pub fn csrf_violation(
|
||||
target: Option<String>,
|
||||
conn: DbConn,
|
||||
intl: I18n,
|
||||
user: Option<User>,
|
||||
) -> Ructe {
|
||||
pub fn csrf_violation(target: Option<String>, rockets: PlumeRocket) -> Ructe {
|
||||
if let Some(uri) = target {
|
||||
eprintln!("Csrf violation while acceding \"{}\"", uri)
|
||||
}
|
||||
render!(errors::csrf(&(&*conn, &intl.catalog, user)))
|
||||
render!(errors::csrf(&rockets.to_context()))
|
||||
}
|
||||
|
||||
+108
-89
@@ -1,6 +1,6 @@
|
||||
use rocket::{
|
||||
request::LenientForm,
|
||||
response::{status, Redirect},
|
||||
response::{status, Flash, Redirect},
|
||||
};
|
||||
use rocket_contrib::json::Json;
|
||||
use rocket_i18n::I18n;
|
||||
@@ -8,32 +8,32 @@ use serde_json;
|
||||
use validator::{Validate, ValidationErrors};
|
||||
|
||||
use inbox;
|
||||
use plume_common::activity_pub::inbox::FromId;
|
||||
use plume_common::activity_pub::{broadcast, inbox::FromId};
|
||||
use plume_models::{
|
||||
admin::Admin, comments::Comment, db_conn::DbConn, headers::Headers, instance::*, posts::Post,
|
||||
safe_string::SafeString, users::User, Error, PlumeRocket, CONFIG,
|
||||
};
|
||||
use routes::{errors::ErrorPage, rocket_uri_macro_static_files, Page};
|
||||
use template_utils::Ructe;
|
||||
use Searcher;
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
|
||||
#[get("/")]
|
||||
pub fn index(conn: DbConn, user: Option<User>, intl: I18n) -> Result<Ructe, ErrorPage> {
|
||||
let inst = Instance::get_local(&*conn)?;
|
||||
let federated = Post::get_recents_page(&*conn, Page::default().limits())?;
|
||||
let local = Post::get_instance_page(&*conn, inst.id, Page::default().limits())?;
|
||||
let user_feed = user.clone().and_then(|user| {
|
||||
let followed = user.get_followed(&*conn).ok()?;
|
||||
pub fn index(rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
let conn = &*rockets.conn;
|
||||
let inst = Instance::get_local()?;
|
||||
let federated = Post::get_recents_page(conn, Page::default().limits())?;
|
||||
let local = Post::get_instance_page(conn, inst.id, Page::default().limits())?;
|
||||
let user_feed = rockets.user.clone().and_then(|user| {
|
||||
let followed = user.get_followed(conn).ok()?;
|
||||
let mut in_feed = followed.into_iter().map(|u| u.id).collect::<Vec<i32>>();
|
||||
in_feed.push(user.id);
|
||||
Post::user_feed_page(&*conn, in_feed, Page::default().limits()).ok()
|
||||
Post::user_feed_page(conn, in_feed, Page::default().limits()).ok()
|
||||
});
|
||||
|
||||
Ok(render!(instance::index(
|
||||
&(&*conn, &intl.catalog, user),
|
||||
&rockets.to_context(),
|
||||
inst,
|
||||
User::count_local(&*conn)?,
|
||||
Post::count_local(&*conn)?,
|
||||
User::count_local(conn)?,
|
||||
Post::count_local(conn)?,
|
||||
local,
|
||||
federated,
|
||||
user_feed
|
||||
@@ -41,61 +41,51 @@ pub fn index(conn: DbConn, user: Option<User>, intl: I18n) -> Result<Ructe, Erro
|
||||
}
|
||||
|
||||
#[get("/local?<page>")]
|
||||
pub fn local(
|
||||
conn: DbConn,
|
||||
user: Option<User>,
|
||||
page: Option<Page>,
|
||||
intl: I18n,
|
||||
) -> Result<Ructe, ErrorPage> {
|
||||
pub fn local(page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
let page = page.unwrap_or_default();
|
||||
let instance = Instance::get_local(&*conn)?;
|
||||
let articles = Post::get_instance_page(&*conn, instance.id, page.limits())?;
|
||||
let instance = Instance::get_local()?;
|
||||
let articles = Post::get_instance_page(&*rockets.conn, instance.id, page.limits())?;
|
||||
Ok(render!(instance::local(
|
||||
&(&*conn, &intl.catalog, user),
|
||||
&rockets.to_context(),
|
||||
instance,
|
||||
articles,
|
||||
page.0,
|
||||
Page::total(Post::count_local(&*conn)? as i32)
|
||||
Page::total(Post::count_local(&*rockets.conn)? as i32)
|
||||
)))
|
||||
}
|
||||
|
||||
#[get("/feed?<page>")]
|
||||
pub fn feed(conn: DbConn, user: User, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
|
||||
pub fn feed(user: User, page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
let page = page.unwrap_or_default();
|
||||
let followed = user.get_followed(&*conn)?;
|
||||
let followed = user.get_followed(&*rockets.conn)?;
|
||||
let mut in_feed = followed.into_iter().map(|u| u.id).collect::<Vec<i32>>();
|
||||
in_feed.push(user.id);
|
||||
let articles = Post::user_feed_page(&*conn, in_feed, page.limits())?;
|
||||
let articles = Post::user_feed_page(&*rockets.conn, in_feed, page.limits())?;
|
||||
Ok(render!(instance::feed(
|
||||
&(&*conn, &intl.catalog, Some(user)),
|
||||
&rockets.to_context(),
|
||||
articles,
|
||||
page.0,
|
||||
Page::total(Post::count_local(&*conn)? as i32)
|
||||
Page::total(Post::count_local(&*rockets.conn)? as i32)
|
||||
)))
|
||||
}
|
||||
|
||||
#[get("/federated?<page>")]
|
||||
pub fn federated(
|
||||
conn: DbConn,
|
||||
user: Option<User>,
|
||||
page: Option<Page>,
|
||||
intl: I18n,
|
||||
) -> Result<Ructe, ErrorPage> {
|
||||
pub fn federated(page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
let page = page.unwrap_or_default();
|
||||
let articles = Post::get_recents_page(&*conn, page.limits())?;
|
||||
let articles = Post::get_recents_page(&*rockets.conn, page.limits())?;
|
||||
Ok(render!(instance::federated(
|
||||
&(&*conn, &intl.catalog, user),
|
||||
&rockets.to_context(),
|
||||
articles,
|
||||
page.0,
|
||||
Page::total(Post::count_local(&*conn)? as i32)
|
||||
Page::total(Post::count_local(&*rockets.conn)? as i32)
|
||||
)))
|
||||
}
|
||||
|
||||
#[get("/admin")]
|
||||
pub fn admin(conn: DbConn, admin: Admin, intl: I18n) -> Result<Ructe, ErrorPage> {
|
||||
let local_inst = Instance::get_local(&*conn)?;
|
||||
pub fn admin(_admin: Admin, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
let local_inst = Instance::get_local()?;
|
||||
Ok(render!(instance::admin(
|
||||
&(&*conn, &intl.catalog, Some(admin.0)),
|
||||
&rockets.to_context(),
|
||||
local_inst.clone(),
|
||||
InstanceSettingsForm {
|
||||
name: local_inst.name.clone(),
|
||||
@@ -121,31 +111,34 @@ pub struct InstanceSettingsForm {
|
||||
|
||||
#[post("/admin", data = "<form>")]
|
||||
pub fn update_settings(
|
||||
conn: DbConn,
|
||||
admin: Admin,
|
||||
_admin: Admin,
|
||||
form: LenientForm<InstanceSettingsForm>,
|
||||
intl: I18n,
|
||||
) -> Result<Redirect, Ructe> {
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Flash<Redirect>, Ructe> {
|
||||
let conn = &*rockets.conn;
|
||||
form.validate()
|
||||
.and_then(|_| {
|
||||
let instance = Instance::get_local(&*conn)
|
||||
.expect("instance::update_settings: local instance error");
|
||||
let instance =
|
||||
Instance::get_local().expect("instance::update_settings: local instance error");
|
||||
instance
|
||||
.update(
|
||||
&*conn,
|
||||
conn,
|
||||
form.name.clone(),
|
||||
form.open_registrations,
|
||||
form.short_description.clone(),
|
||||
form.long_description.clone(),
|
||||
)
|
||||
.expect("instance::update_settings: save error");
|
||||
Ok(Redirect::to(uri!(admin)))
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(admin)),
|
||||
i18n!(rockets.intl.catalog, "Instance settings have been saved."),
|
||||
))
|
||||
})
|
||||
.or_else(|e| {
|
||||
let local_inst = Instance::get_local(&*conn)
|
||||
.expect("instance::update_settings: local instance error");
|
||||
let local_inst =
|
||||
Instance::get_local().expect("instance::update_settings: local instance error");
|
||||
Err(render!(instance::admin(
|
||||
&(&*conn, &intl.catalog, Some(admin.0)),
|
||||
&rockets.to_context(),
|
||||
local_inst,
|
||||
form.clone(),
|
||||
e
|
||||
@@ -155,58 +148,78 @@ pub fn update_settings(
|
||||
|
||||
#[get("/admin/instances?<page>")]
|
||||
pub fn admin_instances(
|
||||
admin: Admin,
|
||||
conn: DbConn,
|
||||
_admin: Admin,
|
||||
page: Option<Page>,
|
||||
intl: I18n,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Ructe, ErrorPage> {
|
||||
let page = page.unwrap_or_default();
|
||||
let instances = Instance::page(&*conn, page.limits())?;
|
||||
let instances = Instance::page(&*rockets.conn, page.limits())?;
|
||||
Ok(render!(instance::list(
|
||||
&(&*conn, &intl.catalog, Some(admin.0)),
|
||||
Instance::get_local(&*conn)?,
|
||||
&rockets.to_context(),
|
||||
Instance::get_local()?,
|
||||
instances,
|
||||
page.0,
|
||||
Page::total(Instance::count(&*conn)? as i32)
|
||||
Page::total(Instance::count(&*rockets.conn)? as i32)
|
||||
)))
|
||||
}
|
||||
|
||||
#[post("/admin/instances/<id>/block")]
|
||||
pub fn toggle_block(_admin: Admin, conn: DbConn, id: i32) -> Result<Redirect, ErrorPage> {
|
||||
if let Ok(inst) = Instance::get(&*conn, id) {
|
||||
inst.toggle_block(&*conn)?;
|
||||
}
|
||||
pub fn toggle_block(
|
||||
_admin: Admin,
|
||||
conn: DbConn,
|
||||
id: i32,
|
||||
intl: I18n,
|
||||
) -> Result<Flash<Redirect>, ErrorPage> {
|
||||
let inst = Instance::get(&*conn, id)?;
|
||||
let message = if inst.blocked {
|
||||
i18n!(intl.catalog, "{} has been unblocked."; &inst.name)
|
||||
} else {
|
||||
i18n!(intl.catalog, "{} has been blocked."; &inst.name)
|
||||
};
|
||||
|
||||
Ok(Redirect::to(uri!(admin_instances: page = _)))
|
||||
inst.toggle_block(&*conn)?;
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(admin_instances: page = _)),
|
||||
message,
|
||||
))
|
||||
}
|
||||
|
||||
#[get("/admin/users?<page>")]
|
||||
pub fn admin_users(
|
||||
admin: Admin,
|
||||
conn: DbConn,
|
||||
_admin: Admin,
|
||||
page: Option<Page>,
|
||||
intl: I18n,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Ructe, ErrorPage> {
|
||||
let page = page.unwrap_or_default();
|
||||
Ok(render!(instance::users(
|
||||
&(&*conn, &intl.catalog, Some(admin.0)),
|
||||
User::get_local_page(&*conn, page.limits())?,
|
||||
&rockets.to_context(),
|
||||
User::get_local_page(&*rockets.conn, page.limits())?,
|
||||
page.0,
|
||||
Page::total(User::count_local(&*conn)? as i32)
|
||||
Page::total(User::count_local(&*rockets.conn)? as i32)
|
||||
)))
|
||||
}
|
||||
|
||||
#[post("/admin/users/<id>/ban")]
|
||||
pub fn ban(
|
||||
_admin: Admin,
|
||||
conn: DbConn,
|
||||
id: i32,
|
||||
searcher: Searcher,
|
||||
) -> Result<Redirect, ErrorPage> {
|
||||
if let Ok(u) = User::get(&*conn, id) {
|
||||
u.delete(&*conn, &searcher)?;
|
||||
pub fn ban(_admin: Admin, id: i32, rockets: PlumeRocket) -> Result<Flash<Redirect>, ErrorPage> {
|
||||
let u = User::get(&*rockets.conn, id)?;
|
||||
u.delete(&*rockets.conn, &rockets.searcher)?;
|
||||
|
||||
if Instance::get_local()
|
||||
.map(|i| u.instance_id == i.id)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let target = User::one_by_instance(&*rockets.conn)?;
|
||||
let delete_act = u.delete_activity(&*rockets.conn)?;
|
||||
let u_clone = u.clone();
|
||||
rockets
|
||||
.worker
|
||||
.execute(move || broadcast(&u_clone, delete_act, target));
|
||||
}
|
||||
Ok(Redirect::to(uri!(admin_users: page = _)))
|
||||
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(admin_users: page = _)),
|
||||
i18n!(rockets.intl.catalog, "{} has been banned."; u.name()),
|
||||
))
|
||||
}
|
||||
|
||||
#[post("/inbox", data = "<data>")]
|
||||
@@ -254,7 +267,7 @@ pub fn nodeinfo(conn: DbConn, version: String) -> Result<Json<serde_json::Value>
|
||||
return Err(ErrorPage::from(Error::NotFound));
|
||||
}
|
||||
|
||||
let local_inst = Instance::get_local(&*conn)?;
|
||||
let local_inst = Instance::get_local()?;
|
||||
let mut doc = json!({
|
||||
"version": version,
|
||||
"software": {
|
||||
@@ -288,20 +301,26 @@ pub fn nodeinfo(conn: DbConn, version: String) -> Result<Json<serde_json::Value>
|
||||
}
|
||||
|
||||
#[get("/about")]
|
||||
pub fn about(user: Option<User>, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> {
|
||||
pub fn about(rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
let conn = &*rockets.conn;
|
||||
Ok(render!(instance::about(
|
||||
&(&*conn, &intl.catalog, user),
|
||||
Instance::get_local(&*conn)?,
|
||||
Instance::get_local(&*conn)?.main_admin(&*conn)?,
|
||||
User::count_local(&*conn)?,
|
||||
Post::count_local(&*conn)?,
|
||||
Instance::count(&*conn)? - 1
|
||||
&rockets.to_context(),
|
||||
Instance::get_local()?,
|
||||
Instance::get_local()?.main_admin(conn)?,
|
||||
User::count_local(conn)?,
|
||||
Post::count_local(conn)?,
|
||||
Instance::count(conn)? - 1
|
||||
)))
|
||||
}
|
||||
|
||||
#[get("/privacy")]
|
||||
pub fn privacy(rockets: PlumeRocket) -> Ructe {
|
||||
render!(instance::privacy(&rockets.to_context()))
|
||||
}
|
||||
|
||||
#[get("/manifest.json")]
|
||||
pub fn web_manifest(conn: DbConn) -> Result<Json<serde_json::Value>, ErrorPage> {
|
||||
let instance = Instance::get_local(&*conn)?;
|
||||
pub fn web_manifest() -> Result<Json<serde_json::Value>, ErrorPage> {
|
||||
let instance = Instance::get_local()?;
|
||||
Ok(Json(json!({
|
||||
"name": &instance.name,
|
||||
"description": &instance.short_description,
|
||||
|
||||
+37
-19
@@ -3,32 +3,32 @@ use multipart::server::{
|
||||
save::{SaveResult, SavedData},
|
||||
Multipart,
|
||||
};
|
||||
use plume_models::{db_conn::DbConn, medias::*, users::User, Error};
|
||||
use plume_models::{db_conn::DbConn, medias::*, users::User, Error, PlumeRocket};
|
||||
use rocket::{
|
||||
http::ContentType,
|
||||
response::{status, Redirect},
|
||||
response::{status, Flash, Redirect},
|
||||
Data,
|
||||
};
|
||||
use rocket_i18n::I18n;
|
||||
use routes::{errors::ErrorPage, Page};
|
||||
use std::fs;
|
||||
use template_utils::Ructe;
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
|
||||
#[get("/medias?<page>")]
|
||||
pub fn list(user: User, conn: DbConn, intl: I18n, page: Option<Page>) -> Result<Ructe, ErrorPage> {
|
||||
pub fn list(user: User, page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
let page = page.unwrap_or_default();
|
||||
let medias = Media::page_for_user(&*conn, &user, page.limits())?;
|
||||
let medias = Media::page_for_user(&*rockets.conn, &user, page.limits())?;
|
||||
Ok(render!(medias::index(
|
||||
&(&*conn, &intl.catalog, Some(user.clone())),
|
||||
&rockets.to_context(),
|
||||
medias,
|
||||
page.0,
|
||||
Page::total(Media::count_for_user(&*conn, &user)? as i32)
|
||||
Page::total(Media::count_for_user(&*rockets.conn, &user)? as i32)
|
||||
)))
|
||||
}
|
||||
|
||||
#[get("/medias/new")]
|
||||
pub fn new(user: User, conn: DbConn, intl: I18n) -> Ructe {
|
||||
render!(medias::new(&(&*conn, &intl.catalog, Some(user))))
|
||||
pub fn new(_user: User, rockets: PlumeRocket) -> Ructe {
|
||||
render!(medias::new(&rockets.to_context()))
|
||||
}
|
||||
|
||||
#[post("/medias/new", data = "<data>")]
|
||||
@@ -122,32 +122,50 @@ fn read(data: &SavedData) -> Result<String, status::BadRequest<&'static str>> {
|
||||
}
|
||||
|
||||
#[get("/medias/<id>")]
|
||||
pub fn details(id: i32, user: User, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> {
|
||||
let media = Media::get(&*conn, id)?;
|
||||
pub fn details(id: i32, user: User, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
let media = Media::get(&*rockets.conn, id)?;
|
||||
if media.owner_id == user.id {
|
||||
Ok(render!(medias::details(
|
||||
&(&*conn, &intl.catalog, Some(user)),
|
||||
media
|
||||
)))
|
||||
Ok(render!(medias::details(&rockets.to_context(), media)))
|
||||
} else {
|
||||
Err(Error::Unauthorized.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/medias/<id>/delete")]
|
||||
pub fn delete(id: i32, user: User, conn: DbConn) -> Result<Redirect, ErrorPage> {
|
||||
pub fn delete(id: i32, user: User, conn: DbConn, intl: I18n) -> Result<Flash<Redirect>, ErrorPage> {
|
||||
let media = Media::get(&*conn, id)?;
|
||||
if media.owner_id == user.id {
|
||||
media.delete(&*conn)?;
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(list: page = _)),
|
||||
i18n!(intl.catalog, "Your media have been deleted."),
|
||||
))
|
||||
} else {
|
||||
Ok(Flash::error(
|
||||
Redirect::to(uri!(list: page = _)),
|
||||
i18n!(intl.catalog, "You are not allowed to delete this media."),
|
||||
))
|
||||
}
|
||||
Ok(Redirect::to(uri!(list: page = _)))
|
||||
}
|
||||
|
||||
#[post("/medias/<id>/avatar")]
|
||||
pub fn set_avatar(id: i32, user: User, conn: DbConn) -> Result<Redirect, ErrorPage> {
|
||||
pub fn set_avatar(
|
||||
id: i32,
|
||||
user: User,
|
||||
conn: DbConn,
|
||||
intl: I18n,
|
||||
) -> Result<Flash<Redirect>, ErrorPage> {
|
||||
let media = Media::get(&*conn, id)?;
|
||||
if media.owner_id == user.id {
|
||||
user.set_avatar(&*conn, media.id)?;
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(details: id = id)),
|
||||
i18n!(intl.catalog, "Your avatar has been updated."),
|
||||
))
|
||||
} else {
|
||||
Ok(Flash::error(
|
||||
Redirect::to(uri!(details: id = id)),
|
||||
i18n!(intl.catalog, "You are not allowed to use this media."),
|
||||
))
|
||||
}
|
||||
Ok(Redirect::to(uri!(details: id = id)))
|
||||
}
|
||||
|
||||
+3
-2
@@ -16,7 +16,7 @@ use plume_models::{posts::Post, Connection};
|
||||
|
||||
const ITEMS_PER_PAGE: i32 = 12;
|
||||
|
||||
#[derive(Copy, Clone, UriDisplayQuery)]
|
||||
#[derive(Shrinkwrap, Copy, Clone, UriDisplayQuery)]
|
||||
pub struct Page(i32);
|
||||
|
||||
impl<'v> FromFormValue<'v> for Page {
|
||||
@@ -52,6 +52,7 @@ impl Page {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Shrinkwrap)]
|
||||
pub struct ContentLen(pub u64);
|
||||
|
||||
impl<'a, 'r> FromRequest<'a, 'r> for ContentLen {
|
||||
@@ -72,7 +73,7 @@ impl Default for Page {
|
||||
}
|
||||
|
||||
/// A form for remote interaction, used by multiple routes
|
||||
#[derive(Clone, Default, FromForm)]
|
||||
#[derive(Shrinkwrap, Clone, Default, FromForm)]
|
||||
pub struct RemoteForm {
|
||||
pub remote: String,
|
||||
}
|
||||
|
||||
@@ -2,23 +2,22 @@ use rocket::response::{Flash, Redirect};
|
||||
use rocket_i18n::I18n;
|
||||
|
||||
use plume_common::utils;
|
||||
use plume_models::{db_conn::DbConn, notifications::Notification, users::User};
|
||||
use plume_models::{notifications::Notification, users::User, PlumeRocket};
|
||||
use routes::{errors::ErrorPage, Page};
|
||||
use template_utils::Ructe;
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
|
||||
#[get("/notifications?<page>")]
|
||||
pub fn notifications(
|
||||
conn: DbConn,
|
||||
user: User,
|
||||
page: Option<Page>,
|
||||
intl: I18n,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Ructe, ErrorPage> {
|
||||
let page = page.unwrap_or_default();
|
||||
Ok(render!(notifications::index(
|
||||
&(&*conn, &intl.catalog, Some(user.clone())),
|
||||
Notification::page_for_user(&*conn, &user, page.limits())?,
|
||||
&rockets.to_context(),
|
||||
Notification::page_for_user(&*rockets.conn, &user, page.limits())?,
|
||||
page.0,
|
||||
Page::total(Notification::count_for_user(&*conn, &user)? as i32)
|
||||
Page::total(Notification::count_for_user(&*rockets.conn, &user)? as i32)
|
||||
)))
|
||||
}
|
||||
|
||||
|
||||
+59
-47
@@ -27,7 +27,7 @@ use plume_models::{
|
||||
Error, PlumeRocket,
|
||||
};
|
||||
use routes::{comments::NewCommentForm, errors::ErrorPage, ContentLen, RemoteForm};
|
||||
use template_utils::Ructe;
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
|
||||
#[get("/~/<blog>/<slug>?<responding_to>", rank = 4)]
|
||||
pub fn details(
|
||||
@@ -51,7 +51,7 @@ pub fn details(
|
||||
let previous = responding_to.and_then(|r| Comment::get(&*conn, r).ok());
|
||||
|
||||
Ok(render!(posts::details(
|
||||
&(&*conn, &rockets.intl.catalog, user.clone()),
|
||||
&rockets.to_context(),
|
||||
post.clone(),
|
||||
blog,
|
||||
&NewCommentForm {
|
||||
@@ -89,7 +89,7 @@ pub fn details(
|
||||
)))
|
||||
} else {
|
||||
Ok(render!(errors::not_authorized(
|
||||
&(&*conn, &rockets.intl.catalog, user.clone()),
|
||||
&rockets.to_context(),
|
||||
i18n!(rockets.intl.catalog, "This post isn't published yet.")
|
||||
)))
|
||||
}
|
||||
@@ -130,25 +130,24 @@ pub fn new_auth(blog: String, i18n: I18n) -> Flash<Redirect> {
|
||||
pub fn new(blog: String, cl: ContentLen, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
let conn = &*rockets.conn;
|
||||
let b = Blog::find_by_fqn(&rockets, &blog)?;
|
||||
let user = rockets.user.unwrap();
|
||||
let intl = rockets.intl;
|
||||
let user = rockets.user.clone().unwrap();
|
||||
|
||||
if !user.is_author_in(&*conn, &b)? {
|
||||
// TODO actually return 403 error code
|
||||
return Ok(render!(errors::not_authorized(
|
||||
&(&*conn, &intl.catalog, Some(user)),
|
||||
i18n!(intl.catalog, "You are not an author of this blog.")
|
||||
&rockets.to_context(),
|
||||
i18n!(rockets.intl.catalog, "You are not an author of this blog.")
|
||||
)));
|
||||
}
|
||||
|
||||
let medias = Media::for_user(&*conn, user.id)?;
|
||||
Ok(render!(posts::new(
|
||||
&(&*conn, &intl.catalog, Some(user)),
|
||||
i18n!(intl.catalog, "New post"),
|
||||
&rockets.to_context(),
|
||||
i18n!(rockets.intl.catalog, "New post"),
|
||||
b,
|
||||
false,
|
||||
&NewPostForm {
|
||||
license: Instance::get_local(&*conn)?.default_license,
|
||||
license: Instance::get_local()?.default_license,
|
||||
..NewPostForm::default()
|
||||
},
|
||||
true,
|
||||
@@ -170,11 +169,11 @@ pub fn edit(
|
||||
let intl = &rockets.intl.catalog;
|
||||
let b = Blog::find_by_fqn(&rockets, &blog)?;
|
||||
let post = Post::find_by_slug(&*conn, &slug, b.id)?;
|
||||
let user = rockets.user.unwrap();
|
||||
let user = rockets.user.clone().unwrap();
|
||||
|
||||
if !user.is_author_in(&*conn, &b)? {
|
||||
return Ok(render!(errors::not_authorized(
|
||||
&(&*conn, intl, Some(user)),
|
||||
&rockets.to_context(),
|
||||
i18n!(intl, "You are not an author of this blog.")
|
||||
)));
|
||||
}
|
||||
@@ -188,7 +187,7 @@ pub fn edit(
|
||||
let medias = Media::for_user(&*conn, user.id)?;
|
||||
let title = post.title.clone();
|
||||
Ok(render!(posts::new(
|
||||
&(&*conn, intl, Some(user)),
|
||||
&rockets.to_context(),
|
||||
i18n!(intl, "Edit {0}"; &title),
|
||||
b,
|
||||
true,
|
||||
@@ -220,7 +219,7 @@ pub fn update(
|
||||
cl: ContentLen,
|
||||
form: LenientForm<NewPostForm>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Redirect, Ructe> {
|
||||
) -> Result<Flash<Redirect>, Ructe> {
|
||||
let conn = &*rockets.conn;
|
||||
let b = Blog::find_by_fqn(&rockets, &blog).expect("post::update: blog error");
|
||||
let mut post =
|
||||
@@ -256,15 +255,18 @@ pub fn update(
|
||||
.expect("posts::update: is author in error")
|
||||
{
|
||||
// actually it's not "Ok"…
|
||||
Ok(Redirect::to(
|
||||
uri!(super::blogs::details: name = blog, page = _),
|
||||
Ok(Flash::error(
|
||||
Redirect::to(uri!(super::blogs::details: name = blog, page = _)),
|
||||
i18n!(&intl, "You are not allowed to publish on this blog."),
|
||||
))
|
||||
} else {
|
||||
let (content, mentions, hashtags) = utils::md_to_html(
|
||||
form.content.to_string().as_ref(),
|
||||
&Instance::get_local(&conn)
|
||||
.expect("posts::update: Error getting local instance")
|
||||
.public_domain,
|
||||
Some(
|
||||
&Instance::get_local()
|
||||
.expect("posts::update: Error getting local instance")
|
||||
.public_domain,
|
||||
),
|
||||
false,
|
||||
Some(Media::get_media_processor(
|
||||
&conn,
|
||||
@@ -312,7 +314,7 @@ pub fn update(
|
||||
.filter(|t| !t.is_empty())
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.filter_map(|t| Tag::build_activity(&conn, t).ok())
|
||||
.filter_map(|t| Tag::build_activity(t).ok())
|
||||
.collect::<Vec<_>>();
|
||||
post.update_tags(&conn, tags)
|
||||
.expect("post::update: tags error");
|
||||
@@ -322,7 +324,7 @@ pub fn update(
|
||||
.map(|h| h.to_camel_case())
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.filter_map(|t| Tag::build_activity(&conn, t).ok())
|
||||
.filter_map(|t| Tag::build_activity(t).ok())
|
||||
.collect::<Vec<_>>();
|
||||
post.update_hashtags(&conn, hashtags)
|
||||
.expect("post::update: hashtags error");
|
||||
@@ -343,14 +345,15 @@ pub fn update(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Redirect::to(
|
||||
uri!(details: blog = blog, slug = new_slug, responding_to = _),
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(details: blog = blog, slug = new_slug, responding_to = _)),
|
||||
i18n!(intl, "Your article has been updated."),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
let medias = Media::for_user(&*conn, user.id).expect("posts:update: medias error");
|
||||
Err(render!(posts::new(
|
||||
&(&*conn, intl, Some(user)),
|
||||
&rockets.to_context(),
|
||||
i18n!(intl, "Edit {0}"; &form.title),
|
||||
b,
|
||||
true,
|
||||
@@ -393,7 +396,7 @@ pub fn create(
|
||||
form: LenientForm<NewPostForm>,
|
||||
cl: ContentLen,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Redirect, Result<Ructe, ErrorPage>> {
|
||||
) -> Result<Flash<Redirect>, Result<Ructe, ErrorPage>> {
|
||||
let conn = &*rockets.conn;
|
||||
let blog = Blog::find_by_fqn(&rockets, &blog_name).expect("post::create: blog error");;
|
||||
let slug = form.title.to_string().to_kebab_case();
|
||||
@@ -420,16 +423,22 @@ pub fn create(
|
||||
.expect("post::create: is author in error")
|
||||
{
|
||||
// actually it's not "Ok"…
|
||||
return Ok(Redirect::to(
|
||||
uri!(super::blogs::details: name = blog_name, page = _),
|
||||
return Ok(Flash::error(
|
||||
Redirect::to(uri!(super::blogs::details: name = blog_name, page = _)),
|
||||
i18n!(
|
||||
&rockets.intl.catalog,
|
||||
"You are not allowed to publish on this blog."
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let (content, mentions, hashtags) = utils::md_to_html(
|
||||
form.content.to_string().as_ref(),
|
||||
&Instance::get_local(&conn)
|
||||
.expect("post::create: local instance error")
|
||||
.public_domain,
|
||||
Some(
|
||||
&Instance::get_local()
|
||||
.expect("post::create: local instance error")
|
||||
.public_domain,
|
||||
),
|
||||
false,
|
||||
Some(Media::get_media_processor(
|
||||
&conn,
|
||||
@@ -518,15 +527,15 @@ pub fn create(
|
||||
worker.execute(move || broadcast(&user, act, dest));
|
||||
}
|
||||
|
||||
Ok(Redirect::to(
|
||||
uri!(details: blog = blog_name, slug = slug, responding_to = _),
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(details: blog = blog_name, slug = slug, responding_to = _)),
|
||||
i18n!(&rockets.intl.catalog, "Your article has been saved."),
|
||||
))
|
||||
} else {
|
||||
let medias = Media::for_user(&*conn, user.id).expect("posts::create: medias error");
|
||||
let intl = rockets.intl;
|
||||
Err(Ok(render!(posts::new(
|
||||
&(&*conn, &intl.catalog, Some(user)),
|
||||
i18n!(intl.catalog, "New post"),
|
||||
&rockets.to_context(),
|
||||
i18n!(rockets.intl.catalog, "New article"),
|
||||
blog,
|
||||
false,
|
||||
&*form,
|
||||
@@ -544,7 +553,8 @@ pub fn delete(
|
||||
blog_name: String,
|
||||
slug: String,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Redirect, ErrorPage> {
|
||||
intl: I18n,
|
||||
) -> Result<Flash<Redirect>, ErrorPage> {
|
||||
let user = rockets.user.clone().unwrap();
|
||||
let post = Blog::find_by_fqn(&rockets, &blog_name)
|
||||
.and_then(|blog| Post::find_by_slug(&*rockets.conn, &slug, blog.id));
|
||||
@@ -555,8 +565,11 @@ pub fn delete(
|
||||
.into_iter()
|
||||
.any(|a| a.id == user.id)
|
||||
{
|
||||
return Ok(Redirect::to(
|
||||
uri!(details: blog = blog_name.clone(), slug = slug.clone(), responding_to = _),
|
||||
return Ok(Flash::error(
|
||||
Redirect::to(
|
||||
uri!(details: blog = blog_name.clone(), slug = slug.clone(), responding_to = _),
|
||||
),
|
||||
i18n!(intl.catalog, "You are not allowed to delete this article."),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -579,13 +592,14 @@ pub fn delete(
|
||||
.expect("Failed to rotate keypair");
|
||||
});
|
||||
|
||||
Ok(Redirect::to(
|
||||
uri!(super::blogs::details: name = blog_name, page = _),
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(super::blogs::details: name = blog_name, page = _)),
|
||||
i18n!(intl.catalog, "Your article has been deleted."),
|
||||
))
|
||||
} else {
|
||||
Ok(Redirect::to(
|
||||
Ok(Flash::error(Redirect::to(
|
||||
uri!(super::blogs::details: name = blog_name, page = _),
|
||||
))
|
||||
), i18n!(intl.catalog, "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,12 +608,11 @@ pub fn remote_interact(
|
||||
rockets: PlumeRocket,
|
||||
blog_name: String,
|
||||
slug: String,
|
||||
i18n: I18n,
|
||||
) -> Result<Ructe, ErrorPage> {
|
||||
let target = Blog::find_by_fqn(&rockets, &blog_name)
|
||||
.and_then(|blog| Post::find_by_slug(&rockets.conn, &slug, blog.id))?;
|
||||
Ok(render!(posts::remote_interact(
|
||||
&(&rockets.conn, &i18n.catalog, None),
|
||||
&rockets.to_context(),
|
||||
target,
|
||||
super::session::LoginForm::default(),
|
||||
ValidationErrors::default(),
|
||||
@@ -614,7 +627,6 @@ pub fn remote_interact_post(
|
||||
blog_name: String,
|
||||
slug: String,
|
||||
remote: LenientForm<RemoteForm>,
|
||||
i18n: I18n,
|
||||
) -> Result<Result<Ructe, Redirect>, ErrorPage> {
|
||||
let target = Blog::find_by_fqn(&rockets, &blog_name)
|
||||
.and_then(|blog| Post::find_by_slug(&rockets.conn, &slug, blog.id))?;
|
||||
@@ -627,12 +639,12 @@ pub fn remote_interact_post(
|
||||
let mut errs = ValidationErrors::new();
|
||||
errs.add("remote", ValidationError {
|
||||
code: Cow::from("invalid_remote"),
|
||||
message: Some(Cow::from(i18n!(&i18n.catalog, "Couldn't obtain enough information about your account. Please make sure your username is correct."))),
|
||||
message: Some(Cow::from(i18n!(rockets.intl.catalog, "Couldn't obtain enough information about your account. Please make sure your username is correct."))),
|
||||
params: HashMap::new(),
|
||||
});
|
||||
//could not get your remote url?
|
||||
Ok(Ok(render!(posts::remote_interact(
|
||||
&(&rockets.conn, &i18n.catalog, None),
|
||||
&rockets.to_context(),
|
||||
target,
|
||||
super::session::LoginForm::default(),
|
||||
ValidationErrors::default(),
|
||||
|
||||
+9
-14
@@ -1,12 +1,10 @@
|
||||
use chrono::offset::Utc;
|
||||
use rocket::request::Form;
|
||||
use rocket_i18n::I18n;
|
||||
|
||||
use plume_models::{db_conn::DbConn, search::Query, users::User};
|
||||
use plume_models::{search::Query, PlumeRocket};
|
||||
use routes::Page;
|
||||
use std::str::FromStr;
|
||||
use template_utils::Ructe;
|
||||
use Searcher;
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
|
||||
#[derive(Default, FromForm)]
|
||||
pub struct SearchQuery {
|
||||
@@ -52,13 +50,8 @@ macro_rules! param_to_query {
|
||||
}
|
||||
|
||||
#[get("/search?<query..>")]
|
||||
pub fn search(
|
||||
query: Option<Form<SearchQuery>>,
|
||||
conn: DbConn,
|
||||
searcher: Searcher,
|
||||
user: Option<User>,
|
||||
intl: I18n,
|
||||
) -> Ructe {
|
||||
pub fn search(query: Option<Form<SearchQuery>>, rockets: PlumeRocket) -> Ructe {
|
||||
let conn = &*rockets.conn;
|
||||
let query = query.map(Form::into_inner).unwrap_or_default();
|
||||
let page = query.page.unwrap_or_default();
|
||||
let mut parsed_query =
|
||||
@@ -73,14 +66,16 @@ pub fn search(
|
||||
|
||||
if str_query.is_empty() {
|
||||
render!(search::index(
|
||||
&(&*conn, &intl.catalog, user),
|
||||
&rockets.to_context(),
|
||||
&format!("{}", Utc::today().format("%Y-%m-d"))
|
||||
))
|
||||
} else {
|
||||
let res = searcher.search_document(&conn, parsed_query, page.limits());
|
||||
let res = rockets
|
||||
.searcher
|
||||
.search_document(&conn, parsed_query, page.limits());
|
||||
let next_page = if res.is_empty() { 0 } else { page.0 + 1 };
|
||||
render!(search::result(
|
||||
&(&*conn, &intl.catalog, user),
|
||||
&rockets.to_context(),
|
||||
&str_query,
|
||||
res,
|
||||
page.0,
|
||||
|
||||
+61
-54
@@ -2,8 +2,8 @@ use lettre::Transport;
|
||||
use rocket::http::ext::IntoOwned;
|
||||
use rocket::{
|
||||
http::{uri::Uri, Cookie, Cookies, SameSite},
|
||||
request::{FlashMessage, Form, LenientForm},
|
||||
response::Redirect,
|
||||
request::{Form, LenientForm},
|
||||
response::{Flash, Redirect},
|
||||
State,
|
||||
};
|
||||
use rocket_i18n::I18n;
|
||||
@@ -12,21 +12,20 @@ use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::Instant,
|
||||
};
|
||||
use template_utils::Ructe;
|
||||
use validator::{Validate, ValidationError, ValidationErrors};
|
||||
|
||||
use mail::{build_mail, Mailer};
|
||||
use plume_models::{
|
||||
db_conn::DbConn,
|
||||
users::{User, AUTH_COOKIE},
|
||||
Error, PlumeRocket, CONFIG,
|
||||
};
|
||||
use routes::errors::ErrorPage;
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
|
||||
#[get("/login?<m>")]
|
||||
pub fn new(user: Option<User>, conn: DbConn, m: Option<String>, intl: I18n) -> Ructe {
|
||||
pub fn new(m: Option<String>, rockets: PlumeRocket) -> Ructe {
|
||||
render!(session::login(
|
||||
&(&*conn, &intl.catalog, user),
|
||||
&rockets.to_context(),
|
||||
m,
|
||||
&LoginForm::default(),
|
||||
ValidationErrors::default()
|
||||
@@ -44,10 +43,9 @@ pub struct LoginForm {
|
||||
#[post("/login", data = "<form>")]
|
||||
pub fn create(
|
||||
form: LenientForm<LoginForm>,
|
||||
flash: Option<FlashMessage>,
|
||||
mut cookies: Cookies,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Redirect, Ructe> {
|
||||
) -> Result<Flash<Redirect>, Ructe> {
|
||||
let conn = &*rockets.conn;
|
||||
let user = User::find_by_email(&*conn, &form.email_or_name)
|
||||
.or_else(|_| User::find_by_fqn(&rockets, &form.email_or_name));
|
||||
@@ -84,31 +82,38 @@ pub fn create(
|
||||
.same_site(SameSite::Lax)
|
||||
.finish(),
|
||||
);
|
||||
let destination = flash
|
||||
.and_then(|f| {
|
||||
if f.name() == "callback" {
|
||||
Some(f.msg().to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
let destination = rockets
|
||||
.flash_msg
|
||||
.clone()
|
||||
.and_then(
|
||||
|(name, msg)| {
|
||||
if name == "callback" {
|
||||
Some(msg)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap_or_else(|| "/".to_owned());
|
||||
|
||||
let uri = Uri::parse(&destination)
|
||||
.map(IntoOwned::into_owned)
|
||||
.map_err(|_| {
|
||||
render!(session::login(
|
||||
&(&*conn, &rockets.intl.catalog, None),
|
||||
&(conn, &rockets.intl.catalog, None, None),
|
||||
None,
|
||||
&*form,
|
||||
errors
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(Redirect::to(uri))
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri),
|
||||
i18n!(&rockets.intl.catalog, "You are now connected."),
|
||||
))
|
||||
} else {
|
||||
Err(render!(session::login(
|
||||
&(&*conn, &rockets.intl.catalog, None),
|
||||
&rockets.to_context(),
|
||||
None,
|
||||
&*form,
|
||||
errors
|
||||
@@ -117,11 +122,14 @@ pub fn create(
|
||||
}
|
||||
|
||||
#[get("/logout")]
|
||||
pub fn delete(mut cookies: Cookies) -> Redirect {
|
||||
pub fn delete(mut cookies: Cookies, intl: I18n) -> Flash<Redirect> {
|
||||
if let Some(cookie) = cookies.get_private(AUTH_COOKIE) {
|
||||
cookies.remove_private(cookie);
|
||||
}
|
||||
Redirect::to("/")
|
||||
Flash::success(
|
||||
Redirect::to("/"),
|
||||
i18n!(intl.catalog, "You are now logged off."),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -138,9 +146,9 @@ impl PartialEq for ResetRequest {
|
||||
}
|
||||
|
||||
#[get("/password-reset")]
|
||||
pub fn password_reset_request_form(conn: DbConn, intl: I18n) -> Ructe {
|
||||
pub fn password_reset_request_form(rockets: PlumeRocket) -> Ructe {
|
||||
render!(session::password_reset_request(
|
||||
&(&*conn, &intl.catalog, None),
|
||||
&rockets.to_context(),
|
||||
&ResetForm::default(),
|
||||
ValidationErrors::default()
|
||||
))
|
||||
@@ -154,17 +162,16 @@ pub struct ResetForm {
|
||||
|
||||
#[post("/password-reset", data = "<form>")]
|
||||
pub fn password_reset_request(
|
||||
conn: DbConn,
|
||||
intl: I18n,
|
||||
mail: State<Arc<Mutex<Mailer>>>,
|
||||
form: Form<ResetForm>,
|
||||
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Ructe {
|
||||
let mut requests = requests.lock().unwrap();
|
||||
// Remove outdated requests (more than 1 day old) to avoid the list to grow too much
|
||||
requests.retain(|r| r.creation_date.elapsed().as_secs() < 24 * 60 * 60);
|
||||
|
||||
if User::find_by_email(&*conn, &form.email).is_ok()
|
||||
if User::find_by_email(&*rockets.conn, &form.email).is_ok()
|
||||
&& !requests.iter().any(|x| x.mail == form.email.clone())
|
||||
{
|
||||
let id = plume_common::utils::random_hex();
|
||||
@@ -178,8 +185,8 @@ pub fn password_reset_request(
|
||||
let link = format!("https://{}/password-reset/{}", CONFIG.base_url, id);
|
||||
if let Some(message) = build_mail(
|
||||
form.email.clone(),
|
||||
i18n!(intl.catalog, "Password reset"),
|
||||
i18n!(intl.catalog, "Here is the link to reset your password: {0}"; link),
|
||||
i18n!(rockets.intl.catalog, "Password reset"),
|
||||
i18n!(rockets.intl.catalog, "Here is the link to reset your password: {0}"; link),
|
||||
) {
|
||||
if let Some(ref mut mail) = *mail.lock().unwrap() {
|
||||
mail.send(message.into())
|
||||
@@ -188,19 +195,14 @@ pub fn password_reset_request(
|
||||
}
|
||||
}
|
||||
}
|
||||
render!(session::password_reset_request_ok(&(
|
||||
&*conn,
|
||||
&intl.catalog,
|
||||
None
|
||||
)))
|
||||
render!(session::password_reset_request_ok(&rockets.to_context()))
|
||||
}
|
||||
|
||||
#[get("/password-reset/<token>")]
|
||||
pub fn password_reset_form(
|
||||
conn: DbConn,
|
||||
intl: I18n,
|
||||
token: String,
|
||||
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Ructe, ErrorPage> {
|
||||
requests
|
||||
.lock()
|
||||
@@ -209,7 +211,7 @@ pub fn password_reset_form(
|
||||
.find(|x| x.id == token.clone())
|
||||
.ok_or(Error::NotFound)?;
|
||||
Ok(render!(session::password_reset(
|
||||
&(&*conn, &intl.catalog, None),
|
||||
&rockets.to_context(),
|
||||
&NewPasswordForm::default(),
|
||||
ValidationErrors::default()
|
||||
)))
|
||||
@@ -236,12 +238,11 @@ fn passwords_match(form: &NewPasswordForm) -> Result<(), ValidationError> {
|
||||
|
||||
#[post("/password-reset/<token>", data = "<form>")]
|
||||
pub fn password_reset(
|
||||
conn: DbConn,
|
||||
intl: I18n,
|
||||
token: String,
|
||||
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
|
||||
form: Form<NewPasswordForm>,
|
||||
) -> Result<Redirect, Ructe> {
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Flash<Redirect>, Ructe> {
|
||||
form.validate()
|
||||
.and_then(|_| {
|
||||
let mut requests = requests.lock().unwrap();
|
||||
@@ -253,24 +254,30 @@ pub fn password_reset(
|
||||
if req.creation_date.elapsed().as_secs() < 60 * 60 * 2 {
|
||||
// Reset link is only valid for 2 hours
|
||||
requests.retain(|r| *r != req);
|
||||
let user = User::find_by_email(&*conn, &req.mail).map_err(to_validation)?;
|
||||
user.reset_password(&*conn, &form.password).ok();
|
||||
Ok(Redirect::to(uri!(
|
||||
new: m = i18n!(intl.catalog, "Your password was successfully reset.")
|
||||
)))
|
||||
let user = User::find_by_email(&*rockets.conn, &req.mail).map_err(to_validation)?;
|
||||
user.reset_password(&*rockets.conn, &form.password).ok();
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(
|
||||
new: m = _
|
||||
)),
|
||||
i18n!(
|
||||
rockets.intl.catalog,
|
||||
"Your password was successfully reset."
|
||||
),
|
||||
))
|
||||
} else {
|
||||
Ok(Redirect::to(uri!(
|
||||
new: m = i18n!(intl.catalog, "Sorry, but the link expired. Try again")
|
||||
)))
|
||||
Ok(Flash::error(
|
||||
Redirect::to(uri!(
|
||||
new: m = _
|
||||
)),
|
||||
i18n!(
|
||||
rockets.intl.catalog,
|
||||
"Sorry, but the link expired. Try again"
|
||||
),
|
||||
))
|
||||
}
|
||||
})
|
||||
.map_err(|err| {
|
||||
render!(session::password_reset(
|
||||
&(&*conn, &intl.catalog, None),
|
||||
&form,
|
||||
err
|
||||
))
|
||||
})
|
||||
.map_err(|err| render!(session::password_reset(&rockets.to_context(), &form, err)))
|
||||
}
|
||||
|
||||
fn to_validation<T>(_: T) -> ValidationErrors {
|
||||
|
||||
+6
-14
@@ -1,24 +1,16 @@
|
||||
use rocket_i18n::I18n;
|
||||
|
||||
use plume_models::{db_conn::DbConn, posts::Post, users::User};
|
||||
use plume_models::{posts::Post, PlumeRocket};
|
||||
use routes::{errors::ErrorPage, Page};
|
||||
use template_utils::Ructe;
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
|
||||
#[get("/tag/<name>?<page>")]
|
||||
pub fn tag(
|
||||
user: Option<User>,
|
||||
conn: DbConn,
|
||||
name: String,
|
||||
page: Option<Page>,
|
||||
intl: I18n,
|
||||
) -> Result<Ructe, ErrorPage> {
|
||||
pub fn tag(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
let page = page.unwrap_or_default();
|
||||
let posts = Post::list_by_tag(&*conn, name.clone(), page.limits())?;
|
||||
let posts = Post::list_by_tag(&*rockets.conn, name.clone(), page.limits())?;
|
||||
Ok(render!(tags::index(
|
||||
&(&*conn, &intl.catalog, user),
|
||||
&rockets.to_context(),
|
||||
name.clone(),
|
||||
posts,
|
||||
page.0,
|
||||
Page::total(Post::count_for_tag(&*conn, name)? as i32)
|
||||
Page::total(Post::count_for_tag(&*rockets.conn, name)? as i32)
|
||||
)))
|
||||
}
|
||||
|
||||
+92
-45
@@ -26,7 +26,7 @@ use plume_models::{
|
||||
Error, PlumeRocket,
|
||||
};
|
||||
use routes::{errors::ErrorPage, Page, RemoteForm};
|
||||
use template_utils::Ructe;
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
|
||||
#[get("/me")]
|
||||
pub fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
|
||||
@@ -48,7 +48,7 @@ pub fn details(
|
||||
let user = User::find_by_fqn(&rockets, &name)?;
|
||||
let recents = Post::get_recents_for_author(&*conn, &user, 6)?;
|
||||
let reshares = Reshare::get_recents_for_author(&*conn, &user, 6)?;
|
||||
let worker = rockets.worker;
|
||||
let worker = &rockets.worker;
|
||||
|
||||
if !user.get_instance(&*conn)?.local {
|
||||
// Fetch new articles
|
||||
@@ -101,15 +101,15 @@ pub fn details(
|
||||
}
|
||||
}
|
||||
|
||||
let account = rockets.user;
|
||||
let intl = rockets.intl;
|
||||
Ok(render!(users::details(
|
||||
&(&*conn, &intl.catalog, account.clone()),
|
||||
&rockets.to_context(),
|
||||
user.clone(),
|
||||
account
|
||||
rockets
|
||||
.user
|
||||
.clone()
|
||||
.and_then(|x| x.is_following(&*conn, user.id).ok())
|
||||
.unwrap_or(false),
|
||||
user.instance_id != Instance::get_local(&*conn)?.id,
|
||||
user.instance_id != Instance::get_local()?.id,
|
||||
user.get_instance(&*conn)?.public_domain,
|
||||
recents,
|
||||
reshares
|
||||
@@ -120,12 +120,12 @@ pub fn details(
|
||||
}
|
||||
|
||||
#[get("/dashboard")]
|
||||
pub fn dashboard(user: User, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> {
|
||||
let blogs = Blog::find_for_author(&*conn, &user)?;
|
||||
pub fn dashboard(user: User, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
let blogs = Blog::find_for_author(&*rockets.conn, &user)?;
|
||||
Ok(render!(users::dashboard(
|
||||
&(&*conn, &intl.catalog, Some(user.clone())),
|
||||
&rockets.to_context(),
|
||||
blogs,
|
||||
Post::drafts_by_author(&*conn, &user)?
|
||||
Post::drafts_by_author(&*rockets.conn, &user)?
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -141,19 +141,25 @@ pub fn dashboard_auth(i18n: I18n) -> Flash<Redirect> {
|
||||
}
|
||||
|
||||
#[post("/@/<name>/follow")]
|
||||
pub fn follow(name: String, user: User, rockets: PlumeRocket) -> Result<Redirect, ErrorPage> {
|
||||
pub fn follow(
|
||||
name: String,
|
||||
user: User,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Flash<Redirect>, ErrorPage> {
|
||||
let conn = &*rockets.conn;
|
||||
let target = User::find_by_fqn(&rockets, &name)?;
|
||||
if let Ok(follow) = follows::Follow::find(&*conn, user.id, target.id) {
|
||||
let message = if let Ok(follow) = follows::Follow::find(&*conn, user.id, target.id) {
|
||||
let delete_act = follow.build_undo(&*conn)?;
|
||||
local_inbox(
|
||||
&rockets,
|
||||
serde_json::to_value(&delete_act).map_err(Error::from)?,
|
||||
)?;
|
||||
|
||||
let msg = i18n!(rockets.intl.catalog, "You are no longer following {}."; target.name());
|
||||
rockets
|
||||
.worker
|
||||
.execute(move || broadcast(&user, delete_act, vec![target]));
|
||||
msg
|
||||
} else {
|
||||
let f = follows::Follow::insert(
|
||||
&*conn,
|
||||
@@ -166,11 +172,16 @@ pub fn follow(name: String, user: User, rockets: PlumeRocket) -> Result<Redirect
|
||||
f.notify(&*conn)?;
|
||||
|
||||
let act = f.to_activity(&*conn)?;
|
||||
let msg = i18n!(rockets.intl.catalog, "You are now following {}."; target.name());
|
||||
rockets
|
||||
.worker
|
||||
.execute(move || broadcast(&user, act, vec![target]));
|
||||
}
|
||||
Ok(Redirect::to(uri!(details: name = name)))
|
||||
msg
|
||||
};
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(details: name = name)),
|
||||
message,
|
||||
))
|
||||
}
|
||||
|
||||
#[post("/@/<name>/follow", data = "<remote_form>", rank = 2)]
|
||||
@@ -182,8 +193,7 @@ pub fn follow_not_connected(
|
||||
) -> Result<Result<Flash<Ructe>, Redirect>, ErrorPage> {
|
||||
let target = User::find_by_fqn(&rockets, &name)?;
|
||||
if let Some(remote_form) = remote_form {
|
||||
let remote = &remote_form.remote;
|
||||
if let Some(uri) = User::fetch_remote_interact_uri(remote)
|
||||
if let Some(uri) = User::fetch_remote_interact_uri(&remote_form)
|
||||
.ok()
|
||||
.and_then(|uri| {
|
||||
rt_format!(
|
||||
@@ -209,7 +219,7 @@ pub fn follow_not_connected(
|
||||
);
|
||||
Ok(Ok(Flash::new(
|
||||
render!(users::follow_remote(
|
||||
&(&rockets.conn, &i18n.catalog, None),
|
||||
&rockets.to_context(),
|
||||
target,
|
||||
super::session::LoginForm::default(),
|
||||
ValidationErrors::default(),
|
||||
@@ -223,7 +233,7 @@ pub fn follow_not_connected(
|
||||
} else {
|
||||
Ok(Ok(Flash::new(
|
||||
render!(users::follow_remote(
|
||||
&(&rockets.conn, &i18n.catalog, None),
|
||||
&rockets.to_context(),
|
||||
target,
|
||||
super::session::LoginForm::default(),
|
||||
ValidationErrors::default(),
|
||||
@@ -260,13 +270,14 @@ pub fn followers(
|
||||
let followers_count = user.count_followers(&*conn)?;
|
||||
|
||||
Ok(render!(users::followers(
|
||||
&(&*conn, &rockets.intl.catalog, rockets.user.clone()),
|
||||
&rockets.to_context(),
|
||||
user.clone(),
|
||||
rockets
|
||||
.user
|
||||
.clone()
|
||||
.and_then(|x| x.is_following(&*conn, user.id).ok())
|
||||
.unwrap_or(false),
|
||||
user.instance_id != Instance::get_local(&*conn)?.id,
|
||||
user.instance_id != Instance::get_local()?.id,
|
||||
user.get_instance(&*conn)?.public_domain,
|
||||
user.get_followers_page(&*conn, page.limits())?,
|
||||
page.0,
|
||||
@@ -283,18 +294,19 @@ pub fn followed(
|
||||
let conn = &*rockets.conn;
|
||||
let page = page.unwrap_or_default();
|
||||
let user = User::find_by_fqn(&rockets, &name)?;
|
||||
let followed_count = user.count_followed(&*conn)?;
|
||||
let followed_count = user.count_followed(conn)?;
|
||||
|
||||
Ok(render!(users::followed(
|
||||
&(&*conn, &rockets.intl.catalog, rockets.user.clone()),
|
||||
&rockets.to_context(),
|
||||
user.clone(),
|
||||
rockets
|
||||
.user
|
||||
.and_then(|x| x.is_following(&*conn, user.id).ok())
|
||||
.clone()
|
||||
.and_then(|x| x.is_following(conn, user.id).ok())
|
||||
.unwrap_or(false),
|
||||
user.instance_id != Instance::get_local(&*conn)?.id,
|
||||
user.get_instance(&*conn)?.public_domain,
|
||||
user.get_followed_page(&*conn, page.limits())?,
|
||||
user.instance_id != Instance::get_local()?.id,
|
||||
user.get_instance(conn)?.public_domain,
|
||||
user.get_followed_page(conn, page.limits())?,
|
||||
page.0,
|
||||
Page::total(followed_count as i32)
|
||||
)))
|
||||
@@ -311,20 +323,20 @@ pub fn activity_details(
|
||||
}
|
||||
|
||||
#[get("/users/new")]
|
||||
pub fn new(user: Option<User>, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> {
|
||||
pub fn new(rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
Ok(render!(users::new(
|
||||
&(&*conn, &intl.catalog, user),
|
||||
Instance::get_local(&*conn)?.open_registrations,
|
||||
&rockets.to_context(),
|
||||
Instance::get_local()?.open_registrations,
|
||||
&NewUserForm::default(),
|
||||
ValidationErrors::default()
|
||||
)))
|
||||
}
|
||||
|
||||
#[get("/@/<name>/edit")]
|
||||
pub fn edit(name: String, user: User, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> {
|
||||
pub fn edit(name: String, user: User, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
if user.username == name && !name.contains('@') {
|
||||
Ok(render!(users::edit(
|
||||
&(&*conn, &intl.catalog, Some(user.clone())),
|
||||
&rockets.to_context(),
|
||||
UpdateUserForm {
|
||||
display_name: user.display_name.clone(),
|
||||
email: user.email.clone().unwrap_or_default(),
|
||||
@@ -361,7 +373,8 @@ pub fn update(
|
||||
conn: DbConn,
|
||||
user: User,
|
||||
form: LenientForm<UpdateUserForm>,
|
||||
) -> Result<Redirect, ErrorPage> {
|
||||
intl: I18n,
|
||||
) -> Result<Flash<Redirect>, ErrorPage> {
|
||||
user.update(
|
||||
&*conn,
|
||||
if !form.display_name.is_empty() {
|
||||
@@ -380,7 +393,10 @@ pub fn update(
|
||||
user.summary.to_string()
|
||||
},
|
||||
)?;
|
||||
Ok(Redirect::to(uri!(me)))
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(me)),
|
||||
i18n!(intl.catalog, "Your profile has been updated."),
|
||||
))
|
||||
}
|
||||
|
||||
#[post("/@/<name>/delete")]
|
||||
@@ -389,18 +405,33 @@ pub fn delete(
|
||||
user: User,
|
||||
mut cookies: Cookies,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Redirect, ErrorPage> {
|
||||
) -> Result<Flash<Redirect>, ErrorPage> {
|
||||
let account = User::find_by_fqn(&rockets, &name)?;
|
||||
if user.id == account.id {
|
||||
account.delete(&*rockets.conn, &rockets.searcher)?;
|
||||
|
||||
let target = User::one_by_instance(&*rockets.conn)?;
|
||||
let delete_act = account.delete_activity(&*rockets.conn)?;
|
||||
rockets
|
||||
.worker
|
||||
.execute(move || broadcast(&account, delete_act, target));
|
||||
|
||||
if let Some(cookie) = cookies.get_private(AUTH_COOKIE) {
|
||||
cookies.remove_private(cookie);
|
||||
}
|
||||
|
||||
Ok(Redirect::to(uri!(super::instance::index)))
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(super::instance::index)),
|
||||
i18n!(rockets.intl.catalog, "Your account has been deleted."),
|
||||
))
|
||||
} else {
|
||||
Ok(Redirect::to(uri!(edit: name = name)))
|
||||
Ok(Flash::error(
|
||||
Redirect::to(uri!(edit: name = name)),
|
||||
i18n!(
|
||||
rockets.intl.catalog,
|
||||
"You can't delete someone else's account."
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,12 +488,22 @@ fn to_validation(_: Error) -> ValidationErrors {
|
||||
}
|
||||
|
||||
#[post("/users/new", data = "<form>")]
|
||||
pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Result<Redirect, Ructe> {
|
||||
if !Instance::get_local(&*conn)
|
||||
pub fn create(
|
||||
form: LenientForm<NewUserForm>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Flash<Redirect>, Ructe> {
|
||||
let conn = &*rockets.conn;
|
||||
if !Instance::get_local()
|
||||
.map(|i| i.open_registrations)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
return Ok(Redirect::to(uri!(new))); // Actually, it is an error
|
||||
return Ok(Flash::error(
|
||||
Redirect::to(uri!(new)),
|
||||
i18n!(
|
||||
rockets.intl.catalog,
|
||||
"Registrations are closed on this instance."
|
||||
),
|
||||
)); // Actually, it is an error
|
||||
}
|
||||
|
||||
let mut form = form.into_inner();
|
||||
@@ -471,7 +512,7 @@ pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Resul
|
||||
form.validate()
|
||||
.and_then(|_| {
|
||||
NewUser::new_local(
|
||||
&*conn,
|
||||
conn,
|
||||
form.username.to_string(),
|
||||
form.username.to_string(),
|
||||
false,
|
||||
@@ -480,12 +521,18 @@ pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Resul
|
||||
User::hash_pass(&form.password).map_err(to_validation)?,
|
||||
)
|
||||
.map_err(to_validation)?;
|
||||
Ok(Redirect::to(uri!(super::session::new: m = _)))
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(super::session::new: m = _)),
|
||||
i18n!(
|
||||
rockets.intl.catalog,
|
||||
"Your account has been created. Now you just need to log in, before you can use it."
|
||||
),
|
||||
))
|
||||
})
|
||||
.map_err(|err| {
|
||||
render!(users::new(
|
||||
&(&*conn, &intl.catalog, None),
|
||||
Instance::get_local(&*conn)
|
||||
&rockets.to_context(),
|
||||
Instance::get_local()
|
||||
.map(|i| i.open_registrations)
|
||||
.unwrap_or(true),
|
||||
&form,
|
||||
@@ -542,7 +589,7 @@ pub fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>>
|
||||
let author = User::find_by_fqn(&rockets, &name).ok()?;
|
||||
let feed = FeedBuilder::default()
|
||||
.title(author.display_name.clone())
|
||||
.id(Instance::get_local(conn)
|
||||
.id(Instance::get_local()
|
||||
.unwrap()
|
||||
.compute_box("@", &name, "atom.xml"))
|
||||
.entries(
|
||||
|
||||
+36
-2
@@ -1,4 +1,4 @@
|
||||
use plume_models::{notifications::*, users::User, Connection};
|
||||
use plume_models::{notifications::*, users::User, Connection, PlumeRocket};
|
||||
|
||||
use rocket::http::hyper::header::{ETag, EntityTag};
|
||||
use rocket::http::{Method, Status};
|
||||
@@ -13,7 +13,41 @@ pub use askama_escape::escape;
|
||||
|
||||
pub static CACHE_NAME: &str = env!("CACHE_ID");
|
||||
|
||||
pub type BaseContext<'a> = &'a (&'a Connection, &'a Catalog, Option<User>);
|
||||
pub type BaseContext<'a> = &'a (
|
||||
&'a Connection,
|
||||
&'a Catalog,
|
||||
Option<User>,
|
||||
Option<(String, String)>,
|
||||
);
|
||||
|
||||
pub trait IntoContext {
|
||||
fn to_context(
|
||||
&self,
|
||||
) -> (
|
||||
&Connection,
|
||||
&Catalog,
|
||||
Option<User>,
|
||||
Option<(String, String)>,
|
||||
);
|
||||
}
|
||||
|
||||
impl IntoContext for PlumeRocket {
|
||||
fn to_context(
|
||||
&self,
|
||||
) -> (
|
||||
&Connection,
|
||||
&Catalog,
|
||||
Option<User>,
|
||||
Option<(String, String)>,
|
||||
) {
|
||||
(
|
||||
&*self.conn,
|
||||
&self.intl.catalog,
|
||||
self.user.clone(),
|
||||
self.flash_msg.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Ructe(pub Vec<u8>);
|
||||
|
||||
+160
-33
@@ -1,7 +1,70 @@
|
||||
// Heading
|
||||
main header.article {
|
||||
overflow: hidden;
|
||||
background: $white;
|
||||
color: $black;
|
||||
display: grid;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
|
||||
&.illustrated {
|
||||
min-height: 75vh;
|
||||
color: $white;
|
||||
|
||||
a, a:visited {
|
||||
color: $white;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-bottom-color 0.1s ease-in;
|
||||
|
||||
&:hover {
|
||||
border-bottom-color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > * {
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
& > div:not(.shadow) {
|
||||
z-index: 3;
|
||||
font-family: $lora;
|
||||
font-size: 1.2em;
|
||||
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-width: $article-width;
|
||||
margin: 2em auto;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: end;
|
||||
|
||||
h1, .article-info {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
& > div.shadow {
|
||||
z-index: 2;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: linear-gradient(180deg, transparent 20vh, black 80vh);
|
||||
}
|
||||
|
||||
& > img {
|
||||
z-index: 1;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
background: $purple;
|
||||
}
|
||||
}
|
||||
|
||||
main .article-info {
|
||||
max-width: 40rem;
|
||||
margin: 0 auto 3em;
|
||||
margin: 0 auto 2em;
|
||||
font-size: 0.95em;
|
||||
font-weight: 400;
|
||||
|
||||
@@ -12,8 +75,8 @@ main .article-info {
|
||||
|
||||
// The article itself
|
||||
main article {
|
||||
max-width: 40rem;
|
||||
margin: 2.5em auto;
|
||||
max-width: $article-width;
|
||||
margin: 1.5em auto;
|
||||
font-family: $lora;
|
||||
font-size: 1.2em;
|
||||
line-height: 1.7;
|
||||
@@ -24,15 +87,20 @@ main article {
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin: 3em auto;
|
||||
margin: 2em auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 1em;
|
||||
background: $lightgray;
|
||||
background: $gray;
|
||||
overflow: auto;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 5px solid $gray;
|
||||
margin: 1em auto;
|
||||
padding: 0em 2em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,37 +114,42 @@ main .article-meta, main .article-meta button {
|
||||
main .article-meta {
|
||||
|
||||
> * {
|
||||
margin: 0 20%;
|
||||
margin: $margin;
|
||||
}
|
||||
|
||||
> .banner {
|
||||
margin: 2em 0;
|
||||
& > * {
|
||||
margin: $margin;
|
||||
}
|
||||
}
|
||||
|
||||
> p {
|
||||
margin: 2em 20%;
|
||||
margin: 2em $horizontal-margin;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
// Tags
|
||||
// Article Tags
|
||||
.tags {
|
||||
list-style: none;
|
||||
display: inline-block;
|
||||
padding: 0px;
|
||||
margin-bottom: 2em;
|
||||
max-width: none;
|
||||
flex: 20;
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
background: $lightgray;
|
||||
padding: 0px;
|
||||
margin: 0px 10px 10px 0px;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s ease-in;
|
||||
border: 1px solid $purple;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
color: $black;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: mix($black, $lightgray, 10%);
|
||||
background: transparentize($purple, 0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,6 +184,7 @@ main .article-meta {
|
||||
color: $black;
|
||||
border: none;
|
||||
font-size: 1.1em;
|
||||
cursor: pointer;
|
||||
|
||||
svg.feather {
|
||||
transition: background 0.1s ease-in;
|
||||
@@ -185,11 +259,7 @@ main .article-meta {
|
||||
|
||||
// Comments
|
||||
.comments {
|
||||
margin: 0;
|
||||
> * {
|
||||
margin-left: 20%;
|
||||
margin-right: 20%;
|
||||
}
|
||||
margin: 0 $horizontal-margin;
|
||||
|
||||
h2 {
|
||||
color: $purple;
|
||||
@@ -204,17 +274,17 @@ main .article-meta {
|
||||
// New comment form
|
||||
> form input[type="submit"] {
|
||||
font-size: 1em;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
// Response/delete buttons
|
||||
// Respond & delete comment buttons
|
||||
a.button, form.inline, form.inline input {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
background: none;
|
||||
color: $black;
|
||||
border: none;
|
||||
margin-right: 2em;
|
||||
font-family: $route159;
|
||||
font-weight: normal;
|
||||
|
||||
&::before {
|
||||
color: $purple;
|
||||
@@ -224,18 +294,40 @@ main .article-meta {
|
||||
&:hover { color: $purple; }
|
||||
}
|
||||
|
||||
.list {
|
||||
display: grid;
|
||||
margin: 0 0 -5em;
|
||||
padding: 0 20%;
|
||||
background: $lightgray;
|
||||
}
|
||||
|
||||
.comment {
|
||||
padding: 2em;
|
||||
margin: 1em 0;
|
||||
font-size: 1em;
|
||||
border: none;
|
||||
|
||||
.content {
|
||||
background: $gray;
|
||||
margin-top: 2.5em;
|
||||
padding: 1em;
|
||||
|
||||
&::before {
|
||||
display: block;
|
||||
content: ' ';
|
||||
border: 1em solid $gray;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
position: relative;
|
||||
top: -2.4em;
|
||||
left: -1em;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.dt-published a {
|
||||
color: transparentize($black, 0.6);
|
||||
}
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -256,6 +348,10 @@ main .article-meta {
|
||||
}
|
||||
}
|
||||
|
||||
& > .comment {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.text {
|
||||
padding: 1.25em 0;
|
||||
font-family: $lora;
|
||||
@@ -274,6 +370,7 @@ main .article-meta {
|
||||
background: transparent;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
resize: auto;
|
||||
button {
|
||||
flex: 0 0 10em;
|
||||
font-size: 1.25em;
|
||||
@@ -303,7 +400,7 @@ main .article-meta {
|
||||
bottom: 20vh;
|
||||
left: 20vw;
|
||||
right: 20vw;
|
||||
background: $lightgray;
|
||||
background: $gray;
|
||||
border: 1px solid $purple;
|
||||
z-index: 2;
|
||||
padding: 2em;
|
||||
@@ -312,6 +409,7 @@ main .article-meta {
|
||||
|
||||
.popup:not(.show), .popup-bg:not(.show) {
|
||||
display: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.popup-bg {
|
||||
@@ -328,10 +426,16 @@ main .article-meta {
|
||||
.cw-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.cw-text {
|
||||
display: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
input[type="checkbox"].cw-checkbox {
|
||||
@@ -355,3 +459,26 @@ input:checked ~ .cw-container > .cw-text {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
// Bottom action bar
|
||||
|
||||
.bottom-bar {
|
||||
z-index: 10;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: $gray;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
|
||||
& > div {
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
& > div:nth-child(2) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
margin: auto $horizontal-margin;
|
||||
}
|
||||
}
|
||||
|
||||
+35
-31
@@ -10,11 +10,11 @@ input, textarea, select {
|
||||
margin: auto;
|
||||
padding: 1em;
|
||||
box-sizing: border-box;
|
||||
-webkit-appearance: textarea;
|
||||
|
||||
background: $form-input-background;
|
||||
color: $black;
|
||||
border: none;
|
||||
border: solid $lightgray thin;
|
||||
border: solid $black thin;
|
||||
|
||||
font-size: 1.2em;
|
||||
font-weight: 400;
|
||||
@@ -23,11 +23,14 @@ input, textarea, select {
|
||||
border-color: $purple;
|
||||
}
|
||||
}
|
||||
form input[type="submit"] { margin: 2em auto; }
|
||||
form input[type="submit"] {
|
||||
margin: 1.5em auto;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
|
||||
overflow-y: scroll;
|
||||
font-family: $lora;
|
||||
font-size: 1.1em;
|
||||
line-height: 1.5;
|
||||
@@ -38,6 +41,7 @@ input[type="checkbox"] {
|
||||
margin: initial;
|
||||
min-width: initial;
|
||||
width: initial;
|
||||
-webkit-appearance: checkbox;
|
||||
}
|
||||
|
||||
/** Inline forms (containing only CSRF token and a <submit>, for protected links) **/
|
||||
@@ -50,11 +54,10 @@ form.inline {
|
||||
|
||||
input[type="submit"] {
|
||||
display: inline-block;
|
||||
color: $purple;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
width: auto;
|
||||
-webkit-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
|
||||
&:not(.button) {
|
||||
margin: 0;
|
||||
@@ -62,41 +65,51 @@ form.inline {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: $purple;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button, input[type="submit"], button {
|
||||
.button, .button:visited, input[type="submit"], input[type="submit"].button {
|
||||
transition: all 0.1s ease-in;
|
||||
display: inline-block;
|
||||
-webkit-appearance: none;
|
||||
|
||||
border-radius: 0.5em;
|
||||
margin: 0.5em auto;
|
||||
padding: 0.75em 1em;
|
||||
|
||||
background: transparent;
|
||||
color: $purple;
|
||||
border: 1px solid $purple;
|
||||
background: $purple;
|
||||
color: $white;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: transparentize($purple, 0.6);
|
||||
color: white;
|
||||
background: transparentize($purple, 0.1);
|
||||
}
|
||||
|
||||
&.destructive {
|
||||
color: $red;
|
||||
border-color: $red;
|
||||
background: $red;
|
||||
|
||||
&:hover {
|
||||
background: transparentize($red, 0.6);
|
||||
color: $white;
|
||||
background: transparentize($red, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: $gray;
|
||||
color: $black;
|
||||
|
||||
&:hover {
|
||||
background: transparentize($black, 0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
input[type="submit"] {
|
||||
display: block;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
input[type="submit"] { display: block; }
|
||||
|
||||
// Writing page
|
||||
form.new-post {
|
||||
@@ -114,20 +127,10 @@ form.new-post {
|
||||
}
|
||||
textarea {
|
||||
min-height: 20em;
|
||||
overflow-y: hidden;
|
||||
resize: none;
|
||||
overflow-y: scroll;
|
||||
resize: vertical;
|
||||
-webkit-appearance: textarea;
|
||||
}
|
||||
input[type="submit"] {
|
||||
background: $lightgray;
|
||||
color: $black;
|
||||
border: none;
|
||||
-webkit-appearance: none;
|
||||
|
||||
font-family: $playfair;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
input[type="submit"]:hover { background: $lightgray; }
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.button + .button {
|
||||
@@ -137,6 +140,7 @@ form.new-post {
|
||||
.split {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
|
||||
& > * {
|
||||
flex-grow: 1;
|
||||
|
||||
+87
-35
@@ -4,11 +4,12 @@ html, body {
|
||||
background: $background;
|
||||
color: $black;
|
||||
font-family: $route159;
|
||||
::selection {
|
||||
background: $lightpurple;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: transparentize($purple, 0.7);
|
||||
}
|
||||
::-moz-selection {
|
||||
background: $lightpurple;
|
||||
background: transparentize($purple, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,12 +38,15 @@ small {
|
||||
padding: 5em;
|
||||
}
|
||||
|
||||
.spaced {
|
||||
margin: 4rem 0;
|
||||
.right {
|
||||
text-align: right;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cards.spaced {
|
||||
margin: 1rem 0 5rem;
|
||||
.spaced {
|
||||
margin: 4rem 0;
|
||||
}
|
||||
|
||||
.banner {
|
||||
@@ -54,25 +58,31 @@ small {
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
/// Main
|
||||
body > main > *, .h-feed > * {
|
||||
margin: 0 20%;
|
||||
margin: 1em $horizontal-margin;
|
||||
}
|
||||
|
||||
body > main > .h-entry, .h-feed {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body > main {
|
||||
min-height: 70vh;
|
||||
}
|
||||
|
||||
main {
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: $route159;
|
||||
line-height: 1.15;
|
||||
font-weight: 300;
|
||||
|
||||
&.article {
|
||||
max-width: 40rem;
|
||||
max-width: $article-width;
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
@@ -93,7 +103,6 @@ main {
|
||||
font-weight: 300;
|
||||
|
||||
&.article {
|
||||
margin: auto;
|
||||
font-size: 1.25em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
@@ -125,8 +134,6 @@ main {
|
||||
p.error {
|
||||
color: $red;
|
||||
font-weight: bold;
|
||||
max-width: 40rem;
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
/// User page
|
||||
@@ -148,7 +155,6 @@ p.error {
|
||||
background: $white;
|
||||
color: $purple;
|
||||
border: 1px solid $purple;
|
||||
border-radius: 1em;
|
||||
|
||||
font-size: 1rem;
|
||||
}
|
||||
@@ -163,6 +169,7 @@ p.error {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 5%;
|
||||
margin: 1rem 0 5rem;
|
||||
}
|
||||
.card {
|
||||
flex: 1;
|
||||
@@ -212,24 +219,44 @@ p.error {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
.list .card {
|
||||
/* TODO */
|
||||
background: 0;
|
||||
margin: 2em 0;
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
|
||||
.list > .card {
|
||||
background: transparent;
|
||||
margin: 2em 0;
|
||||
min-height: 3em;
|
||||
|
||||
padding: 1em;
|
||||
transition: background 0.1s ease-in;
|
||||
|
||||
&:hover {
|
||||
background-color: $gray;
|
||||
}
|
||||
|
||||
&.compact {
|
||||
margin: 0;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Instance presentation
|
||||
.presentation > h2, .presentation > a {
|
||||
text-align: center;
|
||||
}
|
||||
.presentation > a {
|
||||
font-size: 1.2em;
|
||||
margin: 1em;
|
||||
.presentation {
|
||||
max-width: none;
|
||||
|
||||
& > h2, & > a {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
& > a {
|
||||
font-size: 1.2em;
|
||||
margin: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
// Stats
|
||||
// Stats
|
||||
.stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
@@ -281,13 +308,14 @@ p.error {
|
||||
|
||||
.grow {
|
||||
flex: 1;
|
||||
margin: 0 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.left-icon {
|
||||
align-self: center;
|
||||
padding: 1em;
|
||||
background: $lightgray;
|
||||
background: $gray;
|
||||
border-radius: 50px;
|
||||
margin: 1em;
|
||||
margin-right: 2em;
|
||||
@@ -297,13 +325,37 @@ p.error {
|
||||
body > footer {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: space-between;
|
||||
background: $lightgray;
|
||||
padding: 0 20%;
|
||||
justify-content: space-around;
|
||||
background: $purple;
|
||||
color: $white;
|
||||
margin-top: 5em;
|
||||
|
||||
* {
|
||||
margin: 5em 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
transform: skew(-15deg);
|
||||
background: $white;
|
||||
border: none;
|
||||
width: .2em;
|
||||
}
|
||||
|
||||
a, a:visited {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-basis: 20%;
|
||||
margin: 2em 0;
|
||||
transition: all 0.1s ease-in;
|
||||
|
||||
& > * {
|
||||
display: block;
|
||||
margin: 1em 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,9 +437,9 @@ figure {
|
||||
|
||||
/// Tabs
|
||||
.tabs {
|
||||
border-bottom: 1px solid $lightgray;
|
||||
border-bottom: 1px solid $gray;
|
||||
padding: 0px;
|
||||
margin: auto 20% 2em;
|
||||
margin: auto $horizontal-margin 2em;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
|
||||
@@ -400,5 +452,5 @@ figure {
|
||||
color: $purple;
|
||||
border-bottom: 1px solid $purple;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
-5
@@ -1,5 +1,5 @@
|
||||
header {
|
||||
background: $lightgray;
|
||||
body > header {
|
||||
background: $gray;
|
||||
|
||||
#content {
|
||||
display: flex;
|
||||
@@ -10,6 +10,7 @@ header {
|
||||
nav#menu {
|
||||
position: relative;
|
||||
display: none;
|
||||
appearance: none;
|
||||
transform: skewX(-15deg);
|
||||
left: -1em;
|
||||
padding: 1em 1em 1em 2em;
|
||||
@@ -26,7 +27,7 @@ header {
|
||||
height: 1.4em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: $lightgray;
|
||||
color: $gray;
|
||||
font-size: 1.33em;
|
||||
}
|
||||
}
|
||||
@@ -76,15 +77,28 @@ header {
|
||||
|
||||
.messages {
|
||||
& > * {
|
||||
padding: 1em 20%;
|
||||
padding: 1em 20%;
|
||||
margin: 0;
|
||||
max-width: initial;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p.error {
|
||||
color: $red;
|
||||
color: darken($red, 20%);
|
||||
background: lighten($red, 40%);
|
||||
margin: 0;
|
||||
max-width: initial;
|
||||
}
|
||||
|
||||
p.warning {
|
||||
color: darken($yellow, 20%);
|
||||
background: lighten($yellow, 40%);
|
||||
}
|
||||
|
||||
p.success {
|
||||
color: darken($green, 20%);
|
||||
background: lighten($green, 40%);
|
||||
}
|
||||
}
|
||||
|
||||
// Only enable label animations on normal screens
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
// Colors
|
||||
$background: #F4F4F4;
|
||||
$form-input-background: #FFFFFF;
|
||||
$gray: #E3E3E3;
|
||||
$lightgray: #ECECEC;
|
||||
// Color Scheme
|
||||
$gray: #F3F3F3;
|
||||
$black: #242424;
|
||||
$white: #F4F4F4;
|
||||
$white: #F8F8F8;
|
||||
$purple: #7765E3;
|
||||
$lightpurple: #c2bbee;
|
||||
$red: #E92F2F;
|
||||
$yellow: #ffe347;
|
||||
$green: #23f0c7;
|
||||
|
||||
$background: $white;
|
||||
$form-input-background: white;
|
||||
|
||||
// Dimensions
|
||||
|
||||
$article-width: 70ch;
|
||||
$horizontal-margin: 20%;
|
||||
$margin: 0 $horizontal-margin;
|
||||
|
||||
// Fonts
|
||||
|
||||
|
||||
+43
-12
@@ -32,20 +32,24 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
body > header {
|
||||
flex-direction: column;
|
||||
|
||||
nav#menu {
|
||||
display: inline-flex;
|
||||
z-index: 21;
|
||||
}
|
||||
|
||||
#content {
|
||||
display: none;
|
||||
appearance: none;
|
||||
text-align: center;
|
||||
overflow-y: scroll;
|
||||
z-index: 20;
|
||||
}
|
||||
}
|
||||
|
||||
header:focus-within #content, #content.show {
|
||||
body > header:focus-within #content, #content.show {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -103,18 +107,15 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
body > main > * {
|
||||
padding: 0 5%;
|
||||
}
|
||||
main .article-meta {
|
||||
> * {
|
||||
margin: 0 5%;
|
||||
> *, .comments {
|
||||
margin: 0 3%;
|
||||
}
|
||||
> p {
|
||||
margin: 2em 5%;
|
||||
margin: 1em 3%;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.comments > * { margin: auto 5%; }
|
||||
.comments > * { margin: auto 3%; }
|
||||
.comments .comment { padding: 2em 0px; }
|
||||
}
|
||||
main .article-info, main article, main h1.article, main h2.article {
|
||||
@@ -127,7 +128,7 @@ html {
|
||||
}
|
||||
|
||||
.tabs {
|
||||
margin: auto 0px 2em;
|
||||
margin: auto 0px 1em;
|
||||
}
|
||||
|
||||
.stats { flex-direction: column; }
|
||||
@@ -135,11 +136,41 @@ html {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
body > footer * { margin: 1em auto; }
|
||||
body > footer * {
|
||||
margin: 1em auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.flex.wrap { flex-direction: column; }
|
||||
|
||||
.cards {
|
||||
.cards, .list {
|
||||
margin: 1rem 0 5rem;
|
||||
}
|
||||
|
||||
.split {
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
|
||||
& > * {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
& > div {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
main .article-meta .comments .comment {
|
||||
header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-7
@@ -1,4 +1,5 @@
|
||||
@use plume_models::CONFIG;
|
||||
@use plume_models::instance::Instance;
|
||||
@use template_utils::*;
|
||||
@use routes::*;
|
||||
@(ctx: BaseContext, title: String, head: Content, header: Content, content: Content)
|
||||
@@ -69,17 +70,30 @@
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<div class="messages">
|
||||
@if let Some(ref message) = ctx.3 {
|
||||
<p class="flash-message @message.0">@message.1</p>
|
||||
}
|
||||
</div>
|
||||
<main>
|
||||
@:content()
|
||||
</main>
|
||||
<footer>
|
||||
<span>@concat!("Plume ", env!("CARGO_PKG_VERSION"))</span>
|
||||
<a href="@uri!(instance::about)">@i18n!(ctx.1, "About this instance")</a>
|
||||
<a href="https://github.com/Plume-org/Plume">@i18n!(ctx.1, "Source code")</a>
|
||||
<a href="https://riot.im/app/#/room/#plume:disroot.org">@i18n!(ctx.1, "Matrix room")</a>
|
||||
@if ctx.2.clone().map(|a| a.is_admin).unwrap_or(false) {
|
||||
<a href="@uri!(instance::admin)">@i18n!(ctx.1, "Administration")</a>
|
||||
}
|
||||
<hr/>
|
||||
<div>
|
||||
<h3>@Instance::get_local().map(|i| i.name).unwrap_or_default()</h3>
|
||||
<a href="@uri!(instance::about)">@i18n!(ctx.1, "About this instance")</a>
|
||||
<a href="@uri!(instance::privacy)">@i18n!(ctx.1, "Privacy policy")</a>
|
||||
@if ctx.2.clone().map(|a| a.is_admin).unwrap_or(false) {
|
||||
<a href="@uri!(instance::admin)">@i18n!(ctx.1, "Administration")</a>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<h3>@concat!("Plume ", env!("CARGO_PKG_VERSION"))</h3>
|
||||
<a href="https://docs.joinplu.me">@i18n!(ctx.1, "Documentation")</a>
|
||||
<a href="https://github.com/Plume-org/Plume">@i18n!(ctx.1, "Source code")</a>
|
||||
<a href="https://riot.im/app/#/room/#plume:disroot.org">@i18n!(ctx.1, "Matrix room")</a>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="@uri!(plume_static_files: file = "plume-front.js", _build_id = CACHE_NAME)"></script>
|
||||
</body>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user