Compare commits

..

32 Commits

Author SHA1 Message Date
Marek Ľach f2273b984c Update main.scss 2019-06-01 11:19:37 +02:00
Marek Ľach 7be397b76e Update _forms.scss 2019-05-31 13:08:36 +02:00
Marek Ľach d82a7674ce Update main.scss 2019-05-31 13:07:59 +02:00
Marek Ľach 38723a4d20 Update _forms.scss 2019-05-31 10:09:25 +02:00
Marek Ľach 1fc3b3dd4e Update _forms.scss 2019-05-31 10:06:28 +02:00
Marek Ľach d2a684650a Update _article.scss 2019-05-31 09:58:47 +02:00
Marek Ľach 82659391f9 Update _forms.scss 2019-05-31 09:49:33 +02:00
Rob Watson bffce041d7 Confirm deletion of blog (#602) 2019-05-29 13:51:53 +01:00
Marek Ľach 037674ae13 Direct follow-up to #578 (#603) 2019-05-29 12:26:37 +01:00
Igor Galić 8c59c822b6 refactor code to use Shrinkwraprs and diesel-derive-newtype (#598)
* add shrinkwraprs and implement Id thru it

This also means we can automatically convert Id to String without using
.into()!

* cleanup with the help of clippy!

* cleanup with the help of cargo fmt!

* remove extra block

* Shrinkwrap Page, ContentLen and RemoteForm

* translations
2019-05-25 19:23:45 +01:00
Marek Ľach 59023e9602 Prevent comment form submit button distortion on iOS (#592)
Forgot about that one the last time :-)
2019-05-20 19:29:23 +01:00
Baptiste Gelez ad3a8b92d1 Theme update (#553)
* Start to update the theme

- Ligther colors
- No more border radius
- Buttons are now always colored
- Start to redesign the post page (according to the Figma mockups)

* Fix build script: it now recompiles everytime a scss file changed

* Make sure the article illustrations are not too big

* Make articles wider (70 characters)

* Better contrast between gray shades

* Various improvements

* Better mobile style

* New style for the footer

* Improve comment style

* Better responsiveness again

* Limit the size of the article cover

* Last details?

- Improve buttons on the media page
- Improve lists

* Pin the stdweb version that we use

It changed because I removed Cargo.lock to handle a merge conflict

I could have updated cargo web too, but it mean I should have re-built
the CI docker image and it was taking forever.

* Better contrast for links in the header of the article

* Add a basic privacy policy

* Remove "also"

* Fix a few issues

- Don't watch static/css in build.rs
- Another shade of white
- Remove useless margin rule for error messages
2019-05-18 13:09:51 +01:00
Marek Ľach c67f65e684 Fix occurrences of 'have been' to 'has been' (#578)
* Update comments.rs

* Update medias.rs

* Update posts.rs

* Update user.rs

* Update posts.rs

* Update user.rs
2019-05-14 11:54:16 +01:00
Baptiste Gelez d78b3a05ec Fix Crowdin upload (#576) 2019-05-12 20:14:59 +01:00
fdb-hiroshima 5579f64dd0 update dependancies (#574)
[Security] Bump ammonia from 2.0.0 to 2.1.1
Bump bcrypt from 0.2.2 to 0.4.0
Bump clap from 2.32.0 to 2.33.0
Bump colored from 1.7.0 to 1.8.0
Bump ctrlc from 3.1.1 to 3.1.2
Bump diesel from 1.4.1 to 1.4.2
Bump dotenv from 0.13.0 to 0.14.0
Bump hyper from 0.12.25 to 0.12.28
Bump openssl from 0.10.19 to 0.10.22
Bump proc-macro2 from 0.4.27 to 0.4.30
Bump reqwest from 0.9.11 to 0.9.16
Bump rpassword from 2.1.0 to 3.0.2
Bump serde_derive from 1.0.80 to 1.0.82
Bump serde_derive from 1.0.89 to 1.0.91
Bump serde from 1.0.89 to 1.0.91
Bump serde_qs from 0.4.5 to 0.4.6
Bump stdweb from 0.4.14 to 0.4.17
Bump syn from 0.11.11 to 0.15.27
Bump whatlang from 0.7.0 to 0.7.1
2019-05-12 10:41:47 +02:00
fdb-hiroshima 773fbfe7c8 cache local instance (#572)
* cache local instance

fix #564

* don't use local instance cache for plm

* use instance cache for plm, but initialize it

* cargo fmt
2019-05-10 21:59:34 +01:00
fdb-hiroshima 5b50f90d2b use openssl instead of sha256sum for build.rs (#568)
* use openssl instead of sha256sum for build.rs

* use a more portable flag for stat
2019-05-10 16:15:30 +02:00
fdb-hiroshima 90f6ef0380 upload artifact of wasm binary (#571)
fix #570
2019-05-10 15:38:43 +02:00
Marek Ľach 33619abdfb Fix certain improper rendering of forms (#560) 2019-05-06 19:18:53 +01:00
fdb-hiroshima c52aac012c make hashtags work in profile summary (#562)
* make hashtags work in profile summary

fix #541

* cargo fmt
2019-05-04 17:33:50 +02:00
fdb-hiroshima c9070930d2 Improve notification management (#561)
* Don't notify for comment when mentioned

fix #505

* Don't save notification for remote users

fix #472
2019-05-04 17:15:41 +02:00
fdb-hiroshima 918bda14ec rewrite circleci config (#558)
* rewrite circleci config with better syntax

and try using grcov for coverage

* attempt to use less memory and to rerun if memory lack

* actually remove use of kcov

* use the right var

* try looping

* run test serialized

* use feature where we should and make macro path independant

* cargo fmt and remove some '"'

* add zip to dependancies

* reduce number of used thread after first failure

* push translations to crowdin on commit to master

* don't cache result of coverage
2019-05-04 15:35:21 +02:00
fdb-hiroshima 33a0c7dcd3 reduce reqwest timeout to 5s (#557) 2019-04-30 23:30:13 +02:00
Baptiste Gelez 8f1ab3485e Add some feedback when performing some actions (#552)
* Add a way to display flash messages

* Make the flash messages look nice

* Add actual feedback messages

* cargo fmt

* Move flash messages to PlumeRocket

And add trait to convert PlumeRocket to BaseContext

* Remove useless lifetime
2019-04-30 11:04:25 +01:00
fdb-hiroshima 18ae6e26b3 remove the new index lock tantivy uses (#556) 2019-04-29 18:08:32 +01:00
fdb-hiroshima 49bb8cb0bc import migrations and don't require diesel_cli for admins (#555)
* import migrations via macro

* panic on database not to the latest migration

* add subcommand to plm

* create migration that run tantivy index creation

* remove diesel_cli from places it was

* use our migration system for tests

* create table __diesel_schema_migrations if needed
2019-04-29 16:30:20 +02:00
Baptiste Gelez ec57f1e687 Remove Canapi (#540)
* Remove Canapi

It added more complexity than it helped.

* Fail if there are many blog, but none was specified

* cargo fmt
2019-04-28 22:17:21 +01:00
Baptiste Gelez 787eb7f399 Federate user deletion (#551)
* Federate user deletion

- When someone deletes their account
- When a local user is banned

Fixes #509

* cargo fmt
2019-04-28 18:01:41 +01:00
Marek Ľach 85aa0883c8 Update follow_remote.rs.html grammar (#548)
* Update follow_remote.rs.html

* Update follow_remote.rs.html
2019-04-26 18:31:43 +01:00
Marek Ľach ff92ac0763 Make comments in styleshhets a bit clearer (#545)
* Update _variables.scss

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

Some files were not shown because too many files have changed in this diff Show More