Compare commits

...

43 Commits

Author SHA1 Message Date
Ana Gelez 39edca5edc Add a way to change block type 2019-08-25 18:45:02 +02:00
Ana Gelez bce806ac63 Prototype for a WYSIWYG editor
We use pulldown-cmark in plume-front too now, but instead of using the provided
HTML renderer, we use a custom DOM renderer, which let us use contenteditable only where we want,
and which will allow us to add event listeners to provide a good contextual edition experience.

Also removed the character counter, as the API limits are almost unreachable.
2019-08-08 16:33:14 +02:00
Ana Gelez 3669a0097d Update rocket_csrf and re-enable it
Thank you @fdb-hiroshima for the fix!
2019-08-04 12:37:38 +02:00
Ana Gelez cc998e7c61 Rewrite article publication with the REST API
- Add a default App and ApiToken for each user, that is used by the front-end
- Add an API route to update an article (CSRF had to be disabled because of a bug in rocket_csrf)
- Use AJAX to publish and edit articles in the new editor, instead of weird hacks with HTML forms
2019-08-03 23:04:25 +02:00
Ana Gelez 4142e73018 Add a sidebar for the editor
- The layout now uses CSS grids
- We try to generate as much HTML as possible on the server, instead of using the DOM
- Placeholders are in pure CSS now!

You can't publish articles anymore, but it looks nice!!
2019-08-02 23:10:05 +02:00
Ana Gelez 5d03331f0c Sticky toolbar 2019-08-02 16:53:28 +02:00
Ana Gelez 3198f30515 Editor: Make it clearer that editable areas are editable
- Add a border on the left when hovering
- Make full sentences to explain where you can write
2019-08-02 15:36:54 +02:00
fdb-hiroshima 4f7c20fc26 Fix some federation issue (#573)
* send scheme as part of webfinger remote follow template

fix tsileo/microblog.pub#49

* bump webfinger to 0.4.1

* cargo fmt

* revert translations

* Use group: prefix for blog webfinger queries
2019-07-31 11:38:49 +02:00
Mostafa Ahangarha 54c6d21fc5 WIP - initial RTL support #575 (#577)
* initial RTL support

* fix dir in template-util

* fix dir in blogs new

* fix dir in blogs details

* fix dir in posts new

* fix dir in posts details

* fix dir in posts remote

* Fix dir in partials

some parts should be recheck carefully in real usage
2019-07-11 09:34:27 +02:00
Wilfried OLLIVIER c031804464 Add clap to plume in order to print help and version (#631)
* Add clap in order to print help and version

Related to https://github.com/Plume-org/Plume/issues/594

* Update after_help text message
2019-07-03 19:36:42 +01:00
fdb-hiroshima 5289fe872a don't ignore dotenv errors (#630) 2019-07-03 09:30:44 +02:00
Baptiste Gelez 7ea4acc289 Use futures and tokio to send activities (#620)
* Use futures and tokio to send activities

* Reset .po files

* Cargo fmt + small enhancements

- better error messages
- removed a TODO

* Log all messages at the same time when federation succeeds
2019-06-27 17:46:05 +01:00
Baptiste Gelez 6dbc043a7e Update some documents (#616)
* Redirect to the code of conduct in the docs

To centralize it, and because the version in the docs is more up-to-date

* Remove obsolete issue template

* Rework README
2019-06-15 21:42:24 +01:00
KemoNine fb1668ede7 Update dependencies which depended on nix -- fixes arm32 builds (#615) 2019-06-14 09:35:09 +02:00
Igor Galić 3d27e283ad add enum containing all successful route returns (#614)
* add enum containing all successful route returns

This enum derives `Responder`, so it can be used as route result.
We also implement `From`, so it can be converted

* disable clippy warning about this enum

There's no use trying to chase this warning.
The next warning will be about `Redirect`, which itself is 360 byte,
according to @fdb-hiroshima (thanks for the research!)

So, if anything, we can try to get Rocket to work on that!

* refactor routes/posts to only use one level of Result

* admin settings error is not an ErrorPage, so dont use Result<>

* refactor: use early return to remove indent of main logic

This diff is absolutely atrocious.
but the resulting readability is worth it

* refactor routes/post for early returns & RespondOrRedirect

* refactor routes/session for early returns & use RespondOrRedirect

* refactor routes/user -- add another field to enum

* refactor routes/blogs for early returns & RespondOrRedirect

* refactor routes/blogs for early returns & RespondOrRedirect

This is a final refactor of `pub fn update()` for readability.
We're removing at least one indentation level.
2019-06-14 09:33:30 +02:00
Rob Watson 4b205fa995 Store password reset requests in database (#610)
* Store password reset requests in database

Signed-off-by: Rob Watson <rfwatson@users.noreply.github.com>

* Refactor password reset request expiry handling

* Integrate sqlite

* Fix formatting
2019-06-04 19:55:17 +01:00
KemoNine e7126ae335 Fix arm builds (#612) 2019-06-04 19:18:36 +01:00
Marek Ľach b2312d7f1b Update textarea overflow to scroll (#609)
* Update textarea overflow to scroll

* Update _forms.scss

* Update _forms.scss
2019-06-02 21:35:09 +01: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
143 changed files with 7667 additions and 3212 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"]
+2 -46
View File
@@ -1,46 +1,2 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, sexual identity and orientation, etc.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at baptiste@gelez.xyz. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
You can read our Code of Conduct [here](https://docs.joinplu.me/organization/code-of-conduct).
By contributing to this repository, you agree to be bound by this Code of Conduct.
Generated
+851 -566
View File
File diff suppressed because it is too large Load Diff
+11 -10
View File
@@ -8,9 +8,9 @@ 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"
clap = "2.33"
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,15 +22,16 @@ 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"
webfinger = "0.4.1"
[[bin]]
name = "plume"
@@ -42,7 +43,7 @@ version = "0.4"
[dependencies.ctrlc]
features = ["termination"]
version = "3.1.1"
version = "3.1.2"
[dependencies.diesel]
features = ["r2d2", "chrono"]
@@ -63,11 +64,11 @@ path = "plume-common"
path = "plume-models"
[dependencies.rocket_csrf]
git = "https://github.com/fdb-hiroshima/rocket_csrf"
rev = "4a72ea2ec716cb0b26188fb00bccf2ef7d1e031c"
git = "https://github.com/Plume-org/rocket_csrf"
rev = "89ecb380266234f858c651354216bf5bf3cc09b2"
[build-dependencies]
ructe = "0.5.6"
ructe = "0.6.2"
rsass = "0.9"
[features]
@@ -78,4 +79,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"]
-11
View File
@@ -1,11 +0,0 @@
<!--
If your issue is not purely technical, it should be debated on Loomio first
https://framavox.org/g/WK40YHMA/plume
-->
Plume version (and/or commit):
+35 -9
View File
@@ -3,27 +3,53 @@
Plume
</h1>
<p align="center">
<a href="https://travis-ci.org/Plume-org/Plume"><img src="https://travis-ci.org/Plume-org/Plume.svg?branch=master" alt="Build status"></a>
<a href="hhttps://codecov.io/gh/Plume-org/Plume"><img src="https://codecov.io/gh/Plume-org/Plume/branch/master/graph/badge.svg" alt="Code coverage"></a>
<a href="https://github.com/Plume-org/Plume/"><img alt="CircleCI" src="https://img.shields.io/circleci/build/gh/Plume-org/Plume.svg"></a>
<a href="https://codecov.io/gh/Plume-org/Plume"><img src="https://codecov.io/gh/Plume-org/Plume/branch/master/graph/badge.svg" alt="Code coverage"></a>
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/plume"><img src="https://d322cqt584bo4o.cloudfront.net/plume/localized.svg"></a>
<a href="https://hub.docker.com/r/plumeorg/plume"><img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/plumeorg/plume.svg"></a>
<a href="https://liberapay.com/Plume"><img alt="Liberapay patrons" src="https://img.shields.io/liberapay/patrons/Plume.svg"></a>
</p>
<p align="center">
<a href="https://joinplu.me/">Website</a>
<a href="https://docs.joinplu.me/">Documentation</a>
<a href="https://contribute.joinplu.me/">Contribute</a>
<a href="https://docs.joinplu.me/contribute">Contribute</a>
<a href="https://joinplu.me/#instances">Instances list</a>
</p>
Plume is a federated blogging engine, based on ActivityPub. It uses the Rocket framework, and Diesel to interact with the database.
Plume is a **federated blogging engine**, based on *ActivityPub*. It is written in *Rust*, with the *Rocket* framework, and *Diesel* to interact with the database.
The front-end uses *Ructe* templates, *WASM* and *SCSS*.
It is not yet ready for production use, but we have all the basic features (account management, blogs, articles, comments, etc) and a basic federation.
## Features
Feel free to join our Matrix room: `#plume:disroot.org` to discuss about the project!
A lot of features are still missing, but what is already here should be quite stable. Current and planned features include:
We also have a [Loomio group](https://framavox.org/g/WK40YHMA/plume) to debate and vote and features. Feel free to join it to take part in Plume's decision process.
- **A blog-centric approach**: you can create as much blogs as you want with your account, to keep your different publications separated.
- **Media management**: you can upload pictures to illustrate your articles, but also audio files if you host a podcast, and manage them all from Plume.
- **Federation**: Plume is part of a network of interconnected websites called the Fediverse. Each of these websites (often called *instances*) have their own
rules and thematics, but they can all communicate with each other.
- **Collaborative writing**: invite other people to your blogs, and write articles together.
For more informations on the different ways to contribute, check out our [contribution page](https://contribute.joinplu.me). We are welcoming any kind of contribution, and you can probably find a way to help.
## Get involved
As we want the various spaces related to the project (GitHub, Matrix, Loomio, etc) to be as safe as possible for everyone, we adopted [a code of conduct](https://github.com/Plume-org/Plume/blob/master/CODE_OF_CONDUCT.md). Please read it and make sure you accept it before contributing.
If you want to have regular news about the project, the best place is probably [our blog](https://fediverse.blog/~/PlumeDev), or our Matrix room: [`#plume:disroot.org`](https://riot.im/app/#/room/#plume:disroot.org).
If you want to contribute more, a good first step is to read [our contribution guides](https://docs.joinplu.me/contribute). We accept all kind of contribution:
- [Back-end or front-end development](https://docs.joinplu.me/contribute/development/)
- [Translations](https://docs.joinplu.me/contribute/translations/)
- [Documentation](https://docs.joinplu.me/contribute/documentation/)
- UI and/or UX design (we don't have a dedicated guide yet, but [we can talk](https://docs.joinplu.me/contribute/discussion/) to see how we can work together!)
- [Taking part in discussions](https://docs.joinplu.me/contribute/discussion/)
- [Financial support](https://docs.joinplu.me/contribute/donations/)
But this list is not exhaustive and if you want to contribute differently you are welcome too!
As we want the various spaces related to the project (GitHub, Matrix, Loomio, etc) to be as safe as possible for everyone, we adopted [a code of conduct](https://docs.joinplu.me/organization/code-of-conduct). Please read it and make sure you accept it before contributing.
## Starting your own instance
We provide various way to install Plume: from source, with pre-built binaries, with Docker or with YunoHost.
For detailed explanations, please refer to [the documentation](https://docs.joinplu.me/installation/).
+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 @@
DROP TABLE password_reset_requests;
@@ -0,0 +1,9 @@
CREATE TABLE password_reset_requests (
id SERIAL PRIMARY KEY,
email VARCHAR NOT NULL,
token VARCHAR NOT NULL,
expiration_date TIMESTAMP NOT NULL
);
CREATE INDEX password_reset_requests_token ON password_reset_requests (token);
CREATE UNIQUE INDEX password_reset_requests_email ON password_reset_requests (email);
@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DELETE FROM apps WHERE name = 'Plume web interface';
@@ -0,0 +1,35 @@
-- Your SQL goes here
--#!|conn: &Connection, path: &Path| {
--#! use plume_common::utils::random_hex;
--#!
--#! let client_id = random_hex();
--#! let client_secret = random_hex();
--#! let app = crate::apps::App::insert(
--#! &*conn,
--#! crate::apps::NewApp {
--#! name: "Plume web interface".into(),
--#! client_id,
--#! client_secret,
--#! redirect_uri: None,
--#! website: Some("https://joinplu.me".into()),
--#! },
--#! ).unwrap();
--#!
--#! for i in 0..=(crate::users::User::count_local(conn).unwrap() as i32 / 20) {
--#! if let Ok(page) = crate::users::User::get_local_page(conn, (i * 20, (i + 1) * 20)) {
--#! for user in page {
--#! crate::api_tokens::ApiToken::insert(
--#! conn,
--#! crate::api_tokens::NewApiToken {
--#! app_id: app.id,
--#! user_id: user.id,
--#! value: random_hex(),
--#! scopes: "read+write".into(),
--#! },
--#! ).unwrap();
--#! }
--#! }
--#! }
--#!
--#! 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(())
--#!}
@@ -0,0 +1 @@
DROP TABLE password_reset_requests;
@@ -0,0 +1,9 @@
CREATE TABLE password_reset_requests (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
email VARCHAR NOT NULL,
token VARCHAR NOT NULL,
expiration_date DATETIME NOT NULL
);
CREATE INDEX password_reset_requests_token ON password_reset_requests (token);
CREATE UNIQUE INDEX password_reset_requests_email ON password_reset_requests (email);
@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DELETE FROM apps WHERE name = 'Plume web interface';
@@ -0,0 +1,35 @@
-- Your SQL goes here
--#!|conn: &Connection, path: &Path| {
--#! use plume_common::utils::random_hex;
--#!
--#! let client_id = random_hex();
--#! let client_secret = random_hex();
--#! let app = crate::apps::App::insert(
--#! &*conn,
--#! crate::apps::NewApp {
--#! name: "Plume web interface".into(),
--#! client_id,
--#! client_secret,
--#! redirect_uri: None,
--#! website: Some("https://joinplu.me".into()),
--#! },
--#! ).unwrap();
--#!
--#! for i in 0..=(crate::users::User::count_local(conn).unwrap() as i32 / 20) {
--#! if let Ok(page) = crate::users::User::get_local_page(conn, (i * 20, (i + 1) * 20)) {
--#! for user in page {
--#! crate::api_tokens::ApiToken::insert(
--#! conn,
--#! crate::api_tokens::NewApiToken {
--#! app_id: app.id,
--#! user_id: user.id,
--#! value: random_hex(),
--#! scopes: "read+write".into(),
--#! },
--#! ).unwrap();
--#! }
--#! }
--#! }
--#!
--#! 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,
}
+22 -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,19 @@ 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>,
pub url: String,
}
+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"]
+16 -6
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,32 @@ 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();
match dotenv::dotenv() {
Ok(path) => println!("Configuration read from {}", path.display()),
Err(ref e) if e.not_found() => eprintln!("no .env was found"),
e => e.map(|_| ()).unwrap(),
}
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,
)
+4 -2
View File
@@ -11,13 +11,15 @@ 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"
tokio = "0.1.21"
[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,
+30 -32
View File
@@ -1,6 +1,6 @@
use activitypub::{Activity, Link, Object};
use array_tool::vec::Uniq;
use reqwest::Client;
use reqwest::r#async::ClientBuilder;
use rocket::{
http::Status,
request::{FromRequest, Request},
@@ -8,6 +8,7 @@ use rocket::{
Outcome,
};
use serde_json;
use tokio::prelude::*;
use self::sign::Signable;
@@ -129,47 +130,44 @@ where
.sign(sender)
.expect("activity_pub::broadcast: signature error");
let mut rt = tokio::runtime::current_thread::Runtime::new()
.expect("Error while initializing tokio runtime for federation");
let client = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(5))
.build()
.expect("Can't build client");
for inbox in boxes {
// TODO: run it in Sidekiq or something like that
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();
match res {
Ok(mut r) => {
println!("Successfully sent activity to inbox ({})", inbox);
if let Ok(response) = r.text() {
println!("Response: \"{:?}\"\n\n", response)
} else {
println!("Error while reading response")
}
}
Err(e) => println!("Error while sending to inbox ({:?})", e),
}
rt.spawn(
client
.post(&inbox)
.headers(headers.clone())
.header(
"Signature",
request::signature(sender, &headers)
.expect("activity_pub::broadcast: request signature error"),
)
.body(body)
.send()
.and_then(|r| r.into_body().concat2())
.map(move |response| {
println!("Successfully sent activity to inbox ({})", inbox);
println!("Response: \"{:?}\"\n", response)
})
.map_err(|e| println!("Error while sending to inbox ({:?})", e)),
);
}
rt.run().unwrap();
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Shrinkwrap, Clone, Serialize, Deserialize)]
pub struct Id(String);
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())
}
}
+3
View File
@@ -15,9 +15,12 @@ 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;
extern crate tokio;
pub mod activity_pub;
pub mod utils;
+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")
);
}
+9 -2
View File
@@ -4,8 +4,15 @@ 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"
plume-api = { path = "../plume-api" }
serde_json = "1.0"
[dependencies.pulldown-cmark]
default-features = false
version = "0.5"
+417 -285
View File
@@ -1,32 +1,212 @@
use pulldown_cmark::{Event, Options, Parser, Tag};
use stdweb::{
unstable::{TryFrom, TryInto},
web::{event::*, html_element::*, *},
};
use CATALOG;
macro_rules! mv {
( $( $var:ident ),* => $exp:expr ) => {
{
$( let $var = $var.clone(); )*
$exp
fn from_md(md: &str) {
let md_parser = Parser::new_ext(md, Options::all());
md_parser.fold(
document().get_element_by_id("editor-main").unwrap(),
|last_elt, event| {
match event {
Event::Start(tag) => {
let new = match tag {
Tag::Paragraph => document().create_element("p").unwrap(),
Tag::Rule => document().create_element("hr").unwrap(),
Tag::Header(level) => {
document().create_element(&format!("h{}", level)).unwrap()
}
Tag::BlockQuote => document().create_element("blockquote").unwrap(),
Tag::CodeBlock(code) => {
let pre = document().create_element("pre").unwrap();
let code_elt = document().create_element("code").unwrap();
code_elt.append_child(&document().create_text_node(&code));
pre.append_child(&code_elt);
pre
}
Tag::List(None) => document().create_element("ul").unwrap(),
Tag::List(Some(_start_index)) => document().create_element("ol").unwrap(), // TODO: handle start_index
Tag::Item => document().create_element("li").unwrap(),
Tag::FootnoteDefinition(def) => {
let note = document().create_element("div").unwrap();
note.class_list().add("footnote");
note.append_child(&document().create_text_node(&def));
note
}
Tag::HtmlBlock => document().create_element("div").unwrap(),
Tag::Table(_alignements) => document().create_element("table").unwrap(), // TODO: handle alignements
Tag::TableHead => document().create_element("th").unwrap(),
Tag::TableRow => document().create_element("tr").unwrap(),
Tag::TableCell => document().create_element("td").unwrap(),
Tag::Emphasis => document().create_element("em").unwrap(),
Tag::Strong => document().create_element("strong").unwrap(),
Tag::Strikethrough => document().create_element("s").unwrap(),
Tag::Link(_link_type, url, text) => {
let url: &str = &url;
let text: &str = &text;
let link = document().create_element("a").unwrap();
js! {
@{&link}.href = @{url};
@{&link}.title = @{text};
};
link
}
Tag::Image(_link_type, url, text) => {
let url: &str = &url;
let text: &str = &text;
let img = document().create_element("img").unwrap();
js! {
@{&img}.src = @{url};
@{&img}.title = @{text};
@{&img}.alt = @{text};
};
img
}
};
last_elt.append_child(&new);
new
}
Event::End(_) => last_elt.parent_element().unwrap(),
Event::Text(text) => {
let node = document().create_text_node(&text);
last_elt.append_child(&node);
last_elt
}
Event::Code(code) => {
let elt = document().create_element("code").unwrap();
let content = document().create_text_node(&code);
elt.append_child(&content);
last_elt.append_child(&elt);
last_elt
}
Event::Html(html) => {
// TODO: sanitize it?
last_elt.set_attribute("innerHtml", &html);
last_elt
}
Event::InlineHtml(html) => {
let elt = document().create_element("span").unwrap();
elt.set_attribute("innerHtml", &html);
last_elt.append_child(&elt);
last_elt
}
Event::FootnoteReference(reference) => {
last_elt // TODO
}
Event::SoftBreak => {
last_elt.append_child(&document().create_element("br").unwrap());
last_elt
}
Event::HardBreak => {
last_elt // TODO
}
Event::TaskListMarker(done) => {
last_elt // TODO
}
}
},
);
MutationObserver::new(|muts, _obs| {
for m in muts {
console!(log, "mut!!");
}
})
.observe(
&document().get_element_by_id("editor-main").unwrap(),
MutationObserverInit {
child_list: true,
attributes: true,
character_data: false,
subtree: true,
attribute_old_value: true,
character_data_old_value: false,
attribute_filter: None,
},
);
}
fn to_md() -> String {
let root = document().get_element_by_id("editor-main").unwrap();
fold_children(&root).join("")
}
fn fold_children(elt: &Element) -> Vec<String> {
elt.child_nodes().iter().fold(vec![], |mut blocks, node| {
blocks.push(html_to_md(&node));
blocks
})
}
fn html_to_md(node: &Node) -> String {
console!(log, node);
if let Ok(elt) = Element::try_from(node.clone()) {
console!(log, elt.node_name().to_lowercase());
match elt.node_name().to_lowercase().as_ref() {
"hr" => "---".into(),
"h1" => format!("# {}\n\n", fold_children(&elt).join("")),
"h2" => format!("## {}\n\n", fold_children(&elt).join("")),
"h3" => format!("### {}\n\n", fold_children(&elt).join("")),
"h4" => format!("#### {}\n\n", fold_children(&elt).join("")),
"h5" => format!("##### {}\n\n", fold_children(&elt).join("")),
"h6" => format!("###### {}\n\n", fold_children(&elt).join("")),
"blockquote" => format!("> {}\n\n", fold_children(&elt).join("> ")),
"pre" => format!("```\n{}\n```\n\n", node.text_content().unwrap_or_default()),
"li" => match elt
.parent_element()
.unwrap()
.node_name()
.to_lowercase()
.as_ref()
{
"ol" => format!(
"{}. {}\n",
elt.parent_element()
.unwrap()
.child_nodes()
.iter()
.position(|n| Element::try_from(n).unwrap() == elt)
.unwrap_or_default(),
fold_children(&elt).join(""),
),
_ => format!("- {}\n", fold_children(&elt).join("")),
},
"em" => format!("_{}_", fold_children(&elt).join("")),
"strong" => format!("**{}**", fold_children(&elt).join("")),
"s" => format!("~~{}~~", fold_children(&elt).join("")),
"a" => format!(
"[{}]({})",
fold_children(&elt).join(""),
String::try_from(js! { return @{&elt}.href }).unwrap()
),
"img" => format!(
"![{}]({})",
String::try_from(js! { return @{&elt}.alt }).unwrap(),
String::try_from(js! { return @{&elt}.src }).unwrap()
),
other => {
console!(log, "Warning: unhandled element:", other);
String::new()
} // TODO: refs, tables, raw html
}
} else {
node.text_content().unwrap_or_default()
}
}
fn get_elt_value(id: &'static str) -> String {
let elt = document().get_element_by_id(id).unwrap();
let inp: Result<InputElement, _> = elt.clone().try_into();
let select: Result<SelectElement, _> = elt.clone().try_into();
let textarea: Result<TextAreaElement, _> = elt.try_into();
inp.map(|i| i.raw_value())
.unwrap_or_else(|_| textarea.unwrap().value())
}
fn set_value<S: AsRef<str>>(id: &'static str, val: S) {
let elt = document().get_element_by_id(id).unwrap();
let inp: Result<InputElement, _> = elt.clone().try_into();
let textarea: Result<TextAreaElement, _> = elt.try_into();
inp.map(|i| i.set_raw_value(val.as_ref()))
.unwrap_or_else(|_| textarea.unwrap().set_value(val.as_ref()))
let res = inp.map(|i| i.raw_value()).unwrap_or_else(|_| {
textarea
.map(|t| t.value())
.unwrap_or_else(|_| select.unwrap().value().unwrap_or_default())
});
res
}
fn no_return(evt: KeyDownEvent) {
@@ -63,33 +243,7 @@ impl From<stdweb::private::ConversionError> for EditorError {
}
}
fn init_widget(
parent: &Element,
tag: &'static str,
placeholder_text: String,
content: String,
disable_return: bool,
) -> Result<HtmlElement, EditorError> {
let widget = placeholder(make_editable(tag).try_into()?, &placeholder_text);
if !content.is_empty() {
widget.dataset().insert("edited", "true")?;
}
widget.append_child(&document().create_text_node(&content));
if disable_return {
widget.add_event_listener(no_return);
}
parent.append_child(&widget);
// We need to do that to make sure the placeholder is correctly rendered
widget.focus();
widget.blur();
filter_paste(&widget);
Ok(widget)
}
fn filter_paste(elt: &HtmlElement) {
fn filter_paste(elt: &Element) {
// Only insert text when pasting something
js! {
@{&elt}.addEventListener("paste", function (evt) {
@@ -127,64 +281,69 @@ pub fn init() -> Result<(), EditorError> {
fn init_editor() -> Result<(), EditorError> {
if let Some(ed) = document().get_element_by_id("plume-editor") {
document().body()?.set_attribute("id", "editor")?;
let aside = document().get_element_by_id("plume-editor-aside")?;
// Show the editor
js! { @{&ed}.style.display = "block"; };
js! {
@{&ed}.style.display = "grid";
@{&aside}.style.display = "block";
};
// And hide the HTML-only fallback
let old_ed = document().get_element_by_id("plume-fallback-editor")?;
let old_title = document().get_element_by_id("plume-editor-title")?;
js! {
@{&old_ed}.style.display = "none";
@{&old_title}.style.display = "none";
};
// Get content from the old editor (when editing an article for instance)
let title_val = get_elt_value("title");
let subtitle_val = get_elt_value("subtitle");
let content_val = get_elt_value("editor-content");
// And pre-fill the new editor with this values
let title = init_widget(&ed, "h1", i18n!(CATALOG, "Title"), title_val, true)?;
let subtitle = init_widget(
&ed,
"h2",
i18n!(CATALOG, "Subtitle, or summary"),
subtitle_val,
true,
)?;
let content = init_widget(
&ed,
"article",
i18n!(CATALOG, "Write your article here. Markdown is supported."),
content_val.clone(),
false,
)?;
js! { @{&content}.innerHTML = @{content_val}; };
let title = document().get_element_by_id("editor-title")?;
let subtitle = document().get_element_by_id("editor-subtitle")?;
let source = get_elt_value("editor-content");
// character counter
content.add_event_listener(mv!(content => move |_: KeyDownEvent| {
window().set_timeout(mv!(content => move || {
if let Some(e) = document().get_element_by_id("char-count") {
let count = chars_left("#plume-fallback-editor", &content).unwrap_or_default();
let text = i18n!(CATALOG, "Around {} characters left"; count);
HtmlElement::try_from(e).map(|e| {
js!{@{e}.innerText = @{text}};
}).ok();
setup_toolbar();
from_md(&source);
title.add_event_listener(no_return);
subtitle.add_event_listener(no_return);
filter_paste(&title);
filter_paste(&subtitle);
// TODO: filter_paste(&content);
document()
.get_element_by_id("publish")?
.add_event_listener(|_: ClickEvent| {
let publish_page = document().get_element_by_id("publish-page").unwrap();
let options_page = document().get_element_by_id("options-page").unwrap();
js! {
@{&options_page}.style.display = "none";
@{&publish_page}.style.display = "flex";
};
}), 0);
}));
});
document().get_element_by_id("publish")?.add_event_listener(
mv!(title, subtitle, content, old_ed => move |_: ClickEvent| {
let popup = document().get_element_by_id("publish-popup").or_else(||
init_popup(&title, &subtitle, &content, &old_ed).ok()
).unwrap();
let bg = document().get_element_by_id("popup-bg").or_else(||
init_popup_bg().ok()
).unwrap();
document()
.get_element_by_id("cancel-publish")?
.add_event_listener(|_: ClickEvent| {
let publish_page = document().get_element_by_id("publish-page").unwrap();
let options_page = document().get_element_by_id("options-page").unwrap();
js! {
@{&publish_page}.style.display = "none";
@{&options_page}.style.display = "flex";
};
});
popup.class_list().add("show").unwrap();
bg.class_list().add("show").unwrap();
}),
);
document()
.get_element_by_id("confirm-publish")?
.add_event_listener(|_: ClickEvent| {
save(false);
});
document()
.get_element_by_id("save-draft")?
.add_event_listener(|_: ClickEvent| {
save(true);
});
show_errors();
setup_close_button();
@@ -192,6 +351,176 @@ fn init_editor() -> Result<(), EditorError> {
Ok(())
}
fn select_style(style: &str) {
if let Some(select) = document()
.get_element_by_id("toolbar-style")
.and_then(|e| SelectElement::try_from(e).ok())
{
select.set_value(Some(style));
}
}
fn setup_toolbar() {
let toolbar = document().get_element_by_id("editor-toolbar").unwrap();
// List of styles (headings, quote, code, etc)
let style_select =
SelectElement::try_from(document().create_element("select").unwrap()).unwrap();
let options = vec![
("p", i18n!(CATALOG, "Paragraph")),
("ul", i18n!(CATALOG, "List")),
("ol", i18n!(CATALOG, "Ordered list")),
("h1", i18n!(CATALOG, "Heading 1")),
("h2", i18n!(CATALOG, "Heading 2")),
("h3", i18n!(CATALOG, "Heading 3")),
("h4", i18n!(CATALOG, "Heading 4")),
("h5", i18n!(CATALOG, "Heading 5")),
("h6", i18n!(CATALOG, "Heading 6")),
("blockquote", i18n!(CATALOG, "Quote")),
("pre", i18n!(CATALOG, "Code")),
];
for (tag, name) in options.clone() {
let opt = document().create_element("option").unwrap();
opt.set_attribute("value", tag);
opt.append_child(&document().create_text_node(&name));
style_select.append_child(&opt)
}
style_select.set_attribute("id", "toolbar-style");
let options_clone = options.clone();
document().add_event_listener(move |_: SelectionChangeEvent| {
let block = std::iter::successors(
window().get_selection().and_then(|s| s.anchor_node()),
|node| {
let t = node.node_name().to_lowercase();
if options_clone.iter().any(|(tag, _)| *tag == &t) {
None
} else {
node.parent_node()
}
},
)
.last();
if let Some(b) = block {
select_style(&b.node_name().to_lowercase());
}
});
style_select.add_event_listener(move |_: ChangeEvent| {
let block = std::iter::successors(
window().get_selection().and_then(|s| s.anchor_node()),
|node| {
let t = node.node_name().to_lowercase();
if options.iter().any(|(tag, _)| *tag == &t) {
None
} else {
node.parent_node()
}
},
)
.last();
if let Some(block) = block {
if let Some(select) = document()
.get_element_by_id("toolbar-style")
.and_then(|e| SelectElement::try_from(e).ok())
{
let tag = select.value();
let new = document().create_element(&tag.unwrap_or_default()).unwrap();
for ch in block.child_nodes() {
block.remove_child(&ch);
new.append_child(&ch);
}
block.parent_node().unwrap().replace_child(&new, &block);
}
}
});
// Bold
// Italics
// Insert an image
toolbar.append_child(&style_select);
}
fn save(is_draft: bool) {
let req = XmlHttpRequest::new();
if bool::try_from(js! { return window.editing }).unwrap_or(false) {
req.open(
"PUT",
&format!(
"/api/v1/posts/{}",
i32::try_from(js! { return window.post_id }).unwrap()
),
)
.unwrap();
} else {
req.open("POST", "/api/v1/posts").unwrap();
}
req.set_request_header("Accept", "application/json")
.unwrap();
req.set_request_header("Content-Type", "application/json")
.unwrap();
req.set_request_header(
"Authorization",
&format!(
"Bearer {}",
String::try_from(js! { return window.api_token }).unwrap()
),
)
.unwrap();
let req_clone = req.clone();
req.add_event_listener(move |_: ProgressLoadEvent| {
if let Ok(Some(res)) = req_clone.response_text() {
serde_json::from_str(&res)
.map(|res: plume_api::posts::PostData| {
let url = res.url;
js! {
window.location.href = @{url};
};
})
.map_err(|_| {
let json: serde_json::Value = serde_json::from_str(&res).unwrap();
window().alert(&format!(
"Error: {}",
json["error"].as_str().unwrap_or_default()
));
})
.ok();
}
});
console!(log, to_md());
let data = plume_api::posts::NewPostData {
title: HtmlElement::try_from(document().get_element_by_id("editor-title").unwrap())
.unwrap()
.inner_text(),
subtitle: document()
.get_element_by_id("editor-subtitle")
.map(|s| HtmlElement::try_from(s).unwrap().inner_text()),
source: to_md(),
author: String::new(), // it is ignored anyway (TODO: remove it ??)
blog_id: i32::try_from(js! { return window.blog_id }).ok(),
published: Some(!is_draft),
creation_date: None,
license: Some(get_elt_value("license")),
tags: Some(
get_elt_value("tags")
.split(',')
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty())
.collect(),
),
cover_id: get_elt_value("cover").parse().ok(),
};
let json = serde_json::to_string(&data).unwrap();
req.send_with_string(&json).unwrap();
}
fn setup_close_button() {
if let Some(button) = document().get_element_by_id("close-editor") {
button.add_event_listener(|_: ClickEvent| {
@@ -223,200 +552,3 @@ fn show_errors() {
.unwrap();
}
}
fn init_popup(
title: &HtmlElement,
subtitle: &HtmlElement,
content: &HtmlElement,
old_ed: &Element,
) -> Result<Element, EditorError> {
let popup = document().create_element("div")?;
popup.class_list().add("popup")?;
popup.set_attribute("id", "publish-popup")?;
let tags = get_elt_value("tags")
.split(',')
.map(str::trim)
.map(str::to_string)
.collect::<Vec<_>>();
let license = get_elt_value("license");
make_input(&i18n!(CATALOG, "Tags"), "popup-tags", &popup).set_raw_value(&tags.join(", "));
make_input(&i18n!(CATALOG, "License"), "popup-license", &popup).set_raw_value(&license);
let cover_label = document().create_element("label")?;
cover_label.append_child(&document().create_text_node(&i18n!(CATALOG, "Cover")));
cover_label.set_attribute("for", "cover")?;
let cover = document().get_element_by_id("cover")?;
cover.parent_element()?.remove_child(&cover).ok();
popup.append_child(&cover_label);
popup.append_child(&cover);
if let Some(draft_checkbox) = document().get_element_by_id("draft") {
let draft_label = document().create_element("label")?;
draft_label.set_attribute("for", "popup-draft")?;
let draft = document().create_element("input").unwrap();
js! {
@{&draft}.id = "popup-draft";
@{&draft}.name = "popup-draft";
@{&draft}.type = "checkbox";
@{&draft}.checked = @{&draft_checkbox}.checked;
};
draft_label.append_child(&draft);
draft_label.append_child(&document().create_text_node(&i18n!(CATALOG, "This is a draft")));
popup.append_child(&draft_label);
}
let button = document().create_element("input")?;
js! {
@{&button}.type = "submit";
@{&button}.value = @{i18n!(CATALOG, "Publish")};
};
button.append_child(&document().create_text_node(&i18n!(CATALOG, "Publish")));
button.add_event_listener(
mv!(title, subtitle, content, old_ed => move |_: ClickEvent| {
title.focus(); // Remove the placeholder before publishing
set_value("title", title.inner_text());
subtitle.focus();
set_value("subtitle", subtitle.inner_text());
content.focus();
set_value("editor-content", content.child_nodes().iter().fold(String::new(), |md, ch| {
let to_append = match ch.node_type() {
NodeType::Element => {
if js!{ return @{&ch}.tagName; } == "DIV" {
(js!{ return @{&ch}.innerHTML; }).try_into().unwrap_or_default()
} else {
(js!{ return @{&ch}.outerHTML; }).try_into().unwrap_or_default()
}
},
NodeType::Text => ch.node_value().unwrap_or_default(),
_ => unreachable!(),
};
format!("{}\n\n{}", md, to_append)
}));
set_value("tags", get_elt_value("popup-tags"));
if let Some(draft) = document().get_element_by_id("popup-draft") {
js!{
document.getElementById("draft").checked = @{draft}.checked;
};
}
let cover = document().get_element_by_id("cover").unwrap();
cover.parent_element().unwrap().remove_child(&cover).ok();
old_ed.append_child(&cover);
set_value("license", get_elt_value("popup-license"));
js! {
@{&old_ed}.submit();
};
}),
);
popup.append_child(&button);
document().body()?.append_child(&popup);
Ok(popup)
}
fn init_popup_bg() -> Result<Element, EditorError> {
let bg = document().create_element("div")?;
bg.class_list().add("popup-bg")?;
bg.set_attribute("id", "popup-bg")?;
document().body()?.append_child(&bg);
bg.add_event_listener(|_: ClickEvent| close_popup());
Ok(bg)
}
fn chars_left(selector: &str, content: &HtmlElement) -> Option<i32> {
match document().query_selector(selector) {
Ok(Some(form)) => HtmlElement::try_from(form).ok().and_then(|form| {
if let Some(len) = form
.get_attribute("content-size")
.and_then(|s| s.parse::<i32>().ok())
{
(js! {
let x = encodeURIComponent(@{content}.innerHTML)
.replace(/%20/g, "+")
.replace(/%0A/g, "%0D%0A")
.replace(new RegExp("[!'*()]", "g"), "XXX") // replace exceptions of encodeURIComponent with placeholder
.length + 2;
console.log(x);
return x;
})
.try_into()
.map(|c: i32| len - c)
.ok()
} else {
None
}
}),
_ => None,
}
}
fn close_popup() {
let hide = |x: Element| x.class_list().remove("show");
document().get_element_by_id("publish-popup").map(hide);
document().get_element_by_id("popup-bg").map(hide);
}
fn make_input(label_text: &str, name: &'static str, form: &Element) -> InputElement {
let label = document().create_element("label").unwrap();
label.append_child(&document().create_text_node(label_text));
label.set_attribute("for", name).unwrap();
let inp: InputElement = document()
.create_element("input")
.unwrap()
.try_into()
.unwrap();
inp.set_attribute("name", name).unwrap();
inp.set_attribute("id", name).unwrap();
form.append_child(&label);
form.append_child(&inp);
inp
}
fn make_editable(tag: &'static str) -> Element {
let elt = document()
.create_element(tag)
.expect("Couldn't create editable element");
elt.set_attribute("contenteditable", "true")
.expect("Couldn't make the element editable");
elt
}
fn placeholder(elt: HtmlElement, text: &str) -> HtmlElement {
elt.dataset().insert("placeholder", text).unwrap();
elt.dataset().insert("edited", "false").unwrap();
elt.add_event_listener(mv!(elt => move |_: FocusEvent| {
if elt.dataset().get("edited").unwrap().as_str() != "true" {
clear_children(&elt);
}
}));
elt.add_event_listener(mv!(elt => move |_: BlurEvent| {
if elt.dataset().get("edited").unwrap().as_str() != "true" {
clear_children(&elt);
let ph = document().create_element("span").expect("Couldn't create placeholder");
ph.class_list().add("placeholder").expect("Couldn't add class");
ph.append_child(&document().create_text_node(&elt.dataset().get("placeholder").unwrap_or_default()));
elt.append_child(&ph);
}
}));
elt.add_event_listener(mv!(elt => move |_: KeyUpEvent| {
elt.dataset().insert("edited", if elt.inner_text().trim_matches('\n').is_empty() {
"false"
} else {
"true"
}).expect("Couldn't update edition state");
}));
elt
}
fn clear_children(elt: &HtmlElement) {
for child in elt.child_nodes() {
elt.remove_child(&child).unwrap();
}
}
+14
View File
@@ -6,8 +6,10 @@ extern crate gettext;
extern crate gettext_macros;
#[macro_use]
extern crate lazy_static;
extern crate pulldown_cmark;
#[macro_use]
extern crate stdweb;
extern crate serde_json;
use stdweb::web::{event::*, *};
@@ -56,6 +58,18 @@ lazy_static! {
}
fn main() {
std::panic::set_hook(Box::new(|info: &std::panic::PanicInfo| {
let mut msg = info.to_string();
msg.push_str("\n\nStack:\n\n");
let e = error::Error::new("Panicked");
let stack = js! { return @{&e}.stack; }
.into_string()
.unwrap_or_default();
msg.push_str(&stack);
msg.push_str("\n\n");
console!(error, msg);
}));
menu();
search();
editor::init()
+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
}
}
}
+14 -9
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"
@@ -23,8 +23,10 @@ serde_derive = "1.0"
serde_json = "1.0"
tantivy = "0.9.1"
url = "1.7"
webfinger = "0.3.1"
whatlang = "0.7.0"
webfinger = "0.4.1"
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"]
+12
View File
@@ -44,6 +44,18 @@ impl ApiToken {
get!(api_tokens);
insert!(api_tokens, NewApiToken);
find_by!(api_tokens, find_by_value, value as &str);
find_by!(
api_tokens,
find_by_app_and_user,
app_id as i32,
user_id as i32
);
/// The token for Plume's front-end
pub fn web_token(conn: &crate::Connection, user_id: i32) -> Result<ApiToken> {
let app = crate::apps::App::find_by_name(conn, "Plume web interface")?;
Self::find_by_app_and_user(conn, app.id, user_id)
}
pub fn can(&self, what: &'static str, scope: &'static str) -> bool {
let full_scope = what.to_owned() + ":" + scope;
+3 -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,54 +25,9 @@ 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);
find_by!(apps, find_by_client_id, client_id as &str);
find_by!(apps, find_by_name, name as &str);
}
+21 -19
View File
@@ -145,7 +145,7 @@ impl Blog {
}
fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<Blog> {
resolve(acct.to_owned(), true)?
resolve_with_prefix(Prefix::Group, acct.to_owned(), true)?
.links
.into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
@@ -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,
+15 -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")))]
@@ -65,7 +64,9 @@ pub enum Error {
Signature,
Unauthorized,
Url,
Validation(String),
Webfinger,
Expired,
}
impl From<bcrypt::BcryptError> for Error {
@@ -154,8 +155,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 +304,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 +334,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,7 +367,9 @@ pub mod instance;
pub mod likes;
pub mod medias;
pub mod mentions;
pub mod migrations;
pub mod notifications;
pub mod password_reset_requests;
pub mod plume_rocket;
pub mod post_authors;
pub mod posts;
+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! {}
};
+164
View File
@@ -0,0 +1,164 @@
use chrono::{offset::Utc, Duration, NaiveDateTime};
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use schema::password_reset_requests;
use {Connection, Error, Result};
#[derive(Clone, Identifiable, Queryable)]
pub struct PasswordResetRequest {
pub id: i32,
pub email: String,
pub token: String,
pub expiration_date: NaiveDateTime,
}
#[derive(Insertable)]
#[table_name = "password_reset_requests"]
pub struct NewPasswordResetRequest {
pub email: String,
pub token: String,
pub expiration_date: NaiveDateTime,
}
const TOKEN_VALIDITY_HOURS: i64 = 2;
impl PasswordResetRequest {
pub fn insert(conn: &Connection, email: &str) -> Result<String> {
// first, delete other password reset tokens associated with this email:
let existing_requests =
password_reset_requests::table.filter(password_reset_requests::email.eq(email));
diesel::delete(existing_requests).execute(conn)?;
// now, generate a random token, set the expiry date,
// and insert it into the DB:
let token = plume_common::utils::random_hex();
let expiration_date = Utc::now()
.naive_utc()
.checked_add_signed(Duration::hours(TOKEN_VALIDITY_HOURS))
.expect("could not calculate expiration date");
let new_request = NewPasswordResetRequest {
email: email.to_owned(),
token: token.clone(),
expiration_date,
};
diesel::insert_into(password_reset_requests::table)
.values(new_request)
.execute(conn)
.map_err(Error::from)?;
Ok(token)
}
pub fn find_by_token(conn: &Connection, token: &str) -> Result<Self> {
let token = password_reset_requests::table
.filter(password_reset_requests::token.eq(token))
.first::<Self>(conn)
.map_err(Error::from)?;
if token.expiration_date < Utc::now().naive_utc() {
return Err(Error::Expired);
}
Ok(token)
}
pub fn find_and_delete_by_token(conn: &Connection, token: &str) -> Result<Self> {
let request = Self::find_by_token(&conn, &token)?;
let filter =
password_reset_requests::table.filter(password_reset_requests::id.eq(request.id));
diesel::delete(filter).execute(conn)?;
Ok(request)
}
}
#[cfg(test)]
mod tests {
use super::*;
use diesel::Connection;
use tests::db;
use users::tests as user_tests;
#[test]
fn test_insert_and_find_password_reset_request() {
let conn = db();
conn.test_transaction::<_, (), _>(|| {
user_tests::fill_database(&conn);
let admin_email = "admin@example.com";
let token = PasswordResetRequest::insert(&conn, admin_email)
.expect("couldn't insert new request");
let request = PasswordResetRequest::find_by_token(&conn, &token)
.expect("couldn't retrieve request");
assert!(&token.len() > &32);
assert_eq!(&request.email, &admin_email);
Ok(())
});
}
#[test]
fn test_insert_delete_previous_password_reset_request() {
let conn = db();
conn.test_transaction::<_, (), _>(|| {
user_tests::fill_database(&conn);
let admin_email = "admin@example.com";
PasswordResetRequest::insert(&conn, &admin_email).expect("couldn't insert new request");
PasswordResetRequest::insert(&conn, &admin_email)
.expect("couldn't insert second request");
let count = password_reset_requests::table.count().get_result(&*conn);
assert_eq!(Ok(1), count);
Ok(())
});
}
#[test]
fn test_find_password_reset_request_by_token_time() {
let conn = db();
conn.test_transaction::<_, (), _>(|| {
user_tests::fill_database(&conn);
let admin_email = "admin@example.com";
let token = "abcdef";
let now = Utc::now().naive_utc();
diesel::insert_into(password_reset_requests::table)
.values((
password_reset_requests::email.eq(&admin_email),
password_reset_requests::token.eq(&token),
password_reset_requests::expiration_date.eq(now),
))
.execute(&*conn)
.expect("could not insert request");
match PasswordResetRequest::find_by_token(&conn, &token) {
Err(Error::Expired) => (),
_ => panic!("Received unexpected result finding expired token"),
}
Ok(())
});
}
#[test]
fn test_find_and_delete_password_reset_request() {
let conn = db();
conn.test_transaction::<_, (), _>(|| {
user_tests::fill_database(&conn);
let admin_email = "admin@example.com";
let token = PasswordResetRequest::insert(&conn, &admin_email)
.expect("couldn't insert new request");
PasswordResetRequest::find_and_delete_by_token(&conn, &token)
.expect("couldn't find and delete request");
let count = password_reset_requests::table.count().get_result(&*conn);
assert_eq!(Ok(0), count);
Ok(())
});
}
}
+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()?,
},
)?;
+10
View File
@@ -141,6 +141,15 @@ table! {
}
}
table! {
password_reset_requests (id) {
id -> Int4,
email -> Varchar,
token -> Varchar,
expiration_date -> Timestamp,
}
}
table! {
post_authors (id) {
id -> Int4,
@@ -247,6 +256,7 @@ allow_tables_to_appear_in_same_query!(
medias,
mentions,
notifications,
password_reset_requests,
post_authors,
posts,
reshares,
+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)?;
+67 -19
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())
}
@@ -674,7 +707,7 @@ impl User {
mime_type: None,
href: None,
template: Some(format!(
"{}/remote_interact?{{uri}}",
"https://{}/remote_interact?{{uri}}",
self.get_instance(conn)?.public_domain
)),
},
@@ -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
}
+133 -9
View File
@@ -49,6 +49,15 @@ msgstr "لإنشاء مدونة جديدة، تحتاج إلى تسجيل الد
msgid "A blog with the same name already exists."
msgstr ""
# src/routes/session.rs:259
#, fuzzy
msgid "Your blog was successfully created!"
msgstr "تمت إعادة تعيين كلمتك السرية بنجاح."
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:172
msgid "You are not allowed to delete this blog."
msgstr "لا يسمح لك بحذف هذه المدونة."
@@ -65,10 +74,56 @@ msgstr "لا يمكنك استخدام هذه الوسائط كأيقونة لل
msgid "You can't use this media as a blog banner."
msgstr "لا يمكنك استخدام هذه الوسائط كشعار للمدونة."
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
#, fuzzy
msgid "Your comment have been posted."
msgstr "ليس لديك أية وسائط بعد."
#, fuzzy
msgid "Your comment have been deleted."
msgstr "ليس لديك أية وسائط بعد."
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr "يجب عليك تسجيل الدخول أولا للإعجاب بهذا المقال"
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this media."
msgstr "لا يسمح لك بحذف هذه المدونة."
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to use this media."
msgstr "لا يسمح لك بتعديل هذه المدونة."
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr "يجب عليك تسجيل الدخول أولا لعرض الإشعارات"
@@ -93,6 +148,34 @@ msgstr "منشور جديد"
msgid "Edit {0}"
msgstr "تعديل {0}"
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to publish on this blog."
msgstr "لا يسمح لك بتعديل هذه المدونة."
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
#, fuzzy
msgid "Your post have been saved."
msgstr "ليس لديك أية وسائط بعد."
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this article."
msgstr "لا يسمح لك بحذف هذه المدونة."
# src/routes/posts.rs:589
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/posts.rs:630
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -103,6 +186,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr ""
#, fuzzy
msgid "You are now connected."
msgstr "ليست لديك التصريحات اللازمة للقيام بذلك."
#, fuzzy
msgid "You are now logged off."
msgstr "ليست لديك التصريحات اللازمة للقيام بذلك."
# src/routes/session.rs:181
msgid "Password reset"
msgstr "إعادة تعيين كلمة المرور"
@@ -123,6 +214,14 @@ msgstr "عذراً، ولكن انتهت مدة صلاحية الرابط. حا
msgid "To access your dashboard, you need to be logged in"
msgstr "يجب عليك تسجيل الدخول أولاللنفاذ إلى لوح المراقبة"
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:174
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:244
msgid "To subscribe to someone, you need to be logged in"
msgstr ""
@@ -131,6 +230,27 @@ msgstr ""
msgid "To edit your profile, you need to be logged in"
msgstr ""
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
#, fuzzy
msgid "Your account have been deleted."
msgstr "ليس لديك أية وسائط بعد."
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr "Plume"
@@ -265,10 +385,10 @@ msgid "Follow {}"
msgstr "اتبع"
#, fuzzy
msgid "Login to follow"
msgid "Log in to follow"
msgstr "قم بتسجيل الدخول قصد مشاركته"
msgid "Enter your full username to follow"
msgid "Enter your full username handle to follow"
msgstr ""
msgid "Edit your account"
@@ -487,7 +607,8 @@ msgstr "اسم نطاق مثيل الخادم"
msgid "Posted by one of these authors"
msgstr ""
msgid "Authors"
#, fuzzy
msgid "Author(s)"
msgstr "المؤلفون"
# src/template_utils.rs:305
@@ -511,14 +632,17 @@ msgstr ""
msgid "Article license"
msgstr "رخصة المقال"
msgid "Search result for \"{0}\""
msgstr ""
msgid "Search result"
#, fuzzy
msgid "Search result(s) for \"{0}\""
msgstr "نتائج البحث"
msgid "No result for your query"
msgstr ""
#, fuzzy
msgid "Search result(s)"
msgstr "نتائج البحث"
#, fuzzy
msgid "No results for your query"
msgstr "الانتقال إلى معرضك"
msgid "No more results for your query"
msgstr ""
+125 -6
View File
@@ -48,6 +48,14 @@ msgstr ""
msgid "A blog with the same name already exists."
msgstr "Вече съществува блог със същото име."
# src/routes/blogs.rs:142
msgid "Your blog was successfully created!"
msgstr ""
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:172
msgid "You are not allowed to delete this blog."
msgstr ""
@@ -64,10 +72,56 @@ msgstr ""
msgid "You can't use this media as a blog banner."
msgstr ""
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
# src/routes/comments.rs:89
msgid "Your comment have been posted."
msgstr ""
# src/routes/comments.rs:163
msgid "Your comment have been deleted."
msgstr ""
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr ""
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
# src/routes/posts.rs:140
#, fuzzy
msgid "You are not allowed to delete this media."
msgstr "Вие не сте автор на този блог."
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
# src/routes/posts.rs:140
#, fuzzy
msgid "You are not allowed to use this media."
msgstr "Вие не сте автор на този блог."
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr ""
@@ -92,6 +146,34 @@ msgstr "Нова публикация"
msgid "Edit {0}"
msgstr ""
# src/routes/posts.rs:140
#, fuzzy
msgid "You are not allowed to publish on this blog."
msgstr "Вие не сте автор на този блог."
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
# src/routes/posts.rs:527
msgid "Your post have been saved."
msgstr ""
# src/routes/posts.rs:140
#, fuzzy
msgid "You are not allowed to delete this article."
msgstr "Вие не сте автор на този блог."
# src/routes/posts.rs:589
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/posts.rs:630
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -102,6 +184,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr ""
#, fuzzy
msgid "You are now connected."
msgstr "Не сте упълномощени."
#, fuzzy
msgid "You are now logged off."
msgstr "Не сте упълномощени."
# src/routes/session.rs:181
msgid "Password reset"
msgstr ""
@@ -122,6 +212,14 @@ msgstr ""
msgid "To access your dashboard, you need to be logged in"
msgstr ""
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:174
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:244
msgid "To subscribe to someone, you need to be logged in"
msgstr ""
@@ -130,6 +228,27 @@ msgstr ""
msgid "To edit your profile, you need to be logged in"
msgstr ""
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
# src/routes/user.rs:409
msgid "Your account have been deleted."
msgstr ""
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr "Pluma"
@@ -262,10 +381,10 @@ msgstr "Писти Pluma {0}"
msgid "Follow {}"
msgstr ""
msgid "Login to follow"
msgid "Log in to follow"
msgstr ""
msgid "Enter your full username to follow"
msgid "Enter your full username handle to follow"
msgstr ""
msgid "Edit your account"
@@ -486,7 +605,7 @@ msgstr ""
msgid "Posted by one of these authors"
msgstr ""
msgid "Authors"
msgid "Author(s)"
msgstr ""
# src/template_utils.rs:305
@@ -510,13 +629,13 @@ msgstr ""
msgid "Article license"
msgstr ""
msgid "Search result for \"{0}\""
msgid "Search result(s) for \"{0}\""
msgstr ""
msgid "Search result"
msgid "Search result(s)"
msgstr ""
msgid "No result for your query"
msgid "No results for your query"
msgstr ""
msgid "No more results for your query"
+125 -6
View File
@@ -48,6 +48,14 @@ msgstr ""
msgid "A blog with the same name already exists."
msgstr ""
# src/routes/blogs.rs:142
msgid "Your blog was successfully created!"
msgstr ""
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:172
msgid "You are not allowed to delete this blog."
msgstr ""
@@ -64,10 +72,54 @@ msgstr ""
msgid "You can't use this media as a blog banner."
msgstr ""
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
# src/routes/comments.rs:89
msgid "Your comment have been posted."
msgstr ""
# src/routes/comments.rs:163
msgid "Your comment have been deleted."
msgstr ""
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr ""
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
# src/routes/medias.rs:145
msgid "You are not allowed to delete this media."
msgstr ""
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
# src/routes/medias.rs:156
msgid "You are not allowed to use this media."
msgstr ""
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr ""
@@ -92,6 +144,32 @@ msgstr "Apunt nou"
msgid "Edit {0}"
msgstr ""
# src/routes/posts.rs:264
msgid "You are not allowed to publish on this blog."
msgstr ""
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
# src/routes/posts.rs:527
msgid "Your post have been saved."
msgstr ""
# src/routes/posts.rs:565
msgid "You are not allowed to delete this article."
msgstr ""
# src/routes/posts.rs:589
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/posts.rs:630
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -102,6 +180,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr ""
# src/routes/session.rs:108
msgid "You are now connected."
msgstr ""
# src/routes/session.rs:124
msgid "You are now logged off."
msgstr ""
# src/routes/session.rs:181
msgid "Password reset"
msgstr "Reinicialització de contrasenya"
@@ -122,6 +208,14 @@ msgstr ""
msgid "To access your dashboard, you need to be logged in"
msgstr ""
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:174
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:244
msgid "To subscribe to someone, you need to be logged in"
msgstr ""
@@ -130,6 +224,27 @@ msgstr ""
msgid "To edit your profile, you need to be logged in"
msgstr ""
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
# src/routes/user.rs:409
msgid "Your account have been deleted."
msgstr ""
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr ""
@@ -262,10 +377,10 @@ msgstr ""
msgid "Follow {}"
msgstr ""
msgid "Login to follow"
msgid "Log in to follow"
msgstr ""
msgid "Enter your full username to follow"
msgid "Enter your full username handle to follow"
msgstr ""
msgid "Edit your account"
@@ -484,7 +599,8 @@ msgstr ""
msgid "Posted by one of these authors"
msgstr ""
msgid "Authors"
#, fuzzy
msgid "Author(s)"
msgstr "Autors"
# src/template_utils.rs:305
@@ -508,13 +624,16 @@ msgstr "Publicat segons aquesta llicència"
msgid "Article license"
msgstr ""
msgid "Search result for \"{0}\""
#, fuzzy
msgid "Search result(s) for \"{0}\""
msgstr "Resultats de la cerca «{0}»"
msgid "Search result"
#, fuzzy
msgid "Search result(s)"
msgstr "Resultats de la cerca"
msgid "No result for your query"
#, fuzzy
msgid "No results for your query"
msgstr "La vostra consulta no ha produït cap resultat"
msgid "No more results for your query"
+132 -6
View File
@@ -48,6 +48,15 @@ msgstr "Pro vytvoření nového blogu musíte být přihlášeni"
msgid "A blog with the same name already exists."
msgstr "Blog s rovnakým názvem již existuje."
# src/routes/session.rs:259
#, fuzzy
msgid "Your blog was successfully created!"
msgstr "Vaše heslo bylo úspěšně obnoveno."
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:172
msgid "You are not allowed to delete this blog."
msgstr "Nemáte oprávnění zmazat tento blog."
@@ -64,10 +73,56 @@ msgstr "Toto médium nelze použít jako ikonu blogu."
msgid "You can't use this media as a blog banner."
msgstr "Toto médium nelze použít jako banner blogu."
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
#, fuzzy
msgid "Your comment have been posted."
msgstr "Zatím nemáte nahrané žádné média."
#, fuzzy
msgid "Your comment have been deleted."
msgstr "Zatím nemáte nahrané žádné média."
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr "Pro oblíbení příspěvku musíte být přihlášen/a"
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this media."
msgstr "Nemáte oprávnění zmazat tento blog."
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to use this media."
msgstr "Nemáte oprávnění upravovat tento blog."
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr "Pokud chcete vidět vaše notifikace, musíte být přihlášeni"
@@ -92,6 +147,34 @@ msgstr "Nový příspěvek"
msgid "Edit {0}"
msgstr "Upravit {0}"
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to publish on this blog."
msgstr "Nemáte oprávnění upravovat tento blog."
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
#, fuzzy
msgid "Your post have been saved."
msgstr "Zatím nemáte nahrané žádné média."
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this article."
msgstr "Nemáte oprávnění zmazat tento blog."
# src/routes/posts.rs:589
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/posts.rs:630
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -104,6 +187,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr "Pro sdílení příspěvku musíte být přihlášeni"
#, fuzzy
msgid "You are now connected."
msgstr "Nemáte oprávnění."
#, fuzzy
msgid "You are now logged off."
msgstr "Nemáte oprávnění."
# src/routes/session.rs:181
msgid "Password reset"
msgstr "Obnovit heslo"
@@ -124,6 +215,14 @@ msgstr "Omlouváme se, ale odkaz vypršel. Zkuste to znovu"
msgid "To access your dashboard, you need to be logged in"
msgstr "Pro přístup k vaší nástěnce musíte být přihlášen/a"
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:174
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:244
msgid "To subscribe to someone, you need to be logged in"
msgstr "Chcete-li někoho odebírat, musíte být přihlášeni"
@@ -132,6 +231,27 @@ msgstr "Chcete-li někoho odebírat, musíte být přihlášeni"
msgid "To edit your profile, you need to be logged in"
msgstr "Pro úpravu vášho profilu musíte být přihlášeni"
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
#, fuzzy
msgid "Your account have been deleted."
msgstr "Zatím nemáte nahrané žádné média."
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr "Plume"
@@ -264,10 +384,12 @@ msgstr "Beží na Plume {0}"
msgid "Follow {}"
msgstr "Následovat {}"
msgid "Login to follow"
#, fuzzy
msgid "Log in to follow"
msgstr "Pro následování se přihlášte"
msgid "Enter your full username to follow"
#, fuzzy
msgid "Enter your full username handle to follow"
msgstr "Pro následovaní zadejte své úplné uživatelské jméno"
msgid "Edit your account"
@@ -496,7 +618,8 @@ msgstr "Doména instance"
msgid "Posted by one of these authors"
msgstr "Zveřejněno na jedném z těchto autorů"
msgid "Authors"
#, fuzzy
msgid "Author(s)"
msgstr "Autoři"
# src/template_utils.rs:305
@@ -520,13 +643,16 @@ msgstr "Publikováno pod touto licenci"
msgid "Article license"
msgstr "Licence článku"
msgid "Search result for \"{0}\""
#, fuzzy
msgid "Search result(s) for \"{0}\""
msgstr "Výsledky hledání pro \"{0}\""
msgid "Search result"
#, fuzzy
msgid "Search result(s)"
msgstr "Výsledek hledání"
msgid "No result for your query"
#, fuzzy
msgid "No results for your query"
msgstr "Žádný výsledek nenalzen pro váš dotaz"
msgid "No more results for your query"
+130 -6
View File
@@ -48,6 +48,15 @@ msgstr "Um einen neuen Blog zu erstellen, müssen Sie angemeldet sein"
msgid "A blog with the same name already exists."
msgstr ""
# src/routes/session.rs:259
#, fuzzy
msgid "Your blog was successfully created!"
msgstr "Dein Passwort wurde erfolgreich zurückgesetzt."
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:172
msgid "You are not allowed to delete this blog."
msgstr "Sie dürfen diesen Blog nicht löschen."
@@ -64,10 +73,56 @@ msgstr "Sie können dieses Medium nicht als ein Blog-Icon verwenden."
msgid "You can't use this media as a blog banner."
msgstr "Sie können dieses Medium nicht als einen Blog-Banner verwenden."
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
#, fuzzy
msgid "Your comment have been posted."
msgstr "Du hast noch keine Medien."
#, fuzzy
msgid "Your comment have been deleted."
msgstr "Du hast noch keine Medien."
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr "Um einen Beitrag zu liken, musst du angemeldet sein"
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this media."
msgstr "Sie dürfen diesen Blog nicht löschen."
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to use this media."
msgstr "Ihnen fehlt die Berechtigung, um diesen Blog zu bearbeiten."
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr "Um Ihre Benachrichtigungen zu sehen, müssen Sie angemeldet sein"
@@ -92,6 +147,34 @@ msgstr "Neuer Beitrag"
msgid "Edit {0}"
msgstr "Bearbeite {0}"
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to publish on this blog."
msgstr "Ihnen fehlt die Berechtigung, um diesen Blog zu bearbeiten."
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
#, fuzzy
msgid "Your post have been saved."
msgstr "Du hast noch keine Medien."
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this article."
msgstr "Sie dürfen diesen Blog nicht löschen."
# src/routes/posts.rs:589
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/posts.rs:630
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -102,6 +185,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr "Um einen Beitrag zu wiederholen, musst du angemeldet sein"
#, fuzzy
msgid "You are now connected."
msgstr "Nicht berechtigt."
#, fuzzy
msgid "You are now logged off."
msgstr "Nicht berechtigt."
# src/routes/session.rs:181
msgid "Password reset"
msgstr "Passwort zurücksetzen"
@@ -122,6 +213,14 @@ msgstr "Entschuldigung, der Link ist abgelaufen. Versuche es erneut"
msgid "To access your dashboard, you need to be logged in"
msgstr "Um auf dein Dashboard zuzugreifen, musst du angemeldet sein"
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:174
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:244
msgid "To subscribe to someone, you need to be logged in"
msgstr "Um jemanden zu abonnieren, musst du angemeldet sein"
@@ -130,6 +229,27 @@ msgstr "Um jemanden zu abonnieren, musst du angemeldet sein"
msgid "To edit your profile, you need to be logged in"
msgstr "Um dein Profil zu bearbeiten, musst du angemeldet sein"
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
#, fuzzy
msgid "Your account have been deleted."
msgstr "Du hast noch keine Medien."
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr "Plume"
@@ -264,10 +384,10 @@ msgid "Follow {}"
msgstr "Folgen"
#, fuzzy
msgid "Login to follow"
msgid "Log in to follow"
msgstr "Um zu boosten, musst du eingeloggt sein"
msgid "Enter your full username to follow"
msgid "Enter your full username handle to follow"
msgstr ""
msgid "Edit your account"
@@ -496,7 +616,8 @@ msgstr "Instanz-Domain"
msgid "Posted by one of these authors"
msgstr "Von eine*r dieser Autor*innen veröffentlicht"
msgid "Authors"
#, fuzzy
msgid "Author(s)"
msgstr "Autoren"
# src/template_utils.rs:305
@@ -520,13 +641,16 @@ msgstr "Unter dieser Lizenz veröffentlicht"
msgid "Article license"
msgstr "Artikel-Lizenz"
msgid "Search result for \"{0}\""
#, fuzzy
msgid "Search result(s) for \"{0}\""
msgstr "Suchergebnis für {0}"
msgid "Search result"
#, fuzzy
msgid "Search result(s)"
msgstr "Suchergebnis"
msgid "No result for your query"
#, fuzzy
msgid "No results for your query"
msgstr "Keine Ergebnisse für deine Anfrage"
msgid "No more results for your query"
+121 -6
View File
@@ -48,6 +48,14 @@ msgstr ""
msgid "A blog with the same name already exists."
msgstr ""
# src/routes/blogs.rs:142
msgid "Your blog was successfully created!"
msgstr ""
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:172
msgid "You are not allowed to delete this blog."
msgstr ""
@@ -64,10 +72,54 @@ msgstr ""
msgid "You can't use this media as a blog banner."
msgstr ""
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
# src/routes/comments.rs:89
msgid "Your comment have been posted."
msgstr ""
# src/routes/comments.rs:163
msgid "Your comment have been deleted."
msgstr ""
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr ""
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
# src/routes/medias.rs:145
msgid "You are not allowed to delete this media."
msgstr ""
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
# src/routes/medias.rs:156
msgid "You are not allowed to use this media."
msgstr ""
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr ""
@@ -92,6 +144,32 @@ msgstr ""
msgid "Edit {0}"
msgstr ""
# src/routes/posts.rs:264
msgid "You are not allowed to publish on this blog."
msgstr ""
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
# src/routes/posts.rs:527
msgid "Your post have been saved."
msgstr ""
# src/routes/posts.rs:565
msgid "You are not allowed to delete this article."
msgstr ""
# src/routes/posts.rs:589
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/posts.rs:630
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -102,6 +180,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr ""
# src/routes/session.rs:108
msgid "You are now connected."
msgstr ""
# src/routes/session.rs:124
msgid "You are now logged off."
msgstr ""
# src/routes/session.rs:181
msgid "Password reset"
msgstr ""
@@ -122,6 +208,14 @@ msgstr ""
msgid "To access your dashboard, you need to be logged in"
msgstr ""
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:174
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:244
msgid "To subscribe to someone, you need to be logged in"
msgstr ""
@@ -130,6 +224,27 @@ msgstr ""
msgid "To edit your profile, you need to be logged in"
msgstr ""
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
# src/routes/user.rs:409
msgid "Your account have been deleted."
msgstr ""
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr ""
@@ -262,10 +377,10 @@ msgstr ""
msgid "Follow {}"
msgstr ""
msgid "Login to follow"
msgid "Log in to follow"
msgstr ""
msgid "Enter your full username to follow"
msgid "Enter your full username handle to follow"
msgstr ""
msgid "Edit your account"
@@ -484,7 +599,7 @@ msgstr ""
msgid "Posted by one of these authors"
msgstr ""
msgid "Authors"
msgid "Author(s)"
msgstr ""
# src/template_utils.rs:305
@@ -508,13 +623,13 @@ msgstr ""
msgid "Article license"
msgstr ""
msgid "Search result for \"{0}\""
msgid "Search result(s) for \"{0}\""
msgstr ""
msgid "Search result"
msgid "Search result(s)"
msgstr ""
msgid "No result for your query"
msgid "No results for your query"
msgstr ""
msgid "No more results for your query"
+126 -6
View File
@@ -48,6 +48,14 @@ msgstr ""
msgid "A blog with the same name already exists."
msgstr ""
# src/routes/blogs.rs:142
msgid "Your blog was successfully created!"
msgstr ""
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:172
msgid "You are not allowed to delete this blog."
msgstr ""
@@ -64,10 +72,56 @@ msgstr ""
msgid "You can't use this media as a blog banner."
msgstr ""
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
# src/routes/comments.rs:89
msgid "Your comment have been posted."
msgstr ""
# src/routes/comments.rs:163
msgid "Your comment have been deleted."
msgstr ""
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr ""
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to delete this media."
msgstr "Vi ne estas permesita redakti ĉi tiun blogon."
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to use this media."
msgstr "Vi ne estas permesita redakti ĉi tiun blogon."
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr ""
@@ -92,6 +146,34 @@ msgstr "Nova skribaĵo"
msgid "Edit {0}"
msgstr ""
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to publish on this blog."
msgstr "Vi ne estas permesita redakti ĉi tiun blogon."
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
# src/routes/posts.rs:527
msgid "Your post have been saved."
msgstr ""
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to delete this article."
msgstr "Vi ne estas permesita redakti ĉi tiun blogon."
# src/routes/posts.rs:589
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/posts.rs:630
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -102,6 +184,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr ""
# src/routes/session.rs:108
msgid "You are now connected."
msgstr ""
# src/routes/session.rs:124
msgid "You are now logged off."
msgstr ""
# src/routes/session.rs:181
msgid "Password reset"
msgstr ""
@@ -122,6 +212,14 @@ msgstr ""
msgid "To access your dashboard, you need to be logged in"
msgstr ""
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:174
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:244
msgid "To subscribe to someone, you need to be logged in"
msgstr ""
@@ -130,6 +228,27 @@ msgstr ""
msgid "To edit your profile, you need to be logged in"
msgstr ""
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
# src/routes/user.rs:409
msgid "Your account have been deleted."
msgstr ""
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr ""
@@ -262,10 +381,10 @@ msgstr ""
msgid "Follow {}"
msgstr ""
msgid "Login to follow"
msgid "Log in to follow"
msgstr ""
msgid "Enter your full username to follow"
msgid "Enter your full username handle to follow"
msgstr ""
msgid "Edit your account"
@@ -484,7 +603,8 @@ msgstr ""
msgid "Posted by one of these authors"
msgstr ""
msgid "Authors"
#, fuzzy
msgid "Author(s)"
msgstr "Verkistoj"
# src/template_utils.rs:305
@@ -508,13 +628,13 @@ msgstr "Eldonita sub ĉi tiu permesilo"
msgid "Article license"
msgstr "Artikola permesilo"
msgid "Search result for \"{0}\""
msgid "Search result(s) for \"{0}\""
msgstr ""
msgid "Search result"
msgid "Search result(s)"
msgstr ""
msgid "No result for your query"
msgid "No results for your query"
msgstr ""
msgid "No more results for your query"
+130 -6
View File
@@ -48,6 +48,15 @@ msgstr "Para crear un nuevo blog, necesita estar logueado"
msgid "A blog with the same name already exists."
msgstr ""
# src/routes/session.rs:259
#, fuzzy
msgid "Your blog was successfully created!"
msgstr "Su contraseña se ha restablecido correctamente."
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:172
msgid "You are not allowed to delete this blog."
msgstr "No está autorizado a eliminar este registro."
@@ -64,10 +73,56 @@ msgstr "No puede usar este medio como icono del blog."
msgid "You can't use this media as a blog banner."
msgstr "No puede usar este medio como bandera del blog."
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
#, fuzzy
msgid "Your comment have been posted."
msgstr "Todavía no tiene ningún medio."
#, fuzzy
msgid "Your comment have been deleted."
msgstr "Todavía no tiene ningún medio."
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr "Para darle un Me Gusta a un artículo, necesita estar conectado"
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this media."
msgstr "No está autorizado a eliminar este registro."
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to use this media."
msgstr "No tiene permiso para editar este blog."
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr "Para ver tus notificaciones, necesitas estar conectado"
@@ -92,6 +147,34 @@ msgstr "Nueva publicación"
msgid "Edit {0}"
msgstr "Editar {0}"
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to publish on this blog."
msgstr "No tiene permiso para editar este blog."
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
#, fuzzy
msgid "Your post have been saved."
msgstr "Todavía no tiene ningún medio."
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this article."
msgstr "No está autorizado a eliminar este registro."
# src/routes/posts.rs:589
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/posts.rs:630
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -102,6 +185,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr "Para compartir un artículo, necesita estar logueado"
#, fuzzy
msgid "You are now connected."
msgstr "No está autorizado."
#, fuzzy
msgid "You are now logged off."
msgstr "No está autorizado."
# src/routes/session.rs:181
msgid "Password reset"
msgstr "Reiniciar contraseña"
@@ -122,6 +213,14 @@ msgstr "Lo sentimos, pero el enlace expiró. Inténtalo de nuevo"
msgid "To access your dashboard, you need to be logged in"
msgstr "Para acceder a su panel de control, necesita estar conectado"
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:174
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:244
msgid "To subscribe to someone, you need to be logged in"
msgstr "Para suscribirse a alguien, necesita estar conectado"
@@ -130,6 +229,27 @@ msgstr "Para suscribirse a alguien, necesita estar conectado"
msgid "To edit your profile, you need to be logged in"
msgstr "Para editar su perfil, necesita estar conectado"
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
#, fuzzy
msgid "Your account have been deleted."
msgstr "Todavía no tiene ningún medio."
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr "Plume"
@@ -264,10 +384,10 @@ msgid "Follow {}"
msgstr "Seguir"
#, fuzzy
msgid "Login to follow"
msgid "Log in to follow"
msgstr "Dejar de seguir"
msgid "Enter your full username to follow"
msgid "Enter your full username handle to follow"
msgstr ""
msgid "Edit your account"
@@ -491,7 +611,8 @@ msgstr "Dominio de instancia"
msgid "Posted by one of these authors"
msgstr "Publicado por uno de estos autores"
msgid "Authors"
#, fuzzy
msgid "Author(s)"
msgstr "Autores"
# src/template_utils.rs:305
@@ -515,13 +636,16 @@ msgstr "Publicado bajo esta licencia"
msgid "Article license"
msgstr "Licencia de artículo"
msgid "Search result for \"{0}\""
#, fuzzy
msgid "Search result(s) for \"{0}\""
msgstr "Resultado de búsqueda para \"{0}\""
msgid "Search result"
#, fuzzy
msgid "Search result(s)"
msgstr "Resultado de búsqueda"
msgid "No result for your query"
#, fuzzy
msgid "No results for your query"
msgstr "No hay resultado para su consulta"
msgid "No more results for your query"
+130 -6
View File
@@ -48,6 +48,15 @@ msgstr "Vous devez vous connecter pour créer un nouveau blog"
msgid "A blog with the same name already exists."
msgstr "Un blog avec le même nom existe déjà."
# src/routes/session.rs:259
#, fuzzy
msgid "Your blog was successfully created!"
msgstr "Votre mot de passe a été réinitialisé avec succès."
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:172
msgid "You are not allowed to delete this blog."
msgstr "Vous n'êtes pas autorisé⋅e à supprimer ce blog."
@@ -64,10 +73,56 @@ msgstr "Vous ne pouvez pas utiliser ce media comme icône de blog."
msgid "You can't use this media as a blog banner."
msgstr "Vous ne pouvez pas utiliser ce media comme illustration de blog."
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
#, fuzzy
msgid "Your comment have been posted."
msgstr "Vous n'avez pas encore de média."
#, fuzzy
msgid "Your comment have been deleted."
msgstr "Vous n'avez pas encore de média."
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr "Vous devez vous connecter pour aimer un article"
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this media."
msgstr "Vous n'êtes pas autorisé⋅e à supprimer ce blog."
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to use this media."
msgstr "Vous n'êtes pas autorisé à éditer ce blog."
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr "Vous devez vous connecter pour voir vos notifications"
@@ -92,6 +147,34 @@ msgstr "Nouvel article"
msgid "Edit {0}"
msgstr "Modifier {0}"
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to publish on this blog."
msgstr "Vous n'êtes pas autorisé à éditer ce blog."
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
#, fuzzy
msgid "Your post have been saved."
msgstr "Vous n'avez pas encore de média."
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this article."
msgstr "Vous n'êtes pas autorisé⋅e à supprimer ce blog."
# src/routes/posts.rs:589
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/posts.rs:630
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -102,6 +185,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr "Vous devez vous connecter pour partager un article"
#, fuzzy
msgid "You are now connected."
msgstr "Vous navez pas les droits."
#, fuzzy
msgid "You are now logged off."
msgstr "Vous navez pas les droits."
# src/routes/session.rs:181
msgid "Password reset"
msgstr "Réinitialisation du mot de passe"
@@ -122,6 +213,14 @@ msgstr "Désolé, mais le lien a expiré. Réessayez"
msgid "To access your dashboard, you need to be logged in"
msgstr "Vous devez vous connecter pour accéder à votre tableau de bord"
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:174
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:244
msgid "To subscribe to someone, you need to be logged in"
msgstr "Vous devez vous connecter pour vous abonner à quelqu'un"
@@ -130,6 +229,27 @@ msgstr "Vous devez vous connecter pour vous abonner à quelqu'un"
msgid "To edit your profile, you need to be logged in"
msgstr "Vous devez vous connecter pour pour modifier votre profil"
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
#, fuzzy
msgid "Your account have been deleted."
msgstr "Vous n'avez pas encore de média."
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr "Plume"
@@ -265,10 +385,10 @@ msgid "Follow {}"
msgstr "Sabonner"
#, fuzzy
msgid "Login to follow"
msgid "Log in to follow"
msgstr "Connectez-vous pour partager"
msgid "Enter your full username to follow"
msgid "Enter your full username handle to follow"
msgstr ""
msgid "Edit your account"
@@ -500,7 +620,8 @@ msgstr "Domaine d'une instance"
msgid "Posted by one of these authors"
msgstr "Écrit par un de ces auteur⋅ices"
msgid "Authors"
#, fuzzy
msgid "Author(s)"
msgstr "Auteurs"
# src/template_utils.rs:305
@@ -524,13 +645,16 @@ msgstr "Placé sous cette licence"
msgid "Article license"
msgstr "Licence de l'article"
msgid "Search result for \"{0}\""
#, fuzzy
msgid "Search result(s) for \"{0}\""
msgstr "Résultats de la recherche pour \"{0}\""
msgid "Search result"
#, fuzzy
msgid "Search result(s)"
msgstr "Résultats de la recherche"
msgid "No result for your query"
#, fuzzy
msgid "No results for your query"
msgstr "Aucun résultat pour votre recherche"
msgid "No more results for your query"
+130 -6
View File
@@ -48,6 +48,15 @@ msgstr "Para crear un novo blog debe estar conectada"
msgid "A blog with the same name already exists."
msgstr ""
# src/routes/session.rs:259
#, fuzzy
msgid "Your blog was successfully created!"
msgstr "O contrasinal restableceuse correctamente."
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:172
msgid "You are not allowed to delete this blog."
msgstr "Vostede non ten permiso para eliminar este blog."
@@ -64,10 +73,56 @@ msgstr "Vostede non pode utilizar este medio como icona do blog."
msgid "You can't use this media as a blog banner."
msgstr "Vostede non pode utilizar este medio como banner do blog."
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
#, fuzzy
msgid "Your comment have been posted."
msgstr "Aínda non subeu ficheiros de medios."
#, fuzzy
msgid "Your comment have been deleted."
msgstr "Aínda non subeu ficheiros de medios."
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr "Para darlle a gústame, debe estar conectada"
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this media."
msgstr "Vostede non ten permiso para eliminar este blog."
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to use this media."
msgstr "Vostede non pode editar este blog."
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr "Para ver as súas notificacións, debe estar conectada"
@@ -92,6 +147,34 @@ msgstr "Nova entrada"
msgid "Edit {0}"
msgstr "Editar {0}"
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to publish on this blog."
msgstr "Vostede non pode editar este blog."
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
#, fuzzy
msgid "Your post have been saved."
msgstr "Aínda non subeu ficheiros de medios."
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this article."
msgstr "Vostede non ten permiso para eliminar este blog."
# src/routes/posts.rs:589
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/posts.rs:630
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -102,6 +185,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr "Para compartir un artigo, debe estar conectada"
#, fuzzy
msgid "You are now connected."
msgstr "Non ten permiso."
#, fuzzy
msgid "You are now logged off."
msgstr "Non ten permiso."
# src/routes/session.rs:181
msgid "Password reset"
msgstr "Restablecer contrasinal"
@@ -122,6 +213,14 @@ msgstr "Lamentámolo, a ligazón caducou. Inténteo de novo"
msgid "To access your dashboard, you need to be logged in"
msgstr "Para acceder ao taboleiro, debe estar conectada"
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:174
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:244
msgid "To subscribe to someone, you need to be logged in"
msgstr "Para suscribirse a un blog, debe estar conectada"
@@ -130,6 +229,27 @@ msgstr "Para suscribirse a un blog, debe estar conectada"
msgid "To edit your profile, you need to be logged in"
msgstr "Para editar o seu perfil, debe estar conectada"
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
#, fuzzy
msgid "Your account have been deleted."
msgstr "Aínda non subeu ficheiros de medios."
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr "Plume"
@@ -265,10 +385,10 @@ msgid "Follow {}"
msgstr "Seguir"
#, fuzzy
msgid "Login to follow"
msgid "Log in to follow"
msgstr "Conéctese para promover"
msgid "Enter your full username to follow"
msgid "Enter your full username handle to follow"
msgstr ""
msgid "Edit your account"
@@ -495,7 +615,8 @@ msgstr "Dominio da instancia"
msgid "Posted by one of these authors"
msgstr "Publicado por unha de estas autoras"
msgid "Authors"
#, fuzzy
msgid "Author(s)"
msgstr "Autoras"
# src/template_utils.rs:305
@@ -519,13 +640,16 @@ msgstr "Publicado baixo esta licenza"
msgid "Article license"
msgstr "Licenza do artigo"
msgid "Search result for \"{0}\""
#, fuzzy
msgid "Search result(s) for \"{0}\""
msgstr "Resultado da busca por \"{0}\""
msgid "Search result"
#, fuzzy
msgid "Search result(s)"
msgstr "Resultado da busca"
msgid "No result for your query"
#, fuzzy
msgid "No results for your query"
msgstr "Sen resultados para a busca"
msgid "No more results for your query"
+130 -6
View File
@@ -48,6 +48,15 @@ msgstr "नया ब्लॉग बनाने के लिए आपको
msgid "A blog with the same name already exists."
msgstr "ये नाम से पहले ही एक ब्लॉग है"
# src/routes/session.rs:259
#, fuzzy
msgid "Your blog was successfully created!"
msgstr "आपका पासवर्ड रिसेट कर दिया गया है"
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:172
msgid "You are not allowed to delete this blog."
msgstr "आपको ये ब्लॉग डिलीट करने की अनुमति नहीं है"
@@ -64,10 +73,56 @@ msgstr "इस फोटो को ब्लॉग आइकॉन के लि
msgid "You can't use this media as a blog banner."
msgstr "इस media को blog banner के लिए इस्तेमाल नहीं कर सकते"
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
# src/routes/comments.rs:89
msgid "Your comment have been posted."
msgstr ""
# src/routes/comments.rs:163
msgid "Your comment have been deleted."
msgstr ""
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr "Post को like करने के लिए आपको log in करना होगा"
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this media."
msgstr "आपको ये ब्लॉग डिलीट करने की अनुमति नहीं है"
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to use this media."
msgstr "आपको ये ब्लॉग में बदलाव करने की अनुमति नहीं है"
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr "Notifications देखने के लिए आपको log in करना होगा"
@@ -92,6 +147,34 @@ msgstr "नया post"
msgid "Edit {0}"
msgstr "Edit करें {0}"
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to publish on this blog."
msgstr "आपको ये ब्लॉग में बदलाव करने की अनुमति नहीं है"
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
# src/routes/posts.rs:527
msgid "Your post have been saved."
msgstr ""
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this article."
msgstr "आपको ये ब्लॉग डिलीट करने की अनुमति नहीं है"
# src/routes/posts.rs:589
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/posts.rs:630
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -104,6 +187,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr "Post reshare करने के लिए आपको log in करना होगा"
# src/routes/session.rs:108
msgid "You are now connected."
msgstr ""
# src/routes/session.rs:124
msgid "You are now logged off."
msgstr ""
# src/routes/session.rs:181
msgid "Password reset"
msgstr "पासवर्ड रीसेट करें"
@@ -124,6 +215,14 @@ msgstr "क्षमा करें, लेकिन लिंक इस्त
msgid "To access your dashboard, you need to be logged in"
msgstr "डैशबोर्ड पर जाने के लिए, लोग इन करें"
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:174
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:244
msgid "To subscribe to someone, you need to be logged in"
msgstr "सब्सक्राइब करने के लिए, लोग इन करें"
@@ -132,6 +231,27 @@ msgstr "सब्सक्राइब करने के लिए, लोग
msgid "To edit your profile, you need to be logged in"
msgstr "प्रोफाइल में बदलाव करने के लिए, लोग इन करें"
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
# src/routes/user.rs:409
msgid "Your account have been deleted."
msgstr ""
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr "प्लूम"
@@ -264,10 +384,10 @@ msgstr "Plume {0} का इस्तेमाल कर रहे हैं"
msgid "Follow {}"
msgstr ""
msgid "Login to follow"
msgid "Log in to follow"
msgstr ""
msgid "Enter your full username to follow"
msgid "Enter your full username handle to follow"
msgstr ""
msgid "Edit your account"
@@ -486,7 +606,8 @@ msgstr "इंस्टैंस डोमेन"
msgid "Posted by one of these authors"
msgstr "इन लेखकों द्वारा पोस्ट किये गए आर्टिकल्स"
msgid "Authors"
#, fuzzy
msgid "Author(s)"
msgstr "लेखक"
# src/template_utils.rs:305
@@ -510,13 +631,16 @@ msgstr "इस लिसेंसे के साथ पब्लिश कि
msgid "Article license"
msgstr "आर्टिकल लाइसेंस"
msgid "Search result for \"{0}\""
#, fuzzy
msgid "Search result(s) for \"{0}\""
msgstr "{0} के लिए सर्च रिजल्ट"
msgid "Search result"
#, fuzzy
msgid "Search result(s)"
msgstr "सर्च रिजल्ट"
msgid "No result for your query"
#, fuzzy
msgid "No results for your query"
msgstr "आपकी जांच के लिए रिजल्ट"
msgid "No more results for your query"
+125 -6
View File
@@ -49,6 +49,14 @@ msgstr ""
msgid "A blog with the same name already exists."
msgstr ""
# src/routes/blogs.rs:142
msgid "Your blog was successfully created!"
msgstr ""
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:172
msgid "You are not allowed to delete this blog."
msgstr ""
@@ -65,10 +73,56 @@ msgstr ""
msgid "You can't use this media as a blog banner."
msgstr ""
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
# src/routes/comments.rs:89
msgid "Your comment have been posted."
msgstr ""
# src/routes/comments.rs:163
msgid "Your comment have been deleted."
msgstr ""
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr ""
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
# src/routes/posts.rs:140
#, fuzzy
msgid "You are not allowed to delete this media."
msgstr "Ti ne autor ovog bloga."
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
# src/routes/posts.rs:140
#, fuzzy
msgid "You are not allowed to use this media."
msgstr "Ti ne autor ovog bloga."
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr ""
@@ -93,6 +147,34 @@ msgstr "Novi članak"
msgid "Edit {0}"
msgstr "Uredi {0}"
# src/routes/posts.rs:140
#, fuzzy
msgid "You are not allowed to publish on this blog."
msgstr "Ti ne autor ovog bloga."
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
# src/routes/posts.rs:527
msgid "Your post have been saved."
msgstr ""
# src/routes/posts.rs:140
#, fuzzy
msgid "You are not allowed to delete this article."
msgstr "Ti ne autor ovog bloga."
# src/routes/posts.rs:589
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/posts.rs:630
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -103,6 +185,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr ""
# src/routes/session.rs:108
msgid "You are now connected."
msgstr ""
# src/routes/session.rs:124
msgid "You are now logged off."
msgstr ""
# src/routes/session.rs:181
msgid "Password reset"
msgstr "Poništavanje zaporke"
@@ -123,6 +213,14 @@ msgstr ""
msgid "To access your dashboard, you need to be logged in"
msgstr ""
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:174
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:244
msgid "To subscribe to someone, you need to be logged in"
msgstr ""
@@ -131,6 +229,27 @@ msgstr ""
msgid "To edit your profile, you need to be logged in"
msgstr ""
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
# src/routes/user.rs:409
msgid "Your account have been deleted."
msgstr ""
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr "Plume"
@@ -263,10 +382,10 @@ msgstr ""
msgid "Follow {}"
msgstr ""
msgid "Login to follow"
msgid "Log in to follow"
msgstr ""
msgid "Enter your full username to follow"
msgid "Enter your full username handle to follow"
msgstr ""
msgid "Edit your account"
@@ -485,7 +604,7 @@ msgstr ""
msgid "Posted by one of these authors"
msgstr ""
msgid "Authors"
msgid "Author(s)"
msgstr ""
# src/template_utils.rs:305
@@ -509,13 +628,13 @@ msgstr ""
msgid "Article license"
msgstr ""
msgid "Search result for \"{0}\""
msgid "Search result(s) for \"{0}\""
msgstr ""
msgid "Search result"
msgid "Search result(s)"
msgstr ""
msgid "No result for your query"
msgid "No results for your query"
msgstr ""
msgid "No more results for your query"
+130 -6
View File
@@ -48,6 +48,15 @@ msgstr "Per creare un nuovo blog, devi avere effettuato l'accesso"
msgid "A blog with the same name already exists."
msgstr "Un blog con lo stesso nome esiste già."
# src/routes/session.rs:259
#, fuzzy
msgid "Your blog was successfully created!"
msgstr "La tua password è stata reimpostata con successo."
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:172
msgid "You are not allowed to delete this blog."
msgstr "Non ti è consentito di eliminare questo blog."
@@ -64,10 +73,56 @@ msgstr "Non puoi utilizzare questo media come icona del blog."
msgid "You can't use this media as a blog banner."
msgstr "Non puoi utilizzare questo media come copertina del blog."
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
#, fuzzy
msgid "Your comment have been posted."
msgstr "Non hai ancora nessun media."
#, fuzzy
msgid "Your comment have been deleted."
msgstr "Non hai ancora nessun media."
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr "Per mettere mi piace ad un post, devi avere effettuato l'accesso"
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this media."
msgstr "Non ti è consentito di eliminare questo blog."
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to use this media."
msgstr "Non ti è consentito modificare questo blog."
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr "Per vedere le tue notifiche, devi avere effettuato l'accesso"
@@ -92,6 +147,34 @@ msgstr "Nuovo post"
msgid "Edit {0}"
msgstr "Modifica {0}"
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to publish on this blog."
msgstr "Non ti è consentito modificare questo blog."
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
#, fuzzy
msgid "Your post have been saved."
msgstr "Non hai ancora nessun media."
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this article."
msgstr "Non ti è consentito di eliminare questo blog."
# src/routes/posts.rs:589
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/posts.rs:630
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -102,6 +185,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr "Per ricondividere un post, devi avere effettuato l'accesso"
#, fuzzy
msgid "You are now connected."
msgstr "Non sei autorizzato."
#, fuzzy
msgid "You are now logged off."
msgstr "Non sei autorizzato."
# src/routes/session.rs:181
msgid "Password reset"
msgstr "Reimposta password"
@@ -122,6 +213,14 @@ msgstr "Spiacente, ma il collegamento è scaduto. Riprova"
msgid "To access your dashboard, you need to be logged in"
msgstr "Per accedere al tuo pannello, devi avere effettuato l'accesso"
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:174
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:244
msgid "To subscribe to someone, you need to be logged in"
msgstr "Per iscriverti a qualcuno, devi avere effettuato l'accesso"
@@ -130,6 +229,27 @@ msgstr "Per iscriverti a qualcuno, devi avere effettuato l'accesso"
msgid "To edit your profile, you need to be logged in"
msgstr "Per modificare il tuo profilo, devi avere effettuato l'accesso"
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
#, fuzzy
msgid "Your account have been deleted."
msgstr "Non hai ancora nessun media."
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr "Plume"
@@ -264,10 +384,10 @@ msgid "Follow {}"
msgstr "Segui"
#, fuzzy
msgid "Login to follow"
msgid "Log in to follow"
msgstr "Accedi per boostare"
msgid "Enter your full username to follow"
msgid "Enter your full username handle to follow"
msgstr ""
msgid "Edit your account"
@@ -497,7 +617,8 @@ msgstr "Dominio dell'istanza"
msgid "Posted by one of these authors"
msgstr "Pubblicato da uno di questi autori"
msgid "Authors"
#, fuzzy
msgid "Author(s)"
msgstr "Autori"
# src/template_utils.rs:305
@@ -521,13 +642,16 @@ msgstr "Pubblicato sotto questa licenza"
msgid "Article license"
msgstr "Licenza dell'articolo"
msgid "Search result for \"{0}\""
#, fuzzy
msgid "Search result(s) for \"{0}\""
msgstr "Risultati della ricerca per \"{0}\""
msgid "Search result"
#, fuzzy
msgid "Search result(s)"
msgstr "Risultati della ricerca"
msgid "No result for your query"
#, fuzzy
msgid "No results for your query"
msgstr "Nessun risultato per la tua ricerca"
msgid "No more results for your query"
+130 -6
View File
@@ -48,6 +48,15 @@ msgstr "新しいブログを作成するにはログインが必要です"
msgid "A blog with the same name already exists."
msgstr ""
# src/routes/session.rs:259
#, fuzzy
msgid "Your blog was successfully created!"
msgstr "パスワードが正常にリセットされました。"
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:172
msgid "You are not allowed to delete this blog."
msgstr "このブログを削除する権限がありません。"
@@ -64,10 +73,56 @@ msgstr "このメディアはブログアイコンに使用できません。"
msgid "You can't use this media as a blog banner."
msgstr "このメディアはブログバナーに使用できません。"
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
#, fuzzy
msgid "Your comment have been posted."
msgstr "メディアがまだありません。"
#, fuzzy
msgid "Your comment have been deleted."
msgstr "メディアがまだありません。"
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr "投稿をいいねするにはログインが必要です"
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this media."
msgstr "このブログを削除する権限がありません。"
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to use this media."
msgstr "このブログを編集する権限がありません。"
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr "通知を表示するにはログインが必要です"
@@ -92,6 +147,34 @@ msgstr "新しい投稿"
msgid "Edit {0}"
msgstr "{0} を編集"
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to publish on this blog."
msgstr "このブログを編集する権限がありません。"
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
#, fuzzy
msgid "Your post have been saved."
msgstr "メディアがまだありません。"
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this article."
msgstr "このブログを削除する権限がありません。"
# src/routes/posts.rs:589
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/posts.rs:630
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -102,6 +185,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr "投稿を再共有するにはログインが必要です"
#, fuzzy
msgid "You are now connected."
msgstr "許可されていません。"
#, fuzzy
msgid "You are now logged off."
msgstr "許可されていません。"
# src/routes/session.rs:181
msgid "Password reset"
msgstr "パスワードのリセット"
@@ -123,6 +214,14 @@ msgstr ""
msgid "To access your dashboard, you need to be logged in"
msgstr "ダッシュボードにアクセスするにはログインが必要です"
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:174
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:244
msgid "To subscribe to someone, you need to be logged in"
msgstr "誰かをフォローするにはログインが必要です"
@@ -131,6 +230,27 @@ msgstr "誰かをフォローするにはログインが必要です"
msgid "To edit your profile, you need to be logged in"
msgstr "プロフィールを編集するにはログインが必要です"
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
#, fuzzy
msgid "Your account have been deleted."
msgstr "メディアがまだありません。"
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr "Plume"
@@ -267,10 +387,10 @@ msgid "Follow {}"
msgstr "フォロー"
#, fuzzy
msgid "Login to follow"
msgid "Log in to follow"
msgstr "ブーストするにはログインしてください"
msgid "Enter your full username to follow"
msgid "Enter your full username handle to follow"
msgstr ""
msgid "Edit your account"
@@ -495,7 +615,8 @@ msgstr "インスタンスのドメイン"
msgid "Posted by one of these authors"
msgstr "以下のいずれかの投稿者が投稿"
msgid "Authors"
#, fuzzy
msgid "Author(s)"
msgstr "投稿者"
# src/template_utils.rs:305
@@ -519,13 +640,16 @@ msgstr "適用されているライセンス"
msgid "Article license"
msgstr "投稿のライセンス"
msgid "Search result for \"{0}\""
#, fuzzy
msgid "Search result(s) for \"{0}\""
msgstr "\"{0}\" の検索結果"
msgid "Search result"
#, fuzzy
msgid "Search result(s)"
msgstr "検索結果"
msgid "No result for your query"
#, fuzzy
msgid "No results for your query"
msgstr "検索結果はありません"
msgid "No more results for your query"
+121 -14
View File
@@ -46,6 +46,14 @@ msgstr ""
msgid "A blog with the same name already exists."
msgstr "Et innlegg med samme navn finnes allerede."
# src/routes/blogs.rs:142
msgid "Your blog was successfully created!"
msgstr ""
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
#, fuzzy
msgid "You are not allowed to delete this blog."
msgstr "Du er ikke denne bloggens forfatter."
@@ -62,10 +70,54 @@ msgstr "Du er ikke denne bloggens forfatter."
msgid "You can't use this media as a blog banner."
msgstr "Du er ikke denne bloggens forfatter."
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
# src/routes/comments.rs:89
msgid "Your comment have been posted."
msgstr ""
# src/routes/comments.rs:163
msgid "Your comment have been deleted."
msgstr ""
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:47
msgid "To like a post, you need to be logged in"
msgstr ""
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
#, fuzzy
msgid "You are not allowed to delete this media."
msgstr "Du er ikke denne bloggens forfatter."
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
#, fuzzy
msgid "You are not allowed to use this media."
msgstr "Du er ikke denne bloggens forfatter."
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr ""
@@ -89,6 +141,32 @@ msgstr "Nytt innlegg"
msgid "Edit {0}"
msgstr "Kommentér \"{0}\""
#, fuzzy
msgid "You are not allowed to publish on this blog."
msgstr "Du er ikke denne bloggens forfatter."
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
# src/routes/posts.rs:527
msgid "Your post have been saved."
msgstr ""
#, fuzzy
msgid "You are not allowed to delete this article."
msgstr "Du er ikke denne bloggens forfatter."
#, fuzzy
msgid "Your article have been deleted."
msgstr "Ingen innlegg å vise enda."
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/user.rs:222
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -99,6 +177,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr ""
#, fuzzy
msgid "You are now connected."
msgstr "Det har du har ikke tilgang til."
#, fuzzy
msgid "You are now logged off."
msgstr "Det har du har ikke tilgang til."
#, fuzzy
msgid "Password reset"
msgstr "Passord"
@@ -119,6 +205,14 @@ msgstr ""
msgid "To access your dashboard, you need to be logged in"
msgstr ""
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
#, fuzzy
msgid "You are now following {}."
msgstr "{0} har begynt å følge deg"
# src/routes/user.rs:187
msgid "To subscribe to someone, you need to be logged in"
msgstr ""
@@ -127,6 +221,27 @@ msgstr ""
msgid "To edit your profile, you need to be logged in"
msgstr ""
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
# src/routes/user.rs:409
msgid "Your account have been deleted."
msgstr ""
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr "Plume"
@@ -274,10 +389,10 @@ msgid "Follow {}"
msgstr "Følg"
#, fuzzy
msgid "Login to follow"
msgid "Log in to follow"
msgstr "Logg inn"
msgid "Enter your full username to follow"
msgid "Enter your full username handle to follow"
msgstr ""
msgid "Edit your account"
@@ -527,7 +642,7 @@ msgstr "Instillinger for instansen"
msgid "Posted by one of these authors"
msgstr ""
msgid "Authors"
msgid "Author(s)"
msgstr ""
# #-#-#-#-# nb.po (plume) #-#-#-#-#
@@ -558,13 +673,13 @@ msgstr "Denne artikkelen er publisert med lisensen {0}"
msgid "Article license"
msgstr "Standardlisens"
msgid "Search result for \"{0}\""
msgid "Search result(s) for \"{0}\""
msgstr ""
msgid "Search result"
msgid "Search result(s)"
msgstr ""
msgid "No result for your query"
msgid "No results for your query"
msgstr ""
msgid "No more results for your query"
@@ -948,10 +1063,6 @@ msgstr ""
#~ msgid "{0}'s followers"
#~ msgstr "Én følger"
#, fuzzy
#~ msgid "{0} is now following you."
#~ msgstr "{0} har begynt å følge deg"
#, fuzzy
#~ msgid "People {0} follows"
#~ msgstr "Én følger"
@@ -966,10 +1077,6 @@ msgstr ""
#~ msgid "Unfollow"
#~ msgstr "Slutt å følge"
#, fuzzy
#~ msgid "No articles to see here yet."
#~ msgstr "Ingen innlegg å vise enda."
#~ msgid "New blog"
#~ msgstr "Ny blogg"
+131 -6
View File
@@ -50,6 +50,15 @@ msgstr "Aby utworzyć nowy blog, musisz być zalogowany"
msgid "A blog with the same name already exists."
msgstr "Blog o tej samej nazwie już istnieje."
# src/routes/session.rs:259
#, fuzzy
msgid "Your blog was successfully created!"
msgstr "Twoje hasło zostało pomyślnie zresetowane."
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:172
msgid "You are not allowed to delete this blog."
msgstr "Nie masz uprawnień do usunięcia tego bloga."
@@ -66,10 +75,56 @@ msgstr "Nie możesz użyć tego nośnika jako ikony blogu."
msgid "You can't use this media as a blog banner."
msgstr "Nie możesz użyć tego nośnika jako banner na blogu."
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
#, fuzzy
msgid "Your comment have been posted."
msgstr "Nie masz żadnej zawartości multimedialnej."
#, fuzzy
msgid "Your comment have been deleted."
msgstr "Nie masz żadnej zawartości multimedialnej."
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr "Aby polubić post, musisz być zalogowany"
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this media."
msgstr "Nie masz uprawnień do usunięcia tego bloga."
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to use this media."
msgstr "Nie masz uprawnień edytować tego bloga."
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr "Aby zobaczyć powiadomienia, musisz być zalogowany"
@@ -94,6 +149,34 @@ msgstr "Nowy wpis"
msgid "Edit {0}"
msgstr "Edytuj {0}"
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to publish on this blog."
msgstr "Nie masz uprawnień edytować tego bloga."
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
#, fuzzy
msgid "Your post have been saved."
msgstr "Nie masz żadnej zawartości multimedialnej."
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this article."
msgstr "Nie masz uprawnień do usunięcia tego bloga."
# src/routes/posts.rs:589
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/posts.rs:630
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -104,6 +187,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr "Aby udostępnić post, musisz być zalogowany"
#, fuzzy
msgid "You are now connected."
msgstr "Nie jesteś zalogowany."
#, fuzzy
msgid "You are now logged off."
msgstr "Nie jesteś zalogowany."
# src/routes/session.rs:181
msgid "Password reset"
msgstr "Resetowanie hasła"
@@ -124,6 +215,14 @@ msgstr "Przepraszam, ale link wygasł. Spróbuj ponownie"
msgid "To access your dashboard, you need to be logged in"
msgstr "Aby uzyskać dostęp do panelu, musisz być zalogowany"
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:174
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:244
msgid "To subscribe to someone, you need to be logged in"
msgstr "Aby subskrybować do kogoś, musisz być zalogowany"
@@ -132,6 +231,27 @@ msgstr "Aby subskrybować do kogoś, musisz być zalogowany"
msgid "To edit your profile, you need to be logged in"
msgstr "Aby edytować swój profil, musisz być zalogowany"
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
#, fuzzy
msgid "Your account have been deleted."
msgstr "Nie masz żadnej zawartości multimedialnej."
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr "Plume"
@@ -264,10 +384,11 @@ msgstr "Działa na Plume {0}"
msgid "Follow {}"
msgstr "Obserwuj {}"
msgid "Login to follow"
msgid "Log in to follow"
msgstr ""
msgid "Enter your full username to follow"
#, fuzzy
msgid "Enter your full username handle to follow"
msgstr "Wpisz swoją pełną nazwę użytkownika, do naśladowania"
msgid "Edit your account"
@@ -495,7 +616,8 @@ msgstr "Domena instancji"
msgid "Posted by one of these authors"
msgstr "Opublikowany przez jednego z tych autorów"
msgid "Authors"
#, fuzzy
msgid "Author(s)"
msgstr "Autorzy"
# src/template_utils.rs:305
@@ -519,13 +641,16 @@ msgstr "Opublikowany na tej licencji"
msgid "Article license"
msgstr "Licencja artykułu"
msgid "Search result for \"{0}\""
#, fuzzy
msgid "Search result(s) for \"{0}\""
msgstr "Wyniki wyszukiwania dla „{0}”"
msgid "Search result"
#, fuzzy
msgid "Search result(s)"
msgstr "Wyniki wyszukiwania"
msgid "No result for your query"
#, fuzzy
msgid "No results for your query"
msgstr "Brak wyników dla tego kryterium"
msgid "No more results for your query"
+170 -58
View File
@@ -12,59 +12,111 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
# src/template_utils.rs:68
# src/template_utils.rs:102
msgid "{0} commented on your article."
msgstr ""
# src/template_utils.rs:69
# src/template_utils.rs:103
msgid "{0} is subscribed to you."
msgstr ""
# src/template_utils.rs:70
# src/template_utils.rs:104
msgid "{0} liked your article."
msgstr ""
# src/template_utils.rs:71
# src/template_utils.rs:105
msgid "{0} mentioned you."
msgstr ""
# src/template_utils.rs:72
# src/template_utils.rs:106
msgid "{0} boosted your article."
msgstr ""
# src/template_utils.rs:108
# src/template_utils.rs:142
msgid "{0}'s avatar"
msgstr ""
# src/routes/blogs.rs:70
# src/routes/blogs.rs:64
msgid "To create a new blog, you need to be logged in"
msgstr ""
# src/routes/blogs.rs:109
# src/routes/blogs.rs:106
msgid "A blog with the same name already exists."
msgstr ""
# src/routes/blogs.rs:172
# src/routes/blogs.rs:141
msgid "Your blog was successfully created!"
msgstr ""
# src/routes/blogs.rs:163
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:170
msgid "You are not allowed to delete this blog."
msgstr ""
# src/routes/blogs.rs:217
# src/routes/blogs.rs:218
msgid "You are not allowed to edit this blog."
msgstr ""
# src/routes/blogs.rs:262
# src/routes/blogs.rs:263
msgid "You can't use this media as a blog icon."
msgstr ""
# src/routes/blogs.rs:280
# src/routes/blogs.rs:281
msgid "You can't use this media as a blog banner."
msgstr ""
# src/routes/blogs.rs:314
msgid "Your blog information have been updated."
msgstr ""
# src/routes/comments.rs:97
msgid "Your comment have been posted."
msgstr ""
# src/routes/comments.rs:172
msgid "Your comment have been deleted."
msgstr ""
# src/routes/instance.rs:134
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:175
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:177
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:221
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr ""
# src/routes/notifications.rs:29
# src/routes/medias.rs:141
msgid "Your media have been deleted."
msgstr ""
# src/routes/medias.rs:146
msgid "You are not allowed to delete this media."
msgstr ""
# src/routes/medias.rs:163
msgid "Your avatar have been updated."
msgstr ""
# src/routes/medias.rs:168
msgid "You are not allowed to use this media."
msgstr ""
# src/routes/notifications.rs:28
msgid "To see your notifications, you need to be logged in"
msgstr ""
@@ -76,19 +128,43 @@ msgstr ""
msgid "To write a new post, you need to be logged in"
msgstr ""
# src/routes/posts.rs:140
# src/routes/posts.rs:139
msgid "You are not an author of this blog."
msgstr ""
# src/routes/posts.rs:147
# src/routes/posts.rs:146
msgid "New post"
msgstr ""
# src/routes/posts.rs:192
# src/routes/posts.rs:191
msgid "Edit {0}"
msgstr ""
# src/routes/posts.rs:630
# src/routes/posts.rs:260
msgid "You are not allowed to publish on this blog."
msgstr ""
# src/routes/posts.rs:350
msgid "Your article have been updated."
msgstr ""
# src/routes/posts.rs:532
msgid "Your post have been saved."
msgstr ""
# src/routes/posts.rs:572
msgid "You are not allowed to delete this article."
msgstr ""
# src/routes/posts.rs:597
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:602
msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?"
msgstr ""
# src/routes/posts.rs:642
msgid "Couldn't obtain enough information about your account. Please make sure your username is correct."
msgstr ""
@@ -96,19 +172,27 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr ""
# src/routes/session.rs:181
# src/routes/session.rs:112
msgid "You are now connected."
msgstr ""
# src/routes/session.rs:131
msgid "You are now logged off."
msgstr ""
# src/routes/session.rs:188
msgid "Password reset"
msgstr ""
# src/routes/session.rs:182
# src/routes/session.rs:189
msgid "Here is the link to reset your password: {0}"
msgstr ""
# src/routes/session.rs:259
# src/routes/session.rs:264
msgid "Your password was successfully reset."
msgstr ""
# src/routes/session.rs:263
# src/routes/session.rs:274
msgid "Sorry, but the link expired. Try again"
msgstr ""
@@ -116,14 +200,42 @@ msgstr ""
msgid "To access your dashboard, you need to be logged in"
msgstr ""
# src/routes/user.rs:244
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:175
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:255
msgid "To subscribe to someone, you need to be logged in"
msgstr ""
# src/routes/user.rs:344
# src/routes/user.rs:357
msgid "To edit your profile, you need to be logged in"
msgstr ""
# src/routes/user.rs:399
msgid "Your profile have been updated."
msgstr ""
# src/routes/user.rs:426
msgid "Your account have been deleted."
msgstr ""
# src/routes/user.rs:432
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:504
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:528
msgid "Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr ""
@@ -208,11 +320,11 @@ msgstr ""
msgid "Nothing to see here yet. Try subscribing to more people."
msgstr ""
# src/template_utils.rs:217
# src/template_utils.rs:251
msgid "Name"
msgstr ""
# src/template_utils.rs:220
# src/template_utils.rs:254
msgid "Optional"
msgstr ""
@@ -228,7 +340,7 @@ msgstr ""
msgid "Long description"
msgstr ""
# src/template_utils.rs:217
# src/template_utils.rs:251
msgid "Default article license"
msgstr ""
@@ -256,10 +368,10 @@ msgstr ""
msgid "Follow {}"
msgstr ""
msgid "Login to follow"
msgid "Log in to follow"
msgstr ""
msgid "Enter your full username to follow"
msgid "Enter your full username handle to follow"
msgstr ""
msgid "Edit your account"
@@ -274,11 +386,11 @@ msgstr ""
msgid "Upload an avatar"
msgstr ""
# src/template_utils.rs:217
# src/template_utils.rs:251
msgid "Display name"
msgstr ""
# src/template_utils.rs:217
# src/template_utils.rs:251
msgid "Email"
msgstr ""
@@ -327,15 +439,15 @@ msgstr ""
msgid "Create an account"
msgstr ""
# src/template_utils.rs:217
# src/template_utils.rs:251
msgid "Username"
msgstr ""
# src/template_utils.rs:217
# src/template_utils.rs:251
msgid "Password"
msgstr ""
# src/template_utils.rs:217
# src/template_utils.rs:251
msgid "Password confirmation"
msgstr ""
@@ -426,84 +538,84 @@ msgstr ""
msgid "Advanced search"
msgstr ""
# src/template_utils.rs:305
# src/template_utils.rs:339
msgid "Article title matching these words"
msgstr ""
msgid "Title"
msgstr ""
# src/template_utils.rs:305
# src/template_utils.rs:339
msgid "Subtitle matching these words"
msgstr ""
msgid "Subtitle - byline"
msgstr ""
# src/template_utils.rs:305
# src/template_utils.rs:339
msgid "Content matching these words"
msgstr ""
msgid "Body content"
msgstr ""
# src/template_utils.rs:305
# src/template_utils.rs:339
msgid "From this date"
msgstr ""
# src/template_utils.rs:305
# src/template_utils.rs:339
msgid "To this date"
msgstr ""
# src/template_utils.rs:305
# src/template_utils.rs:339
msgid "Containing these tags"
msgstr ""
msgid "Tags"
msgstr ""
# src/template_utils.rs:305
# src/template_utils.rs:339
msgid "Posted on one of these instances"
msgstr ""
msgid "Instance domain"
msgstr ""
# src/template_utils.rs:305
# src/template_utils.rs:339
msgid "Posted by one of these authors"
msgstr ""
msgid "Authors"
msgid "Author(s)"
msgstr ""
# src/template_utils.rs:305
# src/template_utils.rs:339
msgid "Posted on one of these blogs"
msgstr ""
msgid "Blog title"
msgstr ""
# src/template_utils.rs:305
# src/template_utils.rs:339
msgid "Written in this language"
msgstr ""
msgid "Language"
msgstr ""
# src/template_utils.rs:305
# src/template_utils.rs:339
msgid "Published under this license"
msgstr ""
msgid "Article license"
msgstr ""
msgid "Search result for \"{0}\""
msgid "Search result(s) for \"{0}\""
msgstr ""
msgid "Search result"
msgid "Search result(s)"
msgstr ""
msgid "No result for your query"
msgid "No results for your query"
msgstr ""
msgid "No more results for your query"
@@ -512,11 +624,11 @@ msgstr ""
msgid "Reset your password"
msgstr ""
# src/template_utils.rs:217
# src/template_utils.rs:251
msgid "New password"
msgstr ""
# src/template_utils.rs:217
# src/template_utils.rs:251
msgid "Confirmation"
msgstr ""
@@ -529,7 +641,7 @@ msgstr ""
msgid "We sent a mail to the address you gave us, with a link to reset your password."
msgstr ""
# src/template_utils.rs:217
# src/template_utils.rs:251
msgid "E-mail"
msgstr ""
@@ -539,7 +651,7 @@ msgstr ""
msgid "Log in"
msgstr ""
# src/template_utils.rs:217
# src/template_utils.rs:251
msgid "Username, or email"
msgstr ""
@@ -558,7 +670,7 @@ msgstr ""
msgid "Classic editor (any changes will be lost)"
msgstr ""
# src/template_utils.rs:217
# src/template_utils.rs:251
msgid "Subtitle"
msgstr ""
@@ -571,15 +683,15 @@ msgstr ""
msgid "Upload media"
msgstr ""
# src/template_utils.rs:217
# src/template_utils.rs:251
msgid "Tags, separated by commas"
msgstr ""
# src/template_utils.rs:217
# src/template_utils.rs:251
msgid "License"
msgstr ""
# src/template_utils.rs:225
# src/template_utils.rs:259
msgid "Leave it empty to reserve all rights"
msgstr ""
@@ -639,7 +751,7 @@ msgstr ""
msgid "Comments"
msgstr ""
# src/template_utils.rs:217
# src/template_utils.rs:251
msgid "Content warning"
msgstr ""
@@ -746,7 +858,7 @@ msgstr ""
msgid "I'm from another instance"
msgstr ""
# src/template_utils.rs:225
# src/template_utils.rs:259
msgid "Example: user@plu.me"
msgstr ""
+128 -7
View File
@@ -48,6 +48,15 @@ msgstr "Para criar um novo blog, você precisa estar logado"
msgid "A blog with the same name already exists."
msgstr "Um blog com o mesmo nome já existe."
# src/routes/session.rs:259
#, fuzzy
msgid "Your blog was successfully created!"
msgstr "A sua palavra-passe foi redefinida com sucesso."
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:172
msgid "You are not allowed to delete this blog."
msgstr "Não está autorizado a apagar este blog."
@@ -64,10 +73,56 @@ msgstr "Você não pode usar esta mídia como um ícone do blog."
msgid "You can't use this media as a blog banner."
msgstr "Você não pode usar esta mídia como um banner do blog."
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
#, fuzzy
msgid "Your comment have been posted."
msgstr "Você ainda não tem nenhuma mídia."
#, fuzzy
msgid "Your comment have been deleted."
msgstr "Você ainda não tem nenhuma mídia."
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr "Para curtir uma postagem, você precisa estar logado"
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this media."
msgstr "Não está autorizado a apagar este blog."
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to use this media."
msgstr "Você não está autorizado a apagar este blog."
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr "Para ver suas notificações, você precisa estar logado"
@@ -92,6 +147,34 @@ msgstr "Novo artigo"
msgid "Edit {0}"
msgstr "Mudar {0}"
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to publish on this blog."
msgstr "Você não está autorizado a apagar este blog."
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
#, fuzzy
msgid "Your post have been saved."
msgstr "Você ainda não tem nenhuma mídia."
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this article."
msgstr "Não está autorizado a apagar este blog."
# src/routes/posts.rs:589
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/posts.rs:630
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -104,6 +187,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr "Para recompartilhar uma postagem, você precisa estar logado"
#, fuzzy
msgid "You are now connected."
msgstr "Você não está autorizado."
#, fuzzy
msgid "You are now logged off."
msgstr "Você não está autorizado."
# src/routes/session.rs:181
msgid "Password reset"
msgstr "Redefinir palavra-passe"
@@ -124,6 +215,14 @@ msgstr "Desculpe, mas a ligação expirou. Tente novamente"
msgid "To access your dashboard, you need to be logged in"
msgstr "Para acessar seu painel, você precisa estar logado"
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:174
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:244
msgid "To subscribe to someone, you need to be logged in"
msgstr "Para se inscrever em alguém, você precisa estar logado"
@@ -132,6 +231,27 @@ msgstr "Para se inscrever em alguém, você precisa estar logado"
msgid "To edit your profile, you need to be logged in"
msgstr "Para editar seu perfil, você precisa estar logado"
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
#, fuzzy
msgid "Your account have been deleted."
msgstr "Você ainda não tem nenhuma mídia."
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr "Plume"
@@ -266,10 +386,10 @@ msgid "Follow {}"
msgstr "Seguir"
#, fuzzy
msgid "Login to follow"
msgid "Log in to follow"
msgstr "Entrar para gostar"
msgid "Enter your full username to follow"
msgid "Enter your full username handle to follow"
msgstr ""
msgid "Edit your account"
@@ -496,7 +616,7 @@ msgstr ""
msgid "Posted by one of these authors"
msgstr ""
msgid "Authors"
msgid "Author(s)"
msgstr ""
# src/template_utils.rs:305
@@ -520,14 +640,15 @@ msgstr ""
msgid "Article license"
msgstr ""
msgid "Search result for \"{0}\""
msgid "Search result(s) for \"{0}\""
msgstr ""
msgid "Search result"
msgid "Search result(s)"
msgstr ""
msgid "No result for your query"
msgstr ""
#, fuzzy
msgid "No results for your query"
msgstr "Ir para a sua galeria"
msgid "No more results for your query"
msgstr ""
+127 -6
View File
@@ -49,6 +49,15 @@ msgstr "Pentru a crea un nou blog, trebuie sa fii logat"
msgid "A blog with the same name already exists."
msgstr ""
# src/routes/session.rs:259
#, fuzzy
msgid "Your blog was successfully created!"
msgstr "Parola dumneavoastră a fost resetată cu succes."
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:172
msgid "You are not allowed to delete this blog."
msgstr "Nu aveți permisiunea de a șterge acest blog."
@@ -65,10 +74,56 @@ msgstr ""
msgid "You can't use this media as a blog banner."
msgstr ""
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
# src/routes/comments.rs:89
msgid "Your comment have been posted."
msgstr ""
# src/routes/comments.rs:163
msgid "Your comment have been deleted."
msgstr ""
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr ""
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this media."
msgstr "Nu aveți permisiunea de a șterge acest blog."
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to use this media."
msgstr "Nu aveți permisiunea de a șterge acest blog."
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr ""
@@ -93,6 +148,34 @@ msgstr "Postare nouă"
msgid "Edit {0}"
msgstr "Editare {0}"
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to publish on this blog."
msgstr "Nu aveți permisiunea de a șterge acest blog."
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
# src/routes/posts.rs:527
msgid "Your post have been saved."
msgstr ""
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this article."
msgstr "Nu aveți permisiunea de a șterge acest blog."
# src/routes/posts.rs:589
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/posts.rs:630
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -103,6 +186,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr "Pentru a redistribui un post, trebuie să fii logat"
# src/routes/session.rs:108
msgid "You are now connected."
msgstr ""
# src/routes/session.rs:124
msgid "You are now logged off."
msgstr ""
# src/routes/session.rs:181
msgid "Password reset"
msgstr "Resetare parolă"
@@ -123,6 +214,14 @@ msgstr ""
msgid "To access your dashboard, you need to be logged in"
msgstr ""
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:174
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:244
msgid "To subscribe to someone, you need to be logged in"
msgstr ""
@@ -131,6 +230,27 @@ msgstr ""
msgid "To edit your profile, you need to be logged in"
msgstr ""
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
# src/routes/user.rs:409
msgid "Your account have been deleted."
msgstr ""
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr "Plume"
@@ -263,10 +383,10 @@ msgstr ""
msgid "Follow {}"
msgstr ""
msgid "Login to follow"
msgid "Log in to follow"
msgstr ""
msgid "Enter your full username to follow"
msgid "Enter your full username handle to follow"
msgstr ""
msgid "Edit your account"
@@ -485,7 +605,8 @@ msgstr ""
msgid "Posted by one of these authors"
msgstr ""
msgid "Authors"
#, fuzzy
msgid "Author(s)"
msgstr "Autori"
# src/template_utils.rs:305
@@ -509,13 +630,13 @@ msgstr ""
msgid "Article license"
msgstr ""
msgid "Search result for \"{0}\""
msgid "Search result(s) for \"{0}\""
msgstr ""
msgid "Search result"
msgid "Search result(s)"
msgstr ""
msgid "No result for your query"
msgid "No results for your query"
msgstr ""
msgid "No more results for your query"
+123 -7
View File
@@ -50,6 +50,14 @@ msgstr ""
msgid "A blog with the same name already exists."
msgstr ""
# src/routes/blogs.rs:142
msgid "Your blog was successfully created!"
msgstr ""
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:172
msgid "You are not allowed to delete this blog."
msgstr ""
@@ -66,10 +74,54 @@ msgstr ""
msgid "You can't use this media as a blog banner."
msgstr ""
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
# src/routes/comments.rs:89
msgid "Your comment have been posted."
msgstr ""
# src/routes/comments.rs:163
msgid "Your comment have been deleted."
msgstr ""
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr ""
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
# src/routes/medias.rs:145
msgid "You are not allowed to delete this media."
msgstr ""
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
#, fuzzy
msgid "You are not allowed to use this media."
msgstr "Вы не авторизованы."
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr ""
@@ -94,6 +146,32 @@ msgstr "Новый пост"
msgid "Edit {0}"
msgstr ""
#, fuzzy
msgid "You are not allowed to publish on this blog."
msgstr "Вы не авторизованы."
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
# src/routes/posts.rs:527
msgid "Your post have been saved."
msgstr ""
# src/routes/posts.rs:565
msgid "You are not allowed to delete this article."
msgstr ""
# src/routes/posts.rs:589
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/posts.rs:630
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -104,6 +182,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr ""
#, fuzzy
msgid "You are now connected."
msgstr "Вы не авторизованы."
#, fuzzy
msgid "You are now logged off."
msgstr "Вы не авторизованы."
# src/routes/session.rs:181
msgid "Password reset"
msgstr ""
@@ -124,6 +210,14 @@ msgstr ""
msgid "To access your dashboard, you need to be logged in"
msgstr ""
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:174
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:244
msgid "To subscribe to someone, you need to be logged in"
msgstr ""
@@ -132,6 +226,27 @@ msgstr ""
msgid "To edit your profile, you need to be logged in"
msgstr ""
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
# src/routes/user.rs:409
msgid "Your account have been deleted."
msgstr ""
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr ""
@@ -266,10 +381,10 @@ msgid "Follow {}"
msgstr "Подписаться"
#, fuzzy
msgid "Login to follow"
msgid "Log in to follow"
msgstr "Войдите, чтобы продвигать посты"
msgid "Enter your full username to follow"
msgid "Enter your full username handle to follow"
msgstr ""
msgid "Edit your account"
@@ -488,7 +603,7 @@ msgstr ""
msgid "Posted by one of these authors"
msgstr ""
msgid "Authors"
msgid "Author(s)"
msgstr ""
# src/template_utils.rs:305
@@ -512,14 +627,15 @@ msgstr ""
msgid "Article license"
msgstr ""
msgid "Search result for \"{0}\""
msgid "Search result(s) for \"{0}\""
msgstr ""
msgid "Search result"
msgid "Search result(s)"
msgstr ""
msgid "No result for your query"
msgstr ""
#, fuzzy
msgid "No results for your query"
msgstr "Перейти в вашу галерею"
msgid "No more results for your query"
msgstr ""
+132 -6
View File
@@ -48,6 +48,15 @@ msgstr "Aby si vytvoril/a nový blog, musíš sa prihlásiť"
msgid "A blog with the same name already exists."
msgstr "Blog s rovnakým názvom už existuje."
# src/routes/session.rs:259
#, fuzzy
msgid "Your blog was successfully created!"
msgstr "Tvoje heslo bolo úspešne zmenené."
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:172
msgid "You are not allowed to delete this blog."
msgstr "Nemáš povolenie vymazať tento blog."
@@ -64,10 +73,56 @@ msgstr "Tento mediálny súbor nemožno použiť ako ikonku pre blog."
msgid "You can't use this media as a blog banner."
msgstr "Tento mediálny súbor nemožno použiť ako záhlavie pre blog."
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
#, fuzzy
msgid "Your comment have been posted."
msgstr "Ešte nemáš nahrané žiadne multimédiá."
#, fuzzy
msgid "Your comment have been deleted."
msgstr "Ešte nemáš nahrané žiadne multimédiá."
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr "Aby si si obľúbil/a príspevok, musíš sa prihlásiť"
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this media."
msgstr "Nemáš povolenie vymazať tento blog."
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to use this media."
msgstr "Nemáš dovolené upravovať tento blog."
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr "Aby si videl/a notifikácie, musíš sa prihlásiť"
@@ -92,6 +147,34 @@ msgstr "Nový príspevok"
msgid "Edit {0}"
msgstr "Uprav {0}"
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to publish on this blog."
msgstr "Nemáš dovolené upravovať tento blog."
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
#, fuzzy
msgid "Your post have been saved."
msgstr "Ešte nemáš nahrané žiadne multimédiá."
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this article."
msgstr "Nemáš povolenie vymazať tento blog."
# src/routes/posts.rs:589
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/posts.rs:630
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -104,6 +187,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr "Na zdieľanie príspevku sa musíš prihlásiť"
#, fuzzy
msgid "You are now connected."
msgstr "Nemáš oprávnenie."
#, fuzzy
msgid "You are now logged off."
msgstr "Nemáš oprávnenie."
# src/routes/session.rs:181
msgid "Password reset"
msgstr "Obnovenie hesla"
@@ -124,6 +215,14 @@ msgstr "Prepáč, ale tento odkaz už vypŕšal. Skús to znova"
msgid "To access your dashboard, you need to be logged in"
msgstr "Pre prístup k prehľadovému panelu sa musíš prihlásiť"
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:174
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:244
msgid "To subscribe to someone, you need to be logged in"
msgstr "Ak chceš niekoho odoberať, musíš sa prihlásiť"
@@ -132,6 +231,27 @@ msgstr "Ak chceš niekoho odoberať, musíš sa prihlásiť"
msgid "To edit your profile, you need to be logged in"
msgstr "Na upravenie tvojho profilu sa musíš prihlásiť"
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
#, fuzzy
msgid "Your account have been deleted."
msgstr "Ešte nemáš nahrané žiadne multimédiá."
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr "Plume"
@@ -264,10 +384,12 @@ msgstr "Beží na Plume {0}"
msgid "Follow {}"
msgstr "Následuj {}"
msgid "Login to follow"
#, fuzzy
msgid "Log in to follow"
msgstr "Pre následovanie sa prihlás"
msgid "Enter your full username to follow"
#, fuzzy
msgid "Enter your full username handle to follow"
msgstr "Zadaj svoju prezývku v úplnosti, aby si následoval/a"
msgid "Edit your account"
@@ -497,7 +619,8 @@ msgstr "Doména instancie"
msgid "Posted by one of these authors"
msgstr "Uverejnené jedným z týchto autorov"
msgid "Authors"
#, fuzzy
msgid "Author(s)"
msgstr "Autori"
# src/template_utils.rs:305
@@ -521,13 +644,16 @@ msgstr "Uverejnené pod touto licenciou"
msgid "Article license"
msgstr "Článok je pod licenciou"
msgid "Search result for \"{0}\""
#, fuzzy
msgid "Search result(s) for \"{0}\""
msgstr "Výsledok hľadania pre \"{0}\""
msgid "Search result"
#, fuzzy
msgid "Search result(s)"
msgstr "Výsledok hľadania"
msgid "No result for your query"
#, fuzzy
msgid "No results for your query"
msgstr "Žiadny výsledok pre tvoje zadanie"
msgid "No more results for your query"
+121 -6
View File
@@ -49,6 +49,14 @@ msgstr ""
msgid "A blog with the same name already exists."
msgstr ""
# src/routes/blogs.rs:142
msgid "Your blog was successfully created!"
msgstr ""
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:172
msgid "You are not allowed to delete this blog."
msgstr ""
@@ -65,10 +73,54 @@ msgstr ""
msgid "You can't use this media as a blog banner."
msgstr ""
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
# src/routes/comments.rs:89
msgid "Your comment have been posted."
msgstr ""
# src/routes/comments.rs:163
msgid "Your comment have been deleted."
msgstr ""
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr ""
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
# src/routes/medias.rs:145
msgid "You are not allowed to delete this media."
msgstr ""
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
# src/routes/medias.rs:156
msgid "You are not allowed to use this media."
msgstr ""
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr ""
@@ -93,6 +145,32 @@ msgstr ""
msgid "Edit {0}"
msgstr "Uredi {0}"
# src/routes/posts.rs:264
msgid "You are not allowed to publish on this blog."
msgstr ""
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
# src/routes/posts.rs:527
msgid "Your post have been saved."
msgstr ""
# src/routes/posts.rs:565
msgid "You are not allowed to delete this article."
msgstr ""
# src/routes/posts.rs:589
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/posts.rs:630
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -103,6 +181,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr ""
# src/routes/session.rs:108
msgid "You are now connected."
msgstr ""
# src/routes/session.rs:124
msgid "You are now logged off."
msgstr ""
# src/routes/session.rs:181
msgid "Password reset"
msgstr "Poništavanje lozinke"
@@ -123,6 +209,14 @@ msgstr ""
msgid "To access your dashboard, you need to be logged in"
msgstr ""
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:174
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:244
msgid "To subscribe to someone, you need to be logged in"
msgstr ""
@@ -131,6 +225,27 @@ msgstr ""
msgid "To edit your profile, you need to be logged in"
msgstr ""
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
# src/routes/user.rs:409
msgid "Your account have been deleted."
msgstr ""
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr ""
@@ -263,10 +378,10 @@ msgstr ""
msgid "Follow {}"
msgstr ""
msgid "Login to follow"
msgid "Log in to follow"
msgstr ""
msgid "Enter your full username to follow"
msgid "Enter your full username handle to follow"
msgstr ""
msgid "Edit your account"
@@ -485,7 +600,7 @@ msgstr ""
msgid "Posted by one of these authors"
msgstr ""
msgid "Authors"
msgid "Author(s)"
msgstr ""
# src/template_utils.rs:305
@@ -509,13 +624,13 @@ msgstr ""
msgid "Article license"
msgstr ""
msgid "Search result for \"{0}\""
msgid "Search result(s) for \"{0}\""
msgstr ""
msgid "Search result"
msgid "Search result(s)"
msgstr ""
msgid "No result for your query"
msgid "No results for your query"
msgstr ""
msgid "No more results for your query"
+125 -6
View File
@@ -48,6 +48,14 @@ msgstr "För att skapa en ny blogg måste du vara inloggad"
msgid "A blog with the same name already exists."
msgstr ""
# src/routes/blogs.rs:142
msgid "Your blog was successfully created!"
msgstr ""
# src/routes/blogs.rs:167
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:172
msgid "You are not allowed to delete this blog."
msgstr "Du har inte tillstånd att ta bort den här bloggen."
@@ -64,10 +72,56 @@ msgstr ""
msgid "You can't use this media as a blog banner."
msgstr ""
# src/routes/blogs.rs:312
msgid "Your blog information have been updated."
msgstr ""
# src/routes/comments.rs:89
msgid "Your comment have been posted."
msgstr ""
# src/routes/comments.rs:163
msgid "Your comment have been deleted."
msgstr ""
# src/routes/instance.rs:145
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:182
msgid "{} have been unblocked."
msgstr ""
# src/routes/instance.rs:184
msgid "{} have been blocked."
msgstr ""
# src/routes/instance.rs:218
msgid "{} have been banned."
msgstr ""
# src/routes/likes.rs:51
msgid "To like a post, you need to be logged in"
msgstr ""
# src/routes/medias.rs:143
msgid "Your media have been deleted."
msgstr ""
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this media."
msgstr "Du har inte tillstånd att ta bort den här bloggen."
# src/routes/medias.rs:154
msgid "Your avatar have been updated."
msgstr ""
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to use this media."
msgstr "Du har inte tillstånd att redigera den här bloggen."
# src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in"
msgstr ""
@@ -92,6 +146,34 @@ msgstr ""
msgid "Edit {0}"
msgstr ""
# src/routes/blogs.rs:217
#, fuzzy
msgid "You are not allowed to publish on this blog."
msgstr "Du har inte tillstånd att redigera den här bloggen."
# src/routes/posts.rs:351
msgid "Your article have been updated."
msgstr ""
# src/routes/posts.rs:527
msgid "Your post have been saved."
msgstr ""
# src/routes/blogs.rs:172
#, fuzzy
msgid "You are not allowed to delete this article."
msgstr "Du har inte tillstånd att ta bort den här bloggen."
# src/routes/posts.rs:589
msgid "Your article have been deleted."
msgstr ""
# src/routes/posts.rs:593
msgid ""
"It looks like the article you tried to delete doesn't exist. Maybe it is "
"already gone?"
msgstr ""
# src/routes/posts.rs:630
msgid ""
"Couldn't obtain enough information about your account. Please make sure your "
@@ -102,6 +184,14 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr ""
# src/routes/session.rs:108
msgid "You are now connected."
msgstr ""
# src/routes/session.rs:124
msgid "You are now logged off."
msgstr ""
# src/routes/session.rs:181
msgid "Password reset"
msgstr ""
@@ -122,6 +212,14 @@ msgstr ""
msgid "To access your dashboard, you need to be logged in"
msgstr ""
# src/routes/user.rs:158
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:174
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:244
msgid "To subscribe to someone, you need to be logged in"
msgstr ""
@@ -130,6 +228,27 @@ msgstr ""
msgid "To edit your profile, you need to be logged in"
msgstr ""
# src/routes/user.rs:390
msgid "Your profile have been updated."
msgstr ""
# src/routes/user.rs:409
msgid "Your account have been deleted."
msgstr ""
# src/routes/user.rs:411
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:473
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:491
msgid ""
"Your account have been created. You just need to login before you can use it."
msgstr ""
msgid "Plume"
msgstr ""
@@ -262,10 +381,10 @@ msgstr ""
msgid "Follow {}"
msgstr ""
msgid "Login to follow"
msgid "Log in to follow"
msgstr ""
msgid "Enter your full username to follow"
msgid "Enter your full username handle to follow"
msgstr ""
msgid "Edit your account"
@@ -484,7 +603,7 @@ msgstr ""
msgid "Posted by one of these authors"
msgstr ""
msgid "Authors"
msgid "Author(s)"
msgstr ""
# src/template_utils.rs:305
@@ -508,13 +627,13 @@ msgstr ""
msgid "Article license"
msgstr ""
msgid "Search result for \"{0}\""
msgid "Search result(s) for \"{0}\""
msgstr ""
msgid "Search result"
msgid "Search result(s)"
msgstr ""
msgid "No result for your query"
msgid "No results for your query"
msgstr ""
msgid "No more results for your query"
+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"
+2 -6
View File
@@ -24,13 +24,9 @@ if [ $ARCH == "aarch64" -o $ARCH == "armv71" ] ; then
apt-get install -y --no-install-recommends build-essential subversion ninja-build cmake
mkdir -p /scratch/src
cd /scratch/src
# Pin LLVM to post 7.0.1 tag and pin to a known-good revision for Plume builds
svn co -r350977 http://llvm.org/svn/llvm-project/llvm/trunk/ llvm
svn co http://llvm.org/svn/llvm-project/llvm/tags/RELEASE_800/final/ llvm
cd /scratch/src/llvm/tools
# Pin lld to post 7.0.1 tag and pin to a known-good revision for Plume builds
svn co -r350975 http://llvm.org/svn/llvm-project/lld/trunk lld
#svn co http://llvm.org/svn/llvm-project/cfe/trunk clang
#svn co http://llvm.org/svn/llvm-project/clang-tools-extra/trunk extra
svn co http://llvm.org/svn/llvm-project/lld/tags/RELEASE_800/final/ lld
mkdir -p /scratch/build/arm
cd /scratch/build/arm
if [ "$ARCH" == "aarch64" ] ; then
+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))
}
+9
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 {
@@ -29,6 +37,7 @@ impl<'r> Responder<'r> for ApiError {
"error": "You are not authorized to access this resource"
}))
.respond_to(req),
Error::Validation(msg) => Json(json!({ "error": msg })).respond_to(req),
_ => Json(json!({
"error": "Server error"
}))
+372 -39
View File
@@ -1,54 +1,387 @@
use canapi::{Error as ApiError, Provider};
use rocket::http::uri::Origin;
use chrono::{NaiveDateTime, Utc};
use heck::{CamelCase, KebabCase};
use rocket_contrib::json::Json;
use serde_json;
use serde_qs;
use std::collections::HashSet;
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,
url: post.ap_url,
}))
}
#[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,
url: p.ap_url,
})
})
.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()
});
if slug.as_str() == "new" {
return Err(
Error::Validation("Sorry, but your article can't have this title.".into()).into(),
);
}
if payload.title.is_empty() {
return Err(Error::Validation("You have to give your article a title.".into()).into());
}
if payload.source.is_empty() {
return Err(Error::Validation("Your article can't be empty.".into()).into());
}
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 !author.is_author_in(conn, &Blog::get(conn, blog)?)? {
return Err(Error::Unauthorized.into());
}
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 {
let tags = tags
.iter()
.map(|t| t.to_camel_case())
.filter(|t| !t.is_empty())
.collect::<HashSet<_>>()
.into_iter()
.filter_map(|t| Tag::build_activity(t).ok());
for tag in tags {
Tag::insert(
conn,
NewTag {
tag: tag.name_string().unwrap(),
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,
url: post.ap_url,
}))
}
#[put("/posts/<id>", data = "<payload>")]
pub fn update(
id: i32,
auth: Authorization<Write, Post>,
payload: Json<NewPostData>,
rockets: PlumeRocket,
) -> Api<PostData> {
let conn = &*rockets.conn;
let mut post = Post::get(&*conn, id)?;
let author = User::get(conn, auth.0.user_id)?;
let b = post.get_blog(&*conn)?;
let new_slug = if !post.published {
payload.title.to_string().to_kebab_case()
} else {
post.slug.clone()
};
if new_slug != post.slug && Post::find_by_slug(&*conn, &new_slug, b.id).is_ok() {
return Err(Error::Validation("A post with the same title already exists.".into()).into());
}
if !author.is_author_in(&*conn, &b)? {
Err(Error::Unauthorized.into())
} else {
let (content, mentions, hashtags) = md_to_html(
&payload.source,
Some(&Instance::get_local()?.public_domain),
false,
Some(Media::get_media_processor(
&conn,
b.list_authors(&conn)?.iter().collect(),
)),
);
// update publication date if when this article is no longer a draft
let newly_published = if !post.published && payload.published.unwrap_or(post.published) {
post.published = true;
post.creation_date = Utc::now().naive_utc();
true
} else {
false
};
post.slug = new_slug.clone();
post.title = payload.title.clone();
post.subtitle = payload.subtitle.clone().unwrap_or_default();
post.content = SafeString::new(&content);
post.source = payload.source.clone();
post.license = payload.license.clone().unwrap_or_default();
post.cover_id = payload.cover_id;
post.update(&*conn, &rockets.searcher)?;
if post.published {
post.update_mentions(
&conn,
mentions
.into_iter()
.filter_map(|m| Mention::build_activity(&rockets, &m).ok())
.collect(),
)?;
}
let tags = payload
.tags
.clone()
.unwrap_or_default()
.iter()
.map(|t| t.trim().to_camel_case())
.filter(|t| !t.is_empty())
.collect::<HashSet<_>>()
.into_iter()
.filter_map(|t| Tag::build_activity(t).ok())
.collect::<Vec<_>>();
post.update_tags(&conn, tags)?;
let hashtags = hashtags
.into_iter()
.map(|h| h.to_camel_case())
.collect::<HashSet<_>>()
.into_iter()
.filter_map(|t| Tag::build_activity(t).ok())
.collect::<Vec<_>>();
post.update_hashtags(&conn, hashtags)?;
if post.published {
if newly_published {
let act = post.create_activity(&conn)?;
let dest = User::one_by_instance(&*conn)?;
rockets
.worker
.execute(move || broadcast(&author, act, dest));
} else {
let act = post.update_activity(&*conn)?;
let dest = User::one_by_instance(&*conn)?;
rockets
.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,
url: post.ap_url,
}))
}
}
#[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(()))
}
+53 -17
View File
@@ -1,11 +1,11 @@
#![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 clap;
extern crate colored;
extern crate ctrlc;
extern crate diesel;
@@ -39,13 +39,15 @@ extern crate validator;
extern crate validator_derive;
extern crate webfinger;
use clap::App;
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 +64,8 @@ mod mail;
#[macro_use]
mod template_utils;
mod routes;
#[macro_use]
extern crate shrinkwraprs;
#[cfg(feature = "test")]
mod test_routes;
@@ -69,21 +73,54 @@ 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();
match dotenv::dotenv() {
Ok(path) => println!("Configuration read from {}", path.display()),
Err(ref e) if e.not_found() => eprintln!("no .env was found"),
e => e.map(|_| ()).unwrap(),
}
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() {
App::new("Plume")
.bin_name("plume")
.version(env!("CARGO_PKG_VERSION"))
.about("Plume backend server")
.after_help(
r#"
The plume command should be run inside the directory
containing the `.env` configuration file and `static` directory.
See https://docs.joinplu.me/installation/config
and https://docs.joinplu.me/installation/init for more info.
"#,
)
.get_matches();
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 +139,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 +206,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 +275,8 @@ Then try to restart Plume
api::posts::get,
api::posts::list,
api::posts::create,
api::posts::update,
api::posts::delete,
],
)
.register(catchers![
@@ -260,18 +300,14 @@ Then try to restart Plume
(
"/inbox".to_owned(),
"/inbox".to_owned(),
rocket::http::Method::Post,
Some(rocket::http::Method::Post),
),
(
"/@/<name>/inbox".to_owned(),
"/@/<name>/inbox".to_owned(),
rocket::http::Method::Post,
),
(
"/api/<path..>".to_owned(),
"/api/<path..>".to_owned(),
rocket::http::Method::Post,
Some(rocket::http::Method::Post),
),
("/api/<path..>".to_owned(), "/api/<path..>".to_owned(), None),
])
.finalize()
.expect("main: csrf fairing creation error"),
+151 -144
View File
@@ -16,22 +16,20 @@ use plume_models::{
blog_authors::*, blogs::*, instance::Instance, medias::*, posts::Post, safe_string::SafeString,
users::User, Connection, PlumeRocket,
};
use routes::{errors::ErrorPage, Page};
use template_utils::Ructe;
use routes::{errors::ErrorPage, Page, RespondOrRedirect};
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,7 @@ 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) -> RespondOrRedirect {
let slug = utils::make_actor_id(&form.title);
let conn = &*rockets.conn;
let intl = &rockets.intl.catalog;
@@ -114,63 +108,69 @@ pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> Result<Re
);
}
if errors.is_empty() {
let blog = Blog::insert(
&*conn,
NewBlog::new_local(
slug.clone(),
form.title.to_string(),
String::from(""),
Instance::get_local(&*conn)
.expect("blog::create: instance error")
.id,
)
.expect("blog::create: new local error"),
)
.expect("blog::create: error");
BlogAuthor::insert(
&*conn,
NewBlogAuthor {
blog_id: blog.id,
author_id: user.id,
is_owner: true,
},
)
.expect("blog::create: author error");
Ok(Redirect::to(uri!(details: name = slug.clone(), page = _)))
} else {
Err(render!(blogs::new(
&(&*conn, intl, Some(user)),
&*form,
errors
)))
if !errors.is_empty() {
return render!(blogs::new(&rockets.to_context(), &*form, errors)).into();
}
let blog = Blog::insert(
&*conn,
NewBlog::new_local(
slug.clone(),
form.title.to_string(),
String::from(""),
Instance::get_local()
.expect("blog::create: instance error")
.id,
)
.expect("blog::create: new local error"),
)
.expect("blog::create: error");
BlogAuthor::insert(
&*conn,
NewBlogAuthor {
blog_id: blog.id,
author_id: user.id,
is_owner: true,
},
)
.expect("blog::create: author error");
Flash::success(
Redirect::to(uri!(details: name = slug.clone(), page = _)),
&i18n!(intl, "Your blog was successfully created!"),
)
.into()
}
#[post("/~/<name>/delete")]
pub fn delete(name: String, rockets: PlumeRocket) -> Result<Redirect, Ructe> {
pub fn delete(name: String, rockets: PlumeRocket) -> RespondOrRedirect {
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)))
Flash::success(
Redirect::to(uri!(super::instance::index)),
i18n!(rockets.intl.catalog, "Your blog was deleted."),
)
.into()
} 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.")
)))
render!(errors::not_authorized(
&rockets.to_context(),
i18n!(
rockets.intl.catalog,
"You are not allowed to delete this blog."
)
))
.into()
}
}
@@ -190,15 +190,16 @@ pub fn edit(name: String, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
if rockets
.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,101 +236,107 @@ pub fn update(
name: String,
form: LenientForm<EditForm>,
rockets: PlumeRocket,
) -> Result<Redirect, Ructe> {
) -> RespondOrRedirect {
let conn = &*rockets.conn;
let intl = &rockets.intl.catalog;
let mut blog = Blog::find_by_fqn(&rockets, &name).expect("blog::update: blog not found");
if rockets
if !rockets
.user
.clone()
.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");
form.validate()
.and_then(|_| {
if let Some(icon) = form.icon {
if !check_media(&*conn, icon, &user) {
let mut errors = ValidationErrors::new();
errors.add(
"",
ValidationError {
code: Cow::from("icon"),
message: Some(Cow::from(i18n!(
intl,
"You can't use this media as a blog icon."
))),
params: HashMap::new(),
},
);
return Err(errors);
}
}
if let Some(banner) = form.banner {
if !check_media(&*conn, banner, &user) {
let mut errors = ValidationErrors::new();
errors.add(
"",
ValidationError {
code: Cow::from("banner"),
message: Some(Cow::from(i18n!(
intl,
"You can't use this media as a blog banner."
))),
params: HashMap::new(),
},
);
return Err(errors);
}
}
blog.title = form.title.clone();
blog.summary = form.summary.clone();
blog.summary_html = SafeString::new(
&utils::md_to_html(
&form.summary,
"",
true,
Some(Media::get_media_processor(
&conn,
blog.list_authors(&conn)
.expect("Couldn't get list of authors")
.iter()
.collect(),
)),
)
.0,
);
blog.icon_id = form.icon;
blog.banner_id = form.banner;
blog.save_changes::<Blog>(&*conn)
.expect("Couldn't save blog changes");
Ok(Redirect::to(uri!(details: name = name, page = _)))
})
.map_err(|err| {
let medias = Media::for_user(&*conn, user.id).expect("Couldn't list media");
render!(blogs::edit(
&(&*conn, intl, Some(user)),
&blog,
medias,
&*form,
err
))
})
} else {
// TODO actually return 403 error code
Err(render!(errors::not_authorized(
&(&*conn, &rockets.intl.catalog, rockets.user),
return render!(errors::not_authorized(
&rockets.to_context(),
i18n!(
rockets.intl.catalog,
"You are not allowed to edit this blog."
)
)))
))
.into();
}
let user = rockets
.user
.clone()
.expect("blogs::edit: User was None while it shouldn't");
form.validate()
.and_then(|_| {
if let Some(icon) = form.icon {
if !check_media(&*conn, icon, &user) {
let mut errors = ValidationErrors::new();
errors.add(
"",
ValidationError {
code: Cow::from("icon"),
message: Some(Cow::from(i18n!(
intl,
"You can't use this media as a blog icon."
))),
params: HashMap::new(),
},
);
return Err(errors);
}
}
if let Some(banner) = form.banner {
if !check_media(&*conn, banner, &user) {
let mut errors = ValidationErrors::new();
errors.add(
"",
ValidationError {
code: Cow::from("banner"),
message: Some(Cow::from(i18n!(
intl,
"You can't use this media as a blog banner."
))),
params: HashMap::new(),
},
);
return Err(errors);
}
}
blog.title = form.title.clone();
blog.summary = form.summary.clone();
blog.summary_html = SafeString::new(
&utils::md_to_html(
&form.summary,
None,
true,
Some(Media::get_media_processor(
&conn,
blog.list_authors(&conn)
.expect("Couldn't get list of authors")
.iter()
.collect(),
)),
)
.0,
);
blog.icon_id = form.icon;
blog.banner_id = form.banner;
blog.save_changes::<Blog>(&*conn)
.expect("Couldn't save blog changes");
Ok(Flash::success(
Redirect::to(uri!(details: name = name, page = _)),
i18n!(intl, "Your blog information have been updated."),
))
})
.map_err(|err| {
let medias = Media::for_user(&*conn, user.id).expect("Couldn't list media");
render!(blogs::edit(
&rockets.to_context(),
&blog,
medias,
&*form,
err
))
})
.unwrap()
.into()
}
#[get("/~/<name>/outbox")]
@@ -344,7 +351,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()))
}

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