Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c73f993d0 | |||
| 777cbaa69d | |||
| 7ea4acc289 | |||
| 6dbc043a7e | |||
| fb1668ede7 | |||
| 3d27e283ad | |||
| 4b205fa995 | |||
| e7126ae335 | |||
| b2312d7f1b | |||
| 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
|
version: 2.1
|
||||||
|
|
||||||
aliases:
|
executors:
|
||||||
- &plume-docker
|
default:
|
||||||
image: plumeorg/plume-buildenv:v0.0.5
|
parameters:
|
||||||
- &defaults
|
postgres:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
selenium:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
docker:
|
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
|
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:
|
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
|
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:
|
commands:
|
||||||
keys:
|
restore_env:
|
||||||
- v2-plume-notest-{{ checksum "Cargo.lock" }}
|
description: checkout and pull cache
|
||||||
- v2-plume-notest
|
parameters:
|
||||||
- &save_cache
|
cache:
|
||||||
save_cache:
|
type: enum
|
||||||
key: v2-plume-notest-{{ checksum "Cargo.lock" }}
|
default: none
|
||||||
paths:
|
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/
|
- ~/.cargo/
|
||||||
- ./target
|
- ./target
|
||||||
|
|
||||||
- &restore_cache_web
|
clippy:
|
||||||
restore_cache:
|
description: run cargo clippy
|
||||||
keys:
|
parameters:
|
||||||
- v2-plume-web-{{ checksum "Cargo.lock" }}
|
package:
|
||||||
- v2-plume-web
|
type: string
|
||||||
- &save_cache_web
|
default: plume
|
||||||
save_cache:
|
no_feature:
|
||||||
key: v2-plume-web-{{ checksum "Cargo.lock" }}
|
type: boolean
|
||||||
paths:
|
default: false
|
||||||
- ~/.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
|
|
||||||
steps:
|
steps:
|
||||||
- *attach_workspace
|
- run: cargo clippy <<^parameters.no_feature>>--no-default-features --features="${FEATURES}"<</parameters.no_feature>> --release -p <<parameters.package>> -- -D warnings
|
||||||
- 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
|
|
||||||
|
|
||||||
- &test_browser
|
run_with_coverage:
|
||||||
|
description: run command with environment for coverage
|
||||||
|
parameters:
|
||||||
|
cmd:
|
||||||
|
type: string
|
||||||
steps:
|
steps:
|
||||||
- *attach_workspace
|
- run: |
|
||||||
- run:
|
export RUSTFLAGS="-Zprofile -Zfewer-names -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zno-landing-pads -Clink-arg=-Xlinker -Clink-arg=--no-keep-memory -Clink-arg=-Xlinker -Clink-arg=--reduce-memory-overheads"
|
||||||
name: Set cache key
|
export CARGO_INCREMENTAL=0
|
||||||
command: echo "$FEATURES" > /FEATURES
|
<< parameters.cmd >>
|
||||||
- *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
|
|
||||||
|
|
||||||
- &release
|
upload_coverage:
|
||||||
|
description: merge coverage files and upload to codecov.io
|
||||||
|
parameters:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
steps:
|
steps:
|
||||||
- *attach_workspace
|
- run: zip -0 ccov.zip `find . -name 'plume*.gc*' -o -name 'plm*.gc*'`
|
||||||
- *restore_cache_release
|
- run: grcov ccov.zip -s . -t lcov --llvm --branch --ignore-not-existing --ignore-dir '/*' -o lcov.info
|
||||||
- run:
|
- run: bash <(curl -s https://codecov.io/bash) -f lcov.info -F <<parameters.type>>
|
||||||
name: build frontend
|
- run: find . -name 'plume*.gc*' -delete -o -name 'plm*.gc*' -delete
|
||||||
command: cargo web deploy -p plume-front --release
|
- run: rm ccov.zip lcov.info
|
||||||
- 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
|
|
||||||
|
|
||||||
|
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:
|
jobs:
|
||||||
download_deps:
|
cargo fmt:
|
||||||
<<: *defaults
|
executor:
|
||||||
|
name: default
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- restore_env
|
||||||
- *attach_workspace
|
- run: cargo fmt --all -- --check
|
||||||
- *restore_cache
|
|
||||||
- run: git config --global --remove-section url."ssh://git@github.com"
|
|
||||||
- run: cargo fetch
|
|
||||||
- *save_cache
|
|
||||||
- *persist_to_workspace
|
|
||||||
|
|
||||||
cargo_fmt:
|
clippy:
|
||||||
<<: *defaults
|
parameters:
|
||||||
|
postgres:
|
||||||
|
type: boolean
|
||||||
|
executor:
|
||||||
|
name: default
|
||||||
|
postgres: << parameters.postgres >>
|
||||||
steps:
|
steps:
|
||||||
- *attach_workspace
|
- restore_env:
|
||||||
- run: cargo fmt --all -- --check
|
cache: clippy
|
||||||
|
- clippy
|
||||||
|
- clippy:
|
||||||
|
package: plume-cli
|
||||||
|
- clippy:
|
||||||
|
package: plume-front
|
||||||
|
no_feature: true
|
||||||
|
- cache:
|
||||||
|
cache: clippy
|
||||||
|
|
||||||
build_web:
|
unit:
|
||||||
<<: *defaults
|
parameters:
|
||||||
|
postgres:
|
||||||
|
type: boolean
|
||||||
|
executor:
|
||||||
|
name: default
|
||||||
|
postgres: << parameters.postgres >>
|
||||||
steps:
|
steps:
|
||||||
- *attach_workspace
|
- restore_env:
|
||||||
- *restore_cache_web
|
cache: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
|
||||||
- run: cargo clippy -p plume-front -- -D warnings
|
- run_with_coverage:
|
||||||
- run: cargo web deploy -p plume-front
|
cmd: |
|
||||||
- *save_cache_web
|
cmd="cargo test --all --exclude plume-front --exclude plume-macro --no-run --no-default-features --features=${FEATURES} -j"
|
||||||
- *persist_to_workspace
|
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:
|
integration:
|
||||||
<<: *postgresql
|
parameters:
|
||||||
<<: *env_postgresql
|
postgres:
|
||||||
<<: *test_cli
|
type: boolean
|
||||||
|
executor:
|
||||||
test_cli_sqlite:
|
name: default
|
||||||
<<: *defaults
|
postgres: << parameters.postgres >>
|
||||||
<<: *env_sqlite
|
selenium: true
|
||||||
<<: *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
|
|
||||||
steps:
|
steps:
|
||||||
- run: /bin/true
|
- restore_env:
|
||||||
release_postgresql:
|
cache: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
|
||||||
<<: *defaults
|
- run: cargo web deploy -p plume-front
|
||||||
<<: *env_postgresql
|
- run_with_coverage:
|
||||||
<<: *release
|
cmd: |
|
||||||
release_sqlite:
|
cmd="cargo install --debug --no-default-features --features="${FEATURES}",test --force --path . -j"
|
||||||
<<: *defaults
|
for i in 36 4 2 1 1; do
|
||||||
<<: *env_sqlite
|
$cmd $i && exit 0
|
||||||
<<: *release
|
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:
|
workflows:
|
||||||
version: 2
|
version: 2
|
||||||
build_and_test:
|
build and test:
|
||||||
jobs:
|
jobs:
|
||||||
- download_deps
|
- cargo fmt
|
||||||
- cargo_fmt:
|
- clippy:
|
||||||
requires:
|
postgres: false
|
||||||
- download_deps
|
- clippy:
|
||||||
- build_web:
|
postgres: true
|
||||||
requires:
|
- unit:
|
||||||
- download_deps
|
postgres: false
|
||||||
- test_cli_postgresql:
|
- unit:
|
||||||
requires:
|
postgres: true
|
||||||
- download_deps
|
- integration:
|
||||||
- test_cli_sqlite:
|
postgres: false
|
||||||
requires:
|
- integration:
|
||||||
- download_deps
|
postgres: true
|
||||||
- test_unit_postgresql:
|
- release:
|
||||||
requires:
|
postgres: false
|
||||||
- download_deps
|
- release:
|
||||||
- test_unit_sqlite:
|
postgres: true
|
||||||
requires:
|
- push translations:
|
||||||
- download_deps
|
filters:
|
||||||
- test_browser_postgresql:
|
branches:
|
||||||
requires:
|
only:
|
||||||
- build_web
|
- /^master/
|
||||||
- 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
|
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ ENV PATH="/root/.cargo/bin:${PATH}"
|
|||||||
|
|
||||||
#install native/circleci/build dependancies
|
#install native/circleci/build dependancies
|
||||||
RUN apt update &&\
|
RUN apt update &&\
|
||||||
apt install -y git ssh tar gzip ca-certificates &&\
|
apt install -y --no-install-recommends git ssh tar gzip ca-certificates default-jre&&\
|
||||||
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 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
|
#install and configure rust
|
||||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2019-03-23 -y &&\
|
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-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
|
#compile some deps
|
||||||
RUN cargo install cargo-web &&\
|
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
|
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
|
#set some compilation parametters
|
||||||
COPY cargo_config /root/.cargo/config
|
COPY cargo_config /root/.cargo/config
|
||||||
|
|
||||||
@@ -30,3 +27,11 @@ RUN pip3 install selenium
|
|||||||
#install and configure caddy
|
#install and configure caddy
|
||||||
RUN curl https://getcaddy.com | bash -s personal
|
RUN curl https://getcaddy.com | bash -s personal
|
||||||
COPY Caddyfile /Caddyfile
|
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]
|
[target.x86_64-unknown-linux-gnu]
|
||||||
# link dead code for coverage, attempt to reduce linking memory usage to not get killed
|
# 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"]
|
||||||
|
|||||||
+2
-46
@@ -1,46 +1,2 @@
|
|||||||
# Contributor Covenant Code of Conduct
|
You can read our Code of Conduct [here](https://docs.joinplu.me/organization/code-of-conduct).
|
||||||
|
By contributing to this repository, you agree to be bound by this Code of Conduct.
|
||||||
## Our Pledge
|
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, sexual identity and orientation, etc.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment include:
|
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
|
||||||
* Being respectful of differing viewpoints and experiences
|
|
||||||
* Gracefully accepting constructive criticism
|
|
||||||
* Focusing on what is best for the community
|
|
||||||
* Showing empathy towards other community members
|
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
||||||
* Public or private harassment
|
|
||||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
|
||||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
|
||||||
|
|
||||||
## Our Responsibilities
|
|
||||||
|
|
||||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at baptiste@gelez.xyz. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
|
||||||
|
|
||||||
[homepage]: http://contributor-covenant.org
|
|
||||||
[version]: http://contributor-covenant.org/version/1/4/
|
|
||||||
|
|||||||
Generated
+817
-546
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"
|
activitypub = "0.1.3"
|
||||||
askama_escape = "0.1"
|
askama_escape = "0.1"
|
||||||
atom_syndication = "0.6"
|
atom_syndication = "0.6"
|
||||||
canapi = "0.2"
|
colored = "1.8"
|
||||||
colored = "1.7"
|
dotenv = "0.14"
|
||||||
dotenv = "0.13"
|
|
||||||
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
||||||
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||||
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||||
@@ -22,12 +21,13 @@ num_cpus = "1.0"
|
|||||||
rocket = "0.4.0"
|
rocket = "0.4.0"
|
||||||
rocket_contrib = { version = "0.4.0", features = ["json"] }
|
rocket_contrib = { version = "0.4.0", features = ["json"] }
|
||||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||||
rpassword = "2.0"
|
rpassword = "3.0"
|
||||||
runtime-fmt = "0.3.0"
|
runtime-fmt = "0.3.0"
|
||||||
scheduled-thread-pool = "0.2.0"
|
scheduled-thread-pool = "0.2.0"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_qs = "0.4"
|
serde_qs = "0.4"
|
||||||
|
shrinkwraprs = "0.2.1"
|
||||||
validator = "0.8"
|
validator = "0.8"
|
||||||
validator_derive = "0.8"
|
validator_derive = "0.8"
|
||||||
webfinger = "0.3.1"
|
webfinger = "0.3.1"
|
||||||
@@ -42,7 +42,7 @@ version = "0.4"
|
|||||||
|
|
||||||
[dependencies.ctrlc]
|
[dependencies.ctrlc]
|
||||||
features = ["termination"]
|
features = ["termination"]
|
||||||
version = "3.1.1"
|
version = "3.1.2"
|
||||||
|
|
||||||
[dependencies.diesel]
|
[dependencies.diesel]
|
||||||
features = ["r2d2", "chrono"]
|
features = ["r2d2", "chrono"]
|
||||||
@@ -67,7 +67,7 @@ git = "https://github.com/fdb-hiroshima/rocket_csrf"
|
|||||||
rev = "4a72ea2ec716cb0b26188fb00bccf2ef7d1e031c"
|
rev = "4a72ea2ec716cb0b26188fb00bccf2ef7d1e031c"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
ructe = "0.5.6"
|
ructe = "0.6.2"
|
||||||
rsass = "0.9"
|
rsass = "0.9"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
@@ -78,4 +78,4 @@ debug-mailer = []
|
|||||||
test = []
|
test = []
|
||||||
|
|
||||||
[workspace]
|
[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
|
WORKDIR /app
|
||||||
COPY Cargo.toml Cargo.lock rust-toolchain ./
|
COPY Cargo.toml Cargo.lock rust-toolchain ./
|
||||||
RUN cargo install diesel_cli --no-default-features --features postgres --version '=1.3.0'
|
|
||||||
RUN cargo install cargo-web
|
RUN cargo install cargo-web
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
@@ -40,7 +39,6 @@ WORKDIR /app
|
|||||||
COPY --from=builder /app /app
|
COPY --from=builder /app /app
|
||||||
COPY --from=builder /usr/local/cargo/bin/plm /bin/
|
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/plume /bin/
|
||||||
COPY --from=builder /usr/local/cargo/bin/diesel /bin/
|
|
||||||
|
|
||||||
CMD ["plume"]
|
CMD ["plume"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
<!--
|
|
||||||
If your issue is not purely technical, it should be debated on Loomio first
|
|
||||||
|
|
||||||
https://framavox.org/g/WK40YHMA/plume
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Plume version (and/or commit):
|
|
||||||
@@ -3,27 +3,53 @@
|
|||||||
Plume
|
Plume
|
||||||
</h1>
|
</h1>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://travis-ci.org/Plume-org/Plume"><img src="https://travis-ci.org/Plume-org/Plume.svg?branch=master" alt="Build status"></a>
|
<a href="https://github.com/Plume-org/Plume/"><img alt="CircleCI" src="https://img.shields.io/circleci/build/gh/Plume-org/Plume.svg"></a>
|
||||||
<a href="hhttps://codecov.io/gh/Plume-org/Plume"><img src="https://codecov.io/gh/Plume-org/Plume/branch/master/graph/badge.svg" alt="Code coverage"></a>
|
<a href="https://codecov.io/gh/Plume-org/Plume"><img src="https://codecov.io/gh/Plume-org/Plume/branch/master/graph/badge.svg" alt="Code coverage"></a>
|
||||||
|
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/plume"><img src="https://d322cqt584bo4o.cloudfront.net/plume/localized.svg"></a>
|
||||||
|
<a href="https://hub.docker.com/r/plumeorg/plume"><img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/plumeorg/plume.svg"></a>
|
||||||
|
<a href="https://liberapay.com/Plume"><img alt="Liberapay patrons" src="https://img.shields.io/liberapay/patrons/Plume.svg"></a>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://joinplu.me/">Website</a>
|
<a href="https://joinplu.me/">Website</a>
|
||||||
—
|
—
|
||||||
<a href="https://docs.joinplu.me/">Documentation</a>
|
<a href="https://docs.joinplu.me/">Documentation</a>
|
||||||
—
|
—
|
||||||
<a href="https://contribute.joinplu.me/">Contribute</a>
|
<a href="https://docs.joinplu.me/contribute">Contribute</a>
|
||||||
—
|
—
|
||||||
<a href="https://joinplu.me/#instances">Instances list</a>
|
<a href="https://joinplu.me/#instances">Instances list</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Plume is a federated blogging engine, based on ActivityPub. It uses the Rocket framework, and Diesel to interact with the database.
|
Plume is a **federated blogging engine**, based on *ActivityPub*. It is written in *Rust*, with the *Rocket* framework, and *Diesel* to interact with the database.
|
||||||
|
The front-end uses *Ructe* templates, *WASM* and *SCSS*.
|
||||||
|
|
||||||
It is not yet ready for production use, but we have all the basic features (account management, blogs, articles, comments, etc) and a basic federation.
|
## Features
|
||||||
|
|
||||||
Feel free to join our Matrix room: `#plume:disroot.org` to discuss about the project!
|
A lot of features are still missing, but what is already here should be quite stable. Current and planned features include:
|
||||||
|
|
||||||
We also have a [Loomio group](https://framavox.org/g/WK40YHMA/plume) to debate and vote and features. Feel free to join it to take part in Plume's decision process.
|
- **A blog-centric approach**: you can create as much blogs as you want with your account, to keep your different publications separated.
|
||||||
|
- **Media management**: you can upload pictures to illustrate your articles, but also audio files if you host a podcast, and manage them all from Plume.
|
||||||
|
- **Federation**: Plume is part of a network of interconnected websites called the Fediverse. Each of these websites (often called *instances*) have their own
|
||||||
|
rules and thematics, but they can all communicate with each other.
|
||||||
|
- **Collaborative writing**: invite other people to your blogs, and write articles together.
|
||||||
|
|
||||||
For more informations on the different ways to contribute, check out our [contribution page](https://contribute.joinplu.me). We are welcoming any kind of contribution, and you can probably find a way to help.
|
## Get involved
|
||||||
|
|
||||||
As we want the various spaces related to the project (GitHub, Matrix, Loomio, etc) to be as safe as possible for everyone, we adopted [a code of conduct](https://github.com/Plume-org/Plume/blob/master/CODE_OF_CONDUCT.md). Please read it and make sure you accept it before contributing.
|
If you want to have regular news about the project, the best place is probably [our blog](https://fediverse.blog/~/PlumeDev), or our Matrix room: [`#plume:disroot.org`](https://riot.im/app/#/room/#plume:disroot.org).
|
||||||
|
|
||||||
|
If you want to contribute more, a good first step is to read [our contribution guides](https://docs.joinplu.me/contribute). We accept all kind of contribution:
|
||||||
|
|
||||||
|
- [Back-end or front-end development](https://docs.joinplu.me/contribute/development/)
|
||||||
|
- [Translations](https://docs.joinplu.me/contribute/translations/)
|
||||||
|
- [Documentation](https://docs.joinplu.me/contribute/documentation/)
|
||||||
|
- UI and/or UX design (we don't have a dedicated guide yet, but [we can talk](https://docs.joinplu.me/contribute/discussion/) to see how we can work together!)
|
||||||
|
- [Taking part in discussions](https://docs.joinplu.me/contribute/discussion/)
|
||||||
|
- [Financial support](https://docs.joinplu.me/contribute/donations/)
|
||||||
|
|
||||||
|
But this list is not exhaustive and if you want to contribute differently you are welcome too!
|
||||||
|
|
||||||
|
As we want the various spaces related to the project (GitHub, Matrix, Loomio, etc) to be as safe as possible for everyone, we adopted [a code of conduct](https://docs.joinplu.me/organization/code-of-conduct). Please read it and make sure you accept it before contributing.
|
||||||
|
|
||||||
|
## Starting your own instance
|
||||||
|
|
||||||
|
We provide various way to install Plume: from source, with pre-built binaries, with Docker or with YunoHost.
|
||||||
|
For detailed explanations, please refer to [the documentation](https://docs.joinplu.me/installation/).
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::process::{Command, Stdio};
|
|||||||
use std::{env, fs::*, io::Write, path::PathBuf};
|
use std::{env, fs::*, io::Write, path::PathBuf};
|
||||||
|
|
||||||
fn compute_static_hash() -> String {
|
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")
|
let find = Command::new("find")
|
||||||
.args(&["static/", "-type", "f", "!", "-path", "static/media/*"])
|
.args(&["static/", "-type", "f", "!", "-path", "static/media/*"])
|
||||||
@@ -20,17 +20,19 @@ fn compute_static_hash() -> String {
|
|||||||
.expect("failed sort command");
|
.expect("failed sort command");
|
||||||
|
|
||||||
let xargs = Command::new("xargs")
|
let xargs = Command::new("xargs")
|
||||||
.args(&["stat", "--printf='%n %Y\n'"])
|
.args(&["stat", "-c'%n %Y'"])
|
||||||
.stdin(sort.stdout.unwrap())
|
.stdin(sort.stdout.unwrap())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.spawn()
|
.spawn()
|
||||||
.expect("failed xargs command");
|
.expect("failed xargs command");
|
||||||
|
|
||||||
let sha = Command::new("sha256sum")
|
let mut sha = Command::new("openssl")
|
||||||
|
.args(&["dgst", "-r"])
|
||||||
.stdin(xargs.stdout.unwrap())
|
.stdin(xargs.stdout.unwrap())
|
||||||
.output()
|
.output()
|
||||||
.expect("failed sha256sum command");
|
.expect("failed openssl command");
|
||||||
|
|
||||||
|
sha.stdout.resize(64, 0);
|
||||||
String::from_utf8(sha.stdout).unwrap()
|
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");
|
let in_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("templates");
|
||||||
compile_templates(&in_dir, &out_dir).expect("compile 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");
|
let mut out = File::create("static/css/main.css").expect("Couldn't create main.css");
|
||||||
out.write_all(
|
out.write_all(
|
||||||
&rsass::compile_scss_file(
|
&rsass::compile_scss_file(
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
"project_identifier": "plume"
|
||||||
|
"api_key_env": CROWDIN_API_KEY
|
||||||
|
preserve_hierarchy: true
|
||||||
files:
|
files:
|
||||||
- source: /po/plume/plume.pot
|
- source: /po/plume/plume.pot
|
||||||
translation: /po/plume/%two_letters_code%.po
|
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 @@
|
|||||||
|
DROP TABLE password_reset_requests;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE password_reset_requests (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
email VARCHAR NOT NULL,
|
||||||
|
token VARCHAR NOT NULL,
|
||||||
|
expiration_date TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX password_reset_requests_token ON password_reset_requests (token);
|
||||||
|
CREATE UNIQUE INDEX password_reset_requests_email ON password_reset_requests (email);
|
||||||
@@ -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 @@
|
|||||||
|
DROP TABLE password_reset_requests;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE password_reset_requests (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
email VARCHAR NOT NULL,
|
||||||
|
token VARCHAR NOT NULL,
|
||||||
|
expiration_date DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX password_reset_requests_token ON password_reset_requests (token);
|
||||||
|
CREATE UNIQUE INDEX password_reset_requests_email ON password_reset_requests (email);
|
||||||
@@ -4,6 +4,5 @@ version = "0.3.0"
|
|||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
canapi = "0.2"
|
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
use canapi::Endpoint;
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
|
pub struct NewAppData {
|
||||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
|
||||||
pub struct AppEndpoint {
|
|
||||||
pub id: Option<i32>,
|
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub website: Option<String>,
|
pub website: Option<String>,
|
||||||
pub redirect_uri: 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;
|
extern crate serde;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_derive;
|
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 apps;
|
||||||
pub mod posts;
|
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)]
|
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||||
pub struct PostEndpoint {
|
pub struct NewPostData {
|
||||||
pub id: Option<i32>,
|
pub title: String,
|
||||||
pub title: Option<String>,
|
|
||||||
pub subtitle: Option<String>,
|
pub subtitle: Option<String>,
|
||||||
pub content: Option<String>,
|
pub source: String,
|
||||||
pub source: Option<String>,
|
pub author: String,
|
||||||
pub author: Option<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 blog_id: Option<i32>,
|
||||||
pub published: Option<bool>,
|
pub published: Option<bool>,
|
||||||
pub creation_date: Option<String>,
|
pub creation_date: Option<String>,
|
||||||
@@ -16,4 +14,18 @@ pub struct PostEndpoint {
|
|||||||
pub cover_id: Option<i32>,
|
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"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = "2.32"
|
clap = "2.33"
|
||||||
dotenv = "0.13"
|
dotenv = "0.14"
|
||||||
rpassword = "2.1"
|
rpassword = "3.0"
|
||||||
|
|
||||||
[dependencies.diesel]
|
[dependencies.diesel]
|
||||||
features = ["r2d2", "chrono"]
|
features = ["r2d2", "chrono"]
|
||||||
|
|||||||
+12
-6
@@ -6,10 +6,11 @@ extern crate rpassword;
|
|||||||
|
|
||||||
use clap::App;
|
use clap::App;
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
use plume_models::{Connection as Conn, CONFIG};
|
use plume_models::{instance::Instance, Connection as Conn, CONFIG};
|
||||||
use std::io::{self, prelude::*};
|
use std::io::{self, prelude::*};
|
||||||
|
|
||||||
mod instance;
|
mod instance;
|
||||||
|
mod migration;
|
||||||
mod search;
|
mod search;
|
||||||
mod users;
|
mod users;
|
||||||
|
|
||||||
@@ -19,23 +20,28 @@ fn main() {
|
|||||||
.version(env!("CARGO_PKG_VERSION"))
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
.about("Collection of tools to manage your Plume instance.")
|
.about("Collection of tools to manage your Plume instance.")
|
||||||
.subcommand(instance::command())
|
.subcommand(instance::command())
|
||||||
.subcommand(users::command())
|
.subcommand(migration::command())
|
||||||
.subcommand(search::command());
|
.subcommand(search::command())
|
||||||
|
.subcommand(users::command());
|
||||||
let matches = app.clone().get_matches();
|
let matches = app.clone().get_matches();
|
||||||
|
|
||||||
dotenv::dotenv().ok();
|
dotenv::dotenv().expect("error while reading .env");
|
||||||
let conn = Conn::establish(CONFIG.database_url.as_str());
|
let conn = Conn::establish(CONFIG.database_url.as_str());
|
||||||
|
let _ = conn.as_ref().map(|conn| Instance::cache_local(conn));
|
||||||
|
|
||||||
match matches.subcommand() {
|
match matches.subcommand() {
|
||||||
("instance", Some(args)) => {
|
("instance", Some(args)) => {
|
||||||
instance::run(args, &conn.expect("Couldn't connect to the database."))
|
instance::run(args, &conn.expect("Couldn't connect to the database."))
|
||||||
}
|
}
|
||||||
("users", Some(args)) => {
|
("migration", Some(args)) => {
|
||||||
users::run(args, &conn.expect("Couldn't connect to the database."))
|
migration::run(args, &conn.expect("Couldn't connect to the database."))
|
||||||
}
|
}
|
||||||
("search", Some(args)) => {
|
("search", Some(args)) => {
|
||||||
search::run(args, &conn.expect("Couldn't connect to the database."))
|
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"),
|
_ => 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 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::fs::{read_dir, remove_file};
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::path::Path;
|
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 path = Path::new(path).join("search_index");
|
||||||
let searcher = searcher.unwrap_or_else(|| Searcher::open(&path).unwrap());
|
let searcher = searcher.unwrap_or_else(|| Searcher::open(&path).unwrap());
|
||||||
|
|
||||||
let posts = posts::table
|
searcher.fill(conn).expect("Couldn't import post");
|
||||||
.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");
|
|
||||||
}
|
|
||||||
println!("Commiting result");
|
println!("Commiting result");
|
||||||
searcher.commit();
|
searcher.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unlock<'a>(args: &ArgMatches<'a>) {
|
fn unlock<'a>(args: &ArgMatches<'a>) {
|
||||||
let path = args.value_of("path").unwrap_or(".");
|
let path = args.value_of("path").unwrap_or(".");
|
||||||
let path = Path::new(path).join("search_index/.tantivy-indexer.lock");
|
let meta = Path::new(path).join("search_index/.tantivy-meta.lock");
|
||||||
|
remove_file(meta).unwrap();
|
||||||
remove_file(path).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(
|
let user = User::find_by_name(
|
||||||
conn,
|
conn,
|
||||||
&username,
|
&username,
|
||||||
Instance::get_local(conn)
|
Instance::get_local()
|
||||||
.expect("Failed to get local instance")
|
.expect("Failed to get local instance")
|
||||||
.id,
|
.id,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,13 +11,15 @@ array_tool = "1.0"
|
|||||||
base64 = "0.10"
|
base64 = "0.10"
|
||||||
heck = "0.3.0"
|
heck = "0.3.0"
|
||||||
hex = "0.3"
|
hex = "0.3"
|
||||||
hyper = "0.12.20"
|
hyper = "0.12.28"
|
||||||
openssl = "0.10.15"
|
openssl = "0.10.22"
|
||||||
rocket = "0.4.0"
|
rocket = "0.4.0"
|
||||||
reqwest = "0.9"
|
reqwest = "0.9"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
shrinkwraprs = "0.2.1"
|
||||||
|
tokio = "0.1.21"
|
||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
|
|||||||
@@ -279,7 +279,10 @@ pub trait FromId<C>: Sized {
|
|||||||
|
|
||||||
/// Dereferences an ID
|
/// Dereferences an ID
|
||||||
fn deref(id: &str) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
|
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)
|
.get(id)
|
||||||
.header(
|
.header(
|
||||||
ACCEPT,
|
ACCEPT,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use activitypub::{Activity, Link, Object};
|
use activitypub::{Activity, Link, Object};
|
||||||
use array_tool::vec::Uniq;
|
use array_tool::vec::Uniq;
|
||||||
use reqwest::Client;
|
use reqwest::r#async::ClientBuilder;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::Status,
|
http::Status,
|
||||||
request::{FromRequest, Request},
|
request::{FromRequest, Request},
|
||||||
@@ -8,6 +8,7 @@ use rocket::{
|
|||||||
Outcome,
|
Outcome,
|
||||||
};
|
};
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
use tokio::prelude::*;
|
||||||
|
|
||||||
use self::sign::Signable;
|
use self::sign::Signable;
|
||||||
|
|
||||||
@@ -129,47 +130,44 @@ where
|
|||||||
.sign(sender)
|
.sign(sender)
|
||||||
.expect("activity_pub::broadcast: signature error");
|
.expect("activity_pub::broadcast: signature error");
|
||||||
|
|
||||||
|
let mut rt = tokio::runtime::current_thread::Runtime::new()
|
||||||
|
.expect("Error while initializing tokio runtime for federation");
|
||||||
|
let client = ClientBuilder::new()
|
||||||
|
.connect_timeout(std::time::Duration::from_secs(5))
|
||||||
|
.build()
|
||||||
|
.expect("Can't build client");
|
||||||
for inbox in boxes {
|
for inbox in boxes {
|
||||||
// TODO: run it in Sidekiq or something like that
|
|
||||||
let body = signed.to_string();
|
let body = signed.to_string();
|
||||||
let mut headers = request::headers();
|
let mut headers = request::headers();
|
||||||
headers.insert("Digest", request::Digest::digest(&body));
|
headers.insert("Digest", request::Digest::digest(&body));
|
||||||
let res = Client::new()
|
rt.spawn(
|
||||||
.post(&inbox)
|
client
|
||||||
.headers(headers.clone())
|
.post(&inbox)
|
||||||
.header(
|
.headers(headers.clone())
|
||||||
"Signature",
|
.header(
|
||||||
request::signature(sender, &headers)
|
"Signature",
|
||||||
.expect("activity_pub::broadcast: request signature error"),
|
request::signature(sender, &headers)
|
||||||
)
|
.expect("activity_pub::broadcast: request signature error"),
|
||||||
.body(body)
|
)
|
||||||
.send();
|
.body(body)
|
||||||
match res {
|
.send()
|
||||||
Ok(mut r) => {
|
.and_then(|r| r.into_body().concat2())
|
||||||
println!("Successfully sent activity to inbox ({})", inbox);
|
.map(move |response| {
|
||||||
if let Ok(response) = r.text() {
|
println!("Successfully sent activity to inbox ({})", inbox);
|
||||||
println!("Response: \"{:?}\"\n\n", response)
|
println!("Response: \"{:?}\"\n", response)
|
||||||
} else {
|
})
|
||||||
println!("Error while reading response")
|
.map_err(|e| println!("Error while sending to inbox ({:?})", e)),
|
||||||
}
|
);
|
||||||
}
|
|
||||||
Err(e) => println!("Error while sending to inbox ({:?})", e),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
rt.run().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Shrinkwrap, Clone, Serialize, Deserialize)]
|
||||||
pub struct Id(String);
|
pub struct Id(String);
|
||||||
|
|
||||||
impl Id {
|
impl Id {
|
||||||
pub fn new<T: Into<String>>(id: T) -> Id {
|
pub fn new(id: impl ToString) -> Id {
|
||||||
Id(id.into())
|
Id(id.to_string())
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Into<String> for Id {
|
|
||||||
fn into(self) -> String {
|
|
||||||
self.0.clone()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,12 @@ extern crate reqwest;
|
|||||||
extern crate rocket;
|
extern crate rocket;
|
||||||
extern crate serde;
|
extern crate serde;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
extern crate shrinkwraprs;
|
||||||
|
#[macro_use]
|
||||||
extern crate serde_derive;
|
extern crate serde_derive;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
|
extern crate tokio;
|
||||||
|
|
||||||
pub mod activity_pub;
|
pub mod activity_pub;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|||||||
@@ -154,10 +154,15 @@ fn process_image<'a, 'b>(
|
|||||||
/// Returns (HTML, mentions, hashtags)
|
/// Returns (HTML, mentions, hashtags)
|
||||||
pub fn md_to_html<'a>(
|
pub fn md_to_html<'a>(
|
||||||
md: &str,
|
md: &str,
|
||||||
base_url: &str,
|
base_url: Option<&str>,
|
||||||
inline: bool,
|
inline: bool,
|
||||||
media_processor: Option<MediaProcessor<'a>>,
|
media_processor: Option<MediaProcessor<'a>>,
|
||||||
) -> (String, HashSet<String>, HashSet<String>) {
|
) -> (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 = Parser::new_ext(md, Options::all());
|
||||||
|
|
||||||
let (parser, mentions, hashtags): (Vec<Event>, Vec<String>, Vec<String>) = parser
|
let (parser, mentions, hashtags): (Vec<Event>, Vec<String>, Vec<String>) = parser
|
||||||
@@ -185,7 +190,7 @@ pub fn md_to_html<'a>(
|
|||||||
let mention = text_acc;
|
let mention = text_acc;
|
||||||
let short_mention = mention.splitn(1, '@').nth(0).unwrap_or("");
|
let short_mention = mention.splitn(1, '@').nth(0).unwrap_or("");
|
||||||
let link = Tag::Link(
|
let link = Tag::Link(
|
||||||
format!("//{}/@/{}/", base_url, &mention).into(),
|
format!("{}@/{}/", base_url, &mention).into(),
|
||||||
short_mention.to_owned().into(),
|
short_mention.to_owned().into(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -215,7 +220,7 @@ pub fn md_to_html<'a>(
|
|||||||
}
|
}
|
||||||
let hashtag = text_acc;
|
let hashtag = text_acc;
|
||||||
let link = Tag::Link(
|
let link = Tag::Link(
|
||||||
format!("//{}/tag/{}", base_url, &hashtag.to_camel_case())
|
format!("{}tag/{}", base_url, &hashtag.to_camel_case())
|
||||||
.into(),
|
.into(),
|
||||||
hashtag.to_owned().into(),
|
hashtag.to_owned().into(),
|
||||||
);
|
);
|
||||||
@@ -337,7 +342,7 @@ mod tests {
|
|||||||
|
|
||||||
for (md, mentions) in tests {
|
for (md, mentions) in tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
md_to_html(md, "", false, None).1,
|
md_to_html(md, None, false, None).1,
|
||||||
mentions
|
mentions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
@@ -362,7 +367,7 @@ mod tests {
|
|||||||
|
|
||||||
for (md, mentions) in tests {
|
for (md, mentions) in tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
md_to_html(md, "", false, None).2,
|
md_to_html(md, None, false, None).2,
|
||||||
mentions
|
mentions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
@@ -374,11 +379,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_inline() {
|
fn test_inline() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
md_to_html("# Hello", "", false, None).0,
|
md_to_html("# Hello", None, false, None).0,
|
||||||
String::from("<h1>Hello</h1>\n")
|
String::from("<h1>Hello</h1>\n")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
md_to_html("# Hello", "", true, None).0,
|
md_to_html("# Hello", None, true, None).0,
|
||||||
String::from("<p>Hello</p>\n")
|
String::from("<p>Hello</p>\n")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ version = "0.3.0"
|
|||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
|
|
||||||
[dependencies]
|
[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 = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
||||||
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||||
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||||
lazy_static = "1.3"
|
lazy_static = "1.3"
|
||||||
|
|||||||
@@ -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]
|
[dependencies]
|
||||||
activitypub = "0.1.1"
|
activitypub = "0.1.1"
|
||||||
ammonia = "2.0.0"
|
ammonia = "2.1.1"
|
||||||
askama_escape = "0.1"
|
askama_escape = "0.1"
|
||||||
bcrypt = "0.2"
|
bcrypt = "0.4"
|
||||||
canapi = "0.2"
|
|
||||||
guid-create = "0.1"
|
guid-create = "0.1"
|
||||||
heck = "0.3.0"
|
heck = "0.3.0"
|
||||||
itertools = "0.8.0"
|
itertools = "0.8.0"
|
||||||
lazy_static = "*"
|
lazy_static = "*"
|
||||||
openssl = "0.10.15"
|
migrations_internals= "1.4.0"
|
||||||
|
openssl = "0.10.22"
|
||||||
rocket = "0.4.0"
|
rocket = "0.4.0"
|
||||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||||
reqwest = "0.9"
|
reqwest = "0.9"
|
||||||
@@ -24,7 +24,9 @@ serde_json = "1.0"
|
|||||||
tantivy = "0.9.1"
|
tantivy = "0.9.1"
|
||||||
url = "1.7"
|
url = "1.7"
|
||||||
webfinger = "0.3.1"
|
webfinger = "0.3.1"
|
||||||
whatlang = "0.7.0"
|
whatlang = "0.7.1"
|
||||||
|
shrinkwraprs = "0.2.1"
|
||||||
|
diesel-derive-newtype = "0.1.2"
|
||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
@@ -32,7 +34,7 @@ version = "0.4"
|
|||||||
|
|
||||||
[dependencies.diesel]
|
[dependencies.diesel]
|
||||||
features = ["r2d2", "chrono"]
|
features = ["r2d2", "chrono"]
|
||||||
version = "1.3.2"
|
version = "1.4.2"
|
||||||
|
|
||||||
[dependencies.plume-api]
|
[dependencies.plume-api]
|
||||||
path = "../plume-api"
|
path = "../plume-api"
|
||||||
@@ -40,9 +42,12 @@ path = "../plume-api"
|
|||||||
[dependencies.plume-common]
|
[dependencies.plume-common]
|
||||||
path = "../plume-common"
|
path = "../plume-common"
|
||||||
|
|
||||||
|
[dependencies.plume-macro]
|
||||||
|
path = "../plume-macro"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
diesel_migrations = "1.3.0"
|
diesel_migrations = "1.3.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
postgres = ["diesel/postgres"]
|
postgres = ["diesel/postgres", "plume-macro/postgres"]
|
||||||
sqlite = ["diesel/sqlite"]
|
sqlite = ["diesel/sqlite", "plume-macro/sqlite"]
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
use canapi::{Error as ApiError, Provider};
|
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
use plume_api::apps::AppEndpoint;
|
|
||||||
use plume_common::utils::random_hex;
|
|
||||||
use schema::apps;
|
use schema::apps;
|
||||||
use {ApiResult, Connection, Error, Result};
|
use {Error, Result};
|
||||||
|
|
||||||
#[derive(Clone, Queryable)]
|
#[derive(Clone, Queryable, Serialize)]
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -28,52 +25,6 @@ pub struct NewApp {
|
|||||||
pub website: Option<String>,
|
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 {
|
impl App {
|
||||||
get!(apps);
|
get!(apps);
|
||||||
insert!(apps, NewApp);
|
insert!(apps, NewApp);
|
||||||
|
|||||||
+20
-18
@@ -172,7 +172,7 @@ impl Blog {
|
|||||||
let mut icon = Image::default();
|
let mut icon = Image::default();
|
||||||
icon.object_props.set_url_string(
|
icon.object_props.set_url_string(
|
||||||
self.icon_id
|
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(),
|
.unwrap_or_default(),
|
||||||
)?;
|
)?;
|
||||||
icon.object_props.set_attributed_to_link(
|
icon.object_props.set_attributed_to_link(
|
||||||
@@ -189,7 +189,7 @@ impl Blog {
|
|||||||
let mut banner = Image::default();
|
let mut banner = Image::default();
|
||||||
banner.object_props.set_url_string(
|
banner.object_props.set_url_string(
|
||||||
self.banner_id
|
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(),
|
.unwrap_or_default(),
|
||||||
)?;
|
)?;
|
||||||
banner.object_props.set_attributed_to_link(
|
banner.object_props.set_attributed_to_link(
|
||||||
@@ -271,14 +271,14 @@ impl Blog {
|
|||||||
|
|
||||||
pub fn icon_url(&self, conn: &Connection) -> String {
|
pub fn icon_url(&self, conn: &Connection) -> String {
|
||||||
self.icon_id
|
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())
|
.unwrap_or_else(|| "/static/default-avatar.png".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn banner_url(&self, conn: &Connection) -> Option<String> {
|
pub fn banner_url(&self, conn: &Connection) -> Option<String> {
|
||||||
self.banner_id
|
self.banner_id
|
||||||
.and_then(|i| Media::get(conn, i).ok())
|
.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<()> {
|
pub fn delete(&self, conn: &Connection, searcher: &Searcher) -> Result<()> {
|
||||||
@@ -332,7 +332,7 @@ impl FromId<PlumeRocket> for Blog {
|
|||||||
.icon_image()
|
.icon_image()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|icon| {
|
.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(
|
Media::save_remote(
|
||||||
&c.conn,
|
&c.conn,
|
||||||
icon.object_props.url_string().ok()?,
|
icon.object_props.url_string().ok()?,
|
||||||
@@ -348,7 +348,7 @@ impl FromId<PlumeRocket> for Blog {
|
|||||||
.image_image()
|
.image_image()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|banner| {
|
.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(
|
Media::save_remote(
|
||||||
&c.conn,
|
&c.conn,
|
||||||
banner.object_props.url_string().ok()?,
|
banner.object_props.url_string().ok()?,
|
||||||
@@ -407,7 +407,9 @@ impl AsActor<&PlumeRocket> for Blog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_local(&self) -> bool {
|
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(),
|
"BlogName".to_owned(),
|
||||||
"Blog name".to_owned(),
|
"Blog name".to_owned(),
|
||||||
"This is a small blog".to_owned(),
|
"This is a small blog".to_owned(),
|
||||||
Instance::get_local(conn).unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -485,7 +487,7 @@ pub(crate) mod tests {
|
|||||||
"MyBlog".to_owned(),
|
"MyBlog".to_owned(),
|
||||||
"My blog".to_owned(),
|
"My blog".to_owned(),
|
||||||
"Welcome to my blog".to_owned(),
|
"Welcome to my blog".to_owned(),
|
||||||
Instance::get_local(conn).unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -496,7 +498,7 @@ pub(crate) mod tests {
|
|||||||
"WhyILikePlume".to_owned(),
|
"WhyILikePlume".to_owned(),
|
||||||
"Why I like Plume".to_owned(),
|
"Why I like Plume".to_owned(),
|
||||||
"In this blog I will explay you why I like Plume so much".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(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -556,7 +558,7 @@ pub(crate) mod tests {
|
|||||||
"SomeName".to_owned(),
|
"SomeName".to_owned(),
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
"This is some blog".to_owned(),
|
"This is some blog".to_owned(),
|
||||||
Instance::get_local(conn).unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -564,7 +566,7 @@ pub(crate) mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
blog.get_instance(conn).unwrap().id,
|
blog.get_instance(conn).unwrap().id,
|
||||||
Instance::get_local(conn).unwrap().id
|
Instance::get_local().unwrap().id
|
||||||
);
|
);
|
||||||
// TODO add tests for remote instance
|
// TODO add tests for remote instance
|
||||||
|
|
||||||
@@ -584,7 +586,7 @@ pub(crate) mod tests {
|
|||||||
"SomeName".to_owned(),
|
"SomeName".to_owned(),
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
"This is some blog".to_owned(),
|
"This is some blog".to_owned(),
|
||||||
Instance::get_local(conn).unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -595,7 +597,7 @@ pub(crate) mod tests {
|
|||||||
"Blog".to_owned(),
|
"Blog".to_owned(),
|
||||||
"Blog".to_owned(),
|
"Blog".to_owned(),
|
||||||
"I've named my blog Blog".to_owned(),
|
"I've named my blog Blog".to_owned(),
|
||||||
Instance::get_local(conn).unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -687,7 +689,7 @@ pub(crate) mod tests {
|
|||||||
"SomeName".to_owned(),
|
"SomeName".to_owned(),
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
"This is some blog".to_owned(),
|
"This is some blog".to_owned(),
|
||||||
Instance::get_local(conn).unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -711,7 +713,7 @@ pub(crate) mod tests {
|
|||||||
"SomeName".to_owned(),
|
"SomeName".to_owned(),
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
"This is some blog".to_owned(),
|
"This is some blog".to_owned(),
|
||||||
Instance::get_local(conn).unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -749,7 +751,7 @@ pub(crate) mod tests {
|
|||||||
"SomeName".to_owned(),
|
"SomeName".to_owned(),
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
"This is some blog".to_owned(),
|
"This is some blog".to_owned(),
|
||||||
Instance::get_local(conn).unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -760,7 +762,7 @@ pub(crate) mod tests {
|
|||||||
"Blog".to_owned(),
|
"Blog".to_owned(),
|
||||||
"Blog".to_owned(),
|
"Blog".to_owned(),
|
||||||
"I've named my blog Blog".to_owned(),
|
"I've named my blog Blog".to_owned(),
|
||||||
Instance::get_local(conn).unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use medias::Media;
|
|||||||
use mentions::Mention;
|
use mentions::Mention;
|
||||||
use notifications::*;
|
use notifications::*;
|
||||||
use plume_common::activity_pub::{
|
use plume_common::activity_pub::{
|
||||||
inbox::{AsObject, FromId},
|
inbox::{AsActor, AsObject, FromId},
|
||||||
Id, IntoId, PUBLIC_VISIBILITY,
|
Id, IntoId, PUBLIC_VISIBILITY,
|
||||||
};
|
};
|
||||||
use plume_common::utils;
|
use plume_common::utils;
|
||||||
@@ -79,7 +79,7 @@ impl Comment {
|
|||||||
pub fn count_local(conn: &Connection) -> Result<i64> {
|
pub fn count_local(conn: &Connection) -> Result<i64> {
|
||||||
use schema::users;
|
use schema::users;
|
||||||
let local_authors = users::table
|
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);
|
.select(users::id);
|
||||||
comments::table
|
comments::table
|
||||||
.filter(comments::author_id.eq_any(local_authors))
|
.filter(comments::author_id.eq_any(local_authors))
|
||||||
@@ -107,7 +107,7 @@ impl Comment {
|
|||||||
let author = User::get(&c.conn, self.author_id)?;
|
let author = User::get(&c.conn, self.author_id)?;
|
||||||
let (html, mentions, _hashtags) = utils::md_to_html(
|
let (html, mentions, _hashtags) = utils::md_to_html(
|
||||||
self.content.get().as_ref(),
|
self.content.get().as_ref(),
|
||||||
&Instance::get_local(&c.conn)?.public_domain,
|
Some(&Instance::get_local()?.public_domain),
|
||||||
true,
|
true,
|
||||||
Some(Media::get_media_processor(&c.conn, vec![&author])),
|
Some(Media::get_media_processor(&c.conn, vec![&author])),
|
||||||
);
|
);
|
||||||
@@ -157,14 +157,20 @@ impl Comment {
|
|||||||
|
|
||||||
pub fn notify(&self, conn: &Connection) -> Result<()> {
|
pub fn notify(&self, conn: &Connection) -> Result<()> {
|
||||||
for author in self.get_post(conn)?.get_authors(conn)? {
|
for author in self.get_post(conn)?.get_authors(conn)? {
|
||||||
Notification::insert(
|
if Mention::list_for_comment(conn, self.id)?
|
||||||
conn,
|
.iter()
|
||||||
NewNotification {
|
.all(|m| m.get_mentioned(conn).map(|u| u != author).unwrap_or(true))
|
||||||
kind: notification_kind::COMMENT.to_string(),
|
&& author.is_local()
|
||||||
object_id: self.id,
|
{
|
||||||
user_id: author.id,
|
Notification::insert(
|
||||||
},
|
conn,
|
||||||
)?;
|
NewNotification {
|
||||||
|
kind: notification_kind::COMMENT.to_string(),
|
||||||
|
object_id: self.id,
|
||||||
|
user_id: author.id,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -230,10 +236,7 @@ impl FromId<PlumeRocket> for Comment {
|
|||||||
})?,
|
})?,
|
||||||
author_id: User::from_id(
|
author_id: User::from_id(
|
||||||
c,
|
c,
|
||||||
&{
|
¬e.object_props.attributed_to_link::<Id>()?,
|
||||||
let res: String = note.object_props.attributed_to_link::<Id>()?.into();
|
|
||||||
res
|
|
||||||
},
|
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.map_err(|(_, e)| e)?
|
.map_err(|(_, e)| e)?
|
||||||
|
|||||||
+16
-27
@@ -67,15 +67,18 @@ impl Follow {
|
|||||||
Ok(act)
|
Ok(act)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn notify(&self, conn: &Connection) -> Result<Notification> {
|
pub fn notify(&self, conn: &Connection) -> Result<()> {
|
||||||
Notification::insert(
|
if User::get(conn, self.following_id)?.is_local() {
|
||||||
conn,
|
Notification::insert(
|
||||||
NewNotification {
|
conn,
|
||||||
kind: notification_kind::FOLLOW.to_string(),
|
NewNotification {
|
||||||
object_id: self.id,
|
kind: notification_kind::FOLLOW.to_string(),
|
||||||
user_id: self.following_id,
|
object_id: self.id,
|
||||||
},
|
user_id: self.following_id,
|
||||||
)
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// from -> The one sending the follow request
|
/// 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> {
|
fn from_activity(c: &PlumeRocket, follow: FollowAct) -> Result<Self> {
|
||||||
let actor = User::from_id(
|
let actor =
|
||||||
c,
|
User::from_id(c, &follow.follow_props.actor_link::<Id>()?, None).map_err(|(_, e)| e)?;
|
||||||
&{
|
|
||||||
let res: String = follow.follow_props.actor_link::<Id>()?.into();
|
|
||||||
res
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.map_err(|(_, e)| e)?;
|
|
||||||
|
|
||||||
let target = User::from_id(
|
let target = User::from_id(c, &follow.follow_props.object_link::<Id>()?, None)
|
||||||
c,
|
.map_err(|(_, e)| e)?;
|
||||||
&{
|
|
||||||
let res: String = follow.follow_props.object_link::<Id>()?.into();
|
|
||||||
res
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.map_err(|(_, e)| e)?;
|
|
||||||
Follow::accept_follow(&c.conn, &actor, &target, follow, actor.id, target.id)
|
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, Create, Post>()
|
||||||
.with::<User, Delete, Comment>()
|
.with::<User, Delete, Comment>()
|
||||||
.with::<User, Delete, Post>()
|
.with::<User, Delete, Post>()
|
||||||
|
.with::<User, Delete, User>()
|
||||||
.with::<User, Follow, User>()
|
.with::<User, Follow, User>()
|
||||||
.with::<User, Like, Post>()
|
.with::<User, Like, Post>()
|
||||||
.with::<User, Undo, Reshare>()
|
.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]
|
#[test]
|
||||||
fn follow() {
|
fn follow() {
|
||||||
let r = rockets();
|
let r = rockets();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
use std::iter::Iterator;
|
use std::iter::Iterator;
|
||||||
|
use std::sync::RwLock;
|
||||||
|
|
||||||
use ap_url;
|
use ap_url;
|
||||||
use medias::Media;
|
use medias::Media;
|
||||||
@@ -40,8 +41,24 @@ pub struct NewInstance {
|
|||||||
pub short_description_html: String,
|
pub short_description_html: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref LOCAL_INSTANCE: RwLock<Option<Instance>> = RwLock::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
impl Instance {
|
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
|
instances::table
|
||||||
.filter(instances::local.eq(true))
|
.filter(instances::local.eq(true))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
@@ -51,6 +68,10 @@ impl Instance {
|
|||||||
.ok_or(Error::NotFound)
|
.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>> {
|
pub fn get_remotes(conn: &Connection) -> Result<Vec<Instance>> {
|
||||||
instances::table
|
instances::table
|
||||||
.filter(instances::local.eq(false))
|
.filter(instances::local.eq(false))
|
||||||
@@ -131,17 +152,17 @@ impl Instance {
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let (sd, _, _) = md_to_html(
|
let (sd, _, _) = md_to_html(
|
||||||
short_description.as_ref(),
|
short_description.as_ref(),
|
||||||
&self.public_domain,
|
Some(&self.public_domain),
|
||||||
true,
|
true,
|
||||||
Some(Media::get_media_processor(conn, vec![])),
|
Some(Media::get_media_processor(conn, vec![])),
|
||||||
);
|
);
|
||||||
let (ld, _, _) = md_to_html(
|
let (ld, _, _) = md_to_html(
|
||||||
long_description.as_ref(),
|
long_description.as_ref(),
|
||||||
&self.public_domain,
|
Some(&self.public_domain),
|
||||||
false,
|
false,
|
||||||
Some(Media::get_media_processor(conn, vec![])),
|
Some(Media::get_media_processor(conn, vec![])),
|
||||||
);
|
);
|
||||||
diesel::update(self)
|
let res = diesel::update(self)
|
||||||
.set((
|
.set((
|
||||||
instances::name.eq(name),
|
instances::name.eq(name),
|
||||||
instances::open_registrations.eq(open_registrations),
|
instances::open_registrations.eq(open_registrations),
|
||||||
@@ -152,7 +173,11 @@ impl Instance {
|
|||||||
))
|
))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(Error::from)
|
.map_err(Error::from);
|
||||||
|
if self.local {
|
||||||
|
Instance::cache_local(conn);
|
||||||
|
}
|
||||||
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn count(conn: &Connection) -> Result<i64> {
|
pub fn count(conn: &Connection) -> Result<i64> {
|
||||||
@@ -171,7 +196,7 @@ pub(crate) mod tests {
|
|||||||
use Connection as Conn;
|
use Connection as Conn;
|
||||||
|
|
||||||
pub(crate) fn fill_database(conn: &Conn) -> Vec<(NewInstance, Instance)> {
|
pub(crate) fn fill_database(conn: &Conn) -> Vec<(NewInstance, Instance)> {
|
||||||
vec![
|
let res = vec![
|
||||||
NewInstance {
|
NewInstance {
|
||||||
default_license: "WTFPL".to_string(),
|
default_license: "WTFPL".to_string(),
|
||||||
local: true,
|
local: true,
|
||||||
@@ -225,7 +250,9 @@ pub(crate) mod tests {
|
|||||||
.unwrap_or_else(|_| Instance::insert(conn, inst).unwrap()),
|
.unwrap_or_else(|_| Instance::insert(conn, inst).unwrap()),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect();
|
||||||
|
Instance::cache_local(conn);
|
||||||
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -237,7 +264,7 @@ pub(crate) mod tests {
|
|||||||
.map(|(inserted, _)| inserted)
|
.map(|(inserted, _)| inserted)
|
||||||
.find(|inst| inst.local)
|
.find(|inst| inst.local)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let res = Instance::get_local(conn).unwrap();
|
let res = Instance::get_local().unwrap();
|
||||||
|
|
||||||
part_eq!(
|
part_eq!(
|
||||||
res,
|
res,
|
||||||
|
|||||||
+14
-14
@@ -1,12 +1,12 @@
|
|||||||
#![feature(try_trait)]
|
#![feature(try_trait)]
|
||||||
#![feature(never_type)]
|
#![feature(never_type)]
|
||||||
#![feature(custom_attribute)]
|
#![feature(custom_attribute)]
|
||||||
|
#![feature(proc_macro_hygiene)]
|
||||||
|
|
||||||
extern crate activitypub;
|
extern crate activitypub;
|
||||||
extern crate ammonia;
|
extern crate ammonia;
|
||||||
extern crate askama_escape;
|
extern crate askama_escape;
|
||||||
extern crate bcrypt;
|
extern crate bcrypt;
|
||||||
extern crate canapi;
|
|
||||||
extern crate chrono;
|
extern crate chrono;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate diesel;
|
extern crate diesel;
|
||||||
@@ -15,9 +15,12 @@ extern crate heck;
|
|||||||
extern crate itertools;
|
extern crate itertools;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate lazy_static;
|
extern crate lazy_static;
|
||||||
|
extern crate migrations_internals;
|
||||||
extern crate openssl;
|
extern crate openssl;
|
||||||
extern crate plume_api;
|
extern crate plume_api;
|
||||||
extern crate plume_common;
|
extern crate plume_common;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate plume_macro;
|
||||||
extern crate reqwest;
|
extern crate reqwest;
|
||||||
extern crate rocket;
|
extern crate rocket;
|
||||||
extern crate rocket_i18n;
|
extern crate rocket_i18n;
|
||||||
@@ -33,10 +36,6 @@ extern crate url;
|
|||||||
extern crate webfinger;
|
extern crate webfinger;
|
||||||
extern crate whatlang;
|
extern crate whatlang;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[macro_use]
|
|
||||||
extern crate diesel_migrations;
|
|
||||||
|
|
||||||
use plume_common::activity_pub::inbox::InboxError;
|
use plume_common::activity_pub::inbox::InboxError;
|
||||||
|
|
||||||
#[cfg(not(any(feature = "sqlite", feature = "postgres")))]
|
#[cfg(not(any(feature = "sqlite", feature = "postgres")))]
|
||||||
@@ -66,6 +65,7 @@ pub enum Error {
|
|||||||
Unauthorized,
|
Unauthorized,
|
||||||
Url,
|
Url,
|
||||||
Webfinger,
|
Webfinger,
|
||||||
|
Expired,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<bcrypt::BcryptError> for Error {
|
impl From<bcrypt::BcryptError> for Error {
|
||||||
@@ -154,8 +154,6 @@ impl From<InboxError<Error>> for Error {
|
|||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, 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
|
/// Adds a function to a model, that returns the first
|
||||||
/// matching row for a given list of fields.
|
/// matching row for a given list of fields.
|
||||||
///
|
///
|
||||||
@@ -305,18 +303,15 @@ mod tests {
|
|||||||
use diesel::r2d2::ConnectionManager;
|
use diesel::r2d2::ConnectionManager;
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
use diesel::{dsl::sql_query, RunQueryDsl};
|
use diesel::{dsl::sql_query, RunQueryDsl};
|
||||||
|
use migrations::IMPORTED_MIGRATIONS;
|
||||||
|
use plume_common::utils::random_hex;
|
||||||
use scheduled_thread_pool::ScheduledThreadPool;
|
use scheduled_thread_pool::ScheduledThreadPool;
|
||||||
use search;
|
use search;
|
||||||
|
use std::env::temp_dir;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use Connection as Conn;
|
use Connection as Conn;
|
||||||
use CONFIG;
|
use CONFIG;
|
||||||
|
|
||||||
#[cfg(feature = "sqlite")]
|
|
||||||
embed_migrations!("../migrations/sqlite");
|
|
||||||
|
|
||||||
#[cfg(feature = "postgres")]
|
|
||||||
embed_migrations!("../migrations/postgres");
|
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! part_eq {
|
macro_rules! part_eq {
|
||||||
( $x:expr, $y:expr, [$( $var:ident ),*] ) => {
|
( $x:expr, $y:expr, [$( $var:ident ),*] ) => {
|
||||||
@@ -338,7 +333,10 @@ mod tests {
|
|||||||
.connection_customizer(Box::new(db_conn::PragmaForeignKey))
|
.connection_customizer(Box::new(db_conn::PragmaForeignKey))
|
||||||
.build(ConnectionManager::<Conn>::new(CONFIG.database_url.as_str()))
|
.build(ConnectionManager::<Conn>::new(CONFIG.database_url.as_str()))
|
||||||
.unwrap();
|
.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
|
pool
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -368,7 +366,9 @@ pub mod instance;
|
|||||||
pub mod likes;
|
pub mod likes;
|
||||||
pub mod medias;
|
pub mod medias;
|
||||||
pub mod mentions;
|
pub mod mentions;
|
||||||
|
pub mod migrations;
|
||||||
pub mod notifications;
|
pub mod notifications;
|
||||||
|
pub mod password_reset_requests;
|
||||||
pub mod plume_rocket;
|
pub mod plume_rocket;
|
||||||
pub mod post_authors;
|
pub mod post_authors;
|
||||||
pub mod posts;
|
pub mod posts;
|
||||||
|
|||||||
+17
-29
@@ -4,7 +4,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
|||||||
|
|
||||||
use notifications::*;
|
use notifications::*;
|
||||||
use plume_common::activity_pub::{
|
use plume_common::activity_pub::{
|
||||||
inbox::{AsObject, FromId},
|
inbox::{AsActor, AsObject, FromId},
|
||||||
Id, IntoId, PUBLIC_VISIBILITY,
|
Id, IntoId, PUBLIC_VISIBILITY,
|
||||||
};
|
};
|
||||||
use posts::Post;
|
use posts::Post;
|
||||||
@@ -54,14 +54,16 @@ impl Like {
|
|||||||
pub fn notify(&self, conn: &Connection) -> Result<()> {
|
pub fn notify(&self, conn: &Connection) -> Result<()> {
|
||||||
let post = Post::get(conn, self.post_id)?;
|
let post = Post::get(conn, self.post_id)?;
|
||||||
for author in post.get_authors(conn)? {
|
for author in post.get_authors(conn)? {
|
||||||
Notification::insert(
|
if author.is_local() {
|
||||||
conn,
|
Notification::insert(
|
||||||
NewNotification {
|
conn,
|
||||||
kind: notification_kind::LIKE.to_string(),
|
NewNotification {
|
||||||
object_id: self.id,
|
kind: notification_kind::LIKE.to_string(),
|
||||||
user_id: author.id,
|
object_id: self.id,
|
||||||
},
|
user_id: author.id,
|
||||||
)?;
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -113,26 +115,12 @@ impl FromId<PlumeRocket> for Like {
|
|||||||
let res = Like::insert(
|
let res = Like::insert(
|
||||||
&c.conn,
|
&c.conn,
|
||||||
NewLike {
|
NewLike {
|
||||||
post_id: Post::from_id(
|
post_id: Post::from_id(c, &act.like_props.object_link::<Id>()?, None)
|
||||||
c,
|
.map_err(|(_, e)| e)?
|
||||||
&{
|
.id,
|
||||||
let res: String = act.like_props.object_link::<Id>()?.into();
|
user_id: User::from_id(c, &act.like_props.actor_link::<Id>()?, None)
|
||||||
res
|
.map_err(|(_, e)| e)?
|
||||||
},
|
.id,
|
||||||
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,
|
|
||||||
ap_url: act.object_props.id_string()?,
|
ap_url: act.object_props.id_string()?,
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -104,8 +104,8 @@ impl Media {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn html(&self, conn: &Connection) -> Result<SafeString> {
|
pub fn html(&self) -> Result<SafeString> {
|
||||||
let url = self.url(conn)?;
|
let url = self.url()?;
|
||||||
Ok(match self.category() {
|
Ok(match self.category() {
|
||||||
MediaCategory::Image => SafeString::trusted(&format!(
|
MediaCategory::Image => SafeString::trusted(&format!(
|
||||||
r#"<img src="{}" alt="{}" title="{}">"#,
|
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() {
|
Ok(match self.category() {
|
||||||
MediaCategory::Image => {
|
MediaCategory::Image => {
|
||||||
SafeString::new(&format!("", escape(&self.alt_text), self.id))
|
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(""),
|
MediaCategory::Unknown => SafeString::new(""),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn url(&self, conn: &Connection) -> Result<String> {
|
pub fn url(&self) -> Result<String> {
|
||||||
if self.is_remote {
|
if self.is_remote {
|
||||||
Ok(self.remote_url.clone().unwrap_or_default())
|
Ok(self.remote_url.clone().unwrap_or_default())
|
||||||
} else {
|
} else {
|
||||||
Ok(ap_url(&format!(
|
Ok(ap_url(&format!(
|
||||||
"{}/{}",
|
"{}/{}",
|
||||||
Instance::get_local(conn)?.public_domain,
|
Instance::get_local()?.public_domain,
|
||||||
self.file_path
|
self.file_path
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
@@ -237,7 +237,7 @@ impl Media {
|
|||||||
let media = Media::get(conn, id).ok()?;
|
let media = Media::get(conn, id).ok()?;
|
||||||
// if owner is user or check is disabled
|
// if owner is user or check is disabled
|
||||||
if uid.contains(&media.owner_id) || uid.is_empty() {
|
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 {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
|||||||
|
|
||||||
use comments::Comment;
|
use comments::Comment;
|
||||||
use notifications::*;
|
use notifications::*;
|
||||||
|
use plume_common::activity_pub::inbox::AsActor;
|
||||||
use posts::Post;
|
use posts::Post;
|
||||||
use schema::mentions;
|
use schema::mentions;
|
||||||
use users::User;
|
use users::User;
|
||||||
@@ -129,14 +130,18 @@ impl Mention {
|
|||||||
|
|
||||||
fn notify(&self, conn: &Connection) -> Result<()> {
|
fn notify(&self, conn: &Connection) -> Result<()> {
|
||||||
let m = self.get_mentioned(conn)?;
|
let m = self.get_mentioned(conn)?;
|
||||||
Notification::insert(
|
if m.is_local() {
|
||||||
conn,
|
Notification::insert(
|
||||||
NewNotification {
|
conn,
|
||||||
kind: notification_kind::MENTION.to_string(),
|
NewNotification {
|
||||||
object_id: self.id,
|
kind: notification_kind::MENTION.to_string(),
|
||||||
user_id: m.id,
|
object_id: self.id,
|
||||||
},
|
user_id: m.id,
|
||||||
)
|
},
|
||||||
.map(|_| ())
|
)
|
||||||
|
.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! {}
|
||||||
|
};
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
use chrono::{offset::Utc, Duration, NaiveDateTime};
|
||||||
|
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
use schema::password_reset_requests;
|
||||||
|
use {Connection, Error, Result};
|
||||||
|
|
||||||
|
#[derive(Clone, Identifiable, Queryable)]
|
||||||
|
pub struct PasswordResetRequest {
|
||||||
|
pub id: i32,
|
||||||
|
pub email: String,
|
||||||
|
pub token: String,
|
||||||
|
pub expiration_date: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable)]
|
||||||
|
#[table_name = "password_reset_requests"]
|
||||||
|
pub struct NewPasswordResetRequest {
|
||||||
|
pub email: String,
|
||||||
|
pub token: String,
|
||||||
|
pub expiration_date: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOKEN_VALIDITY_HOURS: i64 = 2;
|
||||||
|
|
||||||
|
impl PasswordResetRequest {
|
||||||
|
pub fn insert(conn: &Connection, email: &str) -> Result<String> {
|
||||||
|
// first, delete other password reset tokens associated with this email:
|
||||||
|
let existing_requests =
|
||||||
|
password_reset_requests::table.filter(password_reset_requests::email.eq(email));
|
||||||
|
diesel::delete(existing_requests).execute(conn)?;
|
||||||
|
|
||||||
|
// now, generate a random token, set the expiry date,
|
||||||
|
// and insert it into the DB:
|
||||||
|
let token = plume_common::utils::random_hex();
|
||||||
|
let expiration_date = Utc::now()
|
||||||
|
.naive_utc()
|
||||||
|
.checked_add_signed(Duration::hours(TOKEN_VALIDITY_HOURS))
|
||||||
|
.expect("could not calculate expiration date");
|
||||||
|
let new_request = NewPasswordResetRequest {
|
||||||
|
email: email.to_owned(),
|
||||||
|
token: token.clone(),
|
||||||
|
expiration_date,
|
||||||
|
};
|
||||||
|
diesel::insert_into(password_reset_requests::table)
|
||||||
|
.values(new_request)
|
||||||
|
.execute(conn)
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_by_token(conn: &Connection, token: &str) -> Result<Self> {
|
||||||
|
let token = password_reset_requests::table
|
||||||
|
.filter(password_reset_requests::token.eq(token))
|
||||||
|
.first::<Self>(conn)
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
|
||||||
|
if token.expiration_date < Utc::now().naive_utc() {
|
||||||
|
return Err(Error::Expired);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_and_delete_by_token(conn: &Connection, token: &str) -> Result<Self> {
|
||||||
|
let request = Self::find_by_token(&conn, &token)?;
|
||||||
|
|
||||||
|
let filter =
|
||||||
|
password_reset_requests::table.filter(password_reset_requests::id.eq(request.id));
|
||||||
|
diesel::delete(filter).execute(conn)?;
|
||||||
|
|
||||||
|
Ok(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use diesel::Connection;
|
||||||
|
use tests::db;
|
||||||
|
use users::tests as user_tests;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_insert_and_find_password_reset_request() {
|
||||||
|
let conn = db();
|
||||||
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
|
user_tests::fill_database(&conn);
|
||||||
|
let admin_email = "admin@example.com";
|
||||||
|
|
||||||
|
let token = PasswordResetRequest::insert(&conn, admin_email)
|
||||||
|
.expect("couldn't insert new request");
|
||||||
|
let request = PasswordResetRequest::find_by_token(&conn, &token)
|
||||||
|
.expect("couldn't retrieve request");
|
||||||
|
|
||||||
|
assert!(&token.len() > &32);
|
||||||
|
assert_eq!(&request.email, &admin_email);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_insert_delete_previous_password_reset_request() {
|
||||||
|
let conn = db();
|
||||||
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
|
user_tests::fill_database(&conn);
|
||||||
|
let admin_email = "admin@example.com";
|
||||||
|
|
||||||
|
PasswordResetRequest::insert(&conn, &admin_email).expect("couldn't insert new request");
|
||||||
|
PasswordResetRequest::insert(&conn, &admin_email)
|
||||||
|
.expect("couldn't insert second request");
|
||||||
|
|
||||||
|
let count = password_reset_requests::table.count().get_result(&*conn);
|
||||||
|
assert_eq!(Ok(1), count);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_find_password_reset_request_by_token_time() {
|
||||||
|
let conn = db();
|
||||||
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
|
user_tests::fill_database(&conn);
|
||||||
|
let admin_email = "admin@example.com";
|
||||||
|
let token = "abcdef";
|
||||||
|
let now = Utc::now().naive_utc();
|
||||||
|
|
||||||
|
diesel::insert_into(password_reset_requests::table)
|
||||||
|
.values((
|
||||||
|
password_reset_requests::email.eq(&admin_email),
|
||||||
|
password_reset_requests::token.eq(&token),
|
||||||
|
password_reset_requests::expiration_date.eq(now),
|
||||||
|
))
|
||||||
|
.execute(&*conn)
|
||||||
|
.expect("could not insert request");
|
||||||
|
|
||||||
|
match PasswordResetRequest::find_by_token(&conn, &token) {
|
||||||
|
Err(Error::Expired) => (),
|
||||||
|
_ => panic!("Received unexpected result finding expired token"),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_find_and_delete_password_reset_request() {
|
||||||
|
let conn = db();
|
||||||
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
|
user_tests::fill_database(&conn);
|
||||||
|
let admin_email = "admin@example.com";
|
||||||
|
|
||||||
|
let token = PasswordResetRequest::insert(&conn, &admin_email)
|
||||||
|
.expect("couldn't insert new request");
|
||||||
|
PasswordResetRequest::find_and_delete_by_token(&conn, &token)
|
||||||
|
.expect("couldn't find and delete request");
|
||||||
|
|
||||||
|
let count = password_reset_requests::table.count().get_result(&*conn);
|
||||||
|
assert_eq!(Ok(0), count);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ mod module {
|
|||||||
use crate::search;
|
use crate::search;
|
||||||
use crate::users;
|
use crate::users;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
request::{self, FromRequest, Request},
|
request::{self, FlashMessage, FromRequest, Request},
|
||||||
Outcome, State,
|
Outcome, State,
|
||||||
};
|
};
|
||||||
use scheduled_thread_pool::ScheduledThreadPool;
|
use scheduled_thread_pool::ScheduledThreadPool;
|
||||||
@@ -19,6 +19,7 @@ mod module {
|
|||||||
pub user: Option<users::User>,
|
pub user: Option<users::User>,
|
||||||
pub searcher: Arc<search::Searcher>,
|
pub searcher: Arc<search::Searcher>,
|
||||||
pub worker: Arc<ScheduledThreadPool>,
|
pub worker: Arc<ScheduledThreadPool>,
|
||||||
|
pub flash_msg: Option<(String, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for PlumeRocket {
|
impl<'a, 'r> FromRequest<'a, 'r> for PlumeRocket {
|
||||||
@@ -30,10 +31,12 @@ mod module {
|
|||||||
let user = request.guard::<users::User>().succeeded();
|
let user = request.guard::<users::User>().succeeded();
|
||||||
let worker = request.guard::<State<Arc<ScheduledThreadPool>>>()?;
|
let worker = request.guard::<State<Arc<ScheduledThreadPool>>>()?;
|
||||||
let searcher = request.guard::<State<Arc<search::Searcher>>>()?;
|
let searcher = request.guard::<State<Arc<search::Searcher>>>()?;
|
||||||
|
let flash_msg = request.guard::<FlashMessage>().succeeded();
|
||||||
Outcome::Success(PlumeRocket {
|
Outcome::Success(PlumeRocket {
|
||||||
conn,
|
conn,
|
||||||
intl,
|
intl,
|
||||||
user,
|
user,
|
||||||
|
flash_msg: flash_msg.map(|f| (f.name().into(), f.msg().into())),
|
||||||
worker: worker.clone(),
|
worker: worker.clone(),
|
||||||
searcher: searcher.clone(),
|
searcher: searcher.clone(),
|
||||||
})
|
})
|
||||||
|
|||||||
+28
-287
@@ -4,7 +4,6 @@ use activitypub::{
|
|||||||
object::{Article, Image, Tombstone},
|
object::{Article, Image, Tombstone},
|
||||||
CustomObject,
|
CustomObject,
|
||||||
};
|
};
|
||||||
use canapi::{Error as ApiError, Provider};
|
|
||||||
use chrono::{NaiveDateTime, TimeZone, Utc};
|
use chrono::{NaiveDateTime, TimeZone, Utc};
|
||||||
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||||
use heck::{CamelCase, KebabCase};
|
use heck::{CamelCase, KebabCase};
|
||||||
@@ -15,10 +14,8 @@ use blogs::Blog;
|
|||||||
use instance::Instance;
|
use instance::Instance;
|
||||||
use medias::Media;
|
use medias::Media;
|
||||||
use mentions::Mention;
|
use mentions::Mention;
|
||||||
use plume_api::posts::PostEndpoint;
|
|
||||||
use plume_common::{
|
use plume_common::{
|
||||||
activity_pub::{
|
activity_pub::{
|
||||||
broadcast,
|
|
||||||
inbox::{AsObject, FromId},
|
inbox::{AsObject, FromId},
|
||||||
Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILITY,
|
Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILITY,
|
||||||
},
|
},
|
||||||
@@ -30,7 +27,7 @@ use schema::posts;
|
|||||||
use search::Searcher;
|
use search::Searcher;
|
||||||
use tags::*;
|
use tags::*;
|
||||||
use users::User;
|
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>;
|
pub type LicensedArticle = CustomObject<Licensed, Article>;
|
||||||
|
|
||||||
@@ -67,282 +64,6 @@ pub struct NewPost {
|
|||||||
pub cover_id: Option<i32>,
|
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 {
|
impl Post {
|
||||||
get!(posts);
|
get!(posts);
|
||||||
find_by!(posts, find_by_slug, slug as &str, blog_id as i32);
|
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::post_authors;
|
||||||
use schema::users;
|
use schema::users;
|
||||||
let local_authors = users::table
|
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);
|
.select(users::id);
|
||||||
let local_posts_id = post_authors::table
|
let local_posts_id = post_authors::table
|
||||||
.filter(post_authors::author_id.eq_any(local_authors))
|
.filter(post_authors::author_id.eq_any(local_authors))
|
||||||
@@ -441,6 +162,26 @@ impl Post {
|
|||||||
.map_err(Error::from)
|
.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>> {
|
pub fn get_recents(conn: &Connection, limit: i64) -> Result<Vec<Post>> {
|
||||||
posts::table
|
posts::table
|
||||||
.order(posts::creation_date.desc())
|
.order(posts::creation_date.desc())
|
||||||
@@ -643,7 +384,7 @@ impl Post {
|
|||||||
.collect::<Vec<serde_json::Value>>();
|
.collect::<Vec<serde_json::Value>>();
|
||||||
let mut tags_json = Tag::for_post(conn, self.id)?
|
let mut tags_json = Tag::for_post(conn, self.id)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|t| json!(t.to_activity(conn).ok()))
|
.map(|t| json!(t.to_activity().ok()))
|
||||||
.collect::<Vec<serde_json::Value>>();
|
.collect::<Vec<serde_json::Value>>();
|
||||||
mentions_json.append(&mut tags_json);
|
mentions_json.append(&mut tags_json);
|
||||||
|
|
||||||
@@ -678,7 +419,7 @@ impl Post {
|
|||||||
if let Some(media_id) = self.cover_id {
|
if let Some(media_id) = self.cover_id {
|
||||||
let media = Media::get(conn, media_id)?;
|
let media = Media::get(conn, media_id)?;
|
||||||
let mut cover = Image::default();
|
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 {
|
if media.sensitive {
|
||||||
cover
|
cover
|
||||||
.object_props
|
.object_props
|
||||||
@@ -862,7 +603,7 @@ impl Post {
|
|||||||
pub fn cover_url(&self, conn: &Connection) -> Option<String> {
|
pub fn cover_url(&self, conn: &Connection) -> Option<String> {
|
||||||
self.cover_id
|
self.cover_id
|
||||||
.and_then(|i| Media::get(conn, i).ok())
|
.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> {
|
pub fn build_delete(&self, conn: &Connection) -> Result<Delete> {
|
||||||
@@ -901,7 +642,7 @@ impl FromId<PlumeRocket> for Post {
|
|||||||
.attributed_to_link_vec::<Id>()?
|
.attributed_to_link_vec::<Id>()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.fold((None, vec![]), |(blog, mut authors), link| {
|
.fold((None, vec![]), |(blog, mut authors), link| {
|
||||||
let url: String = link.into();
|
let url = link;
|
||||||
match User::from_id(&c, &url, None) {
|
match User::from_id(&c, &url, None) {
|
||||||
Ok(u) => {
|
Ok(u) => {
|
||||||
authors.push(u);
|
authors.push(u);
|
||||||
@@ -951,7 +692,7 @@ impl FromId<PlumeRocket> for Post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// save mentions and tags
|
// 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
|
.2
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| s.to_camel_case())
|
.map(|s| s.to_camel_case())
|
||||||
@@ -1088,7 +829,7 @@ impl AsObject<User, Update, &PlumeRocket> for PostUpdate {
|
|||||||
post.license = license;
|
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
|
.2
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| s.to_camel_case())
|
.map(|s| s.to_camel_case())
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
|||||||
|
|
||||||
use notifications::*;
|
use notifications::*;
|
||||||
use plume_common::activity_pub::{
|
use plume_common::activity_pub::{
|
||||||
inbox::{AsObject, FromId},
|
inbox::{AsActor, AsObject, FromId},
|
||||||
Id, IntoId, PUBLIC_VISIBILITY,
|
Id, IntoId, PUBLIC_VISIBILITY,
|
||||||
};
|
};
|
||||||
use posts::Post;
|
use posts::Post;
|
||||||
@@ -79,14 +79,16 @@ impl Reshare {
|
|||||||
pub fn notify(&self, conn: &Connection) -> Result<()> {
|
pub fn notify(&self, conn: &Connection) -> Result<()> {
|
||||||
let post = self.get_post(conn)?;
|
let post = self.get_post(conn)?;
|
||||||
for author in post.get_authors(conn)? {
|
for author in post.get_authors(conn)? {
|
||||||
Notification::insert(
|
if author.is_local() {
|
||||||
conn,
|
Notification::insert(
|
||||||
NewNotification {
|
conn,
|
||||||
kind: notification_kind::RESHARE.to_string(),
|
NewNotification {
|
||||||
object_id: self.id,
|
kind: notification_kind::RESHARE.to_string(),
|
||||||
user_id: author.id,
|
object_id: self.id,
|
||||||
},
|
user_id: author.id,
|
||||||
)?;
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -138,26 +140,12 @@ impl FromId<PlumeRocket> for Reshare {
|
|||||||
let res = Reshare::insert(
|
let res = Reshare::insert(
|
||||||
&c.conn,
|
&c.conn,
|
||||||
NewReshare {
|
NewReshare {
|
||||||
post_id: Post::from_id(
|
post_id: Post::from_id(c, &act.announce_props.object_link::<Id>()?, None)
|
||||||
c,
|
.map_err(|(_, e)| e)?
|
||||||
&{
|
.id,
|
||||||
let res: String = act.announce_props.object_link::<Id>()?.into();
|
user_id: User::from_id(c, &act.announce_props.actor_link::<Id>()?, None)
|
||||||
res
|
.map_err(|(_, e)| e)?
|
||||||
},
|
.id,
|
||||||
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,
|
|
||||||
ap_url: act.object_props.id_string()?,
|
ap_url: act.object_props.id_string()?,
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -141,6 +141,15 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
password_reset_requests (id) {
|
||||||
|
id -> Int4,
|
||||||
|
email -> Varchar,
|
||||||
|
token -> Varchar,
|
||||||
|
expiration_date -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
post_authors (id) {
|
post_authors (id) {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
@@ -247,6 +256,7 @@ allow_tables_to_appear_in_same_query!(
|
|||||||
medias,
|
medias,
|
||||||
mentions,
|
mentions,
|
||||||
notifications,
|
notifications,
|
||||||
|
password_reset_requests,
|
||||||
post_authors,
|
post_authors,
|
||||||
posts,
|
posts,
|
||||||
reshares,
|
reshares,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
use instance::Instance;
|
use instance::Instance;
|
||||||
use posts::Post;
|
use posts::Post;
|
||||||
|
use schema::posts;
|
||||||
use tags::Tag;
|
use tags::Tag;
|
||||||
use Connection;
|
use Connection;
|
||||||
|
|
||||||
use chrono::Datelike;
|
use chrono::Datelike;
|
||||||
|
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use std::{cmp, fs::create_dir_all, path::Path, sync::Mutex};
|
use std::{cmp, fs::create_dir_all, path::Path, sync::Mutex};
|
||||||
use tantivy::{
|
use tantivy::{
|
||||||
@@ -222,6 +224,16 @@ impl Searcher {
|
|||||||
.collect()
|
.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) {
|
pub fn commit(&self) {
|
||||||
let mut writer = self.writer.lock().unwrap();
|
let mut writer = self.writer.lock().unwrap();
|
||||||
writer.as_mut().unwrap().commit().unwrap();
|
writer.as_mut().unwrap().commit().unwrap();
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ impl Tag {
|
|||||||
find_by!(tags, find_by_name, tag as &str);
|
find_by!(tags, find_by_name, tag as &str);
|
||||||
list_by!(tags, for_post, post_id as i32);
|
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();
|
let mut ht = Hashtag::default();
|
||||||
ht.set_href_string(ap_url(&format!(
|
ht.set_href_string(ap_url(&format!(
|
||||||
"{}/tag/{}",
|
"{}/tag/{}",
|
||||||
Instance::get_local(conn)?.public_domain,
|
Instance::get_local()?.public_domain,
|
||||||
self.tag
|
self.tag
|
||||||
)))?;
|
)))?;
|
||||||
ht.set_name_string(self.tag.clone())?;
|
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();
|
let mut ht = Hashtag::default();
|
||||||
ht.set_href_string(ap_url(&format!(
|
ht.set_href_string(ap_url(&format!(
|
||||||
"{}/tag/{}",
|
"{}/tag/{}",
|
||||||
Instance::get_local(conn)?.public_domain,
|
Instance::get_local()?.public_domain,
|
||||||
tag
|
tag
|
||||||
)))?;
|
)))?;
|
||||||
ht.set_name_string(tag)?;
|
ht.set_name_string(tag)?;
|
||||||
|
|||||||
+66
-18
@@ -1,5 +1,9 @@
|
|||||||
use activitypub::{
|
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 bcrypt;
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
@@ -12,14 +16,14 @@ use openssl::{
|
|||||||
};
|
};
|
||||||
use plume_common::activity_pub::{
|
use plume_common::activity_pub::{
|
||||||
ap_accept_header,
|
ap_accept_header,
|
||||||
inbox::{AsActor, FromId},
|
inbox::{AsActor, AsObject, FromId},
|
||||||
sign::{gen_keypair, Signer},
|
sign::{gen_keypair, Signer},
|
||||||
ActivityStream, ApSignature, Id, IntoId, PublicKey,
|
ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY,
|
||||||
};
|
};
|
||||||
use plume_common::utils;
|
use plume_common::utils;
|
||||||
use reqwest::{
|
use reqwest::{
|
||||||
header::{HeaderValue, ACCEPT},
|
header::{HeaderValue, ACCEPT},
|
||||||
Client,
|
ClientBuilder,
|
||||||
};
|
};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
outcome::IntoOutcome,
|
outcome::IntoOutcome,
|
||||||
@@ -211,7 +215,7 @@ impl User {
|
|||||||
users::email.eq(email),
|
users::email.eq(email),
|
||||||
users::summary_html.eq(utils::md_to_html(
|
users::summary_html.eq(utils::md_to_html(
|
||||||
&summary,
|
&summary,
|
||||||
"",
|
None,
|
||||||
false,
|
false,
|
||||||
Some(Media::get_media_processor(conn, vec![self])),
|
Some(Media::get_media_processor(conn, vec![self])),
|
||||||
)
|
)
|
||||||
@@ -224,7 +228,7 @@ impl User {
|
|||||||
|
|
||||||
pub fn count_local(conn: &Connection) -> Result<i64> {
|
pub fn count_local(conn: &Connection) -> Result<i64> {
|
||||||
users::table
|
users::table
|
||||||
.filter(users::instance_id.eq(Instance::get_local(conn)?.id))
|
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
||||||
.count()
|
.count()
|
||||||
.get_result(conn)
|
.get_result(conn)
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
@@ -263,7 +267,9 @@ impl User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn fetch(url: &str) -> Result<CustomPerson> {
|
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)
|
.get(url)
|
||||||
.header(
|
.header(
|
||||||
ACCEPT,
|
ACCEPT,
|
||||||
@@ -347,7 +353,7 @@ impl User {
|
|||||||
|
|
||||||
pub fn get_local_page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<User>> {
|
pub fn get_local_page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<User>> {
|
||||||
users::table
|
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())
|
.order(users::username.asc())
|
||||||
.offset(min.into())
|
.offset(min.into())
|
||||||
.limit((max - min).into())
|
.limit((max - min).into())
|
||||||
@@ -365,7 +371,9 @@ impl User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> {
|
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[..])
|
.get(&self.outbox_url[..])
|
||||||
.header(
|
.header(
|
||||||
ACCEPT,
|
ACCEPT,
|
||||||
@@ -388,7 +396,9 @@ impl User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch_followers_ids(&self) -> Result<Vec<String>> {
|
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[..])
|
.get(&self.followers_endpoint[..])
|
||||||
.header(
|
.header(
|
||||||
ACCEPT,
|
ACCEPT,
|
||||||
@@ -624,7 +634,7 @@ impl User {
|
|||||||
let mut avatar = Image::default();
|
let mut avatar = Image::default();
|
||||||
avatar.object_props.set_url_string(
|
avatar.object_props.set_url_string(
|
||||||
self.avatar_id
|
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(),
|
.unwrap_or_default(),
|
||||||
)?;
|
)?;
|
||||||
actor.object_props.set_icon_object(avatar)?;
|
actor.object_props.set_icon_object(avatar)?;
|
||||||
@@ -632,9 +642,32 @@ impl User {
|
|||||||
Ok(CustomPerson::new(actor, ap_signature))
|
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 {
|
pub fn avatar_url(&self, conn: &Connection) -> String {
|
||||||
self.avatar_id
|
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())
|
.unwrap_or_else(|| "/static/default-avatar.png".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -828,7 +861,22 @@ impl AsActor<&PlumeRocket> for User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_local(&self) -> bool {
|
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,
|
display_name,
|
||||||
is_admin,
|
is_admin,
|
||||||
summary: summary.to_owned(),
|
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),
|
email: Some(email),
|
||||||
hashed_password: Some(password),
|
hashed_password: Some(password),
|
||||||
instance_id: Instance::get_local(conn)?.id,
|
instance_id: Instance::get_local()?.id,
|
||||||
ap_url: String::new(),
|
ap_url: String::new(),
|
||||||
public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?,
|
public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?,
|
||||||
private_key: Some(String::from_utf8(priv_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!(
|
assert_eq!(
|
||||||
test_user.id,
|
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()
|
.unwrap()
|
||||||
.id
|
.id
|
||||||
);
|
);
|
||||||
@@ -979,7 +1027,7 @@ pub(crate) mod tests {
|
|||||||
conn,
|
conn,
|
||||||
&format!(
|
&format!(
|
||||||
"https://{}/@/{}/",
|
"https://{}/@/{}/",
|
||||||
Instance::get_local(conn).unwrap().public_domain,
|
Instance::get_local().unwrap().public_domain,
|
||||||
"test"
|
"test"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1010,7 +1058,7 @@ pub(crate) mod tests {
|
|||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let inserted = fill_database(conn);
|
let inserted = fill_database(conn);
|
||||||
let local_inst = Instance::get_local(conn).unwrap();
|
let local_inst = Instance::get_local().unwrap();
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while local_inst.has_admin(conn).unwrap() {
|
while local_inst.has_admin(conn).unwrap() {
|
||||||
assert!(i < 100); //prevent from looping indefinitelly
|
assert!(i < 100); //prevent from looping indefinitelly
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
extern crate diesel;
|
extern crate diesel;
|
||||||
#[macro_use]
|
extern crate plume_common;
|
||||||
extern crate diesel_migrations;
|
|
||||||
|
|
||||||
extern crate plume_models;
|
extern crate plume_models;
|
||||||
|
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
|
use plume_common::utils::random_hex;
|
||||||
|
use plume_models::migrations::IMPORTED_MIGRATIONS;
|
||||||
use plume_models::{Connection as Conn, CONFIG};
|
use plume_models::{Connection as Conn, CONFIG};
|
||||||
|
|
||||||
#[cfg(feature = "sqlite")]
|
use std::env::temp_dir;
|
||||||
embed_migrations!("../migrations/sqlite");
|
|
||||||
|
|
||||||
#[cfg(feature = "postgres")]
|
|
||||||
embed_migrations!("../migrations/postgres");
|
|
||||||
|
|
||||||
fn db() -> Conn {
|
fn db() -> Conn {
|
||||||
let conn =
|
let conn =
|
||||||
Conn::establish(CONFIG.database_url.as_str()).expect("Couldn't connect to the database");
|
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
|
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
|
#!/bin/bash
|
||||||
mkdir bin
|
mkdir bin
|
||||||
cp target/release/{plume,plm} bin
|
cp target/release/{plume,plm} bin
|
||||||
cp "$(which diesel)" bin
|
|
||||||
strip -s 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="
|
export ROCKET_SECRET_KEY="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||||
|
|
||||||
mkdir -p "target/cov/plume"
|
plm migration run
|
||||||
mkdir -p "target/cov/plm"
|
plm migration redo
|
||||||
plm='kcov --exclude-pattern=/.cargo,/usr/lib --verify target/cov/plm plm'
|
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
|
plume &
|
||||||
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 &
|
|
||||||
caddy -conf /Caddyfile &
|
caddy -conf /Caddyfile &
|
||||||
|
|
||||||
until curl http://localhost:7878/test/health -f; do sleep 1; done 2>/dev/null >/dev/null
|
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 -SIGINT %1
|
||||||
kill -SIGKILL %2
|
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"
|
||||||
+2
-6
@@ -24,13 +24,9 @@ if [ $ARCH == "aarch64" -o $ARCH == "armv71" ] ; then
|
|||||||
apt-get install -y --no-install-recommends build-essential subversion ninja-build cmake
|
apt-get install -y --no-install-recommends build-essential subversion ninja-build cmake
|
||||||
mkdir -p /scratch/src
|
mkdir -p /scratch/src
|
||||||
cd /scratch/src
|
cd /scratch/src
|
||||||
# Pin LLVM to post 7.0.1 tag and pin to a known-good revision for Plume builds
|
svn co http://llvm.org/svn/llvm-project/llvm/tags/RELEASE_800/final/ llvm
|
||||||
svn co -r350977 http://llvm.org/svn/llvm-project/llvm/trunk/ llvm
|
|
||||||
cd /scratch/src/llvm/tools
|
cd /scratch/src/llvm/tools
|
||||||
# Pin lld to post 7.0.1 tag and pin to a known-good revision for Plume builds
|
svn co http://llvm.org/svn/llvm-project/lld/tags/RELEASE_800/final/ lld
|
||||||
svn co -r350975 http://llvm.org/svn/llvm-project/lld/trunk lld
|
|
||||||
#svn co http://llvm.org/svn/llvm-project/cfe/trunk clang
|
|
||||||
#svn co http://llvm.org/svn/llvm-project/clang-tools-extra/trunk extra
|
|
||||||
mkdir -p /scratch/build/arm
|
mkdir -p /scratch/build/arm
|
||||||
cd /scratch/build/arm
|
cd /scratch/build/arm
|
||||||
if [ "$ARCH" == "aarch64" ] ; then
|
if [ "$ARCH" == "aarch64" ] ; then
|
||||||
|
|||||||
+19
-7
@@ -1,12 +1,24 @@
|
|||||||
use canapi::Provider;
|
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json;
|
|
||||||
|
|
||||||
use plume_api::apps::AppEndpoint;
|
use crate::api::Api;
|
||||||
use plume_models::{apps::App, db_conn::DbConn, Connection};
|
use plume_api::apps::NewAppData;
|
||||||
|
use plume_common::utils::random_hex;
|
||||||
|
use plume_models::{apps::*, db_conn::DbConn};
|
||||||
|
|
||||||
#[post("/apps", data = "<data>")]
|
#[post("/apps", data = "<data>")]
|
||||||
pub fn create(conn: DbConn, data: Json<AppEndpoint>) -> Json<serde_json::Value> {
|
pub fn create(conn: DbConn, data: Json<NewAppData>) -> Api<App> {
|
||||||
let post = <App as Provider<Connection>>::create(&*conn, (*data).clone()).ok();
|
let client_id = random_hex();
|
||||||
Json(json!(post))
|
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_common::utils::random_hex;
|
||||||
use plume_models::{api_tokens::*, apps::App, users::User, Error, PlumeRocket};
|
use plume_models::{api_tokens::*, apps::App, users::User, Error, PlumeRocket};
|
||||||
|
|
||||||
|
type Api<T> = Result<Json<T>, ApiError>;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ApiError(Error);
|
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 {
|
impl<'r> Responder<'r> for ApiError {
|
||||||
fn respond_to(self, req: &Request) -> response::Result<'r> {
|
fn respond_to(self, req: &Request) -> response::Result<'r> {
|
||||||
match self.0 {
|
match self.0 {
|
||||||
|
|||||||
+221
-39
@@ -1,54 +1,236 @@
|
|||||||
use canapi::{Error as ApiError, Provider};
|
use chrono::NaiveDateTime;
|
||||||
use rocket::http::uri::Origin;
|
use heck::{CamelCase, KebabCase};
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json;
|
|
||||||
use serde_qs;
|
|
||||||
|
|
||||||
use api::authorization::*;
|
use crate::api::{authorization::*, Api};
|
||||||
use plume_api::posts::PostEndpoint;
|
use plume_api::posts::*;
|
||||||
use plume_models::{posts::Post, users::User, PlumeRocket};
|
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>")]
|
#[get("/posts/<id>")]
|
||||||
pub fn get(
|
pub fn get(id: i32, auth: Option<Authorization<Read, Post>>, conn: DbConn) -> Api<PostData> {
|
||||||
id: i32,
|
let user = auth.and_then(|a| User::get(&conn, a.0.user_id).ok());
|
||||||
auth: Option<Authorization<Read, Post>>,
|
let post = Post::get(&conn, id)?;
|
||||||
mut rockets: PlumeRocket,
|
|
||||||
) -> Json<serde_json::Value> {
|
if !post.published
|
||||||
rockets.user = auth.and_then(|a| User::get(&*rockets.conn, a.0.user_id).ok());
|
&& !user
|
||||||
let post = <Post as Provider<PlumeRocket>>::get(&rockets, id).ok();
|
.and_then(|u| post.is_author(&conn, u.id).ok())
|
||||||
Json(json!(post))
|
.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(
|
pub fn list(
|
||||||
uri: &Origin,
|
title: Option<String>,
|
||||||
|
subtitle: Option<String>,
|
||||||
|
content: Option<String>,
|
||||||
auth: Option<Authorization<Read, Post>>,
|
auth: Option<Authorization<Read, Post>>,
|
||||||
mut rockets: PlumeRocket,
|
conn: DbConn,
|
||||||
) -> Json<serde_json::Value> {
|
) -> Api<Vec<PostData>> {
|
||||||
rockets.user = auth.and_then(|a| User::get(&*rockets.conn, a.0.user_id).ok());
|
let user = auth.and_then(|a| User::get(&conn, a.0.user_id).ok());
|
||||||
let query: PostEndpoint =
|
let user_id = user.map(|u| u.id);
|
||||||
serde_qs::from_str(uri.query().unwrap_or("")).expect("api::list: invalid query error");
|
|
||||||
let post = <Post as Provider<PlumeRocket>>::list(&rockets, query);
|
Ok(Json(
|
||||||
Json(json!(post))
|
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>")]
|
#[post("/posts", data = "<payload>")]
|
||||||
pub fn create(
|
pub fn create(
|
||||||
auth: Authorization<Write, Post>,
|
auth: Authorization<Write, Post>,
|
||||||
payload: Json<PostEndpoint>,
|
payload: Json<NewPostData>,
|
||||||
mut rockets: PlumeRocket,
|
rockets: PlumeRocket,
|
||||||
) -> Json<serde_json::Value> {
|
) -> Api<PostData> {
|
||||||
rockets.user = User::get(&*rockets.conn, auth.0.user_id).ok();
|
let conn = &*rockets.conn;
|
||||||
let new_post = <Post as Provider<PlumeRocket>>::create(&rockets, (*payload).clone());
|
let search = &rockets.searcher;
|
||||||
Json(new_post.map(|p| json!(p)).unwrap_or_else(|e| {
|
let worker = &rockets.worker;
|
||||||
json!({
|
|
||||||
"error": "Invalid data, couldn't create new post",
|
let author = User::get(conn, auth.0.user_id)?;
|
||||||
"details": match e {
|
|
||||||
ApiError::Fetch(msg) => msg,
|
let slug = &payload.title.clone().to_kebab_case();
|
||||||
ApiError::SerDe(msg) => msg,
|
let date = payload.creation_date.clone().and_then(|d| {
|
||||||
ApiError::NotFound(msg) => msg,
|
NaiveDateTime::parse_from_str(format!("{} 00:00:00", d).as_ref(), "%Y-%m-%d %H:%M:%S").ok()
|
||||||
ApiError::Authorization(msg) => msg,
|
});
|
||||||
}
|
|
||||||
})
|
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(()))
|
||||||
|
}
|
||||||
|
|||||||
+30
-10
@@ -1,10 +1,9 @@
|
|||||||
#![allow(clippy::too_many_arguments)]
|
#![allow(clippy::too_many_arguments)]
|
||||||
#![feature(decl_macro, proc_macro_hygiene)]
|
#![feature(decl_macro, proc_macro_hygiene, try_trait)]
|
||||||
|
|
||||||
extern crate activitypub;
|
extern crate activitypub;
|
||||||
extern crate askama_escape;
|
extern crate askama_escape;
|
||||||
extern crate atom_syndication;
|
extern crate atom_syndication;
|
||||||
extern crate canapi;
|
|
||||||
extern crate chrono;
|
extern crate chrono;
|
||||||
extern crate colored;
|
extern crate colored;
|
||||||
extern crate ctrlc;
|
extern crate ctrlc;
|
||||||
@@ -42,10 +41,11 @@ extern crate webfinger;
|
|||||||
use diesel::r2d2::ConnectionManager;
|
use diesel::r2d2::ConnectionManager;
|
||||||
use plume_models::{
|
use plume_models::{
|
||||||
db_conn::{DbPool, PragmaForeignKey},
|
db_conn::{DbPool, PragmaForeignKey},
|
||||||
|
instance::Instance,
|
||||||
|
migrations::IMPORTED_MIGRATIONS,
|
||||||
search::{Searcher as UnmanagedSearcher, SearcherError},
|
search::{Searcher as UnmanagedSearcher, SearcherError},
|
||||||
Connection, Error, CONFIG,
|
Connection, Error, CONFIG,
|
||||||
};
|
};
|
||||||
use rocket::State;
|
|
||||||
use rocket_csrf::CsrfFairingBuilder;
|
use rocket_csrf::CsrfFairingBuilder;
|
||||||
use scheduled_thread_pool::ScheduledThreadPool;
|
use scheduled_thread_pool::ScheduledThreadPool;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
@@ -62,6 +62,8 @@ mod mail;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod template_utils;
|
mod template_utils;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate shrinkwraprs;
|
||||||
#[cfg(feature = "test")]
|
#[cfg(feature = "test")]
|
||||||
mod test_routes;
|
mod test_routes;
|
||||||
|
|
||||||
@@ -69,21 +71,37 @@ include!(concat!(env!("OUT_DIR"), "/templates.rs"));
|
|||||||
|
|
||||||
compile_i18n!();
|
compile_i18n!();
|
||||||
|
|
||||||
type Searcher<'a> = State<'a, Arc<UnmanagedSearcher>>;
|
|
||||||
|
|
||||||
/// Initializes a database pool.
|
/// Initializes a database pool.
|
||||||
fn init_pool() -> Option<DbPool> {
|
fn init_pool() -> Option<DbPool> {
|
||||||
dotenv::dotenv().ok();
|
dotenv::dotenv().unwrap();
|
||||||
|
|
||||||
let manager = ConnectionManager::<Connection>::new(CONFIG.database_url.as_str());
|
let manager = ConnectionManager::<Connection>::new(CONFIG.database_url.as_str());
|
||||||
DbPool::builder()
|
let pool = DbPool::builder()
|
||||||
.connection_customizer(Box::new(PragmaForeignKey))
|
.connection_customizer(Box::new(PragmaForeignKey))
|
||||||
.build(manager)
|
.build(manager)
|
||||||
.ok()
|
.ok()?;
|
||||||
|
Instance::cache_local(&pool.get().unwrap());
|
||||||
|
Some(pool)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let dbpool = init_pool().expect("main: database pool initialization error");
|
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());
|
let workpool = ScheduledThreadPool::with_name("worker {}", num_cpus::get());
|
||||||
// we want a fast exit here, so
|
// we want a fast exit here, so
|
||||||
#[allow(clippy::match_wild_err_arm)]
|
#[allow(clippy::match_wild_err_arm)]
|
||||||
@@ -102,8 +120,8 @@ Then try to restart Plume.
|
|||||||
SearcherError::IndexOpeningError => panic!(
|
SearcherError::IndexOpeningError => panic!(
|
||||||
r#"
|
r#"
|
||||||
Plume was unable to open the search index. If you created the index
|
Plume was unable to open the search index. If you created the index
|
||||||
before, make sure to run Plume in the same directory it was created in, or
|
before, make sure to run Plume in the same directory it was created in, or
|
||||||
to set SEARCH_INDEX accordingly. If you did not yet create the search
|
to set SEARCH_INDEX accordingly. If you did not yet create the search
|
||||||
index, run this command:
|
index, run this command:
|
||||||
|
|
||||||
plm search init
|
plm search init
|
||||||
@@ -169,6 +187,7 @@ Then try to restart Plume
|
|||||||
routes::instance::interact,
|
routes::instance::interact,
|
||||||
routes::instance::nodeinfo,
|
routes::instance::nodeinfo,
|
||||||
routes::instance::about,
|
routes::instance::about,
|
||||||
|
routes::instance::privacy,
|
||||||
routes::instance::web_manifest,
|
routes::instance::web_manifest,
|
||||||
routes::likes::create,
|
routes::likes::create,
|
||||||
routes::likes::create_auth,
|
routes::likes::create_auth,
|
||||||
@@ -237,6 +256,7 @@ Then try to restart Plume
|
|||||||
api::posts::get,
|
api::posts::get,
|
||||||
api::posts::list,
|
api::posts::list,
|
||||||
api::posts::create,
|
api::posts::create,
|
||||||
|
api::posts::delete,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.register(catchers![
|
.register(catchers![
|
||||||
|
|||||||
+151
-144
@@ -16,22 +16,20 @@ use plume_models::{
|
|||||||
blog_authors::*, blogs::*, instance::Instance, medias::*, posts::Post, safe_string::SafeString,
|
blog_authors::*, blogs::*, instance::Instance, medias::*, posts::Post, safe_string::SafeString,
|
||||||
users::User, Connection, PlumeRocket,
|
users::User, Connection, PlumeRocket,
|
||||||
};
|
};
|
||||||
use routes::{errors::ErrorPage, Page};
|
use routes::{errors::ErrorPage, Page, RespondOrRedirect};
|
||||||
use template_utils::Ructe;
|
use template_utils::{IntoContext, Ructe};
|
||||||
|
|
||||||
#[get("/~/<name>?<page>", rank = 2)]
|
#[get("/~/<name>?<page>", rank = 2)]
|
||||||
pub fn details(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
pub fn details(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||||
let page = page.unwrap_or_default();
|
let page = page.unwrap_or_default();
|
||||||
let conn = &*rockets.conn;
|
let conn = &*rockets.conn;
|
||||||
let blog = Blog::find_by_fqn(&rockets, &name)?;
|
let blog = Blog::find_by_fqn(&rockets, &name)?;
|
||||||
let posts = Post::blog_page(&*conn, &blog, page.limits())?;
|
let posts = Post::blog_page(conn, &blog, page.limits())?;
|
||||||
let articles_count = Post::count_for_blog(&*conn, &blog)?;
|
let articles_count = Post::count_for_blog(conn, &blog)?;
|
||||||
let authors = &blog.list_authors(&*conn)?;
|
let authors = &blog.list_authors(conn)?;
|
||||||
let user = rockets.user;
|
|
||||||
let intl = rockets.intl;
|
|
||||||
|
|
||||||
Ok(render!(blogs::details(
|
Ok(render!(blogs::details(
|
||||||
&(&*conn, &intl.catalog, user.clone()),
|
&rockets.to_context(),
|
||||||
blog,
|
blog,
|
||||||
authors,
|
authors,
|
||||||
page.0,
|
page.0,
|
||||||
@@ -51,13 +49,9 @@ pub fn activity_details(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/blogs/new")]
|
#[get("/blogs/new")]
|
||||||
pub fn new(rockets: PlumeRocket) -> Ructe {
|
pub fn new(rockets: PlumeRocket, _user: User) -> Ructe {
|
||||||
let user = rockets.user.unwrap();
|
|
||||||
let intl = rockets.intl;
|
|
||||||
let conn = &*rockets.conn;
|
|
||||||
|
|
||||||
render!(blogs::new(
|
render!(blogs::new(
|
||||||
&(&*conn, &intl.catalog, Some(user)),
|
&rockets.to_context(),
|
||||||
&NewBlogForm::default(),
|
&NewBlogForm::default(),
|
||||||
ValidationErrors::default()
|
ValidationErrors::default()
|
||||||
))
|
))
|
||||||
@@ -90,7 +84,7 @@ fn valid_slug(title: &str) -> Result<(), ValidationError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/blogs/new", data = "<form>")]
|
#[post("/blogs/new", data = "<form>")]
|
||||||
pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> Result<Redirect, Ructe> {
|
pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> RespondOrRedirect {
|
||||||
let slug = utils::make_actor_id(&form.title);
|
let slug = utils::make_actor_id(&form.title);
|
||||||
let conn = &*rockets.conn;
|
let conn = &*rockets.conn;
|
||||||
let intl = &rockets.intl.catalog;
|
let intl = &rockets.intl.catalog;
|
||||||
@@ -114,63 +108,69 @@ pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> Result<Re
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.is_empty() {
|
if !errors.is_empty() {
|
||||||
let blog = Blog::insert(
|
return render!(blogs::new(&rockets.to_context(), &*form, errors)).into();
|
||||||
&*conn,
|
|
||||||
NewBlog::new_local(
|
|
||||||
slug.clone(),
|
|
||||||
form.title.to_string(),
|
|
||||||
String::from(""),
|
|
||||||
Instance::get_local(&*conn)
|
|
||||||
.expect("blog::create: instance error")
|
|
||||||
.id,
|
|
||||||
)
|
|
||||||
.expect("blog::create: new local error"),
|
|
||||||
)
|
|
||||||
.expect("blog::create: error");
|
|
||||||
|
|
||||||
BlogAuthor::insert(
|
|
||||||
&*conn,
|
|
||||||
NewBlogAuthor {
|
|
||||||
blog_id: blog.id,
|
|
||||||
author_id: user.id,
|
|
||||||
is_owner: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("blog::create: author error");
|
|
||||||
|
|
||||||
Ok(Redirect::to(uri!(details: name = slug.clone(), page = _)))
|
|
||||||
} else {
|
|
||||||
Err(render!(blogs::new(
|
|
||||||
&(&*conn, intl, Some(user)),
|
|
||||||
&*form,
|
|
||||||
errors
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let blog = Blog::insert(
|
||||||
|
&*conn,
|
||||||
|
NewBlog::new_local(
|
||||||
|
slug.clone(),
|
||||||
|
form.title.to_string(),
|
||||||
|
String::from(""),
|
||||||
|
Instance::get_local()
|
||||||
|
.expect("blog::create: instance error")
|
||||||
|
.id,
|
||||||
|
)
|
||||||
|
.expect("blog::create: new local error"),
|
||||||
|
)
|
||||||
|
.expect("blog::create: error");
|
||||||
|
|
||||||
|
BlogAuthor::insert(
|
||||||
|
&*conn,
|
||||||
|
NewBlogAuthor {
|
||||||
|
blog_id: blog.id,
|
||||||
|
author_id: user.id,
|
||||||
|
is_owner: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("blog::create: author error");
|
||||||
|
|
||||||
|
Flash::success(
|
||||||
|
Redirect::to(uri!(details: name = slug.clone(), page = _)),
|
||||||
|
&i18n!(intl, "Your blog was successfully created!"),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/~/<name>/delete")]
|
#[post("/~/<name>/delete")]
|
||||||
pub fn delete(name: String, rockets: PlumeRocket) -> Result<Redirect, Ructe> {
|
pub fn delete(name: String, rockets: PlumeRocket) -> RespondOrRedirect {
|
||||||
let conn = &*rockets.conn;
|
let conn = &*rockets.conn;
|
||||||
let blog = Blog::find_by_fqn(&rockets, &name).expect("blog::delete: blog not found");
|
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()
|
.clone()
|
||||||
.and_then(|u| u.is_author_in(&*conn, &blog).ok())
|
.and_then(|u| u.is_author_in(&*conn, &blog).ok())
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
blog.delete(&conn, &searcher)
|
blog.delete(&conn, &rockets.searcher)
|
||||||
.expect("blog::expect: deletion error");
|
.expect("blog::expect: deletion error");
|
||||||
Ok(Redirect::to(uri!(super::instance::index)))
|
Flash::success(
|
||||||
|
Redirect::to(uri!(super::instance::index)),
|
||||||
|
i18n!(rockets.intl.catalog, "Your blog was deleted."),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
} else {
|
} else {
|
||||||
// TODO actually return 403 error code
|
// TODO actually return 403 error code
|
||||||
Err(render!(errors::not_authorized(
|
render!(errors::not_authorized(
|
||||||
&(&*conn, &intl.catalog, user),
|
&rockets.to_context(),
|
||||||
i18n!(intl.catalog, "You are not allowed to delete this blog.")
|
i18n!(
|
||||||
)))
|
rockets.intl.catalog,
|
||||||
|
"You are not allowed to delete this blog."
|
||||||
|
)
|
||||||
|
))
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,15 +190,16 @@ pub fn edit(name: String, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
|||||||
if rockets
|
if rockets
|
||||||
.user
|
.user
|
||||||
.clone()
|
.clone()
|
||||||
.and_then(|u| u.is_author_in(&*conn, &blog).ok())
|
.and_then(|u| u.is_author_in(conn, &blog).ok())
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
let user = rockets
|
let user = rockets
|
||||||
.user
|
.user
|
||||||
|
.clone()
|
||||||
.expect("blogs::edit: User was None while it shouldn't");
|
.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(
|
Ok(render!(blogs::edit(
|
||||||
&(&*conn, &rockets.intl.catalog, Some(user)),
|
&rockets.to_context(),
|
||||||
&blog,
|
&blog,
|
||||||
medias,
|
medias,
|
||||||
&EditForm {
|
&EditForm {
|
||||||
@@ -212,7 +213,7 @@ pub fn edit(name: String, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
|||||||
} else {
|
} else {
|
||||||
// TODO actually return 403 error code
|
// TODO actually return 403 error code
|
||||||
Ok(render!(errors::not_authorized(
|
Ok(render!(errors::not_authorized(
|
||||||
&(&*conn, &rockets.intl.catalog, rockets.user),
|
&rockets.to_context(),
|
||||||
i18n!(
|
i18n!(
|
||||||
rockets.intl.catalog,
|
rockets.intl.catalog,
|
||||||
"You are not allowed to edit this blog."
|
"You are not allowed to edit this blog."
|
||||||
@@ -235,101 +236,107 @@ pub fn update(
|
|||||||
name: String,
|
name: String,
|
||||||
form: LenientForm<EditForm>,
|
form: LenientForm<EditForm>,
|
||||||
rockets: PlumeRocket,
|
rockets: PlumeRocket,
|
||||||
) -> Result<Redirect, Ructe> {
|
) -> RespondOrRedirect {
|
||||||
let conn = &*rockets.conn;
|
let conn = &*rockets.conn;
|
||||||
let intl = &rockets.intl.catalog;
|
let intl = &rockets.intl.catalog;
|
||||||
let mut blog = Blog::find_by_fqn(&rockets, &name).expect("blog::update: blog not found");
|
let mut blog = Blog::find_by_fqn(&rockets, &name).expect("blog::update: blog not found");
|
||||||
if rockets
|
if !rockets
|
||||||
.user
|
.user
|
||||||
.clone()
|
.clone()
|
||||||
.and_then(|u| u.is_author_in(&*conn, &blog).ok())
|
.and_then(|u| u.is_author_in(&*conn, &blog).ok())
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
let user = rockets
|
|
||||||
.user
|
|
||||||
.clone()
|
|
||||||
.expect("blogs::edit: User was None while it shouldn't");
|
|
||||||
form.validate()
|
|
||||||
.and_then(|_| {
|
|
||||||
if let Some(icon) = form.icon {
|
|
||||||
if !check_media(&*conn, icon, &user) {
|
|
||||||
let mut errors = ValidationErrors::new();
|
|
||||||
errors.add(
|
|
||||||
"",
|
|
||||||
ValidationError {
|
|
||||||
code: Cow::from("icon"),
|
|
||||||
message: Some(Cow::from(i18n!(
|
|
||||||
intl,
|
|
||||||
"You can't use this media as a blog icon."
|
|
||||||
))),
|
|
||||||
params: HashMap::new(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return Err(errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(banner) = form.banner {
|
|
||||||
if !check_media(&*conn, banner, &user) {
|
|
||||||
let mut errors = ValidationErrors::new();
|
|
||||||
errors.add(
|
|
||||||
"",
|
|
||||||
ValidationError {
|
|
||||||
code: Cow::from("banner"),
|
|
||||||
message: Some(Cow::from(i18n!(
|
|
||||||
intl,
|
|
||||||
"You can't use this media as a blog banner."
|
|
||||||
))),
|
|
||||||
params: HashMap::new(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return Err(errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blog.title = form.title.clone();
|
|
||||||
blog.summary = form.summary.clone();
|
|
||||||
blog.summary_html = SafeString::new(
|
|
||||||
&utils::md_to_html(
|
|
||||||
&form.summary,
|
|
||||||
"",
|
|
||||||
true,
|
|
||||||
Some(Media::get_media_processor(
|
|
||||||
&conn,
|
|
||||||
blog.list_authors(&conn)
|
|
||||||
.expect("Couldn't get list of authors")
|
|
||||||
.iter()
|
|
||||||
.collect(),
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.0,
|
|
||||||
);
|
|
||||||
blog.icon_id = form.icon;
|
|
||||||
blog.banner_id = form.banner;
|
|
||||||
blog.save_changes::<Blog>(&*conn)
|
|
||||||
.expect("Couldn't save blog changes");
|
|
||||||
Ok(Redirect::to(uri!(details: name = name, page = _)))
|
|
||||||
})
|
|
||||||
.map_err(|err| {
|
|
||||||
let medias = Media::for_user(&*conn, user.id).expect("Couldn't list media");
|
|
||||||
render!(blogs::edit(
|
|
||||||
&(&*conn, intl, Some(user)),
|
|
||||||
&blog,
|
|
||||||
medias,
|
|
||||||
&*form,
|
|
||||||
err
|
|
||||||
))
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// TODO actually return 403 error code
|
// TODO actually return 403 error code
|
||||||
Err(render!(errors::not_authorized(
|
return render!(errors::not_authorized(
|
||||||
&(&*conn, &rockets.intl.catalog, rockets.user),
|
&rockets.to_context(),
|
||||||
i18n!(
|
i18n!(
|
||||||
rockets.intl.catalog,
|
rockets.intl.catalog,
|
||||||
"You are not allowed to edit this blog."
|
"You are not allowed to edit this blog."
|
||||||
)
|
)
|
||||||
)))
|
))
|
||||||
|
.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let user = rockets
|
||||||
|
.user
|
||||||
|
.clone()
|
||||||
|
.expect("blogs::edit: User was None while it shouldn't");
|
||||||
|
form.validate()
|
||||||
|
.and_then(|_| {
|
||||||
|
if let Some(icon) = form.icon {
|
||||||
|
if !check_media(&*conn, icon, &user) {
|
||||||
|
let mut errors = ValidationErrors::new();
|
||||||
|
errors.add(
|
||||||
|
"",
|
||||||
|
ValidationError {
|
||||||
|
code: Cow::from("icon"),
|
||||||
|
message: Some(Cow::from(i18n!(
|
||||||
|
intl,
|
||||||
|
"You can't use this media as a blog icon."
|
||||||
|
))),
|
||||||
|
params: HashMap::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return Err(errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(banner) = form.banner {
|
||||||
|
if !check_media(&*conn, banner, &user) {
|
||||||
|
let mut errors = ValidationErrors::new();
|
||||||
|
errors.add(
|
||||||
|
"",
|
||||||
|
ValidationError {
|
||||||
|
code: Cow::from("banner"),
|
||||||
|
message: Some(Cow::from(i18n!(
|
||||||
|
intl,
|
||||||
|
"You can't use this media as a blog banner."
|
||||||
|
))),
|
||||||
|
params: HashMap::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return Err(errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blog.title = form.title.clone();
|
||||||
|
blog.summary = form.summary.clone();
|
||||||
|
blog.summary_html = SafeString::new(
|
||||||
|
&utils::md_to_html(
|
||||||
|
&form.summary,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
Some(Media::get_media_processor(
|
||||||
|
&conn,
|
||||||
|
blog.list_authors(&conn)
|
||||||
|
.expect("Couldn't get list of authors")
|
||||||
|
.iter()
|
||||||
|
.collect(),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.0,
|
||||||
|
);
|
||||||
|
blog.icon_id = form.icon;
|
||||||
|
blog.banner_id = form.banner;
|
||||||
|
blog.save_changes::<Blog>(&*conn)
|
||||||
|
.expect("Couldn't save blog changes");
|
||||||
|
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(
|
||||||
|
&rockets.to_context(),
|
||||||
|
&blog,
|
||||||
|
medias,
|
||||||
|
&*form,
|
||||||
|
err
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/~/<name>/outbox")]
|
#[get("/~/<name>/outbox")]
|
||||||
@@ -344,7 +351,7 @@ pub fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>>
|
|||||||
let conn = &*rockets.conn;
|
let conn = &*rockets.conn;
|
||||||
let feed = FeedBuilder::default()
|
let feed = FeedBuilder::default()
|
||||||
.title(blog.title.clone())
|
.title(blog.title.clone())
|
||||||
.id(Instance::get_local(&*conn)
|
.id(Instance::get_local()
|
||||||
.ok()?
|
.ok()?
|
||||||
.compute_box("~", &name, "atom.xml"))
|
.compute_box("~", &name, "atom.xml"))
|
||||||
.entries(
|
.entries(
|
||||||
|
|||||||
+23
-12
@@ -1,5 +1,8 @@
|
|||||||
use activitypub::object::Note;
|
use activitypub::object::Note;
|
||||||
use rocket::{request::LenientForm, response::Redirect};
|
use rocket::{
|
||||||
|
request::LenientForm,
|
||||||
|
response::{Flash, Redirect},
|
||||||
|
};
|
||||||
use template_utils::Ructe;
|
use template_utils::Ructe;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
@@ -14,6 +17,7 @@ use plume_models::{
|
|||||||
posts::Post, safe_string::SafeString, tags::Tag, users::User, Error, PlumeRocket,
|
posts::Post, safe_string::SafeString, tags::Tag, users::User, Error, PlumeRocket,
|
||||||
};
|
};
|
||||||
use routes::errors::ErrorPage;
|
use routes::errors::ErrorPage;
|
||||||
|
use template_utils::IntoContext;
|
||||||
|
|
||||||
#[derive(Default, FromForm, Debug, Validate)]
|
#[derive(Default, FromForm, Debug, Validate)]
|
||||||
pub struct NewCommentForm {
|
pub struct NewCommentForm {
|
||||||
@@ -30,7 +34,7 @@ pub fn create(
|
|||||||
form: LenientForm<NewCommentForm>,
|
form: LenientForm<NewCommentForm>,
|
||||||
user: User,
|
user: User,
|
||||||
rockets: PlumeRocket,
|
rockets: PlumeRocket,
|
||||||
) -> Result<Redirect, Ructe> {
|
) -> Result<Flash<Redirect>, Ructe> {
|
||||||
let conn = &*rockets.conn;
|
let conn = &*rockets.conn;
|
||||||
let blog = Blog::find_by_fqn(&rockets, &blog_name).expect("comments::create: blog error");
|
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");
|
let post = Post::find_by_slug(&*conn, &slug, blog.id).expect("comments::create: post error");
|
||||||
@@ -38,9 +42,11 @@ pub fn create(
|
|||||||
.map(|_| {
|
.map(|_| {
|
||||||
let (html, mentions, _hashtags) = utils::md_to_html(
|
let (html, mentions, _hashtags) = utils::md_to_html(
|
||||||
form.content.as_ref(),
|
form.content.as_ref(),
|
||||||
&Instance::get_local(&conn)
|
Some(
|
||||||
.expect("comments::create: local instance error")
|
&Instance::get_local()
|
||||||
.public_domain,
|
.expect("comments::create: local instance error")
|
||||||
|
.public_domain,
|
||||||
|
),
|
||||||
true,
|
true,
|
||||||
Some(Media::get_media_processor(&conn, vec![&user])),
|
Some(Media::get_media_processor(&conn, vec![&user])),
|
||||||
);
|
);
|
||||||
@@ -58,7 +64,6 @@ pub fn create(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.expect("comments::create: insert error");
|
.expect("comments::create: insert error");
|
||||||
comm.notify(&*conn).expect("comments::create: notify error");
|
|
||||||
let new_comment = comm
|
let new_comment = comm
|
||||||
.create_activity(&rockets)
|
.create_activity(&rockets)
|
||||||
.expect("comments::create: activity error");
|
.expect("comments::create: activity error");
|
||||||
@@ -76,6 +81,8 @@ pub fn create(
|
|||||||
.expect("comments::create: mention save error");
|
.expect("comments::create: mention save error");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
comm.notify(&*conn).expect("comments::create: notify error");
|
||||||
|
|
||||||
// federate
|
// federate
|
||||||
let dest = User::one_by_instance(&*conn).expect("comments::create: dest error");
|
let dest = User::one_by_instance(&*conn).expect("comments::create: dest error");
|
||||||
let user_clone = user.clone();
|
let user_clone = user.clone();
|
||||||
@@ -83,8 +90,11 @@ pub fn create(
|
|||||||
.worker
|
.worker
|
||||||
.execute(move || broadcast(&user_clone, new_comment, dest));
|
.execute(move || broadcast(&user_clone, new_comment, dest));
|
||||||
|
|
||||||
Redirect::to(
|
Flash::success(
|
||||||
uri!(super::posts::details: blog = blog_name, slug = slug, responding_to = _),
|
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| {
|
.map_err(|errors| {
|
||||||
@@ -97,7 +107,7 @@ pub fn create(
|
|||||||
.and_then(|r| Comment::get(&*conn, r).ok());
|
.and_then(|r| Comment::get(&*conn, r).ok());
|
||||||
|
|
||||||
render!(posts::details(
|
render!(posts::details(
|
||||||
&(&*conn, &rockets.intl.catalog, Some(user.clone())),
|
&rockets.to_context(),
|
||||||
post.clone(),
|
post.clone(),
|
||||||
blog,
|
blog,
|
||||||
&*form,
|
&*form,
|
||||||
@@ -134,7 +144,7 @@ pub fn delete(
|
|||||||
id: i32,
|
id: i32,
|
||||||
user: User,
|
user: User,
|
||||||
rockets: PlumeRocket,
|
rockets: PlumeRocket,
|
||||||
) -> Result<Redirect, ErrorPage> {
|
) -> Result<Flash<Redirect>, ErrorPage> {
|
||||||
if let Ok(comment) = Comment::get(&*rockets.conn, id) {
|
if let Ok(comment) = Comment::get(&*rockets.conn, id) {
|
||||||
if comment.author_id == user.id {
|
if comment.author_id == user.id {
|
||||||
let dest = User::one_by_instance(&*rockets.conn)?;
|
let dest = User::one_by_instance(&*rockets.conn)?;
|
||||||
@@ -157,8 +167,9 @@ pub fn delete(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Redirect::to(
|
Ok(Flash::success(
|
||||||
uri!(super::posts::details: blog = blog, slug = slug, responding_to = _),
|
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::{Error, PlumeRocket};
|
||||||
use plume_models::{db_conn::DbConn, Error};
|
|
||||||
use rocket::{
|
use rocket::{
|
||||||
request::FromRequest,
|
|
||||||
response::{self, Responder},
|
response::{self, Responder},
|
||||||
Request,
|
Request,
|
||||||
};
|
};
|
||||||
use rocket_i18n::I18n;
|
use template_utils::{IntoContext, Ructe};
|
||||||
use template_utils::Ructe;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ErrorPage(Error);
|
pub struct ErrorPage(Error);
|
||||||
@@ -19,78 +16,40 @@ impl From<Error> for ErrorPage {
|
|||||||
|
|
||||||
impl<'r> Responder<'r> for ErrorPage {
|
impl<'r> Responder<'r> for ErrorPage {
|
||||||
fn respond_to(self, req: &Request) -> response::Result<'r> {
|
fn respond_to(self, req: &Request) -> response::Result<'r> {
|
||||||
let conn = req.guard::<DbConn>().succeeded();
|
let rockets = req.guard::<PlumeRocket>().unwrap();
|
||||||
let intl = req.guard::<I18n>().succeeded();
|
|
||||||
let user = User::from_request(req).succeeded();
|
|
||||||
|
|
||||||
match self.0 {
|
match self.0 {
|
||||||
Error::NotFound => render!(errors::not_found(&(
|
Error::NotFound => render!(errors::not_found(&rockets.to_context())).respond_to(req),
|
||||||
&*conn.unwrap(),
|
Error::Unauthorized => {
|
||||||
&intl.unwrap().catalog,
|
render!(errors::not_found(&rockets.to_context())).respond_to(req)
|
||||||
user
|
}
|
||||||
)))
|
_ => render!(errors::not_found(&rockets.to_context())).respond_to(req),
|
||||||
.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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[catch(404)]
|
#[catch(404)]
|
||||||
pub fn not_found(req: &Request) -> Ructe {
|
pub fn not_found(req: &Request) -> Ructe {
|
||||||
let conn = req.guard::<DbConn>().succeeded();
|
let rockets = req.guard::<PlumeRocket>().unwrap();
|
||||||
let intl = req.guard::<I18n>().succeeded();
|
render!(errors::not_found(&rockets.to_context()))
|
||||||
let user = User::from_request(req).succeeded();
|
|
||||||
render!(errors::not_found(&(
|
|
||||||
&*conn.unwrap(),
|
|
||||||
&intl.unwrap().catalog,
|
|
||||||
user
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[catch(422)]
|
#[catch(422)]
|
||||||
pub fn unprocessable_entity(req: &Request) -> Ructe {
|
pub fn unprocessable_entity(req: &Request) -> Ructe {
|
||||||
let conn = req.guard::<DbConn>().succeeded();
|
let rockets = req.guard::<PlumeRocket>().unwrap();
|
||||||
let intl = req.guard::<I18n>().succeeded();
|
render!(errors::unprocessable_entity(&rockets.to_context()))
|
||||||
let user = User::from_request(req).succeeded();
|
|
||||||
render!(errors::unprocessable_entity(&(
|
|
||||||
&*conn.unwrap(),
|
|
||||||
&intl.unwrap().catalog,
|
|
||||||
user
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[catch(500)]
|
#[catch(500)]
|
||||||
pub fn server_error(req: &Request) -> Ructe {
|
pub fn server_error(req: &Request) -> Ructe {
|
||||||
let conn = req.guard::<DbConn>().succeeded();
|
let rockets = req.guard::<PlumeRocket>().unwrap();
|
||||||
let intl = req.guard::<I18n>().succeeded();
|
render!(errors::server_error(&rockets.to_context()))
|
||||||
let user = User::from_request(req).succeeded();
|
|
||||||
render!(errors::server_error(&(
|
|
||||||
&*conn.unwrap(),
|
|
||||||
&intl.unwrap().catalog,
|
|
||||||
user
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/csrf-violation?<target>")]
|
#[post("/csrf-violation?<target>")]
|
||||||
pub fn csrf_violation(
|
pub fn csrf_violation(target: Option<String>, rockets: PlumeRocket) -> Ructe {
|
||||||
target: Option<String>,
|
|
||||||
conn: DbConn,
|
|
||||||
intl: I18n,
|
|
||||||
user: Option<User>,
|
|
||||||
) -> Ructe {
|
|
||||||
if let Some(uri) = target {
|
if let Some(uri) = target {
|
||||||
eprintln!("Csrf violation while acceding \"{}\"", uri)
|
eprintln!("Csrf violation while acceding \"{}\"", uri)
|
||||||
}
|
}
|
||||||
render!(errors::csrf(&(&*conn, &intl.catalog, user)))
|
render!(errors::csrf(&rockets.to_context()))
|
||||||
}
|
}
|
||||||
|
|||||||
+127
-108
@@ -1,6 +1,6 @@
|
|||||||
use rocket::{
|
use rocket::{
|
||||||
request::LenientForm,
|
request::LenientForm,
|
||||||
response::{status, Redirect},
|
response::{status, Flash, Redirect},
|
||||||
};
|
};
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use rocket_i18n::I18n;
|
use rocket_i18n::I18n;
|
||||||
@@ -8,32 +8,32 @@ use serde_json;
|
|||||||
use validator::{Validate, ValidationErrors};
|
use validator::{Validate, ValidationErrors};
|
||||||
|
|
||||||
use inbox;
|
use inbox;
|
||||||
use plume_common::activity_pub::inbox::FromId;
|
use plume_common::activity_pub::{broadcast, inbox::FromId};
|
||||||
use plume_models::{
|
use plume_models::{
|
||||||
admin::Admin, comments::Comment, db_conn::DbConn, headers::Headers, instance::*, posts::Post,
|
admin::Admin, comments::Comment, db_conn::DbConn, headers::Headers, instance::*, posts::Post,
|
||||||
safe_string::SafeString, users::User, Error, PlumeRocket, CONFIG,
|
safe_string::SafeString, users::User, Error, PlumeRocket, CONFIG,
|
||||||
};
|
};
|
||||||
use routes::{errors::ErrorPage, rocket_uri_macro_static_files, Page};
|
use routes::{errors::ErrorPage, rocket_uri_macro_static_files, Page, RespondOrRedirect};
|
||||||
use template_utils::Ructe;
|
use template_utils::{IntoContext, Ructe};
|
||||||
use Searcher;
|
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
pub fn index(conn: DbConn, user: Option<User>, intl: I18n) -> Result<Ructe, ErrorPage> {
|
pub fn index(rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||||
let inst = Instance::get_local(&*conn)?;
|
let conn = &*rockets.conn;
|
||||||
let federated = Post::get_recents_page(&*conn, Page::default().limits())?;
|
let inst = Instance::get_local()?;
|
||||||
let local = Post::get_instance_page(&*conn, inst.id, Page::default().limits())?;
|
let federated = Post::get_recents_page(conn, Page::default().limits())?;
|
||||||
let user_feed = user.clone().and_then(|user| {
|
let local = Post::get_instance_page(conn, inst.id, Page::default().limits())?;
|
||||||
let followed = user.get_followed(&*conn).ok()?;
|
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>>();
|
let mut in_feed = followed.into_iter().map(|u| u.id).collect::<Vec<i32>>();
|
||||||
in_feed.push(user.id);
|
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(
|
Ok(render!(instance::index(
|
||||||
&(&*conn, &intl.catalog, user),
|
&rockets.to_context(),
|
||||||
inst,
|
inst,
|
||||||
User::count_local(&*conn)?,
|
User::count_local(conn)?,
|
||||||
Post::count_local(&*conn)?,
|
Post::count_local(conn)?,
|
||||||
local,
|
local,
|
||||||
federated,
|
federated,
|
||||||
user_feed
|
user_feed
|
||||||
@@ -41,61 +41,51 @@ pub fn index(conn: DbConn, user: Option<User>, intl: I18n) -> Result<Ructe, Erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/local?<page>")]
|
#[get("/local?<page>")]
|
||||||
pub fn local(
|
pub fn local(page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||||
conn: DbConn,
|
|
||||||
user: Option<User>,
|
|
||||||
page: Option<Page>,
|
|
||||||
intl: I18n,
|
|
||||||
) -> Result<Ructe, ErrorPage> {
|
|
||||||
let page = page.unwrap_or_default();
|
let page = page.unwrap_or_default();
|
||||||
let instance = Instance::get_local(&*conn)?;
|
let instance = Instance::get_local()?;
|
||||||
let articles = Post::get_instance_page(&*conn, instance.id, page.limits())?;
|
let articles = Post::get_instance_page(&*rockets.conn, instance.id, page.limits())?;
|
||||||
Ok(render!(instance::local(
|
Ok(render!(instance::local(
|
||||||
&(&*conn, &intl.catalog, user),
|
&rockets.to_context(),
|
||||||
instance,
|
instance,
|
||||||
articles,
|
articles,
|
||||||
page.0,
|
page.0,
|
||||||
Page::total(Post::count_local(&*conn)? as i32)
|
Page::total(Post::count_local(&*rockets.conn)? as i32)
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/feed?<page>")]
|
#[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 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>>();
|
let mut in_feed = followed.into_iter().map(|u| u.id).collect::<Vec<i32>>();
|
||||||
in_feed.push(user.id);
|
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(
|
Ok(render!(instance::feed(
|
||||||
&(&*conn, &intl.catalog, Some(user)),
|
&rockets.to_context(),
|
||||||
articles,
|
articles,
|
||||||
page.0,
|
page.0,
|
||||||
Page::total(Post::count_local(&*conn)? as i32)
|
Page::total(Post::count_local(&*rockets.conn)? as i32)
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/federated?<page>")]
|
#[get("/federated?<page>")]
|
||||||
pub fn federated(
|
pub fn federated(page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||||
conn: DbConn,
|
|
||||||
user: Option<User>,
|
|
||||||
page: Option<Page>,
|
|
||||||
intl: I18n,
|
|
||||||
) -> Result<Ructe, ErrorPage> {
|
|
||||||
let page = page.unwrap_or_default();
|
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(
|
Ok(render!(instance::federated(
|
||||||
&(&*conn, &intl.catalog, user),
|
&rockets.to_context(),
|
||||||
articles,
|
articles,
|
||||||
page.0,
|
page.0,
|
||||||
Page::total(Post::count_local(&*conn)? as i32)
|
Page::total(Post::count_local(&*rockets.conn)? as i32)
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/admin")]
|
#[get("/admin")]
|
||||||
pub fn admin(conn: DbConn, admin: Admin, intl: I18n) -> Result<Ructe, ErrorPage> {
|
pub fn admin(_admin: Admin, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||||
let local_inst = Instance::get_local(&*conn)?;
|
let local_inst = Instance::get_local()?;
|
||||||
Ok(render!(instance::admin(
|
Ok(render!(instance::admin(
|
||||||
&(&*conn, &intl.catalog, Some(admin.0)),
|
&rockets.to_context(),
|
||||||
local_inst.clone(),
|
local_inst.clone(),
|
||||||
InstanceSettingsForm {
|
InstanceSettingsForm {
|
||||||
name: local_inst.name.clone(),
|
name: local_inst.name.clone(),
|
||||||
@@ -121,92 +111,115 @@ pub struct InstanceSettingsForm {
|
|||||||
|
|
||||||
#[post("/admin", data = "<form>")]
|
#[post("/admin", data = "<form>")]
|
||||||
pub fn update_settings(
|
pub fn update_settings(
|
||||||
conn: DbConn,
|
_admin: Admin,
|
||||||
admin: Admin,
|
|
||||||
form: LenientForm<InstanceSettingsForm>,
|
form: LenientForm<InstanceSettingsForm>,
|
||||||
intl: I18n,
|
rockets: PlumeRocket,
|
||||||
) -> Result<Redirect, Ructe> {
|
) -> RespondOrRedirect {
|
||||||
form.validate()
|
let conn = &*rockets.conn;
|
||||||
.and_then(|_| {
|
if let Err(e) = form.validate() {
|
||||||
let instance = Instance::get_local(&*conn)
|
let local_inst =
|
||||||
.expect("instance::update_settings: local instance error");
|
Instance::get_local().expect("instance::update_settings: local instance error");
|
||||||
instance
|
render!(instance::admin(
|
||||||
.update(
|
&rockets.to_context(),
|
||||||
&*conn,
|
local_inst,
|
||||||
form.name.clone(),
|
form.clone(),
|
||||||
form.open_registrations,
|
e
|
||||||
form.short_description.clone(),
|
))
|
||||||
form.long_description.clone(),
|
.into()
|
||||||
)
|
} else {
|
||||||
.expect("instance::update_settings: save error");
|
let instance =
|
||||||
Ok(Redirect::to(uri!(admin)))
|
Instance::get_local().expect("instance::update_settings: local instance error");
|
||||||
})
|
instance
|
||||||
.or_else(|e| {
|
.update(
|
||||||
let local_inst = Instance::get_local(&*conn)
|
conn,
|
||||||
.expect("instance::update_settings: local instance error");
|
form.name.clone(),
|
||||||
Err(render!(instance::admin(
|
form.open_registrations,
|
||||||
&(&*conn, &intl.catalog, Some(admin.0)),
|
form.short_description.clone(),
|
||||||
local_inst,
|
form.long_description.clone(),
|
||||||
form.clone(),
|
)
|
||||||
e
|
.expect("instance::update_settings: save error");
|
||||||
)))
|
Flash::success(
|
||||||
})
|
Redirect::to(uri!(admin)),
|
||||||
|
i18n!(rockets.intl.catalog, "Instance settings have been saved."),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/admin/instances?<page>")]
|
#[get("/admin/instances?<page>")]
|
||||||
pub fn admin_instances(
|
pub fn admin_instances(
|
||||||
admin: Admin,
|
_admin: Admin,
|
||||||
conn: DbConn,
|
|
||||||
page: Option<Page>,
|
page: Option<Page>,
|
||||||
intl: I18n,
|
rockets: PlumeRocket,
|
||||||
) -> Result<Ructe, ErrorPage> {
|
) -> Result<Ructe, ErrorPage> {
|
||||||
let page = page.unwrap_or_default();
|
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(
|
Ok(render!(instance::list(
|
||||||
&(&*conn, &intl.catalog, Some(admin.0)),
|
&rockets.to_context(),
|
||||||
Instance::get_local(&*conn)?,
|
Instance::get_local()?,
|
||||||
instances,
|
instances,
|
||||||
page.0,
|
page.0,
|
||||||
Page::total(Instance::count(&*conn)? as i32)
|
Page::total(Instance::count(&*rockets.conn)? as i32)
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/admin/instances/<id>/block")]
|
#[post("/admin/instances/<id>/block")]
|
||||||
pub fn toggle_block(_admin: Admin, conn: DbConn, id: i32) -> Result<Redirect, ErrorPage> {
|
pub fn toggle_block(
|
||||||
if let Ok(inst) = Instance::get(&*conn, id) {
|
_admin: Admin,
|
||||||
inst.toggle_block(&*conn)?;
|
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>")]
|
#[get("/admin/users?<page>")]
|
||||||
pub fn admin_users(
|
pub fn admin_users(
|
||||||
admin: Admin,
|
_admin: Admin,
|
||||||
conn: DbConn,
|
|
||||||
page: Option<Page>,
|
page: Option<Page>,
|
||||||
intl: I18n,
|
rockets: PlumeRocket,
|
||||||
) -> Result<Ructe, ErrorPage> {
|
) -> Result<Ructe, ErrorPage> {
|
||||||
let page = page.unwrap_or_default();
|
let page = page.unwrap_or_default();
|
||||||
Ok(render!(instance::users(
|
Ok(render!(instance::users(
|
||||||
&(&*conn, &intl.catalog, Some(admin.0)),
|
&rockets.to_context(),
|
||||||
User::get_local_page(&*conn, page.limits())?,
|
User::get_local_page(&*rockets.conn, page.limits())?,
|
||||||
page.0,
|
page.0,
|
||||||
Page::total(User::count_local(&*conn)? as i32)
|
Page::total(User::count_local(&*rockets.conn)? as i32)
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/admin/users/<id>/ban")]
|
#[post("/admin/users/<id>/ban")]
|
||||||
pub fn ban(
|
pub fn ban(_admin: Admin, id: i32, rockets: PlumeRocket) -> Result<Flash<Redirect>, ErrorPage> {
|
||||||
_admin: Admin,
|
let u = User::get(&*rockets.conn, id)?;
|
||||||
conn: DbConn,
|
u.delete(&*rockets.conn, &rockets.searcher)?;
|
||||||
id: i32,
|
|
||||||
searcher: Searcher,
|
if Instance::get_local()
|
||||||
) -> Result<Redirect, ErrorPage> {
|
.map(|i| u.instance_id == i.id)
|
||||||
if let Ok(u) = User::get(&*conn, id) {
|
.unwrap_or(false)
|
||||||
u.delete(&*conn, &searcher)?;
|
{
|
||||||
|
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>")]
|
#[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));
|
return Err(ErrorPage::from(Error::NotFound));
|
||||||
}
|
}
|
||||||
|
|
||||||
let local_inst = Instance::get_local(&*conn)?;
|
let local_inst = Instance::get_local()?;
|
||||||
let mut doc = json!({
|
let mut doc = json!({
|
||||||
"version": version,
|
"version": version,
|
||||||
"software": {
|
"software": {
|
||||||
@@ -288,20 +301,26 @@ pub fn nodeinfo(conn: DbConn, version: String) -> Result<Json<serde_json::Value>
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/about")]
|
#[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(
|
Ok(render!(instance::about(
|
||||||
&(&*conn, &intl.catalog, user),
|
&rockets.to_context(),
|
||||||
Instance::get_local(&*conn)?,
|
Instance::get_local()?,
|
||||||
Instance::get_local(&*conn)?.main_admin(&*conn)?,
|
Instance::get_local()?.main_admin(conn)?,
|
||||||
User::count_local(&*conn)?,
|
User::count_local(conn)?,
|
||||||
Post::count_local(&*conn)?,
|
Post::count_local(conn)?,
|
||||||
Instance::count(&*conn)? - 1
|
Instance::count(conn)? - 1
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/privacy")]
|
||||||
|
pub fn privacy(rockets: PlumeRocket) -> Ructe {
|
||||||
|
render!(instance::privacy(&rockets.to_context()))
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/manifest.json")]
|
#[get("/manifest.json")]
|
||||||
pub fn web_manifest(conn: DbConn) -> Result<Json<serde_json::Value>, ErrorPage> {
|
pub fn web_manifest() -> Result<Json<serde_json::Value>, ErrorPage> {
|
||||||
let instance = Instance::get_local(&*conn)?;
|
let instance = Instance::get_local()?;
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"name": &instance.name,
|
"name": &instance.name,
|
||||||
"description": &instance.short_description,
|
"description": &instance.short_description,
|
||||||
|
|||||||
+100
-83
@@ -3,32 +3,32 @@ use multipart::server::{
|
|||||||
save::{SaveResult, SavedData},
|
save::{SaveResult, SavedData},
|
||||||
Multipart,
|
Multipart,
|
||||||
};
|
};
|
||||||
use plume_models::{db_conn::DbConn, medias::*, users::User, Error};
|
use plume_models::{db_conn::DbConn, medias::*, users::User, Error, PlumeRocket};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::ContentType,
|
http::ContentType,
|
||||||
response::{status, Redirect},
|
response::{status, Flash, Redirect},
|
||||||
Data,
|
Data,
|
||||||
};
|
};
|
||||||
use rocket_i18n::I18n;
|
use rocket_i18n::I18n;
|
||||||
use routes::{errors::ErrorPage, Page};
|
use routes::{errors::ErrorPage, Page};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use template_utils::Ructe;
|
use template_utils::{IntoContext, Ructe};
|
||||||
|
|
||||||
#[get("/medias?<page>")]
|
#[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 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(
|
Ok(render!(medias::index(
|
||||||
&(&*conn, &intl.catalog, Some(user.clone())),
|
&rockets.to_context(),
|
||||||
medias,
|
medias,
|
||||||
page.0,
|
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")]
|
#[get("/medias/new")]
|
||||||
pub fn new(user: User, conn: DbConn, intl: I18n) -> Ructe {
|
pub fn new(_user: User, rockets: PlumeRocket) -> Ructe {
|
||||||
render!(medias::new(&(&*conn, &intl.catalog, Some(user))))
|
render!(medias::new(&rockets.to_context()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/medias/new", data = "<data>")]
|
#[post("/medias/new", data = "<data>")]
|
||||||
@@ -38,76 +38,75 @@ pub fn upload(
|
|||||||
ct: &ContentType,
|
ct: &ContentType,
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
) -> Result<Redirect, status::BadRequest<&'static str>> {
|
) -> Result<Redirect, status::BadRequest<&'static str>> {
|
||||||
if ct.is_form_data() {
|
if !ct.is_form_data() {
|
||||||
let (_, boundary) = ct
|
return Ok(Redirect::to(uri!(new)));
|
||||||
.params()
|
}
|
||||||
.find(|&(k, _)| k == "boundary")
|
|
||||||
.ok_or_else(|| status::BadRequest(Some("No boundary")))?;
|
|
||||||
|
|
||||||
match Multipart::with_body(data.open(), boundary).save().temp() {
|
let (_, boundary) = ct
|
||||||
SaveResult::Full(entries) => {
|
.params()
|
||||||
let fields = entries.fields;
|
.find(|&(k, _)| k == "boundary")
|
||||||
|
.ok_or_else(|| status::BadRequest(Some("No boundary")))?;
|
||||||
|
|
||||||
let filename = fields
|
if let SaveResult::Full(entries) = Multipart::with_body(data.open(), boundary).save().temp() {
|
||||||
.get("file")
|
let fields = entries.fields;
|
||||||
.and_then(|v| v.iter().next())
|
|
||||||
.ok_or_else(|| status::BadRequest(Some("No file uploaded")))?
|
|
||||||
.headers
|
|
||||||
.filename
|
|
||||||
.clone();
|
|
||||||
// Remove extension if it contains something else than just letters and numbers
|
|
||||||
let ext = filename
|
|
||||||
.and_then(|f| {
|
|
||||||
f.rsplit('.')
|
|
||||||
.next()
|
|
||||||
.and_then(|ext| {
|
|
||||||
if ext.chars().any(|c| !c.is_alphanumeric()) {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(ext.to_lowercase())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(|ext| format!(".{}", ext))
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
let dest = format!("static/media/{}{}", GUID::rand().to_string(), ext);
|
|
||||||
|
|
||||||
match fields["file"][0].data {
|
let filename = fields
|
||||||
SavedData::Bytes(ref bytes) => fs::write(&dest, bytes)
|
.get("file")
|
||||||
.map_err(|_| status::BadRequest(Some("Couldn't save upload")))?,
|
.and_then(|v| v.iter().next())
|
||||||
SavedData::File(ref path, _) => {
|
.ok_or_else(|| status::BadRequest(Some("No file uploaded")))?
|
||||||
fs::copy(path, &dest)
|
.headers
|
||||||
.map_err(|_| status::BadRequest(Some("Couldn't copy upload")))?;
|
.filename
|
||||||
}
|
.clone();
|
||||||
_ => {
|
// Remove extension if it contains something else than just letters and numbers
|
||||||
return Ok(Redirect::to(uri!(new)));
|
let ext = filename
|
||||||
}
|
.and_then(|f| {
|
||||||
}
|
f.rsplit('.')
|
||||||
|
.next()
|
||||||
let has_cw = !read(&fields["cw"][0].data)
|
.and_then(|ext| {
|
||||||
.map(|cw| cw.is_empty())
|
if ext.chars().any(|c| !c.is_alphanumeric()) {
|
||||||
.unwrap_or(false);
|
|
||||||
let media = Media::insert(
|
|
||||||
&*conn,
|
|
||||||
NewMedia {
|
|
||||||
file_path: dest,
|
|
||||||
alt_text: read(&fields["alt"][0].data)?,
|
|
||||||
is_remote: false,
|
|
||||||
remote_url: None,
|
|
||||||
sensitive: has_cw,
|
|
||||||
content_warning: if has_cw {
|
|
||||||
Some(read(&fields["cw"][0].data)?)
|
|
||||||
} else {
|
|
||||||
None
|
None
|
||||||
},
|
} else {
|
||||||
owner_id: user.id,
|
Some(ext.to_lowercase())
|
||||||
},
|
}
|
||||||
)
|
})
|
||||||
.map_err(|_| status::BadRequest(Some("Error while saving media")))?;
|
.map(|ext| format!(".{}", ext))
|
||||||
Ok(Redirect::to(uri!(details: id = media.id)))
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
let dest = format!("static/media/{}{}", GUID::rand().to_string(), ext);
|
||||||
|
|
||||||
|
match fields["file"][0].data {
|
||||||
|
SavedData::Bytes(ref bytes) => fs::write(&dest, bytes)
|
||||||
|
.map_err(|_| status::BadRequest(Some("Couldn't save upload")))?,
|
||||||
|
SavedData::File(ref path, _) => {
|
||||||
|
fs::copy(path, &dest)
|
||||||
|
.map_err(|_| status::BadRequest(Some("Couldn't copy upload")))?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Ok(Redirect::to(uri!(new)));
|
||||||
}
|
}
|
||||||
SaveResult::Partial(_, _) | SaveResult::Error(_) => Ok(Redirect::to(uri!(new))),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let has_cw = !read(&fields["cw"][0].data)
|
||||||
|
.map(|cw| cw.is_empty())
|
||||||
|
.unwrap_or(false);
|
||||||
|
let media = Media::insert(
|
||||||
|
&*conn,
|
||||||
|
NewMedia {
|
||||||
|
file_path: dest,
|
||||||
|
alt_text: read(&fields["alt"][0].data)?,
|
||||||
|
is_remote: false,
|
||||||
|
remote_url: None,
|
||||||
|
sensitive: has_cw,
|
||||||
|
content_warning: if has_cw {
|
||||||
|
Some(read(&fields["cw"][0].data)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
owner_id: user.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|_| status::BadRequest(Some("Error while saving media")))?;
|
||||||
|
Ok(Redirect::to(uri!(details: id = media.id)))
|
||||||
} else {
|
} else {
|
||||||
Ok(Redirect::to(uri!(new)))
|
Ok(Redirect::to(uri!(new)))
|
||||||
}
|
}
|
||||||
@@ -122,32 +121,50 @@ fn read(data: &SavedData) -> Result<String, status::BadRequest<&'static str>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/medias/<id>")]
|
#[get("/medias/<id>")]
|
||||||
pub fn details(id: i32, user: User, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> {
|
pub fn details(id: i32, user: User, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||||
let media = Media::get(&*conn, id)?;
|
let media = Media::get(&*rockets.conn, id)?;
|
||||||
if media.owner_id == user.id {
|
if media.owner_id == user.id {
|
||||||
Ok(render!(medias::details(
|
Ok(render!(medias::details(&rockets.to_context(), media)))
|
||||||
&(&*conn, &intl.catalog, Some(user)),
|
|
||||||
media
|
|
||||||
)))
|
|
||||||
} else {
|
} else {
|
||||||
Err(Error::Unauthorized.into())
|
Err(Error::Unauthorized.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/medias/<id>/delete")]
|
#[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)?;
|
let media = Media::get(&*conn, id)?;
|
||||||
if media.owner_id == user.id {
|
if media.owner_id == user.id {
|
||||||
media.delete(&*conn)?;
|
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")]
|
#[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)?;
|
let media = Media::get(&*conn, id)?;
|
||||||
if media.owner_id == user.id {
|
if media.owner_id == user.id {
|
||||||
user.set_avatar(&*conn, media.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)))
|
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-3
@@ -7,16 +7,52 @@ use rocket::{
|
|||||||
RawStr, Status,
|
RawStr, Status,
|
||||||
},
|
},
|
||||||
request::{self, FromFormValue, FromRequest, Request},
|
request::{self, FromFormValue, FromRequest, Request},
|
||||||
response::NamedFile,
|
response::{Flash, NamedFile, Redirect},
|
||||||
Outcome,
|
Outcome,
|
||||||
};
|
};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use template_utils::Ructe;
|
||||||
|
|
||||||
use plume_models::{posts::Post, Connection};
|
use plume_models::{posts::Post, Connection};
|
||||||
|
|
||||||
const ITEMS_PER_PAGE: i32 = 12;
|
const ITEMS_PER_PAGE: i32 = 12;
|
||||||
|
|
||||||
#[derive(Copy, Clone, UriDisplayQuery)]
|
/// Special return type used for routes that "cannot fail", and instead
|
||||||
|
/// `Redirect`, or `Flash<Redirect>`, when we cannot deliver a `Ructe` Response
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
#[derive(Responder)]
|
||||||
|
pub enum RespondOrRedirect {
|
||||||
|
Response(Ructe),
|
||||||
|
FlashResponse(Flash<Ructe>),
|
||||||
|
Redirect(Redirect),
|
||||||
|
FlashRedirect(Flash<Redirect>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Ructe> for RespondOrRedirect {
|
||||||
|
fn from(response: Ructe) -> Self {
|
||||||
|
RespondOrRedirect::Response(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Flash<Ructe>> for RespondOrRedirect {
|
||||||
|
fn from(response: Flash<Ructe>) -> Self {
|
||||||
|
RespondOrRedirect::FlashResponse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Redirect> for RespondOrRedirect {
|
||||||
|
fn from(redirect: Redirect) -> Self {
|
||||||
|
RespondOrRedirect::Redirect(redirect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Flash<Redirect>> for RespondOrRedirect {
|
||||||
|
fn from(redirect: Flash<Redirect>) -> Self {
|
||||||
|
RespondOrRedirect::FlashRedirect(redirect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Shrinkwrap, Copy, Clone, UriDisplayQuery)]
|
||||||
pub struct Page(i32);
|
pub struct Page(i32);
|
||||||
|
|
||||||
impl<'v> FromFormValue<'v> for Page {
|
impl<'v> FromFormValue<'v> for Page {
|
||||||
@@ -52,6 +88,7 @@ impl Page {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Shrinkwrap)]
|
||||||
pub struct ContentLen(pub u64);
|
pub struct ContentLen(pub u64);
|
||||||
|
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for ContentLen {
|
impl<'a, 'r> FromRequest<'a, 'r> for ContentLen {
|
||||||
@@ -72,7 +109,7 @@ impl Default for Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A form for remote interaction, used by multiple routes
|
/// A form for remote interaction, used by multiple routes
|
||||||
#[derive(Clone, Default, FromForm)]
|
#[derive(Shrinkwrap, Clone, Default, FromForm)]
|
||||||
pub struct RemoteForm {
|
pub struct RemoteForm {
|
||||||
pub remote: String,
|
pub remote: String,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,23 +2,22 @@ use rocket::response::{Flash, Redirect};
|
|||||||
use rocket_i18n::I18n;
|
use rocket_i18n::I18n;
|
||||||
|
|
||||||
use plume_common::utils;
|
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 routes::{errors::ErrorPage, Page};
|
||||||
use template_utils::Ructe;
|
use template_utils::{IntoContext, Ructe};
|
||||||
|
|
||||||
#[get("/notifications?<page>")]
|
#[get("/notifications?<page>")]
|
||||||
pub fn notifications(
|
pub fn notifications(
|
||||||
conn: DbConn,
|
|
||||||
user: User,
|
user: User,
|
||||||
page: Option<Page>,
|
page: Option<Page>,
|
||||||
intl: I18n,
|
rockets: PlumeRocket,
|
||||||
) -> Result<Ructe, ErrorPage> {
|
) -> Result<Ructe, ErrorPage> {
|
||||||
let page = page.unwrap_or_default();
|
let page = page.unwrap_or_default();
|
||||||
Ok(render!(notifications::index(
|
Ok(render!(notifications::index(
|
||||||
&(&*conn, &intl.catalog, Some(user.clone())),
|
&rockets.to_context(),
|
||||||
Notification::page_for_user(&*conn, &user, page.limits())?,
|
Notification::page_for_user(&*rockets.conn, &user, page.limits())?,
|
||||||
page.0,
|
page.0,
|
||||||
Page::total(Notification::count_for_user(&*conn, &user)? as i32)
|
Page::total(Notification::count_for_user(&*rockets.conn, &user)? as i32)
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+91
-70
@@ -26,8 +26,10 @@ use plume_models::{
|
|||||||
users::User,
|
users::User,
|
||||||
Error, PlumeRocket,
|
Error, PlumeRocket,
|
||||||
};
|
};
|
||||||
use routes::{comments::NewCommentForm, errors::ErrorPage, ContentLen, RemoteForm};
|
use routes::{
|
||||||
use template_utils::Ructe;
|
comments::NewCommentForm, errors::ErrorPage, ContentLen, RemoteForm, RespondOrRedirect,
|
||||||
|
};
|
||||||
|
use template_utils::{IntoContext, Ructe};
|
||||||
|
|
||||||
#[get("/~/<blog>/<slug>?<responding_to>", rank = 4)]
|
#[get("/~/<blog>/<slug>?<responding_to>", rank = 4)]
|
||||||
pub fn details(
|
pub fn details(
|
||||||
@@ -40,18 +42,24 @@ pub fn details(
|
|||||||
let user = rockets.user.clone();
|
let user = rockets.user.clone();
|
||||||
let blog = Blog::find_by_fqn(&rockets, &blog)?;
|
let blog = Blog::find_by_fqn(&rockets, &blog)?;
|
||||||
let post = Post::find_by_slug(&*conn, &slug, blog.id)?;
|
let post = Post::find_by_slug(&*conn, &slug, blog.id)?;
|
||||||
if post.published
|
if !(post.published
|
||||||
|| post
|
|| post
|
||||||
.get_authors(&*conn)?
|
.get_authors(&*conn)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0))
|
.any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0)))
|
||||||
{
|
{
|
||||||
let comments = CommentTree::from_post(&*conn, &post, user.as_ref())?;
|
return Ok(render!(errors::not_authorized(
|
||||||
|
&rockets.to_context(),
|
||||||
|
i18n!(rockets.intl.catalog, "This post isn't published yet.")
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
let previous = responding_to.and_then(|r| Comment::get(&*conn, r).ok());
|
let comments = CommentTree::from_post(&*conn, &post, user.as_ref())?;
|
||||||
|
|
||||||
Ok(render!(posts::details(
|
let previous = responding_to.and_then(|r| Comment::get(&*conn, r).ok());
|
||||||
&(&*conn, &rockets.intl.catalog, user.clone()),
|
|
||||||
|
Ok(render!(posts::details(
|
||||||
|
&rockets.to_context(),
|
||||||
post.clone(),
|
post.clone(),
|
||||||
blog,
|
blog,
|
||||||
&NewCommentForm {
|
&NewCommentForm {
|
||||||
@@ -87,12 +95,6 @@ pub fn details(
|
|||||||
user.and_then(|u| u.is_following(&*conn, post.get_authors(&*conn).ok()?[0].id).ok()).unwrap_or(false),
|
user.and_then(|u| u.is_following(&*conn, post.get_authors(&*conn).ok()?[0].id).ok()).unwrap_or(false),
|
||||||
post.get_authors(&*conn)?[0].clone()
|
post.get_authors(&*conn)?[0].clone()
|
||||||
)))
|
)))
|
||||||
} else {
|
|
||||||
Ok(render!(errors::not_authorized(
|
|
||||||
&(&*conn, &rockets.intl.catalog, user.clone()),
|
|
||||||
i18n!(rockets.intl.catalog, "This post isn't published yet.")
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/~/<blog>/<slug>", rank = 3)]
|
#[get("/~/<blog>/<slug>", rank = 3)]
|
||||||
@@ -130,25 +132,24 @@ pub fn new_auth(blog: String, i18n: I18n) -> Flash<Redirect> {
|
|||||||
pub fn new(blog: String, cl: ContentLen, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
pub fn new(blog: String, cl: ContentLen, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||||
let conn = &*rockets.conn;
|
let conn = &*rockets.conn;
|
||||||
let b = Blog::find_by_fqn(&rockets, &blog)?;
|
let b = Blog::find_by_fqn(&rockets, &blog)?;
|
||||||
let user = rockets.user.unwrap();
|
let user = rockets.user.clone().unwrap();
|
||||||
let intl = rockets.intl;
|
|
||||||
|
|
||||||
if !user.is_author_in(&*conn, &b)? {
|
if !user.is_author_in(&*conn, &b)? {
|
||||||
// TODO actually return 403 error code
|
// TODO actually return 403 error code
|
||||||
return Ok(render!(errors::not_authorized(
|
return Ok(render!(errors::not_authorized(
|
||||||
&(&*conn, &intl.catalog, Some(user)),
|
&rockets.to_context(),
|
||||||
i18n!(intl.catalog, "You are not an author of this blog.")
|
i18n!(rockets.intl.catalog, "You are not an author of this blog.")
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let medias = Media::for_user(&*conn, user.id)?;
|
let medias = Media::for_user(&*conn, user.id)?;
|
||||||
Ok(render!(posts::new(
|
Ok(render!(posts::new(
|
||||||
&(&*conn, &intl.catalog, Some(user)),
|
&rockets.to_context(),
|
||||||
i18n!(intl.catalog, "New post"),
|
i18n!(rockets.intl.catalog, "New post"),
|
||||||
b,
|
b,
|
||||||
false,
|
false,
|
||||||
&NewPostForm {
|
&NewPostForm {
|
||||||
license: Instance::get_local(&*conn)?.default_license,
|
license: Instance::get_local()?.default_license,
|
||||||
..NewPostForm::default()
|
..NewPostForm::default()
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
@@ -170,11 +171,11 @@ pub fn edit(
|
|||||||
let intl = &rockets.intl.catalog;
|
let intl = &rockets.intl.catalog;
|
||||||
let b = Blog::find_by_fqn(&rockets, &blog)?;
|
let b = Blog::find_by_fqn(&rockets, &blog)?;
|
||||||
let post = Post::find_by_slug(&*conn, &slug, b.id)?;
|
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)? {
|
if !user.is_author_in(&*conn, &b)? {
|
||||||
return Ok(render!(errors::not_authorized(
|
return Ok(render!(errors::not_authorized(
|
||||||
&(&*conn, intl, Some(user)),
|
&rockets.to_context(),
|
||||||
i18n!(intl, "You are not an author of this blog.")
|
i18n!(intl, "You are not an author of this blog.")
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
@@ -188,7 +189,7 @@ pub fn edit(
|
|||||||
let medias = Media::for_user(&*conn, user.id)?;
|
let medias = Media::for_user(&*conn, user.id)?;
|
||||||
let title = post.title.clone();
|
let title = post.title.clone();
|
||||||
Ok(render!(posts::new(
|
Ok(render!(posts::new(
|
||||||
&(&*conn, intl, Some(user)),
|
&rockets.to_context(),
|
||||||
i18n!(intl, "Edit {0}"; &title),
|
i18n!(intl, "Edit {0}"; &title),
|
||||||
b,
|
b,
|
||||||
true,
|
true,
|
||||||
@@ -220,7 +221,7 @@ pub fn update(
|
|||||||
cl: ContentLen,
|
cl: ContentLen,
|
||||||
form: LenientForm<NewPostForm>,
|
form: LenientForm<NewPostForm>,
|
||||||
rockets: PlumeRocket,
|
rockets: PlumeRocket,
|
||||||
) -> Result<Redirect, Ructe> {
|
) -> RespondOrRedirect {
|
||||||
let conn = &*rockets.conn;
|
let conn = &*rockets.conn;
|
||||||
let b = Blog::find_by_fqn(&rockets, &blog).expect("post::update: blog error");
|
let b = Blog::find_by_fqn(&rockets, &blog).expect("post::update: blog error");
|
||||||
let mut post =
|
let mut post =
|
||||||
@@ -256,15 +257,19 @@ pub fn update(
|
|||||||
.expect("posts::update: is author in error")
|
.expect("posts::update: is author in error")
|
||||||
{
|
{
|
||||||
// actually it's not "Ok"…
|
// actually it's not "Ok"…
|
||||||
Ok(Redirect::to(
|
Flash::error(
|
||||||
uri!(super::blogs::details: name = blog, page = _),
|
Redirect::to(uri!(super::blogs::details: name = blog, page = _)),
|
||||||
))
|
i18n!(&intl, "You are not allowed to publish on this blog."),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
} else {
|
} else {
|
||||||
let (content, mentions, hashtags) = utils::md_to_html(
|
let (content, mentions, hashtags) = utils::md_to_html(
|
||||||
form.content.to_string().as_ref(),
|
form.content.to_string().as_ref(),
|
||||||
&Instance::get_local(&conn)
|
Some(
|
||||||
.expect("posts::update: Error getting local instance")
|
&Instance::get_local()
|
||||||
.public_domain,
|
.expect("posts::update: Error getting local instance")
|
||||||
|
.public_domain,
|
||||||
|
),
|
||||||
false,
|
false,
|
||||||
Some(Media::get_media_processor(
|
Some(Media::get_media_processor(
|
||||||
&conn,
|
&conn,
|
||||||
@@ -312,7 +317,7 @@ pub fn update(
|
|||||||
.filter(|t| !t.is_empty())
|
.filter(|t| !t.is_empty())
|
||||||
.collect::<HashSet<_>>()
|
.collect::<HashSet<_>>()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|t| Tag::build_activity(&conn, t).ok())
|
.filter_map(|t| Tag::build_activity(t).ok())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
post.update_tags(&conn, tags)
|
post.update_tags(&conn, tags)
|
||||||
.expect("post::update: tags error");
|
.expect("post::update: tags error");
|
||||||
@@ -322,7 +327,7 @@ pub fn update(
|
|||||||
.map(|h| h.to_camel_case())
|
.map(|h| h.to_camel_case())
|
||||||
.collect::<HashSet<_>>()
|
.collect::<HashSet<_>>()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|t| Tag::build_activity(&conn, t).ok())
|
.filter_map(|t| Tag::build_activity(t).ok())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
post.update_hashtags(&conn, hashtags)
|
post.update_hashtags(&conn, hashtags)
|
||||||
.expect("post::update: hashtags error");
|
.expect("post::update: hashtags error");
|
||||||
@@ -343,14 +348,16 @@ pub fn update(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Redirect::to(
|
Flash::success(
|
||||||
uri!(details: blog = blog, slug = new_slug, responding_to = _),
|
Redirect::to(uri!(details: blog = blog, slug = new_slug, responding_to = _)),
|
||||||
))
|
i18n!(intl, "Your article has been updated."),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let medias = Media::for_user(&*conn, user.id).expect("posts:update: medias error");
|
let medias = Media::for_user(&*conn, user.id).expect("posts:update: medias error");
|
||||||
Err(render!(posts::new(
|
render!(posts::new(
|
||||||
&(&*conn, intl, Some(user)),
|
&rockets.to_context(),
|
||||||
i18n!(intl, "Edit {0}"; &form.title),
|
i18n!(intl, "Edit {0}"; &form.title),
|
||||||
b,
|
b,
|
||||||
true,
|
true,
|
||||||
@@ -360,7 +367,8 @@ pub fn update(
|
|||||||
errors.clone(),
|
errors.clone(),
|
||||||
medias.clone(),
|
medias.clone(),
|
||||||
cl.0
|
cl.0
|
||||||
)))
|
))
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,7 +401,7 @@ pub fn create(
|
|||||||
form: LenientForm<NewPostForm>,
|
form: LenientForm<NewPostForm>,
|
||||||
cl: ContentLen,
|
cl: ContentLen,
|
||||||
rockets: PlumeRocket,
|
rockets: PlumeRocket,
|
||||||
) -> Result<Redirect, Result<Ructe, ErrorPage>> {
|
) -> Result<RespondOrRedirect, ErrorPage> {
|
||||||
let conn = &*rockets.conn;
|
let conn = &*rockets.conn;
|
||||||
let blog = Blog::find_by_fqn(&rockets, &blog_name).expect("post::create: blog error");;
|
let blog = Blog::find_by_fqn(&rockets, &blog_name).expect("post::create: blog error");;
|
||||||
let slug = form.title.to_string().to_kebab_case();
|
let slug = form.title.to_string().to_kebab_case();
|
||||||
@@ -420,16 +428,23 @@ pub fn create(
|
|||||||
.expect("post::create: is author in error")
|
.expect("post::create: is author in error")
|
||||||
{
|
{
|
||||||
// actually it's not "Ok"…
|
// actually it's not "Ok"…
|
||||||
return Ok(Redirect::to(
|
return Ok(Flash::error(
|
||||||
uri!(super::blogs::details: name = blog_name, page = _),
|
Redirect::to(uri!(super::blogs::details: name = blog_name, page = _)),
|
||||||
));
|
i18n!(
|
||||||
|
&rockets.intl.catalog,
|
||||||
|
"You are not allowed to publish on this blog."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let (content, mentions, hashtags) = utils::md_to_html(
|
let (content, mentions, hashtags) = utils::md_to_html(
|
||||||
form.content.to_string().as_ref(),
|
form.content.to_string().as_ref(),
|
||||||
&Instance::get_local(&conn)
|
Some(
|
||||||
.expect("post::create: local instance error")
|
&Instance::get_local()
|
||||||
.public_domain,
|
.expect("post::create: local instance error")
|
||||||
|
.public_domain,
|
||||||
|
),
|
||||||
false,
|
false,
|
||||||
Some(Media::get_media_processor(
|
Some(Media::get_media_processor(
|
||||||
&conn,
|
&conn,
|
||||||
@@ -518,15 +533,16 @@ pub fn create(
|
|||||||
worker.execute(move || broadcast(&user, act, dest));
|
worker.execute(move || broadcast(&user, act, dest));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Redirect::to(
|
Ok(Flash::success(
|
||||||
uri!(details: blog = blog_name, slug = slug, responding_to = _),
|
Redirect::to(uri!(details: blog = blog_name, slug = slug, responding_to = _)),
|
||||||
))
|
i18n!(&rockets.intl.catalog, "Your article has been saved."),
|
||||||
|
)
|
||||||
|
.into())
|
||||||
} else {
|
} else {
|
||||||
let medias = Media::for_user(&*conn, user.id).expect("posts::create: medias error");
|
let medias = Media::for_user(&*conn, user.id).expect("posts::create: medias error");
|
||||||
let intl = rockets.intl;
|
Ok(render!(posts::new(
|
||||||
Err(Ok(render!(posts::new(
|
&rockets.to_context(),
|
||||||
&(&*conn, &intl.catalog, Some(user)),
|
i18n!(rockets.intl.catalog, "New article"),
|
||||||
i18n!(intl.catalog, "New post"),
|
|
||||||
blog,
|
blog,
|
||||||
false,
|
false,
|
||||||
&*form,
|
&*form,
|
||||||
@@ -535,7 +551,8 @@ pub fn create(
|
|||||||
errors.clone(),
|
errors.clone(),
|
||||||
medias,
|
medias,
|
||||||
cl.0
|
cl.0
|
||||||
))))
|
))
|
||||||
|
.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,7 +561,8 @@ pub fn delete(
|
|||||||
blog_name: String,
|
blog_name: String,
|
||||||
slug: String,
|
slug: String,
|
||||||
rockets: PlumeRocket,
|
rockets: PlumeRocket,
|
||||||
) -> Result<Redirect, ErrorPage> {
|
intl: I18n,
|
||||||
|
) -> Result<Flash<Redirect>, ErrorPage> {
|
||||||
let user = rockets.user.clone().unwrap();
|
let user = rockets.user.clone().unwrap();
|
||||||
let post = Blog::find_by_fqn(&rockets, &blog_name)
|
let post = Blog::find_by_fqn(&rockets, &blog_name)
|
||||||
.and_then(|blog| Post::find_by_slug(&*rockets.conn, &slug, blog.id));
|
.and_then(|blog| Post::find_by_slug(&*rockets.conn, &slug, blog.id));
|
||||||
@@ -555,8 +573,11 @@ pub fn delete(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.any(|a| a.id == user.id)
|
.any(|a| a.id == user.id)
|
||||||
{
|
{
|
||||||
return Ok(Redirect::to(
|
return Ok(Flash::error(
|
||||||
uri!(details: blog = blog_name.clone(), slug = slug.clone(), responding_to = _),
|
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 +600,14 @@ pub fn delete(
|
|||||||
.expect("Failed to rotate keypair");
|
.expect("Failed to rotate keypair");
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(Redirect::to(
|
Ok(Flash::success(
|
||||||
uri!(super::blogs::details: name = blog_name, page = _),
|
Redirect::to(uri!(super::blogs::details: name = blog_name, page = _)),
|
||||||
|
i18n!(intl.catalog, "Your article has been deleted."),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
Ok(Redirect::to(
|
Ok(Flash::error(Redirect::to(
|
||||||
uri!(super::blogs::details: name = blog_name, page = _),
|
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 +616,11 @@ pub fn remote_interact(
|
|||||||
rockets: PlumeRocket,
|
rockets: PlumeRocket,
|
||||||
blog_name: String,
|
blog_name: String,
|
||||||
slug: String,
|
slug: String,
|
||||||
i18n: I18n,
|
|
||||||
) -> Result<Ructe, ErrorPage> {
|
) -> Result<Ructe, ErrorPage> {
|
||||||
let target = Blog::find_by_fqn(&rockets, &blog_name)
|
let target = Blog::find_by_fqn(&rockets, &blog_name)
|
||||||
.and_then(|blog| Post::find_by_slug(&rockets.conn, &slug, blog.id))?;
|
.and_then(|blog| Post::find_by_slug(&rockets.conn, &slug, blog.id))?;
|
||||||
Ok(render!(posts::remote_interact(
|
Ok(render!(posts::remote_interact(
|
||||||
&(&rockets.conn, &i18n.catalog, None),
|
&rockets.to_context(),
|
||||||
target,
|
target,
|
||||||
super::session::LoginForm::default(),
|
super::session::LoginForm::default(),
|
||||||
ValidationErrors::default(),
|
ValidationErrors::default(),
|
||||||
@@ -614,30 +635,30 @@ pub fn remote_interact_post(
|
|||||||
blog_name: String,
|
blog_name: String,
|
||||||
slug: String,
|
slug: String,
|
||||||
remote: LenientForm<RemoteForm>,
|
remote: LenientForm<RemoteForm>,
|
||||||
i18n: I18n,
|
) -> Result<RespondOrRedirect, ErrorPage> {
|
||||||
) -> Result<Result<Ructe, Redirect>, ErrorPage> {
|
|
||||||
let target = Blog::find_by_fqn(&rockets, &blog_name)
|
let target = Blog::find_by_fqn(&rockets, &blog_name)
|
||||||
.and_then(|blog| Post::find_by_slug(&rockets.conn, &slug, blog.id))?;
|
.and_then(|blog| Post::find_by_slug(&rockets.conn, &slug, blog.id))?;
|
||||||
if let Some(uri) = User::fetch_remote_interact_uri(&remote.remote)
|
if let Some(uri) = User::fetch_remote_interact_uri(&remote.remote)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|uri| rt_format!(uri, uri = target.ap_url).ok())
|
.and_then(|uri| rt_format!(uri, uri = target.ap_url).ok())
|
||||||
{
|
{
|
||||||
Ok(Err(Redirect::to(uri)))
|
Ok(Redirect::to(uri).into())
|
||||||
} else {
|
} else {
|
||||||
let mut errs = ValidationErrors::new();
|
let mut errs = ValidationErrors::new();
|
||||||
errs.add("remote", ValidationError {
|
errs.add("remote", ValidationError {
|
||||||
code: Cow::from("invalid_remote"),
|
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(),
|
params: HashMap::new(),
|
||||||
});
|
});
|
||||||
//could not get your remote url?
|
//could not get your remote url?
|
||||||
Ok(Ok(render!(posts::remote_interact(
|
Ok(render!(posts::remote_interact(
|
||||||
&(&rockets.conn, &i18n.catalog, None),
|
&rockets.to_context(),
|
||||||
target,
|
target,
|
||||||
super::session::LoginForm::default(),
|
super::session::LoginForm::default(),
|
||||||
ValidationErrors::default(),
|
ValidationErrors::default(),
|
||||||
remote.clone(),
|
remote.clone(),
|
||||||
errs
|
errs
|
||||||
))))
|
))
|
||||||
|
.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-14
@@ -1,12 +1,10 @@
|
|||||||
use chrono::offset::Utc;
|
use chrono::offset::Utc;
|
||||||
use rocket::request::Form;
|
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 routes::Page;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use template_utils::Ructe;
|
use template_utils::{IntoContext, Ructe};
|
||||||
use Searcher;
|
|
||||||
|
|
||||||
#[derive(Default, FromForm)]
|
#[derive(Default, FromForm)]
|
||||||
pub struct SearchQuery {
|
pub struct SearchQuery {
|
||||||
@@ -52,13 +50,8 @@ macro_rules! param_to_query {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/search?<query..>")]
|
#[get("/search?<query..>")]
|
||||||
pub fn search(
|
pub fn search(query: Option<Form<SearchQuery>>, rockets: PlumeRocket) -> Ructe {
|
||||||
query: Option<Form<SearchQuery>>,
|
let conn = &*rockets.conn;
|
||||||
conn: DbConn,
|
|
||||||
searcher: Searcher,
|
|
||||||
user: Option<User>,
|
|
||||||
intl: I18n,
|
|
||||||
) -> Ructe {
|
|
||||||
let query = query.map(Form::into_inner).unwrap_or_default();
|
let query = query.map(Form::into_inner).unwrap_or_default();
|
||||||
let page = query.page.unwrap_or_default();
|
let page = query.page.unwrap_or_default();
|
||||||
let mut parsed_query =
|
let mut parsed_query =
|
||||||
@@ -73,14 +66,16 @@ pub fn search(
|
|||||||
|
|
||||||
if str_query.is_empty() {
|
if str_query.is_empty() {
|
||||||
render!(search::index(
|
render!(search::index(
|
||||||
&(&*conn, &intl.catalog, user),
|
&rockets.to_context(),
|
||||||
&format!("{}", Utc::today().format("%Y-%m-d"))
|
&format!("{}", Utc::today().format("%Y-%m-d"))
|
||||||
))
|
))
|
||||||
} else {
|
} 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 };
|
let next_page = if res.is_empty() { 0 } else { page.0 + 1 };
|
||||||
render!(search::result(
|
render!(search::result(
|
||||||
&(&*conn, &intl.catalog, user),
|
&rockets.to_context(),
|
||||||
&str_query,
|
&str_query,
|
||||||
res,
|
res,
|
||||||
page.0,
|
page.0,
|
||||||
|
|||||||
+82
-121
@@ -2,31 +2,31 @@ use lettre::Transport;
|
|||||||
use rocket::http::ext::IntoOwned;
|
use rocket::http::ext::IntoOwned;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::{uri::Uri, Cookie, Cookies, SameSite},
|
http::{uri::Uri, Cookie, Cookies, SameSite},
|
||||||
request::{FlashMessage, Form, LenientForm},
|
request::{Form, LenientForm},
|
||||||
response::Redirect,
|
response::{Flash, Redirect},
|
||||||
State,
|
State,
|
||||||
};
|
};
|
||||||
use rocket_i18n::I18n;
|
use rocket_i18n::I18n;
|
||||||
|
use routes::RespondOrRedirect;
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
use template_utils::Ructe;
|
|
||||||
use validator::{Validate, ValidationError, ValidationErrors};
|
use validator::{Validate, ValidationError, ValidationErrors};
|
||||||
|
|
||||||
use mail::{build_mail, Mailer};
|
use mail::{build_mail, Mailer};
|
||||||
use plume_models::{
|
use plume_models::{
|
||||||
db_conn::DbConn,
|
password_reset_requests::*,
|
||||||
users::{User, AUTH_COOKIE},
|
users::{User, AUTH_COOKIE},
|
||||||
Error, PlumeRocket, CONFIG,
|
Error, PlumeRocket, CONFIG,
|
||||||
};
|
};
|
||||||
use routes::errors::ErrorPage;
|
use template_utils::{IntoContext, Ructe};
|
||||||
|
|
||||||
#[get("/login?<m>")]
|
#[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(
|
render!(session::login(
|
||||||
&(&*conn, &intl.catalog, user),
|
&rockets.to_context(),
|
||||||
m,
|
m,
|
||||||
&LoginForm::default(),
|
&LoginForm::default(),
|
||||||
ValidationErrors::default()
|
ValidationErrors::default()
|
||||||
@@ -44,10 +44,9 @@ pub struct LoginForm {
|
|||||||
#[post("/login", data = "<form>")]
|
#[post("/login", data = "<form>")]
|
||||||
pub fn create(
|
pub fn create(
|
||||||
form: LenientForm<LoginForm>,
|
form: LenientForm<LoginForm>,
|
||||||
flash: Option<FlashMessage>,
|
|
||||||
mut cookies: Cookies,
|
mut cookies: Cookies,
|
||||||
rockets: PlumeRocket,
|
rockets: PlumeRocket,
|
||||||
) -> Result<Redirect, Ructe> {
|
) -> RespondOrRedirect {
|
||||||
let conn = &*rockets.conn;
|
let conn = &*rockets.conn;
|
||||||
let user = User::find_by_email(&*conn, &form.email_or_name)
|
let user = User::find_by_email(&*conn, &form.email_or_name)
|
||||||
.or_else(|_| User::find_by_fqn(&rockets, &form.email_or_name));
|
.or_else(|_| User::find_by_fqn(&rockets, &form.email_or_name));
|
||||||
@@ -78,50 +77,55 @@ pub fn create(
|
|||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
if errors.is_empty() {
|
if !errors.is_empty() {
|
||||||
cookies.add_private(
|
return render!(session::login(&rockets.to_context(), None, &*form, errors)).into();
|
||||||
Cookie::build(AUTH_COOKIE, user_id)
|
}
|
||||||
.same_site(SameSite::Lax)
|
|
||||||
.finish(),
|
cookies.add_private(
|
||||||
);
|
Cookie::build(AUTH_COOKIE, user_id)
|
||||||
let destination = flash
|
.same_site(SameSite::Lax)
|
||||||
.and_then(|f| {
|
.finish(),
|
||||||
if f.name() == "callback" {
|
);
|
||||||
Some(f.msg().to_owned())
|
let destination = rockets
|
||||||
|
.flash_msg
|
||||||
|
.clone()
|
||||||
|
.and_then(
|
||||||
|
|(name, msg)| {
|
||||||
|
if name == "callback" {
|
||||||
|
Some(msg)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
.unwrap_or_else(|| "/".to_owned());
|
)
|
||||||
|
.unwrap_or_else(|| "/".to_owned());
|
||||||
|
|
||||||
let uri = Uri::parse(&destination)
|
if let Ok(uri) = Uri::parse(&destination).map(IntoOwned::into_owned) {
|
||||||
.map(IntoOwned::into_owned)
|
Flash::success(
|
||||||
.map_err(|_| {
|
Redirect::to(uri),
|
||||||
render!(session::login(
|
i18n!(&rockets.intl.catalog, "You are now connected."),
|
||||||
&(&*conn, &rockets.intl.catalog, None),
|
)
|
||||||
None,
|
.into()
|
||||||
&*form,
|
|
||||||
errors
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(Redirect::to(uri))
|
|
||||||
} else {
|
} else {
|
||||||
Err(render!(session::login(
|
render!(session::login(
|
||||||
&(&*conn, &rockets.intl.catalog, None),
|
&(conn, &rockets.intl.catalog, None, None),
|
||||||
None,
|
None,
|
||||||
&*form,
|
&*form,
|
||||||
errors
|
errors
|
||||||
)))
|
))
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/logout")]
|
#[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) {
|
if let Some(cookie) = cookies.get_private(AUTH_COOKIE) {
|
||||||
cookies.remove_private(cookie);
|
cookies.remove_private(cookie);
|
||||||
}
|
}
|
||||||
Redirect::to("/")
|
Flash::success(
|
||||||
|
Redirect::to("/"),
|
||||||
|
i18n!(intl.catalog, "You are now logged off."),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -138,9 +142,9 @@ impl PartialEq for ResetRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/password-reset")]
|
#[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(
|
render!(session::password_reset_request(
|
||||||
&(&*conn, &intl.catalog, None),
|
&rockets.to_context(),
|
||||||
&ResetForm::default(),
|
&ResetForm::default(),
|
||||||
ValidationErrors::default()
|
ValidationErrors::default()
|
||||||
))
|
))
|
||||||
@@ -154,32 +158,19 @@ pub struct ResetForm {
|
|||||||
|
|
||||||
#[post("/password-reset", data = "<form>")]
|
#[post("/password-reset", data = "<form>")]
|
||||||
pub fn password_reset_request(
|
pub fn password_reset_request(
|
||||||
conn: DbConn,
|
|
||||||
intl: I18n,
|
|
||||||
mail: State<Arc<Mutex<Mailer>>>,
|
mail: State<Arc<Mutex<Mailer>>>,
|
||||||
form: Form<ResetForm>,
|
form: Form<ResetForm>,
|
||||||
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
|
rockets: PlumeRocket,
|
||||||
) -> Ructe {
|
) -> Ructe {
|
||||||
let mut requests = requests.lock().unwrap();
|
if User::find_by_email(&*rockets.conn, &form.email).is_ok() {
|
||||||
// Remove outdated requests (more than 1 day old) to avoid the list to grow too much
|
let token = PasswordResetRequest::insert(&*rockets.conn, &form.email)
|
||||||
requests.retain(|r| r.creation_date.elapsed().as_secs() < 24 * 60 * 60);
|
.expect("password_reset_request::insert: error");
|
||||||
|
|
||||||
if User::find_by_email(&*conn, &form.email).is_ok()
|
let url = format!("https://{}/password-reset/{}", CONFIG.base_url, token);
|
||||||
&& !requests.iter().any(|x| x.mail == form.email.clone())
|
|
||||||
{
|
|
||||||
let id = plume_common::utils::random_hex();
|
|
||||||
|
|
||||||
requests.push(ResetRequest {
|
|
||||||
mail: form.email.clone(),
|
|
||||||
id: id.clone(),
|
|
||||||
creation_date: Instant::now(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let link = format!("https://{}/password-reset/{}", CONFIG.base_url, id);
|
|
||||||
if let Some(message) = build_mail(
|
if let Some(message) = build_mail(
|
||||||
form.email.clone(),
|
form.email.clone(),
|
||||||
i18n!(intl.catalog, "Password reset"),
|
i18n!(rockets.intl.catalog, "Password reset"),
|
||||||
i18n!(intl.catalog, "Here is the link to reset your password: {0}"; link),
|
i18n!(rockets.intl.catalog, "Here is the link to reset your password: {0}"; url),
|
||||||
) {
|
) {
|
||||||
if let Some(ref mut mail) = *mail.lock().unwrap() {
|
if let Some(ref mut mail) = *mail.lock().unwrap() {
|
||||||
mail.send(message.into())
|
mail.send(message.into())
|
||||||
@@ -188,28 +179,16 @@ pub fn password_reset_request(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
render!(session::password_reset_request_ok(&(
|
render!(session::password_reset_request_ok(&rockets.to_context()))
|
||||||
&*conn,
|
|
||||||
&intl.catalog,
|
|
||||||
None
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/password-reset/<token>")]
|
#[get("/password-reset/<token>")]
|
||||||
pub fn password_reset_form(
|
pub fn password_reset_form(token: String, rockets: PlumeRocket) -> Result<Ructe, Ructe> {
|
||||||
conn: DbConn,
|
PasswordResetRequest::find_by_token(&*rockets.conn, &token)
|
||||||
intl: I18n,
|
.map_err(|err| password_reset_error_response(err, &rockets))?;
|
||||||
token: String,
|
|
||||||
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
|
|
||||||
) -> Result<Ructe, ErrorPage> {
|
|
||||||
requests
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.iter()
|
|
||||||
.find(|x| x.id == token.clone())
|
|
||||||
.ok_or(Error::NotFound)?;
|
|
||||||
Ok(render!(session::password_reset(
|
Ok(render!(session::password_reset(
|
||||||
&(&*conn, &intl.catalog, None),
|
&rockets.to_context(),
|
||||||
&NewPasswordForm::default(),
|
&NewPasswordForm::default(),
|
||||||
ValidationErrors::default()
|
ValidationErrors::default()
|
||||||
)))
|
)))
|
||||||
@@ -236,52 +215,34 @@ fn passwords_match(form: &NewPasswordForm) -> Result<(), ValidationError> {
|
|||||||
|
|
||||||
#[post("/password-reset/<token>", data = "<form>")]
|
#[post("/password-reset/<token>", data = "<form>")]
|
||||||
pub fn password_reset(
|
pub fn password_reset(
|
||||||
conn: DbConn,
|
|
||||||
intl: I18n,
|
|
||||||
token: String,
|
token: String,
|
||||||
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
|
|
||||||
form: Form<NewPasswordForm>,
|
form: Form<NewPasswordForm>,
|
||||||
) -> Result<Redirect, Ructe> {
|
rockets: PlumeRocket,
|
||||||
|
) -> Result<Flash<Redirect>, Ructe> {
|
||||||
form.validate()
|
form.validate()
|
||||||
.and_then(|_| {
|
.map_err(|err| render!(session::password_reset(&rockets.to_context(), &form, err)))?;
|
||||||
let mut requests = requests.lock().unwrap();
|
|
||||||
let req = requests
|
PasswordResetRequest::find_and_delete_by_token(&*rockets.conn, &token)
|
||||||
.iter()
|
.and_then(|request| User::find_by_email(&*rockets.conn, &request.email))
|
||||||
.find(|x| x.id == token.clone())
|
.and_then(|user| user.reset_password(&*rockets.conn, &form.password))
|
||||||
.ok_or_else(|| to_validation(0))?
|
.map_err(|err| password_reset_error_response(err, &rockets))?;
|
||||||
.clone();
|
|
||||||
if req.creation_date.elapsed().as_secs() < 60 * 60 * 2 {
|
Ok(Flash::success(
|
||||||
// Reset link is only valid for 2 hours
|
Redirect::to(uri!(
|
||||||
requests.retain(|r| *r != req);
|
new: m = _
|
||||||
let user = User::find_by_email(&*conn, &req.mail).map_err(to_validation)?;
|
)),
|
||||||
user.reset_password(&*conn, &form.password).ok();
|
i18n!(
|
||||||
Ok(Redirect::to(uri!(
|
rockets.intl.catalog,
|
||||||
new: m = i18n!(intl.catalog, "Your password was successfully reset.")
|
"Your password was successfully reset."
|
||||||
)))
|
),
|
||||||
} else {
|
))
|
||||||
Ok(Redirect::to(uri!(
|
|
||||||
new: m = i18n!(intl.catalog, "Sorry, but the link expired. Try again")
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map_err(|err| {
|
|
||||||
render!(session::password_reset(
|
|
||||||
&(&*conn, &intl.catalog, None),
|
|
||||||
&form,
|
|
||||||
err
|
|
||||||
))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_validation<T>(_: T) -> ValidationErrors {
|
fn password_reset_error_response(err: Error, rockets: &PlumeRocket) -> Ructe {
|
||||||
let mut errors = ValidationErrors::new();
|
match err {
|
||||||
errors.add(
|
Error::Expired => render!(session::password_reset_request_expired(
|
||||||
"",
|
&rockets.to_context()
|
||||||
ValidationError {
|
)),
|
||||||
code: Cow::from("server_error"),
|
_ => render!(errors::not_found(&rockets.to_context())),
|
||||||
message: Some(Cow::from("An unknown error occured")),
|
}
|
||||||
params: std::collections::HashMap::new(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
errors
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user