Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f2273b984c | |||
| 7be397b76e | |||
| d82a7674ce | |||
| 38723a4d20 | |||
| 1fc3b3dd4e | |||
| d2a684650a | |||
| 82659391f9 | |||
| bffce041d7 | |||
| 037674ae13 | |||
| 8c59c822b6 | |||
| 59023e9602 | |||
| ad3a8b92d1 | |||
| c67f65e684 | |||
| d78b3a05ec | |||
| 5579f64dd0 | |||
| 773fbfe7c8 | |||
| 5b50f90d2b | |||
| 90f6ef0380 | |||
| 33619abdfb | |||
| c52aac012c | |||
| c9070930d2 | |||
| 918bda14ec | |||
| 33a0c7dcd3 | |||
| 8f1ab3485e | |||
| 18ae6e26b3 | |||
| 49bb8cb0bc | |||
| ec57f1e687 | |||
| 787eb7f399 | |||
| 85aa0883c8 | |||
| ff92ac0763 | |||
| a20d2df50a | |||
| 29e11f552f |
+227
-284
@@ -1,313 +1,256 @@
|
|||||||
version: 2.1
|
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"]
|
||||||
|
|||||||
Generated
+752
-540
File diff suppressed because it is too large
Load Diff
+7
-7
@@ -8,9 +8,8 @@ repository = "https://github.com/Plume-org/Plume"
|
|||||||
activitypub = "0.1.3"
|
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"]
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
--#!|_conn, path: &Path| {
|
||||||
|
--#! let mut pb = path.to_path_buf();
|
||||||
|
--#! pb.push("search_index");
|
||||||
|
--#! std::fs::remove_dir_all(pb).map_err(Error::from)
|
||||||
|
--#!}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
--#!|conn: &Connection, path: &Path| {
|
||||||
|
--#! let mut pb = path.to_path_buf();
|
||||||
|
--#! pb.push("search_index");
|
||||||
|
--#! let searcher = super::search::Searcher::create(&pb)?;
|
||||||
|
--#! searcher.fill(conn)?;
|
||||||
|
--#! searcher.commit();
|
||||||
|
--#! Ok(())
|
||||||
|
--#!}
|
||||||
|
|
||||||
@@ -4,6 +4,5 @@ version = "0.3.0"
|
|||||||
authors = ["Plume contributors"]
|
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"]
|
||||||
|
|||||||
+11
-5
@@ -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().ok();
|
||||||
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,14 @@ 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"
|
||||||
|
|
||||||
[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::ClientBuilder;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::Status,
|
http::Status,
|
||||||
request::{FromRequest, Request},
|
request::{FromRequest, Request},
|
||||||
@@ -134,16 +134,21 @@ where
|
|||||||
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()
|
let res = ClientBuilder::new()
|
||||||
.post(&inbox)
|
.connect_timeout(Some(std::time::Duration::from_secs(5)))
|
||||||
.headers(headers.clone())
|
.build()
|
||||||
.header(
|
.and_then(|client| {
|
||||||
"Signature",
|
client
|
||||||
request::signature(sender, &headers)
|
.post(&inbox)
|
||||||
.expect("activity_pub::broadcast: request signature error"),
|
.headers(headers.clone())
|
||||||
)
|
.header(
|
||||||
.body(body)
|
"Signature",
|
||||||
.send();
|
request::signature(sender, &headers)
|
||||||
|
.expect("activity_pub::broadcast: request signature error"),
|
||||||
|
)
|
||||||
|
.body(body)
|
||||||
|
.send()
|
||||||
|
});
|
||||||
match res {
|
match res {
|
||||||
Ok(mut r) => {
|
Ok(mut r) => {
|
||||||
println!("Successfully sent activity to inbox ({})", inbox);
|
println!("Successfully sent activity to inbox ({})", inbox);
|
||||||
@@ -158,18 +163,12 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Shrinkwrap, Clone, Serialize, Deserialize)]
|
||||||
pub struct Id(String);
|
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,6 +15,8 @@ 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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
+12
-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")))]
|
||||||
@@ -154,8 +153,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 +302,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 +332,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,6 +365,7 @@ 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 plume_rocket;
|
pub mod plume_rocket;
|
||||||
pub mod post_authors;
|
pub mod post_authors;
|
||||||
|
|||||||
+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! {}
|
||||||
|
};
|
||||||
@@ -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()?,
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -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"
|
||||||
+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(()))
|
||||||
|
}
|
||||||
|
|||||||
+29
-9
@@ -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().ok();
|
||||||
|
|
||||||
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![
|
||||||
|
|||||||
+44
-40
@@ -17,21 +17,19 @@ use plume_models::{
|
|||||||
users::User, Connection, PlumeRocket,
|
users::User, Connection, PlumeRocket,
|
||||||
};
|
};
|
||||||
use routes::{errors::ErrorPage, Page};
|
use routes::{errors::ErrorPage, Page};
|
||||||
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,10 @@ 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,
|
||||||
|
) -> Result<Flash<Redirect>, Ructe> {
|
||||||
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;
|
||||||
@@ -121,7 +118,7 @@ pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> Result<Re
|
|||||||
slug.clone(),
|
slug.clone(),
|
||||||
form.title.to_string(),
|
form.title.to_string(),
|
||||||
String::from(""),
|
String::from(""),
|
||||||
Instance::get_local(&*conn)
|
Instance::get_local()
|
||||||
.expect("blog::create: instance error")
|
.expect("blog::create: instance error")
|
||||||
.id,
|
.id,
|
||||||
)
|
)
|
||||||
@@ -139,37 +136,40 @@ pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> Result<Re
|
|||||||
)
|
)
|
||||||
.expect("blog::create: author error");
|
.expect("blog::create: author error");
|
||||||
|
|
||||||
Ok(Redirect::to(uri!(details: name = slug.clone(), page = _)))
|
Ok(Flash::success(
|
||||||
|
Redirect::to(uri!(details: name = slug.clone(), page = _)),
|
||||||
|
&i18n!(intl, "Your blog was successfully created!"),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
Err(render!(blogs::new(
|
Err(render!(blogs::new(&rockets.to_context(), &*form, errors)))
|
||||||
&(&*conn, intl, Some(user)),
|
|
||||||
&*form,
|
|
||||||
errors
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/~/<name>/delete")]
|
#[post("/~/<name>/delete")]
|
||||||
pub fn delete(name: String, rockets: PlumeRocket) -> Result<Redirect, Ructe> {
|
pub fn delete(name: String, rockets: PlumeRocket) -> Result<Flash<Redirect>, Ructe> {
|
||||||
let conn = &*rockets.conn;
|
let 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)))
|
Ok(Flash::success(
|
||||||
|
Redirect::to(uri!(super::instance::index)),
|
||||||
|
i18n!(rockets.intl.catalog, "Your blog was deleted."),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
// TODO actually return 403 error code
|
// TODO actually return 403 error code
|
||||||
Err(render!(errors::not_authorized(
|
Err(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."
|
||||||
|
)
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,7 +236,7 @@ pub fn update(
|
|||||||
name: String,
|
name: String,
|
||||||
form: LenientForm<EditForm>,
|
form: LenientForm<EditForm>,
|
||||||
rockets: PlumeRocket,
|
rockets: PlumeRocket,
|
||||||
) -> Result<Redirect, Ructe> {
|
) -> Result<Flash<Redirect>, Ructe> {
|
||||||
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");
|
||||||
@@ -292,7 +293,7 @@ pub fn update(
|
|||||||
blog.summary_html = SafeString::new(
|
blog.summary_html = SafeString::new(
|
||||||
&utils::md_to_html(
|
&utils::md_to_html(
|
||||||
&form.summary,
|
&form.summary,
|
||||||
"",
|
None,
|
||||||
true,
|
true,
|
||||||
Some(Media::get_media_processor(
|
Some(Media::get_media_processor(
|
||||||
&conn,
|
&conn,
|
||||||
@@ -308,12 +309,15 @@ pub fn update(
|
|||||||
blog.banner_id = form.banner;
|
blog.banner_id = form.banner;
|
||||||
blog.save_changes::<Blog>(&*conn)
|
blog.save_changes::<Blog>(&*conn)
|
||||||
.expect("Couldn't save blog changes");
|
.expect("Couldn't save blog changes");
|
||||||
Ok(Redirect::to(uri!(details: name = name, page = _)))
|
Ok(Flash::success(
|
||||||
|
Redirect::to(uri!(details: name = name, page = _)),
|
||||||
|
i18n!(intl, "Your blog information have been updated."),
|
||||||
|
))
|
||||||
})
|
})
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
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");
|
||||||
render!(blogs::edit(
|
render!(blogs::edit(
|
||||||
&(&*conn, intl, Some(user)),
|
&rockets.to_context(),
|
||||||
&blog,
|
&blog,
|
||||||
medias,
|
medias,
|
||||||
&*form,
|
&*form,
|
||||||
@@ -323,7 +327,7 @@ pub fn update(
|
|||||||
} else {
|
} else {
|
||||||
// TODO actually return 403 error code
|
// TODO actually return 403 error code
|
||||||
Err(render!(errors::not_authorized(
|
Err(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."
|
||||||
@@ -344,7 +348,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()))
|
||||||
}
|
}
|
||||||
|
|||||||
+108
-89
@@ -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};
|
||||||
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,31 +111,34 @@ 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> {
|
) -> Result<Flash<Redirect>, Ructe> {
|
||||||
|
let conn = &*rockets.conn;
|
||||||
form.validate()
|
form.validate()
|
||||||
.and_then(|_| {
|
.and_then(|_| {
|
||||||
let instance = Instance::get_local(&*conn)
|
let instance =
|
||||||
.expect("instance::update_settings: local instance error");
|
Instance::get_local().expect("instance::update_settings: local instance error");
|
||||||
instance
|
instance
|
||||||
.update(
|
.update(
|
||||||
&*conn,
|
conn,
|
||||||
form.name.clone(),
|
form.name.clone(),
|
||||||
form.open_registrations,
|
form.open_registrations,
|
||||||
form.short_description.clone(),
|
form.short_description.clone(),
|
||||||
form.long_description.clone(),
|
form.long_description.clone(),
|
||||||
)
|
)
|
||||||
.expect("instance::update_settings: save error");
|
.expect("instance::update_settings: save error");
|
||||||
Ok(Redirect::to(uri!(admin)))
|
Ok(Flash::success(
|
||||||
|
Redirect::to(uri!(admin)),
|
||||||
|
i18n!(rockets.intl.catalog, "Instance settings have been saved."),
|
||||||
|
))
|
||||||
})
|
})
|
||||||
.or_else(|e| {
|
.or_else(|e| {
|
||||||
let local_inst = Instance::get_local(&*conn)
|
let local_inst =
|
||||||
.expect("instance::update_settings: local instance error");
|
Instance::get_local().expect("instance::update_settings: local instance error");
|
||||||
Err(render!(instance::admin(
|
Err(render!(instance::admin(
|
||||||
&(&*conn, &intl.catalog, Some(admin.0)),
|
&rockets.to_context(),
|
||||||
local_inst,
|
local_inst,
|
||||||
form.clone(),
|
form.clone(),
|
||||||
e
|
e
|
||||||
@@ -155,58 +148,78 @@ pub fn update_settings(
|
|||||||
|
|
||||||
#[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,
|
||||||
|
|||||||
+37
-19
@@ -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>")]
|
||||||
@@ -122,32 +122,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)))
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -16,7 +16,7 @@ use plume_models::{posts::Post, Connection};
|
|||||||
|
|
||||||
const ITEMS_PER_PAGE: i32 = 12;
|
const ITEMS_PER_PAGE: i32 = 12;
|
||||||
|
|
||||||
#[derive(Copy, Clone, UriDisplayQuery)]
|
#[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 +52,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 +73,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)
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+59
-47
@@ -27,7 +27,7 @@ use plume_models::{
|
|||||||
Error, PlumeRocket,
|
Error, PlumeRocket,
|
||||||
};
|
};
|
||||||
use routes::{comments::NewCommentForm, errors::ErrorPage, ContentLen, RemoteForm};
|
use routes::{comments::NewCommentForm, errors::ErrorPage, ContentLen, RemoteForm};
|
||||||
use template_utils::Ructe;
|
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(
|
||||||
@@ -51,7 +51,7 @@ pub fn details(
|
|||||||
let previous = responding_to.and_then(|r| Comment::get(&*conn, r).ok());
|
let previous = responding_to.and_then(|r| Comment::get(&*conn, r).ok());
|
||||||
|
|
||||||
Ok(render!(posts::details(
|
Ok(render!(posts::details(
|
||||||
&(&*conn, &rockets.intl.catalog, user.clone()),
|
&rockets.to_context(),
|
||||||
post.clone(),
|
post.clone(),
|
||||||
blog,
|
blog,
|
||||||
&NewCommentForm {
|
&NewCommentForm {
|
||||||
@@ -89,7 +89,7 @@ pub fn details(
|
|||||||
)))
|
)))
|
||||||
} else {
|
} else {
|
||||||
Ok(render!(errors::not_authorized(
|
Ok(render!(errors::not_authorized(
|
||||||
&(&*conn, &rockets.intl.catalog, user.clone()),
|
&rockets.to_context(),
|
||||||
i18n!(rockets.intl.catalog, "This post isn't published yet.")
|
i18n!(rockets.intl.catalog, "This post isn't published yet.")
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
@@ -130,25 +130,24 @@ pub fn new_auth(blog: String, i18n: I18n) -> Flash<Redirect> {
|
|||||||
pub fn new(blog: String, cl: ContentLen, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
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 +169,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 +187,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 +219,7 @@ pub fn update(
|
|||||||
cl: ContentLen,
|
cl: ContentLen,
|
||||||
form: LenientForm<NewPostForm>,
|
form: LenientForm<NewPostForm>,
|
||||||
rockets: PlumeRocket,
|
rockets: PlumeRocket,
|
||||||
) -> Result<Redirect, Ructe> {
|
) -> Result<Flash<Redirect>, Ructe> {
|
||||||
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 +255,18 @@ 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(
|
Ok(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."),
|
||||||
))
|
))
|
||||||
} 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 +314,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 +324,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 +345,15 @@ pub fn update(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Redirect::to(
|
Ok(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."),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
} 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(
|
Err(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,
|
||||||
@@ -393,7 +396,7 @@ pub fn create(
|
|||||||
form: LenientForm<NewPostForm>,
|
form: LenientForm<NewPostForm>,
|
||||||
cl: ContentLen,
|
cl: ContentLen,
|
||||||
rockets: PlumeRocket,
|
rockets: PlumeRocket,
|
||||||
) -> Result<Redirect, Result<Ructe, ErrorPage>> {
|
) -> Result<Flash<Redirect>, Result<Ructe, 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 +423,22 @@ 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."
|
||||||
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +527,15 @@ 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."),
|
||||||
))
|
))
|
||||||
} 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;
|
|
||||||
Err(Ok(render!(posts::new(
|
Err(Ok(render!(posts::new(
|
||||||
&(&*conn, &intl.catalog, Some(user)),
|
&rockets.to_context(),
|
||||||
i18n!(intl.catalog, "New post"),
|
i18n!(rockets.intl.catalog, "New article"),
|
||||||
blog,
|
blog,
|
||||||
false,
|
false,
|
||||||
&*form,
|
&*form,
|
||||||
@@ -544,7 +553,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 +565,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 +592,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 +608,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,7 +627,6 @@ pub fn remote_interact_post(
|
|||||||
blog_name: String,
|
blog_name: String,
|
||||||
slug: String,
|
slug: String,
|
||||||
remote: LenientForm<RemoteForm>,
|
remote: LenientForm<RemoteForm>,
|
||||||
i18n: I18n,
|
|
||||||
) -> Result<Result<Ructe, Redirect>, 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))?;
|
||||||
@@ -627,12 +639,12 @@ pub fn remote_interact_post(
|
|||||||
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(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(),
|
||||||
|
|||||||
+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,
|
||||||
|
|||||||
+61
-54
@@ -2,8 +2,8 @@ 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;
|
||||||
@@ -12,21 +12,20 @@ use std::{
|
|||||||
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,
|
|
||||||
users::{User, AUTH_COOKIE},
|
users::{User, AUTH_COOKIE},
|
||||||
Error, PlumeRocket, CONFIG,
|
Error, PlumeRocket, CONFIG,
|
||||||
};
|
};
|
||||||
use routes::errors::ErrorPage;
|
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 +43,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> {
|
) -> Result<Flash<Redirect>, Ructe> {
|
||||||
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));
|
||||||
@@ -84,31 +82,38 @@ pub fn create(
|
|||||||
.same_site(SameSite::Lax)
|
.same_site(SameSite::Lax)
|
||||||
.finish(),
|
.finish(),
|
||||||
);
|
);
|
||||||
let destination = flash
|
let destination = rockets
|
||||||
.and_then(|f| {
|
.flash_msg
|
||||||
if f.name() == "callback" {
|
.clone()
|
||||||
Some(f.msg().to_owned())
|
.and_then(
|
||||||
} else {
|
|(name, msg)| {
|
||||||
None
|
if name == "callback" {
|
||||||
}
|
Some(msg)
|
||||||
})
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
.unwrap_or_else(|| "/".to_owned());
|
.unwrap_or_else(|| "/".to_owned());
|
||||||
|
|
||||||
let uri = Uri::parse(&destination)
|
let uri = Uri::parse(&destination)
|
||||||
.map(IntoOwned::into_owned)
|
.map(IntoOwned::into_owned)
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
render!(session::login(
|
render!(session::login(
|
||||||
&(&*conn, &rockets.intl.catalog, None),
|
&(conn, &rockets.intl.catalog, None, None),
|
||||||
None,
|
None,
|
||||||
&*form,
|
&*form,
|
||||||
errors
|
errors
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(Redirect::to(uri))
|
Ok(Flash::success(
|
||||||
|
Redirect::to(uri),
|
||||||
|
i18n!(&rockets.intl.catalog, "You are now connected."),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
Err(render!(session::login(
|
Err(render!(session::login(
|
||||||
&(&*conn, &rockets.intl.catalog, None),
|
&rockets.to_context(),
|
||||||
None,
|
None,
|
||||||
&*form,
|
&*form,
|
||||||
errors
|
errors
|
||||||
@@ -117,11 +122,14 @@ pub fn create(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[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 +146,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,17 +162,16 @@ 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>>>>,
|
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
|
||||||
|
rockets: PlumeRocket,
|
||||||
) -> Ructe {
|
) -> Ructe {
|
||||||
let mut requests = requests.lock().unwrap();
|
let mut requests = requests.lock().unwrap();
|
||||||
// Remove outdated requests (more than 1 day old) to avoid the list to grow too much
|
// Remove outdated requests (more than 1 day old) to avoid the list to grow too much
|
||||||
requests.retain(|r| r.creation_date.elapsed().as_secs() < 24 * 60 * 60);
|
requests.retain(|r| r.creation_date.elapsed().as_secs() < 24 * 60 * 60);
|
||||||
|
|
||||||
if User::find_by_email(&*conn, &form.email).is_ok()
|
if User::find_by_email(&*rockets.conn, &form.email).is_ok()
|
||||||
&& !requests.iter().any(|x| x.mail == form.email.clone())
|
&& !requests.iter().any(|x| x.mail == form.email.clone())
|
||||||
{
|
{
|
||||||
let id = plume_common::utils::random_hex();
|
let id = plume_common::utils::random_hex();
|
||||||
@@ -178,8 +185,8 @@ pub fn password_reset_request(
|
|||||||
let link = format!("https://{}/password-reset/{}", CONFIG.base_url, id);
|
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}"; link),
|
||||||
) {
|
) {
|
||||||
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,19 +195,14 @@ 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(
|
||||||
conn: DbConn,
|
|
||||||
intl: I18n,
|
|
||||||
token: String,
|
token: String,
|
||||||
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
|
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
|
||||||
|
rockets: PlumeRocket,
|
||||||
) -> Result<Ructe, ErrorPage> {
|
) -> Result<Ructe, ErrorPage> {
|
||||||
requests
|
requests
|
||||||
.lock()
|
.lock()
|
||||||
@@ -209,7 +211,7 @@ pub fn password_reset_form(
|
|||||||
.find(|x| x.id == token.clone())
|
.find(|x| x.id == token.clone())
|
||||||
.ok_or(Error::NotFound)?;
|
.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,12 +238,11 @@ 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>>>>,
|
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(|_| {
|
.and_then(|_| {
|
||||||
let mut requests = requests.lock().unwrap();
|
let mut requests = requests.lock().unwrap();
|
||||||
@@ -253,24 +254,30 @@ pub fn password_reset(
|
|||||||
if req.creation_date.elapsed().as_secs() < 60 * 60 * 2 {
|
if req.creation_date.elapsed().as_secs() < 60 * 60 * 2 {
|
||||||
// Reset link is only valid for 2 hours
|
// Reset link is only valid for 2 hours
|
||||||
requests.retain(|r| *r != req);
|
requests.retain(|r| *r != req);
|
||||||
let user = User::find_by_email(&*conn, &req.mail).map_err(to_validation)?;
|
let user = User::find_by_email(&*rockets.conn, &req.mail).map_err(to_validation)?;
|
||||||
user.reset_password(&*conn, &form.password).ok();
|
user.reset_password(&*rockets.conn, &form.password).ok();
|
||||||
Ok(Redirect::to(uri!(
|
Ok(Flash::success(
|
||||||
new: m = i18n!(intl.catalog, "Your password was successfully reset.")
|
Redirect::to(uri!(
|
||||||
)))
|
new: m = _
|
||||||
|
)),
|
||||||
|
i18n!(
|
||||||
|
rockets.intl.catalog,
|
||||||
|
"Your password was successfully reset."
|
||||||
|
),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
Ok(Redirect::to(uri!(
|
Ok(Flash::error(
|
||||||
new: m = i18n!(intl.catalog, "Sorry, but the link expired. Try again")
|
Redirect::to(uri!(
|
||||||
)))
|
new: m = _
|
||||||
|
)),
|
||||||
|
i18n!(
|
||||||
|
rockets.intl.catalog,
|
||||||
|
"Sorry, but the link expired. Try again"
|
||||||
|
),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map_err(|err| {
|
.map_err(|err| render!(session::password_reset(&rockets.to_context(), &form, err)))
|
||||||
render!(session::password_reset(
|
|
||||||
&(&*conn, &intl.catalog, None),
|
|
||||||
&form,
|
|
||||||
err
|
|
||||||
))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_validation<T>(_: T) -> ValidationErrors {
|
fn to_validation<T>(_: T) -> ValidationErrors {
|
||||||
|
|||||||
+6
-14
@@ -1,24 +1,16 @@
|
|||||||
use rocket_i18n::I18n;
|
use plume_models::{posts::Post, PlumeRocket};
|
||||||
|
|
||||||
use plume_models::{db_conn::DbConn, posts::Post, users::User};
|
|
||||||
use routes::{errors::ErrorPage, Page};
|
use routes::{errors::ErrorPage, Page};
|
||||||
use template_utils::Ructe;
|
use template_utils::{IntoContext, Ructe};
|
||||||
|
|
||||||
#[get("/tag/<name>?<page>")]
|
#[get("/tag/<name>?<page>")]
|
||||||
pub fn tag(
|
pub fn tag(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||||
user: Option<User>,
|
|
||||||
conn: DbConn,
|
|
||||||
name: String,
|
|
||||||
page: Option<Page>,
|
|
||||||
intl: I18n,
|
|
||||||
) -> Result<Ructe, ErrorPage> {
|
|
||||||
let page = page.unwrap_or_default();
|
let page = page.unwrap_or_default();
|
||||||
let posts = Post::list_by_tag(&*conn, name.clone(), page.limits())?;
|
let posts = Post::list_by_tag(&*rockets.conn, name.clone(), page.limits())?;
|
||||||
Ok(render!(tags::index(
|
Ok(render!(tags::index(
|
||||||
&(&*conn, &intl.catalog, user),
|
&rockets.to_context(),
|
||||||
name.clone(),
|
name.clone(),
|
||||||
posts,
|
posts,
|
||||||
page.0,
|
page.0,
|
||||||
Page::total(Post::count_for_tag(&*conn, name)? as i32)
|
Page::total(Post::count_for_tag(&*rockets.conn, name)? as i32)
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-45
@@ -26,7 +26,7 @@ use plume_models::{
|
|||||||
Error, PlumeRocket,
|
Error, PlumeRocket,
|
||||||
};
|
};
|
||||||
use routes::{errors::ErrorPage, Page, RemoteForm};
|
use routes::{errors::ErrorPage, Page, RemoteForm};
|
||||||
use template_utils::Ructe;
|
use template_utils::{IntoContext, Ructe};
|
||||||
|
|
||||||
#[get("/me")]
|
#[get("/me")]
|
||||||
pub fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
|
pub fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
|
||||||
@@ -48,7 +48,7 @@ pub fn details(
|
|||||||
let user = User::find_by_fqn(&rockets, &name)?;
|
let user = User::find_by_fqn(&rockets, &name)?;
|
||||||
let recents = Post::get_recents_for_author(&*conn, &user, 6)?;
|
let recents = Post::get_recents_for_author(&*conn, &user, 6)?;
|
||||||
let reshares = Reshare::get_recents_for_author(&*conn, &user, 6)?;
|
let reshares = Reshare::get_recents_for_author(&*conn, &user, 6)?;
|
||||||
let worker = rockets.worker;
|
let worker = &rockets.worker;
|
||||||
|
|
||||||
if !user.get_instance(&*conn)?.local {
|
if !user.get_instance(&*conn)?.local {
|
||||||
// Fetch new articles
|
// Fetch new articles
|
||||||
@@ -101,15 +101,15 @@ pub fn details(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let account = rockets.user;
|
|
||||||
let intl = rockets.intl;
|
|
||||||
Ok(render!(users::details(
|
Ok(render!(users::details(
|
||||||
&(&*conn, &intl.catalog, account.clone()),
|
&rockets.to_context(),
|
||||||
user.clone(),
|
user.clone(),
|
||||||
account
|
rockets
|
||||||
|
.user
|
||||||
|
.clone()
|
||||||
.and_then(|x| x.is_following(&*conn, user.id).ok())
|
.and_then(|x| x.is_following(&*conn, user.id).ok())
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
user.instance_id != Instance::get_local(&*conn)?.id,
|
user.instance_id != Instance::get_local()?.id,
|
||||||
user.get_instance(&*conn)?.public_domain,
|
user.get_instance(&*conn)?.public_domain,
|
||||||
recents,
|
recents,
|
||||||
reshares
|
reshares
|
||||||
@@ -120,12 +120,12 @@ pub fn details(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/dashboard")]
|
#[get("/dashboard")]
|
||||||
pub fn dashboard(user: User, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> {
|
pub fn dashboard(user: User, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||||
let blogs = Blog::find_for_author(&*conn, &user)?;
|
let blogs = Blog::find_for_author(&*rockets.conn, &user)?;
|
||||||
Ok(render!(users::dashboard(
|
Ok(render!(users::dashboard(
|
||||||
&(&*conn, &intl.catalog, Some(user.clone())),
|
&rockets.to_context(),
|
||||||
blogs,
|
blogs,
|
||||||
Post::drafts_by_author(&*conn, &user)?
|
Post::drafts_by_author(&*rockets.conn, &user)?
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,19 +141,25 @@ pub fn dashboard_auth(i18n: I18n) -> Flash<Redirect> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/@/<name>/follow")]
|
#[post("/@/<name>/follow")]
|
||||||
pub fn follow(name: String, user: User, rockets: PlumeRocket) -> Result<Redirect, ErrorPage> {
|
pub fn follow(
|
||||||
|
name: String,
|
||||||
|
user: User,
|
||||||
|
rockets: PlumeRocket,
|
||||||
|
) -> Result<Flash<Redirect>, ErrorPage> {
|
||||||
let conn = &*rockets.conn;
|
let conn = &*rockets.conn;
|
||||||
let target = User::find_by_fqn(&rockets, &name)?;
|
let target = User::find_by_fqn(&rockets, &name)?;
|
||||||
if let Ok(follow) = follows::Follow::find(&*conn, user.id, target.id) {
|
let message = if let Ok(follow) = follows::Follow::find(&*conn, user.id, target.id) {
|
||||||
let delete_act = follow.build_undo(&*conn)?;
|
let delete_act = follow.build_undo(&*conn)?;
|
||||||
local_inbox(
|
local_inbox(
|
||||||
&rockets,
|
&rockets,
|
||||||
serde_json::to_value(&delete_act).map_err(Error::from)?,
|
serde_json::to_value(&delete_act).map_err(Error::from)?,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let msg = i18n!(rockets.intl.catalog, "You are no longer following {}."; target.name());
|
||||||
rockets
|
rockets
|
||||||
.worker
|
.worker
|
||||||
.execute(move || broadcast(&user, delete_act, vec![target]));
|
.execute(move || broadcast(&user, delete_act, vec![target]));
|
||||||
|
msg
|
||||||
} else {
|
} else {
|
||||||
let f = follows::Follow::insert(
|
let f = follows::Follow::insert(
|
||||||
&*conn,
|
&*conn,
|
||||||
@@ -166,11 +172,16 @@ pub fn follow(name: String, user: User, rockets: PlumeRocket) -> Result<Redirect
|
|||||||
f.notify(&*conn)?;
|
f.notify(&*conn)?;
|
||||||
|
|
||||||
let act = f.to_activity(&*conn)?;
|
let act = f.to_activity(&*conn)?;
|
||||||
|
let msg = i18n!(rockets.intl.catalog, "You are now following {}."; target.name());
|
||||||
rockets
|
rockets
|
||||||
.worker
|
.worker
|
||||||
.execute(move || broadcast(&user, act, vec![target]));
|
.execute(move || broadcast(&user, act, vec![target]));
|
||||||
}
|
msg
|
||||||
Ok(Redirect::to(uri!(details: name = name)))
|
};
|
||||||
|
Ok(Flash::success(
|
||||||
|
Redirect::to(uri!(details: name = name)),
|
||||||
|
message,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/@/<name>/follow", data = "<remote_form>", rank = 2)]
|
#[post("/@/<name>/follow", data = "<remote_form>", rank = 2)]
|
||||||
@@ -182,8 +193,7 @@ pub fn follow_not_connected(
|
|||||||
) -> Result<Result<Flash<Ructe>, Redirect>, ErrorPage> {
|
) -> Result<Result<Flash<Ructe>, Redirect>, ErrorPage> {
|
||||||
let target = User::find_by_fqn(&rockets, &name)?;
|
let target = User::find_by_fqn(&rockets, &name)?;
|
||||||
if let Some(remote_form) = remote_form {
|
if let Some(remote_form) = remote_form {
|
||||||
let remote = &remote_form.remote;
|
if let Some(uri) = User::fetch_remote_interact_uri(&remote_form)
|
||||||
if let Some(uri) = User::fetch_remote_interact_uri(remote)
|
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|uri| {
|
.and_then(|uri| {
|
||||||
rt_format!(
|
rt_format!(
|
||||||
@@ -209,7 +219,7 @@ pub fn follow_not_connected(
|
|||||||
);
|
);
|
||||||
Ok(Ok(Flash::new(
|
Ok(Ok(Flash::new(
|
||||||
render!(users::follow_remote(
|
render!(users::follow_remote(
|
||||||
&(&rockets.conn, &i18n.catalog, None),
|
&rockets.to_context(),
|
||||||
target,
|
target,
|
||||||
super::session::LoginForm::default(),
|
super::session::LoginForm::default(),
|
||||||
ValidationErrors::default(),
|
ValidationErrors::default(),
|
||||||
@@ -223,7 +233,7 @@ pub fn follow_not_connected(
|
|||||||
} else {
|
} else {
|
||||||
Ok(Ok(Flash::new(
|
Ok(Ok(Flash::new(
|
||||||
render!(users::follow_remote(
|
render!(users::follow_remote(
|
||||||
&(&rockets.conn, &i18n.catalog, None),
|
&rockets.to_context(),
|
||||||
target,
|
target,
|
||||||
super::session::LoginForm::default(),
|
super::session::LoginForm::default(),
|
||||||
ValidationErrors::default(),
|
ValidationErrors::default(),
|
||||||
@@ -260,13 +270,14 @@ pub fn followers(
|
|||||||
let followers_count = user.count_followers(&*conn)?;
|
let followers_count = user.count_followers(&*conn)?;
|
||||||
|
|
||||||
Ok(render!(users::followers(
|
Ok(render!(users::followers(
|
||||||
&(&*conn, &rockets.intl.catalog, rockets.user.clone()),
|
&rockets.to_context(),
|
||||||
user.clone(),
|
user.clone(),
|
||||||
rockets
|
rockets
|
||||||
.user
|
.user
|
||||||
|
.clone()
|
||||||
.and_then(|x| x.is_following(&*conn, user.id).ok())
|
.and_then(|x| x.is_following(&*conn, user.id).ok())
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
user.instance_id != Instance::get_local(&*conn)?.id,
|
user.instance_id != Instance::get_local()?.id,
|
||||||
user.get_instance(&*conn)?.public_domain,
|
user.get_instance(&*conn)?.public_domain,
|
||||||
user.get_followers_page(&*conn, page.limits())?,
|
user.get_followers_page(&*conn, page.limits())?,
|
||||||
page.0,
|
page.0,
|
||||||
@@ -283,18 +294,19 @@ pub fn followed(
|
|||||||
let conn = &*rockets.conn;
|
let conn = &*rockets.conn;
|
||||||
let page = page.unwrap_or_default();
|
let page = page.unwrap_or_default();
|
||||||
let user = User::find_by_fqn(&rockets, &name)?;
|
let user = User::find_by_fqn(&rockets, &name)?;
|
||||||
let followed_count = user.count_followed(&*conn)?;
|
let followed_count = user.count_followed(conn)?;
|
||||||
|
|
||||||
Ok(render!(users::followed(
|
Ok(render!(users::followed(
|
||||||
&(&*conn, &rockets.intl.catalog, rockets.user.clone()),
|
&rockets.to_context(),
|
||||||
user.clone(),
|
user.clone(),
|
||||||
rockets
|
rockets
|
||||||
.user
|
.user
|
||||||
.and_then(|x| x.is_following(&*conn, user.id).ok())
|
.clone()
|
||||||
|
.and_then(|x| x.is_following(conn, user.id).ok())
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
user.instance_id != Instance::get_local(&*conn)?.id,
|
user.instance_id != Instance::get_local()?.id,
|
||||||
user.get_instance(&*conn)?.public_domain,
|
user.get_instance(conn)?.public_domain,
|
||||||
user.get_followed_page(&*conn, page.limits())?,
|
user.get_followed_page(conn, page.limits())?,
|
||||||
page.0,
|
page.0,
|
||||||
Page::total(followed_count as i32)
|
Page::total(followed_count as i32)
|
||||||
)))
|
)))
|
||||||
@@ -311,20 +323,20 @@ pub fn activity_details(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/users/new")]
|
#[get("/users/new")]
|
||||||
pub fn new(user: Option<User>, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> {
|
pub fn new(rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||||
Ok(render!(users::new(
|
Ok(render!(users::new(
|
||||||
&(&*conn, &intl.catalog, user),
|
&rockets.to_context(),
|
||||||
Instance::get_local(&*conn)?.open_registrations,
|
Instance::get_local()?.open_registrations,
|
||||||
&NewUserForm::default(),
|
&NewUserForm::default(),
|
||||||
ValidationErrors::default()
|
ValidationErrors::default()
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/@/<name>/edit")]
|
#[get("/@/<name>/edit")]
|
||||||
pub fn edit(name: String, user: User, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> {
|
pub fn edit(name: String, user: User, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||||
if user.username == name && !name.contains('@') {
|
if user.username == name && !name.contains('@') {
|
||||||
Ok(render!(users::edit(
|
Ok(render!(users::edit(
|
||||||
&(&*conn, &intl.catalog, Some(user.clone())),
|
&rockets.to_context(),
|
||||||
UpdateUserForm {
|
UpdateUserForm {
|
||||||
display_name: user.display_name.clone(),
|
display_name: user.display_name.clone(),
|
||||||
email: user.email.clone().unwrap_or_default(),
|
email: user.email.clone().unwrap_or_default(),
|
||||||
@@ -361,7 +373,8 @@ pub fn update(
|
|||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
user: User,
|
user: User,
|
||||||
form: LenientForm<UpdateUserForm>,
|
form: LenientForm<UpdateUserForm>,
|
||||||
) -> Result<Redirect, ErrorPage> {
|
intl: I18n,
|
||||||
|
) -> Result<Flash<Redirect>, ErrorPage> {
|
||||||
user.update(
|
user.update(
|
||||||
&*conn,
|
&*conn,
|
||||||
if !form.display_name.is_empty() {
|
if !form.display_name.is_empty() {
|
||||||
@@ -380,7 +393,10 @@ pub fn update(
|
|||||||
user.summary.to_string()
|
user.summary.to_string()
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
Ok(Redirect::to(uri!(me)))
|
Ok(Flash::success(
|
||||||
|
Redirect::to(uri!(me)),
|
||||||
|
i18n!(intl.catalog, "Your profile has been updated."),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/@/<name>/delete")]
|
#[post("/@/<name>/delete")]
|
||||||
@@ -389,18 +405,33 @@ pub fn delete(
|
|||||||
user: User,
|
user: User,
|
||||||
mut cookies: Cookies,
|
mut cookies: Cookies,
|
||||||
rockets: PlumeRocket,
|
rockets: PlumeRocket,
|
||||||
) -> Result<Redirect, ErrorPage> {
|
) -> Result<Flash<Redirect>, ErrorPage> {
|
||||||
let account = User::find_by_fqn(&rockets, &name)?;
|
let account = User::find_by_fqn(&rockets, &name)?;
|
||||||
if user.id == account.id {
|
if user.id == account.id {
|
||||||
account.delete(&*rockets.conn, &rockets.searcher)?;
|
account.delete(&*rockets.conn, &rockets.searcher)?;
|
||||||
|
|
||||||
|
let target = User::one_by_instance(&*rockets.conn)?;
|
||||||
|
let delete_act = account.delete_activity(&*rockets.conn)?;
|
||||||
|
rockets
|
||||||
|
.worker
|
||||||
|
.execute(move || broadcast(&account, delete_act, target));
|
||||||
|
|
||||||
if let Some(cookie) = cookies.get_private(AUTH_COOKIE) {
|
if let Some(cookie) = cookies.get_private(AUTH_COOKIE) {
|
||||||
cookies.remove_private(cookie);
|
cookies.remove_private(cookie);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Redirect::to(uri!(super::instance::index)))
|
Ok(Flash::success(
|
||||||
|
Redirect::to(uri!(super::instance::index)),
|
||||||
|
i18n!(rockets.intl.catalog, "Your account has been deleted."),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
Ok(Redirect::to(uri!(edit: name = name)))
|
Ok(Flash::error(
|
||||||
|
Redirect::to(uri!(edit: name = name)),
|
||||||
|
i18n!(
|
||||||
|
rockets.intl.catalog,
|
||||||
|
"You can't delete someone else's account."
|
||||||
|
),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,12 +488,22 @@ fn to_validation(_: Error) -> ValidationErrors {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/users/new", data = "<form>")]
|
#[post("/users/new", data = "<form>")]
|
||||||
pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Result<Redirect, Ructe> {
|
pub fn create(
|
||||||
if !Instance::get_local(&*conn)
|
form: LenientForm<NewUserForm>,
|
||||||
|
rockets: PlumeRocket,
|
||||||
|
) -> Result<Flash<Redirect>, Ructe> {
|
||||||
|
let conn = &*rockets.conn;
|
||||||
|
if !Instance::get_local()
|
||||||
.map(|i| i.open_registrations)
|
.map(|i| i.open_registrations)
|
||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
{
|
{
|
||||||
return Ok(Redirect::to(uri!(new))); // Actually, it is an error
|
return Ok(Flash::error(
|
||||||
|
Redirect::to(uri!(new)),
|
||||||
|
i18n!(
|
||||||
|
rockets.intl.catalog,
|
||||||
|
"Registrations are closed on this instance."
|
||||||
|
),
|
||||||
|
)); // Actually, it is an error
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut form = form.into_inner();
|
let mut form = form.into_inner();
|
||||||
@@ -471,7 +512,7 @@ pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Resul
|
|||||||
form.validate()
|
form.validate()
|
||||||
.and_then(|_| {
|
.and_then(|_| {
|
||||||
NewUser::new_local(
|
NewUser::new_local(
|
||||||
&*conn,
|
conn,
|
||||||
form.username.to_string(),
|
form.username.to_string(),
|
||||||
form.username.to_string(),
|
form.username.to_string(),
|
||||||
false,
|
false,
|
||||||
@@ -480,12 +521,18 @@ pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Resul
|
|||||||
User::hash_pass(&form.password).map_err(to_validation)?,
|
User::hash_pass(&form.password).map_err(to_validation)?,
|
||||||
)
|
)
|
||||||
.map_err(to_validation)?;
|
.map_err(to_validation)?;
|
||||||
Ok(Redirect::to(uri!(super::session::new: m = _)))
|
Ok(Flash::success(
|
||||||
|
Redirect::to(uri!(super::session::new: m = _)),
|
||||||
|
i18n!(
|
||||||
|
rockets.intl.catalog,
|
||||||
|
"Your account has been created. Now you just need to log in, before you can use it."
|
||||||
|
),
|
||||||
|
))
|
||||||
})
|
})
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
render!(users::new(
|
render!(users::new(
|
||||||
&(&*conn, &intl.catalog, None),
|
&rockets.to_context(),
|
||||||
Instance::get_local(&*conn)
|
Instance::get_local()
|
||||||
.map(|i| i.open_registrations)
|
.map(|i| i.open_registrations)
|
||||||
.unwrap_or(true),
|
.unwrap_or(true),
|
||||||
&form,
|
&form,
|
||||||
@@ -542,7 +589,7 @@ pub fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>>
|
|||||||
let author = User::find_by_fqn(&rockets, &name).ok()?;
|
let author = User::find_by_fqn(&rockets, &name).ok()?;
|
||||||
let feed = FeedBuilder::default()
|
let feed = FeedBuilder::default()
|
||||||
.title(author.display_name.clone())
|
.title(author.display_name.clone())
|
||||||
.id(Instance::get_local(conn)
|
.id(Instance::get_local()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.compute_box("@", &name, "atom.xml"))
|
.compute_box("@", &name, "atom.xml"))
|
||||||
.entries(
|
.entries(
|
||||||
|
|||||||
+36
-2
@@ -1,4 +1,4 @@
|
|||||||
use plume_models::{notifications::*, users::User, Connection};
|
use plume_models::{notifications::*, users::User, Connection, PlumeRocket};
|
||||||
|
|
||||||
use rocket::http::hyper::header::{ETag, EntityTag};
|
use rocket::http::hyper::header::{ETag, EntityTag};
|
||||||
use rocket::http::{Method, Status};
|
use rocket::http::{Method, Status};
|
||||||
@@ -13,7 +13,41 @@ pub use askama_escape::escape;
|
|||||||
|
|
||||||
pub static CACHE_NAME: &str = env!("CACHE_ID");
|
pub static CACHE_NAME: &str = env!("CACHE_ID");
|
||||||
|
|
||||||
pub type BaseContext<'a> = &'a (&'a Connection, &'a Catalog, Option<User>);
|
pub type BaseContext<'a> = &'a (
|
||||||
|
&'a Connection,
|
||||||
|
&'a Catalog,
|
||||||
|
Option<User>,
|
||||||
|
Option<(String, String)>,
|
||||||
|
);
|
||||||
|
|
||||||
|
pub trait IntoContext {
|
||||||
|
fn to_context(
|
||||||
|
&self,
|
||||||
|
) -> (
|
||||||
|
&Connection,
|
||||||
|
&Catalog,
|
||||||
|
Option<User>,
|
||||||
|
Option<(String, String)>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoContext for PlumeRocket {
|
||||||
|
fn to_context(
|
||||||
|
&self,
|
||||||
|
) -> (
|
||||||
|
&Connection,
|
||||||
|
&Catalog,
|
||||||
|
Option<User>,
|
||||||
|
Option<(String, String)>,
|
||||||
|
) {
|
||||||
|
(
|
||||||
|
&*self.conn,
|
||||||
|
&self.intl.catalog,
|
||||||
|
self.user.clone(),
|
||||||
|
self.flash_msg.clone(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Ructe(pub Vec<u8>);
|
pub struct Ructe(pub Vec<u8>);
|
||||||
|
|||||||
+160
-33
@@ -1,7 +1,70 @@
|
|||||||
// Heading
|
// Heading
|
||||||
|
main header.article {
|
||||||
|
overflow: hidden;
|
||||||
|
background: $white;
|
||||||
|
color: $black;
|
||||||
|
display: grid;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
|
||||||
|
&.illustrated {
|
||||||
|
min-height: 75vh;
|
||||||
|
color: $white;
|
||||||
|
|
||||||
|
a, a:visited {
|
||||||
|
color: $white;
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
transition: border-bottom-color 0.1s ease-in;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-bottom-color: $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div:not(.shadow) {
|
||||||
|
z-index: 3;
|
||||||
|
font-family: $lora;
|
||||||
|
font-size: 1.2em;
|
||||||
|
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
max-width: $article-width;
|
||||||
|
margin: 2em auto;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: end;
|
||||||
|
|
||||||
|
h1, .article-info {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.shadow {
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(180deg, transparent 20vh, black 80vh);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > img {
|
||||||
|
z-index: 1;
|
||||||
|
min-width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
background: $purple;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
main .article-info {
|
main .article-info {
|
||||||
max-width: 40rem;
|
margin: 0 auto 2em;
|
||||||
margin: 0 auto 3em;
|
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
@@ -12,8 +75,8 @@ main .article-info {
|
|||||||
|
|
||||||
// The article itself
|
// The article itself
|
||||||
main article {
|
main article {
|
||||||
max-width: 40rem;
|
max-width: $article-width;
|
||||||
margin: 2.5em auto;
|
margin: 1.5em auto;
|
||||||
font-family: $lora;
|
font-family: $lora;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
@@ -24,15 +87,20 @@ main article {
|
|||||||
|
|
||||||
img {
|
img {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 3em auto;
|
margin: 2em auto;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
background: $lightgray;
|
background: $gray;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border-radius: 5px;
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: 5px solid $gray;
|
||||||
|
margin: 1em auto;
|
||||||
|
padding: 0em 2em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,37 +114,42 @@ main .article-meta, main .article-meta button {
|
|||||||
main .article-meta {
|
main .article-meta {
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
margin: 0 20%;
|
margin: $margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .banner {
|
||||||
|
margin: 2em 0;
|
||||||
|
& > * {
|
||||||
|
margin: $margin;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> p {
|
> p {
|
||||||
margin: 2em 20%;
|
margin: 2em $horizontal-margin;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tags
|
// Article Tags
|
||||||
.tags {
|
.tags {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
display: inline-block;
|
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
margin-bottom: 2em;
|
max-width: none;
|
||||||
|
flex: 20;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: $lightgray;
|
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
margin: 0px 10px 10px 0px;
|
margin: 0px 10px 10px 0px;
|
||||||
border-radius: 3px;
|
|
||||||
transition: all 0.2s ease-in;
|
transition: all 0.2s ease-in;
|
||||||
|
border: 1px solid $purple;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
color: $black;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: mix($black, $lightgray, 10%);
|
background: transparentize($purple, 0.9);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,6 +184,7 @@ main .article-meta {
|
|||||||
color: $black;
|
color: $black;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
svg.feather {
|
svg.feather {
|
||||||
transition: background 0.1s ease-in;
|
transition: background 0.1s ease-in;
|
||||||
@@ -185,11 +259,7 @@ main .article-meta {
|
|||||||
|
|
||||||
// Comments
|
// Comments
|
||||||
.comments {
|
.comments {
|
||||||
margin: 0;
|
margin: 0 $horizontal-margin;
|
||||||
> * {
|
|
||||||
margin-left: 20%;
|
|
||||||
margin-right: 20%;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
color: $purple;
|
color: $purple;
|
||||||
@@ -204,17 +274,17 @@ main .article-meta {
|
|||||||
// New comment form
|
// New comment form
|
||||||
> form input[type="submit"] {
|
> form input[type="submit"] {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response/delete buttons
|
// Respond & delete comment buttons
|
||||||
a.button, form.inline, form.inline input {
|
a.button, form.inline, form.inline input {
|
||||||
display: inline-block;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: none;
|
background: none;
|
||||||
color: $black;
|
color: $black;
|
||||||
border: none;
|
|
||||||
margin-right: 2em;
|
margin-right: 2em;
|
||||||
font-family: $route159;
|
font-family: $route159;
|
||||||
|
font-weight: normal;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
color: $purple;
|
color: $purple;
|
||||||
@@ -224,18 +294,40 @@ main .article-meta {
|
|||||||
&:hover { color: $purple; }
|
&:hover { color: $purple; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
|
||||||
display: grid;
|
|
||||||
margin: 0 0 -5em;
|
|
||||||
padding: 0 20%;
|
|
||||||
background: $lightgray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment {
|
.comment {
|
||||||
padding: 2em;
|
margin: 1em 0;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
background: $gray;
|
||||||
|
margin-top: 2.5em;
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: block;
|
||||||
|
content: ' ';
|
||||||
|
border: 1em solid $gray;
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-right-color: transparent;
|
||||||
|
position: relative;
|
||||||
|
top: -2.4em;
|
||||||
|
left: -1em;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dt-published a {
|
||||||
|
color: transparentize($black, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
.author {
|
.author {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -256,6 +348,10 @@ main .article-meta {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > .comment {
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
padding: 1.25em 0;
|
padding: 1.25em 0;
|
||||||
font-family: $lora;
|
font-family: $lora;
|
||||||
@@ -274,6 +370,7 @@ main .article-meta {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
resize: auto;
|
||||||
button {
|
button {
|
||||||
flex: 0 0 10em;
|
flex: 0 0 10em;
|
||||||
font-size: 1.25em;
|
font-size: 1.25em;
|
||||||
@@ -303,7 +400,7 @@ main .article-meta {
|
|||||||
bottom: 20vh;
|
bottom: 20vh;
|
||||||
left: 20vw;
|
left: 20vw;
|
||||||
right: 20vw;
|
right: 20vw;
|
||||||
background: $lightgray;
|
background: $gray;
|
||||||
border: 1px solid $purple;
|
border: 1px solid $purple;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
@@ -312,6 +409,7 @@ main .article-meta {
|
|||||||
|
|
||||||
.popup:not(.show), .popup-bg:not(.show) {
|
.popup:not(.show), .popup-bg:not(.show) {
|
||||||
display: none;
|
display: none;
|
||||||
|
appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-bg {
|
.popup-bg {
|
||||||
@@ -328,10 +426,16 @@ main .article-meta {
|
|||||||
.cw-container {
|
.cw-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cw-text {
|
.cw-text {
|
||||||
display: none;
|
display: none;
|
||||||
|
appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="checkbox"].cw-checkbox {
|
input[type="checkbox"].cw-checkbox {
|
||||||
@@ -355,3 +459,26 @@ input:checked ~ .cw-container > .cw-text {
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bottom action bar
|
||||||
|
|
||||||
|
.bottom-bar {
|
||||||
|
z-index: 10;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: $gray;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div:nth-child(2) {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
margin: auto $horizontal-margin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+36
-32
@@ -10,11 +10,11 @@ input, textarea, select {
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
-webkit-appearance: textarea;
|
||||||
|
|
||||||
background: $form-input-background;
|
background: $form-input-background;
|
||||||
color: $black;
|
color: $black;
|
||||||
border: none;
|
border: solid $black thin;
|
||||||
border: solid $lightgray thin;
|
|
||||||
|
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@@ -23,11 +23,14 @@ input, textarea, select {
|
|||||||
border-color: $purple;
|
border-color: $purple;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
form input[type="submit"] { margin: 2em auto; }
|
form input[type="submit"] {
|
||||||
|
margin: 1.5em auto;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
|
overflow-y: scroll;
|
||||||
font-family: $lora;
|
font-family: $lora;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
@@ -38,6 +41,7 @@ input[type="checkbox"] {
|
|||||||
margin: initial;
|
margin: initial;
|
||||||
min-width: initial;
|
min-width: initial;
|
||||||
width: initial;
|
width: initial;
|
||||||
|
-webkit-appearance: checkbox;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Inline forms (containing only CSRF token and a <submit>, for protected links) **/
|
/** Inline forms (containing only CSRF token and a <submit>, for protected links) **/
|
||||||
@@ -50,11 +54,10 @@ form.inline {
|
|||||||
|
|
||||||
input[type="submit"] {
|
input[type="submit"] {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: $purple;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
width: auto;
|
width: auto;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
|
||||||
&:not(.button) {
|
&:not(.button) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -62,41 +65,51 @@ form.inline {
|
|||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: $purple;
|
color: $purple;
|
||||||
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button, input[type="submit"], button {
|
.button, .button:visited, input[type="submit"], input[type="submit"].button {
|
||||||
transition: all 0.1s ease-in;
|
transition: all 0.1s ease-in;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
|
||||||
border-radius: 0.5em;
|
|
||||||
margin: 0.5em auto;
|
margin: 0.5em auto;
|
||||||
padding: 0.75em 1em;
|
padding: 0.75em 1em;
|
||||||
|
|
||||||
background: transparent;
|
background: $purple;
|
||||||
color: $purple;
|
color: $white;
|
||||||
border: 1px solid $purple;
|
font-weight: bold;
|
||||||
|
border: none;
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: transparentize($purple, 0.6);
|
background: transparentize($purple, 0.1);
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.destructive {
|
&.destructive {
|
||||||
color: $red;
|
background: $red;
|
||||||
border-color: $red;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: transparentize($red, 0.6);
|
background: transparentize($red, 0.1);
|
||||||
color: $white;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
background: $gray;
|
||||||
|
color: $black;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: transparentize($black, 0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input[type="submit"] {
|
||||||
|
display: block;
|
||||||
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
input[type="submit"] { display: block; }
|
|
||||||
|
|
||||||
// Writing page
|
// Writing page
|
||||||
form.new-post {
|
form.new-post {
|
||||||
@@ -114,20 +127,10 @@ form.new-post {
|
|||||||
}
|
}
|
||||||
textarea {
|
textarea {
|
||||||
min-height: 20em;
|
min-height: 20em;
|
||||||
overflow-y: hidden;
|
overflow-y: scroll;
|
||||||
resize: none;
|
resize: vertical;
|
||||||
|
-webkit-appearance: textarea;
|
||||||
}
|
}
|
||||||
input[type="submit"] {
|
|
||||||
background: $lightgray;
|
|
||||||
color: $black;
|
|
||||||
border: none;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
|
|
||||||
font-family: $playfair;
|
|
||||||
font-size: 1.5em;
|
|
||||||
}
|
|
||||||
input[type="submit"]:hover { background: $lightgray; }
|
|
||||||
-webkit-appearance: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.button + .button {
|
.button + .button {
|
||||||
@@ -137,6 +140,7 @@ form.new-post {
|
|||||||
.split {
|
.split {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
@@ -157,4 +161,4 @@ header.center {
|
|||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+88
-36
@@ -4,11 +4,12 @@ html, body {
|
|||||||
background: $background;
|
background: $background;
|
||||||
color: $black;
|
color: $black;
|
||||||
font-family: $route159;
|
font-family: $route159;
|
||||||
::selection {
|
|
||||||
background: $lightpurple;
|
::selection {
|
||||||
}
|
background: transparentize($purple, 0.7);
|
||||||
|
}
|
||||||
::-moz-selection {
|
::-moz-selection {
|
||||||
background: $lightpurple;
|
background: transparentize($purple, 0.7);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ a, a:visited {
|
|||||||
color: $purple;
|
color: $purple;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
a::selection {
|
a::selection {
|
||||||
color: $white;
|
color: $white;
|
||||||
}
|
}
|
||||||
a::-moz-selection {
|
a::-moz-selection {
|
||||||
@@ -37,12 +38,15 @@ small {
|
|||||||
padding: 5em;
|
padding: 5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spaced {
|
.right {
|
||||||
margin: 4rem 0;
|
text-align: right;
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cards.spaced {
|
.spaced {
|
||||||
margin: 1rem 0 5rem;
|
margin: 4rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner {
|
.banner {
|
||||||
@@ -54,25 +58,31 @@ small {
|
|||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
|
appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Main
|
/// Main
|
||||||
body > main > *, .h-feed > * {
|
body > main > *, .h-feed > * {
|
||||||
margin: 0 20%;
|
margin: 1em $horizontal-margin;
|
||||||
}
|
}
|
||||||
|
|
||||||
body > main > .h-entry, .h-feed {
|
body > main > .h-entry, .h-feed {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body > main {
|
||||||
|
min-height: 70vh;
|
||||||
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
font-family: $route159;
|
font-family: $route159;
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
|
|
||||||
&.article {
|
&.article {
|
||||||
max-width: 40rem;
|
max-width: $article-width;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
@@ -93,7 +103,6 @@ main {
|
|||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
|
|
||||||
&.article {
|
&.article {
|
||||||
margin: auto;
|
|
||||||
font-size: 1.25em;
|
font-size: 1.25em;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
@@ -125,8 +134,6 @@ main {
|
|||||||
p.error {
|
p.error {
|
||||||
color: $red;
|
color: $red;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
max-width: 40rem;
|
|
||||||
margin: 1em auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// User page
|
/// User page
|
||||||
@@ -148,7 +155,6 @@ p.error {
|
|||||||
background: $white;
|
background: $white;
|
||||||
color: $purple;
|
color: $purple;
|
||||||
border: 1px solid $purple;
|
border: 1px solid $purple;
|
||||||
border-radius: 1em;
|
|
||||||
|
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
@@ -163,6 +169,7 @@ p.error {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
padding: 0 5%;
|
padding: 0 5%;
|
||||||
|
margin: 1rem 0 5rem;
|
||||||
}
|
}
|
||||||
.card {
|
.card {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -212,24 +219,44 @@ p.error {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.list .card {
|
|
||||||
/* TODO */
|
.list > .card {
|
||||||
background: 0;
|
background: transparent;
|
||||||
margin: 2em 0;
|
margin: 2em 0;
|
||||||
padding: 0;
|
min-height: 3em;
|
||||||
min-height: 0;
|
|
||||||
|
padding: 1em;
|
||||||
|
transition: background 0.1s ease-in;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.compact {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Instance presentation
|
/// Instance presentation
|
||||||
.presentation > h2, .presentation > a {
|
.presentation {
|
||||||
text-align: center;
|
max-width: none;
|
||||||
}
|
|
||||||
.presentation > a {
|
& > h2, & > a {
|
||||||
font-size: 1.2em;
|
text-align: center;
|
||||||
margin: 1em;
|
}
|
||||||
|
|
||||||
|
& > a {
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats
|
// Stats
|
||||||
.stats {
|
.stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
@@ -281,13 +308,14 @@ p.error {
|
|||||||
|
|
||||||
.grow {
|
.grow {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
margin: 0 1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-icon {
|
.left-icon {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
background: $lightgray;
|
background: $gray;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
margin: 1em;
|
margin: 1em;
|
||||||
margin-right: 2em;
|
margin-right: 2em;
|
||||||
@@ -297,13 +325,37 @@ p.error {
|
|||||||
body > footer {
|
body > footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
justify-content: space-between;
|
justify-content: space-around;
|
||||||
background: $lightgray;
|
background: $purple;
|
||||||
padding: 0 20%;
|
color: $white;
|
||||||
margin-top: 5em;
|
margin-top: 5em;
|
||||||
|
|
||||||
* {
|
* {
|
||||||
margin: 5em 0;
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
transform: skew(-15deg);
|
||||||
|
background: $white;
|
||||||
|
border: none;
|
||||||
|
width: .2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, a:visited {
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-basis: 20%;
|
||||||
|
margin: 2em 0;
|
||||||
|
transition: all 0.1s ease-in;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
display: block;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,9 +437,9 @@ figure {
|
|||||||
|
|
||||||
/// Tabs
|
/// Tabs
|
||||||
.tabs {
|
.tabs {
|
||||||
border-bottom: 1px solid $lightgray;
|
border-bottom: 1px solid $gray;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
margin: auto 20% 2em;
|
margin: auto $horizontal-margin 2em;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
@@ -400,5 +452,5 @@ figure {
|
|||||||
color: $purple;
|
color: $purple;
|
||||||
border-bottom: 1px solid $purple;
|
border-bottom: 1px solid $purple;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-5
@@ -1,5 +1,5 @@
|
|||||||
header {
|
body > header {
|
||||||
background: $lightgray;
|
background: $gray;
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -10,6 +10,7 @@ header {
|
|||||||
nav#menu {
|
nav#menu {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: none;
|
display: none;
|
||||||
|
appearance: none;
|
||||||
transform: skewX(-15deg);
|
transform: skewX(-15deg);
|
||||||
left: -1em;
|
left: -1em;
|
||||||
padding: 1em 1em 1em 2em;
|
padding: 1em 1em 1em 2em;
|
||||||
@@ -26,7 +27,7 @@ header {
|
|||||||
height: 1.4em;
|
height: 1.4em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: $lightgray;
|
color: $gray;
|
||||||
font-size: 1.33em;
|
font-size: 1.33em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,15 +77,28 @@ header {
|
|||||||
|
|
||||||
.messages {
|
.messages {
|
||||||
& > * {
|
& > * {
|
||||||
padding: 1em 20%;
|
padding: 1em 20%;
|
||||||
|
margin: 0;
|
||||||
|
max-width: initial;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
p.error {
|
p.error {
|
||||||
color: $red;
|
color: darken($red, 20%);
|
||||||
background: lighten($red, 40%);
|
background: lighten($red, 40%);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
max-width: initial;
|
max-width: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.warning {
|
||||||
|
color: darken($yellow, 20%);
|
||||||
|
background: lighten($yellow, 40%);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.success {
|
||||||
|
color: darken($green, 20%);
|
||||||
|
background: lighten($green, 40%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only enable label animations on normal screens
|
// Only enable label animations on normal screens
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
// Colors
|
// Color Scheme
|
||||||
$background: #F4F4F4;
|
$gray: #F3F3F3;
|
||||||
$form-input-background: #FFFFFF;
|
|
||||||
$gray: #E3E3E3;
|
|
||||||
$lightgray: #ECECEC;
|
|
||||||
$black: #242424;
|
$black: #242424;
|
||||||
$white: #F4F4F4;
|
$white: #F8F8F8;
|
||||||
$purple: #7765E3;
|
$purple: #7765E3;
|
||||||
$lightpurple: #c2bbee;
|
$lightpurple: #c2bbee;
|
||||||
$red: #E92F2F;
|
$red: #E92F2F;
|
||||||
|
$yellow: #ffe347;
|
||||||
|
$green: #23f0c7;
|
||||||
|
|
||||||
|
$background: $white;
|
||||||
|
$form-input-background: white;
|
||||||
|
|
||||||
|
// Dimensions
|
||||||
|
|
||||||
|
$article-width: 70ch;
|
||||||
|
$horizontal-margin: 20%;
|
||||||
|
$margin: 0 $horizontal-margin;
|
||||||
|
|
||||||
// Fonts
|
// Fonts
|
||||||
|
|
||||||
|
|||||||
+43
-12
@@ -32,20 +32,24 @@ html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
body > header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
nav#menu {
|
nav#menu {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
z-index: 21;
|
||||||
}
|
}
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
display: none;
|
display: none;
|
||||||
|
appearance: none;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
overflow-y: scroll;
|
||||||
|
z-index: 20;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
header:focus-within #content, #content.show {
|
body > header:focus-within #content, #content.show {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -103,18 +107,15 @@ html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body > main > * {
|
|
||||||
padding: 0 5%;
|
|
||||||
}
|
|
||||||
main .article-meta {
|
main .article-meta {
|
||||||
> * {
|
> *, .comments {
|
||||||
margin: 0 5%;
|
margin: 0 3%;
|
||||||
}
|
}
|
||||||
> p {
|
> p {
|
||||||
margin: 2em 5%;
|
margin: 1em 3%;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
.comments > * { margin: auto 5%; }
|
.comments > * { margin: auto 3%; }
|
||||||
.comments .comment { padding: 2em 0px; }
|
.comments .comment { padding: 2em 0px; }
|
||||||
}
|
}
|
||||||
main .article-info, main article, main h1.article, main h2.article {
|
main .article-info, main article, main h1.article, main h2.article {
|
||||||
@@ -127,7 +128,7 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
margin: auto 0px 2em;
|
margin: auto 0px 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats { flex-direction: column; }
|
.stats { flex-direction: column; }
|
||||||
@@ -135,11 +136,41 @@ html {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
body > footer * { margin: 1em auto; }
|
body > footer * {
|
||||||
|
margin: 1em auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.flex.wrap { flex-direction: column; }
|
.flex.wrap { flex-direction: column; }
|
||||||
|
|
||||||
.cards {
|
.cards, .list {
|
||||||
margin: 1rem 0 5rem;
|
margin: 1rem 0 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.split {
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
& > div {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main .article-meta .comments .comment {
|
||||||
|
header {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-7
@@ -1,4 +1,5 @@
|
|||||||
@use plume_models::CONFIG;
|
@use plume_models::CONFIG;
|
||||||
|
@use plume_models::instance::Instance;
|
||||||
@use template_utils::*;
|
@use template_utils::*;
|
||||||
@use routes::*;
|
@use routes::*;
|
||||||
@(ctx: BaseContext, title: String, head: Content, header: Content, content: Content)
|
@(ctx: BaseContext, title: String, head: Content, header: Content, content: Content)
|
||||||
@@ -69,17 +70,30 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
<div class="messages">
|
||||||
|
@if let Some(ref message) = ctx.3 {
|
||||||
|
<p class="flash-message @message.0">@message.1</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<main>
|
<main>
|
||||||
@:content()
|
@:content()
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
<span>@concat!("Plume ", env!("CARGO_PKG_VERSION"))</span>
|
<hr/>
|
||||||
<a href="@uri!(instance::about)">@i18n!(ctx.1, "About this instance")</a>
|
<div>
|
||||||
<a href="https://github.com/Plume-org/Plume">@i18n!(ctx.1, "Source code")</a>
|
<h3>@Instance::get_local().map(|i| i.name).unwrap_or_default()</h3>
|
||||||
<a href="https://riot.im/app/#/room/#plume:disroot.org">@i18n!(ctx.1, "Matrix room")</a>
|
<a href="@uri!(instance::about)">@i18n!(ctx.1, "About this instance")</a>
|
||||||
@if ctx.2.clone().map(|a| a.is_admin).unwrap_or(false) {
|
<a href="@uri!(instance::privacy)">@i18n!(ctx.1, "Privacy policy")</a>
|
||||||
<a href="@uri!(instance::admin)">@i18n!(ctx.1, "Administration")</a>
|
@if ctx.2.clone().map(|a| a.is_admin).unwrap_or(false) {
|
||||||
}
|
<a href="@uri!(instance::admin)">@i18n!(ctx.1, "Administration")</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>@concat!("Plume ", env!("CARGO_PKG_VERSION"))</h3>
|
||||||
|
<a href="https://docs.joinplu.me">@i18n!(ctx.1, "Documentation")</a>
|
||||||
|
<a href="https://github.com/Plume-org/Plume">@i18n!(ctx.1, "Source code")</a>
|
||||||
|
<a href="https://riot.im/app/#/room/#plume:disroot.org">@i18n!(ctx.1, "Matrix room")</a>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script src="@uri!(plume_static_files: file = "plume-front.js", _build_id = CACHE_NAME)"></script>
|
<script src="@uri!(plume_static_files: file = "plume-front.js", _build_id = CACHE_NAME)"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user