commit 938cc0f79344d9ec4d7e7b140fb2747a7c8a24ab Author: Katharina Fey Date: Sun Mar 27 22:53:40 2022 +0200 Squashed 'prototypes/pictureblog/' content from commit b9607b32ac6 git-subtree-dir: prototypes/pictureblog git-subtree-split: b9607b32ac6cffb524a8abdf33eb3d83c2cda9c5 diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000000..d8bb9aa45e3 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,10 @@ +[target.wasm32-unknown-unknown] +# required for clippy +rustflags = [ + "--cfg", "web_sys_unstable_apis", +] + +[target.x86_64-unknown-linux-gnu] +rustflags = [ + "--cfg", "web_sys_unstable_apis", +] diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000000..cd7f2d064f6 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,261 @@ +version: 2.1 + +executors: + default: + parameters: + postgres: + type: boolean + default: false + selenium: + type: boolean + default: false + docker: + - image: plumeorg/plume-buildenv:v0.4.0 + - image: <<#parameters.postgres>>circleci/postgres:9.6-alpine<><<^parameters.postgres>>alpine:latest<> + environment: + POSTGRES_USER: postgres + POSTGRES_DB: plume + - image: <<#parameters.selenium>>elgalu/selenium:latest<><<^parameters.selenium>>alpine:latest<> + working_directory: ~/projects/Plume + environment: + RUST_TEST_THREADS: 1 + FEATURES: <<#parameters.postgres>>postgres<><<^parameters.postgres>>sqlite<> + DATABASE_URL: <<#parameters.postgres>>postgres://postgres@localhost/plume<><<^parameters.postgres>>plume.sqlite<> + ROCKET_SECRET_KEY: VN5xV1DN7XdpATadOCYcuGeR/dV0hHfgx9mx9TarLdM= + + +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 + + clippy: + description: run cargo clippy + parameters: + package: + type: string + default: plume + no_feature: + type: boolean + default: false + steps: + - run: cargo clippy <<^parameters.no_feature>>--no-default-features --features="${FEATURES}"<> --release -p <> -- -D warnings + + run_with_coverage: + description: run command with environment for coverage + parameters: + cmd: + type: string + steps: + - run: | + export RUSTFLAGS="-Zprofile -Zfewer-names -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Clink-arg=-Xlinker -Clink-arg=--no-keep-memory -Clink-arg=-Xlinker -Clink-arg=--reduce-memory-overheads" + export CARGO_INCREMENTAL=0 + << parameters.cmd >> + + upload_coverage: + description: merge coverage files and upload to codecov.io + parameters: + type: + type: string + steps: + - 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 '/*' -o lcov.info + - run: bash <(curl -s https://codecov.io/bash) -f lcov.info -F <> + - 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<> --no-default-features --features="${FEATURES}" -p <> -j" + for i in 16 4 2 1 1; do + $cmd $i && exit 0 + done + exit 1 + +jobs: + cargo fmt: + executor: + name: default + steps: + - restore_env + - run: cargo fmt --all -- --check + + clippy: + parameters: + postgres: + type: boolean + executor: + name: default + postgres: << parameters.postgres >> + steps: + - restore_env: + cache: clippy + - clippy + - clippy: + package: plume-cli + - clippy: + package: plume-front + no_feature: true + - cache: + cache: clippy + + unit: + parameters: + postgres: + type: boolean + executor: + name: default + postgres: << parameters.postgres >> + steps: + - restore_env: + cache: <<#parameters.postgres>>postgres<><<^parameters.postgres>>sqlite<> + - run_with_coverage: + cmd: | + cargo build -p plume-cli --no-default-features --features=${FEATURES} -j 4 + ./target/debug/plm migration run + ./target/debug/plm search init + cmd="cargo test --all --exclude plume-front --exclude plume-macro --no-run --no-default-features --features=${FEATURES} -j" + for i in 16 4 2 1 1; do + $cmd $i && break + done + cargo test --all --exclude plume-front --exclude plume-macro --no-default-features --features="${FEATURES}" -j1 + - upload_coverage: + type: unit + - cache: + cache: <<#parameters.postgres>>postgres<><<^parameters.postgres>>sqlite<> + + integration: + parameters: + postgres: + type: boolean + executor: + name: default + postgres: << parameters.postgres >> + selenium: true + steps: + - restore_env: + cache: <<#parameters.postgres>>postgres<><<^parameters.postgres>>sqlite<> + - run: wasm-pack build --target web --release plume-front + - run_with_coverage: + cmd: | + cmd="cargo install --debug --no-default-features --features="${FEATURES}",test --force --path . -j" + for i in 16 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 16 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>>sqlite<> + + release: + parameters: + postgres: + type: boolean + executor: + name: default + postgres: << parameters.postgres >> + steps: + - restore_env: + cache: release-<<#parameters.postgres>>postgres<><<^parameters.postgres>>sqlite<> + - run: wasm-pack build --target web --release plume-front + - build: + package: plume + release: true + - build: + package: plume-cli + release: true + - cache: + cache: release-<<#parameters.postgres>>postgres<><<^parameters.postgres>>sqlite<> + - 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: cargo build + - run: crowdin upload -b master + +workflows: + version: 2 + build and test: + jobs: + - 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/ diff --git a/.circleci/images/plume-buildenv/Caddyfile b/.circleci/images/plume-buildenv/Caddyfile new file mode 100644 index 00000000000..4b38f5aca83 --- /dev/null +++ b/.circleci/images/plume-buildenv/Caddyfile @@ -0,0 +1,3 @@ +localhost { + reverse_proxy localhost:7878 +} diff --git a/.circleci/images/plume-buildenv/Dockerfile b/.circleci/images/plume-buildenv/Dockerfile new file mode 100644 index 00000000000..41ec8072671 --- /dev/null +++ b/.circleci/images/plume-buildenv/Dockerfile @@ -0,0 +1,39 @@ +FROM debian:buster-20210208 +ENV PATH="/root/.cargo/bin:${PATH}" + +#install native/circleci/build dependancies +RUN apt update &&\ + apt install -y --no-install-recommends git ssh tar gzip ca-certificates default-jre&&\ + echo "deb [trusted=yes] https://apt.fury.io/caddy/ /" \ + | tee -a /etc/apt/sources.list.d/caddy-fury.list &&\ + apt update &&\ + 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 libclang-dev clang caddy&&\ + rm -rf /var/lib/apt/lists/* + +#install and configure rust +RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2021-11-27 -y &&\ + rustup component add rustfmt clippy &&\ + rustup component add rust-std --target wasm32-unknown-unknown + +#compile some deps +RUN cargo install wasm-pack &&\ + cargo install grcov &&\ + strip /root/.cargo/bin/* &&\ + rm -fr ~/.cargo/registry + +#set some compilation parametters +COPY cargo_config /root/.cargo/config + +#install selenium for front end tests +RUN pip3 install selenium + +#configure caddy +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 diff --git a/.circleci/images/plume-buildenv/build_and_push.sh b/.circleci/images/plume-buildenv/build_and_push.sh new file mode 100755 index 00000000000..4d59709d955 --- /dev/null +++ b/.circleci/images/plume-buildenv/build_and_push.sh @@ -0,0 +1,4 @@ +#!/bin/bash +[ "$1" = "" ] && echo "you must provide one argument, the build version" && exit 1 +docker build -t plumeorg/plume-buildenv:$1 . +docker push plumeorg/plume-buildenv:$1 diff --git a/.circleci/images/plume-buildenv/cargo_config b/.circleci/images/plume-buildenv/cargo_config new file mode 100644 index 00000000000..889261b10f6 --- /dev/null +++ b/.circleci/images/plume-buildenv/cargo_config @@ -0,0 +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-args=-Xlinker --no-keep-memory -Xlinker --reduce-memory-overheads"] diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 00000000000..4d3e4f3773b --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,26 @@ +codecov: + notify: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "70...100" + + status: + project: no + patch: no + changes: no + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "header, diff" + behavior: default + require_changes: no diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000000..98547cd9682 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +docs +data +Dockerfile +docker-compose.yml +.env diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000000..6409424fc9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{js,rs,css,tera,html}] +charset = utf-8 +indent_size = 4 + +[*.{rs,tera,css,html}] +indent_style = space +indent_size = 4 + +[*.js] +indent_style = space +indent_size = 2 diff --git a/.env.sample b/.env.sample new file mode 100755 index 00000000000..c4dae972955 --- /dev/null +++ b/.env.sample @@ -0,0 +1,59 @@ +# This file contains your instance configuration +# Some documentation about these variables is available here: +# https://docs.joinplu.me/environment/ + +## GENERAL SETTINGS ## + +# The directory containing database migrations +# For Postgres: migrations/postgres +# For SQlite: migrations/sqlite +MIGRATION_DIRECTORY=migrations/postgres + +# The URL of your database (or its path for SQlite databases) +DATABASE_URL=postgres://plume:plume@localhost/plume + +# The domain of your instance +BASE_URL=plu.me + +# Log level for each crate +RUST_LOG=info + +# The secret key for private cookies and CSRF protection +# You can generate one with `openssl rand -base64 32` +ROCKET_SECRET_KEY= + +# Port and address which Plume will use +ROCKET_PORT=7878 +ROCKET_ADDRESS=127.0.0.1 + +## MAIL CONFIG ## +#MAIL_SERVER=smtp.plu.me +#MAIL_ADDRESS=no-reply@plu.me +#MAIL_USER=plume +#MAIL_PASSWORD= +#MAIL_HELO_NAME=no-reply@plu.me + +## ADVANCED OPTIONS ## +#MEDIA_UPLOAD_DIRECTORY=static/media +#SEARCH_INDEX=search_index + +# Sample logo configuration +#PLUME_LOGO=icons/trwnh/paragraphs/plumeParagraphs.svg +#PLUME_LOGO_FAVICON=icons/trwnh/paragraphs/plumeParagraphs32.png +#PLUME_LOGO_48=icons/trwnh/paragraphs/plumeParagraphs48.png +#PLUME_LOGO_72=icons/trwnh/paragraphs/plumeParagraphs72.png +#PLUME_LOGO_96=icons/trwnh/paragraphs/plumeParagraphs96.png +#PLUME_LOGO_144=icons/trwnh/paragraphs/plumeParagraphs144.png +#PLUME_LOGO_160=icons/trwnh/paragraphs/plumeParagraphs160.png +#PLUME_LOGO_192=icons/trwnh/paragraphs/plumeParagraphs192.png +#PLUME_LOGO_256=icons/trwnh/paragraphs/plumeParagraphs256.png +#PLUME_LOGO_512=icons/trwnh/paragraphs/plumeParagraphs512.png + +## LDAP CONFIG ## +# the object that will be bound is "${USER_NAME_ATTR}=${username},${BASE_DN}" +#LDAP_ADDR=ldap://127.0.0.1:1389 +#LDAP_BASE_DN="ou=users,dc=your-org,dc=eu" +#LDAP_USER_NAME_ATTR=cn +#LDAP_USER_MAIL_ATTR=mail +#LDAP_TLS=false + diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000000..68535d306ff --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,28 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'C: Bug' +assignees: '' + +--- + + + + + + + + + + + +- **Plume version:** You can find it in the footer of your instance. If you know the exact commit, please also add it. +- **Operating system:** +- **Web Browser:** diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000000..cda3d172c6d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,29 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + + + + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..62bedb4bb7e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: cargo + directory: / + schedule: + interval: daily diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000000..15e11fcaf8f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,7 @@ + diff --git a/.github/workflows/deploy-docker-latest.yaml b/.github/workflows/deploy-docker-latest.yaml new file mode 100644 index 00000000000..8ff8f7a42cc --- /dev/null +++ b/.github/workflows/deploy-docker-latest.yaml @@ -0,0 +1,30 @@ +name: cd + +on: + push: + branches: + - 'main' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - + name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - + name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + push: true + tags: plumeorg/plume:latest diff --git a/.github/workflows/deploy-docker-tag.yaml b/.github/workflows/deploy-docker-tag.yaml new file mode 100644 index 00000000000..5e4a764ebd8 --- /dev/null +++ b/.github/workflows/deploy-docker-tag.yaml @@ -0,0 +1,36 @@ +name: cd + +on: + push: + tags: + - '*.*.*' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - + name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - + name: Docker meta + id: meta + uses: docker/metadata-action@v3 + with: + images: plumeorg/plume + - + name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + push: true + tags: ${{ steps.meta.outputs.tags }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..bd576f31aba --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +rls +/target +**/*.rs.bk +rls +translations +*.po~ +.env +Rocket.toml +!.gitkeep +static +docker-compose.yml +*.db +*.sqlite +*.sqlite3 +*.swp +tags.* +!tags.rs +search_index +.buildconfig +__pycache__ +.vscode/ +*-journal diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000000..18552e09db8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,260 @@ +# Changelog + + + +## [Unreleased] - ReleaseDate + +### Added + +- Basque language (#1013) + +### Changed + +- Bump Rust to nightly 2022-01-26 (#1015) + +### Fixed + +- Add explanation of sign-up step at sign-up page when email sign-up mode (#1012) +- Add NOT NULL constraint to email_blocklist table fields (#1016) +- Don't fill empty content when switching rich editor (#1017) + +## [[0.7.1]] - 2022-01-12 + +### Added + +- Introduce environment variable `MAIL_PORT` (#980) +- Introduce email sign-up feature (#636, #1002) + +### Changed + +- Some styling improvements (#976, #977, #978) +- Respond with error status code when error (#1002) + +### Fiexed + +- Fix comment link (#974) +- Fix a bug that prevents posting articles (#975) +- Fix a bug that notification page doesn't show (#981) + +## [[0.7.0]] - 2022-01-02 + +### Added + +- Allow `dir` attributes for LtoR text in RtoL document (#860) +- More translation languages (#862) +- Proxy support (#829) +- Riker a actor system library (#870) +- (request-target) and Host header in HTTP Signature (#872) +- Default log levels for RUST_LOG (#885, #886, #919) + +### Changed + +- Upgrade some dependent crates (#858) +- Use tracing crate (#868) +- Update Rust version to nightly-2021-11-27 (#961) +- Upgrade Tantivy to 0.13.3 and lindera-tantivy to 0.7.1 (#878) +- Run searcher on actor system (#870) +- Extract a function to calculate posts' ap_url and share it with some places (#918) +- Use article title as its slug instead of capitalizing and inserting hyphens (#920) +- Sign GET requests to other instances (#957) + +### Fixed + +- Percent-encode URI for remote_interact (#866, #857) +- Menu animation not opening on iOS (#876, #897) +- Make actors subscribe to channel once (#913) +- Upsert posts and media instead of trying to insert and fail (#912) +- Update post's ActivityPub id when published by update (#915) +- Calculate media URI properly even when MEDIA_UPLOAD_DIRECTORY configured (#916) +- Prevent duplicated posts in 'all' timeline (#917) +- Draw side line for blockquote on start (#933) +- Fix URIs of posts on Mastodon (#947) +- Place edit link proper position (#956, #963, #964) + +## [[0.6.0]] - 2020-12-29 + +### Added + +- Vazir font for better support of languages written in Arabic script (#787) +- Login via LDAP (#826) +- cargo-release (#835) +- Care about weak ETag header for better caching (#840) +- Support for right to left languages in post content (#853) + +### Changed + +- Bump Docker base images to Buster flavor (#797) +- Upgrade Rocket to 0.4.5 (#800) +- Keep tags as-is (#832) +- Update Docker image for testing (#838) +- Update Dockerfile.dev (#841) + +### Fixed + +- Recreate search index if its format is outdated (#802) +- Make it possible to switch to rich text editor (#808) +- Fix margins for the mobile devices (#817) +- GPU acceleration for the mobile menu (#818) +- Natural title position for RtoL languages (#825) +- Remove link to unimplemented page (#827) +- Fix displaying not found page when submitting a duplicated blocklist email (#831) + +### Security + +- Validate spoofing of activity + +## [0.5.0] - 2020-06-21 + +### Added + +- Email blocklisting (#718) +- Syntax highlighting (#691) +- Persian localization (#782) +- Switchable tokenizer - enables Japanese full-text search (#776) +- Make database connections configurable by environment variables (#768) + +### Changed + +- Display likes and boost on post cards (#744) +- Rust 2018 (#726) +- Bump to LLVM to 9.0.0 to fix ARM builds (#737) +- Remove dependency on runtime-fmt (#773) +- Drop the -alpha suffix in release names, it is implied that Plume is not stable yet because of the 0 major version (Plume 1.0.0 will be the first stable release). + +### Fixed + +- Fix parsing of mentions inside a Markdown code block (be430c6) +- Fix RSS issues (#720) +- Fix Atom feed (#764) +- Fix default theme (#746) +- Fix shown password on remote interact pages (#741) +- Allow unicode hashtags (#757) +- Fix French grammar for for 0 (#760) +- Don't show boosts and likes for "all" and "local" in timelines (#781) +- Fix liking and boosting posts on remote instances (#762) + +## [0.4.0] - 2019-12-23 + +### Added + +- Add support for generic timeline (#525) +- Federate user deletion (#551) +- import migrations and don't require diesel_cli for admins (#555) +- Cache local instance (#572) +- Initial RTL support #575 (#577) +- Confirm deletion of blog (#602) +- Make a distinction between moderators and admins (#619) +- Theming (#624) +- Add clap to plume in order to print help and version (#631) +- Add Snapcraft metadata and install/maintenance hooks (#666) +- Add environmental variable to control path of media (#683) +- Add autosaving to the editor (#688) +- CI: Upload artifacts to pull request deploy environment (#539) +- CI: Upload artifact of wasm binary (#571) + +### Changed + +- Update follow_remote.rs.html grammar (#548) +- Add some feedback when performing some actions (#552) +- Theme update (#553) +- Remove the new index lock tantivy uses (#556) +- Reduce reqwest timeout to 5s (#557) +- Improve notification management (#561) +- Fix occurrences of 'have been' to 'has been' (#578) + Direct follow-up to #578 (#603) +- Store password reset requests in database (#610) +- Use futures and tokio to send activities (#620) +- Don't ignore dotenv errors (#630) +- Replace the input! macro with an Input builder (#646) +- Update default license (#659) +- Paginate the outbox responses. Fixes #669 (#681) +- Use the "classic" editor by default (#697) +- Fix issue #705 (#708) +- Make comments in styleshhets a bit clearer (#545) +- Rewrite circleci config (#558) +- Use openssl instead of sha256sum for build.rs (#568) +- Update dependencies (#574) +- Refactor code to use Shrinkwraprs and diesel-derive-newtype (#598) +- Add enum containing all successful route returns (#614) +- Update dependencies which depended on nix -- fixes arm32 builds (#615) +- Update some documents (#616) +- Update dependencies (#643) +- Make the comment syntax consistent across all CSS (#487) + +### Fixed + +- Remove r (#535) +- Fix certain improper rendering of forms (#560) +- make hashtags work in profile summary (#562) +- Fix some federation issue (#573) +- Prevent comment form submit button distortion on iOS (#592) +- Update textarea overflow to scroll (#609) +- Fix arm builds (#612) +- Fix theme caching (#647) +- Fix issue #642, frontend not in English if the user language does not exist (#648) +- Don't index drafts (#656) +- Fill entirely user on creation (#657) +- Delete notification on user deletion (#658) +- Order media so that latest added are top (#660) +- Fix logo URL (#664) +- Snap: Ensure cargo-web doesn't erroneously adopt our workspace. (#667) +- Snap: Another fix for building (#668) +- Snap: Fix build for non-Tier-1 Rust platforms (#672) +- Don't split sentences for translations (#677) +- Escape href quotation marks (#678) +- Re-add empty strings in translation (#682) +- Make the search index creation during migration respect SEARCH_INDEX (#689) +- Fix the navigation menu not opening on touch (#690) +- Make search items optional (#693) +- Various snap fixes (#698) +- Fix #637 : Markdown footnotes (#700) +- Fix lettre (#706) +- CI: Fix Crowdin upload (#576) + +### Removed + +- Remove the Canapi dependency (#540) +- Remove use of Rust in migrations (#704) + +## [0.3.0] - 2019-04-19 + +### Added + +- Cover for articles (#299, #387) +- Password reset (#448) +- New editor (#293, #458, #482, #483, #486, #530) +- Search (#324, #375, #445) +- Edit blogs (#460, #494, #497) +- Hashtags in articles (#283, #295) +- API endpoints (#245, #285, #307) +- A bunch of new translations! (#479, #501, #506, #510, #512, #514) + +### Changed + +- Federation improvements (#216, #217, #357, #364, #399, #443, #446, #455, #502, #519) +- Improved build process (#281, #374, #392, #402, #489, #498, #503, #511, #513, #515, #528) + +### Fixes + +- UI usability fixes (#370, #386, #401, #417, #418, #444, #452, #480, #516, #518, #522, #532) + +## [0.2.0] - 2018-09-12 + +### Added + +- Article publishing, or save as a draft +- Like, or boost an article +- Basic Markdown editor +- Federated commenting system +- User account creation +- Limited federation on other platforms and subscribing to users +- Ability to create multiple blogs + + +[Unreleased]: https://github.com/Plume-org/Plume/compare/0.7.1...HEAD +[[0.7.1]]: https://github.com/Plume-org/Plume/compare/0.7.0...0.7.1 +[[0.7.0]]: https://github.com/Plume-org/Plume/compare/0.6.0...0.7.0 +[[0.6.0]]: https://github.com/Plume-org/Plume/compare/0.5.0...0.6.0 +[0.5.0]: https://github.com/Plume-org/Plume/compare/0.4.0-alpha-4...0.5.0 +[0.4.0]: https://github.com/Plume-org/Plume/compare/0.3.0-alpha-2...0.4.0-alpha-4 +[0.3.0]: https://github.com/Plume-org/Plume/compare/0.2.0-alpha-1...0.3.0-alpha-2 +[0.2.0]: https://github.com/Plume-org/Plume/releases/tag/0.2.0-alpha-1 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..7f612a01be9 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,2 @@ +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. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000000..70e40e79028 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,5622 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "activitypub" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bfd311e7b4102971757a2a6f143a93b1a8e6b5afc2c46936af827fd9eab403f" +dependencies = [ + "activitystreams-derive", + "activitystreams-traits", + "activitystreams-types", + "serde 1.0.136", + "serde_derive", + "serde_json", +] + +[[package]] +name = "activitystreams-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176bdecfca82b1980e4769e3d54b6a392284b724083e0bff68272e290f17458f" +dependencies = [ + "proc-macro2 0.3.8", + "quote 0.5.2", + "syn 0.13.11", +] + +[[package]] +name = "activitystreams-traits" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ef03168e704b0cae242e7a5d8b40506772b339687e01a3496fc4afe2e8542" +dependencies = [ + "failure", + "serde 1.0.136", + "serde_json", +] + +[[package]] +name = "activitystreams-types" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff74c5765278614a009f97b9ec12f9a7c732bbcc5e0337fcfcab619b784860ec" +dependencies = [ + "activitystreams-derive", + "activitystreams-traits", + "chrono", + "mime 0.3.16", + "serde 1.0.136", + "serde_derive", + "serde_json", +] + +[[package]] +name = "addr2line" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a2e47a1fbe209ee101dd6d61285226744c6c8d3c21c8dc878ba6cb9f467f3a" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" +dependencies = [ + "generic-array", +] + +[[package]] +name = "aes" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" +dependencies = [ + "aes-soft", + "aesni", + "cipher 0.2.5", +] + +[[package]] +name = "aes-gcm" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5278b5fabbb9bd46e24aa69b2fdea62c99088e0a950a9be40e3e0101298f88da" +dependencies = [ + "aead", + "aes", + "cipher 0.2.5", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aes-soft" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" +dependencies = [ + "cipher 0.2.5", + "opaque-debug", +] + +[[package]] +name = "aesni" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" +dependencies = [ + "cipher 0.2.5", + "opaque-debug", +] + +[[package]] +name = "ahash" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" +dependencies = [ + "const-random", +] + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.4", + "once_cell", + "version_check 0.9.4", +] + +[[package]] +name = "aho-corasick" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +dependencies = [ + "memchr", +] + +[[package]] +name = "ammonia" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea9f21d23d82bae9d33c21080572af1fa749788e68234b5d8fa5e39d3e0783ed" +dependencies = [ + "html5ever", + "lazy_static", + "maplit", + "markup5ever_rcdom", + "tendril", + "url 2.2.2", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "arc-swap" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d78ce20460b82d3fa150275ed9d55e21064fc7951177baacf86a145c4a4b1f" + +[[package]] +name = "array_tool" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f8cb5d814eb646a863c4f24978cff2880c4be96ad8cde2c0f0678732902e271" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "ascii_utils" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "assert-json-diff" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f1c3703dd33532d7f0ca049168930e9099ecac238e23cf932f3a69c42f06da" +dependencies = [ + "serde 1.0.136", + "serde_json", +] + +[[package]] +name = "async-trait" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" +dependencies = [ + "proc-macro2 1.0.36", + "quote 1.0.15", + "syn 1.0.86", +] + +[[package]] +name = "atom_syndication" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21fb6a0b39c6517edafe46f8137e53c51742425a4dae1c73ee12264a37ad7541" +dependencies = [ + "chrono", + "derive_builder", + "diligent-date-parser", + "never", + "quick-xml", +] + +[[package]] +name = "atomicwrites" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a2baf2feb820299c53c7ad1cc4f5914a220a1cb76d7ce321d2522a94b54651f" +dependencies = [ + "nix 0.14.1", + "tempdir", + "winapi 0.3.9", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "autocfg" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "backtrace" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "150ae7828afa7afb6d474f909d64072d21de1f3365b6e8ad8029bf7b1c6350a0" +dependencies = [ + "backtrace-sys", + "cfg-if 0.1.10", + "dbghelp-sys", + "debug-builders", + "kernel32-sys", + "libc", + "winapi 0.2.8", +] + +[[package]] +name = "backtrace" +version = "0.3.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4717cfcbfaa661a0fd48f8453951837ae7e8f81e481fbb136e3202d72805a744" +dependencies = [ + "addr2line", + "cc", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "backtrace-sys" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fbebbe1c9d1f383a9cc7e8ccdb471b91c8d024ee9c2ca5b5346121fe8b4399" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "base64" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" +dependencies = [ + "byteorder", + "safemem", +] + +[[package]] +name = "base64" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" +dependencies = [ + "byteorder", +] + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bcrypt" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe4fef31efb0f76133ae8e3576a88e58edb7cfc5584c81c758c349ba46b43fc" +dependencies = [ + "base64 0.13.0", + "blowfish", + "getrandom 0.2.4", + "zeroize", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde 1.0.136", +] + +[[package]] +name = "bitflags" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitpacking" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c7d2ac73c167c06af4a5f37e6e59d84148d57ccbe4480b76f0273eefea82d7" +dependencies = [ + "crunchy", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher 0.4.3", +] + +[[package]] +name = "buf_redux" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" +dependencies = [ + "memchr", + "safemem", +] + +[[package]] +name = "bufstream" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" + +[[package]] +name = "bumpalo" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" + +[[package]] +name = "bytecount" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" +dependencies = [ + "byteorder", + "either 1.6.1", + "iovec", +] + +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cc" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" + +[[package]] +name = "census" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5927edd8345aef08578bcbb4aea7314f340d80c7f4931f99fbeb40b99d8f5060" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chomp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f74ad218e66339b11fd23f693fb8f1d621e80ba6ac218297be26073365d163d" +dependencies = [ + "bitflags 0.7.0", + "conv", + "debugtrace", + "either 0.1.7", +] + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits 0.2.14", + "serde 1.0.136", + "time 0.1.43", + "winapi 0.3.9", +] + +[[package]] +name = "cipher" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cipher" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim 0.8.0", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "combine" +version = "4.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b727aacc797f9fc28e355d21f34709ac4fc9adecfe470ad07b8f4464f53062" +dependencies = [ + "memchr", +] + +[[package]] +name = "config" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b076e143e1d9538dde65da30f8481c2a6c44040edb8e02b9bf1351edb92ce3" +dependencies = [ + "lazy_static", + "nom 5.1.2", + "rust-ini", + "serde 1.0.136", + "serde-hjson", + "serde_json", + "toml 0.5.8", + "yaml-rust", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen", +] + +[[package]] +name = "const-random" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f590d95d011aa80b063ffe3253422ed5aa462af4e9867d43ce8337562bac77c4" +dependencies = [ + "const-random-macro", + "proc-macro-hack 0.5.19", +] + +[[package]] +name = "const-random-macro" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "615f6e27d000a2bffbc7f2f6a8669179378fa27ee4d0a509e985dfc0a7defb40" +dependencies = [ + "getrandom 0.2.4", + "lazy_static", + "proc-macro-hack 0.5.19", + "tiny-keccak", +] + +[[package]] +name = "conv" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299" +dependencies = [ + "custom_derive", +] + +[[package]] +name = "cookie" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80f6044740a4a516b8aac14c140cdf35c1a640b1bd6b98b6224e49143b2f1566" +dependencies = [ + "aes-gcm", + "base64 0.13.0", + "hkdf", + "hmac", + "percent-encoding 2.1.0", + "rand 0.8.4", + "sha2", + "time 0.1.43", +] + +[[package]] +name = "cookie" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "888604f00b3db336d2af898ec3c1d5d0ddf5e6d462220f2ededc33a87ac4bbd5" +dependencies = [ + "time 0.1.43", + "url 1.7.2", +] + +[[package]] +name = "cookie_store" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46750b3f362965f197996c4448e4a0935e791bf7d6631bfce9ee0af3d24c919c" +dependencies = [ + "cookie 0.12.0", + "failure", + "idna 0.1.5", + "log 0.4.14", + "publicsuffix", + "serde 1.0.136", + "serde_json", + "time 0.1.43", + "try_from", + "url 1.7.2", +] + +[[package]] +name = "core-foundation" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +dependencies = [ + "libc", +] + +[[package]] +name = "cpuid-bool" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" + +[[package]] +name = "crc32fast" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "crossbeam" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69323bff1fb41c635347b8ead484a5ca6c3f11914d784170b158d8449ab07f8e" +dependencies = [ + "cfg-if 0.1.10", + "crossbeam-channel 0.4.4", + "crossbeam-deque 0.7.4", + "crossbeam-epoch 0.8.2", + "crossbeam-queue 0.2.3", + "crossbeam-utils 0.7.2", +] + +[[package]] +name = "crossbeam" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae5588f6b3c3cb05239e90bd110f257254aecd01e4635400391aeae07497845" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-channel 0.5.2", + "crossbeam-deque 0.8.1", + "crossbeam-epoch 0.9.6", + "crossbeam-queue 0.3.3", + "crossbeam-utils 0.8.6", +] + +[[package]] +name = "crossbeam-channel" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" +dependencies = [ + "crossbeam-utils 0.7.2", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils 0.8.6", +] + +[[package]] +name = "crossbeam-deque" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20ff29ded3204c5106278a81a38f4b482636ed4fa1e6cfbeef193291beb29ed" +dependencies = [ + "crossbeam-epoch 0.8.2", + "crossbeam-utils 0.7.2", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-epoch 0.9.6", + "crossbeam-utils 0.8.6", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" +dependencies = [ + "autocfg 1.0.1", + "cfg-if 0.1.10", + "crossbeam-utils 0.7.2", + "lazy_static", + "maybe-uninit", + "memoffset 0.5.6", + "scopeguard", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97242a70df9b89a65d0b6df3c4bf5b9ce03c5b7309019777fbde37e7537f8762" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils 0.8.6", + "lazy_static", + "memoffset 0.6.5", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" +dependencies = [ + "cfg-if 0.1.10", + "crossbeam-utils 0.7.2", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b979d76c9fcb84dffc80a73f7290da0f83e4c95773494674cb44b76d13a7a110" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils 0.8.6", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg 1.0.1", + "cfg-if 0.1.10", + "lazy_static", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120" +dependencies = [ + "cfg-if 1.0.0", + "lazy_static", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "ctr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" +dependencies = [ + "cipher 0.2.5", +] + +[[package]] +name = "ctrlc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19c6cedffdc8c03a3346d723eb20bd85a13362bb96dc2ac000842c6381ec7bf" +dependencies = [ + "nix 0.23.1", + "winapi 0.3.9", +] + +[[package]] +name = "custom_derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9" + +[[package]] +name = "darling" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2 1.0.36", + "quote 1.0.15", + "strsim 0.10.0", + "syn 1.0.86", +] + +[[package]] +name = "darling_macro" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a" +dependencies = [ + "darling_core", + "quote 1.0.15", + "syn 1.0.86", +] + +[[package]] +name = "dashmap" +version = "3.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f260e2fc850179ef410018660006951c1b55b79e8087e87111a2c388994b9b5" +dependencies = [ + "ahash 0.3.8", + "cfg-if 0.1.10", + "num_cpus", +] + +[[package]] +name = "data-encoding" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f47ca1860a761136924ddd2422ba77b2ea54fe8cc75b9040804a0d9d32ad97" + +[[package]] +name = "dbghelp-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97590ba53bcb8ac28279161ca943a924d1fd4a8fb3fa63302591647c4fc5b850" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "debug-builders" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f5d8e3d14cabcb2a8a59d7147289173c6ada77a0bc526f6b85078f941c0cf12" + +[[package]] +name = "debugtrace" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e432bd83c5d70317f6ebd8a50ed4afb32907c64d6e2e1e65e339b06dc553f3" +dependencies = [ + "backtrace 0.1.8", +] + +[[package]] +name = "derive_builder" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d13202debe11181040ae9063d739fa32cfcaaebe2275fe387703460ae2365b30" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66e616858f6187ed828df7c64a6d71720d83767a7f19740b2d1b6fe6327b36e5" +dependencies = [ + "darling", + "proc-macro2 1.0.36", + "quote 1.0.15", + "syn 1.0.86", +] + +[[package]] +name = "derive_builder_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58a94ace95092c5acb1e97a7e846b310cfbd499652f72297da7493f618a98d73" +dependencies = [ + "derive_builder_core", + "syn 1.0.86", +] + +[[package]] +name = "devise" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74e04ba2d03c5fa0d954c061fc8c9c288badadffc272ebb87679a89846de3ed3" +dependencies = [ + "devise_codegen", + "devise_core", +] + +[[package]] +name = "devise_codegen" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "066ceb7928ca93a9bedc6d0e612a8a0424048b0ab1f75971b203d01420c055d7" +dependencies = [ + "devise_core", + "quote 0.6.13", +] + +[[package]] +name = "devise_core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf41c59b22b5e3ec0ea55c7847e5f358d340f3a8d6d53a5cf4f1564967f96487" +dependencies = [ + "bitflags 1.3.2", + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.15.44", +] + +[[package]] +name = "diesel" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b28135ecf6b7d446b43e27e225622a038cc4e2930a1022f51cdb97ada19b8e4d" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "chrono", + "diesel_derives", + "libsqlite3-sys", + "pq-sys", + "r2d2", +] + +[[package]] +name = "diesel-derive-newtype" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e844e8e6f65dcf27aa0b97d4234f974d93dfbf56816033d71b5e0c7eb701709f" +dependencies = [ + "diesel", + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.14.9", +] + +[[package]] +name = "diesel_derives" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3" +dependencies = [ + "proc-macro2 1.0.36", + "quote 1.0.15", + "syn 1.0.86", +] + +[[package]] +name = "diesel_migrations" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3cde8413353dc7f5d72fa8ce0b99a560a359d2c5ef1e5817ca731cd9008f4c" +dependencies = [ + "migrations_internals", + "migrations_macros", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "diligent-date-parser" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d0fd95c7c02e2d6c588c6c5628466fff9bdde4b8c6196465e087b08e792720" +dependencies = [ + "chrono", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + +[[package]] +name = "either" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a39bffec1e2015c5d8a6773cb0cf48d0d758c842398f624c34969071f5499ea7" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "email" +version = "0.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91549a51bb0241165f13d57fc4c72cef063b4088fb078b019ecbf464a45f22e4" +dependencies = [ + "base64 0.9.3", + "chrono", + "encoding", + "lazy_static", + "rand 0.4.6", + "time 0.1.43", + "version_check 0.1.5", +] + +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + +[[package]] +name = "encoding_rs" +version = "0.8.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "fail" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3c61c59fdc91f5dbc3ea31ee8623122ce80057058be560654c5d410d181a6" +dependencies = [ + "lazy_static", + "log 0.4.14", + "rand 0.7.3", +] + +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace 0.3.59", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2 1.0.36", + "quote 1.0.15", + "syn 1.0.86", + "synstructure", +] + +[[package]] +name = "fast_chemail" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" +dependencies = [ + "ascii_utils", +] + +[[package]] +name = "fastrand" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779d043b6a0b90cc4c0ed7ee380a6504394cee7efd7db050e3774eee387324b2" +dependencies = [ + "instant", +] + +[[package]] +name = "filetime" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.2.10", + "winapi 0.3.9", +] + +[[package]] +name = "flate2" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" +dependencies = [ + "cfg-if 1.0.0", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding 2.1.0", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "fsevent" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6" +dependencies = [ + "bitflags 1.3.2", + "fsevent-sys", +] + +[[package]] +name = "fsevent-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0" +dependencies = [ + "libc", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags 1.3.2", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "futf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" + +[[package]] +name = "futures" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" + +[[package]] +name = "futures-cpupool" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" +dependencies = [ + "futures 0.1.31", + "num_cpus", +] + +[[package]] +name = "futures-executor" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", + "num_cpus", +] + +[[package]] +name = "futures-io" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2" + +[[package]] +name = "futures-macro" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c" +dependencies = [ + "proc-macro2 1.0.36", + "quote 1.0.15", + "syn 1.0.86", +] + +[[package]] +name = "futures-sink" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508" + +[[package]] +name = "futures-task" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" + +[[package]] +name = "futures-util" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite 0.2.8", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "version_check 0.9.4", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "gettext" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ebb594e753d5997e4be036e5a8cf048ab9414352870fb45c779557bbc9ba971" +dependencies = [ + "byteorder", + "encoding", +] + +[[package]] +name = "gettext-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "649db3b5cda06091ea6aacb9f66f7002dfe885505b324b8ed795261253ffc2b3" +dependencies = [ + "gettext", + "gettext-utils", + "proc-macro2 1.0.36", + "quote 1.0.15", + "syn 1.0.86", +] + +[[package]] +name = "gettext-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46dd079379f756f6a1ae74b051813e242893f84fbf6ac898bce827fc77958d70" + +[[package]] +name = "ghash" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189" + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "guid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e691c64d9b226c7597e29aeb46be753beb8c9eeef96d8c78dfd4d306338a38da" +dependencies = [ + "chomp", + "failure", + "failure_derive", + "guid-macro-impl", + "guid-parser", + "proc-macro-hack 0.4.3", + "winapi 0.2.8", +] + +[[package]] +name = "guid-create" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31477e35c699193e6af9d34765c8ccaaf750a9695844cc6a7a380259452e308d" +dependencies = [ + "byteorder", + "chomp", + "guid", + "guid-parser", + "rand 0.8.4", + "winapi 0.3.9", +] + +[[package]] +name = "guid-macro-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d50f7c496073b5a5dec0f6f1c149113a50960ce25dd2a559987a5a71190816" +dependencies = [ + "chomp", + "guid-parser", + "proc-macro-hack 0.4.3", + "quote 0.4.2", + "syn 0.12.15", +] + +[[package]] +name = "guid-parser" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc7adb441828023999e6cff9eb1ea63156f7ec37ab5bf690005e8fc6c1148ad" +dependencies = [ + "chomp", + "winapi 0.2.8", +] + +[[package]] +name = "h2" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5b34c246847f938a410a03c5458c7fee2274436675e76d8b903c08efc29c462" +dependencies = [ + "byteorder", + "bytes 0.4.12", + "fnv", + "futures 0.1.31", + "http 0.1.21", + "indexmap", + "log 0.4.14", + "slab", + "string", + "tokio-io", +] + +[[package]] +name = "h2" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535" +dependencies = [ + "bytes 0.5.6", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.6", + "indexmap", + "slab", + "tokio 0.2.25", + "tokio-util 0.3.1", + "tracing", + "tracing-futures", +] + +[[package]] +name = "hashbrown" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96282e96bfcd3da0d3aa9938bedf1e50df3269b6db08b4876d2da0bb1a0841cf" +dependencies = [ + "ahash 0.3.8", + "autocfg 1.0.1", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.6", +] + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "hmac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" +dependencies = [ + "crypto-mac", + "digest", +] + +[[package]] +name = "hostname" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21ceb46a83a85e824ef93669c8b390009623863b5c195d1ba747292c0c72f94e" +dependencies = [ + "libc", + "winutil", +] + +[[package]] +name = "html5ever" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aafcf38a1a36118242d29b92e1b08ef84e67e4a5ed06e0a80be20e6a32bfed6b" +dependencies = [ + "log 0.4.14", + "mac", + "markup5ever", + "proc-macro2 1.0.36", + "quote 1.0.15", + "syn 1.0.86", +] + +[[package]] +name = "htmlescape" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" + +[[package]] +name = "http" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0" +dependencies = [ + "bytes 0.4.12", + "fnv", + "itoa 0.4.8", +] + +[[package]] +name = "http" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" +dependencies = [ + "bytes 1.1.0", + "fnv", + "itoa 1.0.1", +] + +[[package]] +name = "http-body" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "http 0.1.21", + "tokio-buf", +] + +[[package]] +name = "http-body" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" +dependencies = [ + "bytes 0.5.6", + "http 0.2.6", +] + +[[package]] +name = "httparse" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" + +[[package]] +name = "httpdate" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" + +[[package]] +name = "hyper" +version = "0.10.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a0652d9a2609a968c14be1a9ea00bf4b1d64e2e1f53a1b51b6fff3a6e829273" +dependencies = [ + "base64 0.9.3", + "httparse", + "language-tags", + "log 0.3.9", + "mime 0.2.6", + "num_cpus", + "time 0.1.43", + "traitobject", + "typeable", + "unicase 1.4.2", + "url 1.7.2", +] + +[[package]] +name = "hyper" +version = "0.12.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c843caf6296fc1f93444735205af9ed4e109a539005abb2564ae1d6fad34c52" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "futures-cpupool", + "h2 0.1.26", + "http 0.1.21", + "http-body 0.1.0", + "httparse", + "iovec", + "itoa 0.4.8", + "log 0.4.14", + "net2", + "rustc_version", + "time 0.1.43", + "tokio 0.1.22", + "tokio-buf", + "tokio-executor", + "tokio-io", + "tokio-reactor", + "tokio-tcp", + "tokio-threadpool", + "tokio-timer", + "want 0.2.0", +] + +[[package]] +name = "hyper" +version = "0.13.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a6f157065790a3ed2f88679250419b5cdd96e714a0d65f7797fd337186e96bb" +dependencies = [ + "bytes 0.5.6", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.2.7", + "http 0.2.6", + "http-body 0.3.1", + "httparse", + "httpdate", + "itoa 0.4.8", + "pin-project", + "socket2 0.3.19", + "tokio 0.2.25", + "tower-service", + "tracing", + "want 0.3.0", +] + +[[package]] +name = "hyper-tls" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a800d6aa50af4b5850b2b0f659625ce9504df908e9733b635720483be26174f" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "hyper 0.12.36", + "native-tls", + "tokio-io", +] + +[[package]] +name = "hyper-tls" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed" +dependencies = [ + "bytes 0.5.6", + "hyper 0.13.10", + "native-tls", + "tokio 0.2.25", + "tokio-tls", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "if_chain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" + +[[package]] +name = "indexmap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +dependencies = [ + "autocfg 1.0.1", + "hashbrown 0.11.2", +] + +[[package]] +name = "inotify" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "inout" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1f03d4ab4d5dc9ec2d219f86c15d2a15fc08239d1cd3b2d6a19717c0a2f443" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "ipnet" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" + +[[package]] +name = "itertools" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" +dependencies = [ + "either 1.6.1", +] + +[[package]] +name = "itertools" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +dependencies = [ + "either 1.6.1", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "js-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "language-tags" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "lber" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a99b520993b21a6faab32643cf4726573dc18ca4cf2d48cbeb24d248c86c930" +dependencies = [ + "byteorder", + "bytes 1.1.0", + "nom 2.2.1", +] + +[[package]] +name = "ldap3" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8476563db035b64ffddce06a3e45b08c84096c76a561c3993ec8b74fce09fa71" +dependencies = [ + "async-trait", + "bytes 1.1.0", + "futures 0.3.19", + "futures-util", + "lazy_static", + "lber", + "log 0.4.14", + "maplit", + "native-tls", + "nom 2.2.1", + "percent-encoding 2.1.0", + "thiserror", + "tokio 1.17.0", + "tokio-native-tls", + "tokio-stream", + "tokio-util 0.7.0", + "url 2.2.2", +] + +[[package]] +name = "lettre" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ed8677138975b573ab4949c35613931a4addeadd0a8a6aa0327e2a979660de" +dependencies = [ + "base64 0.10.1", + "bufstream", + "fast_chemail", + "hostname", + "log 0.4.14", + "native-tls", + "nom 4.2.3", + "serde 1.0.136", + "serde_derive", + "serde_json", +] + +[[package]] +name = "lettre_email" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd02480f8dcf48798e62113974d6ccca2129a51d241fa20f1ea349c8a42559d5" +dependencies = [ + "base64 0.10.1", + "email", + "lettre", + "mime 0.3.16", + "time 0.1.43", + "uuid 0.7.4", +] + +[[package]] +name = "levenshtein_automata" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" + +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "cfg-if 1.0.0", + "ryu", + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" + +[[package]] +name = "libsqlite3-sys" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290b64917f8b0cb885d9de0f9959fe1f775d7fa12f1da2db9001c1c8ab60f89d" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "lindera" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361efd98843cc0ccbdecbbf62feb9794a66d1c29758af8f45af34c140f7d2143" +dependencies = [ + "bincode", + "byteorder", + "encoding", + "lindera-core", + "lindera-dictionary", + "lindera-ipadic", + "lindera-ipadic-builder", + "serde 1.0.136", + "serde_json", +] + +[[package]] +name = "lindera-core" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c22c6a86b9be6871766dcfa1d333eae7d2331fed217df8c3798514496e0ae110" +dependencies = [ + "bincode", + "byteorder", + "encoding", + "serde 1.0.136", + "yada", +] + +[[package]] +name = "lindera-dictionary" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eabe5730e9d20293e0ed8f295f60ebf656a173f6c129b69f37355a4879e6393" +dependencies = [ + "bincode", + "byteorder", + "lindera-core", +] + +[[package]] +name = "lindera-ipadic" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e69130ce218cacb40abad09b101f859bf464e44acb7b653c2118f90d706f404a" +dependencies = [ + "bincode", + "byteorder", + "flate2", + "lindera-core", + "lindera-ipadic-builder", + "reqwest 0.10.10", + "tar", + "tokio 0.2.25", +] + +[[package]] +name = "lindera-ipadic-builder" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1484640839a69c383b9aa56a55a9f1be9f998a0293e9a9a135c13ff1af1f398b" +dependencies = [ + "bincode", + "byteorder", + "clap", + "encoding", + "glob", + "lindera-core", + "yada", +] + +[[package]] +name = "lindera-tantivy" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceb3ba4b1279b57af415bff512d7b35616baf2c64229cc8e949164e0136fe207" +dependencies = [ + "lindera", + "lindera-core", + "tantivy 0.14.0", +] + +[[package]] +name = "line-wrap" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" +dependencies = [ + "safemem", +] + +[[package]] +name = "linked-hash-map" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd" +dependencies = [ + "serde 0.8.23", + "serde_test", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + +[[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "lock_api" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" +dependencies = [ + "log 0.4.14", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "lru" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ea2d928b485416e8908cff2d97d621db22b27f7b3b6729e438bcf42c671ba91" +dependencies = [ + "hashbrown 0.11.2", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "markup5ever" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" +dependencies = [ + "log 0.4.14", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f015da43bcd8d4f144559a3423f4591d69b8ce0652c905374da7205df336ae2b" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "xml5ever", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "memmap" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "memoffset" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" +dependencies = [ + "autocfg 1.0.1", +] + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg 1.0.1", +] + +[[package]] +name = "migrations_internals" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b4fc84e4af020b837029e017966f86a1c2d5e83e64b589963d5047525995860" +dependencies = [ + "diesel", +] + +[[package]] +name = "migrations_macros" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9753f12909fd8d923f75ae5c3258cae1ed3c8ec052e1b38c93c21a6d157f789c" +dependencies = [ + "migrations_internals", + "proc-macro2 1.0.36", + "quote 1.0.15", + "syn 1.0.86", +] + +[[package]] +name = "mime" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0" +dependencies = [ + "log 0.3.9", +] + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" +dependencies = [ + "mime 0.3.16", + "unicase 2.6.0", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg 1.0.1", +] + +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log 0.4.14", + "miow 0.2.2", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba272f85fa0b41fc91872be579b3bbe0f56b792aa361a380eb669469f68dafb2" +dependencies = [ + "libc", + "log 0.4.14", + "miow 0.3.7", + "ntapi", + "winapi 0.3.9", +] + +[[package]] +name = "mio-extras" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" +dependencies = [ + "lazycell", + "log 0.4.14", + "mio 0.6.23", + "slab", +] + +[[package]] +name = "mio-named-pipes" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0840c1c50fd55e521b247f949c241c9997709f23bd7f023b9762cd561e935656" +dependencies = [ + "log 0.4.14", + "mio 0.6.23", + "miow 0.3.7", + "winapi 0.3.9", +] + +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio 0.6.23", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "multipart" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182" +dependencies = [ + "buf_redux", + "httparse", + "log 0.4.14", + "mime 0.3.16", + "mime_guess", + "quick-error", + "rand 0.8.4", + "safemem", + "tempfile", + "twoway", +] + +[[package]] +name = "murmurhash32" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d736ff882f0e85fe9689fb23db229616c4c00aee2b3ac282f666d8f20eb25d4a" +dependencies = [ + "byteorder", +] + +[[package]] +name = "native-tls" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" +dependencies = [ + "lazy_static", + "libc", + "log 0.4.14", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "net2" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "never" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91" + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "nix" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c722bee1037d430d0f8e687bbdbf222f27cc6e4e68d5caf630857bb2b6dbdce" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if 0.1.10", + "libc", + "void", +] + +[[package]] +name = "nix" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if 1.0.0", + "libc", + "memoffset 0.6.5", +] + +[[package]] +name = "nom" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf51a729ecf40266a2368ad335a5fdde43471f545a967109cd62146ecf8b66ff" + +[[package]] +name = "nom" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +dependencies = [ + "memchr", + "version_check 0.1.5", +] + +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "lexical-core", + "memchr", + "version_check 0.9.4", +] + +[[package]] +name = "nom" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" +dependencies = [ + "memchr", + "minimal-lexical", + "version_check 0.9.4", +] + +[[package]] +name = "nom_locate" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37794436ca3029a3089e0b95d42da1f0b565ad271e4d3bb4bad0c7bb70b10605" +dependencies = [ + "bytecount", + "memchr", + "nom 7.1.0", +] + +[[package]] +name = "notify" +version = "4.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257" +dependencies = [ + "bitflags 1.3.2", + "filetime", + "fsevent", + "fsevent-sys", + "inotify", + "libc", + "mio 0.6.23", + "mio-extras", + "walkdir", + "winapi 0.3.9", +] + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg 1.0.1", + "num-integer", + "num-traits 0.2.14", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg 1.0.1", + "num-traits 0.2.14", +] + +[[package]] +name = "num-rational" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" +dependencies = [ + "autocfg 1.0.1", + "num-integer", + "num-traits 0.2.14", +] + +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +dependencies = [ + "num-traits 0.2.14", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg 1.0.1", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170" + +[[package]] +name = "once_cell" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" + +[[package]] +name = "onig" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ddfe2c93bb389eea6e6d713306880c7f6dcc99a75b659ce145d962c861b225" +dependencies = [ + "bitflags 1.3.2", + "lazy_static", + "libc", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd3eee045c84695b53b20255bb7317063df090b68e18bfac0abb6c39cf7f33e" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" +dependencies = [ + "bitflags 1.3.2", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" +dependencies = [ + "autocfg 1.0.1", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "owned-read" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66d1e235abcebc845cf93550b89b74f468c051496fafb433ede4104b9f71ba1" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "owning_ref" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "parking_lot" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" +dependencies = [ + "lock_api 0.3.4", + "parking_lot_core 0.6.2", + "rustc_version", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api 0.4.5", + "parking_lot_core 0.8.5", +] + +[[package]] +name = "parking_lot_core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi", + "libc", + "redox_syscall 0.1.57", + "rustc_version", + "smallvec 0.6.14", + "winapi 0.3.9", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall 0.2.10", + "smallvec 1.8.0", + "winapi 0.3.9", +] + +[[package]] +name = "pear" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5320f212db967792b67cfe12bd469d08afd6318a249bd917d5c19bc92200ab8a" +dependencies = [ + "pear_codegen", +] + +[[package]] +name = "pear_codegen" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfc1c836fdc3d1ef87c348b237b5b5c4dff922156fb2d968f57734f9669768ca" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.15.44", + "version_check 0.9.4", + "yansi", +] + +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared", + "rand 0.7.3", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +dependencies = [ + "proc-macro2 1.0.36", + "quote 1.0.15", + "syn 1.0.86", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" + +[[package]] +name = "pin-project-lite" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" + +[[package]] +name = "plist" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225" +dependencies = [ + "base64 0.13.0", + "indexmap", + "line-wrap", + "serde 1.0.136", + "time 0.3.5", + "xml-rs", +] + +[[package]] +name = "plume" +version = "0.7.1" +dependencies = [ + "activitypub", + "atom_syndication", + "chrono", + "clap", + "ctrlc", + "diesel", + "dotenv", + "gettext", + "gettext-macros", + "gettext-utils", + "guid-create", + "lettre_email", + "multipart", + "num_cpus", + "plume-api", + "plume-common", + "plume-models", + "riker", + "rocket", + "rocket_contrib", + "rocket_csrf", + "rocket_i18n", + "rsass", + "ructe", + "scheduled-thread-pool", + "serde 1.0.136", + "serde_json", + "shrinkwraprs", + "tracing", + "tracing-subscriber", + "validator", + "webfinger", +] + +[[package]] +name = "plume-api" +version = "0.7.1" +dependencies = [ + "serde 1.0.136", + "serde_derive", +] + +[[package]] +name = "plume-cli" +version = "0.7.1" +dependencies = [ + "clap", + "diesel", + "dotenv", + "plume-models", + "rpassword", +] + +[[package]] +name = "plume-common" +version = "0.7.1" +dependencies = [ + "activitypub", + "activitystreams-derive", + "activitystreams-traits", + "array_tool", + "askama_escape", + "base64 0.13.0", + "chrono", + "heck", + "hex", + "once_cell", + "openssl", + "pulldown-cmark", + "regex-syntax 0.6.25", + "reqwest 0.9.24", + "rocket", + "serde 1.0.136", + "serde_derive", + "serde_json", + "shrinkwraprs", + "syntect", + "tokio 0.1.22", + "tracing", +] + +[[package]] +name = "plume-front" +version = "0.7.1" +dependencies = [ + "console_error_panic_hook", + "gettext", + "gettext-macros", + "gettext-utils", + "js-sys", + "lazy_static", + "serde 1.0.136", + "serde_derive", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plume-macro" +version = "0.7.1" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.15.44", +] + +[[package]] +name = "plume-models" +version = "0.7.1" +dependencies = [ + "activitypub", + "ammonia", + "assert-json-diff", + "bcrypt", + "chrono", + "diesel", + "diesel-derive-newtype", + "diesel_migrations", + "glob", + "guid-create", + "itertools 0.10.3", + "lazy_static", + "ldap3", + "lettre", + "lindera-tantivy", + "migrations_internals", + "native-tls", + "once_cell", + "openssl", + "plume-api", + "plume-common", + "plume-macro", + "reqwest 0.9.24", + "riker", + "rocket", + "rocket_i18n", + "scheduled-thread-pool", + "serde 1.0.136", + "serde_derive", + "serde_json", + "shrinkwraprs", + "tantivy 0.13.3", + "tracing", + "url 2.2.2", + "walkdir", + "webfinger", + "whatlang", +] + +[[package]] +name = "polyval" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" +dependencies = [ + "cpuid-bool", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "pq-sys" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac25eee5a0582f45a67e837e350d784e7003bd29a5f460796772061ca49ffda" +dependencies = [ + "vcpkg", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2 1.0.36", + "quote 1.0.15", + "syn 1.0.86", + "version_check 0.9.4", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2 1.0.36", + "quote 1.0.15", + "version_check 0.9.4", +] + +[[package]] +name = "proc-macro-hack" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7f95648580798cc44ff8efb9bb0d7ee5205ea32e087b31b0732f3e8c2648ee2" +dependencies = [ + "proc-macro-hack-impl", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro-hack-impl" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be55bf0ae1635f4d7c7ddd6efc05c631e98a82104a73d35550bbc52db960027" + +[[package]] +name = "proc-macro2" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd07deb3c6d1d9ff827999c7f9b04cdfd66b1b17ae508e14fe47b620f2282ae0" +dependencies = [ + "unicode-xid 0.1.0", +] + +[[package]] +name = "proc-macro2" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b06e2f335f48d24442b35a19df506a835fb3547bc3c06ef27340da9acf5cae7" +dependencies = [ + "unicode-xid 0.1.0", +] + +[[package]] +name = "proc-macro2" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +dependencies = [ + "unicode-xid 0.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid 0.2.2", +] + +[[package]] +name = "publicsuffix" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b4ce31ff0a27d93c8de1849cf58162283752f065a90d508f1105fa6c9a213f" +dependencies = [ + "idna 0.2.3", + "url 2.2.2", +] + +[[package]] +name = "pulldown-cmark" +version = "0.8.0" +source = "git+https://git.joinplu.me/Plume/pulldown-cmark?branch=bidi-plume#58514a67a52d0fa2233160bd0e8933684397bfd6" +dependencies = [ + "bitflags 1.3.2", + "memchr", + "unicase 2.6.0", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-xml" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b" +dependencies = [ + "encoding_rs", + "memchr", +] + +[[package]] +name = "quote" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1eca14c727ad12702eb4b6bfb5a232287dcf8385cb8ca83a3eeaf6519c44c408" +dependencies = [ + "proc-macro2 0.2.3", +] + +[[package]] +name = "quote" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9949cfe66888ffe1d53e6ec9d9f3b70714083854be20fd5e271b232a017401e8" +dependencies = [ + "proc-macro2 0.3.8", +] + +[[package]] +name = "quote" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +dependencies = [ + "proc-macro2 0.4.30", +] + +[[package]] +name = "quote" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +dependencies = [ + "proc-macro2 1.0.36", +] + +[[package]] +name = "r2d2" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f" +dependencies = [ + "log 0.4.14", + "parking_lot 0.11.2", + "scheduled-thread-pool", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi 0.3.9", +] + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.7", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc 0.1.0", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg 0.1.2", + "rand_xorshift", + "winapi 0.3.9", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", + "rand_pcg 0.2.1", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.7", + "rand_core 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.4", +] + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi 0.3.9", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi 0.3.9", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.7", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rayon" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" +dependencies = [ + "autocfg 1.0.1", + "crossbeam-deque 0.8.1", + "either 1.6.1", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" +dependencies = [ + "crossbeam-channel 0.5.2", + "crossbeam-deque 0.8.1", + "crossbeam-utils 0.8.6", + "lazy_static", + "num_cpus", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.6.25", +] + +[[package]] +name = "regex-syntax" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e931c58b93d86f080c734bfd2bce7dd0079ae2331235818133c8be7f422e20e" + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "reqwest" +version = "0.9.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f88643aea3c1343c804950d7bf983bd2067f5ab59db6d613a08e05572f2714ab" +dependencies = [ + "base64 0.10.1", + "bytes 0.4.12", + "cookie 0.12.0", + "cookie_store", + "encoding_rs", + "flate2", + "futures 0.1.31", + "http 0.1.21", + "hyper 0.12.36", + "hyper-tls 0.3.2", + "log 0.4.14", + "mime 0.3.16", + "mime_guess", + "native-tls", + "serde 1.0.136", + "serde_json", + "serde_urlencoded 0.5.5", + "socks", + "time 0.1.43", + "tokio 0.1.22", + "tokio-executor", + "tokio-io", + "tokio-threadpool", + "tokio-timer", + "url 1.7.2", + "uuid 0.7.4", + "winreg 0.6.2", +] + +[[package]] +name = "reqwest" +version = "0.10.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0718f81a8e14c4dbb3b34cf23dc6aaf9ab8a0dfec160c534b3dbca1aaa21f47c" +dependencies = [ + "base64 0.13.0", + "bytes 0.5.6", + "encoding_rs", + "futures-core", + "futures-util", + "http 0.2.6", + "http-body 0.3.1", + "hyper 0.13.10", + "hyper-tls 0.4.3", + "ipnet", + "js-sys", + "lazy_static", + "log 0.4.14", + "mime 0.3.16", + "mime_guess", + "native-tls", + "percent-encoding 2.1.0", + "pin-project-lite 0.2.8", + "serde 1.0.136", + "serde_urlencoded 0.7.0", + "tokio 0.2.25", + "tokio-tls", + "url 2.2.2", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.7.0", +] + +[[package]] +name = "riker" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abff93ece5a5d3d7f2c54dfba7550657a644c9dc0a871c7ddf8c31381971c41b" +dependencies = [ + "chrono", + "config", + "dashmap", + "futures 0.3.19", + "num_cpus", + "pin-utils", + "rand 0.7.3", + "regex", + "riker-macros", + "slog", + "slog-scope", + "slog-stdlog", + "uuid 0.8.2", +] + +[[package]] +name = "riker-macros" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2a8e8f71c9e7980a596c39c7e3537ea8563054526e15712a610ac97a02dba15" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.15.44", +] + +[[package]] +name = "ring" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4db68a2e35f3497146b7e4563df7d4773a2433230c5e4b448328e31740458a" +dependencies = [ + "cc", + "lazy_static", + "libc", + "untrusted", +] + +[[package]] +name = "rocket" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a7ab1dfdc75bb8bd2be381f37796b1b300c45a3c9145b34d86715e8dd90bf28" +dependencies = [ + "atty", + "base64 0.13.0", + "log 0.4.14", + "memchr", + "num_cpus", + "pear", + "rocket_codegen", + "rocket_http", + "state", + "time 0.1.43", + "toml 0.4.10", + "version_check 0.9.4", + "yansi", +] + +[[package]] +name = "rocket_codegen" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729e687d6d2cf434d174da84fb948f7fef4fac22d20ce94ca61c28b72dbcf9f" +dependencies = [ + "devise", + "glob", + "indexmap", + "quote 0.6.13", + "rocket_http", + "version_check 0.9.4", + "yansi", +] + +[[package]] +name = "rocket_contrib" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b6303dccab46dce6c7ac26c9b9d8d8cde1b19614b027c3f913be6611bff6d9b" +dependencies = [ + "log 0.4.14", + "notify", + "rocket", + "serde 1.0.136", + "serde_json", +] + +[[package]] +name = "rocket_csrf" +version = "0.1.0" +source = "git+https://github.com/fdb-hiroshima/rocket_csrf?rev=29910f2829e7e590a540da3804336577b48c7b31#29910f2829e7e590a540da3804336577b48c7b31" +dependencies = [ + "data-encoding", + "ring", + "rocket", + "serde 1.0.136", + "time 0.1.43", +] + +[[package]] +name = "rocket_http" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6131e6e6d38a9817f4a494ff5da95971451c2eb56a53915579fc9c80f6ef0117" +dependencies = [ + "cookie 0.11.4", + "hyper 0.10.16", + "indexmap", + "pear", + "percent-encoding 1.0.1", + "smallvec 1.8.0", + "state", + "time 0.1.43", + "unicode-xid 0.1.0", +] + +[[package]] +name = "rocket_i18n" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf9f4c872b824ac0506557be9c66e0315d66d0e378d2ae02ee2e7b0fed2a338" +dependencies = [ + "gettext", + "rocket", +] + +[[package]] +name = "rpassword" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf099a1888612545b683d2661a1940089f6c2e5a8e38979b2159da876bfd956" +dependencies = [ + "libc", + "serde 1.0.136", + "serde_json", + "winapi 0.3.9", +] + +[[package]] +name = "rsass" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d74a71f39f2d0e35ada983c76aeaa3a58b6c5735c8865073a87d190219c64eb1" +dependencies = [ + "fastrand", + "lazy_static", + "nom 7.1.0", + "nom_locate", + "num-bigint", + "num-integer", + "num-rational", + "num-traits 0.2.14", +] + +[[package]] +name = "ructe" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef88d8c2492d7266e264b31e0ffcf1149d5ba183bccd3abaf1483ee905fc85de" +dependencies = [ + "base64 0.13.0", + "bytecount", + "itertools 0.10.3", + "md5", + "nom 7.1.0", +] + +[[package]] +name = "rust-ini" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2" + +[[package]] +name = "rust-stemmers" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" +dependencies = [ + "serde 1.0.136", + "serde_derive", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi 0.3.9", +] + +[[package]] +name = "scheduled-thread-pool" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7" +dependencies = [ + "parking_lot 0.11.2", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "security-framework" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8" + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-hjson" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a4e0ea8a88553209f6cc6cfe8724ecad22e1acf372793c27d995290fe74f8" +dependencies = [ + "lazy_static", + "linked-hash-map 0.3.0", + "num-traits 0.1.43", + "regex", + "serde 0.8.23", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2 1.0.36", + "quote 1.0.15", + "syn 1.0.86", +] + +[[package]] +name = "serde_json" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +dependencies = [ + "itoa 1.0.1", + "ryu", + "serde 1.0.136", +] + +[[package]] +name = "serde_test" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "110b3dbdf8607ec493c22d5d947753282f3bae73c0f56d322af1e8c78e4c23d5" +dependencies = [ + "serde 0.8.23", +] + +[[package]] +name = "serde_urlencoded" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "642dd69105886af2efd227f75a520ec9b44a820d65bc133a9131f7d229fd165a" +dependencies = [ + "dtoa", + "itoa 0.4.8", + "serde 1.0.136", + "url 1.7.2", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +dependencies = [ + "form_urlencoded", + "itoa 0.4.8", + "ryu", + "serde 1.0.136", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpufeatures", + "digest", + "opaque-debug", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shrinkwraprs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e63e6744142336dfb606fe2b068afa2e1cca1ee6a5d8377277a92945d81fa331" +dependencies = [ + "bitflags 1.3.2", + "itertools 0.8.2", + "proc-macro2 1.0.36", + "quote 1.0.15", + "syn 1.0.86", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b" + +[[package]] +name = "slab" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" + +[[package]] +name = "slog" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" + +[[package]] +name = "slog-scope" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f95a4b4c3274cd2869549da82b57ccc930859bdbf5bcea0424bc5f140b3c786" +dependencies = [ + "arc-swap", + "lazy_static", + "slog", +] + +[[package]] +name = "slog-stdlog" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8228ab7302adbf4fcb37e66f3cda78003feb521e7fd9e3847ec117a7784d0f5a" +dependencies = [ + "log 0.4.14", + "slog", + "slog-scope", +] + +[[package]] +name = "smallvec" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" +dependencies = [ + "maybe-uninit", +] + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "snap" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45456094d1983e2ee2a18fdfebce3189fa451699d0502cb8e3b49dba5ba41451" + +[[package]] +name = "socket2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "socks" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30f86c7635fadf2814201a4f67efefb0007588ae7422ce299f354ab5c97f61ae" +dependencies = [ + "byteorder", + "libc", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "state" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3015a7d0a5fd5105c91c3710d42f9ccf0abfb287d62206484dcc67f9569a6483" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d" +dependencies = [ + "bytes 0.4.12", +] + +[[package]] +name = "string_cache" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923f0f39b6267d37d23ce71ae7235602134b250ace715dd2c90421998ddac0c6" +dependencies = [ + "lazy_static", + "new_debug_unreachable", + "parking_lot 0.11.2", + "phf_shared", + "precomputed-hash", + "serde 1.0.136", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2 1.0.36", + "quote 1.0.15", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c97c05b8ebc34ddd6b967994d5c6e9852fa92f8b82b3858c39451f97346dcce5" +dependencies = [ + "proc-macro2 0.2.3", + "quote 0.4.2", + "unicode-xid 0.1.0", +] + +[[package]] +name = "syn" +version = "0.13.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f9bf6292f3a61d2c716723fdb789a41bbe104168e6f496dc6497e531ea1b9b" +dependencies = [ + "proc-macro2 0.3.8", + "quote 0.5.2", + "unicode-xid 0.1.0", +] + +[[package]] +name = "syn" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261ae9ecaa397c42b960649561949d69311f08eeaea86a65696e6e46517cf741" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid 0.1.0", +] + +[[package]] +name = "syn" +version = "0.15.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid 0.1.0", +] + +[[package]] +name = "syn" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +dependencies = [ + "proc-macro2 1.0.36", + "quote 1.0.15", + "unicode-xid 0.2.2", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2 1.0.36", + "quote 1.0.15", + "syn 1.0.86", + "unicode-xid 0.2.2", +] + +[[package]] +name = "syntect" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b20815bbe80ee0be06e6957450a841185fcf690fe0178f14d77a05ce2caa031" +dependencies = [ + "bincode", + "bitflags 1.3.2", + "flate2", + "fnv", + "lazy_static", + "lazycell", + "onig", + "plist", + "regex-syntax 0.6.25", + "serde 1.0.136", + "serde_derive", + "serde_json", + "walkdir", + "yaml-rust", +] + +[[package]] +name = "tantivy" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3012f53bceda69a3a50a510a3d53fbf6af60b3c4df2801a4c9c5132d238919" +dependencies = [ + "atomicwrites", + "base64 0.12.3", + "bitpacking", + "byteorder", + "census", + "chrono", + "crc32fast", + "crossbeam 0.7.3", + "downcast-rs", + "fail", + "failure", + "fnv", + "fs2", + "futures 0.3.19", + "htmlescape", + "levenshtein_automata", + "log 0.4.14", + "memmap", + "murmurhash32", + "notify", + "num_cpus", + "once_cell", + "owned-read", + "owning_ref", + "rayon", + "regex", + "rust-stemmers", + "serde 1.0.136", + "serde_json", + "smallvec 1.8.0", + "snap", + "stable_deref_trait", + "tantivy-fst", + "tantivy-query-grammar 0.13.0", + "tempfile", + "uuid 0.8.2", + "winapi 0.3.9", +] + +[[package]] +name = "tantivy" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca90bddda472f39fdc74a031d61d52b08b1de97f2a704afae726a8004abb0d" +dependencies = [ + "base64 0.13.0", + "bitpacking", + "byteorder", + "census", + "chrono", + "crc32fast", + "crossbeam 0.8.1", + "downcast-rs", + "fail", + "fnv", + "fs2", + "futures 0.3.19", + "htmlescape", + "levenshtein_automata", + "log 0.4.14", + "lru", + "memmap", + "murmurhash32", + "num_cpus", + "once_cell", + "rayon", + "regex", + "rust-stemmers", + "serde 1.0.136", + "serde_json", + "smallvec 1.8.0", + "snap", + "stable_deref_trait", + "tantivy-fst", + "tantivy-query-grammar 0.14.0", + "tempfile", + "thiserror", + "uuid 0.8.2", + "winapi 0.3.9", +] + +[[package]] +name = "tantivy-fst" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb20cdc0d83e9184560bdde9cd60142dbb4af2e0f770e88fce45770495224205" +dependencies = [ + "byteorder", + "regex-syntax 0.4.2", + "utf8-ranges", +] + +[[package]] +name = "tantivy-query-grammar" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea03b8224ca9ff4ccfc7dfab790527c8a9d8edbc53f4677bdf6ba0fd8000c75" +dependencies = [ + "combine", +] + +[[package]] +name = "tantivy-query-grammar" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70864085b31ecd5af8f53a76506440ece1c426d187f3d72f4b722e238d2ce19a" +dependencies = [ + "combine", +] + +[[package]] +name = "tar" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if 1.0.0", + "fastrand", + "libc", + "redox_syscall 0.2.10", + "remove_dir_all", + "winapi 0.3.9", +] + +[[package]] +name = "tendril" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9ef557cb397a4f0a5a3a628f06515f78563f2209e64d47055d9dc6052bf5e33" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2 1.0.36", + "quote 1.0.15", + "syn 1.0.86", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "time" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad" +dependencies = [ + "itoa 0.4.8", + "libc", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "mio 0.6.23", + "num_cpus", + "tokio-codec", + "tokio-current-thread", + "tokio-executor", + "tokio-fs", + "tokio-io", + "tokio-reactor", + "tokio-sync", + "tokio-tcp", + "tokio-threadpool", + "tokio-timer", + "tokio-udp", + "tokio-uds", +] + +[[package]] +name = "tokio" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092" +dependencies = [ + "bytes 0.5.6", + "fnv", + "futures-core", + "iovec", + "lazy_static", + "libc", + "memchr", + "mio 0.6.23", + "mio-named-pipes", + "mio-uds", + "num_cpus", + "pin-project-lite 0.1.12", + "signal-hook-registry", + "slab", + "tokio-macros 0.2.6", + "winapi 0.3.9", +] + +[[package]] +name = "tokio" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +dependencies = [ + "bytes 1.1.0", + "libc", + "memchr", + "mio 0.8.0", + "pin-project-lite 0.2.8", + "socket2 0.4.4", + "tokio-macros 1.7.0", + "winapi 0.3.9", +] + +[[package]] +name = "tokio-buf" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb220f46c53859a4b7ec083e41dec9778ff0b1851c0942b211edb89e0ccdc46" +dependencies = [ + "bytes 0.4.12", + "either 1.6.1", + "futures 0.1.31", +] + +[[package]] +name = "tokio-codec" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b2998660ba0e70d18684de5d06b70b70a3a747469af9dea7618cc59e75976b" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "tokio-io", +] + +[[package]] +name = "tokio-current-thread" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1de0e32a83f131e002238d7ccde18211c0a5397f60cbfffcb112868c2e0e20e" +dependencies = [ + "futures 0.1.31", + "tokio-executor", +] + +[[package]] +name = "tokio-executor" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb2d1b8f4548dbf5e1f7818512e9c406860678f29c300cdf0ebac72d1a3a1671" +dependencies = [ + "crossbeam-utils 0.7.2", + "futures 0.1.31", +] + +[[package]] +name = "tokio-fs" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297a1206e0ca6302a0eed35b700d292b275256f596e2f3fea7729d5e629b6ff4" +dependencies = [ + "futures 0.1.31", + "tokio-io", + "tokio-threadpool", +] + +[[package]] +name = "tokio-io" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "log 0.4.14", +] + +[[package]] +name = "tokio-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" +dependencies = [ + "proc-macro2 1.0.36", + "quote 1.0.15", + "syn 1.0.86", +] + +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +dependencies = [ + "proc-macro2 1.0.36", + "quote 1.0.15", + "syn 1.0.86", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio 1.17.0", +] + +[[package]] +name = "tokio-reactor" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09bc590ec4ba8ba87652da2068d150dcada2cfa2e07faae270a5e0409aa51351" +dependencies = [ + "crossbeam-utils 0.7.2", + "futures 0.1.31", + "lazy_static", + "log 0.4.14", + "mio 0.6.23", + "num_cpus", + "parking_lot 0.9.0", + "slab", + "tokio-executor", + "tokio-io", + "tokio-sync", +] + +[[package]] +name = "tokio-stream" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" +dependencies = [ + "futures-core", + "pin-project-lite 0.2.8", + "tokio 1.17.0", +] + +[[package]] +name = "tokio-sync" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfe50152bc8164fcc456dab7891fa9bf8beaf01c5ee7e1dd43a397c3cf87dee" +dependencies = [ + "fnv", + "futures 0.1.31", +] + +[[package]] +name = "tokio-tcp" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98df18ed66e3b72e742f185882a9e201892407957e45fbff8da17ae7a7c51f72" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "iovec", + "mio 0.6.23", + "tokio-io", + "tokio-reactor", +] + +[[package]] +name = "tokio-threadpool" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df720b6581784c118f0eb4310796b12b1d242a7eb95f716a8367855325c25f89" +dependencies = [ + "crossbeam-deque 0.7.4", + "crossbeam-queue 0.2.3", + "crossbeam-utils 0.7.2", + "futures 0.1.31", + "lazy_static", + "log 0.4.14", + "num_cpus", + "slab", + "tokio-executor", +] + +[[package]] +name = "tokio-timer" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93044f2d313c95ff1cb7809ce9a7a05735b012288a888b62d4434fd58c94f296" +dependencies = [ + "crossbeam-utils 0.7.2", + "futures 0.1.31", + "slab", + "tokio-executor", +] + +[[package]] +name = "tokio-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" +dependencies = [ + "native-tls", + "tokio 0.2.25", +] + +[[package]] +name = "tokio-udp" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2a0b10e610b39c38b031a2fcab08e4b82f16ece36504988dcbd81dbba650d82" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "log 0.4.14", + "mio 0.6.23", + "tokio-codec", + "tokio-io", + "tokio-reactor", +] + +[[package]] +name = "tokio-uds" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab57a4ac4111c8c9dbcf70779f6fc8bc35ae4b2454809febac840ad19bd7e4e0" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "iovec", + "libc", + "log 0.4.14", + "mio 0.6.23", + "mio-uds", + "tokio-codec", + "tokio-io", + "tokio-reactor", +] + +[[package]] +name = "tokio-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +dependencies = [ + "bytes 0.5.6", + "futures-core", + "futures-sink", + "log 0.4.14", + "pin-project-lite 0.1.12", + "tokio 0.2.25", +] + +[[package]] +name = "tokio-util" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64910e1b9c1901aaf5375561e35b9c057d95ff41a44ede043a03e09279eabaf1" +dependencies = [ + "bytes 1.1.0", + "futures-core", + "futures-sink", + "log 0.4.14", + "pin-project-lite 0.2.8", + "tokio 1.17.0", +] + +[[package]] +name = "toml" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758664fc71a3a69038656bee8b6be6477d2a6c315a6b81f7081f591bffa4111f" +dependencies = [ + "serde 1.0.136", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde 1.0.136", +] + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1bdf54a7c28a2bbf701e1d2233f6c77f473486b94bee4f9678da5a148dca7f" +dependencies = [ + "cfg-if 1.0.0", + "log 0.4.14", + "pin-project-lite 0.2.8", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e65ce065b4b5c53e73bb28912318cb8c9e9ad3921f1d669eb0e68b4c8143a2b" +dependencies = [ + "proc-macro2 1.0.36", + "quote 1.0.15", + "syn 1.0.86", +] + +[[package]] +name = "tracing-core" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03cfcb51380632a72d3111cb8d3447a8d908e577d31beeac006f836383d29a23" +dependencies = [ + "lazy_static", + "valuable", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-log" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3" +dependencies = [ + "lazy_static", + "log 0.4.14", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e0ab7bdc962035a87fba73f3acca9b8a8d0034c2e6f60b84aeaaddddc155dce" +dependencies = [ + "ansi_term", + "sharded-slab", + "smallvec 1.8.0", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "traitobject" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "try_from" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "283d3b89e1368717881a9d51dad843cc435380d8109c9e47d38780a324698d8b" +dependencies = [ + "cfg-if 0.1.10", +] + +[[package]] +name = "twoway" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" +dependencies = [ + "memchr", +] + +[[package]] +name = "typeable" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "unicase" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33" +dependencies = [ + "version_check 0.1.5", +] + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check 0.9.4", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cd1f4b4e96b46aeb8d4855db4a7a9bd96eeeb5c6a1ab54593328761642ce2f" + +[[package]] +name = "url" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" +dependencies = [ + "idna 0.1.5", + "matches", + "percent-encoding 1.0.1", +] + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna 0.2.3", + "matches", + "percent-encoding 2.1.0", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-ranges" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ae116fef2b7fea257ed6440d3cfcff7f190865f170cdad00bb6465bf18ecba" + +[[package]] +name = "uuid" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a" +dependencies = [ + "rand 0.6.5", +] + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.4", + "serde 1.0.136", +] + +[[package]] +name = "validator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0f08911ab0fee2c5009580f04615fa868898ee57de10692a45da0c3bcc3e5e" +dependencies = [ + "idna 0.2.3", + "lazy_static", + "regex", + "serde 1.0.136", + "serde_derive", + "serde_json", + "url 2.2.2", + "validator_derive", + "validator_types", +] + +[[package]] +name = "validator_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85135714dba11a1bd0b3eb1744169266f1a38977bf4e3ff5e2e1acb8c2b7eee" +dependencies = [ + "if_chain", + "lazy_static", + "proc-macro-error", + "proc-macro2 1.0.36", + "quote 1.0.15", + "regex", + "syn 1.0.86", + "validator_types", +] + +[[package]] +name = "validator_types" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded9d97e1d42327632f5f3bae6403c04886e2de3036261ef42deebd931a6a291" +dependencies = [ + "proc-macro2 1.0.36", + "syn 1.0.86", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi 0.3.9", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6395efa4784b027708f7451087e647ec73cc74f5d9bc2e418404248d679a230" +dependencies = [ + "futures 0.1.31", + "log 0.4.14", + "try-lock", +] + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log 0.4.14", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasm-bindgen" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +dependencies = [ + "cfg-if 1.0.0", + "serde 1.0.136", + "serde_json", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +dependencies = [ + "bumpalo", + "lazy_static", + "log 0.4.14", + "proc-macro2 1.0.36", + "quote 1.0.15", + "syn 1.0.86", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +dependencies = [ + "quote 1.0.15", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +dependencies = [ + "proc-macro2 1.0.36", + "quote 1.0.15", + "syn 1.0.86", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" + +[[package]] +name = "web-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webfinger" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec24b1b0700d4b466d280228ed0f62274eedeaa80206820f071fdc8ed787b664" +dependencies = [ + "reqwest 0.9.24", + "serde 1.0.136", + "serde_derive", +] + +[[package]] +name = "whatlang" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349357fdf0f049dcb402da4a4c5a5aae80a7f6b3e5976b38475ce4ac18e5cd2f" +dependencies = [ + "hashbrown 0.7.2", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winreg" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "winutil" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7daf138b6b14196e3830a588acf1e86966c694d3e8fb026fb105b8b5dca07e6e" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "xattr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" +dependencies = [ + "libc", +] + +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + +[[package]] +name = "xml5ever" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9234163818fd8e2418fcde330655e757900d4236acd8cc70fef345ef91f6d865" +dependencies = [ + "log 0.4.14", + "mac", + "markup5ever", + "time 0.1.43", +] + +[[package]] +name = "yada" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87bb6793d892781be4f1f8d420d6a75bc2e80b3cb365dfa7efad337f50871a5" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map 0.5.4", +] + +[[package]] +name = "yansi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71" + +[[package]] +name = "zeroize" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb5728b8afd3f280a869ce1d4c554ffaed35f45c231fc41bfbd0381bef50317" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000000..f5731334fba --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,79 @@ +[package] +authors = ["Plume contributors"] +name = "plume" +version = "0.7.1" +repository = "https://github.com/Plume-org/Plume" +edition = "2018" + +[dependencies] +activitypub = "0.1.3" +atom_syndication = "0.11.0" +clap = "2.33" +dotenv = "0.15.0" +gettext = "0.4.0" +gettext-macros = "0.6.1" +gettext-utils = "0.1.0" +guid-create = "0.2" +lettre_email = "0.9.2" +num_cpus = "1.10" +rocket = "0.4.6" +rocket_contrib = { version = "0.4.5", features = ["json"] } +rocket_i18n = "0.4.1" +scheduled-thread-pool = "0.2.2" +serde = "1.0" +serde_json = "1.0.79" +shrinkwraprs = "0.3.0" +validator = { version = "0.14", features = ["derive"] } +webfinger = "0.4.1" +tracing = "0.1.32" +tracing-subscriber = "0.3.9" +riker = "0.4.2" + +[[bin]] +name = "plume" +path = "src/main.rs" + +[dependencies.chrono] +features = ["serde"] +version = "0.4" + +[dependencies.ctrlc] +features = ["termination"] +version = "3.1.2" + +[dependencies.diesel] +features = ["r2d2", "chrono"] +version = "1.4.5" + +[dependencies.multipart] +default-features = false +features = ["server"] +version = "0.18" + +[dependencies.plume-api] +path = "plume-api" + +[dependencies.plume-common] +path = "plume-common" + +[dependencies.plume-models] +path = "plume-models" + +[dependencies.rocket_csrf] +git = "https://github.com/fdb-hiroshima/rocket_csrf" +rev = "29910f2829e7e590a540da3804336577b48c7b31" + +[build-dependencies] +ructe = "0.14.0" +rsass = "0.24" + +[features] +default = ["postgres"] +postgres = ["plume-models/postgres", "diesel/postgres"] +sqlite = ["plume-models/sqlite", "diesel/sqlite"] +debug-mailer = [] +test = [] +search-lindera = ["plume-models/search-lindera"] + +[workspace] +members = ["plume-api", "plume-cli", "plume-models", "plume-common", "plume-front", "plume-macro"] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000000..f6196e73e27 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +FROM rust:1-buster as builder + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + gettext \ + postgresql-client \ + libpq-dev \ + git \ + curl \ + gcc \ + make \ + openssl \ + libssl-dev \ + clang + +WORKDIR /scratch +COPY script/wasm-deps.sh . +RUN chmod a+x ./wasm-deps.sh && sleep 1 && ./wasm-deps.sh + +WORKDIR /app +COPY Cargo.toml Cargo.lock rust-toolchain ./ +RUN cargo install wasm-pack + +COPY . . + +RUN chmod a+x ./script/plume-front.sh && sleep 1 && ./script/plume-front.sh +RUN cargo install --path ./ --force --no-default-features --features postgres +RUN cargo install --path plume-cli --force --no-default-features --features postgres +RUN cargo clean + +FROM debian:buster-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + libpq5 \ + libssl1.1 + +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/ + +CMD ["plume"] + +EXPOSE 7878 diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 00000000000..89b9fe16ef6 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,34 @@ +FROM rust:1-buster + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + gettext \ + postgresql-client \ + libpq-dev \ + git \ + curl \ + gcc \ + make \ + openssl \ + libssl-dev\ + clang + +WORKDIR /scratch +COPY script/wasm-deps.sh . +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 wasm-pack + +COPY . . + +RUN chmod a+x ./script/plume-front.sh && sleep 1 && ./script/plume-front.sh +RUN cargo install --path ./ --force --no-default-features --features postgres +RUN cargo install --path plume-cli --force --no-default-features --features postgres +RUN cargo clean + +CMD ["plume"] + +EXPOSE 7878 diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000000..be3f7b28e56 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 00000000000..d07f0715027 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +

+ Plume's logo + Plume +

+

+ CircleCI + Code coverage + + Docker Pulls + Liberapay patrons +

+

+ Website + — + Documentation + — + Contribute + — + Instances list +

+ +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*. + +## Features + +A lot of features are still missing, but what is already here should be quite stable. Current and planned features include: + +- **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. (Not implemented yet, but will be in 1.0) + +## Get involved + +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-blog:matrix.org`](https://matrix.to/#/#plume-blog:matrix.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/). diff --git a/assets/icons/trwnh/README.md b/assets/icons/trwnh/README.md new file mode 100644 index 00000000000..515059da510 --- /dev/null +++ b/assets/icons/trwnh/README.md @@ -0,0 +1,21 @@ +# plumeLogos +Logos designed for Plume, a federated blogging platform similar to Medium. Licensed under CC0. + +# 1st place design - "Feather" +Path | Filled | Silhouette +--- | --- | --- +![plumeFeather](https://raw.githubusercontent.com/trwnh/plumeLogos/master/plumeFeather/plumeFeather512.png) | ![plumeFeatherFilled](https://raw.githubusercontent.com/trwnh/plumeLogos/master/plumeFeatherFilled/plumeFeatherFilled512.png) | ![plumeFeatherBlack](https://raw.githubusercontent.com/trwnh/plumeLogos/master/plumeFeatherBlack/plumeFeatherBlack512.png) + +# 2nd place design - "Paragraphs" +Filled | Silhouette +--- | --- +![plumeParagraphs](https://raw.githubusercontent.com/trwnh/plumeLogos/master/plumeParagraphs/plumeParagraphs512.png) | ![plumeParagraphsBlack](https://raw.githubusercontent.com/trwnh/plumeLogos/master/plumeParagraphsBlack/plumeParagraphsBlack512.png) + +# Links +You may find me at the following locations: +- Website: http://trwnh.com +- ActivityPub: https://mastodon.social/@trwnh + +If you'd like to support me, you can do so: +- One-time: https://paypal.me/trwnh +- Recurring: https://liberapay.com/trwnh diff --git a/assets/icons/trwnh/avatar.png b/assets/icons/trwnh/avatar.png new file mode 100644 index 00000000000..126558136a9 Binary files /dev/null and b/assets/icons/trwnh/avatar.png differ diff --git a/assets/icons/trwnh/avatar.svg b/assets/icons/trwnh/avatar.svg new file mode 100644 index 00000000000..3bf3bce9e60 --- /dev/null +++ b/assets/icons/trwnh/avatar.svg @@ -0,0 +1,98 @@ + + + + + + + + + + image/svg+xml + + + + + + Abdullah Tarawneh (trwnh.com) + + + + + + trwnh + + + + + + + + + + + + + + + + diff --git a/assets/icons/trwnh/avatar2.png b/assets/icons/trwnh/avatar2.png new file mode 100644 index 00000000000..4fd4be7ed63 Binary files /dev/null and b/assets/icons/trwnh/avatar2.png differ diff --git a/assets/icons/trwnh/feather-black/plumeFeatherBlack.svg b/assets/icons/trwnh/feather-black/plumeFeatherBlack.svg new file mode 100644 index 00000000000..9777612dc94 --- /dev/null +++ b/assets/icons/trwnh/feather-black/plumeFeatherBlack.svg @@ -0,0 +1,93 @@ + + + + + Plume Logo - Feather (Black) + + + + + + image/svg+xml + + Plume Logo - Feather (Black) + + 2018/10/07 + + + Abdullah Tarawneh (trwnh.com) + + + A Plume concept logo, with a soft stylized feather. Solid black fill, no path. + + + trwnh + + + + + + + + + + + + + + diff --git a/assets/icons/trwnh/feather-black/plumeFeatherBlack128.png b/assets/icons/trwnh/feather-black/plumeFeatherBlack128.png new file mode 100644 index 00000000000..47184ea503d Binary files /dev/null and b/assets/icons/trwnh/feather-black/plumeFeatherBlack128.png differ diff --git a/assets/icons/trwnh/feather-black/plumeFeatherBlack144.png b/assets/icons/trwnh/feather-black/plumeFeatherBlack144.png new file mode 100644 index 00000000000..524829e26fc Binary files /dev/null and b/assets/icons/trwnh/feather-black/plumeFeatherBlack144.png differ diff --git a/assets/icons/trwnh/feather-black/plumeFeatherBlack16.png b/assets/icons/trwnh/feather-black/plumeFeatherBlack16.png new file mode 100644 index 00000000000..d02ee464279 Binary files /dev/null and b/assets/icons/trwnh/feather-black/plumeFeatherBlack16.png differ diff --git a/assets/icons/trwnh/feather-black/plumeFeatherBlack160.png b/assets/icons/trwnh/feather-black/plumeFeatherBlack160.png new file mode 100644 index 00000000000..2e8a35fc879 Binary files /dev/null and b/assets/icons/trwnh/feather-black/plumeFeatherBlack160.png differ diff --git a/assets/icons/trwnh/feather-black/plumeFeatherBlack192.png b/assets/icons/trwnh/feather-black/plumeFeatherBlack192.png new file mode 100644 index 00000000000..fc553bef41a Binary files /dev/null and b/assets/icons/trwnh/feather-black/plumeFeatherBlack192.png differ diff --git a/assets/icons/trwnh/feather-black/plumeFeatherBlack24.png b/assets/icons/trwnh/feather-black/plumeFeatherBlack24.png new file mode 100644 index 00000000000..773b7d0962f Binary files /dev/null and b/assets/icons/trwnh/feather-black/plumeFeatherBlack24.png differ diff --git a/assets/icons/trwnh/feather-black/plumeFeatherBlack256.png b/assets/icons/trwnh/feather-black/plumeFeatherBlack256.png new file mode 100644 index 00000000000..058b31b5451 Binary files /dev/null and b/assets/icons/trwnh/feather-black/plumeFeatherBlack256.png differ diff --git a/assets/icons/trwnh/feather-black/plumeFeatherBlack32.png b/assets/icons/trwnh/feather-black/plumeFeatherBlack32.png new file mode 100644 index 00000000000..86de52217fc Binary files /dev/null and b/assets/icons/trwnh/feather-black/plumeFeatherBlack32.png differ diff --git a/assets/icons/trwnh/feather-black/plumeFeatherBlack36.png b/assets/icons/trwnh/feather-black/plumeFeatherBlack36.png new file mode 100644 index 00000000000..f0a7e91e168 Binary files /dev/null and b/assets/icons/trwnh/feather-black/plumeFeatherBlack36.png differ diff --git a/assets/icons/trwnh/feather-black/plumeFeatherBlack44.png b/assets/icons/trwnh/feather-black/plumeFeatherBlack44.png new file mode 100644 index 00000000000..1f31c6b848c Binary files /dev/null and b/assets/icons/trwnh/feather-black/plumeFeatherBlack44.png differ diff --git a/assets/icons/trwnh/feather-black/plumeFeatherBlack48.png b/assets/icons/trwnh/feather-black/plumeFeatherBlack48.png new file mode 100644 index 00000000000..585e7d7ecf6 Binary files /dev/null and b/assets/icons/trwnh/feather-black/plumeFeatherBlack48.png differ diff --git a/assets/icons/trwnh/feather-black/plumeFeatherBlack512.png b/assets/icons/trwnh/feather-black/plumeFeatherBlack512.png new file mode 100644 index 00000000000..5953cb3afd9 Binary files /dev/null and b/assets/icons/trwnh/feather-black/plumeFeatherBlack512.png differ diff --git a/assets/icons/trwnh/feather-black/plumeFeatherBlack64.png b/assets/icons/trwnh/feather-black/plumeFeatherBlack64.png new file mode 100644 index 00000000000..572981f410c Binary files /dev/null and b/assets/icons/trwnh/feather-black/plumeFeatherBlack64.png differ diff --git a/assets/icons/trwnh/feather-black/plumeFeatherBlack72.png b/assets/icons/trwnh/feather-black/plumeFeatherBlack72.png new file mode 100644 index 00000000000..35bb0eb0cb7 Binary files /dev/null and b/assets/icons/trwnh/feather-black/plumeFeatherBlack72.png differ diff --git a/assets/icons/trwnh/feather-black/plumeFeatherBlack80.png b/assets/icons/trwnh/feather-black/plumeFeatherBlack80.png new file mode 100644 index 00000000000..138fcdb5280 Binary files /dev/null and b/assets/icons/trwnh/feather-black/plumeFeatherBlack80.png differ diff --git a/assets/icons/trwnh/feather-black/plumeFeatherBlack96.png b/assets/icons/trwnh/feather-black/plumeFeatherBlack96.png new file mode 100644 index 00000000000..8d500b3d427 Binary files /dev/null and b/assets/icons/trwnh/feather-black/plumeFeatherBlack96.png differ diff --git a/assets/icons/trwnh/feather-filled/plumeFeatherFilled.svg b/assets/icons/trwnh/feather-filled/plumeFeatherFilled.svg new file mode 100644 index 00000000000..f0ef5a7d8f7 --- /dev/null +++ b/assets/icons/trwnh/feather-filled/plumeFeatherFilled.svg @@ -0,0 +1,93 @@ + + + + + Plume Logo - Feather (Filled) + + + + + + image/svg+xml + + Plume Logo - Feather (Filled) + 2018/10/07 + + + Abdullah Tarawneh (trwnh.com) + + + A Plume concept logo, with a soft stylized feather. Solid path, solid fill. + + + trwnh + + + + + + + + + + + + + + + diff --git a/assets/icons/trwnh/feather-filled/plumeFeatherFilled128.png b/assets/icons/trwnh/feather-filled/plumeFeatherFilled128.png new file mode 100644 index 00000000000..5b916107ff2 Binary files /dev/null and b/assets/icons/trwnh/feather-filled/plumeFeatherFilled128.png differ diff --git a/assets/icons/trwnh/feather-filled/plumeFeatherFilled144.png b/assets/icons/trwnh/feather-filled/plumeFeatherFilled144.png new file mode 100644 index 00000000000..157aa5501ea Binary files /dev/null and b/assets/icons/trwnh/feather-filled/plumeFeatherFilled144.png differ diff --git a/assets/icons/trwnh/feather-filled/plumeFeatherFilled16.png b/assets/icons/trwnh/feather-filled/plumeFeatherFilled16.png new file mode 100644 index 00000000000..25358770615 Binary files /dev/null and b/assets/icons/trwnh/feather-filled/plumeFeatherFilled16.png differ diff --git a/assets/icons/trwnh/feather-filled/plumeFeatherFilled160.png b/assets/icons/trwnh/feather-filled/plumeFeatherFilled160.png new file mode 100644 index 00000000000..8cceae1f66a Binary files /dev/null and b/assets/icons/trwnh/feather-filled/plumeFeatherFilled160.png differ diff --git a/assets/icons/trwnh/feather-filled/plumeFeatherFilled192.png b/assets/icons/trwnh/feather-filled/plumeFeatherFilled192.png new file mode 100644 index 00000000000..0b16f90d95f Binary files /dev/null and b/assets/icons/trwnh/feather-filled/plumeFeatherFilled192.png differ diff --git a/assets/icons/trwnh/feather-filled/plumeFeatherFilled24.png b/assets/icons/trwnh/feather-filled/plumeFeatherFilled24.png new file mode 100644 index 00000000000..49a94363928 Binary files /dev/null and b/assets/icons/trwnh/feather-filled/plumeFeatherFilled24.png differ diff --git a/assets/icons/trwnh/feather-filled/plumeFeatherFilled256.png b/assets/icons/trwnh/feather-filled/plumeFeatherFilled256.png new file mode 100644 index 00000000000..d97d58cbb9d Binary files /dev/null and b/assets/icons/trwnh/feather-filled/plumeFeatherFilled256.png differ diff --git a/assets/icons/trwnh/feather-filled/plumeFeatherFilled32.png b/assets/icons/trwnh/feather-filled/plumeFeatherFilled32.png new file mode 100644 index 00000000000..456eda70a2b Binary files /dev/null and b/assets/icons/trwnh/feather-filled/plumeFeatherFilled32.png differ diff --git a/assets/icons/trwnh/feather-filled/plumeFeatherFilled36.png b/assets/icons/trwnh/feather-filled/plumeFeatherFilled36.png new file mode 100644 index 00000000000..e02f3befcc3 Binary files /dev/null and b/assets/icons/trwnh/feather-filled/plumeFeatherFilled36.png differ diff --git a/assets/icons/trwnh/feather-filled/plumeFeatherFilled44.png b/assets/icons/trwnh/feather-filled/plumeFeatherFilled44.png new file mode 100644 index 00000000000..90a8a3dde29 Binary files /dev/null and b/assets/icons/trwnh/feather-filled/plumeFeatherFilled44.png differ diff --git a/assets/icons/trwnh/feather-filled/plumeFeatherFilled48.png b/assets/icons/trwnh/feather-filled/plumeFeatherFilled48.png new file mode 100644 index 00000000000..4637ce5d142 Binary files /dev/null and b/assets/icons/trwnh/feather-filled/plumeFeatherFilled48.png differ diff --git a/assets/icons/trwnh/feather-filled/plumeFeatherFilled512.png b/assets/icons/trwnh/feather-filled/plumeFeatherFilled512.png new file mode 100644 index 00000000000..5ecc967bfb4 Binary files /dev/null and b/assets/icons/trwnh/feather-filled/plumeFeatherFilled512.png differ diff --git a/assets/icons/trwnh/feather-filled/plumeFeatherFilled64.png b/assets/icons/trwnh/feather-filled/plumeFeatherFilled64.png new file mode 100644 index 00000000000..917c4189f2c Binary files /dev/null and b/assets/icons/trwnh/feather-filled/plumeFeatherFilled64.png differ diff --git a/assets/icons/trwnh/feather-filled/plumeFeatherFilled72.png b/assets/icons/trwnh/feather-filled/plumeFeatherFilled72.png new file mode 100644 index 00000000000..c9fe71b7093 Binary files /dev/null and b/assets/icons/trwnh/feather-filled/plumeFeatherFilled72.png differ diff --git a/assets/icons/trwnh/feather-filled/plumeFeatherFilled80.png b/assets/icons/trwnh/feather-filled/plumeFeatherFilled80.png new file mode 100644 index 00000000000..28247f5c9b3 Binary files /dev/null and b/assets/icons/trwnh/feather-filled/plumeFeatherFilled80.png differ diff --git a/assets/icons/trwnh/feather-filled/plumeFeatherFilled96.png b/assets/icons/trwnh/feather-filled/plumeFeatherFilled96.png new file mode 100644 index 00000000000..4e3efbf3463 Binary files /dev/null and b/assets/icons/trwnh/feather-filled/plumeFeatherFilled96.png differ diff --git a/assets/icons/trwnh/feather/plumeFeather.svg b/assets/icons/trwnh/feather/plumeFeather.svg new file mode 100644 index 00000000000..8a0e6907fa3 --- /dev/null +++ b/assets/icons/trwnh/feather/plumeFeather.svg @@ -0,0 +1,90 @@ + + + + + Plume Logo - Feather + + + + + + image/svg+xml + + Plume Logo - Feather + 2018/10/07 + + + Abdullah Tarawneh (trwnh.com) + + + + + trwnh + + + A Plume concept logo, with a soft stylized feather. Solid path, no fill. + + + + + + + + + + + + + diff --git a/assets/icons/trwnh/feather/plumeFeather128.png b/assets/icons/trwnh/feather/plumeFeather128.png new file mode 100644 index 00000000000..ad65345d7fa Binary files /dev/null and b/assets/icons/trwnh/feather/plumeFeather128.png differ diff --git a/assets/icons/trwnh/feather/plumeFeather144.png b/assets/icons/trwnh/feather/plumeFeather144.png new file mode 100644 index 00000000000..2f416c8055d Binary files /dev/null and b/assets/icons/trwnh/feather/plumeFeather144.png differ diff --git a/assets/icons/trwnh/feather/plumeFeather16.png b/assets/icons/trwnh/feather/plumeFeather16.png new file mode 100644 index 00000000000..ebdf0364545 Binary files /dev/null and b/assets/icons/trwnh/feather/plumeFeather16.png differ diff --git a/assets/icons/trwnh/feather/plumeFeather160.png b/assets/icons/trwnh/feather/plumeFeather160.png new file mode 100644 index 00000000000..c6ce87d7614 Binary files /dev/null and b/assets/icons/trwnh/feather/plumeFeather160.png differ diff --git a/assets/icons/trwnh/feather/plumeFeather192.png b/assets/icons/trwnh/feather/plumeFeather192.png new file mode 100644 index 00000000000..f1e7c26d44d Binary files /dev/null and b/assets/icons/trwnh/feather/plumeFeather192.png differ diff --git a/assets/icons/trwnh/feather/plumeFeather24.png b/assets/icons/trwnh/feather/plumeFeather24.png new file mode 100644 index 00000000000..07dcf51e64d Binary files /dev/null and b/assets/icons/trwnh/feather/plumeFeather24.png differ diff --git a/assets/icons/trwnh/feather/plumeFeather256.png b/assets/icons/trwnh/feather/plumeFeather256.png new file mode 100644 index 00000000000..748fbb75c85 Binary files /dev/null and b/assets/icons/trwnh/feather/plumeFeather256.png differ diff --git a/assets/icons/trwnh/feather/plumeFeather32.png b/assets/icons/trwnh/feather/plumeFeather32.png new file mode 100644 index 00000000000..de1d74bed03 Binary files /dev/null and b/assets/icons/trwnh/feather/plumeFeather32.png differ diff --git a/assets/icons/trwnh/feather/plumeFeather36.png b/assets/icons/trwnh/feather/plumeFeather36.png new file mode 100644 index 00000000000..67a66e773a7 Binary files /dev/null and b/assets/icons/trwnh/feather/plumeFeather36.png differ diff --git a/assets/icons/trwnh/feather/plumeFeather44.png b/assets/icons/trwnh/feather/plumeFeather44.png new file mode 100644 index 00000000000..a74981c960f Binary files /dev/null and b/assets/icons/trwnh/feather/plumeFeather44.png differ diff --git a/assets/icons/trwnh/feather/plumeFeather48.png b/assets/icons/trwnh/feather/plumeFeather48.png new file mode 100644 index 00000000000..5095334ac36 Binary files /dev/null and b/assets/icons/trwnh/feather/plumeFeather48.png differ diff --git a/assets/icons/trwnh/feather/plumeFeather512.png b/assets/icons/trwnh/feather/plumeFeather512.png new file mode 100644 index 00000000000..5a5848ea7ff Binary files /dev/null and b/assets/icons/trwnh/feather/plumeFeather512.png differ diff --git a/assets/icons/trwnh/feather/plumeFeather64.png b/assets/icons/trwnh/feather/plumeFeather64.png new file mode 100644 index 00000000000..954274d4fe6 Binary files /dev/null and b/assets/icons/trwnh/feather/plumeFeather64.png differ diff --git a/assets/icons/trwnh/feather/plumeFeather72.png b/assets/icons/trwnh/feather/plumeFeather72.png new file mode 100644 index 00000000000..1cf164946a3 Binary files /dev/null and b/assets/icons/trwnh/feather/plumeFeather72.png differ diff --git a/assets/icons/trwnh/feather/plumeFeather80.png b/assets/icons/trwnh/feather/plumeFeather80.png new file mode 100644 index 00000000000..2ce88c160e8 Binary files /dev/null and b/assets/icons/trwnh/feather/plumeFeather80.png differ diff --git a/assets/icons/trwnh/feather/plumeFeather96.png b/assets/icons/trwnh/feather/plumeFeather96.png new file mode 100644 index 00000000000..7ed50a31f50 Binary files /dev/null and b/assets/icons/trwnh/feather/plumeFeather96.png differ diff --git a/assets/icons/trwnh/ideas.svg b/assets/icons/trwnh/ideas.svg new file mode 100644 index 00000000000..dcef668068f --- /dev/null +++ b/assets/icons/trwnh/ideas.svg @@ -0,0 +1,937 @@ + + + + + + + + + + image/svg+xml + + + + + + + + Plume + + #23F0C7 + + #EF767A + + #7765E3 + + #6457A6 + + #FFE347 + Color Palette + Title Typeface: Playfair Display + UI Font: Route 159 + Plume + + + Plume + Plume + + + Plume + Plume + + + Plume + Plume + + + + + + + + + + + + + + + + + + + + + + + Plume + + + #23F0C7 + + #EF767A + + #7765E3 + + #6457A6 + + #FFE347 + Color Palette + Title Typeface: Playfair Display + UI Font: Inter UI + + + + + + + Plume + + + + + + + + Plume + + + + + + + + Plume + + + + + + + + Plume + + + + + + + Plume + 16px + 24px + 32px + 48px + 64px + 96px + + + + + + + + + diff --git a/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack.svg b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack.svg new file mode 100644 index 00000000000..346e4f239a9 --- /dev/null +++ b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack.svg @@ -0,0 +1,92 @@ + + + + + Plume Logo - Paragraphs (Black) + + + + + + image/svg+xml + + Plume Logo - Paragraphs (Black) + + + Abdullah Tarawneh (trwnh.com) + + + 2018/10/07 + + + + trwnh + + + A Plume concept logo, with a stylized paragraph symbol and paragraph blocks. Black silhouette. + + + + + + + + + + + + diff --git a/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack128.png b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack128.png new file mode 100644 index 00000000000..e2d8ccb68ca Binary files /dev/null and b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack128.png differ diff --git a/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack144.png b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack144.png new file mode 100644 index 00000000000..93330d92e2e Binary files /dev/null and b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack144.png differ diff --git a/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack16.png b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack16.png new file mode 100644 index 00000000000..f1381e6332d Binary files /dev/null and b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack16.png differ diff --git a/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack160.png b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack160.png new file mode 100644 index 00000000000..306d2d95254 Binary files /dev/null and b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack160.png differ diff --git a/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack192.png b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack192.png new file mode 100644 index 00000000000..91375b5a288 Binary files /dev/null and b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack192.png differ diff --git a/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack24.png b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack24.png new file mode 100644 index 00000000000..0688abc4f3a Binary files /dev/null and b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack24.png differ diff --git a/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack256.png b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack256.png new file mode 100644 index 00000000000..aa858163a19 Binary files /dev/null and b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack256.png differ diff --git a/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack32.png b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack32.png new file mode 100644 index 00000000000..f2669da463c Binary files /dev/null and b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack32.png differ diff --git a/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack36.png b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack36.png new file mode 100644 index 00000000000..abc8e94df34 Binary files /dev/null and b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack36.png differ diff --git a/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack44.png b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack44.png new file mode 100644 index 00000000000..d232beeed7b Binary files /dev/null and b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack44.png differ diff --git a/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack512.png b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack512.png new file mode 100644 index 00000000000..521e53ac193 Binary files /dev/null and b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack512.png differ diff --git a/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack64.png b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack64.png new file mode 100644 index 00000000000..c47ac9a512c Binary files /dev/null and b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack64.png differ diff --git a/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack72.png b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack72.png new file mode 100644 index 00000000000..35d8d71b994 Binary files /dev/null and b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack72.png differ diff --git a/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack80.png b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack80.png new file mode 100644 index 00000000000..dc3380d160e Binary files /dev/null and b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack80.png differ diff --git a/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack96.png b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack96.png new file mode 100644 index 00000000000..7e8f466b230 Binary files /dev/null and b/assets/icons/trwnh/paragraphs-black/plumeParagraphsBlack96.png differ diff --git a/assets/icons/trwnh/paragraphs/plumeParagraphs.svg b/assets/icons/trwnh/paragraphs/plumeParagraphs.svg new file mode 100644 index 00000000000..8d1dafb8894 --- /dev/null +++ b/assets/icons/trwnh/paragraphs/plumeParagraphs.svg @@ -0,0 +1,122 @@ + + + + + Plume Logo - Paragraphs + + + + + + image/svg+xml + + Plume Logo - Paragraphs + 2018/10/07 + + + Abdullah Tarawneh (trwnh.com) + + + + + trwnh + + + + A Plume concept logo, with a stylized paragraph symbol and paragraph blocks. Solid square. + + + + + + + + + + + + + + + + diff --git a/assets/icons/trwnh/paragraphs/plumeParagraphs128.png b/assets/icons/trwnh/paragraphs/plumeParagraphs128.png new file mode 100644 index 00000000000..b5250a64e9a Binary files /dev/null and b/assets/icons/trwnh/paragraphs/plumeParagraphs128.png differ diff --git a/assets/icons/trwnh/paragraphs/plumeParagraphs144.png b/assets/icons/trwnh/paragraphs/plumeParagraphs144.png new file mode 100644 index 00000000000..00ee7969bc8 Binary files /dev/null and b/assets/icons/trwnh/paragraphs/plumeParagraphs144.png differ diff --git a/assets/icons/trwnh/paragraphs/plumeParagraphs16.png b/assets/icons/trwnh/paragraphs/plumeParagraphs16.png new file mode 100644 index 00000000000..0feada8bc09 Binary files /dev/null and b/assets/icons/trwnh/paragraphs/plumeParagraphs16.png differ diff --git a/assets/icons/trwnh/paragraphs/plumeParagraphs160.png b/assets/icons/trwnh/paragraphs/plumeParagraphs160.png new file mode 100644 index 00000000000..d3af6f4e6ba Binary files /dev/null and b/assets/icons/trwnh/paragraphs/plumeParagraphs160.png differ diff --git a/assets/icons/trwnh/paragraphs/plumeParagraphs192.png b/assets/icons/trwnh/paragraphs/plumeParagraphs192.png new file mode 100644 index 00000000000..bf86407e9c6 Binary files /dev/null and b/assets/icons/trwnh/paragraphs/plumeParagraphs192.png differ diff --git a/assets/icons/trwnh/paragraphs/plumeParagraphs24.png b/assets/icons/trwnh/paragraphs/plumeParagraphs24.png new file mode 100644 index 00000000000..689ad7657de Binary files /dev/null and b/assets/icons/trwnh/paragraphs/plumeParagraphs24.png differ diff --git a/assets/icons/trwnh/paragraphs/plumeParagraphs256.png b/assets/icons/trwnh/paragraphs/plumeParagraphs256.png new file mode 100644 index 00000000000..82684b850b8 Binary files /dev/null and b/assets/icons/trwnh/paragraphs/plumeParagraphs256.png differ diff --git a/assets/icons/trwnh/paragraphs/plumeParagraphs32.png b/assets/icons/trwnh/paragraphs/plumeParagraphs32.png new file mode 100644 index 00000000000..258d178a6ab Binary files /dev/null and b/assets/icons/trwnh/paragraphs/plumeParagraphs32.png differ diff --git a/assets/icons/trwnh/paragraphs/plumeParagraphs36.png b/assets/icons/trwnh/paragraphs/plumeParagraphs36.png new file mode 100644 index 00000000000..841cbba60a1 Binary files /dev/null and b/assets/icons/trwnh/paragraphs/plumeParagraphs36.png differ diff --git a/assets/icons/trwnh/paragraphs/plumeParagraphs44.png b/assets/icons/trwnh/paragraphs/plumeParagraphs44.png new file mode 100644 index 00000000000..7df084ad6e0 Binary files /dev/null and b/assets/icons/trwnh/paragraphs/plumeParagraphs44.png differ diff --git a/assets/icons/trwnh/paragraphs/plumeParagraphs48.png b/assets/icons/trwnh/paragraphs/plumeParagraphs48.png new file mode 100644 index 00000000000..895b956d482 Binary files /dev/null and b/assets/icons/trwnh/paragraphs/plumeParagraphs48.png differ diff --git a/assets/icons/trwnh/paragraphs/plumeParagraphs512.png b/assets/icons/trwnh/paragraphs/plumeParagraphs512.png new file mode 100644 index 00000000000..965dd4590a7 Binary files /dev/null and b/assets/icons/trwnh/paragraphs/plumeParagraphs512.png differ diff --git a/assets/icons/trwnh/paragraphs/plumeParagraphs64.png b/assets/icons/trwnh/paragraphs/plumeParagraphs64.png new file mode 100644 index 00000000000..37cf4438a8c Binary files /dev/null and b/assets/icons/trwnh/paragraphs/plumeParagraphs64.png differ diff --git a/assets/icons/trwnh/paragraphs/plumeParagraphs72.png b/assets/icons/trwnh/paragraphs/plumeParagraphs72.png new file mode 100644 index 00000000000..e2918ee1c89 Binary files /dev/null and b/assets/icons/trwnh/paragraphs/plumeParagraphs72.png differ diff --git a/assets/icons/trwnh/paragraphs/plumeParagraphs80.png b/assets/icons/trwnh/paragraphs/plumeParagraphs80.png new file mode 100644 index 00000000000..f0296f2faeb Binary files /dev/null and b/assets/icons/trwnh/paragraphs/plumeParagraphs80.png differ diff --git a/assets/icons/trwnh/paragraphs/plumeParagraphs96.png b/assets/icons/trwnh/paragraphs/plumeParagraphs96.png new file mode 100644 index 00000000000..5017b0b3217 Binary files /dev/null and b/assets/icons/trwnh/paragraphs/plumeParagraphs96.png differ diff --git a/assets/images/audio-file.svg b/assets/images/audio-file.svg new file mode 100644 index 00000000000..8c8a699c3cb --- /dev/null +++ b/assets/images/audio-file.svg @@ -0,0 +1,65 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/assets/images/default-avatar.png b/assets/images/default-avatar.png new file mode 100644 index 00000000000..126558136a9 Binary files /dev/null and b/assets/images/default-avatar.png differ diff --git a/assets/images/feather-sprite.svg b/assets/images/feather-sprite.svg new file mode 100644 index 00000000000..0af400c24ef --- /dev/null +++ b/assets/images/feather-sprite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/unknown-file.svg b/assets/images/unknown-file.svg new file mode 100644 index 00000000000..79feb4d3277 --- /dev/null +++ b/assets/images/unknown-file.svg @@ -0,0 +1,65 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/assets/images/video-file.svg b/assets/images/video-file.svg new file mode 100644 index 00000000000..e1672df88f9 --- /dev/null +++ b/assets/images/video-file.svg @@ -0,0 +1,115 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/assets/themes/blog-monospace/theme.scss b/assets/themes/blog-monospace/theme.scss new file mode 100644 index 00000000000..5c97c2763e2 --- /dev/null +++ b/assets/themes/blog-monospace/theme.scss @@ -0,0 +1,3 @@ +* { + font-family: monospace; +} diff --git a/assets/themes/default/_article.scss b/assets/themes/default/_article.scss new file mode 100644 index 00000000000..453e2d58ae0 --- /dev/null +++ b/assets/themes/default/_article.scss @@ -0,0 +1,526 @@ +/* Heading */ +main header.article { + overflow: hidden; + background: $background; + color: $text-color; + display: grid; + background-size: cover; + background-position: center; + + &.illustrated { + min-height: 75vh; + color: $white; + + a, a:visited { + color: $white; + border-bottom: 1px solid transparent; + transition: border-bottom-color 0.1s ease-in; + + &:hover { + border-bottom-color: $white; + } + } + } + + & > * { + grid-row: 1; + grid-column: 1; + } + + & > div:not(.shadow) { + z-index: 3; + font-family: $lora; + font-size: 1.2em; + + bottom: 0; + left: 0; + right: 0; + max-width: $article-width; + margin: 2em auto; + + display: flex; + flex-direction: column; + justify-content: flex-end; + + h1, .article-info { + text-align: center; + } + } + + & > div.shadow { + z-index: 2; + + height: 100%; + width: 100%; + background: linear-gradient(180deg, transparent 20vh, $black 80vh); + } + + & > img { + z-index: 1; + min-width: 100%; + min-height: 100%; + background: $primary; + } +} + +main .article-info { + margin: 0 auto 3em; + font-size: 0.95em; + font-weight: 400; + + .author, .author a { + font-weight: 600; + } +} + +/* The article itself */ +main article { + max-width: $article-width; + margin: 2.5em auto; + font-family: $lora; + font-size: 1.2em; + line-height: 1.7; + + a:hover { + text-decoration: underline; + } + + img { + display: block; + margin: 3em auto; + max-width: 100%; + } + + pre { + padding: 1em; + background: $gray; + overflow: auto; + } + + blockquote { + border-inline-start: 5px solid $gray; + margin: 1em auto; + padding: 0em 2em; + } +} + +/* Metadata under the article */ +main .article-meta, main .article-meta button { + padding: 0; + font-size: 1.1em; + margin-top: 10%; +} + +main .article-meta { + + > * { + margin: $margin; + } + + > .banner { + margin: 3em 0; + & > * { + margin: $margin; + } + } + + > p { + margin: 2em $horizontal-margin; + font-size: 0.9em; + } + + /* Article Tags */ + .tags { + list-style: none; + padding: 0px; + max-width: none; + flex: 20; + + li { + display: inline-block; + padding: 0px; + margin: 0px 10px 10px 0px; + transition: all 0.2s ease-in; + border: 1px solid $primary; + + a { + display: inline-block; + padding: 10px 20px; + } + + &:hover { + background: transparentize($primary, 0.9); + } + } + } + + /* Likes & Boosts */ + .actions { + display: flex; + flex-direction: row; + justify-content: space-around; + } + + .likes, .reshares { + display: flex; + flex-direction: column; + align-items: center; + padding: 0.5em 0; + + p { + font-size: 1.5em; + display: inline-block; + margin: 0; + } + + .action { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin: 0; + padding: 0; + background: none; + color: $text-color; + border: none; + font-size: 1.1em; + cursor: pointer; + + svg.feather { + transition: background 0.1s ease-in; + display: flex; + align-items: center; + justify-content: center; + + margin: 0.5em 0; + width: 2.5em; + height: 2.5em; + + border-radius: 50%; + } + + &.reshared, &.liked { + svg.feather { + color: $background; + font-weight: 900; + } + } + } + } + + .likes { + p, .action:hover { color: $red; } + + .action svg.feather { + padding: 0.7em; + box-sizing: border-box; + color: $red; + fill: none; + border: solid $red thin; + } + + .action:hover svg.feather { + background: transparentize($red, 0.85); + } + + .action.liked svg.feather { + background: $red; + fill: currentColor; + } + .action.liked:hover svg.feather { + background: transparentize($red, 0.75) + color: $red; + } + } + + .reshares { + p, .action:hover { color: $primary; } + + .action svg.feather { + padding: 0.7em; + box-sizing: border-box; + color: $primary; + border: solid $primary thin; + font-weight: 600; + } + + .action:hover svg.feather { + background: transparentize($primary, 0.85); + } + + .action.reshared svg.feather { + background: $primary; + } + .action.reshared:hover svg.feather { + background: transparentize($primary, 0.75) + color: $primary; + } + } + + /* Comments */ + .comments { + margin: 0 $horizontal-margin; + + h2 { + color: $primary; + font-size: 1.5em; + font-weight: 600; + } + + summary { + cursor: pointer; + } + + /* New comment form */ + > form input[type="submit"] { + font-size: 1em; + -webkit-appearance: none; + } + + // Respond & delete comment buttons + a.button, form.inline, form.inline input { + padding: 0; + background: none; + color: $text-color; + margin-right: 2em; + font-family: $route159; + font-weight: normal; + + &::before { + color: $primary; + padding-right: 0.5em; + } + + &:hover { color: $primary; } + } + + .comment { + margin: 1em 0; + font-size: 1em; + border: none; + + .content { + background: $gray; + margin-top: 2.5em; + padding: 1em; + + &::before { + display: block; + content: ' '; + border: 1em solid $gray; + border-top-color: transparent; + border-right-color: transparent; + position: relative; + top: -2.4em; + left: -1em; + width: 0; + height: 0; + } + } + + header { + display: flex; + flex-direction: row; + justify-content: space-between; + } + + .dt-published a { + color: transparentize($text-color, 0.6); + } + + .author { + display: flex; + flex-direction: row; + align-items: center; + align-content: center; + + * { + transition: all 0.1s ease-in; + } + + .display-name { + color: $text-color; + } + + &:hover { + .display-name { color: $primary; } + small { opacity: 1; } + } + } + + & > .comment { + padding-left: 2em; + } + + .text { + padding: 1.25em 0; + font-family: $lora; + font-size: 1.1em; + line-height: 1.4; + text-align: left; + } + } + } +} + +#plume-editor { + header { + display: flex; + flex-direction: row-reverse; + background: transparent; + align-items: center; + justify-content: space-between; + button { + flex: 0 0 10em; + font-size: 1.25em; + margin: .5em 0em .5em 1em; + } + } + + & > * { + min-height: 1em; + outline: none; + margin-bottom: 0.5em; + } + + .placeholder { + color: transparentize($text-color, 0.6); + } + + article { + max-width: none; + min-height: 80vh; + } +} + +.popup { + position: fixed; + top: 15vh; + bottom: 20vh; + left: 20vw; + right: 20vw; + background: $gray; + border: 1px solid $primary; + z-index: 2; + padding: 2em; + overflow-y: auto; +} + +.popup:not(.show), .popup-bg:not(.show) { + display: none; + appearance: none; +} + +.popup-bg { + background: rgba(0, 0, 0, 0.1); + position: fixed; + top: 0px; + left: 0px; + right: 0px; + bottom: 0px; +} + + +/* Content warning */ +.cw-container { + position: relative; + display: inline-block; + cursor: pointer; + + img { + margin: auto; + } +} + +.cw-text { + display: none; + appearance: none; +} + +input[type="checkbox"].cw-checkbox { + display: none; +} + +input:checked ~ .cw-container:before { + content: " "; + position: absolute; + height: 100%; + width: 100%; + background: rgba(0, 0, 0, 1); +} + +input:checked ~ .cw-container > .cw-text { + display: inline; + position: absolute; + color: white; + width: 100%; + text-align: center; + top: 50%; + transform: translateY(-50%); +} + +/* Bottom action bar */ + +.bottom-bar { + z-index: 10; + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: $gray; + margin: 0; + display: flex; + + & > div { + margin: 1em; + } + + & > div:nth-child(2) { + flex: 1; + display: flex; + margin: auto $horizontal-margin; + } +} + +/* Footnote related styles */ +.footnote-definition { + p { + font-size: smaller; + /* Make sure the definition is inline with the label-definition */ + display: inline; + } +} + +// Small screens +@media screen and (max-width: 600px) { + #plume-editor header { + flex-direction: column-reverse; + + button { + flex: 0 0 0; + } + } + + .popup { + top: 10vh; + bottom: 10vh; + left: 1vw; + right: 1vw; + } + + main article { + margin: 2.5em .5em; + max-width: none; + } + + main .article-meta > *, main .article-meta .comments, main .article-meta > .banner > * { + margin: 0 5%; + } + + .bottom-bar { + align-items: center; + & > div:nth-child(2) { + margin: 0; + } + } +} diff --git a/assets/themes/default/_dark_variables.scss b/assets/themes/default/_dark_variables.scss new file mode 100644 index 00000000000..fa882718443 --- /dev/null +++ b/assets/themes/default/_dark_variables.scss @@ -0,0 +1,28 @@ +@import '_variables'; + +/* Color Scheme */ +$gray: #1a3854; +$black: #102e4a; +$white: #F8F8F8; +$purple: #7765E3; +$lightpurple: #c2bbee; +$red: #d16666; +$yellow: #ff934f; +$blue: #7f96ff; + +$background: $black; +$form-input-background: $gray; +$form-input-border: $white; +$text-color: $white; +$primary: $purple; +$primary-text-color: $white; // text color on primary background (buttons for instance) +$success-color: $blue; + +//Code Highlighting + +$code-keyword-color: #f79ac1; +$code-source-color: #a6f0ab; +$code-constant-color: #dfec84; +$code-operator-color: #eddf95; +$code-string-color: #f2ae60; +$code-comment-color: #a3b4f9; diff --git a/assets/themes/default/_forms.scss b/assets/themes/default/_forms.scss new file mode 100644 index 00000000000..79e2af2e527 --- /dev/null +++ b/assets/themes/default/_forms.scss @@ -0,0 +1,172 @@ +label { + display: block; + margin: 2em auto .5em; + font-size: 1.2em; +} +input, textarea, select { + transition: all 0.1s ease-in; + display: block; + width: 100%; + margin: auto; + padding: 1em; + box-sizing: border-box; + -webkit-appearance: textarea; + + background: $form-input-background; + color: $text-color; + border: solid $form-input-border thin; + + font-size: 1.2em; + font-weight: 400; + + &:focus { + border-color: $primary; + } +} +form input[type="submit"] { + margin: 2em auto; + -webkit-appearance: none; +} + +textarea { + resize: vertical; + overflow-y: scroll; + font-family: $lora; + font-size: 1.1em; + line-height: 1.5; +} + +input[type="checkbox"] { + display: inline; + margin: initial; + min-width: initial; + width: initial; + -webkit-appearance: checkbox; +} + +/* Inline forms (containing only CSRF token and a , for protected links) */ + +form.inline { + display: inline; + margin: 0px; + padding: 0px; + width: auto; + + input[type="submit"] { + display: inline-block; + cursor: pointer; + font-size: 1em; + width: auto; + -webkit-appearance: none; + + &:not(.button) { + margin: 0; + padding: 0; + border: none; + background: transparent; + color: $primary; + font-weight: normal; + } + } +} + +.button, .button:visited, input[type="submit"], input[type="submit"].button { + transition: all 0.1s ease-in; + display: inline-block; + -webkit-appearance: none; + + margin: 0.5em auto; + padding: 0.75em 1em; + + background: $primary; + color: $primary-text-color; + font-weight: bold; + border: none; + + cursor: pointer; + + &:hover { + background: transparentize($primary, 0.1); + } + + &.destructive { + background: $red; + + &:hover { + background: transparentize($red, 0.1); + } + } + + &.secondary { + background: $gray; + color: $text-color; + + &:hover { + background: transparentize($text-color, 0.9); + } + } +} +input[type="submit"] { + display: block; + -webkit-appearance: none; +} + +/* The writing page */ +form.new-post { + max-width: 60em; + .title { + margin: 0 auto; + padding: 0.75em 0; + + background: none; + border: none; + + font-family: $playfair; + font-size: 2em; + text-align: left; + } + textarea { + min-height: 20em; + overflow-y: scroll; + resize: none; + -webkit-appearance: textarea; + } +} + +.button + .button { + margin-left: 1em; +} + +.split { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + + & > * { + flex-grow: 1; + max-width: 40%; + } +} + +header.center { + display: flex; + flex-direction: column; + align-items: center; + background: transparent; + opacity: 1; + font-weight: normal; + text-align: left; + + > * { + margin-left: 0; + margin-right: 0; + } +} + +form > header { + display: flex; + + input[type="submit"] { + margin-left: 1em; + } +} diff --git a/assets/themes/default/_global.scss b/assets/themes/default/_global.scss new file mode 100644 index 00000000000..fe92d0627d0 --- /dev/null +++ b/assets/themes/default/_global.scss @@ -0,0 +1,613 @@ +html { + box-sizing: border-box; +} +*, *:before, *:after { + box-sizing: inherit; +} + +html, body { + margin: 0; + padding: 0; + background: $background; + color: $text-color; + font-family: $route159; + + ::selection { + background: transparentize($primary, 0.7); + } + ::-moz-selection { + background: transparentize($primary, 0.7); + } +} + +a, a:visited { + color: $primary; + text-decoration: none; +} +a::selection { + color: $background; +} +a::-moz-selection { + color: $background; +} +small { + margin-left: 1em; + color: transparentize($text-color, 0.6); + font-size: 0.75em; + word-wrap: break-word; + word-break: break-all; +} + +.center { + text-align: center; + font-weight: bold; + opacity: 0.6; + padding: 5em; +} + +.right { + text-align: right; + display: flex; + justify-content: end; + align-items: center; +} + +.spaced { + margin: 4rem 0; +} + +.banner { + background: $gray; + padding-top: 2em; + padding-bottom: 1em; + margin: 3em 0px; +} + +.hidden { + display: none; + appearance: none; +} + +/* Main */ +body > main > *, .h-feed > * { + margin: 1em $horizontal-margin; +} + +body > main > .h-entry, .h-feed { + margin: 0; +} + +body > main { + min-height: 70vh; +} + +main { + + h1, h2, h3, h4, h5, h6 { + font-family: $route159; + line-height: 1.15; + font-weight: 300; + + &.article { + max-width: $article-width; + } + } + h1 { + font-size: 2.5em; + font-weight: 300; + margin-top: 1em; + + &.article { + margin: 1em auto 0.5em; + font-family: $playfair; + font-size: 2.5em; + font-weight: normal; + } + } + + h2 { + font-size: 1.75em; + font-weight: 300; + + &.article { + font-size: 1.25em; + margin-bottom: 0.5em; + } + } + + h3, h4, h5, h6 { + font-size: 1.5em; + font-weight: 300; + + &.article { + margin: auto; + font-size: 1.1em; + margin-bottom: 0.5em; + } + } + + .cover { + padding: 0px; + margin: 0px; + width: auto; + min-height: 50vh; + background-position: center; + background-size: cover; + overflow: hidden; + } +} + +/* Errors */ +p.error { + color: $red; + font-weight: bold; +} + +/* User page */ +.user h1 { + display: flex; + flex-direction: row; + align-items: center; + margin: 0px; +} + +.user .avatar.medium { + margin-left: 0px; +} + +.badge { + margin-right: 1em; + padding: 0.35em 1em; + + background: $background; + color: $primary; + border: 1px solid $primary; + + font-size: 1rem; +} + +.user-summary { + margin: 2em 0px; +} + +/* Cards */ +.cards { + display: flex; + flex-direction: row; + flex-wrap: wrap; + padding: 0 5%; + margin: 1rem 0 5rem; +} +.card { + flex: 1; + display: flex; + flex-direction: column; + + position: relative; + + min-width: 20em; + min-height: 20em; + margin: 1em; + box-sizing: border-box; + + background: $gray; + + text-overflow: ellipsis; + + footer.authors { + div { + float: left; + margin-right: 0.25em; + } + + .likes { color: $red; } + .reshares { color: $primary; } + + span.likes, span.resahres { + font-family: "Route159",serif; + font-size: 1em; + } + + svg.feather { + width: 0.85em; + height: 0.85em; + } + } + + + > * { + margin: 20px; + } + + .cover-link { + margin: 0; + + &:hover { + opacity: 0.9; + } + } + + .cover { + min-height: 10em; + background-position: center; + background-size: cover; + margin: 0px; + } + + header { + display: flex; + } + + h3 { + flex-grow: 1; + margin: 0; + font-family: $playfair; + font-size: 1.75em; + font-weight: normal; + line-height: 1.10; + display: inline-block; + position: relative; + a { + display: block; + width: 100%; + height: 100%; + padding-block-start: 0.5em; + transition: color 0.1s ease-in; + color: $text-color; + + &:hover { color: $primary; } + } + } + + .controls { + flex-shrink: 0; + text-align: end; + + .button { + margin-top: 0; + margin-bottom: 0; + } + } + + main { + flex: 1; + + font-family: $lora; + font-size: 1em; + line-height: 1.25; + text-align: initial; + overflow: hidden; + } +} + +.list > .card { + background: transparent; + margin: 2em 0; + min-height: 3em; + + padding: 1em; + transition: background 0.1s ease-in; + + &:hover { + background-color: $gray; + } + + &.compact { + margin: 0; + padding: 0 1em; + } + + h3 { + margin: 0; + } +} + +/* Instance presentation */ +.presentation { + max-width: none; + + & > h2, & > a { + text-align: center; + } + + & > a { + font-size: 1.2em; + margin: 1em; + } +} + +/* Stats */ +.stats { + display: flex; + justify-content: space-around; + margin: 2em; + + > div { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + + p { + text-align: center; + } + + em { + font-weight: bold; + display: block; + margin: 1em 0; + } +} + +/* Pagination */ +.pagination { + display: flex; + justify-content: space-evenly; + + > * { + padding: 2em; + } +} + +/* Flex boxes */ +.flex { + display: flex; + flex-direction: row; + align-items: center; + + &.vertical { + flex-direction: column; + justify-content: space-around; + align-items: flex-start; + + small { + margin: initial; + } + } + + .grow { + flex: 1; + margin: 0 1em; + } + + .grow:first-child { + margin: 1em 0; + } +} + +.left-icon { + align-self: center; + padding: 1em; + background: $gray; + border-radius: 50px; + margin: 1em; + margin-right: 2em; +} + +/* Footer */ +body > footer { + display: flex; + align-content: center; + justify-content: space-around; + background: $primary; + color: $primary-text-color; + margin-top: 5em; + + * { + margin: 0; + } + + hr { + transform: skew(-15deg); + background: $primary-text-color; + border: none; + width: .2em; + } + + a, a:visited { + color: $primary-text-color; + } + + div { + display: flex; + flex-direction: column; + flex-basis: 20%; + margin: 2em 0; + transition: all 0.1s ease-in; + + & > * { + display: block; + margin: 1em 0; + } + } +} + +/// Media +figure { + text-align: center; + margin: 2em; + max-width: 100%; + width: auto; + height: auto; + + > * { + max-width: 100%; + } + + figcaption { + padding: 1em; + } + + audio, video { + width: 100%; + } +} + +.preview { + display: block; + max-width: 100px; + max-height: 100px; + width: auto; + height: auto; + margin-right: 20px; +} + +.media-preview { + min-height: 8em; + + &:not(.image) { + background-color: #7765E3; + background-repeat: no-repeat; + background-position: center; + background-size: 4em; + } + + &.unknown { + background-image: url('/static/images/unknown-file.svg'); + display: block; + } + + &.audio { + background-image: url('/static/images/audio-file.svg'); + } + + &.video { + background-image: url('/static/images/video-file.svg'); + } +} + +/// Avatars +.avatar { + background-position: center !important; + background-size: cover; + border-radius: 100%; + flex-shrink: 0; + + &.small { + width: 50px; + height: 50px; + } + + &.medium { + width: 100px; + height: 100px; + margin: 20px; + } + + &.padded { + margin-right: 2rem; + } +} + +/// Tabs +.tabs { + border-bottom: 1px solid $gray; + padding: 0px; + margin: auto $horizontal-margin 2em; + overflow: auto; + display: flex; + + a { + display: inline-block; + color: $text-color; + padding: 1em; + + &.selected { + color: $primary; + border-bottom: 1px solid $primary; + } + } +} + + +/// Small screens +@media screen and (max-width: 600px) { + body > main > *, .h-feed > * { + margin: 1em; + } + + main .article-meta { + > *, .comments { + margin: 0 5%; + } + > p { + margin: 2em 5%; + font-size: 0.9em; + } + .comments > * { margin: auto 5%; } + .comments .comment { padding: 2em 0px; } + } + main .article-info, main article, main h1.article, main h2.article { + max-width: 90vw; + } + + .card { + min-width: 80%; + min-height: 80%; + } + + .tabs { + margin: auto 0px 2em; + } + + .stats { flex-direction: column; } + body > footer { + flex-direction: column; + align-items: center; + } + body > footer * { + margin: 1em auto; + text-align: center; + } + + .flex.wrap { flex-direction: column; } + + .cards, .list { + margin: 1rem 0 5rem; + } + + .split { + flex-direction: column; + margin: 0; + + & > * { + max-width: 100% !important; + } + } + + main .article-meta .comments .comment { + header { + flex-direction: column; + } + + .content { + margin-top: 0.5em; + } + } +} + +//highlighting +code { + .constant{ + color: $code-constant-color; + } + .string{ + color: $code-string-color; + } + .keyword.type,.keyword.control,.type{ + color: $code-keyword-color; + } + .keyword.operator{ + color: $code-operator-color; + } + .source{ + color: $code-source-color; + } + .comment{ + color: $code-comment-color; + } + .function{ + color:inherit; + } +} diff --git a/assets/themes/default/_header.scss b/assets/themes/default/_header.scss new file mode 100644 index 00000000000..11d05c33aa4 --- /dev/null +++ b/assets/themes/default/_header.scss @@ -0,0 +1,337 @@ +body > header { + background: $gray; + + #content { + display: flex; + align-content: center; + justify-content: space-between; + } + + nav#menu { + position: relative; + display: none; + appearance: none; + transform: skewX(-15deg); + left: -1em; + padding: 1em 1em 1em 2em; + background: $primary; + align-self: flex-start; + + a { + transform: skewX(15deg); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 1.4em; + height: 1.4em; + margin: 0; + padding: 0; + color: $gray; + font-size: 1.33em; + } + } + + nav { + display: flex; + flex-direction: row; + align-items: center; + + hr { + height: 100%; + width: 0.2em; + background: $primary; + border: none; + transform: skewX(-15deg); + } + a { + display: flex; + align-items: center; + position: relative; + align-self: stretch; + margin: 0; + padding: 0 2em; + font-size: 1em; + + i { font-size: 1.2em; } + + &.title { + margin: 0; + text-align: center; + padding: 0.5em 1em; + font-size: 1.75em; + + img { + height: 1.75em; + width: 1.75em; + } + + p { + margin: 0; + padding-left: 0.5em; + } + } + } + } +} + +.messages { + & > * { + padding: 1em 20%; + margin: 0; + max-width: initial; + font-weight: bold; + } + + p.error { + color: darken($red, 20%); + background: lighten($red, 40%); + margin: 0; + max-width: initial; + } + + p.warning { + color: darken($yellow, 20%); + background: lighten($yellow, 40%); + } + + p.success { + color: darken($success-color, 20%); + background: lighten($success-color, 40%); + } +} + +/// Small screens +@media screen and (max-width: 600px) { + @keyframes menuOpening { + from { + transform: scaleX(0); + transform-origin: left; + opacity: 0; + } + to { + transform: scaleX(1); + transform-origin: left; + opacity: 1; + } + } + @-webkit-keyframes menuOpening { + from { + -webkit-transform: scaleX(0); + transform-origin: left; + opacity: 0; + } + to { + -webkit-transform: scaleX(1); + transform-origin: left; + opacity: 1; + } + } + + body > header { + flex-direction: column; + + nav#menu { + display: inline-flex; + z-index: 21; + } + + #content { + display: none; + appearance: none; + text-align: center; + z-index: 20; + } + } + + body > header:focus-within #content, .show + #content { + position: fixed; + display: flex; + flex-direction: column; + justify-content: flex-start; + + top: 0; + left: 0; + width: 100%; + height: 100%; + box-sizing: border-box; + + animation: 0.2s menuOpening; + + &::before { + content: ""; + position: absolute; + transform: skewX(-10deg); + top: 0; + left: -20%; + width: 100%; + height: 100%; + + z-index: -10; + + background: $primary; + } + + > nav { + flex-direction: column; + align-items: flex-start; + + a { + display: flex; + flex-direction: row; + align-items: center; + margin: 0; + padding: 1rem 1.5rem; + color: $background; + font-size: 1.4em; + font-weight: 300; + + &.title { font-size: 1.8em; } + + > *:first-child { width: 3rem; } + > img:first-child { height: 3rem; } + > *:last-child { margin-left: 1rem; } + > nav hr { + display: block; + margin: 0; + width: 100%; + border: solid $background 0.1rem; + } + .mobile-label { display: initial; } + } + } + } +} + +/* Only enable label animations on large screens */ +@media screen and (min-width: 600px) { + header nav a { + i { + transition: all 0.2s ease; + margin: 0; + } + + .mobile-label { + transition: all 0.2s ease; + display: block; + position: absolute; + left: 50%; + transform: translateZ(0); + opacity: 0; + font-size: 0.9em; + white-space: nowrap; + } + + img + .mobile-label { display: none; } + + &:hover { + i { margin-bottom: 0.75em; } + .mobile-label { + opacity: 1; + transform: translate(-50%, 80%); + } + } + } +} + +// Small screens +@media screen and (max-width: 600px) { + @keyframes menuOpening { + from { + transform: scaleX(0); + transform-origin: left; + opacity: 0; + } + to { + transform: scaleX(1); + transform-origin: left; + opacity: 1; + } + } + @-webkit-keyframes menuOpening { + from { + -webkit-transform: scaleX(0); + transform-origin: left; + opacity: 0; + } + to { + -webkit-transform: scaleX(1); + transform-origin: left; + opacity: 1; + } + } + + body > header { + flex-direction: column; + + nav#menu { + display: inline-flex; + z-index: 21; + } + + #content { + display: none; + appearance: none; + text-align: center; + z-index: 20; + } + } + + body > header:focus-within #content, .show + #content { + position: fixed; + display: flex; + flex-direction: column; + justify-content: flex-start; + + top: 0; + left: 0; + width: 100%; + height: 100%; + box-sizing: border-box; + + animation: 0.2s menuOpening; + + &::before { + content: ""; + position: absolute; + transform: skewX(-10deg); + top: 0; + left: -20%; + width: 100%; + height: 100%; + + z-index: -10; + + background: $primary; + } + + > nav { + flex-direction: column; + align-items: flex-start; + + a { + display: flex; + flex-direction: row; + align-items: center; + margin: 0; + padding: 1rem 1.5rem; + color: $background; + font-size: 1.4em; + font-weight: 300; + + &.title { font-size: 1.8em; } + + > *:first-child { width: 3rem; } + > img:first-child { height: 3rem; } + > *:last-child { margin-left: 1rem; } + > nav hr { + display: block; + margin: 0; + width: 100%; + border: solid $background 0.1rem; + } + .mobile-label { display: initial; } + } + } + } +} diff --git a/assets/themes/default/_variables.scss b/assets/themes/default/_variables.scss new file mode 100644 index 00000000000..b76a17cf990 --- /dev/null +++ b/assets/themes/default/_variables.scss @@ -0,0 +1,37 @@ +/* Color Scheme */ +$gray: #f3f3f3; +$black: #242424; +$white: #f8f8f8; +$purple: #7765e3; +$lightpurple: #c2bbee; +$red: #e92f2f; +$yellow: #ffe347; +$green: #23f0c7; + +$background: $white; +$form-input-background: white; +$form-input-border: $black; +$text-color: $black; +$primary: $purple; +$primary-text-color: $white; // text color on primary background (buttons for instance) +$success-color: $green; + +/* Dimensions */ + +$article-width: 70ch; +$horizontal-margin: 20%; +$margin: 0 $horizontal-margin; + +/* Fonts */ + +$route159: "Shabnam", "Route159", serif; +$playfair: "Vazir", "Playfair Display", serif; +$lora: "Vazir", "Lora", serif; + +//Code Highlighting +$code-keyword-color: #45244a; +$code-source-color: #4c588c; +$code-constant-color: scale-color(magenta, $lightness: -5%); +$code-operator-color: scale-color($code-source-color, $lightness: -5%); +$code-string-color: #8a571c; +$code-comment-color: #1c4c8a; diff --git a/assets/themes/default/dark.scss b/assets/themes/default/dark.scss new file mode 100644 index 00000000000..91a30a9055a --- /dev/null +++ b/assets/themes/default/dark.scss @@ -0,0 +1,14 @@ +/* color palette: https://coolors.co/23f0c7-ef767a-7765e3-6457a6-ffe347 */ + +@import url("./feather.css"); +@import url("./fonts/Route159/Route159.css"); +@import url("./fonts/Lora/Lora.css"); +@import url("./fonts/Playfair_Display/PlayfairDisplay.css"); +@import url("./fonts/Vazir_WOL/Vazir_WOL.css"); +@import url("./fonts/Shabnam_WOL/Shabnam_WOL.css"); + +@import "dark_variables"; +@import "global"; +@import "header"; +@import "article"; +@import "forms"; diff --git a/assets/themes/default/feather.css b/assets/themes/default/feather.css new file mode 100644 index 00000000000..e90dd3baa12 --- /dev/null +++ b/assets/themes/default/feather.css @@ -0,0 +1,260 @@ +@font-face { + font-family: "Feather"; + src: url('./fonts/Feather/Feather.eot'); /* IE9 */ + src: url('./fonts/Feather/Feather.eot') format('embedded-opentype'), /* IE6-IE8 */ + url('./fonts/Feather/Feather.woff') format('woff'), /* Chrome, Firefox */ + url('./fonts/Feather/Feather.ttf') format('truetype'), /* Chrome, Firefox, Opera, Safari, Android, iOS 4.2+ */ + url('./fonts/Feather/Feather.svg') format('svg'); /* iOS 4.1- */ +} + +.feather { + width: 24px; + height: 24px; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; + fill: none; +} + +.icon:before { + font-family: "Feather"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-alert-octagon:before { content: "\e81b"; } +.icon-alert-circle:before { content: "\e81c"; } +.icon-activity:before { content: "\e81d"; } +.icon-alert-triangle:before { content: "\e81e"; } +.icon-align-center:before { content: "\e81f"; } +.icon-airplay:before { content: "\e820"; } +.icon-align-justify:before { content: "\e821"; } +.icon-align-left:before { content: "\e822"; } +.icon-align-right:before { content: "\e823"; } +.icon-arrow-down-left:before { content: "\e824"; } +.icon-arrow-down-right:before { content: "\e825"; } +.icon-anchor:before { content: "\e826"; } +.icon-aperture:before { content: "\e827"; } +.icon-arrow-left:before { content: "\e828"; } +.icon-arrow-right:before { content: "\e829"; } +.icon-arrow-down:before { content: "\e82a"; } +.icon-arrow-up-left:before { content: "\e82b"; } +.icon-arrow-up-right:before { content: "\e82c"; } +.icon-arrow-up:before { content: "\e82d"; } +.icon-award:before { content: "\e82e"; } +.icon-bar-chart:before { content: "\e82f"; } +.icon-at-sign:before { content: "\e830"; } +.icon-bar-chart-:before { content: "\e831"; } +.icon-battery-charging:before { content: "\e832"; } +.icon-bell-off:before { content: "\e833"; } +.icon-battery:before { content: "\e834"; } +.icon-bluetooth:before { content: "\e835"; } +.icon-bell:before { content: "\e836"; } +.icon-book:before { content: "\e837"; } +.icon-briefcase:before { content: "\e838"; } +.icon-camera-off:before { content: "\e839"; } +.icon-calendar:before { content: "\e83a"; } +.icon-bookmark:before { content: "\e83b"; } +.icon-box:before { content: "\e83c"; } +.icon-camera:before { content: "\e83d"; } +.icon-check-circle:before { content: "\e83e"; } +.icon-check:before { content: "\e83f"; } +.icon-check-square:before { content: "\e840"; } +.icon-cast:before { content: "\e841"; } +.icon-chevron-down:before { content: "\e842"; } +.icon-chevron-left:before { content: "\e843"; } +.icon-chevron-right:before { content: "\e844"; } +.icon-chevron-up:before { content: "\e845"; } +.icon-chevrons-down:before { content: "\e846"; } +.icon-chevrons-right:before { content: "\e847"; } +.icon-chevrons-up:before { content: "\e848"; } +.icon-chevrons-left:before { content: "\e849"; } +.icon-circle:before { content: "\e84a"; } +.icon-clipboard:before { content: "\e84b"; } +.icon-chrome:before { content: "\e84c"; } +.icon-clock:before { content: "\e84d"; } +.icon-cloud-lightning:before { content: "\e84e"; } +.icon-cloud-drizzle:before { content: "\e84f"; } +.icon-cloud-rain:before { content: "\e850"; } +.icon-cloud-off:before { content: "\e851"; } +.icon-codepen:before { content: "\e852"; } +.icon-cloud-snow:before { content: "\e853"; } +.icon-compass:before { content: "\e854"; } +.icon-copy:before { content: "\e855"; } +.icon-corner-down-right:before { content: "\e856"; } +.icon-corner-down-left:before { content: "\e857"; } +.icon-corner-left-down:before { content: "\e858"; } +.icon-corner-left-up:before { content: "\e859"; } +.icon-corner-up-left:before { content: "\e85a"; } +.icon-corner-up-right:before { content: "\e85b"; } +.icon-corner-right-down:before { content: "\e85c"; } +.icon-corner-right-up:before { content: "\e85d"; } +.icon-cpu:before { content: "\e85e"; } +.icon-credit-card:before { content: "\e85f"; } +.icon-crosshair:before { content: "\e860"; } +.icon-disc:before { content: "\e861"; } +.icon-delete:before { content: "\e862"; } +.icon-download-cloud:before { content: "\e863"; } +.icon-download:before { content: "\e864"; } +.icon-droplet:before { content: "\e865"; } +.icon-edit-:before { content: "\e866"; } +.icon-edit:before { content: "\e867"; } +.icon-edit-1:before { content: "\e868"; } +.icon-external-link:before { content: "\e869"; } +.icon-eye:before { content: "\e86a"; } +.icon-feather:before { content: "\e86b"; } +.icon-facebook:before { content: "\e86c"; } +.icon-file-minus:before { content: "\e86d"; } +.icon-eye-off:before { content: "\e86e"; } +.icon-fast-forward:before { content: "\e86f"; } +.icon-file-text:before { content: "\e870"; } +.icon-film:before { content: "\e871"; } +.icon-file:before { content: "\e872"; } +.icon-file-plus:before { content: "\e873"; } +.icon-folder:before { content: "\e874"; } +.icon-filter:before { content: "\e875"; } +.icon-flag:before { content: "\e876"; } +.icon-globe:before { content: "\e877"; } +.icon-grid:before { content: "\e878"; } +.icon-heart:before { content: "\e879"; } +.icon-home:before { content: "\e87a"; } +.icon-github:before { content: "\e87b"; } +.icon-image:before { content: "\e87c"; } +.icon-inbox:before { content: "\e87d"; } +.icon-layers:before { content: "\e87e"; } +.icon-info:before { content: "\e87f"; } +.icon-instagram:before { content: "\e880"; } +.icon-layout:before { content: "\e881"; } +.icon-link-:before { content: "\e882"; } +.icon-life-buoy:before { content: "\e883"; } +.icon-link:before { content: "\e884"; } +.icon-log-in:before { content: "\e885"; } +.icon-list:before { content: "\e886"; } +.icon-lock:before { content: "\e887"; } +.icon-log-out:before { content: "\e888"; } +.icon-loader:before { content: "\e889"; } +.icon-mail:before { content: "\e88a"; } +.icon-maximize-:before { content: "\e88b"; } +.icon-map:before { content: "\e88c"; } +.icon-maximize:before { content: "\e88d"; } +.icon-map-pin:before { content: "\e88e"; } +.icon-menu:before { content: "\e88f"; } +.icon-message-circle:before { content: "\e890"; } +.icon-message-square:before { content: "\e891"; } +.icon-minimize-:before { content: "\e892"; } +.icon-mic-off:before { content: "\e893"; } +.icon-minus-circle:before { content: "\e894"; } +.icon-mic:before { content: "\e895"; } +.icon-minus-square:before { content: "\e896"; } +.icon-minus:before { content: "\e897"; } +.icon-moon:before { content: "\e898"; } +.icon-monitor:before { content: "\e899"; } +.icon-more-vertical:before { content: "\e89a"; } +.icon-more-horizontal:before { content: "\e89b"; } +.icon-move:before { content: "\e89c"; } +.icon-music:before { content: "\e89d"; } +.icon-navigation-:before { content: "\e89e"; } +.icon-navigation:before { content: "\e89f"; } +.icon-octagon:before { content: "\e8a0"; } +.icon-package:before { content: "\e8a1"; } +.icon-pause-circle:before { content: "\e8a2"; } +.icon-pause:before { content: "\e8a3"; } +.icon-percent:before { content: "\e8a4"; } +.icon-phone-call:before { content: "\e8a5"; } +.icon-phone-forwarded:before { content: "\e8a6"; } +.icon-phone-missed:before { content: "\e8a7"; } +.icon-phone-off:before { content: "\e8a8"; } +.icon-phone-incoming:before { content: "\e8a9"; } +.icon-phone:before { content: "\e8aa"; } +.icon-phone-outgoing:before { content: "\e8ab"; } +.icon-pie-chart:before { content: "\e8ac"; } +.icon-play-circle:before { content: "\e8ad"; } +.icon-play:before { content: "\e8ae"; } +.icon-plus-square:before { content: "\e8af"; } +.icon-plus-circle:before { content: "\e8b0"; } +.icon-plus:before { content: "\e8b1"; } +.icon-pocket:before { content: "\e8b2"; } +.icon-printer:before { content: "\e8b3"; } +.icon-power:before { content: "\e8b4"; } +.icon-radio:before { content: "\e8b5"; } +.icon-repeat:before { content: "\e8b6"; } +.icon-refresh-ccw:before { content: "\e8b7"; } +.icon-rewind:before { content: "\e8b8"; } +.icon-rotate-ccw:before { content: "\e8b9"; } +.icon-refresh-cw:before { content: "\e8ba"; } +.icon-rotate-cw:before { content: "\e8bb"; } +.icon-save:before { content: "\e8bc"; } +.icon-search:before { content: "\e8bd"; } +.icon-server:before { content: "\e8be"; } +.icon-scissors:before { content: "\e8bf"; } +.icon-share-:before { content: "\e8c0"; } +.icon-share:before { content: "\e8c1"; } +.icon-shield:before { content: "\e8c2"; } +.icon-settings:before { content: "\e8c3"; } +.icon-skip-back:before { content: "\e8c4"; } +.icon-shuffle:before { content: "\e8c5"; } +.icon-sidebar:before { content: "\e8c6"; } +.icon-skip-forward:before { content: "\e8c7"; } +.icon-slack:before { content: "\e8c8"; } +.icon-slash:before { content: "\e8c9"; } +.icon-smartphone:before { content: "\e8ca"; } +.icon-square:before { content: "\e8cb"; } +.icon-speaker:before { content: "\e8cc"; } +.icon-star:before { content: "\e8cd"; } +.icon-stop-circle:before { content: "\e8ce"; } +.icon-sun:before { content: "\e8cf"; } +.icon-sunrise:before { content: "\e8d0"; } +.icon-tablet:before { content: "\e8d1"; } +.icon-tag:before { content: "\e8d2"; } +.icon-sunset:before { content: "\e8d3"; } +.icon-target:before { content: "\e8d4"; } +.icon-thermometer:before { content: "\e8d5"; } +.icon-thumbs-up:before { content: "\e8d6"; } +.icon-thumbs-down:before { content: "\e8d7"; } +.icon-toggle-left:before { content: "\e8d8"; } +.icon-toggle-right:before { content: "\e8d9"; } +.icon-trash-:before { content: "\e8da"; } +.icon-trash:before { content: "\e8db"; } +.icon-trending-up:before { content: "\e8dc"; } +.icon-trending-down:before { content: "\e8dd"; } +.icon-triangle:before { content: "\e8de"; } +.icon-type:before { content: "\e8df"; } +.icon-twitter:before { content: "\e8e0"; } +.icon-upload:before { content: "\e8e1"; } +.icon-umbrella:before { content: "\e8e2"; } +.icon-upload-cloud:before { content: "\e8e3"; } +.icon-unlock:before { content: "\e8e4"; } +.icon-user-check:before { content: "\e8e5"; } +.icon-user-minus:before { content: "\e8e6"; } +.icon-user-plus:before { content: "\e8e7"; } +.icon-user-x:before { content: "\e8e8"; } +.icon-user:before { content: "\e8e9"; } +.icon-users:before { content: "\e8ea"; } +.icon-video-off:before { content: "\e8eb"; } +.icon-video:before { content: "\e8ec"; } +.icon-voicemail:before { content: "\e8ed"; } +.icon-volume-x:before { content: "\e8ee"; } +.icon-volume-:before { content: "\e8ef"; } +.icon-volume-1:before { content: "\e8f0"; } +.icon-volume:before { content: "\e8f1"; } +.icon-watch:before { content: "\e8f2"; } +.icon-wifi:before { content: "\e8f3"; } +.icon-x-square:before { content: "\e8f4"; } +.icon-wind:before { content: "\e8f5"; } +.icon-x:before { content: "\e8f6"; } +.icon-x-circle:before { content: "\e8f7"; } +.icon-zap:before { content: "\e8f8"; } +.icon-zoom-in:before { content: "\e8f9"; } +.icon-zoom-out:before { content: "\e8fa"; } +.icon-command:before { content: "\e8fb"; } +.icon-cloud:before { content: "\e8fc"; } +.icon-hash:before { content: "\e8fd"; } +.icon-headphones:before { content: "\e8fe"; } diff --git a/assets/themes/default/fonts/Feather/Feather.eot b/assets/themes/default/fonts/Feather/Feather.eot new file mode 100644 index 00000000000..58371d90858 Binary files /dev/null and b/assets/themes/default/fonts/Feather/Feather.eot differ diff --git a/assets/themes/default/fonts/Feather/Feather.svg b/assets/themes/default/fonts/Feather/Feather.svg new file mode 100644 index 00000000000..5dda143b665 --- /dev/null +++ b/assets/themes/default/fonts/Feather/Feather.svg @@ -0,0 +1,849 @@ + + + + + +Created by iconfont + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/themes/default/fonts/Feather/Feather.ttf b/assets/themes/default/fonts/Feather/Feather.ttf new file mode 100644 index 00000000000..0b33dac7826 Binary files /dev/null and b/assets/themes/default/fonts/Feather/Feather.ttf differ diff --git a/assets/themes/default/fonts/Feather/Feather.woff b/assets/themes/default/fonts/Feather/Feather.woff new file mode 100644 index 00000000000..9b03a72a014 Binary files /dev/null and b/assets/themes/default/fonts/Feather/Feather.woff differ diff --git a/assets/themes/default/fonts/Lora/Lora-Bold.eot b/assets/themes/default/fonts/Lora/Lora-Bold.eot new file mode 100644 index 00000000000..5955a4a0a36 Binary files /dev/null and b/assets/themes/default/fonts/Lora/Lora-Bold.eot differ diff --git a/assets/themes/default/fonts/Lora/Lora-Bold.ttf b/assets/themes/default/fonts/Lora/Lora-Bold.ttf new file mode 100644 index 00000000000..fffb5c58424 Binary files /dev/null and b/assets/themes/default/fonts/Lora/Lora-Bold.ttf differ diff --git a/assets/themes/default/fonts/Lora/Lora-Bold.woff b/assets/themes/default/fonts/Lora/Lora-Bold.woff new file mode 100644 index 00000000000..db3d6e81b9f Binary files /dev/null and b/assets/themes/default/fonts/Lora/Lora-Bold.woff differ diff --git a/assets/themes/default/fonts/Lora/Lora-Bold.woff2 b/assets/themes/default/fonts/Lora/Lora-Bold.woff2 new file mode 100644 index 00000000000..d3ee43512c4 Binary files /dev/null and b/assets/themes/default/fonts/Lora/Lora-Bold.woff2 differ diff --git a/assets/themes/default/fonts/Lora/Lora-BoldItalic.eot b/assets/themes/default/fonts/Lora/Lora-BoldItalic.eot new file mode 100644 index 00000000000..95fed21adcf Binary files /dev/null and b/assets/themes/default/fonts/Lora/Lora-BoldItalic.eot differ diff --git a/assets/themes/default/fonts/Lora/Lora-BoldItalic.ttf b/assets/themes/default/fonts/Lora/Lora-BoldItalic.ttf new file mode 100644 index 00000000000..881a5e5f24d Binary files /dev/null and b/assets/themes/default/fonts/Lora/Lora-BoldItalic.ttf differ diff --git a/assets/themes/default/fonts/Lora/Lora-BoldItalic.woff b/assets/themes/default/fonts/Lora/Lora-BoldItalic.woff new file mode 100644 index 00000000000..b15c86868b8 Binary files /dev/null and b/assets/themes/default/fonts/Lora/Lora-BoldItalic.woff differ diff --git a/assets/themes/default/fonts/Lora/Lora-BoldItalic.woff2 b/assets/themes/default/fonts/Lora/Lora-BoldItalic.woff2 new file mode 100644 index 00000000000..797d3b46d30 Binary files /dev/null and b/assets/themes/default/fonts/Lora/Lora-BoldItalic.woff2 differ diff --git a/assets/themes/default/fonts/Lora/Lora-Italic.eot b/assets/themes/default/fonts/Lora/Lora-Italic.eot new file mode 100644 index 00000000000..6853970459e Binary files /dev/null and b/assets/themes/default/fonts/Lora/Lora-Italic.eot differ diff --git a/assets/themes/default/fonts/Lora/Lora-Italic.ttf b/assets/themes/default/fonts/Lora/Lora-Italic.ttf new file mode 100644 index 00000000000..2c635503a53 Binary files /dev/null and b/assets/themes/default/fonts/Lora/Lora-Italic.ttf differ diff --git a/assets/themes/default/fonts/Lora/Lora-Italic.woff b/assets/themes/default/fonts/Lora/Lora-Italic.woff new file mode 100644 index 00000000000..058fd118d4b Binary files /dev/null and b/assets/themes/default/fonts/Lora/Lora-Italic.woff differ diff --git a/assets/themes/default/fonts/Lora/Lora-Italic.woff2 b/assets/themes/default/fonts/Lora/Lora-Italic.woff2 new file mode 100644 index 00000000000..d01bab1ade3 Binary files /dev/null and b/assets/themes/default/fonts/Lora/Lora-Italic.woff2 differ diff --git a/assets/themes/default/fonts/Lora/Lora-Regular.eot b/assets/themes/default/fonts/Lora/Lora-Regular.eot new file mode 100644 index 00000000000..526f0df5a71 Binary files /dev/null and b/assets/themes/default/fonts/Lora/Lora-Regular.eot differ diff --git a/assets/themes/default/fonts/Lora/Lora-Regular.ttf b/assets/themes/default/fonts/Lora/Lora-Regular.ttf new file mode 100644 index 00000000000..760d1ca3d0d Binary files /dev/null and b/assets/themes/default/fonts/Lora/Lora-Regular.ttf differ diff --git a/assets/themes/default/fonts/Lora/Lora-Regular.woff b/assets/themes/default/fonts/Lora/Lora-Regular.woff new file mode 100644 index 00000000000..279bd7f5682 Binary files /dev/null and b/assets/themes/default/fonts/Lora/Lora-Regular.woff differ diff --git a/assets/themes/default/fonts/Lora/Lora-Regular.woff2 b/assets/themes/default/fonts/Lora/Lora-Regular.woff2 new file mode 100644 index 00000000000..ad4fb0a3259 Binary files /dev/null and b/assets/themes/default/fonts/Lora/Lora-Regular.woff2 differ diff --git a/assets/themes/default/fonts/Lora/Lora.css b/assets/themes/default/fonts/Lora/Lora.css new file mode 100644 index 00000000000..51bab42b85c --- /dev/null +++ b/assets/themes/default/fonts/Lora/Lora.css @@ -0,0 +1,40 @@ +@font-face { + font-family: 'Lora'; + src: url('Lora-Regular.eot'); + src: url('Lora-Regular.eot?#iefix') format('embedded-opentype'), + url('Lora-Regular.woff2') format('woff2'), + url('Lora-Regular.woff') format('woff'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Lora'; + src: url('Lora-Italic.eot'); + src: url('Lora-Italic.eot?#iefix') format('embedded-opentype'), + url('Lora-Italic.woff2') format('woff2'), + url('Lora-Italic.woff') format('woff'); + font-weight: normal; + font-style: italic; +} + +@font-face { + font-family: 'Lora'; + src: url('Lora-Bold.eot'); + src: url('Lora-Bold.eot?#iefix') format('embedded-opentype'), + url('Lora-Bold.woff2') format('woff2'), + url('Lora-Bold.woff') format('woff'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'Lora'; + src: url('Lora-BoldItalic.eot'); + src: url('Lora-BoldItalic.eot?#iefix') format('embedded-opentype'), + url('Lora-BoldItalic.woff2') format('woff2'), + url('Lora-BoldItalic.woff') format('woff'); + font-weight: bold; + font-style: italic; +} + diff --git a/assets/themes/default/fonts/Lora/OFL.txt b/assets/themes/default/fonts/Lora/OFL.txt new file mode 100644 index 00000000000..0f6fdb15a0b --- /dev/null +++ b/assets/themes/default/fonts/Lora/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Lora Project Authors (https://github.com/cyrealtype/Lora-Cyrillic), with Reserved Font Name "Lora". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/themes/default/fonts/Playfair_Display/OFL.txt b/assets/themes/default/fonts/Playfair_Display/OFL.txt new file mode 100644 index 00000000000..9a30d7e1dfc --- /dev/null +++ b/assets/themes/default/fonts/Playfair_Display/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2017 The Playfair Display Project Authors (https://github.com/clauseggers/Playfair-Display), with Reserved Font Name "Playfair Display". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Black.eot b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Black.eot new file mode 100644 index 00000000000..b6cee0ed6c9 Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Black.eot differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Black.ttf b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Black.ttf new file mode 100644 index 00000000000..fec2ac81462 Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Black.ttf differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Black.woff b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Black.woff new file mode 100644 index 00000000000..9144c096d0b Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Black.woff differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Black.woff2 b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Black.woff2 new file mode 100644 index 00000000000..9d23c213724 Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Black.woff2 differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BlackItalic.eot b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BlackItalic.eot new file mode 100644 index 00000000000..db76f291fff Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BlackItalic.eot differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BlackItalic.ttf b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BlackItalic.ttf new file mode 100644 index 00000000000..8e95d1d1886 Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BlackItalic.ttf differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BlackItalic.woff b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BlackItalic.woff new file mode 100644 index 00000000000..3aa85b5cb76 Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BlackItalic.woff differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BlackItalic.woff2 b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BlackItalic.woff2 new file mode 100644 index 00000000000..42d4cf67dce Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BlackItalic.woff2 differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Bold.eot b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Bold.eot new file mode 100644 index 00000000000..2c99270f7e7 Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Bold.eot differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Bold.ttf b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Bold.ttf new file mode 100644 index 00000000000..93a59cf77ce Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Bold.ttf differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Bold.woff b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Bold.woff new file mode 100644 index 00000000000..1d0aa2344b5 Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Bold.woff differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Bold.woff2 b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Bold.woff2 new file mode 100644 index 00000000000..870d7010c30 Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Bold.woff2 differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BoldItalic.eot b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BoldItalic.eot new file mode 100644 index 00000000000..fb25f94a16d Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BoldItalic.eot differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BoldItalic.ttf b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BoldItalic.ttf new file mode 100644 index 00000000000..3d2fbde057b Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BoldItalic.ttf differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BoldItalic.woff b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BoldItalic.woff new file mode 100644 index 00000000000..2cf9e745390 Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BoldItalic.woff differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BoldItalic.woff2 b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BoldItalic.woff2 new file mode 100644 index 00000000000..ee18db41715 Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-BoldItalic.woff2 differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Italic.eot b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Italic.eot new file mode 100644 index 00000000000..2d73a1f2941 Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Italic.eot differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Italic.ttf b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Italic.ttf new file mode 100644 index 00000000000..aa65a5f10dc Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Italic.ttf differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Italic.woff b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Italic.woff new file mode 100644 index 00000000000..c6bbbbdf45c Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Italic.woff differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Italic.woff2 b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Italic.woff2 new file mode 100644 index 00000000000..a50ed090c2b Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Italic.woff2 differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Regular.eot b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Regular.eot new file mode 100644 index 00000000000..fdc561e2107 Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Regular.eot differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Regular.ttf b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Regular.ttf new file mode 100644 index 00000000000..8468e687fac Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Regular.ttf differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Regular.woff b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Regular.woff new file mode 100644 index 00000000000..4bb34273b33 Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Regular.woff differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Regular.woff2 b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Regular.woff2 new file mode 100644 index 00000000000..04e356bf324 Binary files /dev/null and b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay-Regular.woff2 differ diff --git a/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay.css b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay.css new file mode 100644 index 00000000000..50f6e16f704 --- /dev/null +++ b/assets/themes/default/fonts/Playfair_Display/PlayfairDisplay.css @@ -0,0 +1,60 @@ +@font-face { + font-family: 'Playfair Display'; + src: url('PlayfairDisplay-Bold.eot'); + src: url('PlayfairDisplay-Bold.eot?#iefix') format('embedded-opentype'), + url('PlayfairDisplay-Bold.woff2') format('woff2'), + url('PlayfairDisplay-Bold.woff') format('woff'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'Playfair Display'; + src: url('PlayfairDisplay-Black.eot'); + src: url('PlayfairDisplay-Black.eot?#iefix') format('embedded-opentype'), + url('PlayfairDisplay-Black.woff2') format('woff2'), + url('PlayfairDisplay-Black.woff') format('woff'); + font-weight: 900; + font-style: normal; +} + +@font-face { + font-family: 'Playfair Display'; + src: url('PlayfairDisplay-BoldItalic.eot'); + src: url('PlayfairDisplay-BoldItalic.eot?#iefix') format('embedded-opentype'), + url('PlayfairDisplay-BoldItalic.woff2') format('woff2'), + url('PlayfairDisplay-BoldItalic.woff') format('woff'); + font-weight: bold; + font-style: italic; +} + +@font-face { + font-family: 'Playfair Display'; + src: url('PlayfairDisplay-BlackItalic.eot'); + src: url('PlayfairDisplay-BlackItalic.eot?#iefix') format('embedded-opentype'), + url('PlayfairDisplay-BlackItalic.woff2') format('woff2'), + url('PlayfairDisplay-BlackItalic.woff') format('woff'); + font-weight: 900; + font-style: italic; +} + +@font-face { + font-family: 'Playfair Display'; + src: url('PlayfairDisplay-Italic.eot'); + src: url('PlayfairDisplay-Italic.eot?#iefix') format('embedded-opentype'), + url('PlayfairDisplay-Italic.woff2') format('woff2'), + url('PlayfairDisplay-Italic.woff') format('woff'); + font-weight: normal; + font-style: italic; +} + +@font-face { + font-family: 'Playfair Display'; + src: url('PlayfairDisplay-Regular.eot'); + src: url('PlayfairDisplay-Regular.eot?#iefix') format('embedded-opentype'), + url('PlayfairDisplay-Regular.woff2') format('woff2'), + url('PlayfairDisplay-Regular.woff') format('woff'); + font-weight: normal; + font-style: normal; +} + diff --git a/assets/themes/default/fonts/Route159/Route159-Bold.eot b/assets/themes/default/fonts/Route159/Route159-Bold.eot new file mode 100644 index 00000000000..205a0751fb8 Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-Bold.eot differ diff --git a/assets/themes/default/fonts/Route159/Route159-Bold.woff b/assets/themes/default/fonts/Route159/Route159-Bold.woff new file mode 100644 index 00000000000..2f6a576a98e Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-Bold.woff differ diff --git a/assets/themes/default/fonts/Route159/Route159-BoldItalic.eot b/assets/themes/default/fonts/Route159/Route159-BoldItalic.eot new file mode 100644 index 00000000000..813ab8d9cb7 Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-BoldItalic.eot differ diff --git a/assets/themes/default/fonts/Route159/Route159-BoldItalic.woff b/assets/themes/default/fonts/Route159/Route159-BoldItalic.woff new file mode 100644 index 00000000000..6a36eef59d2 Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-BoldItalic.woff differ diff --git a/assets/themes/default/fonts/Route159/Route159-Heavy.eot b/assets/themes/default/fonts/Route159/Route159-Heavy.eot new file mode 100644 index 00000000000..2fbaed62f43 Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-Heavy.eot differ diff --git a/assets/themes/default/fonts/Route159/Route159-Heavy.woff b/assets/themes/default/fonts/Route159/Route159-Heavy.woff new file mode 100644 index 00000000000..4f20bbd05c6 Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-Heavy.woff differ diff --git a/assets/themes/default/fonts/Route159/Route159-HeavyItalic.eot b/assets/themes/default/fonts/Route159/Route159-HeavyItalic.eot new file mode 100644 index 00000000000..97f60423711 Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-HeavyItalic.eot differ diff --git a/assets/themes/default/fonts/Route159/Route159-HeavyItalic.woff b/assets/themes/default/fonts/Route159/Route159-HeavyItalic.woff new file mode 100644 index 00000000000..961ce48b8c4 Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-HeavyItalic.woff differ diff --git a/assets/themes/default/fonts/Route159/Route159-Italic.eot b/assets/themes/default/fonts/Route159/Route159-Italic.eot new file mode 100644 index 00000000000..c130da420d8 Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-Italic.eot differ diff --git a/assets/themes/default/fonts/Route159/Route159-Italic.woff b/assets/themes/default/fonts/Route159/Route159-Italic.woff new file mode 100644 index 00000000000..63a27c75435 Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-Italic.woff differ diff --git a/assets/themes/default/fonts/Route159/Route159-Light.eot b/assets/themes/default/fonts/Route159/Route159-Light.eot new file mode 100644 index 00000000000..c5ac90e3d3a Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-Light.eot differ diff --git a/assets/themes/default/fonts/Route159/Route159-Light.woff b/assets/themes/default/fonts/Route159/Route159-Light.woff new file mode 100644 index 00000000000..e8b931b612d Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-Light.woff differ diff --git a/assets/themes/default/fonts/Route159/Route159-LightItalic.eot b/assets/themes/default/fonts/Route159/Route159-LightItalic.eot new file mode 100644 index 00000000000..b7d0c6fccaf Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-LightItalic.eot differ diff --git a/assets/themes/default/fonts/Route159/Route159-LightItalic.woff b/assets/themes/default/fonts/Route159/Route159-LightItalic.woff new file mode 100644 index 00000000000..d8eb84f0c5c Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-LightItalic.woff differ diff --git a/assets/themes/default/fonts/Route159/Route159-Regular.eot b/assets/themes/default/fonts/Route159/Route159-Regular.eot new file mode 100644 index 00000000000..c804fabee28 Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-Regular.eot differ diff --git a/assets/themes/default/fonts/Route159/Route159-Regular.woff b/assets/themes/default/fonts/Route159/Route159-Regular.woff new file mode 100644 index 00000000000..e9e8046e4d9 Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-Regular.woff differ diff --git a/assets/themes/default/fonts/Route159/Route159-SemiBold.eot b/assets/themes/default/fonts/Route159/Route159-SemiBold.eot new file mode 100644 index 00000000000..21a8a1efa4d Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-SemiBold.eot differ diff --git a/assets/themes/default/fonts/Route159/Route159-SemiBold.woff b/assets/themes/default/fonts/Route159/Route159-SemiBold.woff new file mode 100644 index 00000000000..fd0602a7f08 Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-SemiBold.woff differ diff --git a/assets/themes/default/fonts/Route159/Route159-SemiBoldItalic.eot b/assets/themes/default/fonts/Route159/Route159-SemiBoldItalic.eot new file mode 100644 index 00000000000..5fd076e54db Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-SemiBoldItalic.eot differ diff --git a/assets/themes/default/fonts/Route159/Route159-SemiBoldItalic.woff b/assets/themes/default/fonts/Route159/Route159-SemiBoldItalic.woff new file mode 100644 index 00000000000..99e726b3a99 Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-SemiBoldItalic.woff differ diff --git a/assets/themes/default/fonts/Route159/Route159-UltraLight.eot b/assets/themes/default/fonts/Route159/Route159-UltraLight.eot new file mode 100644 index 00000000000..3633a8e5d7c Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-UltraLight.eot differ diff --git a/assets/themes/default/fonts/Route159/Route159-UltraLight.woff b/assets/themes/default/fonts/Route159/Route159-UltraLight.woff new file mode 100644 index 00000000000..01dd35b4d23 Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-UltraLight.woff differ diff --git a/assets/themes/default/fonts/Route159/Route159-UltraLightItalic.eot b/assets/themes/default/fonts/Route159/Route159-UltraLightItalic.eot new file mode 100644 index 00000000000..7f5dfe288ba Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-UltraLightItalic.eot differ diff --git a/assets/themes/default/fonts/Route159/Route159-UltraLightItalic.woff b/assets/themes/default/fonts/Route159/Route159-UltraLightItalic.woff new file mode 100644 index 00000000000..f5884e741a0 Binary files /dev/null and b/assets/themes/default/fonts/Route159/Route159-UltraLightItalic.woff differ diff --git a/assets/themes/default/fonts/Route159/Route159.css b/assets/themes/default/fonts/Route159/Route159.css new file mode 100644 index 00000000000..1402e1747e9 --- /dev/null +++ b/assets/themes/default/fonts/Route159/Route159.css @@ -0,0 +1,101 @@ +@font-face +{ + font-family: "Route159"; + font-weight: 200; + font-style: normal; + src: url("Route159-UltraLight.woff") format("woff"), + url("Route159-UltraLight.eot") format("embedded-opentype"); +} +@font-face +{ + font-family: "Route159"; + font-weight: 200; + font-style: italic; + src: url("Route159-UltraLightItalic.woff") format("woff"), + url("Route159-UltraLightItalic.eot") format("embedded-opentype"); +} + +@font-face +{ + font-family: "Route159"; + font-weight: 300; + font-style: normal; + src: url("Route159-Light.woff") format("woff"), + url("Route159-Light.eot") format("embedded-opentype"); +} +@font-face +{ + font-family: "Route159"; + font-weight: 300; + font-style: italic; + src: url("Route159-LightItalic.woff") format("woff"), + url("Route159-LightItalic.eot") format("embedded-opentype"); +} + +@font-face +{ + font-family: "Route159"; + font-weight: normal; + font-style: normal; + src: url("Route159-Regular.woff") format("woff"), + url("Route159-Regular.eot") format("embedded-opentype"); +} +@font-face +{ + font-family: "Route159"; + font-weight: normal; + font-style: italic; + src: url("Route159-Italic.woff") format("woff"), + url("Route159-Italic.eot") format("embedded-opentype"); +} + +@font-face +{ + font-family: "Route159"; + font-weight: 600; + font-style: normal; + src: url("Route159-SemiBold.woff") format("woff"), + url("Route159-SemiBold.eot") format("embedded-opentype"); +} +@font-face +{ + font-family: "Route159"; + font-weight: 600; + font-style: italic; + src: url("Route159-SemiBoldItalic.woff") format("woff"), + url("Route159-SemiBoldItalic.eot") format("embedded-opentype"); +} + +@font-face +{ + font-family: "Route159"; + font-weight: bold; + font-style: normal; + src: url("Route159-Bold.woff") format("woff"), + url("Route159-Bold.eot") format("embedded-opentype"); +} +@font-face +{ + font-family: "Route159"; + font-weight: bold; + font-style: italic; + src: url("Route159-BoldItalic.woff") format("woff"), + url("Route159-BoldItalic.eot") format("embedded-opentype"); +} + +@font-face +{ + font-family: "Route159"; + font-weight: 900; + font-style: normal; + src: url("Route159-Heavy.woff") format("woff"), + url("Route159-Heavy.eot") format("embedded-opentype"); +} +@font-face +{ + font-family: "Route159"; + font-weight: 900; + font-style: italic; + src: url("Route159-HeavyItalic.woff") format("woff"), + url("Route159-HeavyItalic.eot") format("embedded-opentype"); +} \ No newline at end of file diff --git a/assets/themes/default/fonts/Shabnam_WOL/LICENSE b/assets/themes/default/fonts/Shabnam_WOL/LICENSE new file mode 100644 index 00000000000..099bf8f69fa --- /dev/null +++ b/assets/themes/default/fonts/Shabnam_WOL/LICENSE @@ -0,0 +1,94 @@ +Copyright (c) 2015, Saber Rastikerdar (saber.rastikerdar@gmail.com), +Glyphs and data from Roboto font are licensed under the Apache License, Version 2.0. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Bold-WOL.eot b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Bold-WOL.eot new file mode 100644 index 00000000000..193d9dda5a2 Binary files /dev/null and b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Bold-WOL.eot differ diff --git a/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Bold-WOL.ttf b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Bold-WOL.ttf new file mode 100644 index 00000000000..e9642963e2f Binary files /dev/null and b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Bold-WOL.ttf differ diff --git a/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Bold-WOL.woff b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Bold-WOL.woff new file mode 100644 index 00000000000..fe2c296ae87 Binary files /dev/null and b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Bold-WOL.woff differ diff --git a/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Bold-WOL.woff2 b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Bold-WOL.woff2 new file mode 100644 index 00000000000..7e91d019c59 Binary files /dev/null and b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Bold-WOL.woff2 differ diff --git a/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Light-WOL.eot b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Light-WOL.eot new file mode 100644 index 00000000000..192cea92e04 Binary files /dev/null and b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Light-WOL.eot differ diff --git a/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Light-WOL.ttf b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Light-WOL.ttf new file mode 100644 index 00000000000..2d996f526c4 Binary files /dev/null and b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Light-WOL.ttf differ diff --git a/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Light-WOL.woff b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Light-WOL.woff new file mode 100644 index 00000000000..96d0106a992 Binary files /dev/null and b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Light-WOL.woff differ diff --git a/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Light-WOL.woff2 b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Light-WOL.woff2 new file mode 100644 index 00000000000..7b38086f740 Binary files /dev/null and b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Light-WOL.woff2 differ diff --git a/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Medium-WOL.eot b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Medium-WOL.eot new file mode 100644 index 00000000000..abf3eb65f0e Binary files /dev/null and b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Medium-WOL.eot differ diff --git a/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Medium-WOL.ttf b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Medium-WOL.ttf new file mode 100644 index 00000000000..ac3859ae97c Binary files /dev/null and b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Medium-WOL.ttf differ diff --git a/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Medium-WOL.woff b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Medium-WOL.woff new file mode 100644 index 00000000000..8110ff1fa35 Binary files /dev/null and b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Medium-WOL.woff differ diff --git a/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Medium-WOL.woff2 b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Medium-WOL.woff2 new file mode 100644 index 00000000000..4847f6f6451 Binary files /dev/null and b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Medium-WOL.woff2 differ diff --git a/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Thin-WOL.eot b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Thin-WOL.eot new file mode 100644 index 00000000000..8245425e5bb Binary files /dev/null and b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Thin-WOL.eot differ diff --git a/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Thin-WOL.ttf b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Thin-WOL.ttf new file mode 100644 index 00000000000..136124263e0 Binary files /dev/null and b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Thin-WOL.ttf differ diff --git a/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Thin-WOL.woff b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Thin-WOL.woff new file mode 100644 index 00000000000..fd5a9c0c471 Binary files /dev/null and b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Thin-WOL.woff differ diff --git a/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Thin-WOL.woff2 b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Thin-WOL.woff2 new file mode 100644 index 00000000000..98ed15e8151 Binary files /dev/null and b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-Thin-WOL.woff2 differ diff --git a/assets/themes/default/fonts/Shabnam_WOL/Shabnam-WOL.eot b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-WOL.eot new file mode 100644 index 00000000000..b574ddee5d9 Binary files /dev/null and b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-WOL.eot differ diff --git a/assets/themes/default/fonts/Shabnam_WOL/Shabnam-WOL.ttf b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-WOL.ttf new file mode 100644 index 00000000000..9f627382e9b Binary files /dev/null and b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-WOL.ttf differ diff --git a/assets/themes/default/fonts/Shabnam_WOL/Shabnam-WOL.woff b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-WOL.woff new file mode 100644 index 00000000000..503c0d1593b Binary files /dev/null and b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-WOL.woff differ diff --git a/assets/themes/default/fonts/Shabnam_WOL/Shabnam-WOL.woff2 b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-WOL.woff2 new file mode 100644 index 00000000000..300ff5a1fb1 Binary files /dev/null and b/assets/themes/default/fonts/Shabnam_WOL/Shabnam-WOL.woff2 differ diff --git a/assets/themes/default/fonts/Shabnam_WOL/Shabnam_WOL.css b/assets/themes/default/fonts/Shabnam_WOL/Shabnam_WOL.css new file mode 100644 index 00000000000..9542cb76f61 --- /dev/null +++ b/assets/themes/default/fonts/Shabnam_WOL/Shabnam_WOL.css @@ -0,0 +1,49 @@ +@font-face { + font-family: Shabnam; + src: url("Shabnam-WOL.eot"); + src: url("Shabnam-WOL.eot?#iefix") format("embedded-opentype"), + url("Shabnam-WOL.woff2") format("woff2"), + url("Shabnam-WOL.woff") format("woff"), + url("Shabnam-WOL.ttf") format("truetype"); + font-weight: normal; +} + +@font-face { + font-family: Shabnam; + src: url("Shabnam-Bold-WOL.eot"); + src: url("Shabnam-Bold-WOL.eot?#iefix") format("embedded-opentype"), + url("Shabnam-Bold-WOL.woff2") format("woff2"), + url("Shabnam-Bold-WOL.woff") format("woff"), + url("Shabnam-Bold-WOL.ttf") format("truetype"); + font-weight: bold; +} + +@font-face { + font-family: Shabnam; + src: url("Shabnam-Thin-WOL.eot"); + src: url("Shabnam-Thin-WOL.eot?#iefix") format("embedded-opentype"), + url("Shabnam-Thin-WOL.woff2") format("woff2"), + url("Shabnam-Thin-WOL.woff") format("woff"), + url("Shabnam-Thin-WOL.ttf") format("truetype"); + font-weight: 100; +} + +@font-face { + font-family: Shabnam; + src: url("Shabnam-Light-WOL.eot"); + src: url("Shabnam-Light-WOL.eot?#iefix") format("embedded-opentype"), + url("Shabnam-Light-WOL.woff2") format("woff2"), + url("Shabnam-Light-WOL.woff") format("woff"), + url("Shabnam-Light-WOL.ttf") format("truetype"); + font-weight: 300; +} + +@font-face { + font-family: Shabnam; + src: url("Shabnam-Medium-WOL.eot"); + src: url("Shabnam-Medium-WOL.eot?#iefix") format("embedded-opentype"), + url("Shabnam-Medium-WOL.woff2") format("woff2"), + url("Shabnam-Medium-WOL.woff") format("woff"), + url("Shabnam-Medium-WOL.ttf") format("truetype"); + font-weight: 500; +} diff --git a/assets/themes/default/fonts/Vazir_WOL/LICENSE b/assets/themes/default/fonts/Vazir_WOL/LICENSE new file mode 100644 index 00000000000..711721180b9 --- /dev/null +++ b/assets/themes/default/fonts/Vazir_WOL/LICENSE @@ -0,0 +1,51 @@ +Changes by Saber Rastikerdar (saber.rastikerdar@gmail.com) are in public domain. +Glyphs and data from Roboto font are licensed under the Apache License, Version 2.0. + +Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. + +Bitstream Vera Fonts Copyright +------------------------------ + +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is +a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license ("Fonts") and associated +documentation files (the "Font Software"), to reproduce and distribute the +Font Software, including without limitation the rights to use, copy, merge, +publish, distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to the +following conditions: + +The above copyright and trademark notices and this permission notice shall +be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular +the designs of glyphs or characters in the Fonts may be modified and +additional glyphs or characters may be added to the Fonts, only if the fonts +are renamed to names not containing either the words "Bitstream" or the word +"Vera". + +This License becomes null and void to the extent applicable to Fonts or Font +Software that has been modified and is distributed under the "Bitstream +Vera" names. + +The Font Software may be sold as part of a larger software package but no +copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, +TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME +FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING +ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE +FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome +Foundation, and Bitstream Inc., shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this Font Software +without prior written authorization from the Gnome Foundation or Bitstream +Inc., respectively. For further information, contact: fonts at gnome dot +org. \ No newline at end of file diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-Black-WOL.eot b/assets/themes/default/fonts/Vazir_WOL/Vazir-Black-WOL.eot new file mode 100644 index 00000000000..ae776a1f906 Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-Black-WOL.eot differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-Black-WOL.ttf b/assets/themes/default/fonts/Vazir_WOL/Vazir-Black-WOL.ttf new file mode 100644 index 00000000000..da23baa5595 Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-Black-WOL.ttf differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-Black-WOL.woff b/assets/themes/default/fonts/Vazir_WOL/Vazir-Black-WOL.woff new file mode 100644 index 00000000000..8acfed2be10 Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-Black-WOL.woff differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-Black-WOL.woff2 b/assets/themes/default/fonts/Vazir_WOL/Vazir-Black-WOL.woff2 new file mode 100644 index 00000000000..0974a7630e1 Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-Black-WOL.woff2 differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-Bold-WOL.eot b/assets/themes/default/fonts/Vazir_WOL/Vazir-Bold-WOL.eot new file mode 100644 index 00000000000..a246f9fb81c Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-Bold-WOL.eot differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-Bold-WOL.ttf b/assets/themes/default/fonts/Vazir_WOL/Vazir-Bold-WOL.ttf new file mode 100644 index 00000000000..47ab36279cd Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-Bold-WOL.ttf differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-Bold-WOL.woff b/assets/themes/default/fonts/Vazir_WOL/Vazir-Bold-WOL.woff new file mode 100644 index 00000000000..32cc01b36d0 Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-Bold-WOL.woff differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-Bold-WOL.woff2 b/assets/themes/default/fonts/Vazir_WOL/Vazir-Bold-WOL.woff2 new file mode 100644 index 00000000000..bb919226e90 Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-Bold-WOL.woff2 differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-Light-WOL.eot b/assets/themes/default/fonts/Vazir_WOL/Vazir-Light-WOL.eot new file mode 100644 index 00000000000..b65a1f0ee27 Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-Light-WOL.eot differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-Light-WOL.ttf b/assets/themes/default/fonts/Vazir_WOL/Vazir-Light-WOL.ttf new file mode 100644 index 00000000000..aaf2b264083 Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-Light-WOL.ttf differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-Light-WOL.woff b/assets/themes/default/fonts/Vazir_WOL/Vazir-Light-WOL.woff new file mode 100644 index 00000000000..adb61f90bc4 Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-Light-WOL.woff differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-Light-WOL.woff2 b/assets/themes/default/fonts/Vazir_WOL/Vazir-Light-WOL.woff2 new file mode 100644 index 00000000000..8c755e6261f Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-Light-WOL.woff2 differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-Medium-WOL.eot b/assets/themes/default/fonts/Vazir_WOL/Vazir-Medium-WOL.eot new file mode 100644 index 00000000000..c95da7a4e10 Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-Medium-WOL.eot differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-Medium-WOL.ttf b/assets/themes/default/fonts/Vazir_WOL/Vazir-Medium-WOL.ttf new file mode 100644 index 00000000000..79046a04fb7 Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-Medium-WOL.ttf differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-Medium-WOL.woff b/assets/themes/default/fonts/Vazir_WOL/Vazir-Medium-WOL.woff new file mode 100644 index 00000000000..f03eedfd225 Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-Medium-WOL.woff differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-Medium-WOL.woff2 b/assets/themes/default/fonts/Vazir_WOL/Vazir-Medium-WOL.woff2 new file mode 100644 index 00000000000..11e9784170a Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-Medium-WOL.woff2 differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-Thin-WOL.eot b/assets/themes/default/fonts/Vazir_WOL/Vazir-Thin-WOL.eot new file mode 100644 index 00000000000..b9527e6a79e Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-Thin-WOL.eot differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-Thin-WOL.ttf b/assets/themes/default/fonts/Vazir_WOL/Vazir-Thin-WOL.ttf new file mode 100644 index 00000000000..7a1970854b6 Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-Thin-WOL.ttf differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-Thin-WOL.woff b/assets/themes/default/fonts/Vazir_WOL/Vazir-Thin-WOL.woff new file mode 100644 index 00000000000..1f211da4f70 Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-Thin-WOL.woff differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-Thin-WOL.woff2 b/assets/themes/default/fonts/Vazir_WOL/Vazir-Thin-WOL.woff2 new file mode 100644 index 00000000000..95d8ccbadce Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-Thin-WOL.woff2 differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-WOL.eot b/assets/themes/default/fonts/Vazir_WOL/Vazir-WOL.eot new file mode 100644 index 00000000000..8eafa991701 Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-WOL.eot differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-WOL.ttf b/assets/themes/default/fonts/Vazir_WOL/Vazir-WOL.ttf new file mode 100644 index 00000000000..4e63195bc77 Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-WOL.ttf differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-WOL.woff b/assets/themes/default/fonts/Vazir_WOL/Vazir-WOL.woff new file mode 100644 index 00000000000..f07de507618 Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-WOL.woff differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir-WOL.woff2 b/assets/themes/default/fonts/Vazir_WOL/Vazir-WOL.woff2 new file mode 100644 index 00000000000..d967cecdf59 Binary files /dev/null and b/assets/themes/default/fonts/Vazir_WOL/Vazir-WOL.woff2 differ diff --git a/assets/themes/default/fonts/Vazir_WOL/Vazir_WOL.css b/assets/themes/default/fonts/Vazir_WOL/Vazir_WOL.css new file mode 100644 index 00000000000..47045016a3c --- /dev/null +++ b/assets/themes/default/fonts/Vazir_WOL/Vazir_WOL.css @@ -0,0 +1,65 @@ +@font-face { + font-family: Vazir; + src: url('Vazir-WOL.eot'); + src: url('Vazir-WOL.eot?#iefix') format('embedded-opentype'), + url('Vazir-WOL.woff2') format('woff2'), + url('Vazir-WOL.woff') format('woff'), + url('Vazir-WOL.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: Vazir; + src: url('Vazir-Bold-WOL.eot'); + src: url('Vazir-Bold-WOL.eot?#iefix') format('embedded-opentype'), + url('Vazir-Bold-WOL.woff2') format('woff2'), + url('Vazir-Bold-WOL.woff') format('woff'), + url('Vazir-Bold-WOL.ttf') format('truetype'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: Vazir; + src: url('Vazir-Black-WOL.eot'); + src: url('Vazir-Black-WOL.eot?#iefix') format('embedded-opentype'), + url('Vazir-Black-WOL.woff2') format('woff2'), + url('Vazir-Black-WOL.woff') format('woff'), + url('Vazir-Black-WOL.ttf') format('truetype'); + font-weight: 900; + font-style: normal; +} + +@font-face { + font-family: Vazir; + src: url('Vazir-Medium-WOL.eot'); + src: url('Vazir-Medium-WOL.eot?#iefix') format('embedded-opentype'), + url('Vazir-Medium-WOL.woff2') format('woff2'), + url('Vazir-Medium-WOL.woff') format('woff'), + url('Vazir-Medium-WOL.ttf') format('truetype'); + font-weight: 500; + font-style: normal; +} + +@font-face { + font-family: Vazir; + src: url('Vazir-Light-WOL.eot'); + src: url('Vazir-Light-WOL.eot?#iefix') format('embedded-opentype'), + url('Vazir-Light-WOL.woff2') format('woff2'), + url('Vazir-Light-WOL.woff') format('woff'), + url('Vazir-Light-WOL.ttf') format('truetype'); + font-weight: 300; + font-style: normal; +} + +@font-face { + font-family: Vazir; + src: url('Vazir-Thin-WOL.eot'); + src: url('Vazir-Thin-WOL.eot?#iefix') format('embedded-opentype'), + url('Vazir-Thin-WOL.woff2') format('woff2'), + url('Vazir-Thin-WOL.woff') format('woff'), + url('Vazir-Thin-WOL.ttf') format('truetype'); + font-weight: 100; + font-style: normal; +} diff --git a/assets/themes/default/light.scss b/assets/themes/default/light.scss new file mode 100644 index 00000000000..7e3f1a4e69f --- /dev/null +++ b/assets/themes/default/light.scss @@ -0,0 +1,14 @@ +/* color palette: https://coolors.co/23f0c7-ef767a-7765e3-6457a6-ffe347 */ + +@import url("./feather.css"); +@import url("./fonts/Route159/Route159.css"); +@import url("./fonts/Lora/Lora.css"); +@import url("./fonts/Playfair_Display/PlayfairDisplay.css"); +@import url("./fonts/Vazir_WOL/Vazir_WOL.css"); +@import url("./fonts/Shabnam_WOL/Shabnam_WOL.css"); + +@import "variables"; +@import "global"; +@import "header"; +@import "article"; +@import "forms"; diff --git a/build.rs b/build.rs new file mode 100644 index 00000000000..0cc50b5b739 --- /dev/null +++ b/build.rs @@ -0,0 +1,154 @@ +use ructe::Ructe; +use std::process::{Command, Stdio}; +use std::{ffi::OsStr, fs::*, io::Write, path::*}; + +fn compute_static_hash() -> String { + //"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/*"]) + .stdout(Stdio::piped()) + .spawn() + .expect("failed find command"); + + let sort = Command::new("sort") + .stdin(find.stdout.unwrap()) + .stdout(Stdio::piped()) + .spawn() + .expect("failed sort command"); + + let xargs = Command::new("xargs") + .args(&["stat", "-c'%n %Y'"]) + .stdin(sort.stdout.unwrap()) + .stdout(Stdio::piped()) + .spawn() + .expect("failed xargs command"); + + let mut sha = Command::new("openssl") + .args(&["dgst", "-r"]) + .stdin(xargs.stdout.unwrap()) + .output() + .expect("failed openssl command"); + + sha.stdout.resize(64, 0); + String::from_utf8(sha.stdout).unwrap() +} + +fn main() { + Ructe::from_env() + .expect("This must be run with cargo") + .compile_templates("templates") + .expect("compile templates"); + + compile_themes().expect("Theme compilation error"); + recursive_copy(&Path::new("assets").join("icons"), Path::new("static")) + .expect("Couldn't copy icons"); + recursive_copy(&Path::new("assets").join("images"), Path::new("static")) + .expect("Couldn't copy images"); + create_dir_all(&Path::new("static").join("media")).expect("Couldn't init media directory"); + + let cache_id = &compute_static_hash()[..8]; + println!("cargo:rerun-if-changed=plume-front/pkg/plume_front_bg.wasm"); + copy( + "plume-front/pkg/plume_front_bg.wasm", + "static/plume_front_bg.wasm", + ) + .and_then(|_| copy("plume-front/pkg/plume_front.js", "static/plume_front.js")) + .ok(); + + println!("cargo:rustc-env=CACHE_ID={}", cache_id) +} + +fn compile_themes() -> std::io::Result<()> { + let input_dir = Path::new("assets").join("themes"); + let output_dir = Path::new("static").join("css"); + + let themes = find_themes(input_dir)?; + + for theme in themes { + compile_theme(&theme, &output_dir)?; + } + + Ok(()) +} + +fn find_themes(path: PathBuf) -> std::io::Result> { + let ext = path.extension().and_then(OsStr::to_str); + if metadata(&path)?.is_dir() { + Ok(read_dir(&path)?.fold(vec![], |mut themes, ch| { + if let Ok(ch) = ch { + if let Ok(mut new) = find_themes(ch.path()) { + themes.append(&mut new); + } + } + themes + })) + } else if (ext == Some("scss") || ext == Some("sass")) + && !path.file_name().unwrap().to_str().unwrap().starts_with('_') + { + Ok(vec![path.clone()]) + } else { + Ok(vec![]) + } +} + +fn compile_theme(path: &Path, out_dir: &Path) -> std::io::Result<()> { + let name = path + .components() + .skip_while(|c| *c != Component::Normal(OsStr::new("themes"))) + .skip(1) + .map(|c| { + c.as_os_str() + .to_str() + .unwrap_or_default() + .split_once('.') + .map_or(c.as_os_str().to_str().unwrap_or_default(), |x| x.0) + }) + .collect::>() + .join("-"); + + let dir = path.parent().unwrap(); + + let out = out_dir.join(name); + create_dir_all(&out)?; + + // copy files of the theme that are not scss + for ch in read_dir(&dir)? { + recursive_copy(&ch?.path(), &out)?; + } + + // compile the .scss/.sass file + let mut out = File::create(out.join("theme.css"))?; + out.write_all( + &rsass::compile_scss_path( + path, + rsass::output::Format { + style: rsass::output::Style::Compressed, + ..rsass::output::Format::default() + }, + ) + .expect("SCSS compilation error"), + )?; + + Ok(()) +} + +fn recursive_copy(path: &Path, out_dir: &Path) -> std::io::Result<()> { + if metadata(path)?.is_dir() { + let out = out_dir.join(path.file_name().unwrap()); + create_dir_all(out.clone())?; + + for ch in read_dir(path)? { + recursive_copy(&ch?.path(), &out)?; + } + } else { + println!("cargo:rerun-if-changed={}", path.display()); + + let ext = path.extension().and_then(OsStr::to_str); + if ext != Some("scss") && ext != Some("sass") { + copy(path, out_dir.join(path.file_name().unwrap()))?; + } + } + + Ok(()) +} diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 00000000000..0eebdc232c3 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,8 @@ +"project_id": 352097 +"api_token_env": "CROWDIN_API_KEY" +preserve_hierarchy: true +files: + - source: /po/plume/plume.pot + translation: /po/plume/%two_letters_code%.po + - source: /po/plume-front/plume-front.pot + translation: /po/plume-front/%two_letters_code%.po diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 00000000000..57dcc64e60b --- /dev/null +++ b/diesel.toml @@ -0,0 +1,5 @@ +# For documentation on how to configure this file, +# see diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "plume-models/src/schema.rs" diff --git a/migrations/.gitkeep b/migrations/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/migrations/postgres/00000000000000_diesel_initial_setup/down.sql b/migrations/postgres/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 00000000000..a9f52609119 --- /dev/null +++ b/migrations/postgres/00000000000000_diesel_initial_setup/down.sql @@ -0,0 +1,6 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + +DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); +DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/migrations/postgres/00000000000000_diesel_initial_setup/up.sql b/migrations/postgres/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 00000000000..d68895b1a7b --- /dev/null +++ b/migrations/postgres/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/migrations/postgres/2018-04-22-093322_create_instances/down.sql b/migrations/postgres/2018-04-22-093322_create_instances/down.sql new file mode 100644 index 00000000000..1ec93bf28b8 --- /dev/null +++ b/migrations/postgres/2018-04-22-093322_create_instances/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE instances; diff --git a/migrations/postgres/2018-04-22-093322_create_instances/up.sql b/migrations/postgres/2018-04-22-093322_create_instances/up.sql new file mode 100644 index 00000000000..e6689b0f2b1 --- /dev/null +++ b/migrations/postgres/2018-04-22-093322_create_instances/up.sql @@ -0,0 +1,9 @@ +-- Your SQL goes here +CREATE TABLE instances ( + id SERIAL PRIMARY KEY, + local_domain VARCHAR NOT NULL, + public_domain VARCHAR NOT NULL, + name VARCHAR NOT NULL, + local BOOLEAN NOT NULL DEFAULT 'f', + blocked BOOLEAN NOT NULL DEFAULT 'f' +) diff --git a/migrations/postgres/2018-04-22-151330_create_user/down.sql b/migrations/postgres/2018-04-22-151330_create_user/down.sql new file mode 100644 index 00000000000..dc3714bd11c --- /dev/null +++ b/migrations/postgres/2018-04-22-151330_create_user/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE users; diff --git a/migrations/postgres/2018-04-22-151330_create_user/up.sql b/migrations/postgres/2018-04-22-151330_create_user/up.sql new file mode 100644 index 00000000000..6c6114e30ff --- /dev/null +++ b/migrations/postgres/2018-04-22-151330_create_user/up.sql @@ -0,0 +1,13 @@ +-- Your SQL goes here +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username VARCHAR NOT NULL, + display_name VARCHAR NOT NULL DEFAULT '', + outbox_url VARCHAR NOT NULL, + inbox_url VARCHAR NOT NULL, + is_admin BOOLEAN NOT NULL DEFAULT 'f', + summary TEXT NOT NULL DEFAULT '', + email TEXT, + hashed_password TEXT, + instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL +); diff --git a/migrations/postgres/2018-04-23-101717_create_blogs/down.sql b/migrations/postgres/2018-04-23-101717_create_blogs/down.sql new file mode 100644 index 00000000000..4f8b0a68091 --- /dev/null +++ b/migrations/postgres/2018-04-23-101717_create_blogs/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE blogs; diff --git a/migrations/postgres/2018-04-23-101717_create_blogs/up.sql b/migrations/postgres/2018-04-23-101717_create_blogs/up.sql new file mode 100644 index 00000000000..4b26e85d1ed --- /dev/null +++ b/migrations/postgres/2018-04-23-101717_create_blogs/up.sql @@ -0,0 +1,10 @@ +-- Your SQL goes here +CREATE TABLE blogs ( + id SERIAL PRIMARY KEY, + actor_id VARCHAR NOT NULL, + title VARCHAR NOT NULL, + summary TEXT NOT NULL DEFAULT '', + outbox_url VARCHAR NOT NULL, + inbox_url VARCHAR NOT NULL, + instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL +) diff --git a/migrations/postgres/2018-04-23-111655_create_blog_authors/down.sql b/migrations/postgres/2018-04-23-111655_create_blog_authors/down.sql new file mode 100644 index 00000000000..cfb62abd0e6 --- /dev/null +++ b/migrations/postgres/2018-04-23-111655_create_blog_authors/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE blog_authors; diff --git a/migrations/postgres/2018-04-23-111655_create_blog_authors/up.sql b/migrations/postgres/2018-04-23-111655_create_blog_authors/up.sql new file mode 100644 index 00000000000..f59e6c7bbfc --- /dev/null +++ b/migrations/postgres/2018-04-23-111655_create_blog_authors/up.sql @@ -0,0 +1,7 @@ +-- Your SQL goes here +CREATE TABLE blog_authors ( + id SERIAL PRIMARY KEY, + blog_id INTEGER REFERENCES blogs(id) ON DELETE CASCADE NOT NULL, + author_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + is_owner BOOLEAN NOT NULL DEFAULT 'f' +) diff --git a/migrations/postgres/2018-04-23-132822_create_posts/down.sql b/migrations/postgres/2018-04-23-132822_create_posts/down.sql new file mode 100644 index 00000000000..56ed16e5387 --- /dev/null +++ b/migrations/postgres/2018-04-23-132822_create_posts/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE posts; diff --git a/migrations/postgres/2018-04-23-132822_create_posts/up.sql b/migrations/postgres/2018-04-23-132822_create_posts/up.sql new file mode 100644 index 00000000000..5a606cd3a28 --- /dev/null +++ b/migrations/postgres/2018-04-23-132822_create_posts/up.sql @@ -0,0 +1,10 @@ +-- Your SQL goes here +CREATE TABLE posts ( + id SERIAL PRIMARY KEY, + blog_id INTEGER REFERENCES blogs(id) ON DELETE CASCADE NOT NULL, + slug VARCHAR NOT NULL, + title VARCHAR NOT NULL, + content TEXT NOT NULL DEFAULT '', + published BOOLEAN NOT NULL DEFAULT 'f', + license VARCHAR NOT NULL DEFAULT 'CC-0' +) diff --git a/migrations/postgres/2018-04-23-142746_create_post_authors/down.sql b/migrations/postgres/2018-04-23-142746_create_post_authors/down.sql new file mode 100644 index 00000000000..129bf59ac6b --- /dev/null +++ b/migrations/postgres/2018-04-23-142746_create_post_authors/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE post_authors; diff --git a/migrations/postgres/2018-04-23-142746_create_post_authors/up.sql b/migrations/postgres/2018-04-23-142746_create_post_authors/up.sql new file mode 100644 index 00000000000..6819c74ed1b --- /dev/null +++ b/migrations/postgres/2018-04-23-142746_create_post_authors/up.sql @@ -0,0 +1,6 @@ +-- Your SQL goes here +CREATE TABLE post_authors ( + id SERIAL PRIMARY KEY, + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL, + author_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL +) diff --git a/migrations/postgres/2018-04-30-170445_timestamps/down.sql b/migrations/postgres/2018-04-30-170445_timestamps/down.sql new file mode 100644 index 00000000000..6c688b746c9 --- /dev/null +++ b/migrations/postgres/2018-04-30-170445_timestamps/down.sql @@ -0,0 +1,5 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE posts DROP COLUMN creation_date; +ALTER TABLE blogs DROP COLUMN creation_date; +ALTER TABLE users DROP COLUMN creation_date; +ALTER TABLE instances DROP COLUMN creation_date; diff --git a/migrations/postgres/2018-04-30-170445_timestamps/up.sql b/migrations/postgres/2018-04-30-170445_timestamps/up.sql new file mode 100644 index 00000000000..23ad535ef40 --- /dev/null +++ b/migrations/postgres/2018-04-30-170445_timestamps/up.sql @@ -0,0 +1,5 @@ +-- Your SQL goes here +ALTER TABLE posts ADD COLUMN creation_date TIMESTAMP NOT NULL DEFAULT now(); +ALTER TABLE blogs ADD COLUMN creation_date TIMESTAMP NOT NULL DEFAULT now(); +ALTER TABLE users ADD COLUMN creation_date TIMESTAMP NOT NULL DEFAULT now(); +ALTER TABLE instances ADD COLUMN creation_date TIMESTAMP NOT NULL DEFAULT now(); diff --git a/migrations/postgres/2018-05-01-124607_create_follow/down.sql b/migrations/postgres/2018-05-01-124607_create_follow/down.sql new file mode 100644 index 00000000000..eee3b972cb7 --- /dev/null +++ b/migrations/postgres/2018-05-01-124607_create_follow/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE follows; diff --git a/migrations/postgres/2018-05-01-124607_create_follow/up.sql b/migrations/postgres/2018-05-01-124607_create_follow/up.sql new file mode 100644 index 00000000000..d5298f25a33 --- /dev/null +++ b/migrations/postgres/2018-05-01-124607_create_follow/up.sql @@ -0,0 +1,6 @@ +-- Your SQL goes here +CREATE TABLE follows ( + id SERIAL PRIMARY KEY, + follower_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + following_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL +) diff --git a/migrations/postgres/2018-05-01-165325_add_ap_url/down.sql b/migrations/postgres/2018-05-01-165325_add_ap_url/down.sql new file mode 100644 index 00000000000..2f1533f1b1d --- /dev/null +++ b/migrations/postgres/2018-05-01-165325_add_ap_url/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE blogs DROP COLUMN ap_url; +ALTER TABLE users DROP COLUMN ap_url; diff --git a/migrations/postgres/2018-05-01-165325_add_ap_url/up.sql b/migrations/postgres/2018-05-01-165325_add_ap_url/up.sql new file mode 100644 index 00000000000..1f2f8e2d860 --- /dev/null +++ b/migrations/postgres/2018-05-01-165325_add_ap_url/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE blogs ADD COLUMN ap_url TEXT NOT NULL default ''; +ALTER TABLE users ADD COLUMN ap_url TEXT NOT NULL default ''; diff --git a/migrations/postgres/2018-05-02-113930_drop_instance_local_domain/down.sql b/migrations/postgres/2018-05-02-113930_drop_instance_local_domain/down.sql new file mode 100644 index 00000000000..fb0ef2e7400 --- /dev/null +++ b/migrations/postgres/2018-05-02-113930_drop_instance_local_domain/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE instances ADD COLUMN local_domain VARCHAR NOT NULL; diff --git a/migrations/postgres/2018-05-02-113930_drop_instance_local_domain/up.sql b/migrations/postgres/2018-05-02-113930_drop_instance_local_domain/up.sql new file mode 100644 index 00000000000..2d78cffa658 --- /dev/null +++ b/migrations/postgres/2018-05-02-113930_drop_instance_local_domain/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE instances DROP COLUMN local_domain; diff --git a/migrations/postgres/2018-05-03-163427_user_add_keys/down.sql b/migrations/postgres/2018-05-03-163427_user_add_keys/down.sql new file mode 100644 index 00000000000..056b83264e4 --- /dev/null +++ b/migrations/postgres/2018-05-03-163427_user_add_keys/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE users DROP COLUMN private_key; +ALTER TABLE users DROP COLUMN public_key; diff --git a/migrations/postgres/2018-05-03-163427_user_add_keys/up.sql b/migrations/postgres/2018-05-03-163427_user_add_keys/up.sql new file mode 100644 index 00000000000..869f8238556 --- /dev/null +++ b/migrations/postgres/2018-05-03-163427_user_add_keys/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE users ADD COLUMN private_key TEXT; +ALTER TABLE users ADD COLUMN public_key TEXT NOT NULL DEFAULT ''; diff --git a/migrations/postgres/2018-05-03-182555_blogs_add_keys/down.sql b/migrations/postgres/2018-05-03-182555_blogs_add_keys/down.sql new file mode 100644 index 00000000000..3646c18b8e0 --- /dev/null +++ b/migrations/postgres/2018-05-03-182555_blogs_add_keys/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE blogs DROP COLUMN private_key; +ALTER TABLE blogs DROP COLUMN public_key; diff --git a/migrations/postgres/2018-05-03-182555_blogs_add_keys/up.sql b/migrations/postgres/2018-05-03-182555_blogs_add_keys/up.sql new file mode 100644 index 00000000000..cbd45305d8a --- /dev/null +++ b/migrations/postgres/2018-05-03-182555_blogs_add_keys/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE blogs ADD COLUMN private_key TEXT; +ALTER TABLE blogs ADD COLUMN public_key TEXT NOT NULL DEFAULT ''; diff --git a/migrations/postgres/2018-05-09-192013_create_comments/down.sql b/migrations/postgres/2018-05-09-192013_create_comments/down.sql new file mode 100644 index 00000000000..d0841ffb256 --- /dev/null +++ b/migrations/postgres/2018-05-09-192013_create_comments/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE comments; diff --git a/migrations/postgres/2018-05-09-192013_create_comments/up.sql b/migrations/postgres/2018-05-09-192013_create_comments/up.sql new file mode 100644 index 00000000000..66ab301fa8b --- /dev/null +++ b/migrations/postgres/2018-05-09-192013_create_comments/up.sql @@ -0,0 +1,12 @@ +-- Your SQL goes here +CREATE TABLE comments ( + id SERIAL PRIMARY KEY, + content TEXT NOT NULL DEFAULT '', + in_response_to_id INTEGER REFERENCES comments(id), + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL, + author_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + creation_date TIMESTAMP NOT NULL DEFAULT now(), + ap_url VARCHAR, + sensitive BOOLEAN NOT NULL DEFAULT 'f', + spoiler_text TEXT NOT NULL DEFAULT '' +) diff --git a/migrations/postgres/2018-05-10-101553_posts_add_ap_url/down.sql b/migrations/postgres/2018-05-10-101553_posts_add_ap_url/down.sql new file mode 100644 index 00000000000..2a1fb813590 --- /dev/null +++ b/migrations/postgres/2018-05-10-101553_posts_add_ap_url/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE posts DROP COLUMN ap_url; diff --git a/migrations/postgres/2018-05-10-101553_posts_add_ap_url/up.sql b/migrations/postgres/2018-05-10-101553_posts_add_ap_url/up.sql new file mode 100644 index 00000000000..0ed4e93905a --- /dev/null +++ b/migrations/postgres/2018-05-10-101553_posts_add_ap_url/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE posts ADD COLUMN ap_url VARCHAR NOT NULL DEFAULT ''; diff --git a/migrations/postgres/2018-05-10-154336_create_likes/down.sql b/migrations/postgres/2018-05-10-154336_create_likes/down.sql new file mode 100644 index 00000000000..2232ad5b4f8 --- /dev/null +++ b/migrations/postgres/2018-05-10-154336_create_likes/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE likes; diff --git a/migrations/postgres/2018-05-10-154336_create_likes/up.sql b/migrations/postgres/2018-05-10-154336_create_likes/up.sql new file mode 100644 index 00000000000..3c5f119dc35 --- /dev/null +++ b/migrations/postgres/2018-05-10-154336_create_likes/up.sql @@ -0,0 +1,7 @@ +-- Your SQL goes here +CREATE TABLE likes ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL, + creation_date TIMESTAMP NOT NULL DEFAULT now() +) diff --git a/migrations/postgres/2018-05-12-213456_likes_add_ap_url/down.sql b/migrations/postgres/2018-05-12-213456_likes_add_ap_url/down.sql new file mode 100644 index 00000000000..b68f195072d --- /dev/null +++ b/migrations/postgres/2018-05-12-213456_likes_add_ap_url/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE likes DROP COLUMN ap_url; diff --git a/migrations/postgres/2018-05-12-213456_likes_add_ap_url/up.sql b/migrations/postgres/2018-05-12-213456_likes_add_ap_url/up.sql new file mode 100644 index 00000000000..42b4e967dd9 --- /dev/null +++ b/migrations/postgres/2018-05-12-213456_likes_add_ap_url/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE likes ADD COLUMN ap_url VARCHAR NOT NULL default ''; diff --git a/migrations/postgres/2018-05-13-122311_create_notifications/down.sql b/migrations/postgres/2018-05-13-122311_create_notifications/down.sql new file mode 100644 index 00000000000..bcebcc05f33 --- /dev/null +++ b/migrations/postgres/2018-05-13-122311_create_notifications/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE notifications; diff --git a/migrations/postgres/2018-05-13-122311_create_notifications/up.sql b/migrations/postgres/2018-05-13-122311_create_notifications/up.sql new file mode 100644 index 00000000000..f8d11849da4 --- /dev/null +++ b/migrations/postgres/2018-05-13-122311_create_notifications/up.sql @@ -0,0 +1,8 @@ +-- Your SQL goes here +CREATE TABLE notifications ( + id SERIAL PRIMARY KEY, + title VARCHAR NOT NULL DEFAULT '', + content TEXT, + link VARCHAR, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL +) diff --git a/migrations/postgres/2018-05-13-175144_users_add_shared_inbox/down.sql b/migrations/postgres/2018-05-13-175144_users_add_shared_inbox/down.sql new file mode 100644 index 00000000000..cd33ad7b7ab --- /dev/null +++ b/migrations/postgres/2018-05-13-175144_users_add_shared_inbox/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE users DROP COLUMN shared_inbox_url; diff --git a/migrations/postgres/2018-05-13-175144_users_add_shared_inbox/up.sql b/migrations/postgres/2018-05-13-175144_users_add_shared_inbox/up.sql new file mode 100644 index 00000000000..a16875f5e77 --- /dev/null +++ b/migrations/postgres/2018-05-13-175144_users_add_shared_inbox/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE users ADD COLUMN shared_inbox_url VARCHAR; diff --git a/migrations/postgres/2018-05-19-091428_create_reshares/down.sql b/migrations/postgres/2018-05-19-091428_create_reshares/down.sql new file mode 100644 index 00000000000..29a2d0fb45b --- /dev/null +++ b/migrations/postgres/2018-05-19-091428_create_reshares/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE reshares; diff --git a/migrations/postgres/2018-05-19-091428_create_reshares/up.sql b/migrations/postgres/2018-05-19-091428_create_reshares/up.sql new file mode 100644 index 00000000000..3366d41b89d --- /dev/null +++ b/migrations/postgres/2018-05-19-091428_create_reshares/up.sql @@ -0,0 +1,8 @@ +-- Your SQL goes here +CREATE TABLE reshares ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL, + ap_url VARCHAR NOT NULL DEFAULT '', + creation_date TIMESTAMP NOT NULL DEFAULT now() +) diff --git a/migrations/postgres/2018-05-24-100613_add_notifications_creation_date/down.sql b/migrations/postgres/2018-05-24-100613_add_notifications_creation_date/down.sql new file mode 100644 index 00000000000..ca04e11e7b5 --- /dev/null +++ b/migrations/postgres/2018-05-24-100613_add_notifications_creation_date/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE notifications DROP COLUMN creation_date; diff --git a/migrations/postgres/2018-05-24-100613_add_notifications_creation_date/up.sql b/migrations/postgres/2018-05-24-100613_add_notifications_creation_date/up.sql new file mode 100644 index 00000000000..fddcc3879d0 --- /dev/null +++ b/migrations/postgres/2018-05-24-100613_add_notifications_creation_date/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE notifications ADD COLUMN creation_date TIMESTAMP NOT NULL DEFAULT now(); diff --git a/migrations/postgres/2018-06-17-200302_notification_add_data/down.sql b/migrations/postgres/2018-06-17-200302_notification_add_data/down.sql new file mode 100644 index 00000000000..41922c38fe3 --- /dev/null +++ b/migrations/postgres/2018-06-17-200302_notification_add_data/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE notifications DROP COLUMN data; diff --git a/migrations/postgres/2018-06-17-200302_notification_add_data/up.sql b/migrations/postgres/2018-06-17-200302_notification_add_data/up.sql new file mode 100644 index 00000000000..f88a7231754 --- /dev/null +++ b/migrations/postgres/2018-06-17-200302_notification_add_data/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE notifications ADD COLUMN data VARCHAR; diff --git a/migrations/postgres/2018-06-20-175532_create_mentions/down.sql b/migrations/postgres/2018-06-20-175532_create_mentions/down.sql new file mode 100644 index 00000000000..e860c9ad6bd --- /dev/null +++ b/migrations/postgres/2018-06-20-175532_create_mentions/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE mentions; diff --git a/migrations/postgres/2018-06-20-175532_create_mentions/up.sql b/migrations/postgres/2018-06-20-175532_create_mentions/up.sql new file mode 100644 index 00000000000..7640e35b77e --- /dev/null +++ b/migrations/postgres/2018-06-20-175532_create_mentions/up.sql @@ -0,0 +1,7 @@ +-- Your SQL goes here +CREATE TABLE mentions ( + id SERIAL PRIMARY KEY, + mentioned_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE, + comment_id INTEGER REFERENCES comments(id) ON DELETE CASCADE +) diff --git a/migrations/postgres/2018-06-20-194538_add_mentions_ap_url/down.sql b/migrations/postgres/2018-06-20-194538_add_mentions_ap_url/down.sql new file mode 100644 index 00000000000..4e626f3b22e --- /dev/null +++ b/migrations/postgres/2018-06-20-194538_add_mentions_ap_url/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE mentions DROP COLUMN ap_url; diff --git a/migrations/postgres/2018-06-20-194538_add_mentions_ap_url/up.sql b/migrations/postgres/2018-06-20-194538_add_mentions_ap_url/up.sql new file mode 100644 index 00000000000..df9c3c7934b --- /dev/null +++ b/migrations/postgres/2018-06-20-194538_add_mentions_ap_url/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE mentions ADD COLUMN ap_url VARCHAR NOT NULL DEFAULT ''; diff --git a/migrations/postgres/2018-07-25-165754_refactor_notifications/down.sql b/migrations/postgres/2018-07-25-165754_refactor_notifications/down.sql new file mode 100644 index 00000000000..53830f4a3b3 --- /dev/null +++ b/migrations/postgres/2018-07-25-165754_refactor_notifications/down.sql @@ -0,0 +1,8 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE notifications ADD COLUMN title VARCHAR NOT NULL; +ALTER TABLE notifications ADD COLUMN content TEXT; +ALTER TABLE notifications ADD COLUMN link VARCHAR; +ALTER TABLE notifications ADD COLUMN data VARCHAR; + +ALTER TABLE notifications DROP COLUMN kind; +ALTER TABLE notifications DROP COLUMN object_id; diff --git a/migrations/postgres/2018-07-25-165754_refactor_notifications/up.sql b/migrations/postgres/2018-07-25-165754_refactor_notifications/up.sql new file mode 100644 index 00000000000..e3ab66410fa --- /dev/null +++ b/migrations/postgres/2018-07-25-165754_refactor_notifications/up.sql @@ -0,0 +1,8 @@ +-- Your SQL goes here +ALTER TABLE notifications DROP COLUMN title; +ALTER TABLE notifications DROP COLUMN content; +ALTER TABLE notifications DROP COLUMN link; +ALTER TABLE notifications DROP COLUMN data; + +ALTER TABLE notifications ADD COLUMN kind VARCHAR NOT NULL DEFAULT 'unknown'; +ALTER TABLE notifications ADD COLUMN object_id INTEGER NOT NULL DEFAULT 0; diff --git a/migrations/postgres/2018-07-27-102221_user_add_followers_endpoint/down.sql b/migrations/postgres/2018-07-27-102221_user_add_followers_endpoint/down.sql new file mode 100644 index 00000000000..3beaffacd0b --- /dev/null +++ b/migrations/postgres/2018-07-27-102221_user_add_followers_endpoint/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE users DROP COLUMN followers_endpoint; diff --git a/migrations/postgres/2018-07-27-102221_user_add_followers_endpoint/up.sql b/migrations/postgres/2018-07-27-102221_user_add_followers_endpoint/up.sql new file mode 100644 index 00000000000..f565e8ed814 --- /dev/null +++ b/migrations/postgres/2018-07-27-102221_user_add_followers_endpoint/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE users ADD COLUMN followers_endpoint VARCHAR NOT NULL DEFAULT ''; diff --git a/migrations/postgres/2018-07-27-125558_instance_customization/down.sql b/migrations/postgres/2018-07-27-125558_instance_customization/down.sql new file mode 100644 index 00000000000..38cca3cd37e --- /dev/null +++ b/migrations/postgres/2018-07-27-125558_instance_customization/down.sql @@ -0,0 +1,5 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE instances DROP COLUMN open_registrations; +ALTER TABLE instances DROP COLUMN short_description; +ALTER TABLE instances DROP COLUMN long_description; +ALTER TABLE instances DROP COLUMN default_license; diff --git a/migrations/postgres/2018-07-27-125558_instance_customization/up.sql b/migrations/postgres/2018-07-27-125558_instance_customization/up.sql new file mode 100644 index 00000000000..1efde74c75f --- /dev/null +++ b/migrations/postgres/2018-07-27-125558_instance_customization/up.sql @@ -0,0 +1,5 @@ +-- Your SQL goes here +ALTER TABLE instances ADD COLUMN open_registrations BOOLEAN NOT NULL DEFAULT 't'; +ALTER TABLE instances ADD COLUMN short_description TEXT NOT NULL DEFAULT ''; +ALTER TABLE instances ADD COLUMN long_description TEXT NOT NULL DEFAULT ''; +ALTER TABLE instances ADD COLUMN default_license TEXT NOT NULL DEFAULT 'CC-0'; diff --git a/migrations/postgres/2018-07-27-194816_instance_description_html/down.sql b/migrations/postgres/2018-07-27-194816_instance_description_html/down.sql new file mode 100644 index 00000000000..99f0b18ef7f --- /dev/null +++ b/migrations/postgres/2018-07-27-194816_instance_description_html/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE instances DROP COLUMN long_description_html; +ALTER TABLE instances DROP COLUMN short_description_html; diff --git a/migrations/postgres/2018-07-27-194816_instance_description_html/up.sql b/migrations/postgres/2018-07-27-194816_instance_description_html/up.sql new file mode 100644 index 00000000000..47cbee03535 --- /dev/null +++ b/migrations/postgres/2018-07-27-194816_instance_description_html/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE instances ADD COLUMN long_description_html VARCHAR NOT NULL DEFAULT ''; +ALTER TABLE instances ADD COLUMN short_description_html VARCHAR NOT NULL DEFAULT ''; diff --git a/migrations/postgres/2018-09-02-111458_create_medias/down.sql b/migrations/postgres/2018-09-02-111458_create_medias/down.sql new file mode 100644 index 00000000000..3ba01786b33 --- /dev/null +++ b/migrations/postgres/2018-09-02-111458_create_medias/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE medias; diff --git a/migrations/postgres/2018-09-02-111458_create_medias/up.sql b/migrations/postgres/2018-09-02-111458_create_medias/up.sql new file mode 100644 index 00000000000..f89f448a573 --- /dev/null +++ b/migrations/postgres/2018-09-02-111458_create_medias/up.sql @@ -0,0 +1,10 @@ +-- Your SQL goes here +CREATE TABLE medias ( + id SERIAL PRIMARY KEY, + file_path TEXT NOT NULL DEFAULT '', + alt_text TEXT NOT NULL DEFAULT '', + is_remote BOOLEAN NOT NULL DEFAULT 'f', + remote_url TEXT, + sensitive BOOLEAN NOT NULL DEFAULT 'f', + content_warning TEXT +) diff --git a/migrations/postgres/2018-09-02-123623_medias_owner_id/down.sql b/migrations/postgres/2018-09-02-123623_medias_owner_id/down.sql new file mode 100644 index 00000000000..c44809d684d --- /dev/null +++ b/migrations/postgres/2018-09-02-123623_medias_owner_id/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE medias DROP COLUMN owner_id; diff --git a/migrations/postgres/2018-09-02-123623_medias_owner_id/up.sql b/migrations/postgres/2018-09-02-123623_medias_owner_id/up.sql new file mode 100644 index 00000000000..52dfa5c9ce0 --- /dev/null +++ b/migrations/postgres/2018-09-02-123623_medias_owner_id/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE medias ADD COLUMN owner_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL; diff --git a/migrations/postgres/2018-09-03-102510_users_add_avatar/down.sql b/migrations/postgres/2018-09-03-102510_users_add_avatar/down.sql new file mode 100644 index 00000000000..cf822ecd6a0 --- /dev/null +++ b/migrations/postgres/2018-09-03-102510_users_add_avatar/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE users DROP COLUMN avatar_id; diff --git a/migrations/postgres/2018-09-03-102510_users_add_avatar/up.sql b/migrations/postgres/2018-09-03-102510_users_add_avatar/up.sql new file mode 100644 index 00000000000..5a97db2acbe --- /dev/null +++ b/migrations/postgres/2018-09-03-102510_users_add_avatar/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE users ADD COLUMN avatar_id INTEGER REFERENCES medias(id) ON DELETE CASCADE; diff --git a/migrations/postgres/2018-09-03-170848_user_add_last_fetched_date/down.sql b/migrations/postgres/2018-09-03-170848_user_add_last_fetched_date/down.sql new file mode 100644 index 00000000000..2b3eb2d4f24 --- /dev/null +++ b/migrations/postgres/2018-09-03-170848_user_add_last_fetched_date/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE users DROP COLUMN last_fetched_date; diff --git a/migrations/postgres/2018-09-03-170848_user_add_last_fetched_date/up.sql b/migrations/postgres/2018-09-03-170848_user_add_last_fetched_date/up.sql new file mode 100644 index 00000000000..9f823f0de94 --- /dev/null +++ b/migrations/postgres/2018-09-03-170848_user_add_last_fetched_date/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE users ADD COLUMN last_fetched_date TIMESTAMP NOT NULL DEFAULT '2000-01-01 00:00:01'; diff --git a/migrations/postgres/2018-09-04-103017_follows_add_ap_url/down.sql b/migrations/postgres/2018-09-04-103017_follows_add_ap_url/down.sql new file mode 100644 index 00000000000..a1d36e37577 --- /dev/null +++ b/migrations/postgres/2018-09-04-103017_follows_add_ap_url/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE follows DROP COLUMN ap_url; diff --git a/migrations/postgres/2018-09-04-103017_follows_add_ap_url/up.sql b/migrations/postgres/2018-09-04-103017_follows_add_ap_url/up.sql new file mode 100644 index 00000000000..e4c7d949eed --- /dev/null +++ b/migrations/postgres/2018-09-04-103017_follows_add_ap_url/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE follows ADD COLUMN ap_url TEXT NOT NULL DEFAULT ''; diff --git a/migrations/postgres/2018-09-04-104828_posts_add_subtitle/down.sql b/migrations/postgres/2018-09-04-104828_posts_add_subtitle/down.sql new file mode 100644 index 00000000000..0c39127b208 --- /dev/null +++ b/migrations/postgres/2018-09-04-104828_posts_add_subtitle/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE posts DROP COLUMN subtitle; diff --git a/migrations/postgres/2018-09-04-104828_posts_add_subtitle/up.sql b/migrations/postgres/2018-09-04-104828_posts_add_subtitle/up.sql new file mode 100644 index 00000000000..17ab4754f89 --- /dev/null +++ b/migrations/postgres/2018-09-04-104828_posts_add_subtitle/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE posts ADD COLUMN subtitle TEXT NOT NULL DEFAULT ''; diff --git a/migrations/postgres/2018-09-05-174106_create_tags/down.sql b/migrations/postgres/2018-09-05-174106_create_tags/down.sql new file mode 100644 index 00000000000..43c79a4bb78 --- /dev/null +++ b/migrations/postgres/2018-09-05-174106_create_tags/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE tags; diff --git a/migrations/postgres/2018-09-05-174106_create_tags/up.sql b/migrations/postgres/2018-09-05-174106_create_tags/up.sql new file mode 100644 index 00000000000..9ef32855417 --- /dev/null +++ b/migrations/postgres/2018-09-05-174106_create_tags/up.sql @@ -0,0 +1,7 @@ +-- Your SQL goes here +CREATE TABLE tags ( + id SERIAL PRIMARY KEY, + tag TEXT NOT NULL DEFAULT '', + is_hastag BOOLEAN NOT NULL DEFAULT 'f', + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL +) diff --git a/migrations/postgres/2018-09-06-182637_posts_add_source/down.sql b/migrations/postgres/2018-09-06-182637_posts_add_source/down.sql new file mode 100644 index 00000000000..43773773579 --- /dev/null +++ b/migrations/postgres/2018-09-06-182637_posts_add_source/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE posts DROP COLUMN source; diff --git a/migrations/postgres/2018-09-06-182637_posts_add_source/up.sql b/migrations/postgres/2018-09-06-182637_posts_add_source/up.sql new file mode 100644 index 00000000000..a880b5ba919 --- /dev/null +++ b/migrations/postgres/2018-09-06-182637_posts_add_source/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE posts ADD COLUMN source TEXT NOT NULL DEFAULT ''; diff --git a/migrations/postgres/2018-09-07-212003_fix_avatar_deletion/down.sql b/migrations/postgres/2018-09-07-212003_fix_avatar_deletion/down.sql new file mode 100644 index 00000000000..a3f0047d797 --- /dev/null +++ b/migrations/postgres/2018-09-07-212003_fix_avatar_deletion/down.sql @@ -0,0 +1,7 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE users + DROP CONSTRAINT users_avatar_id_fkey, + ADD CONSTRAINT users_avatar_id_fkey + FOREIGN KEY (avatar_id) + REFERENCES medias(id) + ON DELETE CASCADE; diff --git a/migrations/postgres/2018-09-07-212003_fix_avatar_deletion/up.sql b/migrations/postgres/2018-09-07-212003_fix_avatar_deletion/up.sql new file mode 100644 index 00000000000..780af5ab9cd --- /dev/null +++ b/migrations/postgres/2018-09-07-212003_fix_avatar_deletion/up.sql @@ -0,0 +1,7 @@ +-- Your SQL goes here +ALTER TABLE users + DROP CONSTRAINT users_avatar_id_fkey, + ADD CONSTRAINT users_avatar_id_fkey + FOREIGN KEY (avatar_id) + REFERENCES medias(id) + ON DELETE SET NULL; diff --git a/migrations/postgres/2018-10-06-161151_change_default_license/down.sql b/migrations/postgres/2018-10-06-161151_change_default_license/down.sql new file mode 100644 index 00000000000..5643a2a6728 --- /dev/null +++ b/migrations/postgres/2018-10-06-161151_change_default_license/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE ONLY posts ALTER COLUMN license SET DEFAULT 'CC-0'; +ALTER TABLE ONLY instances ALTER COLUMN default_license SET DEFAULT 'CC-0'; diff --git a/migrations/postgres/2018-10-06-161151_change_default_license/up.sql b/migrations/postgres/2018-10-06-161151_change_default_license/up.sql new file mode 100644 index 00000000000..376beea1aa6 --- /dev/null +++ b/migrations/postgres/2018-10-06-161151_change_default_license/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes hereALTER TABLE ONLY posts ALTER COLUMN license SET DEFAULT 'CC-BY-SA'; +ALTER TABLE ONLY posts ALTER COLUMN license SET DEFAULT 'CC-BY-SA'; +ALTER TABLE ONLY instances ALTER COLUMN default_license SET DEFAULT 'CC-BY-SA'; diff --git a/migrations/postgres/2018-10-19-165407_create_apps/down.sql b/migrations/postgres/2018-10-19-165407_create_apps/down.sql new file mode 100755 index 00000000000..09b3f60905e --- /dev/null +++ b/migrations/postgres/2018-10-19-165407_create_apps/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE apps; diff --git a/migrations/postgres/2018-10-19-165407_create_apps/up.sql b/migrations/postgres/2018-10-19-165407_create_apps/up.sql new file mode 100755 index 00000000000..48ab4dcff4c --- /dev/null +++ b/migrations/postgres/2018-10-19-165407_create_apps/up.sql @@ -0,0 +1,10 @@ +-- Your SQL goes here +CREATE TABLE apps ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL DEFAULT '', + client_id TEXT NOT NULL, + client_secret TEXT NOT NULL, + redirect_uri TEXT, + website TEXT, + creation_date TIMESTAMP NOT NULL DEFAULT now() +); diff --git a/migrations/postgres/2018-10-20-164036_fix_hastag_typo/down.sql b/migrations/postgres/2018-10-20-164036_fix_hastag_typo/down.sql new file mode 100644 index 00000000000..e96261d7d45 --- /dev/null +++ b/migrations/postgres/2018-10-20-164036_fix_hastag_typo/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE tags RENAME COLUMN is_hashtag TO is_hastag; diff --git a/migrations/postgres/2018-10-20-164036_fix_hastag_typo/up.sql b/migrations/postgres/2018-10-20-164036_fix_hastag_typo/up.sql new file mode 100644 index 00000000000..32914c21f98 --- /dev/null +++ b/migrations/postgres/2018-10-20-164036_fix_hastag_typo/up.sql @@ -0,0 +1 @@ +ALTER TABLE tags RENAME COLUMN is_hastag TO is_hashtag; diff --git a/migrations/postgres/2018-10-21-163227_create_api_token/down.sql b/migrations/postgres/2018-10-21-163227_create_api_token/down.sql new file mode 100644 index 00000000000..e71f14786b0 --- /dev/null +++ b/migrations/postgres/2018-10-21-163227_create_api_token/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE api_tokens; diff --git a/migrations/postgres/2018-10-21-163227_create_api_token/up.sql b/migrations/postgres/2018-10-21-163227_create_api_token/up.sql new file mode 100644 index 00000000000..ecf9f5128f8 --- /dev/null +++ b/migrations/postgres/2018-10-21-163227_create_api_token/up.sql @@ -0,0 +1,9 @@ +-- Your SQL goes here +CREATE TABLE api_tokens ( + id SERIAL PRIMARY KEY, + creation_date TIMESTAMP NOT NULL DEFAULT now(), + value TEXT NOT NULL, + scopes TEXT NOT NULL, + app_id INTEGER NOT NULL REFERENCES apps(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE +) diff --git a/migrations/postgres/2018-10-30-151529_add_post_cover/down.sql b/migrations/postgres/2018-10-30-151529_add_post_cover/down.sql new file mode 100644 index 00000000000..15321a9b7a1 --- /dev/null +++ b/migrations/postgres/2018-10-30-151529_add_post_cover/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE posts DROP COLUMN cover_id; diff --git a/migrations/postgres/2018-10-30-151529_add_post_cover/up.sql b/migrations/postgres/2018-10-30-151529_add_post_cover/up.sql new file mode 100644 index 00000000000..d3239a25bf7 --- /dev/null +++ b/migrations/postgres/2018-10-30-151529_add_post_cover/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE posts ADD COLUMN cover_id INTEGER REFERENCES medias(id) DEFAULT NULL; diff --git a/migrations/postgres/2018-12-08-175515_constraints/down.sql b/migrations/postgres/2018-12-08-175515_constraints/down.sql new file mode 100644 index 00000000000..e189dc2224d --- /dev/null +++ b/migrations/postgres/2018-12-08-175515_constraints/down.sql @@ -0,0 +1,24 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE api_tokens DROP CONSTRAINT IF EXISTS api_tokens_unique_value; +ALTER TABLE blog_authors DROP CONSTRAINT IF EXISTS blog_author_unique; +ALTER TABLE blogs DROP CONSTRAINT IF EXISTS blog_unique; +ALTER TABLE blogs DROP CONSTRAINT IF EXISTS blog_unique_ap_url; +ALTER TABLE blogs DROP CONSTRAINT IF EXISTS blog_unique_outbox; +ALTER TABLE blogs DROP CONSTRAINT IF EXISTS blog_unique_inbox; +ALTER TABLE comments DROP CONSTRAINT IF EXISTS comments_unique_ap_url; +ALTER TABLE follows DROP CONSTRAINT IF EXISTS follows_unique_ap_url; +ALTER TABLE instances DROP CONSTRAINT IF EXISTS instance_unique_domain; +ALTER TABLE likes DROP CONSTRAINT IF EXISTS likes_unique; +ALTER TABLE likes DROP CONSTRAINT IF EXISTS likes_unique_ap_url; +ALTER TABLE mentions DROP CONSTRAINT IF EXISTS mentions_unique_ap_url; +ALTER TABLE post_authors DROP CONSTRAINT IF EXISTS post_authors_unique; +ALTER TABLE posts DROP CONSTRAINT IF EXISTS post_unique_slug; +ALTER TABLE posts DROP CONSTRAINT IF EXISTS post_unique_ap_url; +ALTER TABLE reshares DROP CONSTRAINT IF EXISTS reshares_unique; +ALTER TABLE reshares DROP CONSTRAINT IF EXISTS reshares_unique_ap_url; +ALTER TABLE tags DROP CONSTRAINT IF EXISTS tags_unique; +ALTER TABLE users DROP CONSTRAINT IF EXISTS users_unique; +ALTER TABLE users DROP CONSTRAINT IF EXISTS users_unique_inbox; +ALTER TABLE users DROP CONSTRAINT IF EXISTS users_unique_outbox; +ALTER TABLE users DROP CONSTRAINT IF EXISTS users_unique_ap_url; +ALTER TABLE users DROP CONSTRAINT IF EXISTS users_unique_followers_url; diff --git a/migrations/postgres/2018-12-08-175515_constraints/up.sql b/migrations/postgres/2018-12-08-175515_constraints/up.sql new file mode 100644 index 00000000000..21b6f3b631a --- /dev/null +++ b/migrations/postgres/2018-12-08-175515_constraints/up.sql @@ -0,0 +1,83 @@ +-- Your SQL goes here + +-- First, we delete the already duplicated data so that the constraint can be correctly applied + +DELETE FROM api_tokens a USING api_tokens b WHERE + a.id > b.id + AND a.value = b.value; +DELETE FROM blog_authors a USING blog_authors b WHERE + a.id > b.id + AND a.blog_id = b.blog_id + AND a.author_id = b.author_id; +DELETE FROM blogs a USING blogs b WHERE + a.id > b.id + AND ((a.actor_id = b.actor_id AND a.instance_id = b.instance_id) + OR a.ap_url = b.ap_url + OR a.outbox_url = b.outbox_url + OR a.inbox_url = b.inbox_url); +DELETE FROM comments a USING comments b WHERE + a.id > b.id + AND (a.ap_url = b.ap_url); +DELETE FROM follows a USING follows b + WHERE a.id > b.id + AND (a.ap_url = b.ap_url); +DELETE FROM instances a USING instances b WHERE + a.id > b.id + AND (a.public_domain = b.public_domain); +DELETE FROM likes a USING likes b WHERE + a.id > b.id + AND (a.ap_url = b.ap_url + OR (a.user_id = b.user_id AND a.post_id = b.post_id)); +DELETE FROM mentions a USING mentions b WHERE + a.id > b.id + AND (a.ap_url = b.ap_url); +DELETE FROM post_authors a USING post_authors b WHERE + a.id > b.id + AND a.author_id = b.author_id + AND a.post_id = b.post_id; +DELETE FROM posts a USING posts b WHERE + a.id > b.id + AND ((a.ap_url = b.ap_url) + OR (a.blog_id = b.blog_id AND a.slug = b.slug)); +DELETE FROM reshares a USING reshares b WHERE + a.id > b.id + AND (a.ap_url = b.ap_url + OR (a.user_id = b.user_id AND a.post_id = b.post_id)); +DELETE FROM tags a USING tags b WHERE + a.id > b.id + AND a.tag = b.tag + AND a.post_id = b.post_id + AND a.is_hashtag = b.is_hashtag; +DELETE FROM users a USING users b WHERE + a.id > b.id + AND (a.ap_url = b.ap_url + OR (a.username = b.username AND a.instance_id = b.instance_id) + OR a.outbox_url = b.outbox_url + OR a.inbox_url = b.inbox_url + OR a.followers_endpoint = b.followers_endpoint); + +-- Then we add the UNIQUE constraints + +ALTER TABLE api_tokens ADD CONSTRAINT api_tokens_unique_value UNIQUE (value); +ALTER TABLE blog_authors ADD CONSTRAINT blog_author_unique UNIQUE (blog_id, author_id); +ALTER TABLE blogs ADD CONSTRAINT blog_unique UNIQUE (actor_id, instance_id); +ALTER TABLE blogs ADD CONSTRAINT blog_unique_ap_url UNIQUE (ap_url); +ALTER TABLE blogs ADD CONSTRAINT blog_unique_outbox UNIQUE (outbox_url); +ALTER TABLE blogs ADD CONSTRAINT blog_unique_inbox UNIQUE (inbox_url); +ALTER TABLE comments ADD CONSTRAINT comments_unique_ap_url UNIQUE (ap_url); +ALTER TABLE follows ADD CONSTRAINT follows_unique_ap_url UNIQUE (ap_url); +ALTER TABLE instances ADD CONSTRAINT instance_unique_domain UNIQUE (public_domain); +ALTER TABLE likes ADD CONSTRAINT likes_unique UNIQUE (user_id, post_id); +ALTER TABLE likes ADD CONSTRAINT likes_unique_ap_url UNIQUE (ap_url); +ALTER TABLE mentions ADD CONSTRAINT mentions_unique_ap_url UNIQUE (ap_url); +ALTER TABLE post_authors ADD CONSTRAINT post_authors_unique UNIQUE (post_id, author_id); +ALTER TABLE posts ADD CONSTRAINT post_unique_slug UNIQUE (blog_id, slug); +ALTER TABLE posts ADD CONSTRAINT post_unique_ap_url UNIQUE (ap_url); +ALTER TABLE reshares ADD CONSTRAINT reshares_unique UNIQUE (user_id, post_id); +ALTER TABLE reshares ADD CONSTRAINT reshares_unique_ap_url UNIQUE (ap_url); +ALTER TABLE tags ADD CONSTRAINT tags_unique UNIQUE (tag, post_id, is_hashtag); +ALTER TABLE users ADD CONSTRAINT users_unique UNIQUE (username, instance_id); +ALTER TABLE users ADD CONSTRAINT users_unique_inbox UNIQUE (inbox_url); +ALTER TABLE users ADD CONSTRAINT users_unique_outbox UNIQUE (outbox_url); +ALTER TABLE users ADD CONSTRAINT users_unique_ap_url UNIQUE (ap_url); +ALTER TABLE users ADD CONSTRAINT users_unique_followers_url UNIQUE (followers_endpoint); diff --git a/migrations/postgres/2018-12-17-180104_mention_no_ap_url/down.sql b/migrations/postgres/2018-12-17-180104_mention_no_ap_url/down.sql new file mode 100644 index 00000000000..9ba08b05665 --- /dev/null +++ b/migrations/postgres/2018-12-17-180104_mention_no_ap_url/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE mentions ADD COLUMN ap_url VARCHAR NOT NULL DEFAULT ''; diff --git a/migrations/postgres/2018-12-17-180104_mention_no_ap_url/up.sql b/migrations/postgres/2018-12-17-180104_mention_no_ap_url/up.sql new file mode 100644 index 00000000000..b56c1189ccb --- /dev/null +++ b/migrations/postgres/2018-12-17-180104_mention_no_ap_url/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE mentions DROP COLUMN ap_url; diff --git a/migrations/postgres/2018-12-17-221135_comment_visibility/down.sql b/migrations/postgres/2018-12-17-221135_comment_visibility/down.sql new file mode 100644 index 00000000000..1c26b21f910 --- /dev/null +++ b/migrations/postgres/2018-12-17-221135_comment_visibility/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE comments DROP COLUMN public_visibility; + +DROP TABLE comment_seers; diff --git a/migrations/postgres/2018-12-17-221135_comment_visibility/up.sql b/migrations/postgres/2018-12-17-221135_comment_visibility/up.sql new file mode 100644 index 00000000000..3507d628a32 --- /dev/null +++ b/migrations/postgres/2018-12-17-221135_comment_visibility/up.sql @@ -0,0 +1,9 @@ +-- Your SQL goes here +ALTER TABLE comments ADD public_visibility BOOLEAN NOT NULL DEFAULT 't'; + +CREATE TABLE comment_seers ( + id SERIAL PRIMARY KEY, + comment_id INTEGER REFERENCES comments(id) ON DELETE CASCADE NOT NULL, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + UNIQUE (comment_id, user_id) +) diff --git a/migrations/postgres/2018-12-25-164502_media-cover-deletion/down.sql b/migrations/postgres/2018-12-25-164502_media-cover-deletion/down.sql new file mode 100644 index 00000000000..9189e2061a3 --- /dev/null +++ b/migrations/postgres/2018-12-25-164502_media-cover-deletion/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` + +ALTER TABLE posts DROP CONSTRAINT posts_cover_id_fkey; +ALTER TABLE posts ADD CONSTRAINT posts_cover_id_fkey FOREIGN KEY (cover_id) REFERENCES medias(id); diff --git a/migrations/postgres/2018-12-25-164502_media-cover-deletion/up.sql b/migrations/postgres/2018-12-25-164502_media-cover-deletion/up.sql new file mode 100644 index 00000000000..414e92e672e --- /dev/null +++ b/migrations/postgres/2018-12-25-164502_media-cover-deletion/up.sql @@ -0,0 +1,4 @@ +-- Your SQL goes here + +ALTER TABLE posts DROP CONSTRAINT posts_cover_id_fkey; +ALTER TABLE posts ADD CONSTRAINT posts_cover_id_fkey FOREIGN KEY (cover_id) REFERENCES medias(id) ON DELETE SET NULL; diff --git a/migrations/postgres/2019-03-05-082814_add_fqn/down.sql b/migrations/postgres/2019-03-05-082814_add_fqn/down.sql new file mode 100644 index 00000000000..796c55edcb6 --- /dev/null +++ b/migrations/postgres/2019-03-05-082814_add_fqn/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE blogs DROP COLUMN fqn; +ALTER TABLE users DROP COLUMN fqn; \ No newline at end of file diff --git a/migrations/postgres/2019-03-05-082814_add_fqn/up.sql b/migrations/postgres/2019-03-05-082814_add_fqn/up.sql new file mode 100644 index 00000000000..f28231de628 --- /dev/null +++ b/migrations/postgres/2019-03-05-082814_add_fqn/up.sql @@ -0,0 +1,18 @@ +-- Your SQL goes here +ALTER TABLE blogs ADD COLUMN fqn TEXT NOT NULL DEFAULT ''; +UPDATE blogs SET fqn = + (CASE WHEN (SELECT local FROM instances WHERE id = instance_id) THEN + actor_id + ELSE + (actor_id || '@' || (SELECT public_domain FROM instances WHERE id = instance_id LIMIT 1)) + END) +WHERE fqn = ''; + +ALTER TABLE users ADD COLUMN fqn TEXT NOT NULL DEFAULT ''; +UPDATE users SET fqn = + (CASE WHEN (SELECT local FROM instances WHERE id = instance_id) THEN + username + ELSE + (username || '@' || (SELECT public_domain FROM instances WHERE id = instance_id LIMIT 1)) + END) +WHERE fqn = ''; \ No newline at end of file diff --git a/migrations/postgres/2019-03-06-115158_blog_images/down.sql b/migrations/postgres/2019-03-06-115158_blog_images/down.sql new file mode 100644 index 00000000000..fc03dd7b698 --- /dev/null +++ b/migrations/postgres/2019-03-06-115158_blog_images/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE blogs DROP COLUMN summary_html; +ALTER TABLE blogs DROP COLUMN icon_id; +ALTER TABLE blogs DROP COLUMN banner_id; \ No newline at end of file diff --git a/migrations/postgres/2019-03-06-115158_blog_images/up.sql b/migrations/postgres/2019-03-06-115158_blog_images/up.sql new file mode 100644 index 00000000000..b2207caa81a --- /dev/null +++ b/migrations/postgres/2019-03-06-115158_blog_images/up.sql @@ -0,0 +1,4 @@ +-- Your SQL goes here +ALTER TABLE blogs ADD COLUMN summary_html TEXT NOT NULL DEFAULT ''; +ALTER TABLE blogs ADD COLUMN icon_id INTEGER REFERENCES medias(id) ON DELETE SET NULL DEFAULT NULL; +ALTER TABLE blogs ADD COLUMN banner_id INTEGER REFERENCES medias(id) ON DELETE SET NULL DEFAULT NULL; \ No newline at end of file diff --git a/migrations/postgres/2019-03-16-143637_summary-md/down.sql b/migrations/postgres/2019-03-16-143637_summary-md/down.sql new file mode 100644 index 00000000000..982805e0942 --- /dev/null +++ b/migrations/postgres/2019-03-16-143637_summary-md/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE users DROP COLUMN summary_html; diff --git a/migrations/postgres/2019-03-16-143637_summary-md/up.sql b/migrations/postgres/2019-03-16-143637_summary-md/up.sql new file mode 100644 index 00000000000..bc7d7c89f41 --- /dev/null +++ b/migrations/postgres/2019-03-16-143637_summary-md/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE users ADD COLUMN summary_html TEXT NOT NULL DEFAULT ''; +UPDATE users SET summary_html = summary; diff --git a/migrations/postgres/2019-05-30-173029_create_password_reset_requests_table/down.sql b/migrations/postgres/2019-05-30-173029_create_password_reset_requests_table/down.sql new file mode 100644 index 00000000000..7d4bc8fde3d --- /dev/null +++ b/migrations/postgres/2019-05-30-173029_create_password_reset_requests_table/down.sql @@ -0,0 +1 @@ +DROP TABLE password_reset_requests; diff --git a/migrations/postgres/2019-05-30-173029_create_password_reset_requests_table/up.sql b/migrations/postgres/2019-05-30-173029_create_password_reset_requests_table/up.sql new file mode 100644 index 00000000000..b1fffc2cfdd --- /dev/null +++ b/migrations/postgres/2019-05-30-173029_create_password_reset_requests_table/up.sql @@ -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); diff --git a/migrations/postgres/2019-06-18-152700_moderator_role/down.sql b/migrations/postgres/2019-06-18-152700_moderator_role/down.sql new file mode 100644 index 00000000000..071d3b4230d --- /dev/null +++ b/migrations/postgres/2019-06-18-152700_moderator_role/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE users ADD COLUMN is_admin BOOLEAN NOT NULL DEFAULT 'f'; +UPDATE users SET is_admin = 't' WHERE role = 0; +ALTER TABLE users DROP COLUMN role; diff --git a/migrations/postgres/2019-06-18-152700_moderator_role/up.sql b/migrations/postgres/2019-06-18-152700_moderator_role/up.sql new file mode 100644 index 00000000000..b7ba8152115 --- /dev/null +++ b/migrations/postgres/2019-06-18-152700_moderator_role/up.sql @@ -0,0 +1,4 @@ +-- Your SQL goes here +ALTER TABLE users ADD COLUMN role INTEGER NOT NULL DEFAULT 2; +UPDATE users SET role = 0 WHERE is_admin = 't'; +ALTER TABLE users DROP COLUMN is_admin; diff --git a/migrations/postgres/2019-06-19-141114_themes/down.sql b/migrations/postgres/2019-06-19-141114_themes/down.sql new file mode 100644 index 00000000000..54e85dffc6b --- /dev/null +++ b/migrations/postgres/2019-06-19-141114_themes/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE blogs DROP COLUMN theme; +ALTER TABLE users DROP COLUMN preferred_theme; +ALTER TABLE users DROP COLUMN hide_custom_css; diff --git a/migrations/postgres/2019-06-19-141114_themes/up.sql b/migrations/postgres/2019-06-19-141114_themes/up.sql new file mode 100644 index 00000000000..5a92b2a0beb --- /dev/null +++ b/migrations/postgres/2019-06-19-141114_themes/up.sql @@ -0,0 +1,4 @@ +-- Your SQL goes here +ALTER TABLE blogs ADD COLUMN theme VARCHAR; +ALTER TABLE users ADD COLUMN preferred_theme VARCHAR; +ALTER TABLE users ADD COLUMN hide_custom_css BOOLEAN NOT NULL DEFAULT 'f'; diff --git a/migrations/postgres/2019-06-20-145757_timeline/down.sql b/migrations/postgres/2019-06-20-145757_timeline/down.sql new file mode 100644 index 00000000000..d86fb6ee1f3 --- /dev/null +++ b/migrations/postgres/2019-06-20-145757_timeline/down.sql @@ -0,0 +1,6 @@ +-- This file should undo anything in `up.sql` + +DROP TABLE timeline; +DROP TABLE timeline_definition; +DROP TABLE list_elems; +DROP TABLE lists; diff --git a/migrations/postgres/2019-06-20-145757_timeline/up.sql b/migrations/postgres/2019-06-20-145757_timeline/up.sql new file mode 100644 index 00000000000..a8a7ae821d9 --- /dev/null +++ b/migrations/postgres/2019-06-20-145757_timeline/up.sql @@ -0,0 +1,31 @@ +-- Your SQL goes here + +CREATE TABLE timeline_definition( + id SERIAL PRIMARY KEY, + user_id integer REFERENCES users ON DELETE CASCADE, + name VARCHAR NOT NULL, + query VARCHAR NOT NULL, + CONSTRAINT timeline_unique_user_name UNIQUE(user_id, name) +); + +CREATE TABLE timeline( + id SERIAL PRIMARY KEY, + post_id integer NOT NULL REFERENCES posts ON DELETE CASCADE, + timeline_id integer NOT NULL REFERENCES timeline_definition ON DELETE CASCADE +); + +CREATE TABLE lists( + id SERIAL PRIMARY KEY, + name VARCHAR NOT NULL, + user_id integer REFERENCES users ON DELETE CASCADE, + type integer NOT NULL, + CONSTRAINT list_unique_user_name UNIQUE(user_id, name) +); + +CREATE TABLE list_elems( + id SERIAL PRIMARY KEY, + list_id integer NOT NULL REFERENCES lists ON DELETE CASCADE, + user_id integer REFERENCES users ON DELETE CASCADE, + blog_id integer REFERENCES blogs ON DELETE CASCADE, + word VARCHAR +); diff --git a/migrations/postgres/2019-06-24-101212_use_timelines_for_feed/down.sql b/migrations/postgres/2019-06-24-101212_use_timelines_for_feed/down.sql new file mode 100644 index 00000000000..20382150e6f --- /dev/null +++ b/migrations/postgres/2019-06-24-101212_use_timelines_for_feed/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` +DELETE FROM timeline_definition WHERE name = 'Your feed'; +DELETE FROM timeline_definition WHERE name = 'Local feed' AND query = 'local'; +DELETE FROM timeline_definition WHERE name = 'Federared feed' AND query = 'all'; diff --git a/migrations/postgres/2019-06-24-101212_use_timelines_for_feed/up.sql b/migrations/postgres/2019-06-24-101212_use_timelines_for_feed/up.sql new file mode 100644 index 00000000000..26e42c6f223 --- /dev/null +++ b/migrations/postgres/2019-06-24-101212_use_timelines_for_feed/up.sql @@ -0,0 +1,6 @@ +-- Your SQL goes here +INSERT INTO timeline_definition (name, query) VALUES + ('Local feed', 'local'), + ('Federated feed', 'all'); +INSERT INTO timeline_definition (user_id,name,query) + select id,'Your feed',CONCAT('followed or [',fqn,']') from users; diff --git a/migrations/postgres/2019-12-10-104935_fill_timelines/down.sql b/migrations/postgres/2019-12-10-104935_fill_timelines/down.sql new file mode 100644 index 00000000000..a1708a46bef --- /dev/null +++ b/migrations/postgres/2019-12-10-104935_fill_timelines/down.sql @@ -0,0 +1,8 @@ +DELETE FROM timeline WHERE id IN + ( + SELECT timeline.id FROM timeline + INNER JOIN timeline_definition ON timeline.timeline_id = timeline_definition.id + WHERE timeline_definition.query LIKE 'followed or [%]' OR + timeline_definition.query = 'local' OR + timeline_definition.query = 'all' + ); diff --git a/migrations/postgres/2019-12-10-104935_fill_timelines/up.sql b/migrations/postgres/2019-12-10-104935_fill_timelines/up.sql new file mode 100644 index 00000000000..f6b22996630 --- /dev/null +++ b/migrations/postgres/2019-12-10-104935_fill_timelines/up.sql @@ -0,0 +1,18 @@ +INSERT INTO timeline (post_id, timeline_id) + SELECT posts.id,timeline_definition.id FROM posts,timeline_definition + WHERE timeline_definition.query = 'all'; + +INSERT INTO timeline (post_id, timeline_id) + SELECT posts.id,timeline_definition.id FROM posts + CROSS JOIN timeline_definition + INNER JOIN blogs ON posts.blog_id = blogs.id + INNER JOIN instances ON blogs.instance_id = instances.id + WHERE timeline_definition.query = 'local' and instances.local = true; + +INSERT INTO timeline (post_id, timeline_id) + SELECT posts.id,timeline_definition.id FROM posts + INNER JOIN blog_authors ON posts.blog_id = blog_authors.blog_id + LEFT JOIN follows ON blog_authors.author_id = follows.following_id + INNER JOIN timeline_definition ON follows.follower_id = timeline_definition.user_id + or blog_authors.author_id = timeline_definition.user_id + WHERE timeline_definition.query LIKE 'followed or [%]'; diff --git a/migrations/postgres/2020-01-05-232816_add_blocklist/down.sql b/migrations/postgres/2020-01-05-232816_add_blocklist/down.sql new file mode 100644 index 00000000000..96eb27daca9 --- /dev/null +++ b/migrations/postgres/2020-01-05-232816_add_blocklist/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` + +drop table email_blocklist; diff --git a/migrations/postgres/2020-01-05-232816_add_blocklist/up.sql b/migrations/postgres/2020-01-05-232816_add_blocklist/up.sql new file mode 100644 index 00000000000..57ba05a25af --- /dev/null +++ b/migrations/postgres/2020-01-05-232816_add_blocklist/up.sql @@ -0,0 +1,6 @@ +-- Your SQL goes here +CREATE TABLE email_blocklist(id SERIAL PRIMARY KEY, + email_address TEXT UNIQUE, + note TEXT, + notify_user BOOLEAN DEFAULT FALSE, + notification_text TEXT); diff --git a/migrations/postgres/2021-02-23-153402_medias_index_file_path/down.sql b/migrations/postgres/2021-02-23-153402_medias_index_file_path/down.sql new file mode 100644 index 00000000000..9604e5d0ca1 --- /dev/null +++ b/migrations/postgres/2021-02-23-153402_medias_index_file_path/down.sql @@ -0,0 +1 @@ +DROP INDEX medias_index_file_path; diff --git a/migrations/postgres/2021-02-23-153402_medias_index_file_path/up.sql b/migrations/postgres/2021-02-23-153402_medias_index_file_path/up.sql new file mode 100644 index 00000000000..d26ad494d50 --- /dev/null +++ b/migrations/postgres/2021-02-23-153402_medias_index_file_path/up.sql @@ -0,0 +1 @@ +CREATE INDEX medias_index_file_path ON medias (file_path); diff --git a/migrations/postgres/2022-01-04-122156_create_email_signups_table/down.sql b/migrations/postgres/2022-01-04-122156_create_email_signups_table/down.sql new file mode 100644 index 00000000000..40af0a6ca5a --- /dev/null +++ b/migrations/postgres/2022-01-04-122156_create_email_signups_table/down.sql @@ -0,0 +1 @@ +DROP TABLE email_signups; diff --git a/migrations/postgres/2022-01-04-122156_create_email_signups_table/up.sql b/migrations/postgres/2022-01-04-122156_create_email_signups_table/up.sql new file mode 100644 index 00000000000..720509f6f4d --- /dev/null +++ b/migrations/postgres/2022-01-04-122156_create_email_signups_table/up.sql @@ -0,0 +1,9 @@ +CREATE TABLE email_signups ( + id SERIAL PRIMARY KEY, + email VARCHAR NOT NULL, + token VARCHAR NOT NULL, + expiration_date TIMESTAMP NOT NULL +); + +CREATE INDEX email_signups_token ON email_signups (token); +CREATE UNIQUE INDEX email_signups_token_requests_email ON email_signups (email); diff --git a/migrations/postgres/2022-01-29-154457_add_not_null_constraint_to_email_blocklist/down.sql b/migrations/postgres/2022-01-29-154457_add_not_null_constraint_to_email_blocklist/down.sql new file mode 100644 index 00000000000..e406ff432c1 --- /dev/null +++ b/migrations/postgres/2022-01-29-154457_add_not_null_constraint_to_email_blocklist/down.sql @@ -0,0 +1,4 @@ +ALTER TABLE email_blocklist ALTER COLUMN notification_text DROP NOT NULL; +ALTER TABLE email_blocklist ALTER COLUMN notify_user DROP NOT NULL; +ALTER TABLE email_blocklist ALTER COLUMN note DROP NOT NULL; +ALTER TABLE email_blocklist ALTER COLUMN email_address DROP NOT NULL; diff --git a/migrations/postgres/2022-01-29-154457_add_not_null_constraint_to_email_blocklist/up.sql b/migrations/postgres/2022-01-29-154457_add_not_null_constraint_to_email_blocklist/up.sql new file mode 100644 index 00000000000..ad76cfd4668 --- /dev/null +++ b/migrations/postgres/2022-01-29-154457_add_not_null_constraint_to_email_blocklist/up.sql @@ -0,0 +1,4 @@ +ALTER TABLE email_blocklist ALTER COLUMN email_address SET NOT NULL; +ALTER TABLE email_blocklist ALTER COLUMN note SET NOT NULL; +ALTER TABLE email_blocklist ALTER COLUMN notify_user SET NOT NULL; +ALTER TABLE email_blocklist ALTER COLUMN notification_text SET NOT NULL; diff --git a/migrations/sqlite/2018-04-22-093322_create_instances/down.sql b/migrations/sqlite/2018-04-22-093322_create_instances/down.sql new file mode 100644 index 00000000000..1ec93bf28b8 --- /dev/null +++ b/migrations/sqlite/2018-04-22-093322_create_instances/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE instances; diff --git a/migrations/sqlite/2018-04-22-093322_create_instances/up.sql b/migrations/sqlite/2018-04-22-093322_create_instances/up.sql new file mode 100644 index 00000000000..37f1ef67c07 --- /dev/null +++ b/migrations/sqlite/2018-04-22-093322_create_instances/up.sql @@ -0,0 +1,15 @@ +-- Your SQL goes here +CREATE TABLE instances ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + public_domain VARCHAR NOT NULL, + name VARCHAR NOT NULL, + local BOOLEAN NOT NULL DEFAULT 'f', + blocked BOOLEAN NOT NULL DEFAULT 'f', + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + open_registrations BOOLEAN NOT NULL DEFAULT 't', + short_description TEXT NOT NULL DEFAULT '', + long_description TEXT NOT NULL DEFAULT '', + default_license TEXT NOT NULL DEFAULT 'CC-0', + long_description_html VARCHAR NOT NULL DEFAULT '', + short_description_html VARCHAR NOT NULL DEFAULT '' +) diff --git a/migrations/sqlite/2018-04-22-151330_create_user/down.sql b/migrations/sqlite/2018-04-22-151330_create_user/down.sql new file mode 100644 index 00000000000..dc3714bd11c --- /dev/null +++ b/migrations/sqlite/2018-04-22-151330_create_user/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE users; diff --git a/migrations/sqlite/2018-04-22-151330_create_user/up.sql b/migrations/sqlite/2018-04-22-151330_create_user/up.sql new file mode 100644 index 00000000000..3da556b5e66 --- /dev/null +++ b/migrations/sqlite/2018-04-22-151330_create_user/up.sql @@ -0,0 +1,23 @@ +-- Your SQL goes here +PRAGMA foreign_keys = ON; +CREATE TABLE users ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + username VARCHAR NOT NULL, + display_name VARCHAR NOT NULL DEFAULT '', + outbox_url VARCHAR NOT NULL, + inbox_url VARCHAR NOT NULL, + is_admin BOOLEAN NOT NULL DEFAULT 'f', + summary TEXT NOT NULL DEFAULT '', + email TEXT, + hashed_password TEXT, + instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url TEXT NOT NULL default '', + private_key TEXT, + public_key TEXT NOT NULL DEFAULT '', + shared_inbox_url VARCHAR, + followers_endpoint VARCHAR NOT NULL DEFAULT '', + avatar_id INTEGER REFERENCES medias(id) ON DELETE CASCADE, + last_fetched_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (avatar_id) REFERENCES medias(id) ON DELETE SET NULL +); diff --git a/migrations/sqlite/2018-04-23-101717_create_blogs/down.sql b/migrations/sqlite/2018-04-23-101717_create_blogs/down.sql new file mode 100644 index 00000000000..4f8b0a68091 --- /dev/null +++ b/migrations/sqlite/2018-04-23-101717_create_blogs/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE blogs; diff --git a/migrations/sqlite/2018-04-23-101717_create_blogs/up.sql b/migrations/sqlite/2018-04-23-101717_create_blogs/up.sql new file mode 100644 index 00000000000..30635a5c90e --- /dev/null +++ b/migrations/sqlite/2018-04-23-101717_create_blogs/up.sql @@ -0,0 +1,14 @@ +-- Your SQL goes here +CREATE TABLE blogs ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + actor_id VARCHAR NOT NULL, + title VARCHAR NOT NULL, + summary TEXT NOT NULL DEFAULT '', + outbox_url VARCHAR NOT NULL, + inbox_url VARCHAR NOT NULL, + instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url text not null default '', + private_key TEXT, + public_key TEXT NOT NULL DEFAULT '' +) diff --git a/migrations/sqlite/2018-04-23-111655_create_blog_authors/down.sql b/migrations/sqlite/2018-04-23-111655_create_blog_authors/down.sql new file mode 100644 index 00000000000..cfb62abd0e6 --- /dev/null +++ b/migrations/sqlite/2018-04-23-111655_create_blog_authors/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE blog_authors; diff --git a/migrations/sqlite/2018-04-23-111655_create_blog_authors/up.sql b/migrations/sqlite/2018-04-23-111655_create_blog_authors/up.sql new file mode 100644 index 00000000000..10144614e30 --- /dev/null +++ b/migrations/sqlite/2018-04-23-111655_create_blog_authors/up.sql @@ -0,0 +1,7 @@ +-- Your SQL goes here +CREATE TABLE blog_authors ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + blog_id INTEGER REFERENCES blogs(id) ON DELETE CASCADE NOT NULL, + author_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + is_owner BOOLEAN NOT NULL DEFAULT 'f' +) diff --git a/migrations/sqlite/2018-04-23-132822_create_posts/down.sql b/migrations/sqlite/2018-04-23-132822_create_posts/down.sql new file mode 100644 index 00000000000..56ed16e5387 --- /dev/null +++ b/migrations/sqlite/2018-04-23-132822_create_posts/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE posts; diff --git a/migrations/sqlite/2018-04-23-132822_create_posts/up.sql b/migrations/sqlite/2018-04-23-132822_create_posts/up.sql new file mode 100644 index 00000000000..d88337f52c9 --- /dev/null +++ b/migrations/sqlite/2018-04-23-132822_create_posts/up.sql @@ -0,0 +1,14 @@ +-- Your SQL goes here +CREATE TABLE posts ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + blog_id INTEGER REFERENCES blogs(id) ON DELETE CASCADE NOT NULL, + slug VARCHAR NOT NULL, + title VARCHAR NOT NULL, + content TEXT NOT NULL DEFAULT '', + published BOOLEAN NOT NULL DEFAULT 'f', + license VARCHAR NOT NULL DEFAULT 'CC-0', + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url VARCHAR NOT NULL DEFAULT '', + subtitle TEXT NOT NULL DEFAULT '', + source TEXT NOT NULL DEFAULT '' +) diff --git a/migrations/sqlite/2018-04-23-142746_create_post_authors/down.sql b/migrations/sqlite/2018-04-23-142746_create_post_authors/down.sql new file mode 100644 index 00000000000..129bf59ac6b --- /dev/null +++ b/migrations/sqlite/2018-04-23-142746_create_post_authors/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE post_authors; diff --git a/migrations/sqlite/2018-04-23-142746_create_post_authors/up.sql b/migrations/sqlite/2018-04-23-142746_create_post_authors/up.sql new file mode 100644 index 00000000000..214a6f3ff99 --- /dev/null +++ b/migrations/sqlite/2018-04-23-142746_create_post_authors/up.sql @@ -0,0 +1,6 @@ +-- Your SQL goes here +CREATE TABLE post_authors ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL, + author_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL +) diff --git a/migrations/sqlite/2018-05-01-124607_create_follow/down.sql b/migrations/sqlite/2018-05-01-124607_create_follow/down.sql new file mode 100644 index 00000000000..eee3b972cb7 --- /dev/null +++ b/migrations/sqlite/2018-05-01-124607_create_follow/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE follows; diff --git a/migrations/sqlite/2018-05-01-124607_create_follow/up.sql b/migrations/sqlite/2018-05-01-124607_create_follow/up.sql new file mode 100644 index 00000000000..7eeda5aa9a9 --- /dev/null +++ b/migrations/sqlite/2018-05-01-124607_create_follow/up.sql @@ -0,0 +1,7 @@ +-- Your SQL goes here +CREATE TABLE follows ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + follower_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + following_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + ap_url TEXT NOT NULL default '' +) diff --git a/migrations/sqlite/2018-05-09-192013_create_comments/down.sql b/migrations/sqlite/2018-05-09-192013_create_comments/down.sql new file mode 100644 index 00000000000..d0841ffb256 --- /dev/null +++ b/migrations/sqlite/2018-05-09-192013_create_comments/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE comments; diff --git a/migrations/sqlite/2018-05-09-192013_create_comments/up.sql b/migrations/sqlite/2018-05-09-192013_create_comments/up.sql new file mode 100644 index 00000000000..901c0699ee7 --- /dev/null +++ b/migrations/sqlite/2018-05-09-192013_create_comments/up.sql @@ -0,0 +1,12 @@ +-- Your SQL goes here +CREATE TABLE comments ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + content TEXT NOT NULL DEFAULT '', + in_response_to_id INTEGER REFERENCES comments(id), + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL, + author_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url VARCHAR, + sensitive BOOLEAN NOT NULL DEFAULT 'f', + spoiler_text TEXT NOT NULL DEFAULT '' +) diff --git a/migrations/sqlite/2018-05-10-154336_create_likes/down.sql b/migrations/sqlite/2018-05-10-154336_create_likes/down.sql new file mode 100644 index 00000000000..2232ad5b4f8 --- /dev/null +++ b/migrations/sqlite/2018-05-10-154336_create_likes/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE likes; diff --git a/migrations/sqlite/2018-05-10-154336_create_likes/up.sql b/migrations/sqlite/2018-05-10-154336_create_likes/up.sql new file mode 100644 index 00000000000..b406a7b6e63 --- /dev/null +++ b/migrations/sqlite/2018-05-10-154336_create_likes/up.sql @@ -0,0 +1,8 @@ +-- Your SQL goes here +CREATE TABLE likes ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url VARCHAR NOT NULL default '' +) diff --git a/migrations/sqlite/2018-05-13-122311_create_notifications/down.sql b/migrations/sqlite/2018-05-13-122311_create_notifications/down.sql new file mode 100644 index 00000000000..bcebcc05f33 --- /dev/null +++ b/migrations/sqlite/2018-05-13-122311_create_notifications/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE notifications; diff --git a/migrations/sqlite/2018-05-13-122311_create_notifications/up.sql b/migrations/sqlite/2018-05-13-122311_create_notifications/up.sql new file mode 100644 index 00000000000..ceb45ee8d2a --- /dev/null +++ b/migrations/sqlite/2018-05-13-122311_create_notifications/up.sql @@ -0,0 +1,8 @@ +-- Your SQL goes here +CREATE TABLE notifications ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + kind VARCHAR NOT NULL DEFAULT 'unknown', + object_id INTEGER NOT NULL DEFAULT 0 +) diff --git a/migrations/sqlite/2018-05-19-091428_create_reshares/down.sql b/migrations/sqlite/2018-05-19-091428_create_reshares/down.sql new file mode 100644 index 00000000000..29a2d0fb45b --- /dev/null +++ b/migrations/sqlite/2018-05-19-091428_create_reshares/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE reshares; diff --git a/migrations/sqlite/2018-05-19-091428_create_reshares/up.sql b/migrations/sqlite/2018-05-19-091428_create_reshares/up.sql new file mode 100644 index 00000000000..cee70f748d2 --- /dev/null +++ b/migrations/sqlite/2018-05-19-091428_create_reshares/up.sql @@ -0,0 +1,8 @@ +-- Your SQL goes here +CREATE TABLE reshares ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL, + ap_url VARCHAR NOT NULL DEFAULT '', + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +) diff --git a/migrations/sqlite/2018-06-20-175532_create_mentions/down.sql b/migrations/sqlite/2018-06-20-175532_create_mentions/down.sql new file mode 100644 index 00000000000..e860c9ad6bd --- /dev/null +++ b/migrations/sqlite/2018-06-20-175532_create_mentions/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE mentions; diff --git a/migrations/sqlite/2018-06-20-175532_create_mentions/up.sql b/migrations/sqlite/2018-06-20-175532_create_mentions/up.sql new file mode 100644 index 00000000000..3f28aa9c60e --- /dev/null +++ b/migrations/sqlite/2018-06-20-175532_create_mentions/up.sql @@ -0,0 +1,8 @@ +-- Your SQL goes here +CREATE TABLE mentions ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + mentioned_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE, + comment_id INTEGER REFERENCES comments(id) ON DELETE CASCADE, + ap_url VARCHAR NOT NULL DEFAULT '' +) diff --git a/migrations/sqlite/2018-09-02-111458_create_medias/down.sql b/migrations/sqlite/2018-09-02-111458_create_medias/down.sql new file mode 100644 index 00000000000..3ba01786b33 --- /dev/null +++ b/migrations/sqlite/2018-09-02-111458_create_medias/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE medias; diff --git a/migrations/sqlite/2018-09-02-111458_create_medias/up.sql b/migrations/sqlite/2018-09-02-111458_create_medias/up.sql new file mode 100644 index 00000000000..e2ac093c9c2 --- /dev/null +++ b/migrations/sqlite/2018-09-02-111458_create_medias/up.sql @@ -0,0 +1,11 @@ +-- Your SQL goes here +CREATE TABLE medias ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + file_path TEXT NOT NULL DEFAULT '', + alt_text TEXT NOT NULL DEFAULT '', + is_remote BOOLEAN NOT NULL DEFAULT 'f', + remote_url TEXT, + sensitive BOOLEAN NOT NULL DEFAULT 'f', + content_warning TEXT, + owner_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL +) diff --git a/migrations/sqlite/2018-09-05-174106_create_tags/down.sql b/migrations/sqlite/2018-09-05-174106_create_tags/down.sql new file mode 100644 index 00000000000..43c79a4bb78 --- /dev/null +++ b/migrations/sqlite/2018-09-05-174106_create_tags/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE tags; diff --git a/migrations/sqlite/2018-09-05-174106_create_tags/up.sql b/migrations/sqlite/2018-09-05-174106_create_tags/up.sql new file mode 100644 index 00000000000..031b4ed2724 --- /dev/null +++ b/migrations/sqlite/2018-09-05-174106_create_tags/up.sql @@ -0,0 +1,7 @@ +-- Your SQL goes here +CREATE TABLE tags ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + tag TEXT NOT NULL DEFAULT '', + is_hastag BOOLEAN NOT NULL DEFAULT 'f', + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL +) diff --git a/migrations/sqlite/2018-10-06-161156_change_default_license/down.sql b/migrations/sqlite/2018-10-06-161156_change_default_license/down.sql new file mode 100644 index 00000000000..9e6bc4895fe --- /dev/null +++ b/migrations/sqlite/2018-10-06-161156_change_default_license/down.sql @@ -0,0 +1,40 @@ +-- This file should undo anything in `up.sql` +-- SQLite is great, we can't just change the default value, +-- we have to clone the table with the new value. +CREATE TABLE instances2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + public_domain VARCHAR NOT NULL, + name VARCHAR NOT NULL, + local BOOLEAN NOT NULL DEFAULT 'f', + blocked BOOLEAN NOT NULL DEFAULT 'f', + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + open_registrations BOOLEAN NOT NULL DEFAULT 't', + short_description TEXT NOT NULL DEFAULT '', + long_description TEXT NOT NULL DEFAULT '', + default_license TEXT NOT NULL DEFAULT 'CC-0', + long_description_html VARCHAR NOT NULL DEFAULT '', + short_description_html VARCHAR NOT NULL DEFAULT '' +); + +INSERT INTO instances2 SELECT * FROM instances; +DROP TABLE instances; +ALTER TABLE instances2 RENAME TO instances; + + +CREATE TABLE posts2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + blog_id INTEGER REFERENCES blogs(id) ON DELETE CASCADE NOT NULL, + slug VARCHAR NOT NULL, + title VARCHAR NOT NULL, + content TEXT NOT NULL DEFAULT '', + published BOOLEAN NOT NULL DEFAULT 'f', + license VARCHAR NOT NULL DEFAULT 'CC-0', + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url VARCHAR NOT NULL DEFAULT '', + subtitle TEXT NOT NULL DEFAULT '', + source TEXT NOT NULL DEFAULT '' +); + +INSERT INTO SELECT * FROM posts; +DROP TABLE posts; +ALTER TABLE posts2 RENAME TO posts; diff --git a/migrations/sqlite/2018-10-06-161156_change_default_license/up.sql b/migrations/sqlite/2018-10-06-161156_change_default_license/up.sql new file mode 100644 index 00000000000..df1dc87da6f --- /dev/null +++ b/migrations/sqlite/2018-10-06-161156_change_default_license/up.sql @@ -0,0 +1,40 @@ +-- Your SQL goes here +-- SQLite is great, we can't just change the default value, +-- we have to clone the table with the new value. +CREATE TABLE instances2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + public_domain VARCHAR NOT NULL, + name VARCHAR NOT NULL, + local BOOLEAN NOT NULL DEFAULT 'f', + blocked BOOLEAN NOT NULL DEFAULT 'f', + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + open_registrations BOOLEAN NOT NULL DEFAULT 't', + short_description TEXT NOT NULL DEFAULT '', + long_description TEXT NOT NULL DEFAULT '', + default_license TEXT NOT NULL DEFAULT 'CC-BY-SA', + long_description_html VARCHAR NOT NULL DEFAULT '', + short_description_html VARCHAR NOT NULL DEFAULT '' +); + +INSERT INTO instances2 SELECT * FROM instances; +DROP TABLE instances; +ALTER TABLE instances2 RENAME TO instances; + + + CREATE TABLE posts2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + blog_id INTEGER REFERENCES blogs(id) ON DELETE CASCADE NOT NULL, + slug VARCHAR NOT NULL, + title VARCHAR NOT NULL, + content TEXT NOT NULL DEFAULT '', + published BOOLEAN NOT NULL DEFAULT 'f', + license VARCHAR NOT NULL DEFAULT 'CC-BY-SA', + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url VARCHAR NOT NULL DEFAULT '', + subtitle TEXT NOT NULL DEFAULT '', + source TEXT NOT NULL DEFAULT '' + ); + + INSERT INTO posts2 SELECT * FROM posts; + DROP TABLE posts; + ALTER TABLE posts2 RENAME TO posts; diff --git a/migrations/sqlite/2018-10-19-165450_create_apps/down.sql b/migrations/sqlite/2018-10-19-165450_create_apps/down.sql new file mode 100755 index 00000000000..09b3f60905e --- /dev/null +++ b/migrations/sqlite/2018-10-19-165450_create_apps/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE apps; diff --git a/migrations/sqlite/2018-10-19-165450_create_apps/up.sql b/migrations/sqlite/2018-10-19-165450_create_apps/up.sql new file mode 100755 index 00000000000..0e9408bd307 --- /dev/null +++ b/migrations/sqlite/2018-10-19-165450_create_apps/up.sql @@ -0,0 +1,10 @@ +-- Your SQL goes here +CREATE TABLE apps ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL DEFAULT '', + client_id TEXT NOT NULL, + client_secret TEXT NOT NULL, + redirect_uri TEXT, + website TEXT, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/migrations/sqlite/2018-10-20-164036_fix_hastag_typo/down.sql b/migrations/sqlite/2018-10-20-164036_fix_hastag_typo/down.sql new file mode 100644 index 00000000000..47965c12985 --- /dev/null +++ b/migrations/sqlite/2018-10-20-164036_fix_hastag_typo/down.sql @@ -0,0 +1,10 @@ +CREATE TABLE tags2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + tag TEXT NOT NULL DEFAULT '', + is_hastag BOOLEAN NOT NULL DEFAULT 'f', + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL +); + +INSERT INTO tags2 SELECT * FROM tags; +DROP TABLE tags; +ALTER TABLE tags2 RENAME TO tags; diff --git a/migrations/sqlite/2018-10-20-164036_fix_hastag_typo/up.sql b/migrations/sqlite/2018-10-20-164036_fix_hastag_typo/up.sql new file mode 100644 index 00000000000..5993b3c4f0a --- /dev/null +++ b/migrations/sqlite/2018-10-20-164036_fix_hastag_typo/up.sql @@ -0,0 +1,10 @@ +CREATE TABLE tags2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + tag TEXT NOT NULL DEFAULT '', + is_hashtag BOOLEAN NOT NULL DEFAULT 'f', + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL +); + +INSERT INTO tags2 SELECT * FROM tags; +DROP TABLE tags; +ALTER TABLE tags2 RENAME TO tags; diff --git a/migrations/sqlite/2018-10-21-163241_create_api_token/down.sql b/migrations/sqlite/2018-10-21-163241_create_api_token/down.sql new file mode 100644 index 00000000000..e71f14786b0 --- /dev/null +++ b/migrations/sqlite/2018-10-21-163241_create_api_token/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE api_tokens; diff --git a/migrations/sqlite/2018-10-21-163241_create_api_token/up.sql b/migrations/sqlite/2018-10-21-163241_create_api_token/up.sql new file mode 100644 index 00000000000..7d6f6cf05ab --- /dev/null +++ b/migrations/sqlite/2018-10-21-163241_create_api_token/up.sql @@ -0,0 +1,9 @@ +-- Your SQL goes here +CREATE TABLE api_tokens ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + value TEXT NOT NULL, + scopes TEXT NOT NULL, + app_id INTEGER NOT NULL REFERENCES apps(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE +) diff --git a/migrations/sqlite/2018-10-30-151545_add_post_cover/down.sql b/migrations/sqlite/2018-10-30-151545_add_post_cover/down.sql new file mode 100644 index 00000000000..15321a9b7a1 --- /dev/null +++ b/migrations/sqlite/2018-10-30-151545_add_post_cover/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE posts DROP COLUMN cover_id; diff --git a/migrations/sqlite/2018-10-30-151545_add_post_cover/up.sql b/migrations/sqlite/2018-10-30-151545_add_post_cover/up.sql new file mode 100644 index 00000000000..d3239a25bf7 --- /dev/null +++ b/migrations/sqlite/2018-10-30-151545_add_post_cover/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE posts ADD COLUMN cover_id INTEGER REFERENCES medias(id) DEFAULT NULL; diff --git a/migrations/sqlite/2018-12-08-182930_constraints/down.sql b/migrations/sqlite/2018-12-08-182930_constraints/down.sql new file mode 100644 index 00000000000..5b196d58c0f --- /dev/null +++ b/migrations/sqlite/2018-12-08-182930_constraints/down.sql @@ -0,0 +1,178 @@ +-- This file should undo anything in `up.sql` +CREATE TABLE api_tokens2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + value TEXT NOT NULL, + scopes TEXT NOT NULL, + app_id INTEGER NOT NULL REFERENCES apps(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE +); + +INSERT INTO api_tokens2 SELECT * FROM api_tokens; +DROP TABLE api_tokens; +ALTER TABLE api_tokens2 RENAME TO api_tokens; + +CREATE TABLE blog_authors2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + blog_id INTEGER REFERENCES blogs(id) ON DELETE CASCADE NOT NULL, + author_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + is_owner BOOLEAN NOT NULL DEFAULT 'f' +); + +INSERT INTO blog_authors2 SELECT * FROM blog_authors; +DROP TABLE blog_authors; +ALTER TABLE blog_authors2 RENAME TO blog_authors; + +CREATE TABLE blogs2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + actor_id VARCHAR NOT NULL, + title VARCHAR NOT NULL, + summary TEXT NOT NULL DEFAULT '', + outbox_url VARCHAR NOT NULL, + inbox_url VARCHAR NOT NULL, + instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url text not null default '', + private_key TEXT, + public_key TEXT NOT NULL DEFAULT '' +) + +INSERT INTO blogs2 SELECT * FROM blogs; +DROP TABLE blogs; +ALTER TABLE blogs2 RENAME TO blogs; + +CREATE TABLE comments2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + content TEXT NOT NULL DEFAULT '', + in_response_to_id INTEGER REFERENCES comments(id), + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL, + author_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url VARCHAR, + sensitive BOOLEAN NOT NULL DEFAULT 'f', + spoiler_text TEXT NOT NULL DEFAULT '' +); + +INSERT INTO comments2 SELECT * FROM comments; +DROP TABLE comments; +ALTER TABLE comments2 RENAME TO comments; + +CREATE TABLE follows2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + follower_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + following_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + ap_url TEXT NOT NULL default '' +); + +INSERT INTO follows2 SELECT * FROM follows; +DROP TABLE follows; +ALTER TABLE follows2 RENAME TO follows; + +CREATE TABLE instances2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + public_domain VARCHAR NOT NULL, + name VARCHAR NOT NULL, + local BOOLEAN NOT NULL DEFAULT 'f', + blocked BOOLEAN NOT NULL DEFAULT 'f', + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + open_registrations BOOLEAN NOT NULL DEFAULT 't', + short_description TEXT NOT NULL DEFAULT '', + long_description TEXT NOT NULL DEFAULT '', + default_license TEXT NOT NULL DEFAULT 'CC-BY-SA', + long_description_html VARCHAR NOT NULL DEFAULT '', + short_description_html VARCHAR NOT NULL DEFAULT '' +); + +INSERT INTO instances2 SELECT * FROM instances; +DROP TABLE instances; +ALTER TABLE instances2 RENAME TO instances; + +CREATE TABLE likes2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url VARCHAR NOT NULL default '' +); + +INSERT INTO likes2 SELECT * FROM likes; +DROP TABLE likes; +ALTER TABLE likes2 RENAME TO likes; + +CREATE TABLE mentions2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + mentioned_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE, + comment_id INTEGER REFERENCES comments(id) ON DELETE CASCADE, + ap_url VARCHAR NOT NULL DEFAULT '' +); + +INSERT INTO mentions2 SELECT * FROM mentions; +DROP TABLE mentions; +ALTER TABLE mentions2 RENAME TO mentions; + +CREATE TABLE post_authors2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL, + author_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL +) + +INSERT INTO post_authors2 SELECT * FROM post_authors; +DROP TABLE post_authors; +ALTER TABLE post_authors2 RENAME TO post_authors; + +CREATE TABLE posts2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + blog_id INTEGER REFERENCES blogs(id) ON DELETE CASCADE NOT NULL, + slug VARCHAR NOT NULL, + title VARCHAR NOT NULL, + content TEXT NOT NULL DEFAULT '', + published BOOLEAN NOT NULL DEFAULT 'f', + license VARCHAR NOT NULL DEFAULT 'CC-BY-SA', + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url VARCHAR NOT NULL DEFAULT '', + subtitle TEXT NOT NULL DEFAULT '', + source TEXT NOT NULL DEFAULT '' +); + +INSERT INTO posts2 SELECT * FROM posts; +DROP TABLE posts; +ALTER TABLE posts2 RENAME TO posts; + +CREATE TABLE tags2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + tag TEXT NOT NULL DEFAULT '', + is_hashtag BOOLEAN NOT NULL DEFAULT 'f', + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL +); + +INSERT INTO tags2 SELECT * FROM tags; +DROP TABLE tags; +ALTER TABLE tags2 RENAME TO tags; + +CREATE TABLE users ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + username VARCHAR NOT NULL, + display_name VARCHAR NOT NULL DEFAULT '', + outbox_url VARCHAR NOT NULL, + inbox_url VARCHAR NOT NULL, + is_admin BOOLEAN NOT NULL DEFAULT 'f', + summary TEXT NOT NULL DEFAULT '', + email TEXT, + hashed_password TEXT, + instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url TEXT NOT NULL default '', + private_key TEXT, + public_key TEXT NOT NULL DEFAULT '', + shared_inbox_url VARCHAR, + followers_endpoint VARCHAR NOT NULL DEFAULT '', + avatar_id INTEGER REFERENCES medias(id) ON DELETE CASCADE, + last_fetched_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (avatar_id) REFERENCES medias(id) ON DELETE SET NULL +); + +INSERT INTO users2 SELECT * FROM users; +DROP TABLE users; +ALTER TABLE users2 RENAME TO users; + diff --git a/migrations/sqlite/2018-12-08-182930_constraints/up.sql b/migrations/sqlite/2018-12-08-182930_constraints/up.sql new file mode 100644 index 00000000000..3b94d4018db --- /dev/null +++ b/migrations/sqlite/2018-12-08-182930_constraints/up.sql @@ -0,0 +1,186 @@ +-- Your SQL goes here +CREATE TABLE api_tokens2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + value TEXT NOT NULL UNIQUE, + scopes TEXT NOT NULL, + app_id INTEGER NOT NULL REFERENCES apps(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE +); + +INSERT INTO api_tokens2 SELECT * FROM api_tokens; +DROP TABLE api_tokens; +ALTER TABLE api_tokens2 RENAME TO api_tokens; + +CREATE TABLE blog_authors2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + blog_id INTEGER REFERENCES blogs(id) ON DELETE CASCADE NOT NULL, + author_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + is_owner BOOLEAN NOT NULL DEFAULT 'f', + CONSTRAINT blog_authors_unique UNIQUE (blog_id, author_id) +); + +INSERT INTO blog_authors2 SELECT * FROM blog_authors; +DROP TABLE blog_authors; +ALTER TABLE blog_authors2 RENAME TO blog_authors; + +CREATE TABLE blogs2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + actor_id VARCHAR NOT NULL, + title VARCHAR NOT NULL, + summary TEXT NOT NULL DEFAULT '', + outbox_url VARCHAR NOT NULL UNIQUE, + inbox_url VARCHAR NOT NULL UNIQUE, + instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url text not null default '' UNIQUE, + private_key TEXT, + public_key TEXT NOT NULL DEFAULT '', + CONSTRAINT blog_unique UNIQUE (actor_id, instance_id) +); + +INSERT INTO blogs2 SELECT * FROM blogs; +DROP TABLE blogs; +ALTER TABLE blogs2 RENAME TO blogs; + +CREATE TABLE comments2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + content TEXT NOT NULL DEFAULT '', + in_response_to_id INTEGER REFERENCES comments(id), + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL, + author_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url VARCHAR UNIQUE, + sensitive BOOLEAN NOT NULL DEFAULT 'f', + spoiler_text TEXT NOT NULL DEFAULT '' +); + +INSERT INTO comments2 SELECT * FROM comments; +DROP TABLE comments; +ALTER TABLE comments2 RENAME TO comments; + +CREATE TABLE follows2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + follower_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + following_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + ap_url TEXT NOT NULL default '' UNIQUE +); + +INSERT INTO follows2 SELECT * FROM follows; +DROP TABLE follows; +ALTER TABLE follows2 RENAME TO follows; + +CREATE TABLE instances2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + public_domain VARCHAR NOT NULL UNIQUE, + name VARCHAR NOT NULL, + local BOOLEAN NOT NULL DEFAULT 'f', + blocked BOOLEAN NOT NULL DEFAULT 'f', + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + open_registrations BOOLEAN NOT NULL DEFAULT 't', + short_description TEXT NOT NULL DEFAULT '', + long_description TEXT NOT NULL DEFAULT '', + default_license TEXT NOT NULL DEFAULT 'CC-BY-SA', + long_description_html VARCHAR NOT NULL DEFAULT '', + short_description_html VARCHAR NOT NULL DEFAULT '' +); + +INSERT INTO instances2 SELECT * FROM instances; +DROP TABLE instances; +ALTER TABLE instances2 RENAME TO instances; + +CREATE TABLE likes2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url VARCHAR NOT NULL default '' UNIQUE, + CONSTRAINT likes_unique UNIQUE (user_id, post_id) +); + +INSERT INTO likes2 SELECT * FROM likes; +DROP TABLE likes; +ALTER TABLE likes2 RENAME TO likes; + +CREATE TABLE mentions2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + mentioned_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE, + comment_id INTEGER REFERENCES comments(id) ON DELETE CASCADE, + ap_url VARCHAR NOT NULL DEFAULT '' UNIQUE +); + +INSERT INTO mentions2 SELECT * FROM mentions; +DROP TABLE mentions; +ALTER TABLE mentions2 RENAME TO mentions; + +CREATE TABLE post_authors2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL, + author_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + CONSTRAINT blog_authors_unique UNIQUE (post_id, author_id) +); + +INSERT INTO post_authors2 SELECT * FROM post_authors; +DROP TABLE post_authors; +ALTER TABLE post_authors2 RENAME TO post_authors; + +CREATE TABLE posts2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + blog_id INTEGER REFERENCES blogs(id) ON DELETE CASCADE NOT NULL, + slug VARCHAR NOT NULL, + title VARCHAR NOT NULL, + content TEXT NOT NULL DEFAULT '', + published BOOLEAN NOT NULL DEFAULT 'f', + license VARCHAR NOT NULL DEFAULT 'CC-BY-SA', + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url VARCHAR NOT NULL DEFAULT '' UNIQUE, + subtitle TEXT NOT NULL DEFAULT '', + source TEXT NOT NULL DEFAULT '', + cover_id INTEGER REFERENCES medias(id) DEFAULT NULL, + CONSTRAINT blog_authors_unique UNIQUE (blog_id, slug) +); + +INSERT INTO posts2 SELECT * FROM posts; +DROP TABLE posts; +ALTER TABLE posts2 RENAME TO posts; + +CREATE TABLE tags2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + tag TEXT NOT NULL DEFAULT '', + is_hashtag BOOLEAN NOT NULL DEFAULT 'f', + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL, + CONSTRAINT blog_authors_unique UNIQUE (tag, is_hashtag, post_id) +); + +INSERT INTO tags2 SELECT * FROM tags; +DROP TABLE tags; +ALTER TABLE tags2 RENAME TO tags; + +CREATE TABLE users2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + username VARCHAR NOT NULL, + display_name VARCHAR NOT NULL DEFAULT '', + outbox_url VARCHAR NOT NULL UNIQUE, + inbox_url VARCHAR NOT NULL UNIQUE, + is_admin BOOLEAN NOT NULL DEFAULT 'f', + summary TEXT NOT NULL DEFAULT '', + email TEXT, + hashed_password TEXT, + instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url TEXT NOT NULL default '' UNIQUE, + private_key TEXT, + public_key TEXT NOT NULL DEFAULT '', + shared_inbox_url VARCHAR, + followers_endpoint VARCHAR NOT NULL DEFAULT '' UNIQUE, + avatar_id INTEGER REFERENCES medias(id) ON DELETE CASCADE, + last_fetched_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (avatar_id) REFERENCES medias(id) ON DELETE SET NULL, + CONSTRAINT blog_authors_unique UNIQUE (username, instance_id) +); + +INSERT INTO users2 SELECT * FROM users; +DROP TABLE users; +ALTER TABLE users2 RENAME TO users; + diff --git a/migrations/sqlite/2018-12-17-180104_mention_no_ap_url/down.sql b/migrations/sqlite/2018-12-17-180104_mention_no_ap_url/down.sql new file mode 100644 index 00000000000..9ba08b05665 --- /dev/null +++ b/migrations/sqlite/2018-12-17-180104_mention_no_ap_url/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE mentions ADD COLUMN ap_url VARCHAR NOT NULL DEFAULT ''; diff --git a/migrations/sqlite/2018-12-17-180104_mention_no_ap_url/up.sql b/migrations/sqlite/2018-12-17-180104_mention_no_ap_url/up.sql new file mode 100644 index 00000000000..a748bc9309b --- /dev/null +++ b/migrations/sqlite/2018-12-17-180104_mention_no_ap_url/up.sql @@ -0,0 +1,11 @@ +-- Your SQL goes here +CREATE TABLE mentions2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + mentioned_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE, + comment_id INTEGER REFERENCES comments(id) ON DELETE CASCADE +); + +INSERT INTO mentions2 SELECT id,mentioned_id,post_id,comment_id FROM mentions; +DROP TABLE mentions; +ALTER TABLE mentions2 RENAME TO mentions; diff --git a/migrations/sqlite/2018-12-17-221135_comment_visibility/down.sql b/migrations/sqlite/2018-12-17-221135_comment_visibility/down.sql new file mode 100644 index 00000000000..ea00ebd9ede --- /dev/null +++ b/migrations/sqlite/2018-12-17-221135_comment_visibility/down.sql @@ -0,0 +1,28 @@ +-- This file should undo anything in `up.sql` +CREATE TABLE comments2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + content TEXT NOT NULL DEFAULT '', + in_response_to_id INTEGER REFERENCES comments(id), + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL, + author_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url VARCHAR, + sensitive BOOLEAN NOT NULL DEFAULT 'f', + spoiler_text TEXT NOT NULL DEFAULT '' +); + +INSERT INTO comments2 SELECT + id, + content, + in_response_to_id, + post_id, + author_id, + creation_date, + ap_url, + sensitive, + spoiler_text + FROM comments; +DROP TABLE comments; +ALTER TABLE comments2 RENAME TO comments; + +DROP TABLE comment_seers; diff --git a/migrations/sqlite/2018-12-17-221135_comment_visibility/up.sql b/migrations/sqlite/2018-12-17-221135_comment_visibility/up.sql new file mode 100644 index 00000000000..88a046eae48 --- /dev/null +++ b/migrations/sqlite/2018-12-17-221135_comment_visibility/up.sql @@ -0,0 +1,9 @@ +-- Your SQL goes here +ALTER TABLE comments ADD public_visibility BOOLEAN NOT NULL DEFAULT 't'; + +CREATE TABLE comment_seers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + comment_id INTEGER REFERENCES comments(id) ON DELETE CASCADE NOT NULL, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + UNIQUE (comment_id, user_id) +) diff --git a/migrations/sqlite/2018-12-25-164502_media-cover-deletion/down.sql b/migrations/sqlite/2018-12-25-164502_media-cover-deletion/down.sql new file mode 100644 index 00000000000..9c86ec6739f --- /dev/null +++ b/migrations/sqlite/2018-12-25-164502_media-cover-deletion/down.sql @@ -0,0 +1,21 @@ +-- This file should undo anything in `up.sql` + +CREATE TABLE posts2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + blog_id INTEGER REFERENCES blogs(id) ON DELETE CASCADE NOT NULL, + slug VARCHAR NOT NULL, + title VARCHAR NOT NULL, + content TEXT NOT NULL DEFAULT '', + published BOOLEAN NOT NULL DEFAULT 'f', + license VARCHAR NOT NULL DEFAULT 'CC-BY-SA', + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url VARCHAR NOT NULL DEFAULT '' UNIQUE, + subtitle TEXT NOT NULL DEFAULT '', + source TEXT NOT NULL DEFAULT '', + cover_id INTEGER REFERENCES medias(id) DEFAULT NULL, + CONSTRAINT blog_authors_unique UNIQUE (blog_id, slug) +); + +INSERT INTO posts2 SELECT * from posts; +DROP TABLE posts; +ALTER TABLE posts2 RENAME TO posts; diff --git a/migrations/sqlite/2018-12-25-164502_media-cover-deletion/up.sql b/migrations/sqlite/2018-12-25-164502_media-cover-deletion/up.sql new file mode 100644 index 00000000000..ca1398e59d7 --- /dev/null +++ b/migrations/sqlite/2018-12-25-164502_media-cover-deletion/up.sql @@ -0,0 +1,21 @@ +-- Your SQL goes here + +CREATE TABLE posts2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + blog_id INTEGER REFERENCES blogs(id) ON DELETE CASCADE NOT NULL, + slug VARCHAR NOT NULL, + title VARCHAR NOT NULL, + content TEXT NOT NULL DEFAULT '', + published BOOLEAN NOT NULL DEFAULT 'f', + license VARCHAR NOT NULL DEFAULT 'CC-BY-SA', + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url VARCHAR NOT NULL DEFAULT '' UNIQUE, + subtitle TEXT NOT NULL DEFAULT '', + source TEXT NOT NULL DEFAULT '', + cover_id INTEGER REFERENCES medias(id) ON DELETE SET NULL DEFAULT NULL, + CONSTRAINT blog_authors_unique UNIQUE (blog_id, slug) +); + +INSERT INTO posts2 SELECT * from posts; +DROP TABLE posts; +ALTER TABLE posts2 RENAME TO posts; diff --git a/migrations/sqlite/2019-03-05-082846_add_fqn/down.sql b/migrations/sqlite/2019-03-05-082846_add_fqn/down.sql new file mode 100644 index 00000000000..2767f409793 --- /dev/null +++ b/migrations/sqlite/2019-03-05-082846_add_fqn/down.sql @@ -0,0 +1,77 @@ +-- This file should undo anything in `up.sql` +CREATE TABLE blogs_no_fqn ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + actor_id VARCHAR NOT NULL, + title VARCHAR NOT NULL, + summary TEXT NOT NULL DEFAULT '', + outbox_url VARCHAR NOT NULL UNIQUE, + inbox_url VARCHAR NOT NULL UNIQUE, + instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url text not null default '' UNIQUE, + private_key TEXT, + public_key TEXT NOT NULL DEFAULT '', + CONSTRAINT blog_unique UNIQUE (actor_id, instance_id) +); + +INSERT INTO blogs_no_fqn SELECT + id, + actor_id, + title, + summary, + outbox_url, + inbox_url, + instance_id, + creation_date, + ap_url, + private_key, + public_key +FROM blogs; +DROP TABLE blogs; +ALTER TABLE blogs_no_fqn RENAME TO blogs; + + +CREATE TABLE users_no_fqn ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + username VARCHAR NOT NULL, + display_name VARCHAR NOT NULL DEFAULT '', + outbox_url VARCHAR NOT NULL UNIQUE, + inbox_url VARCHAR NOT NULL UNIQUE, + is_admin BOOLEAN NOT NULL DEFAULT 'f', + summary TEXT NOT NULL DEFAULT '', + email TEXT, + hashed_password TEXT, + instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url TEXT NOT NULL default '' UNIQUE, + private_key TEXT, + public_key TEXT NOT NULL DEFAULT '', + shared_inbox_url VARCHAR, + followers_endpoint VARCHAR NOT NULL DEFAULT '' UNIQUE, + avatar_id INTEGER REFERENCES medias(id) ON DELETE SET NULL, + last_fetched_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT blog_authors_unique UNIQUE (username, instance_id) +); + +INSERT INTO users_no_fqn SELECT + id, + username, + display_name, + outbox_url, + inbox_url, + is_admin, + summary, + email, + hashed_password, + instance_id, + creation_date, + ap_url, + private_key, + public_key, + shared_inbox_url, + followers_endpoint, + avatar_id, + last_fetched_date +FROM users; +DROP TABLE users; +ALTER TABLE users_no_fqn RENAME TO users; \ No newline at end of file diff --git a/migrations/sqlite/2019-03-05-082846_add_fqn/up.sql b/migrations/sqlite/2019-03-05-082846_add_fqn/up.sql new file mode 100644 index 00000000000..f28231de628 --- /dev/null +++ b/migrations/sqlite/2019-03-05-082846_add_fqn/up.sql @@ -0,0 +1,18 @@ +-- Your SQL goes here +ALTER TABLE blogs ADD COLUMN fqn TEXT NOT NULL DEFAULT ''; +UPDATE blogs SET fqn = + (CASE WHEN (SELECT local FROM instances WHERE id = instance_id) THEN + actor_id + ELSE + (actor_id || '@' || (SELECT public_domain FROM instances WHERE id = instance_id LIMIT 1)) + END) +WHERE fqn = ''; + +ALTER TABLE users ADD COLUMN fqn TEXT NOT NULL DEFAULT ''; +UPDATE users SET fqn = + (CASE WHEN (SELECT local FROM instances WHERE id = instance_id) THEN + username + ELSE + (username || '@' || (SELECT public_domain FROM instances WHERE id = instance_id LIMIT 1)) + END) +WHERE fqn = ''; \ No newline at end of file diff --git a/migrations/sqlite/2019-03-16-143637_summary-md/down.sql b/migrations/sqlite/2019-03-16-143637_summary-md/down.sql new file mode 100644 index 00000000000..8b0fd636f39 --- /dev/null +++ b/migrations/sqlite/2019-03-16-143637_summary-md/down.sql @@ -0,0 +1,47 @@ +-- This file should undo anything in `up.sql` +CREATE TABLE users2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + username VARCHAR NOT NULL, + display_name VARCHAR NOT NULL DEFAULT '', + outbox_url VARCHAR NOT NULL UNIQUE, + inbox_url VARCHAR NOT NULL UNIQUE, + is_admin BOOLEAN NOT NULL DEFAULT 'f', + summary TEXT NOT NULL DEFAULT '', + email TEXT, + hashed_password TEXT, + instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url TEXT NOT NULL default '' UNIQUE, + private_key TEXT, + public_key TEXT NOT NULL DEFAULT '', + shared_inbox_url VARCHAR, + followers_endpoint VARCHAR NOT NULL DEFAULT '' UNIQUE, + avatar_id INTEGER REFERENCES medias(id) ON DELETE SET NULL, + last_fetched_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + fqn TEXT NOT NULL DEFAULT '', + CONSTRAINT blog_authors_unique UNIQUE (username, instance_id) +); + +INSERT INTO users2 SELECT + id, + username, + display_name, + outbox_url, + inbox_url, + is_admin, + summary, + email, + hashed_password, + instance_id, + creation_date, + ap_url, + private_key, + public_key, + shared_inbox_url, + followers_endpoint, + avatar_id, + last_fetched_date, + fqn +FROM users; +DROP TABLE users; +ALTER TABLE users2 RENAME TO users; diff --git a/migrations/sqlite/2019-03-16-143637_summary-md/up.sql b/migrations/sqlite/2019-03-16-143637_summary-md/up.sql new file mode 100644 index 00000000000..bc7d7c89f41 --- /dev/null +++ b/migrations/sqlite/2019-03-16-143637_summary-md/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE users ADD COLUMN summary_html TEXT NOT NULL DEFAULT ''; +UPDATE users SET summary_html = summary; diff --git a/migrations/sqlite/2019-03-19-191712_blog_images/down.sql b/migrations/sqlite/2019-03-19-191712_blog_images/down.sql new file mode 100644 index 00000000000..71cc99a4a93 --- /dev/null +++ b/migrations/sqlite/2019-03-19-191712_blog_images/down.sql @@ -0,0 +1,33 @@ +-- This file should undo anything in `up.sql + +CREATE TABLE blogs2 ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + actor_id VARCHAR NOT NULL, + title VARCHAR NOT NULL, + summary TEXT NOT NULL DEFAULT '', + outbox_url VARCHAR NOT NULL UNIQUE, + inbox_url VARCHAR NOT NULL UNIQUE, + instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url text not null default '' UNIQUE, + private_key TEXT, + public_key TEXT NOT NULL DEFAULT '', + fqn TEXT NOT NULL DEFAULT '', + CONSTRAINT blog_unique UNIQUE (actor_id, instance_id) +); +INSERT INTO blogs2 SELECT + id, + actor_id, + title, + summary, + outbox_url, + inbox_url, + instance_id, + creation_date, + ap_url, + private_key, + public_key, + fqn +FROM blogs; +DROP TABLE blogs; +ALTER TABLE blogs2 RENAME TO blogs; \ No newline at end of file diff --git a/migrations/sqlite/2019-03-19-191712_blog_images/up.sql b/migrations/sqlite/2019-03-19-191712_blog_images/up.sql new file mode 100644 index 00000000000..b2207caa81a --- /dev/null +++ b/migrations/sqlite/2019-03-19-191712_blog_images/up.sql @@ -0,0 +1,4 @@ +-- Your SQL goes here +ALTER TABLE blogs ADD COLUMN summary_html TEXT NOT NULL DEFAULT ''; +ALTER TABLE blogs ADD COLUMN icon_id INTEGER REFERENCES medias(id) ON DELETE SET NULL DEFAULT NULL; +ALTER TABLE blogs ADD COLUMN banner_id INTEGER REFERENCES medias(id) ON DELETE SET NULL DEFAULT NULL; \ No newline at end of file diff --git a/migrations/sqlite/2019-03-25-205630_fix-comment-seers/down.sql b/migrations/sqlite/2019-03-25-205630_fix-comment-seers/down.sql new file mode 100644 index 00000000000..68518e3a502 --- /dev/null +++ b/migrations/sqlite/2019-03-25-205630_fix-comment-seers/down.sql @@ -0,0 +1,15 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE comment_seers RENAME TO tmp_comment_seers; + +CREATE TABLE comment_seers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + comment_id INTEGER REFERENCES comments(id) ON DELETE CASCADE NOT NULL, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + UNIQUE (comment_id, user_id) +); + +INSERT INTO comment_seers(id, comment_id, user_id) +SELECT id, comment_id, user_id +FROM tmp_comment_seers; + +DROP TABLE tmp_comment_seers; diff --git a/migrations/sqlite/2019-03-25-205630_fix-comment-seers/up.sql b/migrations/sqlite/2019-03-25-205630_fix-comment-seers/up.sql new file mode 100644 index 00000000000..09c5890fa4a --- /dev/null +++ b/migrations/sqlite/2019-03-25-205630_fix-comment-seers/up.sql @@ -0,0 +1,16 @@ +-- Your SQL goes here +ALTER TABLE comment_seers RENAME TO tmp_comment_seers; + +CREATE TABLE comment_seers ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + comment_id INTEGER REFERENCES comments(id) ON DELETE CASCADE NOT NULL, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL, + UNIQUE (comment_id, user_id) +); + +INSERT INTO comment_seers(id, comment_id, user_id) +SELECT id, comment_id, user_id +FROM tmp_comment_seers +WHERE id NOT NULL; + +DROP TABLE tmp_comment_seers; diff --git a/migrations/sqlite/2019-06-04-102747_create_password_reset_requests_table/down.sql b/migrations/sqlite/2019-06-04-102747_create_password_reset_requests_table/down.sql new file mode 100644 index 00000000000..7d4bc8fde3d --- /dev/null +++ b/migrations/sqlite/2019-06-04-102747_create_password_reset_requests_table/down.sql @@ -0,0 +1 @@ +DROP TABLE password_reset_requests; diff --git a/migrations/sqlite/2019-06-04-102747_create_password_reset_requests_table/up.sql b/migrations/sqlite/2019-06-04-102747_create_password_reset_requests_table/up.sql new file mode 100644 index 00000000000..bf64163f5d6 --- /dev/null +++ b/migrations/sqlite/2019-06-04-102747_create_password_reset_requests_table/up.sql @@ -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); diff --git a/migrations/sqlite/2019-06-18-175952_moderator_role/down.sql b/migrations/sqlite/2019-06-18-175952_moderator_role/down.sql new file mode 100644 index 00000000000..ca9e7ce94e4 --- /dev/null +++ b/migrations/sqlite/2019-06-18-175952_moderator_role/down.sql @@ -0,0 +1,72 @@ +-- This file should undo anything in `up.sql` +CREATE TABLE IF NOT EXISTS "users_without_role" ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + username VARCHAR NOT NULL, + display_name VARCHAR NOT NULL DEFAULT '', + outbox_url VARCHAR NOT NULL UNIQUE, + inbox_url VARCHAR NOT NULL UNIQUE, + is_admin BOOLEAN NOT NULL DEFAULT 'f', + summary TEXT NOT NULL DEFAULT '', + email TEXT, + hashed_password TEXT, + instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url TEXT NOT NULL default '' UNIQUE, + private_key TEXT, + public_key TEXT NOT NULL DEFAULT '', + shared_inbox_url VARCHAR, + followers_endpoint VARCHAR NOT NULL DEFAULT '' UNIQUE, + avatar_id INTEGER REFERENCES medias(id) ON DELETE CASCADE, + last_fetched_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + fqn TEXT NOT NULL DEFAULT '', + summary_html TEXT NOT NULL DEFAULT '', + FOREIGN KEY (avatar_id) REFERENCES medias(id) ON DELETE SET NULL, + CONSTRAINT blog_authors_unique UNIQUE (username, instance_id) +); + +INSERT INTO users_without_role SELECT + id, + username, + display_name, + outbox_url, + inbox_url, + 't', + summary, + email, + hashed_password, + instance_id, + creation_date, + ap_url, + private_key, + public_key, + shared_inbox_url, + followers_endpoint, + avatar_id, + last_fetched_date, + fqn, + summary +FROM users WHERE role = 0; +INSERT INTO users_without_role SELECT + id, + username, + display_name, + outbox_url, + inbox_url, + 'f', + summary, + email, + hashed_password, + instance_id, + creation_date, + ap_url, + private_key, + public_key, + shared_inbox_url, + followers_endpoint, + avatar_id, + last_fetched_date, + fqn, + summary +FROM users WHERE role != 0; +DROP TABLE users; +ALTER TABLE users_without_role RENAME TO users; diff --git a/migrations/sqlite/2019-06-18-175952_moderator_role/up.sql b/migrations/sqlite/2019-06-18-175952_moderator_role/up.sql new file mode 100644 index 00000000000..9c71ab9ad0a --- /dev/null +++ b/migrations/sqlite/2019-06-18-175952_moderator_role/up.sql @@ -0,0 +1,74 @@ +-- Your SQL goes here + +CREATE TABLE IF NOT EXISTS "users_with_role" ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + username VARCHAR NOT NULL, + display_name VARCHAR NOT NULL DEFAULT '', + outbox_url VARCHAR NOT NULL UNIQUE, + inbox_url VARCHAR NOT NULL UNIQUE, + summary TEXT NOT NULL DEFAULT '', + email TEXT, + hashed_password TEXT, + instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url TEXT NOT NULL default '' UNIQUE, + private_key TEXT, + public_key TEXT NOT NULL DEFAULT '', + shared_inbox_url VARCHAR, + followers_endpoint VARCHAR NOT NULL DEFAULT '' UNIQUE, + avatar_id INTEGER REFERENCES medias(id) ON DELETE CASCADE, + last_fetched_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + fqn TEXT NOT NULL DEFAULT '', + summary_html TEXT NOT NULL DEFAULT '', + role INTEGER NOT NULL DEFAULT 2, + FOREIGN KEY (avatar_id) REFERENCES medias(id) ON DELETE SET NULL, + CONSTRAINT blog_authors_unique UNIQUE (username, instance_id) +); + + +INSERT INTO users_with_role SELECT + id, + username, + display_name, + outbox_url, + inbox_url, + summary, + email, + hashed_password, + instance_id, + creation_date, + ap_url, + private_key, + public_key, + shared_inbox_url, + followers_endpoint, + avatar_id, + last_fetched_date, + fqn, + summary, + 0 +FROM users WHERE is_admin = 't'; +INSERT INTO users_with_role SELECT + id, + username, + display_name, + outbox_url, + inbox_url, + summary, + email, + hashed_password, + instance_id, + creation_date, + ap_url, + private_key, + public_key, + shared_inbox_url, + followers_endpoint, + avatar_id, + last_fetched_date, + fqn, + summary, + 2 +FROM users WHERE is_admin = 'f'; +DROP TABLE users; +ALTER TABLE users_with_role RENAME TO users; diff --git a/migrations/sqlite/2019-06-21-154916_themes/down.sql b/migrations/sqlite/2019-06-21-154916_themes/down.sql new file mode 100644 index 00000000000..f9ab4013144 --- /dev/null +++ b/migrations/sqlite/2019-06-21-154916_themes/down.sql @@ -0,0 +1,88 @@ +-- This file should undo anything in `up.sql` + +CREATE TABLE blogs_before_themes ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + actor_id VARCHAR NOT NULL, + title VARCHAR NOT NULL, + summary TEXT NOT NULL DEFAULT '', + outbox_url VARCHAR NOT NULL UNIQUE, + inbox_url VARCHAR NOT NULL UNIQUE, + instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url text not null default '' UNIQUE, + private_key TEXT, + public_key TEXT NOT NULL DEFAULT '', + fqn TEXT NOT NULL DEFAULT '', + summary_html TEXT NOT NULL DEFAULT '', + icon_id INTEGER REFERENCES medias(id) ON DELETE SET NULL DEFAULT NULL, + banner_id INTEGER REFERENCES medias(id) ON DELETE SET NULL DEFAULT NULL, + CONSTRAINT blog_unique UNIQUE (actor_id, instance_id) +); +INSERT INTO blogs_before_themes SELECT + id, + actor_id, + title, + summary, + outbox_url, + inbox_url, + instance_id, + creation_date, + ap_url, + private_key, + public_key, + fqn, + summary_html, + icon_id, + banner_id +FROM blogs; +DROP TABLE blogs; +ALTER TABLE blogs_before_themes RENAME TO blogs; + +CREATE TABLE users_before_themes ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + username VARCHAR NOT NULL, + display_name VARCHAR NOT NULL DEFAULT '', + outbox_url VARCHAR NOT NULL UNIQUE, + inbox_url VARCHAR NOT NULL UNIQUE, + summary TEXT NOT NULL DEFAULT '', + email TEXT, + hashed_password TEXT, + instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ap_url TEXT NOT NULL default '' UNIQUE, + private_key TEXT, + public_key TEXT NOT NULL DEFAULT '', + shared_inbox_url VARCHAR, + followers_endpoint VARCHAR NOT NULL DEFAULT '' UNIQUE, + avatar_id INTEGER REFERENCES medias(id) ON DELETE CASCADE, + last_fetched_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + fqn TEXT NOT NULL DEFAULT '', + summary_html TEXT NOT NULL DEFAULT '', + role INTEGER NOT NULL DEFAULT 2, + FOREIGN KEY (avatar_id) REFERENCES medias(id) ON DELETE SET NULL, + CONSTRAINT blog_authors_unique UNIQUE (username, instance_id) +); +INSERT INTO users_before_themes SELECT + id, + username, + display_name, + outbox_url, + inbox_url, + summary, + email, + hashed_password, + instance_id, + creation_date, + ap_url, + private_key, + public_key, + shared_inbox_url, + followers_endpoint, + avatar_id, + last_fetched_date, + fqn, + summary_html, + role +FROM users; +DROP TABLE users; +ALTER TABLE users_before_themes RENAME TO users; diff --git a/migrations/sqlite/2019-06-21-154916_themes/up.sql b/migrations/sqlite/2019-06-21-154916_themes/up.sql new file mode 100644 index 00000000000..5a92b2a0beb --- /dev/null +++ b/migrations/sqlite/2019-06-21-154916_themes/up.sql @@ -0,0 +1,4 @@ +-- Your SQL goes here +ALTER TABLE blogs ADD COLUMN theme VARCHAR; +ALTER TABLE users ADD COLUMN preferred_theme VARCHAR; +ALTER TABLE users ADD COLUMN hide_custom_css BOOLEAN NOT NULL DEFAULT 'f'; diff --git a/migrations/sqlite/2019-06-22-145757_timeline/down.sql b/migrations/sqlite/2019-06-22-145757_timeline/down.sql new file mode 100644 index 00000000000..d86fb6ee1f3 --- /dev/null +++ b/migrations/sqlite/2019-06-22-145757_timeline/down.sql @@ -0,0 +1,6 @@ +-- This file should undo anything in `up.sql` + +DROP TABLE timeline; +DROP TABLE timeline_definition; +DROP TABLE list_elems; +DROP TABLE lists; diff --git a/migrations/sqlite/2019-06-22-145757_timeline/up.sql b/migrations/sqlite/2019-06-22-145757_timeline/up.sql new file mode 100644 index 00000000000..0b211997103 --- /dev/null +++ b/migrations/sqlite/2019-06-22-145757_timeline/up.sql @@ -0,0 +1,31 @@ +-- Your SQL goes here + +CREATE TABLE timeline_definition( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + name VARCHAR NOT NULL, + query VARCHAR NOT NULL, + CONSTRAINT timeline_unique_user_name UNIQUE(user_id, name) +); + +CREATE TABLE timeline( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + post_id integer NOT NULL REFERENCES posts(id) ON DELETE CASCADE, + timeline_id integer NOT NULL REFERENCES timeline_definition(id) ON DELETE CASCADE +); + +CREATE TABLE lists( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name VARCHAR NOT NULL, + user_id integer REFERENCES users(id) ON DELETE CASCADE, + type integer NOT NULL, + CONSTRAINT timeline_unique_user_name UNIQUE(user_id, name) +); + +CREATE TABLE list_elems( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + list_id integer NOT NULL REFERENCES lists(id) ON DELETE CASCADE, + user_id integer REFERENCES users(id) ON DELETE CASCADE, + blog_id integer REFERENCES blogs(id) ON DELETE CASCADE, + word VARCHAR +); diff --git a/migrations/sqlite/2019-06-24-105533_use_timelines_for_feed/down.sql b/migrations/sqlite/2019-06-24-105533_use_timelines_for_feed/down.sql new file mode 100644 index 00000000000..20382150e6f --- /dev/null +++ b/migrations/sqlite/2019-06-24-105533_use_timelines_for_feed/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` +DELETE FROM timeline_definition WHERE name = 'Your feed'; +DELETE FROM timeline_definition WHERE name = 'Local feed' AND query = 'local'; +DELETE FROM timeline_definition WHERE name = 'Federared feed' AND query = 'all'; diff --git a/migrations/sqlite/2019-06-24-105533_use_timelines_for_feed/up.sql b/migrations/sqlite/2019-06-24-105533_use_timelines_for_feed/up.sql new file mode 100644 index 00000000000..7357b422f9d --- /dev/null +++ b/migrations/sqlite/2019-06-24-105533_use_timelines_for_feed/up.sql @@ -0,0 +1,6 @@ +-- Your SQL goes here +INSERT INTO timeline_definition (name, query) VALUES + ('Local feed', 'local'), + ('Federated feed', 'all'); +INSERT INTO timeline_definition (user_id,name,query) + select id,'Your feed','followed or ['||fqn||']' from users; diff --git a/migrations/sqlite/2019-12-10-104935_fill_timelines/down.sql b/migrations/sqlite/2019-12-10-104935_fill_timelines/down.sql new file mode 100644 index 00000000000..a1708a46bef --- /dev/null +++ b/migrations/sqlite/2019-12-10-104935_fill_timelines/down.sql @@ -0,0 +1,8 @@ +DELETE FROM timeline WHERE id IN + ( + SELECT timeline.id FROM timeline + INNER JOIN timeline_definition ON timeline.timeline_id = timeline_definition.id + WHERE timeline_definition.query LIKE 'followed or [%]' OR + timeline_definition.query = 'local' OR + timeline_definition.query = 'all' + ); diff --git a/migrations/sqlite/2019-12-10-104935_fill_timelines/up.sql b/migrations/sqlite/2019-12-10-104935_fill_timelines/up.sql new file mode 100644 index 00000000000..49f4eba749e --- /dev/null +++ b/migrations/sqlite/2019-12-10-104935_fill_timelines/up.sql @@ -0,0 +1,17 @@ +INSERT INTO timeline (post_id, timeline_id) + SELECT posts.id,timeline_definition.id FROM posts,timeline_definition + WHERE timeline_definition.query = 'all'; + +INSERT INTO timeline (post_id, timeline_id) + SELECT posts.id,timeline_definition.id FROM posts,timeline_definition + INNER JOIN blogs ON posts.blog_id = blogs.id + INNER JOIN instances ON blogs.instance_id = instances.id + WHERE timeline_definition.query = 'local' and instances.local = 1; + +INSERT INTO timeline (post_id, timeline_id) + SELECT posts.id,timeline_definition.id FROM posts + INNER JOIN blog_authors ON posts.blog_id = blog_authors.blog_id + LEFT JOIN follows ON blog_authors.author_id = follows.following_id + INNER JOIN timeline_definition ON follows.follower_id = timeline_definition.user_id + or blog_authors.author_id = timeline_definition.user_id + WHERE timeline_definition.query LIKE 'followed or [%]'; diff --git a/migrations/sqlite/2020-01-05-232816_add_blocklist/down.sql b/migrations/sqlite/2020-01-05-232816_add_blocklist/down.sql new file mode 100644 index 00000000000..96eb27daca9 --- /dev/null +++ b/migrations/sqlite/2020-01-05-232816_add_blocklist/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` + +drop table email_blocklist; diff --git a/migrations/sqlite/2020-01-05-232816_add_blocklist/up.sql b/migrations/sqlite/2020-01-05-232816_add_blocklist/up.sql new file mode 100644 index 00000000000..dadd1446128 --- /dev/null +++ b/migrations/sqlite/2020-01-05-232816_add_blocklist/up.sql @@ -0,0 +1,6 @@ +-- Your SQL goes here +CREATE TABLE email_blocklist(id INTEGER PRIMARY KEY, + email_address TEXT UNIQUE, + note TEXT, + notify_user BOOLEAN DEFAULT FALSE, + notification_text TEXT); diff --git a/migrations/sqlite/2021-02-23-153402_medias_index_file_path/down.sql b/migrations/sqlite/2021-02-23-153402_medias_index_file_path/down.sql new file mode 100644 index 00000000000..9604e5d0ca1 --- /dev/null +++ b/migrations/sqlite/2021-02-23-153402_medias_index_file_path/down.sql @@ -0,0 +1 @@ +DROP INDEX medias_index_file_path; diff --git a/migrations/sqlite/2021-02-23-153402_medias_index_file_path/up.sql b/migrations/sqlite/2021-02-23-153402_medias_index_file_path/up.sql new file mode 100644 index 00000000000..d26ad494d50 --- /dev/null +++ b/migrations/sqlite/2021-02-23-153402_medias_index_file_path/up.sql @@ -0,0 +1 @@ +CREATE INDEX medias_index_file_path ON medias (file_path); diff --git a/migrations/sqlite/2022-01-04-122156_create_email_signups_table/down.sql b/migrations/sqlite/2022-01-04-122156_create_email_signups_table/down.sql new file mode 100644 index 00000000000..40af0a6ca5a --- /dev/null +++ b/migrations/sqlite/2022-01-04-122156_create_email_signups_table/down.sql @@ -0,0 +1 @@ +DROP TABLE email_signups; diff --git a/migrations/sqlite/2022-01-04-122156_create_email_signups_table/up.sql b/migrations/sqlite/2022-01-04-122156_create_email_signups_table/up.sql new file mode 100644 index 00000000000..fa8ce11896c --- /dev/null +++ b/migrations/sqlite/2022-01-04-122156_create_email_signups_table/up.sql @@ -0,0 +1,9 @@ +CREATE TABLE email_signups ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + email VARCHAR NOT NULL, + token VARCHAR NOT NULL, + expiration_date TIMESTAMP NOT NULL +); + +CREATE INDEX email_signups_token ON email_signups (token); +CREATE UNIQUE INDEX email_signups_token_requests_email ON email_signups (email); diff --git a/migrations/sqlite/2022-01-29-154457_add_not_null_constraint_to_email_blocklist/down.sql b/migrations/sqlite/2022-01-29-154457_add_not_null_constraint_to_email_blocklist/down.sql new file mode 100644 index 00000000000..df77c239260 --- /dev/null +++ b/migrations/sqlite/2022-01-29-154457_add_not_null_constraint_to_email_blocklist/down.sql @@ -0,0 +1,9 @@ +CREATE TABLE email_blocklist2(id INTEGER PRIMARY KEY, + email_address TEXT UNIQUE, + note TEXT, + notify_user BOOLEAN DEFAULT FALSE, + notification_text TEXT); + +INSERT INTO email_blocklist2 SELECT * FROM email_blocklist; +DROP TABLE email_blocklist; +ALTER TABLE email_blocklist2 RENAME TO email_blocklist; diff --git a/migrations/sqlite/2022-01-29-154457_add_not_null_constraint_to_email_blocklist/up.sql b/migrations/sqlite/2022-01-29-154457_add_not_null_constraint_to_email_blocklist/up.sql new file mode 100644 index 00000000000..ce725816062 --- /dev/null +++ b/migrations/sqlite/2022-01-29-154457_add_not_null_constraint_to_email_blocklist/up.sql @@ -0,0 +1,9 @@ +CREATE TABLE email_blocklist2(id INTEGER PRIMARY KEY, + email_address TEXT UNIQUE NOT NULL, + note TEXT NOT NULL, + notify_user BOOLEAN DEFAULT FALSE NOT NULL, + notification_text TEXT NOT NULL); + +INSERT INTO email_blocklist2 SELECT * FROM email_blocklist; +DROP TABLE email_blocklist; +ALTER TABLE email_blocklist2 RENAME TO email_blocklist; diff --git a/plume-api/Cargo.toml b/plume-api/Cargo.toml new file mode 100644 index 00000000000..b1da73d86a1 --- /dev/null +++ b/plume-api/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "plume-api" +version = "0.7.1" +authors = ["Plume contributors"] +edition = "2018" + +[dependencies] +serde = "1.0" +serde_derive = "1.0" diff --git a/plume-api/release.toml b/plume-api/release.toml new file mode 100644 index 00000000000..b927687cd89 --- /dev/null +++ b/plume-api/release.toml @@ -0,0 +1,2 @@ +pre-release-hook = ["cargo", "fmt"] +pre-release-replacements = [] diff --git a/plume-api/src/apps.rs b/plume-api/src/apps.rs new file mode 100644 index 00000000000..9e0610d2f5f --- /dev/null +++ b/plume-api/src/apps.rs @@ -0,0 +1,6 @@ +#[derive(Clone, Serialize, Deserialize)] +pub struct NewAppData { + pub name: String, + pub website: Option, + pub redirect_uri: Option, +} diff --git a/plume-api/src/lib.rs b/plume-api/src/lib.rs new file mode 100644 index 00000000000..c22af6a956e --- /dev/null +++ b/plume-api/src/lib.rs @@ -0,0 +1,5 @@ +#[macro_use] +extern crate serde_derive; + +pub mod apps; +pub mod posts; diff --git a/plume-api/src/posts.rs b/plume-api/src/posts.rs new file mode 100644 index 00000000000..57b7cf298e7 --- /dev/null +++ b/plume-api/src/posts.rs @@ -0,0 +1,31 @@ +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct NewPostData { + pub title: String, + pub subtitle: Option, + 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, + pub published: Option, + pub creation_date: Option, + pub license: Option, + pub tags: Option>, + pub cover_id: Option, +} + +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct PostData { + pub id: i32, + pub title: String, + pub subtitle: String, + pub content: String, + pub source: Option, + pub authors: Vec, + pub blog_id: i32, + pub published: bool, + pub creation_date: String, + pub license: String, + pub tags: Vec, + pub cover_id: Option, +} diff --git a/plume-cli/Cargo.toml b/plume-cli/Cargo.toml new file mode 100644 index 00000000000..11303cd5e38 --- /dev/null +++ b/plume-cli/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "plume-cli" +version = "0.7.1" +authors = ["Plume contributors"] +edition = "2018" + +[[bin]] +name = "plm" +path = "src/main.rs" + +[dependencies] +clap = "2.33" +dotenv = "0.15" +rpassword = "6.0.1" + +[dependencies.diesel] +features = ["r2d2", "chrono"] +version = "1.4.5" + +[dependencies.plume-models] +path = "../plume-models" + +[features] +postgres = ["plume-models/postgres", "diesel/postgres"] +sqlite = ["plume-models/sqlite", "diesel/sqlite"] +search-lindera = ["plume-models/search-lindera"] diff --git a/plume-cli/release.toml b/plume-cli/release.toml new file mode 100644 index 00000000000..b927687cd89 --- /dev/null +++ b/plume-cli/release.toml @@ -0,0 +1,2 @@ +pre-release-hook = ["cargo", "fmt"] +pre-release-replacements = [] diff --git a/plume-cli/src/instance.rs b/plume-cli/src/instance.rs new file mode 100644 index 00000000000..35793639d87 --- /dev/null +++ b/plume-cli/src/instance.rs @@ -0,0 +1,73 @@ +use clap::{App, Arg, ArgMatches, SubCommand}; + +use plume_models::{instance::*, safe_string::SafeString, Connection}; +use std::env; + +pub fn command<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("instance") + .about("Manage instances") + .subcommand(SubCommand::with_name("new") + .arg(Arg::with_name("domain") + .short("d") + .long("domain") + .takes_value(true) + .help("The domain name of your instance") + ).arg(Arg::with_name("name") + .short("n") + .long("name") + .takes_value(true) + .help("The name of your instance") + ).arg(Arg::with_name("default-license") + .short("l") + .long("default-license") + .takes_value(true) + .help("The license that will be used by default for new articles on this instance") + ).arg(Arg::with_name("private") + .short("p") + .long("private") + .help("Closes the registrations on this instance") + ).about("Create a new local instance")) +} + +pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) { + let conn = conn; + match args.subcommand() { + ("new", Some(x)) => new(x, conn), + ("", None) => command().print_help().unwrap(), + _ => println!("Unknown subcommand"), + } +} + +fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) { + let domain = args + .value_of("domain") + .map(String::from) + .unwrap_or_else(|| env::var("BASE_URL").unwrap_or_else(|_| super::ask_for("Domain name"))); + let name = args + .value_of("name") + .map(String::from) + .unwrap_or_else(|| super::ask_for("Instance name")); + let license = args + .value_of("default-license") + .map(String::from) + .unwrap_or_else(|| String::from("CC-BY-SA")); + let open_reg = !args.is_present("private"); + + Instance::insert( + conn, + NewInstance { + public_domain: domain, + name, + local: true, + long_description: SafeString::new(""), + short_description: SafeString::new(""), + default_license: license, + open_registrations: open_reg, + short_description_html: String::new(), + long_description_html: String::new(), + }, + ) + .expect("Couldn't save instance"); + Instance::cache_local(conn); + Instance::create_local_instance_user(conn).expect("Couldn't save local instance user"); +} diff --git a/plume-cli/src/main.rs b/plume-cli/src/main.rs new file mode 100644 index 00000000000..3615c10de20 --- /dev/null +++ b/plume-cli/src/main.rs @@ -0,0 +1,56 @@ +use clap::App; +use diesel::Connection; +use plume_models::{instance::Instance, Connection as Conn, CONFIG}; +use std::io::{self, prelude::*}; + +mod instance; +mod migration; +mod search; +mod users; + +fn main() { + let mut app = App::new("Plume CLI") + .bin_name("plm") + .version(env!("CARGO_PKG_VERSION")) + .about("Collection of tools to manage your Plume instance.") + .subcommand(instance::command()) + .subcommand(migration::command()) + .subcommand(search::command()) + .subcommand(users::command()); + let matches = app.clone().get_matches(); + + 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(Instance::cache_local); + + match matches.subcommand() { + ("instance", Some(args)) => { + instance::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"), + }; +} + +pub fn ask_for(something: &str) -> String { + print!("{}: ", something); + io::stdout().flush().expect("Couldn't flush STDOUT"); + let mut input = String::new(); + io::stdin() + .read_line(&mut input) + .expect("Unable to read line"); + input.retain(|c| c != '\n'); + input +} diff --git a/plume-cli/src/migration.rs b/plume-cli/src/migration.rs new file mode 100644 index 00000000000..3a147c6c0be --- /dev/null +++ b/plume-cli/src/migration.rs @@ -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") +} diff --git a/plume-cli/src/search.rs b/plume-cli/src/search.rs new file mode 100644 index 00000000000..a8df9e36384 --- /dev/null +++ b/plume-cli/src/search.rs @@ -0,0 +1,118 @@ +use clap::{App, Arg, ArgMatches, SubCommand}; + +use plume_models::{search::Searcher, Connection, CONFIG}; +use std::fs::{read_dir, remove_file}; +use std::io::ErrorKind; +use std::path::Path; + +pub fn command<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("search") + .about("Manage search index") + .subcommand( + SubCommand::with_name("init") + .arg( + Arg::with_name("path") + .short("p") + .long("path") + .takes_value(true) + .required(false) + .help("Path to Plume's working directory"), + ) + .arg( + Arg::with_name("force") + .short("f") + .long("force") + .help("Ignore already using directory"), + ) + .about("Initialize Plume's internal search engine"), + ) + .subcommand( + SubCommand::with_name("refill") + .arg( + Arg::with_name("path") + .short("p") + .long("path") + .takes_value(true) + .required(false) + .help("Path to Plume's working directory"), + ) + .about("Regenerate Plume's search index"), + ) + .subcommand( + SubCommand::with_name("unlock") + .arg( + Arg::with_name("path") + .short("p") + .long("path") + .takes_value(true) + .required(false) + .help("Path to Plume's working directory"), + ) + .about("Release lock on search directory"), + ) +} + +pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) { + let conn = conn; + match args.subcommand() { + ("init", Some(x)) => init(x, conn), + ("refill", Some(x)) => refill(x, conn, None), + ("unlock", Some(x)) => unlock(x), + ("", None) => command().print_help().unwrap(), + _ => println!("Unknown subcommand"), + } +} + +fn init<'a>(args: &ArgMatches<'a>, conn: &Connection) { + let path = args + .value_of("path") + .map(|p| Path::new(p).join("search_index")) + .unwrap_or_else(|| Path::new(&CONFIG.search_index).to_path_buf()); + let force = args.is_present("force"); + + let can_do = match read_dir(path.clone()) { + // try to read the directory specified + Ok(mut contents) => contents.next().is_none(), + Err(e) => { + if e.kind() == ErrorKind::NotFound { + true + } else { + panic!("Error while initialising search index : {}", e); + } + } + }; + if can_do || force { + let searcher = Searcher::create(&path, &CONFIG.search_tokenizers).unwrap(); + refill(args, conn, Some(searcher)); + } else { + eprintln!( + "Can't create new index, {} exist and is not empty", + path.to_str().unwrap() + ); + } +} + +fn refill<'a>(args: &ArgMatches<'a>, conn: &Connection, searcher: Option) { + let path = args.value_of("path"); + let path = match path { + Some(path) => Path::new(path).join("search_index"), + None => Path::new(&CONFIG.search_index).to_path_buf(), + }; + let searcher = + searcher.unwrap_or_else(|| Searcher::open(&path, &CONFIG.search_tokenizers).unwrap()); + + searcher.fill(conn).expect("Couldn't import post"); + println!("Commiting result"); + searcher.commit(); +} + +fn unlock(args: &ArgMatches) { + let path = match args.value_of("path") { + None => Path::new(&CONFIG.search_index), + Some(x) => Path::new(x), + }; + let meta = Path::new(path).join(".tantivy-meta.lock"); + remove_file(meta).unwrap(); + let writer = Path::new(path).join(".tantivy-writer.lock"); + remove_file(writer).unwrap(); +} diff --git a/plume-cli/src/users.rs b/plume-cli/src/users.rs new file mode 100644 index 00000000000..9df75713e7d --- /dev/null +++ b/plume-cli/src/users.rs @@ -0,0 +1,162 @@ +use clap::{App, Arg, ArgMatches, SubCommand}; + +use plume_models::{instance::Instance, users::*, Connection}; +use std::io::{self, Write}; + +pub fn command<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("users") + .about("Manage users") + .subcommand( + SubCommand::with_name("new") + .arg( + Arg::with_name("name") + .short("n") + .long("name") + .alias("username") + .takes_value(true) + .help("The username of the new user"), + ) + .arg( + Arg::with_name("display-name") + .short("N") + .long("display-name") + .takes_value(true) + .help("The display name of the new user"), + ) + .arg( + Arg::with_name("biography") + .short("b") + .long("bio") + .alias("biography") + .takes_value(true) + .help("The biography of the new user"), + ) + .arg( + Arg::with_name("email") + .short("e") + .long("email") + .takes_value(true) + .help("Email address of the new user"), + ) + .arg( + Arg::with_name("password") + .short("p") + .long("password") + .takes_value(true) + .help("The password of the new user"), + ) + .arg( + Arg::with_name("admin") + .short("a") + .long("admin") + .help("Makes the user an administrator of the instance"), + ) + .arg( + Arg::with_name("moderator") + .short("m") + .long("moderator") + .help("Makes the user a moderator of the instance"), + ) + .about("Create a new user on this instance"), + ) + .subcommand( + SubCommand::with_name("reset-password") + .arg( + Arg::with_name("name") + .short("u") + .long("user") + .alias("username") + .takes_value(true) + .help("The username of the user to reset password to"), + ) + .arg( + Arg::with_name("password") + .short("p") + .long("password") + .takes_value(true) + .help("The password new for the user"), + ) + .about("Reset user password"), + ) +} + +pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) { + let conn = conn; + match args.subcommand() { + ("new", Some(x)) => new(x, conn), + ("reset-password", Some(x)) => reset_password(x, conn), + ("", None) => command().print_help().unwrap(), + _ => println!("Unknown subcommand"), + } +} + +fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) { + let username = args + .value_of("name") + .map(String::from) + .unwrap_or_else(|| super::ask_for("Username")); + let display_name = args + .value_of("display-name") + .map(String::from) + .unwrap_or_else(|| super::ask_for("Display name")); + + let admin = args.is_present("admin"); + let moderator = args.is_present("moderator"); + let role = if admin { + Role::Admin + } else if moderator { + Role::Moderator + } else { + Role::Normal + }; + + let bio = args.value_of("biography").unwrap_or("").to_string(); + let email = args + .value_of("email") + .map(String::from) + .unwrap_or_else(|| super::ask_for("Email address")); + let password = args + .value_of("password") + .map(String::from) + .unwrap_or_else(|| { + print!("Password: "); + io::stdout().flush().expect("Couldn't flush STDOUT"); + rpassword::read_password().expect("Couldn't read your password.") + }); + + NewUser::new_local( + conn, + username, + display_name, + role, + &bio, + email, + Some(User::hash_pass(&password).expect("Couldn't hash password")), + ) + .expect("Couldn't save new user"); +} + +fn reset_password<'a>(args: &ArgMatches<'a>, conn: &Connection) { + let username = args + .value_of("name") + .map(String::from) + .unwrap_or_else(|| super::ask_for("Username")); + let user = User::find_by_name( + conn, + &username, + Instance::get_local() + .expect("Failed to get local instance") + .id, + ) + .expect("Failed to get user"); + let password = args + .value_of("password") + .map(String::from) + .unwrap_or_else(|| { + print!("Password: "); + io::stdout().flush().expect("Couldn't flush STDOUT"); + rpassword::read_password().expect("Couldn't read your password.") + }); + user.reset_password(conn, &password) + .expect("Failed to reset password"); +} diff --git a/plume-common/Cargo.toml b/plume-common/Cargo.toml new file mode 100644 index 00000000000..8974345164b --- /dev/null +++ b/plume-common/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "plume-common" +version = "0.7.1" +authors = ["Plume contributors"] +edition = "2018" + +[dependencies] +activitypub = "0.1.1" +activitystreams-derive = "0.1.1" +activitystreams-traits = "0.1.0" +array_tool = "1.0" +base64 = "0.13" +heck = "0.4.0" +hex = "0.4" +openssl = "0.10.22" +rocket = "0.4.6" +reqwest = { version = "0.9", features = ["socks"] } +serde = "1.0" +serde_derive = "1.0" +serde_json = "1.0.79" +shrinkwraprs = "0.3.0" +syntect = "4.5.0" +tokio = "0.1.22" +regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] } +tracing = "0.1.32" +askama_escape = "0.10.3" + +[dependencies.chrono] +features = ["serde"] +version = "0.4" + +[dependencies.pulldown-cmark] +default-features = false +git = "https://git.joinplu.me/Plume/pulldown-cmark" +branch = "bidi-plume" + +[dev-dependencies] +once_cell = "1.10.0" + +[features] diff --git a/plume-common/release.toml b/plume-common/release.toml new file mode 100644 index 00000000000..b927687cd89 --- /dev/null +++ b/plume-common/release.toml @@ -0,0 +1,2 @@ +pre-release-hook = ["cargo", "fmt"] +pre-release-replacements = [] diff --git a/plume-common/src/activity_pub/inbox.rs b/plume-common/src/activity_pub/inbox.rs new file mode 100644 index 00000000000..48ba6c5f1bf --- /dev/null +++ b/plume-common/src/activity_pub/inbox.rs @@ -0,0 +1,788 @@ +use reqwest; +use std::fmt::Debug; + +use super::{request, sign::Signer}; + +/// Represents an ActivityPub inbox. +/// +/// It routes an incoming Activity through the registered handlers. +/// +/// # Example +/// +/// ```rust +/// # extern crate activitypub; +/// # use activitypub::{actor::Person, activity::{Announce, Create}, object::Note}; +/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa}; +/// # use once_cell::sync::Lazy; +/// # use plume_common::activity_pub::inbox::*; +/// # use plume_common::activity_pub::sign::{gen_keypair, Error as SignError, Result as SignResult, Signer}; +/// # +/// # static MY_SIGNER: Lazy = Lazy::new(|| MySigner::new()); +/// # +/// # struct MySigner { +/// # public_key: String, +/// # private_key: String, +/// # } +/// # +/// # impl MySigner { +/// # fn new() -> Self { +/// # let (pub_key, priv_key) = gen_keypair(); +/// # Self { +/// # public_key: String::from_utf8(pub_key).unwrap(), +/// # private_key: String::from_utf8(priv_key).unwrap(), +/// # } +/// # } +/// # } +/// # +/// # impl Signer for MySigner { +/// # fn get_key_id(&self) -> String { +/// # "mysigner".into() +/// # } +/// # +/// # fn sign(&self, to_sign: &str) -> SignResult> { +/// # let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap()) +/// # .unwrap(); +/// # let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap(); +/// # signer.update(to_sign.as_bytes()).unwrap(); +/// # signer.sign_to_vec().map_err(|_| SignError()) +/// # } +/// # +/// # fn verify(&self, data: &str, signature: &[u8]) -> SignResult { +/// # let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap()) +/// # .unwrap(); +/// # let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap(); +/// # verifier.update(data.as_bytes()).unwrap(); +/// # verifier.verify(&signature).map_err(|_| SignError()) +/// # } +/// # } +/// # +/// # struct User; +/// # impl FromId<()> for User { +/// # type Error = (); +/// # type Object = Person; +/// # +/// # fn from_db(_: &(), _id: &str) -> Result { +/// # Ok(User) +/// # } +/// # +/// # fn from_activity(_: &(), obj: Person) -> Result { +/// # Ok(User) +/// # } +/// # +/// # fn get_sender() -> &'static dyn Signer { +/// # &*MY_SIGNER +/// # } +/// # } +/// # impl AsActor<&()> for User { +/// # fn get_inbox_url(&self) -> String { +/// # String::new() +/// # } +/// # fn is_local(&self) -> bool { false } +/// # } +/// # struct Message; +/// # impl FromId<()> for Message { +/// # type Error = (); +/// # type Object = Note; +/// # +/// # fn from_db(_: &(), _id: &str) -> Result { +/// # Ok(Message) +/// # } +/// # +/// # fn from_activity(_: &(), obj: Note) -> Result { +/// # Ok(Message) +/// # } +/// # +/// # fn get_sender() -> &'static dyn Signer { +/// # &*MY_SIGNER +/// # } +/// # } +/// # impl AsObject for Message { +/// # type Error = (); +/// # type Output = (); +/// # +/// # fn activity(self, _: &(), _actor: User, _id: &str) -> Result<(), ()> { +/// # Ok(()) +/// # } +/// # } +/// # impl AsObject for Message { +/// # type Error = (); +/// # type Output = (); +/// # +/// # fn activity(self, _: &(), _actor: User, _id: &str) -> Result<(), ()> { +/// # Ok(()) +/// # } +/// # } +/// # +/// # let mut act = Create::default(); +/// # act.object_props.set_id_string(String::from("https://test.ap/activity")).unwrap(); +/// # let mut person = Person::default(); +/// # person.object_props.set_id_string(String::from("https://test.ap/actor")).unwrap(); +/// # act.create_props.set_actor_object(person).unwrap(); +/// # act.create_props.set_object_object(Note::default()).unwrap(); +/// # let activity_json = serde_json::to_value(act).unwrap(); +/// # +/// # let conn = (); +/// # +/// let result: Result<(), ()> = Inbox::handle(&conn, activity_json) +/// .with::(None) +/// .with::(None) +/// .done(); +/// ``` +pub enum Inbox<'a, C, E, R> +where + E: From> + Debug, +{ + /// The activity has not been handled yet + /// + /// # Structure + /// + /// - the context to be passed to each handler. + /// - the activity + /// - the reason it has not been handled yet + NotHandled(&'a C, serde_json::Value, InboxError), + + /// A matching handler have been found but failed + /// + /// The wrapped value is the error returned by the handler + Failed(E), + + /// The activity was successfully handled + /// + /// The wrapped value is the value returned by the handler + Handled(R), +} + +/// Possible reasons of inbox failure +#[derive(Debug)] +pub enum InboxError { + /// None of the registered handlers matched + NoMatch, + + /// No ID was provided for the incoming activity, or it was not a string + InvalidID, + + /// The activity type matched for at least one handler, but then the actor was + /// not of the expected type + InvalidActor(Option), + + /// Activity and Actor types matched, but not the Object + InvalidObject(Option), + + /// Error while dereferencing the object + DerefError, +} + +impl From> for () { + fn from(_: InboxError) {} +} + +/* + Type arguments: + - C: Context + - E: Error + - R: Result +*/ +impl<'a, C, E, R> Inbox<'a, C, E, R> +where + E: From> + Debug, +{ + /// Creates a new `Inbox` to handle an incoming activity. + /// + /// # Parameters + /// + /// - `ctx`: the context to pass to each handler + /// - `json`: the JSON representation of the incoming activity + pub fn handle(ctx: &'a C, json: serde_json::Value) -> Inbox<'a, C, E, R> { + Inbox::NotHandled(ctx, json, InboxError::NoMatch) + } + + /// Registers an handler on this Inbox. + pub fn with(self, proxy: Option<&reqwest::Proxy>) -> Inbox<'a, C, E, R> + where + A: AsActor<&'a C> + FromId, + V: activitypub::Activity, + M: AsObject + FromId, + M::Output: Into, + { + if let Inbox::NotHandled(ctx, mut act, e) = self { + if serde_json::from_value::(act.clone()).is_ok() { + let act_clone = act.clone(); + let act_id = match act_clone["id"].as_str() { + Some(x) => x, + None => return Inbox::NotHandled(ctx, act, InboxError::InvalidID), + }; + + // Get the actor ID + let actor_id = match get_id(act["actor"].clone()) { + Some(x) => x, + None => return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(None)), + }; + + if Self::is_spoofed_activity(&actor_id, &act) { + return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(None)); + } + + // Transform this actor to a model (see FromId for details about the from_id function) + let actor = match A::from_id( + ctx, + &actor_id, + serde_json::from_value(act["actor"].clone()).ok(), + proxy, + ) { + Ok(a) => a, + // If the actor was not found, go to the next handler + Err((json, e)) => { + if let Some(json) = json { + act["actor"] = json; + } + return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(Some(e))); + } + }; + + // Same logic for "object" + let obj_id = match get_id(act["object"].clone()) { + Some(x) => x, + None => return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(None)), + }; + let obj = match M::from_id( + ctx, + &obj_id, + serde_json::from_value(act["object"].clone()).ok(), + proxy, + ) { + Ok(o) => o, + Err((json, e)) => { + if let Some(json) = json { + act["object"] = json; + } + return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(Some(e))); + } + }; + + // Handle the activity + match obj.activity(ctx, actor, act_id) { + Ok(res) => Inbox::Handled(res.into()), + Err(e) => Inbox::Failed(e), + } + } else { + // If the Activity type is not matching the expected one for + // this handler, try with the next one. + Inbox::NotHandled(ctx, act, e) + } + } else { + self + } + } + + /// Transforms the inbox in a `Result` + pub fn done(self) -> Result { + match self { + Inbox::Handled(res) => Ok(res), + Inbox::NotHandled(_, _, err) => Err(E::from(err)), + Inbox::Failed(err) => Err(err), + } + } + + fn is_spoofed_activity(actor_id: &str, act: &serde_json::Value) -> bool { + use serde_json::Value::{Array, Object, String}; + + let attributed_to = act["object"].get("attributedTo"); + if attributed_to.is_none() { + return false; + } + let attributed_to = attributed_to.unwrap(); + match attributed_to { + Array(v) => v.iter().all(|i| match i { + String(s) => s != actor_id, + Object(obj) => obj.get("id").map_or(true, |s| s != actor_id), + _ => false, + }), + String(s) => s != actor_id, + Object(obj) => obj.get("id").map_or(true, |s| s != actor_id), + _ => false, + } + } +} + +/// Get the ActivityPub ID of a JSON value. +/// +/// If the value is a string, its value is returned. +/// If it is an object, and that its `id` field is a string, we return it. +/// +/// Otherwise, `None` is returned. +fn get_id(json: serde_json::Value) -> Option { + match json { + serde_json::Value::String(s) => Some(s), + serde_json::Value::Object(map) => map.get("id")?.as_str().map(ToString::to_string), + _ => None, + } +} + +/// A trait for ActivityPub objects that can be retrieved or constructed from ID. +/// +/// The two functions to implement are `from_activity` to create (and save) a new object +/// of this type from its AP representation, and `from_db` to try to find it in the database +/// using its ID. +/// +/// When dealing with the "object" field of incoming activities, `Inbox` will try to see if it is +/// a full object, and if so, save it with `from_activity`. If it is only an ID, it will try to find +/// it in the database with `from_db`, and otherwise dereference (fetch) the full object and parse it +/// with `from_activity`. +pub trait FromId: Sized { + /// The type representing a failure + type Error: From> + Debug; + + /// The ActivityPub object type representing Self + type Object: activitypub::Object; + + /// Tries to get an instance of `Self` from an ActivityPub ID. + /// + /// # Parameters + /// + /// - `ctx`: a context to get this instance (= a database in which to search) + /// - `id`: the ActivityPub ID of the object to find + /// - `object`: optional object that will be used if the object was not found in the database + /// If absent, the ID will be dereferenced. + fn from_id( + ctx: &C, + id: &str, + object: Option, + proxy: Option<&reqwest::Proxy>, + ) -> Result, Self::Error)> { + match Self::from_db(ctx, id) { + Ok(x) => Ok(x), + _ => match object { + Some(o) => Self::from_activity(ctx, o).map_err(|e| (None, e)), + None => Self::from_activity(ctx, Self::deref(id, proxy.cloned())?) + .map_err(|e| (None, e)), + }, + } + } + + /// Dereferences an ID + fn deref( + id: &str, + proxy: Option, + ) -> Result, Self::Error)> { + request::get(id, Self::get_sender(), proxy) + .map_err(|_| (None, InboxError::DerefError)) + .and_then(|mut r| { + let json: serde_json::Value = r + .json() + .map_err(|_| (None, InboxError::InvalidObject(None)))?; + serde_json::from_value(json.clone()) + .map_err(|_| (Some(json), InboxError::InvalidObject(None))) + }) + .map_err(|(json, e)| (json, e.into())) + } + + /// Builds a `Self` from its ActivityPub representation + fn from_activity(ctx: &C, activity: Self::Object) -> Result; + + /// Tries to find a `Self` with a given ID (`id`), using `ctx` (a database) + fn from_db(ctx: &C, id: &str) -> Result; + + fn get_sender() -> &'static dyn Signer; +} + +/// Should be implemented by anything representing an ActivityPub actor. +/// +/// # Type arguments +/// +/// - `C`: the context to be passed to this activity handler from the `Inbox` (usually a database connection) +pub trait AsActor { + /// Return the URL of this actor's inbox + fn get_inbox_url(&self) -> String; + + /// If this actor has shared inbox, its URL should be returned by this function + fn get_shared_inbox_url(&self) -> Option { + None + } + + /// `true` if this actor comes from the running ActivityPub server/instance + fn is_local(&self) -> bool; +} + +/// Should be implemented by anything representing an ActivityPub object. +/// +/// # Type parameters +/// +/// - `A`: the actor type +/// - `V`: the ActivityPub verb/activity +/// - `O`: the ActivityPub type of the Object for this activity (usually the type corresponding to `Self`) +/// - `C`: the context needed to handle the activity (usually a database connection) +/// +/// # Example +/// +/// An implementation of AsObject that handles Note creation by an Account model, +/// representing the Note by a Message type, without any specific context. +/// +/// ```rust +/// # extern crate activitypub; +/// # use activitypub::{activity::Create, actor::Person, object::Note}; +/// # use plume_common::activity_pub::inbox::{AsActor, AsObject, FromId}; +/// # use plume_common::activity_pub::sign::{gen_keypair, Error as SignError, Result as SignResult, Signer}; +/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa}; +/// # use once_cell::sync::Lazy; +/// # +/// # static MY_SIGNER: Lazy = Lazy::new(|| MySigner::new()); +/// # +/// # struct MySigner { +/// # public_key: String, +/// # private_key: String, +/// # } +/// # +/// # impl MySigner { +/// # fn new() -> Self { +/// # let (pub_key, priv_key) = gen_keypair(); +/// # Self { +/// # public_key: String::from_utf8(pub_key).unwrap(), +/// # private_key: String::from_utf8(priv_key).unwrap(), +/// # } +/// # } +/// # } +/// # +/// # impl Signer for MySigner { +/// # fn get_key_id(&self) -> String { +/// # "mysigner".into() +/// # } +/// # +/// # fn sign(&self, to_sign: &str) -> SignResult> { +/// # let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap()) +/// # .unwrap(); +/// # let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap(); +/// # signer.update(to_sign.as_bytes()).unwrap(); +/// # signer.sign_to_vec().map_err(|_| SignError()) +/// # } +/// # +/// # fn verify(&self, data: &str, signature: &[u8]) -> SignResult { +/// # let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap()) +/// # .unwrap(); +/// # let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap(); +/// # verifier.update(data.as_bytes()).unwrap(); +/// # verifier.verify(&signature).map_err(|_| SignError()) +/// # } +/// # } +/// # +/// # struct Account; +/// # impl FromId<()> for Account { +/// # type Error = (); +/// # type Object = Person; +/// # +/// # fn from_db(_: &(), _id: &str) -> Result { +/// # Ok(Account) +/// # } +/// # +/// # fn from_activity(_: &(), obj: Person) -> Result { +/// # Ok(Account) +/// # } +/// # +/// # fn get_sender() -> &'static dyn Signer { +/// # &*MY_SIGNER +/// # } +/// # } +/// # impl AsActor<()> for Account { +/// # fn get_inbox_url(&self) -> String { +/// # String::new() +/// # } +/// # fn is_local(&self) -> bool { false } +/// # } +/// #[derive(Debug)] +/// struct Message { +/// text: String, +/// } +/// +/// impl FromId<()> for Message { +/// type Error = (); +/// type Object = Note; +/// +/// fn from_db(_: &(), _id: &str) -> Result { +/// Ok(Message { text: "From DB".into() }) +/// } +/// +/// fn from_activity(_: &(), obj: Note) -> Result { +/// Ok(Message { text: obj.object_props.content_string().map_err(|_| ())? }) +/// } +/// +/// fn get_sender() -> &'static dyn Signer { +/// &*MY_SIGNER +/// } +/// } +/// +/// impl AsObject for Message { +/// type Error = (); +/// type Output = (); +/// +/// fn activity(self, _: (), _actor: Account, _id: &str) -> Result<(), ()> { +/// println!("New Note: {:?}", self); +/// Ok(()) +/// } +/// } +/// ``` +pub trait AsObject +where + V: activitypub::Activity, +{ + /// What kind of error is returned when something fails + type Error; + + /// What is returned by `AsObject::activity`, if anything is returned + type Output = (); + + /// Handle a specific type of activity dealing with this type of objects. + /// + /// The implementations should check that the actor is actually authorized + /// to perform this action. + /// + /// # Parameters + /// + /// - `self`: the object on which the activity acts + /// - `ctx`: the context passed to `Inbox::handle` + /// - `actor`: the actor who did this activity + /// - `id`: the ID of this activity + fn activity(self, ctx: C, actor: A, id: &str) -> Result; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::activity_pub::sign::{ + gen_keypair, Error as SignError, Result as SignResult, Signer, + }; + use activitypub::{activity::*, actor::Person, object::Note}; + use once_cell::sync::Lazy; + use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa}; + + static MY_SIGNER: Lazy = Lazy::new(|| MySigner::new()); + + struct MySigner { + public_key: String, + private_key: String, + } + + impl MySigner { + fn new() -> Self { + let (pub_key, priv_key) = gen_keypair(); + Self { + public_key: String::from_utf8(pub_key).unwrap(), + private_key: String::from_utf8(priv_key).unwrap(), + } + } + } + + impl Signer for MySigner { + fn get_key_id(&self) -> String { + "mysigner".into() + } + + fn sign(&self, to_sign: &str) -> SignResult> { + let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap()) + .unwrap(); + let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap(); + signer.update(to_sign.as_bytes()).unwrap(); + signer.sign_to_vec().map_err(|_| SignError()) + } + + fn verify(&self, data: &str, signature: &[u8]) -> SignResult { + let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap()) + .unwrap(); + let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap(); + verifier.update(data.as_bytes()).unwrap(); + verifier.verify(&signature).map_err(|_| SignError()) + } + } + + struct MyActor; + impl FromId<()> for MyActor { + type Error = (); + type Object = Person; + + fn from_db(_: &(), _id: &str) -> Result { + Ok(MyActor) + } + + fn from_activity(_: &(), _obj: Person) -> Result { + Ok(MyActor) + } + + fn get_sender() -> &'static dyn Signer { + &*MY_SIGNER + } + } + + impl AsActor<&()> for MyActor { + fn get_inbox_url(&self) -> String { + String::from("https://test.ap/my-actor/inbox") + } + + fn is_local(&self) -> bool { + false + } + } + + struct MyObject; + impl FromId<()> for MyObject { + type Error = (); + type Object = Note; + + fn from_db(_: &(), _id: &str) -> Result { + Ok(MyObject) + } + + fn from_activity(_: &(), _obj: Note) -> Result { + Ok(MyObject) + } + + fn get_sender() -> &'static dyn Signer { + &*MY_SIGNER + } + } + impl AsObject for MyObject { + type Error = (); + type Output = (); + + fn activity(self, _: &(), _actor: MyActor, _id: &str) -> Result { + println!("MyActor is creating a Note"); + Ok(()) + } + } + + impl AsObject for MyObject { + type Error = (); + type Output = (); + + fn activity(self, _: &(), _actor: MyActor, _id: &str) -> Result { + println!("MyActor is liking a Note"); + Ok(()) + } + } + + impl AsObject for MyObject { + type Error = (); + type Output = (); + + fn activity(self, _: &(), _actor: MyActor, _id: &str) -> Result { + println!("MyActor is deleting a Note"); + Ok(()) + } + } + + impl AsObject for MyObject { + type Error = (); + type Output = (); + + fn activity(self, _: &(), _actor: MyActor, _id: &str) -> Result { + println!("MyActor is announcing a Note"); + Ok(()) + } + } + + fn build_create() -> Create { + let mut act = Create::default(); + act.object_props + .set_id_string(String::from("https://test.ap/activity")) + .unwrap(); + let mut person = Person::default(); + person + .object_props + .set_id_string(String::from("https://test.ap/actor")) + .unwrap(); + act.create_props.set_actor_object(person).unwrap(); + let mut note = Note::default(); + note.object_props + .set_id_string(String::from("https://test.ap/note")) + .unwrap(); + act.create_props.set_object_object(note).unwrap(); + act + } + + #[test] + fn test_inbox_basic() { + let act = serde_json::to_value(build_create()).unwrap(); + let res: Result<(), ()> = Inbox::handle(&(), act) + .with::(None) + .done(); + assert!(res.is_ok()); + } + + #[test] + fn test_inbox_multi_handlers() { + let act = serde_json::to_value(build_create()).unwrap(); + let res: Result<(), ()> = Inbox::handle(&(), act) + .with::(None) + .with::(None) + .with::(None) + .with::(None) + .done(); + assert!(res.is_ok()); + } + + #[test] + fn test_inbox_failure() { + let act = serde_json::to_value(build_create()).unwrap(); + // Create is not handled by this inbox + let res: Result<(), ()> = Inbox::handle(&(), act) + .with::(None) + .with::(None) + .done(); + assert!(res.is_err()); + } + + struct FailingActor; + impl FromId<()> for FailingActor { + type Error = (); + type Object = Person; + + fn from_db(_: &(), _id: &str) -> Result { + Err(()) + } + + fn from_activity(_: &(), _obj: Person) -> Result { + Err(()) + } + + fn get_sender() -> &'static dyn Signer { + &*MY_SIGNER + } + } + impl AsActor<&()> for FailingActor { + fn get_inbox_url(&self) -> String { + String::from("https://test.ap/failing-actor/inbox") + } + + fn is_local(&self) -> bool { + false + } + } + + impl AsObject for MyObject { + type Error = (); + type Output = (); + + fn activity( + self, + _: &(), + _actor: FailingActor, + _id: &str, + ) -> Result { + println!("FailingActor is creating a Note"); + Ok(()) + } + } + + #[test] + fn test_inbox_actor_failure() { + let act = serde_json::to_value(build_create()).unwrap(); + + let res: Result<(), ()> = Inbox::handle(&(), act.clone()) + .with::(None) + .done(); + assert!(res.is_err()); + + let res: Result<(), ()> = Inbox::handle(&(), act.clone()) + .with::(None) + .with::(None) + .done(); + assert!(res.is_ok()); + } +} diff --git a/plume-common/src/activity_pub/mod.rs b/plume-common/src/activity_pub/mod.rs new file mode 100644 index 00000000000..23b2b5f4623 --- /dev/null +++ b/plume-common/src/activity_pub/mod.rs @@ -0,0 +1,263 @@ +use activitypub::{Activity, Link, Object}; +use array_tool::vec::Uniq; +use reqwest::{header::HeaderValue, r#async::ClientBuilder, Url}; +use rocket::{ + http::Status, + request::{FromRequest, Request}, + response::{Responder, Response}, + Outcome, +}; +use tokio::prelude::*; +use tracing::{debug, warn}; + +use self::sign::Signable; + +pub mod inbox; +pub mod request; +pub mod sign; + +pub const CONTEXT_URL: &str = "https://www.w3.org/ns/activitystreams"; +pub const PUBLIC_VISIBILITY: &str = "https://www.w3.org/ns/activitystreams#Public"; + +pub const AP_CONTENT_TYPE: &str = + r#"application/ld+json; profile="https://www.w3.org/ns/activitystreams""#; + +pub fn ap_accept_header() -> Vec<&'static str> { + vec![ + "application/ld+json; profile=\"https://w3.org/ns/activitystreams\"", + "application/ld+json;profile=\"https://w3.org/ns/activitystreams\"", + "application/activity+json", + "application/ld+json", + ] +} + +pub fn context() -> serde_json::Value { + json!([ + CONTEXT_URL, + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "movedTo": "as:movedTo", + "Hashtag": "as:Hashtag", + "ostatus":"http://ostatus.org#", + "atomUri":"ostatus:atomUri", + "inReplyToAtomUri":"ostatus:inReplyToAtomUri", + "conversation":"ostatus:conversation", + "toot":"http://joinmastodon.org/ns#", + "Emoji":"toot:Emoji", + "focalPoint": { + "@container":"@list", + "@id":"toot:focalPoint" + }, + "featured":"toot:featured" + } + ]) +} + +pub struct ActivityStream(T); + +impl ActivityStream { + pub fn new(t: T) -> ActivityStream { + ActivityStream(t) + } +} + +impl<'r, O: Object> Responder<'r> for ActivityStream { + fn respond_to(self, request: &Request<'_>) -> Result, Status> { + let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?; + json["@context"] = context(); + serde_json::to_string(&json).respond_to(request).map(|r| { + Response::build_from(r) + .raw_header("Content-Type", "application/activity+json") + .finalize() + }) + } +} + +#[derive(Clone)] +pub struct ApRequest; +impl<'a, 'r> FromRequest<'a, 'r> for ApRequest { + type Error = (); + + fn from_request(request: &'a Request<'r>) -> Outcome { + request + .headers() + .get_one("Accept") + .map(|header| { + header + .split(',') + .map(|ct| match ct.trim() { + // bool for Forward: true if found a valid Content-Type for Plume first (HTML), false otherwise + "application/ld+json; profile=\"https://w3.org/ns/activitystreams\"" + | "application/ld+json;profile=\"https://w3.org/ns/activitystreams\"" + | "application/activity+json" + | "application/ld+json" => Outcome::Success(ApRequest), + "text/html" => Outcome::Forward(true), + _ => Outcome::Forward(false), + }) + .fold(Outcome::Forward(false), |out, ct| { + if out.clone().forwarded().unwrap_or_else(|| out.is_success()) { + out + } else { + ct + } + }) + .map_forward(|_| ()) + }) + .unwrap_or(Outcome::Forward(())) + } +} +pub fn broadcast(sender: &S, act: A, to: Vec, proxy: Option) +where + S: sign::Signer, + A: Activity, + T: inbox::AsActor, +{ + let boxes = to + .into_iter() + .filter(|u| !u.is_local()) + .map(|u| { + u.get_shared_inbox_url() + .unwrap_or_else(|| u.get_inbox_url()) + }) + .collect::>() + .unique(); + + let mut act = serde_json::to_value(act).expect("activity_pub::broadcast: serialization error"); + act["@context"] = context(); + let signed = act + .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"); + for inbox in boxes { + let body = signed.to_string(); + let mut headers = request::headers(); + let url = Url::parse(&inbox); + if url.is_err() { + warn!("Inbox is invalid URL: {:?}", &inbox); + continue; + } + let url = url.unwrap(); + if !url.has_host() { + warn!("Inbox doesn't have host: {:?}", &inbox); + continue; + }; + let host_header_value = HeaderValue::from_str(url.host_str().expect("Unreachable")); + if host_header_value.is_err() { + warn!("Header value is invalid: {:?}", url.host_str()); + continue; + } + headers.insert("Host", host_header_value.unwrap()); + headers.insert("Digest", request::Digest::digest(&body)); + rt.spawn( + if let Some(proxy) = proxy.clone() { + ClientBuilder::new().proxy(proxy) + } else { + ClientBuilder::new() + } + .connect_timeout(std::time::Duration::from_secs(5)) + .build() + .expect("Can't build client") + .post(&inbox) + .headers(headers.clone()) + .header( + "Signature", + request::signature(sender, &headers, ("post", url.path(), url.query())) + .expect("activity_pub::broadcast: request signature error"), + ) + .body(body) + .send() + .and_then(move |r| { + if r.status().is_success() { + debug!("Successfully sent activity to inbox ({})", &inbox); + } else { + warn!("Error while sending to inbox ({:?})", &r) + } + r.into_body().concat2() + }) + .map(move |response| debug!("Response: \"{:?}\"\n", response)) + .map_err(|e| warn!("Error while sending to inbox ({:?})", e)), + ); + } + rt.run().unwrap(); +} + +#[derive(Shrinkwrap, Clone, Serialize, Deserialize)] +pub struct Id(String); + +impl Id { + pub fn new(id: impl ToString) -> Id { + Id(id.to_string()) + } +} + +impl AsRef for Id { + fn as_ref(&self) -> &str { + &self.0 + } +} + +pub trait IntoId { + fn into_id(self) -> Id; +} + +impl Link for Id {} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)] +#[serde(rename_all = "camelCase")] +pub struct ApSignature { + #[activitystreams(concrete(PublicKey), functional)] + pub public_key: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)] +#[serde(rename_all = "camelCase")] +pub struct PublicKey { + #[activitystreams(concrete(String), functional)] + pub id: Option, + + #[activitystreams(concrete(String), functional)] + pub owner: Option, + + #[activitystreams(concrete(String), functional)] + pub public_key_pem: Option, +} + +#[derive(Clone, Debug, Default, UnitString)] +#[activitystreams(Hashtag)] +pub struct HashtagType; + +#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)] +#[serde(rename_all = "camelCase")] +pub struct Hashtag { + #[serde(rename = "type")] + kind: HashtagType, + + #[activitystreams(concrete(String), functional)] + pub href: Option, + + #[activitystreams(concrete(String), functional)] + pub name: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Source { + pub media_type: String, + + pub content: String, +} + +impl Object for Source {} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)] +#[serde(rename_all = "camelCase")] +pub struct Licensed { + #[activitystreams(concrete(String), functional)] + pub license: Option, +} + +impl Object for Licensed {} diff --git a/plume-common/src/activity_pub/request.rs b/plume-common/src/activity_pub/request.rs new file mode 100644 index 00000000000..4258bd7b080 --- /dev/null +++ b/plume-common/src/activity_pub/request.rs @@ -0,0 +1,269 @@ +use chrono::{offset::Utc, DateTime}; +use openssl::hash::{Hasher, MessageDigest}; +use reqwest::{ + header::{ + HeaderMap, HeaderValue, InvalidHeaderValue, ACCEPT, CONTENT_TYPE, DATE, HOST, USER_AGENT, + }, + ClientBuilder, Proxy, Response, Url, UrlError, +}; +use std::ops::Deref; +use std::time::SystemTime; +use tracing::warn; + +use crate::activity_pub::sign::Signer; +use crate::activity_pub::{ap_accept_header, AP_CONTENT_TYPE}; + +const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION")); + +#[derive(Debug)] +pub struct Error(); + +impl From for Error { + fn from(_err: UrlError) -> Self { + Error() + } +} + +impl From for Error { + fn from(_err: InvalidHeaderValue) -> Self { + Error() + } +} + +impl From for Error { + fn from(_err: reqwest::Error) -> Self { + Error() + } +} + +pub struct Digest(String); + +impl Digest { + pub fn digest(body: &str) -> HeaderValue { + let mut hasher = + Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error"); + hasher + .update(body.as_bytes()) + .expect("Digest::digest: content insertion error"); + let res = base64::encode(&hasher.finish().expect("Digest::digest: finalizing error")); + HeaderValue::from_str(&format!("SHA-256={}", res)) + .expect("Digest::digest: header creation error") + } + + pub fn verify(&self, body: &str) -> bool { + if self.algorithm() == "SHA-256" { + let mut hasher = + Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error"); + hasher + .update(body.as_bytes()) + .expect("Digest::digest: content insertion error"); + self.value().deref() + == hasher + .finish() + .expect("Digest::digest: finalizing error") + .deref() + } else { + false //algorithm not supported + } + } + + pub fn verify_header(&self, other: &Digest) -> bool { + self.value() == other.value() + } + + pub fn algorithm(&self) -> &str { + let pos = self + .0 + .find('=') + .expect("Digest::algorithm: invalid header error"); + &self.0[..pos] + } + + pub fn value(&self) -> Vec { + let pos = self + .0 + .find('=') + .expect("Digest::value: invalid header error") + + 1; + base64::decode(&self.0[pos..]).expect("Digest::value: invalid encoding error") + } + + pub fn from_header(dig: &str) -> Result { + if let Some(pos) = dig.find('=') { + let pos = pos + 1; + if base64::decode(&dig[pos..]).is_ok() { + Ok(Digest(dig.to_owned())) + } else { + Err(Error()) + } + } else { + Err(Error()) + } + } + + pub fn from_body(body: &str) -> Self { + let mut hasher = + Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error"); + hasher + .update(body.as_bytes()) + .expect("Digest::digest: content insertion error"); + let res = base64::encode(&hasher.finish().expect("Digest::digest: finalizing error")); + Digest(format!("SHA-256={}", res)) + } +} + +pub fn headers() -> HeaderMap { + let date: DateTime = SystemTime::now().into(); + let date = format!("{}", date.format("%a, %d %b %Y %T GMT")); + + let mut headers = HeaderMap::new(); + headers.insert(USER_AGENT, HeaderValue::from_static(PLUME_USER_AGENT)); + headers.insert( + DATE, + HeaderValue::from_str(&date).expect("request::headers: date error"), + ); + headers.insert( + ACCEPT, + HeaderValue::from_str( + &ap_accept_header() + .into_iter() + .collect::>() + .join(", "), + ) + .expect("request::headers: accept error"), + ); + headers.insert(CONTENT_TYPE, HeaderValue::from_static(AP_CONTENT_TYPE)); + headers +} + +type Method<'a> = &'a str; +type Path<'a> = &'a str; +type Query<'a> = &'a str; +type RequestTarget<'a> = (Method<'a>, Path<'a>, Option>); + +pub fn signature( + signer: &dyn Signer, + headers: &HeaderMap, + request_target: RequestTarget, +) -> Result { + let (method, path, query) = request_target; + let origin_form = if let Some(query) = query { + format!("{}?{}", path, query) + } else { + path.to_string() + }; + + let mut headers_vec = Vec::with_capacity(headers.len()); + for (h, v) in headers.iter() { + let v = v.to_str(); + if v.is_err() { + warn!("invalid header error: {:?}", v.unwrap_err()); + return Err(Error()); + } + headers_vec.push((h.as_str().to_lowercase(), v.expect("Unreachable"))); + } + let request_target = format!("{} {}", method.to_lowercase(), origin_form); + headers_vec.push(("(request-target)".to_string(), &request_target)); + + let signed_string = headers_vec + .iter() + .map(|(h, v)| format!("{}: {}", h, v)) + .collect::>() + .join("\n"); + let signed_headers = headers_vec + .iter() + .map(|(h, _)| h.as_ref()) + .collect::>() + .join(" "); + + let data = signer.sign(&signed_string).map_err(|_| Error())?; + let sign = base64::encode(&data); + + HeaderValue::from_str(&format!( + "keyId=\"{key_id}\",algorithm=\"rsa-sha256\",headers=\"{signed_headers}\",signature=\"{signature}\"", + key_id = signer.get_key_id(), + signed_headers = signed_headers, + signature = sign + )).map_err(|_| Error()) +} + +pub fn get(url_str: &str, sender: &dyn Signer, proxy: Option) -> Result { + let mut headers = headers(); + let url = Url::parse(url_str)?; + if !url.has_host() { + return Err(Error()); + } + let host_header_value = HeaderValue::from_str(url.host_str().expect("Unreachable"))?; + headers.insert(HOST, host_header_value); + if let Some(proxy) = proxy { + ClientBuilder::new().proxy(proxy) + } else { + ClientBuilder::new() + } + .connect_timeout(Some(std::time::Duration::from_secs(5))) + .build()? + .get(url_str) + .headers(headers.clone()) + .header( + "Signature", + signature(sender, &headers, ("get", url.path(), url.query()))?, + ) + .send() + .map_err(|_| Error()) +} + +#[cfg(test)] +mod tests { + use super::signature; + use crate::activity_pub::sign::{gen_keypair, Error, Result, Signer}; + use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa}; + use reqwest::header::HeaderMap; + + struct MySigner { + public_key: String, + private_key: String, + } + + impl MySigner { + fn new() -> Self { + let (pub_key, priv_key) = gen_keypair(); + Self { + public_key: String::from_utf8(pub_key).unwrap(), + private_key: String::from_utf8(priv_key).unwrap(), + } + } + } + + impl Signer for MySigner { + fn get_key_id(&self) -> String { + "mysigner".into() + } + + fn sign(&self, to_sign: &str) -> Result> { + let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap()) + .unwrap(); + let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap(); + signer.update(to_sign.as_bytes()).unwrap(); + signer.sign_to_vec().map_err(|_| Error()) + } + + fn verify(&self, data: &str, signature: &[u8]) -> Result { + let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap()) + .unwrap(); + let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap(); + verifier.update(data.as_bytes()).unwrap(); + verifier.verify(&signature).map_err(|_| Error()) + } + } + + #[test] + fn test_signature_request_target() { + let signer = MySigner::new(); + let headers = HeaderMap::new(); + let result = signature(&signer, &headers, ("post", "/inbox", None)).unwrap(); + let fields: Vec<&str> = result.to_str().unwrap().split(",").collect(); + assert_eq!(r#"headers="(request-target)""#, fields[2]); + let sign = &fields[3][11..(fields[3].len() - 1)]; + assert!(signer.verify("post /inbox", sign.as_bytes()).is_ok()); + } +} diff --git a/plume-common/src/activity_pub/sign.rs b/plume-common/src/activity_pub/sign.rs new file mode 100644 index 00000000000..22aaf9d6029 --- /dev/null +++ b/plume-common/src/activity_pub/sign.rs @@ -0,0 +1,214 @@ +use super::request; +use chrono::{naive::NaiveDateTime, DateTime, Duration, Utc}; +use openssl::{pkey::PKey, rsa::Rsa, sha::sha256}; +use rocket::http::HeaderMap; + +/// Returns (public key, private key) +pub fn gen_keypair() -> (Vec, Vec) { + let keypair = Rsa::generate(2048).expect("sign::gen_keypair: key generation error"); + let keypair = PKey::from_rsa(keypair).expect("sign::gen_keypair: parsing error"); + ( + keypair + .public_key_to_pem() + .expect("sign::gen_keypair: public key encoding error"), + keypair + .private_key_to_pem_pkcs8() + .expect("sign::gen_keypair: private key encoding error"), + ) +} + +#[derive(Debug)] +pub struct Error(); +pub type Result = std::result::Result; + +impl From for Error { + fn from(_: openssl::error::ErrorStack) -> Self { + Self() + } +} + +pub trait Signer { + fn get_key_id(&self) -> String; + + /// Sign some data with the signer keypair + fn sign(&self, to_sign: &str) -> Result>; + /// Verify if the signature is valid + fn verify(&self, data: &str, signature: &[u8]) -> Result; +} + +pub trait Signable { + fn sign(&mut self, creator: &T) -> Result<&mut Self> + where + T: Signer; + fn verify(self, creator: &T) -> bool + where + T: Signer; + + fn hash(data: &str) -> String { + let bytes = data.as_bytes(); + hex::encode(sha256(bytes)) + } +} + +impl Signable for serde_json::Value { + fn sign(&mut self, creator: &T) -> Result<&mut serde_json::Value> { + let creation_date = Utc::now().to_rfc3339(); + let mut options = json!({ + "type": "RsaSignature2017", + "creator": creator.get_key_id(), + "created": creation_date + }); + + let options_hash = Self::hash( + &json!({ + "@context": "https://w3id.org/identity/v1", + "created": creation_date + }) + .to_string(), + ); + let document_hash = Self::hash(&self.to_string()); + let to_be_signed = options_hash + &document_hash; + + let signature = base64::encode(&creator.sign(&to_be_signed).map_err(|_| Error())?); + + options["signatureValue"] = serde_json::Value::String(signature); + self["signature"] = options; + Ok(self) + } + + fn verify(mut self, creator: &T) -> bool { + let signature_obj = + if let Some(sig) = self.as_object_mut().and_then(|o| o.remove("signature")) { + sig + } else { + //signature not present + return false; + }; + let signature = if let Ok(sig) = + base64::decode(&signature_obj["signatureValue"].as_str().unwrap_or("")) + { + sig + } else { + return false; + }; + let creation_date = &signature_obj["created"]; + let options_hash = Self::hash( + &json!({ + "@context": "https://w3id.org/identity/v1", + "created": creation_date + }) + .to_string(), + ); + let creation_date = creation_date.as_str(); + if creation_date.is_none() { + return false; + } + let creation_date = DateTime::parse_from_rfc3339(creation_date.unwrap()); + if creation_date.is_err() { + return false; + } + let diff = creation_date.unwrap().signed_duration_since(Utc::now()); + let future = Duration::hours(12); + let past = Duration::hours(-12); + if !(diff < future && diff > past) { + return false; + } + let document_hash = Self::hash(&self.to_string()); + let to_be_signed = options_hash + &document_hash; + creator.verify(&to_be_signed, &signature).unwrap_or(false) + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum SignatureValidity { + Invalid, + ValidNoDigest, + Valid, + Absent, + Outdated, +} + +impl SignatureValidity { + pub fn is_secure(self) -> bool { + self == SignatureValidity::Valid + } +} + +pub fn verify_http_headers( + sender: &S, + all_headers: &HeaderMap<'_>, + data: &request::Digest, +) -> SignatureValidity { + let sig_header = all_headers.get_one("Signature"); + if sig_header.is_none() { + return SignatureValidity::Absent; + } + let sig_header = sig_header.expect("sign::verify_http_headers: unreachable"); + + let mut _key_id = None; + let mut _algorithm = None; + let mut headers = None; + let mut signature = None; + for part in sig_header.split(',') { + match part { + part if part.starts_with("keyId=") => _key_id = Some(&part[7..part.len() - 1]), + part if part.starts_with("algorithm=") => _algorithm = Some(&part[11..part.len() - 1]), + part if part.starts_with("headers=") => headers = Some(&part[9..part.len() - 1]), + part if part.starts_with("signature=") => signature = Some(&part[11..part.len() - 1]), + _ => {} + } + } + + if signature.is_none() || headers.is_none() { + //missing part of the header + return SignatureValidity::Invalid; + } + let headers = headers + .expect("sign::verify_http_headers: unreachable") + .split_whitespace() + .collect::>(); + let signature = signature.expect("sign::verify_http_headers: unreachable"); + let h = headers + .iter() + .map(|header| (header, all_headers.get_one(header))) + .map(|(header, value)| format!("{}: {}", header.to_lowercase(), value.unwrap_or(""))) + .collect::>() + .join("\n"); + + if !sender + .verify(&h, &base64::decode(signature).unwrap_or_default()) + .unwrap_or(false) + { + return SignatureValidity::Invalid; + } + if !headers.contains(&"digest") { + // signature is valid, but body content is not verified + return SignatureValidity::ValidNoDigest; + } + let digest = all_headers.get_one("digest").unwrap_or(""); + let digest = request::Digest::from_header(digest); + if !digest.map(|d| d.verify_header(data)).unwrap_or(false) { + // signature was valid, but body content does not match its digest + return SignatureValidity::Invalid; + } + if !headers.contains(&"date") { + return SignatureValidity::Valid; //maybe we shouldn't trust a request without date? + } + + let date = all_headers.get_one("date"); + if date.is_none() { + return SignatureValidity::Outdated; + } + let date = NaiveDateTime::parse_from_str(date.unwrap(), "%a, %d %h %Y %T GMT"); + if date.is_err() { + return SignatureValidity::Outdated; + } + let diff = Utc::now().naive_utc() - date.unwrap(); + let future = Duration::hours(12); + let past = Duration::hours(-12); + if diff < future && diff > past { + SignatureValidity::Valid + } else { + SignatureValidity::Outdated + } +} diff --git a/plume-common/src/lib.rs b/plume-common/src/lib.rs new file mode 100755 index 00000000000..878923d4ebd --- /dev/null +++ b/plume-common/src/lib.rs @@ -0,0 +1,13 @@ +#![feature(associated_type_defaults)] + +#[macro_use] +extern crate activitystreams_derive; +#[macro_use] +extern crate shrinkwraprs; +#[macro_use] +extern crate serde_derive; +#[macro_use] +extern crate serde_json; + +pub mod activity_pub; +pub mod utils; diff --git a/plume-common/src/utils.rs b/plume-common/src/utils.rs new file mode 100644 index 00000000000..17912a02936 --- /dev/null +++ b/plume-common/src/utils.rs @@ -0,0 +1,544 @@ +use heck::ToUpperCamelCase; +use openssl::rand::rand_bytes; +use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag}; +use regex_syntax::is_word_character; +use rocket::http::uri::Uri; +use std::collections::HashSet; +use syntect::html::{ClassStyle, ClassedHTMLGenerator}; +use syntect::parsing::SyntaxSet; + +/// Generates an hexadecimal representation of 32 bytes of random data +pub fn random_hex() -> String { + let mut bytes = [0; 32]; + rand_bytes(&mut bytes).expect("Error while generating client id"); + bytes + .iter() + .fold(String::new(), |res, byte| format!("{}{:x}", res, byte)) +} + +/// Remove non alphanumeric characters and CamelCase a string +pub fn make_actor_id(name: &str) -> String { + name.to_upper_camel_case() + .chars() + .filter(|c| c.is_alphanumeric()) + .collect() +} + +/** + * Percent-encode characters which are not allowed in IRI path segments. + * + * Intended to be used for generating Post ap_url. + */ +pub fn iri_percent_encode_seg(segment: &str) -> String { + segment.chars().map(iri_percent_encode_seg_char).collect() +} + +pub fn iri_percent_encode_seg_char(c: char) -> String { + if c.is_alphanumeric() { + c.to_string() + } else { + match c { + '-' + | '.' + | '_' + | '~' + | '\u{A0}'..='\u{D7FF}' + | '\u{20000}'..='\u{2FFFD}' + | '\u{30000}'..='\u{3FFFD}' + | '\u{40000}'..='\u{4FFFD}' + | '\u{50000}'..='\u{5FFFD}' + | '\u{60000}'..='\u{6FFFD}' + | '\u{70000}'..='\u{7FFFD}' + | '\u{80000}'..='\u{8FFFD}' + | '\u{90000}'..='\u{9FFFD}' + | '\u{A0000}'..='\u{AFFFD}' + | '\u{B0000}'..='\u{BFFFD}' + | '\u{C0000}'..='\u{CFFFD}' + | '\u{D0000}'..='\u{DFFFD}' + | '\u{E0000}'..='\u{EFFFD}' + | '!' + | '$' + | '&' + | '\'' + | '(' + | ')' + | '*' + | '+' + | ',' + | ';' + | '=' + | ':' + | '@' => c.to_string(), + _ => { + let s = c.to_string(); + Uri::percent_encode(&s).to_string() + } + } + } +} + +#[derive(Debug)] +enum State { + Mention, + Hashtag, + Word, + Ready, +} + +fn to_inline(tag: Tag<'_>) -> Tag<'_> { + match tag { + Tag::Heading(_) | Tag::Table(_) | Tag::TableHead | Tag::TableRow | Tag::TableCell => { + Tag::Paragraph + } + Tag::Image(typ, url, title) => Tag::Link(typ, url, title), + t => t, + } +} +struct HighlighterContext { + content: Vec, +} +#[allow(clippy::unnecessary_wraps)] +fn highlight_code<'a>( + context: &mut Option, + evt: Event<'a>, +) -> Option>> { + match evt { + Event::Start(Tag::CodeBlock(kind)) => { + match &kind { + CodeBlockKind::Fenced(lang) if !lang.is_empty() => { + *context = Some(HighlighterContext { content: vec![] }); + } + _ => {} + } + Some(vec![Event::Start(Tag::CodeBlock(kind))]) + } + Event::End(Tag::CodeBlock(kind)) => { + let mut result = vec![]; + if let Some(ctx) = context.take() { + let lang = if let CodeBlockKind::Fenced(lang) = &kind { + if lang.is_empty() { + unreachable!(); + } else { + lang + } + } else { + unreachable!(); + }; + let syntax_set = SyntaxSet::load_defaults_newlines(); + let syntax = syntax_set.find_syntax_by_token(lang).unwrap_or_else(|| { + syntax_set + .find_syntax_by_name(lang) + .unwrap_or_else(|| syntax_set.find_syntax_plain_text()) + }); + let mut html = ClassedHTMLGenerator::new_with_class_style( + syntax, + &syntax_set, + ClassStyle::Spaced, + ); + for line in ctx.content { + html.parse_html_for_line_which_includes_newline(&line); + } + let q = html.finalize(); + result.push(Event::Html(q.into())); + } + result.push(Event::End(Tag::CodeBlock(kind))); + *context = None; + Some(result) + } + Event::Text(t) => { + if let Some(mut c) = context.take() { + c.content.push(t.to_string()); + *context = Some(c); + Some(vec![]) + } else { + Some(vec![Event::Text(t)]) + } + } + _ => Some(vec![evt]), + } +} +#[allow(clippy::unnecessary_wraps)] +fn flatten_text<'a>(state: &mut Option, evt: Event<'a>) -> Option>> { + let (s, res) = match evt { + Event::Text(txt) => match state.take() { + Some(mut prev_txt) => { + prev_txt.push_str(&txt); + (Some(prev_txt), vec![]) + } + None => (Some(txt.into_string()), vec![]), + }, + e => match state.take() { + Some(prev) => (None, vec![Event::Text(CowStr::Boxed(prev.into())), e]), + None => (None, vec![e]), + }, + }; + *state = s; + Some(res) +} + +#[allow(clippy::unnecessary_wraps)] +fn inline_tags<'a>( + (state, inline): &mut (Vec>, bool), + evt: Event<'a>, +) -> Option> { + if *inline { + let new_evt = match evt { + Event::Start(t) => { + let tag = to_inline(t); + state.push(tag.clone()); + Event::Start(tag) + } + Event::End(t) => match state.pop() { + Some(other) => Event::End(other), + None => Event::End(t), + }, + e => e, + }; + Some(new_evt) + } else { + Some(evt) + } +} + +pub type MediaProcessor<'a> = Box Option<(String, Option)>>; + +fn process_image<'a, 'b>( + evt: Event<'a>, + inline: bool, + processor: &Option>, +) -> Event<'a> { + if let Some(ref processor) = *processor { + match evt { + Event::Start(Tag::Image(typ, id, title)) => { + if let Some((url, cw)) = id.parse::().ok().and_then(processor.as_ref()) { + if let (Some(cw), false) = (cw, inline) { + // there is a cw, and where are not inline + Event::Html(CowStr::Boxed( + format!( + r#""#, + )) + } + } else { + Event::End(Tag::Image(typ, id, title)) + } + } + e => e, + } + } else { + evt + } +} + +#[derive(Default, Debug)] +struct DocumentContext { + in_code: bool, + in_link: bool, +} + +/// Returns (HTML, mentions, hashtags) +pub fn md_to_html<'a>( + md: &str, + base_url: Option<&str>, + inline: bool, + media_processor: Option>, +) -> (String, HashSet, HashSet) { + let base_url = if let Some(base_url) = base_url { + format!("https://{}/", base_url) + } else { + "/".to_owned() + }; + let parser = Parser::new_ext(md, Options::all()); + + let (parser, mentions, hashtags): (Vec>, Vec, Vec) = parser + // Flatten text because pulldown_cmark break #hashtag in two individual text elements + .scan(None, flatten_text) + .flatten() + .scan(None, highlight_code) + .flatten() + .map(|evt| process_image(evt, inline, &media_processor)) + // Ignore headings, images, and tables if inline = true + .scan((vec![], inline), inline_tags) + .scan(&mut DocumentContext::default(), |ctx, evt| match evt { + Event::Start(Tag::CodeBlock(_)) => { + ctx.in_code = true; + Some((vec![evt], vec![], vec![])) + } + Event::End(Tag::CodeBlock(_)) => { + ctx.in_code = false; + Some((vec![evt], vec![], vec![])) + } + Event::Start(Tag::Link(_, _, _)) => { + ctx.in_link = true; + Some((vec![evt], vec![], vec![])) + } + Event::End(Tag::Link(_, _, _)) => { + ctx.in_link = false; + Some((vec![evt], vec![], vec![])) + } + Event::Text(txt) => { + let (evts, _, _, _, new_mentions, new_hashtags) = txt.chars().fold( + (vec![], State::Ready, String::new(), 0, vec![], vec![]), + |(mut events, state, mut text_acc, n, mut mentions, mut hashtags), c| { + match state { + State::Mention => { + let char_matches = c.is_alphanumeric() || "@.-_".contains(c); + if char_matches && (n < (txt.chars().count() - 1)) { + text_acc.push(c); + (events, State::Mention, text_acc, n + 1, mentions, hashtags) + } else { + if char_matches { + text_acc.push(c) + } + let mention = text_acc; + let link = Tag::Link( + LinkType::Inline, + format!("{}@/{}/", base_url, &mention).into(), + mention.clone().into(), + ); + + mentions.push(mention.clone()); + events.push(Event::Start(link.clone())); + events.push(Event::Text(format!("@{}", &mention).into())); + events.push(Event::End(link)); + + ( + events, + State::Ready, + c.to_string(), + n + 1, + mentions, + hashtags, + ) + } + } + State::Hashtag => { + let char_matches = c == '-' || is_word_character(c); + if char_matches && (n < (txt.chars().count() - 1)) { + text_acc.push(c); + (events, State::Hashtag, text_acc, n + 1, mentions, hashtags) + } else { + if char_matches { + text_acc.push(c); + } + let hashtag = text_acc; + let link = Tag::Link( + LinkType::Inline, + format!("{}tag/{}", base_url, &hashtag).into(), + hashtag.to_owned().into(), + ); + + hashtags.push(hashtag.clone()); + events.push(Event::Start(link.clone())); + events.push(Event::Text(format!("#{}", &hashtag).into())); + events.push(Event::End(link)); + + ( + events, + State::Ready, + c.to_string(), + n + 1, + mentions, + hashtags, + ) + } + } + State::Ready => { + if !ctx.in_code && !ctx.in_link && c == '@' { + events.push(Event::Text(text_acc.into())); + ( + events, + State::Mention, + String::new(), + n + 1, + mentions, + hashtags, + ) + } else if !ctx.in_code && !ctx.in_link && c == '#' { + events.push(Event::Text(text_acc.into())); + ( + events, + State::Hashtag, + String::new(), + n + 1, + mentions, + hashtags, + ) + } else if c.is_alphanumeric() { + text_acc.push(c); + if n >= (txt.chars().count() - 1) { + // Add the text after at the end, even if it is not followed by a mention. + events.push(Event::Text(text_acc.clone().into())) + } + (events, State::Word, text_acc, n + 1, mentions, hashtags) + } else { + text_acc.push(c); + if n >= (txt.chars().count() - 1) { + // Add the text after at the end, even if it is not followed by a mention. + events.push(Event::Text(text_acc.clone().into())) + } + (events, State::Ready, text_acc, n + 1, mentions, hashtags) + } + } + State::Word => { + text_acc.push(c); + if c.is_alphanumeric() { + if n >= (txt.chars().count() - 1) { + // Add the text after at the end, even if it is not followed by a mention. + events.push(Event::Text(text_acc.clone().into())) + } + (events, State::Word, text_acc, n + 1, mentions, hashtags) + } else { + if n >= (txt.chars().count() - 1) { + // Add the text after at the end, even if it is not followed by a mention. + events.push(Event::Text(text_acc.clone().into())) + } + (events, State::Ready, text_acc, n + 1, mentions, hashtags) + } + } + } + }, + ); + Some((evts, new_mentions, new_hashtags)) + } + _ => Some((vec![evt], vec![], vec![])), + }) + .fold( + (vec![], vec![], vec![]), + |(mut parser, mut mention, mut hashtag), (mut p, mut m, mut h)| { + parser.append(&mut p); + mention.append(&mut m); + hashtag.append(&mut h); + (parser, mention, hashtag) + }, + ); + let parser = parser.into_iter(); + let mentions = mentions.into_iter().map(|m| String::from(m.trim())); + let hashtags = hashtags.into_iter().map(|h| String::from(h.trim())); + + // TODO: fetch mentionned profiles in background, if needed + + let mut buf = String::new(); + html::push_html(&mut buf, parser); + (buf, mentions.collect(), hashtags.collect()) +} + +pub fn escape(string: &str) -> askama_escape::Escaped { + askama_escape::escape(string, askama_escape::Html) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mentions() { + let tests = vec![ + ("nothing", vec![]), + ("@mention", vec!["mention"]), + ("@mention@instance.tld", vec!["mention@instance.tld"]), + ("@many @mentions", vec!["many", "mentions"]), + ("@start with a mentions", vec!["start"]), + ("mention at @end", vec!["end"]), + ("between parenthesis (@test)", vec!["test"]), + ("with some punctuation @test!", vec!["test"]), + (" @spaces ", vec!["spaces"]), + ("@is_a@mention", vec!["is_a@mention"]), + ("not_a@mention", vec![]), + ("`@helo`", vec![]), + ("```\n@hello\n```", vec![]), + ("[@atmark in link](https://example.org/)", vec![]), + ]; + + for (md, mentions) in tests { + assert_eq!( + md_to_html(md, None, false, None).1, + mentions + .into_iter() + .map(|s| s.to_string()) + .collect::>() + ); + } + } + + #[test] + fn test_hashtags() { + let tests = vec![ + ("nothing", vec![]), + ("#hashtag", vec!["hashtag"]), + ("#many #hashtags", vec!["many", "hashtags"]), + ("#start with a hashtag", vec!["start"]), + ("hashtag at #end", vec!["end"]), + ("between parenthesis (#test)", vec!["test"]), + ("with some punctuation #test!", vec!["test"]), + (" #spaces ", vec!["spaces"]), + ("not_a#hashtag", vec![]), + ("#نرم‌افزار_آزاد", vec!["نرم‌افزار_آزاد"]), + ("[#hash in link](https://example.org/)", vec![]), + ("#zwsp\u{200b}inhash", vec!["zwsp"]), + ]; + + for (md, mentions) in tests { + assert_eq!( + md_to_html(md, None, false, None).2, + mentions + .into_iter() + .map(|s| s.to_string()) + .collect::>() + ); + } + } + + #[test] + fn test_iri_percent_encode_seg() { + assert_eq!( + &iri_percent_encode_seg("including whitespace"), + "including%20whitespace" + ); + assert_eq!(&iri_percent_encode_seg("%20"), "%2520"); + assert_eq!(&iri_percent_encode_seg("é"), "é"); + assert_eq!( + &iri_percent_encode_seg("空白入り 日本語"), + "空白入り%20日本語" + ); + } + + #[test] + fn test_inline() { + assert_eq!( + md_to_html("# Hello", None, false, None).0, + String::from("

Hello

\n") + ); + assert_eq!( + md_to_html("# Hello", None, true, None).0, + String::from("

Hello

\n") + ); + } +} diff --git a/plume-front/Cargo.toml b/plume-front/Cargo.toml new file mode 100644 index 00000000000..f26e9ad30ca --- /dev/null +++ b/plume-front/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "plume-front" +version = "0.7.1" +authors = ["Plume contributors"] +edition = "2018" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +gettext = "0.4.0" +gettext-macros = "0.6.1" +gettext-utils = "0.1.0" +lazy_static = "1.3" +serde = "1.0" +serde_json = "1.0" +wasm-bindgen = "0.2.70" +js-sys = "0.3.47" +serde_derive = "1.0.123" +console_error_panic_hook = "0.1.6" + +[dependencies.web-sys] +version = "0.3.47" +features = [ + 'console', + 'ClipboardEvent', + 'CssStyleDeclaration', + 'DataTransfer', + 'Document', + 'DomStringMap', + 'DomTokenList', + 'Element', + 'EventTarget', + 'FocusEvent', + 'History', + 'HtmlAnchorElement', + 'HtmlDocument', + 'HtmlFormElement', + 'HtmlInputElement', + 'HtmlSelectElement', + 'HtmlTextAreaElement', + 'KeyboardEvent', + 'Storage', + 'Location', + 'MouseEvent', + 'Navigator', + 'Node', + 'NodeList', + 'Text', + 'TouchEvent', + 'Window' +] diff --git a/plume-front/release.toml b/plume-front/release.toml new file mode 100644 index 00000000000..b927687cd89 --- /dev/null +++ b/plume-front/release.toml @@ -0,0 +1,2 @@ +pre-release-hook = ["cargo", "fmt"] +pre-release-replacements = [] diff --git a/plume-front/src/editor.rs b/plume-front/src/editor.rs new file mode 100644 index 00000000000..94bbbb596a8 --- /dev/null +++ b/plume-front/src/editor.rs @@ -0,0 +1,762 @@ +use crate::{document, CATALOG}; +use js_sys::{encode_uri_component, Date, RegExp}; +use serde_derive::{Deserialize, Serialize}; +use std::{convert::TryInto, sync::Mutex}; +use wasm_bindgen::{prelude::*, JsCast, JsValue}; +use web_sys::{ + console, window, ClipboardEvent, Element, Event, FocusEvent, HtmlAnchorElement, HtmlDocument, + HtmlElement, HtmlFormElement, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement, + KeyboardEvent, MouseEvent, Node, +}; + +macro_rules! mv { + ( $( $var:ident ),* => $exp:expr ) => { + { + $( let $var = $var.clone(); )* + $exp + } + } +} + +fn get_elt_value(id: &'static str) -> String { + let elt = document().get_element_by_id(id).unwrap(); + let inp: Option<&HtmlInputElement> = elt.dyn_ref(); + let textarea: Option<&HtmlTextAreaElement> = elt.dyn_ref(); + let select: Option<&HtmlSelectElement> = elt.dyn_ref(); + inp.map(|i| i.value()).unwrap_or_else(|| { + textarea + .map(|t| t.value()) + .unwrap_or_else(|| select.unwrap().value()) + }) +} + +fn set_value>(id: &'static str, val: S) { + let elt = document().get_element_by_id(id).unwrap(); + let inp: Option<&HtmlInputElement> = elt.dyn_ref(); + let textarea: Option<&HtmlTextAreaElement> = elt.dyn_ref(); + let select: Option<&HtmlSelectElement> = elt.dyn_ref(); + inp.map(|i| i.set_value(val.as_ref())).unwrap_or_else(|| { + textarea + .map(|t| t.set_value(val.as_ref())) + .unwrap_or_else(|| select.unwrap().set_value(val.as_ref())) + }) +} + +fn no_return(evt: KeyboardEvent) { + if evt.key() == "Enter" { + evt.prevent_default(); + } +} + +#[derive(Debug)] +pub enum EditorError { + NoneError, + DOMError, +} + +const AUTOSAVE_DEBOUNCE_TIME: i32 = 5000; +#[derive(Serialize, Deserialize)] +struct AutosaveInformation { + contents: String, + cover: String, + last_saved: f64, + license: String, + subtitle: String, + tags: String, + title: String, +} +fn is_basic_editor() -> bool { + if let Some(basic_editor) = window() + .unwrap() + .local_storage() + .unwrap() + .unwrap() + .get("basic-editor") + .unwrap() + { + &basic_editor == "true" + } else { + false + } +} +fn get_title() -> String { + if is_basic_editor() { + get_elt_value("title") + } else { + document() + .query_selector("#plume-editor > h1") + .unwrap() + .unwrap() + .dyn_ref::() + .unwrap() + .inner_text() + } +} +fn get_autosave_id() -> String { + format!( + "editor_contents={}", + window().unwrap().location().pathname().unwrap() + ) +} +fn get_editor_contents() -> String { + if is_basic_editor() { + get_elt_value("editor-content") + } else { + let editor = document().query_selector("article").unwrap().unwrap(); + let child_nodes = editor.child_nodes(); + let mut md = String::new(); + for i in 0..child_nodes.length() { + let ch = child_nodes.get(i).unwrap(); + let to_append = match ch.node_type() { + Node::ELEMENT_NODE => { + let elt = ch.dyn_ref::().unwrap(); + if elt.tag_name() == "DIV" { + elt.inner_html() + } else { + elt.outer_html() + } + } + Node::TEXT_NODE => ch.node_value().unwrap_or_default(), + _ => unreachable!(), + }; + md = format!("{}\n\n{}", md, to_append); + } + md + } +} +fn get_subtitle() -> String { + if is_basic_editor() { + get_elt_value("subtitle") + } else { + document() + .query_selector("#plume-editor > h2") + .unwrap() + .unwrap() + .dyn_ref::() + .unwrap() + .inner_text() + } +} +fn autosave() { + let info = AutosaveInformation { + contents: get_editor_contents(), + title: get_title(), + subtitle: get_subtitle(), + tags: get_elt_value("tags"), + license: get_elt_value("license"), + last_saved: Date::now(), + cover: get_elt_value("cover"), + }; + let id = get_autosave_id(); + match window() + .unwrap() + .local_storage() + .unwrap() + .unwrap() + .set(&id, &serde_json::to_string(&info).unwrap()) + { + Ok(_) => {} + _ => console::log_1(&"Autosave failed D:".into()), + } +} +fn load_autosave() { + if let Ok(Some(autosave_str)) = window() + .unwrap() + .local_storage() + .unwrap() + .unwrap() + .get(&get_autosave_id()) + { + let autosave_info: AutosaveInformation = serde_json::from_str(&autosave_str).ok().unwrap(); + let message = i18n!( + CATALOG, + "Do you want to load the local autosave last edited at {}?"; + Date::new(&JsValue::from_f64(autosave_info.last_saved)).to_date_string().as_string().unwrap() + ); + if let Ok(true) = window().unwrap().confirm_with_message(&message) { + set_value("editor-content", &autosave_info.contents); + set_value("title", &autosave_info.title); + set_value("subtitle", &autosave_info.subtitle); + set_value("tags", &autosave_info.tags); + set_value("license", &autosave_info.license); + set_value("cover", &autosave_info.cover); + } else { + clear_autosave(); + } + } +} +fn clear_autosave() { + window() + .unwrap() + .local_storage() + .unwrap() + .unwrap() + .remove_item(&get_autosave_id()) + .unwrap(); + console::log_1(&format!("Saved to {}", &get_autosave_id()).into()); +} +type TimeoutHandle = i32; +lazy_static! { + static ref AUTOSAVE_TIMEOUT: Mutex> = Mutex::new(None); +} +fn autosave_debounce() { + let window = window().unwrap(); + let timeout = &mut AUTOSAVE_TIMEOUT.lock().unwrap(); + if let Some(timeout) = timeout.take() { + window.clear_timeout_with_handle(timeout); + } + let callback = Closure::once(autosave); + **timeout = window + .set_timeout_with_callback_and_timeout_and_arguments_0( + callback.as_ref().unchecked_ref(), + AUTOSAVE_DEBOUNCE_TIME, + ) + .ok(); + callback.forget(); +} +fn init_widget( + parent: &Element, + tag: &'static str, + placeholder_text: String, + content: String, + disable_return: bool, +) -> Result { + let widget = placeholder( + make_editable(tag).dyn_into::().unwrap(), + &placeholder_text, + ); + if !content.is_empty() { + widget + .dataset() + .set("edited", "true") + .map_err(|_| EditorError::DOMError)?; + } + widget + .append_child(&document().create_text_node(&content)) + .map_err(|_| EditorError::DOMError)?; + if disable_return { + let callback = Closure::wrap(Box::new(no_return) as Box); + widget + .add_event_listener_with_callback("keydown", callback.as_ref().unchecked_ref()) + .map_err(|_| EditorError::DOMError)?; + callback.forget(); + } + + parent + .append_child(&widget) + .map_err(|_| EditorError::DOMError)?; + // We need to do that to make sure the placeholder is correctly rendered + widget.focus().map_err(|_| EditorError::DOMError)?; + widget.blur().map_err(|_| EditorError::DOMError)?; + + filter_paste(&widget); + + Ok(widget) +} + +fn filter_paste(elt: &HtmlElement) { + // Only insert text when pasting something + let insert_text = Closure::wrap(Box::new(|evt: ClipboardEvent| { + evt.prevent_default(); + if let Some(data) = evt.clipboard_data() { + if let Ok(data) = data.get_data("text") { + document() + .dyn_ref::() + .unwrap() + .exec_command_with_show_ui_and_value("insertText", false, &data) + .unwrap(); + } + } + }) as Box); + elt.add_event_listener_with_callback("paste", insert_text.as_ref().unchecked_ref()) + .unwrap(); + insert_text.forget(); +} + +pub fn init() -> Result<(), EditorError> { + if let Some(ed) = document().get_element_by_id("plume-fallback-editor") { + load_autosave(); + let callback = Closure::wrap(Box::new(|_| clear_autosave()) as Box); + ed.add_event_listener_with_callback("submit", callback.as_ref().unchecked_ref()) + .map_err(|_| EditorError::DOMError)?; + callback.forget(); + } + // Check if the user wants to use the basic editor + if window() + .unwrap() + .local_storage() + .unwrap() + .unwrap() + .get("basic-editor") + .map(|x| x.is_some() && x.unwrap() == "true") + .unwrap_or(true) + { + if let Some(editor) = document().get_element_by_id("plume-fallback-editor") { + if let Ok(Some(title_label)) = document().query_selector("label[for=title]") { + let editor_button = document() + .create_element("a") + .map_err(|_| EditorError::DOMError)?; + editor_button + .dyn_ref::() + .unwrap() + .set_href("#"); + let disable_basic_editor = Closure::wrap(Box::new(|_| { + let window = window().unwrap(); + if window + .local_storage() + .unwrap() + .unwrap() + .set("basic-editor", "false") + .is_err() + { + console::log_1(&"Failed to write into local storage".into()); + } + window.history().unwrap().go_with_delta(0).ok(); // refresh + }) + as Box); + editor_button + .add_event_listener_with_callback( + "click", + disable_basic_editor.as_ref().unchecked_ref(), + ) + .map_err(|_| EditorError::DOMError)?; + disable_basic_editor.forget(); + editor_button + .append_child( + &document().create_text_node(&i18n!(CATALOG, "Open the rich text editor")), + ) + .map_err(|_| EditorError::DOMError)?; + editor + .insert_before(&editor_button, Some(&title_label)) + .ok(); + let callback = Closure::wrap( + Box::new(|_| autosave_debounce()) as Box + ); + document() + .get_element_by_id("editor-content") + .unwrap() + .add_event_listener_with_callback("keydown", callback.as_ref().unchecked_ref()) + .map_err(|_| EditorError::DOMError)?; + callback.forget(); + } + } + + Ok(()) + } else { + init_editor() + } +} + +fn init_editor() -> Result<(), EditorError> { + if let Some(ed) = document().get_element_by_id("plume-editor") { + // Show the editor + ed.dyn_ref::() + .unwrap() + .style() + .set_property("display", "block") + .map_err(|_| EditorError::DOMError)?; + // And hide the HTML-only fallback + let old_ed = document().get_element_by_id("plume-fallback-editor"); + if old_ed.is_none() { + return Ok(()); + } + let old_ed = old_ed.unwrap(); + let old_title = document() + .get_element_by_id("plume-editor-title") + .ok_or(EditorError::NoneError)?; + old_ed + .dyn_ref::() + .unwrap() + .style() + .set_property("display", "none") + .map_err(|_| EditorError::DOMError)?; + old_title + .dyn_ref::() + .unwrap() + .style() + .set_property("display", "none") + .map_err(|_| EditorError::DOMError)?; + + // 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, + )?; + if !content_val.is_empty() { + content.set_inner_html(&content_val); + } + + // character counter + let character_counter = Closure::wrap(Box::new(mv!(content => move |_| { + let update_char_count = Closure::wrap(Box::new(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); + e.dyn_ref::().map(|e| { + e.set_inner_text(&text); + }).unwrap(); + }; + })) as Box); + window().unwrap().set_timeout_with_callback_and_timeout_and_arguments(update_char_count.as_ref().unchecked_ref(), 0, &js_sys::Array::new()).unwrap(); + update_char_count.forget(); + autosave_debounce(); + })) as Box); + content + .add_event_listener_with_callback("keydown", character_counter.as_ref().unchecked_ref()) + .map_err(|_| EditorError::DOMError)?; + character_counter.forget(); + + let show_popup = Closure::wrap(Box::new(mv!(title, subtitle, content, old_ed => move |_| { + 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(); + + popup.class_list().add_1("show").unwrap(); + bg.class_list().add_1("show").unwrap(); + })) as Box); + document() + .get_element_by_id("publish") + .ok_or(EditorError::NoneError)? + .add_event_listener_with_callback("click", show_popup.as_ref().unchecked_ref()) + .map_err(|_| EditorError::DOMError)?; + show_popup.forget(); + + show_errors(); + setup_close_button(); + } + Ok(()) +} + +fn setup_close_button() { + if let Some(button) = document().get_element_by_id("close-editor") { + let close_editor = Closure::wrap(Box::new(|_| { + window() + .unwrap() + .local_storage() + .unwrap() + .unwrap() + .set("basic-editor", "true") + .unwrap(); + window() + .unwrap() + .history() + .unwrap() + .go_with_delta(0) + .unwrap(); // Refresh the page + }) as Box); + button + .add_event_listener_with_callback("click", close_editor.as_ref().unchecked_ref()) + .unwrap(); + close_editor.forget(); + } +} + +fn show_errors() { + let document = document(); + if let Ok(Some(header)) = document.query_selector("header") { + let list = document.create_element("header").unwrap(); + list.class_list().add_1("messages").unwrap(); + let errors = document.query_selector_all("p.error").unwrap(); + for i in 0..errors.length() { + let error = errors.get(i).unwrap(); + error + .parent_element() + .unwrap() + .remove_child(&error) + .unwrap(); + let _ = list.append_child(&error); + } + header + .parent_element() + .unwrap() + .insert_before(&list, header.next_sibling().as_ref()) + .unwrap(); + } +} + +fn init_popup( + title: &HtmlElement, + subtitle: &HtmlElement, + content: &HtmlElement, + old_ed: &Element, +) -> Result { + let document = document(); + let popup = document + .create_element("div") + .map_err(|_| EditorError::DOMError)?; + popup + .class_list() + .add_1("popup") + .map_err(|_| EditorError::DOMError)?; + popup + .set_attribute("id", "publish-popup") + .map_err(|_| EditorError::DOMError)?; + + let tags = get_elt_value("tags") + .split(',') + .map(str::trim) + .map(str::to_string) + .collect::>(); + let license = get_elt_value("license"); + make_input(&i18n!(CATALOG, "Tags"), "popup-tags", &popup).set_value(&tags.join(", ")); + make_input(&i18n!(CATALOG, "License"), "popup-license", &popup).set_value(&license); + + let cover_label = document + .create_element("label") + .map_err(|_| EditorError::DOMError)?; + cover_label + .append_child(&document.create_text_node(&i18n!(CATALOG, "Cover"))) + .map_err(|_| EditorError::DOMError)?; + cover_label + .set_attribute("for", "cover") + .map_err(|_| EditorError::DOMError)?; + let cover = document + .get_element_by_id("cover") + .ok_or(EditorError::NoneError)?; + cover + .parent_element() + .ok_or(EditorError::NoneError)? + .remove_child(&cover) + .ok(); + popup + .append_child(&cover_label) + .map_err(|_| EditorError::DOMError)?; + popup + .append_child(&cover) + .map_err(|_| EditorError::DOMError)?; + + if let Some(draft_checkbox) = document.get_element_by_id("draft") { + let draft_checkbox = draft_checkbox.dyn_ref::().unwrap(); + let draft_label = document + .create_element("label") + .map_err(|_| EditorError::DOMError)?; + draft_label + .set_attribute("for", "popup-draft") + .map_err(|_| EditorError::DOMError)?; + + let draft = document.create_element("input").unwrap(); + draft.set_id("popup-draft"); + let draft = draft.dyn_ref::().unwrap(); + draft.set_name("popup-draft"); + draft.set_type("checkbox"); + draft.set_checked(draft_checkbox.checked()); + + draft_label + .append_child(draft) + .map_err(|_| EditorError::DOMError)?; + draft_label + .append_child(&document.create_text_node(&i18n!(CATALOG, "This is a draft"))) + .map_err(|_| EditorError::DOMError)?; + popup + .append_child(&draft_label) + .map_err(|_| EditorError::DOMError)?; + } + + let button = document + .create_element("input") + .map_err(|_| EditorError::DOMError)?; + button + .append_child(&document.create_text_node(&i18n!(CATALOG, "Publish"))) + .map_err(|_| EditorError::DOMError)?; + let button = button.dyn_ref::().unwrap(); + button.set_type("submit"); + button.set_value(&i18n!(CATALOG, "Publish")); + let callback = Closure::wrap(Box::new(mv!(title, subtitle, content, old_ed => move |_| { + let document = self::document(); + title.focus().unwrap(); // Remove the placeholder before publishing + set_value("title", title.inner_text()); + subtitle.focus().unwrap(); + set_value("subtitle", subtitle.inner_text()); + content.focus().unwrap(); + let mut md = String::new(); + let child_nodes = content.child_nodes(); + for i in 0..child_nodes.length() { + let ch = child_nodes.get(i).unwrap(); + let to_append = match ch.node_type() { + Node::ELEMENT_NODE => { + let ch = ch.dyn_ref::().unwrap(); + if ch.tag_name() == "DIV" { + ch.inner_html() + } else { + ch.outer_html() + } + }, + Node::TEXT_NODE => ch.node_value().unwrap_or_default(), + _ => unreachable!(), + }; + md = format!("{}\n\n{}", md, to_append); + } + set_value("editor-content", md); + set_value("tags", get_elt_value("popup-tags")); + if let Some(draft) = document.get_element_by_id("popup-draft") { + if let Some(draft_checkbox) = document.get_element_by_id("draft") { + let draft_checkbox = draft_checkbox.dyn_ref::().unwrap(); + let draft = draft.dyn_ref::().unwrap(); + draft_checkbox.set_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).unwrap(); + set_value("license", get_elt_value("popup-license")); + clear_autosave(); + let old_ed = old_ed.dyn_ref::().unwrap(); + old_ed.submit().unwrap(); + })) as Box); + button + .add_event_listener_with_callback("click", callback.as_ref().unchecked_ref()) + .map_err(|_| EditorError::DOMError)?; + callback.forget(); + popup + .append_child(button) + .map_err(|_| EditorError::DOMError)?; + + document + .body() + .ok_or(EditorError::NoneError)? + .append_child(&popup) + .map_err(|_| EditorError::DOMError)?; + Ok(popup) +} + +fn init_popup_bg() -> Result { + let bg = document() + .create_element("div") + .map_err(|_| EditorError::DOMError)?; + bg.class_list() + .add_1("popup-bg") + .map_err(|_| EditorError::DOMError)?; + bg.set_attribute("id", "popup-bg") + .map_err(|_| EditorError::DOMError)?; + + document() + .body() + .ok_or(EditorError::NoneError)? + .append_child(&bg) + .map_err(|_| EditorError::DOMError)?; + let callback = Closure::wrap(Box::new(|_| close_popup()) as Box); + bg.add_event_listener_with_callback("click", callback.as_ref().unchecked_ref()) + .unwrap(); + callback.forget(); + Ok(bg) +} + +fn chars_left(selector: &str, content: &HtmlElement) -> Option { + match document().query_selector(selector) { + Ok(Some(form)) => form.dyn_ref::().and_then(|form| { + if let Some(len) = form + .get_attribute("content-size") + .and_then(|s| s.parse::().ok()) + { + (encode_uri_component(&content.inner_html()) + .replace("%20", "+") + .replace("%0A", "%0D0A") + .replace_by_pattern(&RegExp::new("[!'*()]", "g"), "XXX") + .length() + + 2_u32) + .try_into() + .map(|c: i32| len - c) + .ok() + } else { + None + } + }), + _ => None, + } +} + +fn close_popup() { + let hide = |x: Element| x.class_list().remove_1("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) -> HtmlInputElement { + let document = document(); + let label = document.create_element("label").unwrap(); + label + .append_child(&document.create_text_node(label_text)) + .unwrap(); + label.set_attribute("for", name).unwrap(); + + let inp = document.create_element("input").unwrap(); + let inp = inp.dyn_into::().unwrap(); + inp.set_attribute("name", name).unwrap(); + inp.set_attribute("id", name).unwrap(); + + form.append_child(&label).unwrap(); + form.append_child(&inp).unwrap(); + 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().set("placeholder", text).unwrap(); + elt.dataset().set("edited", "false").unwrap(); + + let callback = Closure::wrap(Box::new(mv!(elt => move |_: FocusEvent| { + if elt.dataset().get("edited").unwrap().as_str() != "true" { + clear_children(&elt); + } + })) as Box); + elt.add_event_listener_with_callback("focus", callback.as_ref().unchecked_ref()) + .unwrap(); + callback.forget(); + let callback = Closure::wrap(Box::new(mv!(elt => move |_: Event| { + 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_1("placeholder").expect("Couldn't add class"); + ph.append_child(&document().create_text_node(&elt.dataset().get("placeholder").unwrap_or_default())).unwrap(); + elt.append_child(&ph).unwrap(); + } + })) as Box); + elt.add_event_listener_with_callback("blur", callback.as_ref().unchecked_ref()) + .unwrap(); + callback.forget(); + let callback = Closure::wrap(Box::new(mv!(elt => move |_: KeyboardEvent| { + elt.dataset().set("edited", if elt.inner_text().trim_matches('\n').is_empty() { + "false" + } else { + "true" + }).expect("Couldn't update edition state"); + })) as Box); + elt.add_event_listener_with_callback("keyup", callback.as_ref().unchecked_ref()) + .unwrap(); + callback.forget(); + elt +} + +fn clear_children(elt: &HtmlElement) { + let child_nodes = elt.child_nodes(); + for _ in 0..child_nodes.length() { + elt.remove_child(&child_nodes.get(0).unwrap()).unwrap(); + } +} diff --git a/plume-front/src/lib.rs b/plume-front/src/lib.rs new file mode 100755 index 00000000000..eeecb0ac127 --- /dev/null +++ b/plume-front/src/lib.rs @@ -0,0 +1,175 @@ +#![recursion_limit = "128"] +#![feature(decl_macro, proc_macro_hygiene)] + +#[macro_use] +extern crate gettext_macros; +#[macro_use] +extern crate lazy_static; + +use wasm_bindgen::{prelude::*, JsCast}; +use web_sys::{console, window, Document, Element, Event, HtmlInputElement, TouchEvent}; + +init_i18n!( + "plume-front", + af, + ar, + bg, + ca, + cs, + cy, + da, + de, + el, + en, + eo, + es, + eu, + fa, + fi, + fr, + gl, + he, + hi, + hr, + hu, + it, + ja, + ko, + nb, + nl, + no, + pl, + pt, + ro, + ru, + sat, + si, + sk, + sl, + sr, + sv, + tr, + uk, + vi, + zh +); + +mod editor; + +compile_i18n!(); + +lazy_static! { + static ref CATALOG: gettext::Catalog = { + let catalogs = include_i18n!(); + let lang = window().unwrap().navigator().language().unwrap(); + let lang = lang.split_once('-').map_or("en", |x| x.0); + + let english_position = catalogs + .iter() + .position(|(language_code, _)| *language_code == "en") + .unwrap(); + catalogs + .iter() + .find(|(l, _)| l == &lang) + .unwrap_or(&catalogs[english_position]) + .clone() + .1 + }; +} + +#[wasm_bindgen(start)] +pub fn main() -> Result<(), JsValue> { + extern crate console_error_panic_hook; + use std::panic; + panic::set_hook(Box::new(console_error_panic_hook::hook)); + + menu(); + search(); + editor::init() + .map_err(|e| console::error_1(&format!("Editor error: {:?}", e).into())) + .ok(); + Ok(()) +} + +/// Toggle menu on mobile devices +/// +/// It should normally be working fine even without this code +/// But :focus-within is not yet supported by Webkit/Blink +fn menu() { + let document = document(); + if let Ok(Some(button)) = document.query_selector("#menu a") { + if let Some(menu) = document.get_element_by_id("content") { + let show_menu = Closure::wrap(Box::new(|_: TouchEvent| { + self::document() + .get_element_by_id("menu") + .map(|menu| { + menu.set_attribute("aria-expanded", "true") + .map(|_| menu.class_list().add_1("show")) + }) + .unwrap() + .unwrap() + .unwrap(); + }) as Box); + button + .add_event_listener_with_callback("touchend", show_menu.as_ref().unchecked_ref()) + .unwrap(); + show_menu.forget(); + + let close_menu = Closure::wrap(Box::new(|evt: TouchEvent| { + if evt + .target() + .unwrap() + .dyn_ref::() + .unwrap() + .closest("a") + .unwrap() + .is_some() + { + return; + } + self::document() + .get_element_by_id("menu") + .map(|menu| { + menu.set_attribute("aria-expanded", "false") + .map(|_| menu.class_list().remove_1("show")) + }) + .unwrap() + .unwrap() + .unwrap() + }) as Box); + menu.add_event_listener_with_callback("touchend", close_menu.as_ref().unchecked_ref()) + .unwrap(); + close_menu.forget(); + } + } +} + +/// Clear the URL of the search page before submitting request +fn search() { + if let Some(form) = document().get_element_by_id("form") { + let normalize_query = Closure::wrap(Box::new(|_: Event| { + document() + .query_selector_all("#form input") + .map(|inputs| { + for i in 0..inputs.length() { + let input = inputs.get(i).unwrap(); + let input = input.dyn_ref::().unwrap(); + if input.name().is_empty() { + input.set_name(&input.dyn_ref::().unwrap().id()); + } + if !input.name().is_empty() && input.value().is_empty() { + input.set_name(""); + } + } + }) + .unwrap(); + }) as Box); + form.add_event_listener_with_callback("submit", normalize_query.as_ref().unchecked_ref()) + .unwrap(); + normalize_query.forget(); + } +} + +fn document() -> Document { + window().unwrap().document().unwrap() +} diff --git a/plume-macro/Cargo.toml b/plume-macro/Cargo.toml new file mode 100644 index 00000000000..9c9ac51cb94 --- /dev/null +++ b/plume-macro/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "plume-macro" +version = "0.7.1" +authors = ["Trinity Pointard "] +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 = [] diff --git a/plume-macro/release.toml b/plume-macro/release.toml new file mode 100644 index 00000000000..b927687cd89 --- /dev/null +++ b/plume-macro/release.toml @@ -0,0 +1,2 @@ +pre-release-hook = ["cargo", "fmt"] +pre-release-replacements = [] diff --git a/plume-macro/src/lib.rs b/plume-macro/src/lib.rs new file mode 100755 index 00000000000..b4d20cdd0f4 --- /dev/null +++ b/plume-macro/src/lib.rs @@ -0,0 +1,142 @@ +#![recursion_limit = "128"] + +#[macro_use] +extern crate quote; + +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::>(); + 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::(); + (name, up_sql, down_sql) + }) + .collect::>(); + let migrations_name = migrations.iter().map(|m| &m.0); + let migrations_up = migrations + .iter() + .map(|m| m.1.as_str()) + .map(file_to_migration) + .collect::>(); + let migrations_down = migrations + .iter() + .map(|m| m.2.as_str()) + .map(file_to_migration) + .collect::>(); + + /* + 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 let Some(acc_str) = line.strip_prefix("--#!") { + if !acc.trim().is_empty() { + actions.push(quote!(Action::Sql(#acc))); + } + sql = false; + acc = acc_str.to_string(); + acc.push('\n'); + } else if line.starts_with("--") { + continue; + } else { + acc.push_str(line); + acc.push('\n'); + } + } else if let Some(acc_str) = line.strip_prefix("--#!") { + acc.push_str(acc_str); + 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 + } + } +} diff --git a/plume-models/Cargo.toml b/plume-models/Cargo.toml new file mode 100644 index 00000000000..2aa591274d8 --- /dev/null +++ b/plume-models/Cargo.toml @@ -0,0 +1,63 @@ +[package] +name = "plume-models" +version = "0.7.1" +authors = ["Plume contributors"] +edition = "2018" + +[dependencies] +activitypub = "0.1.1" +ammonia = "3.1.4" +bcrypt = "0.12.1" +guid-create = "0.2" +itertools = "0.10.3" +lazy_static = "1.0" +ldap3 = "0.10.2" +migrations_internals= "1.4.0" +openssl = "0.10.22" +rocket = "0.4.6" +rocket_i18n = "0.4.1" +reqwest = "0.9" +scheduled-thread-pool = "0.2.2" +serde = "1.0" +serde_derive = "1.0" +serde_json = "1.0.79" +tantivy = "0.13.3" +url = "2.1" +walkdir = "2.2" +webfinger = "0.4.1" +whatlang = "0.13.0" +shrinkwraprs = "0.3.0" +diesel-derive-newtype = "0.1.2" +glob = "0.3.0" +lindera-tantivy = { version = "0.7.1", optional = true } +tracing = "0.1.32" +riker = "0.4.2" +once_cell = "1.10.0" +lettre = "0.9.6" +native-tls = "0.2.8" + +[dependencies.chrono] +features = ["serde"] +version = "0.4" + +[dependencies.diesel] +features = ["r2d2", "chrono"] +version = "1.4.5" + +[dependencies.plume-api] +path = "../plume-api" + +[dependencies.plume-common] +path = "../plume-common" + +[dependencies.plume-macro] +path = "../plume-macro" + +[dev-dependencies] +assert-json-diff = "2.0.1" +diesel_migrations = "1.3.0" + +[features] +postgres = ["diesel/postgres", "plume-macro/postgres" ] +sqlite = ["diesel/sqlite", "plume-macro/sqlite" ] +search-lindera = ["lindera-tantivy"] diff --git a/plume-models/release.toml b/plume-models/release.toml new file mode 100644 index 00000000000..b927687cd89 --- /dev/null +++ b/plume-models/release.toml @@ -0,0 +1,2 @@ +pre-release-hook = ["cargo", "fmt"] +pre-release-replacements = [] diff --git a/plume-models/src/admin.rs b/plume-models/src/admin.rs new file mode 100644 index 00000000000..a4fa04551c3 --- /dev/null +++ b/plume-models/src/admin.rs @@ -0,0 +1,38 @@ +use crate::users::User; +use rocket::{ + http::Status, + request::{self, FromRequest, Request}, + Outcome, +}; + +/// Wrapper around User to use as a request guard on pages reserved to admins. +pub struct Admin(pub User); + +impl<'a, 'r> FromRequest<'a, 'r> for Admin { + type Error = (); + + fn from_request(request: &'a Request<'r>) -> request::Outcome { + let user = request.guard::()?; + if user.is_admin() { + Outcome::Success(Admin(user)) + } else { + Outcome::Failure((Status::Unauthorized, ())) + } + } +} + +/// Same as `Admin` but for moderators. +pub struct Moderator(pub User); + +impl<'a, 'r> FromRequest<'a, 'r> for Moderator { + type Error = (); + + fn from_request(request: &'a Request<'r>) -> request::Outcome { + let user = request.guard::()?; + if user.is_moderator() { + Outcome::Success(Moderator(user)) + } else { + Outcome::Failure((Status::Unauthorized, ())) + } + } +} diff --git a/plume-models/src/api_tokens.rs b/plume-models/src/api_tokens.rs new file mode 100644 index 00000000000..dfff41351bb --- /dev/null +++ b/plume-models/src/api_tokens.rs @@ -0,0 +1,113 @@ +use crate::{db_conn::DbConn, schema::api_tokens, Error, Result}; +use chrono::NaiveDateTime; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; +use rocket::{ + http::Status, + request::{self, FromRequest, Request}, + Outcome, +}; + +#[derive(Clone, Queryable)] +pub struct ApiToken { + pub id: i32, + pub creation_date: NaiveDateTime, + pub value: String, + + /// Scopes, separated by + + /// Global scopes are read and write + /// and both can be limited to an endpoint by affixing them with :ENDPOINT + /// + /// Examples : + /// + /// read + /// read+write + /// read:posts + /// read:posts+write:posts + pub scopes: String, + pub app_id: i32, + pub user_id: i32, +} + +#[derive(Insertable)] +#[table_name = "api_tokens"] +pub struct NewApiToken { + pub value: String, + pub scopes: String, + pub app_id: i32, + pub user_id: i32, +} + +impl ApiToken { + get!(api_tokens); + insert!(api_tokens, NewApiToken); + find_by!(api_tokens, find_by_value, value as &str); + + pub fn can(&self, what: &'static str, scope: &'static str) -> bool { + let full_scope = what.to_owned() + ":" + scope; + for s in self.scopes.split('+') { + if s == what || s == full_scope { + return true; + } + } + false + } + + pub fn can_read(&self, scope: &'static str) -> bool { + self.can("read", scope) + } + + pub fn can_write(&self, scope: &'static str) -> bool { + self.can("write", scope) + } +} + +#[derive(Debug)] +pub enum TokenError { + /// The Authorization header was not present + NoHeader, + + /// The type of the token was not specified ("Basic" or "Bearer" for instance) + NoType, + + /// No value was provided + NoValue, + + /// Error while connecting to the database to retrieve all the token metadata + DbError, +} + +impl<'a, 'r> FromRequest<'a, 'r> for ApiToken { + type Error = TokenError; + + fn from_request(request: &'a Request<'r>) -> request::Outcome { + let headers: Vec<_> = request.headers().get("Authorization").collect(); + if headers.len() != 1 { + return Outcome::Failure((Status::BadRequest, TokenError::NoHeader)); + } + + let mut parsed_header = headers[0].split(' '); + let auth_type = parsed_header + .next() + .map_or_else::, _, _>( + || Outcome::Failure((Status::BadRequest, TokenError::NoType)), + Outcome::Success, + )?; + let val = parsed_header + .next() + .map_or_else::, _, _>( + || Outcome::Failure((Status::BadRequest, TokenError::NoValue)), + Outcome::Success, + )?; + + if auth_type == "Bearer" { + let conn = request + .guard::() + .map_failure(|_| (Status::InternalServerError, TokenError::DbError))?; + if let Ok(token) = ApiToken::find_by_value(&*conn, val) { + return Outcome::Success(token); + } + } + + Outcome::Forward(()) + } +} diff --git a/plume-models/src/apps.rs b/plume-models/src/apps.rs new file mode 100644 index 00000000000..bf68777d3c0 --- /dev/null +++ b/plume-models/src/apps.rs @@ -0,0 +1,30 @@ +use crate::{schema::apps, Error, Result}; +use chrono::NaiveDateTime; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; + +#[derive(Clone, Queryable, Serialize)] +pub struct App { + pub id: i32, + pub name: String, + pub client_id: String, + pub client_secret: String, + pub redirect_uri: Option, + pub website: Option, + pub creation_date: NaiveDateTime, +} + +#[derive(Insertable)] +#[table_name = "apps"] +pub struct NewApp { + pub name: String, + pub client_id: String, + pub client_secret: String, + pub redirect_uri: Option, + pub website: Option, +} + +impl App { + get!(apps); + insert!(apps, NewApp); + find_by!(apps, find_by_client_id, client_id as &str); +} diff --git a/plume-models/src/blocklisted_emails.rs b/plume-models/src/blocklisted_emails.rs new file mode 100644 index 00000000000..3d9928b383e --- /dev/null +++ b/plume-models/src/blocklisted_emails.rs @@ -0,0 +1,138 @@ +use crate::{schema::email_blocklist, Connection, Error, Result}; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, TextExpressionMethods}; +use glob::Pattern; + +#[derive(Clone, Queryable, Identifiable)] +#[table_name = "email_blocklist"] +pub struct BlocklistedEmail { + pub id: i32, + pub email_address: String, + pub note: String, + pub notify_user: bool, + pub notification_text: String, +} + +#[derive(Insertable, FromForm)] +#[table_name = "email_blocklist"] +pub struct NewBlocklistedEmail { + pub email_address: String, + pub note: String, + pub notify_user: bool, + pub notification_text: String, +} + +impl BlocklistedEmail { + insert!(email_blocklist, NewBlocklistedEmail); + get!(email_blocklist); + find_by!(email_blocklist, find_by_id, id as i32); + pub fn delete_entries(conn: &Connection, ids: Vec) -> Result { + use diesel::delete; + for i in ids { + let be: BlocklistedEmail = BlocklistedEmail::find_by_id(conn, i)?; + delete(&be).execute(conn)?; + } + Ok(true) + } + pub fn find_for_domain(conn: &Connection, domain: &str) -> Result> { + let effective = format!("%@{}", domain); + email_blocklist::table + .filter(email_blocklist::email_address.like(effective)) + .load::(conn) + .map_err(Error::from) + } + pub fn matches_blocklist(conn: &Connection, email: &str) -> Result> { + let mut result = email_blocklist::table.load::(conn)?; + for i in result.drain(..) { + if let Ok(x) = Pattern::new(&i.email_address) { + if x.matches(email) { + return Ok(Some(i)); + } + } + } + Ok(None) + } + pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Result> { + email_blocklist::table + .offset(min.into()) + .limit((max - min).into()) + .load::(conn) + .map_err(Error::from) + } + pub fn count(conn: &Connection) -> Result { + email_blocklist::table + .count() + .get_result(conn) + .map_err(Error::from) + } + pub fn pattern_errors(pat: &str) -> Option { + let c = Pattern::new(pat); + c.err() + } + pub fn new( + conn: &Connection, + pattern: &str, + note: &str, + show_notification: bool, + notification_text: &str, + ) -> Result { + let c = NewBlocklistedEmail { + email_address: pattern.to_owned(), + note: note.to_owned(), + notify_user: show_notification, + notification_text: notification_text.to_owned(), + }; + BlocklistedEmail::insert(conn, c) + } +} +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::{instance::tests as instance_tests, tests::db, Connection as Conn}; + use diesel::Connection; + + pub(crate) fn fill_database(conn: &Conn) -> Vec { + instance_tests::fill_database(conn); + let domainblock = + BlocklistedEmail::new(conn, "*@bad-actor.com", "Mean spammers", false, "").unwrap(); + let userblock = BlocklistedEmail::new( + conn, + "spammer@lax-administration.com", + "Decent enough domain, but this user is a problem.", + true, + "Stop it please", + ) + .unwrap(); + vec![domainblock, userblock] + } + #[test] + fn test_match() { + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + let various = fill_database(&conn); + let match1 = "user1@bad-actor.com"; + let match2 = "spammer@lax-administration.com"; + let no_match = "happy-user@lax-administration.com"; + assert_eq!( + BlocklistedEmail::matches_blocklist(&conn, match1) + .unwrap() + .unwrap() + .id, + various[0].id + ); + assert_eq!( + BlocklistedEmail::matches_blocklist(&conn, match2) + .unwrap() + .unwrap() + .id, + various[1].id + ); + assert_eq!( + BlocklistedEmail::matches_blocklist(&conn, no_match) + .unwrap() + .is_none(), + true + ); + Ok(()) + }); + } +} diff --git a/plume-models/src/blog_authors.rs b/plume-models/src/blog_authors.rs new file mode 100644 index 00000000000..561b410dea6 --- /dev/null +++ b/plume-models/src/blog_authors.rs @@ -0,0 +1,23 @@ +use crate::{schema::blog_authors, Error, Result}; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; + +#[derive(Clone, Queryable, Identifiable)] +pub struct BlogAuthor { + pub id: i32, + pub blog_id: i32, + pub author_id: i32, + pub is_owner: bool, +} + +#[derive(Insertable)] +#[table_name = "blog_authors"] +pub struct NewBlogAuthor { + pub blog_id: i32, + pub author_id: i32, + pub is_owner: bool, +} + +impl BlogAuthor { + insert!(blog_authors, NewBlogAuthor); + get!(blog_authors); +} diff --git a/plume-models/src/blogs.rs b/plume-models/src/blogs.rs new file mode 100644 index 00000000000..9d0839ae1f5 --- /dev/null +++ b/plume-models/src/blogs.rs @@ -0,0 +1,910 @@ +use crate::{ + ap_url, db_conn::DbConn, instance::*, medias::Media, posts::Post, safe_string::SafeString, + schema::blogs, users::User, Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE, +}; +use activitypub::{ + actor::Group, + collection::{OrderedCollection, OrderedCollectionPage}, + object::Image, + CustomObject, +}; +use chrono::NaiveDateTime; +use diesel::{self, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SaveChangesDsl}; +use openssl::{ + hash::MessageDigest, + pkey::{PKey, Private}, + rsa::Rsa, + sign::{Signer, Verifier}, +}; +use plume_common::activity_pub::{ + inbox::{AsActor, FromId}, + sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source, +}; +use url::Url; +use webfinger::*; + +pub type CustomGroup = CustomObject; + +#[derive(Queryable, Identifiable, Clone, AsChangeset)] +#[changeset_options(treat_none_as_null = "true")] +pub struct Blog { + pub id: i32, + pub actor_id: String, + pub title: String, + pub summary: String, + pub outbox_url: String, + pub inbox_url: String, + pub instance_id: i32, + pub creation_date: NaiveDateTime, + pub ap_url: String, + pub private_key: Option, + pub public_key: String, + pub fqn: String, + pub summary_html: SafeString, + pub icon_id: Option, + pub banner_id: Option, + pub theme: Option, +} + +#[derive(Default, Insertable)] +#[table_name = "blogs"] +pub struct NewBlog { + pub actor_id: String, + pub title: String, + pub summary: String, + pub outbox_url: String, + pub inbox_url: String, + pub instance_id: i32, + pub ap_url: String, + pub private_key: Option, + pub public_key: String, + pub summary_html: SafeString, + pub icon_id: Option, + pub banner_id: Option, + pub theme: Option, +} + +const BLOG_PREFIX: &str = "~"; + +impl Blog { + insert!(blogs, NewBlog, |inserted, conn| { + let instance = inserted.get_instance(conn)?; + if inserted.outbox_url.is_empty() { + inserted.outbox_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "outbox"); + } + + if inserted.inbox_url.is_empty() { + inserted.inbox_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "inbox"); + } + + if inserted.ap_url.is_empty() { + inserted.ap_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, ""); + } + + if inserted.fqn.is_empty() { + if instance.local { + inserted.fqn = inserted.actor_id.clone(); + } else { + inserted.fqn = format!("{}@{}", inserted.actor_id, instance.public_domain); + } + } + + inserted.save_changes(conn).map_err(Error::from) + }); + get!(blogs); + find_by!(blogs, find_by_ap_url, ap_url as &str); + find_by!(blogs, find_by_name, actor_id as &str, instance_id as i32); + + pub fn get_instance(&self, conn: &Connection) -> Result { + Instance::get(conn, self.instance_id) + } + + pub fn list_authors(&self, conn: &Connection) -> Result> { + use crate::schema::blog_authors; + use crate::schema::users; + let authors_ids = blog_authors::table + .filter(blog_authors::blog_id.eq(self.id)) + .select(blog_authors::author_id); + users::table + .filter(users::id.eq_any(authors_ids)) + .load::(conn) + .map_err(Error::from) + } + + pub fn count_authors(&self, conn: &Connection) -> Result { + use crate::schema::blog_authors; + blog_authors::table + .filter(blog_authors::blog_id.eq(self.id)) + .count() + .get_result(conn) + .map_err(Error::from) + } + + pub fn find_for_author(conn: &Connection, author: &User) -> Result> { + use crate::schema::blog_authors; + let author_ids = blog_authors::table + .filter(blog_authors::author_id.eq(author.id)) + .select(blog_authors::blog_id); + blogs::table + .filter(blogs::id.eq_any(author_ids)) + .load::(conn) + .map_err(Error::from) + } + + pub fn find_by_fqn(conn: &DbConn, fqn: &str) -> Result { + let from_db = blogs::table + .filter(blogs::fqn.eq(fqn)) + .first(&**conn) + .optional()?; + if let Some(from_db) = from_db { + Ok(from_db) + } else { + Blog::fetch_from_webfinger(conn, fqn) + } + } + + fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result { + resolve_with_prefix(Prefix::Group, acct.to_owned(), true)? + .links + .into_iter() + .find(|l| l.mime_type == Some(String::from("application/activity+json"))) + .ok_or(Error::Webfinger) + .and_then(|l| { + Blog::from_id( + conn, + &l.href.ok_or(Error::MissingApProperty)?, + None, + CONFIG.proxy(), + ) + .map_err(|(_, e)| e) + }) + } + + pub fn to_activity(&self, conn: &Connection) -> Result { + let mut blog = Group::default(); + blog.ap_actor_props + .set_preferred_username_string(self.actor_id.clone())?; + blog.object_props.set_name_string(self.title.clone())?; + blog.ap_actor_props + .set_outbox_string(self.outbox_url.clone())?; + blog.ap_actor_props + .set_inbox_string(self.inbox_url.clone())?; + blog.object_props + .set_summary_string(self.summary_html.to_string())?; + blog.ap_object_props.set_source_object(Source { + content: self.summary.clone(), + media_type: String::from("text/markdown"), + })?; + + 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()).ok()) + .unwrap_or_default(), + )?; + icon.object_props.set_attributed_to_link( + self.icon_id + .and_then(|id| { + Media::get(conn, id) + .and_then(|m| Ok(User::get(conn, m.owner_id)?.into_id())) + .ok() + }) + .unwrap_or_else(|| Id::new(String::new())), + )?; + blog.object_props.set_icon_object(icon)?; + + 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()).ok()) + .unwrap_or_default(), + )?; + banner.object_props.set_attributed_to_link( + self.banner_id + .and_then(|id| { + Media::get(conn, id) + .and_then(|m| Ok(User::get(conn, m.owner_id)?.into_id())) + .ok() + }) + .unwrap_or_else(|| Id::new(String::new())), + )?; + blog.object_props.set_image_object(banner)?; + + blog.object_props.set_id_string(self.ap_url.clone())?; + + let mut public_key = PublicKey::default(); + public_key.set_id_string(format!("{}#main-key", self.ap_url))?; + public_key.set_owner_string(self.ap_url.clone())?; + public_key.set_public_key_pem_string(self.public_key.clone())?; + let mut ap_signature = ApSignature::default(); + ap_signature.set_public_key_publickey(public_key)?; + + Ok(CustomGroup::new(blog, ap_signature)) + } + + pub fn outbox(&self, conn: &Connection) -> Result> { + let mut coll = OrderedCollection::default(); + coll.collection_props.items = serde_json::to_value(self.get_activities(conn))?; + coll.collection_props + .set_total_items_u64(self.get_activities(conn).len() as u64)?; + coll.collection_props + .set_first_link(Id::new(ap_url(&format!("{}?page=1", &self.outbox_url))))?; + coll.collection_props + .set_last_link(Id::new(ap_url(&format!( + "{}?page={}", + &self.outbox_url, + (self.get_activities(conn).len() as u64 + ITEMS_PER_PAGE as u64 - 1) as u64 + / ITEMS_PER_PAGE as u64 + ))))?; + Ok(ActivityStream::new(coll)) + } + pub fn outbox_page( + &self, + conn: &Connection, + (min, max): (i32, i32), + ) -> Result> { + let mut coll = OrderedCollectionPage::default(); + let acts = self.get_activity_page(conn, (min, max)); + //This still doesn't do anything because the outbox + //doesn't do anything yet + coll.collection_page_props.set_next_link(Id::new(&format!( + "{}?page={}", + &self.outbox_url, + min / ITEMS_PER_PAGE + 1 + )))?; + coll.collection_page_props.set_prev_link(Id::new(&format!( + "{}?page={}", + &self.outbox_url, + min / ITEMS_PER_PAGE - 1 + )))?; + coll.collection_props.items = serde_json::to_value(acts)?; + Ok(ActivityStream::new(coll)) + } + fn get_activities(&self, _conn: &Connection) -> Vec { + vec![] + } + fn get_activity_page( + &self, + _conn: &Connection, + (_min, _max): (i32, i32), + ) -> Vec { + vec![] + } + + pub fn get_keypair(&self) -> Result> { + PKey::from_rsa(Rsa::private_key_from_pem( + self.private_key + .clone() + .ok_or(Error::MissingApProperty)? + .as_ref(), + )?) + .map_err(Error::from) + } + + pub fn webfinger(&self, conn: &Connection) -> Result { + Ok(Webfinger { + subject: format!( + "acct:{}@{}", + self.actor_id, + self.get_instance(conn)?.public_domain + ), + aliases: vec![self.ap_url.clone()], + links: vec![ + Link { + rel: String::from("http://webfinger.net/rel/profile-page"), + mime_type: None, + href: Some(self.ap_url.clone()), + template: None, + }, + Link { + rel: String::from("http://schemas.google.com/g/2010#updates-from"), + mime_type: Some(String::from("application/atom+xml")), + href: Some(self.get_instance(conn)?.compute_box( + BLOG_PREFIX, + &self.actor_id, + "feed.atom", + )), + template: None, + }, + Link { + rel: String::from("self"), + mime_type: Some(String::from("application/activity+json")), + href: Some(self.ap_url.clone()), + template: None, + }, + ], + }) + } + + pub fn icon_url(&self, conn: &Connection) -> String { + self.icon_id + .and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok()) + .unwrap_or_else(|| "/static/images/default-avatar.png".to_string()) + } + + pub fn banner_url(&self, conn: &Connection) -> Option { + self.banner_id + .and_then(|i| Media::get(conn, i).ok()) + .and_then(|c| c.url().ok()) + } + + pub fn delete(&self, conn: &Connection) -> Result<()> { + for post in Post::get_for_blog(conn, self)? { + post.delete(conn)?; + } + diesel::delete(self) + .execute(conn) + .map(|_| ()) + .map_err(Error::from) + } +} + +impl IntoId for Blog { + fn into_id(self) -> Id { + Id::new(self.ap_url) + } +} + +impl FromId for Blog { + type Error = Error; + type Object = CustomGroup; + + fn from_db(conn: &DbConn, id: &str) -> Result { + Self::find_by_ap_url(conn, id) + } + + fn from_activity(conn: &DbConn, acct: CustomGroup) -> Result { + let url = Url::parse(&acct.object.object_props.id_string()?)?; + let inst = url.host_str().ok_or(Error::Url)?; + let instance = Instance::find_by_domain(conn, inst).or_else(|_| { + Instance::insert( + conn, + NewInstance { + public_domain: inst.to_owned(), + name: inst.to_owned(), + local: false, + // We don't really care about all the following for remote instances + long_description: SafeString::new(""), + short_description: SafeString::new(""), + default_license: String::new(), + open_registrations: true, + short_description_html: String::new(), + long_description_html: String::new(), + }, + ) + })?; + let icon_id = acct + .object + .object_props + .icon_image() + .ok() + .and_then(|icon| { + let owner = icon.object_props.attributed_to_link::().ok()?; + Media::save_remote( + conn, + icon.object_props.url_string().ok()?, + &User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?, + ) + .ok() + }) + .map(|m| m.id); + + let banner_id = acct + .object + .object_props + .image_image() + .ok() + .and_then(|banner| { + let owner = banner.object_props.attributed_to_link::().ok()?; + Media::save_remote( + conn, + banner.object_props.url_string().ok()?, + &User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?, + ) + .ok() + }) + .map(|m| m.id); + + let name = acct.object.ap_actor_props.preferred_username_string()?; + if name.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) { + return Err(Error::InvalidValue); + } + + Blog::insert( + conn, + NewBlog { + actor_id: name.clone(), + title: acct.object.object_props.name_string().unwrap_or(name), + outbox_url: acct.object.ap_actor_props.outbox_string()?, + inbox_url: acct.object.ap_actor_props.inbox_string()?, + summary: acct + .object + .ap_object_props + .source_object::() + .map(|s| s.content) + .unwrap_or_default(), + instance_id: instance.id, + ap_url: acct.object.object_props.id_string()?, + public_key: acct + .custom_props + .public_key_publickey()? + .public_key_pem_string()?, + private_key: None, + banner_id, + icon_id, + summary_html: SafeString::new( + &acct + .object + .object_props + .summary_string() + .unwrap_or_default(), + ), + theme: None, + }, + ) + } + + fn get_sender() -> &'static dyn sign::Signer { + Instance::get_local_instance_user().expect("Failed to local instance user") + } +} + +impl AsActor<&PlumeRocket> for Blog { + fn get_inbox_url(&self) -> String { + self.inbox_url.clone() + } + + fn get_shared_inbox_url(&self) -> Option { + None + } + + fn is_local(&self) -> bool { + Instance::get_local() + .map(|i| self.instance_id == i.id) + .unwrap_or(false) + } +} + +impl sign::Signer for Blog { + fn get_key_id(&self) -> String { + format!("{}#main-key", self.ap_url) + } + + fn sign(&self, to_sign: &str) -> sign::Result> { + let key = self.get_keypair().map_err(|_| sign::Error())?; + let mut signer = Signer::new(MessageDigest::sha256(), &key)?; + signer.update(to_sign.as_bytes())?; + signer.sign_to_vec().map_err(sign::Error::from) + } + + fn verify(&self, data: &str, signature: &[u8]) -> sign::Result { + let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?; + let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?; + verifier.update(data.as_bytes())?; + verifier.verify(signature).map_err(sign::Error::from) + } +} + +impl NewBlog { + pub fn new_local( + actor_id: String, + title: String, + summary: String, + instance_id: i32, + ) -> Result { + let (pub_key, priv_key) = sign::gen_keypair(); + Ok(NewBlog { + actor_id, + title, + summary, + instance_id, + public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?, + private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?), + ..NewBlog::default() + }) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::{ + blog_authors::*, instance::tests as instance_tests, medias::NewMedia, tests::db, + users::tests as usersTests, Connection as Conn, + }; + use diesel::Connection; + + pub(crate) fn fill_database(conn: &Conn) -> (Vec, Vec) { + instance_tests::fill_database(conn); + let users = usersTests::fill_database(conn); + let blog1 = Blog::insert( + conn, + NewBlog::new_local( + "BlogName".to_owned(), + "Blog name".to_owned(), + "This is a small blog".to_owned(), + Instance::get_local().unwrap().id, + ) + .unwrap(), + ) + .unwrap(); + let blog2 = Blog::insert( + conn, + NewBlog::new_local( + "MyBlog".to_owned(), + "My blog".to_owned(), + "Welcome to my blog".to_owned(), + Instance::get_local().unwrap().id, + ) + .unwrap(), + ) + .unwrap(); + let blog3 = Blog::insert( + conn, + NewBlog::new_local( + "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().unwrap().id, + ) + .unwrap(), + ) + .unwrap(); + + BlogAuthor::insert( + conn, + NewBlogAuthor { + blog_id: blog1.id, + author_id: users[0].id, + is_owner: true, + }, + ) + .unwrap(); + + BlogAuthor::insert( + conn, + NewBlogAuthor { + blog_id: blog1.id, + author_id: users[1].id, + is_owner: false, + }, + ) + .unwrap(); + + BlogAuthor::insert( + conn, + NewBlogAuthor { + blog_id: blog2.id, + author_id: users[1].id, + is_owner: true, + }, + ) + .unwrap(); + + BlogAuthor::insert( + conn, + NewBlogAuthor { + blog_id: blog3.id, + author_id: users[2].id, + is_owner: true, + }, + ) + .unwrap(); + (users, vec![blog1, blog2, blog3]) + } + + #[test] + fn get_instance() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + fill_database(&conn); + + let blog = Blog::insert( + &conn, + NewBlog::new_local( + "SomeName".to_owned(), + "Some name".to_owned(), + "This is some blog".to_owned(), + Instance::get_local().unwrap().id, + ) + .unwrap(), + ) + .unwrap(); + + assert_eq!( + blog.get_instance(&conn).unwrap().id, + Instance::get_local().unwrap().id + ); + // TODO add tests for remote instance + Ok(()) + }) + } + + #[test] + fn authors() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let (user, _) = fill_database(&conn); + + let b1 = Blog::insert( + &conn, + NewBlog::new_local( + "SomeName".to_owned(), + "Some name".to_owned(), + "This is some blog".to_owned(), + Instance::get_local().unwrap().id, + ) + .unwrap(), + ) + .unwrap(); + let b2 = Blog::insert( + &conn, + NewBlog::new_local( + "Blog".to_owned(), + "Blog".to_owned(), + "I've named my blog Blog".to_owned(), + Instance::get_local().unwrap().id, + ) + .unwrap(), + ) + .unwrap(); + let blog = vec![b1, b2]; + + BlogAuthor::insert( + &conn, + NewBlogAuthor { + blog_id: blog[0].id, + author_id: user[0].id, + is_owner: true, + }, + ) + .unwrap(); + + BlogAuthor::insert( + &conn, + NewBlogAuthor { + blog_id: blog[0].id, + author_id: user[1].id, + is_owner: false, + }, + ) + .unwrap(); + + BlogAuthor::insert( + &conn, + NewBlogAuthor { + blog_id: blog[1].id, + author_id: user[0].id, + is_owner: true, + }, + ) + .unwrap(); + + assert!(blog[0] + .list_authors(&conn) + .unwrap() + .iter() + .any(|a| a.id == user[0].id)); + assert!(blog[0] + .list_authors(&conn) + .unwrap() + .iter() + .any(|a| a.id == user[1].id)); + assert!(blog[1] + .list_authors(&conn) + .unwrap() + .iter() + .any(|a| a.id == user[0].id)); + assert!(!blog[1] + .list_authors(&conn) + .unwrap() + .iter() + .any(|a| a.id == user[1].id)); + + assert!(Blog::find_for_author(&conn, &user[0]) + .unwrap() + .iter() + .any(|b| b.id == blog[0].id)); + assert!(Blog::find_for_author(&conn, &user[1]) + .unwrap() + .iter() + .any(|b| b.id == blog[0].id)); + assert!(Blog::find_for_author(&conn, &user[0]) + .unwrap() + .iter() + .any(|b| b.id == blog[1].id)); + assert!(!Blog::find_for_author(&conn, &user[1]) + .unwrap() + .iter() + .any(|b| b.id == blog[1].id)); + Ok(()) + }) + } + + #[test] + fn find_local() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + fill_database(&conn); + + let blog = Blog::insert( + &conn, + NewBlog::new_local( + "SomeName".to_owned(), + "Some name".to_owned(), + "This is some blog".to_owned(), + Instance::get_local().unwrap().id, + ) + .unwrap(), + ) + .unwrap(); + + assert_eq!(Blog::find_by_fqn(&conn, "SomeName").unwrap().id, blog.id); + Ok(()) + }) + } + + #[test] + fn get_fqn() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + fill_database(&conn); + + let blog = Blog::insert( + &conn, + NewBlog::new_local( + "SomeName".to_owned(), + "Some name".to_owned(), + "This is some blog".to_owned(), + Instance::get_local().unwrap().id, + ) + .unwrap(), + ) + .unwrap(); + + assert_eq!(blog.fqn, "SomeName"); + Ok(()) + }) + } + + #[test] + fn delete() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let (_, blogs) = fill_database(&conn); + + blogs[0].delete(&conn).unwrap(); + assert!(Blog::get(&conn, blogs[0].id).is_err()); + Ok(()) + }) + } + + #[test] + fn delete_via_user() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let (user, _) = fill_database(&conn); + + let b1 = Blog::insert( + &conn, + NewBlog::new_local( + "SomeName".to_owned(), + "Some name".to_owned(), + "This is some blog".to_owned(), + Instance::get_local().unwrap().id, + ) + .unwrap(), + ) + .unwrap(); + let b2 = Blog::insert( + &conn, + NewBlog::new_local( + "Blog".to_owned(), + "Blog".to_owned(), + "I've named my blog Blog".to_owned(), + Instance::get_local().unwrap().id, + ) + .unwrap(), + ) + .unwrap(); + let blog = vec![b1, b2]; + + BlogAuthor::insert( + &conn, + NewBlogAuthor { + blog_id: blog[0].id, + author_id: user[0].id, + is_owner: true, + }, + ) + .unwrap(); + + BlogAuthor::insert( + &conn, + NewBlogAuthor { + blog_id: blog[0].id, + author_id: user[1].id, + is_owner: false, + }, + ) + .unwrap(); + + BlogAuthor::insert( + &conn, + NewBlogAuthor { + blog_id: blog[1].id, + author_id: user[0].id, + is_owner: true, + }, + ) + .unwrap(); + + user[0].delete(&conn).unwrap(); + assert!(Blog::get(&conn, blog[0].id).is_ok()); + assert!(Blog::get(&conn, blog[1].id).is_err()); + user[1].delete(&conn).unwrap(); + assert!(Blog::get(&conn, blog[0].id).is_err()); + Ok(()) + }) + } + + #[test] + fn self_federation() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let (users, mut blogs) = fill_database(&conn); + blogs[0].icon_id = Some( + Media::insert( + &conn, + NewMedia { + file_path: "aaa.png".into(), + alt_text: String::new(), + is_remote: false, + remote_url: None, + sensitive: false, + content_warning: None, + owner_id: users[0].id, + }, + ) + .unwrap() + .id, + ); + blogs[0].banner_id = Some( + Media::insert( + &conn, + NewMedia { + file_path: "bbb.png".into(), + alt_text: String::new(), + is_remote: false, + remote_url: None, + sensitive: false, + content_warning: None, + owner_id: users[0].id, + }, + ) + .unwrap() + .id, + ); + let _: Blog = blogs[0].save_changes(&**conn).unwrap(); + + let ap_repr = blogs[0].to_activity(&conn).unwrap(); + blogs[0].delete(&conn).unwrap(); + let blog = Blog::from_activity(&conn, ap_repr).unwrap(); + + assert_eq!(blog.actor_id, blogs[0].actor_id); + assert_eq!(blog.title, blogs[0].title); + assert_eq!(blog.summary, blogs[0].summary); + assert_eq!(blog.outbox_url, blogs[0].outbox_url); + assert_eq!(blog.inbox_url, blogs[0].inbox_url); + assert_eq!(blog.instance_id, blogs[0].instance_id); + assert_eq!(blog.ap_url, blogs[0].ap_url); + assert_eq!(blog.public_key, blogs[0].public_key); + assert_eq!(blog.fqn, blogs[0].fqn); + assert_eq!(blog.summary_html, blogs[0].summary_html); + assert_eq!(blog.icon_url(&conn), blogs[0].icon_url(&conn)); + assert_eq!(blog.banner_url(&conn), blogs[0].banner_url(&conn)); + + Ok(()) + }) + } +} diff --git a/plume-models/src/comment_seers.rs b/plume-models/src/comment_seers.rs new file mode 100644 index 00000000000..a8d9fa2a5b2 --- /dev/null +++ b/plume-models/src/comment_seers.rs @@ -0,0 +1,29 @@ +use crate::{comments::Comment, schema::comment_seers, users::User, Connection, Error, Result}; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; + +#[derive(Queryable, Clone)] +pub struct CommentSeers { + pub id: i32, + pub comment_id: i32, + pub user_id: i32, +} + +#[derive(Insertable, Default)] +#[table_name = "comment_seers"] +pub struct NewCommentSeers { + pub comment_id: i32, + pub user_id: i32, +} + +impl CommentSeers { + insert!(comment_seers, NewCommentSeers); + + pub fn can_see(conn: &Connection, c: &Comment, u: &User) -> Result { + comment_seers::table + .filter(comment_seers::comment_id.eq(c.id)) + .filter(comment_seers::user_id.eq(u.id)) + .load::(conn) + .map_err(Error::from) + .map(|r| !r.is_empty()) + } +} diff --git a/plume-models/src/comments.rs b/plume-models/src/comments.rs new file mode 100644 index 00000000000..1da82a36346 --- /dev/null +++ b/plume-models/src/comments.rs @@ -0,0 +1,581 @@ +use crate::{ + comment_seers::{CommentSeers, NewCommentSeers}, + db_conn::DbConn, + instance::Instance, + medias::Media, + mentions::Mention, + notifications::*, + posts::Post, + safe_string::SafeString, + schema::comments, + users::User, + Connection, Error, Result, CONFIG, +}; +use activitypub::{ + activity::{Create, Delete}, + link, + object::{Note, Tombstone}, +}; +use chrono::{self, NaiveDateTime, TimeZone, Utc}; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; +use plume_common::{ + activity_pub::{ + inbox::{AsActor, AsObject, FromId}, + sign::Signer, + Id, IntoId, PUBLIC_VISIBILITY, + }, + utils, +}; +use std::collections::HashSet; + +#[derive(Queryable, Identifiable, Clone, AsChangeset)] +pub struct Comment { + pub id: i32, + pub content: SafeString, + pub in_response_to_id: Option, + pub post_id: i32, + pub author_id: i32, + pub creation_date: NaiveDateTime, + pub ap_url: Option, + pub sensitive: bool, + pub spoiler_text: String, + pub public_visibility: bool, +} + +#[derive(Insertable, Default)] +#[table_name = "comments"] +pub struct NewComment { + pub content: SafeString, + pub in_response_to_id: Option, + pub post_id: i32, + pub author_id: i32, + pub ap_url: Option, + pub sensitive: bool, + pub spoiler_text: String, + pub public_visibility: bool, +} + +impl Comment { + insert!(comments, NewComment, |inserted, conn| { + if inserted.ap_url.is_none() { + inserted.ap_url = Some(format!( + "{}/comment/{}", + inserted.get_post(conn)?.ap_url, + inserted.id + )); + let _: Comment = inserted.save_changes(conn)?; + } + Ok(inserted) + }); + get!(comments); + list_by!(comments, list_by_post, post_id as i32); + find_by!(comments, find_by_ap_url, ap_url as &str); + + pub fn get_author(&self, conn: &Connection) -> Result { + User::get(conn, self.author_id) + } + + pub fn get_post(&self, conn: &Connection) -> Result { + Post::get(conn, self.post_id) + } + + pub fn count_local(conn: &Connection) -> Result { + use crate::schema::users; + let local_authors = users::table + .filter(users::instance_id.eq(Instance::get_local()?.id)) + .select(users::id); + comments::table + .filter(comments::author_id.eq_any(local_authors)) + .count() + .get_result(conn) + .map_err(Error::from) + } + + pub fn get_responses(&self, conn: &Connection) -> Result> { + comments::table + .filter(comments::in_response_to_id.eq(self.id)) + .load::(conn) + .map_err(Error::from) + } + + pub fn can_see(&self, conn: &Connection, user: Option<&User>) -> bool { + self.public_visibility + || user + .as_ref() + .map(|u| CommentSeers::can_see(conn, self, u).unwrap_or(false)) + .unwrap_or(false) + } + + pub fn to_activity(&self, conn: &DbConn) -> Result { + let author = User::get(conn, self.author_id)?; + let (html, mentions, _hashtags) = utils::md_to_html( + self.content.get().as_ref(), + Some(&Instance::get_local()?.public_domain), + true, + Some(Media::get_media_processor(conn, vec![&author])), + ); + + let mut note = Note::default(); + let to = vec![Id::new(PUBLIC_VISIBILITY.to_string())]; + + note.object_props + .set_id_string(self.ap_url.clone().unwrap_or_default())?; + note.object_props + .set_summary_string(self.spoiler_text.clone())?; + note.object_props.set_content_string(html)?; + note.object_props + .set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else( + || Ok(Post::get(conn, self.post_id)?.ap_url), + |id| Ok(Comment::get(conn, id)?.ap_url.unwrap_or_default()) as Result, + )?))?; + note.object_props + .set_published_utctime(Utc.from_utc_datetime(&self.creation_date))?; + note.object_props.set_attributed_to_link(author.into_id())?; + note.object_props.set_to_link_vec(to)?; + note.object_props.set_tag_link_vec( + mentions + .into_iter() + .filter_map(|m| Mention::build_activity(conn, &m).ok()) + .collect::>(), + )?; + Ok(note) + } + + pub fn create_activity(&self, conn: &DbConn) -> Result { + let author = User::get(conn, self.author_id)?; + + let note = self.to_activity(conn)?; + let mut act = Create::default(); + act.create_props.set_actor_link(author.into_id())?; + act.create_props.set_object_object(note.clone())?; + act.object_props.set_id_string(format!( + "{}/activity", + self.ap_url.clone().ok_or(Error::MissingApProperty)?, + ))?; + act.object_props + .set_to_link_vec(note.object_props.to_link_vec::()?)?; + act.object_props + .set_cc_link_vec(vec![Id::new(self.get_author(conn)?.followers_endpoint)])?; + Ok(act) + } + + pub fn notify(&self, conn: &Connection) -> Result<()> { + for author in self.get_post(conn)?.get_authors(conn)? { + 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(()) + } + + pub fn build_delete(&self, conn: &Connection) -> Result { + let mut act = Delete::default(); + act.delete_props + .set_actor_link(self.get_author(conn)?.into_id())?; + + let mut tombstone = Tombstone::default(); + tombstone + .object_props + .set_id_string(self.ap_url.clone().ok_or(Error::MissingApProperty)?)?; + act.delete_props.set_object_object(tombstone)?; + + act.object_props + .set_id_string(format!("{}#delete", self.ap_url.clone().unwrap()))?; + act.object_props + .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?; + + Ok(act) + } +} + +impl FromId for Comment { + type Error = Error; + type Object = Note; + + fn from_db(conn: &DbConn, id: &str) -> Result { + Self::find_by_ap_url(conn, id) + } + + fn from_activity(conn: &DbConn, note: Note) -> Result { + let comm = { + let previous_url = note + .object_props + .in_reply_to + .as_ref() + .ok_or(Error::MissingApProperty)? + .as_str() + .ok_or(Error::MissingApProperty)?; + let previous_comment = Comment::find_by_ap_url(conn, previous_url); + + let is_public = |v: &Option| match v + .as_ref() + .unwrap_or(&serde_json::Value::Null) + { + serde_json::Value::Array(v) => v + .iter() + .filter_map(serde_json::Value::as_str) + .any(|s| s == PUBLIC_VISIBILITY), + serde_json::Value::String(s) => s == PUBLIC_VISIBILITY, + _ => false, + }; + + let public_visibility = is_public(¬e.object_props.to) + || is_public(¬e.object_props.bto) + || is_public(¬e.object_props.cc) + || is_public(¬e.object_props.bcc); + + let comm = Comment::insert( + conn, + NewComment { + content: SafeString::new(¬e.object_props.content_string()?), + spoiler_text: note.object_props.summary_string().unwrap_or_default(), + ap_url: note.object_props.id_string().ok(), + in_response_to_id: previous_comment.iter().map(|c| c.id).next(), + post_id: previous_comment.map(|c| c.post_id).or_else(|_| { + Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result + })?, + author_id: User::from_id( + conn, + ¬e.object_props.attributed_to_link::()?, + None, + CONFIG.proxy(), + ) + .map_err(|(_, e)| e)? + .id, + sensitive: note.object_props.summary_string().is_ok(), + public_visibility, + }, + )?; + + // save mentions + if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() { + for tag in tags { + serde_json::from_value::(tag) + .map_err(Error::from) + .and_then(|m| { + let author = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0]; + let not_author = m.link_props.href_string()? != author.ap_url.clone(); + Mention::from_activity(conn, &m, comm.id, false, not_author) + }) + .ok(); + } + } + comm + }; + + if !comm.public_visibility { + let receivers_ap_url = |v: Option| { + let filter = |e: serde_json::Value| { + if let serde_json::Value::String(s) = e { + Some(s) + } else { + None + } + }; + match v.unwrap_or(serde_json::Value::Null) { + serde_json::Value::Array(v) => v, + v => vec![v], + } + .into_iter() + .filter_map(filter) + }; + + let mut note = note; + + let to = receivers_ap_url(note.object_props.to.take()); + let cc = receivers_ap_url(note.object_props.cc.take()); + let bto = receivers_ap_url(note.object_props.bto.take()); + let bcc = receivers_ap_url(note.object_props.bcc.take()); + + let receivers_ap_url = to + .chain(cc) + .chain(bto) + .chain(bcc) + .collect::>() // remove duplicates (don't do a query more than once) + .into_iter() + .flat_map(|v| { + if let Ok(user) = User::from_id(conn, &v, None, CONFIG.proxy()) { + vec![user] + } else { + vec![] // TODO try to fetch collection + } + }) + .filter(|u| u.get_instance(conn).map(|i| i.local).unwrap_or(false)) + .collect::>(); //remove duplicates (prevent db error) + + for user in &receivers_ap_url { + CommentSeers::insert( + conn, + NewCommentSeers { + comment_id: comm.id, + user_id: user.id, + }, + )?; + } + } + + comm.notify(conn)?; + Ok(comm) + } + + fn get_sender() -> &'static dyn Signer { + Instance::get_local_instance_user().expect("Failed to local instance user") + } +} + +impl AsObject for Comment { + type Error = Error; + type Output = Self; + + fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result { + // The actual creation takes place in the FromId impl + Ok(self) + } +} + +impl AsObject for Comment { + type Error = Error; + type Output = (); + + fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { + if self.author_id != actor.id { + return Err(Error::Unauthorized); + } + + for m in Mention::list_for_comment(conn, self.id)? { + for n in Notification::find_for_mention(conn, &m)? { + n.delete(conn)?; + } + m.delete(conn)?; + } + + for n in Notification::find_for_comment(conn, &self)? { + n.delete(&**conn)?; + } + + diesel::update(comments::table) + .filter(comments::in_response_to_id.eq(self.id)) + .set(comments::in_response_to_id.eq(self.in_response_to_id)) + .execute(&**conn)?; + diesel::delete(&self).execute(&**conn)?; + Ok(()) + } +} + +pub struct CommentTree { + pub comment: Comment, + pub responses: Vec, +} + +impl CommentTree { + pub fn from_post(conn: &Connection, p: &Post, user: Option<&User>) -> Result> { + Ok(Comment::list_by_post(conn, p.id)? + .into_iter() + .filter(|c| c.in_response_to_id.is_none()) + .filter(|c| c.can_see(conn, user)) + .filter_map(|c| Self::from_comment(conn, c, user).ok()) + .collect()) + } + + pub fn from_comment(conn: &Connection, comment: Comment, user: Option<&User>) -> Result { + let responses = comment + .get_responses(conn)? + .into_iter() + .filter(|c| c.can_see(conn, user)) + .filter_map(|c| Self::from_comment(conn, c, user).ok()) + .collect(); + Ok(CommentTree { comment, responses }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::blogs::Blog; + use crate::inbox::{inbox, tests::fill_database, InboxResult}; + use crate::safe_string::SafeString; + use crate::tests::{db, format_datetime}; + use assert_json_diff::assert_json_eq; + use diesel::Connection; + use serde_json::{json, to_value}; + + fn prepare_activity(conn: &DbConn) -> (Comment, Vec, Vec, Vec) { + let (posts, users, blogs) = fill_database(&conn); + + let comment = Comment::insert( + conn, + NewComment { + content: SafeString::new("My comment, mentioning to @user"), + in_response_to_id: None, + post_id: posts[0].id, + author_id: users[0].id, + ap_url: None, + sensitive: true, + spoiler_text: "My CW".into(), + public_visibility: true, + }, + ) + .unwrap(); + + (comment, posts, users, blogs) + } + + // creates a post, get it's Create activity, delete the post, + // "send" the Create to the inbox, and check it works + #[test] + fn self_federation() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let (original_comm, posts, users, _blogs) = prepare_activity(&conn); + let act = original_comm.create_activity(&conn).unwrap(); + + assert_json_eq!(to_value(&act).unwrap(), json!({ + "actor": "https://plu.me/@/admin/", + "cc": ["https://plu.me/@/admin/followers"], + "id": format!("https://plu.me/~/BlogName/testing/comment/{}/activity", original_comm.id), + "object": { + "attributedTo": "https://plu.me/@/admin/", + "content": r###"

My comment, mentioning to @user

+"###, + "id": format!("https://plu.me/~/BlogName/testing/comment/{}", original_comm.id), + "inReplyTo": "https://plu.me/~/BlogName/testing", + "published": format_datetime(&original_comm.creation_date), + "summary": "My CW", + "tag": [ + { + "href": "https://plu.me/@/user/", + "name": "@user", + "type": "Mention" + } + ], + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Note" + }, + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Create", + })); + + let reply = Comment::insert( + conn, + NewComment { + content: SafeString::new(""), + in_response_to_id: Some(original_comm.id), + post_id: posts[0].id, + author_id: users[1].id, + ap_url: None, + sensitive: false, + spoiler_text: "".into(), + public_visibility: true, + }, + ) + .unwrap(); + let reply_act = reply.create_activity(&conn).unwrap(); + + assert_json_eq!(to_value(&reply_act).unwrap(), json!({ + "actor": "https://plu.me/@/user/", + "cc": ["https://plu.me/@/user/followers"], + "id": format!("https://plu.me/~/BlogName/testing/comment/{}/activity", reply.id), + "object": { + "attributedTo": "https://plu.me/@/user/", + "content": "", + "id": format!("https://plu.me/~/BlogName/testing/comment/{}", reply.id), + "inReplyTo": format!("https://plu.me/~/BlogName/testing/comment/{}", original_comm.id), + "published": format_datetime(&reply.creation_date), + "summary": "", + "tag": [], + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Note" + }, + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Create" + })); + + inbox( + &conn, + serde_json::to_value(original_comm.build_delete(&conn).unwrap()).unwrap(), + ) + .unwrap(); + + match inbox(&conn, to_value(act).unwrap()).unwrap() { + InboxResult::Commented(c) => { + // TODO: one is HTML, the other markdown: assert_eq!(c.content, original_comm.content); + assert_eq!(c.in_response_to_id, original_comm.in_response_to_id); + assert_eq!(c.post_id, original_comm.post_id); + assert_eq!(c.author_id, original_comm.author_id); + assert_eq!(c.ap_url, original_comm.ap_url); + assert_eq!(c.spoiler_text, original_comm.spoiler_text); + assert_eq!(c.public_visibility, original_comm.public_visibility); + } + _ => panic!("Unexpected result"), + }; + Ok(()) + }) + } + + #[test] + fn to_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (comment, _posts, _users, _blogs) = prepare_activity(&conn); + let act = comment.to_activity(&conn)?; + + let expected = json!({ + "attributedTo": "https://plu.me/@/admin/", + "content": r###"

My comment, mentioning to @user

+"###, + "id": format!("https://plu.me/~/BlogName/testing/comment/{}", comment.id), + "inReplyTo": "https://plu.me/~/BlogName/testing", + "published": format_datetime(&comment.creation_date), + "summary": "My CW", + "tag": [ + { + "href": "https://plu.me/@/user/", + "name": "@user", + "type": "Mention" + } + ], + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Note" + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } + + #[test] + fn build_delete() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (comment, _posts, _users, _blogs) = prepare_activity(&conn); + let act = comment.build_delete(&conn)?; + + let expected = json!({ + "actor": "https://plu.me/@/admin/", + "id": format!("https://plu.me/~/BlogName/testing/comment/{}#delete", comment.id), + "object": { + "id": format!("https://plu.me/~/BlogName/testing/comment/{}", comment.id), + "type": "Tombstone" + }, + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Delete" + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } +} diff --git a/plume-models/src/config.rs b/plume-models/src/config.rs new file mode 100644 index 00000000000..ba705d427ac --- /dev/null +++ b/plume-models/src/config.rs @@ -0,0 +1,384 @@ +use crate::search::TokenizerKind as SearchTokenizer; +use crate::signups::Strategy as SignupStrategy; +use crate::smtp::{SMTP_PORT, SUBMISSIONS_PORT, SUBMISSION_PORT}; +use rocket::config::Limits; +use rocket::Config as RocketConfig; +use std::collections::HashSet; +use std::env::{self, var}; + +#[cfg(not(test))] +const DB_NAME: &str = "plume"; +#[cfg(test)] +const DB_NAME: &str = "plume_tests"; + +pub struct Config { + pub base_url: String, + pub database_url: String, + pub db_name: &'static str, + pub db_max_size: Option, + pub db_min_idle: Option, + pub signup: SignupStrategy, + pub search_index: String, + pub search_tokenizers: SearchTokenizerConfig, + pub rocket: Result, + pub logo: LogoConfig, + pub default_theme: String, + pub media_directory: String, + pub mail: Option, + pub ldap: Option, + pub proxy: Option, +} +impl Config { + pub fn proxy(&self) -> Option<&reqwest::Proxy> { + self.proxy.as_ref().map(|p| &p.proxy) + } +} + +#[derive(Debug, Clone)] +pub enum InvalidRocketConfig { + Env, + Address, + SecretKey, +} + +fn get_rocket_config() -> Result { + let mut c = RocketConfig::active().map_err(|_| InvalidRocketConfig::Env)?; + + let address = var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned()); + let port = var("ROCKET_PORT") + .ok() + .map(|s| s.parse::().unwrap()) + .unwrap_or(7878); + let secret_key = var("ROCKET_SECRET_KEY").map_err(|_| InvalidRocketConfig::SecretKey)?; + let form_size = var("FORM_SIZE") + .unwrap_or_else(|_| "128".to_owned()) + .parse::() + .unwrap(); + let activity_size = var("ACTIVITY_SIZE") + .unwrap_or_else(|_| "1024".to_owned()) + .parse::() + .unwrap(); + + c.set_address(address) + .map_err(|_| InvalidRocketConfig::Address)?; + c.set_port(port); + c.set_secret_key(secret_key) + .map_err(|_| InvalidRocketConfig::SecretKey)?; + + c.set_limits( + Limits::new() + .limit("forms", form_size * 1024) + .limit("json", activity_size * 1024), + ); + + Ok(c) +} + +pub struct LogoConfig { + pub main: String, + pub favicon: String, + pub other: Vec, //url, size, type +} + +#[derive(Serialize)] +pub struct Icon { + pub src: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub sizes: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "type")] + pub image_type: Option, +} + +impl Icon { + pub fn with_prefix(&self, prefix: &str) -> Icon { + Icon { + src: format!("{}/{}", prefix, self.src), + sizes: self.sizes.clone(), + image_type: self.image_type.clone(), + } + } +} + +impl Default for LogoConfig { + fn default() -> Self { + let to_icon = |(src, sizes, image_type): &(&str, Option<&str>, Option<&str>)| Icon { + src: str::to_owned(src), + sizes: sizes.map(str::to_owned), + image_type: image_type.map(str::to_owned), + }; + let icons = [ + ( + "icons/trwnh/feather/plumeFeather48.png", + Some("48x48"), + Some("image/png"), + ), + ( + "icons/trwnh/feather/plumeFeather72.png", + Some("72x72"), + Some("image/png"), + ), + ( + "icons/trwnh/feather/plumeFeather96.png", + Some("96x96"), + Some("image/png"), + ), + ( + "icons/trwnh/feather/plumeFeather144.png", + Some("144x144"), + Some("image/png"), + ), + ( + "icons/trwnh/feather/plumeFeather160.png", + Some("160x160"), + Some("image/png"), + ), + ( + "icons/trwnh/feather/plumeFeather192.png", + Some("192x192"), + Some("image/png"), + ), + ( + "icons/trwnh/feather/plumeFeather256.png", + Some("256x256"), + Some("image/png"), + ), + ( + "icons/trwnh/feather/plumeFeather512.png", + Some("512x512"), + Some("image/png"), + ), + ("icons/trwnh/feather/plumeFeather.svg", None, None), + ] + .iter() + .map(to_icon) + .collect(); + + let custom_main = var("PLUME_LOGO").ok(); + let custom_favicon = var("PLUME_LOGO_FAVICON") + .ok() + .or_else(|| custom_main.clone()); + let other = if let Some(main) = custom_main.clone() { + let ext = |path: &str| match path.rsplit_once('.').map(|x| x.1) { + Some("png") => Some("image/png".to_owned()), + Some("jpg") | Some("jpeg") => Some("image/jpeg".to_owned()), + Some("svg") => Some("image/svg+xml".to_owned()), + Some("webp") => Some("image/webp".to_owned()), + _ => None, + }; + let mut custom_icons = env::vars() + .filter_map(|(var, val)| { + var.strip_prefix("PLUME_LOGO_") + .map(|size| (size.to_owned(), val)) + }) + .filter_map(|(var, val)| var.parse::().ok().map(|var| (var, val))) + .map(|(dim, src)| Icon { + image_type: ext(&src), + src, + sizes: Some(format!("{}x{}", dim, dim)), + }) + .collect::>(); + custom_icons.push(Icon { + image_type: ext(&main), + src: main, + sizes: None, + }); + custom_icons + } else { + icons + }; + + LogoConfig { + main: custom_main + .unwrap_or_else(|| "icons/trwnh/feather/plumeFeather256.png".to_owned()), + favicon: custom_favicon.unwrap_or_else(|| { + "icons/trwnh/feather-filled/plumeFeatherFilled64.png".to_owned() + }), + other, + } + } +} + +pub struct SearchTokenizerConfig { + pub tag_tokenizer: SearchTokenizer, + pub content_tokenizer: SearchTokenizer, + pub property_tokenizer: SearchTokenizer, +} + +impl SearchTokenizerConfig { + pub fn init() -> Self { + use SearchTokenizer::*; + + match var("SEARCH_LANG").ok().as_deref() { + Some("ja") => { + #[cfg(not(feature = "search-lindera"))] + panic!("You need build Plume with search-lindera feature, or execute it with SEARCH_TAG_TOKENIZER=ngram and SEARCH_CONTENT_TOKENIZER=ngram to enable Japanese search feature"); + #[cfg(feature = "search-lindera")] + Self { + tag_tokenizer: Self::determine_tokenizer("SEARCH_TAG_TOKENIZER", Lindera), + content_tokenizer: Self::determine_tokenizer( + "SEARCH_CONTENT_TOKENIZER", + Lindera, + ), + property_tokenizer: Ngram, + } + } + _ => Self { + tag_tokenizer: Self::determine_tokenizer("SEARCH_TAG_TOKENIZER", Whitespace), + content_tokenizer: Self::determine_tokenizer("SEARCH_CONTENT_TOKENIZER", Simple), + property_tokenizer: Ngram, + }, + } + } + + fn determine_tokenizer(var_name: &str, default: SearchTokenizer) -> SearchTokenizer { + use SearchTokenizer::*; + + match var(var_name).ok().as_deref() { + Some("simple") => Simple, + Some("ngram") => Ngram, + Some("whitespace") => Whitespace, + Some("lindera") => { + #[cfg(not(feature = "search-lindera"))] + panic!("You need build Plume with search-lindera feature to use Lindera tokenizer"); + #[cfg(feature = "search-lindera")] + Lindera + } + _ => default, + } + } +} + +pub struct MailConfig { + pub server: String, + pub port: u16, + pub helo_name: String, + pub username: String, + pub password: String, +} + +fn get_mail_config() -> Option { + Some(MailConfig { + server: env::var("MAIL_SERVER").ok()?, + port: env::var("MAIL_PORT").map_or(SUBMISSIONS_PORT, |port| match port.as_str() { + "smtp" => SMTP_PORT, + "submissions" => SUBMISSIONS_PORT, + "submission" => SUBMISSION_PORT, + number => number + .parse() + .expect(r#"MAIL_PORT must be "smtp", "submissions", "submission" or an integer."#), + }), + helo_name: env::var("MAIL_HELO_NAME").unwrap_or_else(|_| "localhost".to_owned()), + username: env::var("MAIL_USER").ok()?, + password: env::var("MAIL_PASSWORD").ok()?, + }) +} + +pub struct LdapConfig { + pub addr: String, + pub base_dn: String, + pub tls: bool, + pub user_name_attr: String, + pub mail_attr: String, +} + +fn get_ldap_config() -> Option { + let addr = var("LDAP_ADDR").ok(); + let base_dn = var("LDAP_BASE_DN").ok(); + match (addr, base_dn) { + (Some(addr), Some(base_dn)) => { + let tls = var("LDAP_TLS").unwrap_or_else(|_| "false".to_owned()); + let tls = match tls.as_ref() { + "1" | "true" | "TRUE" => true, + "0" | "false" | "FALSE" => false, + _ => panic!("Invalid LDAP configuration : tls"), + }; + let user_name_attr = var("LDAP_USER_NAME_ATTR").unwrap_or_else(|_| "cn".to_owned()); + let mail_attr = var("LDAP_USER_MAIL_ATTR").unwrap_or_else(|_| "mail".to_owned()); + Some(LdapConfig { + addr, + base_dn, + tls, + user_name_attr, + mail_attr, + }) + } + (None, None) => None, + (_, _) => { + panic!("Invalid LDAP configuration : both LDAP_ADDR and LDAP_BASE_DN must be set") + } + } +} + +pub struct ProxyConfig { + pub url: reqwest::Url, + pub only_domains: Option>, + pub proxy: reqwest::Proxy, +} + +fn get_proxy_config() -> Option { + let url: reqwest::Url = var("PROXY_URL").ok()?.parse().expect("Invalid PROXY_URL"); + let proxy_url = url.clone(); + let only_domains: Option> = var("PROXY_DOMAINS") + .ok() + .map(|ods| ods.split(',').map(str::to_owned).collect()); + let proxy = if let Some(ref only_domains) = only_domains { + let only_domains = only_domains.clone(); + reqwest::Proxy::custom(move |url| { + if let Some(domain) = url.domain() { + if only_domains.contains(domain) + || only_domains + .iter() + .any(|target| domain.ends_with(&format!(".{}", target))) + { + Some(proxy_url.clone()) + } else { + None + } + } else { + None + } + }) + } else { + reqwest::Proxy::all(proxy_url).expect("Invalid PROXY_URL") + }; + Some(ProxyConfig { + url, + only_domains, + proxy, + }) +} + +lazy_static! { + pub static ref CONFIG: Config = Config { + base_url: var("BASE_URL").unwrap_or_else(|_| format!( + "127.0.0.1:{}", + var("ROCKET_PORT").unwrap_or_else(|_| "7878".to_owned()) + )), + db_name: DB_NAME, + db_max_size: var("DB_MAX_SIZE").map_or(None, |s| Some( + s.parse::() + .expect("Couldn't parse DB_MAX_SIZE into u32") + )), + db_min_idle: var("DB_MIN_IDLE").map_or(None, |s| Some( + s.parse::() + .expect("Couldn't parse DB_MIN_IDLE into u32") + )), + signup: var("SIGNUP").map_or(SignupStrategy::default(), |s| s.parse().unwrap()), + #[cfg(feature = "postgres")] + database_url: var("DATABASE_URL") + .unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME)), + #[cfg(feature = "sqlite")] + database_url: var("DATABASE_URL").unwrap_or_else(|_| format!("{}.sqlite", DB_NAME)), + search_index: var("SEARCH_INDEX").unwrap_or_else(|_| "search_index".to_owned()), + search_tokenizers: SearchTokenizerConfig::init(), + rocket: get_rocket_config(), + logo: LogoConfig::default(), + default_theme: var("DEFAULT_THEME").unwrap_or_else(|_| "default-light".to_owned()), + media_directory: var("MEDIA_UPLOAD_DIRECTORY") + .unwrap_or_else(|_| "static/media".to_owned()), + mail: get_mail_config(), + ldap: get_ldap_config(), + proxy: get_proxy_config(), + }; +} diff --git a/plume-models/src/db_conn.rs b/plume-models/src/db_conn.rs new file mode 100644 index 00000000000..5e461b18f0a --- /dev/null +++ b/plume-models/src/db_conn.rs @@ -0,0 +1,75 @@ +use crate::Connection; +use diesel::r2d2::{ + ConnectionManager, CustomizeConnection, Error as ConnError, Pool, PooledConnection, +}; +#[cfg(feature = "sqlite")] +use diesel::{dsl::sql_query, ConnectionError, RunQueryDsl}; +use rocket::{ + http::Status, + request::{self, FromRequest}, + Outcome, Request, State, +}; +use std::ops::Deref; + +pub type DbPool = Pool>; + +// From rocket documentation + +// Connection request guard type: a wrapper around an r2d2 pooled connection. +pub struct DbConn(pub PooledConnection>); + +/// Attempts to retrieve a single connection from the managed database pool. If +/// no pool is currently managed, fails with an `InternalServerError` status. If +/// no connections are available, fails with a `ServiceUnavailable` status. +impl<'a, 'r> FromRequest<'a, 'r> for DbConn { + type Error = (); + + fn from_request(request: &'a Request<'r>) -> request::Outcome { + let pool = request.guard::>()?; + match pool.get() { + Ok(conn) => Outcome::Success(DbConn(conn)), + Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())), + } + } +} + +// For the convenience of using an &DbConn as an &Connection. +impl Deref for DbConn { + type Target = Connection; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +// Execute a pragma for every new sqlite connection +#[derive(Debug)] +pub struct PragmaForeignKey; +impl CustomizeConnection for PragmaForeignKey { + #[cfg(feature = "sqlite")] // will default to an empty function for postgres + fn on_acquire(&self, conn: &mut Connection) -> Result<(), ConnError> { + sql_query("PRAGMA foreign_keys = on;") + .execute(conn) + .map(|_| ()) + .map_err(|_| { + ConnError::ConnectionError(ConnectionError::BadConnection(String::from( + "PRAGMA foreign_keys = on failed", + ))) + }) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use diesel::Connection as _; + + #[derive(Debug)] + pub struct TestConnectionCustomizer; + impl CustomizeConnection for TestConnectionCustomizer { + fn on_acquire(&self, conn: &mut Connection) -> Result<(), ConnError> { + PragmaForeignKey.on_acquire(conn)?; + Ok(conn.begin_test_transaction().unwrap()) + } + } +} diff --git a/plume-models/src/email_signups.rs b/plume-models/src/email_signups.rs new file mode 100644 index 00000000000..6867e7c708f --- /dev/null +++ b/plume-models/src/email_signups.rs @@ -0,0 +1,143 @@ +use crate::{ + db_conn::DbConn, + schema::email_signups, + users::{NewUser, Role, User}, + Error, Result, +}; +use chrono::{offset::Utc, Duration, NaiveDateTime}; +use diesel::{ + Connection as _, ExpressionMethods, Identifiable, Insertable, QueryDsl, Queryable, RunQueryDsl, +}; +use plume_common::utils::random_hex; +use std::ops::Deref; + +const TOKEN_VALIDITY_HOURS: i64 = 2; + +#[repr(transparent)] +pub struct Token(String); + +impl From for Token { + fn from(string: String) -> Self { + Token(string) + } +} + +impl From for String { + fn from(token: Token) -> Self { + token.0 + } +} + +impl Deref for Token { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Token { + fn generate() -> Self { + Self(random_hex()) + } +} + +#[derive(Identifiable, Queryable)] +pub struct EmailSignup { + pub id: i32, + pub email: String, + pub token: String, + pub expiration_date: NaiveDateTime, +} + +#[derive(Insertable)] +#[table_name = "email_signups"] +pub struct NewEmailSignup<'a> { + pub email: &'a str, + pub token: &'a str, + pub expiration_date: NaiveDateTime, +} + +impl EmailSignup { + pub fn start(conn: &DbConn, email: &str) -> Result { + conn.transaction(|| { + Self::ensure_user_not_exist_by_email(conn, email)?; + let _rows = Self::delete_existings_by_email(conn, email)?; + let token = Token::generate(); + let expiration_date = Utc::now() + .naive_utc() + .checked_add_signed(Duration::hours(TOKEN_VALIDITY_HOURS)) + .expect("could not calculate expiration date"); + let new_signup = NewEmailSignup { + email, + token: &token, + expiration_date, + }; + let _rows = diesel::insert_into(email_signups::table) + .values(new_signup) + .execute(&**conn)?; + + Ok(token) + }) + } + + pub fn find_by_token(conn: &DbConn, token: Token) -> Result { + let signup = email_signups::table + .filter(email_signups::token.eq(token.as_str())) + .first::(&**conn) + .map_err(Error::from)?; + Ok(signup) + } + + pub fn confirm(&self, conn: &DbConn) -> Result<()> { + conn.transaction(|| { + Self::ensure_user_not_exist_by_email(conn, &self.email)?; + if self.expired() { + Self::delete_existings_by_email(conn, &self.email)?; + return Err(Error::Expired); + } + Ok(()) + }) + } + + pub fn complete(&self, conn: &DbConn, username: String, password: String) -> Result { + conn.transaction(|| { + Self::ensure_user_not_exist_by_email(conn, &self.email)?; + let user = NewUser::new_local( + conn, + username, + "".to_string(), + Role::Normal, + "", + self.email.clone(), + Some(User::hash_pass(&password)?), + )?; + self.delete(conn)?; + Ok(user) + }) + } + + fn delete(&self, conn: &DbConn) -> Result<()> { + let _rows = diesel::delete(self).execute(&**conn).map_err(Error::from)?; + Ok(()) + } + + fn ensure_user_not_exist_by_email(conn: &DbConn, email: &str) -> Result<()> { + if User::email_used(conn, email)? { + let _rows = Self::delete_existings_by_email(conn, email)?; + return Err(Error::UserAlreadyExists); + } + Ok(()) + } + + fn delete_existings_by_email(conn: &DbConn, email: &str) -> Result { + let existing_signups = email_signups::table.filter(email_signups::email.eq(email)); + diesel::delete(existing_signups) + .execute(&**conn) + .map_err(Error::from) + } + + fn expired(&self) -> bool { + self.expiration_date < Utc::now().naive_utc() + } +} diff --git a/plume-models/src/follows.rs b/plume-models/src/follows.rs new file mode 100644 index 00000000000..b1e6fdaf311 --- /dev/null +++ b/plume-models/src/follows.rs @@ -0,0 +1,363 @@ +use crate::{ + ap_url, db_conn::DbConn, instance::Instance, notifications::*, schema::follows, users::User, + Connection, Error, Result, CONFIG, +}; +use activitypub::activity::{Accept, Follow as FollowAct, Undo}; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; +use plume_common::activity_pub::{ + broadcast, + inbox::{AsActor, AsObject, FromId}, + sign::Signer, + Id, IntoId, PUBLIC_VISIBILITY, +}; + +#[derive(Clone, Queryable, Identifiable, Associations, AsChangeset)] +#[belongs_to(User, foreign_key = "following_id")] +pub struct Follow { + pub id: i32, + pub follower_id: i32, + pub following_id: i32, + pub ap_url: String, +} + +#[derive(Insertable)] +#[table_name = "follows"] +pub struct NewFollow { + pub follower_id: i32, + pub following_id: i32, + pub ap_url: String, +} + +impl Follow { + insert!( + follows, + NewFollow, + |inserted, conn| if inserted.ap_url.is_empty() { + inserted.ap_url = ap_url(&format!("{}/follows/{}", CONFIG.base_url, inserted.id)); + inserted.save_changes(conn).map_err(Error::from) + } else { + Ok(inserted) + } + ); + get!(follows); + find_by!(follows, find_by_ap_url, ap_url as &str); + + pub fn find(conn: &Connection, from: i32, to: i32) -> Result { + follows::table + .filter(follows::follower_id.eq(from)) + .filter(follows::following_id.eq(to)) + .get_result(conn) + .map_err(Error::from) + } + + pub fn to_activity(&self, conn: &Connection) -> Result { + let user = User::get(conn, self.follower_id)?; + let target = User::get(conn, self.following_id)?; + + let mut act = FollowAct::default(); + act.follow_props.set_actor_link::(user.into_id())?; + act.follow_props + .set_object_link::(target.clone().into_id())?; + act.object_props.set_id_string(self.ap_url.clone())?; + act.object_props.set_to_link_vec(vec![target.into_id()])?; + act.object_props + .set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?; + Ok(act) + } + + 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 + /// target -> The target of the request, responding with Accept + pub fn accept_follow + IntoId, T>( + conn: &Connection, + from: &B, + target: &A, + follow: FollowAct, + from_id: i32, + target_id: i32, + ) -> Result { + let res = Follow::insert( + conn, + NewFollow { + follower_id: from_id, + following_id: target_id, + ap_url: follow.object_props.id_string()?, + }, + )?; + res.notify(conn)?; + + let accept = res.build_accept(from, target, follow)?; + broadcast( + &*target, + accept, + vec![from.clone()], + CONFIG.proxy().cloned(), + ); + Ok(res) + } + + pub fn build_accept + IntoId, T>( + &self, + from: &B, + target: &A, + follow: FollowAct, + ) -> Result { + let mut accept = Accept::default(); + let accept_id = ap_url(&format!( + "{}/follows/{}/accept", + CONFIG.base_url.as_str(), + self.id + )); + accept.object_props.set_id_string(accept_id)?; + accept + .object_props + .set_to_link_vec(vec![from.clone().into_id()])?; + accept + .object_props + .set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?; + accept + .accept_props + .set_actor_link::(target.clone().into_id())?; + accept.accept_props.set_object_object(follow)?; + + Ok(accept) + } + + pub fn build_undo(&self, conn: &Connection) -> Result { + let mut undo = Undo::default(); + undo.undo_props + .set_actor_link(User::get(conn, self.follower_id)?.into_id())?; + undo.object_props + .set_id_string(format!("{}/undo", self.ap_url))?; + undo.undo_props + .set_object_link::(self.clone().into_id())?; + undo.object_props + .set_to_link_vec(vec![User::get(conn, self.following_id)?.into_id()])?; + undo.object_props + .set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?; + Ok(undo) + } +} + +impl AsObject for User { + type Error = Error; + type Output = Follow; + + fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result { + // Mastodon (at least) requires the full Follow object when accepting it, + // so we rebuilt it here + let mut follow = FollowAct::default(); + follow.object_props.set_id_string(id.to_string())?; + follow + .follow_props + .set_actor_link::(actor.clone().into_id())?; + Follow::accept_follow(conn, &actor, &self, follow, actor.id, self.id) + } +} + +impl FromId for Follow { + type Error = Error; + type Object = FollowAct; + + fn from_db(conn: &DbConn, id: &str) -> Result { + Follow::find_by_ap_url(conn, id) + } + + fn from_activity(conn: &DbConn, follow: FollowAct) -> Result { + let actor = User::from_id( + conn, + &follow.follow_props.actor_link::()?, + None, + CONFIG.proxy(), + ) + .map_err(|(_, e)| e)?; + + let target = User::from_id( + conn, + &follow.follow_props.object_link::()?, + None, + CONFIG.proxy(), + ) + .map_err(|(_, e)| e)?; + Follow::accept_follow(conn, &actor, &target, follow, actor.id, target.id) + } + + fn get_sender() -> &'static dyn Signer { + Instance::get_local_instance_user().expect("Failed to local instance user") + } +} + +impl AsObject for Follow { + type Error = Error; + type Output = (); + + fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { + let conn = conn; + if self.follower_id == actor.id { + diesel::delete(&self).execute(&**conn)?; + + // delete associated notification if any + if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) { + diesel::delete(¬if).execute(&**conn)?; + } + + Ok(()) + } else { + Err(Error::Unauthorized) + } + } +} + +impl IntoId for Follow { + fn into_id(self) -> Id { + Id::new(self.ap_url) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{tests::db, users::tests as user_tests, users::tests::fill_database}; + use assert_json_diff::assert_json_eq; + use diesel::Connection; + use serde_json::{json, to_value}; + + fn prepare_activity(conn: &DbConn) -> (Follow, User, User, Vec) { + let users = fill_database(conn); + let following = &users[1]; + let follower = &users[2]; + let mut follow = Follow::insert( + conn, + NewFollow { + follower_id: follower.id, + following_id: following.id, + ap_url: "".into(), + }, + ) + .unwrap(); + // following.ap_url = format!("https://plu.me/follows/{}", follow.id); + follow.ap_url = format!("https://plu.me/follows/{}", follow.id); + + (follow, following.to_owned(), follower.to_owned(), users) + } + + #[test] + fn test_id() { + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + let users = user_tests::fill_database(&conn); + let follow = Follow::insert( + &conn, + NewFollow { + follower_id: users[0].id, + following_id: users[1].id, + ap_url: String::new(), + }, + ) + .expect("Couldn't insert new follow"); + assert_eq!( + follow.ap_url, + format!("https://{}/follows/{}", CONFIG.base_url, follow.id) + ); + + let follow = Follow::insert( + &conn, + NewFollow { + follower_id: users[1].id, + following_id: users[0].id, + ap_url: String::from("https://some.url/"), + }, + ) + .expect("Couldn't insert new follow"); + assert_eq!(follow.ap_url, String::from("https://some.url/")); + + Ok(()) + }) + } + + #[test] + fn to_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (follow, _following, _follower, _users) = prepare_activity(&conn); + let act = follow.to_activity(&conn)?; + + let expected = json!({ + "actor": "https://plu.me/@/other/", + "cc": ["https://www.w3.org/ns/activitystreams#Public"], + "id": format!("https://plu.me/follows/{}", follow.id), + "object": "https://plu.me/@/user/", + "to": ["https://plu.me/@/user/"], + "type": "Follow" + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } + + #[test] + fn build_accept() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (follow, following, follower, _users) = prepare_activity(&conn); + let act = follow.build_accept(&follower, &following, follow.to_activity(&conn)?)?; + + let expected = json!({ + "actor": "https://plu.me/@/user/", + "cc": ["https://www.w3.org/ns/activitystreams#Public"], + "id": format!("https://127.0.0.1:7878/follows/{}/accept", follow.id), + "object": { + "actor": "https://plu.me/@/other/", + "cc": ["https://www.w3.org/ns/activitystreams#Public"], + "id": format!("https://plu.me/follows/{}", follow.id), + "object": "https://plu.me/@/user/", + "to": ["https://plu.me/@/user/"], + "type": "Follow" + }, + "to": ["https://plu.me/@/other/"], + "type": "Accept" + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } + + #[test] + fn build_undo() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (follow, _following, _follower, _users) = prepare_activity(&conn); + let act = follow.build_undo(&conn)?; + + let expected = json!({ + "actor": "https://plu.me/@/other/", + "cc": ["https://www.w3.org/ns/activitystreams#Public"], + "id": format!("https://plu.me/follows/{}/undo", follow.id), + "object": format!("https://plu.me/follows/{}", follow.id), + "to": ["https://plu.me/@/user/"], + "type": "Undo" + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } +} diff --git a/plume-models/src/headers.rs b/plume-models/src/headers.rs new file mode 100644 index 00000000000..0d5d29231ca --- /dev/null +++ b/plume-models/src/headers.rs @@ -0,0 +1,29 @@ +use rocket::request::{self, FromRequest, Request}; +use rocket::{ + http::{Header, HeaderMap}, + Outcome, +}; + +pub struct Headers<'r>(pub HeaderMap<'r>); + +impl<'a, 'r> FromRequest<'a, 'r> for Headers<'r> { + type Error = (); + + fn from_request(request: &'a Request<'r>) -> request::Outcome { + let mut headers = HeaderMap::new(); + for header in request.headers().clone().into_iter() { + headers.add(header); + } + let ori = request.uri(); + let uri = if let Some(query) = ori.query() { + format!("{}?{}", ori.path(), query) + } else { + ori.path().to_owned() + }; + headers.add(Header::new( + "(request-target)", + format!("{} {}", request.method().as_str().to_lowercase(), uri), + )); + Outcome::Success(Headers(headers)) + } +} diff --git a/plume-models/src/inbox.rs b/plume-models/src/inbox.rs new file mode 100644 index 00000000000..21af9d88ff2 --- /dev/null +++ b/plume-models/src/inbox.rs @@ -0,0 +1,688 @@ +use activitypub::activity::*; + +use crate::{ + comments::Comment, + db_conn::DbConn, + follows, likes, + posts::{Post, PostUpdate}, + reshares::Reshare, + users::User, + Error, CONFIG, +}; +use plume_common::activity_pub::inbox::Inbox; + +macro_rules! impl_into_inbox_result { + ( $( $t:ty => $variant:ident ),+ ) => { + $( + impl From<$t> for InboxResult { + fn from(x: $t) -> InboxResult { + InboxResult::$variant(x) + } + } + )+ + } +} + +pub enum InboxResult { + Commented(Comment), + Followed(follows::Follow), + Liked(likes::Like), + Other, + Post(Post), + Reshared(Reshare), +} + +impl From<()> for InboxResult { + fn from(_: ()) -> InboxResult { + InboxResult::Other + } +} + +impl_into_inbox_result! { + Comment => Commented, + follows::Follow => Followed, + likes::Like => Liked, + Post => Post, + Reshare => Reshared +} + +pub fn inbox(conn: &DbConn, act: serde_json::Value) -> Result { + Inbox::handle(conn, act) + .with::(CONFIG.proxy()) + .with::(CONFIG.proxy()) + .with::(CONFIG.proxy()) + .with::(CONFIG.proxy()) + .with::(CONFIG.proxy()) + .with::(CONFIG.proxy()) + .with::(CONFIG.proxy()) + .with::(CONFIG.proxy()) + .with::(CONFIG.proxy()) + .with::(CONFIG.proxy()) + .with::(CONFIG.proxy()) + .with::(CONFIG.proxy()) + .done() +} + +#[cfg(test)] +pub(crate) mod tests { + use super::InboxResult; + use crate::blogs::tests::fill_database as blog_fill_db; + use crate::db_conn::DbConn; + use crate::safe_string::SafeString; + use crate::tests::db; + use diesel::Connection; + + pub fn fill_database( + conn: &DbConn, + ) -> ( + Vec, + Vec, + Vec, + ) { + use crate::post_authors::*; + use crate::posts::*; + + let (users, blogs) = blog_fill_db(&conn); + let post = Post::insert( + &conn, + NewPost { + blog_id: blogs[0].id, + slug: "testing".to_owned(), + title: "Testing".to_owned(), + content: crate::safe_string::SafeString::new("Hello"), + published: true, + license: "WTFPL".to_owned(), + creation_date: None, + ap_url: format!("https://plu.me/~/{}/testing", blogs[0].actor_id), + subtitle: String::new(), + source: String::new(), + cover_id: None, + }, + ) + .unwrap(); + + PostAuthor::insert( + &conn, + NewPostAuthor { + post_id: post.id, + author_id: users[0].id, + }, + ) + .unwrap(); + + (vec![post], users, blogs) + } + + #[test] + fn announce_post() { + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + let (posts, users, _) = fill_database(&conn); + let act = json!({ + "id": "https://plu.me/announce/1", + "actor": users[0].ap_url, + "object": posts[0].ap_url, + "type": "Announce", + }); + + match super::inbox(&conn, act).unwrap() { + super::InboxResult::Reshared(r) => { + assert_eq!(r.post_id, posts[0].id); + assert_eq!(r.user_id, users[0].id); + assert_eq!(r.ap_url, "https://plu.me/announce/1".to_owned()); + } + _ => panic!("Unexpected result"), + }; + Ok(()) + }); + } + + #[test] + fn create_comment() { + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + let (posts, users, _) = fill_database(&conn); + let act = json!({ + "id": "https://plu.me/comment/1/activity", + "actor": users[0].ap_url, + "object": { + "type": "Note", + "id": "https://plu.me/comment/1", + "attributedTo": users[0].ap_url, + "inReplyTo": posts[0].ap_url, + "content": "Hello.", + "to": [plume_common::activity_pub::PUBLIC_VISIBILITY] + }, + "type": "Create", + }); + + match super::inbox(&conn, act).unwrap() { + super::InboxResult::Commented(c) => { + assert_eq!(c.author_id, users[0].id); + assert_eq!(c.post_id, posts[0].id); + assert_eq!(c.in_response_to_id, None); + assert_eq!(c.content, SafeString::new("Hello.")); + assert!(c.public_visibility); + } + _ => panic!("Unexpected result"), + }; + Ok(()) + }); + } + + #[test] + fn spoof_comment() { + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + let (posts, users, _) = fill_database(&conn); + let act = json!({ + "id": "https://plu.me/comment/1/activity", + "actor": users[0].ap_url, + "object": { + "type": "Note", + "id": "https://plu.me/comment/1", + "attributedTo": users[1].ap_url, + "inReplyTo": posts[0].ap_url, + "content": "Hello.", + "to": [plume_common::activity_pub::PUBLIC_VISIBILITY] + }, + "type": "Create", + }); + + assert!(matches!( + super::inbox(&conn, act.clone()), + Err(super::Error::Inbox( + box plume_common::activity_pub::inbox::InboxError::InvalidObject(_), + )) + )); + Ok(()) + }); + } + + #[test] + fn spoof_comment_by_object_with_id() { + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + let (posts, users, _) = fill_database(&conn); + let act = json!({ + "id": "https://plu.me/comment/1/activity", + "actor": users[0].ap_url, + "object": { + "type": "Note", + "id": "https://plu.me/comment/1", + "attributedTo": { + "id": users[1].ap_url + }, + "inReplyTo": posts[0].ap_url, + "content": "Hello.", + "to": [plume_common::activity_pub::PUBLIC_VISIBILITY] + }, + "type": "Create", + }); + + assert!(matches!( + super::inbox(&conn, act.clone()), + Err(super::Error::Inbox( + box plume_common::activity_pub::inbox::InboxError::InvalidObject(_), + )) + )); + Ok(()) + }); + } + #[test] + fn spoof_comment_by_object_without_id() { + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + let (posts, users, _) = fill_database(&conn); + let act = json!({ + "id": "https://plu.me/comment/1/activity", + "actor": users[0].ap_url, + "object": { + "type": "Note", + "id": "https://plu.me/comment/1", + "attributedTo": {}, + "inReplyTo": posts[0].ap_url, + "content": "Hello.", + "to": [plume_common::activity_pub::PUBLIC_VISIBILITY] + }, + "type": "Create", + }); + + assert!(matches!( + super::inbox(&conn, act.clone()), + Err(super::Error::Inbox( + box plume_common::activity_pub::inbox::InboxError::InvalidObject(_), + )) + )); + Ok(()) + }); + } + + #[test] + fn create_post() { + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + let (_, users, blogs) = fill_database(&conn); + let act = json!({ + "id": "https://plu.me/comment/1/activity", + "actor": users[0].ap_url, + "object": { + "type": "Article", + "id": "https://plu.me/~/Blog/my-article", + "attributedTo": [users[0].ap_url, blogs[0].ap_url], + "content": "Hello.", + "name": "My Article", + "summary": "Bye.", + "source": { + "content": "Hello.", + "mediaType": "text/markdown" + }, + "published": "2014-12-12T12:12:12Z", + "to": [plume_common::activity_pub::PUBLIC_VISIBILITY] + }, + "type": "Create", + }); + + match super::inbox(&conn, act).unwrap() { + super::InboxResult::Post(p) => { + assert!(p.is_author(&conn, users[0].id).unwrap()); + assert_eq!(p.source, "Hello.".to_owned()); + assert_eq!(p.blog_id, blogs[0].id); + assert_eq!(p.content, SafeString::new("Hello.")); + assert_eq!(p.subtitle, "Bye.".to_owned()); + assert_eq!(p.title, "My Article".to_owned()); + } + _ => panic!("Unexpected result"), + }; + Ok(()) + }); + } + + #[test] + fn spoof_post() { + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + let (_, users, blogs) = fill_database(&conn); + let act = json!({ + "id": "https://plu.me/comment/1/activity", + "actor": users[0].ap_url, + "object": { + "type": "Article", + "id": "https://plu.me/~/Blog/my-article", + "attributedTo": [users[1].ap_url, blogs[0].ap_url], + "content": "Hello.", + "name": "My Article", + "summary": "Bye.", + "source": { + "content": "Hello.", + "mediaType": "text/markdown" + }, + "published": "2014-12-12T12:12:12Z", + "to": [plume_common::activity_pub::PUBLIC_VISIBILITY] + }, + "type": "Create", + }); + + assert!(matches!( + super::inbox(&conn, act.clone()), + Err(super::Error::Inbox( + box plume_common::activity_pub::inbox::InboxError::InvalidObject(_), + )) + )); + Ok(()) + }); + } + + #[test] + fn spoof_post_by_object_with_id() { + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + let (_, users, blogs) = fill_database(&conn); + let act = json!({ + "id": "https://plu.me/comment/1/activity", + "actor": users[0].ap_url, + "object": { + "type": "Article", + "id": "https://plu.me/~/Blog/my-article", + "attributedTo": [ + {"id": users[1].ap_url}, + blogs[0].ap_url + ], + "content": "Hello.", + "name": "My Article", + "summary": "Bye.", + "source": { + "content": "Hello.", + "mediaType": "text/markdown" + }, + "published": "2014-12-12T12:12:12Z", + "to": [plume_common::activity_pub::PUBLIC_VISIBILITY] + }, + "type": "Create", + }); + + assert!(matches!( + super::inbox(&conn, act.clone()), + Err(super::Error::Inbox( + box plume_common::activity_pub::inbox::InboxError::InvalidObject(_), + )) + )); + Ok(()) + }); + } + + #[test] + fn spoof_post_by_object_without_id() { + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + let (_, users, blogs) = fill_database(&conn); + let act = json!({ + "id": "https://plu.me/comment/1/activity", + "actor": users[0].ap_url, + "object": { + "type": "Article", + "id": "https://plu.me/~/Blog/my-article", + "attributedTo": [{}, blogs[0].ap_url], + "content": "Hello.", + "name": "My Article", + "summary": "Bye.", + "source": { + "content": "Hello.", + "mediaType": "text/markdown" + }, + "published": "2014-12-12T12:12:12Z", + "to": [plume_common::activity_pub::PUBLIC_VISIBILITY] + }, + "type": "Create", + }); + + assert!(matches!( + super::inbox(&conn, act.clone()), + Err(super::Error::Inbox( + box plume_common::activity_pub::inbox::InboxError::InvalidObject(_), + )) + )); + Ok(()) + }); + } + + #[test] + fn delete_comment() { + use crate::comments::*; + + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + let (posts, users, _) = fill_database(&conn); + Comment::insert( + &conn, + NewComment { + content: SafeString::new("My comment"), + in_response_to_id: None, + post_id: posts[0].id, + author_id: users[0].id, + ap_url: Some("https://plu.me/comment/1".to_owned()), + sensitive: false, + spoiler_text: "spoiler".to_owned(), + public_visibility: true, + }, + ) + .unwrap(); + + let fail_act = json!({ + "id": "https://plu.me/comment/1/delete", + "actor": users[1].ap_url, // Not the author of the comment, it should fail + "object": "https://plu.me/comment/1", + "type": "Delete", + }); + assert!(super::inbox(&conn, fail_act).is_err()); + + let ok_act = json!({ + "id": "https://plu.me/comment/1/delete", + "actor": users[0].ap_url, + "object": "https://plu.me/comment/1", + "type": "Delete", + }); + assert!(super::inbox(&conn, ok_act).is_ok()); + Ok(()) + }) + } + + #[test] + fn delete_post() { + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + let (posts, users, _) = fill_database(&conn); + + let fail_act = json!({ + "id": "https://plu.me/comment/1/delete", + "actor": users[1].ap_url, // Not the author of the post, it should fail + "object": posts[0].ap_url, + "type": "Delete", + }); + assert!(super::inbox(&conn, fail_act).is_err()); + + let ok_act = json!({ + "id": "https://plu.me/comment/1/delete", + "actor": users[0].ap_url, + "object": posts[0].ap_url, + "type": "Delete", + }); + assert!(super::inbox(&conn, ok_act).is_ok()); + Ok(()) + }); + } + + #[test] + fn delete_user() { + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + let (_, users, _) = fill_database(&conn); + + 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(&conn, 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(&conn, ok_act).is_ok()); + assert!(crate::users::User::get(&conn, users[0].id).is_err()); + + Ok(()) + }); + } + + #[test] + fn follow() { + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + let (_, users, _) = fill_database(&conn); + + let act = json!({ + "id": "https://plu.me/follow/1", + "actor": users[0].ap_url, + "object": users[1].ap_url, + "type": "Follow", + }); + match super::inbox(&conn, act).unwrap() { + InboxResult::Followed(f) => { + assert_eq!(f.follower_id, users[0].id); + assert_eq!(f.following_id, users[1].id); + assert_eq!(f.ap_url, "https://plu.me/follow/1".to_owned()); + } + _ => panic!("Unexpected result"), + } + Ok(()) + }); + } + + #[test] + fn like() { + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + let (posts, users, _) = fill_database(&conn); + + let act = json!({ + "id": "https://plu.me/like/1", + "actor": users[1].ap_url, + "object": posts[0].ap_url, + "type": "Like", + }); + match super::inbox(&conn, act).unwrap() { + InboxResult::Liked(l) => { + assert_eq!(l.user_id, users[1].id); + assert_eq!(l.post_id, posts[0].id); + assert_eq!(l.ap_url, "https://plu.me/like/1".to_owned()); + } + _ => panic!("Unexpected result"), + } + Ok(()) + }); + } + + #[test] + fn undo_reshare() { + use crate::reshares::*; + + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + let (posts, users, _) = fill_database(&conn); + + let announce = Reshare::insert( + &conn, + NewReshare { + post_id: posts[0].id, + user_id: users[1].id, + ap_url: "https://plu.me/announce/1".to_owned(), + }, + ) + .unwrap(); + + let fail_act = json!({ + "id": "https://plu.me/undo/1", + "actor": users[0].ap_url, + "object": announce.ap_url, + "type": "Undo", + }); + assert!(super::inbox(&conn, fail_act).is_err()); + + let ok_act = json!({ + "id": "https://plu.me/undo/1", + "actor": users[1].ap_url, + "object": announce.ap_url, + "type": "Undo", + }); + assert!(super::inbox(&conn, ok_act).is_ok()); + Ok(()) + }); + } + + #[test] + fn undo_follow() { + use crate::follows::*; + + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + let (_, users, _) = fill_database(&conn); + + let follow = Follow::insert( + &conn, + NewFollow { + follower_id: users[0].id, + following_id: users[1].id, + ap_url: "https://plu.me/follow/1".to_owned(), + }, + ) + .unwrap(); + + let fail_act = json!({ + "id": "https://plu.me/undo/1", + "actor": users[2].ap_url, + "object": follow.ap_url, + "type": "Undo", + }); + assert!(super::inbox(&conn, fail_act).is_err()); + + let ok_act = json!({ + "id": "https://plu.me/undo/1", + "actor": users[0].ap_url, + "object": follow.ap_url, + "type": "Undo", + }); + assert!(super::inbox(&conn, ok_act).is_ok()); + Ok(()) + }); + } + + #[test] + fn undo_like() { + use crate::likes::*; + + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + let (posts, users, _) = fill_database(&conn); + + let like = Like::insert( + &conn, + NewLike { + post_id: posts[0].id, + user_id: users[1].id, + ap_url: "https://plu.me/like/1".to_owned(), + }, + ) + .unwrap(); + + let fail_act = json!({ + "id": "https://plu.me/undo/1", + "actor": users[0].ap_url, + "object": like.ap_url, + "type": "Undo", + }); + assert!(super::inbox(&conn, fail_act).is_err()); + + let ok_act = json!({ + "id": "https://plu.me/undo/1", + "actor": users[1].ap_url, + "object": like.ap_url, + "type": "Undo", + }); + assert!(super::inbox(&conn, ok_act).is_ok()); + Ok(()) + }); + } + + #[test] + fn update_post() { + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + let (posts, users, _) = fill_database(&conn); + + let act = json!({ + "id": "https://plu.me/update/1", + "actor": users[0].ap_url, + "object": { + "type": "Article", + "id": posts[0].ap_url, + "name": "Mia Artikolo", + "summary": "Jes, mi parolas esperanton nun", + "content": "Saluton, mi skribas testojn", + "source": { + "mediaType": "text/markdown", + "content": "**Saluton**, mi skribas testojn" + }, + }, + "type": "Update", + }); + + super::inbox(&conn, act).unwrap(); + Ok(()) + }); + } +} diff --git a/plume-models/src/instance.rs b/plume-models/src/instance.rs new file mode 100644 index 00000000000..86cb3d46262 --- /dev/null +++ b/plume-models/src/instance.rs @@ -0,0 +1,546 @@ +use crate::{ + ap_url, + medias::Media, + safe_string::SafeString, + schema::{instances, users}, + users::{NewUser, Role, User}, + Connection, Error, Result, +}; +use chrono::NaiveDateTime; +use diesel::{self, result::Error::NotFound, ExpressionMethods, QueryDsl, RunQueryDsl}; +use once_cell::sync::OnceCell; +use plume_common::utils::md_to_html; +use std::sync::RwLock; + +#[derive(Clone, Identifiable, Queryable)] +pub struct Instance { + pub id: i32, + pub public_domain: String, + pub name: String, + pub local: bool, + pub blocked: bool, + pub creation_date: NaiveDateTime, + pub open_registrations: bool, + pub short_description: SafeString, + pub long_description: SafeString, + pub default_license: String, + pub long_description_html: SafeString, + pub short_description_html: SafeString, +} + +#[derive(Clone, Insertable)] +#[table_name = "instances"] +pub struct NewInstance { + pub public_domain: String, + pub name: String, + pub local: bool, + pub open_registrations: bool, + pub short_description: SafeString, + pub long_description: SafeString, + pub default_license: String, + pub long_description_html: String, + pub short_description_html: String, +} + +lazy_static! { + static ref LOCAL_INSTANCE: RwLock> = RwLock::new(None); +} + +const LOCAL_INSTANCE_USERNAME: &str = "__instance__"; +static LOCAL_INSTANCE_USER: OnceCell = OnceCell::new(); + +impl Instance { + pub fn set_local(self) { + LOCAL_INSTANCE.write().unwrap().replace(self); + } + + pub fn get_local() -> Result { + LOCAL_INSTANCE + .read() + .unwrap() + .clone() + .ok_or(Error::NotFound) + } + + pub fn get_local_uncached(conn: &Connection) -> Result { + instances::table + .filter(instances::local.eq(true)) + .first(conn) + .map_err(Error::from) + } + + pub fn cache_local(conn: &Connection) { + *LOCAL_INSTANCE.write().unwrap() = Instance::get_local_uncached(conn).ok(); + } + + pub fn get_remotes(conn: &Connection) -> Result> { + instances::table + .filter(instances::local.eq(false)) + .load::(conn) + .map_err(Error::from) + } + + pub fn create_local_instance_user(conn: &Connection) -> Result { + let instance = Instance::get_local()?; + let email = format!("{}@{}", LOCAL_INSTANCE_USERNAME, &instance.public_domain); + NewUser::new_local( + conn, + LOCAL_INSTANCE_USERNAME.into(), + instance.public_domain, + Role::Instance, + "Local instance", + email, + None, + ) + } + + pub fn get_local_instance_user() -> Option<&'static User> { + LOCAL_INSTANCE_USER.get() + } + + pub fn get_local_instance_user_uncached(conn: &Connection) -> Result { + users::table + .filter(users::role.eq(3)) + .first(conn) + .or_else(|err| match err { + NotFound => Self::create_local_instance_user(conn), + _ => Err(Error::Db(err)), + }) + } + + pub fn cache_local_instance_user(conn: &Connection) { + let _ = LOCAL_INSTANCE_USER.get_or_init(|| { + Self::get_local_instance_user_uncached(conn) + .or_else(|_| Self::create_local_instance_user(conn)) + .expect("Failed to cache local instance user") + }); + } + + pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Result> { + instances::table + .order(instances::public_domain.asc()) + .offset(min.into()) + .limit((max - min).into()) + .load::(conn) + .map_err(Error::from) + } + + insert!(instances, NewInstance); + get!(instances); + find_by!(instances, find_by_domain, public_domain as &str); + + pub fn toggle_block(&self, conn: &Connection) -> Result<()> { + diesel::update(self) + .set(instances::blocked.eq(!self.blocked)) + .execute(conn) + .map(|_| ()) + .map_err(Error::from) + } + + /// id: AP object id + pub fn is_blocked(conn: &Connection, id: &str) -> Result { + for block in instances::table + .filter(instances::blocked.eq(true)) + .get_results::(conn)? + { + if id.starts_with(&format!("https://{}/", block.public_domain)) { + return Ok(true); + } + } + + Ok(false) + } + + pub fn has_admin(&self, conn: &Connection) -> Result { + users::table + .filter(users::instance_id.eq(self.id)) + .filter(users::role.eq(Role::Admin as i32)) + .load::(conn) + .map_err(Error::from) + .map(|r| !r.is_empty()) + } + + pub fn main_admin(&self, conn: &Connection) -> Result { + users::table + .filter(users::instance_id.eq(self.id)) + .filter(users::role.eq(Role::Admin as i32)) + .first(conn) + .map_err(Error::from) + } + + pub fn compute_box(&self, prefix: &str, name: &str, box_name: &str) -> String { + ap_url(&format!( + "{instance}/{prefix}/{name}/{box_name}", + instance = self.public_domain, + prefix = prefix, + name = name, + box_name = box_name + )) + } + + pub fn update( + &self, + conn: &Connection, + name: String, + open_registrations: bool, + short_description: SafeString, + long_description: SafeString, + default_license: String, + ) -> Result<()> { + let (sd, _, _) = md_to_html( + short_description.as_ref(), + Some(&self.public_domain), + true, + Some(Media::get_media_processor(conn, vec![])), + ); + let (ld, _, _) = md_to_html( + long_description.as_ref(), + Some(&self.public_domain), + false, + Some(Media::get_media_processor(conn, vec![])), + ); + let res = diesel::update(self) + .set(( + instances::name.eq(name), + instances::open_registrations.eq(open_registrations), + instances::short_description.eq(short_description), + instances::long_description.eq(long_description), + instances::short_description_html.eq(sd), + instances::long_description_html.eq(ld), + instances::default_license.eq(default_license), + )) + .execute(conn) + .map(|_| ()) + .map_err(Error::from); + if self.local { + Instance::cache_local(conn); + } + res + } + + pub fn count(conn: &Connection) -> Result { + instances::table + .count() + .get_result(conn) + .map_err(Error::from) + } + + /// Returns a list of the local instance themes (all files matching `static/css/NAME/theme.css`) + /// + /// The list only contains the name of the themes, without their extension or full path. + pub fn list_themes() -> Result> { + // List all the files in static/css + std::path::Path::new("static") + .join("css") + .read_dir() + .map(|files| { + files + .filter_map(std::result::Result::ok) + // Only keep actual directories (each theme has its own dir) + .filter(|f| f.file_type().map(|t| t.is_dir()).unwrap_or(false)) + // Only keep the directory name (= theme name) + .filter_map(|f| { + f.path() + .file_name() + .and_then(std::ffi::OsStr::to_str) + .map(std::borrow::ToOwned::to_owned) + }) + // Ignore the one starting with "blog-": these are the blog themes + .filter(|f| !f.starts_with("blog-")) + .collect() + }) + .map_err(Error::from) + } + + /// Returns a list of the local blog themes (all files matching `static/css/blog-NAME/theme.css`) + /// + /// The list only contains the name of the themes, without their extension or full path. + pub fn list_blog_themes() -> Result> { + // List all the files in static/css + std::path::Path::new("static") + .join("css") + .read_dir() + .map(|files| { + files + .filter_map(std::result::Result::ok) + // Only keep actual directories (each theme has its own dir) + .filter(|f| f.file_type().map(|t| t.is_dir()).unwrap_or(false)) + // Only keep the directory name (= theme name) + .filter_map(|f| { + f.path() + .file_name() + .and_then(std::ffi::OsStr::to_str) + .map(std::borrow::ToOwned::to_owned) + }) + // Only keep the one starting with "blog-": these are the blog themes + .filter(|f| f.starts_with("blog-")) + .collect() + }) + .map_err(Error::from) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::{tests::db, Connection as Conn}; + use diesel::Connection; + + pub(crate) fn fill_database(conn: &Conn) -> Vec<(NewInstance, Instance)> { + diesel::delete(instances::table).execute(conn).unwrap(); + let res = vec![ + NewInstance { + default_license: "WTFPL".to_string(), + local: true, + long_description: SafeString::new("This is my instance."), + long_description_html: "

This is my instance

".to_string(), + short_description: SafeString::new("My instance."), + short_description_html: "

My instance

".to_string(), + name: "My instance".to_string(), + open_registrations: true, + public_domain: "plu.me".to_string(), + }, + NewInstance { + default_license: "WTFPL".to_string(), + local: false, + long_description: SafeString::new("This is an instance."), + long_description_html: "

This is an instance

".to_string(), + short_description: SafeString::new("An instance."), + short_description_html: "

An instance

".to_string(), + name: "An instance".to_string(), + open_registrations: true, + public_domain: "1plu.me".to_string(), + }, + NewInstance { + default_license: "CC-0".to_string(), + local: false, + long_description: SafeString::new("This is the instance of someone."), + long_description_html: "

This is the instance of someone

".to_string(), + short_description: SafeString::new("Someone instance."), + short_description_html: "

Someone instance

".to_string(), + name: "Someone instance".to_string(), + open_registrations: false, + public_domain: "2plu.me".to_string(), + }, + NewInstance { + default_license: "CC-0-BY-SA".to_string(), + local: false, + long_description: SafeString::new("Good morning"), + long_description_html: "

Good morning

".to_string(), + short_description: SafeString::new("Hello"), + short_description_html: "

Hello

".to_string(), + name: "Nice day".to_string(), + open_registrations: true, + public_domain: "3plu.me".to_string(), + }, + ] + .into_iter() + .map(|inst| { + ( + inst.clone(), + Instance::find_by_domain(conn, &inst.public_domain) + .unwrap_or_else(|_| Instance::insert(conn, inst).unwrap()), + ) + }) + .collect(); + Instance::cache_local(conn); + Instance::cache_local_instance_user(conn); + res + } + + #[test] + fn local_instance() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let inserted = fill_database(conn) + .into_iter() + .map(|(inserted, _)| inserted) + .find(|inst| inst.local) + .unwrap(); + let res = Instance::get_local().unwrap(); + + part_eq!( + res, + inserted, + [ + default_license, + local, + long_description, + short_description, + name, + open_registrations, + public_domain + ] + ); + assert_eq!( + res.long_description_html.get(), + &inserted.long_description_html + ); + assert_eq!( + res.short_description_html.get(), + &inserted.short_description_html + ); + Ok(()) + }); + } + + #[test] + fn remote_instance() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let inserted = fill_database(conn); + assert_eq!(Instance::count(conn).unwrap(), inserted.len() as i64); + + let res = Instance::get_remotes(conn).unwrap(); + assert_eq!( + res.len(), + inserted.iter().filter(|(inst, _)| !inst.local).count() + ); + + inserted + .iter() + .filter(|(newinst, _)| !newinst.local) + .map(|(newinst, inst)| (newinst, res.iter().find(|res| res.id == inst.id).unwrap())) + .for_each(|(newinst, inst)| { + part_eq!( + newinst, + inst, + [ + default_license, + local, + long_description, + short_description, + name, + open_registrations, + public_domain + ] + ); + assert_eq!( + &newinst.long_description_html, + inst.long_description_html.get() + ); + assert_eq!( + &newinst.short_description_html, + inst.short_description_html.get() + ); + }); + + let page = Instance::page(conn, (0, 2)).unwrap(); + assert_eq!(page.len(), 2); + let page1 = &page[0]; + let page2 = &page[1]; + assert!(page1.public_domain <= page2.public_domain); + + let mut last_domaine: String = Instance::page(conn, (0, 1)).unwrap()[0] + .public_domain + .clone(); + for i in 1..inserted.len() as i32 { + let page = Instance::page(conn, (i, i + 1)).unwrap(); + assert_eq!(page.len(), 1); + assert!(last_domaine <= page[0].public_domain); + last_domaine = page[0].public_domain.clone(); + } + Ok(()) + }); + } + + #[test] + fn blocked() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let inst_list = fill_database(conn); + let inst = &inst_list[0].1; + let inst_list = &inst_list[1..]; + + let blocked = inst.blocked; + inst.toggle_block(conn).unwrap(); + let inst = Instance::get(conn, inst.id).unwrap(); + assert_eq!(inst.blocked, !blocked); + assert_eq!( + inst_list + .iter() + .filter( + |(_, inst)| inst.blocked != Instance::get(conn, inst.id).unwrap().blocked + ) + .count(), + 0 + ); + assert_eq!( + Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain)) + .unwrap(), + inst.blocked + ); + assert_eq!( + Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain)) + .unwrap(), + Instance::find_by_domain(conn, &format!("{}a", inst.public_domain)) + .map(|inst| inst.blocked) + .unwrap_or(false) + ); + + inst.toggle_block(conn).unwrap(); + let inst = Instance::get(conn, inst.id).unwrap(); + assert_eq!(inst.blocked, blocked); + assert_eq!( + Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain)) + .unwrap(), + inst.blocked + ); + assert_eq!( + Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain)) + .unwrap(), + Instance::find_by_domain(conn, &format!("{}a", inst.public_domain)) + .map(|inst| inst.blocked) + .unwrap_or(false) + ); + assert_eq!( + inst_list + .iter() + .filter( + |(_, inst)| inst.blocked != Instance::get(conn, inst.id).unwrap().blocked + ) + .count(), + 0 + ); + Ok(()) + }); + } + + #[test] + fn update() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let inst = &fill_database(conn)[0].1; + + inst.update( + conn, + "NewName".to_owned(), + false, + SafeString::new("[short](#link)"), + SafeString::new("[long_description](/with_link)"), + "CC-BY-SAO".to_owned(), + ) + .unwrap(); + let inst = Instance::get(conn, inst.id).unwrap(); + assert_eq!(inst.name, "NewName".to_owned()); + assert_eq!(inst.open_registrations, false); + assert_eq!( + inst.long_description.get(), + "[long_description](/with_link)" + ); + assert_eq!( + inst.long_description_html, + SafeString::new( + "

long_description

\n" + ) + ); + assert_eq!(inst.short_description.get(), "[short](#link)"); + assert_eq!( + inst.short_description_html, + SafeString::new("

short

\n") + ); + assert_eq!(inst.default_license, "CC-BY-SAO".to_owned()); + Ok(()) + }); + } +} diff --git a/plume-models/src/lib.rs b/plume-models/src/lib.rs new file mode 100644 index 00000000000..546d5997a0e --- /dev/null +++ b/plume-models/src/lib.rs @@ -0,0 +1,433 @@ +#![feature(never_type)] +#![feature(proc_macro_hygiene)] +#![feature(box_patterns)] + +#[macro_use] +extern crate diesel; +#[macro_use] +extern crate lazy_static; +#[macro_use] +extern crate plume_macro; +#[macro_use] +extern crate rocket; +extern crate serde_derive; +#[macro_use] +extern crate serde_json; +#[macro_use] +extern crate tantivy; + +pub use lettre; +pub use lettre::smtp; +use once_cell::sync::Lazy; +use plume_common::activity_pub::{inbox::InboxError, request, sign}; +use posts::PostEvent; +use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder}; +use users::UserEvent; + +#[cfg(not(any(feature = "sqlite", feature = "postgres")))] +compile_error!("Either feature \"sqlite\" or \"postgres\" must be enabled for this crate."); +#[cfg(all(feature = "sqlite", feature = "postgres"))] +compile_error!("Either feature \"sqlite\" or \"postgres\" must be enabled for this crate."); + +#[cfg(all(feature = "sqlite", not(feature = "postgres")))] +pub type Connection = diesel::SqliteConnection; + +#[cfg(all(not(feature = "sqlite"), feature = "postgres"))] +pub type Connection = diesel::PgConnection; + +pub(crate) static ACTOR_SYS: Lazy = Lazy::new(|| { + SystemBuilder::new() + .name("plume") + .create() + .expect("Failed to create actor system") +}); + +pub(crate) static USER_CHAN: Lazy> = + Lazy::new(|| channel("user_events", &*ACTOR_SYS).expect("Failed to create user channel")); + +pub(crate) static POST_CHAN: Lazy> = + Lazy::new(|| channel("post_events", &*ACTOR_SYS).expect("Failed to create post channel")); + +/// All the possible errors that can be encoutered in this crate +#[derive(Debug)] +pub enum Error { + Blocklisted(bool, String), + Db(diesel::result::Error), + Inbox(Box>), + InvalidValue, + Io(std::io::Error), + MissingApProperty, + NotFound, + Request, + SerDe, + Search(search::SearcherError), + Signature, + TimelineQuery(timeline::query::QueryError), + Unauthorized, + Url, + Webfinger, + Expired, + UserAlreadyExists, +} + +impl From for Error { + fn from(_: bcrypt::BcryptError) -> Self { + Error::Signature + } +} +pub const ITEMS_PER_PAGE: i32 = 12; +impl From for Error { + fn from(_: openssl::error::ErrorStack) -> Self { + Error::Signature + } +} + +impl From for Error { + fn from(_: sign::Error) -> Self { + Error::Signature + } +} + +impl From for Error { + fn from(err: diesel::result::Error) -> Self { + Error::Db(err) + } +} + +impl From for Error { + fn from(_: url::ParseError) -> Self { + Error::Url + } +} + +impl From for Error { + fn from(_: serde_json::Error) -> Self { + Error::SerDe + } +} + +impl From for Error { + fn from(_: reqwest::Error) -> Self { + Error::Request + } +} + +impl From for Error { + fn from(_: reqwest::header::InvalidHeaderValue) -> Self { + Error::Request + } +} + +impl From for Error { + fn from(err: activitypub::Error) -> Self { + match err { + activitypub::Error::NotFound => Error::MissingApProperty, + _ => Error::SerDe, + } + } +} + +impl From for Error { + fn from(_: webfinger::WebfingerError) -> Self { + Error::Webfinger + } +} + +impl From for Error { + fn from(err: search::SearcherError) -> Self { + Error::Search(err) + } +} + +impl From for Error { + fn from(err: timeline::query::QueryError) -> Self { + Error::TimelineQuery(err) + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } +} + +impl From> for Error { + fn from(err: InboxError) -> Error { + match err { + InboxError::InvalidActor(Some(e)) | InboxError::InvalidObject(Some(e)) => e, + e => Error::Inbox(Box::new(e)), + } + } +} + +impl From for Error { + fn from(_err: request::Error) -> Error { + Error::Request + } +} + +pub type Result = std::result::Result; + +/// Adds a function to a model, that returns the first +/// matching row for a given list of fields. +/// +/// Usage: +/// +/// ```rust +/// impl Model { +/// find_by!(model_table, name_of_the_function, field1 as String, field2 as i32); +/// } +/// +/// // Get the Model with field1 == "", and field2 == 0 +/// Model::name_of_the_function(connection, String::new(), 0); +/// ``` +macro_rules! find_by { + ($table:ident, $fn:ident, $($col:ident as $type:ty),+) => { + /// Try to find a $table with a given $col + pub fn $fn(conn: &crate::Connection, $($col: $type),+) -> Result { + $table::table + $(.filter($table::$col.eq($col)))+ + .first(conn) + .map_err(Error::from) + } + }; +} + +/// List all rows of a model, with field-based filtering. +/// +/// Usage: +/// +/// ```rust +/// impl Model { +/// list_by!(model_table, name_of_the_function, field1 as String); +/// } +/// +/// // To get all Models with field1 == "" +/// Model::name_of_the_function(connection, String::new()); +/// ``` +macro_rules! list_by { + ($table:ident, $fn:ident, $($col:ident as $type:ty),+) => { + /// Try to find a $table with a given $col + pub fn $fn(conn: &crate::Connection, $($col: $type),+) -> Result> { + $table::table + $(.filter($table::$col.eq($col)))+ + .load::(conn) + .map_err(Error::from) + } + }; +} + +/// Adds a function to a model to retrieve a row by ID +/// +/// # Usage +/// +/// ```rust +/// impl Model { +/// get!(model_table); +/// } +/// +/// // Get the Model with ID 1 +/// Model::get(connection, 1); +/// ``` +macro_rules! get { + ($table:ident) => { + pub fn get(conn: &crate::Connection, id: i32) -> Result { + $table::table + .filter($table::id.eq(id)) + .first(conn) + .map_err(Error::from) + } + }; +} + +/// Adds a function to a model to insert a new row +/// +/// # Usage +/// +/// ```rust +/// impl Model { +/// insert!(model_table, NewModelType); +/// } +/// +/// // Insert a new row +/// Model::insert(connection, NewModelType::new()); +/// ``` +macro_rules! insert { + ($table:ident, $from:ty) => { + insert!($table, $from, |x, _conn| Ok(x)); + }; + ($table:ident, $from:ty, |$val:ident, $conn:ident | $( $after:tt )+) => { + last!($table); + + #[allow(dead_code)] + pub fn insert(conn: &crate::Connection, new: $from) -> Result { + diesel::insert_into($table::table) + .values(new) + .execute(conn)?; + #[allow(unused_mut)] + let mut $val = Self::last(conn)?; + let $conn = conn; + $( $after )+ + } + }; +} + +/// Returns the last row of a table. +/// +/// # Usage +/// +/// ```rust +/// impl Model { +/// last!(model_table); +/// } +/// +/// // Get the last Model +/// Model::last(connection) +/// ``` +macro_rules! last { + ($table:ident) => { + #[allow(dead_code)] + pub fn last(conn: &crate::Connection) -> Result { + $table::table + .order_by($table::id.desc()) + .first(conn) + .map_err(Error::from) + } + }; +} + +mod config; +pub use config::CONFIG; + +pub fn ap_url(url: &str) -> String { + format!("https://{}", url) +} + +pub trait SmtpNewWithAddr { + fn new_with_addr( + addr: (&str, u16), + ) -> std::result::Result; +} + +impl SmtpNewWithAddr for smtp::SmtpClient { + // Stolen from lettre::smtp::SmtpClient::new_simple() + fn new_with_addr(addr: (&str, u16)) -> std::result::Result { + use native_tls::TlsConnector; + use smtp::{ + client::net::{ClientTlsParameters, DEFAULT_TLS_PROTOCOLS}, + ClientSecurity, SmtpClient, + }; + + let (domain, port) = addr; + + let mut tls_builder = TlsConnector::builder(); + tls_builder.min_protocol_version(Some(DEFAULT_TLS_PROTOCOLS[0])); + + let tls_parameters = + ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap()); + + SmtpClient::new((domain, port), ClientSecurity::Wrapper(tls_parameters)) + } +} + +#[cfg(test)] +#[macro_use] +mod tests { + use crate::{db_conn, migrations::IMPORTED_MIGRATIONS, Connection as Conn, CONFIG}; + use chrono::{naive::NaiveDateTime, Datelike, Timelike}; + use diesel::r2d2::ConnectionManager; + use plume_common::utils::random_hex; + use std::env::temp_dir; + + #[macro_export] + macro_rules! part_eq { + ( $x:expr, $y:expr, [$( $var:ident ),*] ) => { + { + $( + assert_eq!($x.$var, $y.$var); + )* + } + }; + } + + pub fn db<'a>() -> db_conn::DbConn { + db_conn::DbConn((*DB_POOL).get().unwrap()) + } + + lazy_static! { + static ref DB_POOL: db_conn::DbPool = { + let pool = db_conn::DbPool::builder() + .connection_customizer(Box::new(db_conn::tests::TestConnectionCustomizer)) + .build(ConnectionManager::::new(CONFIG.database_url.as_str())) + .unwrap(); + let dir = temp_dir().join(format!("plume-test-{}", random_hex())); + IMPORTED_MIGRATIONS + .run_pending_migrations(&pool.get().unwrap(), &dir) + .expect("Migrations error"); + pool + }; + } + + #[cfg(feature = "postgres")] + pub(crate) fn format_datetime(dt: &NaiveDateTime) -> String { + format!( + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z", + dt.year(), + dt.month(), + dt.day(), + dt.hour(), + dt.minute(), + dt.second(), + dt.timestamp_subsec_micros() + ) + } + + #[cfg(feature = "sqlite")] + pub(crate) fn format_datetime(dt: &NaiveDateTime) -> String { + format!( + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", + dt.year(), + dt.month(), + dt.day(), + dt.hour(), + dt.minute(), + dt.second() + ) + } +} + +pub mod admin; +pub mod api_tokens; +pub mod apps; +pub mod blocklisted_emails; +pub mod blog_authors; +pub mod blogs; +pub mod comment_seers; +pub mod comments; +pub mod db_conn; +pub mod email_signups; +pub mod follows; +pub mod headers; +pub mod inbox; +pub mod instance; +pub mod likes; +pub mod lists; +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; +pub mod remote_fetch_actor; +pub mod reshares; +pub mod safe_string; +#[allow(unused_imports)] +pub mod schema; +pub mod search; +pub mod signups; +pub mod tags; +pub mod timeline; +pub mod users; +pub use plume_rocket::PlumeRocket; diff --git a/plume-models/src/likes.rs b/plume-models/src/likes.rs new file mode 100644 index 00000000000..9c119177a19 --- /dev/null +++ b/plume-models/src/likes.rs @@ -0,0 +1,239 @@ +use crate::{ + db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*, + users::User, Connection, Error, Result, CONFIG, +}; +use activitypub::activity; +use chrono::NaiveDateTime; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; +use plume_common::activity_pub::{ + inbox::{AsActor, AsObject, FromId}, + sign::Signer, + Id, IntoId, PUBLIC_VISIBILITY, +}; + +#[derive(Clone, Queryable, Identifiable)] +pub struct Like { + pub id: i32, + pub user_id: i32, + pub post_id: i32, + pub creation_date: NaiveDateTime, + pub ap_url: String, +} + +#[derive(Default, Insertable)] +#[table_name = "likes"] +pub struct NewLike { + pub user_id: i32, + pub post_id: i32, + pub ap_url: String, +} + +impl Like { + insert!(likes, NewLike); + get!(likes); + find_by!(likes, find_by_ap_url, ap_url as &str); + find_by!(likes, find_by_user_on_post, user_id as i32, post_id as i32); + + pub fn to_activity(&self, conn: &Connection) -> Result { + let mut act = activity::Like::default(); + act.like_props + .set_actor_link(User::get(conn, self.user_id)?.into_id())?; + act.like_props + .set_object_link(Post::get(conn, self.post_id)?.into_id())?; + act.object_props + .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?; + act.object_props.set_cc_link_vec(vec![Id::new( + User::get(conn, self.user_id)?.followers_endpoint, + )])?; + act.object_props.set_id_string(self.ap_url.clone())?; + + Ok(act) + } + + pub fn notify(&self, conn: &Connection) -> Result<()> { + let post = Post::get(conn, self.post_id)?; + for author in post.get_authors(conn)? { + if author.is_local() { + Notification::insert( + conn, + NewNotification { + kind: notification_kind::LIKE.to_string(), + object_id: self.id, + user_id: author.id, + }, + )?; + } + } + Ok(()) + } + + pub fn build_undo(&self, conn: &Connection) -> Result { + let mut act = activity::Undo::default(); + act.undo_props + .set_actor_link(User::get(conn, self.user_id)?.into_id())?; + act.undo_props.set_object_object(self.to_activity(conn)?)?; + act.object_props + .set_id_string(format!("{}#delete", self.ap_url))?; + act.object_props + .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?; + act.object_props.set_cc_link_vec(vec![Id::new( + User::get(conn, self.user_id)?.followers_endpoint, + )])?; + + Ok(act) + } +} + +impl AsObject for Post { + type Error = Error; + type Output = Like; + + fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result { + let res = Like::insert( + conn, + NewLike { + post_id: self.id, + user_id: actor.id, + ap_url: id.to_string(), + }, + )?; + res.notify(conn)?; + + Timeline::add_to_all_timelines(conn, &self, Kind::Like(&actor))?; + Ok(res) + } +} + +impl FromId for Like { + type Error = Error; + type Object = activity::Like; + + fn from_db(conn: &DbConn, id: &str) -> Result { + Like::find_by_ap_url(conn, id) + } + + fn from_activity(conn: &DbConn, act: activity::Like) -> Result { + let res = Like::insert( + conn, + NewLike { + post_id: Post::from_id( + conn, + &act.like_props.object_link::()?, + None, + CONFIG.proxy(), + ) + .map_err(|(_, e)| e)? + .id, + user_id: User::from_id( + conn, + &act.like_props.actor_link::()?, + None, + CONFIG.proxy(), + ) + .map_err(|(_, e)| e)? + .id, + ap_url: act.object_props.id_string()?, + }, + )?; + res.notify(conn)?; + Ok(res) + } + + fn get_sender() -> &'static dyn Signer { + Instance::get_local_instance_user().expect("Failed to local instance user") + } +} + +impl AsObject for Like { + type Error = Error; + type Output = (); + + fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { + if actor.id == self.user_id { + diesel::delete(&self).execute(&**conn)?; + + // delete associated notification if any + if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) { + diesel::delete(¬if).execute(&**conn)?; + } + Ok(()) + } else { + Err(Error::Unauthorized) + } + } +} + +impl NewLike { + pub fn new(p: &Post, u: &User) -> Self { + let ap_url = format!("{}like/{}", u.ap_url, p.ap_url); + NewLike { + post_id: p.id, + user_id: u.id, + ap_url, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::diesel::Connection; + use crate::{inbox::tests::fill_database, tests::db}; + use assert_json_diff::assert_json_eq; + use serde_json::{json, to_value}; + + #[test] + fn to_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (posts, _users, _blogs) = fill_database(&conn); + let post = &posts[0]; + let user = &post.get_authors(&conn)?[0]; + let like = Like::insert(&*conn, NewLike::new(post, user))?; + let act = like.to_activity(&conn).unwrap(); + + let expected = json!({ + "actor": "https://plu.me/@/admin/", + "cc": ["https://plu.me/@/admin/followers"], + "id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing", + "object": "https://plu.me/~/BlogName/testing", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Like", + }); + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } + + #[test] + fn build_undo() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (posts, _users, _blogs) = fill_database(&conn); + let post = &posts[0]; + let user = &post.get_authors(&conn)?[0]; + let like = Like::insert(&*conn, NewLike::new(post, user))?; + let act = like.build_undo(&*conn)?; + + let expected = json!({ + "actor": "https://plu.me/@/admin/", + "cc": ["https://plu.me/@/admin/followers"], + "id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing#delete", + "object": { + "actor": "https://plu.me/@/admin/", + "cc": ["https://plu.me/@/admin/followers"], + "id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing", + "object": "https://plu.me/~/BlogName/testing", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Like", + }, + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Undo", + }); + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } +} diff --git a/plume-models/src/lists.rs b/plume-models/src/lists.rs new file mode 100644 index 00000000000..5b393f64753 --- /dev/null +++ b/plume-models/src/lists.rs @@ -0,0 +1,556 @@ +use crate::{ + blogs::Blog, + schema::{blogs, list_elems, lists, users}, + users::User, + Connection, Error, Result, +}; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; +use std::convert::{TryFrom, TryInto}; + +/// Represent what a list is supposed to store. Represented in database as an integer +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ListType { + User, + Blog, + Word, + Prefix, +} + +impl TryFrom for ListType { + type Error = (); + + fn try_from(i: i32) -> std::result::Result { + match i { + 0 => Ok(ListType::User), + 1 => Ok(ListType::Blog), + 2 => Ok(ListType::Word), + 3 => Ok(ListType::Prefix), + _ => Err(()), + } + } +} + +impl From for i32 { + fn from(list_type: ListType) -> Self { + match list_type { + ListType::User => 0, + ListType::Blog => 1, + ListType::Word => 2, + ListType::Prefix => 3, + } + } +} + +#[derive(Clone, Queryable, Identifiable)] +pub struct List { + pub id: i32, + pub name: String, + pub user_id: Option, + type_: i32, +} + +#[derive(Default, Insertable)] +#[table_name = "lists"] +struct NewList<'a> { + pub name: &'a str, + pub user_id: Option, + type_: i32, +} + +macro_rules! func { + (@elem User $id:expr, $value:expr) => { + NewListElem { + list_id: $id, + user_id: Some(*$value), + blog_id: None, + word: None, + } + }; + (@elem Blog $id:expr, $value:expr) => { + NewListElem { + list_id: $id, + user_id: None, + blog_id: Some(*$value), + word: None, + } + }; + (@elem Word $id:expr, $value:expr) => { + NewListElem { + list_id: $id, + user_id: None, + blog_id: None, + word: Some($value), + } + }; + (@elem Prefix $id:expr, $value:expr) => { + NewListElem { + list_id: $id, + user_id: None, + blog_id: None, + word: Some($value), + } + }; + (@in_type User) => { i32 }; + (@in_type Blog) => { i32 }; + (@in_type Word) => { &str }; + (@in_type Prefix) => { &str }; + (@out_type User) => { User }; + (@out_type Blog) => { Blog }; + (@out_type Word) => { String }; + (@out_type Prefix) => { String }; + + (add: $fn:ident, $kind:ident) => { + pub fn $fn(&self, conn: &Connection, vals: &[func!(@in_type $kind)]) -> Result<()> { + if self.kind() != ListType::$kind { + return Err(Error::InvalidValue); + } + diesel::insert_into(list_elems::table) + .values( + vals + .iter() + .map(|u| func!(@elem $kind self.id, u)) + .collect::>(), + ) + .execute(conn)?; + Ok(()) + } + }; + + (list: $fn:ident, $kind:ident, $table:ident) => { + pub fn $fn(&self, conn: &Connection) -> Result> { + if self.kind() != ListType::$kind { + return Err(Error::InvalidValue); + } + list_elems::table + .filter(list_elems::list_id.eq(self.id)) + .inner_join($table::table) + .select($table::all_columns) + .load(conn) + .map_err(Error::from) + } + }; + + + + (set: $fn:ident, $kind:ident, $add:ident) => { + pub fn $fn(&self, conn: &Connection, val: &[func!(@in_type $kind)]) -> Result<()> { + if self.kind() != ListType::$kind { + return Err(Error::InvalidValue); + } + self.clear(conn)?; + self.$add(conn, val) + } + } +} + +#[allow(dead_code)] +#[derive(Clone, Queryable, Identifiable)] +struct ListElem { + pub id: i32, + pub list_id: i32, + pub user_id: Option, + pub blog_id: Option, + pub word: Option, +} + +#[derive(Default, Insertable)] +#[table_name = "list_elems"] +struct NewListElem<'a> { + pub list_id: i32, + pub user_id: Option, + pub blog_id: Option, + pub word: Option<&'a str>, +} + +impl List { + last!(lists); + get!(lists); + + fn insert(conn: &Connection, val: NewList<'_>) -> Result { + diesel::insert_into(lists::table) + .values(val) + .execute(conn)?; + List::last(conn) + } + + pub fn list_for_user(conn: &Connection, user_id: Option) -> Result> { + if let Some(user_id) = user_id { + lists::table + .filter(lists::user_id.eq(user_id)) + .load::(conn) + .map_err(Error::from) + } else { + lists::table + .filter(lists::user_id.is_null()) + .load::(conn) + .map_err(Error::from) + } + } + + pub fn find_for_user_by_name( + conn: &Connection, + user_id: Option, + name: &str, + ) -> Result { + if let Some(user_id) = user_id { + lists::table + .filter(lists::user_id.eq(user_id)) + .filter(lists::name.eq(name)) + .first(conn) + .map_err(Error::from) + } else { + lists::table + .filter(lists::user_id.is_null()) + .filter(lists::name.eq(name)) + .first(conn) + .map_err(Error::from) + } + } + + pub fn new(conn: &Connection, name: &str, user: Option<&User>, kind: ListType) -> Result { + Self::insert( + conn, + NewList { + name, + user_id: user.map(|u| u.id), + type_: kind.into(), + }, + ) + } + + /// Returns the kind of a list + pub fn kind(&self) -> ListType { + self.type_.try_into().expect("invalid list was constructed") + } + + /// Return Ok(true) if the list contain the given user, Ok(false) otherwiser, + /// and Err(_) on error + pub fn contains_user(&self, conn: &Connection, user: i32) -> Result { + private::ListElem::user_in_list(conn, self, user) + } + + /// Return Ok(true) if the list contain the given blog, Ok(false) otherwiser, + /// and Err(_) on error + pub fn contains_blog(&self, conn: &Connection, blog: i32) -> Result { + private::ListElem::blog_in_list(conn, self, blog) + } + + /// Return Ok(true) if the list contain the given word, Ok(false) otherwiser, + /// and Err(_) on error + pub fn contains_word(&self, conn: &Connection, word: &str) -> Result { + private::ListElem::word_in_list(conn, self, word) + } + + /// Return Ok(true) if the list match the given prefix, Ok(false) otherwiser, + /// and Err(_) on error + pub fn contains_prefix(&self, conn: &Connection, word: &str) -> Result { + private::ListElem::prefix_in_list(conn, self, word) + } + + // Insert new users in a list + func! {add: add_users, User} + + // Insert new blogs in a list + func! {add: add_blogs, Blog} + + // Insert new words in a list + func! {add: add_words, Word} + + // Insert new prefixes in a list + func! {add: add_prefixes, Prefix} + + // Get all users in the list + func! {list: list_users, User, users} + + // Get all blogs in the list + func! {list: list_blogs, Blog, blogs} + + /// Get all words in the list + pub fn list_words(&self, conn: &Connection) -> Result> { + self.list_stringlike(conn, ListType::Word) + } + + /// Get all prefixes in the list + pub fn list_prefixes(&self, conn: &Connection) -> Result> { + self.list_stringlike(conn, ListType::Prefix) + } + + #[inline(always)] + fn list_stringlike(&self, conn: &Connection, t: ListType) -> Result> { + if self.kind() != t { + return Err(Error::InvalidValue); + } + list_elems::table + .filter(list_elems::list_id.eq(self.id)) + .filter(list_elems::word.is_not_null()) + .select(list_elems::word) + .load::>(conn) + .map_err(Error::from) + // .map(|r| r.into_iter().filter_map(|o| o).collect::>()) + .map(|r| r.into_iter().flatten().collect::>()) + } + + pub fn clear(&self, conn: &Connection) -> Result<()> { + diesel::delete(list_elems::table.filter(list_elems::list_id.eq(self.id))) + .execute(conn) + .map(|_| ()) + .map_err(Error::from) + } + + func! {set: set_users, User, add_users} + func! {set: set_blogs, Blog, add_blogs} + func! {set: set_words, Word, add_words} + func! {set: set_prefixes, Prefix, add_prefixes} +} + +mod private { + pub use super::*; + use diesel::{ + dsl, + sql_types::{Nullable, Text}, + IntoSql, TextExpressionMethods, + }; + + impl ListElem { + insert!(list_elems, NewListElem<'_>); + + pub fn user_in_list(conn: &Connection, list: &List, user: i32) -> Result { + dsl::select(dsl::exists( + list_elems::table + .filter(list_elems::list_id.eq(list.id)) + .filter(list_elems::user_id.eq(Some(user))), + )) + .get_result(conn) + .map_err(Error::from) + } + + pub fn blog_in_list(conn: &Connection, list: &List, blog: i32) -> Result { + dsl::select(dsl::exists( + list_elems::table + .filter(list_elems::list_id.eq(list.id)) + .filter(list_elems::blog_id.eq(Some(blog))), + )) + .get_result(conn) + .map_err(Error::from) + } + + pub fn word_in_list(conn: &Connection, list: &List, word: &str) -> Result { + dsl::select(dsl::exists( + list_elems::table + .filter(list_elems::list_id.eq(list.id)) + .filter(list_elems::word.eq(word)), + )) + .get_result(conn) + .map_err(Error::from) + } + + pub fn prefix_in_list(conn: &Connection, list: &List, word: &str) -> Result { + dsl::select(dsl::exists( + list_elems::table + .filter( + word.into_sql::>() + .like(list_elems::word.concat("%")), + ) + .filter(list_elems::list_id.eq(list.id)), + )) + .get_result(conn) + .map_err(Error::from) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{blogs::tests as blog_tests, tests::db}; + use diesel::Connection; + + #[test] + fn list_type() { + for i in 0..4 { + assert_eq!(i, Into::::into(ListType::try_from(i).unwrap())); + } + ListType::try_from(4).unwrap_err(); + } + + #[test] + fn list_lists() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let (users, _) = blog_tests::fill_database(conn); + + let l1 = List::new(conn, "list1", None, ListType::User).unwrap(); + let l2 = List::new(conn, "list2", None, ListType::Blog).unwrap(); + let l1u = List::new(conn, "list1", Some(&users[0]), ListType::Word).unwrap(); + + let l_eq = |l1: &List, l2: &List| { + assert_eq!(l1.id, l2.id); + assert_eq!(l1.user_id, l2.user_id); + assert_eq!(l1.name, l2.name); + assert_eq!(l1.type_, l2.type_); + }; + + let l1bis = List::get(conn, l1.id).unwrap(); + l_eq(&l1, &l1bis); + + let l_inst = List::list_for_user(conn, None).unwrap(); + let l_user = List::list_for_user(conn, Some(users[0].id)).unwrap(); + assert_eq!(2, l_inst.len()); + assert_eq!(1, l_user.len()); + assert!(l_inst.iter().all(|l| l.id != l1u.id)); + + l_eq(&l1u, &l_user[0]); + if l_inst[0].id == l1.id { + l_eq(&l1, &l_inst[0]); + l_eq(&l2, &l_inst[1]); + } else { + l_eq(&l1, &l_inst[1]); + l_eq(&l2, &l_inst[0]); + } + + l_eq( + &l1, + &List::find_for_user_by_name(conn, l1.user_id, &l1.name).unwrap(), + ); + l_eq( + &&l1u, + &List::find_for_user_by_name(conn, l1u.user_id, &l1u.name).unwrap(), + ); + Ok(()) + }); + } + + #[test] + fn test_user_list() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let (users, blogs) = blog_tests::fill_database(conn); + + let l = List::new(conn, "list", None, ListType::User).unwrap(); + + assert_eq!(l.kind(), ListType::User); + assert!(l.list_users(conn).unwrap().is_empty()); + + assert!(!l.contains_user(conn, users[0].id).unwrap()); + assert!(l.add_users(conn, &[users[0].id]).is_ok()); + assert!(l.contains_user(conn, users[0].id).unwrap()); + + assert!(l.add_users(conn, &[users[1].id]).is_ok()); + assert!(l.contains_user(conn, users[0].id).unwrap()); + assert!(l.contains_user(conn, users[1].id).unwrap()); + assert_eq!(2, l.list_users(conn).unwrap().len()); + + assert!(l.set_users(conn, &[users[0].id]).is_ok()); + assert!(l.contains_user(conn, users[0].id).unwrap()); + assert!(!l.contains_user(conn, users[1].id).unwrap()); + assert_eq!(1, l.list_users(conn).unwrap().len()); + assert!(users[0] == l.list_users(conn).unwrap()[0]); + + l.clear(conn).unwrap(); + assert!(l.list_users(conn).unwrap().is_empty()); + + assert!(l.add_blogs(conn, &[blogs[0].id]).is_err()); + Ok(()) + }); + } + + #[test] + fn test_blog_list() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let (users, blogs) = blog_tests::fill_database(conn); + + let l = List::new(conn, "list", None, ListType::Blog).unwrap(); + + assert_eq!(l.kind(), ListType::Blog); + assert!(l.list_blogs(conn).unwrap().is_empty()); + + assert!(!l.contains_blog(conn, blogs[0].id).unwrap()); + assert!(l.add_blogs(conn, &[blogs[0].id]).is_ok()); + assert!(l.contains_blog(conn, blogs[0].id).unwrap()); + + assert!(l.add_blogs(conn, &[blogs[1].id]).is_ok()); + assert!(l.contains_blog(conn, blogs[0].id).unwrap()); + assert!(l.contains_blog(conn, blogs[1].id).unwrap()); + assert_eq!(2, l.list_blogs(conn).unwrap().len()); + + assert!(l.set_blogs(conn, &[blogs[0].id]).is_ok()); + assert!(l.contains_blog(conn, blogs[0].id).unwrap()); + assert!(!l.contains_blog(conn, blogs[1].id).unwrap()); + assert_eq!(1, l.list_blogs(conn).unwrap().len()); + assert_eq!(blogs[0].id, l.list_blogs(conn).unwrap()[0].id); + + l.clear(conn).unwrap(); + assert!(l.list_blogs(conn).unwrap().is_empty()); + + assert!(l.add_users(conn, &[users[0].id]).is_err()); + Ok(()) + }); + } + + #[test] + fn test_word_list() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let l = List::new(conn, "list", None, ListType::Word).unwrap(); + + assert_eq!(l.kind(), ListType::Word); + assert!(l.list_words(conn).unwrap().is_empty()); + + assert!(!l.contains_word(conn, "plume").unwrap()); + assert!(l.add_words(conn, &["plume"]).is_ok()); + assert!(l.contains_word(conn, "plume").unwrap()); + assert!(!l.contains_word(conn, "plumelin").unwrap()); + + assert!(l.add_words(conn, &["amsterdam"]).is_ok()); + assert!(l.contains_word(conn, "plume").unwrap()); + assert!(l.contains_word(conn, "amsterdam").unwrap()); + assert_eq!(2, l.list_words(conn).unwrap().len()); + + assert!(l.set_words(conn, &["plume"]).is_ok()); + assert!(l.contains_word(conn, "plume").unwrap()); + assert!(!l.contains_word(conn, "amsterdam").unwrap()); + assert_eq!(1, l.list_words(conn).unwrap().len()); + assert_eq!("plume", l.list_words(conn).unwrap()[0]); + + l.clear(conn).unwrap(); + assert!(l.list_words(conn).unwrap().is_empty()); + + assert!(l.add_prefixes(conn, &["something"]).is_err()); + Ok(()) + }); + } + + #[test] + fn test_prefix_list() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let l = List::new(conn, "list", None, ListType::Prefix).unwrap(); + + assert_eq!(l.kind(), ListType::Prefix); + assert!(l.list_prefixes(conn).unwrap().is_empty()); + + assert!(!l.contains_prefix(conn, "plume").unwrap()); + assert!(l.add_prefixes(conn, &["plume"]).is_ok()); + assert!(l.contains_prefix(conn, "plume").unwrap()); + assert!(l.contains_prefix(conn, "plumelin").unwrap()); + + assert!(l.add_prefixes(conn, &["amsterdam"]).is_ok()); + assert!(l.contains_prefix(conn, "plume").unwrap()); + assert!(l.contains_prefix(conn, "amsterdam").unwrap()); + assert_eq!(2, l.list_prefixes(conn).unwrap().len()); + + assert!(l.set_prefixes(conn, &["plume"]).is_ok()); + assert!(l.contains_prefix(conn, "plume").unwrap()); + assert!(!l.contains_prefix(conn, "amsterdam").unwrap()); + assert_eq!(1, l.list_prefixes(conn).unwrap().len()); + assert_eq!("plume", l.list_prefixes(conn).unwrap()[0]); + + l.clear(conn).unwrap(); + assert!(l.list_prefixes(conn).unwrap().is_empty()); + + assert!(l.add_words(conn, &["something"]).is_err()); + Ok(()) + }); + } +} diff --git a/plume-models/src/medias.rs b/plume-models/src/medias.rs new file mode 100644 index 00000000000..1af5f3b6891 --- /dev/null +++ b/plume-models/src/medias.rs @@ -0,0 +1,494 @@ +use crate::{ + ap_url, db_conn::DbConn, instance::Instance, safe_string::SafeString, schema::medias, + users::User, Connection, Error, Result, CONFIG, +}; +use activitypub::object::Image; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; +use guid_create::GUID; +use plume_common::{ + activity_pub::{inbox::FromId, request, Id}, + utils::{escape, MediaProcessor}, +}; +use std::{ + fs::{self, DirBuilder}, + path::{self, Path, PathBuf}, +}; +use tracing::warn; +use url::Url; + +const REMOTE_MEDIA_DIRECTORY: &str = "remote"; + +#[derive(Clone, Identifiable, Queryable, AsChangeset)] +pub struct Media { + pub id: i32, + pub file_path: String, + pub alt_text: String, + pub is_remote: bool, + pub remote_url: Option, + pub sensitive: bool, + pub content_warning: Option, + pub owner_id: i32, +} + +#[derive(Insertable)] +#[table_name = "medias"] +pub struct NewMedia { + pub file_path: String, + pub alt_text: String, + pub is_remote: bool, + pub remote_url: Option, + pub sensitive: bool, + pub content_warning: Option, + pub owner_id: i32, +} + +#[derive(PartialEq)] +pub enum MediaCategory { + Image, + Audio, + Video, + Unknown, +} + +impl MediaCategory { + pub fn to_string(&self) -> &str { + match *self { + MediaCategory::Image => "image", + MediaCategory::Audio => "audio", + MediaCategory::Video => "video", + MediaCategory::Unknown => "unknown", + } + } +} + +impl Media { + insert!(medias, NewMedia); + get!(medias); + find_by!(medias, find_by_file_path, file_path as &str); + + pub fn for_user(conn: &Connection, owner: i32) -> Result> { + medias::table + .filter(medias::owner_id.eq(owner)) + .order(medias::id.desc()) + .load::(conn) + .map_err(Error::from) + } + + pub fn list_all_medias(conn: &Connection) -> Result> { + medias::table.load::(conn).map_err(Error::from) + } + + pub fn page_for_user( + conn: &Connection, + user: &User, + (min, max): (i32, i32), + ) -> Result> { + medias::table + .filter(medias::owner_id.eq(user.id)) + .order(medias::id.desc()) + .offset(i64::from(min)) + .limit(i64::from(max - min)) + .load::(conn) + .map_err(Error::from) + } + + pub fn count_for_user(conn: &Connection, user: &User) -> Result { + medias::table + .filter(medias::owner_id.eq(user.id)) + .count() + .get_result(conn) + .map_err(Error::from) + } + + pub fn category(&self) -> MediaCategory { + match &*self + .file_path + .rsplit_once('.') + .map(|x| x.1) + .expect("Media::category: extension error") + .to_lowercase() + { + "png" | "jpg" | "jpeg" | "gif" | "svg" => MediaCategory::Image, + "mp3" | "wav" | "flac" => MediaCategory::Audio, + "mp4" | "avi" | "webm" | "mov" => MediaCategory::Video, + _ => MediaCategory::Unknown, + } + } + + pub fn html(&self) -> Result { + let url = self.url()?; + Ok(match self.category() { + MediaCategory::Image => SafeString::trusted(&format!( + r#"{}"#, + url, + escape(&self.alt_text), + escape(&self.alt_text) + )), + MediaCategory::Audio => SafeString::trusted(&format!( + r#"
"#, + url, + escape(&self.alt_text) + )), + MediaCategory::Video => SafeString::trusted(&format!( + r#""#, + url, + escape(&self.alt_text) + )), + MediaCategory::Unknown => SafeString::trusted(&format!( + r#""#, + url, + )), + }) + } + + pub fn markdown(&self) -> Result { + Ok(match self.category() { + MediaCategory::Image => { + SafeString::new(&format!("![{}]({})", escape(&self.alt_text), self.id)) + } + MediaCategory::Audio | MediaCategory::Video => self.html()?, + MediaCategory::Unknown => SafeString::new(""), + }) + } + + pub fn url(&self) -> Result { + if self.is_remote { + Ok(self.remote_url.clone().unwrap_or_default()) + } else { + let file_path = self.file_path.replace(path::MAIN_SEPARATOR, "/").replacen( + &CONFIG.media_directory, + "static/media", + 1, + ); // "static/media" from plume::routs::plume_media_files() + Ok(ap_url(&format!( + "{}/{}", + Instance::get_local()?.public_domain, + &file_path + ))) + } + } + + pub fn delete(&self, conn: &Connection) -> Result<()> { + if !self.is_remote { + fs::remove_file(self.file_path.as_str())?; + } + diesel::delete(self) + .execute(conn) + .map(|_| ()) + .map_err(Error::from) + } + + pub fn save_remote(conn: &Connection, url: String, user: &User) -> Result { + if url.contains(&['<', '>', '"'][..]) { + Err(Error::Url) + } else { + Media::insert( + conn, + NewMedia { + file_path: String::new(), + alt_text: String::new(), + is_remote: true, + remote_url: Some(url), + sensitive: false, + content_warning: None, + owner_id: user.id, + }, + ) + } + } + + pub fn set_owner(&self, conn: &Connection, user: &User) -> Result<()> { + diesel::update(self) + .set(medias::owner_id.eq(user.id)) + .execute(conn) + .map(|_| ()) + .map_err(Error::from) + } + + // TODO: merge with save_remote? + pub fn from_activity(conn: &DbConn, image: &Image) -> Result { + let remote_url = image + .object_props + .url_string() + .or(Err(Error::MissingApProperty))?; + let path = determine_mirror_file_path(&remote_url); + let parent = path.parent().ok_or(Error::InvalidValue)?; + if !parent.is_dir() { + DirBuilder::new().recursive(true).create(parent)?; + } + + let mut dest = fs::File::create(path.clone())?; + // TODO: conditional GET + request::get( + remote_url.as_str(), + User::get_sender(), + CONFIG.proxy().cloned(), + )? + .copy_to(&mut dest)?; + + Media::find_by_file_path(conn, path.to_str().ok_or(Error::InvalidValue)?) + .and_then(|mut media| { + let mut updated = false; + + let alt_text = image + .object_props + .content_string() + .or(Err(Error::NotFound))?; + let sensitive = image.object_props.summary_string().is_ok(); + let content_warning = image.object_props.summary_string().ok(); + if media.alt_text != alt_text { + media.alt_text = alt_text; + updated = true; + } + if media.is_remote { + media.is_remote = false; + updated = true; + } + if media.remote_url.is_some() { + media.remote_url = None; + updated = true; + } + if media.sensitive != sensitive { + media.sensitive = sensitive; + updated = true; + } + if media.content_warning != content_warning { + media.content_warning = content_warning; + updated = true; + } + if updated { + diesel::update(&media).set(&media).execute(&**conn)?; + } + Ok(media) + }) + .or_else(|_| { + Media::insert( + conn, + NewMedia { + file_path: path.to_str().ok_or(Error::InvalidValue)?.to_string(), + alt_text: image + .object_props + .content_string() + .or(Err(Error::NotFound))?, + is_remote: false, + remote_url: None, + sensitive: image.object_props.summary_string().is_ok(), + content_warning: image.object_props.summary_string().ok(), + owner_id: User::from_id( + conn, + image + .object_props + .attributed_to_link_vec::() + .or(Err(Error::NotFound))? + .into_iter() + .next() + .ok_or(Error::NotFound)? + .as_ref(), + None, + CONFIG.proxy(), + ) + .map_err(|(_, e)| e)? + .id, + }, + ) + }) + } + + pub fn get_media_processor<'a>(conn: &'a Connection, user: Vec<&User>) -> MediaProcessor<'a> { + let uid = user.iter().map(|u| u.id).collect::>(); + Box::new(move |id| { + 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().ok()?, media.content_warning)) + } else { + None + } + }) + } +} + +fn determine_mirror_file_path(url: &str) -> PathBuf { + let mut file_path = Path::new(&super::CONFIG.media_directory).join(REMOTE_MEDIA_DIRECTORY); + Url::parse(url) + .map(|url| { + if !url.has_host() { + return; + } + file_path.push(url.host_str().unwrap()); + for segment in url.path_segments().expect("FIXME") { + file_path.push(segment); + } + // TODO: handle query + // HINT: Use characters which must be percent-encoded in path as separator between path and query + // HINT: handle extension + }) + .unwrap_or_else(|err| { + warn!("Failed to parse url: {} {}", &url, err); + let ext = url + .rsplit('.') + .next() + .map(ToOwned::to_owned) + .unwrap_or_else(|| String::from("png")); + file_path.push(format!("{}.{}", GUID::rand(), ext)); + }); + file_path +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::{tests::db, users::tests as usersTests, Connection as Conn}; + use diesel::Connection; + use std::env::{current_dir, set_current_dir}; + use std::fs; + use std::path::Path; + + pub(crate) fn fill_database(conn: &Conn) -> (Vec, Vec) { + let mut wd = current_dir().unwrap().to_path_buf(); + while wd.pop() { + if wd.join(".git").exists() { + set_current_dir(wd).unwrap(); + break; + } + } + + let users = usersTests::fill_database(conn); + let user_one = users[0].id; + let user_two = users[1].id; + let f1 = "static/media/1.png".to_owned(); + let f2 = "static/media/2.mp3".to_owned(); + fs::write(f1.clone(), []).unwrap(); + fs::write(f2.clone(), []).unwrap(); + ( + users, + vec![ + NewMedia { + file_path: f1, + alt_text: "some alt".to_owned(), + is_remote: false, + remote_url: None, + sensitive: false, + content_warning: None, + owner_id: user_one, + }, + NewMedia { + file_path: f2, + alt_text: "alt message".to_owned(), + is_remote: false, + remote_url: None, + sensitive: true, + content_warning: Some("Content warning".to_owned()), + owner_id: user_one, + }, + NewMedia { + file_path: "".to_owned(), + alt_text: "another alt".to_owned(), + is_remote: true, + remote_url: Some("https://example.com/".to_owned()), + sensitive: false, + content_warning: None, + owner_id: user_two, + }, + ] + .into_iter() + .map(|nm| Media::insert(conn, nm).unwrap()) + .collect(), + ) + } + + pub(crate) fn clean(conn: &Conn) { + //used to remove files generated by tests + for media in Media::list_all_medias(conn).unwrap() { + if let Some(err) = media.delete(conn).err() { + match &err { + Error::Io(e) => match e.kind() { + std::io::ErrorKind::NotFound => (), + _ => panic!("{:?}", err), + }, + _ => panic!("{:?}", err), + } + } + } + } + + #[test] + fn delete() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let user = fill_database(conn).0[0].id; + + let path = "static/media/test_deletion".to_owned(); + fs::write(path.clone(), []).unwrap(); + + let media = Media::insert( + conn, + NewMedia { + file_path: path.clone(), + alt_text: "alt message".to_owned(), + is_remote: false, + remote_url: None, + sensitive: false, + content_warning: None, + owner_id: user, + }, + ) + .unwrap(); + + assert!(Path::new(&path).exists()); + media.delete(conn).unwrap(); + assert!(!Path::new(&path).exists()); + + clean(conn); + Ok(()) + }); + } + + #[test] + fn set_owner() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let (users, _) = fill_database(conn); + let u1 = &users[0]; + let u2 = &users[1]; + + let path = "static/media/test_set_owner".to_owned(); + fs::write(path.clone(), []).unwrap(); + + let media = Media::insert( + conn, + NewMedia { + file_path: path.clone(), + alt_text: "alt message".to_owned(), + is_remote: false, + remote_url: None, + sensitive: false, + content_warning: None, + owner_id: u1.id, + }, + ) + .unwrap(); + + assert!(Media::for_user(conn, u1.id) + .unwrap() + .iter() + .any(|m| m.id == media.id)); + assert!(!Media::for_user(conn, u2.id) + .unwrap() + .iter() + .any(|m| m.id == media.id)); + media.set_owner(conn, u2).unwrap(); + assert!(!Media::for_user(conn, u1.id) + .unwrap() + .iter() + .any(|m| m.id == media.id)); + assert!(Media::for_user(conn, u2.id) + .unwrap() + .iter() + .any(|m| m.id == media.id)); + + clean(conn); + Ok(()) + }); + } +} diff --git a/plume-models/src/mentions.rs b/plume-models/src/mentions.rs new file mode 100644 index 00000000000..1667278147b --- /dev/null +++ b/plume-models/src/mentions.rs @@ -0,0 +1,206 @@ +use crate::{ + comments::Comment, db_conn::DbConn, notifications::*, posts::Post, schema::mentions, + users::User, Connection, Error, Result, +}; +use activitypub::link; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; +use plume_common::activity_pub::inbox::AsActor; + +#[derive(Clone, Queryable, Identifiable)] +pub struct Mention { + pub id: i32, + pub mentioned_id: i32, + pub post_id: Option, + pub comment_id: Option, +} + +#[derive(Insertable)] +#[table_name = "mentions"] +pub struct NewMention { + pub mentioned_id: i32, + pub post_id: Option, + pub comment_id: Option, +} + +impl Mention { + insert!(mentions, NewMention); + get!(mentions); + list_by!(mentions, list_for_user, mentioned_id as i32); + list_by!(mentions, list_for_post, post_id as i32); + list_by!(mentions, list_for_comment, comment_id as i32); + + pub fn get_mentioned(&self, conn: &Connection) -> Result { + User::get(conn, self.mentioned_id) + } + + pub fn get_post(&self, conn: &Connection) -> Result { + self.post_id + .ok_or(Error::NotFound) + .and_then(|id| Post::get(conn, id)) + } + + pub fn get_comment(&self, conn: &Connection) -> Result { + self.comment_id + .ok_or(Error::NotFound) + .and_then(|id| Comment::get(conn, id)) + } + + pub fn get_user(&self, conn: &Connection) -> Result { + match self.get_post(conn) { + Ok(p) => Ok(p + .get_authors(conn)? + .into_iter() + .next() + .ok_or(Error::NotFound)?), + Err(_) => self.get_comment(conn).and_then(|c| c.get_author(conn)), + } + } + + pub fn build_activity(conn: &DbConn, ment: &str) -> Result { + let user = User::find_by_fqn(conn, ment)?; + let mut mention = link::Mention::default(); + mention.link_props.set_href_string(user.ap_url)?; + mention.link_props.set_name_string(format!("@{}", ment))?; + Ok(mention) + } + + pub fn to_activity(&self, conn: &Connection) -> Result { + let user = self.get_mentioned(conn)?; + let mut mention = link::Mention::default(); + mention.link_props.set_href_string(user.ap_url.clone())?; + mention + .link_props + .set_name_string(format!("@{}", user.fqn))?; + Ok(mention) + } + + pub fn from_activity( + conn: &Connection, + ment: &link::Mention, + inside: i32, + in_post: bool, + notify: bool, + ) -> Result { + let ap_url = ment.link_props.href_string().or(Err(Error::NotFound))?; + let mentioned = User::find_by_ap_url(conn, &ap_url)?; + + if in_post { + Post::get(conn, inside).and_then(|post| { + let res = Mention::insert( + conn, + NewMention { + mentioned_id: mentioned.id, + post_id: Some(post.id), + comment_id: None, + }, + )?; + if notify { + res.notify(conn)?; + } + Ok(res) + }) + } else { + Comment::get(conn, inside).and_then(|comment| { + let res = Mention::insert( + conn, + NewMention { + mentioned_id: mentioned.id, + post_id: None, + comment_id: Some(comment.id), + }, + )?; + if notify { + res.notify(conn)?; + } + Ok(res) + }) + } + } + + pub fn delete(&self, conn: &Connection) -> Result<()> { + //find related notifications and delete them + if let Ok(n) = Notification::find(conn, notification_kind::MENTION, self.id) { + n.delete(conn)?; + } + diesel::delete(self) + .execute(conn) + .map(|_| ()) + .map_err(Error::from) + } + + fn notify(&self, conn: &Connection) -> Result<()> { + let m = self.get_mentioned(conn)?; + 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(()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{inbox::tests::fill_database, tests::db, Error}; + use assert_json_diff::assert_json_eq; + use diesel::Connection; + use serde_json::{json, to_value}; + + #[test] + fn build_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (_posts, users, _blogs) = fill_database(&conn); + let user = &users[0]; + let name = &user.username; + let act = Mention::build_activity(&conn, name)?; + + let expected = json!({ + "href": "https://plu.me/@/admin/", + "name": "@admin", + "type": "Mention", + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } + + #[test] + fn to_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (posts, users, _blogs) = fill_database(&conn); + let post = &posts[0]; + let user = &users[0]; + let mention = Mention::insert( + &conn, + NewMention { + mentioned_id: user.id, + post_id: Some(post.id), + comment_id: None, + }, + )?; + let act = mention.to_activity(&conn)?; + + let expected = json!({ + "href": "https://plu.me/@/admin/", + "name": "@admin", + "type": "Mention", + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } +} diff --git a/plume-models/src/migrations.rs b/plume-models/src/migrations.rs new file mode 100644 index 00000000000..6a7c756cc1d --- /dev/null +++ b/plume-models/src/migrations.rs @@ -0,0 +1,120 @@ +use crate::{Connection, Error, Result}; +use diesel::connection::{Connection as Conn, SimpleConnection}; +use migrations_internals::{setup_database, MigrationConnection}; +use std::path::Path; +use tracing::info; + +#[allow(dead_code)] //variants might not be constructed if not required by current migrations +enum Action { + Sql(&'static str), + Function(&'static dyn 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<()> { + info!("Running migration {}", self.name); + for step in self.up { + step.run(conn, path)? + } + Ok(()) + } + + fn revert(&self, conn: &Connection, path: &Path) -> Result<()> { + info!("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::( + "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::( + "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 { + 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()) + .ok_or(Error::NotFound)?; + let migration = &self.0[id]; + conn.transaction(|| { + migration.revert(conn, path)?; + migration.run(conn, path) + }) + } +} + +pub const IMPORTED_MIGRATIONS: ImportedMigrations = { + import_migrations! {} +}; diff --git a/plume-models/src/notifications.rs b/plume-models/src/notifications.rs new file mode 100644 index 00000000000..0fff320aa17 --- /dev/null +++ b/plume-models/src/notifications.rs @@ -0,0 +1,178 @@ +use crate::{ + comments::Comment, + follows::Follow, + likes::Like, + mentions::Mention, + posts::Post, + reshares::Reshare, + schema::{follows, notifications}, + users::User, + Connection, Error, Result, +}; +use chrono::NaiveDateTime; +use diesel::{self, ExpressionMethods, JoinOnDsl, QueryDsl, RunQueryDsl}; + +pub mod notification_kind { + pub const COMMENT: &str = "COMMENT"; + pub const FOLLOW: &str = "FOLLOW"; + pub const LIKE: &str = "LIKE"; + pub const MENTION: &str = "MENTION"; + pub const RESHARE: &str = "RESHARE"; +} + +#[derive(Clone, Queryable, Identifiable)] +pub struct Notification { + pub id: i32, + pub user_id: i32, + pub creation_date: NaiveDateTime, + pub kind: String, + pub object_id: i32, +} + +#[derive(Insertable)] +#[table_name = "notifications"] +pub struct NewNotification { + pub user_id: i32, + pub kind: String, + pub object_id: i32, +} + +impl Notification { + insert!(notifications, NewNotification); + get!(notifications); + + pub fn find_for_user(conn: &Connection, user: &User) -> Result> { + notifications::table + .filter(notifications::user_id.eq(user.id)) + .order_by(notifications::creation_date.desc()) + .load::(conn) + .map_err(Error::from) + } + + pub fn find_for_mention(conn: &Connection, mention: &Mention) -> Result> { + notifications::table + .filter(notifications::kind.eq(notification_kind::MENTION)) + .filter(notifications::object_id.eq(mention.id)) + .load::(conn) + .map_err(Error::from) + } + + pub fn find_for_comment(conn: &Connection, comment: &Comment) -> Result> { + notifications::table + .filter(notifications::kind.eq(notification_kind::COMMENT)) + .filter(notifications::object_id.eq(comment.id)) + .load::(conn) + .map_err(Error::from) + } + + pub fn find_followed_by(conn: &Connection, user: &User) -> Result> { + notifications::table + .inner_join(follows::table.on(notifications::object_id.eq(follows::id))) + .filter(notifications::kind.eq(notification_kind::FOLLOW)) + .filter(follows::follower_id.eq(user.id)) + .select(notifications::all_columns) + .load::(conn) + .map_err(Error::from) + } + + pub fn count_for_user(conn: &Connection, user: &User) -> Result { + notifications::table + .filter(notifications::user_id.eq(user.id)) + .count() + .get_result(conn) + .map_err(Error::from) + } + + pub fn page_for_user( + conn: &Connection, + user: &User, + (min, max): (i32, i32), + ) -> Result> { + notifications::table + .filter(notifications::user_id.eq(user.id)) + .order_by(notifications::creation_date.desc()) + .offset(min.into()) + .limit((max - min).into()) + .load::(conn) + .map_err(Error::from) + } + + pub fn find>(conn: &Connection, kind: S, obj: i32) -> Result { + notifications::table + .filter(notifications::kind.eq(kind.into())) + .filter(notifications::object_id.eq(obj)) + .get_result::(conn) + .map_err(Error::from) + } + + pub fn get_url(&self, conn: &Connection) -> Option { + match self.kind.as_ref() { + notification_kind::COMMENT => self + .get_post(conn) + .and_then(|p| Some(format!("{}#comment-{}", p.url(conn).ok()?, self.object_id))), + notification_kind::FOLLOW => Some(format!("/@/{}/", self.get_actor(conn).ok()?.fqn)), + notification_kind::MENTION => Mention::get(conn, self.object_id) + .and_then(|mention| { + mention + .get_post(conn) + .and_then(|p| p.url(conn)) + .or_else(|_| { + let comment = mention.get_comment(conn)?; + Ok(format!( + "{}#comment-{}", + comment.get_post(conn)?.url(conn)?, + comment.id + )) + }) + }) + .ok(), + _ => None, + } + } + + pub fn get_post(&self, conn: &Connection) -> Option { + match self.kind.as_ref() { + notification_kind::COMMENT => Comment::get(conn, self.object_id) + .and_then(|comment| comment.get_post(conn)) + .ok(), + notification_kind::LIKE => Like::get(conn, self.object_id) + .and_then(|like| Post::get(conn, like.post_id)) + .ok(), + notification_kind::RESHARE => Reshare::get(conn, self.object_id) + .and_then(|reshare| reshare.get_post(conn)) + .ok(), + _ => None, + } + } + + pub fn get_actor(&self, conn: &Connection) -> Result { + Ok(match self.kind.as_ref() { + notification_kind::COMMENT => Comment::get(conn, self.object_id)?.get_author(conn)?, + notification_kind::FOLLOW => { + User::get(conn, Follow::get(conn, self.object_id)?.follower_id)? + } + notification_kind::LIKE => User::get(conn, Like::get(conn, self.object_id)?.user_id)?, + notification_kind::MENTION => Mention::get(conn, self.object_id)?.get_user(conn)?, + notification_kind::RESHARE => Reshare::get(conn, self.object_id)?.get_user(conn)?, + _ => unreachable!("Notification::get_actor: Unknow type"), + }) + } + + pub fn icon_class(&self) -> &'static str { + match self.kind.as_ref() { + notification_kind::COMMENT => "icon-message-circle", + notification_kind::FOLLOW => "icon-user-plus", + notification_kind::LIKE => "icon-heart", + notification_kind::MENTION => "icon-at-sign", + notification_kind::RESHARE => "icon-repeat", + _ => unreachable!("Notification::get_actor: Unknow type"), + } + } + + pub fn delete(&self, conn: &Connection) -> Result<()> { + diesel::delete(self) + .execute(conn) + .map(|_| ()) + .map_err(Error::from) + } +} diff --git a/plume-models/src/password_reset_requests.rs b/plume-models/src/password_reset_requests.rs new file mode 100644 index 00000000000..cfd9352939b --- /dev/null +++ b/plume-models/src/password_reset_requests.rs @@ -0,0 +1,162 @@ +use crate::{schema::password_reset_requests, Connection, Error, Result}; +use chrono::{offset::Utc, Duration, NaiveDateTime}; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; + +#[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 { + // 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 { + let token = password_reset_requests::table + .filter(password_reset_requests::token.eq(token)) + .first::(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 { + 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 crate::{tests::db, users::tests as user_tests}; + use diesel::Connection; + + #[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(()) + }); + } +} diff --git a/plume-models/src/plume_rocket.rs b/plume-models/src/plume_rocket.rs new file mode 100644 index 00000000000..e7d40089fa5 --- /dev/null +++ b/plume-models/src/plume_rocket.rs @@ -0,0 +1,73 @@ +pub use self::module::PlumeRocket; + +#[cfg(not(test))] +mod module { + use crate::{search, users}; + use rocket::{ + request::{self, FlashMessage, FromRequest, Request}, + Outcome, State, + }; + use scheduled_thread_pool::ScheduledThreadPool; + use std::sync::Arc; + + /// Common context needed by most routes and operations on models + pub struct PlumeRocket { + pub intl: rocket_i18n::I18n, + pub user: Option, + pub searcher: Arc, + pub worker: Arc, + pub flash_msg: Option<(String, String)>, + } + + impl<'a, 'r> FromRequest<'a, 'r> for PlumeRocket { + type Error = (); + + fn from_request(request: &'a Request<'r>) -> request::Outcome { + let intl = request.guard::()?; + let user = request.guard::().succeeded(); + let worker = request.guard::<'_, State<'_, Arc>>()?; + let searcher = request.guard::<'_, State<'_, Arc>>()?; + let flash_msg = request.guard::>().succeeded(); + Outcome::Success(PlumeRocket { + intl, + user, + flash_msg: flash_msg.map(|f| (f.name().into(), f.msg().into())), + worker: worker.clone(), + searcher: searcher.clone(), + }) + } + } +} + +#[cfg(test)] +mod module { + use crate::{search, users}; + use rocket::{ + request::{self, FromRequest, Request}, + Outcome, State, + }; + use scheduled_thread_pool::ScheduledThreadPool; + use std::sync::Arc; + + /// Common context needed by most routes and operations on models + pub struct PlumeRocket { + pub user: Option, + pub searcher: Arc, + pub worker: Arc, + } + + impl<'a, 'r> FromRequest<'a, 'r> for PlumeRocket { + type Error = (); + + fn from_request(request: &'a Request<'r>) -> request::Outcome { + let user = request.guard::().succeeded(); + let worker = request.guard::<'_, State<'_, Arc>>()?; + let searcher = request.guard::<'_, State<'_, Arc>>()?; + Outcome::Success(PlumeRocket { + user, + worker: worker.clone(), + searcher: searcher.clone(), + }) + } + } +} diff --git a/plume-models/src/post_authors.rs b/plume-models/src/post_authors.rs new file mode 100644 index 00000000000..32b6f6daf33 --- /dev/null +++ b/plume-models/src/post_authors.rs @@ -0,0 +1,23 @@ +use crate::{posts::Post, schema::post_authors, users::User, Error, Result}; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; + +#[derive(Clone, Queryable, Identifiable, Associations)] +#[belongs_to(Post)] +#[belongs_to(User, foreign_key = "author_id")] +pub struct PostAuthor { + pub id: i32, + pub post_id: i32, + pub author_id: i32, +} + +#[derive(Insertable)] +#[table_name = "post_authors"] +pub struct NewPostAuthor { + pub post_id: i32, + pub author_id: i32, +} + +impl PostAuthor { + insert!(post_authors, NewPostAuthor); + get!(post_authors); +} diff --git a/plume-models/src/posts.rs b/plume-models/src/posts.rs new file mode 100644 index 00000000000..65c98735b8b --- /dev/null +++ b/plume-models/src/posts.rs @@ -0,0 +1,1209 @@ +use crate::{ + ap_url, blogs::Blog, db_conn::DbConn, instance::Instance, medias::Media, mentions::Mention, + post_authors::*, safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User, + Connection, Error, PostEvent::*, Result, CONFIG, POST_CHAN, +}; +use activitypub::{ + activity::{Create, Delete, Update}, + link, + object::{Article, Image, Tombstone}, + CustomObject, +}; +use chrono::{NaiveDateTime, TimeZone, Utc}; +use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl}; +use once_cell::sync::Lazy; +use plume_common::{ + activity_pub::{ + inbox::{AsActor, AsObject, FromId}, + sign::Signer, + Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILITY, + }, + utils::{iri_percent_encode_seg, md_to_html}, +}; +use riker::actors::{Publish, Tell}; +use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, Mutex}; + +pub type LicensedArticle = CustomObject; + +static BLOG_FQN_CACHE: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); + +#[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)] +#[changeset_options(treat_none_as_null = "true")] +pub struct Post { + pub id: i32, + pub blog_id: i32, + pub slug: String, + pub title: String, + pub content: SafeString, + pub published: bool, + pub license: String, + pub creation_date: NaiveDateTime, + pub ap_url: String, + pub subtitle: String, + pub source: String, + pub cover_id: Option, +} + +#[derive(Insertable)] +#[table_name = "posts"] +pub struct NewPost { + pub blog_id: i32, + pub slug: String, + pub title: String, + pub content: SafeString, + pub published: bool, + pub license: String, + pub creation_date: Option, + pub ap_url: String, + pub subtitle: String, + pub source: String, + pub cover_id: Option, +} + +impl Post { + get!(posts); + find_by!(posts, find_by_slug, slug as &str, blog_id as i32); + find_by!(posts, find_by_ap_url, ap_url as &str); + + last!(posts); + pub fn insert(conn: &Connection, mut new: NewPost) -> Result { + if new.ap_url.is_empty() { + let blog = Blog::get(conn, new.blog_id)?; + new.ap_url = Self::ap_url(blog, &new.slug); + } + diesel::insert_into(posts::table) + .values(new) + .execute(conn)?; + let post = Self::last(conn)?; + + if post.published { + post.publish_published(); + } + + Ok(post) + } + + pub fn update(&self, conn: &Connection) -> Result { + diesel::update(self).set(self).execute(conn)?; + let post = Self::get(conn, self.id)?; + // TODO: Call publish_published() when newly published + if post.published { + let blog = post.get_blog(conn); + if blog.is_ok() && blog.unwrap().is_local() { + self.publish_updated(); + } + } + Ok(post) + } + + pub fn delete(&self, conn: &Connection) -> Result<()> { + for m in Mention::list_for_post(conn, self.id)? { + m.delete(conn)?; + } + diesel::delete(self).execute(conn)?; + self.publish_deleted(); + Ok(()) + } + + pub fn list_by_tag( + conn: &Connection, + tag: String, + (min, max): (i32, i32), + ) -> Result> { + use crate::schema::tags; + + let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id); + posts::table + .filter(posts::id.eq_any(ids)) + .filter(posts::published.eq(true)) + .order(posts::creation_date.desc()) + .offset(min.into()) + .limit((max - min).into()) + .load(conn) + .map_err(Error::from) + } + + pub fn count_for_tag(conn: &Connection, tag: String) -> Result { + use crate::schema::tags; + let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id); + posts::table + .filter(posts::id.eq_any(ids)) + .filter(posts::published.eq(true)) + .count() + .load(conn)? + .get(0) + .cloned() + .ok_or(Error::NotFound) + } + + pub fn count_local(conn: &Connection) -> Result { + use crate::schema::post_authors; + use crate::schema::users; + let local_authors = users::table + .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)) + .select(post_authors::post_id); + posts::table + .filter(posts::id.eq_any(local_posts_id)) + .filter(posts::published.eq(true)) + .count() + .get_result(conn) + .map_err(Error::from) + } + + pub fn count(conn: &Connection) -> Result { + posts::table + .filter(posts::published.eq(true)) + .count() + .get_result(conn) + .map_err(Error::from) + } + + pub fn list_filtered( + conn: &Connection, + title: Option, + subtitle: Option, + content: Option, + ) -> Result> { + 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::(conn).map_err(Error::from) + } + + pub fn get_recents_for_author( + conn: &Connection, + author: &User, + limit: i64, + ) -> Result> { + use crate::schema::post_authors; + + let posts = PostAuthor::belonging_to(author).select(post_authors::post_id); + posts::table + .filter(posts::id.eq_any(posts)) + .filter(posts::published.eq(true)) + .order(posts::creation_date.desc()) + .limit(limit) + .load::(conn) + .map_err(Error::from) + } + + pub fn get_recents_for_blog(conn: &Connection, blog: &Blog, limit: i64) -> Result> { + posts::table + .filter(posts::blog_id.eq(blog.id)) + .filter(posts::published.eq(true)) + .order(posts::creation_date.desc()) + .limit(limit) + .load::(conn) + .map_err(Error::from) + } + + pub fn get_for_blog(conn: &Connection, blog: &Blog) -> Result> { + posts::table + .filter(posts::blog_id.eq(blog.id)) + .filter(posts::published.eq(true)) + .load::(conn) + .map_err(Error::from) + } + + pub fn count_for_blog(conn: &Connection, blog: &Blog) -> Result { + posts::table + .filter(posts::blog_id.eq(blog.id)) + .filter(posts::published.eq(true)) + .count() + .get_result(conn) + .map_err(Error::from) + } + + pub fn blog_page(conn: &Connection, blog: &Blog, (min, max): (i32, i32)) -> Result> { + posts::table + .filter(posts::blog_id.eq(blog.id)) + .filter(posts::published.eq(true)) + .order(posts::creation_date.desc()) + .offset(min.into()) + .limit((max - min).into()) + .load::(conn) + .map_err(Error::from) + } + + pub fn drafts_by_author(conn: &Connection, author: &User) -> Result> { + use crate::schema::post_authors; + + let posts = PostAuthor::belonging_to(author).select(post_authors::post_id); + posts::table + .order(posts::creation_date.desc()) + .filter(posts::published.eq(false)) + .filter(posts::id.eq_any(posts)) + .load::(conn) + .map_err(Error::from) + } + + pub fn ap_url(blog: Blog, slug: &str) -> String { + ap_url(&format!( + "{}/~/{}/{}/", + CONFIG.base_url, + blog.fqn, + iri_percent_encode_seg(slug) + )) + } + + // It's better to calc slug in insert and update + pub fn slug(title: &str) -> &str { + title + } + + pub fn get_authors(&self, conn: &Connection) -> Result> { + use crate::schema::post_authors; + use crate::schema::users; + let author_list = PostAuthor::belonging_to(self).select(post_authors::author_id); + users::table + .filter(users::id.eq_any(author_list)) + .load::(conn) + .map_err(Error::from) + } + + pub fn is_author(&self, conn: &Connection, author_id: i32) -> Result { + use crate::schema::post_authors; + Ok(PostAuthor::belonging_to(self) + .filter(post_authors::author_id.eq(author_id)) + .count() + .get_result::(conn)? + > 0) + } + + pub fn get_blog(&self, conn: &Connection) -> Result { + use crate::schema::blogs; + blogs::table + .filter(blogs::id.eq(self.blog_id)) + .first(conn) + .map_err(Error::from) + } + + /// This method exists for use in templates to reduce database access. + /// This should not be used for other purpose. + /// + /// This caches query result. The best way to cache query result is holding it in `Post`s field + /// but Diesel doesn't allow it currently. + /// If sometime Diesel allow it, this method should be removed. + pub fn get_blog_fqn(&self, conn: &Connection) -> String { + if let Some(blog_fqn) = BLOG_FQN_CACHE.lock().unwrap().get(&self.blog_id) { + return blog_fqn.to_string(); + } + let blog_fqn = self.get_blog(conn).unwrap().fqn; + BLOG_FQN_CACHE + .lock() + .unwrap() + .insert(self.blog_id, blog_fqn.clone()); + blog_fqn + } + + pub fn count_likes(&self, conn: &Connection) -> Result { + use crate::schema::likes; + likes::table + .filter(likes::post_id.eq(self.id)) + .count() + .get_result(conn) + .map_err(Error::from) + } + + pub fn count_reshares(&self, conn: &Connection) -> Result { + use crate::schema::reshares; + reshares::table + .filter(reshares::post_id.eq(self.id)) + .count() + .get_result(conn) + .map_err(Error::from) + } + + pub fn get_receivers_urls(&self, conn: &Connection) -> Result> { + Ok(self + .get_authors(conn)? + .into_iter() + .filter_map(|a| a.get_followers(conn).ok()) + .fold(vec![], |mut acc, f| { + for x in f { + acc.push(x.ap_url); + } + acc + })) + } + + pub fn to_activity(&self, conn: &Connection) -> Result { + let cc = self.get_receivers_urls(conn)?; + let to = vec![PUBLIC_VISIBILITY.to_string()]; + + let mut mentions_json = Mention::list_for_post(conn, self.id)? + .into_iter() + .map(|m| json!(m.to_activity(conn).ok())) + .collect::>(); + let mut tags_json = Tag::for_post(conn, self.id)? + .into_iter() + .map(|t| json!(t.to_activity().ok())) + .collect::>(); + mentions_json.append(&mut tags_json); + + let mut article = Article::default(); + article.object_props.set_name_string(self.title.clone())?; + article.object_props.set_id_string(self.ap_url.clone())?; + + let mut authors = self + .get_authors(conn)? + .into_iter() + .map(|x| Id::new(x.ap_url)) + .collect::>(); + authors.push(self.get_blog(conn)?.into_id()); // add the blog URL here too + article + .object_props + .set_attributed_to_link_vec::(authors)?; + article + .object_props + .set_content_string(self.content.get().clone())?; + article.ap_object_props.set_source_object(Source { + content: self.source.clone(), + media_type: String::from("text/markdown"), + })?; + article + .object_props + .set_published_utctime(Utc.from_utc_datetime(&self.creation_date))?; + article + .object_props + .set_summary_string(self.subtitle.clone())?; + article.object_props.tag = Some(json!(mentions_json)); + + 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()?)?; + if media.sensitive { + cover + .object_props + .set_summary_string(media.content_warning.unwrap_or_default())?; + } + cover.object_props.set_content_string(media.alt_text)?; + cover + .object_props + .set_attributed_to_link_vec(vec![User::get(conn, media.owner_id)?.into_id()])?; + article.object_props.set_icon_object(cover)?; + } + + article.object_props.set_url_string(self.ap_url.clone())?; + article + .object_props + .set_to_link_vec::(to.into_iter().map(Id::new).collect())?; + article + .object_props + .set_cc_link_vec::(cc.into_iter().map(Id::new).collect())?; + let mut license = Licensed::default(); + license.set_license_string(self.license.clone())?; + Ok(LicensedArticle::new(article, license)) + } + + pub fn create_activity(&self, conn: &Connection) -> Result { + let article = self.to_activity(conn)?; + let mut act = Create::default(); + act.object_props + .set_id_string(format!("{}/activity", self.ap_url))?; + act.object_props + .set_to_link_vec::(article.object.object_props.to_link_vec()?)?; + act.object_props + .set_cc_link_vec::(article.object.object_props.cc_link_vec()?)?; + act.create_props + .set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?; + act.create_props.set_object_object(article)?; + Ok(act) + } + + pub fn update_activity(&self, conn: &Connection) -> Result { + let article = self.to_activity(conn)?; + let mut act = Update::default(); + act.object_props.set_id_string(format!( + "{}/update-{}", + self.ap_url, + Utc::now().timestamp() + ))?; + act.object_props + .set_to_link_vec::(article.object.object_props.to_link_vec()?)?; + act.object_props + .set_cc_link_vec::(article.object.object_props.cc_link_vec()?)?; + act.update_props + .set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?; + act.update_props.set_object_object(article)?; + Ok(act) + } + + pub fn update_mentions(&self, conn: &Connection, mentions: Vec) -> Result<()> { + let mentions = mentions + .into_iter() + .map(|m| { + ( + m.link_props + .href_string() + .ok() + .and_then(|ap_url| User::find_by_ap_url(conn, &ap_url).ok()) + .map(|u| u.id), + m, + ) + }) + .filter_map(|(id, m)| id.map(|id| (m, id))) + .collect::>(); + + let old_mentions = Mention::list_for_post(conn, self.id)?; + let old_user_mentioned = old_mentions + .iter() + .map(|m| m.mentioned_id) + .collect::>(); + for (m, id) in &mentions { + if !old_user_mentioned.contains(id) { + Mention::from_activity(&*conn, m, self.id, true, true)?; + } + } + + let new_mentions = mentions + .into_iter() + .map(|(_m, id)| id) + .collect::>(); + for m in old_mentions + .iter() + .filter(|m| !new_mentions.contains(&m.mentioned_id)) + { + m.delete(conn)?; + } + Ok(()) + } + + pub fn update_tags(&self, conn: &Connection, tags: Vec) -> Result<()> { + let tags_name = tags + .iter() + .filter_map(|t| t.name_string().ok()) + .collect::>(); + + let old_tags = Tag::for_post(&*conn, self.id)?; + let old_tags_name = old_tags + .iter() + .filter_map(|tag| { + if !tag.is_hashtag { + Some(tag.tag.clone()) + } else { + None + } + }) + .collect::>(); + + for t in tags { + if !t + .name_string() + .map(|n| old_tags_name.contains(&n)) + .unwrap_or(true) + { + Tag::from_activity(conn, &t, self.id, false)?; + } + } + + for ot in old_tags.iter().filter(|t| !t.is_hashtag) { + if !tags_name.contains(&ot.tag) { + ot.delete(conn)?; + } + } + Ok(()) + } + + pub fn update_hashtags(&self, conn: &Connection, tags: Vec) -> Result<()> { + let tags_name = tags + .iter() + .filter_map(|t| t.name_string().ok()) + .collect::>(); + + let old_tags = Tag::for_post(&*conn, self.id)?; + let old_tags_name = old_tags + .iter() + .filter_map(|tag| { + if tag.is_hashtag { + Some(tag.tag.clone()) + } else { + None + } + }) + .collect::>(); + + for t in tags { + if !t + .name_string() + .map(|n| old_tags_name.contains(&n)) + .unwrap_or(true) + { + Tag::from_activity(conn, &t, self.id, true)?; + } + } + + for ot in old_tags.into_iter().filter(|t| t.is_hashtag) { + if !tags_name.contains(&ot.tag) { + ot.delete(conn)?; + } + } + Ok(()) + } + + pub fn url(&self, conn: &Connection) -> Result { + let blog = self.get_blog(conn)?; + Ok(format!("/~/{}/{}", blog.fqn, self.slug)) + } + + pub fn cover_url(&self, conn: &Connection) -> Option { + self.cover_id + .and_then(|i| Media::get(conn, i).ok()) + .and_then(|c| c.url().ok()) + } + + pub fn build_delete(&self, conn: &Connection) -> Result { + let mut act = Delete::default(); + act.delete_props + .set_actor_link(self.get_authors(conn)?[0].clone().into_id())?; + + let mut tombstone = Tombstone::default(); + tombstone.object_props.set_id_string(self.ap_url.clone())?; + act.delete_props.set_object_object(tombstone)?; + + act.object_props + .set_id_string(format!("{}#delete", self.ap_url))?; + act.object_props + .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?; + Ok(act) + } + + fn publish_published(&self) { + POST_CHAN.tell( + Publish { + msg: PostPublished(Arc::new(self.clone())), + topic: "post.published".into(), + }, + None, + ) + } + + fn publish_updated(&self) { + POST_CHAN.tell( + Publish { + msg: PostUpdated(Arc::new(self.clone())), + topic: "post.updated".into(), + }, + None, + ) + } + + fn publish_deleted(&self) { + POST_CHAN.tell( + Publish { + msg: PostDeleted(Arc::new(self.clone())), + topic: "post.deleted".into(), + }, + None, + ) + } +} + +impl FromId for Post { + type Error = Error; + type Object = LicensedArticle; + + fn from_db(conn: &DbConn, id: &str) -> Result { + Self::find_by_ap_url(conn, id) + } + + fn from_activity(conn: &DbConn, article: LicensedArticle) -> Result { + let conn = conn; + let license = article.custom_props.license_string().unwrap_or_default(); + let article = article.object; + + let (blog, authors) = article + .object_props + .attributed_to_link_vec::()? + .into_iter() + .fold((None, vec![]), |(blog, mut authors), link| { + let url = link; + match User::from_id(conn, &url, None, CONFIG.proxy()) { + Ok(u) => { + authors.push(u); + (blog, authors) + } + Err(_) => ( + blog.or_else(|| Blog::from_id(conn, &url, None, CONFIG.proxy()).ok()), + authors, + ), + } + }); + + let cover = article + .object_props + .icon_object::() + .ok() + .and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id)); + + let title = article.object_props.name_string()?; + let ap_url = article + .object_props + .url_string() + .or_else(|_| article.object_props.id_string())?; + let post = Post::from_db(conn, &ap_url) + .and_then(|mut post| { + let mut updated = false; + + let slug = Self::slug(&title); + let content = SafeString::new(&article.object_props.content_string()?); + let subtitle = article.object_props.summary_string()?; + let source = article.ap_object_props.source_object::()?.content; + if post.slug != slug { + post.slug = slug.to_string(); + updated = true; + } + if post.title != title { + post.title = title.clone(); + updated = true; + } + if post.content != content { + post.content = content; + updated = true; + } + if post.license != license { + post.license = license.clone(); + updated = true; + } + if post.subtitle != subtitle { + post.subtitle = subtitle; + updated = true; + } + if post.source != source { + post.source = source; + updated = true; + } + if post.cover_id != cover { + post.cover_id = cover; + updated = true; + } + + if updated { + post.update(conn)?; + } + + Ok(post) + }) + .or_else(|_| { + Post::insert( + conn, + NewPost { + blog_id: blog.ok_or(Error::NotFound)?.id, + slug: Self::slug(&title).to_string(), + title, + content: SafeString::new(&article.object_props.content_string()?), + published: true, + license, + // FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields + ap_url, + creation_date: Some(article.object_props.published_utctime()?.naive_utc()), + subtitle: article.object_props.summary_string()?, + source: article.ap_object_props.source_object::()?.content, + cover_id: cover, + }, + ) + .and_then(|post| { + for author in authors { + PostAuthor::insert( + conn, + NewPostAuthor { + post_id: post.id, + author_id: author.id, + }, + )?; + } + + Ok(post) + }) + })?; + + // save mentions and tags + let mut hashtags = md_to_html(&post.source, None, false, None) + .2 + .into_iter() + .collect::>(); + if let Some(serde_json::Value::Array(tags)) = article.object_props.tag { + for tag in tags { + serde_json::from_value::(tag.clone()) + .map(|m| Mention::from_activity(conn, &m, post.id, true, true)) + .ok(); + + serde_json::from_value::(tag.clone()) + .map_err(Error::from) + .and_then(|t| { + let tag_name = t.name_string()?; + Ok(Tag::from_activity( + conn, + &t, + post.id, + hashtags.remove(&tag_name), + )) + }) + .ok(); + } + } + + Timeline::add_to_all_timelines(conn, &post, Kind::Original)?; + + Ok(post) + } + + fn get_sender() -> &'static dyn Signer { + Instance::get_local_instance_user().expect("Failed to local instance user") + } +} + +impl AsObject for Post { + type Error = Error; + type Output = Post; + + fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result { + // TODO: check that _actor is actually one of the author? + Ok(self) + } +} + +impl AsObject for Post { + type Error = Error; + type Output = (); + + fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { + let can_delete = self + .get_authors(conn)? + .into_iter() + .any(|a| actor.id == a.id); + if can_delete { + self.delete(conn).map(|_| ()) + } else { + Err(Error::Unauthorized) + } + } +} + +pub struct PostUpdate { + pub ap_url: String, + pub title: Option, + pub subtitle: Option, + pub content: Option, + pub cover: Option, + pub source: Option, + pub license: Option, + pub tags: Option, +} + +impl FromId for PostUpdate { + type Error = Error; + type Object = LicensedArticle; + + fn from_db(_: &DbConn, _: &str) -> Result { + // Always fail because we always want to deserialize the AP object + Err(Error::NotFound) + } + + fn from_activity(conn: &DbConn, updated: LicensedArticle) -> Result { + Ok(PostUpdate { + ap_url: updated.object.object_props.id_string()?, + title: updated.object.object_props.name_string().ok(), + subtitle: updated.object.object_props.summary_string().ok(), + content: updated.object.object_props.content_string().ok(), + cover: updated + .object + .object_props + .icon_object::() + .ok() + .and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id)), + source: updated + .object + .ap_object_props + .source_object::() + .ok() + .map(|x| x.content), + license: updated.custom_props.license_string().ok(), + tags: updated.object.object_props.tag, + }) + } + + fn get_sender() -> &'static dyn Signer { + Instance::get_local_instance_user().expect("Failed to local instance user") + } +} + +impl AsObject for PostUpdate { + type Error = Error; + type Output = (); + + fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { + let mut post = + Post::from_id(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?; + + if !post.is_author(conn, actor.id)? { + // TODO: maybe the author was added in the meantime + return Err(Error::Unauthorized); + } + + if let Some(title) = self.title { + post.slug = Post::slug(&title).to_string(); + post.title = title; + } + + if let Some(content) = self.content { + post.content = SafeString::new(&content); + } + + if let Some(subtitle) = self.subtitle { + post.subtitle = subtitle; + } + + post.cover_id = self.cover; + + if let Some(source) = self.source { + post.source = source; + } + + if let Some(license) = self.license { + post.license = license; + } + + let mut txt_hashtags = md_to_html(&post.source, None, false, None) + .2 + .into_iter() + .collect::>(); + if let Some(serde_json::Value::Array(mention_tags)) = self.tags { + let mut mentions = vec![]; + let mut tags = vec![]; + let mut hashtags = vec![]; + for tag in mention_tags { + serde_json::from_value::(tag.clone()) + .map(|m| mentions.push(m)) + .ok(); + + serde_json::from_value::(tag.clone()) + .map_err(Error::from) + .and_then(|t| { + let tag_name = t.name_string()?; + if txt_hashtags.remove(&tag_name) { + hashtags.push(t); + } else { + tags.push(t); + } + Ok(()) + }) + .ok(); + } + post.update_mentions(conn, mentions)?; + post.update_tags(conn, tags)?; + post.update_hashtags(conn, hashtags)?; + } + + post.update(conn)?; + Ok(()) + } +} + +impl IntoId for Post { + fn into_id(self) -> Id { + Id::new(self.ap_url) + } +} + +#[derive(Clone, Debug)] +pub enum PostEvent { + PostPublished(Arc), + PostUpdated(Arc), + PostDeleted(Arc), +} + +impl From for Arc { + fn from(event: PostEvent) -> Self { + use PostEvent::*; + + match event { + PostPublished(post) => post, + PostUpdated(post) => post, + PostDeleted(post) => post, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::inbox::{inbox, tests::fill_database, InboxResult}; + use crate::mentions::{Mention, NewMention}; + use crate::safe_string::SafeString; + use crate::tests::{db, format_datetime}; + use assert_json_diff::assert_json_eq; + use diesel::Connection; + use serde_json::{json, to_value}; + + fn prepare_activity(conn: &DbConn) -> (Post, Mention, Vec, Vec, Vec) { + let (posts, users, blogs) = fill_database(conn); + let post = &posts[0]; + let mentioned = &users[1]; + let mention = Mention::insert( + &conn, + NewMention { + mentioned_id: mentioned.id, + post_id: Some(post.id), + comment_id: None, + }, + ) + .unwrap(); + (post.to_owned(), mention.to_owned(), posts, users, blogs) + } + + // creates a post, get it's Create activity, delete the post, + // "send" the Create to the inbox, and check it works + #[test] + fn self_federation() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let (_, users, blogs) = fill_database(&conn); + let post = Post::insert( + &conn, + NewPost { + blog_id: blogs[0].id, + slug: "yo".into(), + title: "Yo".into(), + content: SafeString::new("Hello"), + published: true, + license: "WTFPL".to_string(), + creation_date: None, + ap_url: String::new(), // automatically updated when inserting + subtitle: "Testing".into(), + source: "Hello".into(), + cover_id: None, + }, + ) + .unwrap(); + PostAuthor::insert( + &conn, + NewPostAuthor { + post_id: post.id, + author_id: users[0].id, + }, + ) + .unwrap(); + let create = post.create_activity(&conn).unwrap(); + post.delete(&conn).unwrap(); + + match inbox(&conn, serde_json::to_value(create).unwrap()).unwrap() { + InboxResult::Post(p) => { + assert!(p.is_author(&conn, users[0].id).unwrap()); + assert_eq!(p.source, "Hello".to_owned()); + assert_eq!(p.blog_id, blogs[0].id); + assert_eq!(p.content, SafeString::new("Hello")); + assert_eq!(p.subtitle, "Testing".to_owned()); + assert_eq!(p.title, "Yo".to_owned()); + } + _ => panic!("Unexpected result"), + }; + Ok(()) + }); + } + + #[test] + fn licensed_article_serde() { + let mut article = Article::default(); + article.object_props.set_id_string("Yo".into()).unwrap(); + let mut license = Licensed::default(); + license.set_license_string("WTFPL".into()).unwrap(); + let full_article = LicensedArticle::new(article, license); + + let json = serde_json::to_value(full_article).unwrap(); + let article_from_json: LicensedArticle = serde_json::from_value(json).unwrap(); + assert_eq!( + "Yo", + &article_from_json.object.object_props.id_string().unwrap() + ); + assert_eq!( + "WTFPL", + &article_from_json.custom_props.license_string().unwrap() + ); + } + + #[test] + fn licensed_article_deserialization() { + let json = json!({ + "type": "Article", + "id": "https://plu.me/~/Blog/my-article", + "attributedTo": ["https://plu.me/@/Admin", "https://plu.me/~/Blog"], + "content": "Hello.", + "name": "My Article", + "summary": "Bye.", + "source": { + "content": "Hello.", + "mediaType": "text/markdown" + }, + "published": "2014-12-12T12:12:12Z", + "to": [plume_common::activity_pub::PUBLIC_VISIBILITY] + }); + let article: LicensedArticle = serde_json::from_value(json).unwrap(); + assert_eq!( + "https://plu.me/~/Blog/my-article", + &article.object.object_props.id_string().unwrap() + ); + } + + #[test] + fn to_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn); + let act = post.to_activity(&conn)?; + + let expected = json!({ + "attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"], + "cc": [], + "content": "Hello", + "id": "https://plu.me/~/BlogName/testing", + "license": "WTFPL", + "name": "Testing", + "published": format_datetime(&post.creation_date), + "source": { + "content": "", + "mediaType": "text/markdown" + }, + "summary": "", + "tag": [ + { + "href": "https://plu.me/@/user/", + "name": "@user", + "type": "Mention" + } + ], + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Article", + "url": "https://plu.me/~/BlogName/testing" + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } + + #[test] + fn create_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn); + let act = post.create_activity(&conn)?; + + let expected = json!({ + "actor": "https://plu.me/@/admin/", + "cc": [], + "id": "https://plu.me/~/BlogName/testing/activity", + "object": { + "attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"], + "cc": [], + "content": "Hello", + "id": "https://plu.me/~/BlogName/testing", + "license": "WTFPL", + "name": "Testing", + "published": format_datetime(&post.creation_date), + "source": { + "content": "", + "mediaType": "text/markdown" + }, + "summary": "", + "tag": [ + { + "href": "https://plu.me/@/user/", + "name": "@user", + "type": "Mention" + } + ], + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Article", + "url": "https://plu.me/~/BlogName/testing" + }, + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Create" + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } + + #[test] + fn update_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn); + let act = post.update_activity(&conn)?; + + let expected = json!({ + "actor": "https://plu.me/@/admin/", + "cc": [], + "id": "https://plu.me/~/BlogName/testing/update-", + "object": { + "attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"], + "cc": [], + "content": "Hello", + "id": "https://plu.me/~/BlogName/testing", + "license": "WTFPL", + "name": "Testing", + "published": format_datetime(&post.creation_date), + "source": { + "content": "", + "mediaType": "text/markdown" + }, + "summary": "", + "tag": [ + { + "href": "https://plu.me/@/user/", + "name": "@user", + "type": "Mention" + } + ], + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Article", + "url": "https://plu.me/~/BlogName/testing" + }, + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Update" + }); + let actual = to_value(act)?; + + let id = actual["id"].to_string(); + let (id_pre, id_post) = id.rsplit_once("-").unwrap(); + assert_eq!(post.ap_url, "https://plu.me/~/BlogName/testing"); + assert_eq!( + id_pre, + to_value("\"https://plu.me/~/BlogName/testing/update") + .unwrap() + .as_str() + .unwrap() + ); + assert_eq!(id_post.len(), 11); + assert_eq!( + id_post.matches(char::is_numeric).collect::().len(), + 10 + ); + for (key, value) in actual.as_object().unwrap().into_iter() { + if key == "id" { + continue; + } + assert_eq!(value, expected.get(key).unwrap()); + } + + Ok(()) + }); + } +} diff --git a/plume-models/src/remote_fetch_actor.rs b/plume-models/src/remote_fetch_actor.rs new file mode 100644 index 00000000000..097fb1dadfc --- /dev/null +++ b/plume-models/src/remote_fetch_actor.rs @@ -0,0 +1,123 @@ +use crate::{ + db_conn::{DbConn, DbPool}, + follows, + posts::{LicensedArticle, Post}, + users::{User, UserEvent}, + ACTOR_SYS, CONFIG, USER_CHAN, +}; +use activitypub::activity::Create; +use plume_common::activity_pub::inbox::FromId; +use riker::actors::{Actor, ActorFactoryArgs, ActorRefFactory, Context, Sender, Subscribe, Tell}; +use std::sync::Arc; +use tracing::{error, info, warn}; + +pub struct RemoteFetchActor { + conn: DbPool, +} + +impl RemoteFetchActor { + pub fn init(conn: DbPool) { + let actor = ACTOR_SYS + .actor_of_args::("remote-fetch", conn) + .expect("Failed to initialize remote fetch actor"); + + USER_CHAN.tell( + Subscribe { + actor: Box::new(actor), + topic: "*".into(), + }, + None, + ) + } +} + +impl Actor for RemoteFetchActor { + type Msg = UserEvent; + + fn recv(&mut self, _ctx: &Context, msg: Self::Msg, _sender: Sender) { + use UserEvent::*; + + match msg { + RemoteUserFound(user) => match self.conn.get() { + Ok(conn) => { + let conn = DbConn(conn); + // Don't call these functions in parallel + // for the case database connections limit is too small + fetch_and_cache_articles(&user, &conn); + fetch_and_cache_followers(&user, &conn); + if user.needs_update() { + fetch_and_cache_user(&user, &conn); + } + } + _ => { + error!("Failed to get database connection"); + } + }, + } + } +} + +impl ActorFactoryArgs for RemoteFetchActor { + fn create_args(conn: DbPool) -> Self { + Self { conn } + } +} + +fn fetch_and_cache_articles(user: &Arc, conn: &DbConn) { + let create_acts = user.fetch_outbox::(); + match create_acts { + Ok(create_acts) => { + for create_act in create_acts { + match create_act.create_props.object_object::() { + Ok(article) => { + Post::from_activity(conn, article) + .expect("Article from remote user couldn't be saved"); + info!("Fetched article from remote user"); + } + Err(e) => warn!("Error while fetching articles in background: {:?}", e), + } + } + } + Err(err) => { + error!("Failed to fetch outboxes: {:?}", err); + } + } +} + +fn fetch_and_cache_followers(user: &Arc, conn: &DbConn) { + let follower_ids = user.fetch_followers_ids(); + match follower_ids { + Ok(user_ids) => { + for user_id in user_ids { + let follower = User::from_id(conn, &user_id, None, CONFIG.proxy()); + match follower { + Ok(follower) => { + let inserted = follows::Follow::insert( + conn, + follows::NewFollow { + follower_id: follower.id, + following_id: user.id, + ap_url: String::new(), + }, + ); + if inserted.is_err() { + error!("Couldn't save follower for remote user: {:?}", user_id); + } + } + Err(err) => { + error!("Couldn't fetch follower: {:?}", err); + } + } + } + } + Err(err) => { + error!("Failed to fetch follower: {:?}", err); + } + } +} + +fn fetch_and_cache_user(user: &Arc, conn: &DbConn) { + if user.refetch(conn).is_err() { + error!("Couldn't update user info: {:?}", user); + } +} diff --git a/plume-models/src/reshares.rs b/plume-models/src/reshares.rs new file mode 100644 index 00000000000..90ca0cc8464 --- /dev/null +++ b/plume-models/src/reshares.rs @@ -0,0 +1,265 @@ +use crate::{ + db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::reshares, + timeline::*, users::User, Connection, Error, Result, CONFIG, +}; +use activitypub::activity::{Announce, Undo}; +use chrono::NaiveDateTime; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; +use plume_common::activity_pub::{ + inbox::{AsActor, AsObject, FromId}, + sign::Signer, + Id, IntoId, PUBLIC_VISIBILITY, +}; + +#[derive(Clone, Queryable, Identifiable)] +pub struct Reshare { + pub id: i32, + pub user_id: i32, + pub post_id: i32, + pub ap_url: String, + pub creation_date: NaiveDateTime, +} + +#[derive(Insertable)] +#[table_name = "reshares"] +pub struct NewReshare { + pub user_id: i32, + pub post_id: i32, + pub ap_url: String, +} + +impl Reshare { + insert!(reshares, NewReshare); + get!(reshares); + find_by!(reshares, find_by_ap_url, ap_url as &str); + find_by!( + reshares, + find_by_user_on_post, + user_id as i32, + post_id as i32 + ); + + pub fn get_recents_for_author( + conn: &Connection, + user: &User, + limit: i64, + ) -> Result> { + reshares::table + .filter(reshares::user_id.eq(user.id)) + .order(reshares::creation_date.desc()) + .limit(limit) + .load::(conn) + .map_err(Error::from) + } + + pub fn get_post(&self, conn: &Connection) -> Result { + Post::get(conn, self.post_id) + } + + pub fn get_user(&self, conn: &Connection) -> Result { + User::get(conn, self.user_id) + } + + pub fn to_activity(&self, conn: &Connection) -> Result { + let mut act = Announce::default(); + act.announce_props + .set_actor_link(User::get(conn, self.user_id)?.into_id())?; + act.announce_props + .set_object_link(Post::get(conn, self.post_id)?.into_id())?; + act.object_props.set_id_string(self.ap_url.clone())?; + act.object_props + .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?; + act.object_props + .set_cc_link_vec(vec![Id::new(self.get_user(conn)?.followers_endpoint)])?; + + Ok(act) + } + + pub fn notify(&self, conn: &Connection) -> Result<()> { + let post = self.get_post(conn)?; + for author in post.get_authors(conn)? { + if author.is_local() { + Notification::insert( + conn, + NewNotification { + kind: notification_kind::RESHARE.to_string(), + object_id: self.id, + user_id: author.id, + }, + )?; + } + } + Ok(()) + } + + pub fn build_undo(&self, conn: &Connection) -> Result { + let mut act = Undo::default(); + act.undo_props + .set_actor_link(User::get(conn, self.user_id)?.into_id())?; + act.undo_props.set_object_object(self.to_activity(conn)?)?; + act.object_props + .set_id_string(format!("{}#delete", self.ap_url))?; + act.object_props + .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?; + act.object_props + .set_cc_link_vec(vec![Id::new(self.get_user(conn)?.followers_endpoint)])?; + + Ok(act) + } +} + +impl AsObject for Post { + type Error = Error; + type Output = Reshare; + + fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result { + let conn = conn; + let reshare = Reshare::insert( + conn, + NewReshare { + post_id: self.id, + user_id: actor.id, + ap_url: id.to_string(), + }, + )?; + reshare.notify(conn)?; + + Timeline::add_to_all_timelines(conn, &self, Kind::Reshare(&actor))?; + Ok(reshare) + } +} + +impl FromId for Reshare { + type Error = Error; + type Object = Announce; + + fn from_db(conn: &DbConn, id: &str) -> Result { + Reshare::find_by_ap_url(conn, id) + } + + fn from_activity(conn: &DbConn, act: Announce) -> Result { + let res = Reshare::insert( + conn, + NewReshare { + post_id: Post::from_id( + conn, + &act.announce_props.object_link::()?, + None, + CONFIG.proxy(), + ) + .map_err(|(_, e)| e)? + .id, + user_id: User::from_id( + conn, + &act.announce_props.actor_link::()?, + None, + CONFIG.proxy(), + ) + .map_err(|(_, e)| e)? + .id, + ap_url: act.object_props.id_string()?, + }, + )?; + res.notify(conn)?; + Ok(res) + } + + fn get_sender() -> &'static dyn Signer { + Instance::get_local_instance_user().expect("Failed to local instance user") + } +} + +impl AsObject for Reshare { + type Error = Error; + type Output = (); + + fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { + if actor.id == self.user_id { + diesel::delete(&self).execute(&**conn)?; + + // delete associated notification if any + if let Ok(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) { + diesel::delete(¬if).execute(&**conn)?; + } + + Ok(()) + } else { + Err(Error::Unauthorized) + } + } +} + +impl NewReshare { + pub fn new(p: &Post, u: &User) -> Self { + let ap_url = format!("{}reshare/{}", u.ap_url, p.ap_url); + NewReshare { + post_id: p.id, + user_id: u.id, + ap_url, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::diesel::Connection; + use crate::{inbox::tests::fill_database, tests::db}; + use assert_json_diff::assert_json_eq; + use serde_json::{json, to_value}; + + #[test] + fn to_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (posts, _users, _blogs) = fill_database(&conn); + let post = &posts[0]; + let user = &post.get_authors(&conn)?[0]; + let reshare = Reshare::insert(&*conn, NewReshare::new(post, user))?; + let act = reshare.to_activity(&conn).unwrap(); + + let expected = json!({ + "actor": "https://plu.me/@/admin/", + "cc": ["https://plu.me/@/admin/followers"], + "id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing", + "object": "https://plu.me/~/BlogName/testing", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Announce", + }); + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } + + #[test] + fn build_undo() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (posts, _users, _blogs) = fill_database(&conn); + let post = &posts[0]; + let user = &post.get_authors(&conn)?[0]; + let reshare = Reshare::insert(&*conn, NewReshare::new(post, user))?; + let act = reshare.build_undo(&*conn)?; + + let expected = json!({ + "actor": "https://plu.me/@/admin/", + "cc": ["https://plu.me/@/admin/followers"], + "id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing#delete", + "object": { + "actor": "https://plu.me/@/admin/", + "cc": ["https://plu.me/@/admin/followers"], + "id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing", + "object": "https://plu.me/~/BlogName/testing", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Announce" + }, + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Undo", + }); + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } +} diff --git a/plume-models/src/safe_string.rs b/plume-models/src/safe_string.rs new file mode 100644 index 00000000000..fedcdd70924 --- /dev/null +++ b/plume-models/src/safe_string.rs @@ -0,0 +1,224 @@ +use ammonia::{Builder, UrlRelative}; +use diesel::{ + self, + deserialize::Queryable, + serialize::{self, Output}, + sql_types::Text, + types::ToSql, +}; +use serde::{self, de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; +use std::{ + borrow::{Borrow, Cow}, + fmt::{self, Display}, + io::Write, + ops::Deref, +}; + +lazy_static! { + static ref CLEAN: Builder<'static> = { + let mut b = Builder::new(); + b.add_generic_attributes(&["id", "dir"]) + .add_tags(&["iframe", "video", "audio", "label", "input"]) + .id_prefix(Some("postcontent-")) + .url_relative(UrlRelative::Custom(Box::new(url_add_prefix))) + .add_tag_attributes( + "iframe", + ["width", "height", "src", "frameborder"].iter().cloned(), + ) + .add_tag_attributes("video", ["src", "title", "controls"].iter()) + .add_tag_attributes("audio", ["src", "title", "controls"].iter()) + .add_tag_attributes("label", ["for"].iter()) + .add_tag_attributes("input", ["type", "checked"].iter()) + .add_allowed_classes("input", ["cw-checkbox"].iter()) + .add_allowed_classes( + "span", + [ + "cw-container", + "cw-text", + //Scope classes for the syntax highlighting. + "attribute-name", + "comment", + "constant", + "control", + "declaration", + "entity", + "function", + "invalid", + "keyword", + "language", + "modifier", + "name", + "numeric", + "operator", + "parameter", + "punctuation", + "source", + "storage", + "string", + "support", + "tag", + "type", + "variable", + ] + .iter(), + ) + // Related to https://github.com/Plume-org/Plume/issues/637 + .add_allowed_classes("sup", ["footnote-reference", "footnote-definition-label"].iter()) + .add_allowed_classes("div", ["footnote-definition"].iter()) + .attribute_filter(|elem, att, val| match (elem, att) { + ("input", "type") => Some("checkbox".into()), + ("input", "checked") => Some("checked".into()), + ("label", "for") => { + if val.starts_with("postcontent-cw-") { + Some(val.into()) + } else { + None + } + } + _ => Some(val.into()), + }); + b + }; +} + +#[allow(clippy::unnecessary_wraps)] +fn url_add_prefix(url: &str) -> Option> { + if url.starts_with('#') && !url.starts_with("#postcontent-") { + //if start with an # + let mut new_url = "#postcontent-".to_owned(); //change to valid id + new_url.push_str(&url[1..]); + Some(Cow::Owned(new_url)) + } else { + Some(Cow::Borrowed(url)) + } +} + +#[derive(Debug, Clone, PartialEq, AsExpression, FromSqlRow, Default)] +#[sql_type = "Text"] +pub struct SafeString { + value: String, +} + +impl SafeString { + pub fn new(value: &str) -> Self { + SafeString { + value: CLEAN.clean(value).to_string(), + } + } + + /// Creates a new `SafeString`, but without escaping the given value. + /// + /// Only use when you are sure you can trust the input (when the HTML + /// is entirely generated by Plume, not depending on user-inputed data). + /// Prefer `SafeString::new` as much as possible. + pub fn trusted(value: impl AsRef) -> Self { + SafeString { + value: value.as_ref().to_string(), + } + } + + pub fn set(&mut self, value: &str) { + self.value = CLEAN.clean(value).to_string(); + } + pub fn get(&self) -> &String { + &self.value + } +} + +impl Serialize for SafeString { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.value) + } +} + +struct SafeStringVisitor; + +impl<'de> Visitor<'de> for SafeStringVisitor { + type Value = SafeString; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a string") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + Ok(SafeString::new(value)) + } +} + +impl<'de> Deserialize<'de> for SafeString { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_string(SafeStringVisitor) + } +} + +#[cfg(all(feature = "postgres", not(feature = "sqlite")))] +impl Queryable for SafeString { + type Row = String; + fn build(value: Self::Row) -> Self { + SafeString::new(&value) + } +} + +#[cfg(all(feature = "sqlite", not(feature = "postgres")))] +impl Queryable for SafeString { + type Row = String; + fn build(value: Self::Row) -> Self { + SafeString::new(&value) + } +} + +impl ToSql for SafeString +where + DB: diesel::backend::Backend, + str: ToSql, +{ + fn to_sql(&self, out: &mut Output<'_, W, DB>) -> serialize::Result { + str::to_sql(&self.value, out) + } +} + +impl Borrow for SafeString { + fn borrow(&self) -> &str { + &self.value + } +} + +impl Display for SafeString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.value) + } +} + +impl Deref for SafeString { + type Target = str; + fn deref(&self) -> &str { + &self.value + } +} + +impl AsRef for SafeString { + fn as_ref(&self) -> &str { + &self.value + } +} + +use rocket::http::RawStr; +use rocket::request::FromFormValue; + +impl<'v> FromFormValue<'v> for SafeString { + type Error = &'v RawStr; + + fn from_form_value(form_value: &'v RawStr) -> Result { + let val = String::from_form_value(form_value)?; + Ok(SafeString::new(&val)) + } +} diff --git a/plume-models/src/schema.rs b/plume-models/src/schema.rs new file mode 100644 index 00000000000..70be8419dae --- /dev/null +++ b/plume-models/src/schema.rs @@ -0,0 +1,337 @@ +table! { + api_tokens (id) { + id -> Int4, + creation_date -> Timestamp, + value -> Text, + scopes -> Text, + app_id -> Int4, + user_id -> Int4, + } +} + +table! { + apps (id) { + id -> Int4, + name -> Text, + client_id -> Text, + client_secret -> Text, + redirect_uri -> Nullable, + website -> Nullable, + creation_date -> Timestamp, + } +} + +table! { + blog_authors (id) { + id -> Int4, + blog_id -> Int4, + author_id -> Int4, + is_owner -> Bool, + } +} + +table! { + blogs (id) { + id -> Int4, + actor_id -> Varchar, + title -> Varchar, + summary -> Text, + outbox_url -> Varchar, + inbox_url -> Varchar, + instance_id -> Int4, + creation_date -> Timestamp, + ap_url -> Text, + private_key -> Nullable, + public_key -> Text, + fqn -> Text, + summary_html -> Text, + icon_id -> Nullable, + banner_id -> Nullable, + theme -> Nullable, + } +} + +table! { + comments (id) { + id -> Int4, + content -> Text, + in_response_to_id -> Nullable, + post_id -> Int4, + author_id -> Int4, + creation_date -> Timestamp, + ap_url -> Nullable, + sensitive -> Bool, + spoiler_text -> Text, + public_visibility -> Bool, + } +} + +table! { + comment_seers (id) { + id -> Int4, + comment_id -> Int4, + user_id -> Int4, + } +} + +table! { + email_blocklist (id) { + id -> Int4, + email_address -> Text, + note -> Text, + notify_user -> Bool, + notification_text -> Text, + } +} + +table! { + email_signups (id) { + id -> Int4, + email -> Varchar, + token -> Varchar, + expiration_date -> Timestamp, + } +} + +table! { + follows (id) { + id -> Int4, + follower_id -> Int4, + following_id -> Int4, + ap_url -> Text, + } +} + +table! { + instances (id) { + id -> Int4, + public_domain -> Varchar, + name -> Varchar, + local -> Bool, + blocked -> Bool, + creation_date -> Timestamp, + open_registrations -> Bool, + short_description -> Text, + long_description -> Text, + default_license -> Text, + long_description_html -> Varchar, + short_description_html -> Varchar, + } +} + +table! { + likes (id) { + id -> Int4, + user_id -> Int4, + post_id -> Int4, + creation_date -> Timestamp, + ap_url -> Varchar, + } +} + +table! { + list_elems (id) { + id -> Int4, + list_id -> Int4, + user_id -> Nullable, + blog_id -> Nullable, + word -> Nullable, + } +} + +table! { + lists (id) { + id -> Int4, + name -> Varchar, + user_id -> Nullable, + #[sql_name = "type"] + type_ -> Int4, + } +} + +table! { + medias (id) { + id -> Int4, + file_path -> Text, + alt_text -> Text, + is_remote -> Bool, + remote_url -> Nullable, + sensitive -> Bool, + content_warning -> Nullable, + owner_id -> Int4, + } +} + +table! { + mentions (id) { + id -> Int4, + mentioned_id -> Int4, + post_id -> Nullable, + comment_id -> Nullable, + } +} + +table! { + notifications (id) { + id -> Int4, + user_id -> Int4, + creation_date -> Timestamp, + kind -> Varchar, + object_id -> Int4, + } +} + +table! { + password_reset_requests (id) { + id -> Int4, + email -> Varchar, + token -> Varchar, + expiration_date -> Timestamp, + } +} + +table! { + post_authors (id) { + id -> Int4, + post_id -> Int4, + author_id -> Int4, + } +} + +table! { + posts (id) { + id -> Int4, + blog_id -> Int4, + slug -> Varchar, + title -> Varchar, + content -> Text, + published -> Bool, + license -> Varchar, + creation_date -> Timestamp, + ap_url -> Varchar, + subtitle -> Text, + source -> Text, + cover_id -> Nullable, + } +} + +table! { + reshares (id) { + id -> Int4, + user_id -> Int4, + post_id -> Int4, + ap_url -> Varchar, + creation_date -> Timestamp, + } +} + +table! { + tags (id) { + id -> Int4, + tag -> Text, + is_hashtag -> Bool, + post_id -> Int4, + } +} + +table! { + timeline (id) { + id -> Int4, + post_id -> Int4, + timeline_id -> Int4, + } +} + +table! { + timeline_definition (id) { + id -> Int4, + user_id -> Nullable, + name -> Varchar, + query -> Varchar, + } +} + +table! { + users (id) { + id -> Int4, + username -> Varchar, + display_name -> Varchar, + outbox_url -> Varchar, + inbox_url -> Varchar, + summary -> Text, + email -> Nullable, + hashed_password -> Nullable, + instance_id -> Int4, + creation_date -> Timestamp, + ap_url -> Text, + private_key -> Nullable, + public_key -> Text, + shared_inbox_url -> Nullable, + followers_endpoint -> Varchar, + avatar_id -> Nullable, + last_fetched_date -> Timestamp, + fqn -> Text, + summary_html -> Text, + role -> Int4, + preferred_theme -> Nullable, + hide_custom_css -> Bool, + } +} + +joinable!(api_tokens -> apps (app_id)); +joinable!(api_tokens -> users (user_id)); +joinable!(blog_authors -> blogs (blog_id)); +joinable!(blog_authors -> users (author_id)); +joinable!(blogs -> instances (instance_id)); +joinable!(comment_seers -> comments (comment_id)); +joinable!(comment_seers -> users (user_id)); +joinable!(comments -> posts (post_id)); +joinable!(comments -> users (author_id)); +joinable!(likes -> posts (post_id)); +joinable!(likes -> users (user_id)); +joinable!(list_elems -> blogs (blog_id)); +joinable!(list_elems -> lists (list_id)); +joinable!(list_elems -> users (user_id)); +joinable!(lists -> users (user_id)); +joinable!(mentions -> comments (comment_id)); +joinable!(mentions -> posts (post_id)); +joinable!(mentions -> users (mentioned_id)); +joinable!(notifications -> users (user_id)); +joinable!(post_authors -> posts (post_id)); +joinable!(post_authors -> users (author_id)); +joinable!(posts -> blogs (blog_id)); +joinable!(posts -> medias (cover_id)); +joinable!(reshares -> posts (post_id)); +joinable!(reshares -> users (user_id)); +joinable!(tags -> posts (post_id)); +joinable!(timeline -> posts (post_id)); +joinable!(timeline -> timeline_definition (timeline_id)); +joinable!(timeline_definition -> users (user_id)); +joinable!(users -> instances (instance_id)); + +allow_tables_to_appear_in_same_query!( + api_tokens, + apps, + blog_authors, + blogs, + comments, + comment_seers, + email_blocklist, + email_signups, + follows, + instances, + likes, + list_elems, + lists, + medias, + mentions, + notifications, + password_reset_requests, + post_authors, + posts, + reshares, + tags, + timeline, + timeline_definition, + users, +); diff --git a/plume-models/src/search/actor.rs b/plume-models/src/search/actor.rs new file mode 100644 index 00000000000..d97718e08b8 --- /dev/null +++ b/plume-models/src/search/actor.rs @@ -0,0 +1,214 @@ +use super::Searcher; +use crate::{db_conn::DbPool, posts::PostEvent, ACTOR_SYS, POST_CHAN}; +use riker::actors::{Actor, ActorFactoryArgs, ActorRefFactory, Context, Sender, Subscribe, Tell}; +use std::sync::Arc; +use std::thread::sleep; +use std::time::Duration; +use tracing::error; + +pub struct SearchActor { + searcher: Arc, + conn: DbPool, +} + +impl SearchActor { + pub fn init(searcher: Arc, conn: DbPool) { + let actor = ACTOR_SYS + .actor_of_args::("search", (searcher, conn)) + .expect("Failed to initialize searcher actor"); + + POST_CHAN.tell( + Subscribe { + actor: Box::new(actor), + topic: "*".into(), + }, + None, + ) + } +} + +impl Actor for SearchActor { + type Msg = PostEvent; + + fn recv(&mut self, _ctx: &Context, msg: Self::Msg, _sender: Sender) { + use PostEvent::*; + + // Wait for transaction commited + sleep(Duration::from_millis(500)); + + match msg { + PostPublished(post) => { + let conn = self.conn.get(); + match conn { + Ok(conn) => { + self.searcher + .add_document(&conn, &post) + .unwrap_or_else(|e| error!("{:?}", e)); + } + _ => { + error!("Failed to get database connection"); + } + } + } + PostUpdated(post) => { + let conn = self.conn.get(); + match conn { + Ok(_) => { + self.searcher + .update_document(&conn.unwrap(), &post) + .unwrap_or_else(|e| error!("{:?}", e)); + } + _ => { + error!("Failed to get database connection"); + } + } + } + PostDeleted(post) => self.searcher.delete_document(&post), + } + } +} + +impl ActorFactoryArgs<(Arc, DbPool)> for SearchActor { + fn create_args((searcher, conn): (Arc, DbPool)) -> Self { + Self { searcher, conn } + } +} + +#[cfg(test)] +mod tests { + use crate::diesel::Connection; + use crate::{ + blog_authors::{BlogAuthor, NewBlogAuthor}, + blogs::{Blog, NewBlog}, + db_conn::{DbPool, PragmaForeignKey}, + instance::{Instance, NewInstance}, + post_authors::{NewPostAuthor, PostAuthor}, + posts::{NewPost, Post}, + safe_string::SafeString, + search::{actor::SearchActor, tests::get_searcher, Query}, + users::{NewUser, User}, + Connection as Conn, CONFIG, + }; + use diesel::r2d2::ConnectionManager; + use plume_common::utils::random_hex; + use std::str::FromStr; + use std::sync::Arc; + use std::thread::sleep; + use std::time::Duration; + + #[test] + fn post_updated() { + // Need to commit so that searcher on another thread retrieve records. + // So, build DbPool instead of using DB_POOL for testing. + let manager = ConnectionManager::::new(CONFIG.database_url.as_str()); + let db_pool = DbPool::builder() + .connection_customizer(Box::new(PragmaForeignKey)) + .build(manager) + .unwrap(); + + let searcher = Arc::new(get_searcher(&CONFIG.search_tokenizers)); + SearchActor::init(searcher.clone(), db_pool.clone()); + let conn = db_pool.clone().get().unwrap(); + + let title = random_hex()[..8].to_owned(); + let (_instance, _user, blog) = fill_database(&conn); + let author = &blog.list_authors(&conn).unwrap()[0]; + + let post = Post::insert( + &conn, + NewPost { + blog_id: blog.id, + slug: title.clone(), + title: title.clone(), + content: SafeString::new(""), + published: true, + license: "CC-BY-SA".to_owned(), + ap_url: "".to_owned(), + creation_date: None, + subtitle: "".to_owned(), + source: "".to_owned(), + cover_id: None, + }, + ) + .unwrap(); + PostAuthor::insert( + &conn, + NewPostAuthor { + post_id: post.id, + author_id: author.id, + }, + ) + .unwrap(); + let post_id = post.id; + + // Wait for searcher on another thread add document asynchronously + sleep(Duration::from_millis(700)); + searcher.commit(); + assert_eq!( + searcher.search_document(&conn, Query::from_str(&title).unwrap(), (0, 1))[0].id, + post_id + ); + } + + fn fill_database(conn: &Conn) -> (Instance, User, Blog) { + conn.transaction::<(Instance, User, Blog), diesel::result::Error, _>(|| { + let instance = Instance::insert( + conn, + NewInstance { + default_license: "CC-0-BY-SA".to_string(), + local: false, + long_description: SafeString::new("Good morning"), + long_description_html: "

Good morning

".to_string(), + short_description: SafeString::new("Hello"), + short_description_html: "

Hello

".to_string(), + name: random_hex().to_string(), + open_registrations: true, + public_domain: random_hex().to_string(), + }, + ) + .unwrap(); + let user = User::insert( + conn, + NewUser { + username: random_hex().to_string(), + display_name: random_hex().to_string(), + outbox_url: random_hex().to_string(), + inbox_url: random_hex().to_string(), + summary: "".to_string(), + email: None, + hashed_password: None, + instance_id: instance.id, + ap_url: random_hex().to_string(), + private_key: None, + public_key: "".to_string(), + shared_inbox_url: None, + followers_endpoint: random_hex().to_string(), + avatar_id: None, + summary_html: SafeString::new(""), + role: 0, + fqn: random_hex().to_string(), + }, + ) + .unwrap(); + let mut blog = NewBlog::default(); + blog.instance_id = instance.id; + blog.actor_id = random_hex().to_string(); + blog.ap_url = random_hex().to_string(); + blog.inbox_url = random_hex().to_string(); + blog.outbox_url = random_hex().to_string(); + let blog = Blog::insert(conn, blog).unwrap(); + BlogAuthor::insert( + conn, + NewBlogAuthor { + blog_id: blog.id, + author_id: user.id, + is_owner: true, + }, + ) + .unwrap(); + + Ok((instance, user, blog)) + }) + .unwrap() + } +} diff --git a/plume-models/src/search/mod.rs b/plume-models/src/search/mod.rs new file mode 100644 index 00000000000..917f7778e37 --- /dev/null +++ b/plume-models/src/search/mod.rs @@ -0,0 +1,250 @@ +pub mod actor; +mod query; +mod searcher; +mod tokenizer; +pub use self::query::PlumeQuery as Query; +pub use self::searcher::*; +pub use self::tokenizer::TokenizerKind; + +#[cfg(test)] +pub(crate) mod tests { + use super::{Query, Searcher}; + use crate::{ + blogs::tests::fill_database, + config::SearchTokenizerConfig, + post_authors::*, + posts::{NewPost, Post}, + safe_string::SafeString, + tests::db, + CONFIG, + }; + use diesel::Connection; + use plume_common::utils::random_hex; + use std::env::temp_dir; + use std::str::FromStr; + + pub(crate) fn get_searcher(tokenizers: &SearchTokenizerConfig) -> Searcher { + let dir = temp_dir().join(&format!("plume-test-{}", random_hex())); + if dir.exists() { + Searcher::open(&dir, tokenizers) + } else { + Searcher::create(&dir, tokenizers) + } + .unwrap() + } + + #[test] + fn get_first_token() { + let vector = vec![ + ("+\"my token\" other", ("+\"my token\"", " other")), + ("-\"my token\" other", ("-\"my token\"", " other")), + (" \"my token\" other", ("\"my token\"", " other")), + ("\"my token\" other", ("\"my token\"", " other")), + ("+my token other", ("+my", " token other")), + ("-my token other", ("-my", " token other")), + (" my token other", ("my", " token other")), + ("my token other", ("my", " token other")), + ("+\"my token other", ("+\"my token other", "")), + ("-\"my token other", ("-\"my token other", "")), + (" \"my token other", ("\"my token other", "")), + ("\"my token other", ("\"my token other", "")), + ]; + for (source, res) in vector { + assert_eq!(Query::get_first_token(source), res); + } + } + + #[test] + fn from_str() { + let vector = vec![ + ("", ""), + ("a query", "a query"), + ("\"a query\"", "\"a query\""), + ("+a -\"query\"", "+a -query"), + ("title:\"something\" a query", "a query title:something"), + ("-title:\"something\" a query", "a query -title:something"), + ("author:user@domain", "author:user@domain"), + ("-author:@user@domain", "-author:user@domain"), + ("before:2017-11-05 before:2018-01-01", "before:2017-11-05"), + ("after:2017-11-05 after:2018-01-01", "after:2018-01-01"), + ]; + for (source, res) in vector { + assert_eq!(&Query::from_str(source).unwrap().to_string(), res); + assert_eq!(Query::new().parse_query(source).to_string(), res); + } + } + + #[test] + fn setters() { + let vector = vec![ + ("something", "title:something"), + ("+something", "+title:something"), + ("-something", "-title:something"), + ("+\"something\"", "+title:something"), + ("+some thing", "+title:\"some thing\""), + ]; + for (source, res) in vector { + assert_eq!(&Query::new().title(source, None).to_string(), res); + } + + let vector = vec![ + ("something", "author:something"), + ("+something", "+author:something"), + ("-something", "-author:something"), + ("+\"something\"", "+author:something"), + ("+@someone@somewhere", "+author:someone@somewhere"), + ]; + for (source, res) in vector { + assert_eq!(&Query::new().author(source, None).to_string(), res); + } + } + + #[test] + fn open() { + let dir = temp_dir().join(format!("plume-test-{}", random_hex())); + { + Searcher::create(&dir, &CONFIG.search_tokenizers).unwrap(); + } + Searcher::open(&dir, &CONFIG.search_tokenizers).unwrap(); + } + + #[test] + fn create() { + let dir = temp_dir().join(format!("plume-test-{}", random_hex())); + + assert!(Searcher::open(&dir, &CONFIG.search_tokenizers).is_err()); + { + Searcher::create(&dir, &CONFIG.search_tokenizers).unwrap(); + } + Searcher::open(&dir, &CONFIG.search_tokenizers).unwrap(); //verify it's well created + } + + #[test] + fn search() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let searcher = get_searcher(&CONFIG.search_tokenizers); + let blog = &fill_database(conn).1[0]; + let author = &blog.list_authors(conn).unwrap()[0]; + + let title = random_hex()[..8].to_owned(); + + let mut post = Post::insert( + conn, + NewPost { + blog_id: blog.id, + slug: title.clone(), + title: title.clone(), + content: SafeString::new(""), + published: true, + license: "CC-BY-SA".to_owned(), + ap_url: "".to_owned(), + creation_date: None, + subtitle: "".to_owned(), + source: "".to_owned(), + cover_id: None, + }, + ) + .unwrap(); + PostAuthor::insert( + conn, + NewPostAuthor { + post_id: post.id, + author_id: author.id, + }, + ) + .unwrap(); + searcher.add_document(&conn, &post).unwrap(); + searcher.commit(); + assert_eq!( + searcher.search_document(conn, Query::from_str(&title).unwrap(), (0, 1))[0].id, + post.id + ); + + let newtitle = random_hex()[..8].to_owned(); + post.title = newtitle.clone(); + post.update(conn).unwrap(); + searcher.update_document(conn, &post).unwrap(); + searcher.commit(); + assert_eq!( + searcher.search_document(conn, Query::from_str(&newtitle).unwrap(), (0, 1))[0].id, + post.id + ); + assert!(searcher + .search_document(conn, Query::from_str(&title).unwrap(), (0, 1)) + .is_empty()); + + searcher.delete_document(&post); + searcher.commit(); + assert!(searcher + .search_document(conn, Query::from_str(&newtitle).unwrap(), (0, 1)) + .is_empty()); + Ok(()) + }); + } + + #[cfg(feature = "search-lindera")] + #[test] + fn search_japanese() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let tokenizers = SearchTokenizerConfig { + tag_tokenizer: TokenizerKind::Lindera, + content_tokenizer: TokenizerKind::Lindera, + property_tokenizer: TokenizerKind::Ngram, + }; + let searcher = get_searcher(&tokenizers); + let blog = &fill_database(conn).1[0]; + + let title = random_hex()[..8].to_owned(); + + let post = Post::insert( + conn, + NewPost { + blog_id: blog.id, + slug: title.clone(), + title: title.clone(), + content: SafeString::new("ブログエンジンPlumeです。"), + published: true, + license: "CC-BY-SA".to_owned(), + ap_url: "".to_owned(), + creation_date: None, + subtitle: "".to_owned(), + source: "".to_owned(), + cover_id: None, + }, + ) + .unwrap(); + + searcher.commit(); + + assert_eq!( + searcher.search_document(conn, Query::from_str("ブログエンジン").unwrap(), (0, 1)) + [0] + .id, + post.id + ); + assert_eq!( + searcher.search_document(conn, Query::from_str("Plume").unwrap(), (0, 1))[0].id, + post.id + ); + assert_eq!( + searcher.search_document(conn, Query::from_str("です").unwrap(), (0, 1))[0].id, + post.id + ); + assert_eq!( + searcher.search_document(conn, Query::from_str("。").unwrap(), (0, 1))[0].id, + post.id + ); + + Ok(()) + }); + } + + #[test] + fn drop_writer() { + let searcher = get_searcher(&CONFIG.search_tokenizers); + searcher.drop_writer(); + get_searcher(&CONFIG.search_tokenizers); + } +} diff --git a/plume-models/src/search/query.rs b/plume-models/src/search/query.rs new file mode 100644 index 00000000000..a2c4f2529c1 --- /dev/null +++ b/plume-models/src/search/query.rs @@ -0,0 +1,386 @@ +use crate::search::searcher::Searcher; +use chrono::{naive::NaiveDate, offset::Utc, Datelike}; +use std::{cmp, ops::Bound}; +use tantivy::{query::*, schema::*, Term}; + +//Generate functions for advanced search +macro_rules! gen_func { + ( $($field:ident),*; strip: $($strip:ident),* ) => { + $( //most fields go here, it's kinda the "default" way + pub fn $field(&mut self, mut val: &str, occur: Option) -> &mut Self { + if !val.trim_matches(&[' ', '"', '+', '-'][..]).is_empty() { + let occur = if let Some(occur) = occur { + occur + } else { + if val.get(0..1).map(|v| v=="+").unwrap_or(false) { + val = &val[1..]; + Occur::Must + } else if val.get(0..1).map(|v| v=="-").unwrap_or(false) { + val = &val[1..]; + Occur::MustNot + } else { + Occur::Should + } + }; + self.$field.push((occur, val.trim_matches(&[' ', '"'][..]).to_owned())); + } + self + } + )* + $( // blog and author go here, leading @ get dismissed + pub fn $strip(&mut self, mut val: &str, occur: Option) -> &mut Self { + if !val.trim_matches(&[' ', '"', '+', '-'][..]).is_empty() { + let occur = if let Some(occur) = occur { + occur + } else { + if val.get(0..1).map(|v| v=="+").unwrap_or(false) { + val = &val[1..]; + Occur::Must + } else if val.get(0..1).map(|v| v=="-").unwrap_or(false) { + val = &val[1..]; + Occur::MustNot + } else { + Occur::Should + } + }; + self.$strip.push((occur, val.trim_matches(&[' ', '"', '@'][..]).to_owned())); + } + self + } + )* + } +} + +//generate the parser for advanced query from string +macro_rules! gen_parser { + ( $self:ident, $query:ident, $occur:ident; normal: $($field:ident),*; date: $($date:ident),*) => { + $( // most fields go here + if $query.starts_with(concat!(stringify!($field), ':')) { + let new_query = &$query[concat!(stringify!($field), ':').len()..]; + let (token, rest) = Self::get_first_token(new_query); + $query = rest; + $self.$field(token, Some($occur)); + } else + )* + $( // dates (before/after) got here + if $query.starts_with(concat!(stringify!($date), ':')) { + let new_query = &$query[concat!(stringify!($date), ':').len()..]; + let (token, rest) = Self::get_first_token(new_query); + $query = rest; + if let Ok(token) = NaiveDate::parse_from_str(token, "%Y-%m-%d") { + $self.$date(&token); + } + } else + )* // fields without 'fieldname:' prefix are considered bare words, and will be searched in title, subtitle and content + { + let (token, rest) = Self::get_first_token($query); + $query = rest; + $self.text(token, Some($occur)); + } + } +} + +// generate the to_string, giving back a textual query from a PlumeQuery +macro_rules! gen_to_string { + ( $self:ident, $result:ident; normal: $($field:ident),*; date: $($date:ident),*) => { + $( + for (occur, val) in &$self.$field { + if val.contains(' ') { + $result.push_str(&format!("{}{}:\"{}\" ", Self::occur_to_str(*occur), stringify!($field), val)); + } else { + $result.push_str(&format!("{}{}:{} ", Self::occur_to_str(*occur), stringify!($field), val)); + } + } + )* + $( + for val in &$self.$date { + $result.push_str(&format!("{}:{} ", stringify!($date), NaiveDate::from_num_days_from_ce(*val as i32).format("%Y-%m-%d"))); + } + )* + } +} + +// convert PlumeQuery to Tantivy's Query +macro_rules! gen_to_query { + ( $self:ident, $result:ident; normal: $($normal:ident),*; oneoff: $($oneoff:ident),*) => { + $( // classic fields + for (occur, token) in $self.$normal { + $result.push((occur, Self::token_to_query(&token, stringify!($normal)))); + } + )* + $( // fields where having more than on Must make no sense in general, so it's considered a Must be one of these instead. + // Those fields are instance, author, blog, lang and license + let mut subresult = Vec::new(); + for (occur, token) in $self.$oneoff { + match occur { + Occur::Must => subresult.push((Occur::Should, Self::token_to_query(&token, stringify!($oneoff)))), + occur => $result.push((occur, Self::token_to_query(&token, stringify!($oneoff)))), + } + } + if !subresult.is_empty() { + $result.push((Occur::Must, Box::new(BooleanQuery::from(subresult)))); + } + )* + } +} + +#[derive(Default)] +pub struct PlumeQuery { + text: Vec<(Occur, String)>, + title: Vec<(Occur, String)>, + subtitle: Vec<(Occur, String)>, + content: Vec<(Occur, String)>, + tag: Vec<(Occur, String)>, + instance: Vec<(Occur, String)>, + author: Vec<(Occur, String)>, + blog: Vec<(Occur, String)>, + lang: Vec<(Occur, String)>, + license: Vec<(Occur, String)>, + before: Option, + after: Option, +} + +impl PlumeQuery { + /// Create a new empty Query + pub fn new() -> Self { + Default::default() + } + + /// Parse a query string into this Query + pub fn parse_query(&mut self, query: &str) -> &mut Self { + self.from_str_req(query.trim()) + } + + /// Convert this Query to a Tantivy Query + pub fn into_query(self) -> BooleanQuery { + let mut result: Vec<(Occur, Box)> = Vec::new(); + gen_to_query!(self, result; normal: title, subtitle, content, tag; + oneoff: instance, author, blog, lang, license); + + for (occur, token) in self.text { + // text entries need to be added as multiple Terms + match occur { + Occur::Must => { + // a Must mean this must be in one of title subtitle or content, not in all 3 + let subresult = vec![ + (Occur::Should, Self::token_to_query(&token, "title")), + (Occur::Should, Self::token_to_query(&token, "subtitle")), + (Occur::Should, Self::token_to_query(&token, "content")), + ]; + + result.push((Occur::Must, Box::new(BooleanQuery::from(subresult)))); + } + occur => { + result.push((occur, Self::token_to_query(&token, "title"))); + result.push((occur, Self::token_to_query(&token, "subtitle"))); + result.push((occur, Self::token_to_query(&token, "content"))); + } + } + } + + if self.before.is_some() || self.after.is_some() { + // if at least one range bound is provided + let after = self + .after + .unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce())); + let before = self + .before + .unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce())); + let field = Searcher::schema().get_field("creation_date").unwrap(); + let range = + RangeQuery::new_i64_bounds(field, Bound::Included(after), Bound::Included(before)); + result.push((Occur::Must, Box::new(range))); + } + + result.into() + } + + //generate most setters functions + gen_func!(text, title, subtitle, content, tag, instance, lang, license; strip: author, blog); + + // documents newer than the provided date will be ignored + pub fn before(&mut self, date: &D) -> &mut Self { + let before = self + .before + .unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce())); + self.before = Some(cmp::min(before, i64::from(date.num_days_from_ce()))); + self + } + + // documents older than the provided date will be ignored + pub fn after(&mut self, date: &D) -> &mut Self { + let after = self + .after + .unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce())); + self.after = Some(cmp::max(after, i64::from(date.num_days_from_ce()))); + self + } + + // split a string into a token and a rest + pub fn get_first_token(mut query: &str) -> (&str, &str) { + query = query.trim(); + if query.is_empty() { + ("", "") + } else if query.get(0..1).map(|v| v == "\"").unwrap_or(false) { + if let Some(index) = query[1..].find('"') { + query.split_at(index + 2) + } else { + (query, "") + } + } else if query + .get(0..2) + .map(|v| v == "+\"" || v == "-\"") + .unwrap_or(false) + { + if let Some(index) = query[2..].find('"') { + query.split_at(index + 3) + } else { + (query, "") + } + } else if let Some(index) = query.find(' ') { + query.split_at(index) + } else { + (query, "") + } + } + + // map each Occur state to a prefix + fn occur_to_str(occur: Occur) -> &'static str { + match occur { + Occur::Should => "", + Occur::Must => "+", + Occur::MustNot => "-", + } + } + + // recursive parser for query string + // allow this clippy lint for now, until someone figures out how to + // refactor this better. + #[allow(clippy::wrong_self_convention)] + fn from_str_req(&mut self, mut query: &str) -> &mut Self { + query = query.trim_start(); + if query.is_empty() { + return self; + } + + let occur = if query.get(0..1).map(|v| v == "+").unwrap_or(false) { + query = &query[1..]; + Occur::Must + } else if query.get(0..1).map(|v| v == "-").unwrap_or(false) { + query = &query[1..]; + Occur::MustNot + } else { + Occur::Should + }; + gen_parser!(self, query, occur; normal: title, subtitle, content, tag, + instance, author, blog, lang, license; + date: after, before); + self.from_str_req(query) + } + + // map a token and it's field to a query + fn token_to_query(token: &str, field_name: &str) -> Box { + let token = token.to_lowercase(); + let token = token.as_str(); + let field = Searcher::schema().get_field(field_name).unwrap(); + if token.contains('@') && (field_name == "author" || field_name == "blog") { + let pos = token.find('@').unwrap(); + let user_term = Term::from_field_text(field, &token[..pos]); + let instance_term = Term::from_field_text( + Searcher::schema().get_field("instance").unwrap(), + &token[pos + 1..], + ); + Box::new(BooleanQuery::from(vec![ + ( + Occur::Must, + Box::new(TermQuery::new( + user_term, + if field_name == "author" { + IndexRecordOption::Basic + } else { + IndexRecordOption::WithFreqsAndPositions + }, + )) as Box, + ), + ( + Occur::Must, + Box::new(TermQuery::new(instance_term, IndexRecordOption::Basic)), + ), + ])) + } else if token.contains(' ') { + // phrase query + match field_name { + "instance" | "author" | "tag" => + // phrase query are not available on these fields, treat it as multiple Term queries + { + Box::new(BooleanQuery::from( + token + .split_whitespace() + .map(|token| { + let term = Term::from_field_text(field, token); + ( + Occur::Should, + Box::new(TermQuery::new(term, IndexRecordOption::Basic)) + as Box, + ) + }) + .collect::>(), + )) + } + _ => Box::new(PhraseQuery::new( + token + .split_whitespace() + .map(|token| Term::from_field_text(field, token)) + .collect(), + )), + } + } else { + // Term Query + let term = Term::from_field_text(field, token); + let index_option = match field_name { + "instance" | "author" | "tag" => IndexRecordOption::Basic, + _ => IndexRecordOption::WithFreqsAndPositions, + }; + Box::new(TermQuery::new(term, index_option)) + } + } +} + +impl std::str::FromStr for PlumeQuery { + type Err = !; + + /// Create a new Query from &str + /// Same as doing + /// ```rust + /// # extern crate plume_models; + /// # use plume_models::search::Query; + /// let mut q = Query::new(); + /// q.parse_query("some query"); + /// ``` + fn from_str(query: &str) -> Result { + let mut res: PlumeQuery = Default::default(); + + res.from_str_req(query.trim()); + Ok(res) + } +} + +impl ToString for PlumeQuery { + fn to_string(&self) -> String { + let mut result = String::new(); + for (occur, val) in &self.text { + if val.contains(' ') { + result.push_str(&format!("{}\"{}\" ", Self::occur_to_str(*occur), val)); + } else { + result.push_str(&format!("{}{} ", Self::occur_to_str(*occur), val)); + } + } + + gen_to_string!(self, result; normal: title, subtitle, content, tag, + instance, author, blog, lang, license; + date: before, after); + + result.pop(); // remove trailing ' ' + result + } +} diff --git a/plume-models/src/search/searcher.rs b/plume-models/src/search/searcher.rs new file mode 100644 index 00000000000..d4ebb642413 --- /dev/null +++ b/plume-models/src/search/searcher.rs @@ -0,0 +1,310 @@ +use crate::{ + config::SearchTokenizerConfig, instance::Instance, posts::Post, schema::posts, + search::query::PlumeQuery, tags::Tag, Connection, Error, Result, +}; +use chrono::{Datelike, Utc}; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; +use itertools::Itertools; +use std::fs; +use std::{cmp, fs::create_dir_all, io, path::Path, sync::Mutex}; +use tantivy::{ + collector::TopDocs, directory::MmapDirectory, schema::*, Index, IndexReader, IndexWriter, + ReloadPolicy, TantivyError, Term, +}; +use tracing::warn; +use whatlang::{detect as detect_lang, Lang}; + +#[derive(Debug)] +pub enum SearcherError { + IndexCreationError, + WriteLockAcquisitionError, + IndexOpeningError, + IndexEditionError, + InvalidIndexDataError, +} + +pub struct Searcher { + index: Index, + reader: IndexReader, + writer: Mutex>, +} + +impl Searcher { + pub fn schema() -> Schema { + let tag_indexing = TextOptions::default().set_indexing_options( + TextFieldIndexing::default() + .set_tokenizer("tag_tokenizer") + .set_index_option(IndexRecordOption::Basic), + ); + + let content_indexing = TextOptions::default().set_indexing_options( + TextFieldIndexing::default() + .set_tokenizer("content_tokenizer") + .set_index_option(IndexRecordOption::WithFreqsAndPositions), + ); + + let property_indexing = TextOptions::default().set_indexing_options( + TextFieldIndexing::default() + .set_tokenizer("property_tokenizer") + .set_index_option(IndexRecordOption::WithFreqsAndPositions), + ); + + let mut schema_builder = SchemaBuilder::default(); + + schema_builder.add_i64_field("post_id", STORED | INDEXED); + schema_builder.add_i64_field("creation_date", INDEXED); + + schema_builder.add_text_field("instance", tag_indexing.clone()); + schema_builder.add_text_field("author", tag_indexing.clone()); + schema_builder.add_text_field("tag", tag_indexing); + + schema_builder.add_text_field("blog", content_indexing.clone()); + schema_builder.add_text_field("content", content_indexing.clone()); + schema_builder.add_text_field("subtitle", content_indexing.clone()); + schema_builder.add_text_field("title", content_indexing); + + schema_builder.add_text_field("lang", property_indexing.clone()); + schema_builder.add_text_field("license", property_indexing); + + schema_builder.build() + } + + pub fn open_or_recreate(path: &dyn AsRef, tokenizers: &SearchTokenizerConfig) -> Self { + let mut open_searcher = Self::open(path, tokenizers); + if let Err(Error::Search(SearcherError::InvalidIndexDataError)) = open_searcher { + if Self::create(path, tokenizers).is_err() { + let backup_path = format!("{}.{}", path.as_ref().display(), Utc::now().timestamp()); + let backup_path = Path::new(&backup_path); + fs::rename(path, backup_path) + .expect("main: error on backing up search index directory for recreating"); + if Self::create(path, tokenizers).is_ok() { + if fs::remove_dir_all(backup_path).is_err() { + warn!( + "error on removing backup directory: {}. it remains", + backup_path.display() + ); + } + } else { + panic!("main: error on recreating search index in new index format. remove search index and run `plm search init` manually"); + } + } + open_searcher = Self::open(path, tokenizers); + } + match open_searcher { + Ok(s) => s, + Err(Error::Search(e)) => match e { + SearcherError::WriteLockAcquisitionError => panic!( + r#" +Your search index is locked. Plume can't start. To fix this issue +make sure no other Plume instance is started, and run: + + plm search unlock + +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 +index, run this command: + + plm search init + +Then try to restart Plume +"# + ), + e => Err(e).unwrap(), + }, + _ => panic!("Unexpected error while opening search index"), + } + } + + pub fn create(path: &dyn AsRef, tokenizers: &SearchTokenizerConfig) -> Result { + let schema = Self::schema(); + + create_dir_all(path).map_err(|_| SearcherError::IndexCreationError)?; + let index = Index::create( + MmapDirectory::open(path).map_err(|_| SearcherError::IndexCreationError)?, + schema, + ) + .map_err(|_| SearcherError::IndexCreationError)?; + + { + let tokenizer_manager = index.tokenizers(); + tokenizer_manager.register("tag_tokenizer", tokenizers.tag_tokenizer); + tokenizer_manager.register("content_tokenizer", tokenizers.content_tokenizer); + tokenizer_manager.register("property_tokenizer", tokenizers.property_tokenizer); + } //to please the borrow checker + Ok(Self { + writer: Mutex::new(Some( + index + .writer(50_000_000) + .map_err(|_| SearcherError::WriteLockAcquisitionError)?, + )), + reader: index + .reader_builder() + .reload_policy(ReloadPolicy::Manual) + .try_into() + .map_err(|_| SearcherError::IndexCreationError)?, + index, + }) + } + + pub fn open(path: &dyn AsRef, tokenizers: &SearchTokenizerConfig) -> Result { + let mut index = + Index::open(MmapDirectory::open(path).map_err(|_| SearcherError::IndexOpeningError)?) + .map_err(|_| SearcherError::IndexOpeningError)?; + + { + let tokenizer_manager = index.tokenizers(); + tokenizer_manager.register("tag_tokenizer", tokenizers.tag_tokenizer); + tokenizer_manager.register("content_tokenizer", tokenizers.content_tokenizer); + tokenizer_manager.register("property_tokenizer", tokenizers.property_tokenizer); + } //to please the borrow checker + let writer = index + .writer(50_000_000) + .map_err(|_| SearcherError::WriteLockAcquisitionError)?; + + // Since Tantivy v0.12.0, IndexWriter::garbage_collect_files() returns Future. + // To avoid conflict with Plume async project, we don't introduce async now. + // After async is introduced to Plume, we can use garbage_collect_files() again. + // Algorithm stolen from Tantivy's SegmentUpdater::list_files() + use std::collections::HashSet; + use std::path::PathBuf; + let mut files: HashSet = index + .list_all_segment_metas() + .into_iter() + .flat_map(|segment_meta| segment_meta.list_files()) + .collect(); + files.insert(Path::new("meta.json").to_path_buf()); + index + .directory_mut() + .garbage_collect(|| files) + .map_err(|_| SearcherError::IndexEditionError)?; + + Ok(Self { + writer: Mutex::new(Some(writer)), + reader: index + .reader_builder() + .reload_policy(ReloadPolicy::Manual) + .try_into() + .map_err(|e| { + if let TantivyError::IOError(err) = e { + let err: io::Error = err.into(); + if err.kind() == io::ErrorKind::InvalidData { + // Search index was created in older Tantivy format. + SearcherError::InvalidIndexDataError + } else { + SearcherError::IndexCreationError + } + } else { + SearcherError::IndexCreationError + } + })?, + index, + }) + } + + pub fn add_document(&self, conn: &Connection, post: &Post) -> Result<()> { + if !post.published { + return Ok(()); + } + + let schema = self.index.schema(); + + let post_id = schema.get_field("post_id").unwrap(); + let creation_date = schema.get_field("creation_date").unwrap(); + + let instance = schema.get_field("instance").unwrap(); + let author = schema.get_field("author").unwrap(); + let tag = schema.get_field("tag").unwrap(); + + let blog_name = schema.get_field("blog").unwrap(); + let content = schema.get_field("content").unwrap(); + let subtitle = schema.get_field("subtitle").unwrap(); + let title = schema.get_field("title").unwrap(); + + let lang = schema.get_field("lang").unwrap(); + let license = schema.get_field("license").unwrap(); + + let mut writer = self.writer.lock().unwrap(); + let writer = writer.as_mut().unwrap(); + writer.add_document(doc!( + post_id => i64::from(post.id), + author => post.get_authors(conn)?.into_iter().map(|u| u.fqn).join(" "), + creation_date => i64::from(post.creation_date.num_days_from_ce()), + instance => Instance::get(conn, post.get_blog(conn)?.instance_id)?.public_domain, + tag => Tag::for_post(conn, post.id)?.into_iter().map(|t| t.tag).join(" "), + blog_name => post.get_blog(conn)?.title, + content => post.content.get().clone(), + subtitle => post.subtitle.clone(), + title => post.title.clone(), + lang => detect_lang(post.content.get()).and_then(|i| if i.is_reliable() { Some(i.lang()) } else {None} ).unwrap_or(Lang::Eng).name(), + license => post.license.clone(), + )); + Ok(()) + } + + pub fn delete_document(&self, post: &Post) { + let schema = self.index.schema(); + let post_id = schema.get_field("post_id").unwrap(); + + let doc_id = Term::from_field_i64(post_id, i64::from(post.id)); + let mut writer = self.writer.lock().unwrap(); + let writer = writer.as_mut().unwrap(); + writer.delete_term(doc_id); + } + + pub fn update_document(&self, conn: &Connection, post: &Post) -> Result<()> { + self.delete_document(post); + self.add_document(conn, post) + } + + pub fn search_document( + &self, + conn: &Connection, + query: PlumeQuery, + (min, max): (i32, i32), + ) -> Vec { + let schema = self.index.schema(); + let post_id = schema.get_field("post_id").unwrap(); + + let collector = TopDocs::with_limit(cmp::max(1, max) as usize); + + let searcher = self.reader.searcher(); + let res = searcher.search(&query.into_query(), &collector).unwrap(); + + res.get(min as usize..) + .unwrap_or(&[]) + .iter() + .filter_map(|(_, doc_add)| { + let doc = searcher.doc(*doc_add).ok()?; + let id = doc.get_first(post_id)?; + Post::get(conn, id.i64_value() as i32).ok() + //borrow checker don't want me to use filter_map or and_then here + }) + .collect() + } + + pub fn fill(&self, conn: &Connection) -> Result<()> { + for post in posts::table + .filter(posts::published.eq(true)) + .load::(conn)? + { + self.update_document(conn, &post)? + } + Ok(()) + } + + pub fn commit(&self) { + let mut writer = self.writer.lock().unwrap(); + writer.as_mut().unwrap().commit().unwrap(); + self.reader.reload().unwrap(); + } + + pub fn drop_writer(&self) { + self.writer.lock().unwrap().take(); + } +} diff --git a/plume-models/src/search/tokenizer.rs b/plume-models/src/search/tokenizer.rs new file mode 100644 index 00000000000..83228e6795e --- /dev/null +++ b/plume-models/src/search/tokenizer.rs @@ -0,0 +1,94 @@ +#[cfg(feature = "search-lindera")] +use lindera_tantivy::tokenizer::LinderaTokenizer; +use std::str::CharIndices; +use tantivy::tokenizer::*; + +#[derive(Clone, Copy)] +pub enum TokenizerKind { + Simple, + Ngram, + Whitespace, + #[cfg(feature = "search-lindera")] + Lindera, +} + +impl From for TextAnalyzer { + fn from(tokenizer: TokenizerKind) -> TextAnalyzer { + use TokenizerKind::*; + + match tokenizer { + Simple => TextAnalyzer::from(SimpleTokenizer) + .filter(RemoveLongFilter::limit(40)) + .filter(LowerCaser), + Ngram => TextAnalyzer::from(NgramTokenizer::new(2, 8, false)).filter(LowerCaser), + Whitespace => TextAnalyzer::from(WhitespaceTokenizer).filter(LowerCaser), + #[cfg(feature = "search-lindera")] + Lindera => { + TextAnalyzer::from(LinderaTokenizer::new("decompose", "")).filter(LowerCaser) + } + } + } +} + +/// Tokenize the text by splitting on whitespaces. Pretty much a copy of Tantivy's SimpleTokenizer, +/// but not splitting on punctuation +#[derive(Clone)] +pub struct WhitespaceTokenizer; + +pub struct WhitespaceTokenStream<'a> { + text: &'a str, + chars: CharIndices<'a>, + token: Token, +} + +impl Tokenizer for WhitespaceTokenizer { + fn token_stream<'a>(&self, text: &'a str) -> BoxTokenStream<'a> { + BoxTokenStream::from(WhitespaceTokenStream { + text, + chars: text.char_indices(), + token: Token::default(), + }) + } +} +impl<'a> WhitespaceTokenStream<'a> { + // search for the end of the current token. + fn search_token_end(&mut self) -> usize { + (&mut self.chars) + .filter(|&(_, ref c)| c.is_whitespace()) + .map(|(offset, _)| offset) + .next() + .unwrap_or(self.text.len()) + } +} + +impl<'a> TokenStream for WhitespaceTokenStream<'a> { + fn advance(&mut self) -> bool { + self.token.text.clear(); + self.token.position = self.token.position.wrapping_add(1); + + loop { + match self.chars.next() { + Some((offset_from, c)) => { + if !c.is_whitespace() { + let offset_to = self.search_token_end(); + self.token.offset_from = offset_from; + self.token.offset_to = offset_to; + self.token.text.push_str(&self.text[offset_from..offset_to]); + return true; + } + } + None => { + return false; + } + } + } + } + + fn token(&self) -> &Token { + &self.token + } + + fn token_mut(&mut self) -> &mut Token { + &mut self.token + } +} diff --git a/plume-models/src/signups.rs b/plume-models/src/signups.rs new file mode 100644 index 00000000000..7a520eab4ae --- /dev/null +++ b/plume-models/src/signups.rs @@ -0,0 +1,72 @@ +use crate::CONFIG; +use rocket::request::{FromRequest, Outcome, Request}; +use std::fmt; +use std::str::FromStr; + +pub enum Strategy { + Password, + Email, +} + +impl Default for Strategy { + fn default() -> Self { + Self::Password + } +} + +impl FromStr for Strategy { + type Err = StrategyError; + + fn from_str(s: &str) -> Result { + use self::Strategy::*; + + match s { + "password" => Ok(Password), + "email" => Ok(Email), + s => Err(StrategyError::Unsupported(s.to_string())), + } + } +} + +#[derive(Debug)] +pub enum StrategyError { + Unsupported(String), +} + +impl fmt::Display for StrategyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use self::StrategyError::*; + + match self { + // FIXME: Calc option strings from enum + Unsupported(s) => write!(f, "Unsupported strategy: {}. Choose password or email", s), + } + } +} + +impl std::error::Error for StrategyError {} + +pub struct Password(); +pub struct Email(); + +impl<'a, 'r> FromRequest<'a, 'r> for Password { + type Error = (); + + fn from_request(_request: &'a Request<'r>) -> Outcome { + match matches!(CONFIG.signup, Strategy::Password) { + true => Outcome::Success(Self()), + false => Outcome::Forward(()), + } + } +} + +impl<'a, 'r> FromRequest<'a, 'r> for Email { + type Error = (); + + fn from_request(_request: &'a Request<'r>) -> Outcome { + match matches!(CONFIG.signup, Strategy::Email) { + true => Outcome::Success(Self()), + false => Outcome::Forward(()), + } + } +} diff --git a/plume-models/src/tags.rs b/plume-models/src/tags.rs new file mode 100644 index 00000000000..0460efc208d --- /dev/null +++ b/plume-models/src/tags.rs @@ -0,0 +1,140 @@ +use crate::{ap_url, instance::Instance, schema::tags, Connection, Error, Result}; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; +use plume_common::activity_pub::Hashtag; + +#[derive(Clone, Identifiable, Queryable)] +pub struct Tag { + pub id: i32, + pub tag: String, + pub is_hashtag: bool, + pub post_id: i32, +} + +#[derive(Insertable)] +#[table_name = "tags"] +pub struct NewTag { + pub tag: String, + pub is_hashtag: bool, + pub post_id: i32, +} + +impl Tag { + insert!(tags, NewTag); + get!(tags); + find_by!(tags, find_by_name, tag as &str); + list_by!(tags, for_post, post_id as i32); + + pub fn to_activity(&self) -> Result { + let mut ht = Hashtag::default(); + ht.set_href_string(ap_url(&format!( + "{}/tag/{}", + Instance::get_local()?.public_domain, + self.tag + )))?; + ht.set_name_string(self.tag.clone())?; + Ok(ht) + } + + pub fn from_activity( + conn: &Connection, + tag: &Hashtag, + post: i32, + is_hashtag: bool, + ) -> Result { + Tag::insert( + conn, + NewTag { + tag: tag.name_string()?, + is_hashtag, + post_id: post, + }, + ) + } + + pub fn build_activity(tag: String) -> Result { + let mut ht = Hashtag::default(); + ht.set_href_string(ap_url(&format!( + "{}/tag/{}", + Instance::get_local()?.public_domain, + tag + )))?; + ht.set_name_string(tag)?; + Ok(ht) + } + + pub fn delete(&self, conn: &Connection) -> Result<()> { + diesel::delete(self) + .execute(conn) + .map(|_| ()) + .map_err(Error::from) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::db; + use crate::{diesel::Connection, inbox::tests::fill_database}; + use assert_json_diff::assert_json_eq; + use serde_json::to_value; + + #[test] + fn to_activity() { + let conn = &db(); + conn.test_transaction::<_, Error, _>(|| { + fill_database(conn); + let tag = Tag { + id: 0, + tag: "a_tag".into(), + is_hashtag: false, + post_id: 0, + }; + let act = tag.to_activity()?; + let expected = json!({ + "href": "https://plu.me/tag/a_tag", + "name": "a_tag", + "type": "Hashtag" + }); + + assert_json_eq!(to_value(&act)?, expected); + + Ok(()) + }) + } + + #[test] + fn from_activity() { + let conn = &db(); + conn.test_transaction::<_, Error, _>(|| { + let (posts, _users, _blogs) = fill_database(conn); + let post_id = posts[0].id; + let mut ht = Hashtag::default(); + ht.set_href_string(ap_url(&format!("https://plu.me/tag/a_tag")))?; + ht.set_name_string("a_tag".into())?; + let tag = Tag::from_activity(conn, &ht, post_id, true)?; + + assert_eq!(&tag.tag, "a_tag"); + assert!(tag.is_hashtag); + + Ok(()) + }); + } + + #[test] + fn build_activity() { + let conn = &db(); + conn.test_transaction::<_, Error, _>(|| { + fill_database(conn); + let act = Tag::build_activity("a_tag".into())?; + let expected = json!({ + "href": "https://plu.me/tag/a_tag", + "name": "a_tag", + "type": "Hashtag" + }); + + assert_json_eq!(to_value(&act)?, expected); + + Ok(()) + }); + } +} diff --git a/plume-models/src/timeline/mod.rs b/plume-models/src/timeline/mod.rs new file mode 100644 index 00000000000..d6b2a59d4f4 --- /dev/null +++ b/plume-models/src/timeline/mod.rs @@ -0,0 +1,858 @@ +use crate::{ + db_conn::DbConn, + lists::List, + posts::Post, + schema::{posts, timeline, timeline_definition}, + Connection, Error, Result, +}; +use diesel::{self, BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl}; +use std::ops::Deref; + +pub(crate) mod query; + +pub use self::query::Kind; +use self::query::{QueryError, TimelineQuery}; + +#[derive(Clone, Debug, PartialEq, Queryable, Identifiable, AsChangeset)] +#[table_name = "timeline_definition"] +pub struct Timeline { + pub id: i32, + pub user_id: Option, + pub name: String, + pub query: String, +} + +#[derive(Default, Insertable)] +#[table_name = "timeline_definition"] +pub struct NewTimeline { + user_id: Option, + name: String, + query: String, +} + +#[derive(Default, Insertable)] +#[table_name = "timeline"] +struct TimelineEntry { + pub post_id: i32, + pub timeline_id: i32, +} + +impl Timeline { + insert!(timeline_definition, NewTimeline); + get!(timeline_definition); + + pub fn find_for_user_by_name( + conn: &Connection, + user_id: Option, + name: &str, + ) -> Result { + if let Some(user_id) = user_id { + timeline_definition::table + .filter(timeline_definition::user_id.eq(user_id)) + .filter(timeline_definition::name.eq(name)) + .first(conn) + .map_err(Error::from) + } else { + timeline_definition::table + .filter(timeline_definition::user_id.is_null()) + .filter(timeline_definition::name.eq(name)) + .first(conn) + .map_err(Error::from) + } + } + + pub fn list_for_user(conn: &Connection, user_id: Option) -> Result> { + if let Some(user_id) = user_id { + timeline_definition::table + .filter(timeline_definition::user_id.eq(user_id)) + .load::(conn) + .map_err(Error::from) + } else { + timeline_definition::table + .filter(timeline_definition::user_id.is_null()) + .load::(conn) + .map_err(Error::from) + } + } + + /// Same as `list_for_user`, but also includes instance timelines if `user_id` is `Some`. + pub fn list_all_for_user(conn: &Connection, user_id: Option) -> Result> { + if let Some(user_id) = user_id { + timeline_definition::table + .filter( + timeline_definition::user_id + .eq(user_id) + .or(timeline_definition::user_id.is_null()), + ) + .load::(conn) + .map_err(Error::from) + } else { + timeline_definition::table + .filter(timeline_definition::user_id.is_null()) + .load::(conn) + .map_err(Error::from) + } + } + + pub fn new_for_user( + conn: &Connection, + user_id: i32, + name: String, + query_string: String, + ) -> Result { + { + let query = TimelineQuery::parse(&query_string)?; // verify the query is valid + if let Some(err) = + query + .list_used_lists() + .into_iter() + .find_map(|(name, kind)| { + let list = List::find_for_user_by_name(conn, Some(user_id), &name) + .map(|l| l.kind() == kind); + match list { + Ok(true) => None, + Ok(false) => Some(Error::TimelineQuery(QueryError::RuntimeError( + format!("list '{}' has the wrong type for this usage", name), + ))), + Err(_) => Some(Error::TimelineQuery(QueryError::RuntimeError( + format!("list '{}' was not found", name), + ))), + } + }) + { + return Err(err); + } + } + Self::insert( + conn, + NewTimeline { + user_id: Some(user_id), + name, + query: query_string, + }, + ) + } + + pub fn new_for_instance( + conn: &Connection, + name: String, + query_string: String, + ) -> Result { + { + let query = TimelineQuery::parse(&query_string)?; // verify the query is valid + if let Some(err) = + query + .list_used_lists() + .into_iter() + .find_map(|(name, kind)| { + let list = List::find_for_user_by_name(conn, None, &name) + .map(|l| l.kind() == kind); + match list { + Ok(true) => None, + Ok(false) => Some(Error::TimelineQuery(QueryError::RuntimeError( + format!("list '{}' has the wrong type for this usage", name), + ))), + Err(_) => Some(Error::TimelineQuery(QueryError::RuntimeError( + format!("list '{}' was not found", name), + ))), + } + }) + { + return Err(err); + } + } + Self::insert( + conn, + NewTimeline { + user_id: None, + name, + query: query_string, + }, + ) + } + + pub fn update(&self, conn: &Connection) -> Result { + diesel::update(self).set(self).execute(conn)?; + let timeline = Self::get(conn, self.id)?; + Ok(timeline) + } + + pub fn delete(&self, conn: &Connection) -> Result<()> { + diesel::delete(self) + .execute(conn) + .map(|_| ()) + .map_err(Error::from) + } + + pub fn get_latest(&self, conn: &Connection, count: i32) -> Result> { + self.get_page(conn, (0, count)) + } + + pub fn get_page(&self, conn: &Connection, (min, max): (i32, i32)) -> Result> { + timeline::table + .filter(timeline::timeline_id.eq(self.id)) + .inner_join(posts::table) + .order(posts::creation_date.desc()) + .offset(min.into()) + .limit((max - min).into()) + .select(posts::all_columns) + .load::(conn) + .map_err(Error::from) + } + + pub fn count_posts(&self, conn: &Connection) -> Result { + timeline::table + .filter(timeline::timeline_id.eq(self.id)) + .inner_join(posts::table) + .count() + .get_result(conn) + .map_err(Error::from) + } + + pub fn add_to_all_timelines(conn: &DbConn, post: &Post, kind: Kind<'_>) -> Result<()> { + let timelines = timeline_definition::table + .load::(conn.deref()) + .map_err(Error::from)?; + + for t in timelines { + if t.matches(conn, post, kind)? { + t.add_post(conn, post)?; + } + } + Ok(()) + } + + pub fn add_post(&self, conn: &Connection, post: &Post) -> Result<()> { + if self.includes_post(conn, post)? { + return Ok(()); + } + diesel::insert_into(timeline::table) + .values(TimelineEntry { + post_id: post.id, + timeline_id: self.id, + }) + .execute(conn)?; + Ok(()) + } + + pub fn matches(&self, conn: &DbConn, post: &Post, kind: Kind<'_>) -> Result { + let query = TimelineQuery::parse(&self.query)?; + query.matches(conn, self, post, kind) + } + + fn includes_post(&self, conn: &Connection, post: &Post) -> Result { + diesel::dsl::select(diesel::dsl::exists( + timeline::table + .filter(timeline::timeline_id.eq(self.id)) + .filter(timeline::post_id.eq(post.id)), + )) + .get_result(conn) + .map_err(Error::from) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + blogs::tests as blogTests, + follows::*, + lists::ListType, + post_authors::{NewPostAuthor, PostAuthor}, + posts::NewPost, + safe_string::SafeString, + tags::Tag, + tests::db, + users::tests as userTests, + }; + use diesel::Connection; + + #[test] + fn test_timeline() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let users = userTests::fill_database(&conn); + + let mut tl1_u1 = Timeline::new_for_user( + &conn, + users[0].id, + "my timeline".to_owned(), + "all".to_owned(), + ) + .unwrap(); + List::new( + &conn, + "languages I speak", + Some(&users[1]), + ListType::Prefix, + ) + .unwrap(); + let tl2_u1 = Timeline::new_for_user( + &conn, + users[0].id, + "another timeline".to_owned(), + "followed".to_owned(), + ) + .unwrap(); + let tl1_u2 = Timeline::new_for_user( + &conn, + users[1].id, + "english posts".to_owned(), + "lang in \"languages I speak\"".to_owned(), + ) + .unwrap(); + let tl1_instance = Timeline::new_for_instance( + &conn, + "english posts".to_owned(), + "license in [cc]".to_owned(), + ) + .unwrap(); + + assert_eq!(tl1_u1, Timeline::get(&conn, tl1_u1.id).unwrap()); + assert_eq!( + tl2_u1, + Timeline::find_for_user_by_name(&conn, Some(users[0].id), "another timeline") + .unwrap() + ); + assert_eq!( + tl1_instance, + Timeline::find_for_user_by_name(&conn, None, "english posts").unwrap() + ); + + let tl_u1 = Timeline::list_for_user(&conn, Some(users[0].id)).unwrap(); + assert_eq!(3, tl_u1.len()); // it is not 2 because there is a "Your feed" tl created for each user automatically + assert!(tl_u1.iter().fold(false, |res, tl| { res || *tl == tl1_u1 })); + assert!(tl_u1.iter().fold(false, |res, tl| { res || *tl == tl2_u1 })); + + let tl_instance = Timeline::list_for_user(&conn, None).unwrap(); + assert_eq!(3, tl_instance.len()); // there are also the local and federated feed by default + assert!(tl_instance + .iter() + .fold(false, |res, tl| { res || *tl == tl1_instance })); + + tl1_u1.name = "My Super TL".to_owned(); + let new_tl1_u2 = tl1_u2.update(&conn).unwrap(); + + let tl_u2 = Timeline::list_for_user(&conn, Some(users[1].id)).unwrap(); + assert_eq!(2, tl_u2.len()); // same here + assert!(tl_u2 + .iter() + .fold(false, |res, tl| { res || *tl == new_tl1_u2 })); + + Ok(()) + }); + } + + #[test] + fn test_timeline_creation_error() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let users = userTests::fill_database(&conn); + + assert!(Timeline::new_for_user( + &conn, + users[0].id, + "my timeline".to_owned(), + "invalid keyword".to_owned(), + ) + .is_err()); + assert!(Timeline::new_for_instance( + &conn, + "my timeline".to_owned(), + "invalid keyword".to_owned(), + ) + .is_err()); + + assert!(Timeline::new_for_user( + &conn, + users[0].id, + "my timeline".to_owned(), + "author in non_existant_list".to_owned(), + ) + .is_err()); + assert!(Timeline::new_for_instance( + &conn, + "my timeline".to_owned(), + "lang in dont-exist".to_owned(), + ) + .is_err()); + + List::new(&conn, "friends", Some(&users[0]), ListType::User).unwrap(); + List::new(&conn, "idk", None, ListType::Blog).unwrap(); + + assert!(Timeline::new_for_user( + &conn, + users[0].id, + "my timeline".to_owned(), + "blog in friends".to_owned(), + ) + .is_err()); + assert!(Timeline::new_for_instance( + &conn, + "my timeline".to_owned(), + "not author in idk".to_owned(), + ) + .is_err()); + + Ok(()) + }); + } + + #[test] + fn test_simple_match() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let (users, blogs) = blogTests::fill_database(&conn); + + let gnu_tl = Timeline::new_for_user( + &conn, + users[0].id, + "GNU timeline".to_owned(), + "license in [AGPL, LGPL, GPL]".to_owned(), + ) + .unwrap(); + + let gnu_post = Post::insert( + &conn, + NewPost { + blog_id: blogs[0].id, + slug: "slug".to_string(), + title: "About Linux".to_string(), + content: SafeString::new("you must say GNU/Linux, not Linux!!!"), + published: true, + license: "GPL".to_string(), + ap_url: "".to_string(), + creation_date: None, + subtitle: "".to_string(), + source: "you must say GNU/Linux, not Linux!!!".to_string(), + cover_id: None, + }, + ) + .unwrap(); + assert!(gnu_tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); + + let non_free_post = Post::insert( + &conn, + NewPost { + blog_id: blogs[0].id, + slug: "slug2".to_string(), + title: "Private is bad".to_string(), + content: SafeString::new("so is Microsoft"), + published: true, + license: "all right reserved".to_string(), + ap_url: "".to_string(), + creation_date: None, + subtitle: "".to_string(), + source: "so is Microsoft".to_string(), + cover_id: None, + }, + ) + .unwrap(); + assert!(!gnu_tl + .matches(&conn, &non_free_post, Kind::Original) + .unwrap()); + + Ok(()) + }); + } + + #[test] + fn test_complex_match() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let (users, blogs) = blogTests::fill_database(&conn); + Follow::insert( + &conn, + NewFollow { + follower_id: users[0].id, + following_id: users[1].id, + ap_url: String::new(), + }, + ) + .unwrap(); + + let fav_blogs_list = + List::new(&conn, "fav_blogs", Some(&users[0]), ListType::Blog).unwrap(); + fav_blogs_list.add_blogs(&conn, &[blogs[0].id]).unwrap(); + + let my_tl = Timeline::new_for_user( + &conn, + users[0].id, + "My timeline".to_owned(), + "blog in fav_blogs and not has_cover or local and followed exclude likes" + .to_owned(), + ) + .unwrap(); + + let post = Post::insert( + &conn, + NewPost { + blog_id: blogs[0].id, + slug: "about-linux".to_string(), + title: "About Linux".to_string(), + content: SafeString::new("you must say GNU/Linux, not Linux!!!"), + published: true, + license: "GPL".to_string(), + source: "you must say GNU/Linux, not Linux!!!".to_string(), + ap_url: "".to_string(), + creation_date: None, + subtitle: "".to_string(), + cover_id: None, + }, + ) + .unwrap(); + assert!(my_tl.matches(&conn, &post, Kind::Original).unwrap()); // matches because of "blog in fav_blogs" (and there is no cover) + + let post = Post::insert( + &conn, + NewPost { + blog_id: blogs[1].id, + slug: "about-linux-2".to_string(), + title: "About Linux (2)".to_string(), + content: SafeString::new( + "Actually, GNU+Linux, GNU×Linux, or GNU¿Linux are better.", + ), + published: true, + license: "GPL".to_string(), + source: "Actually, GNU+Linux, GNU×Linux, or GNU¿Linux are better.".to_string(), + ap_url: "".to_string(), + creation_date: None, + subtitle: "".to_string(), + cover_id: None, + }, + ) + .unwrap(); + assert!(!my_tl.matches(&conn, &post, Kind::Like(&users[1])).unwrap()); + + Ok(()) + }); + } + + #[test] + fn test_add_to_all_timelines() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let (users, blogs) = blogTests::fill_database(&conn); + + let gnu_tl = Timeline::new_for_user( + &conn, + users[0].id, + "GNU timeline".to_owned(), + "license in [AGPL, LGPL, GPL]".to_owned(), + ) + .unwrap(); + let non_gnu_tl = Timeline::new_for_user( + &conn, + users[0].id, + "Stallman disapproved timeline".to_owned(), + "not license in [AGPL, LGPL, GPL]".to_owned(), + ) + .unwrap(); + + let gnu_post = Post::insert( + &conn, + NewPost { + blog_id: blogs[0].id, + slug: "slug".to_string(), + title: "About Linux".to_string(), + content: SafeString::new("you must say GNU/Linux, not Linux!!!"), + published: true, + license: "GPL".to_string(), + ap_url: "".to_string(), + creation_date: None, + subtitle: "".to_string(), + source: "you must say GNU/Linux, not Linux!!!".to_string(), + cover_id: None, + }, + ) + .unwrap(); + + let non_free_post = Post::insert( + &conn, + NewPost { + blog_id: blogs[0].id, + slug: "slug2".to_string(), + title: "Private is bad".to_string(), + content: SafeString::new("so is Microsoft"), + published: true, + license: "all right reserved".to_string(), + ap_url: "".to_string(), + creation_date: None, + subtitle: "".to_string(), + source: "so is Microsoft".to_string(), + cover_id: None, + }, + ) + .unwrap(); + + Timeline::add_to_all_timelines(&conn, &gnu_post, Kind::Original).unwrap(); + Timeline::add_to_all_timelines(&conn, &non_free_post, Kind::Original).unwrap(); + + let res = gnu_tl.get_latest(&conn, 2).unwrap(); + assert_eq!(res.len(), 1); + assert_eq!(res[0].id, gnu_post.id); + let res = non_gnu_tl.get_latest(&conn, 2).unwrap(); + assert_eq!(res.len(), 1); + assert_eq!(res[0].id, non_free_post.id); + + Ok(()) + }); + } + + #[test] + fn test_matches_lists_direct() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let (users, blogs) = blogTests::fill_database(&conn); + + let gnu_post = Post::insert( + &conn, + NewPost { + blog_id: blogs[0].id, + slug: "slug".to_string(), + title: "About Linux".to_string(), + content: SafeString::new("you must say GNU/Linux, not Linux!!!"), + published: true, + license: "GPL".to_string(), + ap_url: "".to_string(), + creation_date: None, + subtitle: "".to_string(), + source: "you must say GNU/Linux, not Linux!!!".to_string(), + cover_id: None, + }, + ) + .unwrap(); + gnu_post + .update_tags(&conn, vec![Tag::build_activity("free".to_owned()).unwrap()]) + .unwrap(); + PostAuthor::insert( + &conn, + NewPostAuthor { + post_id: gnu_post.id, + author_id: blogs[0].list_authors(&conn).unwrap()[0].id, + }, + ) + .unwrap(); + + let tl = Timeline::new_for_user( + &conn, + users[0].id, + "blog timeline".to_owned(), + format!("blog in [{}]", blogs[0].fqn), + ) + .unwrap(); + assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); + tl.delete(&conn).unwrap(); + let tl = Timeline::new_for_user( + &conn, + users[0].id, + "blog timeline".to_owned(), + "blog in [no_one@nowhere]".to_owned(), + ) + .unwrap(); + assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); + tl.delete(&conn).unwrap(); + + let tl = Timeline::new_for_user( + &conn, + users[0].id, + "author timeline".to_owned(), + format!( + "author in [{}]", + blogs[0].list_authors(&conn).unwrap()[0].fqn + ), + ) + .unwrap(); + assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); + tl.delete(&conn).unwrap(); + let tl = Timeline::new_for_user( + &conn, + users[0].id, + "author timeline".to_owned(), + format!("author in [{}]", users[2].fqn), + ) + .unwrap(); + assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); + assert!(tl + .matches(&conn, &gnu_post, Kind::Reshare(&users[2])) + .unwrap()); + assert!(!tl.matches(&conn, &gnu_post, Kind::Like(&users[2])).unwrap()); + tl.delete(&conn).unwrap(); + let tl = Timeline::new_for_user( + &conn, + users[0].id, + "author timeline".to_owned(), + format!( + "author in [{}] include likes exclude reshares", + users[2].fqn + ), + ) + .unwrap(); + assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); + assert!(!tl + .matches(&conn, &gnu_post, Kind::Reshare(&users[2])) + .unwrap()); + assert!(tl.matches(&conn, &gnu_post, Kind::Like(&users[2])).unwrap()); + tl.delete(&conn).unwrap(); + + let tl = Timeline::new_for_user( + &conn, + users[0].id, + "tag timeline".to_owned(), + "tags in [free]".to_owned(), + ) + .unwrap(); + assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); + tl.delete(&conn).unwrap(); + let tl = Timeline::new_for_user( + &conn, + users[0].id, + "tag timeline".to_owned(), + "tags in [private]".to_owned(), + ) + .unwrap(); + assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); + tl.delete(&conn).unwrap(); + + let tl = Timeline::new_for_user( + &conn, + users[0].id, + "english timeline".to_owned(), + "lang in [en]".to_owned(), + ) + .unwrap(); + assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); + tl.delete(&conn).unwrap(); + let tl = Timeline::new_for_user( + &conn, + users[0].id, + "franco-italian timeline".to_owned(), + "lang in [fr, it]".to_owned(), + ) + .unwrap(); + assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); + tl.delete(&conn).unwrap(); + + Ok(()) + }); + } + + /* + #[test] + fn test_matches_lists_saved() { + let r = &rockets(); + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let (users, blogs) = blogTests::fill_database(&conn); + + let gnu_post = Post::insert( + &conn, + NewPost { + blog_id: blogs[0].id, + slug: "slug".to_string(), + title: "About Linux".to_string(), + content: SafeString::new("you must say GNU/Linux, not Linux!!!"), + published: true, + license: "GPL".to_string(), + ap_url: "".to_string(), + creation_date: None, + subtitle: "".to_string(), + source: "you must say GNU/Linux, not Linux!!!".to_string(), + cover_id: None, + }, + ) + .unwrap(); + gnu_post.update_tags(&conn, vec![Tag::build_activity("free".to_owned()).unwrap()]).unwrap(); + PostAuthor::insert(&conn, NewPostAuthor {post_id: gnu_post.id, author_id: blogs[0].list_authors(&conn).unwrap()[0].id}).unwrap(); + + unimplemented!(); + + Ok(()) + }); + }*/ + + #[test] + fn test_matches_keyword() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let (users, blogs) = blogTests::fill_database(&conn); + + let gnu_post = Post::insert( + &conn, + NewPost { + blog_id: blogs[0].id, + slug: "slug".to_string(), + title: "About Linux".to_string(), + content: SafeString::new("you must say GNU/Linux, not Linux!!!"), + published: true, + license: "GPL".to_string(), + ap_url: "".to_string(), + creation_date: None, + subtitle: "Stallman is our god".to_string(), + source: "you must say GNU/Linux, not Linux!!!".to_string(), + cover_id: None, + }, + ) + .unwrap(); + + let tl = Timeline::new_for_user( + &conn, + users[0].id, + "Linux title".to_owned(), + "title contains Linux".to_owned(), + ) + .unwrap(); + assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); + tl.delete(&conn).unwrap(); + let tl = Timeline::new_for_user( + &conn, + users[0].id, + "Microsoft title".to_owned(), + "title contains Microsoft".to_owned(), + ) + .unwrap(); + assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); + tl.delete(&conn).unwrap(); + + let tl = Timeline::new_for_user( + &conn, + users[0].id, + "Linux subtitle".to_owned(), + "subtitle contains Stallman".to_owned(), + ) + .unwrap(); + assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); + tl.delete(&conn).unwrap(); + let tl = Timeline::new_for_user( + &conn, + users[0].id, + "Microsoft subtitle".to_owned(), + "subtitle contains Nadella".to_owned(), + ) + .unwrap(); + assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); + tl.delete(&conn).unwrap(); + + let tl = Timeline::new_for_user( + &conn, + users[0].id, + "Linux content".to_owned(), + "content contains Linux".to_owned(), + ) + .unwrap(); + assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); + tl.delete(&conn).unwrap(); + let tl = Timeline::new_for_user( + &conn, + users[0].id, + "Microsoft content".to_owned(), + "subtitle contains Windows".to_owned(), + ) + .unwrap(); + assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); + tl.delete(&conn).unwrap(); + + Ok(()) + }); + } +} diff --git a/plume-models/src/timeline/query.rs b/plume-models/src/timeline/query.rs new file mode 100644 index 00000000000..0050944046d --- /dev/null +++ b/plume-models/src/timeline/query.rs @@ -0,0 +1,898 @@ +use crate::{ + blogs::Blog, + db_conn::DbConn, + lists::{self, ListType}, + posts::Post, + tags::Tag, + timeline::Timeline, + users::User, + Result, +}; +use plume_common::activity_pub::inbox::AsActor; +use whatlang::{self, Lang}; + +#[derive(Debug, Clone, PartialEq)] +pub enum QueryError { + SyntaxError(usize, usize, String), + UnexpectedEndOfQuery, + RuntimeError(String), +} + +pub type QueryResult = std::result::Result; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Kind<'a> { + Original, + Reshare(&'a User), + Like(&'a User), +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum Token<'a> { + LParent(usize), + RParent(usize), + LBracket(usize), + RBracket(usize), + Comma(usize), + Word(usize, usize, &'a str), +} + +impl<'a> Token<'a> { + fn get_text(&self) -> &'a str { + match self { + Token::Word(_, _, s) => s, + Token::LParent(_) => "(", + Token::RParent(_) => ")", + Token::LBracket(_) => "[", + Token::RBracket(_) => "]", + Token::Comma(_) => ",", + } + } + + fn get_pos(&self) -> (usize, usize) { + match self { + Token::Word(a, b, _) => (*a, *b), + Token::LParent(a) + | Token::RParent(a) + | Token::LBracket(a) + | Token::RBracket(a) + | Token::Comma(a) => (*a, 1), + } + } + + fn get_error(&self, token: Token<'_>) -> QueryResult { + let (b, e) = self.get_pos(); + let message = format!( + "Syntax Error: Expected {}, got {}", + token.to_string(), + self.to_string() + ); + Err(QueryError::SyntaxError(b, e, message)) + } +} + +impl<'a> ToString for Token<'a> { + fn to_string(&self) -> String { + if let Token::Word(0, 0, v) = self { + return (*v).to_string(); + } + format!( + "'{}'", + match self { + Token::Word(_, _, v) => v, + Token::LParent(_) => "(", + Token::RParent(_) => ")", + Token::LBracket(_) => "[", + Token::RBracket(_) => "]", + Token::Comma(_) => ",", + } + ) + } +} + +macro_rules! gen_tokenizer { + ( ($c:ident,$i:ident), $state:ident, $quote:ident; $([$char:tt, $variant:tt]),*) => { + match $c { + space if !*$quote && space.is_whitespace() => match $state.take() { + Some(v) => vec![v], + None => vec![], + }, + $( + $char if !*$quote => match $state.take() { + Some(v) => vec![v, Token::$variant($i)], + None => vec![Token::$variant($i)], + }, + )* + '"' => { + *$quote = !*$quote; + vec![] + }, + _ => match $state.take() { + Some(Token::Word(b, l, _)) => { + *$state = Some(Token::Word(b, l+1, &"")); + vec![] + }, + None => { + *$state = Some(Token::Word($i,1,&"")); + vec![] + }, + _ => unreachable!(), + } + } + } +} + +fn lex(stream: &str) -> Vec> { + stream + .chars() + .chain(" ".chars()) // force a last whitespace to empty scan's state + .zip(0..) + .scan((None, false), |(state, quote), (c, i)| { + Some(gen_tokenizer!((c,i), state, quote; + ['(', LParent], [')', RParent], + ['[', LBracket], [']', RBracket], + [',', Comma])) + }) + .flatten() + .map(|t| { + if let Token::Word(b, e, _) = t { + Token::Word(b, e, &stream[b..b + e]) + } else { + t + } + }) + .collect() +} + +/// Private internals of TimelineQuery +#[derive(Debug, Clone, PartialEq)] +enum TQ<'a> { + Or(Vec>), + And(Vec>), + Arg(Arg<'a>, bool), +} + +impl<'a> TQ<'a> { + fn matches( + &self, + conn: &DbConn, + timeline: &Timeline, + post: &Post, + kind: Kind<'_>, + ) -> Result { + match self { + TQ::Or(inner) => inner.iter().try_fold(false, |s, e| { + e.matches(conn, timeline, post, kind).map(|r| s || r) + }), + TQ::And(inner) => inner.iter().try_fold(true, |s, e| { + e.matches(conn, timeline, post, kind).map(|r| s && r) + }), + TQ::Arg(inner, invert) => Ok(inner.matches(conn, timeline, post, kind)? ^ invert), + } + } + + fn list_used_lists(&self) -> Vec<(String, ListType)> { + match self { + TQ::Or(inner) => inner.iter().flat_map(TQ::list_used_lists).collect(), + TQ::And(inner) => inner.iter().flat_map(TQ::list_used_lists).collect(), + TQ::Arg(Arg::In(typ, List::List(name)), _) => vec![( + (*name).to_string(), + match typ { + WithList::Blog => ListType::Blog, + WithList::Author { .. } => ListType::User, + WithList::License => ListType::Word, + WithList::Tags => ListType::Word, + WithList::Lang => ListType::Prefix, + }, + )], + TQ::Arg(_, _) => vec![], + } + } +} + +#[derive(Debug, Clone, PartialEq)] +enum Arg<'a> { + In(WithList, List<'a>), + Contains(WithContains, &'a str), + Boolean(Bool), +} + +impl<'a> Arg<'a> { + pub fn matches( + &self, + conn: &DbConn, + timeline: &Timeline, + post: &Post, + kind: Kind<'_>, + ) -> Result { + match self { + Arg::In(t, l) => t.matches(conn, timeline, post, l, kind), + Arg::Contains(t, v) => t.matches(post, v), + Arg::Boolean(t) => t.matches(conn, timeline, post, kind), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +enum WithList { + Blog, + Author { boosts: bool, likes: bool }, + License, + Tags, + Lang, +} + +impl WithList { + pub fn matches( + &self, + conn: &DbConn, + timeline: &Timeline, + post: &Post, + list: &List<'_>, + kind: Kind<'_>, + ) -> Result { + match list { + List::List(name) => { + let list = lists::List::find_for_user_by_name(conn, timeline.user_id, name)?; + match (self, list.kind()) { + (WithList::Blog, ListType::Blog) => list.contains_blog(conn, post.blog_id), + (WithList::Author { boosts, likes }, ListType::User) => match kind { + Kind::Original => Ok(list + .list_users(conn)? + .iter() + .any(|a| post.is_author(conn, a.id).unwrap_or(false))), + Kind::Reshare(u) => { + if *boosts { + list.contains_user(conn, u.id) + } else { + Ok(false) + } + } + Kind::Like(u) => { + if *likes { + list.contains_user(conn, u.id) + } else { + Ok(false) + } + } + }, + (WithList::License, ListType::Word) => list.contains_word(conn, &post.license), + (WithList::Tags, ListType::Word) => { + let tags = Tag::for_post(conn, post.id)?; + Ok(list + .list_words(conn)? + .iter() + .any(|s| tags.iter().any(|t| s == &t.tag))) + } + (WithList::Lang, ListType::Prefix) => { + let lang = whatlang::detect(post.content.get()) + .and_then(|i| { + if i.is_reliable() { + Some(i.lang()) + } else { + None + } + }) + .unwrap_or(Lang::Eng) + .name(); + list.contains_prefix(conn, lang) + } + (_, _) => Err(QueryError::RuntimeError(format!( + "The list '{}' is of the wrong type for this usage", + name + )) + .into()), + } + } + List::Array(list) => match self { + WithList::Blog => Ok(list + .iter() + .filter_map(|b| Blog::find_by_fqn(conn, b).ok()) + .any(|b| b.id == post.blog_id)), + WithList::Author { boosts, likes } => match kind { + Kind::Original => Ok(list + .iter() + .filter_map(|a| User::find_by_fqn(&*conn, a).ok()) + .any(|a| post.is_author(conn, a.id).unwrap_or(false))), + Kind::Reshare(u) => { + if *boosts { + Ok(list.iter().any(|user| &u.fqn == user)) + } else { + Ok(false) + } + } + Kind::Like(u) => { + if *likes { + Ok(list.iter().any(|user| &u.fqn == user)) + } else { + Ok(false) + } + } + }, + WithList::License => Ok(list.iter().any(|s| s == &post.license)), + WithList::Tags => { + let tags = Tag::for_post(conn, post.id)?; + Ok(list.iter().any(|s| tags.iter().any(|t| s == &t.tag))) + } + WithList::Lang => { + let lang = whatlang::detect(post.content.get()) + .and_then(|i| { + if i.is_reliable() { + Some(i.lang()) + } else { + None + } + }) + .unwrap_or(Lang::Eng) + .name() + .to_lowercase(); + Ok(list.iter().any(|s| lang.starts_with(&s.to_lowercase()))) + } + }, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +enum WithContains { + Title, + Subtitle, + Content, +} + +impl WithContains { + pub fn matches(&self, post: &Post, value: &str) -> Result { + match self { + WithContains::Title => Ok(post.title.contains(value)), + WithContains::Subtitle => Ok(post.subtitle.contains(value)), + WithContains::Content => Ok(post.content.contains(value)), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +enum Bool { + Followed { boosts: bool, likes: bool }, + HasCover, + Local, + All, +} + +impl Bool { + pub fn matches( + &self, + conn: &DbConn, + timeline: &Timeline, + post: &Post, + kind: Kind<'_>, + ) -> Result { + match self { + Bool::Followed { boosts, likes } => { + if timeline.user_id.is_none() { + return Ok(false); + } + let user = timeline.user_id.unwrap(); + match kind { + Kind::Original => post + .get_authors(conn)? + .iter() + .try_fold(false, |s, a| a.is_followed_by(conn, user).map(|r| s || r)), + Kind::Reshare(u) => { + if *boosts { + u.is_followed_by(conn, user) + } else { + Ok(false) + } + } + Kind::Like(u) => { + if *likes { + u.is_followed_by(conn, user) + } else { + Ok(false) + } + } + } + } + Bool::HasCover => Ok(post.cover_id.is_some()), + Bool::Local => Ok(post.get_blog(conn)?.is_local() && kind == Kind::Original), + Bool::All => Ok(kind == Kind::Original), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +enum List<'a> { + List(&'a str), + Array(Vec<&'a str>), +} + +fn parse_s<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> { + let mut res = Vec::new(); + let (left, token) = parse_a(stream)?; + res.push(token); + stream = left; + while !stream.is_empty() { + match stream[0] { + Token::Word(_, _, and) if and == "or" => {} + _ => break, + } + let (left, token) = parse_a(&stream[1..])?; + res.push(token); + stream = left; + } + + if res.len() == 1 { + Ok((stream, res.remove(0))) + } else { + Ok((stream, TQ::Or(res))) + } +} + +fn parse_a<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> { + let mut res = Vec::new(); + let (left, token) = parse_b(stream)?; + res.push(token); + stream = left; + while !stream.is_empty() { + match stream[0] { + Token::Word(_, _, and) if and == "and" => {} + _ => break, + } + let (left, token) = parse_b(&stream[1..])?; + res.push(token); + stream = left; + } + + if res.len() == 1 { + Ok((stream, res.remove(0))) + } else { + Ok((stream, TQ::And(res))) + } +} + +fn parse_b<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> { + match stream.get(0) { + Some(Token::LParent(_)) => { + let (left, token) = parse_s(&stream[1..])?; + match left.get(0) { + Some(Token::RParent(_)) => Ok((&left[1..], token)), + Some(t) => t.get_error(Token::RParent(0)), + None => Err(QueryError::UnexpectedEndOfQuery), + } + } + _ => parse_c(stream), + } +} + +fn parse_c<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> { + match stream.get(0) { + Some(Token::Word(_, _, not)) if not == &"not" => { + let (left, token) = parse_d(&stream[1..])?; + Ok((left, TQ::Arg(token, true))) + } + _ => { + let (left, token) = parse_d(stream)?; + Ok((left, TQ::Arg(token, false))) + } + } +} + +fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Arg<'a>)> { + match stream + .get(0) + .map(Token::get_text) + .ok_or(QueryError::UnexpectedEndOfQuery)? + { + s @ "blog" | s @ "author" | s @ "license" | s @ "tags" | s @ "lang" => { + match stream.get(1).ok_or(QueryError::UnexpectedEndOfQuery)? { + Token::Word(_, _, r#in) if r#in == &"in" => { + let (mut left, list) = parse_l(&stream[2..])?; + let kind = match s { + "blog" => WithList::Blog, + "author" => { + let mut boosts = true; + let mut likes = false; + while let Some(Token::Word(s, e, clude)) = left.get(0) { + if *clude != "include" && *clude != "exclude" { + break; + } + match ( + *clude, + left.get(1) + .map(Token::get_text) + .ok_or(QueryError::UnexpectedEndOfQuery)?, + ) { + ("include", "reshares") | ("include", "reshare") => { + boosts = true + } + ("exclude", "reshares") | ("exclude", "reshare") => { + boosts = false + } + ("include", "likes") | ("include", "like") => likes = true, + ("exclude", "likes") | ("exclude", "like") => likes = false, + (_, w) => { + return Token::Word(*s, *e, w).get_error(Token::Word( + 0, + 0, + "one of 'likes' or 'reshares'", + )) + } + } + left = &left[2..]; + } + WithList::Author { boosts, likes } + } + "license" => WithList::License, + "tags" => WithList::Tags, + "lang" => WithList::Lang, + _ => unreachable!(), + }; + Ok((left, Arg::In(kind, list))) + } + t => t.get_error(Token::Word(0, 0, "'in'")), + } + } + s @ "title" | s @ "subtitle" | s @ "content" => match ( + stream.get(1).ok_or(QueryError::UnexpectedEndOfQuery)?, + stream.get(2).ok_or(QueryError::UnexpectedEndOfQuery)?, + ) { + (Token::Word(_, _, contains), Token::Word(_, _, w)) if contains == &"contains" => Ok(( + &stream[3..], + Arg::Contains( + match s { + "title" => WithContains::Title, + "subtitle" => WithContains::Subtitle, + "content" => WithContains::Content, + _ => unreachable!(), + }, + w, + ), + )), + (Token::Word(_, _, contains), t) if contains == &"contains" => { + t.get_error(Token::Word(0, 0, "any word")) + } + (t, _) => t.get_error(Token::Word(0, 0, "'contains'")), + }, + s @ "followed" | s @ "has_cover" | s @ "local" | s @ "all" => match s { + "followed" => { + let mut boosts = true; + let mut likes = false; + while let Some(Token::Word(s, e, clude)) = stream.get(1) { + if *clude != "include" && *clude != "exclude" { + break; + } + match ( + *clude, + stream + .get(2) + .map(Token::get_text) + .ok_or(QueryError::UnexpectedEndOfQuery)?, + ) { + ("include", "reshares") | ("include", "reshare") => boosts = true, + ("exclude", "reshares") | ("exclude", "reshare") => boosts = false, + ("include", "likes") | ("include", "like") => likes = true, + ("exclude", "likes") | ("exclude", "like") => likes = false, + (_, w) => { + return Token::Word(*s, *e, w).get_error(Token::Word( + 0, + 0, + "one of 'likes' or 'boosts'", + )) + } + } + stream = &stream[2..]; + } + Ok((&stream[1..], Arg::Boolean(Bool::Followed { boosts, likes }))) + } + "has_cover" => Ok((&stream[1..], Arg::Boolean(Bool::HasCover))), + "local" => Ok((&stream[1..], Arg::Boolean(Bool::Local))), + "all" => Ok((&stream[1..], Arg::Boolean(Bool::All))), + _ => unreachable!(), + }, + _ => stream + .get(0) + .ok_or(QueryError::UnexpectedEndOfQuery)? + .get_error(Token::Word( + 0, + 0, + "one of 'blog', 'author', 'license', 'tags', 'lang', \ + 'title', 'subtitle', 'content', 'followed', 'has_cover', 'local' or 'all'", + )), + } +} + +fn parse_l<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], List<'a>)> { + match stream.get(0).ok_or(QueryError::UnexpectedEndOfQuery)? { + Token::LBracket(_) => { + let (left, list) = parse_m(&stream[1..])?; + match left.get(0).ok_or(QueryError::UnexpectedEndOfQuery)? { + Token::RBracket(_) => Ok((&left[1..], List::Array(list))), + t => t.get_error(Token::Word(0, 0, "one of ']' or ','")), + } + } + Token::Word(_, _, list) => Ok((&stream[1..], List::List(list))), + t => t.get_error(Token::Word(0, 0, "one of [list, of, words] or list_name")), + } +} + +fn parse_m<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Vec<&'a str>)> { + let mut res: Vec<&str> = vec![ + match stream.get(0).ok_or(QueryError::UnexpectedEndOfQuery)? { + Token::Word(_, _, w) => w, + t => return t.get_error(Token::Word(0, 0, "any word")), + }, + ]; + stream = &stream[1..]; + while let Token::Comma(_) = stream[0] { + res.push( + match stream.get(1).ok_or(QueryError::UnexpectedEndOfQuery)? { + Token::Word(_, _, w) => w, + t => return t.get_error(Token::Word(0, 0, "any word")), + }, + ); + stream = &stream[2..]; + } + + Ok((stream, res)) +} + +#[derive(Debug, Clone)] +pub struct TimelineQuery<'a>(TQ<'a>); + +impl<'a> TimelineQuery<'a> { + pub fn parse(query: &'a str) -> QueryResult { + parse_s(&lex(query)) + .and_then(|(left, res)| { + if left.is_empty() { + Ok(res) + } else { + left[0].get_error(Token::Word(0, 0, "on of 'or' or 'and'")) + } + }) + .map(TimelineQuery) + } + + pub fn matches( + &self, + conn: &DbConn, + timeline: &Timeline, + post: &Post, + kind: Kind<'_>, + ) -> Result { + self.0.matches(conn, timeline, post, kind) + } + + pub fn list_used_lists(&self) -> Vec<(String, ListType)> { + self.0.list_used_lists() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_lexer() { + assert_eq!( + lex("()[ ],two words \"something quoted with , and [\""), + vec![ + Token::LParent(0), + Token::RParent(1), + Token::LBracket(2), + Token::RBracket(4), + Token::Comma(5), + Token::Word(6, 3, "two"), + Token::Word(10, 5, "words"), + Token::Word(17, 29, "something quoted with , and ["), + ] + ); + } + + #[test] + fn test_parser() { + let q = TimelineQuery::parse(r#"lang in [fr, en] and (license in my_fav_lic or not followed) or title contains "Plume is amazing""#) + .unwrap(); + assert_eq!( + q.0, + TQ::Or(vec![ + TQ::And(vec![ + TQ::Arg( + Arg::In(WithList::Lang, List::Array(vec!["fr", "en"]),), + false + ), + TQ::Or(vec![ + TQ::Arg(Arg::In(WithList::License, List::List("my_fav_lic"),), false), + TQ::Arg( + Arg::Boolean(Bool::Followed { + boosts: true, + likes: false + }), + true + ), + ]), + ]), + TQ::Arg( + Arg::Contains(WithContains::Title, "Plume is amazing",), + false + ), + ]) + ); + + let lists = TimelineQuery::parse( + r#"blog in a or author in b include likes or license in c or tags in d or lang in e "#, + ) + .unwrap(); + assert_eq!( + lists.0, + TQ::Or(vec![ + TQ::Arg(Arg::In(WithList::Blog, List::List("a"),), false), + TQ::Arg( + Arg::In( + WithList::Author { + boosts: true, + likes: true + }, + List::List("b"), + ), + false + ), + TQ::Arg(Arg::In(WithList::License, List::List("c"),), false), + TQ::Arg(Arg::In(WithList::Tags, List::List("d"),), false), + TQ::Arg(Arg::In(WithList::Lang, List::List("e"),), false), + ]) + ); + + let contains = TimelineQuery::parse( + r#"title contains a or subtitle contains b or content contains c"#, + ) + .unwrap(); + assert_eq!( + contains.0, + TQ::Or(vec![ + TQ::Arg(Arg::Contains(WithContains::Title, "a"), false), + TQ::Arg(Arg::Contains(WithContains::Subtitle, "b"), false), + TQ::Arg(Arg::Contains(WithContains::Content, "c"), false), + ]) + ); + + let booleans = TimelineQuery::parse( + r#"followed include like exclude reshares and has_cover and local and all"#, + ) + .unwrap(); + assert_eq!( + booleans.0, + TQ::And(vec![ + TQ::Arg( + Arg::Boolean(Bool::Followed { + boosts: false, + likes: true + }), + false + ), + TQ::Arg(Arg::Boolean(Bool::HasCover), false), + TQ::Arg(Arg::Boolean(Bool::Local), false), + TQ::Arg(Arg::Boolean(Bool::All), false), + ]) + ); + } + + #[test] + fn test_rejection_parser() { + let missing_and_or = TimelineQuery::parse(r#"followed or has_cover local"#).unwrap_err(); + assert_eq!( + missing_and_or, + QueryError::SyntaxError( + 22, + 5, + "Syntax Error: Expected on of 'or' or 'and', got 'local'".to_owned() + ) + ); + + let unbalanced_parent = + TimelineQuery::parse(r#"followed and (has_cover or local"#).unwrap_err(); + assert_eq!(unbalanced_parent, QueryError::UnexpectedEndOfQuery); + + let missing_and_or_in_par = + TimelineQuery::parse(r#"(title contains "abc def" followed)"#).unwrap_err(); + assert_eq!( + missing_and_or_in_par, + QueryError::SyntaxError( + 26, + 8, + "Syntax Error: Expected ')', got 'followed'".to_owned() + ) + ); + + let expect_in = TimelineQuery::parse(r#"lang contains abc"#).unwrap_err(); + assert_eq!( + expect_in, + QueryError::SyntaxError( + 5, + 8, + "Syntax Error: Expected 'in', got 'contains'".to_owned() + ) + ); + + let expect_contains = TimelineQuery::parse(r#"title in abc"#).unwrap_err(); + assert_eq!( + expect_contains, + QueryError::SyntaxError( + 6, + 2, + "Syntax Error: Expected 'contains', got 'in'".to_owned() + ) + ); + + let expect_keyword = TimelineQuery::parse(r#"not_a_field contains something"#).unwrap_err(); + assert_eq!( + expect_keyword, + QueryError::SyntaxError( + 0, + 11, + "Syntax Error: Expected one of 'blog', \ +'author', 'license', 'tags', 'lang', 'title', 'subtitle', 'content', 'followed', 'has_cover', \ +'local' or 'all', got 'not_a_field'" + .to_owned() + ) + ); + + let expect_bracket_or_comma = TimelineQuery::parse(r#"lang in [en ["#).unwrap_err(); + assert_eq!( + expect_bracket_or_comma, + QueryError::SyntaxError( + 12, + 1, + "Syntax Error: Expected one of ']' or ',', \ + got '['" + .to_owned() + ) + ); + + let expect_bracket = TimelineQuery::parse(r#"lang in )abc"#).unwrap_err(); + assert_eq!( + expect_bracket, + QueryError::SyntaxError( + 8, + 1, + "Syntax Error: Expected one of [list, of, words] or list_name, \ + got ')'" + .to_owned() + ) + ); + + let expect_word = TimelineQuery::parse(r#"title contains ,"#).unwrap_err(); + assert_eq!( + expect_word, + QueryError::SyntaxError(15, 1, "Syntax Error: Expected any word, got ','".to_owned()) + ); + + let got_bracket = TimelineQuery::parse(r#"lang in []"#).unwrap_err(); + assert_eq!( + got_bracket, + QueryError::SyntaxError(9, 1, "Syntax Error: Expected any word, got ']'".to_owned()) + ); + + let got_par = TimelineQuery::parse(r#"lang in [a, ("#).unwrap_err(); + assert_eq!( + got_par, + QueryError::SyntaxError(12, 1, "Syntax Error: Expected any word, got '('".to_owned()) + ); + } + + #[test] + fn test_list_used_lists() { + let q = TimelineQuery::parse(r#"lang in [fr, en] and blog in blogs or author in my_fav_authors or tags in hashtag and lang in spoken or license in copyleft"#) + .unwrap(); + let used_lists = q.list_used_lists(); + assert_eq!( + used_lists, + vec![ + ("blogs".to_owned(), ListType::Blog), + ("my_fav_authors".to_owned(), ListType::User), + ("hashtag".to_owned(), ListType::Word), + ("spoken".to_owned(), ListType::Prefix), + ("copyleft".to_owned(), ListType::Word), + ] + ); + } +} diff --git a/plume-models/src/users.rs b/plume-models/src/users.rs new file mode 100644 index 00000000000..1e27fb19102 --- /dev/null +++ b/plume-models/src/users.rs @@ -0,0 +1,1559 @@ +use crate::{ + ap_url, blocklisted_emails::BlocklistedEmail, blogs::Blog, db_conn::DbConn, follows::Follow, + instance::*, medias::Media, notifications::Notification, post_authors::PostAuthor, posts::Post, + safe_string::SafeString, schema::users, timeline::Timeline, Connection, Error, Result, + UserEvent::*, CONFIG, ITEMS_PER_PAGE, USER_CHAN, +}; +use activitypub::{ + activity::Delete, + actor::Person, + collection::{OrderedCollection, OrderedCollectionPage}, + object::{Image, Tombstone}, + Activity, CustomObject, Endpoint, +}; +use chrono::{NaiveDateTime, Utc}; +use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl}; +use ldap3::{LdapConn, Scope, SearchEntry}; +use openssl::{ + hash::MessageDigest, + pkey::{PKey, Private}, + rsa::Rsa, + sign, +}; +use plume_common::{ + activity_pub::{ + inbox::{AsActor, AsObject, FromId}, + request::get, + sign::{gen_keypair, Error as SignError, Result as SignResult, Signer}, + ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY, + }, + utils, +}; +use riker::actors::{Publish, Tell}; +use rocket::{ + outcome::IntoOutcome, + request::{self, FromRequest, Request}, +}; +use std::{ + cmp::PartialEq, + hash::{Hash, Hasher}, + sync::Arc, +}; +use url::Url; +use webfinger::*; + +pub type CustomPerson = CustomObject; + +pub enum Role { + Admin = 0, + Moderator = 1, + Normal = 2, + Instance = 3, +} + +#[derive(Queryable, Identifiable, Clone, Debug, AsChangeset)] +#[changeset_options(treat_none_as_null = "true")] +pub struct User { + pub id: i32, + pub username: String, + pub display_name: String, + pub outbox_url: String, + pub inbox_url: String, + pub summary: String, + pub email: Option, + pub hashed_password: Option, + pub instance_id: i32, + pub creation_date: NaiveDateTime, + pub ap_url: String, + pub private_key: Option, + pub public_key: String, + pub shared_inbox_url: Option, + pub followers_endpoint: String, + pub avatar_id: Option, + pub last_fetched_date: NaiveDateTime, + pub fqn: String, + pub summary_html: SafeString, + /// 0 = admin + /// 1 = moderator + /// 3 = local instance + /// anything else = normal user + pub role: i32, + pub preferred_theme: Option, + pub hide_custom_css: bool, +} + +#[derive(Default, Insertable)] +#[table_name = "users"] +pub struct NewUser { + pub username: String, + pub display_name: String, + pub outbox_url: String, + pub inbox_url: String, + pub summary: String, + pub email: Option, + pub hashed_password: Option, + pub instance_id: i32, + pub ap_url: String, + pub private_key: Option, + pub public_key: String, + pub shared_inbox_url: Option, + pub followers_endpoint: String, + pub avatar_id: Option, + pub summary_html: SafeString, + pub role: i32, + pub fqn: String, +} + +pub const AUTH_COOKIE: &str = "user_id"; +const USER_PREFIX: &str = "@"; + +impl User { + insert!(users, NewUser); + get!(users); + find_by!(users, find_by_email, email as &str); + find_by!(users, find_by_name, username as &str, instance_id as i32); + find_by!(users, find_by_ap_url, ap_url as &str); + + pub fn is_moderator(&self) -> bool { + self.role == Role::Admin as i32 || self.role == Role::Moderator as i32 + } + + pub fn is_admin(&self) -> bool { + self.role == Role::Admin as i32 + } + + pub fn one_by_instance(conn: &Connection) -> Result> { + users::table + .filter(users::instance_id.eq_any(users::table.select(users::instance_id).distinct())) + .load::(conn) + .map_err(Error::from) + } + + pub fn delete(&self, conn: &Connection) -> Result<()> { + use crate::schema::post_authors; + + for blog in Blog::find_for_author(conn, self)? + .iter() + .filter(|b| b.count_authors(conn).map(|c| c <= 1).unwrap_or(false)) + { + blog.delete(conn)?; + } + // delete the posts if they is the only author + let all_their_posts_ids: Vec = post_authors::table + .filter(post_authors::author_id.eq(self.id)) + .select(post_authors::post_id) + .load(conn)?; + for post_id in all_their_posts_ids { + // disabling this lint, because otherwise we'd have to turn it on + // the head, and make it even harder to follow! + #[allow(clippy::op_ref)] + let has_other_authors = post_authors::table + .filter(post_authors::post_id.eq(post_id)) + .filter(post_authors::author_id.ne(self.id)) + .count() + .load(conn)? + .first() + .unwrap_or(&0) + > &0; + if !has_other_authors { + Post::get(conn, post_id)?.delete(conn)?; + } + } + + for notif in Notification::find_followed_by(conn, self)? { + notif.delete(conn)? + } + + diesel::delete(self) + .execute(conn) + .map(|_| ()) + .map_err(Error::from) + } + + pub fn get_instance(&self, conn: &Connection) -> Result { + Instance::get(conn, self.instance_id) + } + + pub fn set_role(&self, conn: &Connection, new_role: Role) -> Result<()> { + diesel::update(self) + .set(users::role.eq(new_role as i32)) + .execute(conn) + .map(|_| ()) + .map_err(Error::from) + } + + pub fn count_local(conn: &Connection) -> Result { + users::table + .filter(users::instance_id.eq(Instance::get_local()?.id)) + .count() + .get_result(&*conn) + .map_err(Error::from) + } + + pub fn find_by_fqn(conn: &DbConn, fqn: &str) -> Result { + let from_db = users::table + .filter(users::fqn.eq(fqn)) + .first(&**conn) + .optional()?; + if let Some(from_db) = from_db { + Ok(from_db) + } else { + User::fetch_from_webfinger(conn, fqn) + } + } + + /** + * TODO: Should create user record with normalized(lowercased) email + */ + pub fn email_used(conn: &DbConn, email: &str) -> Result { + use diesel::dsl::{exists, select}; + + select(exists( + users::table + .filter(users::instance_id.eq(Instance::get_local()?.id)) + .filter(users::email.eq(email)) + .or_filter(users::email.eq(email.to_ascii_lowercase())), + )) + .get_result(&**conn) + .map_err(Error::from) + } + + fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result { + let link = resolve(acct.to_owned(), true)? + .links + .into_iter() + .find(|l| l.mime_type == Some(String::from("application/activity+json"))) + .ok_or(Error::Webfinger)?; + User::from_id( + conn, + link.href.as_ref().ok_or(Error::Webfinger)?, + None, + CONFIG.proxy(), + ) + .map_err(|(_, e)| e) + } + + pub fn fetch_remote_interact_uri(acct: &str) -> Result { + resolve(acct.to_owned(), true)? + .links + .into_iter() + .find(|l| l.rel == "http://ostatus.org/schema/1.0/subscribe") + .and_then(|l| l.template) + .ok_or(Error::Webfinger) + } + + fn fetch(url: &str) -> Result { + let mut res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?; + let text = &res.text()?; + // without this workaround, publicKey is not correctly deserialized + let ap_sign = serde_json::from_str::(text)?; + let mut json = serde_json::from_str::(text)?; + json.custom_props = ap_sign; + Ok(json) + } + + pub fn fetch_from_url(conn: &DbConn, url: &str) -> Result { + User::fetch(url).and_then(|json| User::from_activity(conn, json)) + } + + pub fn refetch(&self, conn: &Connection) -> Result<()> { + User::fetch(&self.ap_url.clone()).and_then(|json| { + let avatar = Media::save_remote( + conn, + json.object + .object_props + .icon_image()? // FIXME: Fails when icon is not set + .object_props + .url_string()?, + self, + ) + .ok(); + + diesel::update(self) + .set(( + users::username.eq(json.object.ap_actor_props.preferred_username_string()?), + users::display_name.eq(json.object.object_props.name_string()?), + users::outbox_url.eq(json.object.ap_actor_props.outbox_string()?), + users::inbox_url.eq(json.object.ap_actor_props.inbox_string()?), + users::summary.eq(SafeString::new( + &json + .object + .object_props + .summary_string() + .unwrap_or_default(), + )), + users::followers_endpoint.eq(json.object.ap_actor_props.followers_string()?), + users::avatar_id.eq(avatar.map(|a| a.id)), + users::last_fetched_date.eq(Utc::now().naive_utc()), + users::public_key.eq(json + .custom_props + .public_key_publickey()? + .public_key_pem_string()?), + )) + .execute(conn) + .map(|_| ()) + .map_err(Error::from) + }) + } + + pub fn hash_pass(pass: &str) -> Result { + bcrypt::hash(pass, 10).map_err(Error::from) + } + + fn ldap_register(conn: &Connection, name: &str, password: &str) -> Result { + if CONFIG.ldap.is_none() { + return Err(Error::NotFound); + } + let ldap = CONFIG.ldap.as_ref().unwrap(); + + let mut ldap_conn = LdapConn::new(&ldap.addr).map_err(|_| Error::NotFound)?; + let ldap_name = format!("{}={},{}", ldap.user_name_attr, name, ldap.base_dn); + let bind = ldap_conn + .simple_bind(&ldap_name, password) + .map_err(|_| Error::NotFound)?; + + if bind.success().is_err() { + return Err(Error::NotFound); + } + + let search = ldap_conn + .search( + &ldap_name, + Scope::Base, + "(|(objectClass=person)(objectClass=user))", + vec![&ldap.mail_attr], + ) + .map_err(|_| Error::NotFound)? + .success() + .map_err(|_| Error::NotFound)?; + for entry in search.0 { + let entry = SearchEntry::construct(entry); + let email = entry.attrs.get("mail").and_then(|vec| vec.first()); + if let Some(email) = email { + let _ = ldap_conn.unbind(); + return NewUser::new_local( + conn, + name.to_owned(), + name.to_owned(), + Role::Normal, + "", + email.to_owned(), + None, + ); + } + } + let _ = ldap_conn.unbind(); + Err(Error::NotFound) + } + + fn ldap_login(&self, password: &str) -> bool { + if let Some(ldap) = CONFIG.ldap.as_ref() { + let mut conn = if let Ok(conn) = LdapConn::new(&ldap.addr) { + conn + } else { + return false; + }; + let name = format!( + "{}={},{}", + ldap.user_name_attr, &self.username, ldap.base_dn + ); + if let Ok(bind) = conn.simple_bind(&name, password) { + bind.success().is_ok() + } else { + false + } + } else { + false + } + } + + pub fn login(conn: &Connection, ident: &str, password: &str) -> Result { + let local_id = Instance::get_local()?.id; + let user = match User::find_by_email(conn, ident) { + Ok(user) => Ok(user), + _ => User::find_by_name(conn, ident, local_id), + } + .and_then(|u| { + if u.instance_id == local_id { + Ok(u) + } else { + Err(Error::NotFound) + } + }); + + match user { + Ok(user) if user.hashed_password.is_some() => { + if bcrypt::verify(password, user.hashed_password.as_ref().unwrap()).unwrap_or(false) + { + Ok(user) + } else { + Err(Error::NotFound) + } + } + Ok(user) => { + if user.ldap_login(password) { + Ok(user) + } else { + Err(Error::NotFound) + } + } + e => { + if let Ok(user) = User::ldap_register(conn, ident, password) { + return Ok(user); + } + // if no user was found, and we were unable to auto-register from ldap + // fake-verify a password, and return an error. + let other = User::get(&*conn, 1) + .expect("No user is registered") + .hashed_password; + other.map(|pass| bcrypt::verify(password, &pass)); + e + } + } + } + + pub fn reset_password(&self, conn: &Connection, pass: &str) -> Result<()> { + diesel::update(self) + .set(users::hashed_password.eq(User::hash_pass(pass)?)) + .execute(conn)?; + Ok(()) + } + + pub fn get_local_page(conn: &Connection, (min, max): (i32, i32)) -> Result> { + users::table + .filter(users::instance_id.eq(Instance::get_local()?.id)) + .order(users::username.asc()) + .offset(min.into()) + .limit((max - min).into()) + .load::(conn) + .map_err(Error::from) + } + pub fn outbox(&self, conn: &Connection) -> Result> { + Ok(ActivityStream::new(self.outbox_collection(conn)?)) + } + pub fn outbox_collection(&self, conn: &Connection) -> Result { + let mut coll = OrderedCollection::default(); + let first = &format!("{}?page=1", &self.outbox_url); + let last = &format!( + "{}?page={}", + &self.outbox_url, + self.get_activities_count(conn) / i64::from(ITEMS_PER_PAGE) + 1 + ); + coll.collection_props.set_first_link(Id::new(first))?; + coll.collection_props.set_last_link(Id::new(last))?; + coll.collection_props + .set_total_items_u64(self.get_activities_count(conn) as u64)?; + Ok(coll) + } + pub fn outbox_page( + &self, + conn: &Connection, + (min, max): (i32, i32), + ) -> Result> { + Ok(ActivityStream::new( + self.outbox_collection_page(conn, (min, max))?, + )) + } + pub fn outbox_collection_page( + &self, + conn: &Connection, + (min, max): (i32, i32), + ) -> Result { + let acts = self.get_activities_page(conn, (min, max))?; + let n_acts = self.get_activities_count(conn); + let mut coll = OrderedCollectionPage::default(); + if n_acts - i64::from(min) >= i64::from(ITEMS_PER_PAGE) { + coll.collection_page_props.set_next_link(Id::new(&format!( + "{}?page={}", + &self.outbox_url, + min / ITEMS_PER_PAGE + 2 + )))?; + } + if min > 0 { + coll.collection_page_props.set_prev_link(Id::new(&format!( + "{}?page={}", + &self.outbox_url, + min / ITEMS_PER_PAGE + )))?; + } + coll.collection_props.items = serde_json::to_value(acts)?; + coll.collection_page_props + .set_part_of_link(Id::new(&self.outbox_url))?; + Ok(coll) + } + fn fetch_outbox_page(&self, url: &str) -> Result<(Vec, Option)> { + let mut res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?; + let text = &res.text()?; + let json: serde_json::Value = serde_json::from_str(text)?; + let items = json["items"] + .as_array() + .unwrap_or(&vec![]) + .iter() + .filter_map(|j| serde_json::from_value(j.clone()).ok()) + .collect::>(); + + let next = json.get("next").map(|x| x.as_str().unwrap().to_owned()); + Ok((items, next)) + } + pub fn fetch_outbox(&self) -> Result> { + let mut res = get( + &self.outbox_url[..], + Self::get_sender(), + CONFIG.proxy().cloned(), + )?; + let text = &res.text()?; + let json: serde_json::Value = serde_json::from_str(text)?; + if let Some(first) = json.get("first") { + let mut items: Vec = Vec::new(); + let mut next = first.as_str().unwrap().to_owned(); + while let Ok((mut page, nxt)) = self.fetch_outbox_page(&next) { + if page.is_empty() { + break; + } + items.append(&mut page); + if let Some(n) = nxt { + if n == next { + break; + } + next = n; + } else { + break; + } + } + Ok(items) + } else { + Ok(json["items"] + .as_array() + .unwrap_or(&vec![]) + .iter() + .filter_map(|j| serde_json::from_value(j.clone()).ok()) + .collect::>()) + } + } + + pub fn fetch_followers_ids(&self) -> Result> { + let mut res = get( + &self.followers_endpoint[..], + Self::get_sender(), + CONFIG.proxy().cloned(), + )?; + let text = &res.text()?; + let json: serde_json::Value = serde_json::from_str(text)?; + Ok(json["items"] + .as_array() + .unwrap_or(&vec![]) + .iter() + .filter_map(|j| serde_json::from_value(j.clone()).ok()) + .collect::>()) + } + fn get_activities_count(&self, conn: &Connection) -> i64 { + use crate::schema::post_authors; + use crate::schema::posts; + let posts_by_self = PostAuthor::belonging_to(self).select(post_authors::post_id); + posts::table + .filter(posts::published.eq(true)) + .filter(posts::id.eq_any(posts_by_self)) + .count() + .first(conn) + .unwrap() + } + fn get_activities_page( + &self, + conn: &Connection, + (min, max): (i32, i32), + ) -> Result> { + use crate::schema::post_authors; + use crate::schema::posts; + let posts_by_self = PostAuthor::belonging_to(self).select(post_authors::post_id); + let posts = posts::table + .filter(posts::published.eq(true)) + .filter(posts::id.eq_any(posts_by_self)) + .order(posts::creation_date.desc()) + .offset(min.into()) + .limit((max - min).into()) + .load::(conn)?; + Ok(posts + .into_iter() + .filter_map(|p| { + p.create_activity(conn) + .ok() + .and_then(|a| serde_json::to_value(a).ok()) + }) + .collect::>()) + } + + pub fn get_followers(&self, conn: &Connection) -> Result> { + use crate::schema::follows; + let follows = Follow::belonging_to(self).select(follows::follower_id); + users::table + .filter(users::id.eq_any(follows)) + .load::(conn) + .map_err(Error::from) + } + + pub fn count_followers(&self, conn: &Connection) -> Result { + use crate::schema::follows; + let follows = Follow::belonging_to(self).select(follows::follower_id); + users::table + .filter(users::id.eq_any(follows)) + .count() + .get_result(conn) + .map_err(Error::from) + } + + pub fn get_followers_page( + &self, + conn: &Connection, + (min, max): (i32, i32), + ) -> Result> { + use crate::schema::follows; + let follows = Follow::belonging_to(self).select(follows::follower_id); + users::table + .filter(users::id.eq_any(follows)) + .offset(min.into()) + .limit((max - min).into()) + .load::(conn) + .map_err(Error::from) + } + + pub fn get_followed(&self, conn: &Connection) -> Result> { + use crate::schema::follows::dsl::*; + let f = follows.filter(follower_id.eq(self.id)).select(following_id); + users::table + .filter(users::id.eq_any(f)) + .load::(conn) + .map_err(Error::from) + } + + pub fn count_followed(&self, conn: &Connection) -> Result { + use crate::schema::follows; + follows::table + .filter(follows::follower_id.eq(self.id)) + .count() + .get_result(conn) + .map_err(Error::from) + } + + pub fn get_followed_page( + &self, + conn: &Connection, + (min, max): (i32, i32), + ) -> Result> { + use crate::schema::follows; + let follows = follows::table + .filter(follows::follower_id.eq(self.id)) + .select(follows::following_id) + .limit((max - min).into()); + users::table + .filter(users::id.eq_any(follows)) + .offset(min.into()) + .load::(conn) + .map_err(Error::from) + } + + pub fn is_followed_by(&self, conn: &Connection, other_id: i32) -> Result { + use crate::schema::follows; + follows::table + .filter(follows::follower_id.eq(other_id)) + .filter(follows::following_id.eq(self.id)) + .count() + .get_result::(conn) + .map_err(Error::from) + .map(|r| r > 0) + } + + pub fn is_following(&self, conn: &Connection, other_id: i32) -> Result { + use crate::schema::follows; + follows::table + .filter(follows::follower_id.eq(self.id)) + .filter(follows::following_id.eq(other_id)) + .count() + .get_result::(conn) + .map_err(Error::from) + .map(|r| r > 0) + } + + pub fn has_liked(&self, conn: &Connection, post: &Post) -> Result { + use crate::schema::likes; + likes::table + .filter(likes::post_id.eq(post.id)) + .filter(likes::user_id.eq(self.id)) + .count() + .get_result::(conn) + .map_err(Error::from) + .map(|r| r > 0) + } + + pub fn has_reshared(&self, conn: &Connection, post: &Post) -> Result { + use crate::schema::reshares; + reshares::table + .filter(reshares::post_id.eq(post.id)) + .filter(reshares::user_id.eq(self.id)) + .count() + .get_result::(conn) + .map_err(Error::from) + .map(|r| r > 0) + } + + pub fn is_author_in(&self, conn: &Connection, blog: &Blog) -> Result { + use crate::schema::blog_authors; + blog_authors::table + .filter(blog_authors::author_id.eq(self.id)) + .filter(blog_authors::blog_id.eq(blog.id)) + .count() + .get_result::(conn) + .map_err(Error::from) + .map(|r| r > 0) + } + + pub fn get_keypair(&self) -> Result> { + PKey::from_rsa(Rsa::private_key_from_pem( + self.private_key.clone().ok_or(Error::Signature)?.as_ref(), + )?) + .map_err(Error::from) + } + + pub fn rotate_keypair(&self, conn: &Connection) -> Result> { + if self.private_key.is_none() { + return Err(Error::InvalidValue); + } + if (Utc::now().naive_utc() - self.last_fetched_date).num_minutes() < 10 { + //rotated recently + self.get_keypair() + } else { + let (public_key, private_key) = gen_keypair(); + let public_key = + String::from_utf8(public_key).expect("NewUser::new_local: public key error"); + let private_key = + String::from_utf8(private_key).expect("NewUser::new_local: private key error"); + let res = PKey::from_rsa(Rsa::private_key_from_pem(private_key.as_ref())?)?; + diesel::update(self) + .set(( + users::public_key.eq(public_key), + users::private_key.eq(Some(private_key)), + users::last_fetched_date.eq(Utc::now().naive_utc()), + )) + .execute(conn) + .map_err(Error::from) + .map(|_| res) + } + } + + pub fn to_activity(&self, conn: &Connection) -> Result { + let mut actor = Person::default(); + actor.object_props.set_id_string(self.ap_url.clone())?; + actor + .object_props + .set_name_string(self.display_name.clone())?; + actor + .object_props + .set_summary_string(self.summary_html.get().clone())?; + actor.object_props.set_url_string(self.ap_url.clone())?; + actor + .ap_actor_props + .set_inbox_string(self.inbox_url.clone())?; + actor + .ap_actor_props + .set_outbox_string(self.outbox_url.clone())?; + actor + .ap_actor_props + .set_preferred_username_string(self.username.clone())?; + actor + .ap_actor_props + .set_followers_string(self.followers_endpoint.clone())?; + + if let Some(shared_inbox_url) = self.shared_inbox_url.clone() { + let mut endpoints = Endpoint::default(); + endpoints.set_shared_inbox_string(shared_inbox_url)?; + actor.ap_actor_props.set_endpoints_endpoint(endpoints)?; + } + + let mut public_key = PublicKey::default(); + public_key.set_id_string(format!("{}#main-key", self.ap_url))?; + public_key.set_owner_string(self.ap_url.clone())?; + public_key.set_public_key_pem_string(self.public_key.clone())?; + let mut ap_signature = ApSignature::default(); + ap_signature.set_public_key_publickey(public_key)?; + + if let Some(avatar_id) = self.avatar_id { + let mut avatar = Image::default(); + avatar + .object_props + .set_url_string(Media::get(conn, avatar_id)?.url()?)?; + actor.object_props.set_icon_object(avatar)?; + } + + Ok(CustomPerson::new(actor, ap_signature)) + } + + pub fn delete_activity(&self, conn: &Connection) -> Result { + 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()).ok()) + .unwrap_or_else(|| "/static/images/default-avatar.png".to_string()) + } + + pub fn webfinger(&self, conn: &Connection) -> Result { + Ok(Webfinger { + subject: format!("acct:{}", self.acct_authority(conn)?), + aliases: vec![self.ap_url.clone()], + links: vec![ + Link { + rel: String::from("http://webfinger.net/rel/profile-page"), + mime_type: Some(String::from("text/html")), + href: Some(self.ap_url.clone()), + template: None, + }, + Link { + rel: String::from("http://schemas.google.com/g/2010#updates-from"), + mime_type: Some(String::from("application/atom+xml")), + href: Some(self.get_instance(conn)?.compute_box( + USER_PREFIX, + &self.username, + "feed.atom", + )), + template: None, + }, + Link { + rel: String::from("self"), + mime_type: Some(String::from("application/activity+json")), + href: Some(self.ap_url.clone()), + template: None, + }, + Link { + rel: String::from("http://ostatus.org/schema/1.0/subscribe"), + mime_type: None, + href: None, + template: Some(format!( + "https://{}/remote_interact?target={{uri}}", + self.get_instance(conn)?.public_domain + )), + }, + ], + }) + } + + pub fn acct_authority(&self, conn: &Connection) -> Result { + Ok(format!( + "{}@{}", + self.username, + self.get_instance(conn)?.public_domain + )) + } + + pub fn set_avatar(&self, conn: &Connection, id: i32) -> Result<()> { + diesel::update(self) + .set(users::avatar_id.eq(id)) + .execute(conn) + .map(|_| ()) + .map_err(Error::from) + } + + pub fn needs_update(&self) -> bool { + (Utc::now().naive_utc() - self.last_fetched_date).num_days() > 1 + } + + pub fn name(&self) -> String { + if !self.display_name.is_empty() { + self.display_name.clone() + } else { + self.fqn.clone() + } + } + + pub fn remote_user_found(&self) { + tracing::trace!("{:?}", self); + self.publish_remote_user_found(); + } + + fn publish_remote_user_found(&self) { + USER_CHAN.tell( + Publish { + msg: RemoteUserFound(Arc::new(self.clone())), + topic: "user.remote_user_found".into(), + }, + None, + ) + } +} + +impl<'a, 'r> FromRequest<'a, 'r> for User { + type Error = (); + + fn from_request(request: &'a Request<'r>) -> request::Outcome { + let conn = request.guard::()?; + request + .cookies() + .get_private(AUTH_COOKIE) + .and_then(|cookie| cookie.value().parse().ok()) + .and_then(|id| User::get(&*conn, id).ok()) + .or_forward(()) + } +} + +impl IntoId for User { + fn into_id(self) -> Id { + Id::new(self.ap_url) + } +} + +impl Eq for User {} + +impl FromId for User { + type Error = Error; + type Object = CustomPerson; + + fn from_db(conn: &DbConn, id: &str) -> Result { + Self::find_by_ap_url(conn, id) + } + + fn from_activity(conn: &DbConn, acct: CustomPerson) -> Result { + let url = Url::parse(&acct.object.object_props.id_string()?)?; + let inst = url.host_str().ok_or(Error::Url)?; + let instance = Instance::find_by_domain(conn, inst).or_else(|_| { + Instance::insert( + conn, + NewInstance { + name: inst.to_owned(), + public_domain: inst.to_owned(), + local: false, + // We don't really care about all the following for remote instances + long_description: SafeString::new(""), + short_description: SafeString::new(""), + default_license: String::new(), + open_registrations: true, + short_description_html: String::new(), + long_description_html: String::new(), + }, + ) + })?; + + let username = acct.object.ap_actor_props.preferred_username_string()?; + + if username.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) { + return Err(Error::InvalidValue); + } + + let fqn = if instance.local { + username.clone() + } else { + format!("{}@{}", username, instance.public_domain) + }; + + let user = User::insert( + conn, + NewUser { + display_name: acct + .object + .object_props + .name_string() + .unwrap_or_else(|_| username.clone()), + username, + outbox_url: acct.object.ap_actor_props.outbox_string()?, + inbox_url: acct.object.ap_actor_props.inbox_string()?, + role: 2, + summary: acct + .object + .object_props + .summary_string() + .unwrap_or_default(), + summary_html: SafeString::new( + &acct + .object + .object_props + .summary_string() + .unwrap_or_default(), + ), + email: None, + hashed_password: None, + instance_id: instance.id, + ap_url: acct.object.object_props.id_string()?, + public_key: acct + .custom_props + .public_key_publickey()? + .public_key_pem_string()?, + private_key: None, + shared_inbox_url: acct + .object + .ap_actor_props + .endpoints_endpoint() + .and_then(|e| e.shared_inbox_string()) + .ok(), + followers_endpoint: acct.object.ap_actor_props.followers_string()?, + fqn, + avatar_id: None, + }, + )?; + + if let Ok(icon) = acct.object.object_props.icon_image() { + if let Ok(url) = icon.object_props.url_string() { + let avatar = Media::save_remote(conn, url, &user); + + if let Ok(avatar) = avatar { + user.set_avatar(conn, avatar.id)?; + } + } + } + + Ok(user) + } + + fn get_sender() -> &'static dyn Signer { + Instance::get_local_instance_user().expect("Failed to local instance user") + } +} + +impl AsActor<&DbConn> for User { + fn get_inbox_url(&self) -> String { + self.inbox_url.clone() + } + + fn get_shared_inbox_url(&self) -> Option { + self.shared_inbox_url.clone() + } + + fn is_local(&self) -> bool { + Instance::get_local() + .map(|i| self.instance_id == i.id) + .unwrap_or(false) + } +} + +impl AsObject for User { + type Error = Error; + type Output = (); + + fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { + if self.id == actor.id { + self.delete(conn).map(|_| ()) + } else { + Err(Error::Unauthorized) + } + } +} + +impl Signer for User { + fn get_key_id(&self) -> String { + format!("{}#main-key", self.ap_url) + } + + fn sign(&self, to_sign: &str) -> SignResult> { + let key = self.get_keypair().map_err(|_| SignError())?; + let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?; + signer.update(to_sign.as_bytes())?; + signer.sign_to_vec().map_err(SignError::from) + } + + fn verify(&self, data: &str, signature: &[u8]) -> SignResult { + let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?; + let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?; + verifier.update(data.as_bytes())?; + verifier.verify(signature).map_err(SignError::from) + } +} + +impl PartialEq for User { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Hash for User { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +impl NewUser { + /// Creates a new local user + pub fn new_local( + conn: &Connection, + username: String, + display_name: String, + role: Role, + summary: &str, + email: String, + password: Option, + ) -> Result { + let (pub_key, priv_key) = gen_keypair(); + let instance = Instance::get_local()?; + let blocklisted = BlocklistedEmail::matches_blocklist(conn, &email)?; + if let Some(x) = blocklisted { + return Err(Error::Blocklisted(x.notify_user, x.notification_text)); + } + + let res = User::insert( + conn, + NewUser { + username: username.clone(), + display_name, + role: role as i32, + summary: summary.to_owned(), + summary_html: SafeString::new(&utils::md_to_html(summary, None, false, None).0), + email: Some(email), + hashed_password: password, + instance_id: instance.id, + public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?, + private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?), + outbox_url: instance.compute_box(USER_PREFIX, &username, "outbox"), + inbox_url: instance.compute_box(USER_PREFIX, &username, "inbox"), + ap_url: instance.compute_box(USER_PREFIX, &username, ""), + shared_inbox_url: Some(ap_url(&format!("{}/inbox", &instance.public_domain))), + followers_endpoint: instance.compute_box(USER_PREFIX, &username, "followers"), + fqn: username, + avatar_id: None, + }, + )?; + + // create default timeline + Timeline::new_for_user(conn, res.id, "My feed".into(), "followed".into())?; + + Ok(res) + } +} + +#[derive(Clone, Debug)] +pub enum UserEvent { + RemoteUserFound(Arc), +} + +impl From for Arc { + fn from(event: UserEvent) -> Self { + use UserEvent::*; + + match event { + RemoteUserFound(user) => user, + } + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::{ + instance::{tests as instance_tests, Instance}, + medias::{Media, NewMedia}, + tests::db, + Connection as Conn, ITEMS_PER_PAGE, + }; + use assert_json_diff::assert_json_eq; + use diesel::{Connection, SaveChangesDsl}; + use serde_json::to_value; + + pub(crate) fn fill_database(conn: &Conn) -> Vec { + instance_tests::fill_database(conn); + let admin = NewUser::new_local( + conn, + "admin".to_owned(), + "The admin".to_owned(), + Role::Admin, + "Hello there, I'm the admin", + "admin@example.com".to_owned(), + Some("invalid_admin_password".to_owned()), + ) + .unwrap(); + let user = NewUser::new_local( + conn, + "user".to_owned(), + "Some user".to_owned(), + Role::Normal, + "Hello there, I'm no one", + "user@example.com".to_owned(), + Some("invalid_user_password".to_owned()), + ) + .unwrap(); + let mut other = NewUser::new_local( + conn, + "other".to_owned(), + "Another user".to_owned(), + Role::Normal, + "Hello there, I'm someone else", + "other@example.com".to_owned(), + Some("invalid_other_password".to_owned()), + ) + .unwrap(); + let avatar = Media::insert( + conn, + NewMedia { + file_path: "static/media/example.png".into(), + alt_text: "Another user".into(), + is_remote: false, + remote_url: None, + sensitive: false, + content_warning: None, + owner_id: other.id, + }, + ) + .unwrap(); + other.avatar_id = Some(avatar.id); + let other = other.save_changes::(&*conn).unwrap(); + + vec![admin, user, other] + } + + fn fill_pages( + conn: &DbConn, + ) -> ( + Vec, + Vec, + Vec, + ) { + use crate::post_authors::NewPostAuthor; + use crate::posts::NewPost; + + let (mut posts, users, blogs) = crate::inbox::tests::fill_database(conn); + let user = &users[0]; + let blog = &blogs[0]; + + for i in 1..(ITEMS_PER_PAGE * 4 + 3) { + let title = format!("Post {}", i); + let content = format!("Content for post {}.", i); + let post = Post::insert( + conn, + NewPost { + blog_id: blog.id, + slug: title.clone(), + title: title.clone(), + content: SafeString::new(&content), + published: true, + license: "CC-0".into(), + creation_date: None, + ap_url: format!("{}/{}", blog.ap_url, title), + subtitle: "".into(), + source: content, + cover_id: None, + }, + ) + .unwrap(); + PostAuthor::insert( + conn, + NewPostAuthor { + post_id: post.id, + author_id: user.id, + }, + ) + .unwrap(); + posts.push(post); + } + + (posts, users, blogs) + } + + #[test] + fn find_by() { + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + fill_database(&conn); + let test_user = NewUser::new_local( + &conn, + "test".to_owned(), + "test user".to_owned(), + Role::Normal, + "Hello I'm a test", + "test@example.com".to_owned(), + Some(User::hash_pass("test_password").unwrap()), + ) + .unwrap(); + assert_eq!( + test_user.id, + User::find_by_name(&conn, "test", Instance::get_local().unwrap().id) + .unwrap() + .id + ); + assert_eq!( + test_user.id, + User::find_by_fqn(&conn, &test_user.fqn).unwrap().id + ); + assert_eq!( + test_user.id, + User::find_by_email(&conn, "test@example.com").unwrap().id + ); + assert_eq!( + test_user.id, + User::find_by_ap_url( + &conn, + &format!( + "https://{}/@/{}/", + Instance::get_local().unwrap().public_domain, + "test" + ) + ) + .unwrap() + .id + ); + Ok(()) + }); + } + + #[test] + fn delete() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let inserted = fill_database(&conn); + + assert!(User::get(&conn, inserted[0].id).is_ok()); + inserted[0].delete(&conn).unwrap(); + assert!(User::get(&conn, inserted[0].id).is_err()); + Ok(()) + }); + } + + #[test] + fn admin() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let inserted = fill_database(&conn); + 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 + local_inst + .main_admin(&conn) + .unwrap() + .set_role(&conn, Role::Normal) + .unwrap(); + i += 1; + } + inserted[0].set_role(&conn, Role::Admin).unwrap(); + assert_eq!(inserted[0].id, local_inst.main_admin(&conn).unwrap().id); + Ok(()) + }); + } + + #[test] + fn auth() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + fill_database(&conn); + let test_user = NewUser::new_local( + &conn, + "test".to_owned(), + "test user".to_owned(), + Role::Normal, + "Hello I'm a test", + "test@example.com".to_owned(), + Some(User::hash_pass("test_password").unwrap()), + ) + .unwrap(); + + assert_eq!( + User::login(&conn, "test", "test_password").unwrap().id, + test_user.id + ); + assert!(User::login(&conn, "test", "other_password").is_err()); + Ok(()) + }); + } + + #[test] + fn get_local_page() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + fill_database(&conn); + + let page = User::get_local_page(&conn, (0, 2)).unwrap(); + assert_eq!(page.len(), 2); + assert!(page[0].username <= page[1].username); + + let mut last_username = User::get_local_page(&conn, (0, 1)).unwrap()[0] + .username + .clone(); + for i in 1..User::count_local(&conn).unwrap() as i32 { + let page = User::get_local_page(&conn, (i, i + 1)).unwrap(); + assert_eq!(page.len(), 1); + assert!(last_username <= page[0].username); + last_username = page[0].username.clone(); + } + assert_eq!( + User::get_local_page(&conn, (0, User::count_local(&conn).unwrap() as i32 + 10)) + .unwrap() + .len() as i64, + User::count_local(&conn).unwrap() + ); + Ok(()) + }); + } + + #[test] + fn self_federation() { + let conn = db(); + conn.test_transaction::<_, (), _>(|| { + let users = fill_database(&conn); + + let ap_repr = users[0].to_activity(&conn).unwrap(); + users[0].delete(&conn).unwrap(); + let user = User::from_activity(&conn, ap_repr).unwrap(); + + assert_eq!(user.username, users[0].username); + assert_eq!(user.display_name, users[0].display_name); + assert_eq!(user.outbox_url, users[0].outbox_url); + assert_eq!(user.inbox_url, users[0].inbox_url); + assert_eq!(user.instance_id, users[0].instance_id); + assert_eq!(user.ap_url, users[0].ap_url); + assert_eq!(user.public_key, users[0].public_key); + assert_eq!(user.shared_inbox_url, users[0].shared_inbox_url); + assert_eq!(user.followers_endpoint, users[0].followers_endpoint); + assert_eq!(user.avatar_url(&conn), users[0].avatar_url(&conn)); + assert_eq!(user.fqn, users[0].fqn); + assert_eq!(user.summary_html, users[0].summary_html); + Ok(()) + }); + } + + #[test] + fn to_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let users = fill_database(&conn); + let user = &users[0]; + let act = user.to_activity(&conn)?; + + let expected = json!({ + "endpoints": { + "sharedInbox": "https://plu.me/inbox" + }, + "followers": "https://plu.me/@/admin/followers", + "following": null, + "id": "https://plu.me/@/admin/", + "inbox": "https://plu.me/@/admin/inbox", + "liked": null, + "name": "The admin", + "outbox": "https://plu.me/@/admin/outbox", + "preferredUsername": "admin", + "publicKey": { + "id": "https://plu.me/@/admin/#main-key", + "owner": "https://plu.me/@/admin/", + "publicKeyPem": user.public_key, + }, + "summary": "

Hello there, I’m the admin

\n", + "type": "Person", + "url": "https://plu.me/@/admin/" + }); + + assert_json_eq!(to_value(act)?, expected); + + let other = &users[2]; + let other_act = other.to_activity(&conn)?; + let expected_other = json!({ + "endpoints": { + "sharedInbox": "https://plu.me/inbox" + }, + "followers": "https://plu.me/@/other/followers", + "following": null, + "icon": { + "url": "https://plu.me/static/media/example.png", + "type": "Image", + }, + "id": "https://plu.me/@/other/", + "inbox": "https://plu.me/@/other/inbox", + "liked": null, + "name": "Another user", + "outbox": "https://plu.me/@/other/outbox", + "preferredUsername": "other", + "publicKey": { + "id": "https://plu.me/@/other/#main-key", + "owner": "https://plu.me/@/other/", + "publicKeyPem": other.public_key, + }, + "summary": "

Hello there, I’m someone else

\n", + "type": "Person", + "url": "https://plu.me/@/other/" + }); + + assert_json_eq!(to_value(other_act)?, expected_other); + + Ok(()) + }); + } + + #[test] + fn delete_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let users = fill_database(&conn); + let user = &users[1]; + let act = user.delete_activity(&conn)?; + + let expected = json!({ + "actor": "https://plu.me/@/user/", + "cc": [], + "id": "https://plu.me/@/user/#delete", + "object": { + "id": "https://plu.me/@/user/", + "type": "Tombstone", + }, + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Delete", + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } + + #[test] + fn outbox_collection() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (_pages, users, _blogs) = fill_pages(&conn); + let user = &users[0]; + let act = user.outbox_collection(&conn)?; + + let expected = json!({ + "first": "https://plu.me/@/admin/outbox?page=1", + "items": null, + "last": "https://plu.me/@/admin/outbox?page=5", + "totalItems": 51, + "type": "OrderedCollection", + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } + + #[test] + fn outbox_collection_page() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let users = fill_database(&conn); + let user = &users[0]; + let act = user.outbox_collection_page(&conn, (33, 36))?; + + let expected = json!({ + "items": [], + "partOf": "https://plu.me/@/admin/outbox", + "prev": "https://plu.me/@/admin/outbox?page=2", + "type": "OrderedCollectionPage", + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } +} diff --git a/plume-models/tests/lib.rs b/plume-models/tests/lib.rs new file mode 100644 index 00000000000..3b563b03f24 --- /dev/null +++ b/plume-models/tests/lib.rs @@ -0,0 +1,22 @@ +use diesel::Connection; +use plume_common::utils::random_hex; +use plume_models::migrations::IMPORTED_MIGRATIONS; +use plume_models::{Connection as Conn, CONFIG}; + +use std::env::temp_dir; + +fn db() -> Conn { + let conn = + Conn::establish(CONFIG.database_url.as_str()).expect("Couldn't connect to the database"); + let dir = temp_dir().join(format!("plume-test-{}", random_hex())); + IMPORTED_MIGRATIONS + .run_pending_migrations(&conn, &dir) + .expect("Couldn't run migrations"); + conn +} + +#[test] +fn empty_test() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| Ok(())); +} diff --git a/po/plume-front/af.po b/po/plume-front/af.po new file mode 100644 index 00000000000..2993a6d1717 --- /dev/null +++ b/po/plume-front/af.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Afrikaans\n" +"Language: af_ZA\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: af\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "" + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "" + diff --git a/po/plume-front/ar.po b/po/plume-front/ar.po new file mode 100644 index 00000000000..d8470302f60 --- /dev/null +++ b/po/plume-front/ar.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Arabic\n" +"Language: ar_SA\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: ar\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "فتح محرر النصوص الغني" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "العنوان" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "العنوان الثانوي أو الملخص" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "اكتب مقالك هنا. ماركداون مُدَعَّم." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "يتبقا {} حرفا تقريبا" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "الوسوم" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "الرخصة" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "الغلاف" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "هذه مسودة" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "نشر كتابا" + diff --git a/po/plume-front/bg.po b/po/plume-front/bg.po new file mode 100644 index 00000000000..9a841390920 --- /dev/null +++ b/po/plume-front/bg.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Bulgarian\n" +"Language: bg_BG\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: bg\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "Искате ли да активирате локално автоматично запаметяване, последно редактирано в {}?" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "Отворете редактора с богат текст" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Заглавие" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "Подзаглавие или резюме" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "Напишете статията си тук. Поддържа се Markdown." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "Остават {} знака вляво" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Етикети" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Лиценз" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "Основно изображение" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "Това е проект" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Публикувай" + diff --git a/po/plume-front/ca.po b/po/plume-front/ca.po new file mode 100644 index 00000000000..efee83d45b6 --- /dev/null +++ b/po/plume-front/ca.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Catalan\n" +"Language: ca_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: ca\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "Obre l’editor de text enriquit" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Títol" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "Subtítol o resum" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "Escriviu el vostre article ací. Podeu fer servir el Markdown." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "Queden uns {} caràcters" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Etiquetes" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Llicència" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "Coberta" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "Açò és un esborrany" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Publica" + diff --git a/po/plume-front/cs.po b/po/plume-front/cs.po new file mode 100644 index 00000000000..6ed9e878713 --- /dev/null +++ b/po/plume-front/cs.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Czech\n" +"Language: cs_CZ\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: cs\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "Otevřít editor formátovaného textu" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Nadpis" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "Podnadpis, nebo shrnutí" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "Sem napište svůj článek. Markdown je podporován." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "Zbývá kolem {} znaků" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Tagy" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Licence" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "Titulka" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "Tohle je koncept" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Zveřejnit" + diff --git a/po/plume-front/cy.po b/po/plume-front/cy.po new file mode 100644 index 00000000000..4c722366bef --- /dev/null +++ b/po/plume-front/cy.po @@ -0,0 +1,62 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2019-04-06 15:05\n" +"Last-Translator: AnaGelez \n" +"Language-Team: Welsh\n" +"Language: cy_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=(n == 0) ? 0 : ((n == 1) ? 1 : ((n == 2) ? " +"2 : ((n == 3) ? 3 : ((n == 6) ? 4 : 5))));\n" +"X-Generator: crowdin.com\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Language: cy\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" + +# plume-front/src/editor.rs:189 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:114 +msgid "Open the rich text editor" +msgstr "" + +# plume-front/src/editor.rs:143 +msgid "Title" +msgstr "" + +# plume-front/src/editor.rs:319 +msgid "Subtitle, or summary" +msgstr "" + +# plume-front/src/editor.rs:154 +msgid "Write your article here. Markdown is supported." +msgstr "" + +# plume-front/src/editor.rs:165 +msgid "Around {} characters left" +msgstr "" + +# plume-front/src/editor.rs:228 +msgid "Tags" +msgstr "" + +# plume-front/src/editor.rs:229 +msgid "License" +msgstr "" + +# plume-front/src/editor.rs:232 +msgid "Cover" +msgstr "" + +# plume-front/src/editor.rs:252 +msgid "This is a draft" +msgstr "" + +# plume-front/src/editor.rs:259 +msgid "Publish" +msgstr "" diff --git a/po/plume-front/da.po b/po/plume-front/da.po new file mode 100644 index 00000000000..cc7a87abbfb --- /dev/null +++ b/po/plume-front/da.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Danish\n" +"Language: da_DK\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: da\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "" + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "" + diff --git a/po/plume-front/de.po b/po/plume-front/de.po new file mode 100644 index 00000000000..8f780149538 --- /dev/null +++ b/po/plume-front/de.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-26 13:16\n" +"Last-Translator: \n" +"Language-Team: German\n" +"Language: de_DE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: de\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "Möchten Sie die lokale automatische Speicherung laden, die zuletzt um {} bearbeitet wurde?" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr " Rich Text Editor (RTE) öffnen" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Titel" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "Untertitel oder Zusammenfassung" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "Schreiben deinen Artikel hier. Markdown wird unterstützt." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "Ungefähr {} Zeichen übrig" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Schlagwörter" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Lizenz" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "Einband" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "Dies ist ein Entwurf" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Veröffentlichen" + diff --git a/po/plume-front/el.po b/po/plume-front/el.po new file mode 100644 index 00000000000..18b65cee57a --- /dev/null +++ b/po/plume-front/el.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Greek\n" +"Language: el_GR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: el\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "" + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "" + diff --git a/po/plume-front/en.po b/po/plume-front/en.po new file mode 100644 index 00000000000..b83f496943b --- /dev/null +++ b/po/plume-front/en.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: English\n" +"Language: en_US\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: en\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "" + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "" + diff --git a/po/plume-front/eo.po b/po/plume-front/eo.po new file mode 100644 index 00000000000..ac8ea168fd8 --- /dev/null +++ b/po/plume-front/eo.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Esperanto\n" +"Language: eo_UY\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: eo\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "Malfermi la riĉan redaktilon" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Titolo" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "Verku vian artikolon ĉi tie. Markdown estas subtenita." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "Proksimume {} signoj restantaj" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Etikedoj" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Permesilo" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "Kovro" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "Malfinias" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Eldoni" + diff --git a/po/plume-front/es.po b/po/plume-front/es.po new file mode 100644 index 00000000000..73cf37068d4 --- /dev/null +++ b/po/plume-front/es.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-26 13:16\n" +"Last-Translator: \n" +"Language-Team: Spanish\n" +"Language: es_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: es-ES\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "¿Quieres cargar el guardado automático local editado por última vez en {}?" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "Abrir el editor de texto enriquecido" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Título" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "Subtítulo, o resumen" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "Escriba su artículo aquí. Puede utilizar Markdown." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "Quedan unos {} caracteres" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Etiquetas" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Licencia" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "Cubierta" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "Esto es un borrador" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Publicar" + diff --git a/po/plume-front/eu.po b/po/plume-front/eu.po new file mode 100644 index 00000000000..cf873964be4 --- /dev/null +++ b/po/plume-front/eu.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-26 13:15\n" +"Last-Translator: \n" +"Language-Team: Basque\n" +"Language: eu_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: eu\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "{}(t)an automatikoki gordetako azken kopia lokala kargatu nahi al duzu?" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "Ireki testu formatutzaile aberatsa" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Izenburua" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "Azpititulua edo laburpena" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "Idatzi hemen testua. Markdown erabil dezakezu." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "%{count} karaktere geratzen dira" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Etiketak" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Lizentzia" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "Azala" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "Zirriborro bat da" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Argitaratu" + diff --git a/po/plume-front/fa.po b/po/plume-front/fa.po new file mode 100644 index 00000000000..d28ccb2795f --- /dev/null +++ b/po/plume-front/fa.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Persian\n" +"Language: fa_IR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: fa\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "آیا می‌خواهید نسخهٔ ذخیره شدهٔ خودکار محلّی از آخرین ویرایش در {} را بار کنید؟" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "باز کردن ویرایش‌گر غنی" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "عنوان" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "زیرعنوان، یا چکیده" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "مقاله‌تان را این‌جا بنویسید. از مارک‌داون پشتیبانی می‌شود." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "نزدیک به {} حرف باقی مانده است" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "برچسب‌ها" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "پروانه" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "جلد" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "این، یک پیش‌نویس است" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "انتشار" + diff --git a/po/plume-front/fi.po b/po/plume-front/fi.po new file mode 100644 index 00000000000..3d385a643c4 --- /dev/null +++ b/po/plume-front/fi.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Finnish\n" +"Language: fi_FI\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: fi\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "Avaa edistynyt tekstieditori" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Otsikko" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "Alaotsikko tai tiivistelmä" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "Kirjoita artikkelisi tähän. Markdown -kuvauskieli on tuettu." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "%{count} merkkiä jäljellä" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Tagit" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Lisenssi" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "Kansi" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "Tämä on luonnos" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Julkaise" + diff --git a/po/plume-front/fr.po b/po/plume-front/fr.po new file mode 100644 index 00000000000..af9c03697f5 --- /dev/null +++ b/po/plume-front/fr.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: French\n" +"Language: fr_FR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: fr\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "Voulez vous charger la sauvegarde automatique locale, éditée la dernière fois à {}?" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "Ouvrir l'éditeur de texte avancé" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Titre" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "Sous-titre ou résumé" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "Écrivez votre article ici. Vous pouvez utiliser du Markdown." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "Environ {} caractères restant" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Étiquettes" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Licence" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "Illustration" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "Ceci est un brouillon" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Publier" + diff --git a/po/plume-front/gl.po b/po/plume-front/gl.po new file mode 100644 index 00000000000..e94948d22ee --- /dev/null +++ b/po/plume-front/gl.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-26 13:16\n" +"Last-Translator: \n" +"Language-Team: Galician\n" +"Language: gl_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: gl\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "Queres cargar a última copia gardada editada o {}?" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "Abre o editor de texto enriquecido" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Título" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "Subtítulo, ou resumo" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "Escribe aquí o teu artigo: podes utilizar Markdown." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "Dispós de {} caracteres" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Etiquetas" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Licenza" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "Portada" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "Este é un borrador" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Publicar" + diff --git a/po/plume-front/he.po b/po/plume-front/he.po new file mode 100644 index 00000000000..b72995132c1 --- /dev/null +++ b/po/plume-front/he.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Hebrew\n" +"Language: he_IL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: he\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "" + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "" + diff --git a/po/plume-front/hi.po b/po/plume-front/hi.po new file mode 100644 index 00000000000..e7e25ada376 --- /dev/null +++ b/po/plume-front/hi.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Hindi\n" +"Language: hi_IN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: hi\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "शीर्षक" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "अपना आर्टिकल या लेख यहाँ लिखें. Markdown उपलब्ध है." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "लगभग {} अक्षर बाकी हैं" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "टैग्स" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "लाइसेंस" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "पब्लिश करें" + diff --git a/po/plume-front/hr.po b/po/plume-front/hr.po new file mode 100644 index 00000000000..d0d4aad8292 --- /dev/null +++ b/po/plume-front/hr.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Croatian\n" +"Language: hr_HR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: hr\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Naslov" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "" + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Tagovi" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Licenca" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Objavi" + diff --git a/po/plume-front/hu.po b/po/plume-front/hu.po new file mode 100644 index 00000000000..562e53e9efe --- /dev/null +++ b/po/plume-front/hu.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Hungarian\n" +"Language: hu_HU\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: hu\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "" + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "" + diff --git a/po/plume-front/it.po b/po/plume-front/it.po new file mode 100644 index 00000000000..5b958228d7b --- /dev/null +++ b/po/plume-front/it.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Italian\n" +"Language: it_IT\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: it\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "Apri il compositore di testo avanzato" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Titolo" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "Sottotitolo, o sommario" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "Scrivi qui il tuo articolo. È supportato il Markdown." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "Circa {} caratteri rimasti" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Etichette" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Licenza" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "Copertina" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "Questa è una bozza" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Pubblica" + diff --git a/po/plume-front/ja.po b/po/plume-front/ja.po new file mode 100644 index 00000000000..5c2786d882a --- /dev/null +++ b/po/plume-front/ja.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Japanese\n" +"Language: ja_JP\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: ja\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "リッチテキストエディターを開く" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "タイトル" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "サブタイトル、または概要" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "投稿をここに書きます。Markdown がサポートされています。" + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "残り約 {} 文字" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "タグ" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "ライセンス" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "カバー" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "これは下書きです" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "公開" + diff --git a/po/plume-front/ko.po b/po/plume-front/ko.po new file mode 100644 index 00000000000..2322d6f4e2f --- /dev/null +++ b/po/plume-front/ko.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Korean\n" +"Language: ko_KR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: ko\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "" + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "" + diff --git a/po/plume-front/nb.po b/po/plume-front/nb.po new file mode 100644 index 00000000000..c37b51b59a0 --- /dev/null +++ b/po/plume-front/nb.po @@ -0,0 +1,57 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume-front\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2018-06-15 16:33-0700\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: nb\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +# plume-front/src/editor.rs:189 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:115 +msgid "Open the rich text editor" +msgstr "" + +# plume-front/src/editor.rs:145 +msgid "Title" +msgstr "" + +# plume-front/src/editor.rs:149 +msgid "Subtitle, or summary" +msgstr "" + +# plume-front/src/editor.rs:156 +msgid "Write your article here. Markdown is supported." +msgstr "" + +# plume-front/src/editor.rs:167 +msgid "Around {} characters left" +msgstr "" + +# plume-front/src/editor.rs:243 +msgid "Tags" +msgstr "" + +# plume-front/src/editor.rs:244 +msgid "License" +msgstr "" + +# plume-front/src/editor.rs:247 +msgid "Cover" +msgstr "" + +# plume-front/src/editor.rs:267 +msgid "This is a draft" +msgstr "" + +# plume-front/src/editor.rs:274 +msgid "Publish" +msgstr "" diff --git a/po/plume-front/nl.po b/po/plume-front/nl.po new file mode 100644 index 00000000000..ad21b939b33 --- /dev/null +++ b/po/plume-front/nl.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Dutch\n" +"Language: nl_NL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: nl\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "Wilt u de lokale auto-opslaan laden? Laatst bewerkt om: {}" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "Open de rich-text editor" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Titel" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "Ondertitel of samenvatting" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "Schrijf hier je artikel. Markdown wordt ondersteund." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "Ongeveer {} tekens over" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Tags" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Licentie" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "Hoofdafbeelding" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "Dit is een concept" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Publiceren" + diff --git a/po/plume-front/no.po b/po/plume-front/no.po new file mode 100644 index 00000000000..2188a87620f --- /dev/null +++ b/po/plume-front/no.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Norwegian\n" +"Language: no_NO\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: no\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "" + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Lisens" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Publiser" + diff --git a/po/plume-front/pl.po b/po/plume-front/pl.po new file mode 100644 index 00000000000..a6ae1943f9f --- /dev/null +++ b/po/plume-front/pl.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Polish\n" +"Language: pl_PL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: pl\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "Otwórz edytor tekstu sformatowanego" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Tytuł" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "Podtytuł, lub podsumowanie" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "Napisz swój artykuł tutaj. Markdown jest obsługiwany." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "Pozostało w okolicy {} znaków" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Tagi" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Licencja" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "Okładka" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "To jest szkic" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Publikuj" + diff --git a/po/plume-front/plume-front.pot b/po/plume-front/plume-front.pot new file mode 100644 index 00000000000..5152f596b9a --- /dev/null +++ b/po/plume-front/plume-front.pot @@ -0,0 +1,57 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume-front\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "" + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "" diff --git a/po/plume-front/pt.po b/po/plume-front/pt.po new file mode 100644 index 00000000000..d871abf434c --- /dev/null +++ b/po/plume-front/pt.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Portuguese, Brazilian\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: pt-BR\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "Você quer carregar o último conteúdo salvo localmente editado em {}?" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "Abrir o editor de rich text" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Título" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "Subtítulo ou resumo" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "Escreva seu artigo aqui. Markdown é suportado." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "Cerca de {} caracteres restantes" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Tags" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Licença" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "Capa" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "Isso é um rascunho" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Publicar" + diff --git a/po/plume-front/ro.po b/po/plume-front/ro.po new file mode 100644 index 00000000000..2de7b7e7b74 --- /dev/null +++ b/po/plume-front/ro.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Romanian\n" +"Language: ro_RO\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: ro\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "Deschide editorul de text" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Titlu" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "Scrie articolul tău aici. Markdown este acceptat." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "În apropiere de {} caractere rămase" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Etichete" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Licenţă" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "Coperta" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "Aceasta este o ciornă" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Publică" + diff --git a/po/plume-front/ru.po b/po/plume-front/ru.po new file mode 100644 index 00000000000..603443f8879 --- /dev/null +++ b/po/plume-front/ru.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Russian\n" +"Language: ru_RU\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: ru\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "Хотите ли вы загрузить локальное автосохранение, сделанное в {}?" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "Открыть в визуальном редакторе" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Заголовок" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "Подзаголовок или резюме" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "Пишите свою статью здесь. Markdown поддерживается." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "Осталось около {} символов" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Теги" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Лицензия" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "Обложка" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "Это черновик" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Опубликовать" + diff --git a/po/plume-front/sat.po b/po/plume-front/sat.po new file mode 100644 index 00000000000..4c95da4179b --- /dev/null +++ b/po/plume-front/sat.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Santali\n" +"Language: sat_IN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: sat\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "ᱟᱢ ᱪᱮᱫ ᱢᱟᱲᱟᱝ {} ᱨᱮ ᱥᱟᱯᱲᱟᱣ ᱟᱠᱟᱱ ᱞᱚᱠᱟᱞ ᱚᱴᱚᱥᱮᱣ ᱞᱟᱫᱮ ᱥᱟᱱᱟᱢ ᱠᱟᱱᱟ ᱥᱮ?" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "ᱨᱤᱪ ᱚᱞ ᱥᱟᱯᱟᱣᱤᱡ ᱠᱷᱩᱞᱟᱹᱭ ᱢᱮ" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "ᱴᱭᱴᱚᱞ" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "ᱥᱟᱹᱵᱴᱟᱭᱴᱟᱹᱞ, ᱟᱨ ᱵᱟᱝ ᱥᱟᱹᱢᱢᱟᱨᱭ" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "ᱟᱢᱟᱜ ᱚᱱᱚᱞ ᱱᱚᱰᱮ ᱚᱞ ᱛᱟᱢ ᱾ ᱪᱤᱱᱦᱟᱹ ᱥᱟᱯᱚᱴ ᱜᱮᱭᱟ ᱾" + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "ᱡᱷᱚᱛᱚ ᱨᱮ {} ᱡᱤᱱᱤᱥ ᱵᱟᱧᱪᱟᱣᱠᱟᱱᱟ" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "ᱴᱮᱜᱽᱥ" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "ᱞᱚᱭᱥᱮᱱᱥ" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "ᱢᱚᱭᱞᱟᱹᱴ" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "ᱱᱚᱶᱟ ᱫᱚ ᱰᱨᱟᱯᱷᱼᱴ ᱠᱟᱱᱟ" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "ᱯᱟᱨᱥᱟᱞ" + diff --git a/po/plume-front/si.po b/po/plume-front/si.po new file mode 100644 index 00000000000..9d8a1a334fc --- /dev/null +++ b/po/plume-front/si.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Sinhala\n" +"Language: si_LK\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: si-LK\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "මාතෘකාව" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "" + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "බලපත්‍රය" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "" + diff --git a/po/plume-front/sk.po b/po/plume-front/sk.po new file mode 100644 index 00000000000..d114c57f4a1 --- /dev/null +++ b/po/plume-front/sk.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-26 13:16\n" +"Last-Translator: \n" +"Language-Team: Slovak\n" +"Language: sk_SK\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: sk\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "Chceš načítať automaticky uloženú zálohu, s poslednou úpravou {}?" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "Otvor editor formátovaného textu" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Nadpis" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "Zhrnutie, alebo podnadpis" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "Tu napíš svoj článok. Markdown je podporovaný." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "Zostáva asi {} znakov" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Štítky" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Licencia" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "Obálka" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "Toto je koncept" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Zverejniť" + diff --git a/po/plume-front/sl.po b/po/plume-front/sl.po new file mode 100644 index 00000000000..76d7be5c679 --- /dev/null +++ b/po/plume-front/sl.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Slovenian\n" +"Language: sl_SI\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: sl\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Naslov" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "" + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Oznake" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Licenca" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "To je osnutek" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Objavi" + diff --git a/po/plume-front/sr.po b/po/plume-front/sr.po new file mode 100644 index 00000000000..565a341a4a5 --- /dev/null +++ b/po/plume-front/sr.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Serbian (Latin)\n" +"Language: sr_CS\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: sr-CS\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "Otvori uređivač sa stilizacijom" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Naslov" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "Podnaslov, ili sažetak" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "Napišite vaš članak ovde. Na raspolaganju vam je Markdown." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "Preostalo oko {} znakova" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Markeri" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Licenca" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "Naslovna strana" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "Ovo je nacrt" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Objavi" + diff --git a/po/plume-front/sv.po b/po/plume-front/sv.po new file mode 100644 index 00000000000..610e2c3e458 --- /dev/null +++ b/po/plume-front/sv.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Swedish\n" +"Language: sv_SE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: sv-SE\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Titel" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "Skriv din artikel här. Markdown stöds." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "Ungefär {} karaktärer kvar" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Taggar" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Licens" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "Omslag" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Publicera" + diff --git a/po/plume-front/tr.po b/po/plume-front/tr.po new file mode 100644 index 00000000000..fdeb62dcd55 --- /dev/null +++ b/po/plume-front/tr.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Turkish\n" +"Language: tr_TR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: tr\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "Zengin metin editörünü (RTE) aç" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Başlık" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "Alt başlık, veya açıklama" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "Makaleni buraya yaz. Markdown kullanabilirsin." + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "Yaklaşık {} karakter kaldı" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "Etiketler" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "Lisans" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "Kapak" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "Bu bir taslaktır" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "Yayınla" + diff --git a/po/plume-front/uk.po b/po/plume-front/uk.po new file mode 100644 index 00000000000..fecc80202f1 --- /dev/null +++ b/po/plume-front/uk.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Ukrainian\n" +"Language: uk_UA\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: uk\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "" + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "" + diff --git a/po/plume-front/ur.po b/po/plume-front/ur.po new file mode 100644 index 00000000000..d899c8bdae7 --- /dev/null +++ b/po/plume-front/ur.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Urdu (Pakistan)\n" +"Language: ur_PK\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: ur-PK\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "" + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "" + diff --git a/po/plume-front/vi.po b/po/plume-front/vi.po new file mode 100644 index 00000000000..827e2b164bd --- /dev/null +++ b/po/plume-front/vi.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Vietnamese\n" +"Language: vi_VN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: vi\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "Văn bản của tôi" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "Tiêu Châu" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "" + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "" + diff --git a/po/plume-front/zh.po b/po/plume-front/zh.po new file mode 100644 index 00000000000..62582685480 --- /dev/null +++ b/po/plume-front/zh.po @@ -0,0 +1,63 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:20\n" +"Last-Translator: \n" +"Language-Team: Chinese Traditional\n" +"Language: zh_TW\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: zh-TW\n" +"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" +"X-Crowdin-File-ID: 12\n" + +# plume-front/src/editor.rs:172 +msgid "Do you want to load the local autosave last edited at {}?" +msgstr "你想要載入上次在 {} 编辑的本地自動保存嗎?" + +# plume-front/src/editor.rs:326 +msgid "Open the rich text editor" +msgstr "開啟 RTF 編輯器" + +# plume-front/src/editor.rs:385 +msgid "Title" +msgstr "標題" + +# plume-front/src/editor.rs:389 +msgid "Subtitle, or summary" +msgstr "副標題,或摘要" + +# plume-front/src/editor.rs:396 +msgid "Write your article here. Markdown is supported." +msgstr "在這裡寫下您的文章。支援 Markdown 語法。" + +# plume-front/src/editor.rs:407 +msgid "Around {} characters left" +msgstr "大約還可輸入 {} 字符" + +# plume-front/src/editor.rs:517 +msgid "Tags" +msgstr "標籤" + +# plume-front/src/editor.rs:518 +msgid "License" +msgstr "授權條款" + +# plume-front/src/editor.rs:524 +msgid "Cover" +msgstr "封面" + +# plume-front/src/editor.rs:564 +msgid "This is a draft" +msgstr "這是草稿" + +# plume-front/src/editor.rs:575 +msgid "Publish" +msgstr "發布" + diff --git a/po/plume/af.po b/po/plume/af.po new file mode 100644 index 00000000000..ef20782946a --- /dev/null +++ b/po/plume/af.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Afrikaans\n" +"Language: af_ZA\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: af\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "" + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "" + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "" + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "" + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "" + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "" + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "" + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "" + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "" + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "" + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "" + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "" + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "" + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "" + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "" + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "" + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "" + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "" + +# src/routes/medias.rs:180 +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 "" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "" + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "" + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "" + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "" + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "" + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "" + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "" + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "" + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "" + +msgid "Create an account" +msgstr "" + +msgid "Email" +msgstr "" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Your media" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "" + +msgid "Menu" +msgstr "" + +msgid "Search" +msgstr "" + +msgid "Dashboard" +msgstr "" + +msgid "Notifications" +msgstr "" + +msgid "Log Out" +msgstr "" + +msgid "My account" +msgstr "" + +msgid "Log In" +msgstr "" + +msgid "Register" +msgstr "" + +msgid "About this instance" +msgstr "" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "" + +msgid "It is you" +msgstr "" + +msgid "Edit your profile" +msgstr "" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "" + +msgid "Follow {}" +msgstr "" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "" + +msgid "Subscribers" +msgstr "" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "" + +msgid "Your Drafts" +msgstr "" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "" + +msgid "Your Profile" +msgstr "" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "" + +msgid "Configuration" +msgstr "" + +msgid "Instances" +msgstr "" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "" + +msgid "Unblock" +msgstr "" + +msgid "Block" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Allow anyone to register here" +msgstr "" + +msgid "Short description" +msgstr "" + +msgid "Markdown syntax is supported" +msgstr "" + +msgid "Long description" +msgstr "" + +msgid "Default article license" +msgstr "" + +msgid "Save these settings" +msgstr "" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +msgid "Administred by" +msgstr "" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +msgid "Tags, separated by commas" +msgstr "" + +msgid "License" +msgstr "" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "" +msgstr[1] "" + +msgid "I don't like this anymore" +msgstr "" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" +msgstr[1] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "" + +msgid "Comments" +msgstr "" + +msgid "Your comment" +msgstr "" + +msgid "Submit comment" +msgstr "" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "" + +msgid "Log in" +msgstr "" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +msgid "New password" +msgstr "" + +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "" + +msgid "Create a blog" +msgstr "" + +msgid "Create blog" +msgstr "" + +msgid "Edit \"{}\"" +msgstr "" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" +msgstr[1] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +msgid "From this date" +msgstr "" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" + diff --git a/po/plume/ar.po b/po/plume/ar.po new file mode 100644 index 00000000000..84c230084a0 --- /dev/null +++ b/po/plume/ar.po @@ -0,0 +1,1046 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Arabic\n" +"Language: ar_SA\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: ar\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "علّق {0} على مقالك." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} مشترك لك." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} أعجبهم مقالك." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "أشار إليك {0}." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} دعمو مقالك." + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "خيطك" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "الخيط المحلي" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "الخيط الموحد" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "الصورة الرمزية لـ {0}" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "الصفحة السابقة" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "الصفحة التالية" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "اختياري" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "لإنشاء مدونة جديدة، تحتاج إلى تسجيل الدخول" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "توجد مدونة تحمل نفس العنوان." + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "تم إنشاء مدونتك بنجاح!" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "تم حذف مدونتك." + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "لا يسمح لك بحذف هذه المدونة." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "لا يسمح لك بتعديل هذه المدونة." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "لا يمكنك استخدام هذه الوسائط كأيقونة للمدونة." + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "لا يمكنك استخدام هذه الوسائط كشعار للمدونة." + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "تم تحديث معلومات مُدوّنتك." + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "تم نشر تعليقك." + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "تم حذف تعليقك." + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "التسجيلات مُغلقة على مثيل الخادم هذ." + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "لقد تم إنشاء حسابك. ما عليك إلّا الولوج الآن للتمكّن مِن استعماله." + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "تم حفظ إعدادات المثيل." + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "تم إلغاء حظر {}." + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "تم حظر {}." + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "البريد الإلكتروني محظور" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "لا يسمح لك القيام بهذا الإجراء." + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "تم." + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "يجب عليك تسجيل الدخول أولا للإعجاب بهذا المقال" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "لقد تم حذف وسائطك." + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "لا يسمح لك بحذف هذه الوسائط." + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "تم تحديث صورتك الشخصية." + +# src/routes/medias.rs:180 +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 "يجب عليك تسجيل الدخول أولا لعرض الإشعارات" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "هذا المقال ليس منشورا بعد." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "يجب عليك تسجيل الدخول أولا لكتابة مقال جديد" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "لست مِن محرري هذه المدونة." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "منشور جديد" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "تعديل {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "لا يسمح لك بالنشر على هذه المدونة." + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "تم تحديث مقالك." + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "تم حفظ مقالك." + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "مقال جديد" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "لا يسمح لك بحذف هذا المقال." + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "تم حذف مقالك." + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "لم يتم العثور على المقال الذي تحاول حذفه. ربما سبق حذفه؟" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "تعذر العثور عن معلومات حسابك. المرجو التحقق من صحة إسم المستخدم." + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "يجب عليك تسجيل الدخول أولا للإعادت نشر هذا المقال" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "أنت الآن متصل." + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "لقد قمتَ بالخروج للتوّ." + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "إعادة تعيين كلمة المرور" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "ها هو رابط إعادة تعيين كلمتك السرية: {0}" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "تمت إعادة تعيين كلمتك السرية بنجاح." + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "يجب عليك تسجيل الدخول أولاللنفاذ إلى لوح المراقبة" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "أنت لم تعد تتابع {}." + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "أنت الآن تتابع {}." + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "للإشتراك بأحد ما، يجب تسجيل الدخول أولا" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "لتعديل الحساب، يجب تسجيل الدخول أولا" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "تم تحديث ملفك الشخصي." + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "تم حذف حسابك." + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "لا يمكنك حذف حساب شخص آخر." + +msgid "Create your account" +msgstr "انشئ حسابك" + +msgid "Create an account" +msgstr "انشئ حسابا" + +msgid "Email" +msgstr "البريد الالكتروني" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "المعذرة، لاكن التسجيل مغلق في هذا المثيل بالدات. يمكنك إجاد مثيل آخر للتسجيل." + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "تحقق من علبة الوارد الخاصة بك!" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "اسم المستخدم" + +msgid "Password" +msgstr "كلمة السر" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "إرسال الوسائط" + +msgid "Description" +msgstr "الوصف" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "مفيدة للأشخاص المعاقين بصريا، فضلا عن معلومات الترخيص" + +msgid "Content warning" +msgstr "تحذير عن المحتوى" + +msgid "Leave it empty, if none is needed" +msgstr "إتركه فارغا إن لم تكن في الحاجة" + +msgid "File" +msgstr "الملف" + +msgid "Send" +msgstr "أرسل" + +msgid "Your media" +msgstr "وسائطك" + +msgid "Upload" +msgstr "إرسال" + +msgid "You don't have any media yet." +msgstr "ليس لديك أية وسائط بعد." + +msgid "Content warning: {0}" +msgstr "تحذير عن المحتوى: {0}" + +msgid "Delete" +msgstr "حذف" + +msgid "Details" +msgstr "التفاصيل" + +msgid "Media details" +msgstr "تفاصيل الصورة" + +msgid "Go back to the gallery" +msgstr "العودة إلى المعرض" + +msgid "Markdown syntax" +msgstr "صياغت ماركداون" + +msgid "Copy it into your articles, to insert this media:" +msgstr "قم بنسخه في مقالاتك منأجل إدراج الوسائط:" + +msgid "Use as an avatar" +msgstr "استخدمها كصورة رمزية" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "القائمة" + +msgid "Search" +msgstr "البحث" + +msgid "Dashboard" +msgstr "لوح المراقبة" + +msgid "Notifications" +msgstr "الإشعارات" + +msgid "Log Out" +msgstr "الخروج" + +msgid "My account" +msgstr "حسابي" + +msgid "Log In" +msgstr "تسجيل الدخول" + +msgid "Register" +msgstr "إنشاء حساب" + +msgid "About this instance" +msgstr "عن مثيل الخادوم هذا" + +msgid "Privacy policy" +msgstr "سياسة الخصوصية" + +msgid "Administration" +msgstr "الإدارة" + +msgid "Documentation" +msgstr "الدليل" + +msgid "Source code" +msgstr "الشيفرة المصدرية" + +msgid "Matrix room" +msgstr "غرفة المحادثة على ماتريكس" + +msgid "Admin" +msgstr "المدير" + +msgid "It is you" +msgstr "هو أنت" + +msgid "Edit your profile" +msgstr "تعديل ملفك الشخصي" + +msgid "Open on {0}" +msgstr "افتح على {0}" + +msgid "Unsubscribe" +msgstr "إلغاء الاشتراك" + +msgid "Subscribe" +msgstr "إشترِك" + +msgid "Follow {}" +msgstr "تابِع {}" + +msgid "Log in to follow" +msgstr "قم بتسجيل الدخول للمتابعة" + +msgid "Enter your full username handle to follow" +msgstr "اخل اسم مستخدمك كاملا للمتابعة" + +msgid "{0}'s subscribers" +msgstr "{0} مشتركين" + +msgid "Articles" +msgstr "المقالات" + +msgid "Subscribers" +msgstr "المشترِكون" + +msgid "Subscriptions" +msgstr "الاشتراكات" + +msgid "{0}'s subscriptions" +msgstr "{0} اشتراكات" + +msgid "Your Dashboard" +msgstr "لوح المراقبة" + +msgid "Your Blogs" +msgstr "مدوناتك" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "ليس لديك أيت مدونة. قم بإنشاء مدونتك أو أطلب الإنظمام لواحدة." + +msgid "Start a new blog" +msgstr "انشئ مدونة جديدة" + +msgid "Your Drafts" +msgstr "مسوداتك" + +msgid "Go to your gallery" +msgstr "الانتقال إلى معرضك" + +msgid "Edit your account" +msgstr "تعديل حسابك" + +msgid "Your Profile" +msgstr "ملفك الشخصي" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "لتغير الصورة التشخيصية قم أولا برفعها إلى الألبوم ثم قم بتعينها من هنالك." + +msgid "Upload an avatar" +msgstr "تحميل صورة رمزية" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "الملخص" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "تحديث الحساب" + +msgid "Danger zone" +msgstr "منطقة الخطر" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "نوخى الحذر هنا، فكل إجراء تأخذه هنا لا يمكن الغاؤه." + +msgid "Delete your account" +msgstr "احذف حسابك" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "المعذرة ولاكن كمدير لايمكنك مغادرة مثيلك الخاص." + +msgid "Latest articles" +msgstr "آخر المقالات" + +msgid "Atom feed" +msgstr "تدفق أتوم" + +msgid "Recently boosted" +msgstr "تم ترقيتها حديثا" + +msgid "Articles tagged \"{0}\"" +msgstr "المقالات الموسومة بـ \"{0}\"" + +msgid "There are currently no articles with such a tag" +msgstr "لا يوجد حالي أي مقال بهذا الوسام" + +msgid "The content you sent can't be processed." +msgstr "لا يمكن معالجة المحتوى الذي قمت بإرساله." + +msgid "Maybe it was too long." +msgstr "ربما كان طويلا جدا." + +msgid "Internal server error" +msgstr "خطأ داخلي في الخادم" + +msgid "Something broke on our side." +msgstr "حصل خطأ ما مِن جهتنا." + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "نعتذر عن الإزعاج. إن كنت تضن أن هذه مشكلة، يرجى إبلاغنا." + +msgid "Invalid CSRF token" +msgstr "الرمز المميز CSRF غير صالح" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "هناكخطأم ما في رمز CSRF. تحقق أن الكوكيز مفعل في متصفحك وأعد تحميل الصفحة. إذا واجهتهذا الخطأ منجديد يرجى التبليغ." + +msgid "You are not authorized." +msgstr "ليست لديك التصريحات اللازمة للقيام بذلك." + +msgid "Page not found" +msgstr "الصفحة غير موجودة" + +msgid "We couldn't find this page." +msgstr "تعذر العثور على هذه الصفحة." + +msgid "The link that led you here may be broken." +msgstr "مِن المشتبه أنك قد قمت باتباع رابط غير صالح." + +msgid "Users" +msgstr "المستخدمون" + +msgid "Configuration" +msgstr "الإعدادات" + +msgid "Instances" +msgstr "مثيلات الخوادم" + +msgid "Email blocklist" +msgstr "قائمة حظر عناوين البريد الإلكتروني" + +msgid "Grant admin rights" +msgstr "منحه صلاحيات المدير" + +msgid "Revoke admin rights" +msgstr "سحب صلاحيات المدير منه" + +msgid "Grant moderator rights" +msgstr "منحه صلاحيات المشرف" + +msgid "Revoke moderator rights" +msgstr "سحب صلاحيات المشرف منه" + +msgid "Ban" +msgstr "اطرد" + +msgid "Run on selected users" +msgstr "نفّذ الإجراء على المستخدمين الذين تم اختيارهم" + +msgid "Moderator" +msgstr "مُشرف" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "إدارة {0}" + +msgid "Unblock" +msgstr "الغاء الحظر" + +msgid "Block" +msgstr "حظر" + +msgid "Name" +msgstr "الاسم" + +msgid "Allow anyone to register here" +msgstr "السماح للجميع بإنشاء حساب" + +msgid "Short description" +msgstr "وصف مختصر" + +msgid "Markdown syntax is supported" +msgstr "صياغت ماركداون مدعمة" + +msgid "Long description" +msgstr "الوصف الطويل" + +msgid "Default article license" +msgstr "الرخصة الافتراضية للمقال" + +msgid "Save these settings" +msgstr "احفظ هذه الإعدادات" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "إذا كنت تصفح هذا الموقع كزائر ، لا يتم تجميع أي بيانات عنك." + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "مرحبا بكم في {0}" + +msgid "View all" +msgstr "عرضها كافة" + +msgid "About {0}" +msgstr "عن {0}" + +msgid "Runs Plume {0}" +msgstr "مدعوم بـ Plume {0}" + +msgid "Home to {0} people" +msgstr "يستضيف {0} أشخاص" + +msgid "Who wrote {0} articles" +msgstr "قاموا بتحرير {0} مقالات" + +msgid "And are connected to {0} other instances" +msgstr "ومتصل بـ {0} مثيلات خوادم أخرى" + +msgid "Administred by" +msgstr "يديره" + +msgid "Interact with {}" +msgstr "التفاعل مع {}" + +msgid "Log in to interact" +msgstr "قم بتسجيل الدخول قصد التفاعل" + +msgid "Enter your full username to interact" +msgstr "أدخل إسم المستخدم الخاص بك كاملا للتفاعل" + +msgid "Publish" +msgstr "انشر" + +msgid "Classic editor (any changes will be lost)" +msgstr "المحرر العادي (ستفقد كل التغيرات)" + +msgid "Title" +msgstr "العنوان" + +msgid "Subtitle" +msgstr "العنوان الثانوي" + +msgid "Content" +msgstr "المحتوى" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "يكنك رفع الوسائط للألبوم ومن ثم نسخ شفرة الماركداون في مقالاتك لإدراجها." + +msgid "Upload media" +msgstr "تحميل وسائط" + +msgid "Tags, separated by commas" +msgstr "الكلمات الدلالية، مفصولة بفواصل" + +msgid "License" +msgstr "الرخصة" + +msgid "Illustration" +msgstr "الصورة الإيضاحية" + +msgid "This is a draft, don't publish it yet." +msgstr "هذه مُسودّة، لا تقم بنشرها الآن." + +msgid "Update" +msgstr "تحديث" + +msgid "Update, or publish" +msgstr "تحديث أو نشر" + +msgid "Publish your post" +msgstr "انشر منشورك" + +msgid "Written by {0}" +msgstr "كتبه {0}" + +msgid "All rights reserved." +msgstr "جميع الحقوق محفوظة." + +msgid "This article is under the {0} license." +msgstr "تم نشر هذا المقال تحت رخصة {0}." + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "بدون اعجاب" +msgstr[1] "إعجاب واحد" +msgstr[2] "إعجابَين" +msgstr[3] "{0} إعجاب" +msgstr[4] "{0} إعجابات" +msgstr[5] "{0} إعجابات" + +msgid "I don't like this anymore" +msgstr "لم يعد يعجبني هذا" + +msgid "Add yours" +msgstr "أعجبني" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "لم يدعم" +msgstr[1] "دُعِّم مرة واحدة" +msgstr[2] "دُعِم مرتين إثنتين" +msgstr[3] "دُعِّم {0} مرات" +msgstr[4] "دُعِّم {0} مرة" +msgstr[5] "دُعِّم {0} مرة" + +msgid "I don't want to boost this anymore" +msgstr "لم أعد أرغب في دعم هذا" + +msgid "Boost" +msgstr "رقّي" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "{0}قم بتسجيل الدخول{1} أو {2}استخدم حسابك على الفديفرس{3} إن كنت ترغب في التفاعل مع هذا المقال" + +msgid "Comments" +msgstr "التعليقات" + +msgid "Your comment" +msgstr "تعليقك" + +msgid "Submit comment" +msgstr "ارسال التعليق" + +msgid "No comments yet. Be the first to react!" +msgstr "لا توجد هناك تعليقات بعد. كن أول مَن يتفاعل معه!" + +msgid "Are you sure?" +msgstr "هل أنت واثق؟" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "لا يزال هذا المقال مجرّد مسودّة. إلّا أنت والمحررون الآخرون يمكنهم رؤيته." + +msgid "Only you and other authors can edit this article." +msgstr "إلّا أنت والمحرّرون الآخرون يمكنهم تعديل هذا المقال." + +msgid "Edit" +msgstr "تعديل" + +msgid "I'm from this instance" +msgstr "أنا أنتمي إلى مثيل الخادم هذا" + +msgid "Username, or email" +msgstr "اسم المستخدم أو عنوان البريد الالكتروني" + +msgid "Log in" +msgstr "تسجيل الدخول" + +msgid "I'm from another instance" +msgstr "أنا أنتمي إلى مثيل خادم آخر" + +msgid "Continue to your instance" +msgstr "واصل إلى مثيل خادمك" + +msgid "Reset your password" +msgstr "أعد تعيين كلمتك السرية" + +msgid "New password" +msgstr "كلمة السر الجديدة" + +msgid "Confirmation" +msgstr "تأكيد" + +msgid "Update password" +msgstr "تحديث الكلمة السرية" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "لقد أرسلنا رسالة للعنوان الذي توصلنا به من طرفك تضمنرابط لإعادت تحديد كلمة المرور." + +msgid "Send password reset link" +msgstr "أرسل رابط إعادة تعيين الكلمة السرية" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "مدونة جديدة" + +msgid "Create a blog" +msgstr "انشئ مدونة" + +msgid "Create blog" +msgstr "انشاء مدونة" + +msgid "Edit \"{}\"" +msgstr "تعديل \"{}\"" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "يمكن رفع الصور إلى ألبومك من أجل إستعمالها كأيقونة المدونة أو الشعار." + +msgid "Upload images" +msgstr "رفع صور" + +msgid "Blog icon" +msgstr "أيقونة المدونة" + +msgid "Blog banner" +msgstr "شعار المدونة" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "تحديث المدونة" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "توخى الحذر هنا، فأي إجراء تأخذه هنا لا يمكن الغاؤه." + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "احذف هذه المدونة نهائيا" + +msgid "{}'s icon" +msgstr "أيقونة {}" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "ليس هنالك مؤلف في هذه المدونة : " +msgstr[1] "هنالك مؤلف واحد في هذه المدونة :" +msgstr[2] "هنالك مؤلفين إثنين في هءه المدونة :" +msgstr[3] "هنالك {0} مؤلفين في هذه المدونة :" +msgstr[4] "هنالك {0} مؤلفون في هذه المدونة :" +msgstr[5] "هنلك {0} مؤلفون في هذه المدونة: " + +msgid "No posts to see here yet." +msgstr "في الوقت الراهن لا توجد أية منشورات هنا." + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "لا شيء" + +msgid "No description" +msgstr "مِن دون وصف" + +msgid "Respond" +msgstr "رد" + +msgid "Delete this comment" +msgstr "احذف هذا التعليق" + +msgid "What is Plume?" +msgstr "ما هو بلوم Plume؟" + +msgid "Plume is a decentralized blogging engine." +msgstr "بلوم محرك لامركزي للمدونات." + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "يمكن للمحررين أن يديرو العديد من المدونات كل واحدة كموقع منفرد." + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "ستكون المقالات معروضة على مواقع بلومالأخرى حيث يمكنكم التفاعل معها مباشرة عبر أية منصة أخرى مثل ماستدون." + +msgid "Read the detailed rules" +msgstr "إقرأ القواعد بالتفصيل" + +msgid "By {0}" +msgstr "مِن طرف {0}" + +msgid "Draft" +msgstr "مسودة" + +msgid "Search result(s) for \"{0}\"" +msgstr "نتائج البحث عن \"{0}\"" + +msgid "Search result(s)" +msgstr "نتائج البحث" + +msgid "No results for your query" +msgstr "لا توجد نتيجة لطلبك" + +msgid "No more results for your query" +msgstr "لم تتبقى نتائج لطلبك" + +msgid "Advanced search" +msgstr "البحث المتقدم" + +msgid "Article title matching these words" +msgstr "عنوان المقالات المطابقة لهذه الكلمات" + +msgid "Subtitle matching these words" +msgstr "العناوين الثانوية للمقالات المطابقة لهذه الكلمات" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "محتوى العرض" + +msgid "From this date" +msgstr "اعتبارا من هذا التاريخ" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "تتضمن هذه الوسوم" + +msgid "Tags" +msgstr "الوسوم" + +msgid "Posted on one of these instances" +msgstr "نُشر في واحدة من هاته المثائل" + +msgid "Instance domain" +msgstr "اسم نطاق مثيل الخادم" + +msgid "Posted by one of these authors" +msgstr "نُشر من طرف واحد من هاؤلاء المؤلفين" + +msgid "Author(s)" +msgstr "المؤلفون" + +msgid "Posted on one of these blogs" +msgstr "نُشر في واحدة من هاته المدونات" + +msgid "Blog title" +msgstr "عنوان المدونة" + +msgid "Written in this language" +msgstr "كتب في هذه اللغة" + +msgid "Language" +msgstr "اللغة" + +msgid "Published under this license" +msgstr "نشرتحت هذا الترخيص" + +msgid "Article license" +msgstr "رخصة المقال" + diff --git a/po/plume/bg.po b/po/plume/bg.po new file mode 100644 index 00000000000..85d1dbe720f --- /dev/null +++ b/po/plume/bg.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Bulgarian\n" +"Language: bg_BG\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: bg\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} коментира(ха) твоя статия." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} абониран(и) за вас." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} хареса(ха) вашата статия." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} ви спомена(ха)." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} подсили(ха) вашата статия." + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "Вашата емисия" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "Местна емисия" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "Федерална емисия" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "Аватар {0}" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "Предишна страница" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "Следваща страница" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "По избор" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "За да създадете нов блог, трябва да влезете" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "Вече съществува блог със същото име." + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "Блогът Ви бе успешно създаден!" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "Блогът ви бе изтрит." + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "Нямате права за да изтриете този блог." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "Нямате права за да редактирате този блог." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "Не можете да използвате тази медия като икона на блога." + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "Не можете да използвате тази медия като банер на блога." + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "Информацията в блога ви бе актуализирана." + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "Коментарът е публикуван." + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "Коментарът бе изтрит." + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "Регистрациите са затворени в тази инстанция." + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "Вашият акаунт беше създаден. Сега просто трябва да влезете за да можете да го използвате." + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "Настройките на инстанциите са запазени." + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "{} са отблокирани." + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "{} са блокирани." + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "Блоковете са изтрити" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "Имейлът вече е блокиран" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "Блокиран Email" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "Не можете да промените собствените си права." + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "Нямате права да предприемате това действие." + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "Свършен." + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "За да харесате публикация, трябва да сте влезли в профила си" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "Медията ви е изтрита." + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "Вие нямате права да изтриете тази медия." + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "Вашият аватар е актуализиран." + +# src/routes/medias.rs:180 +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 "За да видите известията си, трябва да сте влезли в профила си" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "Тази публикация все още не е публикувана." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "За да напишете нова публикация, трябва да влезете" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "Вие не сте автор на този блог." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "Нова публикация" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "Редактирано от {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "Нямате права за публикуване в този блог." + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "Статията ви е актуализирана." + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "Вашата статия е запазена." + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "Нова статия" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "Нямате права за изтриване на тази статия." + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "Статията е изтрита." + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "Изглежда, че статията, която се опитвате да изтриете не съществува. Може би вече я няма?" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "Не можа да се получи достатъчно информация за профила ви. Моля, уверете се, че потребителското ви име е правилно." + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "За да споделите отново публикация, трябва да сте влезли в профила си" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "Вече сте свързани." + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "Вече сте изключени." + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "Нулиране на паролата" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "Ето и връзка, на която да зададете нова парола: {0}" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "Вашата парола бе успешно възстановена." + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "За да получите достъп до таблото си за управление, трябва да сте влезли в профила си" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "Вече не следвате {}." + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "Вече следите {}." + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "За да се абонирате за някого, трябва да сте влезли в системата" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "За да редактирате профила си, трябва да влезете" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "Вашият профил е актуализиран." + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "Вашият акаунт е изтрит." + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "Не можете да изтриете профила на някой друг." + +msgid "Create your account" +msgstr "Създай профил" + +msgid "Create an account" +msgstr "Създай профил" + +msgid "Email" +msgstr "Електронна поща" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "Извиняваме се, но регистрациите са затворени за тази конкретна инстанция. Можете обаче да намерите друга." + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "Проверете си пощата!" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "Потребителско име" + +msgid "Password" +msgstr "Парола" + +msgid "Password confirmation" +msgstr "Потвърждение на парола" + +msgid "Media upload" +msgstr "Качи медия" + +msgid "Description" +msgstr "Описание" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "Полезно е за хора със зрителни увреждания, както и лицензна информация" + +msgid "Content warning" +msgstr "Предупреждение за съдържанието" + +msgid "Leave it empty, if none is needed" +msgstr "Оставете го празно, ако не е необходимо" + +msgid "File" +msgstr "Файл" + +msgid "Send" +msgstr "Изпрати" + +msgid "Your media" +msgstr "Вашите медия файлове" + +msgid "Upload" +msgstr "Качи" + +msgid "You don't have any media yet." +msgstr "Все още нямате никакви медии." + +msgid "Content warning: {0}" +msgstr "Предупреждение за съдържание: {0}" + +msgid "Delete" +msgstr "Изтрий" + +msgid "Details" +msgstr "Детайли" + +msgid "Media details" +msgstr "Детайли за медията" + +msgid "Go back to the gallery" +msgstr "Върнете се в галерията" + +msgid "Markdown syntax" +msgstr "Markdown синтаксис" + +msgid "Copy it into your articles, to insert this media:" +msgstr "Копирайте го в статиите си, за да вмъкнете медията:" + +msgid "Use as an avatar" +msgstr "Използвайте като аватар" + +msgid "Plume" +msgstr "Pluma" + +msgid "Menu" +msgstr "Меню" + +msgid "Search" +msgstr "Търсене" + +msgid "Dashboard" +msgstr "Контролен панел" + +msgid "Notifications" +msgstr "Известия" + +msgid "Log Out" +msgstr "Излез" + +msgid "My account" +msgstr "Моят профил" + +msgid "Log In" +msgstr "Влез" + +msgid "Register" +msgstr "Регистрация" + +msgid "About this instance" +msgstr "За тази инстанция" + +msgid "Privacy policy" +msgstr "Декларация за поверителност" + +msgid "Administration" +msgstr "Aдминистрация" + +msgid "Documentation" +msgstr "Документация" + +msgid "Source code" +msgstr "Изходен код" + +msgid "Matrix room" +msgstr "Matrix стая" + +msgid "Admin" +msgstr "Администратор" + +msgid "It is you" +msgstr "Това си ти" + +msgid "Edit your profile" +msgstr "Редактиране на вашият профил" + +msgid "Open on {0}" +msgstr "Отворен на {0}" + +msgid "Unsubscribe" +msgstr "Отписване" + +msgid "Subscribe" +msgstr "Абонирай се" + +msgid "Follow {}" +msgstr "Последвай {}" + +msgid "Log in to follow" +msgstr "Влезте, за да следвате" + +msgid "Enter your full username handle to follow" +msgstr "Въведете пълното потребителско име, което искате да следвате" + +msgid "{0}'s subscribers" +msgstr "{0} абонати" + +msgid "Articles" +msgstr "Статии" + +msgid "Subscribers" +msgstr "Абонати" + +msgid "Subscriptions" +msgstr "Абонаменти" + +msgid "{0}'s subscriptions" +msgstr "{0} абонаменти" + +msgid "Your Dashboard" +msgstr "Вашият контролен панел" + +msgid "Your Blogs" +msgstr "Вашият Блог" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "Все още нямате блог. Създайте свой собствен или поискайте да се присъедините към някой друг." + +msgid "Start a new blog" +msgstr "Започнете нов блог" + +msgid "Your Drafts" +msgstr "Вашите Проекти" + +msgid "Go to your gallery" +msgstr "Отидете в галерията си" + +msgid "Edit your account" +msgstr "Редактирайте профила си" + +msgid "Your Profile" +msgstr "Вашият профил" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "За да промените аватара си, качете го в галерията и след това го изберете." + +msgid "Upload an avatar" +msgstr "Качете аватар" + +msgid "Display name" +msgstr "Показвано име" + +msgid "Summary" +msgstr "Резюме" + +msgid "Theme" +msgstr "Тема" + +msgid "Default theme" +msgstr "Тема по подразбиране" + +msgid "Error while loading theme selector." +msgstr "Грешка при зареждане на селектора с теми." + +msgid "Never load blogs custom themes" +msgstr "Никога не зареждайте в блога теми по поръчка" + +msgid "Update account" +msgstr "Актуализиране на профил" + +msgid "Danger zone" +msgstr "Опасна зона" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "Бъдете много внимателни, всяко действие предприето тук не може да бъде отменено." + +msgid "Delete your account" +msgstr "Изтриване на вашият профил" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "За съжаление, като администратор не можете да напуснете своята собствена инстанция." + +msgid "Latest articles" +msgstr "Последни статии" + +msgid "Atom feed" +msgstr "Atom емисия" + +msgid "Recently boosted" +msgstr "Наскоро подсилен" + +msgid "Articles tagged \"{0}\"" +msgstr "Маркирани статии \"{0}\"" + +msgid "There are currently no articles with such a tag" +msgstr "Понастоящем няма статии с такъв маркер" + +msgid "The content you sent can't be processed." +msgstr "Съдържанието, което сте изпратили не може да бъде обработено." + +msgid "Maybe it was too long." +msgstr "Може би беше твърде дълго." + +msgid "Internal server error" +msgstr "Вътрешна грешка в сървъра" + +msgid "Something broke on our side." +msgstr "Възникна грешка от ваша страна." + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "Извиняваме се за това. Ако смятате, че това е грешка, моля докладвайте я." + +msgid "Invalid CSRF token" +msgstr "Невалиден CSRF token (маркер)" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "Нещо не е наред с вашия CSRF token (маркер). Уверете се, че бисквитките са активирани в браузъра и опитайте да заредите отново тази страница. Ако продължите да виждате това съобщение за грешка, моля, подайте сигнал за това." + +msgid "You are not authorized." +msgstr "Не сте упълномощени." + +msgid "Page not found" +msgstr "Страницата не е намерена" + +msgid "We couldn't find this page." +msgstr "Не можахме да намерим тази страница." + +msgid "The link that led you here may be broken." +msgstr "Възможно е връзката, от която сте дошли да е неправилна." + +msgid "Users" +msgstr "Потребители" + +msgid "Configuration" +msgstr "Конфигурация" + +msgid "Instances" +msgstr "Инстанция" + +msgid "Email blocklist" +msgstr "Черен списък с е-mail" + +msgid "Grant admin rights" +msgstr "Предоставяне на администраторски права" + +msgid "Revoke admin rights" +msgstr "Анулиране на администраторски права" + +msgid "Grant moderator rights" +msgstr "Даване на модераторски права" + +msgid "Revoke moderator rights" +msgstr "Анулиране на модераторски права" + +msgid "Ban" +msgstr "Забрани" + +msgid "Run on selected users" +msgstr "Пускане на избрани потребители" + +msgid "Moderator" +msgstr "Модератор" + +msgid "Moderation" +msgstr "Модерация" + +msgid "Home" +msgstr "Начало" + +msgid "Administration of {0}" +msgstr "Администрирано от {0}" + +msgid "Unblock" +msgstr "Отблокирай" + +msgid "Block" +msgstr "Блокирай" + +msgid "Name" +msgstr "Име" + +msgid "Allow anyone to register here" +msgstr "Позволете на всеки да се регистрира" + +msgid "Short description" +msgstr "Кратко описание" + +msgid "Markdown syntax is supported" +msgstr "Поддържа се Markdown синтаксис" + +msgid "Long description" +msgstr "Дълго описание" + +msgid "Default article license" +msgstr "Лиценз по подразбиране" + +msgid "Save these settings" +msgstr "Запаметете тези настройки" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "Ако разглеждате този сайт като посетител, не се събират данни за вас." + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "Като регистриран потребител трябва да предоставите потребителско име (което не е задължително да е вашето истинско име), вашия функционален имейл адрес и парола за да можете да влезете, да напишете статии и коментари. Съдържанието, което изпращате се съхранява докато не го изтриете." + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "Когато влезете в системата, съхраняваме две „бисквитки“, една за отваряне на сесията, а втората за да попречи на други хора да действат от ваше име. Ние не съхраняваме никакви други бисквитки." + +msgid "Blocklisted Emails" +msgstr "Имейли в череният списък" + +msgid "Email address" +msgstr "Имейл адрес" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "Имейл адресът, който искате да блокирате. За да блокирате домейните можете да използвате широкообхватен синтаксис, например '*@example.com' блокира всички адреси от example.com" + +msgid "Note" +msgstr "Бележка" + +msgid "Notify the user?" +msgstr "Уведомяване на потребителя?" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "По желание, показва съобщение на потребителя, когато той се опита да създаде акаунт с този адрес" + +msgid "Blocklisting notification" +msgstr "Известие за блокиране от списък" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "Съобщението, което трябва да се покаже, когато потребителят се опита да създаде акаунт с този имейл адрес" + +msgid "Add blocklisted address" +msgstr "Добавяне на адрес в черният списък" + +msgid "There are no blocked emails on your instance" +msgstr "Няма блокирани имейли във вашата инстанция" + +msgid "Delete selected emails" +msgstr "Изтриване на избраните имейли" + +msgid "Email address:" +msgstr "Имейл адрес:" + +msgid "Blocklisted for:" +msgstr "Черен списък за:" + +msgid "Will notify them on account creation with this message:" +msgstr "Ще бъдат уведомени с това съобщение при създаване на акаунт:" + +msgid "The user will be silently prevented from making an account" +msgstr "Потребителят тихо ще бъде възпрепятстван да направи акаунт" + +msgid "Welcome to {}" +msgstr "Добре дошли в {}" + +msgid "View all" +msgstr "Виж всичко" + +msgid "About {0}" +msgstr "Относно {0}" + +msgid "Runs Plume {0}" +msgstr "Осъществено с Plume {0}" + +msgid "Home to {0} people" +msgstr "Дом за {0} хора" + +msgid "Who wrote {0} articles" +msgstr "Кой е написал {0} статии" + +msgid "And are connected to {0} other instances" +msgstr "И е свързана с {0} други инстанции" + +msgid "Administred by" +msgstr "Администрира се от" + +msgid "Interact with {}" +msgstr "Взаимодействие с {}" + +msgid "Log in to interact" +msgstr "Влезте, за да си взаимодействате" + +msgid "Enter your full username to interact" +msgstr "Въведете пълното си потребителско име, за да си взаимодействате" + +msgid "Publish" +msgstr "Публикувайте" + +msgid "Classic editor (any changes will be lost)" +msgstr "Класически редактор (всички промени ще бъдат загубени)" + +msgid "Title" +msgstr "Заглавие" + +msgid "Subtitle" +msgstr "Подзаглавие" + +msgid "Content" +msgstr "Съдържание" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "Можете да качвате мултимедия в галерията си, а след това да копирате Markdown кода в статиите си за да я вмъкнете." + +msgid "Upload media" +msgstr "Качете медия" + +msgid "Tags, separated by commas" +msgstr "Тагове, разделени със запетая" + +msgid "License" +msgstr "Лиценз" + +msgid "Illustration" +msgstr "Илюстрация" + +msgid "This is a draft, don't publish it yet." +msgstr "Това е проект, все още не го публикувайте." + +msgid "Update" +msgstr "Актуализация" + +msgid "Update, or publish" +msgstr "Актуализирайте или публикувайте" + +msgid "Publish your post" +msgstr "Публикувайте публикацията си" + +msgid "Written by {0}" +msgstr "Написано от {0}" + +msgid "All rights reserved." +msgstr "Всички права запазени." + +msgid "This article is under the {0} license." +msgstr "Тази статия се разпространява под {0} лиценз." + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "Едно харесване" +msgstr[1] "{0} харесвания" + +msgid "I don't like this anymore" +msgstr "Това вече не ми харесва" + +msgid "Add yours" +msgstr "Добави твоето" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "Едно Подсилване" +msgstr[1] "{0} Подсилвания" + +msgid "I don't want to boost this anymore" +msgstr "Не искам повече да го подсилвам" + +msgid "Boost" +msgstr "Подсилване" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "{0}Влезте {1}или {2}използвайте акаунта си в Fediverse{3}, за да взаимодействате с тази статия" + +msgid "Comments" +msgstr "Коментари" + +msgid "Your comment" +msgstr "Вашият коментар" + +msgid "Submit comment" +msgstr "Публикувайте коментар" + +msgid "No comments yet. Be the first to react!" +msgstr "Все още няма коментари. Бъдете първите!" + +msgid "Are you sure?" +msgstr "Сигурен ли си?" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "Тази статия все още е проект. Само вие и другите автори можете да я видите." + +msgid "Only you and other authors can edit this article." +msgstr "Само вие и другите автори можете да редактирате тази статия." + +msgid "Edit" +msgstr "Редакция" + +msgid "I'm from this instance" +msgstr "Аз съм от тази инстанция" + +msgid "Username, or email" +msgstr "Потребителско име или имейл" + +msgid "Log in" +msgstr "Влез" + +msgid "I'm from another instance" +msgstr "Аз съм от друга инстанция" + +msgid "Continue to your instance" +msgstr "Продължете към инстанцията си" + +msgid "Reset your password" +msgstr "Промяна на паролата ви" + +msgid "New password" +msgstr "Нова парола" + +msgid "Confirmation" +msgstr "Потвърждение" + +msgid "Update password" +msgstr "Обнови паролата" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "Изпратихме емайл с връзка за възстановяване на паролата ви, на адреса който ни дадохте." + +msgid "Send password reset link" +msgstr "Изпращане на връзка за възстановяване на парола" + +msgid "This token has expired" +msgstr "Този токен е изтекъл" + +msgid "Please start the process again by clicking here." +msgstr "Моля, стартирайте процеса отново като щракнете тук." + +msgid "New Blog" +msgstr "Нов блог" + +msgid "Create a blog" +msgstr "Създайте блог" + +msgid "Create blog" +msgstr "Създайте блог" + +msgid "Edit \"{}\"" +msgstr "редактирам \"{}\"" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "Можете да качвате изображения в галерията си и да ги използвате като икони на блога или банери." + +msgid "Upload images" +msgstr "Качване на изображения" + +msgid "Blog icon" +msgstr "Икона на блога" + +msgid "Blog banner" +msgstr "Банер в блога" + +msgid "Custom theme" +msgstr "Собствена тема" + +msgid "Update blog" +msgstr "Актуализация блог" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "Бъдете много внимателни, всяко действие, предприето тук не може да бъде отменено." + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "Сигурни ли сте, че искате да изтриете окончателно този блог?" + +msgid "Permanently delete this blog" +msgstr "Изтрийте Завинаги този блог" + +msgid "{}'s icon" +msgstr "{} икона" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "Има Един автор в този блог: " +msgstr[1] "Има {0} автора в този блог: " + +msgid "No posts to see here yet." +msgstr "Все още няма публикации." + +msgid "Nothing to see here yet." +msgstr "Тук още няма какво да се види." + +msgid "None" +msgstr "Няма" + +msgid "No description" +msgstr "Няма описание" + +msgid "Respond" +msgstr "Отговори" + +msgid "Delete this comment" +msgstr "Изтриване на този коментар" + +msgid "What is Plume?" +msgstr "Какво е Pluma?" + +msgid "Plume is a decentralized blogging engine." +msgstr "Pluma е децентрализиран двигател за блогове." + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "Авторите могат да управляват множество блогове, всеки като свой уникален уебсайт." + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "Статиите се виждат и на други Plume инстанции като можете да взаимодействате с тях директно и от други платформи като Mastodon." + +msgid "Read the detailed rules" +msgstr "Прочетете подробните правила" + +msgid "By {0}" +msgstr "От {0}" + +msgid "Draft" +msgstr "Проект" + +msgid "Search result(s) for \"{0}\"" +msgstr "Резултат(и) от търсенето за \"{0}\"" + +msgid "Search result(s)" +msgstr "Резултат(и) от търсенето" + +msgid "No results for your query" +msgstr "Няма резултати от вашата заявка" + +msgid "No more results for your query" +msgstr "Няма повече резултати за вашата заявка" + +msgid "Advanced search" +msgstr "Разширено търсене" + +msgid "Article title matching these words" +msgstr "Заглавие на статията, съответстващо на тези думи" + +msgid "Subtitle matching these words" +msgstr "Подзаглавие, съответстващо на тези думи" + +msgid "Content macthing these words" +msgstr "Съдържание, съвпадащо с тези думи" + +msgid "Body content" +msgstr "Съдържание на тялото" + +msgid "From this date" +msgstr "От тази дата" + +msgid "To this date" +msgstr "До тази дата" + +msgid "Containing these tags" +msgstr "Съдържащи тези етикети" + +msgid "Tags" +msgstr "Етикети" + +msgid "Posted on one of these instances" +msgstr "Публикувано в една от тези инстанции" + +msgid "Instance domain" +msgstr "Домейн на инстанцията" + +msgid "Posted by one of these authors" +msgstr "Публикувано от един от тези автори" + +msgid "Author(s)" +msgstr "Автор(и)" + +msgid "Posted on one of these blogs" +msgstr "Публикувано в един от тези блогове" + +msgid "Blog title" +msgstr "Заглавие на блога" + +msgid "Written in this language" +msgstr "Написано на този език" + +msgid "Language" +msgstr "Език" + +msgid "Published under this license" +msgstr "Публикувано под този лиценз" + +msgid "Article license" +msgstr "Лиценз на статията" + diff --git a/po/plume/ca.po b/po/plume/ca.po new file mode 100644 index 00000000000..c9dd54d4965 --- /dev/null +++ b/po/plume/ca.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Catalan\n" +"Language: ca_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: ca\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} han comentat el teu article." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} s'ha subscrit." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "A {0} li ha agradat el vostre article." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} us ha esmentat." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} ha impulsat el teu article." + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "El teu feed" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "Feed local" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "Feed federat" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "avatar de {0}" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "Pàgina anterior" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "Pàgina següent" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "Opcional" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "Per a crear un blog nou, heu d’iniciar una sessió" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "Ja existeix un blog amb el mateix nom." + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "S’ha creat el vostre blog correctament." + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "S’ha suprimit el vostre blog." + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "No tens permís per a esborrar aquest blog." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "No tens permís per a editar aquest blog." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "No pots usar aquest Mèdia com a icona del blog." + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "No pots usar aquest Mèdia com a capçalera del blog." + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "S’ha actualitzat la informació del vostre blog." + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "S’ha publicat el vostre comentari." + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "S’ha suprimit el vostre comentari." + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "El registre d'aquesta instància és tancat." + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "S'ha creat el teu compte. Ara cal iniciar sessió per a començar a usar-lo." + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "S'han desat les configuracions de l'instància." + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "{0} ha estat desbloquejat." + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "{0} ha estat bloquejat." + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "Bloquejos esborrats" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "Adreça de correu bloquejada" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "No pots canviar els teus propis drets." + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "No tens permís per a prendre aquesta acció." + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "Fet." + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "Per a agradar-te una publicació necessites iniciar sessió" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "S'ha esborrat el teu Mèdia." + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "No tens permís per a esborrar aquest Mèdia." + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "S'ha actualitzat el teu avatar." + +# src/routes/medias.rs:180 +msgid "You are not allowed to use this media." +msgstr "No tens permís per a usar aquest Mèdia." + +# src/routes/notifications.rs:29 +msgid "To see your notifications, you need to be logged in" +msgstr "Per a veure les teves notificacions necessites iniciar sessió" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "Aquesta entrada encara no està publicada." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "Per a escriure una nova entrada cal iniciar sessió" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "No ets un autor d'aquest blog." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "Apunt nou" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "Edita {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "No tens permís per a publicar en aquest blog." + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "S’ha actualitzat el vostre article." + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "S’ha desat el vostre article." + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "Article nou" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "No tens permís per a esborrar aquest article." + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "S’ha suprimit el vostre article." + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "Sembla que l'article que intentes esborrar no existeix. Potser ja no hi és?" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "No s'ha pogut obtenir informació sobre el teu compte. Si us plau, assegura't que el teu nom d'usuari és correcte." + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "Per a impulsar una entrada cal iniciar sessió" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "Ara estàs connectat." + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "Ara estàs desconnectat." + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "Reinicialització de contrasenya" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "Aquí està l'enllaç per a reiniciar la teva contrasenya: {0}" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "S’ha reinicialitzat la vostra contrasenya correctament." + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "Per a accedir al teu panell cal iniciar sessió" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "Ja no segueixes a {}." + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "Ara estàs seguint a {}." + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "Per a subscriure't a algú cal iniciar sessió" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "Per a editar el teu perfil cal iniciar sessió" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "El teu perfil s'ha actualitzat." + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "El teu compte s'ha esborrat." + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "No pots esborrar el compte d'algú altre." + +msgid "Create your account" +msgstr "Crea el teu compte" + +msgid "Create an account" +msgstr "Crear un compte" + +msgid "Email" +msgstr "Adreça electrònica" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "Disculpa, el registre d'aquesta instància és tancat. Pots trobar-ne un altre diferent." + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "Reviseu la vostra safata d’entrada." + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "Nom d’usuari" + +msgid "Password" +msgstr "Contrasenya" + +msgid "Password confirmation" +msgstr "Confirmació de la contrasenya" + +msgid "Media upload" +msgstr "Carregar Mèdia" + +msgid "Description" +msgstr "Descripció" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "Molt útil per a persones amb deficiències visuals aixó com informació sobre llicències" + +msgid "Content warning" +msgstr "Advertència sobre el contingut" + +msgid "Leave it empty, if none is needed" +msgstr "Deixa-ho buit si no és necessari cap" + +msgid "File" +msgstr "Fitxer" + +msgid "Send" +msgstr "Envia" + +msgid "Your media" +msgstr "Els teus Mèdia" + +msgid "Upload" +msgstr "Puja" + +msgid "You don't have any media yet." +msgstr "Encara no tens cap Mèdia." + +msgid "Content warning: {0}" +msgstr "Advertència sobre el contingut: {0}" + +msgid "Delete" +msgstr "Suprimeix" + +msgid "Details" +msgstr "Detalls" + +msgid "Media details" +msgstr "Detalls del fitxer multimèdia" + +msgid "Go back to the gallery" +msgstr "Torna a la galeria" + +msgid "Markdown syntax" +msgstr "Sintaxi Markdown" + +msgid "Copy it into your articles, to insert this media:" +msgstr "Copia-ho dins els teus articles, per a inserir aquest Mèdia:" + +msgid "Use as an avatar" +msgstr "Utilitza-ho com a avatar" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "Menú" + +msgid "Search" +msgstr "Cerca" + +msgid "Dashboard" +msgstr "Panell de control" + +msgid "Notifications" +msgstr "Notificacions" + +msgid "Log Out" +msgstr "Finalitza la sessió" + +msgid "My account" +msgstr "El meu compte" + +msgid "Log In" +msgstr "Inicia la sessió" + +msgid "Register" +msgstr "Registre" + +msgid "About this instance" +msgstr "Quant a aquesta instància" + +msgid "Privacy policy" +msgstr "Política de privadesa" + +msgid "Administration" +msgstr "Administració" + +msgid "Documentation" +msgstr "Documentació" + +msgid "Source code" +msgstr "Codi font" + +msgid "Matrix room" +msgstr "Sala Matrix" + +msgid "Admin" +msgstr "Admin" + +msgid "It is you" +msgstr "Ets tu" + +msgid "Edit your profile" +msgstr "Edita el teu perfil" + +msgid "Open on {0}" +msgstr "Obrir a {0}" + +msgid "Unsubscribe" +msgstr "Cancel·lar la subscripció" + +msgid "Subscribe" +msgstr "Subscriure’s" + +msgid "Follow {}" +msgstr "Segueix a {}" + +msgid "Log in to follow" +msgstr "Inicia sessió per seguir-lo" + +msgid "Enter your full username handle to follow" +msgstr "Introdueix el teu nom d'usuari complet per a seguir-lo" + +msgid "{0}'s subscribers" +msgstr "Subscriptors de {0}" + +msgid "Articles" +msgstr "Articles" + +msgid "Subscribers" +msgstr "Subscriptors" + +msgid "Subscriptions" +msgstr "Subscripcions" + +msgid "{0}'s subscriptions" +msgstr "Subscripcions de {0}" + +msgid "Your Dashboard" +msgstr "El teu panell de control" + +msgid "Your Blogs" +msgstr "Els vostres blogs" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "Encara no tens cap bloc. Crea el teu propi o pregunta per a unir-te a un." + +msgid "Start a new blog" +msgstr "Inicia un nou bloc" + +msgid "Your Drafts" +msgstr "Els teus esborranys" + +msgid "Go to your gallery" +msgstr "Anar a la teva galeria" + +msgid "Edit your account" +msgstr "Edita el teu compte" + +msgid "Your Profile" +msgstr "El vostre perfil" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "Per a canviar el teu avatar, puja'l a la teva galeria i desprès selecciona'l allà." + +msgid "Upload an avatar" +msgstr "Puja un avatar" + +msgid "Display name" +msgstr "Nom a mostrar" + +msgid "Summary" +msgstr "Resum" + +msgid "Theme" +msgstr "Tema" + +msgid "Default theme" +msgstr "Tema per defecte" + +msgid "Error while loading theme selector." +msgstr "S'ha produït un error al carregar el selector de temes." + +msgid "Never load blogs custom themes" +msgstr "No carregar mai els temes personalitzats dels blocs" + +msgid "Update account" +msgstr "Actualitza el compte" + +msgid "Danger zone" +msgstr "Zona perillosa" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "Vés amb compte: qualsevol acció presa aquí no es pot desfer." + +msgid "Delete your account" +msgstr "Elimina el teu compte" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "Ho sentim però com a admin, no pots abandonar la teva pròpia instància." + +msgid "Latest articles" +msgstr "Darrers articles" + +msgid "Atom feed" +msgstr "Font Atom" + +msgid "Recently boosted" +msgstr "Impulsat recentment" + +msgid "Articles tagged \"{0}\"" +msgstr "Articles amb l’etiqueta «{0}»" + +msgid "There are currently no articles with such a tag" +msgstr "No hi ha cap article amb aquesta etiqueta" + +msgid "The content you sent can't be processed." +msgstr "El contingut que has enviat no pot ser processat." + +msgid "Maybe it was too long." +msgstr "Potser era massa gran." + +msgid "Internal server error" +msgstr "Error intern del servidor" + +msgid "Something broke on our side." +msgstr "Alguna cosa nostre s'ha trencat." + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "Disculpa-n's. Si creus que això és un error, si us plau reporta-ho." + +msgid "Invalid CSRF token" +msgstr "Token CSRF invalid" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "Alguna cosa no és correcte amb el teu token CSRF. Assegura't que no bloqueges les galetes en el teu navegador i prova refrescant la pàgina. Si continues veient aquest missatge d'error, si us plau informa-ho." + +msgid "You are not authorized." +msgstr "No estàs autoritzat." + +msgid "Page not found" +msgstr "No s’ha trobat la pàgina" + +msgid "We couldn't find this page." +msgstr "No podem trobar aquesta pàgina." + +msgid "The link that led you here may be broken." +msgstr "L'enllaç que t'ha portat aquí podria estar trencat." + +msgid "Users" +msgstr "Usuaris" + +msgid "Configuration" +msgstr "Configuració" + +msgid "Instances" +msgstr "Instàncies" + +msgid "Email blocklist" +msgstr "Llista d'adreces de correu bloquejades" + +msgid "Grant admin rights" +msgstr "Dóna drets d'administrador" + +msgid "Revoke admin rights" +msgstr "Treu els drets d'administrador" + +msgid "Grant moderator rights" +msgstr "Dona els drets de moderador" + +msgid "Revoke moderator rights" +msgstr "Treu els drets de moderador" + +msgid "Ban" +msgstr "Prohibir" + +msgid "Run on selected users" +msgstr "Executar sobre els usuaris seleccionats" + +msgid "Moderator" +msgstr "Moderador" + +msgid "Moderation" +msgstr "Moderació" + +msgid "Home" +msgstr "Inici" + +msgid "Administration of {0}" +msgstr "Administració de {0}" + +msgid "Unblock" +msgstr "Desbloca" + +msgid "Block" +msgstr "Bloca" + +msgid "Name" +msgstr "Nom" + +msgid "Allow anyone to register here" +msgstr "Permetre a qualsevol registrar-se aquí" + +msgid "Short description" +msgstr "Descripció breu" + +msgid "Markdown syntax is supported" +msgstr "La sintaxi de Markdown és suportada" + +msgid "Long description" +msgstr "Descripció extensa" + +msgid "Default article license" +msgstr "Llicència per defecte dels articles" + +msgid "Save these settings" +msgstr "Desa aquests paràmetres" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "Si estàs navegant aquest lloc com a visitant cap dada sobre tu serà recollida." + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "Com a usuari registrat has de proporcionar un nom d'usuari (que no ha de ser el teu nom real), una adreça de correu funcional i una contrasenya per a poder ser capaç d'iniciar sessió, escriure articles i comentar-los. El contingut que enviïs es guarda fins que tu l'esborris." + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "Quan inicies sessió guardem dues galetes, una per a mantenir la sessió oberta i l l'altre per a evitar que d'altres persones actuïn en el teu nom. No guardem cap altre galeta." + +msgid "Blocklisted Emails" +msgstr "Llista de bloqueig d'adreces de correu" + +msgid "Email address" +msgstr "Adreça de correu electrònic" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "L'adreça de correu electrònic que desitges bloquejar. Per a bloquejar dominis pots usar la sintaxi global, per exemple '*@exemple.com' bloqueja totes les adreces de exemple.com" + +msgid "Note" +msgstr "Nota" + +msgid "Notify the user?" +msgstr "Notificar l'usuari?" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "Opcional, mostra un missatge al usuari quan intenta crear un compte amb aquesta adreça" + +msgid "Blocklisting notification" +msgstr "Notificacions de la llista de bloqueig" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "El missatge per a ser mostrat quan l'usuari intenta crear un compte amb aquesta adreça de correu" + +msgid "Add blocklisted address" +msgstr "Afegir adreça a la llista de bloquejos" + +msgid "There are no blocked emails on your instance" +msgstr "En la teva instància no hi ha adreces de correu bloquejades" + +msgid "Delete selected emails" +msgstr "Esborra les adreces de correu seleccionades" + +msgid "Email address:" +msgstr "Adreça de correu electrònic:" + +msgid "Blocklisted for:" +msgstr "Bloquejat per:" + +msgid "Will notify them on account creation with this message:" +msgstr "S'els notificarà en la creació del compte amb aquest missatge:" + +msgid "The user will be silently prevented from making an account" +msgstr "L’usuari es veurà impedit en silenci de crear un compte" + +msgid "Welcome to {}" +msgstr "Us donem la benvinguda a {}" + +msgid "View all" +msgstr "Mostra-ho tot" + +msgid "About {0}" +msgstr "Quant a {0}" + +msgid "Runs Plume {0}" +msgstr "Funciona amb el Plume {0}" + +msgid "Home to {0} people" +msgstr "Llar de {0} persones" + +msgid "Who wrote {0} articles" +msgstr "Les quals han escrit {0} articles" + +msgid "And are connected to {0} other instances" +msgstr "I estan connectats a {0} altres instàncies" + +msgid "Administred by" +msgstr "Administrat per" + +msgid "Interact with {}" +msgstr "Interacciona amb {}" + +msgid "Log in to interact" +msgstr "Inicia sessió per a interactuar" + +msgid "Enter your full username to interact" +msgstr "Introdueix el teu nom d'usuari complet per a interactuar" + +msgid "Publish" +msgstr "Publica" + +msgid "Classic editor (any changes will be lost)" +msgstr "Editor clàssic (es perdera qualsevol canvi)" + +msgid "Title" +msgstr "Títol" + +msgid "Subtitle" +msgstr "Subtítol" + +msgid "Content" +msgstr "Contingut" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "Pots pujar Mèdia a la teva galeria i desprès copiar el codi Markdown dels teus articles per a inserir-los." + +msgid "Upload media" +msgstr "Pujar Mèdia" + +msgid "Tags, separated by commas" +msgstr "Etiquetes, separades per comes" + +msgid "License" +msgstr "Llicència" + +msgid "Illustration" +msgstr "Iŀlustració" + +msgid "This is a draft, don't publish it yet." +msgstr "Això és un esborrany, no ho publiquis encara." + +msgid "Update" +msgstr "Actualitza" + +msgid "Update, or publish" +msgstr "Actualitza o publica" + +msgid "Publish your post" +msgstr "Publica l'entrada" + +msgid "Written by {0}" +msgstr "Escrit per {0}" + +msgid "All rights reserved." +msgstr "Tots els drets reservats." + +msgid "This article is under the {0} license." +msgstr "Aquest article està sota la llicència {0}." + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "Un favorit" +msgstr[1] "{0} favorits" + +msgid "I don't like this anymore" +msgstr "Això ja no m’agrada" + +msgid "Add yours" +msgstr "Afegeix el teu" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "Un impuls" +msgstr[1] "{0} impulsos" + +msgid "I don't want to boost this anymore" +msgstr "Ja no vull impulsar més això" + +msgid "Boost" +msgstr "Impuls" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "{0}Inicia sessió{1}, o {2}usa el teu compte del Fedivers{3} per a interactuar amb aquest article" + +msgid "Comments" +msgstr "Comentaris" + +msgid "Your comment" +msgstr "El vostre comentari" + +msgid "Submit comment" +msgstr "Envia el comentari" + +msgid "No comments yet. Be the first to react!" +msgstr "Encara sense comentaris. Sigues el primer a reaccionar!" + +msgid "Are you sure?" +msgstr "N’esteu segur?" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "Aquest article és encara un esborrany. Només tu i altres autors podeu veure'l." + +msgid "Only you and other authors can edit this article." +msgstr "Només tu i altres autors podeu editar aquest article." + +msgid "Edit" +msgstr "Edita" + +msgid "I'm from this instance" +msgstr "Sóc d'aquesta instància" + +msgid "Username, or email" +msgstr "Nom d’usuari o adreça electrònica" + +msgid "Log in" +msgstr "Inicia una sessió" + +msgid "I'm from another instance" +msgstr "Sóc d'un altre instància" + +msgid "Continue to your instance" +msgstr "Continua a la teva instància" + +msgid "Reset your password" +msgstr "Reinicialitza la contrasenya" + +msgid "New password" +msgstr "Contrasenya nova" + +msgid "Confirmation" +msgstr "Confirmació" + +msgid "Update password" +msgstr "Actualitza la contrasenya" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "Hem enviat un correu a l'adreça que ens vas donar, amb un enllaç per a reiniciar la teva contrasenya." + +msgid "Send password reset link" +msgstr "Envia l'enllaç per a reiniciar la contrasenya" + +msgid "This token has expired" +msgstr "Aquest token ha caducat" + +msgid "Please start the process again by clicking here." +msgstr "Si us plau inicia el procés clicant aquí." + +msgid "New Blog" +msgstr "Blog nou" + +msgid "Create a blog" +msgstr "Crea un blog" + +msgid "Create blog" +msgstr "Crea un blog" + +msgid "Edit \"{}\"" +msgstr "Edita «{}»" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "Pots pujar imatges a la teva galeria per a usar-les com a icones o capçaleres del bloc." + +msgid "Upload images" +msgstr "Pujar imatges" + +msgid "Blog icon" +msgstr "Icona del blog" + +msgid "Blog banner" +msgstr "Bàner del blog" + +msgid "Custom theme" +msgstr "Tema personalitzat" + +msgid "Update blog" +msgstr "Actualitza el blog" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "Aneu amb compte: les accions que són ací no es poden desfer." + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "Estàs segur que vols esborrar permanentment aquest bloc?" + +msgid "Permanently delete this blog" +msgstr "Suprimeix permanentment aquest blog" + +msgid "{}'s icon" +msgstr "Icona per a {}" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "Hi ha 1 autor en aquest blog: " +msgstr[1] "Hi ha {0} autors en aquest blog: " + +msgid "No posts to see here yet." +msgstr "Encara no hi ha cap apunt." + +msgid "Nothing to see here yet." +msgstr "Encara res a veure aquí." + +msgid "None" +msgstr "Cap" + +msgid "No description" +msgstr "Cap descripció" + +msgid "Respond" +msgstr "Respondre" + +msgid "Delete this comment" +msgstr "Suprimeix aquest comentari" + +msgid "What is Plume?" +msgstr "Què és el Plume?" + +msgid "Plume is a decentralized blogging engine." +msgstr "Plume és un motor de blocs descentralitzats." + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "Els autors poden gestionar diversos blocs, cadascun amb la seva pròpia pàgina." + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "Els articles son visibles en altres instàncies Plume i pots interactuar directament amb ells des d'altres plataformes com ara Mastodon." + +msgid "Read the detailed rules" +msgstr "Llegeix les normes detallades" + +msgid "By {0}" +msgstr "Per {0}" + +msgid "Draft" +msgstr "Esborrany" + +msgid "Search result(s) for \"{0}\"" +msgstr "Resultat(s) de la cerca per a \"{0}\"" + +msgid "Search result(s)" +msgstr "Resultat(s) de la cerca" + +msgid "No results for your query" +msgstr "La teva consulta no té resultats" + +msgid "No more results for your query" +msgstr "No hi ha més resultats per a la teva consulta" + +msgid "Advanced search" +msgstr "Cerca avançada" + +msgid "Article title matching these words" +msgstr "Títol d'article que coincideix amb aquestes paraules" + +msgid "Subtitle matching these words" +msgstr "Subtítol que coincideix amb aquestes paraules" + +msgid "Content macthing these words" +msgstr "Contingut que coincideix amb aquestes paraules" + +msgid "Body content" +msgstr "Contingut del cos" + +msgid "From this date" +msgstr "A partir d’aquesta data" + +msgid "To this date" +msgstr "Fins a aquesta data" + +msgid "Containing these tags" +msgstr "Que conté aquestes etiquetes" + +msgid "Tags" +msgstr "Etiquetes" + +msgid "Posted on one of these instances" +msgstr "Publicat en una d'aquestes instàncies" + +msgid "Instance domain" +msgstr "Domini de l'instància" + +msgid "Posted by one of these authors" +msgstr "Publicat per un d'aquests autors" + +msgid "Author(s)" +msgstr "Autor(s)" + +msgid "Posted on one of these blogs" +msgstr "Publicat en un d'aquests blocs" + +msgid "Blog title" +msgstr "Títol del blog" + +msgid "Written in this language" +msgstr "Escrit en aquesta llengua" + +msgid "Language" +msgstr "Llengua" + +msgid "Published under this license" +msgstr "Publicat segons aquesta llicència" + +msgid "Article license" +msgstr "Llicència del article" + diff --git a/po/plume/cs.po b/po/plume/cs.po new file mode 100644 index 00000000000..d0aa40d5724 --- /dev/null +++ b/po/plume/cs.po @@ -0,0 +1,1040 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Czech\n" +"Language: cs_CZ\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: cs\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} komentoval/a váš článek." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} vás odebírá." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} si oblíbil/a váš článek." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} vás zmínil/a." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} povýšil/a váš článek." + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "Vaše zdroje" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "Místní zdroje" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "Federované zdroje" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "Avatar uživatele {0}" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "Předchozí stránka" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "Následující strana" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "Volitelné" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "Pro vytvoření nového blogu musíte být přihlášeni" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "Blog s rovnakým názvem již existuje." + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "Váš blog byl úspěšně vytvořen!" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "Váš blog byl smazán." + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "Nemáte oprávnění zmazat tento blog." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "Nemáte oprávnění upravovat tento blog." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "Toto médium nelze použít jako ikonu blogu." + +# src/routes/blogs.rs:298 +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:332 +msgid "Your blog information have been updated." +msgstr "Údaje o vašem blogu byly aktualizovány." + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "Váš komentář byl zveřejněn." + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "Váš komentář byl odstraněn." + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "Registrace jsou na téhle instanci uzavřeny." + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "Váš účet byl vytvořen. Nyní se stačí jenom přihlásit, než ho budete moci používat." + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "Nastavení instance bylo uloženo." + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "{} byl/a odblokován/a." + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "{} byl/a zablokován/a." + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "Blokování odstraněno" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "Email je již zablokován" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "Email zablokován" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "Nemůžete změnit vlastní oprávnění." + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "Nemáte oprávnění k provedení tohoto úkonu." + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "Hotovo." + +# src/routes/likes.rs:58 +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:153 +msgid "Your media have been deleted." +msgstr "Vaše média byla smazána." + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "Nemáte oprávnění k smazání tohoto média." + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "Váš avatar byl aktualizován." + +# src/routes/medias.rs:180 +msgid "You are not allowed to use this media." +msgstr "Nemáte oprávnění k použití tohoto média." + +# 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" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "Tento příspěvek ještě není zveřejněn." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "K napsaní nového příspěvku musíte být přihlášeni" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "Nejste autorem tohto blogu." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "Nový příspěvek" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "Upravit {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "Nemáte oprávnění zveřejňovat na tomto blogu." + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "Váš článek byl upraven." + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "Váš článek byl uložen." + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "Nový článek" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "Nemáte oprávnění zmazat tento článek." + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "Váš článek byl smazán." + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "Zdá se, že článek, který jste se snažili smazat, neexistuje, možná je již pryč?" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "Nemohli jsme zjistit dostatečné množství informací ohledne vašeho účtu. Prosím ověřte si, že vaše předzývka je správná." + +# src/routes/reshares.rs:58 +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" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "Nyní jste připojeni." + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "Nyní jste odhlášeni." + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "Obnovit heslo" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "Zde je odkaz na obnovení vášho hesla: {0}" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "Vaše heslo bylo úspěšně obnoveno." + +# src/routes/user.rs:87 +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:109 +msgid "You are no longer following {}." +msgstr "Již nenásledujete {}." + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "Teď již nenásledujete {}." + +# src/routes/user.rs:203 +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" + +# src/routes/user.rs:323 +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:369 +msgid "Your profile has been updated." +msgstr "Váš profil byl upraven." + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "Váš účet byl odstraněn." + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "Nemůžete smazat účet někoho jiného." + +msgid "Create your account" +msgstr "Vytvořit váš účet" + +msgid "Create an account" +msgstr "Vytvořit účet" + +msgid "Email" +msgstr "Email" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "Omlouváme se, ale registrace je uzavřena na této konkrétní instanci. Můžete však najít jinou." + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "Zkontrolujte svou příchozí poštu!" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "Uživatelské jméno" + +msgid "Password" +msgstr "Heslo" + +msgid "Password confirmation" +msgstr "Potvrzení hesla" + +msgid "Media upload" +msgstr "Nahrávaní médií" + +msgid "Description" +msgstr "Popis" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "Užitečné pro zrakově postižené lidi a také pro informace o licencování" + +msgid "Content warning" +msgstr "Varování o obsahu" + +msgid "Leave it empty, if none is needed" +msgstr "Ponechte prázdne, pokud žádné není potřeba" + +msgid "File" +msgstr "Soubor" + +msgid "Send" +msgstr "Odeslat" + +msgid "Your media" +msgstr "Vaše média" + +msgid "Upload" +msgstr "Nahrát" + +msgid "You don't have any media yet." +msgstr "Zatím nemáte nahrané žádné média." + +msgid "Content warning: {0}" +msgstr "Upozornení na obsah: {0}" + +msgid "Delete" +msgstr "Smazat" + +msgid "Details" +msgstr "Podrobnosti" + +msgid "Media details" +msgstr "Podrobnosti média" + +msgid "Go back to the gallery" +msgstr "Přejít zpět do galerie" + +msgid "Markdown syntax" +msgstr "Markdown syntaxe" + +msgid "Copy it into your articles, to insert this media:" +msgstr "Pro vložení tohoto média zkopírujte tento kód do vašich článků:" + +msgid "Use as an avatar" +msgstr "Použít jak avatar" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "Nabídka" + +msgid "Search" +msgstr "Hledat" + +msgid "Dashboard" +msgstr "Nástěnka" + +msgid "Notifications" +msgstr "Notifikace" + +msgid "Log Out" +msgstr "Odhlásit se" + +msgid "My account" +msgstr "Můj účet" + +msgid "Log In" +msgstr "Přihlásit se" + +msgid "Register" +msgstr "Vytvořit účet" + +msgid "About this instance" +msgstr "O této instanci" + +msgid "Privacy policy" +msgstr "Zásady soukromí" + +msgid "Administration" +msgstr "Správa" + +msgid "Documentation" +msgstr "Dokumentace" + +msgid "Source code" +msgstr "Zdrojový kód" + +msgid "Matrix room" +msgstr "Matrix místnost" + +msgid "Admin" +msgstr "Správce" + +msgid "It is you" +msgstr "To jste vy" + +msgid "Edit your profile" +msgstr "Upravit profil" + +msgid "Open on {0}" +msgstr "Otevřít na {0}" + +msgid "Unsubscribe" +msgstr "Odhlásit se z odběru" + +msgid "Subscribe" +msgstr "Přihlásit se k odběru" + +msgid "Follow {}" +msgstr "Následovat {}" + +msgid "Log in to follow" +msgstr "Pro následování se přihlášte" + +msgid "Enter your full username handle to follow" +msgstr "Pro následovaní zadejte své úplné uživatelské jméno" + +msgid "{0}'s subscribers" +msgstr "Odběratelé uživatele {0}" + +msgid "Articles" +msgstr "Články" + +msgid "Subscribers" +msgstr "Odběratelé" + +msgid "Subscriptions" +msgstr "Odběry" + +msgid "{0}'s subscriptions" +msgstr "Odběry uživatele {0}" + +msgid "Your Dashboard" +msgstr "Vaše nástěnka" + +msgid "Your Blogs" +msgstr "Vaše Blogy" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "Zatím nemáte žádný blog. Vytvořte si vlastní, nebo požádejte v nejakém o členství." + +msgid "Start a new blog" +msgstr "Začít nový blog" + +msgid "Your Drafts" +msgstr "Váše návrhy" + +msgid "Go to your gallery" +msgstr "Přejít do galerie" + +msgid "Edit your account" +msgstr "Upravit váš účet" + +msgid "Your Profile" +msgstr "Váš profil" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "Chcete-li změnit svůj avatar, nahrejte ho do své galérie a pak ho odtud zvolte." + +msgid "Upload an avatar" +msgstr "Nahrát avatara" + +msgid "Display name" +msgstr "Zobrazované jméno" + +msgid "Summary" +msgstr "Souhrn" + +msgid "Theme" +msgstr "Motiv" + +msgid "Default theme" +msgstr "Výchozí motiv" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "Aktualizovat účet" + +msgid "Danger zone" +msgstr "Nebezpečná zóna" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "Buďte velmi opatrný/á, jakákoliv zde provedená akce nemůže být zrušena." + +msgid "Delete your account" +msgstr "Smazat váš účet" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "Omlouváme se, ale jako administrátor nemůžete opustit svou vlastní instanci." + +msgid "Latest articles" +msgstr "Nejposlednejší články" + +msgid "Atom feed" +msgstr "Atom kanál" + +msgid "Recently boosted" +msgstr "Nedávno podpořené" + +msgid "Articles tagged \"{0}\"" +msgstr "Články pod štítkem \"{0}\"" + +msgid "There are currently no articles with such a tag" +msgstr "Zatím tu nejsou žádné články s takovým štítkem" + +msgid "The content you sent can't be processed." +msgstr "Obsah, který jste poslali, nelze zpracovat." + +msgid "Maybe it was too long." +msgstr "Možná to bylo příliš dlouhé." + +msgid "Internal server error" +msgstr "Vnitřní chyba serveru" + +msgid "Something broke on our side." +msgstr "Neco se pokazilo na naší strane." + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "Omlouváme se. Pokud si myslíte, že jde o chybu, prosím nahlašte ji." + +msgid "Invalid CSRF token" +msgstr "Neplatný CSRF token" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "S vaším tokenem CSRF něco není v pořádku. Ujistěte se, že máte v prohlížeči povolené cookies a zkuste obnovit stránku. Pokud tuto chybovou zprávu budete nadále vidět, prosím nahlašte ji." + +msgid "You are not authorized." +msgstr "Nemáte oprávnění." + +msgid "Page not found" +msgstr "Stránka nenalezena" + +msgid "We couldn't find this page." +msgstr "Tu stránku jsme nemohli najít." + +msgid "The link that led you here may be broken." +msgstr "Odkaz, který vás sem přivedl je asi porušen." + +msgid "Users" +msgstr "Uživatelé" + +msgid "Configuration" +msgstr "Nastavení" + +msgid "Instances" +msgstr "Instance" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "Zakázat" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "Moderátor" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "Domů" + +msgid "Administration of {0}" +msgstr "Správa {0}" + +msgid "Unblock" +msgstr "Odblokovat" + +msgid "Block" +msgstr "Blokovat" + +msgid "Name" +msgstr "Pojmenování" + +msgid "Allow anyone to register here" +msgstr "Povolit komukoli se zde zaregistrovat" + +msgid "Short description" +msgstr "Stručný popis" + +msgid "Markdown syntax is supported" +msgstr "Markdown syntaxe je podporována" + +msgid "Long description" +msgstr "Detailní popis" + +msgid "Default article license" +msgstr "Výchozí licence článků" + +msgid "Save these settings" +msgstr "Uložit tyhle nastavení" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "Pokud si tuto stránku prohlížete jako návštěvník, žádné údaje o vás nejsou shromažďovány." + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "Jako registrovaný uživatel musíte poskytnout uživatelské jméno (které nemusí být vaším skutečným jménem), funkční e-mailovou adresu a heslo, aby jste se mohl přihlásit, psát články a komentář." + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "Když se přihlásíte, ukládáme dvě cookies, jedno, aby bylo možné udržet vaše zasedání otevřené, druhé, aby se zabránilo jiným lidem jednat ve vašem jméně. Žádné další cookies neukládáme." + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "Smazat vybrané emaily" + +msgid "Email address:" +msgstr "Emailová adresa:" + +msgid "Blocklisted for:" +msgstr "Blokováno pro:" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "Vítejte na {}" + +msgid "View all" +msgstr "Zobrazit všechny" + +msgid "About {0}" +msgstr "O {0}" + +msgid "Runs Plume {0}" +msgstr "Beží na Plume {0}" + +msgid "Home to {0} people" +msgstr "Domov pro {0} lidí" + +msgid "Who wrote {0} articles" +msgstr "Co napsali {0} článků" + +msgid "And are connected to {0} other instances" +msgstr "A jsou napojeni na {0} dalších instancí" + +msgid "Administred by" +msgstr "Správcem je" + +msgid "Interact with {}" +msgstr "Interagujte s {}" + +msgid "Log in to interact" +msgstr "Pro interakci se přihlaste" + +msgid "Enter your full username to interact" +msgstr "Pro interakci zadejte své úplné uživatelské jméno" + +msgid "Publish" +msgstr "Zveřejnit" + +msgid "Classic editor (any changes will be lost)" +msgstr "Klasický editor (jakékoli změny budou ztraceny)" + +msgid "Title" +msgstr "Nadpis" + +msgid "Subtitle" +msgstr "Podtitul" + +msgid "Content" +msgstr "Obsah" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "Můžete nahrát média do své galerie, a pak zkopírovat jejich kód Markdown do vašich článků, pro vložení." + +msgid "Upload media" +msgstr "Nahrát média" + +msgid "Tags, separated by commas" +msgstr "Štítky, oddělené čárkami" + +msgid "License" +msgstr "Licence" + +msgid "Illustration" +msgstr "Ilustrace" + +msgid "This is a draft, don't publish it yet." +msgstr "Tohle je koncept, ještě ho nezveřejňovat." + +msgid "Update" +msgstr "Aktualizovat" + +msgid "Update, or publish" +msgstr "Aktualizovat, nebo zveřejnit" + +msgid "Publish your post" +msgstr "Zveřejnit váš příspěvek" + +msgid "Written by {0}" +msgstr "Napsal/a {0}" + +msgid "All rights reserved." +msgstr "Všechna práva vyhrazena." + +msgid "This article is under the {0} license." +msgstr "Tento článek je pod {0} licencí." + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "Jedno oblíbení" +msgstr[1] "{0} oblíbili" +msgstr[2] "{0} oblíbili" +msgstr[3] "{0} oblíbili" + +msgid "I don't like this anymore" +msgstr "Tohle se mi už nelíbí" + +msgid "Add yours" +msgstr "Přidejte své" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "Jedno boostnoutí" +msgstr[1] "{0} boostnoutí" +msgstr[2] "{0} boostnoutí" +msgstr[3] "{0} boostnoutí" + +msgid "I don't want to boost this anymore" +msgstr "Už to nechci dále boostovat" + +msgid "Boost" +msgstr "Boostnout" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "{0}Přihlasit se{1}, nebo {2}použít váš Fediverse účet{3} k interakci s tímto článkem" + +msgid "Comments" +msgstr "Komentáře" + +msgid "Your comment" +msgstr "Váš komentář" + +msgid "Submit comment" +msgstr "Odeslat komentář" + +msgid "No comments yet. Be the first to react!" +msgstr "Zatím bez komentáře. Buďte první, kdo zareaguje!" + +msgid "Are you sure?" +msgstr "Jste si jisti?" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "Tento článek je stále konceptem. Jenom vy, a další autoři ho mohou vidět." + +msgid "Only you and other authors can edit this article." +msgstr "Jenom vy, a další autoři mohou upravovat tento článek." + +msgid "Edit" +msgstr "Upravit" + +msgid "I'm from this instance" +msgstr "Jsem z téhle instance" + +msgid "Username, or email" +msgstr "Uživatelské jméno, nebo email" + +msgid "Log in" +msgstr "Přihlásit se" + +msgid "I'm from another instance" +msgstr "Jsem z jiné instance" + +msgid "Continue to your instance" +msgstr "Pokračujte na vaši instanci" + +msgid "Reset your password" +msgstr "Obnovte své heslo" + +msgid "New password" +msgstr "Nové heslo" + +msgid "Confirmation" +msgstr "Potvrzení" + +msgid "Update password" +msgstr "Aktualizovat heslo" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "Zaslali jsme email na adresu, kterou jste nám dodali, s odkazem na obnovu vášho hesla." + +msgid "Send password reset link" +msgstr "Poslat odkaz na obnovení hesla" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "Nový Blog" + +msgid "Create a blog" +msgstr "Vytvořit blog" + +msgid "Create blog" +msgstr "Vytvořit blog" + +msgid "Edit \"{}\"" +msgstr "Upravit \"{}\"" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "Můžete nahrát obrázky do své galerie, aby je šlo použít jako ikony blogu, nebo bannery." + +msgid "Upload images" +msgstr "Nahrát obrázky" + +msgid "Blog icon" +msgstr "Ikonka blogu" + +msgid "Blog banner" +msgstr "Blog banner" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "Aktualizovat blog" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "Buďte velmi opatrný/á, jakákoliv zde provedená akce nemůže být vrácena." + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "Trvale smazat tento blog" + +msgid "{}'s icon" +msgstr "Ikona pro {0}" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "Tento blog má jednoho autora: " +msgstr[1] "Na tomto blogu jsou {0} autoři: " +msgstr[2] "Tento blog má {0} autorů: " +msgstr[3] "Tento blog má {0} autorů: " + +msgid "No posts to see here yet." +msgstr "Ještě zde nejsou k vidění žádné příspěvky." + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "Žádné" + +msgid "No description" +msgstr "Bez popisu" + +msgid "Respond" +msgstr "Odpovědět" + +msgid "Delete this comment" +msgstr "Odstranit tento komentář" + +msgid "What is Plume?" +msgstr "Co je Plume?" + +msgid "Plume is a decentralized blogging engine." +msgstr "Plume je decentralizovaný blogování systém." + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "Autoři mohou spravovat vícero blogů, každý jako svou vlastní stránku." + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "Články jsou viditelné také na ostatních Plume instancích, a můžete s nimi narábět přímo i v rámci jiných platforem, jako je Mastodon." + +msgid "Read the detailed rules" +msgstr "Přečtěte si podrobná pravidla" + +msgid "By {0}" +msgstr "Od {0}" + +msgid "Draft" +msgstr "Koncept" + +msgid "Search result(s) for \"{0}\"" +msgstr "Výsledky hledání pro \"{0}\"" + +msgid "Search result(s)" +msgstr "Výsledky hledání" + +msgid "No results for your query" +msgstr "Žádné výsledky pro váš dotaz nenalzeny" + +msgid "No more results for your query" +msgstr "Žádné další výsledeky pro váše zadaní" + +msgid "Advanced search" +msgstr "Pokročilé vyhledávání" + +msgid "Article title matching these words" +msgstr "Nadpis článku odpovídající těmto slovům" + +msgid "Subtitle matching these words" +msgstr "Podnadpis odpovídající těmto slovům" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "Tělo článku" + +msgid "From this date" +msgstr "Od tohoto data" + +msgid "To this date" +msgstr "Do tohoto data" + +msgid "Containing these tags" +msgstr "Obsahuje tyto štítky" + +msgid "Tags" +msgstr "Tagy" + +msgid "Posted on one of these instances" +msgstr "Zveřejněno na jedné z těchto instancí" + +msgid "Instance domain" +msgstr "Doména instance" + +msgid "Posted by one of these authors" +msgstr "Zveřejněno na jedném z těchto autorů" + +msgid "Author(s)" +msgstr "Autoři" + +msgid "Posted on one of these blogs" +msgstr "Zveřejněno na jedném z těchto blogů" + +msgid "Blog title" +msgstr "Název blogu" + +msgid "Written in this language" +msgstr "Napsané v tomto jazyce" + +msgid "Language" +msgstr "Jazyk" + +msgid "Published under this license" +msgstr "Zveřejněn pod touto licenci" + +msgid "Article license" +msgstr "Licence článku" + diff --git a/po/plume/cy.po b/po/plume/cy.po new file mode 100644 index 00000000000..4aa6563a724 --- /dev/null +++ b/po/plume/cy.po @@ -0,0 +1,1104 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2019-04-06 15:05\n" +"Last-Translator: AnaGelez \n" +"Language-Team: Welsh\n" +"Language: cy_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=(n == 0) ? 0 : ((n == 1) ? 1 : ((n == 2) ? " +"2 : ((n == 3) ? 3 : ((n == 6) ? 4 : 5))));\n" +"X-Generator: crowdin.com\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Language: cy\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:68 +msgid "{0} commented on your article." +msgstr "" + +# src/template_utils.rs:69 +msgid "{0} is subscribed to you." +msgstr "" + +# src/template_utils.rs:70 +msgid "{0} liked your article." +msgstr "" + +# src/template_utils.rs:71 +msgid "{0} mentioned you." +msgstr "" + +# src/template_utils.rs:72 +msgid "{0} boosted your article." +msgstr "" + +msgid "Your feed" +msgstr "" + +msgid "Local feed" +msgstr "" + +msgid "Federated feed" +msgstr "" + +# src/template_utils.rs:108 +msgid "{0}'s avatar" +msgstr "" + +# src/template_utils.rs:198 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:209 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:220 +msgid "Optional" +msgstr "" + +# src/routes/blogs.rs:70 +msgid "To create a new blog, you need to be logged in" +msgstr "" + +# src/routes/blogs.rs:102 +msgid "A blog with the same name already exists." +msgstr "" + +# src/routes/blogs.rs:140 +msgid "Your blog was successfully created!" +msgstr "" + +# src/routes/blogs.rs:160 +msgid "Your blog was deleted." +msgstr "" + +# src/routes/blogs.rs:169 +msgid "You are not allowed to delete this blog." +msgstr "" + +# src/routes/blogs.rs:214 +msgid "You are not allowed to edit this blog." +msgstr "" + +# src/routes/blogs.rs:253 +msgid "You can't use this media as a blog icon." +msgstr "" + +# src/routes/blogs.rs:271 +msgid "You can't use this media as a blog banner." +msgstr "" + +# src/routes/blogs.rs:327 +msgid "Your blog information have been updated." +msgstr "" + +# src/routes/comments.rs:97 +msgid "Your comment has been posted." +msgstr "" + +# src/routes/comments.rs:172 +msgid "Your comment has been deleted." +msgstr "" + +# src/routes/user.rs:526 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:132 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:133 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/user.rs:549 +msgid "" +"Your account has been created. Now you just need to log in, before you can " +"use it." +msgstr "" + +# src/routes/instance.rs:120 +msgid "Instance settings have been saved." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:154 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:218 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:223 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:314 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:325 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:362 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:47 +msgid "To like a post, you need to be logged in" +msgstr "" + +# src/routes/medias.rs:145 +msgid "Your media have been deleted." +msgstr "" + +# src/routes/medias.rs:150 +msgid "You are not allowed to delete this media." +msgstr "" + +# src/routes/medias.rs:167 +msgid "Your avatar has been updated." +msgstr "" + +# src/routes/medias.rs:172 +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 "" + +# src/routes/posts.rs:92 +msgid "This post isn't published yet." +msgstr "" + +# src/routes/posts.rs:120 +msgid "To write a new post, you need to be logged in" +msgstr "" + +# src/routes/posts.rs:138 +msgid "You are not an author of this blog." +msgstr "" + +# src/routes/posts.rs:145 +msgid "New post" +msgstr "" + +# src/routes/posts.rs:190 +msgid "Edit {0}" +msgstr "" + +# src/routes/posts.rs:263 +msgid "You are not allowed to publish on this blog." +msgstr "" + +# src/routes/posts.rs:355 +msgid "Your article has been updated." +msgstr "" + +# src/routes/posts.rs:542 +msgid "Your article has been saved." +msgstr "" + +msgid "New article" +msgstr "" + +# src/routes/posts.rs:582 +msgid "You are not allowed to delete this article." +msgstr "" + +# src/routes/posts.rs:607 +msgid "Your article has been deleted." +msgstr "" + +# src/routes/posts.rs:612 +msgid "" +"It looks like the article you tried to delete doesn't exist. Maybe it is " +"already gone?" +msgstr "" + +# src/routes/posts.rs:652 +msgid "" +"Couldn't obtain enough information about your account. Please make sure your " +"username is correct." +msgstr "" + +# src/routes/reshares.rs:47 +msgid "To reshare a post, you need to be logged in" +msgstr "" + +# src/routes/session.rs:87 +msgid "You are now connected." +msgstr "" + +# src/routes/session.rs:108 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:181 +msgid "Password reset" +msgstr "" + +# src/routes/session.rs:182 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:259 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:148 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:163 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:180 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:187 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:287 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:409 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:436 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:442 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "" + +msgid "Create an account" +msgstr "" + +# src/template_utils.rs:217 +msgid "Email" +msgstr "" + +msgid "Email confirmation" +msgstr "" + +msgid "" +"Apologies, but registrations are closed on this particular instance. You " +"can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "" +"We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +# src/template_utils.rs:217 +msgid "Username" +msgstr "" + +# src/template_utils.rs:217 +msgid "Password" +msgstr "" + +# src/template_utils.rs:217 +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +# src/template_utils.rs:217 +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Your media" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "" + +msgid "Menu" +msgstr "" + +msgid "Search" +msgstr "" + +msgid "Dashboard" +msgstr "" + +msgid "Notifications" +msgstr "" + +msgid "Log Out" +msgstr "" + +msgid "My account" +msgstr "" + +msgid "Log In" +msgstr "" + +msgid "Register" +msgstr "" + +msgid "About this instance" +msgstr "" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "" + +msgid "It is you" +msgstr "" + +msgid "Edit your profile" +msgstr "" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "" + +msgid "Follow {}" +msgstr "" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "" + +msgid "Subscribers" +msgstr "" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "" + +msgid "Your Drafts" +msgstr "" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "" + +msgid "Your Profile" +msgstr "" + +msgid "" +"To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +# src/template_utils.rs:217 +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "" +"Something is wrong with your CSRF token. Make sure cookies are enabled in " +"you browser, and try reloading this page. If you continue to see this error " +"message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "" + +msgid "Configuration" +msgstr "" + +msgid "Instances" +msgstr "" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "" + +msgid "Unblock" +msgstr "" + +msgid "Block" +msgstr "" + +# src/template_utils.rs:217 +msgid "Name" +msgstr "" + +msgid "Allow anyone to register here" +msgstr "" + +msgid "Short description" +msgstr "" + +msgid "Markdown syntax is supported" +msgstr "" + +msgid "Long description" +msgstr "" + +# src/template_utils.rs:217 +msgid "Default article license" +msgstr "" + +msgid "Save these settings" +msgstr "" + +msgid "" +"If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "" +"As a registered user, you have to provide your username (which does not have " +"to be your real name), your functional email address and a password, in " +"order to be able to log in, write articles and comment. The content you " +"submit is stored until you delete it." +msgstr "" + +msgid "" +"When you log in, we store two cookies, one to keep your session open, the " +"second to prevent other people to act on your behalf. We don't store any " +"other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "" +"The email address you wish to block. In order to block domains, you can use " +"globbing syntax, for example '*@example.com' blocks all addresses from " +"example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "" +"Optional, shows a message to the user when they attempt to create an account " +"with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "" +"The message to be shown when the user attempts to create an account with " +"this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +msgid "Administred by" +msgstr "" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "" + +# src/template_utils.rs:217 +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "" + +msgid "" +"You can upload media to your gallery, and then copy their Markdown code into " +"your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +# src/template_utils.rs:217 +msgid "Tags, separated by commas" +msgstr "" + +# src/template_utils.rs:217 +msgid "License" +msgstr "" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" +msgstr[4] "" +msgstr[5] "" + +msgid "I don't like this anymore" +msgstr "" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" +msgstr[4] "" +msgstr[5] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "" + +msgid "" +"{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this " +"article" +msgstr "" + +msgid "Comments" +msgstr "" + +msgid "Your comment" +msgstr "" + +msgid "Submit comment" +msgstr "" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "I'm from this instance" +msgstr "" + +# src/template_utils.rs:217 +msgid "Username, or email" +msgstr "" + +msgid "Log in" +msgstr "" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +# src/template_utils.rs:217 +msgid "New password" +msgstr "" + +# src/template_utils.rs:217 +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "" +"We sent a mail to the address you gave us, with a link to reset your " +"password." +msgstr "" + +msgid "Send password reset link" +msgstr "" + +msgid "This token has expired" +msgstr "" + +msgid "" +"Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "" + +msgid "Create a blog" +msgstr "" + +msgid "Create blog" +msgstr "" + +msgid "Edit \"{}\"" +msgstr "" + +msgid "" +"You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" +msgstr[4] "" +msgstr[5] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "" +"Articles are also visible on other Plume instances, and you can interact " +"with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +# src/template_utils.rs:305 +msgid "Article title matching these words" +msgstr "" + +# src/template_utils.rs:305 +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +# src/template_utils.rs:305 +msgid "From this date" +msgstr "" + +# src/template_utils.rs:305 +msgid "To this date" +msgstr "" + +# src/template_utils.rs:305 +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "" + +# src/template_utils.rs:305 +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +# src/template_utils.rs:305 +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +# src/template_utils.rs:305 +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +# src/template_utils.rs:305 +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "" + +# src/template_utils.rs:305 +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" diff --git a/po/plume/da.po b/po/plume/da.po new file mode 100644 index 00000000000..2051d1c18a9 --- /dev/null +++ b/po/plume/da.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Danish\n" +"Language: da_DK\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: da\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "" + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "" + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "" + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "" + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "" + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "" + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "" + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "" + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "" + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "" + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "" + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "" + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "" + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "" + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "" + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "" + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "" + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "" + +# src/routes/medias.rs:180 +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 "" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "" + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "" + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "" + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "" + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "" + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "" + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "" + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "" + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "" + +msgid "Create an account" +msgstr "" + +msgid "Email" +msgstr "" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Your media" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "" + +msgid "Menu" +msgstr "" + +msgid "Search" +msgstr "" + +msgid "Dashboard" +msgstr "" + +msgid "Notifications" +msgstr "" + +msgid "Log Out" +msgstr "" + +msgid "My account" +msgstr "" + +msgid "Log In" +msgstr "" + +msgid "Register" +msgstr "" + +msgid "About this instance" +msgstr "" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "" + +msgid "It is you" +msgstr "" + +msgid "Edit your profile" +msgstr "" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "" + +msgid "Follow {}" +msgstr "" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "" + +msgid "Subscribers" +msgstr "" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "" + +msgid "Your Drafts" +msgstr "" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "" + +msgid "Your Profile" +msgstr "" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "" + +msgid "Configuration" +msgstr "" + +msgid "Instances" +msgstr "" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "" + +msgid "Unblock" +msgstr "" + +msgid "Block" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Allow anyone to register here" +msgstr "" + +msgid "Short description" +msgstr "" + +msgid "Markdown syntax is supported" +msgstr "" + +msgid "Long description" +msgstr "" + +msgid "Default article license" +msgstr "" + +msgid "Save these settings" +msgstr "" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +msgid "Administred by" +msgstr "" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +msgid "Tags, separated by commas" +msgstr "" + +msgid "License" +msgstr "" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "" +msgstr[1] "" + +msgid "I don't like this anymore" +msgstr "" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" +msgstr[1] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "" + +msgid "Comments" +msgstr "" + +msgid "Your comment" +msgstr "" + +msgid "Submit comment" +msgstr "" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "" + +msgid "Log in" +msgstr "" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +msgid "New password" +msgstr "" + +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "" + +msgid "Create a blog" +msgstr "" + +msgid "Create blog" +msgstr "" + +msgid "Edit \"{}\"" +msgstr "" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" +msgstr[1] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +msgid "From this date" +msgstr "" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" + diff --git a/po/plume/de.po b/po/plume/de.po new file mode 100644 index 00000000000..cb9d77b2a31 --- /dev/null +++ b/po/plume/de.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-26 13:16\n" +"Last-Translator: \n" +"Language-Team: German\n" +"Language: de_DE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: de\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "Jemand" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} hat deinen Artikel kommentiert." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} hat dich abonniert." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} gefällt Ihr Artikel." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} hat dich erwähnt." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} hat deinen Artikel geboosted." + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "Dein Feed" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "Lokaler Feed" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "Föderierter Feed" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "{0}'s Profilbild" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "Vorherige Seite" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "Nächste Seite" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "Optional" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "Du musst angemeldet sein, um einen Blog zu erstellen" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "Es existiert bereits ein Blog mit diesem Namen." + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "Dein Blog wurde erfolgreich erstellt!" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "Dein Blog wurde gelöscht." + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "Du bist nicht berechtigt, diesen Blog zu löschen." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "Du bist nicht berechtigt, diesen Blog zu bearbeiten." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "Du kannst dieses Medium nicht als Blog-Symbol verwenden." + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "Du kannst diese Datei nicht als Blog-Banner verwenden." + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "Informationen des Blog wurden aktualisiert." + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "Dein Kommentar wurde veröffentlicht." + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "Dein Kommentar wurde gelöscht." + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "Anmeldungen sind auf dieser Instanz aktuell nicht möglich." + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "Benutzerregistrierung" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "Dies ist der Link für die Anmeldung: {0}" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "Dein Konto wurde erstellt. Jetzt musst du dich nur noch anmelden, um es nutzen zu können." + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "Die Instanzeinstellungen wurden gespeichert." + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "{} wurde entsperrt." + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "{} wurde gesperrt." + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "Blöcke gelöscht" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "E-Mail-Adresse bereits gesperrt" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "E-Mail-Adresse gesperrt" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "Du kannst deine eigenen Berechtigungen nicht ändern." + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "Du bist nicht berechtigt, diese Aktion auszuführen." + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "Fertig" + +# src/routes/likes.rs:58 +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:153 +msgid "Your media have been deleted." +msgstr "Deine Datei wurde gelöscht." + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "Dir fehlt die Berechtigung, diese Datei zu löschen." + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "Dein Benutzerbild wurde aktualisiert." + +# src/routes/medias.rs:180 +msgid "You are not allowed to use this media." +msgstr "Dir fehlt die Berechtigung, um diese Datei zu nutzen." + +# src/routes/notifications.rs:29 +msgid "To see your notifications, you need to be logged in" +msgstr "Um deine Benachrichtigungen zu sehen, musst du angemeldet sein" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "Dieser Beitrag wurde noch nicht veröffentlicht." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "Um einen neuen Beitrag zu schreiben, musst du angemeldet sein" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "Du bist kein Autor dieses Blogs." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "Neuer Beitrag" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "{0} bearbeiten" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "Dir fehlt die Berechtigung, in diesem Blog zu veröffentlichen." + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "Dein Artikel wurde aktualisiert." + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "Dein Artikel wurde gespeichert." + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "Neuer Artikel" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "Dir fehlt die Berechtigung, diesen Artikel zu löschen." + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "Dein Artikel wurde gelöscht." + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "Möglicherweise ist der zu löschende Artikel nicht (mehr) vorhanden. Wurde er vielleicht schon entfernt?" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "Wir konnten nicht genug Informationen über dein Konto finden. Bitte stelle sicher, dass dein Benutzername richtig ist." + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "Um einen Beitrag erneut zu veröffentlichen, musst du angemeldet sein" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "Du bist nun verbunden." + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "Du bist jetzt abgemeldet." + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "Passwort zurücksetzen" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "Hier der Link, um das Passwort zurückzusetzen: {0}" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "Dein Passwort wurde erfolgreich zurückgesetzt." + +# src/routes/user.rs:87 +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:109 +msgid "You are no longer following {}." +msgstr "Du folgst {} nun nicht mehr." + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "Du folgst nun {}." + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "Um jemanden zu abonnieren, musst du angemeldet sein" + +# src/routes/user.rs:323 +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:369 +msgid "Your profile has been updated." +msgstr "Dein Profil wurde aktualisiert." + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "Dein Benutzerkonto wurde gelöscht." + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "Dir fehlt die Berechtigung, das Konto eines anderen zu löschen." + +msgid "Create your account" +msgstr "Eigenen Account erstellen" + +msgid "Create an account" +msgstr "Konto erstellen" + +msgid "Email" +msgstr "E-Mail-Adresse" + +msgid "Email confirmation" +msgstr "E-Mail-Bestätigung" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "Entschuldigung, Registrierungen sind auf dieser Instanz geschlossen. Du kannst jedoch eine andere finden." + +msgid "Registration" +msgstr "Registrierung" + +msgid "Check your inbox!" +msgstr "Posteingang prüfen!" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "Wir haben eine E-Mail mit einem Link zur Registrierung an die von Ihnen angegebene Adresse gesendet." + +msgid "Username" +msgstr "Benutzername" + +msgid "Password" +msgstr "Passwort" + +msgid "Password confirmation" +msgstr "Passwort bestätigen" + +msgid "Media upload" +msgstr "Hochladen von Mediendateien" + +msgid "Description" +msgstr "Beschreibung" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "Nützlich für sehbehinderte Menschen sowie Lizenzinformationen" + +msgid "Content warning" +msgstr "Inhaltswarnung" + +msgid "Leave it empty, if none is needed" +msgstr "Leer lassen, falls nicht benötigt" + +msgid "File" +msgstr "Datei" + +msgid "Send" +msgstr "Senden" + +msgid "Your media" +msgstr "Ihre Medien" + +msgid "Upload" +msgstr "Hochladen" + +msgid "You don't have any media yet." +msgstr "Du hast noch keine Medien." + +msgid "Content warning: {0}" +msgstr "Warnhinweis zum Inhalt: {0}" + +msgid "Delete" +msgstr "Löschen" + +msgid "Details" +msgstr "Details" + +msgid "Media details" +msgstr "Medien-Details" + +msgid "Go back to the gallery" +msgstr "Zurück zur Galerie" + +msgid "Markdown syntax" +msgstr "Markdown-Syntax" + +msgid "Copy it into your articles, to insert this media:" +msgstr "Kopiere Folgendes in deine Artikel, um dieses Medium einzufügen:" + +msgid "Use as an avatar" +msgstr "Als Profilbild nutzen" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "Menü" + +msgid "Search" +msgstr "Suchen" + +msgid "Dashboard" +msgstr "Dashboard" + +msgid "Notifications" +msgstr "Benachrichtigungen" + +msgid "Log Out" +msgstr "Abmelden" + +msgid "My account" +msgstr "Mein Konto" + +msgid "Log In" +msgstr "Anmelden" + +msgid "Register" +msgstr "Registrieren" + +msgid "About this instance" +msgstr "Über diese Instanz" + +msgid "Privacy policy" +msgstr "Datenschutzrichtlinien" + +msgid "Administration" +msgstr "Administration" + +msgid "Documentation" +msgstr "Dokumentation" + +msgid "Source code" +msgstr "Quelltext" + +msgid "Matrix room" +msgstr "Matrix-Raum" + +msgid "Admin" +msgstr "Admin" + +msgid "It is you" +msgstr "Das bist du" + +msgid "Edit your profile" +msgstr "Eigenes Profil bearbeiten" + +msgid "Open on {0}" +msgstr "Öffnen mit {0}" + +msgid "Unsubscribe" +msgstr "Abbestellen" + +msgid "Subscribe" +msgstr "Abonnieren" + +msgid "Follow {}" +msgstr "{} folgen" + +msgid "Log in to follow" +msgstr "Zum Folgen anmelden" + +msgid "Enter your full username handle to follow" +msgstr "Gebe deinen vollen Benutzernamen ein, um zu folgen" + +msgid "{0}'s subscribers" +msgstr "{0}'s Abonnenten" + +msgid "Articles" +msgstr "Artikel" + +msgid "Subscribers" +msgstr "Abonnenten" + +msgid "Subscriptions" +msgstr "Abonnement" + +msgid "{0}'s subscriptions" +msgstr "{0}'s Abonnements" + +msgid "Your Dashboard" +msgstr "Dein Dashboard" + +msgid "Your Blogs" +msgstr "Deine Blogs" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "Du hast noch keinen Blog. Erstelle deinen eigenen, oder frage, um dich einem anzuschließen." + +msgid "Start a new blog" +msgstr "Neuen Blog beginnen" + +msgid "Your Drafts" +msgstr "Deine Entwürfe" + +msgid "Go to your gallery" +msgstr "Zu deiner Galerie" + +msgid "Edit your account" +msgstr "Eigenes Profil bearbeiten" + +msgid "Your Profile" +msgstr "Dein Profil" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "Um dein Profilbild zu ändern, lade es in deine Galerie hoch und wähle es dort aus." + +msgid "Upload an avatar" +msgstr "Ein Profilbild hochladen" + +msgid "Display name" +msgstr "Angezeigter Name" + +msgid "Summary" +msgstr "Zusammenfassung" + +msgid "Theme" +msgstr "Farbschema" + +msgid "Default theme" +msgstr "Standard-Design" + +msgid "Error while loading theme selector." +msgstr "Fehler beim Laden der Themenauswahl." + +msgid "Never load blogs custom themes" +msgstr "Benutzerdefinierte Themen in Blogs niemals laden" + +msgid "Update account" +msgstr "Konto aktualisieren" + +msgid "Danger zone" +msgstr "Gefahrenbereich" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "Sei sehr vorsichtig, jede Handlung hier kann nicht abgebrochen werden." + +msgid "Delete your account" +msgstr "Eigenen Account löschen" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "Entschuldingung, aber als Administrator kannst du deine eigene Instanz nicht verlassen." + +msgid "Latest articles" +msgstr "Neueste Artikel" + +msgid "Atom feed" +msgstr "Atom-Feed" + +msgid "Recently boosted" +msgstr "Kürzlich geboostet" + +msgid "Articles tagged \"{0}\"" +msgstr "Artikel, die mit \"{0}\" getaggt sind" + +msgid "There are currently no articles with such a tag" +msgstr "Es gibt derzeit keine Artikel mit einem solchen Tag" + +msgid "The content you sent can't be processed." +msgstr "Der gesendete Inhalt konnte nicht verarbeitet werden." + +msgid "Maybe it was too long." +msgstr "Vielleicht war es zu lang." + +msgid "Internal server error" +msgstr "Interner Serverfehler" + +msgid "Something broke on our side." +msgstr "Bei dir ist etwas schief gegangen." + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "Das tut uns leid. Wenn du denkst, dass dies ein Bug ist, melde ihn bitte." + +msgid "Invalid CSRF token" +msgstr "Ungültiges CSRF-Token" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "Irgendetwas stimmt mit deinem CSRF token nicht. Vergewissere dich, dass Cookies in deinem Browser aktiviert sind und versuche diese Seite neu zu laden. Bitte melde diesen Fehler, falls er erneut auftritt." + +msgid "You are not authorized." +msgstr "Berechtigung fehlt" + +msgid "Page not found" +msgstr "Seite nicht gefunden" + +msgid "We couldn't find this page." +msgstr "Diese Seite konnte nicht gefunden werden." + +msgid "The link that led you here may be broken." +msgstr "Der Link, welcher dich hier her führte, ist wohl kaputt." + +msgid "Users" +msgstr "Nutzer*innen" + +msgid "Configuration" +msgstr "Konfiguration" + +msgid "Instances" +msgstr "Instanzen" + +msgid "Email blocklist" +msgstr "E-Mail-Sperrliste" + +msgid "Grant admin rights" +msgstr "Admin-Rechte einräumen" + +msgid "Revoke admin rights" +msgstr "Admin-Rechte entziehen" + +msgid "Grant moderator rights" +msgstr "Moderations-Rechte einräumen" + +msgid "Revoke moderator rights" +msgstr "Moderatorrechte entziehen" + +msgid "Ban" +msgstr "Bannen" + +msgid "Run on selected users" +msgstr "Für ausgewählte Benutzer ausführen" + +msgid "Moderator" +msgstr "Moderator" + +msgid "Moderation" +msgstr "Moderation" + +msgid "Home" +msgstr "Startseite" + +msgid "Administration of {0}" +msgstr "Administration von {0}" + +msgid "Unblock" +msgstr "Block aufheben" + +msgid "Block" +msgstr "Blockieren" + +msgid "Name" +msgstr "Name" + +msgid "Allow anyone to register here" +msgstr "Allen erlauben, sich hier zu registrieren" + +msgid "Short description" +msgstr "Kurzbeschreibung" + +msgid "Markdown syntax is supported" +msgstr "Markdown-Syntax wird unterstützt" + +msgid "Long description" +msgstr "Ausführliche Beschreibung" + +msgid "Default article license" +msgstr "Voreingestellte Artikel-Lizenz" + +msgid "Save these settings" +msgstr "Diese Einstellungen speichern" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "Wenn Sie diese Website als Besucher nutzen, werden keine Daten über Sie erhoben." + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "Als registrierter Benutzer müssen Sie Ihren Benutzernamen (der nicht Ihr richtiger Name sein muss), Ihre E-Mail-Adresse und ein Passwort angeben, um sich anmelden, Artikel schreiben und kommentieren zu können. Die von Ihnen übermittelten Inhalte werden gespeichert, bis Sie sie löschen." + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "Wenn Sie sich anmelden, speichern wir zwei Cookies, eines, um Ihre Sitzung offen zu halten, das andere, um zu verhindern, dass andere Personen in Ihrem Namen handeln. Wir speichern keine weiteren Cookies." + +msgid "Blocklisted Emails" +msgstr "Gesperrte E-Mail-Adressen" + +msgid "Email address" +msgstr "E‐Mail‐Adresse" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "Die E-Mail-Adresse, die du sperren möchtest. Um bestimmte Domänen zu sperren, kannst du den Globbing-Syntax verwenden: Beispielsweise: *@example.com” sperrt alle Adressen von example.com" + +msgid "Note" +msgstr "Notiz" + +msgid "Notify the user?" +msgstr "Benutzer benachrichtigen?" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "Optional: Dem Benutzer wird eine Nachricht angezeigt, wenn er versucht, ein Konto mit dieser Adresse zu erstellen" + +msgid "Blocklisting notification" +msgstr "Sperrlisten-Benachrichtigung" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "Die Nachricht, die angezeigt wird, wenn der Benutzer versucht, ein Konto mit dieser E-Mail-Adresse zu erstellen" + +msgid "Add blocklisted address" +msgstr "Adresse zur Sperrliste hinzufügen" + +msgid "There are no blocked emails on your instance" +msgstr "Derzeit sind auf deiner Instanz keine E-Mail-Adressen gesperrt" + +msgid "Delete selected emails" +msgstr "Ausgewähle E-Mail-Adressen löschen" + +msgid "Email address:" +msgstr "E‐Mail‐Adresse:" + +msgid "Blocklisted for:" +msgstr "Gesperrt für:" + +msgid "Will notify them on account creation with this message:" +msgstr "Du wirst beim Erstellen eines Kontos mit dieser Nachricht benachrichtigt:" + +msgid "The user will be silently prevented from making an account" +msgstr "Der Benutzer wird stillschweigend daran gehindert, ein Konto einzurichten" + +msgid "Welcome to {}" +msgstr "Willkommen bei {}" + +msgid "View all" +msgstr "Alles anzeigen" + +msgid "About {0}" +msgstr "Über {0}" + +msgid "Runs Plume {0}" +msgstr "Läuft mit Plume {0}" + +msgid "Home to {0} people" +msgstr "Heimat von {0} Personen" + +msgid "Who wrote {0} articles" +msgstr "Welche {0} Artikel geschrieben haben" + +msgid "And are connected to {0} other instances" +msgstr "Und mit {0} anderen Instanzen verbunden sind" + +msgid "Administred by" +msgstr "Administriert von" + +msgid "Interact with {}" +msgstr "Interaktion mit {}" + +msgid "Log in to interact" +msgstr "Anmelden, um zu interagieren" + +msgid "Enter your full username to interact" +msgstr "Gib deinen vollständigen Benutzernamen ein, um zu interagieren" + +msgid "Publish" +msgstr "Veröffentlichen" + +msgid "Classic editor (any changes will be lost)" +msgstr "Klassischer Editor (alle Änderungen gehen verloren)" + +msgid "Title" +msgstr "Titel" + +msgid "Subtitle" +msgstr "Untertitel" + +msgid "Content" +msgstr "Inhalt" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "Du kannst Medien in deine Galerie hochladen und dann deren Markdown-Code in deine Artikel kopieren, um sie einzufügen." + +msgid "Upload media" +msgstr "Medien hochladen" + +msgid "Tags, separated by commas" +msgstr "Tags, durch Kommas getrennt" + +msgid "License" +msgstr "Lizenz" + +msgid "Illustration" +msgstr "Illustration" + +msgid "This is a draft, don't publish it yet." +msgstr "Dies ist ein Entwurf, veröffentliche ihn noch nicht." + +msgid "Update" +msgstr "Aktualisieren" + +msgid "Update, or publish" +msgstr "Aktualisieren oder veröffentlichen" + +msgid "Publish your post" +msgstr "Veröffentliche deinen Beitrag" + +msgid "Written by {0}" +msgstr "Geschrieben von {0}" + +msgid "All rights reserved." +msgstr "Alle Rechte vorbehalten." + +msgid "This article is under the {0} license." +msgstr "Dieser Artikel ist unter {0} lizensiert." + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "Ein Like" +msgstr[1] "{0} Likes" + +msgid "I don't like this anymore" +msgstr "Ich mag das nicht mehr" + +msgid "Add yours" +msgstr "Füge deins hinzu" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "Ein Boost" +msgstr[1] "{0} boosts" + +msgid "I don't want to boost this anymore" +msgstr "Ich möchte das nicht mehr boosten" + +msgid "Boost" +msgstr "Boosten" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "{0}Anmelden{1} oder {2}Ihr Fediverse-Konto verwenden{3}, um mit diesem Artikel zu interagieren." + +msgid "Comments" +msgstr "Kommentare" + +msgid "Your comment" +msgstr "Ihr Kommentar" + +msgid "Submit comment" +msgstr "Kommentar senden" + +msgid "No comments yet. Be the first to react!" +msgstr "Noch keine Kommentare. Sei der erste, der reagiert!" + +msgid "Are you sure?" +msgstr "Bist du dir sicher?" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "Dieser Artikel ist noch ein Entwurf. Nur Sie und andere Autoren können ihn sehen." + +msgid "Only you and other authors can edit this article." +msgstr "Nur Sie und andere Autoren können diesen Artikel bearbeiten." + +msgid "Edit" +msgstr "Bearbeiten" + +msgid "I'm from this instance" +msgstr "Ich bin von dieser Instanz" + +msgid "Username, or email" +msgstr "Benutzername oder E-Mail-Adresse" + +msgid "Log in" +msgstr "Anmelden" + +msgid "I'm from another instance" +msgstr "Ich bin von einer anderen Instanz" + +msgid "Continue to your instance" +msgstr "Weiter zu Ihrer Instanz" + +msgid "Reset your password" +msgstr "Passwort zurücksetzen" + +msgid "New password" +msgstr "Neues Passwort" + +msgid "Confirmation" +msgstr "Bestätigung" + +msgid "Update password" +msgstr "Passwort aktualisieren" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "Wir haben eine Mail an die von dir angegebene Adresse gesendet, mit einem Link, um dein Passwort zurückzusetzen." + +msgid "Send password reset link" +msgstr "Link zum Zurücksetzen des Passworts senden" + +msgid "This token has expired" +msgstr "Diese Token ist veraltet" + +msgid "Please start the process again by clicking here." +msgstr "Bitte starten Sie den Prozess erneut, indem Sie hier klicken." + +msgid "New Blog" +msgstr "Neuer Blog" + +msgid "Create a blog" +msgstr "Blog erstellen" + +msgid "Create blog" +msgstr "Blog erstellen" + +msgid "Edit \"{}\"" +msgstr "„{}” bearbeiten" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "Sie können Bilder in Ihre Galerie hochladen, um sie als Blog-Symbol oder Banner zu verwenden." + +msgid "Upload images" +msgstr "Bilder hochladen" + +msgid "Blog icon" +msgstr "Blog-Symbol" + +msgid "Blog banner" +msgstr "Blog-Banner" + +msgid "Custom theme" +msgstr "Benutzerdefiniertes Farbschema" + +msgid "Update blog" +msgstr "Blog aktualisieren" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "Seien Sie sehr vorsichtig, alle hier getroffenen Aktionen können nicht widerrufen werden." + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "Möchten Sie diesen Blog wirklich dauerhaft löschen?" + +msgid "Permanently delete this blog" +msgstr "Diesen Blog dauerhaft löschen" + +msgid "{}'s icon" +msgstr "{}'s Symbol" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "Es gibt einen Autor auf diesem Blog: " +msgstr[1] "Es gibt {0} Autoren auf diesem Blog: " + +msgid "No posts to see here yet." +msgstr "Bisher keine Beiträge vorhanden." + +msgid "Nothing to see here yet." +msgstr "Hier gibt es noch nichts zu sehen." + +msgid "None" +msgstr "Keine" + +msgid "No description" +msgstr "Keine Beschreibung" + +msgid "Respond" +msgstr "Antworten" + +msgid "Delete this comment" +msgstr "Diesen Kommentar löschen" + +msgid "What is Plume?" +msgstr "Was ist Plume?" + +msgid "Plume is a decentralized blogging engine." +msgstr "Plume ist eine dezentrale Blogging-Engine." + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "Autoren können mehrere Blogs verwalten, jeden als eigene Website." + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "Artikel sind auch auf anderen Plume-Instanzen sichtbar und du kannst mit ihnen direkt von anderen Plattformen wie Mastodon interagieren." + +msgid "Read the detailed rules" +msgstr "Die detaillierten Regeln lesen" + +msgid "By {0}" +msgstr "Von {0}" + +msgid "Draft" +msgstr "Entwurf" + +msgid "Search result(s) for \"{0}\"" +msgstr "Suchergebnis(se) für „{0}”" + +msgid "Search result(s)" +msgstr "Suchergebnis(se)" + +msgid "No results for your query" +msgstr "Keine Ergebnisse für Ihre Anfrage" + +msgid "No more results for your query" +msgstr "Keine weiteren Ergebnisse für deine Anfrage" + +msgid "Advanced search" +msgstr "Erweiterte Suche" + +msgid "Article title matching these words" +msgstr "Artikelüberschrift, die diesen Wörtern entspricht" + +msgid "Subtitle matching these words" +msgstr "Untertitel, der diesen Wörtern entspricht" + +msgid "Content macthing these words" +msgstr "Inhalt, der diesen Wörtern entspricht" + +msgid "Body content" +msgstr "Textinhalt" + +msgid "From this date" +msgstr "Ab diesem Datum" + +msgid "To this date" +msgstr "Bis zu diesem Datum" + +msgid "Containing these tags" +msgstr "Enthält diese Schlagwörter" + +msgid "Tags" +msgstr "Schlagwörter" + +msgid "Posted on one of these instances" +msgstr "Auf einer dieser Instanzen veröffentlicht" + +msgid "Instance domain" +msgstr "Instanz-Domain" + +msgid "Posted by one of these authors" +msgstr "Von eine*r dieser Autor*innen veröffentlicht" + +msgid "Author(s)" +msgstr "Autor(en)" + +msgid "Posted on one of these blogs" +msgstr "Auf einem dieser Blogs veröffentlicht" + +msgid "Blog title" +msgstr "Blog-Titel" + +msgid "Written in this language" +msgstr "In dieser Sprache verfasst" + +msgid "Language" +msgstr "Sprache" + +msgid "Published under this license" +msgstr "Unter dieser Lizenz veröffentlicht" + +msgid "Article license" +msgstr "Artikel-Lizenz" + diff --git a/po/plume/el.po b/po/plume/el.po new file mode 100644 index 00000000000..3956ed96de9 --- /dev/null +++ b/po/plume/el.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Greek\n" +"Language: el_GR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: el\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "" + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "" + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "" + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "" + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "" + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "" + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "" + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "" + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "" + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "" + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "" + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "" + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "" + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "" + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "" + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "" + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "" + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "" + +# src/routes/medias.rs:180 +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 "" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "" + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "" + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "" + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "" + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "" + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "" + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "" + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "" + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "" + +msgid "Create an account" +msgstr "" + +msgid "Email" +msgstr "" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Your media" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "" + +msgid "Menu" +msgstr "" + +msgid "Search" +msgstr "" + +msgid "Dashboard" +msgstr "" + +msgid "Notifications" +msgstr "" + +msgid "Log Out" +msgstr "" + +msgid "My account" +msgstr "" + +msgid "Log In" +msgstr "" + +msgid "Register" +msgstr "" + +msgid "About this instance" +msgstr "" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "" + +msgid "It is you" +msgstr "" + +msgid "Edit your profile" +msgstr "" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "" + +msgid "Follow {}" +msgstr "" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "" + +msgid "Subscribers" +msgstr "" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "" + +msgid "Your Drafts" +msgstr "" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "" + +msgid "Your Profile" +msgstr "" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "" + +msgid "Configuration" +msgstr "" + +msgid "Instances" +msgstr "" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "" + +msgid "Unblock" +msgstr "" + +msgid "Block" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Allow anyone to register here" +msgstr "" + +msgid "Short description" +msgstr "" + +msgid "Markdown syntax is supported" +msgstr "" + +msgid "Long description" +msgstr "" + +msgid "Default article license" +msgstr "" + +msgid "Save these settings" +msgstr "" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +msgid "Administred by" +msgstr "" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +msgid "Tags, separated by commas" +msgstr "" + +msgid "License" +msgstr "" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "" +msgstr[1] "" + +msgid "I don't like this anymore" +msgstr "" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" +msgstr[1] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "" + +msgid "Comments" +msgstr "" + +msgid "Your comment" +msgstr "" + +msgid "Submit comment" +msgstr "" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "" + +msgid "Log in" +msgstr "" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +msgid "New password" +msgstr "" + +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "" + +msgid "Create a blog" +msgstr "" + +msgid "Create blog" +msgstr "" + +msgid "Edit \"{}\"" +msgstr "" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" +msgstr[1] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +msgid "From this date" +msgstr "" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" + diff --git a/po/plume/en.po b/po/plume/en.po new file mode 100644 index 00000000000..d3859839747 --- /dev/null +++ b/po/plume/en.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: English\n" +"Language: en_US\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: en\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "" + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "" + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "" + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "" + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "" + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "" + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "" + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "" + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "" + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "" + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "" + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "" + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "" + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "" + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "" + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "" + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "" + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "" + +# src/routes/medias.rs:180 +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 "" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "" + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "" + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "" + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "" + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "" + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "" + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "" + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "" + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "" + +msgid "Create an account" +msgstr "" + +msgid "Email" +msgstr "" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Your media" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "" + +msgid "Menu" +msgstr "" + +msgid "Search" +msgstr "" + +msgid "Dashboard" +msgstr "" + +msgid "Notifications" +msgstr "" + +msgid "Log Out" +msgstr "" + +msgid "My account" +msgstr "" + +msgid "Log In" +msgstr "" + +msgid "Register" +msgstr "" + +msgid "About this instance" +msgstr "" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "" + +msgid "It is you" +msgstr "" + +msgid "Edit your profile" +msgstr "" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "" + +msgid "Follow {}" +msgstr "" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "" + +msgid "Subscribers" +msgstr "" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "" + +msgid "Your Drafts" +msgstr "" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "" + +msgid "Your Profile" +msgstr "" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "" + +msgid "Configuration" +msgstr "" + +msgid "Instances" +msgstr "" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "" + +msgid "Unblock" +msgstr "" + +msgid "Block" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Allow anyone to register here" +msgstr "" + +msgid "Short description" +msgstr "" + +msgid "Markdown syntax is supported" +msgstr "" + +msgid "Long description" +msgstr "" + +msgid "Default article license" +msgstr "" + +msgid "Save these settings" +msgstr "" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +msgid "Administred by" +msgstr "" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +msgid "Tags, separated by commas" +msgstr "" + +msgid "License" +msgstr "" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "" +msgstr[1] "" + +msgid "I don't like this anymore" +msgstr "" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" +msgstr[1] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "" + +msgid "Comments" +msgstr "" + +msgid "Your comment" +msgstr "" + +msgid "Submit comment" +msgstr "" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "" + +msgid "Log in" +msgstr "" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +msgid "New password" +msgstr "" + +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "" + +msgid "Create a blog" +msgstr "" + +msgid "Create blog" +msgstr "" + +msgid "Edit \"{}\"" +msgstr "" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" +msgstr[1] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +msgid "From this date" +msgstr "" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" + diff --git a/po/plume/eo.po b/po/plume/eo.po new file mode 100644 index 00000000000..e6d0a4a3284 --- /dev/null +++ b/po/plume/eo.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Esperanto\n" +"Language: eo_UY\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: eo\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} komentis pri vian afiŝon." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} abonis pri vian." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} ŝatis vian artikolon." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} menciis vin." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "" + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "Profilbildo de {0}" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "Por krei novan blogon, vi devas ensaluti" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "Blogon kun la sama nomo jam ekzistas." + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "Sukcesas krei vian blogon!" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "Via blogo estis forigita." + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "Vi ne rajtas forigi ĉi tiun blogon." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "Vi ne estas permesita redakti ĉi tiun blogon." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "Vi ne povas uzi ĉi tiun aŭdovidaĵon kiel simbolo de blogo." + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "" + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "Viaj blogaj informaĵoj estis ĝisdatigita." + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "" + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "Via komento estis forigita." + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "" + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "Via aŭdovidaĵo estis forigita." + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "Vi ne rajtas forigi ĉi tiun aŭdovidaĵon." + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "Via profilbildo estis gîstatiga." + +# src/routes/medias.rs:180 +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 "" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "Ĉi tiu skribaĵo ankoraŭ ne estas eldonita." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "Skribi novan skribaĵo, vi bezonas ensaluti vin" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "Vi ne estas la verkisto de ĉi tiu blogo." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "Nova skribaĵo" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "Ŝanĝo {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "Via artikolo estis ĝisdatigita." + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "Via artikolo estis konservita." + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "Nova artikolo" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "Vi ne rajtas forigi ĉi tiun artikolon." + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "Via artikolo estis forigita." + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "" + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "" + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "Via profilo estis ĝisdatigita." + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "Via konto estis forigita." + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "Vi ne povas forigi konton de aliulo." + +msgid "Create your account" +msgstr "Krei vian konton" + +msgid "Create an account" +msgstr "Krei konton" + +msgid "Email" +msgstr "Retpoŝtadreso" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "Uzantnomo" + +msgid "Password" +msgstr "Pasvorto" + +msgid "Password confirmation" +msgstr "Konfirmo de la pasvorto" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "Priskribo" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "Dosiero" + +msgid "Send" +msgstr "Sendi" + +msgid "Your media" +msgstr "Viaj aŭdovidaĵoj" + +msgid "Upload" +msgstr "Alŝuti" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "Forigi" + +msgid "Details" +msgstr "Detaloj" + +msgid "Media details" +msgstr "Detaloj de aŭdovidaĵo" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "Uzi kiel profilbildo" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "Menuo" + +msgid "Search" +msgstr "Serĉi" + +msgid "Dashboard" +msgstr "" + +msgid "Notifications" +msgstr "Sciigoj" + +msgid "Log Out" +msgstr "Elsaluti" + +msgid "My account" +msgstr "Mia konto" + +msgid "Log In" +msgstr "Ensaluti" + +msgid "Register" +msgstr "" + +msgid "About this instance" +msgstr "" + +msgid "Privacy policy" +msgstr "Privateca politiko" + +msgid "Administration" +msgstr "Administrado" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "Fontkodo" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "Administristo" + +msgid "It is you" +msgstr "Ĝi estas vi" + +msgid "Edit your profile" +msgstr "Redakti vian profilon" + +msgid "Open on {0}" +msgstr "Malfermi en {0}" + +msgid "Unsubscribe" +msgstr "Malaboni" + +msgid "Subscribe" +msgstr "Aboni" + +msgid "Follow {}" +msgstr "Sekvi {}" + +msgid "Log in to follow" +msgstr "Ensaluti por sekvi" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "Artikoloj" + +msgid "Subscribers" +msgstr "" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "Viaj Blogoj" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "Ekigi novan blogon" + +msgid "Your Drafts" +msgstr "Viaj Malnetoj" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "Redakti vian konton" + +msgid "Your Profile" +msgstr "Via profilo" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "Por ĉanĝi vian profilbildon, retsendu ĝin en via bildaro kaj selektu ol kie." + +msgid "Upload an avatar" +msgstr "Retsendi profilbildo" + +msgid "Display name" +msgstr "Publika nomo" + +msgid "Summary" +msgstr "" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "Ĝisdatigi konton" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "Forigi vian konton" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "Lastaj artikoloj" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "Eble ĝi estis tro longa." + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "Paĝo ne trovita" + +msgid "We couldn't find this page." +msgstr "Ni ne povis trovi ĉi tiun paĝon." + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "Uzantoj" + +msgid "Configuration" +msgstr "" + +msgid "Instances" +msgstr "" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "" + +msgid "Unblock" +msgstr "" + +msgid "Block" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Allow anyone to register here" +msgstr "Permesi iu ajn registriĝi ĉi tie" + +msgid "Short description" +msgstr "" + +msgid "Markdown syntax is supported" +msgstr "" + +msgid "Long description" +msgstr "" + +msgid "Default article license" +msgstr "" + +msgid "Save these settings" +msgstr "Konservi ĉi tiujn agordojn" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "Pri {0}" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +msgid "Administred by" +msgstr "" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "Eldoni" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "Titolo" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "Enhavo" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "Alŝuti aŭdovidaĵo" + +msgid "Tags, separated by commas" +msgstr "Etikedoj, disigitaj per komoj" + +msgid "License" +msgstr "Permesilo" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "Ĝisdatigi" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "Skribita per {0}" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "Iu ŝatas" +msgstr[1] "{0} ŝatas" + +msgid "I don't like this anymore" +msgstr "Mi ne plu ŝatas ĉi tion" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" +msgstr[1] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "" + +msgid "Comments" +msgstr "Komentoj" + +msgid "Your comment" +msgstr "Via komento" + +msgid "Submit comment" +msgstr "Sendi la komento" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "Ĉu vi certas?" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "Redakti" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "Uzantnomo aŭ retpoŝtadreso" + +msgid "Log in" +msgstr "Ensaluti" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "Restarigi vian pasvorton" + +msgid "New password" +msgstr "Nova pasvorto" + +msgid "Confirmation" +msgstr "Konfirmo" + +msgid "Update password" +msgstr "Ĝisdatigi pasvorton" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "Sendi ligilon por restarigi pasvorton" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "Nova blogo" + +msgid "Create a blog" +msgstr "Krei blogon" + +msgid "Create blog" +msgstr "Krei blogon" + +msgid "Edit \"{}\"" +msgstr "Redakti “{}”" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "Alŝuti bildojn" + +msgid "Blog icon" +msgstr "Simbolo de blogo" + +msgid "Blog banner" +msgstr "Rubando de blogo" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "Ĝisdatigi blogon" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" +msgstr[1] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "Neniu" + +msgid "No description" +msgstr "Neniu priskribo" + +msgid "Respond" +msgstr "Respondi" + +msgid "Delete this comment" +msgstr "Forigi ĉi tiun komenton" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "Per {0}" + +msgid "Draft" +msgstr "Malneto" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +msgid "From this date" +msgstr "Ekde ĉi tiu dato" + +msgid "To this date" +msgstr "Ĝis ĉi tiu dato" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "Etikedoj" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "Blogtitolo" + +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "Lingvo" + +msgid "Published under this license" +msgstr "Eldonita sub ĉi tiu permesilo" + +msgid "Article license" +msgstr "Artikola permesilo" + diff --git a/po/plume/es.po b/po/plume/es.po new file mode 100644 index 00000000000..09f88e138ae --- /dev/null +++ b/po/plume/es.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-26 13:16\n" +"Last-Translator: \n" +"Language-Team: Spanish\n" +"Language: es_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: es-ES\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "Alguien" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} ha comentado tu artículo." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} esta suscrito a tí." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "A {0} le gustó tu artículo." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} te ha mencionado." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} compartió su artículo." + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "Tu Feed" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "Feed local" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "Feed federada" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "Avatar de {0}" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "Página anterior" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "Página siguiente" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "Opcional" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "Para crear un nuevo blog, necesitas estar conectado" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "Ya existe un blog con el mismo nombre." + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "¡Tu blog se ha creado satisfactoriamente!" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "Tu blog fue eliminado." + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "No está autorizado a eliminar este registro." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "No tiene permiso para editar este blog." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "No puede usar este medio como icono del blog." + +# src/routes/blogs.rs:298 +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:332 +msgid "Your blog information have been updated." +msgstr "La información de tu blog ha sido actualizada." + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "Se ha publicado el comentario." + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "Se ha eliminado el comentario." + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "Los registros están cerrados en esta instancia." + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "Registro de usuarios" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "Aquí está el enlace para la inscripción: {0}" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "Tu cuenta ha sido creada. Ahora solo necesitas iniciar sesión, antes de poder usarla." + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "Se han guardado los ajustes de la instancia." + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "{} ha sido desbloqueado." + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "{} ha sido bloqueado." + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "Bloqueos eliminados" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "Correo electrónico ya bloqueado" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "Email bloqueado" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "No puedes cambiar tus propios derechos." + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "No te está permitido realizar esta acción." + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "Hecho." + +# src/routes/likes.rs:58 +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:153 +msgid "Your media have been deleted." +msgstr "Tus medios han sido eliminados." + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "No tienes permisos para eliminar este medio." + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "Tu avatar ha sido actualizado." + +# src/routes/medias.rs:180 +msgid "You are not allowed to use this media." +msgstr "No tienes permisos para usar este medio." + +# src/routes/notifications.rs:29 +msgid "To see your notifications, you need to be logged in" +msgstr "Para ver tus notificaciones, necesitas estar conectado" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "Esta publicación aún no está publicada." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "Para escribir un nuevo artículo, necesitas estar conectado" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "No eres un autor de este blog." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "Nueva publicación" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "Editar {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "No tienes permiso para publicar en este blog." + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "Se ha actualizado el artículo." + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "Se ha guardado el artículo." + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "Nueva publicación" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "No tienes permiso para eliminar este artículo." + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "Se ha eliminado el artículo." + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "Parece que el artículo que intentaste eliminar no existe. ¿Tal vez ya haya desaparecido?" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "No se pudo obtener suficiente información sobre su cuenta. Por favor, asegúrese de que su nombre de usuario es correcto." + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "Para compartir un artículo, necesita estar logueado" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "Ahora estás conectado." + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "Ahora estás desconectado." + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "Reiniciar contraseña" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "Aquí está el enlace para restablecer tu contraseña: {0}" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "Su contraseña se ha restablecido correctamente." + +# src/routes/user.rs:87 +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:109 +msgid "You are no longer following {}." +msgstr "Ya no estás siguiendo a {}." + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "Ahora estás siguiendo a {}." + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "Para suscribirse a alguien, necesita estar conectado" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "Para editar su perfil, necesita estar conectado" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "Tu perfil ha sido actualizado." + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "Tu cuenta ha sido eliminada." + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "No puedes eliminar la cuenta de otra persona." + +msgid "Create your account" +msgstr "Crea tu cuenta" + +msgid "Create an account" +msgstr "Crear una cuenta" + +msgid "Email" +msgstr "Correo electrónico" + +msgid "Email confirmation" +msgstr "Confirmación de correo electrónico" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "Lo sentimos, pero las inscripciones están cerradas en esta instancia. Sin embargo, puede encontrar una instancia distinta." + +msgid "Registration" +msgstr "Inscripción" + +msgid "Check your inbox!" +msgstr "Revise su bandeja de entrada!" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "Hemos enviado un correo a la dirección que nos has facilitado, con un enlace para la inscripción." + +msgid "Username" +msgstr "Nombre de usuario" + +msgid "Password" +msgstr "Contraseña" + +msgid "Password confirmation" +msgstr "Confirmación de contraseña" + +msgid "Media upload" +msgstr "Subir medios" + +msgid "Description" +msgstr "Descripción" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "Útil para personas con discapacidad visual, tanto como información de licencias" + +msgid "Content warning" +msgstr "Aviso de contenido" + +msgid "Leave it empty, if none is needed" +msgstr "Dejarlo vacío, si no se necesita nada" + +msgid "File" +msgstr "Archivo" + +msgid "Send" +msgstr "Enviar" + +msgid "Your media" +msgstr "Sus medios" + +msgid "Upload" +msgstr "Subir" + +msgid "You don't have any media yet." +msgstr "Todavía no tiene ningún medio." + +msgid "Content warning: {0}" +msgstr "Aviso de contenido: {0}" + +msgid "Delete" +msgstr "Eliminar" + +msgid "Details" +msgstr "Detalles" + +msgid "Media details" +msgstr "Detalles de los archivos multimedia" + +msgid "Go back to the gallery" +msgstr "Volver a la galería" + +msgid "Markdown syntax" +msgstr "Sintaxis Markdown" + +msgid "Copy it into your articles, to insert this media:" +msgstr "Cópielo en sus artículos, para insertar este medio:" + +msgid "Use as an avatar" +msgstr "Usar como avatar" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "Menú" + +msgid "Search" +msgstr "Buscar" + +msgid "Dashboard" +msgstr "Panel" + +msgid "Notifications" +msgstr "Notificaciones" + +msgid "Log Out" +msgstr "Cerrar Sesión" + +msgid "My account" +msgstr "Mi cuenta" + +msgid "Log In" +msgstr "Iniciar Sesión" + +msgid "Register" +msgstr "Registrarse" + +msgid "About this instance" +msgstr "Acerca de esta instancia" + +msgid "Privacy policy" +msgstr "Política de privacidad" + +msgid "Administration" +msgstr "Administración" + +msgid "Documentation" +msgstr "Documentación" + +msgid "Source code" +msgstr "Código fuente" + +msgid "Matrix room" +msgstr "Sala de Matrix" + +msgid "Admin" +msgstr "Administrador" + +msgid "It is you" +msgstr "Eres tú" + +msgid "Edit your profile" +msgstr "Edita tu perfil" + +msgid "Open on {0}" +msgstr "Abrir en {0}" + +msgid "Unsubscribe" +msgstr "Cancelar suscripción" + +msgid "Subscribe" +msgstr "Subscribirse" + +msgid "Follow {}" +msgstr "Seguir {}" + +msgid "Log in to follow" +msgstr "Inicia sesión para seguir" + +msgid "Enter your full username handle to follow" +msgstr "Introduce tu nombre de usuario completo para seguir" + +msgid "{0}'s subscribers" +msgstr "{0}'s suscriptores" + +msgid "Articles" +msgstr "Artículos" + +msgid "Subscribers" +msgstr "Suscriptores" + +msgid "Subscriptions" +msgstr "Suscripciones" + +msgid "{0}'s subscriptions" +msgstr "Suscripciones de {0}" + +msgid "Your Dashboard" +msgstr "Tu Tablero" + +msgid "Your Blogs" +msgstr "Tus blogs" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "Aún no tienes blog. Crea uno propio o pide unirte a uno." + +msgid "Start a new blog" +msgstr "Iniciar un nuevo blog" + +msgid "Your Drafts" +msgstr "Tus borradores" + +msgid "Go to your gallery" +msgstr "Ir a tu galería" + +msgid "Edit your account" +msgstr "Edita tu cuenta" + +msgid "Your Profile" +msgstr "Tu perfil" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "Para cambiar tu avatar, súbalo a su galería y seleccione de ahí." + +msgid "Upload an avatar" +msgstr "Subir un avatar" + +msgid "Display name" +msgstr "Nombre mostrado" + +msgid "Summary" +msgstr "Resumen" + +msgid "Theme" +msgstr "Tema" + +msgid "Default theme" +msgstr "Tema por defecto" + +msgid "Error while loading theme selector." +msgstr "Error al cargar el selector de temas." + +msgid "Never load blogs custom themes" +msgstr "Nunca cargar temas personalizados de blogs" + +msgid "Update account" +msgstr "Actualizar cuenta" + +msgid "Danger zone" +msgstr "Zona de peligro" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "Tenga mucho cuidado, cualquier acción tomada aquí es irreversible." + +msgid "Delete your account" +msgstr "Eliminar tu cuenta" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "Lo sentimos, pero como un administrador, no puede dejar su propia instancia." + +msgid "Latest articles" +msgstr "Últimas publicaciones" + +msgid "Atom feed" +msgstr "Fuente Atom" + +msgid "Recently boosted" +msgstr "Compartido recientemente" + +msgid "Articles tagged \"{0}\"" +msgstr "Artículos etiquetados \"{0}\"" + +msgid "There are currently no articles with such a tag" +msgstr "Actualmente, no hay artículo con esa etiqueta" + +msgid "The content you sent can't be processed." +msgstr "El contenido que envió no puede ser procesado." + +msgid "Maybe it was too long." +msgstr "Quizás fue demasiado largo." + +msgid "Internal server error" +msgstr "Error interno del servidor" + +msgid "Something broke on our side." +msgstr "Algo ha salido mal de nuestro lado." + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "Disculpe la molestia. Si cree que esto es un defecto, por favor repórtalo." + +msgid "Invalid CSRF token" +msgstr "Token CSRF inválido" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "Hay un problema con su token CSRF. Asegúrase de que las cookies están habilitadas en su navegador, e intente recargar esta página. Si sigue viendo este mensaje de error, por favor infórmelo." + +msgid "You are not authorized." +msgstr "No está autorizado." + +msgid "Page not found" +msgstr "Página no encontrada" + +msgid "We couldn't find this page." +msgstr "No pudimos encontrar esta página." + +msgid "The link that led you here may be broken." +msgstr "El enlace que le llevó aquí puede estar roto." + +msgid "Users" +msgstr "Usuarios" + +msgid "Configuration" +msgstr "Configuración" + +msgid "Instances" +msgstr "Instancias" + +msgid "Email blocklist" +msgstr "Lista de correos bloqueados" + +msgid "Grant admin rights" +msgstr "Otorgar derechos de administrador" + +msgid "Revoke admin rights" +msgstr "Revocar derechos de administrador" + +msgid "Grant moderator rights" +msgstr "Conceder derechos de moderador" + +msgid "Revoke moderator rights" +msgstr "Revocar derechos de moderador" + +msgid "Ban" +msgstr "Banear" + +msgid "Run on selected users" +msgstr "Ejecutar sobre usuarios seleccionados" + +msgid "Moderator" +msgstr "Moderador" + +msgid "Moderation" +msgstr "Moderación" + +msgid "Home" +msgstr "Inicio" + +msgid "Administration of {0}" +msgstr "Administración de {0}" + +msgid "Unblock" +msgstr "Desbloquear" + +msgid "Block" +msgstr "Bloquear" + +msgid "Name" +msgstr "Nombre" + +msgid "Allow anyone to register here" +msgstr "Permite a cualquiera registrarse aquí" + +msgid "Short description" +msgstr "Descripción corta" + +msgid "Markdown syntax is supported" +msgstr "Se puede utilizar la sintaxis Markdown" + +msgid "Long description" +msgstr "Descripción larga" + +msgid "Default article license" +msgstr "Licencia del artículo por defecto" + +msgid "Save these settings" +msgstr "Guardar estos ajustes" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "Si está navegando por este sitio como visitante, no se recopilan datos sobre usted." + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "Como usuario registrado, tienes que proporcionar tu nombre de usuario (que no tiene que ser tu nombre real), tu dirección de correo electrónico funcional y una contraseña, con el fin de poder iniciar sesión, escribir artículos y comentarios. El contenido que envíes se almacena hasta que lo elimines." + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "Cuando inicias sesión, guardamos dos cookies, una para mantener tu sesión abierta, la segunda para evitar que otras personas actúen en tu nombre. No almacenamos ninguna otra cookie." + +msgid "Blocklisted Emails" +msgstr "Correos en la lista de bloqueos" + +msgid "Email address" +msgstr "Dirección de correo electrónico" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "La dirección de correo electrónico que deseas bloquear. Para bloquear dominios, puedes usar sintaxis de globbing, por ejemplo '*@example.com' bloquea todas las direcciones de example.com" + +msgid "Note" +msgstr "Nota" + +msgid "Notify the user?" +msgstr "¿Notificar al usuario?" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "Opcional, muestra un mensaje al usuario cuando intenta crear una cuenta con esa dirección" + +msgid "Blocklisting notification" +msgstr "Notificación de bloqueo" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "El mensaje que se mostrará cuando el usuario intente crear una cuenta con esta dirección de correo electrónico" + +msgid "Add blocklisted address" +msgstr "Añadir dirección bloqueada" + +msgid "There are no blocked emails on your instance" +msgstr "No hay correos bloqueados en tu instancia" + +msgid "Delete selected emails" +msgstr "Eliminar correos seleccionados" + +msgid "Email address:" +msgstr "Dirección de correo electrónico:" + +msgid "Blocklisted for:" +msgstr "Este texto no tiene información de contexto. El texto es usado en plume.pot. Posición en el archivo: 115:" + +msgid "Will notify them on account creation with this message:" +msgstr "Les notificará al crear la cuenta con este mensaje:" + +msgid "The user will be silently prevented from making an account" +msgstr "Se impedirá silenciosamente al usuario crear una cuenta" + +msgid "Welcome to {}" +msgstr "Bienvenido a {}" + +msgid "View all" +msgstr "Ver todo" + +msgid "About {0}" +msgstr "Acerca de {0}" + +msgid "Runs Plume {0}" +msgstr "Ejecuta Plume {0}" + +msgid "Home to {0} people" +msgstr "Hogar de {0} usuarios" + +msgid "Who wrote {0} articles" +msgstr "Que escribieron {0} artículos" + +msgid "And are connected to {0} other instances" +msgstr "Y están conectados a {0} otras instancias" + +msgid "Administred by" +msgstr "Administrado por" + +msgid "Interact with {}" +msgstr "Interactuar con {}" + +msgid "Log in to interact" +msgstr "Inicia sesión para interactuar" + +msgid "Enter your full username to interact" +msgstr "Introduzca su nombre de usuario completo para interactuar" + +msgid "Publish" +msgstr "Publicar" + +msgid "Classic editor (any changes will be lost)" +msgstr "Editor clásico (cualquier cambio estará perdido)" + +msgid "Title" +msgstr "Título" + +msgid "Subtitle" +msgstr "Subtítulo" + +msgid "Content" +msgstr "Contenido" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "Puede subir los medios a su galería, y luego copiar su código Markdown en sus artículos para insertarlos." + +msgid "Upload media" +msgstr "Cargar medios" + +msgid "Tags, separated by commas" +msgstr "Etiquetas, separadas por comas" + +msgid "License" +msgstr "Licencia" + +msgid "Illustration" +msgstr "Ilustración" + +msgid "This is a draft, don't publish it yet." +msgstr "Es un borrador, aún no lo publique." + +msgid "Update" +msgstr "Actualizar" + +msgid "Update, or publish" +msgstr "Actualizar, o publicar" + +msgid "Publish your post" +msgstr "Publique su artículo" + +msgid "Written by {0}" +msgstr "Escrito por {0}" + +msgid "All rights reserved." +msgstr "Todos los derechos reservados." + +msgid "This article is under the {0} license." +msgstr "Este artículo está bajo la licencia {0}." + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "Un Me Gusta" +msgstr[1] "{0} Me Gusta" + +msgid "I don't like this anymore" +msgstr "Ya no me gusta esto" + +msgid "Add yours" +msgstr "Agregue el suyo" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "Un reparto" +msgstr[1] "{0} repartos" + +msgid "I don't want to boost this anymore" +msgstr "Ya no quiero compartir esto" + +msgid "Boost" +msgstr "Compartir" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "{0}Inicie sesión{1}, o {2}utilice su cuenta del Fediverso{3} para interactuar con este artículo" + +msgid "Comments" +msgstr "Comentários" + +msgid "Your comment" +msgstr "Su comentario" + +msgid "Submit comment" +msgstr "Enviar comentario" + +msgid "No comments yet. Be the first to react!" +msgstr "No hay comentarios todavía. ¡Sea el primero en reaccionar!" + +msgid "Are you sure?" +msgstr "¿Está seguro?" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "Este artículo sigue siendo un borrador. Sólo tú y otros autores pueden verlo." + +msgid "Only you and other authors can edit this article." +msgstr "Sólo tú y otros autores pueden editar este artículo." + +msgid "Edit" +msgstr "Editar" + +msgid "I'm from this instance" +msgstr "Soy de esta instancia" + +msgid "Username, or email" +msgstr "Nombre de usuario, o correo electrónico" + +msgid "Log in" +msgstr "Iniciar sesión" + +msgid "I'm from another instance" +msgstr "Soy de otra instancia" + +msgid "Continue to your instance" +msgstr "Continuar a tu instancia" + +msgid "Reset your password" +msgstr "Restablecer su contraseña" + +msgid "New password" +msgstr "Nueva contraseña" + +msgid "Confirmation" +msgstr "Confirmación" + +msgid "Update password" +msgstr "Actualizar contraseña" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "Enviamos un correo a la dirección que nos dio, con un enlace para restablecer su contraseña." + +msgid "Send password reset link" +msgstr "Enviar enlace de restablecimiento de contraseña" + +msgid "This token has expired" +msgstr "Este token ha caducado" + +msgid "Please start the process again by clicking here." +msgstr "Por favor, vuelva a iniciar el proceso haciendo click aquí." + +msgid "New Blog" +msgstr "Nuevo Blog" + +msgid "Create a blog" +msgstr "Crear un blog" + +msgid "Create blog" +msgstr "Crear el blog" + +msgid "Edit \"{}\"" +msgstr "Editar \"{}\"" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "Puede subir imágenes a su galería, para usarlas como iconos de blog, o banderas." + +msgid "Upload images" +msgstr "Subir imágenes" + +msgid "Blog icon" +msgstr "Icono del blog" + +msgid "Blog banner" +msgstr "Bandera del blog" + +msgid "Custom theme" +msgstr "Tema personalizado" + +msgid "Update blog" +msgstr "Actualizar el blog" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "Tenga mucho cuidado, cualquier acción que se tome aquí no puede ser invertida." + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "¿Está seguro que desea eliminar permanentemente este blog?" + +msgid "Permanently delete this blog" +msgstr "Eliminar permanentemente este blog" + +msgid "{}'s icon" +msgstr "Icono de {}" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "Hay un autor en este blog: " +msgstr[1] "Hay {0} autores en este blog: " + +msgid "No posts to see here yet." +msgstr "Ningún artículo aún." + +msgid "Nothing to see here yet." +msgstr "No hay nada que ver aquí todavía." + +msgid "None" +msgstr "Ninguno" + +msgid "No description" +msgstr "Ninguna descripción" + +msgid "Respond" +msgstr "Responder" + +msgid "Delete this comment" +msgstr "Eliminar este comentario" + +msgid "What is Plume?" +msgstr "¿Qué es Plume?" + +msgid "Plume is a decentralized blogging engine." +msgstr "Plume es un motor de blogs descentralizado." + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "Los autores pueden administrar múltiples blogs, cada uno como su propio sitio web." + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "Los artículos también son visibles en otras instancias de Plume, y puede interactuar con ellos directamente desde otras plataformas como Mastodon." + +msgid "Read the detailed rules" +msgstr "Leer las reglas detalladas" + +msgid "By {0}" +msgstr "Por {0}" + +msgid "Draft" +msgstr "Borrador" + +msgid "Search result(s) for \"{0}\"" +msgstr "Resultado(s) de búsqueda para \"{0}\"" + +msgid "Search result(s)" +msgstr "Resultado(s) de búsqueda" + +msgid "No results for your query" +msgstr "No hay resultados para tu consulta" + +msgid "No more results for your query" +msgstr "No hay más resultados para su consulta" + +msgid "Advanced search" +msgstr "Búsqueda avanzada" + +msgid "Article title matching these words" +msgstr "Título del artículo que coincide con estas palabras" + +msgid "Subtitle matching these words" +msgstr "Subtítulo que coincide con estas palabras" + +msgid "Content macthing these words" +msgstr "Contenido que coincide con estas palabras" + +msgid "Body content" +msgstr "Contenido" + +msgid "From this date" +msgstr "Desde esta fecha" + +msgid "To this date" +msgstr "Hasta esta fecha" + +msgid "Containing these tags" +msgstr "Con estas etiquetas" + +msgid "Tags" +msgstr "Etiquetas" + +msgid "Posted on one of these instances" +msgstr "Publicado en una de estas instancias" + +msgid "Instance domain" +msgstr "Dominio de instancia" + +msgid "Posted by one of these authors" +msgstr "Publicado por uno de estos autores" + +msgid "Author(s)" +msgstr "Autor(es)" + +msgid "Posted on one of these blogs" +msgstr "Publicado en uno de estos blogs" + +msgid "Blog title" +msgstr "Título del blog" + +msgid "Written in this language" +msgstr "Escrito en este idioma" + +msgid "Language" +msgstr "Idioma" + +msgid "Published under this license" +msgstr "Publicado bajo esta licencia" + +msgid "Article license" +msgstr "Licencia de artículo" + diff --git a/po/plume/eu.po b/po/plume/eu.po new file mode 100644 index 00000000000..13d2bc649ef --- /dev/null +++ b/po/plume/eu.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-26 13:15\n" +"Last-Translator: \n" +"Language-Team: Basque\n" +"Language: eu_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: eu\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "Norbaitek" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0}(e)k iruzkina egin du." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} zurekin harpidetuta dago." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0}-ri zure artikulua gustatu zitzaion." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0}(e)k aipatu zaitu." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0}(e)k zure artikulua partekatu zuen." + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "Zure feed-a" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "Feed-a lokala" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "Feed federatua" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "{0}-ko avatarra" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "Aurreko orrialdea" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "Hurrengo orrialdea" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "Hautazkoa" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "Blog berri bat sortzeko, konektatuta egon behar duzu" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "Badago izen bereko blog bat." + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "Zure bloga behar bezala sortu da!" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "Zure bloga ezabatu egin da." + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "Ez duzu baimenik erregistro hau ezabatzeko." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "Ez duzu blog hau editatzeko baimenik." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "Ezin duzu baliabide hau blogaren ikono gisa erabili." + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "Ezin duzu baliabide hau blogaren bannerra gisa erabili." + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "Zure blogeko informazioa eguneratu egin da." + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "Zure iruzkina argitaratu da." + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "Zure iruzkina ezabatu da." + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "Erregistroak itxita daude instantzia honetan." + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "Erabiltzaileen erregistroa" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "Hemen duzu izena emateko esteka: {0}" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "Zure kontua sortu da. Orain saioa hasi besterik ez duzu egin behar, erabili aurretik." + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "Instantziako ezarpenak gorde dira." + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "{} desblokeatu egin da." + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "{} blokeatu egin da." + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "Blokeoak ezabatu egin dira" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "Posta elektronikoa blokeatuta zegoen" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "Posta elektronikoa blokeatuta" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "Ezin dituzu zure eskubideak aldatu." + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "Ezin duzu ekintza hori egin." + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "Eginda." + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "Artikulu bati atsegite bat emateko, konektatuta egon behar duzu" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "Zure baliabideak ezabatu egin dira." + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "Ez duzu baimenik baliabide hau ezabatzeko." + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "Zure avatarra eguneratu egin da." + +# src/routes/medias.rs:180 +msgid "You are not allowed to use this media." +msgstr "Ez duzu baimenik baliabide hau erabiltzeko." + +# src/routes/notifications.rs:29 +msgid "To see your notifications, you need to be logged in" +msgstr "Zure jakinarazpenak ikusteko, konektatuta egon behar duzu" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "Argitalpen hau oraindik ez da argitaratu." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "Artikulu berri bat idazteko, konektatuta egon behar duzu" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "Ez zara blog honen egilea." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "Artikulu berria" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "Editatu {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "Ez duzu baimenik blog honetan argitaratzeko." + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "Artikulua eguneratu da." + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "Artikulua gorde da." + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "Artikulu berria" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "Ez duzu artikulu hau ezabatzeko baimenik." + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "Zure artikulua ezabatu da." + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "Badirudi ezabatzen saiatu zaren artikulua ez dela existitzen. Beharbada dagoeneko ezabatuta dago." + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "Ezin izan da zure kontuari buruzko nahikoa informazio lortu. Mesedez, egiaztatu erabiltzailea zuzena dela." + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "Artikulua bultzatzeko saioa hasi behar duzu" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "Saioa hasi duzu." + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "Saioa itxi duzu." + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "Berrezarri pasahitza" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "Hemen duzu pasahitza berrezartzeko esteka: {0}" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "Pasahitza behar bezala berrezarri da." + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "Saioa hasi behar duzu aginte-mahaia ikusi ahal izateko" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "{} jarraitzeari utzi diozu." + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "{} jarraitzen hasi zara." + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "Norbaiten harpidetza egiteko, konektatuta egon behar duzu" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "Saioa hasi behar duzu profila moldatu ahal izateko" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "Zure profila eguneratu egin da." + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "Kontua ezabatu da." + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "Ezin duzu beste norbaiten kontua ezabatu." + +msgid "Create your account" +msgstr "Sortu kontua" + +msgid "Create an account" +msgstr "Kontu bat sortu" + +msgid "Email" +msgstr "Posta elektronikoa" + +msgid "Email confirmation" +msgstr "Baieztatu helbide elektronikoa" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "Sentitzen dugu, baina inskripzioak itxita daude instantzia honetan. Hala ere, beste instantzia bat aurki dezakezu." + +msgid "Registration" +msgstr "Izen-ematea" + +msgid "Check your inbox!" +msgstr "Begiratu zure sarrera-erretilua!" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "Mezu bat bidali dugu eman diguzun helbidera, izena emateko estekarekin." + +msgid "Username" +msgstr "Erabiltzailea" + +msgid "Password" +msgstr "Pasahitza" + +msgid "Password confirmation" +msgstr "Baieztatu pasahitza" + +msgid "Media upload" +msgstr "Baliabideak igo" + +msgid "Description" +msgstr "Azalpena" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "Ikusmen-desgaitasuna duten pertsonentzat erabilgarria, bai eta lizentziei buruzko informazioa ere" + +msgid "Content warning" +msgstr "Edukiari buruzko oharra" + +msgid "Leave it empty, if none is needed" +msgstr "Utzi hutsik, behar ez bada" + +msgid "File" +msgstr "Fitxategia" + +msgid "Send" +msgstr "Bidali" + +msgid "Your media" +msgstr "Zure baliabideak" + +msgid "Upload" +msgstr "Igo" + +msgid "You don't have any media yet." +msgstr "Oraindik ez duzu baliabiderik." + +msgid "Content warning: {0}" +msgstr "Edukiari buruzko oharra: {0}" + +msgid "Delete" +msgstr "Ezabatu" + +msgid "Details" +msgstr "Xehetasunak" + +msgid "Media details" +msgstr "Baliabide fitxategien xehetasunak" + +msgid "Go back to the gallery" +msgstr "Itzuli galeriara" + +msgid "Markdown syntax" +msgstr "Markdown sintaxia" + +msgid "Copy it into your articles, to insert this media:" +msgstr "Kopiatu artikuluetan irudi hau txertatzeko:" + +msgid "Use as an avatar" +msgstr "Erabili avatar gisa" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "Menua" + +msgid "Search" +msgstr "Bilatu" + +msgid "Dashboard" +msgstr "Panela" + +msgid "Notifications" +msgstr "Jakinarazpenak" + +msgid "Log Out" +msgstr "Amaitu saioa" + +msgid "My account" +msgstr "Nire kontua" + +msgid "Log In" +msgstr "Saioa hasi" + +msgid "Register" +msgstr "Erregistratu" + +msgid "About this instance" +msgstr "Instantzia honi buruz" + +msgid "Privacy policy" +msgstr "Pribatutasun politika" + +msgid "Administration" +msgstr "Administrazioa" + +msgid "Documentation" +msgstr "Dokumentazioa" + +msgid "Source code" +msgstr "Iturburu kodea" + +msgid "Matrix room" +msgstr "Matrix.org gela" + +msgid "Admin" +msgstr "Administratzailea" + +msgid "It is you" +msgstr "Zu zara" + +msgid "Edit your profile" +msgstr "Editatu zure profila" + +msgid "Open on {0}" +msgstr "Ireki {0}-n" + +msgid "Unsubscribe" +msgstr "Ezeztatu harpidetza" + +msgid "Subscribe" +msgstr "Harpidetu" + +msgid "Follow {}" +msgstr "Jarraitu {}" + +msgid "Log in to follow" +msgstr "Hasi saioa jarraitzeko" + +msgid "Enter your full username handle to follow" +msgstr "Sartu zure erabiltzaile-izen osoa jarraitzeko" + +msgid "{0}'s subscribers" +msgstr "{0} harpidedun" + +msgid "Articles" +msgstr "Artikuluak" + +msgid "Subscribers" +msgstr "Harpidedunak" + +msgid "Subscriptions" +msgstr "Harpidetzak" + +msgid "{0}'s subscriptions" +msgstr "{0}-ko harpidetzak" + +msgid "Your Dashboard" +msgstr "Zure panela" + +msgid "Your Blogs" +msgstr "Zure blogak" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "Oraindik ez duzu blogik. Sortu bat zeure buruarentzat edo eskatu batekin bat egitea." + +msgid "Start a new blog" +msgstr "Blog berri bat hasi" + +msgid "Your Drafts" +msgstr "Zure zirriborroak" + +msgid "Go to your gallery" +msgstr "Joan zure galeriara" + +msgid "Edit your account" +msgstr "Editatu zure kontua" + +msgid "Your Profile" +msgstr "Zure profila" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "Zure avatarra aldatzeko, sartu zure galerian eta aukeratu hortik." + +msgid "Upload an avatar" +msgstr "Avatar bat igo" + +msgid "Display name" +msgstr "Erakusteko izena" + +msgid "Summary" +msgstr "Laburpena" + +msgid "Theme" +msgstr "Gaia" + +msgid "Default theme" +msgstr "Lehenetsitako gaia" + +msgid "Error while loading theme selector." +msgstr "Errorea gai-hautatzailea kargatzean." + +msgid "Never load blogs custom themes" +msgstr "Inoiz ez kargatu blogetako gai pertsonalizatuak" + +msgid "Update account" +msgstr "Eguneratu kontua" + +msgid "Danger zone" +msgstr "Arrisku-eremua" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "Kontuz ibili, hemen hartutako edozein ekintza atzeraezina da." + +msgid "Delete your account" +msgstr "Ezabatu zure kontua" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "Sentitzen dugu, baina administratzaile gisa, ezin duzu zure instantzia utzi." + +msgid "Latest articles" +msgstr "Azken artikuluak" + +msgid "Atom feed" +msgstr "Atom iturria" + +msgid "Recently boosted" +msgstr "Duela gutxi partekatua" + +msgid "Articles tagged \"{0}\"" +msgstr "\"{0}\" etiketa duten artikuluak" + +msgid "There are currently no articles with such a tag" +msgstr "Oraingoz ez dago etiketa hori duen artikulurik" + +msgid "The content you sent can't be processed." +msgstr "Bidali duzun edukia ezin da prozesatu." + +msgid "Maybe it was too long." +msgstr "Agian luzeegia izan zen." + +msgid "Internal server error" +msgstr "Zerbitzariaren barne errorea" + +msgid "Something broke on our side." +msgstr "Zerbait gaizki atera zaigu." + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "Sentitzen dugu. Akats bat dela uste baduzu, jakinaraziguzu." + +msgid "Invalid CSRF token" +msgstr "CSRF token baliogabea" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "Arazo bat dago zure CSRF tokenarekin. Ziurtatu cookieak zure nabigatzailean gaituta daudela, eta saiatu orri hau kargatzen. Errore-mezu hau ikusten jarraitzen baduzu, mesedez, informatu." + +msgid "You are not authorized." +msgstr "Ez duzu baimenik." + +msgid "Page not found" +msgstr "Ez da orrialdea aurkitu" + +msgid "We couldn't find this page." +msgstr "Ezin izan dugu orrialdea aurkitu." + +msgid "The link that led you here may be broken." +msgstr "Hona ekarri zaituen esteka hondatuta egon liteke." + +msgid "Users" +msgstr "Erabiltzaileak" + +msgid "Configuration" +msgstr "Ezarpenak" + +msgid "Instances" +msgstr "Instantziak" + +msgid "Email blocklist" +msgstr "Blokeatutako posta elektronikoen zerrenda" + +msgid "Grant admin rights" +msgstr "Eman administrazio-eskubideak" + +msgid "Revoke admin rights" +msgstr "Kendu administrazio-eskubideak" + +msgid "Grant moderator rights" +msgstr "Eman moderatzaile-eskubideak" + +msgid "Revoke moderator rights" +msgstr "Kendu moderatzaile-eskubideak" + +msgid "Ban" +msgstr "Debekatu, galarazi (?)" + +msgid "Run on selected users" +msgstr "Aukeratutako erabiltzaileei" + +msgid "Moderator" +msgstr "Moderatzailea" + +msgid "Moderation" +msgstr "Moderazioa" + +msgid "Home" +msgstr "Hasiera" + +msgid "Administration of {0}" +msgstr "{0}(r)en administrazioa" + +msgid "Unblock" +msgstr "Desblokeatu" + +msgid "Block" +msgstr "Blokeatu" + +msgid "Name" +msgstr "Izena" + +msgid "Allow anyone to register here" +msgstr "Baimendu edonork izena ematea" + +msgid "Short description" +msgstr "Deskribapen (azalpen?) laburra" + +msgid "Markdown syntax is supported" +msgstr "Markdown sintaxia erabil dezakezu" + +msgid "Long description" +msgstr "Deskribapen luzea" + +msgid "Default article license" +msgstr "Artikuluen lizentzia besterik adierazi ezean" + +msgid "Save these settings" +msgstr "Gorde ezarpenak" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "Saiorik hasi ez baduzu, ez dugu zuri buruzko inolako daturik gordeko." + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "Izena eman baduzu, erabiltzaile izena (ez da beharrezkoa zure benetako izena izatea), helbide elektronikoa eta pasahitza zehaztu behar dituzu saioa hasi ahal izateko, artikuluak idazteko eta iruzkinak egiteko. Igotzen duzun edukia gorde egingo da zuk ezabatzen duzun arte." + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "Saioa hasten duzunean bi cookie gordetzen ditugu: bata saioa irekita mantentzeko; bestea beste batzuek zu izango bazina jardun ahal ez izateko. Ez dugu bestelako cookierik gordetzen." + +msgid "Blocklisted Emails" +msgstr "Debekatutako helbide elektronikoak" + +msgid "Email address" +msgstr "Helbide elektronikoa" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "Blokeatu nahiko zenukeen helbide elektronikoa. Domeinu bat blokeatzeko globbing sintaxia erabil dezakezu, adibidez '*@adibidea.eus'-ek adibidea.eus domeinuko helbide elektroniko guztiak blokeatuko ditu" + +msgid "Note" +msgstr "Oharra" + +msgid "Notify the user?" +msgstr "Erabiltzailea jakinarazi?" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "Hautazkoa, mezu bat erakusten die erabiltzaileei helbide honekin kontu bat sortzen saiatzen direnean" + +msgid "Blocklisting notification" +msgstr "Blokeoaren jakinarazpena" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "Erabiltzailea helbide elektroniko hau duen kontu bat sortzen saiatzen denean agertuko den mezua" + +msgid "Add blocklisted address" +msgstr "Gehitu helbide blokeatua" + +msgid "There are no blocked emails on your instance" +msgstr "Ez dago posta blokeaturik zure instantzian" + +msgid "Delete selected emails" +msgstr "Ezabatu aukeratutako posta elektronikoak" + +msgid "Email address:" +msgstr "Helbide elektronikoa:" + +msgid "Blocklisted for:" +msgstr "Blokeoaren arrazoia:" + +msgid "Will notify them on account creation with this message:" +msgstr "Kontua sortzean, mezu honekin jakinaraziko zaie:" + +msgid "The user will be silently prevented from making an account" +msgstr "Erabiltzaileari isilean eragotziko zaio kontu bat sortzea" + +msgid "Welcome to {}" +msgstr "Ongi etorri {}(e)ra" + +msgid "View all" +msgstr "Ikusi guztia" + +msgid "About {0}" +msgstr "{0}ri buruz" + +msgid "Runs Plume {0}" +msgstr "{0}(e)k Plume darabil" + +msgid "Home to {0} people" +msgstr "{0} erabiltzaileen etxea" + +msgid "Who wrote {0} articles" +msgstr "{0} artikulu idatzi zituztenak" + +msgid "And are connected to {0} other instances" +msgstr "Eta beste {0} instantziarekin daude konektatuta" + +msgid "Administred by" +msgstr "Kudeatzailea:" + +msgid "Interact with {}" +msgstr "{}-rekin elkarreragin" + +msgid "Log in to interact" +msgstr "Hasi saioa parte hartzeko" + +msgid "Enter your full username to interact" +msgstr "Idatzi erabiltzaile-izen osoa parte hartzeko" + +msgid "Publish" +msgstr "Argitaratu" + +msgid "Classic editor (any changes will be lost)" +msgstr "Editore klasikoa (edozein aldaketa galduta egongo da)" + +msgid "Title" +msgstr "Titulua" + +msgid "Subtitle" +msgstr "Azpititulua" + +msgid "Content" +msgstr "Edukia" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "Baliabideak igo ditzakezu zure galeriara eta ondoren Markdown kodea kopiatu artikuluan txertatzeko." + +msgid "Upload media" +msgstr "Baliabideak igo" + +msgid "Tags, separated by commas" +msgstr "Etiketak, komen bidez bereizita" + +msgid "License" +msgstr "Lizentzia" + +msgid "Illustration" +msgstr "Irudi nagusia" + +msgid "This is a draft, don't publish it yet." +msgstr "Zirriborro bat da, ez argitaratu oraingoz." + +msgid "Update" +msgstr "Eguneratu" + +msgid "Update, or publish" +msgstr "Eguneratu edo argitaratu" + +msgid "Publish your post" +msgstr "Argitaratu artikulua" + +msgid "Written by {0}" +msgstr "{0}(e)k idatzia" + +msgid "All rights reserved." +msgstr "Eskubide guztiak erreserbatuta." + +msgid "This article is under the {0} license." +msgstr "Artikulu honen lizentzia {0} da." + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "Atsegite {0}" +msgstr[1] "{0} atsegite" + +msgid "I don't like this anymore" +msgstr "Orain hau ez zait gustatzen" + +msgid "Add yours" +msgstr "Zuk ere gogoko egin" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "Bultzada bat" +msgstr[1] "{0} bultzada" + +msgid "I don't want to boost this anymore" +msgstr "Orain ez dut hau partekatu nahi" + +msgid "Boost" +msgstr "Bultzatu" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "{0}Hasi saioa{1} edo {2}erabili Fedibertsoko kontua{3}" + +msgid "Comments" +msgstr "Iruzkinak" + +msgid "Your comment" +msgstr "Zure iruzkina" + +msgid "Submit comment" +msgstr "Bidali iruzkina" + +msgid "No comments yet. Be the first to react!" +msgstr "Oraingoz ez du iruzkinik. Izan lehena!" + +msgid "Are you sure?" +msgstr "Ziur al zaude?" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "Artikulu hau zirriborro bat baino ez da. Zuk eta beste egileek bakarrik ikus dezakezue." + +msgid "Only you and other authors can edit this article." +msgstr "Zuk eta beste egileek bakarrik moldatu dezakezue artikulu hau." + +msgid "Edit" +msgstr "Editatu (edo moldatu?)" + +msgid "I'm from this instance" +msgstr "Instantzia honetakoa naiz" + +msgid "Username, or email" +msgstr "Erabiltzailea edo helbide elektronikoa" + +msgid "Log in" +msgstr "Hasi saioa" + +msgid "I'm from another instance" +msgstr "Beste instantzia batekoa naiz" + +msgid "Continue to your instance" +msgstr "Joan zure instantziara" + +msgid "Reset your password" +msgstr "Berrezarri pasahitza" + +msgid "New password" +msgstr "Pasahitz berria" + +msgid "Confirmation" +msgstr "Baieztapena" + +msgid "Update password" +msgstr "Eguneratu pasahitza" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "Pasahitza berrezartzeko esteka bidali dizugu eman zenigun helbidera." + +msgid "Send password reset link" +msgstr "Bidali pasahitza berrezartzeko esteka" + +msgid "This token has expired" +msgstr "Token hau iraungi egin da" + +msgid "Please start the process again by clicking here." +msgstr "Mesedez, hasi berriro prozesua hemen klik eginez." + +msgid "New Blog" +msgstr "Blog berria" + +msgid "Create a blog" +msgstr "Sortu bloga" + +msgid "Create blog" +msgstr "Sortu bloga" + +msgid "Edit \"{}\"" +msgstr "Editatu \"{}\"" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "Irudiak igo ditzakezu zure galeriara, blogeko ikono edo baner gisa erabiltzeko." + +msgid "Upload images" +msgstr "Igo irudiak" + +msgid "Blog icon" +msgstr "Blogeko ikonoa" + +msgid "Blog banner" +msgstr "Blogeko banerra" + +msgid "Custom theme" +msgstr "Pertsonalizatutako gaia" + +msgid "Update blog" +msgstr "Eguneratu bloga" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "Adi! Hemen egindako ekintzak ezin dira desegin." + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "Ziur al zaude blog hau betiko ezabatu nahi duzula?" + +msgid "Permanently delete this blog" +msgstr "Betiko ezabatu blog hau" + +msgid "{}'s icon" +msgstr "{}(r)en ikonoa" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "Egile bat dago blog honetan: " +msgstr[1] "{0} egile daude blog honetan: " + +msgid "No posts to see here yet." +msgstr "Oraindik ez dago artikulurik ikusgai." + +msgid "Nothing to see here yet." +msgstr "Oraindik ez dago zerikusirik hemen." + +msgid "None" +msgstr "Bat ere ez" + +msgid "No description" +msgstr "Deskribapenik gabe" + +msgid "Respond" +msgstr "Erantzun" + +msgid "Delete this comment" +msgstr "Ezabatu iruzkina" + +msgid "What is Plume?" +msgstr "Zer da Plume?" + +msgid "Plume is a decentralized blogging engine." +msgstr "Plume blog-motor deszentralizatua da." + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "Egileek blog bat baino gehiago kudeatu ditzakete, bakoitza webgune bat bailitzan." + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "Artikuluak beste Plume instantzietan daude ikusgai, eta Mastodon bezalako plataformekin parte hartu dezakezu." + +msgid "Read the detailed rules" +msgstr "Irakurri arauak bere osotasunean" + +msgid "By {0}" +msgstr "{0}(r)i esker" + +msgid "Draft" +msgstr "Zirriborroa" + +msgid "Search result(s) for \"{0}\"" +msgstr "\"{0}\" bilaketaren emaitza(k)" + +msgid "Search result(s)" +msgstr "Bilatu emaitzak" + +msgid "No results for your query" +msgstr "Ez dago emaitzarik zure kontsultarako" + +msgid "No more results for your query" +msgstr "Ez dago emaitza gehiagorik zure kontsultarako" + +msgid "Advanced search" +msgstr "Bilaketa aurreratua" + +msgid "Article title matching these words" +msgstr "Hitz hauekin bat datozen artikuluen tituluak" + +msgid "Subtitle matching these words" +msgstr "Hitz hauekin bat datozen azpitituluak" + +msgid "Content macthing these words" +msgstr "Hitz hauekin bat datozen edukiak" + +msgid "Body content" +msgstr "Gorputzeko edukia" + +msgid "From this date" +msgstr "Data honetatik aurrera" + +msgid "To this date" +msgstr "Data honetaraino" + +msgid "Containing these tags" +msgstr "Etiketa hauek ditu(zt)enak" + +msgid "Tags" +msgstr "Etiketak" + +msgid "Posted on one of these instances" +msgstr "Instantzia hauetako batean argitaratuta" + +msgid "Instance domain" +msgstr "Instantziaren domeinua" + +msgid "Posted by one of these authors" +msgstr "Egile hauetako batek argitaratuta" + +msgid "Author(s)" +msgstr "Egilea(k)" + +msgid "Posted on one of these blogs" +msgstr "Blog hauetako batean argitaratuta" + +msgid "Blog title" +msgstr "Blogaren izena" + +msgid "Written in this language" +msgstr "Hizkuntza honetan idatzia" + +msgid "Language" +msgstr "Hizkuntza" + +msgid "Published under this license" +msgstr "Lizentzia honekin argitaratua" + +msgid "Article license" +msgstr "Artikuluaren lizentzia" + diff --git a/po/plume/fa.po b/po/plume/fa.po new file mode 100644 index 00000000000..269c14b38d4 --- /dev/null +++ b/po/plume/fa.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Persian\n" +"Language: fa_IR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: fa\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} روی مقالهٔ شما نظر داد." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} شما را دنبال می‌کند." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} مقالهٔ شما را پسندید." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} به شما اشاره کرد." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} مقالهٔ شما را تقویت کرد." + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "خوراک شما" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "خوراک محلی" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "خوراک سراسری" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "چهرک {0}" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "برگ پیشین" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "برگ پسین" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "اختیاری" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "برای ساخت یک بلاگ بایستی وارد شوید" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "بلاگی با همین نام از قبل وجود دارد." + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "بلاگ شما با موفقیت ساخته شد!" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "بلاگ شما پاک شد." + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "شما مجاز به پاک کردن این بلاگ نیستید." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "شما مجاز به ویرایش این بلاگ نیستید." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "شما نمی‌توانید این رسانه را به عنوان تصویر بلاگ استفاده کنید." + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "شما نمی‌توانید از این رسانه به عنوان تصویر سردر بلاگ استفاده کنید." + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "اطلاعات بلاگ شما به‌روز شده است." + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "نظر شما فرستاده شده است." + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "نظر شما پاک شده است." + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "ثبت‌نام روی این نمونه بسته شده است." + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "حساب شما ایجاد شده است. اکنون برای استفاده از آن تنها نیاز است که واردش شوید." + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "تنظیمات نمونه ذخیره شده است." + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "مسدودیت {} رفع شده است." + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "{} مسدود شده است." + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "مسدودسازی‌ها حذف شدند" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "رایانامه قبلاً مسدود شده است" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "رایانامه مسدود شده" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "شما نمی‌توانید نقش خود را تغییر دهید." + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "شما مجاز به انجام این کار نیستید." + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "انجام شد." + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "برای پسندیدن یک فرسته، بایستی وارد شده باشید" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "رسانه شما پاک شده است." + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "شما مجاز به پاک کردن این رسانه نیستید." + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "چهرک شما به‌روز شده است." + +# src/routes/medias.rs:180 +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 "برای دیدن اعلانات خود بایستی وارد شده باشید" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "این فرسته هنوز منتشر نشده است." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "برای نوشتن یک فرستهٔ جدید، بایستی وارد شده باشید" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "شما نویسندهٔ این بلاگ نیستید." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "فرستهٔ جدید" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "ویرایش {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "شما مجاز به انتشار روی این بلاگ نیستید." + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "مقالهٔ شما به‌روز شده است." + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "مقالهٔ شما ذخیره شده است." + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "مقالهٔ جدید" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "شما مجاز به حذف این مقاله نیستید." + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "مقالهٔ شما پاک شده است." + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "به نظر می‌رسد مقاله‌ای را که می‌خواهید پاک کنید، وجود ندارد. قبلا پاک نشده است؟" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "نتوانستیم اطّلاعات کافی دربارهٔ حساب شما دریافت کنیم. لطفاً مطمئن شوید که نام کاربری درست است." + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "برای هم‌رسانی یک فرسته لازم است وارد شوید" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "شما اکنون متصل هستید." + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "شما اکنون خارج شدید." + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "بازنشانی گذرواژه" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "اینجا، پیوندی برای بازنشانی گذرواژهٔ شماست: {0}" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "گذرواژه شما با موفقیت بازنشانی شد." + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "برای دسترسی به پیشخوان بایستی وارد شده باشید" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "دیگر {} را دنبال نمی‌کنید." + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "اکنون {} را دنبال می‌کنید." + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "برای دنبال کردن یک نفر، باید وارد شوید" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "برای ویرایش نمایهٔ خود، باید وارد شوید" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "نمایهٔ شما به‌روز شده است." + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "حساب شما پاک شده است." + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "نمی‌توانید حساب شخص دیگری را پاک کنید." + +msgid "Create your account" +msgstr "حسابی برای خود بسازید" + +msgid "Create an account" +msgstr "حسابی بسازید" + +msgid "Email" +msgstr "رایانامه" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "معذرت می‌خواهیم. ثبت‌نام روی این نمونه خاص بسته شده است. با این حال شما می‌توانید یک نمونه دیگر پیدا کنید." + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "صندوق پستی خود را بررسی کنید!" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "نام کاربری" + +msgid "Password" +msgstr "گذرواژه" + +msgid "Password confirmation" +msgstr "تایید گذرواژه" + +msgid "Media upload" +msgstr "بارگذاری رسانه" + +msgid "Description" +msgstr "توضیحات" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "مناسب برای کسانی که مشکل بینایی دارند و نیز درج اطّلاعات پروانه نشر" + +msgid "Content warning" +msgstr "هشدار محتوا" + +msgid "Leave it empty, if none is needed" +msgstr "اگر هیچ یک از موارد نیاز نیست، خالی بگذارید" + +msgid "File" +msgstr "پرونده" + +msgid "Send" +msgstr "بفرست" + +msgid "Your media" +msgstr "رسانه‌های شما" + +msgid "Upload" +msgstr "بارگذاری" + +msgid "You don't have any media yet." +msgstr "هنوز هیچ رسانه‌ای ندارید." + +msgid "Content warning: {0}" +msgstr "هشدار محتوا: {0}" + +msgid "Delete" +msgstr "حذف" + +msgid "Details" +msgstr "جزئیات" + +msgid "Media details" +msgstr "جزئیات رسانه" + +msgid "Go back to the gallery" +msgstr "بازگشت به نگارخانه" + +msgid "Markdown syntax" +msgstr "نحو مارک‌داون" + +msgid "Copy it into your articles, to insert this media:" +msgstr "برای درج این رسانه، این را در مقاله‌تان رونویسی کنید:" + +msgid "Use as an avatar" +msgstr "استفاده به عنوان آواتار" + +msgid "Plume" +msgstr "پلوم (Plume)" + +msgid "Menu" +msgstr "منو" + +msgid "Search" +msgstr "جستجو" + +msgid "Dashboard" +msgstr "پیش‌خوان" + +msgid "Notifications" +msgstr "اعلانات" + +msgid "Log Out" +msgstr "خروج" + +msgid "My account" +msgstr "حساب من" + +msgid "Log In" +msgstr "ورود" + +msgid "Register" +msgstr "نام‌نویسی" + +msgid "About this instance" +msgstr "دربارهٔ این نمونه" + +msgid "Privacy policy" +msgstr "سیاست حفظ حریم شخصی" + +msgid "Administration" +msgstr "مدیریت" + +msgid "Documentation" +msgstr "مستندات" + +msgid "Source code" +msgstr "کد منبع" + +msgid "Matrix room" +msgstr "اتاق گفتگوی ماتریس" + +msgid "Admin" +msgstr "مدیر" + +msgid "It is you" +msgstr "خودتان هستید" + +msgid "Edit your profile" +msgstr "نمایه خود را ویرایش کنید" + +msgid "Open on {0}" +msgstr "بازکردن روی {0}" + +msgid "Unsubscribe" +msgstr "لغو پیگیری" + +msgid "Subscribe" +msgstr "پیگیری" + +msgid "Follow {}" +msgstr "پیگیری {}" + +msgid "Log in to follow" +msgstr "برای پیگیری، وارد شوید" + +msgid "Enter your full username handle to follow" +msgstr "برای پیگیری، نام کاربری کامل خود را وارد کنید" + +msgid "{0}'s subscribers" +msgstr "دنبال‌کنندگان {0}" + +msgid "Articles" +msgstr "مقالات" + +msgid "Subscribers" +msgstr "دنبال‌کنندگان" + +msgid "Subscriptions" +msgstr "دنبال‌شوندگان" + +msgid "{0}'s subscriptions" +msgstr "دنبال‌شوندگان {0}" + +msgid "Your Dashboard" +msgstr "پیش‌خوان شما" + +msgid "Your Blogs" +msgstr "بلاگ‌های شما" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "شما هنوز هیچ بلاگی ندارید. یکی بسازید یا درخواست پیوستن به یکی را بدهید." + +msgid "Start a new blog" +msgstr "شروع یک بلاگ جدید" + +msgid "Your Drafts" +msgstr "پیش‌نویس‌های شما" + +msgid "Go to your gallery" +msgstr "رفتن به نگارخانه" + +msgid "Edit your account" +msgstr "حساب‌تان را ویرایش کنید" + +msgid "Your Profile" +msgstr "نمایهٔ شما" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "برای تغییر تصویر حساب‌تان، ابتدا آن را در نگارخانه بارگذاری کرده و سپس از همان جا انتخابش کنید." + +msgid "Upload an avatar" +msgstr "بارگذاری تصویر حساب" + +msgid "Display name" +msgstr "نام نمایشی" + +msgid "Summary" +msgstr "چکیده" + +msgid "Theme" +msgstr "پوسته" + +msgid "Default theme" +msgstr "پوستهٔ پیش‌فرض" + +msgid "Error while loading theme selector." +msgstr "خطا هنگام بار شدن گزینش‌گر پوسته." + +msgid "Never load blogs custom themes" +msgstr "هرگز پوسته‌های سفارشی بلاگ‌ها بار نشوند" + +msgid "Update account" +msgstr "به‌روزرسانی حساب" + +msgid "Danger zone" +msgstr "منطقه خطر" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "بسیار دقت کنید. هر اقدامی که انجام دهید قابل لغو کردن نخواهد بود." + +msgid "Delete your account" +msgstr "حساب‌تان را پاک کنید" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "ببخشید اما به عنوان مدیر، نمی‌توانید نمونهٔ خودتان را ترک کنید." + +msgid "Latest articles" +msgstr "آخرین مقالات" + +msgid "Atom feed" +msgstr "خوراک اتم" + +msgid "Recently boosted" +msgstr "به‌تازگی تقویت شده" + +msgid "Articles tagged \"{0}\"" +msgstr "مقالات دارای برچسب «{0}»" + +msgid "There are currently no articles with such a tag" +msgstr "در حال حاضر مقاله‌ای با این برچسب وجود ندارد" + +msgid "The content you sent can't be processed." +msgstr "محتوایی که فرستادید قابل پردازش نیست." + +msgid "Maybe it was too long." +msgstr "شاید بیش از حد طولانی بوده است." + +msgid "Internal server error" +msgstr "خطای درونی کارساز" + +msgid "Something broke on our side." +msgstr "مشکلی سمت ما پیش آمد." + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "از این بابت متاسفیم. اگر فکر می‌کنید این اتفاق ناشی از یک اشکال فنی است، لطفا آن را گزارش کنید." + +msgid "Invalid CSRF token" +msgstr "توکن CSRF نامعتبر" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "مشکلی در ارتباط با توکن CSRF ما وجود دارد. اطمینان حاصل کنید که کوکی در مرورگرتان فعّال است و سپس این صفحه را مجددا فراخوانی کنید. اگر این پیام خطا را باز هم مشاهده کردید، موضوع را گزارش کنید." + +msgid "You are not authorized." +msgstr "شما مجاز به این کار نیستید." + +msgid "Page not found" +msgstr "صفحه مورد نظر یافت نشد" + +msgid "We couldn't find this page." +msgstr "ما نتوانستیم این صفحه را بیابیم." + +msgid "The link that led you here may be broken." +msgstr "پیوندی که شما را به اینجا هدایت کرده احتمالا مشکل داشته است." + +msgid "Users" +msgstr "کاربران" + +msgid "Configuration" +msgstr "پیکربندی" + +msgid "Instances" +msgstr "نمونه‌ها" + +msgid "Email blocklist" +msgstr "فهرست مسدودی رایانامه" + +msgid "Grant admin rights" +msgstr "اعطای دسترسی مدیر" + +msgid "Revoke admin rights" +msgstr "سلب دسترسی مدیر" + +msgid "Grant moderator rights" +msgstr "اعطای دسترسی ناظم" + +msgid "Revoke moderator rights" +msgstr "سلب دسترسی دسترسی" + +msgid "Ban" +msgstr "ممنوع‌کردن" + +msgid "Run on selected users" +msgstr "روی کاربرهای انتخاب شده اجرا شود" + +msgid "Moderator" +msgstr "ناظم" + +msgid "Moderation" +msgstr "ناظمی" + +msgid "Home" +msgstr "خانه" + +msgid "Administration of {0}" +msgstr "مدیریت {0}" + +msgid "Unblock" +msgstr "رفع مسدودیت" + +msgid "Block" +msgstr "مسدود‌سازی" + +msgid "Name" +msgstr "نام" + +msgid "Allow anyone to register here" +msgstr "به همه اجازه دهید اینجا ثبت‌نام کنند" + +msgid "Short description" +msgstr "توضیحات کوتاه" + +msgid "Markdown syntax is supported" +msgstr "نحو مارک‌داون پشتیبانی می‌شود" + +msgid "Long description" +msgstr "توضیحات بلند" + +msgid "Default article license" +msgstr "پروانهٔ پیش‌فرض مقاله" + +msgid "Save these settings" +msgstr "ذخیره این تنظیمات" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "اگر شما به عنوان یک بازدیدکننده در حال مرور این پایگاه هستید، هیچ داده‌ای درباره شما گردآوری نمی‌شود." + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "به عنوان یک کاربر ثبت‌نام شده، برای آن‌که بتوانید وارد شده و مقاله بنویسید یا نظر بدهید، لازم است که نام کاربری (که لازم نیست نام واقعی شما باشد) و نشانی رایانامهٔ فعّال‌تان را ارائه کنید. محتوایی که ثبت می‌کنید، تا زمانی که خودتان آن را پاک نکنید نگه‌داری می‌شود." + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "وقتی وارد می‌شوید، ما دو کوکی ذخیره می‌کنیم. یکی برای باز نگه‌داشتن نشست جاری و دومی برای اجتناب از فعّالیت دیگران از جانب شما. ما هیچ کوکی دیگری ذخیره نمی‌کنیم." + +msgid "Blocklisted Emails" +msgstr "رایانامه‌های مسدود شده" + +msgid "Email address" +msgstr "نشانی رایانامه" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "نشانی رایانامه‌ای که می‌خواهید مسدود کنید. اگر می‌خواهید دامنه‌ها را مسدود کنید، می‌تواند از نحو گِلاب استفاده کنید. مثلاً عبارت '‎*@example.com' تمام نشانی‌ها از دامنه example.com را مسدود می‌کند" + +msgid "Note" +msgstr "یادداشت" + +msgid "Notify the user?" +msgstr "به کاربر اعلان شود؟" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "اختیاری، هنگامی که کاربر با آن نشانی تلاش برای ایجاد حساب کند، پیامی به او نشان می‌دهد" + +msgid "Blocklisting notification" +msgstr "اعلان‌ مسدودسازی" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "پیامی که هنگام تلاش کاربر برای ساخت حساب با این نشانی رایانامه به او نشان داده می‌شود" + +msgid "Add blocklisted address" +msgstr "اضافه کردن نشانی مسدودشده" + +msgid "There are no blocked emails on your instance" +msgstr "هیچ رایانامه‌ای مسدود شده‌ای در نمونهٔ شما نیست" + +msgid "Delete selected emails" +msgstr "حذف رایانامه‌های انتخاب‌شده" + +msgid "Email address:" +msgstr "نشانی رایانامه:" + +msgid "Blocklisted for:" +msgstr "مسدود شده به دلیل:" + +msgid "Will notify them on account creation with this message:" +msgstr "در زمان ساخت حساب، این پیام به کاربر اعلان خواهد شد:" + +msgid "The user will be silently prevented from making an account" +msgstr "بی سر و صدا از ساختن حساب توسط این کاربر جلوگیری خواهد شد" + +msgid "Welcome to {}" +msgstr "به {} خوش‌آمدید" + +msgid "View all" +msgstr "دیدن همه" + +msgid "About {0}" +msgstr "دربارهٔ {0}" + +msgid "Runs Plume {0}" +msgstr "در حال اجرای Plume (پلوم) {0}" + +msgid "Home to {0} people" +msgstr "میزبان {0} نفر" + +msgid "Who wrote {0} articles" +msgstr "که تاکنون {0} مقاله نوشته‌اند" + +msgid "And are connected to {0} other instances" +msgstr "و به {0} نمونه دیگر متصل‌اند" + +msgid "Administred by" +msgstr "به مدیریت" + +msgid "Interact with {}" +msgstr "تعامل با {}" + +msgid "Log in to interact" +msgstr "برای تعامل، وارد شوید" + +msgid "Enter your full username to interact" +msgstr "برای تعامل، نام کاربری‌تان را کامل وارد کنید" + +msgid "Publish" +msgstr "انتشار" + +msgid "Classic editor (any changes will be lost)" +msgstr "ویرایش‌گر کلاسیک (تمام تغییرات از دست خواهند رفت)" + +msgid "Title" +msgstr "عنوان" + +msgid "Subtitle" +msgstr "زیرعنوان" + +msgid "Content" +msgstr "محتوا" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "می‌تواند رسانه را در نگار‌خانهٔ خود بارگذاری کنید، و سپس کد مارک‌داون آن‌ها را درون مقاله‌تان درج کنید." + +msgid "Upload media" +msgstr "بارگذاری رسانه" + +msgid "Tags, separated by commas" +msgstr "برچسب‌ها، با ویرگول از هم جدا شوند" + +msgid "License" +msgstr "پروانه" + +msgid "Illustration" +msgstr "تصویر" + +msgid "This is a draft, don't publish it yet." +msgstr "این یک پیش‌نویس است، هنوز منتشرش نکنید." + +msgid "Update" +msgstr "به‌روزرسانی" + +msgid "Update, or publish" +msgstr "به‌روزرسانی، یا انتشار" + +msgid "Publish your post" +msgstr "انتشار فرسته‌تان" + +msgid "Written by {0}" +msgstr "نوشته شده توسط {0}" + +msgid "All rights reserved." +msgstr "تمامی حقوق محفوظ است." + +msgid "This article is under the {0} license." +msgstr "این مقاله تحت پروانهٔ {0} است." + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "یک پسند" +msgstr[1] "{0} پسند" + +msgid "I don't like this anymore" +msgstr "این را دیگر نمی‌پسندم" + +msgid "Add yours" +msgstr "پسندیدن خود را نشان دهید" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "یک تقویت" +msgstr[1] "{0} تقویت" + +msgid "I don't want to boost this anymore" +msgstr "دیگر نمی‌خوام این را تقویت کنم" + +msgid "Boost" +msgstr "تقویت" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "{0}وارد شوید{1}، یا {2}از حساب فدیورس خود{3} برای تعامل با این مقاله استفاده کنید" + +msgid "Comments" +msgstr "نظرات" + +msgid "Your comment" +msgstr "نظر شما" + +msgid "Submit comment" +msgstr "فرستادن نظر" + +msgid "No comments yet. Be the first to react!" +msgstr "هنوز نظری وجود ندارد. اولین کسی باشید که که واکنش نشان می‌دهد!" + +msgid "Are you sure?" +msgstr "مطمئنید؟" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "این مقاله هنوز یک پیش‌نویس است. تنها شما و دیگر نویسندگان می‌توانید آن را ببینید." + +msgid "Only you and other authors can edit this article." +msgstr "تنها شما و دیگر نویسندگان می‌توانید این مقاله را ویرایش کنید." + +msgid "Edit" +msgstr "ویرایش" + +msgid "I'm from this instance" +msgstr "من از این نمونه هستم" + +msgid "Username, or email" +msgstr "نام‌کاربری یا رایانامه" + +msgid "Log in" +msgstr "ورود" + +msgid "I'm from another instance" +msgstr "من از نمونهٔ دیگری هستم" + +msgid "Continue to your instance" +msgstr "ادامه روی نمونهٔ خودتان" + +msgid "Reset your password" +msgstr "بازنشانی گذرواژه" + +msgid "New password" +msgstr "گذرواژهٔ جدید" + +msgid "Confirmation" +msgstr "تایید" + +msgid "Update password" +msgstr "به‌روزرسانی گذرواژه" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "ما، یک رایانامه به نشانی‌ای که به ما دادید فرستاده‌ایم. با پیوندی که در آن است می‌توانید گذرواژه خود را تغییر دهید." + +msgid "Send password reset link" +msgstr "فرستادن پیوند بازنشانی گذرواژه" + +msgid "This token has expired" +msgstr "این توکن منقضی شده است" + +msgid "Please start the process again by clicking here." +msgstr "لطفاً برای شروع فرایند، اینجا کلیک کنید." + +msgid "New Blog" +msgstr "بلاگ جدید" + +msgid "Create a blog" +msgstr "ساخت یک بلاگ" + +msgid "Create blog" +msgstr "ایجاد بلاگ" + +msgid "Edit \"{}\"" +msgstr "ویرایش «{}»" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "می‌توانید تصاویرتان را در نگارخانه بارگذاری کرده و از آن‌ها به عنوان شکلک و یا تصویر سردر بلاگ استفاده کنید." + +msgid "Upload images" +msgstr "بارگذاری تصاویر" + +msgid "Blog icon" +msgstr "شکلک بلاگ" + +msgid "Blog banner" +msgstr "تصویر سردر بلاگ" + +msgid "Custom theme" +msgstr "پوسته سفارشی" + +msgid "Update blog" +msgstr "به‌روزرسانی بلاگ" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "حواس‌تان خیلی جمع باشد! هر اقدامی که اینجا انجام دهید غیرقابل بازگشت است." + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "آیا مطمئن هستید که می‌خواهید این بلاگ را برای همیشه حذف کنید؟" + +msgid "Permanently delete this blog" +msgstr "این بلاگ برای همیشه حذف شود" + +msgid "{}'s icon" +msgstr "شکلک {}" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "یک نویسنده در این بلاگ است: " +msgstr[1] "{0} نویسنده در این بلاگ هستند: " + +msgid "No posts to see here yet." +msgstr "هنوز فرسته‌ای برای دیدن وجود ندارد." + +msgid "Nothing to see here yet." +msgstr "هنوز اینجا چیزی برای دیدن نیست." + +msgid "None" +msgstr "هیچ‌کدام" + +msgid "No description" +msgstr "بدون توضیح" + +msgid "Respond" +msgstr "پاسخ" + +msgid "Delete this comment" +msgstr "این نظر را پاک کن" + +msgid "What is Plume?" +msgstr "پلوم (Plume) چیست؟" + +msgid "Plume is a decentralized blogging engine." +msgstr "پلوم یک موتور بلاگ‌نویسی غیرمتمرکز است." + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "نویسندگان می‌توانند چندین بلاگ را مدیریت کنند که هر کدام‌شان مانند یک پایگاه وب مستقل هستند." + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "مقالات در سایر نمونه‌های پلوم نیز قابل مشاهده هستند و شما می‌توانید با آن‌ها به صورت مستقیم و از دیگر بن‌سازه‌ها مانند ماستودون تعامل داشته باشید." + +msgid "Read the detailed rules" +msgstr "قوانین کامل را مطالعه کنید" + +msgid "By {0}" +msgstr "توسط {0}" + +msgid "Draft" +msgstr "پیش‌نویس" + +msgid "Search result(s) for \"{0}\"" +msgstr "نتایج جستجو برای «{0}»" + +msgid "Search result(s)" +msgstr "نتایج جستجو" + +msgid "No results for your query" +msgstr "نتیجه‌ای برای درخواست شما وجود ندارد" + +msgid "No more results for your query" +msgstr "نتیجهٔ دیگری برای درخواست شما وجود ندارد" + +msgid "Advanced search" +msgstr "جستجوی پیشرفته" + +msgid "Article title matching these words" +msgstr "عنوان مقاله با این واژگان انطباق داشته باشد" + +msgid "Subtitle matching these words" +msgstr "زیرعنوان با این واژگان انطباق داشته باشد" + +msgid "Content macthing these words" +msgstr "محتوای منطبق با این واژگان" + +msgid "Body content" +msgstr "متن اصلی" + +msgid "From this date" +msgstr "از تاریخ" + +msgid "To this date" +msgstr "تا این تاریخ" + +msgid "Containing these tags" +msgstr "حاوی این برچسب‌ها" + +msgid "Tags" +msgstr "برچسب‌ها" + +msgid "Posted on one of these instances" +msgstr "فرستاده شده روی یکی از این نمونه‌ها" + +msgid "Instance domain" +msgstr "دامنهٔ نمونه" + +msgid "Posted by one of these authors" +msgstr "فرستاده شده توسط یکی از این نویسندگان" + +msgid "Author(s)" +msgstr "نویسنده(ها)" + +msgid "Posted on one of these blogs" +msgstr "فرستاده شده روی یکی از این بلاگ‌ها" + +msgid "Blog title" +msgstr "عنوان بلاگ" + +msgid "Written in this language" +msgstr "نوشته شده به این زبان" + +msgid "Language" +msgstr "زبان" + +msgid "Published under this license" +msgstr "منتشر شده تحت این پروانه" + +msgid "Article license" +msgstr "پروانهٔ مقاله" + diff --git a/po/plume/fi.po b/po/plume/fi.po new file mode 100644 index 00000000000..a279a86b544 --- /dev/null +++ b/po/plume/fi.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Finnish\n" +"Language: fi_FI\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: fi\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} kommentoi mediaasi." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "Sinulla on {0} tilaajaa." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} tykkää artikkeleistasi." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} on maininnut sinut." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "" + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "Artikkelivirtasi" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "Paikallinen artikkelivirta" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "Yhdistetty artikkelivirta" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "{0}n avatar" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "Valinnainen" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "Luodaksesi blogin sinun tulee olla sisäänkirjautuneena" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "Saman niminen blogi on jo olemassa." + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "Blogisi luotiin onnistuneesti!" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "Blogisi poistettiin." + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "Sinulla ei ole oikeutta poistaa tätä blogia." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "Sinulla ei ole oikeutta muokata tätä blogia." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "Et voi käyttää tätä mediaa blogin ikonina." + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "Et voi käyttää tätä mediaa blogin bannerina." + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "Blogisi tiedot on päivitetty." + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "Kommentisi lähetettiin." + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "Sisältösi poistettiin." + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "Instanssin asetukset on tallennettu." + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "Tykätäksesi postauksesta sinun tulee olla sisäänkirjautuneena" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "Mediasi on poistettu." + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "Sinulla ei ole oikeutta poistaa tätä mediaa." + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "Avatarisi on päivitetty." + +# src/routes/medias.rs:180 +msgid "You are not allowed to use this media." +msgstr "Sinulla ei ole oikeutta käyttää tätä mediaa." + +# src/routes/notifications.rs:29 +msgid "To see your notifications, you need to be logged in" +msgstr "Nähdäksesi ilmoituksesi sinun tulee olla sisäänkirjautuneena" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "Tätä postausta ei ole vielä julkaistu." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "Kirjoittaaksesi uuden postauksen sinun tulee olla sisäänkirjautuneena" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "Et ole tämän blogin kirjoittaja." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "Uusi postaus" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "Muokkaa {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "Sinulla ei ole oikeutta julkaista tällä blogilla." + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "Artikkeli päivitetty." + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "Artikkeli on tallennettu." + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "Uusi artikkeli" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "Sinulla ei ole oikeutta poistaa tätä artikkelia." + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "Artikkelisi on poistettu." + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "Näyttää siltä, että koetat poistaa artikkelia jota ei ole olemassa. Ehkä se on jo poistettu?" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "Tunnuksestasi ei saatu haettua tarpeeksi tietoja. Varmistathan että käyttäjätunkuksesi on oikein." + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "Uudelleenjakaaksesi postauksen sinun tulee olla sisäänkirjatuneena" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "Olette nyt yhdistetty." + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "" + +msgid "Create an account" +msgstr "" + +msgid "Email" +msgstr "" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Your media" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "" + +msgid "Menu" +msgstr "" + +msgid "Search" +msgstr "" + +msgid "Dashboard" +msgstr "" + +msgid "Notifications" +msgstr "" + +msgid "Log Out" +msgstr "" + +msgid "My account" +msgstr "" + +msgid "Log In" +msgstr "" + +msgid "Register" +msgstr "" + +msgid "About this instance" +msgstr "" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "" + +msgid "It is you" +msgstr "" + +msgid "Edit your profile" +msgstr "" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "" + +msgid "Follow {}" +msgstr "Seuraa {}" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "" + +msgid "Subscribers" +msgstr "" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "" + +msgid "Your Drafts" +msgstr "" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "" + +msgid "Your Profile" +msgstr "" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "" + +msgid "Configuration" +msgstr "" + +msgid "Instances" +msgstr "" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "" + +msgid "Unblock" +msgstr "" + +msgid "Block" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Allow anyone to register here" +msgstr "Salli kenen tahansa rekisteröityä tänne" + +msgid "Short description" +msgstr "Lyhyt kuvaus" + +msgid "Markdown syntax is supported" +msgstr "Markdown on tuettu" + +msgid "Long description" +msgstr "Pitkä kuvaus" + +msgid "Default article license" +msgstr "Oletus lisenssi artikelleille" + +msgid "Save these settings" +msgstr "Tallenna nämä asetukset" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "Jos aelailet tätä sivua vierailijana, sinusta ei kerätä yhtään dataa." + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "Tietoja {0}" + +msgid "Runs Plume {0}" +msgstr "Plumen versio {0}" + +msgid "Home to {0} people" +msgstr "{0} ihmisen koti" + +msgid "Who wrote {0} articles" +msgstr "Joka on kirjoittanut {0} artikkelia" + +msgid "And are connected to {0} other instances" +msgstr "Ja on yhdistetty {0} toiseen instanssiin" + +msgid "Administred by" +msgstr "Ylläpitäjä" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +msgid "Tags, separated by commas" +msgstr "" + +msgid "License" +msgstr "" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "" +msgstr[1] "" + +msgid "I don't like this anymore" +msgstr "" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" +msgstr[1] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "" + +msgid "Comments" +msgstr "" + +msgid "Your comment" +msgstr "" + +msgid "Submit comment" +msgstr "" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "" + +msgid "Log in" +msgstr "" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +msgid "New password" +msgstr "" + +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "" + +msgid "Create a blog" +msgstr "" + +msgid "Create blog" +msgstr "" + +msgid "Edit \"{}\"" +msgstr "" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" +msgstr[1] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +msgid "From this date" +msgstr "" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" + diff --git a/po/plume/fr.po b/po/plume/fr.po new file mode 100644 index 00000000000..d067adf8ed0 --- /dev/null +++ b/po/plume/fr.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: French\n" +"Language: fr_FR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: fr\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} a commenté votre article." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} vous suit." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} a aimé votre article." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} vous a mentionné." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} a boosté votre article." + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "Votre flux" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "Flux local" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "Flux fédéré" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "Avatar de {0}" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "Page précédente" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "Page suivante" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "Optionnel" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "Vous devez vous connecter pour créer un nouveau blog" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "Un blog avec le même nom existe déjà." + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "Votre blog a été créé avec succès !" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "Votre blog a été supprimé." + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "Vous n'êtes pas autorisé⋅e à supprimer ce blog." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "Vous n'êtes pas autorisé à éditer ce blog." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "Vous ne pouvez pas utiliser ce media comme icône de blog." + +# src/routes/blogs.rs:298 +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:332 +msgid "Your blog information have been updated." +msgstr "Les informations de votre blog ont été mise à jour." + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "Votre commentaire a été publié." + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "Votre commentaire a été supprimé." + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "Les inscriptions sont fermées sur cette instance." + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "Votre compte a été créé. Vous avez juste à vous connecter, avant de pouvoir l'utiliser." + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "Les paramètres de votre instance ont été enregistrés." + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "{} a été débloqué⋅e." + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "{} a été bloqué⋅e." + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "Blocages supprimés" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "E-mail déjà bloqué" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "E-mail bloqué" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "Vous ne pouvez pas changer vos propres droits." + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "Vous n'avez pas l'autorisation d'effectuer cette action." + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "Terminé." + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "Vous devez vous connecter pour aimer un article" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "Votre média a été supprimé." + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "Vous n'êtes pas autorisé à supprimer ce média." + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "Votre avatar a été mis à jour." + +# src/routes/medias.rs:180 +msgid "You are not allowed to use this media." +msgstr "Vous n'êtes pas autorisé à utiliser ce média." + +# 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" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "Cet article n’est pas encore publié." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "Vous devez vous connecter pour écrire un nouvel article" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "Vous n'êtes pas auteur⋅rice de ce blog." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "Nouvel article" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "Modifier {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "Vous n'êtes pas autorisé à publier sur ce blog." + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "Votre article a été mis à jour." + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "Votre article a été enregistré." + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "Nouvel article" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "Vous n'êtes pas autorisé à supprimer cet article." + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "Votre article a été supprimé." + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "Il semble que l'article que vous avez essayé de supprimer n'existe pas. Peut-être a-t-il déjà été supprimé ?" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "Nous n'avons pas pu obtenir assez d'informations à propos de votre compte. Veuillez vous assurer que votre nom d'utilisateur est correct." + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "Vous devez vous connecter pour partager un article" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "Vous êtes maintenant connecté." + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "Vous êtes maintenant déconnecté." + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "Réinitialisation du mot de passe" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "Voici le lien pour réinitialiser votre mot de passe : {0}" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "Votre mot de passe a été réinitialisé avec succès." + +# src/routes/user.rs:87 +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:109 +msgid "You are no longer following {}." +msgstr "Vous ne suivez plus {}." + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "Vous suivez maintenant {}." + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "Vous devez vous connecter pour vous abonner à quelqu'un" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "Vous devez vous connecter pour modifier votre profil" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "Votre profil a été mis à jour." + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "Votre compte a été supprimé." + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "Vous ne pouvez pas supprimer le compte d'une autre personne." + +msgid "Create your account" +msgstr "Créer votre compte" + +msgid "Create an account" +msgstr "Créer un compte" + +msgid "Email" +msgstr "Adresse électronique" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "Désolé, mais les inscriptions sont fermées sur cette instance en particulier. Vous pouvez, toutefois, en trouver une autre." + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "Vérifiez votre boîte de réception !" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "Nom d’utilisateur" + +msgid "Password" +msgstr "Mot de passe" + +msgid "Password confirmation" +msgstr "Confirmation du mot de passe" + +msgid "Media upload" +msgstr "Téléversement de média" + +msgid "Description" +msgstr "Description" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "Utile pour les personnes malvoyantes, ainsi que pour les informations de licence" + +msgid "Content warning" +msgstr "Avertissement" + +msgid "Leave it empty, if none is needed" +msgstr "Laissez vide, si aucun avertissement n'est nécessaire" + +msgid "File" +msgstr "Fichier" + +msgid "Send" +msgstr "Envoyer" + +msgid "Your media" +msgstr "Vos médias" + +msgid "Upload" +msgstr "Téléverser" + +msgid "You don't have any media yet." +msgstr "Vous n'avez pas encore de média." + +msgid "Content warning: {0}" +msgstr "Avertissement du contenu : {0}" + +msgid "Delete" +msgstr "Supprimer" + +msgid "Details" +msgstr "Détails" + +msgid "Media details" +msgstr "Détails du média" + +msgid "Go back to the gallery" +msgstr "Revenir à la galerie" + +msgid "Markdown syntax" +msgstr "Syntaxe markdown" + +msgid "Copy it into your articles, to insert this media:" +msgstr "Copiez-le dans vos articles, pour insérer ce média :" + +msgid "Use as an avatar" +msgstr "Utiliser comme avatar" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "Menu" + +msgid "Search" +msgstr "Rechercher" + +msgid "Dashboard" +msgstr "Tableau de bord" + +msgid "Notifications" +msgstr "Notifications" + +msgid "Log Out" +msgstr "Se déconnecter" + +msgid "My account" +msgstr "Mon compte" + +msgid "Log In" +msgstr "Se connecter" + +msgid "Register" +msgstr "S’inscrire" + +msgid "About this instance" +msgstr "À propos de cette instance" + +msgid "Privacy policy" +msgstr "Politique de confidentialité" + +msgid "Administration" +msgstr "Administration" + +msgid "Documentation" +msgstr "Documentation" + +msgid "Source code" +msgstr "Code source" + +msgid "Matrix room" +msgstr "Salon Matrix" + +msgid "Admin" +msgstr "Administrateur" + +msgid "It is you" +msgstr "C'est vous" + +msgid "Edit your profile" +msgstr "Modifier votre profil" + +msgid "Open on {0}" +msgstr "Ouvrir sur {0}" + +msgid "Unsubscribe" +msgstr "Se désabonner" + +msgid "Subscribe" +msgstr "S'abonner" + +msgid "Follow {}" +msgstr "Suivre {}" + +msgid "Log in to follow" +msgstr "Connectez-vous pour suivre" + +msgid "Enter your full username handle to follow" +msgstr "Entrez votre nom d'utilisateur complet pour suivre" + +msgid "{0}'s subscribers" +msgstr "{0}'s abonnés" + +msgid "Articles" +msgstr "Articles" + +msgid "Subscribers" +msgstr "Abonnés" + +msgid "Subscriptions" +msgstr "Abonnements" + +msgid "{0}'s subscriptions" +msgstr "Abonnements de {0}" + +msgid "Your Dashboard" +msgstr "Votre tableau de bord" + +msgid "Your Blogs" +msgstr "Vos Blogs" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "Vous n'avez pas encore de blog. Créez votre propre blog, ou demandez de vous joindre à un." + +msgid "Start a new blog" +msgstr "Commencer un nouveau blog" + +msgid "Your Drafts" +msgstr "Vos brouillons" + +msgid "Go to your gallery" +msgstr "Aller à votre galerie" + +msgid "Edit your account" +msgstr "Modifier votre compte" + +msgid "Your Profile" +msgstr "Votre Profil" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "Pour modifier votre avatar, téléversez-le dans votre galerie puis sélectionnez-le à partir de là." + +msgid "Upload an avatar" +msgstr "Téléverser un avatar" + +msgid "Display name" +msgstr "Nom affiché" + +msgid "Summary" +msgstr "Description" + +msgid "Theme" +msgstr "Thème" + +msgid "Default theme" +msgstr "Thème par défaut" + +msgid "Error while loading theme selector." +msgstr "Erreur lors du chargement du sélecteur de thème." + +msgid "Never load blogs custom themes" +msgstr "Ne jamais charger les thèmes personnalisés des blogs" + +msgid "Update account" +msgstr "Mettre à jour le compte" + +msgid "Danger zone" +msgstr "Zone à risque" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "Attention, toute action prise ici ne peut pas être annulée." + +msgid "Delete your account" +msgstr "Supprimer votre compte" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "Désolé, mais en tant qu'administrateur, vous ne pouvez pas quitter votre propre instance." + +msgid "Latest articles" +msgstr "Derniers articles" + +msgid "Atom feed" +msgstr "Flux atom" + +msgid "Recently boosted" +msgstr "Récemment partagé" + +msgid "Articles tagged \"{0}\"" +msgstr "Articles marqués \"{0}\"" + +msgid "There are currently no articles with such a tag" +msgstr "Il n'y a actuellement aucun article avec un tel tag" + +msgid "The content you sent can't be processed." +msgstr "Le contenu que vous avez envoyé ne peut pas être traité." + +msgid "Maybe it was too long." +msgstr "Peut-être que c’était trop long." + +msgid "Internal server error" +msgstr "Erreur interne du serveur" + +msgid "Something broke on our side." +msgstr "Nous avons cassé quelque chose." + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "Nous sommes désolé⋅e⋅s. Si vous pensez que c’est un bogue, merci de le signaler." + +msgid "Invalid CSRF token" +msgstr "Jeton CSRF invalide" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "Quelque chose ne va pas avec votre jeton CSRF. Assurez-vous que les cookies sont activés dans votre navigateur, et essayez de recharger cette page. Si vous continuez à voir cette erreur, merci de la signaler." + +msgid "You are not authorized." +msgstr "Vous n’avez pas les droits." + +msgid "Page not found" +msgstr "Page non trouvée" + +msgid "We couldn't find this page." +msgstr "Page introuvable." + +msgid "The link that led you here may be broken." +msgstr "Vous avez probablement suivi un lien cassé." + +msgid "Users" +msgstr "Utilisateurs" + +msgid "Configuration" +msgstr "Configuration" + +msgid "Instances" +msgstr "Instances" + +msgid "Email blocklist" +msgstr "Liste des e-mails proscrits" + +msgid "Grant admin rights" +msgstr "Accorder les droits d'administration" + +msgid "Revoke admin rights" +msgstr "Révoquer les droits d'administration" + +msgid "Grant moderator rights" +msgstr "Accorder les droits de modération" + +msgid "Revoke moderator rights" +msgstr "Révoquer les droits de modération" + +msgid "Ban" +msgstr "Bannir" + +msgid "Run on selected users" +msgstr "Appliquer aux utilisateurices sélectionné⋅e⋅s" + +msgid "Moderator" +msgstr "Modérateurice" + +msgid "Moderation" +msgstr "Modération" + +msgid "Home" +msgstr "Accueil" + +msgid "Administration of {0}" +msgstr "Administration de {0}" + +msgid "Unblock" +msgstr "Débloquer" + +msgid "Block" +msgstr "Bloquer" + +msgid "Name" +msgstr "Nom" + +msgid "Allow anyone to register here" +msgstr "Permettre à tous de s'enregistrer" + +msgid "Short description" +msgstr "Description courte" + +msgid "Markdown syntax is supported" +msgstr "La syntaxe Markdown est supportée" + +msgid "Long description" +msgstr "Description longue" + +msgid "Default article license" +msgstr "Licence d'article par défaut" + +msgid "Save these settings" +msgstr "Sauvegarder ces paramètres" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "Si vous naviguez sur ce site en tant que visiteur, aucune donnée ne vous concernant n'est collectée." + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "En tant qu'utilisateur enregistré, vous devez fournir votre nom d'utilisateur (qui n'est pas forcément votre vrai nom), votre adresse e-mail fonctionnelle et un mot de passe, afin de pouvoir vous connecter, écrire des articles et commenter. Le contenu que vous soumettez est stocké jusqu'à ce que vous le supprimiez." + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "Lorsque vous vous connectez, nous stockons deux cookies, l'un pour garder votre session ouverte, le second pour empêcher d'autres personnes d'agir en votre nom. Nous ne stockons aucun autre cookie." + +msgid "Blocklisted Emails" +msgstr "Emails bloqués" + +msgid "Email address" +msgstr "Adresse e-mail" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "L'adresse e-mail que vous souhaitez bloquer. Pour bloquer des domaines, vous pouvez utiliser la syntaxe : '*@example.com' qui bloque toutes les adresses du domaine exemple.com" + +msgid "Note" +msgstr "Note" + +msgid "Notify the user?" +msgstr "Notifier l'utilisateurice ?" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "Optionnel, affiche un message lors de la création d'un compte avec cette adresse" + +msgid "Blocklisting notification" +msgstr "Notification de blocage" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "Le message à afficher lors de la création d'un compte avec cette adresse e-mail" + +msgid "Add blocklisted address" +msgstr "Bloquer une adresse" + +msgid "There are no blocked emails on your instance" +msgstr "Il n'y a pas d'adresses bloquées sur votre instance" + +msgid "Delete selected emails" +msgstr "Supprimer le(s) adresse(s) sélectionnée(s)" + +msgid "Email address:" +msgstr "Adresse e-mail:" + +msgid "Blocklisted for:" +msgstr "Bloqué pour :" + +msgid "Will notify them on account creation with this message:" +msgstr "Avertissement lors de la création du compte avec ce message :" + +msgid "The user will be silently prevented from making an account" +msgstr "L'utilisateurice sera silencieusement empêché.e de créer un compte" + +msgid "Welcome to {}" +msgstr "Bienvenue sur {0}" + +msgid "View all" +msgstr "Tout afficher" + +msgid "About {0}" +msgstr "À propos de {0}" + +msgid "Runs Plume {0}" +msgstr "Propulsé par Plume {0}" + +msgid "Home to {0} people" +msgstr "Refuge de {0} personnes" + +msgid "Who wrote {0} articles" +msgstr "Qui ont écrit {0} articles" + +msgid "And are connected to {0} other instances" +msgstr "Et sont connecté⋅es à {0} autres instances" + +msgid "Administred by" +msgstr "Administré par" + +msgid "Interact with {}" +msgstr "Interagir avec {}" + +msgid "Log in to interact" +msgstr "Connectez-vous pour interagir" + +msgid "Enter your full username to interact" +msgstr "Entrez votre nom d'utilisateur complet pour interagir" + +msgid "Publish" +msgstr "Publier" + +msgid "Classic editor (any changes will be lost)" +msgstr "Éditeur classique (tout changement sera perdu)" + +msgid "Title" +msgstr "Titre" + +msgid "Subtitle" +msgstr "Sous-titre" + +msgid "Content" +msgstr "Contenu" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "Vous pouvez télécharger des médias dans votre galerie, et copier leur code Markdown dans vos articles pour les insérer." + +msgid "Upload media" +msgstr "Téléverser un média" + +msgid "Tags, separated by commas" +msgstr "Mots-clés, séparés par des virgules" + +msgid "License" +msgstr "Licence" + +msgid "Illustration" +msgstr "Illustration" + +msgid "This is a draft, don't publish it yet." +msgstr "C'est un brouillon, ne le publiez pas encore." + +msgid "Update" +msgstr "Mettre à jour" + +msgid "Update, or publish" +msgstr "Mettre à jour, ou publier" + +msgid "Publish your post" +msgstr "Publiez votre message" + +msgid "Written by {0}" +msgstr "Écrit par {0}" + +msgid "All rights reserved." +msgstr "Tous droits réservés." + +msgid "This article is under the {0} license." +msgstr "Cet article est sous licence {0}." + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "Un like" +msgstr[1] "{0} likes" + +msgid "I don't like this anymore" +msgstr "Je n'aime plus cela" + +msgid "Add yours" +msgstr "Ajouter le vôtre" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "Un repartage" +msgstr[1] "{0} repartages" + +msgid "I don't want to boost this anymore" +msgstr "Je ne veux plus le repartager" + +msgid "Boost" +msgstr "Partager" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "{0}Connectez-vous{1}, ou {2}utilisez votre compte sur le Fediverse{3} pour interagir avec cet article" + +msgid "Comments" +msgstr "Commentaires" + +msgid "Your comment" +msgstr "Votre commentaire" + +msgid "Submit comment" +msgstr "Soumettre le commentaire" + +msgid "No comments yet. Be the first to react!" +msgstr "Pas encore de commentaires. Soyez le premier à réagir !" + +msgid "Are you sure?" +msgstr "Êtes-vous sûr⋅e ?" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "Cet article est toujours un brouillon. Seuls vous et les autres auteur·e·s peuvent le voir." + +msgid "Only you and other authors can edit this article." +msgstr "Seuls vous et les autres auteur·e·s peuvent modifier cet article." + +msgid "Edit" +msgstr "Modifier" + +msgid "I'm from this instance" +msgstr "Je suis de cette instance" + +msgid "Username, or email" +msgstr "Nom d'utilisateur⋅ice ou e-mail" + +msgid "Log in" +msgstr "Se connecter" + +msgid "I'm from another instance" +msgstr "Je viens d'une autre instance" + +msgid "Continue to your instance" +msgstr "Continuez sur votre instance" + +msgid "Reset your password" +msgstr "Réinitialiser votre mot de passe" + +msgid "New password" +msgstr "Nouveau mot de passe" + +msgid "Confirmation" +msgstr "Confirmation" + +msgid "Update password" +msgstr "Mettre à jour le mot de passe" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "Nous avons envoyé un mail à l'adresse que vous nous avez donnée, avec un lien pour réinitialiser votre mot de passe." + +msgid "Send password reset link" +msgstr "Envoyer un lien pour réinitialiser le mot de passe" + +msgid "This token has expired" +msgstr "Ce jeton a expiré" + +msgid "Please start the process again by clicking here." +msgstr "Veuillez recommencer le processus en cliquant ici." + +msgid "New Blog" +msgstr "Nouveau Blog" + +msgid "Create a blog" +msgstr "Créer un blog" + +msgid "Create blog" +msgstr "Créer le blog" + +msgid "Edit \"{}\"" +msgstr "Modifier \"{}\"" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "Vous pouvez téléverser des images dans votre galerie, pour les utiliser comme icônes de blog ou illustrations." + +msgid "Upload images" +msgstr "Téléverser des images" + +msgid "Blog icon" +msgstr "Icône de blog" + +msgid "Blog banner" +msgstr "Bannière de blog" + +msgid "Custom theme" +msgstr "Thème personnalisé" + +msgid "Update blog" +msgstr "Mettre à jour le blog" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "Attention, toute action prise ici ne peut pas être annulée." + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "Êtes-vous sûr de vouloir supprimer définitivement ce blog ?" + +msgid "Permanently delete this blog" +msgstr "Supprimer définitivement ce blog" + +msgid "{}'s icon" +msgstr "icône de {}" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "Il y a un auteur sur ce blog: " +msgstr[1] "Il y a {0} auteurs sur ce blog: " + +msgid "No posts to see here yet." +msgstr "Aucun article pour le moment." + +msgid "Nothing to see here yet." +msgstr "Rien à voir ici pour le moment." + +msgid "None" +msgstr "Aucun" + +msgid "No description" +msgstr "Aucune description" + +msgid "Respond" +msgstr "Répondre" + +msgid "Delete this comment" +msgstr "Supprimer ce commentaire" + +msgid "What is Plume?" +msgstr "Qu’est-ce que Plume ?" + +msgid "Plume is a decentralized blogging engine." +msgstr "Plume est un moteur de blog décentralisé." + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "Les auteurs peuvent avoir plusieurs blogs, chacun étant comme un site indépendant." + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "Les articles sont également visibles sur d'autres instances Plume, et vous pouvez interagir avec eux directement à partir d'autres plateformes comme Mastodon." + +msgid "Read the detailed rules" +msgstr "Lire les règles détaillées" + +msgid "By {0}" +msgstr "Par {0}" + +msgid "Draft" +msgstr "Brouillon" + +msgid "Search result(s) for \"{0}\"" +msgstr "Résultat(s) de la recherche pour \"{0}\"" + +msgid "Search result(s)" +msgstr "Résultat(s) de la recherche" + +msgid "No results for your query" +msgstr "Pas de résultat pour votre requête" + +msgid "No more results for your query" +msgstr "Plus de résultats pour votre recherche" + +msgid "Advanced search" +msgstr "Recherche avancée" + +msgid "Article title matching these words" +msgstr "Titre contenant ces mots" + +msgid "Subtitle matching these words" +msgstr "Sous-titre contenant ces mots" + +msgid "Content macthing these words" +msgstr "Contenu correspondant à ces mots" + +msgid "Body content" +msgstr "Texte" + +msgid "From this date" +msgstr "À partir de cette date" + +msgid "To this date" +msgstr "Avant cette date" + +msgid "Containing these tags" +msgstr "Avec ces étiquettes" + +msgid "Tags" +msgstr "Étiquettes" + +msgid "Posted on one of these instances" +msgstr "Publié sur une de ces instances" + +msgid "Instance domain" +msgstr "Domaine d'une instance" + +msgid "Posted by one of these authors" +msgstr "Écrit par un de ces auteur⋅ices" + +msgid "Author(s)" +msgstr "Auteur·e(s)" + +msgid "Posted on one of these blogs" +msgstr "Publié dans un de ces blogs" + +msgid "Blog title" +msgstr "Nom du blog" + +msgid "Written in this language" +msgstr "Écrit en" + +msgid "Language" +msgstr "Langue" + +msgid "Published under this license" +msgstr "Placé sous cette licence" + +msgid "Article license" +msgstr "Licence de l'article" + diff --git a/po/plume/gl.po b/po/plume/gl.po new file mode 100644 index 00000000000..5c6b1cac70a --- /dev/null +++ b/po/plume/gl.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-26 13:16\n" +"Last-Translator: \n" +"Language-Team: Galician\n" +"Language: gl_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: gl\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "Alguén" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} comentou o teu artigo." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} está suscrita aos teus artigos." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "a {0} gustoulle o teu artigo." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} mencionoute." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} promoveu o teu artigo." + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "O seu contido" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "Contido local" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "Contido federado" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "Avatar de {0}" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "Páxina anterior" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "Páxina seguinte" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "Opcional" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "Para crear un novo blog debes estar conectada" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "Xa existe un blog co mesmo nome." + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "O teu blog creouse correctamente!" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "Eliminaches o blog." + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "Non tes permiso para eliminar este blog." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "Non podes editar este blog." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "Non podes utilizar este medio como icona do blog." + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "Non podes utilizar este medio como cabeceira do blog." + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "Actualizouse a información sobre o blog." + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "O teu comentario foi publicado." + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "Eliminouse o comentario." + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "O rexistro está pechado en esta instancia." + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "Rexistro de usuarias" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "Aquí tes a ligazón para crear a conta: {0}" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "Creouse a túa conta. Agora só tes que conectarte para poder utilizala." + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "Gardáronse os axustes das instancia." + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "{} foi desbloqueada." + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "{} foi bloqueada." + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "Bloqueos eliminados" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "O email xa está bloqueado" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "Email bloqueado" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "Non podes cambiar os teus propios permisos." + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "Non tes permiso para realizar esta acción." + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "Feito." + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "Para darlle a gústame, debes estar conectada" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "Eliminouse o ficheiro de medios." + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "Non tes permiso para eliminar este ficheiro." + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "Actualizouse o avatar." + +# src/routes/medias.rs:180 +msgid "You are not allowed to use this media." +msgstr "Non tes permiso para usar este ficheiro de medios." + +# src/routes/notifications.rs:29 +msgid "To see your notifications, you need to be logged in" +msgstr "Para ver as túas notificacións, debes estar conectada" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "Esto é un borrador, non publicar por agora." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "Para escribir un novo artigo, debes estar conectada" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "Non es autora de este blog." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "Novo artigo" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "Editar {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "Non tes permiso para publicar en este blog." + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "Actualizouse o artigo." + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "Gardouse o artigo." + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "Novo artigo" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "Non tes permiso para eliminar este artigo." + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "Eliminouse o artigo." + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "Semella que o artigo que quere eliminar non existe. Igual xa foi eliminado?" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "Non se puido obter información suficiente sobre a súa conta. Por favor asegúrese de que o nome de usuaria é correcto." + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "Para compartir un artigo, debe estar conectada" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "Está conectada." + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "Está desconectada." + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "Restablecer contrasinal" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "Aquí está a ligazón para restablecer o contrasinal: {0}" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "O contrasinal restableceuse correctamente." + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "Para acceder ao taboleiro, debes estar conectada" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "Xa non está a seguir a {}." + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "Está a seguir a {}." + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "Para suscribirse a un blog, debe estar conectada" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "Para editar o seu perfil, debe estar conectada" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "Actualizouse o perfil." + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "Eliminouse a túa conta." + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "Non pode eliminar a conta de outra persoa." + +msgid "Create your account" +msgstr "Cree a súa conta" + +msgid "Create an account" +msgstr "Crear unha conta" + +msgid "Email" +msgstr "Correo-e" + +msgid "Email confirmation" +msgstr "Email de confirmación" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "Desculpe, pero o rexistro en esta instancia está pechado. Porén pode atopar outra no fediverso." + +msgid "Registration" +msgstr "Rexistro" + +msgid "Check your inbox!" +msgstr "Comprobe o seu correo!" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "Enviamos un email ao enderezo indicado cunha ligazón para crear a conta." + +msgid "Username" +msgstr "Nome de usuaria" + +msgid "Password" +msgstr "Contrasinal" + +msgid "Password confirmation" +msgstr "Confirmación do contrasinal" + +msgid "Media upload" +msgstr "Subir medios" + +msgid "Description" +msgstr "Descrición" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "Útil para persoas con deficiencias visuais, así como información da licenza" + +msgid "Content warning" +msgstr "Aviso sobre o contido" + +msgid "Leave it empty, if none is needed" +msgstr "Deixar baldeiro se non precisa ningunha" + +msgid "File" +msgstr "Ficheiro" + +msgid "Send" +msgstr "Enviar" + +msgid "Your media" +msgstr "O teu multimedia" + +msgid "Upload" +msgstr "Subir" + +msgid "You don't have any media yet." +msgstr "Aínda non subeu ficheiros de medios." + +msgid "Content warning: {0}" +msgstr "Aviso de contido: {0}" + +msgid "Delete" +msgstr "Eliminar" + +msgid "Details" +msgstr "Detalles" + +msgid "Media details" +msgstr "Detalle dos medios" + +msgid "Go back to the gallery" +msgstr "Voltar a galería" + +msgid "Markdown syntax" +msgstr "Sintaxe Markdown" + +msgid "Copy it into your articles, to insert this media:" +msgstr "Copie e pegue este código para incrustar no artigo:" + +msgid "Use as an avatar" +msgstr "Utilizar como avatar" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "Menú" + +msgid "Search" +msgstr "Buscar" + +msgid "Dashboard" +msgstr "Taboleiro" + +msgid "Notifications" +msgstr "Notificacións" + +msgid "Log Out" +msgstr "Desconectar" + +msgid "My account" +msgstr "A miña conta" + +msgid "Log In" +msgstr "Conectar" + +msgid "Register" +msgstr "Rexistrar" + +msgid "About this instance" +msgstr "Sobre esta instancia" + +msgid "Privacy policy" +msgstr "Política de intimidade" + +msgid "Administration" +msgstr "Administración" + +msgid "Documentation" +msgstr "Documentación" + +msgid "Source code" +msgstr "Código fonte" + +msgid "Matrix room" +msgstr "Sala Matrix" + +msgid "Admin" +msgstr "Admin" + +msgid "It is you" +msgstr "Es ti" + +msgid "Edit your profile" +msgstr "Edita o teu perfil" + +msgid "Open on {0}" +msgstr "Aberto en {0}" + +msgid "Unsubscribe" +msgstr "Cancelar subscrición" + +msgid "Subscribe" +msgstr "Subscribirse" + +msgid "Follow {}" +msgstr "Seguimento {}" + +msgid "Log in to follow" +msgstr "Conéctese para seguir" + +msgid "Enter your full username handle to follow" +msgstr "Introduza o se nome de usuaria completo para continuar" + +msgid "{0}'s subscribers" +msgstr "Subscritoras de {0}" + +msgid "Articles" +msgstr "Artigos" + +msgid "Subscribers" +msgstr "Subscritoras" + +msgid "Subscriptions" +msgstr "Subscricións" + +msgid "{0}'s subscriptions" +msgstr "Suscricións de {0}" + +msgid "Your Dashboard" +msgstr "O teu taboleiro" + +msgid "Your Blogs" +msgstr "Os teus Blogs" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "Aínda non ten blogs. Publique un de seu ou ben solicite unirse a un." + +msgid "Start a new blog" +msgstr "Iniciar un blog" + +msgid "Your Drafts" +msgstr "Os teus Borradores" + +msgid "Go to your gallery" +msgstr "Ir a súa galería" + +msgid "Edit your account" +msgstr "Edite a súa conta" + +msgid "Your Profile" +msgstr "O seu Perfil" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "Para cambiar o avatar, suba a imaxe a súa galería e despois escollaa desde alí." + +msgid "Upload an avatar" +msgstr "Subir un avatar" + +msgid "Display name" +msgstr "Mostrar nome" + +msgid "Summary" +msgstr "Resumen" + +msgid "Theme" +msgstr "Decorado" + +msgid "Default theme" +msgstr "Decorado por omisión" + +msgid "Error while loading theme selector." +msgstr "Erro ao cargar o selector de decorados." + +msgid "Never load blogs custom themes" +msgstr "Non cargar decorados de blog personalizados" + +msgid "Update account" +msgstr "Actualizar conta" + +msgid "Danger zone" +msgstr "Zona perigosa" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "Teña tino, todo o que faga aquí non se pode retrotraer." + +msgid "Delete your account" +msgstr "Eliminar a súa conta" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "Lamentámolo, pero como administradora, non pode deixar a súa propia instancia." + +msgid "Latest articles" +msgstr "Últimos artigos" + +msgid "Atom feed" +msgstr "Fonte Atom" + +msgid "Recently boosted" +msgstr "Promocionada recentemente" + +msgid "Articles tagged \"{0}\"" +msgstr "Artigos etiquetados \"{0}\"" + +msgid "There are currently no articles with such a tag" +msgstr "Non hai artigos con esa etiqueta" + +msgid "The content you sent can't be processed." +msgstr "O contido que enviou non se pode procesar." + +msgid "Maybe it was too long." +msgstr "Pode que sexa demasiado longo." + +msgid "Internal server error" +msgstr "Fallo interno do servidor" + +msgid "Something broke on our side." +msgstr "Algo fallou pola nosa parte" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "Lamentálmolo. Si cree que é un bug, infórmenos por favor." + +msgid "Invalid CSRF token" +msgstr "Testemuño CSRF non válido" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "Hai un problema co seu testemuño CSRF. Asegúrese de ter as cookies activadas no navegador, e recargue a páxina. Si persiste o aviso de este fallo, informe por favor." + +msgid "You are not authorized." +msgstr "Non ten permiso." + +msgid "Page not found" +msgstr "Non se atopou a páxina" + +msgid "We couldn't find this page." +msgstr "Non atopamos esta páxina" + +msgid "The link that led you here may be broken." +msgstr "A ligazón que a trouxo aquí podería estar quebrado" + +msgid "Users" +msgstr "Usuarias" + +msgid "Configuration" +msgstr "Axustes" + +msgid "Instances" +msgstr "Instancias" + +msgid "Email blocklist" +msgstr "Lista de bloqueo" + +msgid "Grant admin rights" +msgstr "Conceder permisos de admin" + +msgid "Revoke admin rights" +msgstr "Revogar permisos de admin" + +msgid "Grant moderator rights" +msgstr "Conceder permisos de moderación" + +msgid "Revoke moderator rights" +msgstr "Revogar permisos de moderación" + +msgid "Ban" +msgstr "Prohibir" + +msgid "Run on selected users" +msgstr "Executar en usuarias seleccionadas" + +msgid "Moderator" +msgstr "Moderadora" + +msgid "Moderation" +msgstr "Moderación" + +msgid "Home" +msgstr "Inicio" + +msgid "Administration of {0}" +msgstr "Administración de {0}" + +msgid "Unblock" +msgstr "Desbloquear" + +msgid "Block" +msgstr "Bloquear" + +msgid "Name" +msgstr "Nome" + +msgid "Allow anyone to register here" +msgstr "Permitir o rexistro aberto a calquera" + +msgid "Short description" +msgstr "Descrición curta" + +msgid "Markdown syntax is supported" +msgstr "Pode utilizar sintaxe Markdown" + +msgid "Long description" +msgstr "Descrición longa" + +msgid "Default article license" +msgstr "Licenza por omisión dos artigos" + +msgid "Save these settings" +msgstr "Gardar estas preferencias" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "Se estás lendo esta web como visitante non se recollen datos sobre ti." + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "Como usuaria rexistrada, tes que proporcionar un nome de usuaria (que non ten que ser o teu nome real), un enderezo activo de correo electrónico e un contrasinal, para poder conectarte, escribir artigos e comentar. O contido que envíes permanece ata que o borres." + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "Cando te conectas, gardamos dous testemuños, un para manter a sesión aberta e o segundo para previr que outra xente actúe no teu nome. Non gardamos máis testemuños." + +msgid "Blocklisted Emails" +msgstr "Emails na lista de bloqueo" + +msgid "Email address" +msgstr "Enderezo de email" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "O enderezo de email que queres bloquear. Para poder bloquear dominios, podes usar sintaxe globbing, por exemplo '*@exemplo.com' bloquea todos os enderezos de exemplo.com" + +msgid "Note" +msgstr "Nota" + +msgid "Notify the user?" +msgstr "Notificar a usuaria?" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "Optativo, mostra unha mensaxe a usuaria cando intenta crear unha conta con ese enderezo" + +msgid "Blocklisting notification" +msgstr "Notificación do bloqueo" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "A mensaxe será amosada cando a usuaria intente crear unha conta on ese enderezo de email" + +msgid "Add blocklisted address" +msgstr "Engadir a lista de bloqueo" + +msgid "There are no blocked emails on your instance" +msgstr "Non hai emails bloqueados na túa instancia" + +msgid "Delete selected emails" +msgstr "Eliminar emails seleccionados" + +msgid "Email address:" +msgstr "Enderezo de email:" + +msgid "Blocklisted for:" +msgstr "Bloqueado por:" + +msgid "Will notify them on account creation with this message:" +msgstr "Enviaralles notificación con esta mensaxe cando se cree a conta:" + +msgid "The user will be silently prevented from making an account" +msgstr "Previrase caladamente que a usuaria cree conta" + +msgid "Welcome to {}" +msgstr "Benvida a {}" + +msgid "View all" +msgstr "Ver todos" + +msgid "About {0}" +msgstr "Acerca de {0}" + +msgid "Runs Plume {0}" +msgstr "Versión Plume {0}" + +msgid "Home to {0} people" +msgstr "Lar de {0} persoas" + +msgid "Who wrote {0} articles" +msgstr "Que escribiron {0} artigos" + +msgid "And are connected to {0} other instances" +msgstr "E están conectadas a outras {0} instancias" + +msgid "Administred by" +msgstr "Administrada por" + +msgid "Interact with {}" +msgstr "Interactúe con {}" + +msgid "Log in to interact" +msgstr "Conecte para interactuar" + +msgid "Enter your full username to interact" +msgstr "Introduza o seu nome de usuaria completo para interactuar" + +msgid "Publish" +msgstr "Publicar" + +msgid "Classic editor (any changes will be lost)" +msgstr "Editor clásico (calquera perderanse os cambios)" + +msgid "Title" +msgstr "Título" + +msgid "Subtitle" +msgstr "Subtítulo" + +msgid "Content" +msgstr "Contido" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "Pode subir medios a galería e despois copiar o seu código Markdown nos artigos para incrustalos." + +msgid "Upload media" +msgstr "Subir medios" + +msgid "Tags, separated by commas" +msgstr "Etiquetas, separadas por vírgulas" + +msgid "License" +msgstr "Licenza" + +msgid "Illustration" +msgstr "Ilustración" + +msgid "This is a draft, don't publish it yet." +msgstr "Esto é un borrador, non publicar por agora." + +msgid "Update" +msgstr "Actualizar" + +msgid "Update, or publish" +msgstr "Actualizar ou publicar" + +msgid "Publish your post" +msgstr "Publicar o artigo" + +msgid "Written by {0}" +msgstr "Escrito por {0}" + +msgid "All rights reserved." +msgstr "Todos os dereitos reservados." + +msgid "This article is under the {0} license." +msgstr "Este artigo ten licenza {0}." + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "Un gústame" +msgstr[1] "{0} gústame" + +msgid "I don't like this anymore" +msgstr "Xa non me gusta" + +msgid "Add yours" +msgstr "Engade os teus" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "Unha promoción" +msgstr[1] "{0} promocións" + +msgid "I don't want to boost this anymore" +msgstr "Xa non quero promocionar este artigo" + +msgid "Boost" +msgstr "Promover" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "{0}Conectar{1}, ou {2}utilice a súa conta no Fediverso{3} para interactuar con este artigo" + +msgid "Comments" +msgstr "Comentarios" + +msgid "Your comment" +msgstr "O seu comentario" + +msgid "Submit comment" +msgstr "Enviar comentario" + +msgid "No comments yet. Be the first to react!" +msgstr "Sen comentarios. Sexa a primeira persoa en facelo!" + +msgid "Are you sure?" +msgstr "Está segura?" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "Este artigo é un borrador. Só ti e as outras autoras poden velo." + +msgid "Only you and other authors can edit this article." +msgstr "Só ti e as outras autoras poden editar este artigo." + +msgid "Edit" +msgstr "Editar" + +msgid "I'm from this instance" +msgstr "Eu formo parte de esta instancia" + +msgid "Username, or email" +msgstr "Nome de usuaria ou correo" + +msgid "Log in" +msgstr "Conectar" + +msgid "I'm from another instance" +msgstr "Veño desde outra instancia" + +msgid "Continue to your instance" +msgstr "Continuar hacia a súa instancia" + +msgid "Reset your password" +msgstr "Restablecer contrasinal" + +msgid "New password" +msgstr "Novo contrasinal" + +msgid "Confirmation" +msgstr "Confirmación" + +msgid "Update password" +msgstr "Actualizar contrasinal" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "Enviamosche un correo ao enderezo que indicado, cunha ligazón para restablecer o contrasinal." + +msgid "Send password reset link" +msgstr "Enviar ligazón para restablecer contrasinal" + +msgid "This token has expired" +msgstr "O testemuño caducou" + +msgid "Please start the process again by clicking here." +msgstr "Inicia o preceso de novo premendo aquí." + +msgid "New Blog" +msgstr "Novo Blog" + +msgid "Create a blog" +msgstr "Crear un blog" + +msgid "Create blog" +msgstr "Crear blog" + +msgid "Edit \"{}\"" +msgstr "Editar \"{}\"" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "Pode subir imaxes a súa galería, e utilizalas como iconas do blog ou banners." + +msgid "Upload images" +msgstr "Subir imaxes" + +msgid "Blog icon" +msgstr "Icona de blog" + +msgid "Blog banner" +msgstr "Banner do blog" + +msgid "Custom theme" +msgstr "Decorado personalizado" + +msgid "Update blog" +msgstr "Actualizar blog" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "Teña tino, todo o que faga aquí non se pode reverter." + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "Tes a certeza de querer eliminar definitivamente este blog?" + +msgid "Permanently delete this blog" +msgstr "Eliminar o blog de xeito permanente" + +msgid "{}'s icon" +msgstr "Icona de {}" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "Este blog ten unha autora: " +msgstr[1] "Este blog ten {0} autoras: " + +msgid "No posts to see here yet." +msgstr "Aínda non hai entradas publicadas" + +msgid "Nothing to see here yet." +msgstr "Aínda non hai nada publicado." + +msgid "None" +msgstr "Ningunha" + +msgid "No description" +msgstr "Sen descrición" + +msgid "Respond" +msgstr "Respostar" + +msgid "Delete this comment" +msgstr "Eliminar o comentario" + +msgid "What is Plume?" +msgstr "Qué é Plume?" + +msgid "Plume is a decentralized blogging engine." +msgstr "Plume é un motor de publicación descentralizada." + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "As autoras poden xestionar múltiples blogs, cada un no seu propio sitio web." + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "Os artigos tamén son visibles en outras instancias Plume, e pode interactuar con eles directamente ou desde plataformas como Mastodon." + +msgid "Read the detailed rules" +msgstr "Lea o detalle das normas" + +msgid "By {0}" +msgstr "Por {0}" + +msgid "Draft" +msgstr "Borrador" + +msgid "Search result(s) for \"{0}\"" +msgstr "Resultado(s) da busca \"{0}\"" + +msgid "Search result(s)" +msgstr "Resultado(s) da busca" + +msgid "No results for your query" +msgstr "Sen resultados para a consulta" + +msgid "No more results for your query" +msgstr "Sen máis resultados para a súa consulta" + +msgid "Advanced search" +msgstr "Busca avanzada" + +msgid "Article title matching these words" +msgstr "Título de artigo coincidente con estas palabras" + +msgid "Subtitle matching these words" +msgstr "O subtítulo coincide con estas palabras" + +msgid "Content macthing these words" +msgstr "Contido coincidente con estas palabras" + +msgid "Body content" +msgstr "Contido do corpo" + +msgid "From this date" +msgstr "Desde esta data" + +msgid "To this date" +msgstr "Ata esta data" + +msgid "Containing these tags" +msgstr "Contendo estas etiquetas" + +msgid "Tags" +msgstr "Etiquetas" + +msgid "Posted on one of these instances" +msgstr "Publicado en algunha de estas instancias" + +msgid "Instance domain" +msgstr "Dominio da instancia" + +msgid "Posted by one of these authors" +msgstr "Publicado por unha de estas autoras" + +msgid "Author(s)" +msgstr "Autor(es)" + +msgid "Posted on one of these blogs" +msgstr "Publicado en un de estos blogs" + +msgid "Blog title" +msgstr "Título do blog" + +msgid "Written in this language" +msgstr "Escrito en este idioma" + +msgid "Language" +msgstr "Idioma" + +msgid "Published under this license" +msgstr "Publicado baixo esta licenza" + +msgid "Article license" +msgstr "Licenza do artigo" + diff --git a/po/plume/he.po b/po/plume/he.po new file mode 100644 index 00000000000..a2a22192427 --- /dev/null +++ b/po/plume/he.po @@ -0,0 +1,1040 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Hebrew\n" +"Language: he_IL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: he\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "" + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "" + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "" + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "" + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "" + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "" + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "" + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "" + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "" + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "" + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "" + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "" + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "" + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "" + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "" + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "" + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "" + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "" + +# src/routes/medias.rs:180 +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 "" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "" + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "" + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "" + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "" + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "" + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "" + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "" + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "" + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "" + +msgid "Create an account" +msgstr "" + +msgid "Email" +msgstr "" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Your media" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "" + +msgid "Menu" +msgstr "" + +msgid "Search" +msgstr "" + +msgid "Dashboard" +msgstr "" + +msgid "Notifications" +msgstr "" + +msgid "Log Out" +msgstr "" + +msgid "My account" +msgstr "" + +msgid "Log In" +msgstr "" + +msgid "Register" +msgstr "" + +msgid "About this instance" +msgstr "" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "" + +msgid "It is you" +msgstr "" + +msgid "Edit your profile" +msgstr "" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "" + +msgid "Follow {}" +msgstr "" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "" + +msgid "Subscribers" +msgstr "" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "" + +msgid "Your Drafts" +msgstr "" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "" + +msgid "Your Profile" +msgstr "" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "" + +msgid "Configuration" +msgstr "" + +msgid "Instances" +msgstr "" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "" + +msgid "Unblock" +msgstr "" + +msgid "Block" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Allow anyone to register here" +msgstr "" + +msgid "Short description" +msgstr "" + +msgid "Markdown syntax is supported" +msgstr "" + +msgid "Long description" +msgstr "" + +msgid "Default article license" +msgstr "" + +msgid "Save these settings" +msgstr "" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +msgid "Administred by" +msgstr "" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +msgid "Tags, separated by commas" +msgstr "" + +msgid "License" +msgstr "" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +msgid "I don't like this anymore" +msgstr "" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "" + +msgid "Comments" +msgstr "" + +msgid "Your comment" +msgstr "" + +msgid "Submit comment" +msgstr "" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "" + +msgid "Log in" +msgstr "" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +msgid "New password" +msgstr "" + +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "" + +msgid "Create a blog" +msgstr "" + +msgid "Create blog" +msgstr "" + +msgid "Edit \"{}\"" +msgstr "" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +msgid "From this date" +msgstr "" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" + diff --git a/po/plume/hi.po b/po/plume/hi.po new file mode 100644 index 00000000000..88d871d3f5b --- /dev/null +++ b/po/plume/hi.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Hindi\n" +"Language: hi_IN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: hi\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} ने आपके लेख पे कॉमेंट किया है" + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} ने आपको सब्सक्राइब किया है" + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} ने आपके लेख को लाइक किया" + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} ने आपको मेंशन किया" + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} ने आपके आर्टिकल को बूस्ट किया" + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "आपकी फीड" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "लोकल फीड" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "फ़ेडरेटेड फीड" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "{0} का avtar" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "वैकल्पिक" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "नया ब्लॉग बनाने के लिए आपको लोग इन करना होगा" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "ये नाम से पहले ही एक ब्लॉग है" + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "" + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "आपको ये ब्लॉग डिलीट करने की अनुमति नहीं है" + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "आपको ये ब्लॉग में बदलाव करने की अनुमति नहीं है" + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "इस फोटो को ब्लॉग आइकॉन के लिए इस्तेमाल नहीं कर सकते" + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "इस media को blog banner के लिए इस्तेमाल नहीं कर सकते" + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "" + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "" + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "" + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "" + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "Post को like करने के लिए आपको log in करना होगा" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "" + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "" + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "" + +# src/routes/medias.rs:180 +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 करना होगा" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "इस post को publish नहीं किया गया है" + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "नया post लिखने के लिए आपको log in करना होगा" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "आप इस blog के लेखक नहीं हैं" + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "नया post" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "Edit करें {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "" + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "" + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "नया लेख" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "" + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "" + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "आपके अकाउंट के बारे में पर्याप्त जानकारी नहीं मिल पायी. कृपया जांच करें की आपका यूजरनाम सही है." + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "Post reshare करने के लिए आपको log in करना होगा" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "" + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "पासवर्ड रीसेट करें" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "आपका पासवर्ड रिसेट करने का लिंक: {0}" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "आपका पासवर्ड रिसेट कर दिया गया है" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "डैशबोर्ड पर जाने के लिए, लोग इन करें" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "सब्सक्राइब करने के लिए, लोग इन करें" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "प्रोफाइल में बदलाव करने के लिए, लोग इन करें" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "अपना अकाउंट बनाएं" + +msgid "Create an account" +msgstr "" + +msgid "Email" +msgstr "ईमेल" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "आपका इनबॉक्स चेक करें" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "" + +msgid "Password" +msgstr "पासवर्ड" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "वर्णन" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "चेतावनी" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Your media" +msgstr "आपकी मीडिया" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "प्लूम" + +msgid "Menu" +msgstr "मेंन्यू" + +msgid "Search" +msgstr "ढूंढें" + +msgid "Dashboard" +msgstr "डैशबोर्ड" + +msgid "Notifications" +msgstr "सूचनाएँ" + +msgid "Log Out" +msgstr "लॉग आउट" + +msgid "My account" +msgstr "मेरा अकाउंट" + +msgid "Log In" +msgstr "लॉग इन" + +msgid "Register" +msgstr "रजिस्टर" + +msgid "About this instance" +msgstr "इंस्टैंस के बारे में जानकारी" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "संचालन" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "सोर्स कोड" + +msgid "Matrix room" +msgstr "मैट्रिक्स रूम" + +msgid "Admin" +msgstr "" + +msgid "It is you" +msgstr "" + +msgid "Edit your profile" +msgstr "आपकी प्रोफाइल में बदलाव करें" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "अनसब्सक्राइब" + +msgid "Subscribe" +msgstr "सब्सक्राइब" + +msgid "Follow {}" +msgstr "" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "" + +msgid "Subscribers" +msgstr "" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "आपका डैशबोर्ड" + +msgid "Your Blogs" +msgstr "आपके ब्लोग्स" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "आपके कोई ब्लोग्स नहीं हैं. आप स्वयं ब्लॉग बना सकते हैं या किसी और ब्लॉग से जुड़ सकते हैं" + +msgid "Start a new blog" +msgstr "नया ब्लॉग बनाएं" + +msgid "Your Drafts" +msgstr "आपके ड्राफ्ट्स" + +msgid "Go to your gallery" +msgstr "गैलरी में जाएँ" + +msgid "Edit your account" +msgstr "अपने अकाउंट में बदलाव करें" + +msgid "Your Profile" +msgstr "आपकी प्रोफाइल" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "सारांश" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "अकाउंट अपडेट करें" + +msgid "Danger zone" +msgstr "खतरे का क्षेत्र" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "सावधानी रखें, यहाँ पे कोई भी किया गया कार्य कैंसिल नहीं किया जा सकता" + +msgid "Delete your account" +msgstr "खाता रद्द करें" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "माफ़ करें, एडमिन होने की वजह से, आप अपना इंस्टैंस नहीं छोड़ सकते" + +msgid "Latest articles" +msgstr "नवीनतम लेख" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "\"{0}\" टैग किये गए लेख" + +msgid "There are currently no articles with such a tag" +msgstr "वर्तमान में ऐसे टैग के साथ कोई लेख नहीं है" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "उसेर्स" + +msgid "Configuration" +msgstr "कॉन्फ़िगरेशन" + +msgid "Instances" +msgstr "इन्सटेंस" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "बैन" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "{0} का संचालन" + +msgid "Unblock" +msgstr "अनब्लॉक" + +msgid "Block" +msgstr "ब्लॉक" + +msgid "Name" +msgstr "नाम" + +msgid "Allow anyone to register here" +msgstr "किसी को भी रजिस्टर करने की अनुमति दें" + +msgid "Short description" +msgstr "संक्षिप्त वर्णन" + +msgid "Markdown syntax is supported" +msgstr "मार्कडौं सिंटेक्स उपलब्ध है" + +msgid "Long description" +msgstr "दीर्घ वर्णन" + +msgid "Default article license" +msgstr "डिफ़ॉल्ट आलेख लायसेंस" + +msgid "Save these settings" +msgstr "इन सेटिंग्स को सेव करें" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "{} में स्वागत" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "{0} के बारे में" + +msgid "Runs Plume {0}" +msgstr "Plume {0} का इस्तेमाल कर रहे हैं" + +msgid "Home to {0} people" +msgstr "यहाँ {0} यूज़र्स हैं" + +msgid "Who wrote {0} articles" +msgstr "जिन्होनें {0} आर्टिकल्स लिखे हैं" + +msgid "And are connected to {0} other instances" +msgstr "और {0} इन्सटेंसेस से जुड़े हैं" + +msgid "Administred by" +msgstr "द्वारा संचालित" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "इंटरैक्ट करने के लिए लोग इन करें" + +msgid "Enter your full username to interact" +msgstr "इंटरैक्ट करने के लिए आपका पूर्ण यूज़रनेम दर्ज करें" + +msgid "Publish" +msgstr "पब्लिश" + +msgid "Classic editor (any changes will be lost)" +msgstr "क्लासिक एडिटर (किये गए बदलाव सेव नहीं किये जायेंगे)" + +msgid "Title" +msgstr "शीर्षक" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +msgid "Tags, separated by commas" +msgstr "" + +msgid "License" +msgstr "" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "अपडेट" + +msgid "Update, or publish" +msgstr "अपडेट या पब्लिश" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "{0} द्वारा लिखित" + +msgid "All rights reserved." +msgstr "सर्वाधिकार सुरक्षित" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "एक लाइक" +msgstr[1] "{0} लाइक्स" + +msgid "I don't like this anymore" +msgstr "मैं ये अब पसंद नहीं है" + +msgid "Add yours" +msgstr "अपका लाइक दें" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" +msgstr[1] "{0} बूस्ट्स" + +msgid "I don't want to boost this anymore" +msgstr "मुझे अब इसे बूस्ट नहीं करना है" + +msgid "Boost" +msgstr "बूस्ट" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "{0}लोग इन करें{1}, या {2}आपके फेडिवेर्से अकाउंट का इस्तेमाल करें{3} इस आर्टिकल से इंटरैक्ट करने के लिए" + +msgid "Comments" +msgstr "कमैंट्स" + +msgid "Your comment" +msgstr "आपकी कमेंट" + +msgid "Submit comment" +msgstr "कमेंट सबमिट करें" + +msgid "No comments yet. Be the first to react!" +msgstr "कोई कमेंट नहीं हैं. आप अपनी प्रतिक्रिया दें." + +msgid "Are you sure?" +msgstr "क्या आप निश्चित हैं?" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "बदलाव करें" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "यूजरनेम या इ-मेल" + +msgid "Log in" +msgstr "लौग इन" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "पासवर्ड रिसेट करें" + +msgid "New password" +msgstr "नया पासवर्ड" + +msgid "Confirmation" +msgstr "पुष्टीकरण" + +msgid "Update password" +msgstr "पासवर्ड अपडेट करें" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "हमने आपके दिए गए इ-मेल पे पासवर्ड रिसेट लिंक भेज दिया है." + +msgid "Send password reset link" +msgstr "पासवर्ड रिसेट करने के लिए लिंक भेजें" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "नया ब्लॉग" + +msgid "Create a blog" +msgstr "ब्लॉग बनाएं" + +msgid "Create blog" +msgstr "ब्लॉग बनाएं" + +msgid "Edit \"{}\"" +msgstr "{0} में बदलाव करें" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "आप गैलरी में फोटो दाल कर, उनका ब्लॉग आइकॉन या बैनर के लिए उपयोग कर सकते हैं" + +msgid "Upload images" +msgstr "फोटो अपलोड करें" + +msgid "Blog icon" +msgstr "ब्लॉग आइकॉन" + +msgid "Blog banner" +msgstr "ब्लॉग बैनर" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "ब्लॉग अपडेट करें" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "सावधानी रखें, यहाँ पे कोई भी किया गया कार्य कैंसिल नहीं किया जा सकता" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "इस ब्लॉग को स्थाई रूप से हटाएं" + +msgid "{}'s icon" +msgstr "{} का आइकॉन" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "ये ब्लॉग पे एक लेखक हैं: " +msgstr[1] "ये ब्लॉग पे {0} लेखक हैं: " + +msgid "No posts to see here yet." +msgstr "यहाँ वर्तमान में कोई पोस्ट्स नहीं है." + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "ड्राफ्ट" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "आपकी जांच के लिए और रिजल्ट्स नहीं है" + +msgid "Advanced search" +msgstr "एडवांस्ड सर्च" + +msgid "Article title matching these words" +msgstr "सर्च से मैच करने वाले लेख शीर्षक" + +msgid "Subtitle matching these words" +msgstr "सर्च से मैच करने वाले लेख उपशीर्षक" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "बॉडी कंटेंट" + +msgid "From this date" +msgstr "इस तारीख से" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "इन टैग्स से युक्त" + +msgid "Tags" +msgstr "टैग्स" + +msgid "Posted on one of these instances" +msgstr "इन इन्सटेंसेस में पोस्ट किया गया" + +msgid "Instance domain" +msgstr "इंस्टैंस डोमेन" + +msgid "Posted by one of these authors" +msgstr "इन लेखकों द्वारा पोस्ट किये गए आर्टिकल्स" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "इन ब्लोग्स में से एक ब्लॉग पर पोस्ट किया गया" + +msgid "Blog title" +msgstr "ब्लॉग टाइटल" + +msgid "Written in this language" +msgstr "इन भाषाओँ में लिखे गए" + +msgid "Language" +msgstr "भाषा" + +msgid "Published under this license" +msgstr "इस लिसेंसे के साथ पब्लिश किया गया" + +msgid "Article license" +msgstr "आर्टिकल लाइसेंस" + diff --git a/po/plume/hr.po b/po/plume/hr.po new file mode 100644 index 00000000000..741aec6033f --- /dev/null +++ b/po/plume/hr.po @@ -0,0 +1,1037 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Croatian\n" +"Language: hr_HR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: hr\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} komentira na vaš članak." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "" + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} se svidio vaš članak." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "" + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "" + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "Lokalnog kanala" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "Federalni kanala" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "" + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "" + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "" + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "" + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "" + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "" + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "" + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "" + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "" + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "" + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "" + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "" + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "" + +# src/routes/medias.rs:180 +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 "" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "Ovaj post još nije objavljen." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "Ti ne autor ovog bloga." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "Novi članak" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "Uredi {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "" + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "" + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "" + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "" + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "" + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "" + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "Poništavanje zaporke" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "" + +msgid "Create an account" +msgstr "" + +msgid "Email" +msgstr "E-pošta" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Your media" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "Izbornik" + +msgid "Search" +msgstr "Traži" + +msgid "Dashboard" +msgstr "Upravljačka ploča" + +msgid "Notifications" +msgstr "Obavijesti" + +msgid "Log Out" +msgstr "Odjaviti se" + +msgid "My account" +msgstr "Moj račun" + +msgid "Log In" +msgstr "Prijaviti se" + +msgid "Register" +msgstr "Registrirajte se" + +msgid "About this instance" +msgstr "" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "Administracija" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "Izvorni kod" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "" + +msgid "It is you" +msgstr "" + +msgid "Edit your profile" +msgstr "" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "" + +msgid "Follow {}" +msgstr "" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "Članci" + +msgid "Subscribers" +msgstr "" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "" + +msgid "Your Drafts" +msgstr "" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "Uredite svoj račun" + +msgid "Your Profile" +msgstr "Tvoj Profil" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "Sažetak" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "Ažuriraj račun" + +msgid "Danger zone" +msgstr "Opasna zona" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "Izbrišite svoj račun" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "Najnoviji članci" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "Korisnici" + +msgid "Configuration" +msgstr "Konfiguracija" + +msgid "Instances" +msgstr "" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "Zabraniti" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "Administracija od {0}" + +msgid "Unblock" +msgstr "Odblokiraj" + +msgid "Block" +msgstr "Blokirati" + +msgid "Name" +msgstr "" + +msgid "Allow anyone to register here" +msgstr "" + +msgid "Short description" +msgstr "Kratki opis" + +msgid "Markdown syntax is supported" +msgstr "Markdown sintaksa je podržana" + +msgid "Long description" +msgstr "Dugi opis" + +msgid "Default article license" +msgstr "" + +msgid "Save these settings" +msgstr "" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "Dobrodošli u {0}" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "O {0}" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +msgid "Administred by" +msgstr "" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +msgid "Tags, separated by commas" +msgstr "" + +msgid "License" +msgstr "" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +msgid "I don't like this anymore" +msgstr "" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "" + +msgid "Comments" +msgstr "" + +msgid "Your comment" +msgstr "" + +msgid "Submit comment" +msgstr "" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "" + +msgid "Log in" +msgstr "" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +msgid "New password" +msgstr "" + +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "" + +msgid "Create a blog" +msgstr "" + +msgid "Create blog" +msgstr "" + +msgid "Edit \"{}\"" +msgstr "" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +msgid "From this date" +msgstr "" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" + diff --git a/po/plume/hu.po b/po/plume/hu.po new file mode 100644 index 00000000000..536ff1f270e --- /dev/null +++ b/po/plume/hu.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Hungarian\n" +"Language: hu_HU\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: hu\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "" + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "" + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "" + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "" + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "" + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "" + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "" + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "" + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "" + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "" + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "" + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "" + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "" + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "" + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "" + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "" + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "" + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "" + +# src/routes/medias.rs:180 +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 "" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "" + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "" + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "" + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "" + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "" + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "" + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "" + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "" + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "" + +msgid "Create an account" +msgstr "" + +msgid "Email" +msgstr "" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Your media" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "" + +msgid "Menu" +msgstr "" + +msgid "Search" +msgstr "" + +msgid "Dashboard" +msgstr "" + +msgid "Notifications" +msgstr "" + +msgid "Log Out" +msgstr "" + +msgid "My account" +msgstr "" + +msgid "Log In" +msgstr "" + +msgid "Register" +msgstr "" + +msgid "About this instance" +msgstr "" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "" + +msgid "It is you" +msgstr "" + +msgid "Edit your profile" +msgstr "" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "" + +msgid "Follow {}" +msgstr "" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "" + +msgid "Subscribers" +msgstr "" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "" + +msgid "Your Drafts" +msgstr "" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "" + +msgid "Your Profile" +msgstr "" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "" + +msgid "Configuration" +msgstr "" + +msgid "Instances" +msgstr "" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "" + +msgid "Unblock" +msgstr "" + +msgid "Block" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Allow anyone to register here" +msgstr "" + +msgid "Short description" +msgstr "" + +msgid "Markdown syntax is supported" +msgstr "" + +msgid "Long description" +msgstr "" + +msgid "Default article license" +msgstr "" + +msgid "Save these settings" +msgstr "" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +msgid "Administred by" +msgstr "" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +msgid "Tags, separated by commas" +msgstr "" + +msgid "License" +msgstr "" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "" +msgstr[1] "" + +msgid "I don't like this anymore" +msgstr "" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" +msgstr[1] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "" + +msgid "Comments" +msgstr "" + +msgid "Your comment" +msgstr "" + +msgid "Submit comment" +msgstr "" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "" + +msgid "Log in" +msgstr "" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +msgid "New password" +msgstr "" + +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "" + +msgid "Create a blog" +msgstr "" + +msgid "Create blog" +msgstr "" + +msgid "Edit \"{}\"" +msgstr "" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" +msgstr[1] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +msgid "From this date" +msgstr "" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" + diff --git a/po/plume/it.po b/po/plume/it.po new file mode 100644 index 00000000000..f08191d0fed --- /dev/null +++ b/po/plume/it.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Italian\n" +"Language: it_IT\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: it\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} ha commentato il tuo articolo." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} si è iscritto a te." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} ha apprezzato il tuo articolo." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} ti ha menzionato." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} ha boostato il tuo articolo." + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "Il tuo flusso" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "Flusso locale" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "Flusso federato" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "Avatar di {0}" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "Pagina precedente" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "Pagina successiva" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "Opzionale" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "Per creare un nuovo blog, devi avere effettuato l'accesso" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "Un blog con lo stesso nome esiste già." + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "Il tuo blog è stato creato con successo!" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "Il tuo blog è stato eliminato." + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "Non ti è consentito di eliminare questo blog." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "Non ti è consentito modificare questo blog." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "Non puoi utilizzare questo media come icona del blog." + +# src/routes/blogs.rs:298 +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:332 +msgid "Your blog information have been updated." +msgstr "Le informazioni del tuo blog sono state aggiornate." + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "Il tuo commento è stato pubblicato." + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "Il tuo commento è stato eliminato." + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "Le registrazioni sono chiuse su questa istanza." + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "Il tuo account è stato creato. Ora devi solo effettuare l'accesso prima di poterlo utilizzare." + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "Le impostazioni dell'istanza sono state salvate." + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "{} è stato sbloccato." + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "{} è stato bloccato." + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "Blocco eliminato" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "Email Bloccata" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "Non puoi modificare i tuoi diritti." + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "Non puoi compiere quest'azione." + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "Fatto." + +# src/routes/likes.rs:58 +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:153 +msgid "Your media have been deleted." +msgstr "I tuoi media sono stati eliminati." + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "Non ti è consentito rimuovere questo media." + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "La tua immagine di profilo è stata aggiornata." + +# src/routes/medias.rs:180 +msgid "You are not allowed to use this media." +msgstr "Non ti è consentito utilizzare questo media." + +# 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" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "Questo post non è ancora stato pubblicato." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "Per scrivere un nuovo post, devi avere effettuato l'accesso" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "Non sei un autore di questo blog." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "Nuovo post" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "Modifica {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "Non ti è consentito pubblicare su questo blog." + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "Il tuo articolo è stato aggiornato." + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "Il tuo articolo è stato salvato." + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "Nuovo articolo" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "Non è consentito eliminare questo articolo." + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "Il tuo articolo è stato eliminato." + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "Sembra che l'articolo che cerchi di eliminare non esista. Forse è già stato cancellato?" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "Non è stato possibile ottenere abbastanza informazioni sul tuo account. Per favore assicurati che il tuo nome utente sia corretto." + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "Per ricondividere un post, devi avere effettuato l'accesso" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "Ora sei connesso." + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "Ti sei disconnesso." + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "Reimposta password" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "Qui c'è il collegamento per reimpostare la tua password: {0}" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "La tua password è stata reimpostata con successo." + +# src/routes/user.rs:87 +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:109 +msgid "You are no longer following {}." +msgstr "Non stai più seguendo {}." + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "Ora stai seguendo {}." + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "Per iscriverti a qualcuno, devi avere effettuato l'accesso" + +# src/routes/user.rs:323 +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:369 +msgid "Your profile has been updated." +msgstr "Il tuo profilo è stato aggiornato." + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "Il tuo account è stato eliminato." + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "Non puoi eliminare l'account di qualcun altro." + +msgid "Create your account" +msgstr "Crea il tuo account" + +msgid "Create an account" +msgstr "Crea un account" + +msgid "Email" +msgstr "Email" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "Spiacenti, ma le registrazioni sono chiuse per questa istanza. Puoi comunque trovarne un'altra." + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "Controlla la tua casella di posta in arrivo!" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "Nome utente" + +msgid "Password" +msgstr "Password" + +msgid "Password confirmation" +msgstr "Conferma password" + +msgid "Media upload" +msgstr "Caricamento di un media" + +msgid "Description" +msgstr "Descrizione" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "Utile per persone ipovedenti, ed anche per informazioni sulla licenza" + +msgid "Content warning" +msgstr "Avviso di contenuto sensibile" + +msgid "Leave it empty, if none is needed" +msgstr "Lascia vuoto, se non è necessario" + +msgid "File" +msgstr "File" + +msgid "Send" +msgstr "Invia" + +msgid "Your media" +msgstr "I tuoi media" + +msgid "Upload" +msgstr "Carica" + +msgid "You don't have any media yet." +msgstr "Non hai ancora nessun media." + +msgid "Content warning: {0}" +msgstr "Avviso di contenuto sensibile: {0}" + +msgid "Delete" +msgstr "Elimina" + +msgid "Details" +msgstr "Dettagli" + +msgid "Media details" +msgstr "Dettagli media" + +msgid "Go back to the gallery" +msgstr "Torna alla galleria" + +msgid "Markdown syntax" +msgstr "Sintassi Markdown" + +msgid "Copy it into your articles, to insert this media:" +msgstr "Copialo nei tuoi articoli, per inserire questo media:" + +msgid "Use as an avatar" +msgstr "Usa come immagine di profilo" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "Menu" + +msgid "Search" +msgstr "Cerca" + +msgid "Dashboard" +msgstr "Pannello" + +msgid "Notifications" +msgstr "Notifiche" + +msgid "Log Out" +msgstr "Disconnettiti" + +msgid "My account" +msgstr "Il mio account" + +msgid "Log In" +msgstr "Accedi" + +msgid "Register" +msgstr "Registrati" + +msgid "About this instance" +msgstr "A proposito di questa istanza" + +msgid "Privacy policy" +msgstr "Politica sulla Riservatezza" + +msgid "Administration" +msgstr "Amministrazione" + +msgid "Documentation" +msgstr "Documentazione" + +msgid "Source code" +msgstr "Codice sorgente" + +msgid "Matrix room" +msgstr "Stanza Matrix" + +msgid "Admin" +msgstr "Amministratore" + +msgid "It is you" +msgstr "Sei tu" + +msgid "Edit your profile" +msgstr "Modifica il tuo profilo" + +msgid "Open on {0}" +msgstr "Apri su {0}" + +msgid "Unsubscribe" +msgstr "Annulla iscrizione" + +msgid "Subscribe" +msgstr "Iscriviti" + +msgid "Follow {}" +msgstr "Segui {}" + +msgid "Log in to follow" +msgstr "Accedi per seguire" + +msgid "Enter your full username handle to follow" +msgstr "Inserisci il tuo nome utente completo (handle) per seguire" + +msgid "{0}'s subscribers" +msgstr "Iscritti di {0}" + +msgid "Articles" +msgstr "Articoli" + +msgid "Subscribers" +msgstr "Iscritti" + +msgid "Subscriptions" +msgstr "Sottoscrizioni" + +msgid "{0}'s subscriptions" +msgstr "Iscrizioni di {0}" + +msgid "Your Dashboard" +msgstr "Il tuo Pannello" + +msgid "Your Blogs" +msgstr "I Tuoi Blog" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "Non hai ancora nessun blog. Creane uno tuo, o chiedi di unirti ad uno esistente." + +msgid "Start a new blog" +msgstr "Inizia un nuovo blog" + +msgid "Your Drafts" +msgstr "Le tue Bozze" + +msgid "Go to your gallery" +msgstr "Vai alla tua galleria" + +msgid "Edit your account" +msgstr "Modifica il tuo account" + +msgid "Your Profile" +msgstr "Il Tuo Profilo" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "Per modificare la tua immagine di profilo, caricala nella tua galleria e poi selezionala da là." + +msgid "Upload an avatar" +msgstr "Carica un'immagine di profilo" + +msgid "Display name" +msgstr "Nome visualizzato" + +msgid "Summary" +msgstr "Riepilogo" + +msgid "Theme" +msgstr "Tema" + +msgid "Default theme" +msgstr "Tema predefinito" + +msgid "Error while loading theme selector." +msgstr "Errore durante il caricamento del selettore del tema." + +msgid "Never load blogs custom themes" +msgstr "Non caricare mai i temi personalizzati dei blog" + +msgid "Update account" +msgstr "Aggiorna account" + +msgid "Danger zone" +msgstr "Zona pericolosa" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "Fai molta attenzione, qualsiasi scelta fatta qui non può essere annullata." + +msgid "Delete your account" +msgstr "Elimina il tuo account" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "Spiacente, ma come amministratore, non puoi lasciare la tua istanza." + +msgid "Latest articles" +msgstr "Ultimi articoli" + +msgid "Atom feed" +msgstr "Flusso Atom" + +msgid "Recently boosted" +msgstr "Boostato recentemente" + +msgid "Articles tagged \"{0}\"" +msgstr "Articoli etichettati \"{0}\"" + +msgid "There are currently no articles with such a tag" +msgstr "Attualmente non ci sono articoli con quest'etichetta" + +msgid "The content you sent can't be processed." +msgstr "Il contenuto che hai inviato non può essere processato." + +msgid "Maybe it was too long." +msgstr "Probabilmente era troppo lungo." + +msgid "Internal server error" +msgstr "Errore interno del server" + +msgid "Something broke on our side." +msgstr "Qualcosa non va da questo lato." + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "Scusa per questo. Se pensi sia un bug, per favore segnalacelo." + +msgid "Invalid CSRF token" +msgstr "Token CSRF non valido" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "Qualcosa è andato storto con il tuo token CSRF. Assicurati di aver abilitato i cookies nel tuo browser, e prova a ricaricare questa pagina. Se l'errore si dovesse ripresentare, per favore segnalacelo." + +msgid "You are not authorized." +msgstr "Non sei autorizzato." + +msgid "Page not found" +msgstr "Pagina non trovata" + +msgid "We couldn't find this page." +msgstr "Non riusciamo a trovare questa pagina." + +msgid "The link that led you here may be broken." +msgstr "Il collegamento che ti ha portato qui potrebbe non essere valido." + +msgid "Users" +msgstr "Utenti" + +msgid "Configuration" +msgstr "Configurazione" + +msgid "Instances" +msgstr "Istanze" + +msgid "Email blocklist" +msgstr "Blocklist dell'email" + +msgid "Grant admin rights" +msgstr "Garantisci diritti dell'admin" + +msgid "Revoke admin rights" +msgstr "Revoca diritti dell'admin" + +msgid "Grant moderator rights" +msgstr "Garantisci diritti del moderatore" + +msgid "Revoke moderator rights" +msgstr "Revoca diritti del moderatore" + +msgid "Ban" +msgstr "Bandisci" + +msgid "Run on selected users" +msgstr "Esegui sugli utenti selezionati" + +msgid "Moderator" +msgstr "Moderatore" + +msgid "Moderation" +msgstr "Moderazione" + +msgid "Home" +msgstr "Home" + +msgid "Administration of {0}" +msgstr "Amministrazione di {0}" + +msgid "Unblock" +msgstr "Sblocca" + +msgid "Block" +msgstr "Blocca" + +msgid "Name" +msgstr "Nome" + +msgid "Allow anyone to register here" +msgstr "Permetti a chiunque di registrarsi qui" + +msgid "Short description" +msgstr "Descrizione breve" + +msgid "Markdown syntax is supported" +msgstr "La sintassi Markdown è supportata" + +msgid "Long description" +msgstr "Descrizione lunga" + +msgid "Default article license" +msgstr "Licenza predefinita degli articoli" + +msgid "Save these settings" +msgstr "Salva queste impostazioni" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "Se stai navigando in questo sito come visitatore, non vengono raccolti dati su di te." + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "Come utente registrato, devi fornire il tuo nome utente (che può anche non essere il tuo vero nome), un tuo indirizzo email funzionante e una password, per poter accedere, scrivere articoli e commenti. Il contenuto che invii è memorizzato fino a quando non lo elimini." + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "Quando accedi, conserviamo due cookie, uno per mantenere aperta la sessione, il secondo per impedire ad altre persone di agire al tuo posto. Non conserviamo nessun altro cookie." + +msgid "Blocklisted Emails" +msgstr "Email Blocklist" + +msgid "Email address" +msgstr "Indirizzo email" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "L'indirizzo email che vuoi bloccare. Per bloccare i domini, puoi usare la sintassi di globbing, per esempio '*@example.com' blocca tutti gli indirizzi da example.com" + +msgid "Note" +msgstr "Nota" + +msgid "Notify the user?" +msgstr "Notifica l'utente?" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "Opzionale, mostra un messaggio all'utente quando tenta di creare un conto con quell'indirizzo" + +msgid "Blocklisting notification" +msgstr "Notifica di blocklist" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "Il messaggio da mostrare quando l'utente tenta di creare un profilo con questo indirizzo email" + +msgid "Add blocklisted address" +msgstr "Aggiungi indirizzo messo in blocklist" + +msgid "There are no blocked emails on your instance" +msgstr "Non ci sono email bloccate sulla tua istanza" + +msgid "Delete selected emails" +msgstr "Elimina email selezionata" + +msgid "Email address:" +msgstr "Indirizzo email:" + +msgid "Blocklisted for:" +msgstr "Messo in blocklist per:" + +msgid "Will notify them on account creation with this message:" +msgstr "Li notificherà alla creazione del profilo con questo messaggio:" + +msgid "The user will be silently prevented from making an account" +msgstr "L'utente sarà prevenuto silenziosamente dal creare un profilo" + +msgid "Welcome to {}" +msgstr "Benvenuto su {}" + +msgid "View all" +msgstr "Vedi tutto" + +msgid "About {0}" +msgstr "A proposito di {0}" + +msgid "Runs Plume {0}" +msgstr "Utilizza Plume {0}" + +msgid "Home to {0} people" +msgstr "Casa di {0} persone" + +msgid "Who wrote {0} articles" +msgstr "Che hanno scritto {0} articoli" + +msgid "And are connected to {0} other instances" +msgstr "E sono connessi ad altre {0} istanze" + +msgid "Administred by" +msgstr "Amministrata da" + +msgid "Interact with {}" +msgstr "Interagisci con {}" + +msgid "Log in to interact" +msgstr "Accedi per interagire" + +msgid "Enter your full username to interact" +msgstr "Inserisci il tuo nome utente completo per interagire" + +msgid "Publish" +msgstr "Pubblica" + +msgid "Classic editor (any changes will be lost)" +msgstr "Editor classico (eventuali modifiche andranno perse)" + +msgid "Title" +msgstr "Titolo" + +msgid "Subtitle" +msgstr "Sottotitolo" + +msgid "Content" +msgstr "Contenuto" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "Puoi caricare media nella tua galleria, e poi copiare il loro codice Markdown nei tuoi articoli per inserirli." + +msgid "Upload media" +msgstr "Carica media" + +msgid "Tags, separated by commas" +msgstr "Etichette, separate da virgole" + +msgid "License" +msgstr "Licenza" + +msgid "Illustration" +msgstr "Illustrazione" + +msgid "This is a draft, don't publish it yet." +msgstr "Questa è una bozza, non pubblicarla ancora." + +msgid "Update" +msgstr "Aggiorna" + +msgid "Update, or publish" +msgstr "Aggiorna, o pubblica" + +msgid "Publish your post" +msgstr "Pubblica il tuo post" + +msgid "Written by {0}" +msgstr "Scritto da {0}" + +msgid "All rights reserved." +msgstr "Tutti i diritti riservati." + +msgid "This article is under the {0} license." +msgstr "Questo articolo è rilasciato con licenza {0}." + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "Un mi piace" +msgstr[1] "{0} mi piace" + +msgid "I don't like this anymore" +msgstr "Non mi piace più questo" + +msgid "Add yours" +msgstr "Aggiungi il tuo" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "Un boost" +msgstr[1] "{0} boost" + +msgid "I don't want to boost this anymore" +msgstr "Non voglio più boostare questo" + +msgid "Boost" +msgstr "Boost" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "{0}Accedi{1}, o {2}usa il tuo account del Fediverso{3} per interagire con questo articolo" + +msgid "Comments" +msgstr "Commenti" + +msgid "Your comment" +msgstr "Il tuo commento" + +msgid "Submit comment" +msgstr "Invia commento" + +msgid "No comments yet. Be the first to react!" +msgstr "Ancora nessun commento. Sii il primo ad aggiungere la tua reazione!" + +msgid "Are you sure?" +msgstr "Sei sicuro?" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "Questo articolo è ancora una bozza. Solo voi e gli altri autori la potete vedere." + +msgid "Only you and other authors can edit this article." +msgstr "Solo tu e gli altri autori potete modificare questo articolo." + +msgid "Edit" +msgstr "Modifica" + +msgid "I'm from this instance" +msgstr "Io appartengo a questa istanza" + +msgid "Username, or email" +msgstr "Nome utente, o email" + +msgid "Log in" +msgstr "Accedi" + +msgid "I'm from another instance" +msgstr "Io sono di un'altra istanza" + +msgid "Continue to your instance" +msgstr "Continua verso la tua istanza" + +msgid "Reset your password" +msgstr "Reimposta la tua password" + +msgid "New password" +msgstr "Nuova password" + +msgid "Confirmation" +msgstr "Conferma" + +msgid "Update password" +msgstr "Aggiorna password" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "Ti abbiamo inviato una mail all'indirizzo che ci hai fornito, con il collegamento per reimpostare la tua password." + +msgid "Send password reset link" +msgstr "Invia collegamento per reimpostare la password" + +msgid "This token has expired" +msgstr "Questo token è scaduto" + +msgid "Please start the process again by clicking here." +msgstr "Sei pregato di riavviare il processo cliccando qui." + +msgid "New Blog" +msgstr "Nuovo Blog" + +msgid "Create a blog" +msgstr "Crea un blog" + +msgid "Create blog" +msgstr "Crea blog" + +msgid "Edit \"{}\"" +msgstr "Modifica \"{}\"" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "Puoi caricare immagini nella tua galleria, ed utilizzarle come icone del blog, o copertine." + +msgid "Upload images" +msgstr "Carica immagini" + +msgid "Blog icon" +msgstr "Icona del blog" + +msgid "Blog banner" +msgstr "Copertina del blog" + +msgid "Custom theme" +msgstr "Tema personalizzato" + +msgid "Update blog" +msgstr "Aggiorna blog" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "Fai molta attenzione, qualsiasi scelta fatta qui non può essere annullata." + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "Sei sicuro di voler eliminare permanentemente questo blog?" + +msgid "Permanently delete this blog" +msgstr "Elimina permanentemente questo blog" + +msgid "{}'s icon" +msgstr "Icona di {}" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "C'è un autore su questo blog: " +msgstr[1] "Ci sono {0} autori su questo blog: " + +msgid "No posts to see here yet." +msgstr "Nessun post da mostrare qui." + +msgid "Nothing to see here yet." +msgstr "Ancora niente da vedere qui." + +msgid "None" +msgstr "Nessuna" + +msgid "No description" +msgstr "Nessuna descrizione" + +msgid "Respond" +msgstr "Rispondi" + +msgid "Delete this comment" +msgstr "Elimina questo commento" + +msgid "What is Plume?" +msgstr "Cos'è Plume?" + +msgid "Plume is a decentralized blogging engine." +msgstr "Plume è un motore di blog decentralizzato." + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "Gli autori possono gestire blog multipli, ognuno come fosse un sito web differente." + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "Gli articoli sono anche visibili su altre istanze Plume, e puoi interagire con loro direttamente da altre piattaforme come Mastodon." + +msgid "Read the detailed rules" +msgstr "Leggi le regole dettagliate" + +msgid "By {0}" +msgstr "Da {0}" + +msgid "Draft" +msgstr "Bozza" + +msgid "Search result(s) for \"{0}\"" +msgstr "Risultato(i) della ricerca per \"{0}\"" + +msgid "Search result(s)" +msgstr "Risultato(i) della ricerca" + +msgid "No results for your query" +msgstr "Nessun risultato per la tua ricerca" + +msgid "No more results for your query" +msgstr "Nessun altro risultato per la tua ricerca" + +msgid "Advanced search" +msgstr "Ricerca avanzata" + +msgid "Article title matching these words" +msgstr "Titoli di articolo che corrispondono a queste parole" + +msgid "Subtitle matching these words" +msgstr "Sottotitoli che corrispondono a queste parole" + +msgid "Content macthing these words" +msgstr "Corrispondenza del contenuto di queste parole" + +msgid "Body content" +msgstr "Contenuto del testo" + +msgid "From this date" +msgstr "Da questa data" + +msgid "To this date" +msgstr "A questa data" + +msgid "Containing these tags" +msgstr "Contenente queste etichette" + +msgid "Tags" +msgstr "Etichette" + +msgid "Posted on one of these instances" +msgstr "Pubblicato su una di queste istanze" + +msgid "Instance domain" +msgstr "Dominio dell'istanza" + +msgid "Posted by one of these authors" +msgstr "Pubblicato da uno di questi autori" + +msgid "Author(s)" +msgstr "Autore(i)" + +msgid "Posted on one of these blogs" +msgstr "Pubblicato da uno di questi blog" + +msgid "Blog title" +msgstr "Titolo del blog" + +msgid "Written in this language" +msgstr "Scritto in questa lingua" + +msgid "Language" +msgstr "Lingua" + +msgid "Published under this license" +msgstr "Pubblicato sotto questa licenza" + +msgid "Article license" +msgstr "Licenza dell'articolo" + diff --git a/po/plume/ja.po b/po/plume/ja.po new file mode 100644 index 00000000000..5b3e8f1519a --- /dev/null +++ b/po/plume/ja.po @@ -0,0 +1,1031 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Japanese\n" +"Language: ja_JP\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: ja\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} さんがあなたの投稿にコメントしました。" + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} さんがあなたをフォローしました。" + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} さんがあなたの投稿にいいねしました。" + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} さんがあなたをメンションしました。" + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} さんがあなたの投稿をブーストしました。" + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "自分のフィード" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "このインスタンスのフィード" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "全インスタンスのフィード" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "{0} さんのアバター" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "前のページ" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "次のページ" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "省略可" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "新しいブログを作成するにはログインが必要です" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "同じ名前のブログがすでに存在しています。" + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "ブログは正常に作成されました。" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "ブログを削除しました。" + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "このブログを削除する権限がありません。" + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "このブログを編集する権限がありません。" + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "このメディアはブログアイコンに使用できません。" + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "このメディアはブログバナーに使用できません。" + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "ブログ情報を更新しました。" + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "コメントを投稿しました。" + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "コメントを削除しました。" + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "このインスタンスでは登録がクローズされています。" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "ユーザー登録" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "こちらのリンクから、ユーザー登録できます: {0}" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "アカウントを作成しました。使用前に、ログインする必要があります。" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "インスタンスの設定を保存しました。" + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "{} のブロックを解除しました。" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "{} をブロックしました。" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "ブロックリストから削除しました" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "メールは既にブロックされています" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "メールがブロックされました" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "あなた自身の権限は変更できません。" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "この操作を行う権限がありません。" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "完了しました。" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "投稿をいいねするにはログインが必要です" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "メディアを削除しました。" + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "このメディアを削除する権限がありません。" + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "アバターを更新しました。" + +# src/routes/medias.rs:180 +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 "通知を表示するにはログインが必要です" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "この投稿はまだ公開されていません。" + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "新しい投稿を書くにはログインが必要です" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "あなたはこのブログの投稿者ではありません。" + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "新しい投稿" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "{0} を編集" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "このブログで投稿を公開する権限がありません。" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "投稿を更新しました。" + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "投稿を保存しました。" + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "新しい投稿" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "この投稿を削除する権限がありません。" + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "投稿を削除しました。" + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "削除しようとしている投稿は存在しないようです。すでに削除していませんか?" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "お使いのアカウントに関する十分な情報を取得できませんでした。ご自身のユーザー名が正しいことを確認してください。" + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "投稿を再共有するにはログインが必要です" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "接続しました。" + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "ログアウトしました。" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "パスワードのリセット" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "こちらのリンクから、パスワードをリセットできます: {0}" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "パスワードが正常にリセットされました。" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "ダッシュボードにアクセスするにはログインが必要です" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "{} のフォローを解除しました。" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "{} をフォローしました。" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "誰かをフォローするにはログインが必要です" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "プロフィールを編集するにはログインが必要です" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "プロフィールを更新しました。" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "アカウントを削除しました。" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "他人のアカウントは削除できません。" + +msgid "Create your account" +msgstr "アカウントを作成" + +msgid "Create an account" +msgstr "アカウントを作成" + +msgid "Email" +msgstr "メールアドレス" + +msgid "Email confirmation" +msgstr "メール確認" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "申し訳ありませんが、このインスタンスでの登録は限定されています。ですが、他のインスタンスを見つけることはできます。" + +msgid "Registration" +msgstr "ユーザー登録" + +msgid "Check your inbox!" +msgstr "受信トレイを確認してください!" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "指定された宛先に、ユーザー登録するためのリンクを記載したメールを送信しました。" + +msgid "Username" +msgstr "ユーザー名" + +msgid "Password" +msgstr "パスワード" + +msgid "Password confirmation" +msgstr "パスワードの確認" + +msgid "Media upload" +msgstr "メディアのアップロード" + +msgid "Description" +msgstr "説明" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "ライセンス情報と同様に、視覚に障害のある方に役立ちます" + +msgid "Content warning" +msgstr "コンテンツの警告" + +msgid "Leave it empty, if none is needed" +msgstr "何も必要でない場合は、空欄にしてください" + +msgid "File" +msgstr "ファイル" + +msgid "Send" +msgstr "送信" + +msgid "Your media" +msgstr "メディア" + +msgid "Upload" +msgstr "アップロード" + +msgid "You don't have any media yet." +msgstr "メディアがまだありません。" + +msgid "Content warning: {0}" +msgstr "コンテンツの警告: {0}" + +msgid "Delete" +msgstr "削除" + +msgid "Details" +msgstr "詳細" + +msgid "Media details" +msgstr "メディアの詳細" + +msgid "Go back to the gallery" +msgstr "ギャラリーに戻る" + +msgid "Markdown syntax" +msgstr "Markdown 記法" + +msgid "Copy it into your articles, to insert this media:" +msgstr "このメディアを挿入するには、これを投稿にコピーしてください。" + +msgid "Use as an avatar" +msgstr "アバターとして使う" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "メニュー" + +msgid "Search" +msgstr "検索" + +msgid "Dashboard" +msgstr "ダッシュボード" + +msgid "Notifications" +msgstr "通知" + +msgid "Log Out" +msgstr "ログアウト" + +msgid "My account" +msgstr "自分のアカウント" + +msgid "Log In" +msgstr "ログイン" + +msgid "Register" +msgstr "登録" + +msgid "About this instance" +msgstr "このインスタンスについて" + +msgid "Privacy policy" +msgstr "プライバシーポリシー" + +msgid "Administration" +msgstr "管理" + +msgid "Documentation" +msgstr "ドキュメンテーション" + +msgid "Source code" +msgstr "ソースコード" + +msgid "Matrix room" +msgstr "Matrix ルーム" + +msgid "Admin" +msgstr "管理者" + +msgid "It is you" +msgstr "自分" + +msgid "Edit your profile" +msgstr "プロフィールを編集" + +msgid "Open on {0}" +msgstr "{0} で開く" + +msgid "Unsubscribe" +msgstr "フォロー解除" + +msgid "Subscribe" +msgstr "フォロー" + +msgid "Follow {}" +msgstr "{} をフォロー" + +msgid "Log in to follow" +msgstr "フォローするにはログインしてください" + +msgid "Enter your full username handle to follow" +msgstr "フォローするにはご自身の完全なユーザー名を入力してください" + +msgid "{0}'s subscribers" +msgstr "{0} のフォロワー" + +msgid "Articles" +msgstr "投稿" + +msgid "Subscribers" +msgstr "フォロワー" + +msgid "Subscriptions" +msgstr "フォロー" + +msgid "{0}'s subscriptions" +msgstr "{0} がフォロー中のユーザー" + +msgid "Your Dashboard" +msgstr "ダッシュボード" + +msgid "Your Blogs" +msgstr "ブログ" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "ブログはまだありません。ご自身のブログを作成するか、他の人のブログに参加できるか確認しましょう。" + +msgid "Start a new blog" +msgstr "新しいブログを開始" + +msgid "Your Drafts" +msgstr "下書き" + +msgid "Go to your gallery" +msgstr "ギャラリーを参照" + +msgid "Edit your account" +msgstr "アカウントを編集" + +msgid "Your Profile" +msgstr "プロフィール" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "アバターを変更するには、ギャラリーにアップロードして選択してください。" + +msgid "Upload an avatar" +msgstr "アバターをアップロード" + +msgid "Display name" +msgstr "表示名" + +msgid "Summary" +msgstr "概要" + +msgid "Theme" +msgstr "テーマ" + +msgid "Default theme" +msgstr "デフォルトテーマ" + +msgid "Error while loading theme selector." +msgstr "テーマセレクターの読み込み中にエラーが発生しました。" + +msgid "Never load blogs custom themes" +msgstr "ブログのカスタムテーマを読み込まない" + +msgid "Update account" +msgstr "アカウントを更新" + +msgid "Danger zone" +msgstr "危険な設定" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "ここで行われた操作は取り消しできません。十分注意してください。" + +msgid "Delete your account" +msgstr "アカウントを削除" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "申し訳ありませんが、管理者は自身のインスタンスから離脱できません。" + +msgid "Latest articles" +msgstr "最新の投稿" + +msgid "Atom feed" +msgstr "Atom フィード" + +msgid "Recently boosted" +msgstr "最近ブーストしたもの" + +msgid "Articles tagged \"{0}\"" +msgstr "\"{0}\" タグがついた投稿" + +msgid "There are currently no articles with such a tag" +msgstr "現在このタグがついた投稿はありません" + +msgid "The content you sent can't be processed." +msgstr "送信された内容を処理できません。" + +msgid "Maybe it was too long." +msgstr "長すぎる可能性があります。" + +msgid "Internal server error" +msgstr "内部サーバーエラー" + +msgid "Something broke on our side." +msgstr "サーバー側で何らかの問題が発生しました。" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "申し訳ありません。これがバグだと思われる場合は、問題を報告してください。" + +msgid "Invalid CSRF token" +msgstr "無効な CSRF トークンです" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "CSRF トークンに問題が発生しました。お使いのブラウザーで Cookie が有効になっていることを確認して、このページを再読み込みしてみてください。このエラーメッセージが表示され続ける場合は、問題を報告してください。" + +msgid "You are not authorized." +msgstr "許可されていません。" + +msgid "Page not found" +msgstr "ページが見つかりません" + +msgid "We couldn't find this page." +msgstr "このページは見つかりませんでした。" + +msgid "The link that led you here may be broken." +msgstr "このリンクは切れている可能性があります。" + +msgid "Users" +msgstr "ユーザー" + +msgid "Configuration" +msgstr "設定" + +msgid "Instances" +msgstr "インスタンス" + +msgid "Email blocklist" +msgstr "メールのブロックリスト" + +msgid "Grant admin rights" +msgstr "管理者権限を付与" + +msgid "Revoke admin rights" +msgstr "管理者権限を取り消す" + +msgid "Grant moderator rights" +msgstr "モデレーター権限を付与" + +msgid "Revoke moderator rights" +msgstr "モデレーター権限を取り消す" + +msgid "Ban" +msgstr "アカウント停止" + +msgid "Run on selected users" +msgstr "選択したユーザーで実行" + +msgid "Moderator" +msgstr "モデレーター" + +msgid "Moderation" +msgstr "モデレーション" + +msgid "Home" +msgstr "ホーム" + +msgid "Administration of {0}" +msgstr "{0} の管理" + +msgid "Unblock" +msgstr "ブロック解除" + +msgid "Block" +msgstr "ブロック" + +msgid "Name" +msgstr "名前" + +msgid "Allow anyone to register here" +msgstr "不特定多数に登録を許可" + +msgid "Short description" +msgstr "短い説明" + +msgid "Markdown syntax is supported" +msgstr "Markdown 記法に対応しています。" + +msgid "Long description" +msgstr "長い説明" + +msgid "Default article license" +msgstr "投稿のデフォルトのライセンス" + +msgid "Save these settings" +msgstr "設定を保存" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "閲覧者としてこのサイトをご覧になっている場合、ご自身のデータは一切収集されません。" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "登録ユーザーの場合、ログインしたり記事やコメントを投稿したりできるようにするため、ご自身のユーザー名(本名である必要はありません)、利用可能なメールアドレス、パスワードを指定する必要があります。投稿したコンテンツは、削除しない限り保存されます。" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "ログインの際に、2 個の Cookie を保存します。1 つはセッションを開いた状態にするため、もう 1 つは誰かがあなたになりすますのを防ぐために使われます。この他には、一切の Cookie を保存しません。" + +msgid "Blocklisted Emails" +msgstr "ブロックするメール" + +msgid "Email address" +msgstr "メールアドレス" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "ブロックするメールアドレス。*記法を使ってドメインをブロックすることができます。例えば '*@example.com' はexample.com の全てのアドレスをブロックします。" + +msgid "Note" +msgstr "メモ" + +msgid "Notify the user?" +msgstr "ユーザーに知らせるか?" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "省略可。このアドレスでアカウントを作ろうとした時にユーザーにメッセージを表示します" + +msgid "Blocklisting notification" +msgstr "ブロックリストの通知" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "ユーザーがこのメールアドレスでアカウントを作ろうとした時に表示するメッセージ" + +msgid "Add blocklisted address" +msgstr "ブロックするアドレスを追加" + +msgid "There are no blocked emails on your instance" +msgstr "このインスタンス上でブロックされたメールアドレスはありません" + +msgid "Delete selected emails" +msgstr "選択したメールアドレスを削除" + +msgid "Email address:" +msgstr "メールアドレス:" + +msgid "Blocklisted for:" +msgstr "ブロック理由:" + +msgid "Will notify them on account creation with this message:" +msgstr "アカウント作成時にユーザーにこのメッセージが通知されます:" + +msgid "The user will be silently prevented from making an account" +msgstr "ユーザーには知らせずにアカウント作成を防ぎます" + +msgid "Welcome to {}" +msgstr "{} へようこそ" + +msgid "View all" +msgstr "すべて表示" + +msgid "About {0}" +msgstr "{0} について" + +msgid "Runs Plume {0}" +msgstr "Plume {0} を実行中" + +msgid "Home to {0} people" +msgstr "ユーザー登録者数 {0} 人" + +msgid "Who wrote {0} articles" +msgstr "投稿記事数 {0} 件" + +msgid "And are connected to {0} other instances" +msgstr "他のインスタンスからの接続数 {0}" + +msgid "Administred by" +msgstr "管理者" + +msgid "Interact with {}" +msgstr "{} と関わる" + +msgid "Log in to interact" +msgstr "関わるにはログインしてください" + +msgid "Enter your full username to interact" +msgstr "関わるにはご自身の完全なユーザー名を入力してください" + +msgid "Publish" +msgstr "公開" + +msgid "Classic editor (any changes will be lost)" +msgstr "クラシックエディター (すべての変更を破棄します)" + +msgid "Title" +msgstr "タイトル" + +msgid "Subtitle" +msgstr "サブタイトル" + +msgid "Content" +msgstr "コンテンツ" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "メディアをギャラリーにアップロードして、その Markdown コードをコピーして投稿に挿入できます。" + +msgid "Upload media" +msgstr "メディアをアップロード" + +msgid "Tags, separated by commas" +msgstr "タグ (コンマ区切り)" + +msgid "License" +msgstr "ライセンス" + +msgid "Illustration" +msgstr "図" + +msgid "This is a draft, don't publish it yet." +msgstr "これは下書きなので、まだ公開しないでください。" + +msgid "Update" +msgstr "更新" + +msgid "Update, or publish" +msgstr "更新または公開" + +msgid "Publish your post" +msgstr "投稿を公開" + +msgid "Written by {0}" +msgstr "投稿者 {0}" + +msgid "All rights reserved." +msgstr "著作権は投稿者が保有しています。" + +msgid "This article is under the {0} license." +msgstr "この投稿は、{0} ライセンスの元で公開されています。" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "{0} いいね" + +msgid "I don't like this anymore" +msgstr "このいいねを取り消します" + +msgid "Add yours" +msgstr "いいねする" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "{0} ブースト" + +msgid "I don't want to boost this anymore" +msgstr "このブーストを取り消します" + +msgid "Boost" +msgstr "ブースト" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "この記事と関わるには{0}ログイン{1}するか {2}Fediverse アカウントを使用{3}してください" + +msgid "Comments" +msgstr "コメント" + +msgid "Your comment" +msgstr "あなたのコメント" + +msgid "Submit comment" +msgstr "コメントを保存" + +msgid "No comments yet. Be the first to react!" +msgstr "コメントがまだありません。最初のコメントを書きましょう!" + +msgid "Are you sure?" +msgstr "本当によろしいですか?" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "この投稿は下書きです。あなたと他の投稿者のみが閲覧できます。" + +msgid "Only you and other authors can edit this article." +msgstr "あなたと他の投稿者のみがこの投稿を編集できます。" + +msgid "Edit" +msgstr "編集" + +msgid "I'm from this instance" +msgstr "このインスタンスから閲覧しています" + +msgid "Username, or email" +msgstr "ユーザー名またはメールアドレス" + +msgid "Log in" +msgstr "ログイン" + +msgid "I'm from another instance" +msgstr "別のインスタンスから閲覧しています" + +msgid "Continue to your instance" +msgstr "ご自身のインスタンスに移動" + +msgid "Reset your password" +msgstr "パスワードをリセット" + +msgid "New password" +msgstr "新しいパスワード" + +msgid "Confirmation" +msgstr "確認" + +msgid "Update password" +msgstr "パスワードを更新" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "指定された宛先に、パスワードをリセットするためのリンクを記載したメールを送信しました。" + +msgid "Send password reset link" +msgstr "パスワードリセットリンクを送信" + +msgid "This token has expired" +msgstr "このトークンは有効期限切れです" + +msgid "Please start the process again by clicking here." +msgstr "こちらをクリックして、手順をやり直してください。" + +msgid "New Blog" +msgstr "新しいブログ" + +msgid "Create a blog" +msgstr "ブログを作成" + +msgid "Create blog" +msgstr "ブログを作成" + +msgid "Edit \"{}\"" +msgstr "\"{}\" を編集" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "ギャラリーにアップロードした画像を、ブログアイコンやバナーに使用できます。" + +msgid "Upload images" +msgstr "画像をアップロード" + +msgid "Blog icon" +msgstr "ブログアイコン" + +msgid "Blog banner" +msgstr "ブログバナー" + +msgid "Custom theme" +msgstr "カスタムテーマ" + +msgid "Update blog" +msgstr "ブログを更新" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "ここで行われた操作は元に戻せません。十分注意してください。" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "このブログを完全に削除してもよろしいですか?" + +msgid "Permanently delete this blog" +msgstr "このブログを完全に削除" + +msgid "{}'s icon" +msgstr "{} さんのアイコン" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "このブログには {0} 人の投稿者がいます: " + +msgid "No posts to see here yet." +msgstr "ここには表示できる投稿はまだありません。" + +msgid "Nothing to see here yet." +msgstr "ここにはまだ表示できる項目がありません。" + +msgid "None" +msgstr "なし" + +msgid "No description" +msgstr "説明がありません" + +msgid "Respond" +msgstr "返信" + +msgid "Delete this comment" +msgstr "このコメントを削除" + +msgid "What is Plume?" +msgstr "Plume とは?" + +msgid "Plume is a decentralized blogging engine." +msgstr "Plume は分散型ブログエンジンです。" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "作成者は、それぞれ独自のウェブサイトとして複数のブログを管理できます。" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "投稿は他の Plume インスタンスからも閲覧可能であり、Mastdon のように他のプラットフォームから直接記事にアクセスできます。" + +msgid "Read the detailed rules" +msgstr "詳細な規則を読む" + +msgid "By {0}" +msgstr "投稿者 {0}" + +msgid "Draft" +msgstr "下書き" + +msgid "Search result(s) for \"{0}\"" +msgstr "\"{0}\" の検索結果" + +msgid "Search result(s)" +msgstr "検索結果" + +msgid "No results for your query" +msgstr "検索結果はありません" + +msgid "No more results for your query" +msgstr "これ以上の検索結果はありません" + +msgid "Advanced search" +msgstr "高度な検索" + +msgid "Article title matching these words" +msgstr "投稿のタイトルに一致する語句" + +msgid "Subtitle matching these words" +msgstr "サブタイトルに一致する語句" + +msgid "Content macthing these words" +msgstr "内容に一致する語句" + +msgid "Body content" +msgstr "本文の内容" + +msgid "From this date" +msgstr "この日付以降を検索" + +msgid "To this date" +msgstr "この日付以前を検索" + +msgid "Containing these tags" +msgstr "含まれるタグ" + +msgid "Tags" +msgstr "タグ" + +msgid "Posted on one of these instances" +msgstr "以下のいずれかのインスタンスに投稿" + +msgid "Instance domain" +msgstr "インスタンスのドメイン" + +msgid "Posted by one of these authors" +msgstr "以下のいずれかの投稿者が投稿" + +msgid "Author(s)" +msgstr "投稿者" + +msgid "Posted on one of these blogs" +msgstr "以下のいずれかのブログに投稿" + +msgid "Blog title" +msgstr "ブログのタイトル" + +msgid "Written in this language" +msgstr "投稿の言語" + +msgid "Language" +msgstr "言語" + +msgid "Published under this license" +msgstr "適用されているライセンス" + +msgid "Article license" +msgstr "投稿のライセンス" + diff --git a/po/plume/ko.po b/po/plume/ko.po new file mode 100644 index 00000000000..01e90b4dfcf --- /dev/null +++ b/po/plume/ko.po @@ -0,0 +1,1031 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Korean\n" +"Language: ko_KR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: ko\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "" + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "" + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "" + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "" + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "" + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "" + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "" + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "" + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "" + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "" + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "" + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "" + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "" + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "" + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "" + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "" + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "" + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "" + +# src/routes/medias.rs:180 +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 "" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "" + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "" + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "" + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "" + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "" + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "" + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "" + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "" + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "" + +msgid "Create an account" +msgstr "" + +msgid "Email" +msgstr "" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Your media" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "" + +msgid "Menu" +msgstr "" + +msgid "Search" +msgstr "" + +msgid "Dashboard" +msgstr "" + +msgid "Notifications" +msgstr "" + +msgid "Log Out" +msgstr "" + +msgid "My account" +msgstr "" + +msgid "Log In" +msgstr "" + +msgid "Register" +msgstr "" + +msgid "About this instance" +msgstr "" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "" + +msgid "It is you" +msgstr "" + +msgid "Edit your profile" +msgstr "" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "" + +msgid "Follow {}" +msgstr "" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "" + +msgid "Subscribers" +msgstr "" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "" + +msgid "Your Drafts" +msgstr "" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "" + +msgid "Your Profile" +msgstr "" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "" + +msgid "Configuration" +msgstr "" + +msgid "Instances" +msgstr "" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "" + +msgid "Unblock" +msgstr "" + +msgid "Block" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Allow anyone to register here" +msgstr "" + +msgid "Short description" +msgstr "" + +msgid "Markdown syntax is supported" +msgstr "" + +msgid "Long description" +msgstr "" + +msgid "Default article license" +msgstr "" + +msgid "Save these settings" +msgstr "" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +msgid "Administred by" +msgstr "" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +msgid "Tags, separated by commas" +msgstr "" + +msgid "License" +msgstr "" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "" + +msgid "I don't like this anymore" +msgstr "" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "" + +msgid "Comments" +msgstr "" + +msgid "Your comment" +msgstr "" + +msgid "Submit comment" +msgstr "" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "" + +msgid "Log in" +msgstr "" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +msgid "New password" +msgstr "" + +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "" + +msgid "Create a blog" +msgstr "" + +msgid "Create blog" +msgstr "" + +msgid "Edit \"{}\"" +msgstr "" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +msgid "From this date" +msgstr "" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" + diff --git a/po/plume/nb.po b/po/plume/nb.po new file mode 100644 index 00000000000..f1c0a12040e --- /dev/null +++ b/po/plume/nb.po @@ -0,0 +1,1329 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2018-12-22 11:48+0000\n" +"Last-Translator: Allan Nordhøy \n" +"Language-Team: Norwegian Bokmål \n" +"Language: nb\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 3.2.2\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +#, fuzzy +msgid "{0} commented on your article." +msgstr "{0} la inn en kommentar til artikkelen din" + +# src/template_utils.rs:35 +msgid "{0} is subscribed to you." +msgstr "" + +#, fuzzy +msgid "{0} liked your article." +msgstr "{0} likte artikkelen din" + +#, fuzzy +msgid "{0} mentioned you." +msgstr "{0} la inn en kommentar til artikkelen din" + +#, fuzzy +msgid "{0} boosted your article." +msgstr "{0} la inn en kommentar til artikkelen din" + +#, fuzzy +msgid "Your feed" +msgstr "Din kommentar" + +#, fuzzy +msgid "Local feed" +msgstr "Din kommentar" + +msgid "Federated feed" +msgstr "" + +# src/template_utils.rs:68 +msgid "{0}'s avatar" +msgstr "" + +# src/template_utils.rs:198 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:209 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:146 +msgid "Optional" +msgstr "" + +# src/routes/blogs.rs:70 +msgid "To create a new blog, you need to be logged in" +msgstr "" + +#, fuzzy +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." + +#, fuzzy +msgid "You are not allowed to edit this blog." +msgstr "Du er ikke denne bloggens forfatter." + +#, fuzzy +msgid "You can't use this media as a blog icon." +msgstr "Du er ikke denne bloggens forfatter." + +#, fuzzy +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 "" + +#, fuzzy +msgid "Your comment has been posted." +msgstr "Ingen innlegg å vise enda." + +#, fuzzy +msgid "Your comment has been deleted." +msgstr "Ingen innlegg å vise enda." + +# src/routes/user.rs:473 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:132 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:133 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/user.rs:527 +msgid "" +"Your account has been created. Now you just need to log in, before you can " +"use it." +msgstr "" + +# src/routes/instance.rs:145 +msgid "Instance settings have been saved." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:154 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:218 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:223 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:314 +msgid "You can't change your own rights." +msgstr "" + +#, fuzzy +msgid "You are not allowed to take this action." +msgstr "Du er ikke denne bloggens forfatter." + +# src/routes/instance.rs:362 +msgid "Done." +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." + +#, fuzzy +msgid "Your avatar has been updated." +msgstr "Ingen innlegg å vise enda." + +#, 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 "" + +# src/routes/posts.rs:81 +msgid "This post isn't published yet." +msgstr "" + +# src/routes/posts.rs:120 +msgid "To write a new post, you need to be logged in" +msgstr "" + +#, fuzzy +msgid "You are not an author of this blog." +msgstr "Du er ikke denne bloggens forfatter." + +msgid "New post" +msgstr "Nytt innlegg" + +#, fuzzy +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." + +#, fuzzy +msgid "Your article has been updated." +msgstr "Ingen innlegg å vise enda." + +#, fuzzy +msgid "Your article has been saved." +msgstr "Ingen innlegg å vise enda." + +msgid "New article" +msgstr "Ny artikkel" + +#, fuzzy +msgid "You are not allowed to delete this article." +msgstr "Du er ikke denne bloggens forfatter." + +#, fuzzy +msgid "Your article has 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 " +"username is correct." +msgstr "" + +# src/routes/reshares.rs:47 +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" + +# src/routes/session.rs:156 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:199 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:148 +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 "" + +# src/routes/user.rs:287 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +#, fuzzy +msgid "Your profile has been updated." +msgstr "Ingen innlegg å vise enda." + +#, fuzzy +msgid "Your account has been deleted." +msgstr "Ingen innlegg å vise enda." + +# src/routes/user.rs:411 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "Opprett din konto" + +msgid "Create an account" +msgstr "Lag en ny konto" + +msgid "Email" +msgstr "Epost" + +#, fuzzy +msgid "Email confirmation" +msgstr "Oppsett" + +msgid "" +"Apologies, but registrations are closed on this particular instance. You " +"can, however, find a different one." +msgstr "" + +#, fuzzy +msgid "Registration" +msgstr "Registrér deg" + +msgid "Check your inbox!" +msgstr "" + +msgid "" +"We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "Brukernavn" + +msgid "Password" +msgstr "Passord" + +msgid "Password confirmation" +msgstr "Passordbekreftelse" + +msgid "Media upload" +msgstr "" + +#, fuzzy +msgid "Description" +msgstr "Lang beskrivelse" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +#, fuzzy +msgid "Content warning" +msgstr "Innhold" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +#, fuzzy +msgid "Your media" +msgstr "Din kommentar" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +#, fuzzy +msgid "Content warning: {0}" +msgstr "Innhold" + +msgid "Delete" +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +#, fuzzy +msgid "Markdown syntax" +msgstr "Du kan bruke markdown" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "Meny" + +msgid "Search" +msgstr "" + +msgid "Dashboard" +msgstr "Oversikt" + +#, fuzzy +msgid "Notifications" +msgstr "Oppsett" + +#, fuzzy +msgid "Log Out" +msgstr "Logg inn" + +msgid "My account" +msgstr "Min konto" + +msgid "Log In" +msgstr "Logg inn" + +msgid "Register" +msgstr "Registrér deg" + +msgid "About this instance" +msgstr "Om denne instansen" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "Administrasjon" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "Kildekode" + +msgid "Matrix room" +msgstr "Snakkerom" + +msgid "Admin" +msgstr "" + +#, fuzzy +msgid "It is you" +msgstr "Dette er deg" + +#, fuzzy +msgid "Edit your profile" +msgstr "Din profil" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "" + +#, fuzzy +msgid "Follow {}" +msgstr "Følg" + +#, fuzzy +msgid "Log in to follow" +msgstr "Logg inn" + +msgid "Enter your full username handle to follow" +msgstr "" + +#, fuzzy +msgid "{0}'s subscribers" +msgstr "Lang beskrivelse" + +#, fuzzy +msgid "Articles" +msgstr "artikler" + +#, fuzzy +msgid "Subscribers" +msgstr "Lang beskrivelse" + +#, fuzzy +msgid "Subscriptions" +msgstr "Lang beskrivelse" + +#, fuzzy +msgid "{0}'s subscriptions" +msgstr "Lang beskrivelse" + +msgid "Your Dashboard" +msgstr "Din oversikt" + +msgid "Your Blogs" +msgstr "" + +#, fuzzy +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" +"Du har ingen blogger enda. Lag din egen, eller be om å få bli med på en " +"annen." + +#, fuzzy +msgid "Start a new blog" +msgstr "Lag en ny blogg" + +#, fuzzy +msgid "Your Drafts" +msgstr "Din oversikt" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "Rediger kontoen din" + +#, fuzzy +msgid "Your Profile" +msgstr "Din profil" + +msgid "" +"To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +#, fuzzy +msgid "Display name" +msgstr "Visningsnavn" + +msgid "Summary" +msgstr "Sammendrag" + +msgid "Theme" +msgstr "" + +#, fuzzy +msgid "Default theme" +msgstr "Standardlisens" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "Oppdater konto" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +#, fuzzy +msgid "Delete your account" +msgstr "Opprett din konto" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "Siste artikler" + +#, fuzzy +msgid "Atom feed" +msgstr "Din kommentar" + +msgid "Recently boosted" +msgstr "Nylig delt" + +#, fuzzy +msgid "Articles tagged \"{0}\"" +msgstr "Om {0}" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "Noe gikk feil i vår ende." + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" +"Beklager så mye. Dersom du tror dette er en bug, vær grei å rapportér det " +"til oss." + +#, fuzzy +msgid "Invalid CSRF token" +msgstr "Ugyldig navn" + +msgid "" +"Something is wrong with your CSRF token. Make sure cookies are enabled in " +"you browser, and try reloading this page. If you continue to see this error " +"message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "Det har du har ikke tilgang til." + +msgid "Page not found" +msgstr "" + +#, fuzzy +msgid "We couldn't find this page." +msgstr "Den siden fant vi ikke." + +msgid "The link that led you here may be broken." +msgstr "Kanhende lenken som førte deg hit er ødelagt." + +#, fuzzy +msgid "Users" +msgstr "Brukernavn" + +msgid "Configuration" +msgstr "Oppsett" + +#, fuzzy +msgid "Instances" +msgstr "Instillinger for instansen" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +#, fuzzy +msgid "Moderation" +msgstr "Lang beskrivelse" + +msgid "Home" +msgstr "" + +#, fuzzy +msgid "Administration of {0}" +msgstr "Administrasjon" + +msgid "Unblock" +msgstr "" + +msgid "Block" +msgstr "" + +# src/template_utils.rs:144 +msgid "Name" +msgstr "" + +#, fuzzy +msgid "Allow anyone to register here" +msgstr "Tillat at hvem som helst registrerer seg" + +#, fuzzy +msgid "Short description" +msgstr "Lang beskrivelse" + +#, fuzzy +msgid "Markdown syntax is supported" +msgstr "Du kan bruke markdown" + +msgid "Long description" +msgstr "Lang beskrivelse" + +#, fuzzy +msgid "Default article license" +msgstr "Standardlisens" + +#, fuzzy +msgid "Save these settings" +msgstr "Lagre innstillingene" + +msgid "" +"If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "" +"As a registered user, you have to provide your username (which does not have " +"to be your real name), your functional email address and a password, in " +"order to be able to log in, write articles and comment. The content you " +"submit is stored until you delete it." +msgstr "" + +msgid "" +"When you log in, we store two cookies, one to keep your session open, the " +"second to prevent other people to act on your behalf. We don't store any " +"other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "" +"The email address you wish to block. In order to block domains, you can use " +"globbing syntax, for example '*@example.com' blocks all addresses from " +"example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "" +"Optional, shows a message to the user when they attempt to create an account " +"with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "" +"The message to be shown when the user attempts to create an account with " +"this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +#, fuzzy +msgid "Administred by" +msgstr "Administrasjon" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "Tittel" + +#, fuzzy +msgid "Subtitle" +msgstr "Tittel" + +msgid "Content" +msgstr "Innhold" + +msgid "" +"You can upload media to your gallery, and then copy their Markdown code into " +"your articles to insert them." +msgstr "" + +#, fuzzy +msgid "Upload media" +msgstr "Din kommentar" + +# src/template_utils.rs:143 +msgid "Tags, separated by commas" +msgstr "" + +# src/template_utils.rs:143 +msgid "License" +msgstr "" + +#, fuzzy +msgid "Illustration" +msgstr "Administrasjon" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +#, fuzzy +msgid "Update" +msgstr "Oppdater konto" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +#, fuzzy +msgid "This article is under the {0} license." +msgstr "Denne artikkelen er publisert med lisensen {0}" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "Ett hjerte" +msgstr[1] "{0} hjerter" + +#, fuzzy +msgid "I don't like this anymore" +msgstr "Jeg liker ikke dette lengre" + +msgid "Add yours" +msgstr "Legg til din" + +#, fuzzy +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "Én fremhevning" +msgstr[1] "{0} fremhevninger" + +#, fuzzy +msgid "I don't want to boost this anymore" +msgstr "Jeg ønsker ikke å dele dette lengre" + +msgid "Boost" +msgstr "" + +#, fuzzy +msgid "" +"{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this " +"article" +msgstr "" +"Logg inn eller bruk din Fediverse-konto for å gjøre noe med denne artikkelen" + +msgid "Comments" +msgstr "Kommetarer" + +msgid "Your comment" +msgstr "Din kommentar" + +msgid "Submit comment" +msgstr "Send kommentar" + +#, fuzzy +msgid "No comments yet. Be the first to react!" +msgstr "Ingen kommentarer enda. Vær den første!" + +msgid "Are you sure?" +msgstr "" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "" + +#, fuzzy +msgid "I'm from this instance" +msgstr "Om denne instansen" + +#, fuzzy +msgid "Username, or email" +msgstr "Brukernavn eller epost" + +#, fuzzy +msgid "Log in" +msgstr "Logg inn" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +#, fuzzy +msgid "New password" +msgstr "Passord" + +#, fuzzy +msgid "Confirmation" +msgstr "Oppsett" + +#, fuzzy +msgid "Update password" +msgstr "Oppdater konto" + +msgid "" +"We sent a mail to the address you gave us, with a link to reset your " +"password." +msgstr "" + +#, fuzzy +msgid "Send password reset link" +msgstr "Passord" + +msgid "This token has expired" +msgstr "" + +msgid "" +"Please start the process again by clicking here." +msgstr "" + +#, fuzzy +msgid "New Blog" +msgstr "Ny blogg" + +msgid "Create a blog" +msgstr "Lag en ny blogg" + +msgid "Create blog" +msgstr "Opprett blogg" + +#, fuzzy +msgid "Edit \"{}\"" +msgstr "Kommentér \"{0}\"" + +msgid "" +"You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +#, fuzzy +msgid "Upload images" +msgstr "Din kommentar" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +#, fuzzy +msgid "Update blog" +msgstr "Opprett blogg" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +#, fuzzy +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "Du er ikke denne bloggens forfatter." + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +#, fuzzy +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "Én forfatter av denne bloggen: " +msgstr[1] "{0} forfattere av denne bloggen: " + +msgid "No posts to see here yet." +msgstr "Ingen innlegg å vise enda." + +#, fuzzy +msgid "Nothing to see here yet." +msgstr "Ingen innlegg å vise enda." + +msgid "None" +msgstr "" + +#, fuzzy +msgid "No description" +msgstr "Lang beskrivelse" + +msgid "Respond" +msgstr "Svar" + +#, fuzzy +msgid "Delete this comment" +msgstr "Siste artikler" + +msgid "What is Plume?" +msgstr "Hva er Plume?" + +#, fuzzy +msgid "Plume is a decentralized blogging engine." +msgstr "Plume er et desentralisert bloggsystem." + +#, fuzzy +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "Forfattere kan administrere forskjellige blogger fra en unik webside." + +#, fuzzy +msgid "" +"Articles are also visible on other Plume instances, and you can interact " +"with them directly from other platforms like Mastodon." +msgstr "" +"Artiklene er også synlige på andre websider som kjører Plume, og du kan " +"interagere med dem direkte fra andre plattformer som f.eks. Mastodon." + +msgid "Read the detailed rules" +msgstr "Les reglene" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +# #-#-#-#-# nb.po (plume) #-#-#-#-# +# src/template_utils.rs:183 +msgid "Article title matching these words" +msgstr "" + +# #-#-#-#-# nb.po (plume) #-#-#-#-# +# src/template_utils.rs:183 +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +#, fuzzy +msgid "Body content" +msgstr "Innhold" + +# #-#-#-#-# nb.po (plume) #-#-#-#-# +# src/template_utils.rs:183 +msgid "From this date" +msgstr "" + +# #-#-#-#-# nb.po (plume) #-#-#-#-# +# src/template_utils.rs:183 +msgid "To this date" +msgstr "" + +# #-#-#-#-# nb.po (plume) #-#-#-#-# +# src/template_utils.rs:183 +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "" + +# #-#-#-#-# nb.po (plume) #-#-#-#-# +# src/template_utils.rs:183 +msgid "Posted on one of these instances" +msgstr "" + +#, fuzzy +msgid "Instance domain" +msgstr "Instillinger for instansen" + +# #-#-#-#-# nb.po (plume) #-#-#-#-# +# src/template_utils.rs:183 +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +# #-#-#-#-# nb.po (plume) #-#-#-#-# +# src/template_utils.rs:183 +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +# #-#-#-#-# nb.po (plume) #-#-#-#-# +# src/template_utils.rs:183 +#, fuzzy +msgid "Written in this language" +msgstr "" +"#-#-#-#-# nb.po (plume) #-#-#-#-#\n" +"#-#-#-#-# nb.po (plume) #-#-#-#-#\n" +"Den siden fant vi ikke." + +msgid "Language" +msgstr "" + +#, fuzzy +msgid "Published under this license" +msgstr "Denne artikkelen er publisert med lisensen {0}" + +#, fuzzy +msgid "Article license" +msgstr "Standardlisens" + +#, fuzzy +#~ msgid "Your query" +#~ msgstr "Din kommentar" + +#, fuzzy +#~ msgid "Subtitle - byline" +#~ msgstr "Tittel" + +#, fuzzy +#~ msgid "Articles from {}" +#~ msgstr "Om {0}" + +#, fuzzy +#~ msgid "E-mail" +#~ msgstr "Epost" + +#, fuzzy +#~ msgid "Delete this article" +#~ msgstr "Siste artikler" + +#, fuzzy +#~ msgid "Short description - byline" +#~ msgstr "Kort beskrivelse" + +#~ msgid "Login" +#~ msgstr "Logg inn" + +#, fuzzy +#~ msgid "You need to be logged in order to create a new blog" +#~ msgstr "Du må være logget inn for å lage en ny blogg" + +#, fuzzy +#~ msgid "You need to be logged in order to like a post" +#~ msgstr "Du må være logget inn for å like et innlegg" + +#~ msgid "You need to be logged in order to see your notifications" +#~ msgstr "Du må være logget inn for å se meldingene dine" + +#, fuzzy +#~ msgid "You need to be logged in order to write a new post" +#~ msgstr "Du må være logget inn for å skrive et nytt innlegg" + +#, fuzzy +#~ msgid "You need to be logged in order to reshare a post" +#~ msgstr "Du må være logget inn for å se meldingene dine" + +#, fuzzy +#~ msgid "You need to be logged in order to access your dashboard" +#~ msgstr "Du må være logget inn for å redigere profilen din" + +#, fuzzy +#~ msgid "You need to be logged in order to subscribe to someone" +#~ msgstr "Du må være logget inn for å følge noen" + +#, fuzzy +#~ msgid "You need to be logged in order to edit your profile" +#~ msgstr "Du må være logget inn for å redigere profilen din" + +#, fuzzy +#~ msgid "There's one article on this blog" +#~ msgid_plural "There are {0} articles on this blog" +#~ msgstr[0] "Én artikkel i denne bloggen" +#~ msgstr[1] "{0} artikler i denne bloggen" + +#, fuzzy +#~ msgid "{0}'s followers" +#~ msgstr "Én følger" + +#, fuzzy +#~ msgid "People {0} follows" +#~ msgstr "Én følger" + +#~ msgid "Followers" +#~ msgstr "Følgere" + +#, fuzzy +#~ msgid "Followed" +#~ msgstr "Følg" + +#~ msgid "Unfollow" +#~ msgstr "Slutt å følge" + +#~ msgid "New blog" +#~ msgstr "Ny blogg" + +#, fuzzy +#~ msgid "Create the blog" +#~ msgstr "Lag en ny blogg" + +#, fuzzy +#~ msgid "Submit your comment" +#~ msgstr "Send kommentar" + +#, fuzzy +#~ msgid "Create an article" +#~ msgstr "Lag en ny konto" + +#~ msgid "One follower" +#~ msgid_plural "{0} followers" +#~ msgstr[0] "Én følger" +#~ msgstr[1] "{0} følgere" + +#~ msgid "Display Name" +#~ msgstr "Visningsnavn" + +#, fuzzy +#~ msgid "Update your account" +#~ msgstr "Oppdater konto" + +#, fuzzy +#~ msgid "You need to be signed in, to be able to like a post" +#~ msgstr "Du må være logget inn for å skrive et nytt innlegg" + +#, fuzzy +#~ msgid "You need to be logged in, so that you can see your notifications" +#~ msgstr "Du må være logget inn for å se meldingene dine" + +#, fuzzy +#~ msgid "{0} gave a boost to your article" +#~ msgstr "{0} la inn en kommentar til artikkelen din" + +#, fuzzy +#~ msgid "{0} liked your post" +#~ msgstr "{0} likte artikkelen din" + +#, fuzzy +#~ msgid "You are not authorized to access this page." +#~ msgstr "Du er ikke denne bloggens forfatter." + +#, fuzzy +#~ msgid "The comment field can't be left empty" +#~ msgstr "Kommentaren din kan ikke være tom" + +#, fuzzy +#~ msgid "Your password field can't be left empty" +#~ msgstr "Kommentaren din kan ikke være tom" + +#, fuzzy +#~ msgid "The 'username' field can't be left empty" +#~ msgstr "Brukernavnet kan ikke være tomt" + +#~ msgid "Invalid email" +#~ msgstr "Ugyldig epost" + +#, fuzzy +#~ msgid "Your drafts" +#~ msgstr "Din oversikt" + +#, fuzzy +#~ msgid "Create a new post" +#~ msgstr "Lag et nytt innlegg" + +#~ msgid "Create a post" +#~ msgstr "Lag et nytt innlegg" + +#, fuzzy +#~ msgid "You need to be signed in, in order for you to like a post" +#~ msgstr "Du må være logget inn for å like et innlegg" diff --git a/po/plume/nl.po b/po/plume/nl.po new file mode 100644 index 00000000000..51d67ecfc15 --- /dev/null +++ b/po/plume/nl.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Dutch\n" +"Language: nl_NL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: nl\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} reageerde op je bericht." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} is op je geabonneerd." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} vond je artikel leuk." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} vermeldde jou." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} heeft je artikel geboost." + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "Jouw feed" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "Lokale feed" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "Gefedereerde feed" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "{0}'s avatar" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "Vorige pagina" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "Volgende pagina" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "Optioneel" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "Om een nieuwe blog te maken moet je ingelogd zijn" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "Er bestaat al een blog met dezelfde naam." + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "Je blog is succesvol aangemaakt!" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "Je blog is verwijderd." + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "Je mag deze blog niet verwijderen." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "Je mag deze blog niet bewerken." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "Je kunt dit object niet als blogpictogram gebruiken." + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "Je kunt dit object niet als blog banner gebruiken." + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "Je bloginformatie is bijgewerkt." + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "Je reactie is geplaatst." + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "Je reactie is verwijderd." + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "Registraties zijn gesloten op deze server." + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "Je account is aangemaakt. Nu hoe je alleen maar in te loggen, om het te kunnen gebruiken." + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "Serverinstellingen zijn opgeslagen." + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "{} is gedeblokkeerd." + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "{} is geblokkeerd." + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "Blokken verwijderd" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "E-mail al geblokkeerd" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "E-mailadres geblokkeerd" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "Je kunt je eigen rechten niet veranderen." + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "Je mag deze actie niet uitvoeren." + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "Klaar." + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "Om een bericht leuk te vinden, moet je ingelogd zijn" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "Je media zijn verwijderd." + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "Je mag dit medium niet verwijderen." + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "Je avatar is bijgewerkt." + +# src/routes/medias.rs:180 +msgid "You are not allowed to use this media." +msgstr "Je mag dit mediabestand niet gebruiken." + +# src/routes/notifications.rs:29 +msgid "To see your notifications, you need to be logged in" +msgstr "Om je meldingen te kunnen zien, moet je ingelogd zijn" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "Dit bericht is nog niet gepubliceerd." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "Om een nieuwe bericht te schrijven moet je ingelogd zijn" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "Je bent geen schrijver van deze blog." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "Nieuw bericht" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "{0} bewerken" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "Je mag niet publiceren op deze blog." + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "Je artikel is bijgewerkt." + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "Je artikel is opgeslagen." + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "Nieuw artikel" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "Je mag dit artikel niet verwijderen." + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "Je artikel is verwijderd." + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "Het lijkt erop dat het artikel dat je probeerde te verwijderen niet bestaat. Misschien is het al verdwenen?" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "Kon niet genoeg informatie over je account opvragen. Controleer of je gebruikersnaam juist is." + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "Om een bericht opnieuw te kunnen delen, moet je ingelogd zijn" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "Je bent nu verbonden." + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "Je bent nu uitgelogd." + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "Wachtwoord opnieuw instellen" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "Hier is de link om je wachtwoord opnieuw in te stellen: {0}" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "Je wachtwoord is succesvol ingesteld." + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "Om toegang te krijgen tot je dashboard, moet je ingelogd zijn" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "Je volgt {} niet langer." + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "Je volgt nu {}." + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "Om je te abonneren op iemand, moet je ingelogd zijn" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "Om je profiel te bewerken moet je ingelogd zijn" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "Je profiel is bijgewerkt." + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "Je account is verwijderd." + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "Je kunt het account van iemand anders niet verwijderen." + +msgid "Create your account" +msgstr "Maak je account aan" + +msgid "Create an account" +msgstr "Maak een account aan" + +msgid "Email" +msgstr "E-mailadres" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "Excuses, maar registraties zijn gesloten voor deze server. Je kunt wel een andere vinden." + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "Ga naar je inbox!" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "Gebruikersnaam" + +msgid "Password" +msgstr "Wachtwoord" + +msgid "Password confirmation" +msgstr "Wachtwoordbevestiging" + +msgid "Media upload" +msgstr "Media uploaden" + +msgid "Description" +msgstr "Beschrijving" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "Handig voor slechtzienden en eveneens licentiegegevens" + +msgid "Content warning" +msgstr "Inhoudswaarschuwing" + +msgid "Leave it empty, if none is needed" +msgstr "Laat het leeg als niets nodig is" + +msgid "File" +msgstr "Bestand" + +msgid "Send" +msgstr "Verstuur" + +msgid "Your media" +msgstr "Je media" + +msgid "Upload" +msgstr "Uploaden" + +msgid "You don't have any media yet." +msgstr "Je hebt nog geen media." + +msgid "Content warning: {0}" +msgstr "Waarschuwing voor inhoud: {0}" + +msgid "Delete" +msgstr "Verwijderen" + +msgid "Details" +msgstr "Details" + +msgid "Media details" +msgstr "Media details" + +msgid "Go back to the gallery" +msgstr "Terug naar de galerij" + +msgid "Markdown syntax" +msgstr "Markdown syntax" + +msgid "Copy it into your articles, to insert this media:" +msgstr "Kopieer in je artikelen om deze media in te voegen:" + +msgid "Use as an avatar" +msgstr "Gebruik als avatar" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "Menu" + +msgid "Search" +msgstr "Zoeken" + +msgid "Dashboard" +msgstr "Dashboard" + +msgid "Notifications" +msgstr "Meldingen" + +msgid "Log Out" +msgstr "Uitloggen" + +msgid "My account" +msgstr "Mijn account" + +msgid "Log In" +msgstr "Inloggen" + +msgid "Register" +msgstr "Aanmelden" + +msgid "About this instance" +msgstr "Over deze server" + +msgid "Privacy policy" +msgstr "Privacybeleid" + +msgid "Administration" +msgstr "Beheer" + +msgid "Documentation" +msgstr "Documentatie" + +msgid "Source code" +msgstr "Broncode" + +msgid "Matrix room" +msgstr "Matrix kamer" + +msgid "Admin" +msgstr "Beheerder" + +msgid "It is you" +msgstr "Dat ben jij" + +msgid "Edit your profile" +msgstr "Bewerk je profiel" + +msgid "Open on {0}" +msgstr "Open op {0}" + +msgid "Unsubscribe" +msgstr "Afmelden" + +msgid "Subscribe" +msgstr "Abonneren" + +msgid "Follow {}" +msgstr "Volg {}" + +msgid "Log in to follow" +msgstr "Inloggen om te volgen" + +msgid "Enter your full username handle to follow" +msgstr "Geef je volledige gebruikersnaam op om te volgen" + +msgid "{0}'s subscribers" +msgstr "{0}'s abonnees" + +msgid "Articles" +msgstr "Artikelen" + +msgid "Subscribers" +msgstr "Abonnees" + +msgid "Subscriptions" +msgstr "Abonnementen" + +msgid "{0}'s subscriptions" +msgstr "{0}'s abonnementen" + +msgid "Your Dashboard" +msgstr "Je dashboard" + +msgid "Your Blogs" +msgstr "Je blogs" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "Je hebt nog geen blog. Maak een blog, of vraag om aan een blog mee te mogen doen." + +msgid "Start a new blog" +msgstr "Start een nieuwe blog" + +msgid "Your Drafts" +msgstr "Je concepten" + +msgid "Go to your gallery" +msgstr "Ga naar je galerij" + +msgid "Edit your account" +msgstr "Bewerk je account" + +msgid "Your Profile" +msgstr "Je profiel" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "Om je avatar te veranderen upload je die naar je galerij en selecteer je avatar daar." + +msgid "Upload an avatar" +msgstr "Upload een avatar" + +msgid "Display name" +msgstr "Weergavenaam" + +msgid "Summary" +msgstr "Samenvatting" + +msgid "Theme" +msgstr "Thema" + +msgid "Default theme" +msgstr "Standaardthema" + +msgid "Error while loading theme selector." +msgstr "Fout bij het laden van de themaselector." + +msgid "Never load blogs custom themes" +msgstr "Nooit blogs maatwerkthema's laden" + +msgid "Update account" +msgstr "Account bijwerken" + +msgid "Danger zone" +msgstr "Gevarenzone" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "Voorzichtig, elke actie hier kan niet worden geannuleerd." + +msgid "Delete your account" +msgstr "Verwijder je account" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "Sorry, maar als beheerder, kan je je eigen server niet verlaten." + +msgid "Latest articles" +msgstr "Nieuwste artikelen" + +msgid "Atom feed" +msgstr "Atom feed" + +msgid "Recently boosted" +msgstr "Onlangs geboost" + +msgid "Articles tagged \"{0}\"" +msgstr "Artikelen gelabeld \"{0}\"" + +msgid "There are currently no articles with such a tag" +msgstr "Er zijn momenteel geen artikelen met zo'n label" + +msgid "The content you sent can't be processed." +msgstr "Je verstuurde bijdrage kan niet worden verwerkt." + +msgid "Maybe it was too long." +msgstr "Misschien was het te lang." + +msgid "Internal server error" +msgstr "Interne serverfout" + +msgid "Something broke on our side." +msgstr "Iets fout aan onze kant." + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "Sorry. Als je denkt dat dit een bug is, rapporteer het dan." + +msgid "Invalid CSRF token" +msgstr "Ongeldig CSRF token" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "Er is iets mis met het CSRF-token. Zorg ervoor dat cookies ingeschakeld zijn in je browser en probeer deze pagina opnieuw te laden. Als je dit foutbericht blijft zien, rapporteer het dan." + +msgid "You are not authorized." +msgstr "Je bent niet geautoriseerd." + +msgid "Page not found" +msgstr "Pagina niet gevonden" + +msgid "We couldn't find this page." +msgstr "We konden deze pagina niet vinden." + +msgid "The link that led you here may be broken." +msgstr "De link die jou hier naartoe heeft geleid, kan kapot zijn." + +msgid "Users" +msgstr "Gebruikers" + +msgid "Configuration" +msgstr "Configuratie" + +msgid "Instances" +msgstr "Exemplaren" + +msgid "Email blocklist" +msgstr "E-mail blokkeerlijst" + +msgid "Grant admin rights" +msgstr "Beheerdersrechten toekennen" + +msgid "Revoke admin rights" +msgstr "Beheerdersrechten intrekken" + +msgid "Grant moderator rights" +msgstr "Moderatorrechten toekennen" + +msgid "Revoke moderator rights" +msgstr "Moderatorrechten intrekken" + +msgid "Ban" +msgstr "Verbannen" + +msgid "Run on selected users" +msgstr "Uitvoeren op geselecteerde gebruikers" + +msgid "Moderator" +msgstr "Moderator" + +msgid "Moderation" +msgstr "Moderatie" + +msgid "Home" +msgstr "Startpagina" + +msgid "Administration of {0}" +msgstr "Beheer van {0}" + +msgid "Unblock" +msgstr "Deblokkeer" + +msgid "Block" +msgstr "Blokkeer" + +msgid "Name" +msgstr "Naam" + +msgid "Allow anyone to register here" +msgstr "Laat iedereen hier een account aanmaken" + +msgid "Short description" +msgstr "Korte beschrijving" + +msgid "Markdown syntax is supported" +msgstr "Markdown syntax wordt ondersteund" + +msgid "Long description" +msgstr "Uitgebreide omschrijving" + +msgid "Default article license" +msgstr "Standaard artikellicentie" + +msgid "Save these settings" +msgstr "Deze instellingen opslaan" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "Als je deze site als bezoeker bekijkt, worden er geen gegevens over jou verzameld." + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "Als geregistreerde gebruiker moet je je gebruikersnaam invoeren (dat hoeft niet je echte naam te zijn), je bestaande e-mailadres en een wachtwoord om in te kunnen loggen, artikelen en commentaar te schrijven. De inhoud die je opgeeft wordt opgeslagen totdat je die verwijdert." + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "Als je inlogt, slaan we twee cookies op, de eerste om je sessie open te houden, de tweede om andere mensen te verhinderen om namens jou iets te doen. We slaan geen andere cookies op." + +msgid "Blocklisted Emails" +msgstr "Geblokkeerde e-mailadressen" + +msgid "Email address" +msgstr "E-mailadres" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "Het e-mailadres dat je wilt blokkeren. Om hele domeinen te blokkeren, kunt je de globale syntax gebruiken, bijvoorbeeld '*@example.com' blokkeert alle adressen van example.com" + +msgid "Note" +msgstr "Opmerking" + +msgid "Notify the user?" +msgstr "De gebruiker informeren?" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "Optioneel. Toont een bericht aan gebruikers wanneer ze een account met dat adres proberen aan te maken" + +msgid "Blocklisting notification" +msgstr "Blokkeringsmelding" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "Het bericht dat wordt getoond als iemand een account met dit e-mailadres probeert aan te maken" + +msgid "Add blocklisted address" +msgstr "Te blokkeren adres toevoegen" + +msgid "There are no blocked emails on your instance" +msgstr "Er zijn geen geblokkeerde e-mails op jouw server" + +msgid "Delete selected emails" +msgstr "Geselecteerde e-mails wissen" + +msgid "Email address:" +msgstr "E-mailadres:" + +msgid "Blocklisted for:" +msgstr "Geblokkeerd voor:" + +msgid "Will notify them on account creation with this message:" +msgstr "Bij aanmaken account dit bericht sturen:" + +msgid "The user will be silently prevented from making an account" +msgstr "De gebruiker wordt stilletjes verhinderd om een account aan te maken" + +msgid "Welcome to {}" +msgstr "Welkom bij {}" + +msgid "View all" +msgstr "Bekijk alles" + +msgid "About {0}" +msgstr "Over {0}" + +msgid "Runs Plume {0}" +msgstr "Draait Plume {0}" + +msgid "Home to {0} people" +msgstr "Thuis voor {0} mensen" + +msgid "Who wrote {0} articles" +msgstr "Die {0} artikelen hebben geschreven" + +msgid "And are connected to {0} other instances" +msgstr "En zijn verbonden met {0} andere servers" + +msgid "Administred by" +msgstr "Beheerd door" + +msgid "Interact with {}" +msgstr "Interactie met {}" + +msgid "Log in to interact" +msgstr "Inloggen voor interactie" + +msgid "Enter your full username to interact" +msgstr "Voer je volledige gebruikersnaam in om te interacteren" + +msgid "Publish" +msgstr "Publiceren" + +msgid "Classic editor (any changes will be lost)" +msgstr "Klassieke editor (alle wijzigingen zullen verloren gaan)" + +msgid "Title" +msgstr "Titel" + +msgid "Subtitle" +msgstr "Ondertitel" + +msgid "Content" +msgstr "Inhoud" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "Je kunt media uploaden naar je galerij en vervolgens de Markdown code in je artikelen kopiëren om ze in te voegen." + +msgid "Upload media" +msgstr "Media uploaden" + +msgid "Tags, separated by commas" +msgstr "Tags, gescheiden door komma's" + +msgid "License" +msgstr "Licentie" + +msgid "Illustration" +msgstr "Afbeelding" + +msgid "This is a draft, don't publish it yet." +msgstr "Dit is een concept, nog niet publiceren." + +msgid "Update" +msgstr "Bijwerken" + +msgid "Update, or publish" +msgstr "Bijwerken of publiceren" + +msgid "Publish your post" +msgstr "Publiceer je bericht" + +msgid "Written by {0}" +msgstr "Geschreven door {0}" + +msgid "All rights reserved." +msgstr "Alle rechten voorbehouden." + +msgid "This article is under the {0} license." +msgstr "Dit artikel valt onder de {0} -licentie." + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "Eén vind-ik-leuk" +msgstr[1] "{0} vind-ik-leuks" + +msgid "I don't like this anymore" +msgstr "Ik vind dit niet meer leuk" + +msgid "Add yours" +msgstr "Voeg die van jou toe" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "Één boost" +msgstr[1] "{0} boosts" + +msgid "I don't want to boost this anymore" +msgstr "Ik wil dit niet meer boosten" + +msgid "Boost" +msgstr "Boosten" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "{0}Log in{1}, of {2}gebruik je Fediverse account{3} om over dit artikel te communiceren" + +msgid "Comments" +msgstr "Reacties" + +msgid "Your comment" +msgstr "Jouw reactie" + +msgid "Submit comment" +msgstr "Verstuur reactie" + +msgid "No comments yet. Be the first to react!" +msgstr "Nog geen reacties. Wees de eerste om te reageren!" + +msgid "Are you sure?" +msgstr "Weet je het zeker?" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "Dit artikel is nog een concept. Alleen jij en andere auteurs kunnen het bekijken." + +msgid "Only you and other authors can edit this article." +msgstr "Alleen jij en andere auteurs kunnen dit artikel bewerken." + +msgid "Edit" +msgstr "Bewerken" + +msgid "I'm from this instance" +msgstr "Ik zit op deze server" + +msgid "Username, or email" +msgstr "Gebruikersnaam of e-mailadres" + +msgid "Log in" +msgstr "Inloggen" + +msgid "I'm from another instance" +msgstr "Ik kom van een andere server" + +msgid "Continue to your instance" +msgstr "Ga door naar je server" + +msgid "Reset your password" +msgstr "Wachtwoord opnieuw instellen" + +msgid "New password" +msgstr "Nieuw wachtwoord" + +msgid "Confirmation" +msgstr "Bevestiging" + +msgid "Update password" +msgstr "Wachtwoord bijwerken" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "We hebben een e-mail gestuurd naar het adres dat je hebt opgegeven, met een link om je wachtwoord te resetten." + +msgid "Send password reset link" +msgstr "Wachtwoordresetlink versturen" + +msgid "This token has expired" +msgstr "Dit token is verlopen" + +msgid "Please start the process again by clicking here." +msgstr "Start het proces opnieuw door hier te klikken." + +msgid "New Blog" +msgstr "Nieuwe blog" + +msgid "Create a blog" +msgstr "Maak een blog aan" + +msgid "Create blog" +msgstr "Creëer blog" + +msgid "Edit \"{}\"" +msgstr "\"{}\" bewerken" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "Je kunt afbeeldingen uploaden naar je galerij om ze als blogpictogrammen of banners te gebruiken." + +msgid "Upload images" +msgstr "Afbeeldingen opladen" + +msgid "Blog icon" +msgstr "Blogpictogram" + +msgid "Blog banner" +msgstr "Blogbanner" + +msgid "Custom theme" +msgstr "Aangepast thema" + +msgid "Update blog" +msgstr "Blog bijwerken" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "Voorzichtig, elke actie hier kan niet worden teruggedraaid." + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "Weet je zeker dat je deze blog permanent wilt verwijderen?" + +msgid "Permanently delete this blog" +msgstr "Deze blog permanent verwijderen" + +msgid "{}'s icon" +msgstr "{}'s pictogram" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "Er is één auteur voor dit blog: " +msgstr[1] "Er zijn {0} auteurs op dit blog: " + +msgid "No posts to see here yet." +msgstr "Er is nog niets te zien." + +msgid "Nothing to see here yet." +msgstr "Nog niets te zien hier." + +msgid "None" +msgstr "Geen" + +msgid "No description" +msgstr "Geen omschrijving" + +msgid "Respond" +msgstr "Reageer" + +msgid "Delete this comment" +msgstr "Verwijder deze reactie" + +msgid "What is Plume?" +msgstr "Wat is Plume?" + +msgid "Plume is a decentralized blogging engine." +msgstr "Plume is een gedecentraliseerde blogging-engine." + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "Auteurs kunnen meerdere blogs beheren, elk als een eigen website." + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "Artikelen zijn ook zichtbaar op andere Plume-servers en je kunt ze direct vanuit andere platforms zoals Mastodon gebruiken." + +msgid "Read the detailed rules" +msgstr "Lees de gedetailleerde instructies" + +msgid "By {0}" +msgstr "Door {0}" + +msgid "Draft" +msgstr "Concept" + +msgid "Search result(s) for \"{0}\"" +msgstr "Zoekresultaat voor \"{0}\"" + +msgid "Search result(s)" +msgstr "Zoekresultaten" + +msgid "No results for your query" +msgstr "Geen zoekresultaten" + +msgid "No more results for your query" +msgstr "Geen zoekresultaten meer" + +msgid "Advanced search" +msgstr "Uitgebreid zoeken" + +msgid "Article title matching these words" +msgstr "Artikeltitel die overeenkomt met deze woorden" + +msgid "Subtitle matching these words" +msgstr "Ondertitel die overeenkomt met deze woorden" + +msgid "Content macthing these words" +msgstr "Inhoud die overeenkomt met deze woorden" + +msgid "Body content" +msgstr "Inhoud artikeltekst" + +msgid "From this date" +msgstr "Vanaf deze datum" + +msgid "To this date" +msgstr "Tot deze datum" + +msgid "Containing these tags" +msgstr "Met deze tags" + +msgid "Tags" +msgstr "Tags" + +msgid "Posted on one of these instances" +msgstr "Geplaatst op een van deze servers" + +msgid "Instance domain" +msgstr "Serverdomein" + +msgid "Posted by one of these authors" +msgstr "Geplaatst door een van deze auteurs" + +msgid "Author(s)" +msgstr "Auteur(s)" + +msgid "Posted on one of these blogs" +msgstr "Geplaatst op een van deze blogs" + +msgid "Blog title" +msgstr "Blogtitel" + +msgid "Written in this language" +msgstr "Geschreven in deze taal" + +msgid "Language" +msgstr "Taal" + +msgid "Published under this license" +msgstr "Gepubliceerd onder deze licentie" + +msgid "Article license" +msgstr "Artikel licentie" + diff --git a/po/plume/no.po b/po/plume/no.po new file mode 100644 index 00000000000..bc7d1b061d2 --- /dev/null +++ b/po/plume/no.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Norwegian\n" +"Language: no_NO\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: no\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} har kommentert artikkelen din." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} har abbonert på deg." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} likte artikkelen din." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} nevnte deg." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} har fremhevet artikkelen din." + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "Din tidslinje" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "Lokal tidslinje" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "Føderert tidslinje" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "{0}s avatar" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "Valgfritt" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "Du må være logget inn for å lage en ny blogg" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "Det eksisterer allerede en blogg med dette navnet." + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "Bloggen ble opprettet!" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "Bloggen din er nå slettet." + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "Du har ikke rettigheter til å slette denne bloggen." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "Du har ikke rettigheter til å endre denne bloggen." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "Du kan ikke bruke dette bildet som bloggikon." + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "Du kan ikke bruke dette bildet som bloggbanner." + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "Informasjon om bloggen er oppdatert." + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "Kommentaren din er lagt til." + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "Kommentaren din er slettet." + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "Registrering er lukket på denne instansen." + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "Kontoen din er opprettet. Du må logge inn for å bruke den." + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "Innstillingene for instansen er lagret." + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "Du må være innlogget for å like ett innlegg" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "Mediet er slettet." + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "Du har ikke rettigheter til å slette dette mediet." + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "Avataren din er oppdatert." + +# src/routes/medias.rs:180 +msgid "You are not allowed to use this media." +msgstr "Du har ikke rettigheter til å bruke dette mediet." + +# src/routes/notifications.rs:29 +msgid "To see your notifications, you need to be logged in" +msgstr "Du må være innlogget for se varsler" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "Dette innlegget er ikke publisert enda." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "Du må være innlogget for å skrive ett nytt innlegg" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "Du er ikke forfatter av denne bloggen." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "Nytt innlegg" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "Rediger {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "Du har ikke rettigheter til å publisere på denne bloggen." + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "Artikkelen er oppdatert." + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "Artikkelen er lagret." + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "Ny artikkel" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "Du har ikke rettigheter til å slette denne artikkelen." + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "Artikkelen er slettet." + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "Det ser ut som arikkelen du prøvde allerede er slettet; Kanskje den allerede er fjernet?" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "Klarte ikke å hente informasjon om kontoen din. Vennligst sjekk at brukernavnet er korrekt." + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "Du må være innlogget for å dele ett innlegg" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "Du er nå koblet til." + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "Du er logget ut." + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "Gjenopprette passord" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "Her denne pekeren for å gjenopprette passordet ditt: {0}" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "Passordet ditt er gjenopprettet." + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "Du må være innlogget for å se skrivebordet" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "Du følger ikke lenger {}." + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "Du følger nå {}." + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "Du må være innlogget for å følge noen" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "Du må være innlogget for å endre profilen din" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "Profilen din er oppdatert." + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "Kontoen din er slettet." + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "Du kan ikke slette andres kontoer." + +msgid "Create your account" +msgstr "Opprett kontoen din" + +msgid "Create an account" +msgstr "Opprett en konto" + +msgid "Email" +msgstr "E-post" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "Beklager, nyregistreringer er lukket på denne instansen. Du kan istedet finne en annen instans." + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "Brukernavn" + +msgid "Password" +msgstr "Passord" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "Last opp medie" + +msgid "Description" +msgstr "Beskrivelse" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "Anvendelig for synshemmede, samt for lisensinformasjon" + +msgid "Content warning" +msgstr "Varsel om følsomt innhold" + +msgid "Leave it empty, if none is needed" +msgstr "La være tomt, hvis ingen trengs" + +msgid "File" +msgstr "Fil" + +msgid "Send" +msgstr "Send" + +msgid "Your media" +msgstr "Dine medier" + +msgid "Upload" +msgstr "Last opp" + +msgid "You don't have any media yet." +msgstr "Du har ingen medier enda." + +msgid "Content warning: {0}" +msgstr "Varsel om følsomt innhold: {0}" + +msgid "Delete" +msgstr "Slett" + +msgid "Details" +msgstr "Detaljer" + +msgid "Media details" +msgstr "Mediedetaljer" + +msgid "Go back to the gallery" +msgstr "Gå tilbake til galleriet" + +msgid "Markdown syntax" +msgstr "Markdown syntaks" + +msgid "Copy it into your articles, to insert this media:" +msgstr "Kopier inn i artikkelen, for å sette inn dette mediet:" + +msgid "Use as an avatar" +msgstr "Bruk som avatar" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "Meny" + +msgid "Search" +msgstr "Søk" + +msgid "Dashboard" +msgstr "Skrivebord" + +msgid "Notifications" +msgstr "Varsler" + +msgid "Log Out" +msgstr "Logg ut" + +msgid "My account" +msgstr "Min konto" + +msgid "Log In" +msgstr "Logg inn" + +msgid "Register" +msgstr "Registrer deg" + +msgid "About this instance" +msgstr "Om denne instansen" + +msgid "Privacy policy" +msgstr "Retningslinjer for personvern" + +msgid "Administration" +msgstr "Administrasjon" + +msgid "Documentation" +msgstr "Dokumentasjon" + +msgid "Source code" +msgstr "Kildekode" + +msgid "Matrix room" +msgstr "Matrix rom" + +msgid "Admin" +msgstr "Administrator" + +msgid "It is you" +msgstr "Det er deg" + +msgid "Edit your profile" +msgstr "Rediger din profil" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "" + +msgid "Follow {}" +msgstr "Følg {}" + +msgid "Log in to follow" +msgstr "Logg inn for å følge" + +msgid "Enter your full username handle to follow" +msgstr "Skriv inn hele brukernavnet ditt for å følge" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "Artikler" + +msgid "Subscribers" +msgstr "Abonnenter" + +msgid "Subscriptions" +msgstr "Abonnenter" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "Skrivebordet ditt" + +msgid "Your Blogs" +msgstr "Dine Blogger" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "Du har ikke en blogg enda. Lag din egen, eller spør om å bli med i en." + +msgid "Start a new blog" +msgstr "Opprett en ny blogg" + +msgid "Your Drafts" +msgstr "Dine Utkast" + +msgid "Go to your gallery" +msgstr "Gå til galleriet ditt" + +msgid "Edit your account" +msgstr "Endre kontoen din" + +msgid "Your Profile" +msgstr "Din profil" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "For å endre avataren din må du legge den til galleriet og velge den der." + +msgid "Upload an avatar" +msgstr "Last opp en avatar" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "Sammendrag" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "Oppdater konto" + +msgid "Danger zone" +msgstr "Faresone" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "Vær forsiktig. Endringer her kan ikke avbrytes." + +msgid "Delete your account" +msgstr "Slett kontoen din" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "Beklager, en administrator kan ikke forlate sin egen instans." + +msgid "Latest articles" +msgstr "Siste artikler" + +msgid "Atom feed" +msgstr "Atom strøm" + +msgid "Recently boosted" +msgstr "Nylig fremhveet" + +msgid "Articles tagged \"{0}\"" +msgstr "Artikler med emneknaggen \"{0}\"" + +msgid "There are currently no articles with such a tag" +msgstr "Det er ingen artikler med den emneknaggen" + +msgid "The content you sent can't be processed." +msgstr "Innholdet du sendte inn kan ikke bearbeides." + +msgid "Maybe it was too long." +msgstr "Kanskje det var for langt." + +msgid "Internal server error" +msgstr "Intern feil" + +msgid "Something broke on our side." +msgstr "Noe brakk hos oss." + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "Beklager! Hvis du tror at dette er en programfeil, setter vi pris på at du sier ifra." + +msgid "Invalid CSRF token" +msgstr "Ugyldig CSRF token" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "Noe er galt med CSRF token. Påse at informasjonskapsler er aktivert i nettleseren, prøv så å hente nettsiden på nytt. Hvis du fortsatt ser denne feilen, setter vi pris på om du sier ifra." + +msgid "You are not authorized." +msgstr "Du har ikke tilgang." + +msgid "Page not found" +msgstr "Siden ble ikke funnet" + +msgid "We couldn't find this page." +msgstr "Vi fant desverre ikke denne siden." + +msgid "The link that led you here may be broken." +msgstr "Lenken som ledet deg hit kan være utdatert." + +msgid "Users" +msgstr "Brukere" + +msgid "Configuration" +msgstr "Innstillinger" + +msgid "Instances" +msgstr "Instanser" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "Bannlys" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "Administrasjon av {0}" + +msgid "Unblock" +msgstr "Fjern blokkering" + +msgid "Block" +msgstr "Blokker" + +msgid "Name" +msgstr "Navn" + +msgid "Allow anyone to register here" +msgstr "Tillat alle å registrere seg" + +msgid "Short description" +msgstr "Kort beskrivelse" + +msgid "Markdown syntax is supported" +msgstr "Markdown syntax støttes" + +msgid "Long description" +msgstr "Lang beskrivelse" + +msgid "Default article license" +msgstr "Standardlisens for artikler" + +msgid "Save these settings" +msgstr "Lagre innstillingene" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "Hvis du besøker denne siden som gjest, lagres ingen informasjon om deg." + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "For å registrere deg må du oppgi ett brukernavn (dette behøver ikke å samsvare med ditt ekte navn), en fungerende epost-adresse og ett passord. Dette gjør at du kan logge inn, skrive artikler og kommentarer. Innholdet du legger inn blir lagret inntil du sletter det." + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "Når du logger inn lagrer vi to informasjonskapsler. Den har informasjon om sesjonen din, den andre beskytter identiteten din. Vi lagrer ingen andre informasjonskapsler." + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "Velkommen til {}" + +msgid "View all" +msgstr "Vis alle" + +msgid "About {0}" +msgstr "Om {0}" + +msgid "Runs Plume {0}" +msgstr "Kjører Plume {0}" + +msgid "Home to {0} people" +msgstr "Hjemmet til {0} mennesker" + +msgid "Who wrote {0} articles" +msgstr "Som har skrevet {0} artikler" + +msgid "And are connected to {0} other instances" +msgstr "Og er koblet til {0} andre instanser" + +msgid "Administred by" +msgstr "Administrert av" + +msgid "Interact with {}" +msgstr "Interakter med {}" + +msgid "Log in to interact" +msgstr "Logg inn for å interaktere" + +msgid "Enter your full username to interact" +msgstr "Skriv inn ditt fulle brukernavn for å interaktere" + +msgid "Publish" +msgstr "Publiser" + +msgid "Classic editor (any changes will be lost)" +msgstr "Klassisk redigeringsverktøy (alle endringer vil gå tapt)" + +msgid "Title" +msgstr "" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "Innhold" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "Du kan laste opp medier til galleriet, og så lime inn Markdown syntaksen inn i artiklen for å bruke dem." + +msgid "Upload media" +msgstr "Last opp medie" + +msgid "Tags, separated by commas" +msgstr "Knagger, adskilt med komma" + +msgid "License" +msgstr "Lisens" + +msgid "Illustration" +msgstr "Illustrasjon" + +msgid "This is a draft, don't publish it yet." +msgstr "Dette er ett utkast, ikke publiser det enda." + +msgid "Update" +msgstr "Oppdater" + +msgid "Update, or publish" +msgstr "Oppdater eller publiser" + +msgid "Publish your post" +msgstr "Publiser innlegget" + +msgid "Written by {0}" +msgstr "Skrevet av {0}" + +msgid "All rights reserved." +msgstr "Alt innhold er opphavsrettslig beskyttet." + +msgid "This article is under the {0} license." +msgstr "Denne artikkelen er lisensiert under {0}." + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "En like" +msgstr[1] "{0} likes" + +msgid "I don't like this anymore" +msgstr "Jeg liker ikke dette lenger" + +msgid "Add yours" +msgstr "Legg inn din egen" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "Fremhevet en gang" +msgstr[1] "{0} fremhevinger" + +msgid "I don't want to boost this anymore" +msgstr "Jeg vil ikke fremheve dette lenger" + +msgid "Boost" +msgstr "Fremhev" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "{0}Logg inn{1} eller {2}bruk din Fediverse konto{3} for å interaktere med denne artikkelen" + +msgid "Comments" +msgstr "Kommentarer" + +msgid "Your comment" +msgstr "Din kommentar" + +msgid "Submit comment" +msgstr "Send kommentar" + +msgid "No comments yet. Be the first to react!" +msgstr "Ingen kommentarer enda. Bli den første!" + +msgid "Are you sure?" +msgstr "" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "Denne artikkelen er ett utkast. Bare du og andre forfattere kan se den." + +msgid "Only you and other authors can edit this article." +msgstr "Bare du og andre forfattere kan endre denne artikkelen." + +msgid "Edit" +msgstr "Rediger" + +msgid "I'm from this instance" +msgstr "Jeg er fra denne instansen" + +msgid "Username, or email" +msgstr "Brukernavn eller e-post" + +msgid "Log in" +msgstr "Logg Inn" + +msgid "I'm from another instance" +msgstr "Jeg er fra en annen instans" + +msgid "Continue to your instance" +msgstr "Gå til din instans" + +msgid "Reset your password" +msgstr "" + +msgid "New password" +msgstr "" + +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "Send lenke for tilbakestilling av passord" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "Ny Blogg" + +msgid "Create a blog" +msgstr "Opprett en blogg" + +msgid "Create blog" +msgstr "Opprett blogg" + +msgid "Edit \"{}\"" +msgstr "Rediger \"{}\"" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "Du kan legge opp bilder til galleriet ditt for å bruke den som bloggikoner eller bannere." + +msgid "Upload images" +msgstr "Last opp bilder" + +msgid "Blog icon" +msgstr "Bloggikon" + +msgid "Blog banner" +msgstr "Bloggbanner" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "Oppdater blogg" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "Vær forsiktig. Endringer her kan ikke reverteres." + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "Slett denne bloggen permanent" + +msgid "{}'s icon" +msgstr "{}s ikon" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "Det er en forfatter av denne bloggen: " +msgstr[1] "Det er {0} forfattere av denne bloggen: " + +msgid "No posts to see here yet." +msgstr "Det er ingen artikler her enda." + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +msgid "From this date" +msgstr "" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" + diff --git a/po/plume/pl.po b/po/plume/pl.po new file mode 100644 index 00000000000..79e3564e5b6 --- /dev/null +++ b/po/plume/pl.po @@ -0,0 +1,1040 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Polish\n" +"Language: pl_PL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: pl\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} skomentował(a) Twój artykuł." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} jest subskrybentem do ciebie." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} polubił(a) Twój artykuł." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} wspomniał(a) o Tobie." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} podbił(a) Twój artykuł." + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "Twój strumień" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "Lokalna" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "Strumień federacji" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "Awatar {0}" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "Poprzednia strona" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "Następna strona" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "Nieobowiązkowe" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "Aby utworzyć nowy blog, musisz być zalogowany" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "Blog o tej samej nazwie już istnieje." + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "Twój blog został pomyślnie utworzony!" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "Twój blog został usunięty." + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "Nie masz uprawnień do usunięcia tego bloga." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "Nie masz uprawnień edytować tego bloga." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "Nie możesz użyć tego nośnika jako ikony blogu." + +# src/routes/blogs.rs:298 +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:332 +msgid "Your blog information have been updated." +msgstr "Twoje informacje o blogu zostały zaktualizowane." + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "Twój komentarz został opublikowany." + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "Twój komentarz został usunięty." + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "Rejestracje są zamknięte w tej instancji." + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "Twoje konto zostało utworzone. Zanim będziesz mógł(-ogła) z niego korzystać, musisz się zalogować." + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "Zapisano ustawienia instancji." + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "{} został(a) odblokowany(-a)." + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "{} został(a) zablokowany(-a)." + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "Usunięte blokady" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "E-mail Zablokowany" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "Nie możesz zmienićswoich własnych uprawnień." + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "Nie masz uprawnień do wykonania tego działania." + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "Gotowe." + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "Aby polubić post, musisz być zalogowany" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "Twoje media zostały usunięte." + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "Nie można usunąć tego medium." + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "Twój awatar został zaktualizowany." + +# src/routes/medias.rs:180 +msgid "You are not allowed to use this media." +msgstr "Nie możesz użyć tego medium." + +# src/routes/notifications.rs:29 +msgid "To see your notifications, you need to be logged in" +msgstr "Aby zobaczyć powiadomienia, musisz być zalogowany" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "Ten wpis nie został jeszcze opublikowany." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "Aby napisać nowy artykuł, musisz być zalogowany" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "Nie jesteś autorem tego bloga." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "Nowy wpis" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "Edytuj {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "Nie możesz publikować na tym blogu." + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "Twój artykuł został zaktualizowany." + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "Twój artykuł został zapisany." + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "Nowy artykuł" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "Nie można usunąć tego artykułu." + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "Twój artykuł został usunięty." + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "Wygląda na to, że artykuł który próbowałeś(-aś) usunąć nie istnieje. Może został usunięty wcześniej?" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "Nie można uzyskać wystarczającej ilości informacji o Twoim koncie. Upewnij się, że nazwa użytkownika jest prawidłowa." + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "Aby udostępnić post, musisz być zalogowany" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "Teraz jesteś połączony." + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "Teraz jesteś wylogowany." + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "Resetowanie hasła" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "Tutaj jest link do zresetowania hasła: {0}" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "Twoje hasło zostało pomyślnie zresetowane." + +# src/routes/user.rs:87 +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:109 +msgid "You are no longer following {}." +msgstr "Już nie obserwujesz użytkownika {}." + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "Obserwujesz teraz użytkownika {}." + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "Aby subskrybować do kogoś, musisz być zalogowany" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "Aby edytować swój profil, musisz być zalogowany" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "Twój profil został zaktualizowany." + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "Twoje konto zostało usunięte." + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "Nie możesz usunąć konta innej osoby." + +msgid "Create your account" +msgstr "Utwórz konto" + +msgid "Create an account" +msgstr "Utwórz nowe konto" + +msgid "Email" +msgstr "Adres e-mail" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "Przepraszamy, rejestracja jest zamknięta na tej instancji. Spróbuj znaleźć inną." + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "Sprawdź do swoją skrzynki odbiorczej!" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "Nazwa użytkownika" + +msgid "Password" +msgstr "Hasło" + +msgid "Password confirmation" +msgstr "Potwierdzenie hasła" + +msgid "Media upload" +msgstr "Wysyłanie zawartości multimedialnej" + +msgid "Description" +msgstr "Opis" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "Przydatny dla osób z problemami ze wzrokiem oraz do umieszczenia informacji o licencji" + +msgid "Content warning" +msgstr "Ostrzeżenie o zawartości" + +msgid "Leave it empty, if none is needed" +msgstr "Pozostaw puste, jeżeli niepotrzebne" + +msgid "File" +msgstr "Plik" + +msgid "Send" +msgstr "Wyślij" + +msgid "Your media" +msgstr "Twoja zawartość multimedialna" + +msgid "Upload" +msgstr "Wyślij" + +msgid "You don't have any media yet." +msgstr "Nie masz żadnej zawartości multimedialnej." + +msgid "Content warning: {0}" +msgstr "Ostrzeżenie o zawartości: {0}" + +msgid "Delete" +msgstr "Usuń" + +msgid "Details" +msgstr "Bliższe szczegóły" + +msgid "Media details" +msgstr "Szczegóły zawartości multimedialnej" + +msgid "Go back to the gallery" +msgstr "Powróć do galerii" + +msgid "Markdown syntax" +msgstr "Kod Markdown" + +msgid "Copy it into your articles, to insert this media:" +msgstr "Skopiuj do swoich artykułów, aby wstawić tę zawartość multimedialną:" + +msgid "Use as an avatar" +msgstr "Użyj jako awataru" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "Menu" + +msgid "Search" +msgstr "Szukaj" + +msgid "Dashboard" +msgstr "Panel" + +msgid "Notifications" +msgstr "Powiadomienia" + +msgid "Log Out" +msgstr "Wyloguj się" + +msgid "My account" +msgstr "Moje konto" + +msgid "Log In" +msgstr "Zaloguj się" + +msgid "Register" +msgstr "Zarejestruj się" + +msgid "About this instance" +msgstr "O tej instancji" + +msgid "Privacy policy" +msgstr "Polityka prywatności" + +msgid "Administration" +msgstr "Administracja" + +msgid "Documentation" +msgstr "Dokumentacja" + +msgid "Source code" +msgstr "Kod źródłowy" + +msgid "Matrix room" +msgstr "Pokój Matrix.org" + +msgid "Admin" +msgstr "Administrator" + +msgid "It is you" +msgstr "To Ty" + +msgid "Edit your profile" +msgstr "Edytuj swój profil" + +msgid "Open on {0}" +msgstr "Otwórz w {0}" + +msgid "Unsubscribe" +msgstr "Przestań subskrybować" + +msgid "Subscribe" +msgstr "Subskrybuj" + +msgid "Follow {}" +msgstr "Obserwuj {}" + +msgid "Log in to follow" +msgstr "Zaloguj się, aby śledzić" + +msgid "Enter your full username handle to follow" +msgstr "Wpisz swoją pełny uchwyt nazwy użytkownika, aby móc śledzić" + +msgid "{0}'s subscribers" +msgstr "Subskrybujący {0}" + +msgid "Articles" +msgstr "Artykuły" + +msgid "Subscribers" +msgstr "Subskrybenci" + +msgid "Subscriptions" +msgstr "Subskrypcje" + +msgid "{0}'s subscriptions" +msgstr "Subskrypcje {0}" + +msgid "Your Dashboard" +msgstr "Twój panel rozdzielczy" + +msgid "Your Blogs" +msgstr "Twoje blogi" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "Nie posiadasz żadnego bloga. Utwórz własny, lub poproś o dołączanie do istniejącego." + +msgid "Start a new blog" +msgstr "Utwórz nowy blog" + +msgid "Your Drafts" +msgstr "Twoje szkice" + +msgid "Go to your gallery" +msgstr "Przejdź do swojej galerii" + +msgid "Edit your account" +msgstr "Edytuj swoje konto" + +msgid "Your Profile" +msgstr "Twój profil" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "Aby zmienić swój awatar, prześlij go do Twojej galerii, a następnie wybierz go stamtąd." + +msgid "Upload an avatar" +msgstr "Wczytaj awatara" + +msgid "Display name" +msgstr "Nazwa wyświetlana" + +msgid "Summary" +msgstr "Opis" + +msgid "Theme" +msgstr "Motyw" + +msgid "Default theme" +msgstr "Domyślny motyw" + +msgid "Error while loading theme selector." +msgstr "Błąd podczas ładowania selektora motywu." + +msgid "Never load blogs custom themes" +msgstr "Nigdy nie ładuj niestandardowych motywów blogów" + +msgid "Update account" +msgstr "Aktualizuj konto" + +msgid "Danger zone" +msgstr "Niebezpieczna strefa" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "Bądź ostrożny(-a), działania podjęte tutaj nie mogą zostać cofnięte." + +msgid "Delete your account" +msgstr "Usuń swoje konto" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "Przepraszamy, jako administrator nie możesz opuścić swojej instancji." + +msgid "Latest articles" +msgstr "Najnowsze artykuły" + +msgid "Atom feed" +msgstr "Kanał Atom" + +msgid "Recently boosted" +msgstr "Ostatnio podbite" + +msgid "Articles tagged \"{0}\"" +msgstr "Artykuły oznaczone „{0}”" + +msgid "There are currently no articles with such a tag" +msgstr "Obecnie nie istnieją artykuły z tym tagiem" + +msgid "The content you sent can't be processed." +msgstr "Nie udało się przetworzyć wysłanej zawartości." + +msgid "Maybe it was too long." +msgstr "Możliwe, że była za długa." + +msgid "Internal server error" +msgstr "Wewnętrzny błąd serwera" + +msgid "Something broke on our side." +msgstr "Coś poszło nie tak." + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "Przepraszamy. Jeżeli uważasz że wystąpił błąd, prosimy o zgłoszenie go." + +msgid "Invalid CSRF token" +msgstr "Nieprawidłowy token CSRF" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "Coś poszło nie tak z tokenem CSRF. Upewnij się, że w przeglądarce są włączone pliki cookies i spróbuj odświeżyć stronę. Jeżeli wciąż widzisz tę wiadomość, zgłoś to." + +msgid "You are not authorized." +msgstr "Nie jesteś zalogowany." + +msgid "Page not found" +msgstr "Nie odnaleziono strony" + +msgid "We couldn't find this page." +msgstr "Nie udało się odnaleźć tej strony." + +msgid "The link that led you here may be broken." +msgstr "Odnośnik który Cię tu zaprowadził może być uszkodzony." + +msgid "Users" +msgstr "Użytkownicy" + +msgid "Configuration" +msgstr "Konfiguracja" + +msgid "Instances" +msgstr "Instancje" + +msgid "Email blocklist" +msgstr "Lista blokowanych e-maili" + +msgid "Grant admin rights" +msgstr "Przyznaj uprawnienia administratora" + +msgid "Revoke admin rights" +msgstr "Odbierz uprawnienia administratora" + +msgid "Grant moderator rights" +msgstr "Przyznaj uprawnienia moderatora" + +msgid "Revoke moderator rights" +msgstr "Odbierz uprawnienia moderatora" + +msgid "Ban" +msgstr "Zbanuj" + +msgid "Run on selected users" +msgstr "Wykonaj na zaznaczonych użytkownikach" + +msgid "Moderator" +msgstr "Moderator" + +msgid "Moderation" +msgstr "Moderacja" + +msgid "Home" +msgstr "Strona główna" + +msgid "Administration of {0}" +msgstr "Administracja {0}" + +msgid "Unblock" +msgstr "Odblokuj" + +msgid "Block" +msgstr "Zablikuj" + +msgid "Name" +msgstr "Nazwa" + +msgid "Allow anyone to register here" +msgstr "Pozwól każdemu na rejestrację" + +msgid "Short description" +msgstr "Krótki opis" + +msgid "Markdown syntax is supported" +msgstr "Składnia Markdown jest obsługiwana" + +msgid "Long description" +msgstr "Szczegółowy opis" + +msgid "Default article license" +msgstr "Domyślna licencja artykułów" + +msgid "Save these settings" +msgstr "Zapisz te ustawienia" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "Jeśli przeglądasz tę witrynę jako odwiedzający, nie zbierasz żadnych danych o Tobie." + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "Jako zarejestrowany użytkownik, musisz podać swoją nazwę użytkownika (nie musi to być Twoje imię i nazwisko), działający adres e-mail i hasło, aby móc zalogować się, pisać artykuły i komentować. Dodane treści są przechowywane do czasu, gdy je usuniesz." + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "Po zalogowaniu się, przechowujemy dwa ciasteczka – jedno, aby utrzymać aktywną sesję i drugie, aby uniemożliwić innym podszywanie się pod Ciebie. Nie przechowujemy innych plików cookie." + +msgid "Blocklisted Emails" +msgstr "Zablokowane adresy e-mail" + +msgid "Email address" +msgstr "Adresy e-mail" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "Adres e-mail, który chcesz zablokować. Aby zablokować domeny, możesz użyć globbing syntax, na przykład '*@example.com' blokuje wszystkie adresy z example.com" + +msgid "Note" +msgstr "Notatka" + +msgid "Notify the user?" +msgstr "Powiadomić użytkownika?" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "Opcjonalnie, pokazuje wiadomość użytkownikowi gdy próbuje utworzyć konto o tym adresie" + +msgid "Blocklisting notification" +msgstr "Zablokuj powiadomienie" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "Wiadomość do wyświetlenia, gdy użytkownik próbuje utworzyć konto z tym adresem e-mail" + +msgid "Add blocklisted address" +msgstr "Dodaj zablokowany adres" + +msgid "There are no blocked emails on your instance" +msgstr "Na Twojej instancji nie ma żadnych blokowanych adresów e-mail" + +msgid "Delete selected emails" +msgstr "Usuń wybrane adresy e-mail" + +msgid "Email address:" +msgstr "Adres e-mail:" + +msgid "Blocklisted for:" +msgstr "Zablokowane dla:" + +msgid "Will notify them on account creation with this message:" +msgstr "Powiadomi o utworzeniu konta za pomocą tej wiadomości:" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "Witamy na {}" + +msgid "View all" +msgstr "Zobacz wszystko" + +msgid "About {0}" +msgstr "O {0}" + +msgid "Runs Plume {0}" +msgstr "Działa na Plume {0}" + +msgid "Home to {0} people" +msgstr "Używana przez {0} użytkowników" + +msgid "Who wrote {0} articles" +msgstr "Którzy napisali {0} artykułów" + +msgid "And are connected to {0} other instances" +msgstr "Sa połączone z {0} innymi instancjami" + +msgid "Administred by" +msgstr "Administrowany przez" + +msgid "Interact with {}" +msgstr "Interaguj z {}" + +msgid "Log in to interact" +msgstr "Zaloguj się, aby wejść w interakcję" + +msgid "Enter your full username to interact" +msgstr "Wprowadź swoją pełną nazwę użytkownika, aby wejść w interakcję" + +msgid "Publish" +msgstr "Opublikuj" + +msgid "Classic editor (any changes will be lost)" +msgstr "Klasyczny edytor (wszelkie zmiany zostaną utracone)" + +msgid "Title" +msgstr "Tytuł" + +msgid "Subtitle" +msgstr "Podtytuł" + +msgid "Content" +msgstr "Zawartość" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "Możesz przesłać multimedia do swojej galerii, i następnie skopiuj ich kod Markdown do artykułów, aby je wstawić." + +msgid "Upload media" +msgstr "Przesłać media" + +msgid "Tags, separated by commas" +msgstr "Tagi, oddzielone przecinkami" + +msgid "License" +msgstr "Licencja" + +msgid "Illustration" +msgstr "Ilustracja" + +msgid "This is a draft, don't publish it yet." +msgstr "To jest szkic, nie publikuj go jeszcze." + +msgid "Update" +msgstr "Aktualizuj" + +msgid "Update, or publish" +msgstr "Aktualizuj lub publikuj" + +msgid "Publish your post" +msgstr "Opublikuj wpis" + +msgid "Written by {0}" +msgstr "Napisany przez {0}" + +msgid "All rights reserved." +msgstr "Wszelkie prawa zastrzeżone." + +msgid "This article is under the {0} license." +msgstr "Ten artykuł został opublikowany na licencji {0}." + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "Jedno polubienie" +msgstr[1] "{0} polubienia" +msgstr[2] "{0} polubień" +msgstr[3] "{0} polubień" + +msgid "I don't like this anymore" +msgstr "Już tego nie lubię" + +msgid "Add yours" +msgstr "Dodaj swoje" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "Jedno podbicie" +msgstr[1] "{0} podbicia" +msgstr[2] "{0} podbić" +msgstr[3] "{0} podbić" + +msgid "I don't want to boost this anymore" +msgstr "Nie chcę tego podbijać" + +msgid "Boost" +msgstr "Podbij" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "{0}Zaloguj się{1} lub {2}użyj konta w Fediwersum{3}, aby wejść w interakcje z tym artykułem" + +msgid "Comments" +msgstr "Komentarze" + +msgid "Your comment" +msgstr "Twój komentarz" + +msgid "Submit comment" +msgstr "Wyślij komentarz" + +msgid "No comments yet. Be the first to react!" +msgstr "Brak komentarzy. Bądź pierwszy(-a)!" + +msgid "Are you sure?" +msgstr "Czy jesteś pewny?" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "Ten artykuł jest szkicem. Tylko Ty i inni autorzy mogą go zobaczyć." + +msgid "Only you and other authors can edit this article." +msgstr "Tylko Ty i inni autorzy mogą edytować ten artykuł." + +msgid "Edit" +msgstr "Edytuj" + +msgid "I'm from this instance" +msgstr "Jestem z tej instancji" + +msgid "Username, or email" +msgstr "Nazwa użytkownika, lub adres e-mail" + +msgid "Log in" +msgstr "Zaloguj się" + +msgid "I'm from another instance" +msgstr "Jestem z innej instancji" + +msgid "Continue to your instance" +msgstr "Przejdź na swoją instancję" + +msgid "Reset your password" +msgstr "Zmień swoje hasło" + +msgid "New password" +msgstr "Nowe hasło" + +msgid "Confirmation" +msgstr "Potwierdzenie" + +msgid "Update password" +msgstr "Zaktualizuj hasło" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "Wysłaliśmy maila na adres, który nam podałeś, z linkiem do zresetowania hasła." + +msgid "Send password reset link" +msgstr "Wyślij e-mail resetujący hasło" + +msgid "This token has expired" +msgstr "Ten token utracił własność" + +msgid "Please start the process again by clicking here." +msgstr "Rozpocznij ten proces ponownie klikając tutaj." + +msgid "New Blog" +msgstr "Nowy blog" + +msgid "Create a blog" +msgstr "Utwórz blog" + +msgid "Create blog" +msgstr "Utwórz blog" + +msgid "Edit \"{}\"" +msgstr "Edytuj \"{}\"" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "Możesz przesłać zdjęcia do swojej galerii, aby używać ich jako ikon, lub banery blogów." + +msgid "Upload images" +msgstr "Przesyłać zdjęcia" + +msgid "Blog icon" +msgstr "Ikona bloga" + +msgid "Blog banner" +msgstr "Banner bloga" + +msgid "Custom theme" +msgstr "Niestandardowy motyw" + +msgid "Update blog" +msgstr "Aktualizuj bloga" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "Bądź ostrożny(-a), działania podjęte tutaj nie mogą zostać cofnięte." + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "Czy na pewno chcesz nieodwracalnie usunąć ten blog?" + +msgid "Permanently delete this blog" +msgstr "Bezpowrotnie usuń ten blog" + +msgid "{}'s icon" +msgstr "Ikona {}" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "Ten blog ma jednego autora: " +msgstr[1] "Ten blog ma {0} autorów: " +msgstr[2] "Ten blog ma {0} autorów: " +msgstr[3] "Ten blog ma {0} autorów: " + +msgid "No posts to see here yet." +msgstr "Brak wpisów do wyświetlenia." + +msgid "Nothing to see here yet." +msgstr "Niczego tu jeszcze nie ma." + +msgid "None" +msgstr "Brak" + +msgid "No description" +msgstr "Brak opisu" + +msgid "Respond" +msgstr "Odpowiedz" + +msgid "Delete this comment" +msgstr "Usuń ten komentarz" + +msgid "What is Plume?" +msgstr "Czym jest Plume?" + +msgid "Plume is a decentralized blogging engine." +msgstr "Plume jest zdecentralizowanym silnikiem blogowym." + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "Autorzy mogą zarządzać różne blogi, każdy jako unikalny stronie." + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "Artykuły są również widoczne w innych instancjach Plume i możesz też wchodzić bezpośrednio interakcje z nimi z innych platform, takich jak Mastodon." + +msgid "Read the detailed rules" +msgstr "Przeczytaj szczegółowe zasady" + +msgid "By {0}" +msgstr "Od {0}" + +msgid "Draft" +msgstr "Szkic" + +msgid "Search result(s) for \"{0}\"" +msgstr "Wyniki wyszukiwania dla \"{0}\"" + +msgid "Search result(s)" +msgstr "Wyniki wyszukiwania" + +msgid "No results for your query" +msgstr "Nie znaleziono wyników dla twojego zapytania" + +msgid "No more results for your query" +msgstr "Nie ma więcej wyników pasujących do tych kryteriów" + +msgid "Advanced search" +msgstr "Zaawansowane wyszukiwanie" + +msgid "Article title matching these words" +msgstr "Tytuł artykułu pasujący do tych słów" + +msgid "Subtitle matching these words" +msgstr "Podtytuł artykułu pasujący do tych słów" + +msgid "Content macthing these words" +msgstr "Zawartość pasująca do tych słów" + +msgid "Body content" +msgstr "Zawartość wpisu" + +msgid "From this date" +msgstr "Od tej daty" + +msgid "To this date" +msgstr "Do tej daty" + +msgid "Containing these tags" +msgstr "Zawierający te tagi" + +msgid "Tags" +msgstr "Tagi" + +msgid "Posted on one of these instances" +msgstr "Opublikowany na jednej z tych instancji" + +msgid "Instance domain" +msgstr "Domena instancji" + +msgid "Posted by one of these authors" +msgstr "Opublikowany przez jednego z tych autorów" + +msgid "Author(s)" +msgstr "Autor(rzy)" + +msgid "Posted on one of these blogs" +msgstr "Opublikowany na jednym z tych blogów" + +msgid "Blog title" +msgstr "Tytuł bloga" + +msgid "Written in this language" +msgstr "Napisany w tym języku" + +msgid "Language" +msgstr "Język" + +msgid "Published under this license" +msgstr "Opublikowany na tej licencji" + +msgid "Article license" +msgstr "Licencja artykułu" + diff --git a/po/plume/plume.pot b/po/plume/plume.pot new file mode 100644 index 00000000000..7e57eff5f22 --- /dev/null +++ b/po/plume/plume.pot @@ -0,0 +1,1028 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "" + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "" + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "" + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "" + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "" + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "" + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "" + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "" + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "" + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "" + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "" + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "" + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "" + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "" + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "" + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "" + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "" + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "" + +# src/routes/medias.rs:180 +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 "" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "" + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "" + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "" + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "" + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "" + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "" + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "" + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "" + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "" + +msgid "Create an account" +msgstr "" + +msgid "Email" +msgstr "" + +msgid "Email confirmation" +msgstr "" + +msgid "An email will be sent to provided email. You can continue signing-up via the email." +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Your media" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "" + +msgid "Menu" +msgstr "" + +msgid "Search" +msgstr "" + +msgid "Dashboard" +msgstr "" + +msgid "Notifications" +msgstr "" + +msgid "Log Out" +msgstr "" + +msgid "My account" +msgstr "" + +msgid "Log In" +msgstr "" + +msgid "Register" +msgstr "" + +msgid "About this instance" +msgstr "" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "" + +msgid "It is you" +msgstr "" + +msgid "Edit your profile" +msgstr "" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "" + +msgid "Follow {}" +msgstr "" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "" + +msgid "Subscribers" +msgstr "" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "" + +msgid "Your Drafts" +msgstr "" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "" + +msgid "Your Profile" +msgstr "" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "" + +msgid "Configuration" +msgstr "" + +msgid "Instances" +msgstr "" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "" + +msgid "Unblock" +msgstr "" + +msgid "Block" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Allow anyone to register here" +msgstr "" + +msgid "Short description" +msgstr "" + +msgid "Markdown syntax is supported" +msgstr "" + +msgid "Long description" +msgstr "" + +msgid "Default article license" +msgstr "" + +msgid "Save these settings" +msgstr "" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +msgid "Administred by" +msgstr "" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +msgid "Tags, separated by commas" +msgstr "" + +msgid "License" +msgstr "" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "" + +msgid "I don't like this anymore" +msgstr "" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "" + +msgid "Comments" +msgstr "" + +msgid "Your comment" +msgstr "" + +msgid "Submit comment" +msgstr "" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "" + +msgid "Log in" +msgstr "" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +msgid "New password" +msgstr "" + +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "" + +msgid "Create a blog" +msgstr "" + +msgid "Create blog" +msgstr "" + +msgid "Edit \"{}\"" +msgstr "" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +msgid "From this date" +msgstr "" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" diff --git a/po/plume/pt.po b/po/plume/pt.po new file mode 100644 index 00000000000..577b9cf5f0f --- /dev/null +++ b/po/plume/pt.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Portuguese, Brazilian\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: pt-BR\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} comentou o seu artigo." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} se inscreveu." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} curtiu o seu artigo." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} te mencionou." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} compartilhou seu artigo." + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "Seu feed" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "Feed local" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "Feed global" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "Imagem de perfil de {0}" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "Página anterior" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "Próxima página" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "Opcional" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "Para criar um novo blog, você precisa entrar" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "Um blog com o mesmo nome já existe." + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "Seu blog foi criado com sucesso!" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "Seu blog foi excluído." + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "Você não tem permissão para excluir este blog." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "Você não tem permissão para editar este blog." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "Você não pode usar esta mídia como ícone do blog." + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "Você não pode usar esta mídia como capa do blog." + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "Os dados do seu blog foram atualizados." + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "Seu comentário foi publicado." + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "Seu comentário foi excluído." + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "Os registros estão fechados nesta instância." + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "Sua conta foi criada. Agora você só precisa entrar para poder usá-la." + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "As configurações da instância foram salvas." + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "{0} foi desbloqueado." + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "{0} foi bloqueado." + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "Bloqueios excluídos" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "E-mail já está bloqueado" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "E-mail bloqueado" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "Você não pode alterar sua própria permissão." + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "Você não tem permissão para executar esta ação." + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "Feito." + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "Para curtir um artigo, você precisa entrar" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "Sua mídia foi excluída." + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "Você não tem permissão para excluir esta mídia." + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "Sua imagem de perfil foi atualizada." + +# src/routes/medias.rs:180 +msgid "You are not allowed to use this media." +msgstr "Você não tem permissão para usar esta mídia." + +# src/routes/notifications.rs:29 +msgid "To see your notifications, you need to be logged in" +msgstr "Para ver suas notificações, você precisa entrar" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "Este artigo ainda não foi publicado." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "Para escrever um novo artigo, você precisa entrar" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "Você não é um autor deste blog." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "Nova postagem" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "Editar {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "Você não tem permissão para postar neste blog." + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "Seu artigo foi atualizado." + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "Seu artigo foi salvo." + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "Novo artigo" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "Você não tem permissão para excluir este artigo." + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "Seu artigo foi excluído." + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "Parece que o artigo que você tentou excluir não existe. Talvez ele já tenha sido excluído?" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "Não foi possível obter informações sobre sua conta. Por favor, certifique-se de que seu nome de usuário completo está certo." + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "Para compartilhar um artigo, você precisa entrar" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "Agora você está conectado." + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "Você saiu." + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "Redefinir senha" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "Aqui está o link para redefinir sua senha: {0}" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "Sua senha foi redefinida com sucesso." + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "Para acessar seu painel, você precisa entrar" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "Você deixou de seguir {}." + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "Você seguiu {}." + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "Para se inscrever, você precisa entrar" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "Para editar seu perfil, você precisa entrar" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "Seu perfil foi atualizado." + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "Sua conta foi excluída." + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "Você não pode excluir a conta de outra pessoa." + +msgid "Create your account" +msgstr "Criar sua conta" + +msgid "Create an account" +msgstr "Criar uma conta" + +msgid "Email" +msgstr "E-mail" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "Desculpe, mas os registros estão fechados nesta instância. Você pode, no entanto, procurar outra." + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "Verifique sua caixa de entrada!" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "Nome de usuário" + +msgid "Password" +msgstr "Senha" + +msgid "Password confirmation" +msgstr "Confirmação de senha" + +msgid "Media upload" +msgstr "Envio de mídia" + +msgid "Description" +msgstr "Descrição" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "Útil para pessoas com deficiência visual e para informações de licenciamento" + +msgid "Content warning" +msgstr "Alerta de conteúdo" + +msgid "Leave it empty, if none is needed" +msgstr "Deixe vazio se nenhum for necessário" + +msgid "File" +msgstr "Arquivo" + +msgid "Send" +msgstr "Enviar" + +msgid "Your media" +msgstr "Sua mídia" + +msgid "Upload" +msgstr "Enviar" + +msgid "You don't have any media yet." +msgstr "Sem mídia." + +msgid "Content warning: {0}" +msgstr "Alerta de conteúdo: {0}" + +msgid "Delete" +msgstr "Excluir" + +msgid "Details" +msgstr "Detalhes" + +msgid "Media details" +msgstr "Detalhes da mídia" + +msgid "Go back to the gallery" +msgstr "Voltar para a galeria" + +msgid "Markdown syntax" +msgstr "Sintaxe Markdown" + +msgid "Copy it into your articles, to insert this media:" +msgstr "Para inserir esta mídia, copie isso para o artigo:" + +msgid "Use as an avatar" +msgstr "Usar como imagem de perfil" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "Menu" + +msgid "Search" +msgstr "Pesquisar" + +msgid "Dashboard" +msgstr "Painel" + +msgid "Notifications" +msgstr "Notificações" + +msgid "Log Out" +msgstr "Sair" + +msgid "My account" +msgstr "Minha conta" + +msgid "Log In" +msgstr "Entrar" + +msgid "Register" +msgstr "Registrar" + +msgid "About this instance" +msgstr "Sobre a instância" + +msgid "Privacy policy" +msgstr "Política de privacidade" + +msgid "Administration" +msgstr "Administração" + +msgid "Documentation" +msgstr "Documentação" + +msgid "Source code" +msgstr "Código fonte" + +msgid "Matrix room" +msgstr "Sala Matrix" + +msgid "Admin" +msgstr "Administrador" + +msgid "It is you" +msgstr "Este é você" + +msgid "Edit your profile" +msgstr "Editar seu perfil" + +msgid "Open on {0}" +msgstr "Abrir em {0}" + +msgid "Unsubscribe" +msgstr "Cancelar inscrição" + +msgid "Subscribe" +msgstr "Inscrever-se" + +msgid "Follow {}" +msgstr "Seguir {}" + +msgid "Log in to follow" +msgstr "Entre para seguir" + +msgid "Enter your full username handle to follow" +msgstr "Digite seu nome de usuário completo para seguir" + +msgid "{0}'s subscribers" +msgstr "Inscritos de {0}" + +msgid "Articles" +msgstr "Artigos" + +msgid "Subscribers" +msgstr "Inscritos" + +msgid "Subscriptions" +msgstr "Inscrições" + +msgid "{0}'s subscriptions" +msgstr "Inscrições de {0}" + +msgid "Your Dashboard" +msgstr "Seu Painel" + +msgid "Your Blogs" +msgstr "Seus Blogs" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "Você ainda não tem nenhum blog. Crie o seu ou entre em um." + +msgid "Start a new blog" +msgstr "Criar um novo blog" + +msgid "Your Drafts" +msgstr "Seus rascunhos" + +msgid "Go to your gallery" +msgstr "Ir para a sua galeria" + +msgid "Edit your account" +msgstr "Editar sua conta" + +msgid "Your Profile" +msgstr "Seu Perfil" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "Para mudar sua imagem de perfil, selecione uma nova na galeria." + +msgid "Upload an avatar" +msgstr "Enviar uma imagem de perfil" + +msgid "Display name" +msgstr "Nome de exibição" + +msgid "Summary" +msgstr "Resumo" + +msgid "Theme" +msgstr " Tema" + +msgid "Default theme" +msgstr "Tema padrão" + +msgid "Error while loading theme selector." +msgstr "Erro ao carregar o seletor de tema." + +msgid "Never load blogs custom themes" +msgstr "Nunca carregar temas personalizados de blogs" + +msgid "Update account" +msgstr "Atualizar conta" + +msgid "Danger zone" +msgstr "Zona de risco" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "Tenha muito cuidado, qualquer ação tomada aqui não poderá ser desfeita." + +msgid "Delete your account" +msgstr "Excluir sua conta" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "Desculpe, mas como administrador(a), você não pode sair da sua própria instância." + +msgid "Latest articles" +msgstr "Artigos recentes" + +msgid "Atom feed" +msgstr "Feed Atom" + +msgid "Recently boosted" +msgstr "Recentemente compartilhado" + +msgid "Articles tagged \"{0}\"" +msgstr "Artigos com a tag \"{0}\"" + +msgid "There are currently no articles with such a tag" +msgstr "Não há artigos com a tag ainda" + +msgid "The content you sent can't be processed." +msgstr "O conteúdo que você enviou não pôde ser processado." + +msgid "Maybe it was too long." +msgstr "Talvez tenha sido longo demais." + +msgid "Internal server error" +msgstr "Erro interno do servidor" + +msgid "Something broke on our side." +msgstr "Algo deu errado aqui." + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "Desculpe por isso. Se você acha que é um bug, por favor, reporte-o." + +msgid "Invalid CSRF token" +msgstr "Token CSRF inválido" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "Algo está errado com seu token CSRF. Certifique-se de que os cookies estão habilitados no seu navegador e tente atualizar esta página. Se você continuar vendo esta mensagem de erro, por favor reporte-a." + +msgid "You are not authorized." +msgstr "Você não tem permissão." + +msgid "Page not found" +msgstr "Página não encontrada" + +msgid "We couldn't find this page." +msgstr "Não foi possível encontrar esta página." + +msgid "The link that led you here may be broken." +msgstr "O link que você usou pode estar quebrado." + +msgid "Users" +msgstr "Usuários" + +msgid "Configuration" +msgstr "Configuração" + +msgid "Instances" +msgstr "Instâncias" + +msgid "Email blocklist" +msgstr "Lista de bloqueio de e-mails" + +msgid "Grant admin rights" +msgstr "Conceder permissão de administração" + +msgid "Revoke admin rights" +msgstr "Revogar permissão de administração" + +msgid "Grant moderator rights" +msgstr "Conceder permissão de moderação" + +msgid "Revoke moderator rights" +msgstr "Revogar permissão de moderação" + +msgid "Ban" +msgstr "Banir" + +msgid "Run on selected users" +msgstr "Executar em usuários selecionados" + +msgid "Moderator" +msgstr "Moderador(a)" + +msgid "Moderation" +msgstr "Moderação" + +msgid "Home" +msgstr "Página Inicial" + +msgid "Administration of {0}" +msgstr "Administração de {0}" + +msgid "Unblock" +msgstr "Desbloquear" + +msgid "Block" +msgstr "Bloquear" + +msgid "Name" +msgstr "Nome" + +msgid "Allow anyone to register here" +msgstr "Permitir que qualquer um se registre aqui" + +msgid "Short description" +msgstr "Descrição breve" + +msgid "Markdown syntax is supported" +msgstr "Suporta sintaxe Markdown" + +msgid "Long description" +msgstr "Descrição longa" + +msgid "Default article license" +msgstr "Licença padrão do artigo" + +msgid "Save these settings" +msgstr "Salvar estas configurações" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "Se você está navegando neste site como um visitante, nenhum dado sobre você é coletado." + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "Como usuário registrado, você deve fornecer seu nome de usuário (que não precisa ser seu nome real), seu endereço de e-mail funcional e uma senha, para poder entrar, escrever artigos e comentários. O conteúdo que você enviar é armazenado até que você o exclua." + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "Quando você entra, armazenamos dois cookies, um para manter a sua sessão aberta e o outro para impedir outras pessoas de agirem em seu nome. Não armazenamos nenhum outro cookies." + +msgid "Blocklisted Emails" +msgstr "E-mails Bloqueados" + +msgid "Email address" +msgstr "Endereço de e-mail" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "O endereço de e-mail que você deseja bloquear. Para bloquear domínios, você pode usar sintaxe global, por exemplo, '*@exemplo.com' bloqueia todos os endereços de exemplo.com" + +msgid "Note" +msgstr "Nota" + +msgid "Notify the user?" +msgstr "Notificar o usuário?" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "Opcional, mostra uma mensagem para o usuário quando ele tenta criar uma conta com este endereço de e-mail" + +msgid "Blocklisting notification" +msgstr "Notificação de bloqueio" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "A mensagem a ser mostrada quando o usuário tenta criar uma conta com este endereço de e-mail" + +msgid "Add blocklisted address" +msgstr "Adicionar endereço bloqueado" + +msgid "There are no blocked emails on your instance" +msgstr "Não há e-mails bloqueados na sua instância" + +msgid "Delete selected emails" +msgstr "Excluir email(s) selecionado(s)" + +msgid "Email address:" +msgstr "E-mail:" + +msgid "Blocklisted for:" +msgstr "Bloqueado por:" + +msgid "Will notify them on account creation with this message:" +msgstr "Irá notificá-los na criação da conta com esta mensagem:" + +msgid "The user will be silently prevented from making an account" +msgstr "O usuário será impedido de criar uma conta silenciosamente" + +msgid "Welcome to {}" +msgstr "Boas vindas ao {}" + +msgid "View all" +msgstr "Ver tudo" + +msgid "About {0}" +msgstr "Sobre {0}" + +msgid "Runs Plume {0}" +msgstr "Roda Plume {0}" + +msgid "Home to {0} people" +msgstr "Lar de {0} usuários" + +msgid "Who wrote {0} articles" +msgstr "Que escreveu {0} artigos" + +msgid "And are connected to {0} other instances" +msgstr "E federa com {0} outras instâncias" + +msgid "Administred by" +msgstr "Administrado por" + +msgid "Interact with {}" +msgstr "Interagir com {}" + +msgid "Log in to interact" +msgstr "Entre para interagir" + +msgid "Enter your full username to interact" +msgstr "Digite seu nome de usuário completo para interagir" + +msgid "Publish" +msgstr "Publicar" + +msgid "Classic editor (any changes will be lost)" +msgstr "Editor clássico (quaisquer alterações serão perdidas)" + +msgid "Title" +msgstr "Título" + +msgid "Subtitle" +msgstr "Subtítulo" + +msgid "Content" +msgstr "Conteúdo" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "Você pode enviar mídia da sua galeria e inserí-la no artigo usando o código Markdown." + +msgid "Upload media" +msgstr "Enviar mídia" + +msgid "Tags, separated by commas" +msgstr "Tags, separadas por vírgulas" + +msgid "License" +msgstr "Licença" + +msgid "Illustration" +msgstr "Ilustração" + +msgid "This is a draft, don't publish it yet." +msgstr "É um rascunho, não publique ainda." + +msgid "Update" +msgstr "Atualizar" + +msgid "Update, or publish" +msgstr "Atualizar ou publicar" + +msgid "Publish your post" +msgstr "Publicar seu artigo" + +msgid "Written by {0}" +msgstr "Escrito por {0}" + +msgid "All rights reserved." +msgstr "Todos os direitos reservados." + +msgid "This article is under the {0} license." +msgstr "Este artigo está sob a licença {0}." + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "Uma curtida" +msgstr[1] "{0} curtidas" + +msgid "I don't like this anymore" +msgstr "Eu não curto mais isso" + +msgid "Add yours" +msgstr "Curtir" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "Um compartilhamento" +msgstr[1] "{0} compartilhamentos" + +msgid "I don't want to boost this anymore" +msgstr "Não quero mais compartilhar isso" + +msgid "Boost" +msgstr "Compartilhar" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "{0}Entrar{1}, ou {2}usar sua conta do Fediverso{3} para interagir com este artigo" + +msgid "Comments" +msgstr "Comentários" + +msgid "Your comment" +msgstr "Seu comentário" + +msgid "Submit comment" +msgstr "Enviar comentário" + +msgid "No comments yet. Be the first to react!" +msgstr "Sem comentários ainda. Seja o primeiro!" + +msgid "Are you sure?" +msgstr "Você tem certeza?" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "Este artigo ainda é um rascunho. Apenas você e outros autores podem vê-lo." + +msgid "Only you and other authors can edit this article." +msgstr "Apenas você e outros autores podem editar este artigo." + +msgid "Edit" +msgstr "Editar" + +msgid "I'm from this instance" +msgstr "Eu sou dessa instância" + +msgid "Username, or email" +msgstr "Nome de usuário ou e-mail" + +msgid "Log in" +msgstr "Entrar" + +msgid "I'm from another instance" +msgstr "Eu sou de outra instância" + +msgid "Continue to your instance" +msgstr "Continuar para sua instância" + +msgid "Reset your password" +msgstr "Redefinir sua senha" + +msgid "New password" +msgstr "Nova senha" + +msgid "Confirmation" +msgstr "Confirmação" + +msgid "Update password" +msgstr "Atualizar senha" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "Enviamos para você um e-mail com um link para redefinir sua senha." + +msgid "Send password reset link" +msgstr "Enviar link para redefinir senha" + +msgid "This token has expired" +msgstr "Este token expirou" + +msgid "Please start the process again by clicking here." +msgstr "Por favor, inicie o processo novamente clicando em aqui." + +msgid "New Blog" +msgstr "Novo Blog" + +msgid "Create a blog" +msgstr "Criar um blog" + +msgid "Create blog" +msgstr "Criar blog" + +msgid "Edit \"{}\"" +msgstr "Editar \"{}\"" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "Você pode enviar imagens da sua galeria, para usá-las como ícones ou capas do blog." + +msgid "Upload images" +msgstr "Enviar imagens" + +msgid "Blog icon" +msgstr "Ícone do blog" + +msgid "Blog banner" +msgstr "Capa do blog" + +msgid "Custom theme" +msgstr "Tema personalizado" + +msgid "Update blog" +msgstr "Atualizar blog" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "Tenha muito cuidado, qualquer ação tomada aqui não poderá ser desfeita." + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "Tem certeza que deseja apagar permanentemente este blog?" + +msgid "Permanently delete this blog" +msgstr "Excluir permanentemente este blog" + +msgid "{}'s icon" +msgstr "Ícone de {}" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "Há apenas um autor neste blog: " +msgstr[1] "Há {0} autores neste blog: " + +msgid "No posts to see here yet." +msgstr "Sem artigos ainda." + +msgid "Nothing to see here yet." +msgstr "Nada para ver aqui ainda." + +msgid "None" +msgstr "Nenhum" + +msgid "No description" +msgstr "Sem descrição" + +msgid "Respond" +msgstr "Responder" + +msgid "Delete this comment" +msgstr "Excluir este comentário" + +msgid "What is Plume?" +msgstr "O que é Plume?" + +msgid "Plume is a decentralized blogging engine." +msgstr "Plume é um motor de blogs descentralizado." + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "Autores podem gerenciar vários blogs, cada um como seu próprio site." + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "Os artigos também são visíveis em outras instâncias Plume, e você pode interagir com elas diretamente de outras plataformas como o Mastodon." + +msgid "Read the detailed rules" +msgstr "Leia as regras detalhadas" + +msgid "By {0}" +msgstr "Por {0}" + +msgid "Draft" +msgstr "Rascunho" + +msgid "Search result(s) for \"{0}\"" +msgstr "Resultado da pesquisa para \"{0}\"" + +msgid "Search result(s)" +msgstr "Resultado da pesquisa" + +msgid "No results for your query" +msgstr "Sem resultado" + +msgid "No more results for your query" +msgstr "Sem mais resultados" + +msgid "Advanced search" +msgstr "Pesquisa avançada" + +msgid "Article title matching these words" +msgstr "Título de artigo correspondente a estas palavras" + +msgid "Subtitle matching these words" +msgstr "Subtítulo correspondente a estas palavras" + +msgid "Content macthing these words" +msgstr "Conteúdo que contenha estas palavras" + +msgid "Body content" +msgstr "Conteúdo do artigo" + +msgid "From this date" +msgstr "A partir desta data" + +msgid "To this date" +msgstr "Até esta data" + +msgid "Containing these tags" +msgstr "Contendo estas tags" + +msgid "Tags" +msgstr "Tags" + +msgid "Posted on one of these instances" +msgstr "Publicado em uma destas instâncias" + +msgid "Instance domain" +msgstr "Domínio da instância" + +msgid "Posted by one of these authors" +msgstr "Publicado por um desses autores" + +msgid "Author(s)" +msgstr "Autor(es)" + +msgid "Posted on one of these blogs" +msgstr "Publicado em um desses blogs" + +msgid "Blog title" +msgstr "Título do blog" + +msgid "Written in this language" +msgstr "Escrito neste idioma" + +msgid "Language" +msgstr "Idioma" + +msgid "Published under this license" +msgstr "Publicado sob esta licença" + +msgid "Article license" +msgstr "Licença do artigo" + diff --git a/po/plume/ro.po b/po/plume/ro.po new file mode 100644 index 00000000000..cc77b79d5cb --- /dev/null +++ b/po/plume/ro.po @@ -0,0 +1,1037 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Romanian\n" +"Language: ro_RO\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: ro\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} a comentat pe articolul tău." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} este abonat la tine." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} i-a plăcut articolul tău." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} te-a menționat." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} impulsionat articolul tău." + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "Avatarul lui {0}" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "Opţional" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "Pentru a crea un nou blog, trebuie sa fii logat" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "" + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "" + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "Nu aveți permisiunea de a șterge acest blog." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "" + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "" + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "" + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "" + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "" + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "" + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "" + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "" + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "" + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "" + +# src/routes/medias.rs:180 +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 "" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "Acest post nu a fost publicată încă." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "Pentru a scrie un post nou, trebuie să fii logat" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "" + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "Postare nouă" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "Editare {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "" + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "" + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "" + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "" + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "" + +# src/routes/reshares.rs:58 +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:95 +msgid "You are now connected." +msgstr "" + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "Resetare parolă" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "Parola dumneavoastră a fost resetată cu succes." + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "" + +msgid "Create an account" +msgstr "" + +msgid "Email" +msgstr "Email" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "Nume utilizator" + +msgid "Password" +msgstr "Parolă" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Your media" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "Meniu" + +msgid "Search" +msgstr "Caută" + +msgid "Dashboard" +msgstr "Tablou de bord" + +msgid "Notifications" +msgstr "Notificări" + +msgid "Log Out" +msgstr "Deconectare" + +msgid "My account" +msgstr "Contul meu" + +msgid "Log In" +msgstr "Autentificare" + +msgid "Register" +msgstr "Înregistrare" + +msgid "About this instance" +msgstr "Despre această instanță" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "Administrație" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "Cod sursă" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "Admin" + +msgid "It is you" +msgstr "" + +msgid "Edit your profile" +msgstr "Editează-ți profilul" + +msgid "Open on {0}" +msgstr "Deschide la {0}" + +msgid "Unsubscribe" +msgstr "Dezabonare" + +msgid "Subscribe" +msgstr "Abonare" + +msgid "Follow {}" +msgstr "" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "Articole" + +msgid "Subscribers" +msgstr "Abonaţi" + +msgid "Subscriptions" +msgstr "Abonamente" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "" + +msgid "Your Drafts" +msgstr "" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "" + +msgid "Your Profile" +msgstr "" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "Ultimele articole" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "Utilizatori" + +msgid "Configuration" +msgstr "Configurare" + +msgid "Instances" +msgstr "Instanțe" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "Interzice" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "" + +msgid "Unblock" +msgstr "Deblochează" + +msgid "Block" +msgstr "Bloc" + +msgid "Name" +msgstr "Nume" + +msgid "Allow anyone to register here" +msgstr "" + +msgid "Short description" +msgstr "" + +msgid "Markdown syntax is supported" +msgstr "" + +msgid "Long description" +msgstr "" + +msgid "Default article license" +msgstr "" + +msgid "Save these settings" +msgstr "" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +msgid "Administred by" +msgstr "" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "Titlu" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +msgid "Tags, separated by commas" +msgstr "" + +msgid "License" +msgstr "" + +msgid "Illustration" +msgstr "Ilustraţie" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "Actualizare" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +msgid "I don't like this anymore" +msgstr "" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "Boost" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "" + +msgid "Comments" +msgstr "" + +msgid "Your comment" +msgstr "Comentariul tău" + +msgid "Submit comment" +msgstr "" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "Sînteți sigur?" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "Editare" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "" + +msgid "Log in" +msgstr "" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +msgid "New password" +msgstr "" + +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "" + +msgid "Create a blog" +msgstr "" + +msgid "Create blog" +msgstr "" + +msgid "Edit \"{}\"" +msgstr "" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "Răspuns" + +msgid "Delete this comment" +msgstr "Şterge comentariul" + +msgid "What is Plume?" +msgstr "Ce este Plume?" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "Ciornă" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "Conţinut de corp" + +msgid "From this date" +msgstr "" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "Etichete" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "Postat pe unul dintre aceste bloguri" + +msgid "Blog title" +msgstr "" + +msgid "Written in this language" +msgstr "Scris în această limbă" + +msgid "Language" +msgstr "" + +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" + diff --git a/po/plume/ru.po b/po/plume/ru.po new file mode 100644 index 00000000000..953dacec3f1 --- /dev/null +++ b/po/plume/ru.po @@ -0,0 +1,1040 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Russian\n" +"Language: ru_RU\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: ru\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} прокомментировал(а) Вашу статью." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} подписан на вас." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} понравилась ваша статья." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} упомянул вас." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} продвинули вашу статью." + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "Ваша лента" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "Локальная лента" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "Объединенная лента" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "Аватар {0}" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "Предыдущая страница" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "Следующая страница" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "Не обязательно" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "Для создания нового блога необходимо войти в систему" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "Блог с таким именем уже существует." + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "Ваш блог был успешно создан!" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "Ваш блог был удален." + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "Вы не можете удалить этот блог." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "Вы не можете редактировать этот блог." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "Вы не можете использовать этот объект в качестве иконки блога." + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "Вы не можете использовать этот объект как баннер блога." + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "Информация о вашем блоге обновлена." + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "Ваш комментарий опубликован." + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "Ваш комментарий был удалён." + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "Регистрации на данном экземпляре закрыты." + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "Ваша учетная запись была создана. Теперь вам нужно авторизоваться, прежде чем вы сможете ее использовать." + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "Настройки экземпляра сохранены." + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "Пользователь {} был разблокирован." + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "Пользователь {} был заблокирован." + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "Блоки удалены" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "Почтовый адрес уже заблокирован" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "Email заблокирован" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "Вы не можете изменить ваши собственные права." + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "Вы не можете выполнить это действие." + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "Выполнено." + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "Чтобы лайкнуть сообщением, вы должны войти в систему" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "Ваш файл был удалён." + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "У вас нет прав на удаление этого файла." + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "Ваш аватар был обновлен." + +# src/routes/medias.rs:180 +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 "Чтобы увидеть ваши уведомления, вы должны войти в систему" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "Этот пост ещё не опубликован." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "Чтобы написать новое сообщение, необходимо войти в систему" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "Вы не являетесь автором этого блога." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "Новый пост" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "Редактировать {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "Вы не имеете прав на публикацию в этом блоге." + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "Ваша статья была обновлена." + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "Ваша статья сохранена." + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "Новая статья" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "Вы не можете удалить эту статью." + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "Ваша статья была удалена." + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "Похоже, статья, которую вы пытаетесь удалить, не существует. Возможно она уже исчезла?" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "Не удалось получить достаточную информацию о вашей учетной записи. Пожалуйста, убедитесь, что ваше имя пользователя правильно." + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "Чтобы поделиться сообщением, вы должны войти в систему" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "Теперь в списке контактов." + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "Вы теперь вышли." + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "Восстановление пароля" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "Перейдите по ссылке для сброса вашего пароля: {0}" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "Ваш пароль был успешно сброшен." + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "Для доступа к панели инструментов необходимо войти в систему" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "Вы больше не отслеживаете {}." + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "Теперь вы отслеживаете {}." + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "Чтобы подписаться на кого-то, необходимо войти в систему" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "Для редактирования профиля необходимо войти в систему" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "Ваш профиль был изменен." + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "Ваша учетная запись удалена." + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "Вы не можете удалить чужую учетную запись." + +msgid "Create your account" +msgstr "Создать аккаунт" + +msgid "Create an account" +msgstr "Создать новый аккаунт" + +msgid "Email" +msgstr "Электронная почта" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "Приносим извинения, но регистрация на данном узле закрыта. Однако, вы можете найти другой." + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "Проверьте ваш почтовый ящик!" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "Имя пользователя" + +msgid "Password" +msgstr "Пароль" + +msgid "Password confirmation" +msgstr "Подтверждение пароля" + +msgid "Media upload" +msgstr "Загрузка медиафайлов" + +msgid "Description" +msgstr "Описание" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "Полезно для людей с недостатками зрения и для информации об авторских правах" + +msgid "Content warning" +msgstr "Предупреждение о контенте" + +msgid "Leave it empty, if none is needed" +msgstr "Оставьте пустым, если оно вам не нужно" + +msgid "File" +msgstr "Файл" + +msgid "Send" +msgstr "Отправить" + +msgid "Your media" +msgstr "Ваши медиафайлы" + +msgid "Upload" +msgstr "Загрузить" + +msgid "You don't have any media yet." +msgstr "У вас еще нет медиа-файлов." + +msgid "Content warning: {0}" +msgstr "Предупреждение о содержимом: {0}" + +msgid "Delete" +msgstr "Удалить" + +msgid "Details" +msgstr "Подробности" + +msgid "Media details" +msgstr "Детали медиафайла" + +msgid "Go back to the gallery" +msgstr "Вернуться в галерею" + +msgid "Markdown syntax" +msgstr "Синтаксис Markdown" + +msgid "Copy it into your articles, to insert this media:" +msgstr "Скопируйте это в статьи, чтобы вставить этот объект:" + +msgid "Use as an avatar" +msgstr "Установить в качестве аватара" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "Меню" + +msgid "Search" +msgstr "Поиск" + +msgid "Dashboard" +msgstr "Панель управления" + +msgid "Notifications" +msgstr "Уведомления" + +msgid "Log Out" +msgstr "Выйти" + +msgid "My account" +msgstr "Мой аккаунт" + +msgid "Log In" +msgstr "Войти" + +msgid "Register" +msgstr "Зарегистрироваться" + +msgid "About this instance" +msgstr "Об этом узле" + +msgid "Privacy policy" +msgstr "Политика приватности" + +msgid "Administration" +msgstr "Администрирование" + +msgid "Documentation" +msgstr "Документация" + +msgid "Source code" +msgstr "Исходный код" + +msgid "Matrix room" +msgstr "Комната в Matrix" + +msgid "Admin" +msgstr "Администратор" + +msgid "It is you" +msgstr "Это вы" + +msgid "Edit your profile" +msgstr "Редактировать ваш профиль" + +msgid "Open on {0}" +msgstr "Открыть в {0}" + +msgid "Unsubscribe" +msgstr "Отписаться" + +msgid "Subscribe" +msgstr "Подписаться" + +msgid "Follow {}" +msgstr "Отслеживать {}" + +msgid "Log in to follow" +msgstr "Войдите, чтобы включить отслеживание" + +msgid "Enter your full username handle to follow" +msgstr "Введите ваше полное имя пользователя для того, чтобы отслеживать" + +msgid "{0}'s subscribers" +msgstr "{0} подписчиков" + +msgid "Articles" +msgstr "Статьи" + +msgid "Subscribers" +msgstr "Подписчики" + +msgid "Subscriptions" +msgstr "Подписки" + +msgid "{0}'s subscriptions" +msgstr "{0} подписок" + +msgid "Your Dashboard" +msgstr "Ваша панель управления" + +msgid "Your Blogs" +msgstr "Ваш блог" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "У вас пока нет ни одного блога. Создайте свой собственный блог, или попросите разрешения присоединиться к одному из существующих." + +msgid "Start a new blog" +msgstr "Начать новый блог" + +msgid "Your Drafts" +msgstr "Ваши черновики" + +msgid "Go to your gallery" +msgstr "Перейти в вашу галерею" + +msgid "Edit your account" +msgstr "Редактировать ваш аккаунт" + +msgid "Your Profile" +msgstr "Ваш профиль" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "Чтобы изменить свой аватар, загрузите его в галерею и затем выберите из неё." + +msgid "Upload an avatar" +msgstr "Загрузить аватар" + +msgid "Display name" +msgstr "Имя для отображения" + +msgid "Summary" +msgstr "Краткое описание" + +msgid "Theme" +msgstr "Тема" + +msgid "Default theme" +msgstr "Тема по умолчанию" + +msgid "Error while loading theme selector." +msgstr "Ошибка при загрузке селектора темы." + +msgid "Never load blogs custom themes" +msgstr "Никогда не загружать пользовательские темы блогов" + +msgid "Update account" +msgstr "Обновить учетную запись" + +msgid "Danger zone" +msgstr "Опасная зона" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "Будьте очень осторожны, никакие действия здесь не могут быть отменены." + +msgid "Delete your account" +msgstr "Удалить ваш аккаунт" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "Извините, но как администратор вы не можете покинуть свой собственный экземпляр." + +msgid "Latest articles" +msgstr "Последние статьи" + +msgid "Atom feed" +msgstr "Канал Atom" + +msgid "Recently boosted" +msgstr "Недавно продвинутые" + +msgid "Articles tagged \"{0}\"" +msgstr "Статьи с тегом \"{0}\"" + +msgid "There are currently no articles with such a tag" +msgstr "В настоящее время нет статей с таким тегом" + +msgid "The content you sent can't be processed." +msgstr "Контент, который вы отправили, не может быть обработан." + +msgid "Maybe it was too long." +msgstr "Может быть, он слишком длинный." + +msgid "Internal server error" +msgstr "Внутренняя ошибка сервера" + +msgid "Something broke on our side." +msgstr "На нашей стороне что-то пошло не так." + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "Приносим извинения. Если вы считаете что это ошибка, пожалуйста сообщите о ней." + +msgid "Invalid CSRF token" +msgstr "Неверный CSRF токен" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "Что-то не так с вашим CSRF-токеном. Убедитесь что в вашем браузере включены cookies и попробуйте перезагрузить страницу. Если вы продолжите видеть это сообщение об ошибке, сообщите об этом." + +msgid "You are not authorized." +msgstr "Вы не авторизованы." + +msgid "Page not found" +msgstr "Страница не найдена" + +msgid "We couldn't find this page." +msgstr "Мы не можем найти эту страницу." + +msgid "The link that led you here may be broken." +msgstr "Сcылка, которая привела вас сюда, вероятно, оибочная." + +msgid "Users" +msgstr "Пользователи" + +msgid "Configuration" +msgstr "Конфигурация" + +msgid "Instances" +msgstr "Узлы" + +msgid "Email blocklist" +msgstr "Черный список E-mail" + +msgid "Grant admin rights" +msgstr "Предоставить права администратора" + +msgid "Revoke admin rights" +msgstr "Отозвать права администратора" + +msgid "Grant moderator rights" +msgstr "Дать права модератора" + +msgid "Revoke moderator rights" +msgstr "Отозвать права модератора" + +msgid "Ban" +msgstr "Заблокировать" + +msgid "Run on selected users" +msgstr "Запуск для выбранных пользователей" + +msgid "Moderator" +msgstr "Модератор" + +msgid "Moderation" +msgstr "Модерация" + +msgid "Home" +msgstr "Главная страница" + +msgid "Administration of {0}" +msgstr "Администрирование {0}" + +msgid "Unblock" +msgstr "Разблокировать" + +msgid "Block" +msgstr "Заблокировать" + +msgid "Name" +msgstr "Имя" + +msgid "Allow anyone to register here" +msgstr "Разрешить любому зарегистрироваться здесь" + +msgid "Short description" +msgstr "Краткое описание" + +msgid "Markdown syntax is supported" +msgstr "Поддерживается синтаксис Markdown" + +msgid "Long description" +msgstr "Длинное описание" + +msgid "Default article license" +msgstr "Лицензия статей по умолчанию" + +msgid "Save these settings" +msgstr "Сохранить эти настройки" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "Если вы просматриваете этот сайт в качестве посетителя, никаких данных о вас не собирается." + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "Как зарегистрированный пользователь, вы должны указать ваше имя пользователя (которое не должно быть вашим настоящим именем), ваш действующий адрес электронной почты и пароль, чтобы иметь возможность войти в систему, писать статьи и комментарии. Составленный вами контент хранится до тех пор, пока вы его не удаляете." + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "Когда вы входите в систему, мы храним два cookie-файла, один - для того, чтобы поддерживать вашу сессию открытой,, второй, чтобы не допустить действий других людей от вашего имени. Мы не храним другие куки." + +msgid "Blocklisted Emails" +msgstr "Заблокированные адреса электронной почты" + +msgid "Email address" +msgstr "Адрес электронной почты" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "Адрес электронной почты, который вы хотите заблокировать. Чтобы заблокировать домены, вы можете использовать метасимволы, например '*@example.com' блокирует все адреса от example.com" + +msgid "Note" +msgstr "Заметка" + +msgid "Notify the user?" +msgstr "Уведомить пользователя?" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "Необязательно, показывает пользователю сообщение при попытке создать учетную запись с этим адресом" + +msgid "Blocklisting notification" +msgstr "Уведомление о Блокировке" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "Сообщение, которое будет показано, когда пользователь пытается создать учетную запись с этим адресом электронной почты" + +msgid "Add blocklisted address" +msgstr "Добавить адрес в список блокировки" + +msgid "There are no blocked emails on your instance" +msgstr "На вашем узле нет заблокированных адресов электронной почты" + +msgid "Delete selected emails" +msgstr "Удалить выбранные адреса" + +msgid "Email address:" +msgstr "Адрес электронной почты:" + +msgid "Blocklisted for:" +msgstr "Блокировано для:" + +msgid "Will notify them on account creation with this message:" +msgstr "Уведомлять их при создании учетной записи с таким сообщением:" + +msgid "The user will be silently prevented from making an account" +msgstr "Пользователю не будет позволено создать учетную запись без объяснения причин" + +msgid "Welcome to {}" +msgstr "Добро пожаловать в {}" + +msgid "View all" +msgstr "Показать все" + +msgid "About {0}" +msgstr "О {0}" + +msgid "Runs Plume {0}" +msgstr "Работает на Plume {0}" + +msgid "Home to {0} people" +msgstr "Место общения {0} человек" + +msgid "Who wrote {0} articles" +msgstr "Которые написали {0} статей" + +msgid "And are connected to {0} other instances" +msgstr "И подключены к {0} другим узлам" + +msgid "Administred by" +msgstr "Администрируется" + +msgid "Interact with {}" +msgstr "Взаимодействовать с {}" + +msgid "Log in to interact" +msgstr "Войдите, чтобы взаимодействовать" + +msgid "Enter your full username to interact" +msgstr "Введите полное имя пользователя для взаимодействия" + +msgid "Publish" +msgstr "Опубликовать" + +msgid "Classic editor (any changes will be lost)" +msgstr "Классический редактор (все изменения будут утеряны)" + +msgid "Title" +msgstr "Заголовок" + +msgid "Subtitle" +msgstr "Подзаголовок" + +msgid "Content" +msgstr "Содержимое" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "Вы можете загрузить медиафайлы в галерею, а затем скопировать их код Markdown в ваши статьи, чтобы вставить их." + +msgid "Upload media" +msgstr "Загрузить медиафайл" + +msgid "Tags, separated by commas" +msgstr "Теги через запятую" + +msgid "License" +msgstr "Лицензия" + +msgid "Illustration" +msgstr "Иллюстрация" + +msgid "This is a draft, don't publish it yet." +msgstr "Это черновик, пока не публикуйте его." + +msgid "Update" +msgstr "Обновить" + +msgid "Update, or publish" +msgstr "Обновить или опубликовать" + +msgid "Publish your post" +msgstr "Опубликовать ваш пост" + +msgid "Written by {0}" +msgstr "Написано {0}" + +msgid "All rights reserved." +msgstr "Все права защищены." + +msgid "This article is under the {0} license." +msgstr "Эта статья опубликована под лицензией {0}." + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "Один лайк" +msgstr[1] "{0} лайка" +msgstr[2] "{0} лайков" +msgstr[3] "{0} лайков" + +msgid "I don't like this anymore" +msgstr "Мне это больше не нравится" + +msgid "Add yours" +msgstr "Добавьте ваш" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "Одно продвижение" +msgstr[1] "{0} продвижения" +msgstr[2] "{0} продвижений" +msgstr[3] "{0} продвижений" + +msgid "I don't want to boost this anymore" +msgstr "Я не хочу больше повышать это" + +msgid "Boost" +msgstr "Продвинуть" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "{0}Войдите{1}, или {2}используйте ваш Fediverse аккаунт{3} для взаимодействия с этой статьей" + +msgid "Comments" +msgstr "Комментарии" + +msgid "Your comment" +msgstr "Ваш комментарий" + +msgid "Submit comment" +msgstr "Отправить комментарий" + +msgid "No comments yet. Be the first to react!" +msgstr "Комментариев пока нет. Будьте первым, кто выскажется!" + +msgid "Are you sure?" +msgstr "Вы уверены?" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "Эта статья все еще является черновиком. Только вы и другие авторы могут ее видеть." + +msgid "Only you and other authors can edit this article." +msgstr "Только вы и другие авторы могут редактировать эту статью." + +msgid "Edit" +msgstr "Редактировать" + +msgid "I'm from this instance" +msgstr "Я пользователь этого узла" + +msgid "Username, or email" +msgstr "Имя пользователя или адрес эл. почты" + +msgid "Log in" +msgstr "Войти" + +msgid "I'm from another instance" +msgstr "Я пользователь другого узла" + +msgid "Continue to your instance" +msgstr "Продолжить на вашем узле" + +msgid "Reset your password" +msgstr "Сбросить пароль" + +msgid "New password" +msgstr "Новый пароль" + +msgid "Confirmation" +msgstr "Подтверждение" + +msgid "Update password" +msgstr "Сменить пароль" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "Мы отправили письмо на адрес, который вы нам дали, со ссылкой для сброса пароля." + +msgid "Send password reset link" +msgstr "Отправить ссылку для сброса пароля" + +msgid "This token has expired" +msgstr "Истек срок действия этого токена" + +msgid "Please start the process again by clicking here." +msgstr "Пожалуйста, начните процесс еще раз, нажав здесь." + +msgid "New Blog" +msgstr "Новый блог" + +msgid "Create a blog" +msgstr "Создать блог" + +msgid "Create blog" +msgstr "Создать блог" + +msgid "Edit \"{}\"" +msgstr "Редактировать \"{}\"" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "Вы можете загружать изображения в галерею, использовать их как иконки блога, или баннеры." + +msgid "Upload images" +msgstr "Загрузить изображения" + +msgid "Blog icon" +msgstr "Иконка блога" + +msgid "Blog banner" +msgstr "Баннер блога" + +msgid "Custom theme" +msgstr "Пользовательская тема" + +msgid "Update blog" +msgstr "Обновить блог" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "Будьте очень осторожны, никакие действия здесь не могут быть отменены." + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "Вы уверены, что хотите навсегда удалить этот блог?" + +msgid "Permanently delete this blog" +msgstr "Навсегда удалить этот блог" + +msgid "{}'s icon" +msgstr "Значок {}" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "В этом блоге один автор: " +msgstr[1] "В этом блоге {0} автора: " +msgstr[2] "В этом блоге {0} автора: " +msgstr[3] "В этом блоге {0} авторов: " + +msgid "No posts to see here yet." +msgstr "Здесь пока нет постов." + +msgid "Nothing to see here yet." +msgstr "Здесь ничего нет." + +msgid "None" +msgstr "Нет" + +msgid "No description" +msgstr "Нет описания" + +msgid "Respond" +msgstr "Ответить" + +msgid "Delete this comment" +msgstr "Удалить этот комментарий" + +msgid "What is Plume?" +msgstr "Что такое Plume?" + +msgid "Plume is a decentralized blogging engine." +msgstr "Plume это децентрализованный движок для блоггинга." + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "Авторы могут управлять несколькими блогами, каждый из которых является собственным сайтом." + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "Статьи также видны на других экземплярах Plume, и вы можете взаимодействовать с ними непосредственно с других платформ, таких как Мастодонт." + +msgid "Read the detailed rules" +msgstr "Прочитать подробные правила" + +msgid "By {0}" +msgstr "От: {0}" + +msgid "Draft" +msgstr "Черновик" + +msgid "Search result(s) for \"{0}\"" +msgstr "Результат(ы) поиска для \"{0}\"" + +msgid "Search result(s)" +msgstr "Результат(ы) поиска" + +msgid "No results for your query" +msgstr "По вашему запросу ничего не найдено" + +msgid "No more results for your query" +msgstr "Больше результатов по вашему запросу нет" + +msgid "Advanced search" +msgstr "Расширенный поиск" + +msgid "Article title matching these words" +msgstr "Заголовок статьи, содержащий эти слова" + +msgid "Subtitle matching these words" +msgstr "Подзаголовок. содержащий эти слова" + +msgid "Content macthing these words" +msgstr "Текст статьи, содержащий эти слова" + +msgid "Body content" +msgstr "Содержимое тела" + +msgid "From this date" +msgstr "С этой даты" + +msgid "To this date" +msgstr "До этой даты" + +msgid "Containing these tags" +msgstr "Содержащие эти тэги" + +msgid "Tags" +msgstr "Теги" + +msgid "Posted on one of these instances" +msgstr "Опубликовано на одном из этих узлов" + +msgid "Instance domain" +msgstr "Домен узла" + +msgid "Posted by one of these authors" +msgstr "Опубликовано одним из этих авторов" + +msgid "Author(s)" +msgstr "Автор(ы)" + +msgid "Posted on one of these blogs" +msgstr "Размещено на одном из блогов" + +msgid "Blog title" +msgstr "Заголовок блога" + +msgid "Written in this language" +msgstr "Написано на этом языке" + +msgid "Language" +msgstr "Язык" + +msgid "Published under this license" +msgstr "Опубликовано по этой лицензии" + +msgid "Article license" +msgstr "Лицензия статьи" + diff --git a/po/plume/sat.po b/po/plume/sat.po new file mode 100644 index 00000000000..00381db315a --- /dev/null +++ b/po/plume/sat.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Santali\n" +"Language: sat_IN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: sat\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "" + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "" + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "" + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "" + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "" + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "ᱟᱢᱟᱜ ᱯᱷᱤᱤᱰ" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "ᱞᱚᱠᱟᱞ ᱯᱷᱤᱤᱰ" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "ᱯᱷᱮᱰᱟᱹᱨᱮᱮᱴᱰ ᱯᱷᱤᱤᱰ" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "{0} ᱟᱹᱣᱛᱟᱨ ᱠᱚ" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "ᱢᱟᱲᱟᱝ ᱥᱟᱦᱴᱟ" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "ᱤᱱᱟᱹ ᱛᱟᱭᱚᱢ ᱥᱟᱦᱴᱟ" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "ᱚᱯᱥᱚᱱᱟᱞ" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "" + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "" + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "" + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "" + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "" + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "" + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "" + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "" + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "" + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "" + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "ᱦᱩᱭᱮᱱᱟ ᱾" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "" + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "" + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "" + +# src/routes/medias.rs:180 +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 "" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "" + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "" + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "ᱱᱟᱣᱟ ᱯᱚᱥᱴ" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "ᱥᱟᱯᱲᱟᱣ {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "" + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "" + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "ᱱᱟᱶᱟ ᱚᱱᱚᱞ" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "" + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "" + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "" + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "" + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "" + +msgid "Create an account" +msgstr "" + +msgid "Email" +msgstr "" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Your media" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "ᱜᱮᱫ ᱜᱤᱰᱤ" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "" + +msgid "Menu" +msgstr "" + +msgid "Search" +msgstr "ᱥᱮᱸᱫᱽᱨᱟᱭ" + +msgid "Dashboard" +msgstr "" + +msgid "Notifications" +msgstr "" + +msgid "Log Out" +msgstr "" + +msgid "My account" +msgstr "" + +msgid "Log In" +msgstr "" + +msgid "Register" +msgstr "" + +msgid "About this instance" +msgstr "" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "ᱮᱰᱢᱤᱱ" + +msgid "It is you" +msgstr "ᱱᱩᱭ ᱫᱚ ᱟᱢ ᱠᱟᱱᱟᱢ" + +msgid "Edit your profile" +msgstr "ᱢᱚᱦᱲᱟ ᱥᱟᱯᱲᱟᱣ ᱢᱮ" + +msgid "Open on {0}" +msgstr "{0} ᱨᱮ ᱠᱷᱩᱟᱞᱹᱭ ᱢᱮ" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "" + +msgid "Follow {}" +msgstr "" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "" + +msgid "Subscribers" +msgstr "" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "" + +msgid "Your Drafts" +msgstr "" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "" + +msgid "Your Profile" +msgstr "" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "" + +msgid "Configuration" +msgstr "" + +msgid "Instances" +msgstr "" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "" + +msgid "Unblock" +msgstr "" + +msgid "Block" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Allow anyone to register here" +msgstr "" + +msgid "Short description" +msgstr "" + +msgid "Markdown syntax is supported" +msgstr "" + +msgid "Long description" +msgstr "" + +msgid "Default article license" +msgstr "" + +msgid "Save these settings" +msgstr "" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "{0} ᱵᱟᱵᱚᱛ" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +msgid "Administred by" +msgstr "" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "ᱴᱭᱴᱚᱞ" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +msgid "Tags, separated by commas" +msgstr "" + +msgid "License" +msgstr "" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "" +msgstr[1] "" + +msgid "I don't like this anymore" +msgstr "" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" +msgstr[1] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "" + +msgid "Comments" +msgstr "" + +msgid "Your comment" +msgstr "" + +msgid "Submit comment" +msgstr "" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "" + +msgid "Log in" +msgstr "" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +msgid "New password" +msgstr "" + +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "ᱱᱟᱶᱟ ᱵᱞᱚᱜ" + +msgid "Create a blog" +msgstr "ᱵᱞᱚᱜ ᱛᱮᱭᱟᱨ ᱢᱮ" + +msgid "Create blog" +msgstr "ᱵᱞᱚᱜ ᱛᱮᱭᱟᱨ ᱢᱮ" + +msgid "Edit \"{}\"" +msgstr "\"{}\" ᱥᱟᱯᱰᱟᱣ" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" +msgstr[1] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +msgid "From this date" +msgstr "" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" + diff --git a/po/plume/si.po b/po/plume/si.po new file mode 100644 index 00000000000..04839e4b67e --- /dev/null +++ b/po/plume/si.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Sinhala\n" +"Language: si_LK\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: si-LK\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "" + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "" + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "" + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "" + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "" + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "ඊළඟ පිටුව" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "" + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "" + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "" + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "" + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "" + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "" + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "" + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "" + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "" + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "" + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "" + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "" + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "" + +# src/routes/medias.rs:180 +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 "" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "" + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "" + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "" + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "" + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "" + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "" + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "" + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "" + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "" + +msgid "Create an account" +msgstr "" + +msgid "Email" +msgstr "" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "පරිශීලක නාමය" + +msgid "Password" +msgstr "මුර පදය" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Your media" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "" + +msgid "Menu" +msgstr "" + +msgid "Search" +msgstr "" + +msgid "Dashboard" +msgstr "" + +msgid "Notifications" +msgstr "" + +msgid "Log Out" +msgstr "" + +msgid "My account" +msgstr "" + +msgid "Log In" +msgstr "" + +msgid "Register" +msgstr "" + +msgid "About this instance" +msgstr "" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "" + +msgid "It is you" +msgstr "" + +msgid "Edit your profile" +msgstr "" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "" + +msgid "Follow {}" +msgstr "" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "" + +msgid "Subscribers" +msgstr "" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "" + +msgid "Your Drafts" +msgstr "" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "" + +msgid "Your Profile" +msgstr "" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "පරිශීලකයින්" + +msgid "Configuration" +msgstr "" + +msgid "Instances" +msgstr "" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "" + +msgid "Unblock" +msgstr "අනවහිර" + +msgid "Block" +msgstr "අවහිර" + +msgid "Name" +msgstr "නම" + +msgid "Allow anyone to register here" +msgstr "" + +msgid "Short description" +msgstr "" + +msgid "Markdown syntax is supported" +msgstr "" + +msgid "Long description" +msgstr "" + +msgid "Default article license" +msgstr "" + +msgid "Save these settings" +msgstr "" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "විද්‍යුත් තැපැල් ලිපිනය" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "සටහන" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "විද්‍යුත් තැපැල් ලිපිනය:" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +msgid "Administred by" +msgstr "" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "මාතෘකාව" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "අන්තර්ගතය" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +msgid "Tags, separated by commas" +msgstr "" + +msgid "License" +msgstr "බලපත්‍රය" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "" +msgstr[1] "" + +msgid "I don't like this anymore" +msgstr "" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" +msgstr[1] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "" + +msgid "Comments" +msgstr "අදහස්" + +msgid "Your comment" +msgstr "ඔබගේ අදහස" + +msgid "Submit comment" +msgstr "" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "සංස්කරණය" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "පරිශීලක නාමය හෝ වි-තැපෑල" + +msgid "Log in" +msgstr "පිවිසෙන්න" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +msgid "New password" +msgstr "නව මුරපදය" + +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "" + +msgid "Create a blog" +msgstr "" + +msgid "Create blog" +msgstr "" + +msgid "Edit \"{}\"" +msgstr "" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" +msgstr[1] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "කිසිවක් නැත" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +msgid "From this date" +msgstr "" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" + diff --git a/po/plume/sk.po b/po/plume/sk.po new file mode 100644 index 00000000000..ed8d25e3bb7 --- /dev/null +++ b/po/plume/sk.po @@ -0,0 +1,1040 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-26 13:16\n" +"Last-Translator: \n" +"Language-Team: Slovak\n" +"Language: sk_SK\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: sk\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "Niekto" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} okomentoval/a tvoj článok." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} odoberá tvoje príspevky." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} si obľúbil/a tvoj článok." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} sa o tebe zmienil/a." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} vyzdvihli tvoj článok." + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "Tvoje zdroje" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "Miestny zdroj" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "Federované zdroje" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "Avatar užívateľa {0}" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "Predošlá stránka" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "Ďalšia stránka" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "Volitelné/Nepovinný údaj" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "Aby si vytvoril/a nový blog, musíš byť prihlásený/á" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "Blog s rovnakým názvom už existuje." + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "Tvoj blog bol úspešne vytvorený!" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "Tvoj blog bol zmazaný." + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "Nemáš povolenie vymazať tento blog." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "Nemáš dovolené upravovať tento blog." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "Tento mediálny súbor nemožno použiť ako ikonku pre blog." + +# src/routes/blogs.rs:298 +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:332 +msgid "Your blog information have been updated." +msgstr "Informácie o tvojom blogu boli aktualizované." + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "Tvoj komentár bol odoslaný." + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "Tvoj komentár bol vymazaný." + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "Registrácie na tejto instancii sú uzatvorené." + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "Registrácia užívateľa" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "Link pre zaregistrovanie je tu: {0}" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "Tvoj účet bol vytvorený. K jeho užívaniu sa teraz musíš už len prihlásiť." + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "Nastavenia instancie boli uložené." + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "{} bol/a odblokovaný/á." + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "{} bol/a zablokovaný/á." + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "Blokovania vymazané" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "Email už je zablokovaný" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "Email zablokovaný" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "Nemôžeš zmeniť svoje vlastné oprávnenia." + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "Nemáš oprávnenie vykonať túto akciu." + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "Hotovo." + +# src/routes/likes.rs:58 +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:153 +msgid "Your media have been deleted." +msgstr "Tvoj mediálny súbor bol vymazaný." + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "Nemáš povolenie vymazať tento mediálny súbor." + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "Tvoj avatár bol aktualizovaný." + +# src/routes/medias.rs:180 +msgid "You are not allowed to use this media." +msgstr "Nemáš povolenie použiť tento mediálny súbor." + +# 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ť" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "Tento príspevok ešte nie je uverejnený." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "Pre napísanie nového príspevku sa musíš prihlásiť" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "Nie si autorom na tomto blogu." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "Nový príspevok" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "Uprav {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "Na tomto blogu nemáš dovolené uverejňovať." + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "Tvoj článok bol upravený." + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "Tvoj článok bol uložený." + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "Nový článok" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "Nemáš povolenie vymazať tento článok." + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "Tvoj článok bol vymazaný." + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "Vyzerá to tak, že článok, ktorý si sa pokúšal/a vymazať neexistuje. Možno je už preč?" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "Nebolo možné zistiť postačujúce množstvo informácií o tvojom účte. Prosím over si, že tvoja prezývka je zadaná správne, v celku." + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "Na zdieľanie príspevku sa musíš prihlásiť" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "Teraz si pripojený/á." + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "Teraz si odhlásený." + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "Obnovenie hesla" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "Tu je odkaz na obnovenie tvojho hesla: {0}" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "Tvoje heslo bolo úspešne zmenené." + +# src/routes/user.rs:87 +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:109 +msgid "You are no longer following {}." +msgstr "Už nenásleduješ {}." + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "Teraz už následuješ {}." + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "Ak chceš niekoho odoberať, musíš sa prihlásiť" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "Na upravenie svojho profilu sa musíš prihlásiť" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "Tvoj profil bol upravený." + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "Tvoj účet bol vymazaný." + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "Nemôžeš vymazať účet niekoho iného." + +msgid "Create your account" +msgstr "Vytvor si účet" + +msgid "Create an account" +msgstr "Vytvor účet" + +msgid "Email" +msgstr "Emailová adresa" + +msgid "Email confirmation" +msgstr "Potvrdenie emailom" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "Ospravedlňujeme sa, ale na tejto konkrétnej instancii sú registrácie zatvorené. Môžeš si však nájsť inú." + +msgid "Registration" +msgstr "Registrovať" + +msgid "Check your inbox!" +msgstr "Pozri si svoju Doručenú poštu!" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "Užívateľské meno" + +msgid "Password" +msgstr "Heslo" + +msgid "Password confirmation" +msgstr "Potvrdenie hesla" + +msgid "Media upload" +msgstr "Nahrávanie mediálnych súborov" + +msgid "Description" +msgstr "Popis" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "Užitočné pre zrakovo postihnutých ľudí, ako aj pre informácie o licencovaní" + +msgid "Content warning" +msgstr "Varovanie o obsahu" + +msgid "Leave it empty, if none is needed" +msgstr "Nechaj prázdne, ak žiadne nieje treba" + +msgid "File" +msgstr "Súbor" + +msgid "Send" +msgstr "Pošli" + +msgid "Your media" +msgstr "Tvoje multimédiá" + +msgid "Upload" +msgstr "Nahraj" + +msgid "You don't have any media yet." +msgstr "Ešte nemáš nahraté žiadne médiá." + +msgid "Content warning: {0}" +msgstr "Upozornenie o obsahu: {0}" + +msgid "Delete" +msgstr "Zmazať" + +msgid "Details" +msgstr "Podrobnosti" + +msgid "Media details" +msgstr "Podrobnosti o médiu" + +msgid "Go back to the gallery" +msgstr "Prejdi späť do galérie" + +msgid "Markdown syntax" +msgstr "Markdown syntaxia" + +msgid "Copy it into your articles, to insert this media:" +msgstr "Kód skopíruj do tvojho článku, pre vloženie tohto mediálneho súboru:" + +msgid "Use as an avatar" +msgstr "Použi ako avatar" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "Ponuka" + +msgid "Search" +msgstr "Hľadaj" + +msgid "Dashboard" +msgstr "Prehľadový panel" + +msgid "Notifications" +msgstr "Oboznámenia" + +msgid "Log Out" +msgstr "Odhlás sa" + +msgid "My account" +msgstr "Môj účet" + +msgid "Log In" +msgstr "Prihlás sa" + +msgid "Register" +msgstr "Registrácia" + +msgid "About this instance" +msgstr "O tejto instancii" + +msgid "Privacy policy" +msgstr "Zásady súkromia" + +msgid "Administration" +msgstr "Administrácia" + +msgid "Documentation" +msgstr "Dokumentácia" + +msgid "Source code" +msgstr "Zdrojový kód" + +msgid "Matrix room" +msgstr "Matrix miestnosť" + +msgid "Admin" +msgstr "Správca" + +msgid "It is you" +msgstr "Toto si ty" + +msgid "Edit your profile" +msgstr "Uprav svoj profil" + +msgid "Open on {0}" +msgstr "Otvor na {0}" + +msgid "Unsubscribe" +msgstr "Neodoberaj" + +msgid "Subscribe" +msgstr "Odoberaj" + +msgid "Follow {}" +msgstr "Následuj {}" + +msgid "Log in to follow" +msgstr "Pre následovanie sa prihlás" + +msgid "Enter your full username handle to follow" +msgstr "Pre následovanie zadaj svoju prezývku v úplnosti, aby si následoval/a" + +msgid "{0}'s subscribers" +msgstr "Odberatelia obsahu od {0}" + +msgid "Articles" +msgstr "Články" + +msgid "Subscribers" +msgstr "Odberatelia" + +msgid "Subscriptions" +msgstr "Odoberané" + +msgid "{0}'s subscriptions" +msgstr "Odoberané užívateľom {0}" + +msgid "Your Dashboard" +msgstr "Tvoja nástenka" + +msgid "Your Blogs" +msgstr "Tvoje blogy" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "Ešte nemáš žiaden blog. Vytvor si svoj vlastný, alebo požiadaj v niektorom o členstvo." + +msgid "Start a new blog" +msgstr "Začni nový blog" + +msgid "Your Drafts" +msgstr "Tvoje koncepty" + +msgid "Go to your gallery" +msgstr "Prejdi do svojej galérie" + +msgid "Edit your account" +msgstr "Uprav svoj účet" + +msgid "Your Profile" +msgstr "Tvoj profil" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "Pre zmenu tvojho avataru ho nahraj do svojej galérie a potom ho odtiaľ zvoľ." + +msgid "Upload an avatar" +msgstr "Nahraj avatar" + +msgid "Display name" +msgstr "Zobrazované meno" + +msgid "Summary" +msgstr "Súhrn" + +msgid "Theme" +msgstr "Vzhľad" + +msgid "Default theme" +msgstr "Predvolený vzhľad" + +msgid "Error while loading theme selector." +msgstr "Chyba pri načítaní výberu vzhľadov." + +msgid "Never load blogs custom themes" +msgstr "Nikdy nenačítavaj vlastné témy blogov" + +msgid "Update account" +msgstr "Aktualizuj účet" + +msgid "Danger zone" +msgstr "Riziková zóna" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "Buď veľmi opatrný/á, akýkoľvek úkon vykonaný v tomto priestore nieje možné vziať späť." + +msgid "Delete your account" +msgstr "Vymaž svoj účet" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "Prepáč, ale ako jej správca, ty nemôžeš opustiť svoju vlastnú instanciu." + +msgid "Latest articles" +msgstr "Najnovšie články" + +msgid "Atom feed" +msgstr "Atom zdroj" + +msgid "Recently boosted" +msgstr "Nedávno vyzdvihnuté" + +msgid "Articles tagged \"{0}\"" +msgstr "Články otagované pod \"{0}\"" + +msgid "There are currently no articles with such a tag" +msgstr "Momentálne tu niesú žiadné články pod takýmto tagom" + +msgid "The content you sent can't be processed." +msgstr "Obsah, ktorý si odoslal/a nemožno spracovať." + +msgid "Maybe it was too long." +msgstr "Možno to bolo príliš dlhé." + +msgid "Internal server error" +msgstr "Vnútorná chyba v rámci serveru" + +msgid "Something broke on our side." +msgstr "Niečo sa pokazilo na našej strane." + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "Prepáč ohľadom toho. Ak si myslíš, že ide o chybu, prosím nahlás ju." + +msgid "Invalid CSRF token" +msgstr "Neplatný CSRF token" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "Niečo nieje v poriadku s tvojím CSRF tokenom. Uisti sa, že máš vo svojom prehliadači povolené cookies, potom skús načítať stránku znovu. Ak budeš aj naďalej vidieť túto chybovú správu, prosím nahlás ju." + +msgid "You are not authorized." +msgstr "Nemáš oprávnenie." + +msgid "Page not found" +msgstr "Stránka nenájdená" + +msgid "We couldn't find this page." +msgstr "Tú stránku sa nepodarilo nájsť." + +msgid "The link that led you here may be broken." +msgstr "Odkaz, ktorý ťa sem zaviedol je azda narušený." + +msgid "Users" +msgstr "Užívatelia" + +msgid "Configuration" +msgstr "Nastavenia" + +msgid "Instances" +msgstr "Instancie" + +msgid "Email blocklist" +msgstr "Blokované emaily" + +msgid "Grant admin rights" +msgstr "Prideľ administrátorské práva" + +msgid "Revoke admin rights" +msgstr "Odober administrátorské práva" + +msgid "Grant moderator rights" +msgstr "Prideľ moderovacie práva" + +msgid "Revoke moderator rights" +msgstr "Odober moderovacie práva" + +msgid "Ban" +msgstr "Zakáž" + +msgid "Run on selected users" +msgstr "Vykonaj vybraným užívateľom" + +msgid "Moderator" +msgstr "Správca" + +msgid "Moderation" +msgstr "Moderovanie" + +msgid "Home" +msgstr "Domov" + +msgid "Administration of {0}" +msgstr "Spravovanie {0}" + +msgid "Unblock" +msgstr "Odblokuj" + +msgid "Block" +msgstr "Blokuj" + +msgid "Name" +msgstr "Pomenovanie" + +msgid "Allow anyone to register here" +msgstr "Umožni komukoľvek sa tu zaregistrovať" + +msgid "Short description" +msgstr "Stručný popis" + +msgid "Markdown syntax is supported" +msgstr "Markdown syntaxia je podporovaná" + +msgid "Long description" +msgstr "Podrobný popis" + +msgid "Default article license" +msgstr "Predvolená licencia článkov" + +msgid "Save these settings" +msgstr "Ulož tieto nastavenia" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "Pokiaľ si túto stránku prezeráš ako návštevník, niesú o tebe zaznamenávané žiadne dáta." + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "Ako registrovaný užívateľ musíš poskytnúť svoje užívateľské meno (čo zároveň nemusí byť tvoje skutočné meno), tvoju fungujúcu emailovú adresu a helso, aby sa ti bolo možné prihlásiť, písať články a komentovať. Obsah, ktorý zverejníš je uložený len pokiaľ ho nevymažeš." + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "Keď sa prihlásiš, ukladáme dve cookies, jedno aby tvoja sezóna mohla ostať otvorená, druhé je na zabránenie iným ľudom, aby konali za teba. Žiadne iné cookies neukladáme." + +msgid "Blocklisted Emails" +msgstr "Blokované emaily" + +msgid "Email address" +msgstr "Emailová adresa" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "Poznámka" + +msgid "Notify the user?" +msgstr "Oboznámiť používateľa?" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "Oboznámenie o blokovaní" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "Pridaj blokovanú adresu" + +msgid "There are no blocked emails on your instance" +msgstr "Na tvojej instancii niesú žiadné emailové adresy zakázané" + +msgid "Delete selected emails" +msgstr "Vymaž vybrané emaily" + +msgid "Email address:" +msgstr "Emailová adresa:" + +msgid "Blocklisted for:" +msgstr "Zablokovaná kvôli:" + +msgid "Will notify them on account creation with this message:" +msgstr "Pri vytvorení účtu budú užívatelia oboznámení touto správou:" + +msgid "The user will be silently prevented from making an account" +msgstr "Užívateľovi bude v tichosti znemožnené vytvorenie účtu" + +msgid "Welcome to {}" +msgstr "Vitaj na {}" + +msgid "View all" +msgstr "Ukázať všetky" + +msgid "About {0}" +msgstr "O {0}" + +msgid "Runs Plume {0}" +msgstr "Beží na Plume {0}" + +msgid "Home to {0} people" +msgstr "Domov pre {0} ľudí" + +msgid "Who wrote {0} articles" +msgstr "Ktorí napísal/i {0} článkov" + +msgid "And are connected to {0} other instances" +msgstr "A sú pripojení k {0} ďalším instanciám" + +msgid "Administred by" +msgstr "Správcom je" + +msgid "Interact with {}" +msgstr "Narábaj s {}" + +msgid "Log in to interact" +msgstr "Pre zapojenie sa prihlás" + +msgid "Enter your full username to interact" +msgstr "Zadaj svoju prezývku v úplnosti, aby si sa zapojil/a" + +msgid "Publish" +msgstr "Zverejni" + +msgid "Classic editor (any changes will be lost)" +msgstr "Klasický editor (akékoľvek zmeny budú stratené)" + +msgid "Title" +msgstr "Nadpis" + +msgid "Subtitle" +msgstr "Podnadpis" + +msgid "Content" +msgstr "Obsah" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "Do svojej galérie môžeš nahrávať multimédiá, a potom skopírovať ich Markdown syntaxiu do tvojích článkov, aby si ich vložil/a." + +msgid "Upload media" +msgstr "Nahraj multimédiá" + +msgid "Tags, separated by commas" +msgstr "Štítky, oddelené čiarkami" + +msgid "License" +msgstr "Licencia" + +msgid "Illustration" +msgstr "Ilustrácia" + +msgid "This is a draft, don't publish it yet." +msgstr "Toto je zatiaľ iba koncept, ešte ho nezverejňovať." + +msgid "Update" +msgstr "Dopĺň" + +msgid "Update, or publish" +msgstr "Dopĺň, alebo zverejni" + +msgid "Publish your post" +msgstr "Zverejni svoj príspevok" + +msgid "Written by {0}" +msgstr "Napísal/a {0}" + +msgid "All rights reserved." +msgstr "Všetky práva vyhradné." + +msgid "This article is under the {0} license." +msgstr "Tento článok je uverejnený pod licenciou {0}." + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "Jeden obľúbil" +msgstr[1] "{0} obľúbilo" +msgstr[2] "{0} obľúbili" +msgstr[3] "{0} obľúbili" + +msgid "I don't like this anymore" +msgstr "Už sa mi to nepáči" + +msgid "Add yours" +msgstr "Pridaj tvoje" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "Jedno vyzdvihnutie" +msgstr[1] "{0} vyzdvihnutí" +msgstr[2] "{0} vyzdvihnutí" +msgstr[3] "{0} vyzdvihnutia" + +msgid "I don't want to boost this anymore" +msgstr "Už to viac nechcem vyzdvihovať" + +msgid "Boost" +msgstr "Vyzdvihni" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "{0}Prihlás sa{1}, alebo {2}použi svoj účet v rámci Fediversa{3} pre narábanie s týmto článkom" + +msgid "Comments" +msgstr "Komentáre" + +msgid "Your comment" +msgstr "Tvoj komentár" + +msgid "Submit comment" +msgstr "Pošli komentár" + +msgid "No comments yet. Be the first to react!" +msgstr "Zatiaľ žiadne komentáre. Buď prvý kto zareaguje!" + +msgid "Are you sure?" +msgstr "Ste si istý/á?" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "Tento článok je ešte len konceptom. Vidieť ho môžeš iba ty, a ostatní jeho autori." + +msgid "Only you and other authors can edit this article." +msgstr "Iba ty, a ostatní autori môžu upravovať tento článok." + +msgid "Edit" +msgstr "Uprav" + +msgid "I'm from this instance" +msgstr "Som z tejto instancie" + +msgid "Username, or email" +msgstr "Užívateľské meno, alebo email" + +msgid "Log in" +msgstr "Prihlás sa" + +msgid "I'm from another instance" +msgstr "Som z inej instancie" + +msgid "Continue to your instance" +msgstr "Pokračuj na tvoju instanciu" + +msgid "Reset your password" +msgstr "Obnov svoje heslo" + +msgid "New password" +msgstr "Nové heslo" + +msgid "Confirmation" +msgstr "Potvrdenie" + +msgid "Update password" +msgstr "Aktualizovať heslo" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "Email s odkazom na obnovenie hesla bol odoslaný na adresu, ktorú si nám dal/a." + +msgid "Send password reset link" +msgstr "Pošli odkaz na obnovu hesla" + +msgid "This token has expired" +msgstr "Toto token oprávnenie vypršalo" + +msgid "Please start the process again by clicking here." +msgstr "Prosím začni odznovu, kliknutím sem." + +msgid "New Blog" +msgstr "Nový blog" + +msgid "Create a blog" +msgstr "Vytvor blog" + +msgid "Create blog" +msgstr "Vytvor blog" + +msgid "Edit \"{}\"" +msgstr "Uprav \"{}\"" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "Do svojej galérie môžeš nahrávať obrázky, ktoré sa potom dajú použiť aj ako ikonky, či záhlavie pre blogy." + +msgid "Upload images" +msgstr "Nahraj obrázky" + +msgid "Blog icon" +msgstr "Ikonka blogu" + +msgid "Blog banner" +msgstr "Banner blogu" + +msgid "Custom theme" +msgstr "Vlastný vzhľad" + +msgid "Update blog" +msgstr "Aktualizuj blog" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "Buď veľmi opatrný/á, akýkoľvek úkon vykonaný v tomto priestore nieje možné vziať späť." + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "Si si istý/á, že chceš natrvalo zmazať tento blog?" + +msgid "Permanently delete this blog" +msgstr "Vymaž tento blog natrvalo" + +msgid "{}'s icon" +msgstr "Ikonka pre {}" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "Tento blog má jedného autora: " +msgstr[1] "Tento blog má {0} autorov: " +msgstr[2] "Tento blog má {0} autorov: " +msgstr[3] "Tento blog má {0} autorov: " + +msgid "No posts to see here yet." +msgstr "Ešte tu nemožno vidieť žiadné príspevky." + +msgid "Nothing to see here yet." +msgstr "Ešte tu nieje nič k videniu." + +msgid "None" +msgstr "Žiadne" + +msgid "No description" +msgstr "Žiaden popis" + +msgid "Respond" +msgstr "Odpovedz" + +msgid "Delete this comment" +msgstr "Vymaž tento komentár" + +msgid "What is Plume?" +msgstr "Čo je to Plume?" + +msgid "Plume is a decentralized blogging engine." +msgstr "Plume je decentralizovaná blogovacia platforma." + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "Autori môžu spravovať viacero blogov, každý ako osobitnú stránku." + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "Články sú tiež viditeľné na iných Plume instanciách a môžeš s nimi interaktovať priamo aj z iných federovaných platforiem, ako napríklad Mastodon." + +msgid "Read the detailed rules" +msgstr "Prečítaj si podrobné pravidlá" + +msgid "By {0}" +msgstr "Od {0}" + +msgid "Draft" +msgstr "Koncept" + +msgid "Search result(s) for \"{0}\"" +msgstr "Výsledky hľadania pre \"{0}\"" + +msgid "Search result(s)" +msgstr "Výsledky hľadania" + +msgid "No results for your query" +msgstr "Žiadne výsledky pre tvoje zadanie" + +msgid "No more results for your query" +msgstr "Žiadne ďalšie výsledky pre tvoje zadanie" + +msgid "Advanced search" +msgstr "Pokročilé vyhľadávanie" + +msgid "Article title matching these words" +msgstr "Nadpis článku vyhovujúci týmto slovám" + +msgid "Subtitle matching these words" +msgstr "Podnadpis zhodujúci sa s týmito slovami" + +msgid "Content macthing these words" +msgstr "Obsah zodpovedajúci týmto slovám" + +msgid "Body content" +msgstr "Obsah článku" + +msgid "From this date" +msgstr "Od tohto dátumu" + +msgid "To this date" +msgstr "Do tohto dátumu" + +msgid "Containing these tags" +msgstr "Obsahuje tieto štítky" + +msgid "Tags" +msgstr "Štítky" + +msgid "Posted on one of these instances" +msgstr "Uverejnené na jednej z týchto instancií" + +msgid "Instance domain" +msgstr "Doména instancie" + +msgid "Posted by one of these authors" +msgstr "Uverejnené jedným z týchto autorov" + +msgid "Author(s)" +msgstr "Autori" + +msgid "Posted on one of these blogs" +msgstr "Uverejnené na jednom z týchto blogov" + +msgid "Blog title" +msgstr "Titulok blogu" + +msgid "Written in this language" +msgstr "Písané v tomto jazyku" + +msgid "Language" +msgstr "Jazyk" + +msgid "Published under this license" +msgstr "Uverejnené pod touto licenciou" + +msgid "Article license" +msgstr "Článok je pod licenciou" + diff --git a/po/plume/sl.po b/po/plume/sl.po new file mode 100644 index 00000000000..8f1fe86a18b --- /dev/null +++ b/po/plume/sl.po @@ -0,0 +1,1040 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Slovenian\n" +"Language: sl_SI\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: sl\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} komentiral vaš članek." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "" + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "" + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "" + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "" + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "" + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "" + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "" + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "" + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "" + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "" + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "" + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "" + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "" + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "" + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "" + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "" + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "" + +# src/routes/medias.rs:180 +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 "" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "Ta članek še ni objavljen." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "Nisi avtor tega spletnika." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "Nov članek" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "Uredi {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "" + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "" + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "" + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "" + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "" + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "Zdaj sta povezana." + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "Ponastavitev gesla" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "" + +msgid "Create an account" +msgstr "" + +msgid "Email" +msgstr "E-pošta" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Your media" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "Meni" + +msgid "Search" +msgstr "Najdi" + +msgid "Dashboard" +msgstr "Nadzorna plošča" + +msgid "Notifications" +msgstr "Obvestila" + +msgid "Log Out" +msgstr "Odjava" + +msgid "My account" +msgstr "Moj račun" + +msgid "Log In" +msgstr "Prijavi se" + +msgid "Register" +msgstr "Registriraj" + +msgid "About this instance" +msgstr "" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "Administracija" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "Izvorna koda" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "" + +msgid "It is you" +msgstr "" + +msgid "Edit your profile" +msgstr "" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "" + +msgid "Follow {}" +msgstr "" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "" + +msgid "Subscribers" +msgstr "" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "" + +msgid "Your Drafts" +msgstr "" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "" + +msgid "Your Profile" +msgstr "" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "Povzetek" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "Posodobi stranko" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "Najnovejši članki" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "Uporabniki" + +msgid "Configuration" +msgstr "Nastavitve" + +msgid "Instances" +msgstr "" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "Prepoved" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "" + +msgid "Unblock" +msgstr "Odblokiraj" + +msgid "Block" +msgstr "Blokiraj" + +msgid "Name" +msgstr "" + +msgid "Allow anyone to register here" +msgstr "" + +msgid "Short description" +msgstr "Kratek opis" + +msgid "Markdown syntax is supported" +msgstr "" + +msgid "Long description" +msgstr "" + +msgid "Default article license" +msgstr "" + +msgid "Save these settings" +msgstr "" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "Dobrodošli na {}" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "O {0}" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +msgid "Administred by" +msgstr "" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +msgid "Tags, separated by commas" +msgstr "" + +msgid "License" +msgstr "" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +msgid "I don't like this anymore" +msgstr "" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "" + +msgid "Comments" +msgstr "" + +msgid "Your comment" +msgstr "" + +msgid "Submit comment" +msgstr "" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "" + +msgid "Log in" +msgstr "" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +msgid "New password" +msgstr "" + +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "" + +msgid "Create a blog" +msgstr "" + +msgid "Create blog" +msgstr "" + +msgid "Edit \"{}\"" +msgstr "" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +msgid "From this date" +msgstr "" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" + diff --git a/po/plume/sr.po b/po/plume/sr.po new file mode 100644 index 00000000000..cbb71bb06ae --- /dev/null +++ b/po/plume/sr.po @@ -0,0 +1,1037 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Serbian (Latin)\n" +"Language: sr_CS\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: sr-CS\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} komentarisala tvoj članak." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "" + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "" + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} vas je spomenuo/la." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "" + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "" + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "" + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "" + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "" + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "" + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "" + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "" + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "" + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "" + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "" + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "" + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "" + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "" + +# src/routes/medias.rs:180 +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 "" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "" + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "" + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "Uredi {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "" + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "" + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "" + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "" + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "" + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "" + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "Poništavanje lozinke" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "" + +msgid "Create an account" +msgstr "" + +msgid "Email" +msgstr "" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Your media" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "" + +msgid "Menu" +msgstr "Meni" + +msgid "Search" +msgstr "" + +msgid "Dashboard" +msgstr "" + +msgid "Notifications" +msgstr "Obaveštenja" + +msgid "Log Out" +msgstr "Odjavi se" + +msgid "My account" +msgstr "Moj nalog" + +msgid "Log In" +msgstr "Prijaviti se" + +msgid "Register" +msgstr "Registracija" + +msgid "About this instance" +msgstr "" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "Administracija" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "Izvorni kod" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "" + +msgid "It is you" +msgstr "" + +msgid "Edit your profile" +msgstr "" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "" + +msgid "Follow {}" +msgstr "" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "" + +msgid "Subscribers" +msgstr "" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "" + +msgid "Your Drafts" +msgstr "" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "" + +msgid "Your Profile" +msgstr "" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "Najnoviji članci" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "Korisnici" + +msgid "Configuration" +msgstr "" + +msgid "Instances" +msgstr "" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "" + +msgid "Unblock" +msgstr "Odblokirajte" + +msgid "Block" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Allow anyone to register here" +msgstr "" + +msgid "Short description" +msgstr "" + +msgid "Markdown syntax is supported" +msgstr "" + +msgid "Long description" +msgstr "" + +msgid "Default article license" +msgstr "" + +msgid "Save these settings" +msgstr "" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "Dobrodošli u {0}" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +msgid "Administred by" +msgstr "" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +msgid "Tags, separated by commas" +msgstr "" + +msgid "License" +msgstr "" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +msgid "I don't like this anymore" +msgstr "" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "" + +msgid "Comments" +msgstr "" + +msgid "Your comment" +msgstr "" + +msgid "Submit comment" +msgstr "" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "" + +msgid "Log in" +msgstr "" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +msgid "New password" +msgstr "" + +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "" + +msgid "Create a blog" +msgstr "" + +msgid "Create blog" +msgstr "" + +msgid "Edit \"{}\"" +msgstr "" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +msgid "From this date" +msgstr "" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" + diff --git a/po/plume/sv.po b/po/plume/sv.po new file mode 100644 index 00000000000..ebb40e2f560 --- /dev/null +++ b/po/plume/sv.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Swedish\n" +"Language: sv_SE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: sv-SE\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} kommenterade på din artikel." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} abbonerar på dig." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} gillade din artikel." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} nämnde dig." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} boostade din artikel." + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "{0}s avatar" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "För att skapa en ny blogg måste du vara inloggad" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "" + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "" + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "Du har inte tillstånd att ta bort den här bloggen." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "Du har inte tillstånd att redigera den här bloggen." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "" + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "" + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "" + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "" + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "" + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "" + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "" + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "" + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "" + +# src/routes/medias.rs:180 +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 "" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "" + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "" + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "" + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "" + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "" + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "" + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "" + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "" + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "" + +msgid "Create an account" +msgstr "" + +msgid "Email" +msgstr "" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "Användarnamn" + +msgid "Password" +msgstr "Lösenord" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "Fil" + +msgid "Send" +msgstr "Skicka" + +msgid "Your media" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "Radera" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "" + +msgid "Menu" +msgstr "Meny" + +msgid "Search" +msgstr "Sök" + +msgid "Dashboard" +msgstr "" + +msgid "Notifications" +msgstr "" + +msgid "Log Out" +msgstr "Logga ut" + +msgid "My account" +msgstr "" + +msgid "Log In" +msgstr "Logga in" + +msgid "Register" +msgstr "" + +msgid "About this instance" +msgstr "" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "" + +msgid "It is you" +msgstr "" + +msgid "Edit your profile" +msgstr "" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "Prenumerera" + +msgid "Follow {}" +msgstr "Följ {}" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "" + +msgid "Subscribers" +msgstr "Prenumeranter" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "" + +msgid "Your Drafts" +msgstr "" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "" + +msgid "Your Profile" +msgstr "" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "" + +msgid "Configuration" +msgstr "" + +msgid "Instances" +msgstr "" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "" + +msgid "Unblock" +msgstr "" + +msgid "Block" +msgstr "" + +msgid "Name" +msgstr "Namn" + +msgid "Allow anyone to register here" +msgstr "" + +msgid "Short description" +msgstr "" + +msgid "Markdown syntax is supported" +msgstr "" + +msgid "Long description" +msgstr "" + +msgid "Default article license" +msgstr "" + +msgid "Save these settings" +msgstr "" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "Välkommen till {}" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "Om {0}" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +msgid "Administred by" +msgstr "" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "Publicera" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "Titel" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +msgid "Tags, separated by commas" +msgstr "" + +msgid "License" +msgstr "Licens" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "" +msgstr[1] "" + +msgid "I don't like this anymore" +msgstr "" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" +msgstr[1] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "" + +msgid "Comments" +msgstr "Kommentarer" + +msgid "Your comment" +msgstr "" + +msgid "Submit comment" +msgstr "" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "Är du säker?" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "Redigera" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "" + +msgid "Log in" +msgstr "Logga in" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +msgid "New password" +msgstr "" + +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "" + +msgid "Create a blog" +msgstr "" + +msgid "Create blog" +msgstr "" + +msgid "Edit \"{}\"" +msgstr "Redigera \"{}\"" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" +msgstr[1] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "Vad är Plume?" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "Av {0}" + +msgid "Draft" +msgstr "Utkast" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +msgid "From this date" +msgstr "" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "Taggar" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" + diff --git a/po/plume/tr.po b/po/plume/tr.po new file mode 100644 index 00000000000..4124cf037f7 --- /dev/null +++ b/po/plume/tr.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Turkish\n" +"Language: tr_TR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: tr\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} makalene yorum yaptı." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} sana abone oldu." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} makaleni beğendi." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} senden bahsetti." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} makaleni destekledi." + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "Özet akışınız" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "Yerel özet akışı" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "Birleşik özet akışı" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "{0} adlı kişinin avatarı" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "İsteğe bağlı" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "Yeni bir günlük oluşturmak için, giriş yapman lazım" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "Aynı isme sahip bir günlük zaten var." + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "Günlüğün başarıyla oluşturuldu!" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "Günlüğün silindi." + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "Bu günlüğü silmeye iznin yok." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "Bu günlüğü düzenlemeye iznin yok." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "Bu dosyayı günlük simgen olarak kullanamazsın." + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "Bu dosyayı günlük kapak fotoğrafın olarak kullanamazsın." + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "Günlüğün hakkındaki bilgiler güncellendi." + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "Yorumun yayınlandı." + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "Yorumun silindi." + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "Bu örnekte kayıtlar kapalıdır." + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "Hesabınız oluşturuldu. Şimdi kullanabilmeniz için giriş yapmanız yeterlidir." + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "Oluşumunun ayarları kaydedildi." + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "Bir yazıyı beğenmek için, giriş yapman lazım" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "Dosyan silindi." + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "Bu dosyayı silme iznin yok." + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "Avatarın güncellendi." + +# src/routes/medias.rs:180 +msgid "You are not allowed to use this media." +msgstr "Bu dosyayı kullanma iznin yok." + +# src/routes/notifications.rs:29 +msgid "To see your notifications, you need to be logged in" +msgstr "Bildirimlerini görmek için, giriş yapman lazım" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "Bu gönderi henüz yayınlanmamış." + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "Yeni bir yazı yazmak için, giriş yapman lazım" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "Bu günlüğün sahibi değilsin." + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "Yeni gönderi" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "{0} günlüğünü düzenle" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "Bu günlükte yayınlamak için iznin yok." + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "Makalen güncellendi." + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "Makalen kaydedildi." + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "Yeni makale" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "Bu makaleyi silmek için iznin yok." + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "Makalen silindi." + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "Görünüşe göre silmek istediğin makale mevcut değil. Belki de zaten silinmiştir?" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "Hesabın hakkında yeterli bilgi edinemedik. Lütfen kullanıcı adını doğru yazdığından emin ol." + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "Bir yazıyı yeniden paylaşmak için, giriş yapman lazım" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "Artık bağlısın." + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "Şimdi çıkış yaptınız." + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "Şifre Sıfırlama" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "İşte şifreni sıfırlamak için kullabileceğin bağlantı: {0}" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "Şifren başarıyla sıfırlandı." + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "Yönetim panelinize erişmek için giriş yapmanız gerekmektedir" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "Artık {} kullanıcısını takip etmiyorsunuz." + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "Artık {} kullanıcısını takip ediyorsunuz." + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "Birisine abone olmak için giriş yapmanız gerekmektedir" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "Profilinizi düzenlemek için giriş yapmanız gerekmektedir" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "Profiliniz güncellendi." + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "Hesabınız silindi." + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "Başka birisinin hesabını silemezsiniz." + +msgid "Create your account" +msgstr "Hesabınızı oluşturun" + +msgid "Create an account" +msgstr "Bir hesap oluştur" + +msgid "Email" +msgstr "E-posta" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "Özür dileriz, ancak bu örnek kayıt olmaya kapalıdır. Ama farklı bir tane bulabilirsiniz." + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "Gelen kutunuzu kontrol edin!" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "Kullanıcı adı" + +msgid "Password" +msgstr "Parola" + +msgid "Password confirmation" +msgstr "Parola doğrulama" + +msgid "Media upload" +msgstr "Medya karşıya yükleme" + +msgid "Description" +msgstr "Açıklama" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "Görme engelli kişiler ve lisanslama bilgileri için kullanışlıdır" + +msgid "Content warning" +msgstr "İçerik uyarısı" + +msgid "Leave it empty, if none is needed" +msgstr "Gerekmiyorsa boş bırakın" + +msgid "File" +msgstr "Dosya" + +msgid "Send" +msgstr "Gönder" + +msgid "Your media" +msgstr "Medyanız" + +msgid "Upload" +msgstr "Karşıya yükle" + +msgid "You don't have any media yet." +msgstr "Henüz medyanız yok." + +msgid "Content warning: {0}" +msgstr "İçerik uyarısı: {0}" + +msgid "Delete" +msgstr "Sil" + +msgid "Details" +msgstr "Detaylar" + +msgid "Media details" +msgstr "Medya detayları" + +msgid "Go back to the gallery" +msgstr "Galeriye geri dön" + +msgid "Markdown syntax" +msgstr "Markdown sözdizimi" + +msgid "Copy it into your articles, to insert this media:" +msgstr "Bu medya ögesini makalelerine eklemek için, bunu kopyala:" + +msgid "Use as an avatar" +msgstr "Avatar olarak kullan" + +msgid "Plume" +msgstr "Plume" + +msgid "Menu" +msgstr "Menü" + +msgid "Search" +msgstr "Ara" + +msgid "Dashboard" +msgstr "Yönetim paneli" + +msgid "Notifications" +msgstr "Bildirimler" + +msgid "Log Out" +msgstr "Çıkış Yap" + +msgid "My account" +msgstr "Hesabım" + +msgid "Log In" +msgstr "Giriş Yap" + +msgid "Register" +msgstr "Kayıt Ol" + +msgid "About this instance" +msgstr "Bu örnek hakkında" + +msgid "Privacy policy" +msgstr "Gizlilik politikası" + +msgid "Administration" +msgstr "Yönetim" + +msgid "Documentation" +msgstr "Dokümantasyon" + +msgid "Source code" +msgstr "Kaynak kodu" + +msgid "Matrix room" +msgstr "Matrix odası" + +msgid "Admin" +msgstr "Yönetici" + +msgid "It is you" +msgstr "Bu sizsiniz" + +msgid "Edit your profile" +msgstr "Profilinizi düzenleyin" + +msgid "Open on {0}" +msgstr "{0}'da Aç" + +msgid "Unsubscribe" +msgstr "Abonelikten Çık" + +msgid "Subscribe" +msgstr "Abone Ol" + +msgid "Follow {}" +msgstr "Takip et: {}" + +msgid "Log in to follow" +msgstr "Takip etmek için giriş yapın" + +msgid "Enter your full username handle to follow" +msgstr "Takip etmek için kullanıcı adınızın tamamını girin" + +msgid "{0}'s subscribers" +msgstr "{0}'in aboneleri" + +msgid "Articles" +msgstr "Makaleler" + +msgid "Subscribers" +msgstr "Aboneler" + +msgid "Subscriptions" +msgstr "Abonelikler" + +msgid "{0}'s subscriptions" +msgstr "{0}'in abonelikleri" + +msgid "Your Dashboard" +msgstr "Yönetim Panelin" + +msgid "Your Blogs" +msgstr "Günlüklerin" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "Henüz hiç günlüğün yok :( Bir tane yarat veya birine katılmak için izin al." + +msgid "Start a new blog" +msgstr "Yeni bir günlük başlat" + +msgid "Your Drafts" +msgstr "Taslakların" + +msgid "Go to your gallery" +msgstr "Galerine git" + +msgid "Edit your account" +msgstr "Hesabınızı düzenleyin" + +msgid "Your Profile" +msgstr "Profiliniz" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "Avatarınızı değiştirmek için galerinize yükleyin ve ardından oradan seçin." + +msgid "Upload an avatar" +msgstr "Bir avatar yükle" + +msgid "Display name" +msgstr "Görünen isim" + +msgid "Summary" +msgstr "Özet" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "Hesap güncelleme" + +msgid "Danger zone" +msgstr "Tehlikeli bölge" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "Çok dikkatli ol, burada yapacağın herhangi bir işlem iptal edilemez." + +msgid "Delete your account" +msgstr "Hesabını Sil" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "Üzgünüm ama bir yönetici olarak kendi oluşumundan çıkamazsın." + +msgid "Latest articles" +msgstr "Son makaleler" + +msgid "Atom feed" +msgstr "Atom haber akışı" + +msgid "Recently boosted" +msgstr "Yakın zamanda desteklenler" + +msgid "Articles tagged \"{0}\"" +msgstr "\"{0}\" etiketine sahip makaleler" + +msgid "There are currently no articles with such a tag" +msgstr "Öyle bir etikete sahip bir makale henüz yok" + +msgid "The content you sent can't be processed." +msgstr "Gönderdiğiniz içerik işlenemiyor." + +msgid "Maybe it was too long." +msgstr "Belki çok uzundu." + +msgid "Internal server error" +msgstr "İç sunucu hatası" + +msgid "Something broke on our side." +msgstr "Bizim tarafımızda bir şeyler bozuldu." + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "Bunun için üzgünüz. Bunun bir hata olduğunu düşünüyorsanız, lütfen bildirin." + +msgid "Invalid CSRF token" +msgstr "Geçersiz CSRF belirteci" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "CSRF belirtecinizle ilgili bir sorun var. Tarayıcınızda çerezlerin etkinleştirildiğinden emin olun ve bu sayfayı yeniden yüklemeyi deneyin. Bu hata mesajını görmeye devam ederseniz, lütfen bildirin." + +msgid "You are not authorized." +msgstr "Yetkiniz yok." + +msgid "Page not found" +msgstr "Sayfa bulunamadı" + +msgid "We couldn't find this page." +msgstr "Bu sayfayı bulamadık." + +msgid "The link that led you here may be broken." +msgstr "Seni buraya getiren bağlantı bozuk olabilir." + +msgid "Users" +msgstr "Kullanıcılar" + +msgid "Configuration" +msgstr "Yapılandırma" + +msgid "Instances" +msgstr "Örnekler" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "Yasakla" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "{0} yönetimi" + +msgid "Unblock" +msgstr "Engellemeyi kaldır" + +msgid "Block" +msgstr "Engelle" + +msgid "Name" +msgstr "İsim" + +msgid "Allow anyone to register here" +msgstr "Herkesin buraya kaydolmasına izin ver" + +msgid "Short description" +msgstr "Kısa açıklama" + +msgid "Markdown syntax is supported" +msgstr "Markdown kullanabilirsin" + +msgid "Long description" +msgstr "Uzun açıklama" + +msgid "Default article license" +msgstr "Varsayılan makale lisansı" + +msgid "Save these settings" +msgstr "Bu ayarları kaydet" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "Bu siteye ziyaretçi olarak göz atıyorsanız, hakkınızda veri toplanmamaktadır." + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "Kayıtlı bir kullanıcı olarak, giriş yapmak, makale yazmak ve yorum yapmak için bir kullanıcı adına (ki bu kullanıcı adının senin gerçek adın olması gerekmiyor), kullanabildiğin bir e-posta adresine ve bir şifreye ihtiyacın var. Girdiğin bilgiler sen hesabını silene dek depolanır." + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "Giriş yaptığınızda; biri oturumunuzu açık tutmak için, ikincisi başkalarının sizin adınıza hareket etmesini önlemek için iki çerez saklamaktayız. Başka çerez saklamıyoruz." + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "{}'e hoş geldiniz" + +msgid "View all" +msgstr "Hepsini görüntüle" + +msgid "About {0}" +msgstr "{0} hakkında" + +msgid "Runs Plume {0}" +msgstr "Plume {0} kullanır" + +msgid "Home to {0} people" +msgstr "{0} kişinin evi" + +msgid "Who wrote {0} articles" +msgstr "Makale yazan {0} yazar" + +msgid "And are connected to {0} other instances" +msgstr "Ve diğer {0} örneğe bağlı" + +msgid "Administred by" +msgstr "Yönetenler" + +msgid "Interact with {}" +msgstr "{} ile etkileşime geç" + +msgid "Log in to interact" +msgstr "Etkileşime geçmek için giriş yap" + +msgid "Enter your full username to interact" +msgstr "Etkileşim için tam kullanıcı adınızı girin" + +msgid "Publish" +msgstr "Yayınla" + +msgid "Classic editor (any changes will be lost)" +msgstr "Klasik editör (bir değişiklik yaparsan kaydedilmeyecek)" + +msgid "Title" +msgstr "Başlık" + +msgid "Subtitle" +msgstr "Altyazı" + +msgid "Content" +msgstr "İçerik" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "Galerine bir resim ekleyebilir, daha sonra makalene eklemek için resmin Markdown kodunu kopyalayabilirsin." + +msgid "Upload media" +msgstr "Medyayı karşıya yükle" + +msgid "Tags, separated by commas" +msgstr "Etiketler, virgül ile ayrılmış" + +msgid "License" +msgstr "Lisans" + +msgid "Illustration" +msgstr "İllüstrasyon" + +msgid "This is a draft, don't publish it yet." +msgstr "Bu bir taslak, henüz yayınlama." + +msgid "Update" +msgstr "Güncelle" + +msgid "Update, or publish" +msgstr "Güncelle, ya da yayınla" + +msgid "Publish your post" +msgstr "Gönderinizi yayınlayın" + +msgid "Written by {0}" +msgstr "{0} tarafından yazıldı" + +msgid "All rights reserved." +msgstr "Tüm hakları saklıdır." + +msgid "This article is under the {0} license." +msgstr "Bu makale {0} lisansı altındadır." + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "Bir beğeni" +msgstr[1] "{0} beğeni" + +msgid "I don't like this anymore" +msgstr "Artık bundan hoşlanmıyorum" + +msgid "Add yours" +msgstr "Siz de beğenin" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "Bir destek" +msgstr[1] "{0} destek" + +msgid "I don't want to boost this anymore" +msgstr "Bunu artık desteklemek istemiyorum" + +msgid "Boost" +msgstr "Destekle" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "{0}Giriş yaparak{1}, ya da {2}Fediverse hesabını kullanarak{3} bu makaleyle iletişime geç" + +msgid "Comments" +msgstr "Yorumlar" + +msgid "Your comment" +msgstr "Yorumunuz" + +msgid "Submit comment" +msgstr "Yorum gönder" + +msgid "No comments yet. Be the first to react!" +msgstr "Henüz yorum yok. İlk tepki veren siz olun!" + +msgid "Are you sure?" +msgstr "Emin misiniz?" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "Bu makale hâlâ bir taslak. Yalnızca sen ve diğer yazarlar bunu görebilir." + +msgid "Only you and other authors can edit this article." +msgstr "Sadece sen ve diğer yazarlar bu makaleyi düzenleyebilir." + +msgid "Edit" +msgstr "Düzenle" + +msgid "I'm from this instance" +msgstr "Ben bu oluşumdanım!" + +msgid "Username, or email" +msgstr "Kullanıcı adı veya e-posta" + +msgid "Log in" +msgstr "Oturum aç" + +msgid "I'm from another instance" +msgstr "Başka bir oluşumdanım ben!" + +msgid "Continue to your instance" +msgstr "Oluşumuna devam et" + +msgid "Reset your password" +msgstr "Parolanızı sıfırlayın" + +msgid "New password" +msgstr "Yeni parola" + +msgid "Confirmation" +msgstr "Doğrulama" + +msgid "Update password" +msgstr "Parolayı güncelle" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "Bize verdiğiniz adrese, parola sıfırlama linki içeren bir e-posta gönderdik." + +msgid "Send password reset link" +msgstr "Parola sıfırlama linki gönder" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "Yeni Günlük" + +msgid "Create a blog" +msgstr "Bir günlük oluştur" + +msgid "Create blog" +msgstr "Günlük oluştur" + +msgid "Edit \"{}\"" +msgstr "Düzenle: \"{}\"" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "Günlük simgeleri veya manşeti olarak kullanmak için resimleri galerinize yükleyebilirsiniz." + +msgid "Upload images" +msgstr "Resimleri karşıya yükle" + +msgid "Blog icon" +msgstr "Günlük simgesi" + +msgid "Blog banner" +msgstr "Günlük manşeti" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "Günlüğü güncelle" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "Çok dikkatli ol, burada yapacağın herhangi bir işlem geri alınamaz." + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "Bu günlüğü kalıcı olarak sil" + +msgid "{}'s icon" +msgstr "{}'in simgesi" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "Bu günlükte bir tane yazar bulunmaktadır: " +msgstr[1] "Bu günlükte {0} tane yazar bulunmaktadır: " + +msgid "No posts to see here yet." +msgstr "Burada henüz görülecek gönderi yok." + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "Resim ayarlanmamış" + +msgid "No description" +msgstr "Açıklama yok" + +msgid "Respond" +msgstr "Yanıtla" + +msgid "Delete this comment" +msgstr "Bu yorumu sil" + +msgid "What is Plume?" +msgstr "Plume Nedir?" + +msgid "Plume is a decentralized blogging engine." +msgstr "Plume merkezi olmayan bir internet günlüğü yazma motorudur." + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "Yazarlar her biri farklı sitedeki birden çok günlüğü yönetebilirler." + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "Makaleler ayrıca diğer Plume oluşumlarında da görünür. Mastodon gibi platformlardan yazılarla doğrudan etkileşime geçebilirsin." + +msgid "Read the detailed rules" +msgstr "Detaylı kuralları oku" + +msgid "By {0}" +msgstr "{0} tarafından" + +msgid "Draft" +msgstr "Taslak" + +msgid "Search result(s) for \"{0}\"" +msgstr "\"{0}\" için arama sonuçları" + +msgid "Search result(s)" +msgstr "Arama sonuçları" + +msgid "No results for your query" +msgstr "Sorgunuz için sonuç yok" + +msgid "No more results for your query" +msgstr "Sorgunuz için başka sonuç yok" + +msgid "Advanced search" +msgstr "Gelişmiş arama" + +msgid "Article title matching these words" +msgstr "Şu kelimelerle eşleşen makale başlıkları:" + +msgid "Subtitle matching these words" +msgstr "Bu kelimelerle eşleşen altyazı" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "Gövde içeriği" + +msgid "From this date" +msgstr "Bu tarihten itibaren" + +msgid "To this date" +msgstr "Bu tarihe kadar" + +msgid "Containing these tags" +msgstr "Bu etiketleri içeren" + +msgid "Tags" +msgstr "Etiketler" + +msgid "Posted on one of these instances" +msgstr "Bu örneklerden birinde yayınlanan" + +msgid "Instance domain" +msgstr "Örnek alan adı" + +msgid "Posted by one of these authors" +msgstr "Bu yazarlardan biri tarafından gönderilen" + +msgid "Author(s)" +msgstr "Yazar(lar)" + +msgid "Posted on one of these blogs" +msgstr "Bu günlüklerden birinde yayınlanan" + +msgid "Blog title" +msgstr "Günlük başlığı" + +msgid "Written in this language" +msgstr "Şu dilde yazılmış:" + +msgid "Language" +msgstr "Dil" + +msgid "Published under this license" +msgstr "Bu lisans altında yayınlanan" + +msgid "Article license" +msgstr "Makale lisansı" + diff --git a/po/plume/uk.po b/po/plume/uk.po new file mode 100644 index 00000000000..c3840d5a64c --- /dev/null +++ b/po/plume/uk.po @@ -0,0 +1,1040 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Ukrainian\n" +"Language: uk_UA\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: uk\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} прокоментував ваш допис." + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} підписався на вас." + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} вподобав ваш допис." + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} згадав вас." + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} підтримав ваш допис." + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "Ваша стрічка" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "Локальна стрічка" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "Федеративна стрічка" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "Мармизка користувача {0}" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "Попередня сторінка" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "Наступна сторінка" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "Необов'язково" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "Щоб створити новий дописник, ви повинні увійти" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "Дописник з такою назвою вже існує." + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "Ваш дописник успішно створено!" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "Ваш дописник видалений." + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "Вам не дозволено видаляти цей дописник." + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "Вам не дозволено редагувати цей дописник." + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "Ви не можете використовувати цю медіа як іконку у дописнику." + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "Ви не можете використовувати цю медіа як банер у дописнику." + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "Інформація вашого дописника оновлена." + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "Ваш коментар додано." + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "Ваш коментар вилучений." + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "Налаштування були збережені." + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "{} розблоковано." + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "{} заблоковано." + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "Блоки видалено" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "Email заблоковано" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "Ви не можете змінити вашу власну роль." + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "Ви не маєте права для виконання цієї дії." + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "Готово." + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "Щоб вподобати допис, ви повинні увійти" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "Вашу медіа вилучено." + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "Вам не дозволено видаляти дану медіа." + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "Ваша мармизка оновлена." + +# src/routes/medias.rs:180 +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 "" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "" + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "" + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "" + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "" + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "" + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "" + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "" + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "" + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "" + +msgid "Create an account" +msgstr "" + +msgid "Email" +msgstr "" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Your media" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "" + +msgid "Menu" +msgstr "" + +msgid "Search" +msgstr "" + +msgid "Dashboard" +msgstr "" + +msgid "Notifications" +msgstr "" + +msgid "Log Out" +msgstr "" + +msgid "My account" +msgstr "" + +msgid "Log In" +msgstr "" + +msgid "Register" +msgstr "" + +msgid "About this instance" +msgstr "" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "" + +msgid "It is you" +msgstr "" + +msgid "Edit your profile" +msgstr "" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "" + +msgid "Follow {}" +msgstr "" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "" + +msgid "Subscribers" +msgstr "" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "" + +msgid "Your Drafts" +msgstr "" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "" + +msgid "Your Profile" +msgstr "" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "" + +msgid "Configuration" +msgstr "" + +msgid "Instances" +msgstr "" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "" + +msgid "Unblock" +msgstr "" + +msgid "Block" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Allow anyone to register here" +msgstr "" + +msgid "Short description" +msgstr "" + +msgid "Markdown syntax is supported" +msgstr "" + +msgid "Long description" +msgstr "" + +msgid "Default article license" +msgstr "" + +msgid "Save these settings" +msgstr "" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +msgid "Administred by" +msgstr "" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +msgid "Tags, separated by commas" +msgstr "" + +msgid "License" +msgstr "" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +msgid "I don't like this anymore" +msgstr "" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "" + +msgid "Comments" +msgstr "" + +msgid "Your comment" +msgstr "" + +msgid "Submit comment" +msgstr "" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "" + +msgid "Log in" +msgstr "" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +msgid "New password" +msgstr "" + +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "" + +msgid "Create a blog" +msgstr "" + +msgid "Create blog" +msgstr "" + +msgid "Edit \"{}\"" +msgstr "" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +msgid "From this date" +msgstr "" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" + diff --git a/po/plume/ur.po b/po/plume/ur.po new file mode 100644 index 00000000000..e263035dfd0 --- /dev/null +++ b/po/plume/ur.po @@ -0,0 +1,1034 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Urdu (Pakistan)\n" +"Language: ur_PK\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: ur-PK\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "" + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "" + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "" + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "" + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "" + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "" + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "" + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "" + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "" + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "" + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "" + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "" + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "" + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "" + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "" + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "" + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "" + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "" + +# src/routes/medias.rs:180 +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 "" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "" + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "" + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "" + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "" + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "" + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "" + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "" + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "" + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "" + +msgid "Create an account" +msgstr "" + +msgid "Email" +msgstr "" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Your media" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "" + +msgid "Menu" +msgstr "" + +msgid "Search" +msgstr "" + +msgid "Dashboard" +msgstr "" + +msgid "Notifications" +msgstr "" + +msgid "Log Out" +msgstr "" + +msgid "My account" +msgstr "" + +msgid "Log In" +msgstr "" + +msgid "Register" +msgstr "" + +msgid "About this instance" +msgstr "" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "" + +msgid "It is you" +msgstr "" + +msgid "Edit your profile" +msgstr "" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "" + +msgid "Follow {}" +msgstr "" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "" + +msgid "Subscribers" +msgstr "" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "" + +msgid "Your Drafts" +msgstr "" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "" + +msgid "Your Profile" +msgstr "" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "" + +msgid "Configuration" +msgstr "" + +msgid "Instances" +msgstr "" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "" + +msgid "Unblock" +msgstr "" + +msgid "Block" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Allow anyone to register here" +msgstr "" + +msgid "Short description" +msgstr "" + +msgid "Markdown syntax is supported" +msgstr "" + +msgid "Long description" +msgstr "" + +msgid "Default article license" +msgstr "" + +msgid "Save these settings" +msgstr "" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +msgid "Administred by" +msgstr "" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +msgid "Tags, separated by commas" +msgstr "" + +msgid "License" +msgstr "" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "" +msgstr[1] "" + +msgid "I don't like this anymore" +msgstr "" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" +msgstr[1] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "" + +msgid "Comments" +msgstr "" + +msgid "Your comment" +msgstr "" + +msgid "Submit comment" +msgstr "" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "" + +msgid "Log in" +msgstr "" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +msgid "New password" +msgstr "" + +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "" + +msgid "Create a blog" +msgstr "" + +msgid "Create blog" +msgstr "" + +msgid "Edit \"{}\"" +msgstr "" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" +msgstr[1] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +msgid "From this date" +msgstr "" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" + diff --git a/po/plume/vi.po b/po/plume/vi.po new file mode 100644 index 00000000000..3e99d9fcdb7 --- /dev/null +++ b/po/plume/vi.po @@ -0,0 +1,1031 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Vietnamese\n" +"Language: vi_VN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: vi\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "" + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "" + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "" + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "" + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "" + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "" + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "" + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "" + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "" + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "" + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "" + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "" + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "" + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "" + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "" + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "" + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "" + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "" + +# src/routes/medias.rs:180 +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 "" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "" + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "" + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "" + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "" + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "" + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "" + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "" + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "" + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "" + +msgid "Create an account" +msgstr "" + +msgid "Email" +msgstr "" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Your media" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "" + +msgid "Menu" +msgstr "" + +msgid "Search" +msgstr "" + +msgid "Dashboard" +msgstr "" + +msgid "Notifications" +msgstr "" + +msgid "Log Out" +msgstr "" + +msgid "My account" +msgstr "" + +msgid "Log In" +msgstr "" + +msgid "Register" +msgstr "" + +msgid "About this instance" +msgstr "" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "" + +msgid "It is you" +msgstr "" + +msgid "Edit your profile" +msgstr "" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "" + +msgid "Follow {}" +msgstr "" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "" + +msgid "Subscribers" +msgstr "" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "" + +msgid "Your Drafts" +msgstr "" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "" + +msgid "Your Profile" +msgstr "" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "" + +msgid "Configuration" +msgstr "" + +msgid "Instances" +msgstr "" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "" + +msgid "Unblock" +msgstr "" + +msgid "Block" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Allow anyone to register here" +msgstr "" + +msgid "Short description" +msgstr "" + +msgid "Markdown syntax is supported" +msgstr "" + +msgid "Long description" +msgstr "" + +msgid "Default article license" +msgstr "" + +msgid "Save these settings" +msgstr "" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +msgid "Administred by" +msgstr "" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +msgid "Tags, separated by commas" +msgstr "" + +msgid "License" +msgstr "" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "" + +msgid "I don't like this anymore" +msgstr "" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "" + +msgid "Comments" +msgstr "" + +msgid "Your comment" +msgstr "" + +msgid "Submit comment" +msgstr "" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "" + +msgid "Log in" +msgstr "" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +msgid "New password" +msgstr "" + +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "" + +msgid "Create a blog" +msgstr "" + +msgid "Create blog" +msgstr "" + +msgid "Edit \"{}\"" +msgstr "" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +msgid "From this date" +msgstr "" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" + diff --git a/po/plume/zh.po b/po/plume/zh.po new file mode 100644 index 00000000000..6c5083df757 --- /dev/null +++ b/po/plume/zh.po @@ -0,0 +1,1031 @@ +msgid "" +msgstr "" +"Project-Id-Version: plume\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-15 16:33-0700\n" +"PO-Revision-Date: 2022-01-12 01:28\n" +"Last-Translator: \n" +"Language-Team: Chinese Traditional\n" +"Language: zh_TW\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: plume\n" +"X-Crowdin-Project-ID: 352097\n" +"X-Crowdin-Language: zh-TW\n" +"X-Crowdin-File: /master/po/plume/plume.pot\n" +"X-Crowdin-File-ID: 8\n" + +# src/template_utils.rs:105 +msgid "Someone" +msgstr "" + +# src/template_utils.rs:107 +msgid "{0} commented on your article." +msgstr "{0} 評論了您的文章。" + +# src/template_utils.rs:108 +msgid "{0} is subscribed to you." +msgstr "{0} 訂閱了您。" + +# src/template_utils.rs:109 +msgid "{0} liked your article." +msgstr "{0} 喜欢了您的文章。" + +# src/template_utils.rs:110 +msgid "{0} mentioned you." +msgstr "{0} 提到了您。" + +# src/template_utils.rs:111 +msgid "{0} boosted your article." +msgstr "{0} 推薦了您的文章。" + +# src/template_utils.rs:118 +msgid "Your feed" +msgstr "您的推流" + +# src/template_utils.rs:119 +msgid "Local feed" +msgstr "本站推流" + +# src/template_utils.rs:120 +msgid "Federated feed" +msgstr "跨站推流" + +# src/template_utils.rs:156 +msgid "{0}'s avatar" +msgstr "{0} 的頭像" + +# src/template_utils.rs:200 +msgid "Previous page" +msgstr "上一頁" + +# src/template_utils.rs:211 +msgid "Next page" +msgstr "下一頁" + +# src/template_utils.rs:365 +msgid "Optional" +msgstr "可選" + +# src/routes/blogs.rs:68 +msgid "To create a new blog, you need to be logged in" +msgstr "您需要登入才能創建新的部落格" + +# src/routes/blogs.rs:110 +msgid "A blog with the same name already exists." +msgstr "已存在同名的部落格。" + +# src/routes/blogs.rs:148 +msgid "Your blog was successfully created!" +msgstr "您的部落格已成功創建!" + +# src/routes/blogs.rs:166 +msgid "Your blog was deleted." +msgstr "您的部落格已經刪除。" + +# src/routes/blogs.rs:174 +msgid "You are not allowed to delete this blog." +msgstr "您不能刪除此部落格。" + +# src/routes/blogs.rs:224 +msgid "You are not allowed to edit this blog." +msgstr "您不能編輯此部落格。" + +# src/routes/blogs.rs:280 +msgid "You can't use this media as a blog icon." +msgstr "您不能將此媒體用作部落格圖標。" + +# src/routes/blogs.rs:298 +msgid "You can't use this media as a blog banner." +msgstr "您不能將此媒體用作部落格橫幅。" + +# src/routes/blogs.rs:332 +msgid "Your blog information have been updated." +msgstr "您的部落格資訊已經更新。" + +# src/routes/comments.rs:100 +msgid "Your comment has been posted." +msgstr "您的評論已經發表。" + +# src/routes/comments.rs:177 +msgid "Your comment has been deleted." +msgstr "您的評論已經刪除。" + +# src/routes/email_signups.rs:82 +msgid "Registrations are closed on this instance." +msgstr "" + +# src/routes/email_signups.rs:119 +msgid "User registration" +msgstr "" + +# src/routes/email_signups.rs:120 +msgid "Here is the link for registration: {0}" +msgstr "" + +# src/routes/email_signups.rs:219 +msgid "Your account has been created. Now you just need to log in, before you can use it." +msgstr "" + +# src/routes/instance.rs:117 +msgid "Instance settings have been saved." +msgstr "實例設定已儲存。" + +# src/routes/instance.rs:150 +msgid "{} has been unblocked." +msgstr "已解除封鎖 {}。" + +# src/routes/instance.rs:152 +msgid "{} has been blocked." +msgstr "已封鎖 {}。" + +# src/routes/instance.rs:203 +msgid "Blocks deleted" +msgstr "已刪除封鎖的地址。" + +# src/routes/instance.rs:219 +msgid "Email already blocked" +msgstr "Email 地址已被屏蔽" + +# src/routes/instance.rs:224 +msgid "Email Blocked" +msgstr "Email 地址被屏蔽了" + +# src/routes/instance.rs:317 +msgid "You can't change your own rights." +msgstr "您不能改變自己的權限。" + +# src/routes/instance.rs:328 +msgid "You are not allowed to take this action." +msgstr "您不能執行此操作。" + +# src/routes/instance.rs:363 +msgid "Done." +msgstr "完成。" + +# src/routes/likes.rs:58 +msgid "To like a post, you need to be logged in" +msgstr "您需要登入才能標記喜歡一篇文。" + +# src/routes/medias.rs:153 +msgid "Your media have been deleted." +msgstr "您的媒體已刪除。" + +# src/routes/medias.rs:158 +msgid "You are not allowed to delete this media." +msgstr "您不能刪除此媒體。" + +# src/routes/medias.rs:175 +msgid "Your avatar has been updated." +msgstr "您的頭像已更新。" + +# src/routes/medias.rs:180 +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 "您需要登入才能看到您的通知" + +# src/routes/posts.rs:56 +msgid "This post isn't published yet." +msgstr "此文尚未發布。" + +# src/routes/posts.rs:126 +msgid "To write a new post, you need to be logged in" +msgstr "您需要登入後才能寫新文章。" + +# src/routes/posts.rs:147 +msgid "You are not an author of this blog." +msgstr "您不是這個部落格的作者。" + +# src/routes/posts.rs:154 +msgid "New post" +msgstr "新文章" + +# src/routes/posts.rs:199 +msgid "Edit {0}" +msgstr "編輯 {0}" + +# src/routes/posts.rs:268 +msgid "You are not allowed to publish on this blog." +msgstr "您不能在此部落格上發布。" + +# src/routes/posts.rs:368 +msgid "Your article has been updated." +msgstr "已更新您的文章。" + +# src/routes/posts.rs:557 +msgid "Your article has been saved." +msgstr "已儲存您的文章。" + +# src/routes/posts.rs:564 +msgid "New article" +msgstr "新增文章" + +# src/routes/posts.rs:602 +msgid "You are not allowed to delete this article." +msgstr "您不能刪除此文章。" + +# src/routes/posts.rs:626 +msgid "Your article has been deleted." +msgstr "已刪除您的文章。" + +# src/routes/posts.rs:631 +msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?" +msgstr "您試圖刪除的文章不存在。可能早已被刪除?" + +# src/routes/posts.rs:673 +msgid "Couldn't obtain enough information about your account. Please make sure your username is correct." +msgstr "" + +# src/routes/reshares.rs:58 +msgid "To reshare a post, you need to be logged in" +msgstr "" + +# src/routes/session.rs:95 +msgid "You are now connected." +msgstr "" + +# src/routes/session.rs:116 +msgid "You are now logged off." +msgstr "" + +# src/routes/session.rs:162 +msgid "Password reset" +msgstr "" + +# src/routes/session.rs:163 +msgid "Here is the link to reset your password: {0}" +msgstr "" + +# src/routes/session.rs:235 +msgid "Your password was successfully reset." +msgstr "" + +# src/routes/user.rs:87 +msgid "To access your dashboard, you need to be logged in" +msgstr "" + +# src/routes/user.rs:109 +msgid "You are no longer following {}." +msgstr "" + +# src/routes/user.rs:126 +msgid "You are now following {}." +msgstr "" + +# src/routes/user.rs:203 +msgid "To subscribe to someone, you need to be logged in" +msgstr "" + +# src/routes/user.rs:323 +msgid "To edit your profile, you need to be logged in" +msgstr "" + +# src/routes/user.rs:369 +msgid "Your profile has been updated." +msgstr "" + +# src/routes/user.rs:397 +msgid "Your account has been deleted." +msgstr "" + +# src/routes/user.rs:403 +msgid "You can't delete someone else's account." +msgstr "" + +msgid "Create your account" +msgstr "" + +msgid "Create an account" +msgstr "" + +msgid "Email" +msgstr "" + +msgid "Email confirmation" +msgstr "" + +msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one." +msgstr "" + +msgid "Registration" +msgstr "" + +msgid "Check your inbox!" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link for registration." +msgstr "" + +msgid "Username" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password confirmation" +msgstr "" + +msgid "Media upload" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Useful for visually impaired people, as well as licensing information" +msgstr "" + +msgid "Content warning" +msgstr "" + +msgid "Leave it empty, if none is needed" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Your media" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "You don't have any media yet." +msgstr "" + +msgid "Content warning: {0}" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Media details" +msgstr "" + +msgid "Go back to the gallery" +msgstr "" + +msgid "Markdown syntax" +msgstr "" + +msgid "Copy it into your articles, to insert this media:" +msgstr "" + +msgid "Use as an avatar" +msgstr "" + +msgid "Plume" +msgstr "" + +msgid "Menu" +msgstr "" + +msgid "Search" +msgstr "" + +msgid "Dashboard" +msgstr "" + +msgid "Notifications" +msgstr "" + +msgid "Log Out" +msgstr "" + +msgid "My account" +msgstr "" + +msgid "Log In" +msgstr "" + +msgid "Register" +msgstr "" + +msgid "About this instance" +msgstr "" + +msgid "Privacy policy" +msgstr "" + +msgid "Administration" +msgstr "" + +msgid "Documentation" +msgstr "" + +msgid "Source code" +msgstr "" + +msgid "Matrix room" +msgstr "" + +msgid "Admin" +msgstr "" + +msgid "It is you" +msgstr "" + +msgid "Edit your profile" +msgstr "" + +msgid "Open on {0}" +msgstr "" + +msgid "Unsubscribe" +msgstr "" + +msgid "Subscribe" +msgstr "" + +msgid "Follow {}" +msgstr "" + +msgid "Log in to follow" +msgstr "" + +msgid "Enter your full username handle to follow" +msgstr "" + +msgid "{0}'s subscribers" +msgstr "" + +msgid "Articles" +msgstr "" + +msgid "Subscribers" +msgstr "" + +msgid "Subscriptions" +msgstr "" + +msgid "{0}'s subscriptions" +msgstr "" + +msgid "Your Dashboard" +msgstr "" + +msgid "Your Blogs" +msgstr "" + +msgid "You don't have any blog yet. Create your own, or ask to join one." +msgstr "" + +msgid "Start a new blog" +msgstr "" + +msgid "Your Drafts" +msgstr "" + +msgid "Go to your gallery" +msgstr "" + +msgid "Edit your account" +msgstr "" + +msgid "Your Profile" +msgstr "" + +msgid "To change your avatar, upload it to your gallery and then select from there." +msgstr "" + +msgid "Upload an avatar" +msgstr "" + +msgid "Display name" +msgstr "" + +msgid "Summary" +msgstr "" + +msgid "Theme" +msgstr "" + +msgid "Default theme" +msgstr "" + +msgid "Error while loading theme selector." +msgstr "" + +msgid "Never load blogs custom themes" +msgstr "" + +msgid "Update account" +msgstr "" + +msgid "Danger zone" +msgstr "" + +msgid "Be very careful, any action taken here can't be cancelled." +msgstr "" + +msgid "Delete your account" +msgstr "" + +msgid "Sorry, but as an admin, you can't leave your own instance." +msgstr "" + +msgid "Latest articles" +msgstr "" + +msgid "Atom feed" +msgstr "" + +msgid "Recently boosted" +msgstr "" + +msgid "Articles tagged \"{0}\"" +msgstr "" + +msgid "There are currently no articles with such a tag" +msgstr "" + +msgid "The content you sent can't be processed." +msgstr "" + +msgid "Maybe it was too long." +msgstr "" + +msgid "Internal server error" +msgstr "" + +msgid "Something broke on our side." +msgstr "" + +msgid "Sorry about that. If you think this is a bug, please report it." +msgstr "" + +msgid "Invalid CSRF token" +msgstr "" + +msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." +msgstr "" + +msgid "You are not authorized." +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +msgid "Users" +msgstr "" + +msgid "Configuration" +msgstr "" + +msgid "Instances" +msgstr "" + +msgid "Email blocklist" +msgstr "" + +msgid "Grant admin rights" +msgstr "" + +msgid "Revoke admin rights" +msgstr "" + +msgid "Grant moderator rights" +msgstr "" + +msgid "Revoke moderator rights" +msgstr "" + +msgid "Ban" +msgstr "" + +msgid "Run on selected users" +msgstr "" + +msgid "Moderator" +msgstr "" + +msgid "Moderation" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Administration of {0}" +msgstr "" + +msgid "Unblock" +msgstr "" + +msgid "Block" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Allow anyone to register here" +msgstr "" + +msgid "Short description" +msgstr "" + +msgid "Markdown syntax is supported" +msgstr "" + +msgid "Long description" +msgstr "" + +msgid "Default article license" +msgstr "" + +msgid "Save these settings" +msgstr "" + +msgid "If you are browsing this site as a visitor, no data about you is collected." +msgstr "" + +msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it." +msgstr "" + +msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies." +msgstr "" + +msgid "Blocklisted Emails" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com" +msgstr "" + +msgid "Note" +msgstr "" + +msgid "Notify the user?" +msgstr "" + +msgid "Optional, shows a message to the user when they attempt to create an account with that address" +msgstr "" + +msgid "Blocklisting notification" +msgstr "" + +msgid "The message to be shown when the user attempts to create an account with this email address" +msgstr "" + +msgid "Add blocklisted address" +msgstr "" + +msgid "There are no blocked emails on your instance" +msgstr "" + +msgid "Delete selected emails" +msgstr "" + +msgid "Email address:" +msgstr "" + +msgid "Blocklisted for:" +msgstr "" + +msgid "Will notify them on account creation with this message:" +msgstr "" + +msgid "The user will be silently prevented from making an account" +msgstr "" + +msgid "Welcome to {}" +msgstr "" + +msgid "View all" +msgstr "" + +msgid "About {0}" +msgstr "" + +msgid "Runs Plume {0}" +msgstr "" + +msgid "Home to {0} people" +msgstr "" + +msgid "Who wrote {0} articles" +msgstr "" + +msgid "And are connected to {0} other instances" +msgstr "" + +msgid "Administred by" +msgstr "" + +msgid "Interact with {}" +msgstr "" + +msgid "Log in to interact" +msgstr "" + +msgid "Enter your full username to interact" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Classic editor (any changes will be lost)" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Subtitle" +msgstr "" + +msgid "Content" +msgstr "" + +msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them." +msgstr "" + +msgid "Upload media" +msgstr "" + +msgid "Tags, separated by commas" +msgstr "" + +msgid "License" +msgstr "" + +msgid "Illustration" +msgstr "" + +msgid "This is a draft, don't publish it yet." +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update, or publish" +msgstr "" + +msgid "Publish your post" +msgstr "" + +msgid "Written by {0}" +msgstr "" + +msgid "All rights reserved." +msgstr "" + +msgid "This article is under the {0} license." +msgstr "" + +msgid "One like" +msgid_plural "{0} likes" +msgstr[0] "" + +msgid "I don't like this anymore" +msgstr "" + +msgid "Add yours" +msgstr "" + +msgid "One boost" +msgid_plural "{0} boosts" +msgstr[0] "" + +msgid "I don't want to boost this anymore" +msgstr "" + +msgid "Boost" +msgstr "" + +msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article" +msgstr "" + +msgid "Comments" +msgstr "" + +msgid "Your comment" +msgstr "" + +msgid "Submit comment" +msgstr "" + +msgid "No comments yet. Be the first to react!" +msgstr "" + +msgid "Are you sure?" +msgstr "" + +msgid "This article is still a draft. Only you and other authors can see it." +msgstr "" + +msgid "Only you and other authors can edit this article." +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "I'm from this instance" +msgstr "" + +msgid "Username, or email" +msgstr "" + +msgid "Log in" +msgstr "" + +msgid "I'm from another instance" +msgstr "" + +msgid "Continue to your instance" +msgstr "" + +msgid "Reset your password" +msgstr "" + +msgid "New password" +msgstr "" + +msgid "Confirmation" +msgstr "" + +msgid "Update password" +msgstr "" + +msgid "We sent a mail to the address you gave us, with a link to reset your password." +msgstr "" + +msgid "Send password reset link" +msgstr "" + +msgid "This token has expired" +msgstr "" + +msgid "Please start the process again by clicking here." +msgstr "" + +msgid "New Blog" +msgstr "" + +msgid "Create a blog" +msgstr "" + +msgid "Create blog" +msgstr "" + +msgid "Edit \"{}\"" +msgstr "" + +msgid "You can upload images to your gallery, to use them as blog icons, or banners." +msgstr "" + +msgid "Upload images" +msgstr "" + +msgid "Blog icon" +msgstr "" + +msgid "Blog banner" +msgstr "" + +msgid "Custom theme" +msgstr "" + +msgid "Update blog" +msgstr "" + +msgid "Be very careful, any action taken here can't be reversed." +msgstr "" + +msgid "Are you sure that you want to permanently delete this blog?" +msgstr "" + +msgid "Permanently delete this blog" +msgstr "" + +msgid "{}'s icon" +msgstr "" + +msgid "There's one author on this blog: " +msgid_plural "There are {0} authors on this blog: " +msgstr[0] "" + +msgid "No posts to see here yet." +msgstr "" + +msgid "Nothing to see here yet." +msgstr "" + +msgid "None" +msgstr "" + +msgid "No description" +msgstr "" + +msgid "Respond" +msgstr "" + +msgid "Delete this comment" +msgstr "" + +msgid "What is Plume?" +msgstr "" + +msgid "Plume is a decentralized blogging engine." +msgstr "" + +msgid "Authors can manage multiple blogs, each as its own website." +msgstr "" + +msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon." +msgstr "" + +msgid "Read the detailed rules" +msgstr "" + +msgid "By {0}" +msgstr "" + +msgid "Draft" +msgstr "" + +msgid "Search result(s) for \"{0}\"" +msgstr "" + +msgid "Search result(s)" +msgstr "" + +msgid "No results for your query" +msgstr "" + +msgid "No more results for your query" +msgstr "" + +msgid "Advanced search" +msgstr "" + +msgid "Article title matching these words" +msgstr "" + +msgid "Subtitle matching these words" +msgstr "" + +msgid "Content macthing these words" +msgstr "" + +msgid "Body content" +msgstr "" + +msgid "From this date" +msgstr "" + +msgid "To this date" +msgstr "" + +msgid "Containing these tags" +msgstr "" + +msgid "Tags" +msgstr "" + +msgid "Posted on one of these instances" +msgstr "" + +msgid "Instance domain" +msgstr "" + +msgid "Posted by one of these authors" +msgstr "" + +msgid "Author(s)" +msgstr "" + +msgid "Posted on one of these blogs" +msgstr "" + +msgid "Blog title" +msgstr "" + +msgid "Written in this language" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "Published under this license" +msgstr "" + +msgid "Article license" +msgstr "" + diff --git a/release.toml b/release.toml new file mode 100644 index 00000000000..644e43ad69c --- /dev/null +++ b/release.toml @@ -0,0 +1,19 @@ +# we don't have a crate yet, so +publish = true +# change when we all have gpg keys +sign-commit = false +dev-version-ext = 'dev' +# update all crates in plume at once: +consolidate-commits = true + +tag-name = "{{prefix}}{{version}}" + +pre-release-hook = ["crowdin", "pull", "--branch", "master"] + +pre-release-replacements = [ + {file="CHANGELOG.md", search="Unreleased", replace="[{{version}}]"}, + {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1}, + {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}"}, + {file="CHANGELOG.md", search="", replace="\n\n## [Unreleased] - ReleaseDate", exactly=1}, + {file="CHANGELOG.md", search="", replace="\n[Unreleased]: https://github.com/Plume-org/Plume/compare/{{tag_name}}...HEAD", exactly=1}, +] diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 00000000000..63af764ba6d --- /dev/null +++ b/rust-toolchain @@ -0,0 +1 @@ +nightly-2022-01-27 diff --git a/script/browser_test/__init__.py b/script/browser_test/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/script/browser_test/instance.py b/script/browser_test/instance.py new file mode 100644 index 00000000000..ceb7231c59d --- /dev/null +++ b/script/browser_test/instance.py @@ -0,0 +1,7 @@ +#!/usr/bin/python3 +from utils import Browser + +class InstanceName(Browser): + def test_name_in_title(self): + self.get("/") + self.assertIn("plume-test", self.driver.title) diff --git a/script/browser_test/utils.py b/script/browser_test/utils.py new file mode 100644 index 00000000000..df07eb1face --- /dev/null +++ b/script/browser_test/utils.py @@ -0,0 +1,22 @@ +#!/usr/bin/python3 +import unittest,os +from selenium import webdriver +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities + +class Browser(unittest.TestCase): + def setUp(self): + if os.environ["BROWSER"] == "firefox": + capabilities=DesiredCapabilities.FIREFOX + elif os.environ["BROWSER"] == "chrome": + capabilities=DesiredCapabilities.CHROME + else: + raise Exception("No browser was requested") + capabilities['acceptSslCerts'] = True + self.driver = webdriver.Remote( + command_executor='http://localhost:24444/wd/hub', + desired_capabilities=capabilities) + def tearDown(self): + self.driver.close() + + def get(self, url): + return self.driver.get("https://localhost" + url) diff --git a/script/generate_artifact.sh b/script/generate_artifact.sh new file mode 100755 index 00000000000..8f3a72983a6 --- /dev/null +++ b/script/generate_artifact.sh @@ -0,0 +1,5 @@ +#!/bin/bash +mkdir bin +cp target/release/{plume,plm} bin +tar -cvzf plume.tar.gz bin/ static/ +tar -cvzf wasm.tar.gz static/plume_front{.js,_bg.wasm} diff --git a/script/plume-front.sh b/script/plume-front.sh new file mode 100755 index 00000000000..63b2f5ddbf3 --- /dev/null +++ b/script/plume-front.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +ARCH=$(python </dev/null >/dev/null + +cd $(dirname $0)/browser_test/ +python3 -m unittest *.py + +kill -SIGINT %1 +kill -SIGKILL %2 +sleep 15 diff --git a/script/upload_test_environment.sh b/script/upload_test_environment.sh new file mode 100755 index 00000000000..5f4d946ec1d --- /dev/null +++ b/script/upload_test_environment.sh @@ -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" diff --git a/script/wasm-deps.sh b/script/wasm-deps.sh new file mode 100644 index 00000000000..33beda6b0ef --- /dev/null +++ b/script/wasm-deps.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +ARCH=$(python < ${SNAP_COMMON}/rocket-secret-key diff --git a/snap/hooks/post-refresh b/snap/hooks/post-refresh new file mode 100644 index 00000000000..4be65e5b8bc --- /dev/null +++ b/snap/hooks/post-refresh @@ -0,0 +1,4 @@ +#!/bin/sh + +cd ${SNAP} +exec ./set-environment bin/plm migration run --path ${SNAP_DATA} diff --git a/snap/local/set-environment b/snap/local/set-environment new file mode 100755 index 00000000000..06dc1b0eed9 --- /dev/null +++ b/snap/local/set-environment @@ -0,0 +1,30 @@ +#!/bin/sh + +enabled="$(snapctl get enabled)" +if [ -z "${enabled}" -o "${enabled}" != "true" ] +then + echo "Plume not yet enabled" + exit 0 +fi + +export BASE_URL="$(snapctl get base-url)" +database_type="$(snapctl get db.type)" + +if [ z"${database_type}" = z"sqlite" ] +then + export DATABASE_URL=${SNAP_DATA}/plume.db + export MIGRATION_DIR=migrations/sqlite +else + # Must be postgres, so must have set db.url + export DATABASE_URL="$(snapctl get db.url)" + export MIGRATION_DIRECTORY=migrations/postgres +fi + +ROCKET_ADDRESS="$(snapctl get listen.address)" +ROCKET_PORT="$(snapctl get listen.port)" +export ROCKET_SECRET_KEY="$(cat ${SNAP_COMMON}/rocket-secret-key)" +export SEARCH_INDEX="${SNAP_DATA}/search_index" +export MEDIA_UPLOAD_DIRECTORY="${SNAP_DATA}/media" + +cd ${SNAP} +exec $@ diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 00000000000..e61eb2ab1a4 --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,61 @@ +name: plume +base: core18 +adopt-info: plume +summary: Multi-user blogging platform, federated over ActivityPub +description: | + Plume is a federated blogging platform, featuring: + * 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. +grade: stable +confinement: strict + +apps: + plume: + daemon: simple + command: set-environment bin/plume + plugs: + - network + - network-bind + plm: + command: set-environment bin/plm + +parts: + plume: + plugin: rust + source: . + rust-revision: nightly-2021-11-27 + build-packages: + - libssl-dev + - pkg-config + - libsqlite3-dev + - gettext + - libclang-8-dev + - on arm64,armhf,ppc64el,s390x: + - lld-8 + override-build: | + snapcraftctl set-version $(git describe --tags) + export PATH=$PATH:$HOME/.cargo/bin + rustup install stable + cargo +stable install --force wasm-pack + + # Only Tier 1 Rust platforms get rust-lld + # On the others (arm64, armhf, powerpc64, s390x) fall back to using + # the system LLD we've installed earlier. + case ${SNAPCRAFT_ARCH_TRIPLET} in \ + aarch64-linux-gnu|arm-linux-gnueabihf|powerpc64-linux-gnu|s390x-linux-gnu) \ + RUSTFLAGS="-C linker=lld" wasm-pack build --target web --release plume-front \ + ;; \ + *) \ + wasm-pack build --target web --release plume-front \ + ;; \ + esac + + cargo install --force --no-default-features --features sqlite --path . --root ${SNAPCRAFT_PART_INSTALL} + cargo install --force --no-default-features --features sqlite --path plume-cli --root ${SNAPCRAFT_PART_INSTALL} + cp -a assets migrations static target translations ${SNAPCRAFT_PART_INSTALL} + cp snap/local/set-environment ${SNAPCRAFT_PART_INSTALL} + stage-packages: + - openssl + - libsqlite3-0 diff --git a/src/api/apps.rs b/src/api/apps.rs new file mode 100644 index 00000000000..7bbc2d7a927 --- /dev/null +++ b/src/api/apps.rs @@ -0,0 +1,24 @@ +use rocket_contrib::json::Json; + +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 = "")] +pub fn create(conn: DbConn, data: Json) -> Api { + 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)) +} diff --git a/src/api/authorization.rs b/src/api/authorization.rs new file mode 100644 index 00000000000..8a23ada57ab --- /dev/null +++ b/src/api/authorization.rs @@ -0,0 +1,57 @@ +use plume_models::{self, api_tokens::ApiToken}; +use rocket::{ + http::Status, + request::{self, FromRequest, Request}, + Outcome, +}; +use std::marker::PhantomData; + +// Actions +pub trait Action { + fn to_str() -> &'static str; +} +pub struct Read; +impl Action for Read { + fn to_str() -> &'static str { + "read" + } +} +pub struct Write; +impl Action for Write { + fn to_str() -> &'static str { + "write" + } +} + +// Scopes +pub trait Scope { + fn to_str() -> &'static str; +} +impl Scope for plume_models::posts::Post { + fn to_str() -> &'static str { + "posts" + } +} + +pub struct Authorization(pub ApiToken, PhantomData<(A, S)>); + +impl<'a, 'r, A, S> FromRequest<'a, 'r> for Authorization +where + A: Action, + S: Scope, +{ + type Error = (); + + fn from_request(request: &'a Request<'r>) -> request::Outcome, ()> { + request + .guard::() + .map_failure(|_| (Status::Unauthorized, ())) + .and_then(|token| { + if token.can(A::to_str(), S::to_str()) { + Outcome::Success(Authorization(token, PhantomData)) + } else { + Outcome::Failure((Status::Unauthorized, ())) + } + }) + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100755 index 00000000000..cd575c35e6a --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,81 @@ +#![warn(clippy::too_many_arguments)] +use rocket::{ + request::{Form, Request}, + response::{self, Responder}, +}; +use rocket_contrib::json::Json; + +use plume_common::utils::random_hex; +use plume_models::{api_tokens::*, apps::App, db_conn::DbConn, users::User, Error}; + +type Api = Result, ApiError>; + +#[derive(Debug)] +pub struct ApiError(Error); + +impl From for ApiError { + fn from(err: Error) -> ApiError { + ApiError(err) + } +} + +impl<'r> Responder<'r> for ApiError { + fn respond_to(self, req: &Request<'_>) -> response::Result<'r> { + match self.0 { + Error::NotFound => Json(json!({ + "error": "Not found" + })) + .respond_to(req), + Error::Unauthorized => Json(json!({ + "error": "You are not authorized to access this resource" + })) + .respond_to(req), + _ => Json(json!({ + "error": "Server error" + })) + .respond_to(req), + } + } +} + +#[derive(FromForm)] +pub struct OAuthRequest { + client_id: String, + client_secret: String, + password: String, + username: String, + scopes: String, +} + +#[get("/oauth2?")] +pub fn oauth(query: Form, conn: DbConn) -> Result, ApiError> { + let app = App::find_by_client_id(&conn, &query.client_id)?; + if app.client_secret == query.client_secret { + if let Ok(user) = User::login(&conn, &query.username, &query.password) { + let token = ApiToken::insert( + &conn, + NewApiToken { + app_id: app.id, + user_id: user.id, + value: random_hex(), + scopes: query.scopes.clone(), + }, + )?; + Ok(Json(json!({ + "token": token.value + }))) + } else { + Ok(Json(json!({ + "error": "Invalid credentials" + }))) + } + } else { + Ok(Json(json!({ + "error": "Invalid client_secret" + }))) + } +} + +pub mod apps; +pub mod authorization; +pub mod posts; diff --git a/src/api/posts.rs b/src/api/posts.rs new file mode 100644 index 00000000000..56c80da969f --- /dev/null +++ b/src/api/posts.rs @@ -0,0 +1,243 @@ +use chrono::NaiveDateTime; +use rocket_contrib::json::Json; + +use crate::api::{authorization::*, Api, ApiError}; +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::*, timeline::*, users::User, Error, PlumeRocket, + CONFIG, +}; + +#[get("/posts/")] +pub fn get(id: i32, auth: Option>, conn: DbConn) -> Api { + let user = auth.and_then(|a| User::get(&conn, a.0.user_id).ok()); + let post = Post::get(&conn, id)?; + + if !post.published + && !user + .and_then(|u| post.is_author(&conn, u.id).ok()) + .unwrap_or(false) + { + return Err(Error::Unauthorized.into()); + } + + Ok(Json(PostData { + authors: post + .get_authors(&conn)? + .into_iter() + .map(|a| a.username) + .collect(), + creation_date: post.creation_date.format("%Y-%m-%d").to_string(), + tags: Tag::for_post(&conn, post.id)? + .into_iter() + .map(|t| t.tag) + .collect(), + + id: post.id, + title: post.title, + subtitle: post.subtitle, + content: post.content.to_string(), + source: Some(post.source), + blog_id: post.blog_id, + published: post.published, + license: post.license, + cover_id: post.cover_id, + })) +} + +#[get("/posts?&<subtitle>&<content>")] +pub fn list( + title: Option<String>, + subtitle: Option<String>, + content: Option<String>, + auth: Option<Authorization<Read, Post>>, + conn: DbConn, +) -> Api<Vec<PostData>> { + let user = auth.and_then(|a| User::get(&conn, a.0.user_id).ok()); + let user_id = user.map(|u| u.id); + + Ok(Json( + Post::list_filtered(&conn, title, subtitle, content)? + .into_iter() + .filter(|p| { + p.published + || user_id + .and_then(|u| p.is_author(&conn, u).ok()) + .unwrap_or(false) + }) + .filter_map(|p| { + Some(PostData { + authors: p + .get_authors(&conn) + .ok()? + .into_iter() + .map(|a| a.username) + .collect(), + creation_date: p.creation_date.format("%Y-%m-%d").to_string(), + tags: Tag::for_post(&conn, p.id) + .ok()? + .into_iter() + .map(|t| t.tag) + .collect(), + + id: p.id, + title: p.title, + subtitle: p.subtitle, + content: p.content.to_string(), + source: Some(p.source), + blog_id: p.blog_id, + published: p.published, + license: p.license, + cover_id: p.cover_id, + }) + }) + .collect(), + )) +} + +#[post("/posts", data = "<payload>")] +pub fn create( + auth: Authorization<Write, Post>, + payload: Json<NewPostData>, + conn: DbConn, + rockets: PlumeRocket, +) -> Api<PostData> { + let worker = &rockets.worker; + + let author = User::get(&conn, auth.0.user_id)?; + + let slug = Post::slug(&payload.title); + let date = payload.creation_date.clone().and_then(|d| { + NaiveDateTime::parse_from_str(format!("{} 00:00:00", d).as_ref(), "%Y-%m-%d %H:%M:%S").ok() + }); + + let domain = &Instance::get_local()?.public_domain; + let (content, mentions, hashtags) = md_to_html( + &payload.source, + Some(domain), + false, + Some(Media::get_media_processor(&conn, vec![&author])), + ); + + let blog = payload + .blog_id + .or_else(|| { + let blogs = Blog::find_for_author(&conn, &author).ok()?; + if blogs.len() == 1 { + Some(blogs[0].id) + } else { + None + } + }) + .ok_or(ApiError(Error::NotFound))?; + + 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, + }, + )?; + + PostAuthor::insert( + &conn, + NewPostAuthor { + author_id: author.id, + post_id: post.id, + }, + )?; + + if let Some(ref tags) = payload.tags { + for tag in tags { + Tag::insert( + &conn, + NewTag { + tag: tag.to_string(), + is_hashtag: false, + post_id: post.id, + }, + )?; + } + } + for hashtag in hashtags { + Tag::insert( + &conn, + NewTag { + tag: hashtag, + is_hashtag: true, + post_id: post.id, + }, + )?; + } + + if post.published { + for m in mentions.into_iter() { + Mention::from_activity( + &conn, + &Mention::build_activity(&conn, &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, CONFIG.proxy().cloned())); + } + + Timeline::add_to_all_timelines(&conn, &post, Kind::Original)?; + + Ok(Json(PostData { + authors: post + .get_authors(&conn)? + .into_iter() + .map(|a| a.fqn) + .collect(), + creation_date: post.creation_date.format("%Y-%m-%d").to_string(), + tags: Tag::for_post(&conn, post.id)? + .into_iter() + .map(|t| t.tag) + .collect(), + + id: post.id, + title: post.title, + subtitle: post.subtitle, + content: post.content.to_string(), + source: Some(post.source), + blog_id: post.blog_id, + published: post.published, + license: post.license, + cover_id: post.cover_id, + })) +} + +#[delete("/posts/<id>")] +pub fn delete(auth: Authorization<Write, Post>, conn: DbConn, id: i32) -> Api<()> { + let author = User::get(&conn, auth.0.user_id)?; + if let Ok(post) = Post::get(&conn, id) { + if post.is_author(&conn, author.id).unwrap_or(false) { + post.delete(&conn)?; + } + } + Ok(Json(())) +} diff --git a/src/inbox.rs b/src/inbox.rs new file mode 100644 index 00000000000..78069b69726 --- /dev/null +++ b/src/inbox.rs @@ -0,0 +1,104 @@ +use plume_common::activity_pub::{ + inbox::FromId, + request::Digest, + sign::{verify_http_headers, Signable}, +}; +use plume_models::{ + db_conn::DbConn, headers::Headers, inbox::inbox, instance::Instance, users::User, Error, CONFIG, +}; +use rocket::{data::*, http::Status, response::status, Outcome::*, Request}; +use rocket_contrib::json::*; +use serde::Deserialize; +use std::io::Read; +use tracing::warn; + +pub fn handle_incoming( + conn: DbConn, + data: SignedJson<serde_json::Value>, + headers: Headers<'_>, +) -> Result<String, status::BadRequest<&'static str>> { + let act = data.1.into_inner(); + let sig = data.0; + + let activity = act.clone(); + let actor_id = activity["actor"] + .as_str() + .or_else(|| activity["actor"]["id"].as_str()) + .ok_or(status::BadRequest(Some("Missing actor id for activity")))?; + + let actor = User::from_id(&conn, actor_id, None, CONFIG.proxy()) + .expect("instance::shared_inbox: user error"); + if !verify_http_headers(&actor, &headers.0, &sig).is_secure() && !act.clone().verify(&actor) { + // maybe we just know an old key? + actor + .refetch(&conn) + .and_then(|_| User::get(&conn, actor.id)) + .and_then(|u| { + if verify_http_headers(&u, &headers.0, &sig).is_secure() || act.clone().verify(&u) { + Ok(()) + } else { + Err(Error::Signature) + } + }) + .map_err(|_| { + warn!( + "Rejected invalid activity supposedly from {}, with headers {:?}", + actor.username, headers.0 + ); + status::BadRequest(Some("Invalid signature")) + })?; + } + + if Instance::is_blocked(&conn, actor_id) + .map_err(|_| status::BadRequest(Some("Can't tell if instance is blocked")))? + { + return Ok(String::new()); + } + + Ok(match inbox(&conn, act) { + Ok(_) => String::new(), + Err(e) => { + warn!("Shared inbox error: {:?}", e); + format!("Error: {:?}", e) + } + }) +} + +const JSON_LIMIT: u64 = 1 << 20; + +pub struct SignedJson<T>(pub Digest, pub Json<T>); + +impl<'a, T: Deserialize<'a>> FromData<'a> for SignedJson<T> { + type Error = JsonError<'a>; + type Owned = String; + type Borrowed = str; + + fn transform( + r: &Request<'_>, + d: Data, + ) -> Transform<rocket::data::Outcome<Self::Owned, Self::Error>> { + let size_limit = r.limits().get("json").unwrap_or(JSON_LIMIT); + let mut s = String::with_capacity(512); + match d.open().take(size_limit).read_to_string(&mut s) { + Ok(_) => Transform::Borrowed(Success(s)), + Err(e) => Transform::Borrowed(Failure((Status::BadRequest, JsonError::Io(e)))), + } + } + + fn from_data( + _: &Request<'_>, + o: Transformed<'a, Self>, + ) -> rocket::data::Outcome<Self, Self::Error> { + let string = o.borrowed()?; + match serde_json::from_str(string) { + Ok(v) => Success(SignedJson(Digest::from_body(string), Json(v))), + Err(e) => { + if e.is_data() { + Failure((Status::UnprocessableEntity, JsonError::Parse(string, e))) + } else { + Failure((Status::BadRequest, JsonError::Parse(string, e))) + } + } + } + } +} diff --git a/src/mail.rs b/src/mail.rs new file mode 100644 index 00000000000..0eb5bd5ed26 --- /dev/null +++ b/src/mail.rs @@ -0,0 +1,93 @@ +#![warn(clippy::too_many_arguments)] +use lettre_email::Email; +use std::env; + +pub use self::mailer::*; + +#[cfg(feature = "debug-mailer")] +mod mailer { + use plume_models::smtp::{SendableEmail, Transport}; + use std::io::Read; + + pub struct DebugTransport; + + impl<'a> Transport<'a> for DebugTransport { + type Result = Result<(), ()>; + + fn send(&mut self, email: SendableEmail) -> Self::Result { + println!( + "{}: from=<{}> to=<{:?}>\n{:#?}", + email.message_id().to_string(), + email + .envelope() + .from() + .map(ToString::to_string) + .unwrap_or_default(), + email.envelope().to().to_vec(), + { + let mut message = String::new(); + email + .message() + .read_to_string(&mut message) + .map_err(|_| ())?; + message + }, + ); + Ok(()) + } + } + + pub type Mailer = Option<DebugTransport>; + + pub fn init() -> Mailer { + Some(DebugTransport) + } +} + +#[cfg(not(feature = "debug-mailer"))] +mod mailer { + use plume_models::smtp::{ + authentication::{Credentials, Mechanism}, + extension::ClientId, + ConnectionReuseParameters, SmtpClient, SmtpTransport, + }; + use plume_models::{SmtpNewWithAddr, CONFIG}; + + pub type Mailer = Option<SmtpTransport>; + + pub fn init() -> Mailer { + let config = CONFIG.mail.as_ref()?; + let mail = SmtpClient::new_with_addr((&config.server, config.port)) + .unwrap() + .hello_name(ClientId::Domain(config.helo_name.clone())) + .credentials(Credentials::new( + config.username.clone(), + config.password.clone(), + )) + .smtp_utf8(true) + .authentication_mechanism(Mechanism::Plain) + .connection_reuse(ConnectionReuseParameters::NoReuse) + .transport(); + Some(mail) + } +} + +pub fn build_mail(dest: String, subject: String, body: String) -> Option<Email> { + Email::builder() + .from( + env::var("MAIL_ADDRESS") + .or_else(|_| { + Ok(format!( + "{}@{}", + env::var("MAIL_USER")?, + env::var("MAIL_SERVER")? + )) as Result<_, env::VarError> + }) + .expect("The email server is not configured correctly"), + ) + .to(dest) + .subject(subject) + .text(body) + .build() + .ok() +} diff --git a/src/main.rs b/src/main.rs new file mode 100755 index 00000000000..5e3c0e7865a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,289 @@ +#![allow(clippy::too_many_arguments)] +#![feature(decl_macro, proc_macro_hygiene)] + +#[macro_use] +extern crate gettext_macros; +#[macro_use] +extern crate rocket; +#[macro_use] +extern crate serde_json; + +use clap::App; +use diesel::r2d2::ConnectionManager; +use plume_models::{ + db_conn::{DbPool, PragmaForeignKey}, + instance::Instance, + migrations::IMPORTED_MIGRATIONS, + remote_fetch_actor::RemoteFetchActor, + search::{actor::SearchActor, Searcher as UnmanagedSearcher}, + Connection, CONFIG, +}; +use rocket_csrf::CsrfFairingBuilder; +use scheduled_thread_pool::ScheduledThreadPool; +use std::process::exit; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use tracing::warn; + +init_i18n!( + "plume", af, ar, bg, ca, cs, cy, da, de, el, en, eo, es, eu, fa, fi, fr, gl, he, hi, hr, hu, + it, ja, ko, nb, nl, no, pl, pt, ro, ru, sat, si, sk, sl, sr, sv, tr, uk, vi, zh +); + +mod api; +mod inbox; +mod mail; +mod utils; +#[macro_use] +mod template_utils; +mod routes; +#[macro_use] +extern crate shrinkwraprs; +#[cfg(feature = "test")] +mod test_routes; + +include!(concat!(env!("OUT_DIR"), "/templates.rs")); + +compile_i18n!(); + +/// Initializes a database pool. +fn init_pool() -> Option<DbPool> { + let manager = ConnectionManager::<Connection>::new(CONFIG.database_url.as_str()); + let mut builder = DbPool::builder() + .connection_customizer(Box::new(PragmaForeignKey)) + .min_idle(CONFIG.db_min_idle); + if let Some(max_size) = CONFIG.db_max_size { + builder = builder.max_size(max_size); + }; + let pool = builder.build(manager).ok()?; + let conn = pool.get().unwrap(); + Instance::cache_local(&conn); + let _ = Instance::create_local_instance_user(&conn); + Instance::cache_local_instance_user(&conn); + Some(pool) +} + +pub(crate) fn init_rocket() -> rocket::Rocket { + match dotenv::dotenv() { + Ok(path) => eprintln!("Configuration read from {}", path.display()), + Err(ref e) if e.not_found() => eprintln!("no .env was found"), + e => e.map(|_| ()).unwrap(), + } + tracing_subscriber::fmt::init(); + + 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 + let searcher = Arc::new(UnmanagedSearcher::open_or_recreate( + &CONFIG.search_index, + &CONFIG.search_tokenizers, + )); + RemoteFetchActor::init(dbpool.clone()); + SearchActor::init(searcher.clone(), dbpool.clone()); + let commiter = searcher.clone(); + workpool.execute_with_fixed_delay( + Duration::from_secs(5), + Duration::from_secs(60 * 30), + move || commiter.commit(), + ); + + let search_unlocker = searcher.clone(); + ctrlc::set_handler(move || { + search_unlocker.commit(); + search_unlocker.drop_writer(); + exit(0); + }) + .expect("Error setting Ctrl-c handler"); + + let mail = mail::init(); + if mail.is_none() && CONFIG.rocket.as_ref().unwrap().environment.is_prod() { + warn!("Warning: the email server is not configured (or not completely)."); + warn!("Please refer to the documentation to see how to configure it."); + } + + rocket::custom(CONFIG.rocket.clone().unwrap()) + .mount( + "/", + routes![ + routes::blogs::details, + routes::blogs::activity_details, + routes::blogs::outbox, + routes::blogs::outbox_page, + routes::blogs::new, + routes::blogs::new_auth, + routes::blogs::create, + routes::blogs::delete, + routes::blogs::edit, + routes::blogs::update, + routes::blogs::atom_feed, + routes::comments::create, + routes::comments::delete, + routes::comments::activity_pub, + routes::email_signups::create, + routes::email_signups::created, + routes::email_signups::show, + routes::email_signups::signup, + routes::instance::index, + routes::instance::admin, + routes::instance::admin_mod, + routes::instance::admin_instances, + routes::instance::admin_users, + routes::instance::admin_email_blocklist, + routes::instance::add_email_blocklist, + routes::instance::delete_email_blocklist, + routes::instance::edit_users, + routes::instance::toggle_block, + routes::instance::update_settings, + routes::instance::shared_inbox, + routes::instance::interact, + routes::instance::nodeinfo, + routes::instance::about, + routes::instance::privacy, + routes::instance::web_manifest, + routes::likes::create, + routes::likes::create_auth, + routes::medias::list, + routes::medias::new, + routes::medias::upload, + routes::medias::details, + routes::medias::delete, + routes::medias::set_avatar, + routes::notifications::notifications, + routes::notifications::notifications_auth, + routes::posts::details, + routes::posts::activity_details, + routes::posts::edit, + routes::posts::update, + routes::posts::new, + routes::posts::new_auth, + routes::posts::create, + routes::posts::delete, + routes::posts::remote_interact, + routes::posts::remote_interact_post, + routes::reshares::create, + routes::reshares::create_auth, + routes::search::search, + routes::session::new, + routes::session::create, + routes::session::delete, + routes::session::password_reset_request_form, + routes::session::password_reset_request, + routes::session::password_reset_form, + routes::session::password_reset, + routes::theme_files, + routes::plume_static_files, + routes::static_files, + routes::plume_media_files, + routes::tags::tag, + routes::timelines::details, + routes::timelines::new, + routes::timelines::create, + routes::timelines::edit, + routes::timelines::update, + routes::timelines::delete, + routes::user::me, + routes::user::details, + routes::user::dashboard, + routes::user::dashboard_auth, + routes::user::followers, + routes::user::followed, + routes::user::edit, + routes::user::edit_auth, + routes::user::update, + routes::user::delete, + routes::user::follow, + routes::user::follow_not_connected, + routes::user::follow_auth, + routes::user::activity_details, + routes::user::outbox, + routes::user::outbox_page, + routes::user::inbox, + routes::user::ap_followers, + routes::user::new, + routes::user::create, + routes::user::atom_feed, + routes::well_known::host_meta, + routes::well_known::nodeinfo, + routes::well_known::webfinger, + routes::errors::csrf_violation + ], + ) + .mount( + "/api/v1", + routes![ + api::oauth, + api::apps::create, + api::posts::get, + api::posts::list, + api::posts::create, + api::posts::delete, + ], + ) + .register(catchers![ + routes::errors::not_found, + routes::errors::unprocessable_entity, + routes::errors::server_error + ]) + .manage(Arc::new(Mutex::new(mail))) + .manage::<Arc<Mutex<Vec<routes::session::ResetRequest>>>>(Arc::new(Mutex::new(vec![]))) + .manage(dbpool) + .manage(Arc::new(workpool)) + .manage(searcher) + .manage(include_i18n!()) + .attach( + CsrfFairingBuilder::new() + .set_default_target( + "/csrf-violation?target=<uri>".to_owned(), + rocket::http::Method::Post, + ) + .add_exceptions(vec![ + ("/inbox".to_owned(), "/inbox".to_owned(), None), + ( + "/@/<name>/inbox".to_owned(), + "/@/<name>/inbox".to_owned(), + None, + ), + ("/api/<path..>".to_owned(), "/api/<path..>".to_owned(), None), + ]) + .finalize() + .expect("main: csrf fairing creation error"), + ) +} + +fn main() { + let rocket = init_rocket(); + + #[cfg(feature = "test")] + let rocket = rocket.mount("/test", routes![test_routes::health,]); + + rocket.launch(); +} diff --git a/src/routes/blogs.rs b/src/routes/blogs.rs new file mode 100644 index 00000000000..c56b5a6ee22 --- /dev/null +++ b/src/routes/blogs.rs @@ -0,0 +1,527 @@ +use activitypub::collection::{OrderedCollection, OrderedCollectionPage}; +use diesel::SaveChangesDsl; +use rocket::{ + http::ContentType, + request::LenientForm, + response::{content::Content, Flash, Redirect}, +}; +use rocket_i18n::I18n; +use std::{borrow::Cow, collections::HashMap}; +use validator::{Validate, ValidationError, ValidationErrors}; + +use crate::routes::{errors::ErrorPage, Page, RespondOrRedirect}; +use crate::template_utils::{IntoContext, Ructe}; +use crate::utils::requires_login; +use plume_common::activity_pub::{ActivityStream, ApRequest}; +use plume_common::utils; +use plume_models::{ + blog_authors::*, blogs::*, db_conn::DbConn, instance::Instance, medias::*, posts::Post, + safe_string::SafeString, users::User, Connection, PlumeRocket, +}; + +#[get("/~/<name>?<page>", rank = 2)] +pub fn details( + name: String, + page: Option<Page>, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Ructe, ErrorPage> { + let page = page.unwrap_or_default(); + let blog = Blog::find_by_fqn(&conn, &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)?; + + Ok(render!(blogs::details( + &(&conn, &rockets).to_context(), + blog, + authors, + page.0, + Page::total(articles_count as i32), + posts + ))) +} + +#[get("/~/<name>", rank = 1)] +pub fn activity_details( + name: String, + conn: DbConn, + _ap: ApRequest, +) -> Option<ActivityStream<CustomGroup>> { + let blog = Blog::find_by_fqn(&conn, &name).ok()?; + Some(ActivityStream::new(blog.to_activity(&conn).ok()?)) +} + +#[get("/blogs/new")] +pub fn new(conn: DbConn, rockets: PlumeRocket, _user: User) -> Ructe { + render!(blogs::new( + &(&conn, &rockets).to_context(), + &NewBlogForm::default(), + ValidationErrors::default() + )) +} + +#[get("/blogs/new", rank = 2)] +pub fn new_auth(i18n: I18n) -> Flash<Redirect> { + requires_login( + &i18n!( + i18n.catalog, + "To create a new blog, you need to be logged in" + ), + uri!(new), + ) +} + +#[derive(Default, FromForm, Validate)] +pub struct NewBlogForm { + #[validate(custom(function = "valid_slug", message = "Invalid name"))] + pub title: String, +} + +fn valid_slug(title: &str) -> Result<(), ValidationError> { + let slug = utils::make_actor_id(title); + if slug.is_empty() { + Err(ValidationError::new("empty_slug")) + } else { + Ok(()) + } +} + +#[post("/blogs/new", data = "<form>")] +pub fn create( + form: LenientForm<NewBlogForm>, + conn: DbConn, + rockets: PlumeRocket, +) -> RespondOrRedirect { + let slug = utils::make_actor_id(&form.title); + let intl = &rockets.intl.catalog; + let user = rockets.user.clone().unwrap(); + + let mut errors = match form.validate() { + Ok(_) => ValidationErrors::new(), + Err(e) => e, + }; + if Blog::find_by_fqn(&conn, &slug).is_ok() { + errors.add( + "title", + ValidationError { + code: Cow::from("existing_slug"), + message: Some(Cow::from(i18n!( + intl, + "A blog with the same name already exists." + ))), + params: HashMap::new(), + }, + ); + } + + if !errors.is_empty() { + return render!(blogs::new(&(&conn, &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, page = _)), + &i18n!(intl, "Your blog was successfully created!"), + ) + .into() +} + +#[post("/~/<name>/delete")] +pub fn delete(name: String, conn: DbConn, rockets: PlumeRocket) -> RespondOrRedirect { + let blog = Blog::find_by_fqn(&conn, &name).expect("blog::delete: blog not found"); + + if rockets + .user + .clone() + .and_then(|u| u.is_author_in(&conn, &blog).ok()) + .unwrap_or(false) + { + blog.delete(&*conn).expect("blog::expect: deletion error"); + Flash::success( + Redirect::to(uri!(super::instance::index)), + i18n!(rockets.intl.catalog, "Your blog was deleted."), + ) + .into() + } else { + // TODO actually return 403 error code + render!(errors::not_authorized( + &(&conn, &rockets).to_context(), + i18n!( + rockets.intl.catalog, + "You are not allowed to delete this blog." + ) + )) + .into() + } +} + +#[derive(FromForm, Validate)] +pub struct EditForm { + #[validate(custom(function = "valid_slug", message = "Invalid name"))] + pub title: String, + pub summary: String, + pub icon: Option<i32>, + pub banner: Option<i32>, + pub theme: Option<String>, +} + +#[get("/~/<name>/edit")] +pub fn edit(name: String, conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> { + let blog = Blog::find_by_fqn(&conn, &name)?; + 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"); + let medias = Media::for_user(&conn, user.id).expect("Couldn't list media"); + Ok(render!(blogs::edit( + &(&conn, &rockets).to_context(), + &blog, + medias, + &EditForm { + title: blog.title.clone(), + summary: blog.summary.clone(), + icon: blog.icon_id, + banner: blog.banner_id, + theme: blog.theme.clone(), + }, + ValidationErrors::default() + ))) + } else { + // TODO actually return 403 error code + Ok(render!(errors::not_authorized( + &(&conn, &rockets).to_context(), + i18n!( + rockets.intl.catalog, + "You are not allowed to edit this blog." + ) + ))) + } +} + +/// Returns true if the media is owned by `user` and is a picture +fn check_media(conn: &Connection, id: i32, user: &User) -> bool { + if let Ok(media) = Media::get(conn, id) { + media.owner_id == user.id && media.category() == MediaCategory::Image + } else { + false + } +} + +#[put("/~/<name>/edit", data = "<form>")] +pub fn update( + name: String, + form: LenientForm<EditForm>, + conn: DbConn, + rockets: PlumeRocket, +) -> RespondOrRedirect { + let intl = &rockets.intl.catalog; + let mut blog = Blog::find_by_fqn(&conn, &name).expect("blog::update: blog not found"); + if !rockets + .user + .clone() + .and_then(|u| u.is_author_in(&conn, &blog).ok()) + .unwrap_or(false) + { + // TODO actually return 403 error code + return render!(errors::not_authorized( + &(&conn, &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.theme = form.theme.clone(); + 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( + &(&conn, &rockets).to_context(), + &blog, + medias, + &*form, + err + )) + }) + .unwrap() + .into() +} + +#[get("/~/<name>/outbox")] +pub fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection>> { + let blog = Blog::find_by_fqn(&conn, &name).ok()?; + blog.outbox(&conn).ok() +} +#[allow(unused_variables)] +#[get("/~/<name>/outbox?<page>")] +pub fn outbox_page( + name: String, + page: Page, + conn: DbConn, +) -> Option<ActivityStream<OrderedCollectionPage>> { + let blog = Blog::find_by_fqn(&conn, &name).ok()?; + blog.outbox_page(&conn, page.limits()).ok() +} +#[get("/~/<name>/atom.xml")] +pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> { + let blog = Blog::find_by_fqn(&conn, &name).ok()?; + let entries = Post::get_recents_for_blog(&*conn, &blog, 15).ok()?; + let uri = Instance::get_local() + .ok()? + .compute_box("~", &name, "atom.xml"); + let title = &blog.title; + let default_updated = &blog.creation_date; + let feed = super::build_atom_feed(entries, &uri, title, default_updated, &conn); + Some(Content( + ContentType::new("application", "atom+xml"), + feed.to_string(), + )) +} + +#[cfg(test)] +mod tests { + use crate::init_rocket; + use diesel::Connection; + use plume_common::utils::random_hex; + use plume_models::{ + blog_authors::{BlogAuthor, NewBlogAuthor}, + blogs::{Blog, NewBlog}, + db_conn::{DbConn, DbPool}, + instance::{Instance, NewInstance}, + post_authors::{NewPostAuthor, PostAuthor}, + posts::{NewPost, Post}, + safe_string::SafeString, + users::{NewUser, User, AUTH_COOKIE}, + Connection as Conn, CONFIG, + }; + use rocket::{ + http::{Cookie, Cookies, SameSite}, + local::{Client, LocalRequest}, + }; + + #[test] + fn edit_link_within_post_card() { + let conn = Conn::establish(CONFIG.database_url.as_str()).unwrap(); + Instance::insert( + &conn, + NewInstance { + public_domain: "example.org".to_string(), + name: "Plume".to_string(), + local: true, + long_description: SafeString::new(""), + short_description: SafeString::new(""), + default_license: "CC-BY-SA".to_string(), + open_registrations: true, + short_description_html: String::new(), + long_description_html: String::new(), + }, + ) + .unwrap(); + let rocket = init_rocket(); + let client = Client::new(rocket).expect("valid rocket instance"); + let dbpool = client.rocket().state::<DbPool>().unwrap(); + let conn = &DbConn(dbpool.get().unwrap()); + + let (_instance, user, blog, post) = create_models(conn); + + let blog_path = uri!(super::activity_details: name = &blog.fqn).to_string(); + let edit_link = uri!( + super::super::posts::edit: blog = &blog.fqn, + slug = &post.slug + ) + .to_string(); + + let mut response = client.get(&blog_path).dispatch(); + let body = response.body_string().unwrap(); + assert!(!body.contains(&edit_link)); + + let request = client.get(&blog_path); + login(&request, &user); + let mut response = request.dispatch(); + let body = response.body_string().unwrap(); + assert!(body.contains(&edit_link)); + } + + fn create_models(conn: &DbConn) -> (Instance, User, Blog, Post) { + conn.transaction::<(Instance, User, Blog, Post), diesel::result::Error, _>(|| { + let instance = Instance::get_local().unwrap_or_else(|_| { + let instance = Instance::insert( + conn, + NewInstance { + default_license: "CC-0-BY-SA".to_string(), + local: true, + long_description: SafeString::new("Good morning"), + long_description_html: "<p>Good morning</p>".to_string(), + short_description: SafeString::new("Hello"), + short_description_html: "<p>Hello</p>".to_string(), + name: random_hex().to_string(), + open_registrations: true, + public_domain: random_hex().to_string(), + }, + ) + .unwrap(); + Instance::cache_local(conn); + instance + }); + let mut user = NewUser::default(); + user.instance_id = instance.id; + user.username = random_hex().to_string(); + user.ap_url = random_hex().to_string(); + user.inbox_url = random_hex().to_string(); + user.outbox_url = random_hex().to_string(); + user.followers_endpoint = random_hex().to_string(); + let user = User::insert(conn, user).unwrap(); + let mut blog = NewBlog::default(); + blog.instance_id = instance.id; + blog.actor_id = random_hex().to_string(); + blog.ap_url = random_hex().to_string(); + blog.inbox_url = random_hex().to_string(); + blog.outbox_url = random_hex().to_string(); + let blog = Blog::insert(conn, blog).unwrap(); + BlogAuthor::insert( + conn, + NewBlogAuthor { + blog_id: blog.id, + author_id: user.id, + is_owner: true, + }, + ) + .unwrap(); + let post = Post::insert( + conn, + NewPost { + blog_id: blog.id, + slug: random_hex()[..8].to_owned(), + title: random_hex()[..8].to_owned(), + content: SafeString::new(""), + published: true, + license: "CC-By-SA".to_owned(), + ap_url: "".to_owned(), + creation_date: None, + subtitle: "".to_owned(), + source: "".to_owned(), + cover_id: None, + }, + ) + .unwrap(); + PostAuthor::insert( + conn, + NewPostAuthor { + post_id: post.id, + author_id: user.id, + }, + ) + .unwrap(); + + Ok((instance, user, blog, post)) + }) + .unwrap() + } + + fn login(request: &LocalRequest, user: &User) { + request.inner().guard::<Cookies>().unwrap().add_private( + Cookie::build(AUTH_COOKIE, user.id.to_string()) + .same_site(SameSite::Lax) + .finish(), + ); + } +} diff --git a/src/routes/comments.rs b/src/routes/comments.rs new file mode 100644 index 00000000000..5585afff246 --- /dev/null +++ b/src/routes/comments.rs @@ -0,0 +1,193 @@ +use crate::template_utils::Ructe; +use activitypub::object::Note; +use rocket::{ + request::LenientForm, + response::{Flash, Redirect}, +}; +use validator::Validate; + +use std::time::Duration; + +use crate::routes::errors::ErrorPage; +use crate::template_utils::IntoContext; +use plume_common::{ + activity_pub::{broadcast, ActivityStream, ApRequest}, + utils, +}; +use plume_models::{ + blogs::Blog, comments::*, db_conn::DbConn, inbox::inbox, instance::Instance, medias::Media, + mentions::Mention, posts::Post, safe_string::SafeString, tags::Tag, users::User, Error, + PlumeRocket, CONFIG, +}; + +#[derive(Default, FromForm, Debug, Validate)] +pub struct NewCommentForm { + pub responding_to: Option<i32>, + #[validate(length(min = 1, message = "Your comment can't be empty"))] + pub content: String, + pub warning: String, +} + +#[post("/~/<blog_name>/<slug>/comment", data = "<form>")] +pub fn create( + blog_name: String, + slug: String, + form: LenientForm<NewCommentForm>, + user: User, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Flash<Redirect>, Ructe> { + let blog = Blog::find_by_fqn(&conn, &blog_name).expect("comments::create: blog error"); + let post = Post::find_by_slug(&conn, &slug, blog.id).expect("comments::create: post error"); + form.validate() + .map(|_| { + let (html, mentions, _hashtags) = utils::md_to_html( + form.content.as_ref(), + Some( + &Instance::get_local() + .expect("comments::create: local instance error") + .public_domain, + ), + true, + Some(Media::get_media_processor(&conn, vec![&user])), + ); + let comm = Comment::insert( + &conn, + NewComment { + content: SafeString::new(html.as_ref()), + in_response_to_id: form.responding_to, + post_id: post.id, + author_id: user.id, + ap_url: None, + sensitive: !form.warning.is_empty(), + spoiler_text: form.warning.clone(), + public_visibility: true, + }, + ) + .expect("comments::create: insert error"); + let new_comment = comm + .create_activity(&conn) + .expect("comments::create: activity error"); + + // save mentions + for ment in mentions { + Mention::from_activity( + &conn, + &Mention::build_activity(&conn, &ment) + .expect("comments::create: build mention error"), + comm.id, + false, + true, + ) + .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(); + rockets.worker.execute(move || { + broadcast(&user_clone, new_comment, dest, CONFIG.proxy().cloned()) + }); + + 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| { + // TODO: de-duplicate this code + let comments = CommentTree::from_post(&conn, &post, Some(&user)) + .expect("comments::create: comments error"); + + let previous = form.responding_to.and_then(|r| Comment::get(&conn, r).ok()); + + render!(posts::details( + &(&conn, &rockets).to_context(), + post.clone(), + blog, + &*form, + errors, + Tag::for_post(&conn, post.id).expect("comments::create: tags error"), + comments, + previous, + post.count_likes(&conn) + .expect("comments::create: count likes error"), + post.count_reshares(&conn) + .expect("comments::create: count reshares error"), + user.has_liked(&conn, &post) + .expect("comments::create: liked error"), + user.has_reshared(&conn, &post) + .expect("comments::create: reshared error"), + user.is_following( + &*conn, + post.get_authors(&conn) + .expect("comments::create: authors error")[0] + .id + ) + .expect("comments::create: following error"), + post.get_authors(&conn) + .expect("comments::create: authors error")[0] + .clone() + )) + }) +} + +#[post("/~/<blog>/<slug>/comment/<id>/delete")] +pub fn delete( + blog: String, + slug: String, + id: i32, + user: User, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Flash<Redirect>, ErrorPage> { + if let Ok(comment) = Comment::get(&conn, id) { + if comment.author_id == user.id { + let dest = User::one_by_instance(&conn)?; + let delete_activity = comment.build_delete(&conn)?; + inbox( + &conn, + serde_json::to_value(&delete_activity).map_err(Error::from)?, + )?; + + let user_c = user.clone(); + rockets.worker.execute(move || { + broadcast(&user_c, delete_activity, dest, CONFIG.proxy().cloned()) + }); + rockets + .worker + .execute_after(Duration::from_secs(10 * 60), move || { + user.rotate_keypair(&conn) + .expect("Failed to rotate keypair"); + }); + } + } + Ok(Flash::success( + Redirect::to(uri!( + super::posts::details: blog = blog, + slug = slug, + responding_to = _ + )), + i18n!(&rockets.intl.catalog, "Your comment has been deleted."), + )) +} + +#[get("/~/<_blog>/<_slug>/comment/<id>")] +pub fn activity_pub( + _blog: String, + _slug: String, + id: i32, + _ap: ApRequest, + conn: DbConn, +) -> Option<ActivityStream<Note>> { + Comment::get(&conn, id) + .and_then(|c| c.to_activity(&conn)) + .ok() + .map(ActivityStream::new) +} diff --git a/src/routes/email_signups.rs b/src/routes/email_signups.rs new file mode 100644 index 00000000000..7a364dd0f27 --- /dev/null +++ b/src/routes/email_signups.rs @@ -0,0 +1,223 @@ +use crate::{ + mail::{build_mail, Mailer}, + routes::{errors::ErrorPage, RespondOrRedirect}, + template_utils::{IntoContext, Ructe}, +}; +use plume_models::{ + db_conn::DbConn, email_signups::EmailSignup, instance::Instance, lettre::Transport, signups, + Error, PlumeRocket, CONFIG, +}; +use rocket::{ + http::Status, + request::LenientForm, + response::{Flash, Redirect}, + State, +}; +use std::sync::{Arc, Mutex}; +use tracing::warn; +use validator::{Validate, ValidationError, ValidationErrors}; + +#[derive(Default, FromForm, Validate)] +#[validate(schema( + function = "emails_match", + skip_on_field_errors = false, + message = "Emails are not matching" +))] +pub struct EmailSignupForm { + #[validate(email(message = "Invalid email"))] + pub email: String, + #[validate(email(message = "Invalid email"))] + pub email_confirmation: String, +} + +fn emails_match(form: &EmailSignupForm) -> Result<(), ValidationError> { + if form.email_confirmation == form.email { + Ok(()) + } else { + Err(ValidationError::new("emails_match")) + } +} + +#[derive(Default, FromForm, Validate)] +#[validate(schema( + function = "passwords_match", + skip_on_field_errors = false, + message = "Passwords are not matching" +))] +pub struct NewUserForm { + #[validate(length(min = 1, message = "Username should be at least 1 characters long"))] + pub username: String, + #[validate(length(min = 8, message = "Password should be at least 8 characters long"))] + pub password: String, + #[validate(length(min = 8, message = "Password should be at least 8 characters long"))] + pub password_confirmation: String, + pub email: String, + pub token: String, +} + +pub fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> { + if form.password != form.password_confirmation { + Err(ValidationError::new("password_match")) + } else { + Ok(()) + } +} + +#[post("/email_signups/new", data = "<form>")] +pub fn create( + mail: State<'_, Arc<Mutex<Mailer>>>, + form: LenientForm<EmailSignupForm>, + conn: DbConn, + rockets: PlumeRocket, + _enabled: signups::Email, +) -> Result<RespondOrRedirect, ErrorPage> { + let registration_open = Instance::get_local() + .map(|i| i.open_registrations) + .unwrap_or(true); + + if !registration_open { + return Ok(Flash::error( + Redirect::to(uri!(super::user::new)), + i18n!( + rockets.intl.catalog, + "Registrations are closed on this instance." + ), + ) + .into()); // Actually, it is an error + } + let mut form = form.into_inner(); + form.email = form.email.trim().to_owned(); + if let Err(err) = form.validate() { + return Ok(render!(email_signups::new( + &(&conn, &rockets).to_context(), + registration_open, + &form, + err + )) + .into()); + } + let res = EmailSignup::start(&conn, &form.email); + if let Some(err) = res.as_ref().err() { + return Ok(match err { + Error::UserAlreadyExists => { + // TODO: Notify to admin (and the user?) + warn!("Registration attempted for existing user: {}. Registraion halted and email sending skipped.", &form.email); + render!(email_signups::create(&(&conn, &rockets).to_context())).into() + } + Error::NotFound => render!(errors::not_found(&(&conn, &rockets).to_context())).into(), + _ => render!(errors::not_found(&(&conn, &rockets).to_context())).into(), // FIXME + }); + } + let token = res.unwrap(); + let url = format!( + "https://{}{}", + CONFIG.base_url, + uri!(show: token = token.to_string()) + ); + let message = build_mail( + form.email, + i18n!(rockets.intl.catalog, "User registration"), + i18n!(rockets.intl.catalog, "Here is the link for registration: {0}"; url), + ) + .expect("Mail configuration has already been done at ignition process"); + // TODO: Render error page + if let Some(ref mut mailer) = *mail.lock().unwrap() { + mailer.send(message.into()).ok(); // TODO: Render error page + } + + Ok(render!(email_signups::create(&(&conn, &rockets).to_context())).into()) +} + +#[get("/email_signups/new")] +pub fn created(conn: DbConn, rockets: PlumeRocket, _enabled: signups::Email) -> Ructe { + render!(email_signups::create(&(&conn, &rockets).to_context())) +} + +#[get("/email_signups/<token>")] +pub fn show( + token: String, + conn: DbConn, + rockets: PlumeRocket, + _enabled: signups::Email, +) -> Result<Ructe, ErrorPage> { + let signup = EmailSignup::find_by_token(&conn, token.into())?; + let confirmation = signup.confirm(&conn); + if let Some(err) = confirmation.err() { + match err { + Error::Expired => { + return Ok(render!(email_signups::new( + &(&conn, &rockets).to_context(), + Instance::get_local()?.open_registrations, + &EmailSignupForm::default(), + ValidationErrors::default() + ))) + } // TODO: Flash and redirect + Error::NotFound => return Err(Error::NotFound.into()), + _ => return Err(Error::NotFound.into()), // FIXME + } + } + + let form = NewUserForm { + email: signup.email, + token: signup.token, + ..NewUserForm::default() + }; + Ok(render!(email_signups::edit( + &(&conn, &rockets).to_context(), + Instance::get_local()?.open_registrations, + &form, + ValidationErrors::default() + ))) +} + +#[post("/email_signups/signup", data = "<form>")] +pub fn signup( + form: LenientForm<NewUserForm>, + conn: DbConn, + rockets: PlumeRocket, + _enabled: signups::Email, +) -> Result<RespondOrRedirect, Status> { + use RespondOrRedirect::{FlashRedirect, Response}; + + let instance = Instance::get_local().map_err(|e| { + warn!("{:?}", e); + Status::InternalServerError + })?; + if let Some(err) = form.validate().err() { + return Ok(Response(render!(email_signups::edit( + &(&conn, &rockets).to_context(), + instance.open_registrations, + &form, + err + )))); + } + let signup = EmailSignup::find_by_token(&conn, form.token.clone().into()) + .map_err(|_| Status::NotFound)?; + if form.email != signup.email { + let mut err = ValidationErrors::default(); + err.add("email", ValidationError::new("Email couldn't changed")); + let form = NewUserForm { + email: signup.email, + ..form.into_inner() + }; + return Ok(Response(render!(email_signups::edit( + &(&conn, &rockets).to_context(), + instance.open_registrations, + &form, + err + )))); + } + let _user = signup + .complete(&conn, form.username.clone(), form.password.clone()) + .map_err(|e| { + warn!("{:?}", e); + Status::UnprocessableEntity + })?; + Ok(FlashRedirect(Flash::success( + Redirect::to(uri!(super::session::new: m = _)), + i18n!( + rockets.intl.catalog, + "Your account has been created. Now you just need to log in, before you can use it." + ), + ))) +} diff --git a/src/routes/errors.rs b/src/routes/errors.rs new file mode 100644 index 00000000000..74dc4dd5870 --- /dev/null +++ b/src/routes/errors.rs @@ -0,0 +1,60 @@ +use crate::template_utils::{IntoContext, Ructe}; +use plume_models::{db_conn::DbConn, Error, PlumeRocket}; +use rocket::{ + http::Status, + response::{self, Responder}, + Request, +}; +use tracing::warn; + +#[derive(Debug)] +pub struct ErrorPage(Error); + +impl From<Error> for ErrorPage { + fn from(err: Error) -> ErrorPage { + ErrorPage(err) + } +} + +impl<'r> Responder<'r> for ErrorPage { + fn respond_to(self, _req: &Request<'_>) -> response::Result<'r> { + warn!("{:?}", self.0); + + match self.0 { + Error::NotFound => Err(Status::NotFound), + Error::Unauthorized => Err(Status::NotFound), + _ => Err(Status::InternalServerError), + } + } +} + +#[catch(404)] +pub fn not_found(req: &Request<'_>) -> Ructe { + let conn = req.guard::<DbConn>().unwrap(); + let rockets = req.guard::<PlumeRocket>().unwrap(); + render!(errors::not_found(&(&conn, &rockets).to_context())) +} + +#[catch(422)] +pub fn unprocessable_entity(req: &Request<'_>) -> Ructe { + let conn = req.guard::<DbConn>().unwrap(); + let rockets = req.guard::<PlumeRocket>().unwrap(); + render!(errors::unprocessable_entity( + &(&conn, &rockets).to_context() + )) +} + +#[catch(500)] +pub fn server_error(req: &Request<'_>) -> Ructe { + let conn = req.guard::<DbConn>().unwrap(); + let rockets = req.guard::<PlumeRocket>().unwrap(); + render!(errors::server_error(&(&conn, &rockets).to_context())) +} + +#[post("/csrf-violation?<target>")] +pub fn csrf_violation(target: Option<String>, conn: DbConn, rockets: PlumeRocket) -> Ructe { + if let Some(uri) = target { + warn!("Csrf violation while accessing \"{}\"", uri) + } + render!(errors::csrf(&(&conn, &rockets).to_context())) +} diff --git a/src/routes/instance.rs b/src/routes/instance.rs new file mode 100644 index 00000000000..cedaa90050e --- /dev/null +++ b/src/routes/instance.rs @@ -0,0 +1,501 @@ +use rocket::{ + request::{Form, FormItems, FromForm, LenientForm}, + response::{status, Flash, Redirect}, +}; +use rocket_contrib::json::Json; +use rocket_i18n::I18n; +use scheduled_thread_pool::ScheduledThreadPool; +use std::str::FromStr; +use validator::{Validate, ValidationErrors}; + +use crate::inbox; +use crate::routes::{errors::ErrorPage, rocket_uri_macro_static_files, Page, RespondOrRedirect}; +use crate::template_utils::{IntoContext, Ructe}; +use plume_common::activity_pub::{broadcast, inbox::FromId}; +use plume_models::{ + admin::*, + blocklisted_emails::*, + comments::Comment, + db_conn::DbConn, + headers::Headers, + instance::*, + posts::Post, + safe_string::SafeString, + timeline::Timeline, + users::{Role, User}, + Connection, Error, PlumeRocket, CONFIG, +}; + +#[get("/")] +pub fn index(conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> { + let inst = Instance::get_local()?; + let timelines = Timeline::list_all_for_user(&conn, rockets.user.clone().map(|u| u.id))? + .into_iter() + .filter_map(|t| { + if let Ok(latest) = t.get_latest(&conn, 12) { + Some((t, latest)) + } else { + None + } + }) + .collect(); + + Ok(render!(instance::index( + &(&conn, &rockets).to_context(), + inst, + User::count_local(&conn)?, + Post::count_local(&conn)?, + timelines + ))) +} + +#[get("/admin")] +pub fn admin(_admin: Admin, conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> { + let local_inst = Instance::get_local()?; + Ok(render!(instance::admin( + &(&conn, &rockets).to_context(), + local_inst.clone(), + InstanceSettingsForm { + name: local_inst.name.clone(), + open_registrations: local_inst.open_registrations, + short_description: local_inst.short_description, + long_description: local_inst.long_description, + default_license: local_inst.default_license, + }, + ValidationErrors::default() + ))) +} + +#[get("/admin", rank = 2)] +pub fn admin_mod(_mod: Moderator, conn: DbConn, rockets: PlumeRocket) -> Ructe { + render!(instance::admin_mod(&(&conn, &rockets).to_context())) +} + +#[derive(Clone, FromForm, Validate)] +pub struct InstanceSettingsForm { + #[validate(length(min = 1))] + pub name: String, + pub open_registrations: bool, + pub short_description: SafeString, + pub long_description: SafeString, + #[validate(length(min = 1))] + pub default_license: String, +} + +#[post("/admin", data = "<form>")] +pub fn update_settings( + _admin: Admin, + form: LenientForm<InstanceSettingsForm>, + conn: DbConn, + rockets: PlumeRocket, +) -> RespondOrRedirect { + if let Err(e) = form.validate() { + let local_inst = + Instance::get_local().expect("instance::update_settings: local instance error"); + render!(instance::admin( + &(&conn, &rockets).to_context(), + local_inst, + form.clone(), + e + )) + .into() + } else { + let instance = + Instance::get_local().expect("instance::update_settings: local instance error"); + instance + .update( + &*conn, + form.name.clone(), + form.open_registrations, + form.short_description.clone(), + form.long_description.clone(), + form.default_license.clone(), + ) + .expect("instance::update_settings: save error"); + Flash::success( + Redirect::to(uri!(admin)), + i18n!(rockets.intl.catalog, "Instance settings have been saved."), + ) + .into() + } +} + +#[get("/admin/instances?<page>")] +pub fn admin_instances( + _mod: Moderator, + page: Option<Page>, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Ructe, ErrorPage> { + let page = page.unwrap_or_default(); + let instances = Instance::page(&conn, page.limits())?; + Ok(render!(instance::list( + &(&conn, &rockets).to_context(), + Instance::get_local()?, + instances, + page.0, + Page::total(Instance::count(&conn)? as i32) + ))) +} + +#[post("/admin/instances/<id>/block")] +pub fn toggle_block( + _mod: Moderator, + conn: DbConn, + id: i32, + intl: I18n, +) -> Result<Flash<Redirect>, ErrorPage> { + let inst = Instance::get(&conn, id)?; + let message = if inst.blocked { + i18n!(intl.catalog, "{} has been unblocked."; &inst.name) + } else { + i18n!(intl.catalog, "{} has been blocked."; &inst.name) + }; + + inst.toggle_block(&conn)?; + Ok(Flash::success( + Redirect::to(uri!(admin_instances: page = _)), + message, + )) +} + +#[get("/admin/users?<page>")] +pub fn admin_users( + _mod: Moderator, + page: Option<Page>, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Ructe, ErrorPage> { + let page = page.unwrap_or_default(); + Ok(render!(instance::users( + &(&conn, &rockets).to_context(), + User::get_local_page(&conn, page.limits())?, + page.0, + Page::total(User::count_local(&conn)? as i32) + ))) +} +pub struct BlocklistEmailDeletion { + ids: Vec<i32>, +} +impl<'f> FromForm<'f> for BlocklistEmailDeletion { + type Error = (); + fn from_form(items: &mut FormItems<'f>, _strict: bool) -> Result<BlocklistEmailDeletion, ()> { + let mut c: BlocklistEmailDeletion = BlocklistEmailDeletion { ids: Vec::new() }; + for item in items { + let key = item.key.parse::<i32>(); + if let Ok(i) = key { + c.ids.push(i); + } + } + Ok(c) + } +} +#[post("/admin/emails/delete", data = "<form>")] +pub fn delete_email_blocklist( + _mod: Moderator, + form: Form<BlocklistEmailDeletion>, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Flash<Redirect>, ErrorPage> { + BlocklistedEmail::delete_entries(&conn, form.0.ids)?; + Ok(Flash::success( + Redirect::to(uri!(admin_email_blocklist: page = None)), + i18n!(rockets.intl.catalog, "Blocks deleted"), + )) +} + +#[post("/admin/emails/new", data = "<form>")] +pub fn add_email_blocklist( + _mod: Moderator, + form: LenientForm<NewBlocklistedEmail>, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Flash<Redirect>, ErrorPage> { + let result = BlocklistedEmail::insert(&conn, form.0); + + if let Err(Error::Db(_)) = result { + Ok(Flash::error( + Redirect::to(uri!(admin_email_blocklist: page = None)), + i18n!(rockets.intl.catalog, "Email already blocked"), + )) + } else { + Ok(Flash::success( + Redirect::to(uri!(admin_email_blocklist: page = None)), + i18n!(rockets.intl.catalog, "Email Blocked"), + )) + } +} +#[get("/admin/emails?<page>")] +pub fn admin_email_blocklist( + _mod: Moderator, + page: Option<Page>, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Ructe, ErrorPage> { + let page = page.unwrap_or_default(); + Ok(render!(instance::emailblocklist( + &(&conn, &rockets).to_context(), + BlocklistedEmail::page(&conn, page.limits())?, + page.0, + Page::total(BlocklistedEmail::count(&conn)? as i32) + ))) +} + +/// A structure to handle forms that are a list of items on which actions are applied. +/// +/// This is for instance the case of the user list in the administration. +pub struct MultiAction<T> +where + T: FromStr, +{ + ids: Vec<i32>, + action: T, +} + +impl<'f, T> FromForm<'f> for MultiAction<T> +where + T: FromStr, +{ + type Error = (); + + fn from_form(items: &mut FormItems<'_>, _strict: bool) -> Result<Self, Self::Error> { + let (ids, act) = items.fold((vec![], None), |(mut ids, act), item| { + let (name, val) = item.key_value_decoded(); + + if name == "action" { + (ids, T::from_str(&val).ok()) + } else if let Ok(id) = name.parse::<i32>() { + ids.push(id); + (ids, act) + } else { + (ids, act) + } + }); + + if let Some(act) = act { + Ok(MultiAction { ids, action: act }) + } else { + Err(()) + } + } +} + +pub enum UserActions { + Admin, + RevokeAdmin, + Moderator, + RevokeModerator, + Ban, +} + +impl FromStr for UserActions { + type Err = (); + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "admin" => Ok(UserActions::Admin), + "un-admin" => Ok(UserActions::RevokeAdmin), + "moderator" => Ok(UserActions::Moderator), + "un-moderator" => Ok(UserActions::RevokeModerator), + "ban" => Ok(UserActions::Ban), + _ => Err(()), + } + } +} + +#[post("/admin/users/edit", data = "<form>")] +pub fn edit_users( + moderator: Moderator, + form: LenientForm<MultiAction<UserActions>>, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Flash<Redirect>, ErrorPage> { + // you can't change your own rights + if form.ids.contains(&moderator.0.id) { + return Ok(Flash::error( + Redirect::to(uri!(admin_users: page = _)), + i18n!(rockets.intl.catalog, "You can't change your own rights."), + )); + } + + // moderators can't grant or revoke admin rights + if !moderator.0.is_admin() { + match form.action { + UserActions::Admin | UserActions::RevokeAdmin => { + return Ok(Flash::error( + Redirect::to(uri!(admin_users: page = _)), + i18n!( + rockets.intl.catalog, + "You are not allowed to take this action." + ), + )) + } + _ => {} + } + } + + let worker = &*rockets.worker; + match form.action { + UserActions::Admin => { + for u in form.ids.clone() { + User::get(&conn, u)?.set_role(&conn, Role::Admin)?; + } + } + UserActions::Moderator => { + for u in form.ids.clone() { + User::get(&conn, u)?.set_role(&conn, Role::Moderator)?; + } + } + UserActions::RevokeAdmin | UserActions::RevokeModerator => { + for u in form.ids.clone() { + User::get(&conn, u)?.set_role(&conn, Role::Normal)?; + } + } + UserActions::Ban => { + for u in form.ids.clone() { + ban(u, &conn, worker)?; + } + } + } + + Ok(Flash::success( + Redirect::to(uri!(admin_users: page = _)), + i18n!(rockets.intl.catalog, "Done."), + )) +} + +fn ban(id: i32, conn: &Connection, worker: &ScheduledThreadPool) -> Result<(), ErrorPage> { + let u = User::get(&*conn, id)?; + u.delete(&*conn)?; + if Instance::get_local() + .map(|i| u.instance_id == i.id) + .unwrap_or(false) + { + BlocklistedEmail::insert( + conn, + NewBlocklistedEmail { + email_address: u.email.clone().unwrap(), + note: "Banned".to_string(), + notify_user: false, + notification_text: "".to_owned(), + }, + ) + .unwrap(); + let target = User::one_by_instance(&*conn)?; + let delete_act = u.delete_activity(&*conn)?; + worker.execute(move || broadcast(&u, delete_act, target, CONFIG.proxy().cloned())); + } + + Ok(()) +} + +#[post("/inbox", data = "<data>")] +pub fn shared_inbox( + conn: DbConn, + data: inbox::SignedJson<serde_json::Value>, + headers: Headers<'_>, +) -> Result<String, status::BadRequest<&'static str>> { + inbox::handle_incoming(conn, data, headers) +} + +#[get("/remote_interact?<target>")] +pub fn interact(conn: DbConn, user: Option<User>, target: String) -> Option<Redirect> { + if User::find_by_fqn(&conn, &target).is_ok() { + return Some(Redirect::to(uri!(super::user::details: name = target))); + } + + if let Ok(post) = Post::from_id(&conn, &target, None, CONFIG.proxy()) { + return Some(Redirect::to(uri!( + super::posts::details: blog = post.get_blog(&conn).expect("Can't retrieve blog").fqn, + slug = &post.slug, + responding_to = _ + ))); + } + + if let Ok(comment) = Comment::from_id(&conn, &target, None, CONFIG.proxy()) { + if comment.can_see(&conn, user.as_ref()) { + let post = comment.get_post(&conn).expect("Can't retrieve post"); + return Some(Redirect::to(uri!( + super::posts::details: blog = + post.get_blog(&conn).expect("Can't retrieve blog").fqn, + slug = &post.slug, + responding_to = comment.id + ))); + } + } + None +} + +#[get("/nodeinfo/<version>")] +pub fn nodeinfo(conn: DbConn, version: String) -> Result<Json<serde_json::Value>, ErrorPage> { + if version != "2.0" && version != "2.1" { + return Err(ErrorPage::from(Error::NotFound)); + } + + let local_inst = Instance::get_local()?; + let mut doc = json!({ + "version": version, + "software": { + "name": env!("CARGO_PKG_NAME"), + "version": env!("CARGO_PKG_VERSION"), + }, + "protocols": ["activitypub"], + "services": { + "inbound": [], + "outbound": [] + }, + "openRegistrations": local_inst.open_registrations, + "usage": { + "users": { + "total": User::count_local(&conn)? + }, + "localPosts": Post::count_local(&conn)?, + "localComments": Comment::count_local(&conn)? + }, + "metadata": { + "nodeName": local_inst.name, + "nodeDescription": local_inst.short_description + } + }); + + if version == "2.1" { + doc["software"]["repository"] = json!(env!("CARGO_PKG_REPOSITORY")); + } + + Ok(Json(doc)) +} + +#[get("/about")] +pub fn about(conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> { + Ok(render!(instance::about( + &(&conn, &rockets).to_context(), + Instance::get_local()?, + Instance::get_local()?.main_admin(&conn)?, + User::count_local(&conn)?, + Post::count_local(&conn)?, + Instance::count(&conn)? - 1 + ))) +} + +#[get("/privacy")] +pub fn privacy(conn: DbConn, rockets: PlumeRocket) -> Ructe { + render!(instance::privacy(&(&conn, &rockets).to_context())) +} + +#[get("/manifest.json")] +pub fn web_manifest() -> Result<Json<serde_json::Value>, ErrorPage> { + let instance = Instance::get_local()?; + Ok(Json(json!({ + "name": &instance.name, + "description": &instance.short_description, + "start_url": String::from("/"), + "scope": String::from("/"), + "display": String::from("standalone"), + "background_color": String::from("#f4f4f4"), + "theme_color": String::from("#7765e3"), + "categories": [String::from("social")], + "icons": CONFIG.logo.other.iter() + .map(|i| i.with_prefix(&uri!(static_files: file = "").to_string())) + .collect::<Vec<_>>() + }))) +} diff --git a/src/routes/likes.rs b/src/routes/likes.rs new file mode 100644 index 00000000000..25ca68bfd3d --- /dev/null +++ b/src/routes/likes.rs @@ -0,0 +1,61 @@ +use rocket::response::{Flash, Redirect}; +use rocket_i18n::I18n; + +use crate::routes::errors::ErrorPage; +use crate::utils::requires_login; +use plume_common::activity_pub::broadcast; +use plume_models::{ + blogs::Blog, db_conn::DbConn, inbox::inbox, likes, posts::Post, timeline::*, users::User, + Error, PlumeRocket, CONFIG, +}; + +#[post("/~/<blog>/<slug>/like")] +pub fn create( + blog: String, + slug: String, + user: User, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Redirect, ErrorPage> { + let b = Blog::find_by_fqn(&conn, &blog)?; + let post = Post::find_by_slug(&conn, &slug, b.id)?; + + if !user.has_liked(&*conn, &post)? { + let like = likes::Like::insert(&*conn, likes::NewLike::new(&post, &user))?; + like.notify(&*conn)?; + + Timeline::add_to_all_timelines(&conn, &post, Kind::Like(&user))?; + + let dest = User::one_by_instance(&*conn)?; + let act = like.to_activity(&*conn)?; + rockets + .worker + .execute(move || broadcast(&user, act, dest, CONFIG.proxy().cloned())); + } else { + let like = likes::Like::find_by_user_on_post(&conn, user.id, post.id)?; + let delete_act = like.build_undo(&conn)?; + inbox( + &conn, + serde_json::to_value(&delete_act).map_err(Error::from)?, + )?; + + let dest = User::one_by_instance(&conn)?; + rockets + .worker + .execute(move || broadcast(&user, delete_act, dest, CONFIG.proxy().cloned())); + } + + Ok(Redirect::to(uri!( + super::posts::details: blog = blog, + slug = slug, + responding_to = _ + ))) +} + +#[post("/~/<blog>/<slug>/like", rank = 2)] +pub fn create_auth(blog: String, slug: String, i18n: I18n) -> Flash<Redirect> { + requires_login( + &i18n!(i18n.catalog, "To like a post, you need to be logged in"), + uri!(create: blog = blog, slug = slug), + ) +} diff --git a/src/routes/medias.rs b/src/routes/medias.rs new file mode 100644 index 00000000000..8f920036625 --- /dev/null +++ b/src/routes/medias.rs @@ -0,0 +1,183 @@ +use crate::routes::{errors::ErrorPage, Page}; +use crate::template_utils::{IntoContext, Ructe}; +use guid_create::GUID; +use multipart::server::{ + save::{SaveResult, SavedData}, + Multipart, +}; +use plume_models::{db_conn::DbConn, medias::*, users::User, Error, PlumeRocket, CONFIG}; +use rocket::{ + http::ContentType, + response::{status, Flash, Redirect}, + Data, +}; +use rocket_i18n::I18n; +use std::fs; + +#[get("/medias?<page>")] +pub fn list( + user: User, + page: Option<Page>, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Ructe, ErrorPage> { + let page = page.unwrap_or_default(); + let medias = Media::page_for_user(&conn, &user, page.limits())?; + Ok(render!(medias::index( + &(&conn, &rockets).to_context(), + medias, + page.0, + Page::total(Media::count_for_user(&conn, &user)? as i32) + ))) +} + +#[get("/medias/new")] +pub fn new(_user: User, conn: DbConn, rockets: PlumeRocket) -> Ructe { + render!(medias::new(&(&conn, &rockets).to_context())) +} + +#[post("/medias/new", data = "<data>")] +pub fn upload( + user: User, + data: Data, + ct: &ContentType, + conn: DbConn, +) -> Result<Redirect, status::BadRequest<&'static str>> { + if !ct.is_form_data() { + return Ok(Redirect::to(uri!(new))); + } + + let (_, boundary) = ct + .params() + .find(|&(k, _)| k == "boundary") + .ok_or(status::BadRequest(Some("No boundary")))?; + + if let SaveResult::Full(entries) = Multipart::with_body(data.open(), boundary).save().temp() { + let fields = entries.fields; + + let filename = fields + .get("file") + .and_then(|v| v.iter().next()) + .ok_or(status::BadRequest(Some("No file uploaded")))? + .headers + .filename + .clone(); + // Remove extension if it contains something else than just letters and numbers + let ext = filename + .and_then(|f| { + f.rsplit('.') + .next() + .and_then(|ext| { + if ext.chars().any(|c| !c.is_alphanumeric()) { + None + } else { + Some(ext.to_lowercase()) + } + }) + .map(|ext| format!(".{}", ext)) + }) + .unwrap_or_default(); + let dest = format!("{}/{}{}", CONFIG.media_directory, GUID::rand(), ext); + + match fields["file"][0].data { + SavedData::Bytes(ref bytes) => fs::write(&dest, bytes) + .map_err(|_| status::BadRequest(Some("Couldn't save upload")))?, + SavedData::File(ref path, _) => { + fs::copy(path, &dest) + .map_err(|_| status::BadRequest(Some("Couldn't copy upload")))?; + } + _ => { + return Ok(Redirect::to(uri!(new))); + } + } + + let has_cw = !read(&fields["cw"][0].data) + .map(|cw| cw.is_empty()) + .unwrap_or(false); + let media = Media::insert( + &conn, + NewMedia { + file_path: dest, + alt_text: read(&fields["alt"][0].data)?, + is_remote: false, + remote_url: None, + sensitive: has_cw, + content_warning: if has_cw { + Some(read(&fields["cw"][0].data)?) + } else { + None + }, + owner_id: user.id, + }, + ) + .map_err(|_| status::BadRequest(Some("Error while saving media")))?; + Ok(Redirect::to(uri!(details: id = media.id))) + } else { + Ok(Redirect::to(uri!(new))) + } +} + +fn read(data: &SavedData) -> Result<String, status::BadRequest<&'static str>> { + if let SavedData::Text(s) = data { + Ok(s.clone()) + } else { + Err(status::BadRequest(Some("Error while reading data"))) + } +} + +#[get("/medias/<id>")] +pub fn details( + id: i32, + user: User, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Ructe, ErrorPage> { + let media = Media::get(&conn, id)?; + if media.owner_id == user.id { + Ok(render!(medias::details( + &(&conn, &rockets).to_context(), + media + ))) + } else { + Err(Error::Unauthorized.into()) + } +} + +#[post("/medias/<id>/delete")] +pub fn delete(id: i32, user: User, conn: DbConn, intl: I18n) -> Result<Flash<Redirect>, ErrorPage> { + let media = Media::get(&*conn, id)?; + if media.owner_id == user.id { + media.delete(&*conn)?; + Ok(Flash::success( + Redirect::to(uri!(list: page = _)), + i18n!(intl.catalog, "Your media have been deleted."), + )) + } else { + Ok(Flash::error( + Redirect::to(uri!(list: page = _)), + i18n!(intl.catalog, "You are not allowed to delete this media."), + )) + } +} + +#[post("/medias/<id>/avatar")] +pub fn set_avatar( + id: i32, + user: User, + conn: DbConn, + intl: I18n, +) -> Result<Flash<Redirect>, ErrorPage> { + let media = Media::get(&*conn, id)?; + if media.owner_id == user.id { + user.set_avatar(&*conn, media.id)?; + Ok(Flash::success( + Redirect::to(uri!(details: id = id)), + i18n!(intl.catalog, "Your avatar has been updated."), + )) + } else { + Ok(Flash::error( + Redirect::to(uri!(details: id = id)), + i18n!(intl.catalog, "You are not allowed to use this media."), + )) + } +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs new file mode 100755 index 00000000000..b239abecc3b --- /dev/null +++ b/src/routes/mod.rs @@ -0,0 +1,265 @@ +#![warn(clippy::too_many_arguments)] +use crate::template_utils::Ructe; +use atom_syndication::{ + ContentBuilder, Entry, EntryBuilder, Feed, FeedBuilder, LinkBuilder, Person, PersonBuilder, +}; +use chrono::{naive::NaiveDateTime, DateTime, Utc}; +use plume_models::{posts::Post, Connection, CONFIG, ITEMS_PER_PAGE}; +use rocket::{ + http::{ + hyper::header::{CacheControl, CacheDirective, ETag, EntityTag}, + uri::{FromUriParam, Query}, + RawStr, Status, + }, + request::{self, FromFormValue, FromRequest, Request}, + response::{self, Flash, NamedFile, Redirect, Responder, Response}, + Outcome, +}; +use std::{ + collections::hash_map::DefaultHasher, + hash::Hasher, + path::{Path, PathBuf}, +}; + +/// Special return type used for routes that "cannot fail", and instead +/// `Redirect`, or `Flash<Redirect>`, when we cannot deliver a `Ructe` Response +#[allow(clippy::large_enum_variant)] +#[derive(Responder)] +pub enum RespondOrRedirect { + Response(Ructe), + FlashResponse(Flash<Ructe>), + Redirect(Redirect), + FlashRedirect(Flash<Redirect>), +} + +impl From<Ructe> for RespondOrRedirect { + fn from(response: Ructe) -> Self { + RespondOrRedirect::Response(response) + } +} + +impl From<Flash<Ructe>> for RespondOrRedirect { + fn from(response: Flash<Ructe>) -> Self { + RespondOrRedirect::FlashResponse(response) + } +} + +impl From<Redirect> for RespondOrRedirect { + fn from(redirect: Redirect) -> Self { + RespondOrRedirect::Redirect(redirect) + } +} + +impl From<Flash<Redirect>> for RespondOrRedirect { + fn from(redirect: Flash<Redirect>) -> Self { + RespondOrRedirect::FlashRedirect(redirect) + } +} + +#[derive(Shrinkwrap, Copy, Clone, UriDisplayQuery)] +pub struct Page(i32); + +impl<'v> FromFormValue<'v> for Page { + type Error = &'v RawStr; + fn from_form_value(form_value: &'v RawStr) -> Result<Page, &'v RawStr> { + match form_value.parse::<i32>() { + Ok(page) => Ok(Page(page)), + _ => Err(form_value), + } + } +} + +impl FromUriParam<Query, Option<Page>> for Page { + type Target = Page; + + fn from_uri_param(val: Option<Page>) -> Page { + val.unwrap_or_default() + } +} + +impl Page { + /// Computes the total number of pages needed to display n_items + pub fn total(n_items: i32) -> i32 { + if n_items % ITEMS_PER_PAGE == 0 { + n_items / ITEMS_PER_PAGE + } else { + (n_items / ITEMS_PER_PAGE) + 1 + } + } + + pub fn limits(self) -> (i32, i32) { + ((self.0 - 1) * ITEMS_PER_PAGE, self.0 * ITEMS_PER_PAGE) + } +} + +#[derive(Shrinkwrap)] +pub struct ContentLen(pub u64); + +impl<'a, 'r> FromRequest<'a, 'r> for ContentLen { + type Error = (); + + fn from_request(r: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { + match r.limits().get("forms") { + Some(l) => Outcome::Success(ContentLen(l)), + None => Outcome::Failure((Status::InternalServerError, ())), + } + } +} + +impl Default for Page { + fn default() -> Self { + Page(1) + } +} + +/// A form for remote interaction, used by multiple routes +#[derive(Shrinkwrap, Clone, Default, FromForm)] +pub struct RemoteForm { + pub remote: String, +} + +pub fn build_atom_feed( + entries: Vec<Post>, + uri: &str, + title: &str, + default_updated: &NaiveDateTime, + conn: &Connection, +) -> Feed { + let updated = if entries.is_empty() { + default_updated + } else { + &entries[0].creation_date + }; + + FeedBuilder::default() + .title(title) + .id(uri) + .updated(DateTime::<Utc>::from_utc(*updated, Utc)) + .entries( + entries + .into_iter() + .map(|p| post_to_atom(p, conn)) + .collect::<Vec<Entry>>(), + ) + .links(vec![LinkBuilder::default() + .href(uri) + .rel("self") + .mime_type("application/atom+xml".to_string()) + .build()]) + .build() +} + +fn post_to_atom(post: Post, conn: &Connection) -> Entry { + EntryBuilder::default() + .title(format!("<![CDATA[{}]]>", post.title)) + .content( + ContentBuilder::default() + .value(format!("<![CDATA[{}]]>", *post.content.get())) + .content_type("html".to_string()) + .build(), + ) + .authors( + post.get_authors(&*conn) + .expect("Atom feed: author error") + .into_iter() + .map(|a| { + PersonBuilder::default() + .name(a.display_name) + .uri(a.ap_url) + .build() + }) + .collect::<Vec<Person>>(), + ) + // Using RFC 4287 format, see https://tools.ietf.org/html/rfc4287#section-3.3 for dates + // eg: 2003-12-13T18:30:02Z (Z is here because there is no timezone support with the NaiveDateTime crate) + .published(Some( + DateTime::<Utc>::from_utc(post.creation_date, Utc).into(), + )) + .updated(DateTime::<Utc>::from_utc(post.creation_date, Utc)) + .id(post.ap_url.clone()) + .links(vec![LinkBuilder::default().href(post.ap_url).build()]) + .build() +} + +pub mod blogs; +pub mod comments; +pub mod email_signups; +pub mod errors; +pub mod instance; +pub mod likes; +pub mod medias; +pub mod notifications; +pub mod posts; +pub mod reshares; +pub mod search; +pub mod session; +pub mod tags; +pub mod timelines; +pub mod user; +pub mod well_known; + +#[derive(Responder)] +#[response()] +pub struct CachedFile { + inner: NamedFile, + cache_control: CacheControl, +} + +#[derive(Debug)] +pub struct ThemeFile(NamedFile); + +impl<'r> Responder<'r> for ThemeFile { + fn respond_to(self, r: &Request<'_>) -> response::Result<'r> { + let contents = std::fs::read(self.0.path()).map_err(|_| Status::InternalServerError)?; + + let mut hasher = DefaultHasher::new(); + hasher.write(&contents); + let etag = format!("{:x}", hasher.finish()); + + if r.headers() + .get("If-None-Match") + .any(|s| s[1..s.len() - 1] == etag) + { + Response::build() + .status(Status::NotModified) + .header(ETag(EntityTag::strong(etag))) + .ok() + } else { + Response::build() + .merge(self.0.respond_to(r)?) + .header(ETag(EntityTag::strong(etag))) + .ok() + } + } +} + +#[get("/static/cached/<_build_id>/css/<file..>", rank = 1)] +pub fn theme_files(file: PathBuf, _build_id: &RawStr) -> Option<ThemeFile> { + NamedFile::open(Path::new("static/css/").join(file)) + .ok() + .map(ThemeFile) +} + +#[allow(unused_variables)] +#[get("/static/cached/<build_id>/<file..>", rank = 2)] +pub fn plume_static_files(file: PathBuf, build_id: &RawStr) -> Option<CachedFile> { + static_files(file) +} +#[get("/static/media/<file..>")] +pub fn plume_media_files(file: PathBuf) -> Option<CachedFile> { + NamedFile::open(Path::new(&CONFIG.media_directory).join(file)) + .ok() + .map(|f| CachedFile { + inner: f, + cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]), + }) +} +#[get("/static/<file..>", rank = 3)] +pub fn static_files(file: PathBuf) -> Option<CachedFile> { + NamedFile::open(Path::new("static/").join(file)) + .ok() + .map(|f| CachedFile { + inner: f, + cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]), + }) +} diff --git a/src/routes/notifications.rs b/src/routes/notifications.rs new file mode 100644 index 00000000000..2f473ed0133 --- /dev/null +++ b/src/routes/notifications.rs @@ -0,0 +1,34 @@ +use rocket::response::{Flash, Redirect}; +use rocket_i18n::I18n; + +use crate::routes::{errors::ErrorPage, Page}; +use crate::template_utils::{IntoContext, Ructe}; +use crate::utils::requires_login; +use plume_models::{db_conn::DbConn, notifications::Notification, users::User, PlumeRocket}; + +#[get("/notifications?<page>")] +pub fn notifications( + user: User, + page: Option<Page>, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Ructe, ErrorPage> { + let page = page.unwrap_or_default(); + Ok(render!(notifications::index( + &(&conn, &rockets).to_context(), + Notification::page_for_user(&conn, &user, page.limits())?, + page.0, + Page::total(Notification::count_for_user(&conn, &user)? as i32) + ))) +} + +#[get("/notifications?<page>", rank = 2)] +pub fn notifications_auth(i18n: I18n, page: Option<Page>) -> Flash<Redirect> { + requires_login( + &i18n!( + i18n.catalog, + "To see your notifications, you need to be logged in" + ), + uri!(notifications: page = page), + ) +} diff --git a/src/routes/posts.rs b/src/routes/posts.rs new file mode 100644 index 00000000000..854a5621a35 --- /dev/null +++ b/src/routes/posts.rs @@ -0,0 +1,687 @@ +use chrono::Utc; +use rocket::http::uri::Uri; +use rocket::request::LenientForm; +use rocket::response::{Flash, Redirect}; +use rocket_i18n::I18n; +use std::{ + borrow::Cow, + collections::{HashMap, HashSet}, + time::Duration, +}; +use validator::{Validate, ValidationError, ValidationErrors}; + +use crate::routes::{ + comments::NewCommentForm, errors::ErrorPage, ContentLen, RemoteForm, RespondOrRedirect, +}; +use crate::template_utils::{IntoContext, Ructe}; +use crate::utils::requires_login; +use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest}; +use plume_common::utils::md_to_html; +use plume_models::{ + blogs::*, + comments::{Comment, CommentTree}, + db_conn::DbConn, + inbox::inbox, + instance::Instance, + medias::Media, + mentions::Mention, + post_authors::*, + posts::*, + safe_string::SafeString, + tags::*, + timeline::*, + users::User, + Error, PlumeRocket, CONFIG, +}; + +#[get("/~/<blog>/<slug>?<responding_to>", rank = 4)] +pub fn details( + blog: String, + slug: String, + responding_to: Option<i32>, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Ructe, ErrorPage> { + let user = rockets.user.clone(); + let blog = Blog::find_by_fqn(&conn, &blog)?; + let post = Post::find_by_slug(&conn, &slug, blog.id)?; + if !(post.published + || post + .get_authors(&conn)? + .into_iter() + .any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0))) + { + return Ok(render!(errors::not_authorized( + &(&conn, &rockets).to_context(), + i18n!(rockets.intl.catalog, "This post isn't published yet.") + ))); + } + + let comments = CommentTree::from_post(&conn, &post, user.as_ref())?; + + let previous = responding_to.and_then(|r| Comment::get(&conn, r).ok()); + + Ok(render!(posts::details( + &(&conn, &rockets).to_context(), + post.clone(), + blog, + &NewCommentForm { + warning: previous.clone().map(|p| p.spoiler_text).unwrap_or_default(), + content: previous.clone().and_then(|p| Some(format!( + "@{} {}", + p.get_author(&conn).ok()?.fqn, + Mention::list_for_comment(&conn, p.id).ok()? + .into_iter() + .filter_map(|m| { + let user = user.clone(); + if let Ok(mentioned) = m.get_mentioned(&conn) { + if user.is_none() || mentioned.id != user.expect("posts::details_response: user error while listing mentions").id { + Some(format!("@{}", mentioned.fqn)) + } else { + None + } + } else { + None + } + }).collect::<Vec<String>>().join(" ")) + )).unwrap_or_default(), + ..NewCommentForm::default() + }, + ValidationErrors::default(), + Tag::for_post(&conn, post.id)?, + comments, + previous, + post.count_likes(&conn)?, + post.count_reshares(&conn)?, + user.clone().and_then(|u| u.has_liked(&conn, &post).ok()).unwrap_or(false), + user.clone().and_then(|u| u.has_reshared(&conn, &post).ok()).unwrap_or(false), + user.and_then(|u| u.is_following(&conn, post.get_authors(&conn).ok()?[0].id).ok()).unwrap_or(false), + post.get_authors(&conn)?[0].clone() + ))) +} + +#[get("/~/<blog>/<slug>", rank = 3)] +pub fn activity_details( + blog: String, + slug: String, + _ap: ApRequest, + conn: DbConn, +) -> Result<ActivityStream<LicensedArticle>, Option<String>> { + let blog = Blog::find_by_fqn(&conn, &blog).map_err(|_| None)?; + let post = Post::find_by_slug(&conn, &slug, blog.id).map_err(|_| None)?; + if post.published { + Ok(ActivityStream::new( + post.to_activity(&conn) + .map_err(|_| String::from("Post serialization error"))?, + )) + } else { + Err(Some(String::from("Not published yet."))) + } +} + +#[get("/~/<blog>/new", rank = 2)] +pub fn new_auth(blog: String, i18n: I18n) -> Flash<Redirect> { + requires_login( + &i18n!( + i18n.catalog, + "To write a new post, you need to be logged in" + ), + uri!(new: blog = blog), + ) +} + +#[get("/~/<blog>/new", rank = 1)] +pub fn new( + blog: String, + cl: ContentLen, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Ructe, ErrorPage> { + let b = Blog::find_by_fqn(&conn, &blog)?; + let user = rockets.user.clone().unwrap(); + + if !user.is_author_in(&conn, &b)? { + // TODO actually return 403 error code + return Ok(render!(errors::not_authorized( + &(&conn, &rockets).to_context(), + i18n!(rockets.intl.catalog, "You are not an author of this blog.") + ))); + } + + let medias = Media::for_user(&conn, user.id)?; + Ok(render!(posts::new( + &(&conn, &rockets).to_context(), + i18n!(rockets.intl.catalog, "New post"), + b, + false, + &NewPostForm { + license: Instance::get_local()?.default_license, + ..NewPostForm::default() + }, + true, + None, + ValidationErrors::default(), + medias, + cl.0 + ))) +} + +#[get("/~/<blog>/<slug>/edit")] +pub fn edit( + blog: String, + slug: String, + cl: ContentLen, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Ructe, ErrorPage> { + let intl = &rockets.intl.catalog; + let b = Blog::find_by_fqn(&conn, &blog)?; + let post = Post::find_by_slug(&conn, &slug, b.id)?; + let user = rockets.user.clone().unwrap(); + + if !user.is_author_in(&conn, &b)? { + return Ok(render!(errors::not_authorized( + &(&conn, &rockets).to_context(), + i18n!(intl, "You are not an author of this blog.") + ))); + } + + let source = if !post.source.is_empty() { + post.source.clone() + } else { + post.content.get().clone() // fallback to HTML if the markdown was not stored + }; + + let medias = Media::for_user(&conn, user.id)?; + let title = post.title.clone(); + Ok(render!(posts::new( + &(&conn, &rockets).to_context(), + i18n!(intl, "Edit {0}"; &title), + b, + true, + &NewPostForm { + title: post.title.clone(), + subtitle: post.subtitle.clone(), + content: source, + tags: Tag::for_post(&conn, post.id)? + .into_iter() + .filter_map(|t| if !t.is_hashtag { Some(t.tag) } else { None }) + .collect::<Vec<String>>() + .join(", "), + license: post.license.clone(), + draft: true, + cover: post.cover_id, + }, + !post.published, + Some(post), + ValidationErrors::default(), + medias, + cl.0 + ))) +} + +#[post("/~/<blog>/<slug>/edit", data = "<form>")] +pub fn update( + blog: String, + slug: String, + cl: ContentLen, + form: LenientForm<NewPostForm>, + conn: DbConn, + rockets: PlumeRocket, +) -> RespondOrRedirect { + let b = Blog::find_by_fqn(&conn, &blog).expect("post::update: blog error"); + let mut post = + Post::find_by_slug(&conn, &slug, b.id).expect("post::update: find by slug error"); + let user = rockets.user.clone().unwrap(); + let intl = &rockets.intl.catalog; + + let new_slug = if !post.published { + Post::slug(&form.title).to_string() + } else { + post.slug.clone() + }; + + let mut errors = match form.validate() { + Ok(_) => ValidationErrors::new(), + Err(e) => e, + }; + + if new_slug != slug && Post::find_by_slug(&conn, &new_slug, b.id).is_ok() { + errors.add( + "title", + ValidationError { + code: Cow::from("existing_slug"), + message: Some(Cow::from("A post with the same title already exists.")), + params: HashMap::new(), + }, + ); + } + + if errors.is_empty() { + if !user + .is_author_in(&conn, &b) + .expect("posts::update: is author in error") + { + // actually it's not "Ok"… + Flash::error( + Redirect::to(uri!(super::blogs::details: name = blog, page = _)), + i18n!(&intl, "You are not allowed to publish on this blog."), + ) + .into() + } else { + let (content, mentions, hashtags) = md_to_html( + form.content.to_string().as_ref(), + Some( + &Instance::get_local() + .expect("posts::update: Error getting local instance") + .public_domain, + ), + false, + Some(Media::get_media_processor( + &conn, + b.list_authors(&conn) + .expect("Could not get author list") + .iter() + .collect(), + )), + ); + + // update publication date if when this article is no longer a draft + let newly_published = if !post.published && !form.draft { + post.published = true; + post.creation_date = Utc::now().naive_utc(); + post.ap_url = Post::ap_url(post.get_blog(&conn).unwrap(), &new_slug); + true + } else { + false + }; + + post.slug = new_slug.clone(); + post.title = form.title.clone(); + post.subtitle = form.subtitle.clone(); + post.content = SafeString::new(&content); + post.source = form.content.clone(); + post.license = form.license.clone(); + post.cover_id = form.cover; + post.update(&conn).expect("post::update: update error"); + + if post.published { + post.update_mentions( + &conn, + mentions + .into_iter() + .filter_map(|m| Mention::build_activity(&conn, &m).ok()) + .collect(), + ) + .expect("post::update: mentions error"); + } + + let tags = form + .tags + .split(',') + .map(|t| t.trim()) + .filter(|t| !t.is_empty()) + .collect::<HashSet<_>>() + .into_iter() + .filter_map(|t| Tag::build_activity(t.to_string()).ok()) + .collect::<Vec<_>>(); + post.update_tags(&conn, tags) + .expect("post::update: tags error"); + + let hashtags = hashtags + .into_iter() + .collect::<HashSet<_>>() + .into_iter() + .filter_map(|t| Tag::build_activity(t).ok()) + .collect::<Vec<_>>(); + post.update_hashtags(&conn, hashtags) + .expect("post::update: hashtags error"); + + if post.published { + if newly_published { + let act = post + .create_activity(&conn) + .expect("post::update: act error"); + let dest = User::one_by_instance(&conn).expect("post::update: dest error"); + rockets + .worker + .execute(move || broadcast(&user, act, dest, CONFIG.proxy().cloned())); + + Timeline::add_to_all_timelines(&conn, &post, Kind::Original).ok(); + } else { + let act = post + .update_activity(&conn) + .expect("post::update: act error"); + let dest = User::one_by_instance(&conn).expect("posts::update: dest error"); + rockets + .worker + .execute(move || broadcast(&user, act, dest, CONFIG.proxy().cloned())); + } + } + + Flash::success( + Redirect::to(uri!( + details: blog = blog, + slug = new_slug, + responding_to = _ + )), + i18n!(intl, "Your article has been updated."), + ) + .into() + } + } else { + let medias = Media::for_user(&conn, user.id).expect("posts:update: medias error"); + render!(posts::new( + &(&conn, &rockets).to_context(), + i18n!(intl, "Edit {0}"; &form.title), + b, + true, + &*form, + form.draft, + Some(post), + errors, + medias, + cl.0 + )) + .into() + } +} + +#[derive(Default, FromForm, Validate)] +pub struct NewPostForm { + #[validate(custom(function = "valid_slug", message = "Invalid title"))] + pub title: String, + pub subtitle: String, + pub content: String, + pub tags: String, + pub license: String, + pub draft: bool, + pub cover: Option<i32>, +} + +pub fn valid_slug(title: &str) -> Result<(), ValidationError> { + let slug = Post::slug(title); + if slug.is_empty() { + Err(ValidationError::new("empty_slug")) + } else if slug == "new" { + Err(ValidationError::new("invalid_slug")) + } else { + Ok(()) + } +} + +#[post("/~/<blog_name>/new", data = "<form>")] +pub fn create( + blog_name: String, + form: LenientForm<NewPostForm>, + cl: ContentLen, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<RespondOrRedirect, ErrorPage> { + let blog = Blog::find_by_fqn(&conn, &blog_name).expect("post::create: blog error"); + let slug = Post::slug(&form.title); + let user = rockets.user.clone().unwrap(); + + let mut errors = match form.validate() { + Ok(_) => ValidationErrors::new(), + Err(e) => e, + }; + if Post::find_by_slug(&conn, slug, blog.id).is_ok() { + errors.add( + "title", + ValidationError { + code: Cow::from("existing_slug"), + message: Some(Cow::from("A post with the same title already exists.")), + params: HashMap::new(), + }, + ); + } + + if errors.is_empty() { + if !user + .is_author_in(&conn, &blog) + .expect("post::create: is author in error") + { + // actually it's not "Ok"… + return Ok(Flash::error( + Redirect::to(uri!(super::blogs::details: name = blog_name, page = _)), + i18n!( + &rockets.intl.catalog, + "You are not allowed to publish on this blog." + ), + ) + .into()); + } + + let (content, mentions, hashtags) = md_to_html( + form.content.to_string().as_ref(), + Some( + &Instance::get_local() + .expect("post::create: local instance error") + .public_domain, + ), + false, + Some(Media::get_media_processor( + &conn, + blog.list_authors(&conn) + .expect("Could not get author list") + .iter() + .collect(), + )), + ); + + let post = Post::insert( + &conn, + NewPost { + blog_id: blog.id, + slug: slug.to_string(), + title: form.title.to_string(), + content: SafeString::new(&content), + published: !form.draft, + license: form.license.clone(), + ap_url: "".to_string(), + creation_date: None, + subtitle: form.subtitle.clone(), + source: form.content.clone(), + cover_id: form.cover, + }, + ) + .expect("post::create: post save error"); + + PostAuthor::insert( + &conn, + NewPostAuthor { + post_id: post.id, + author_id: user.id, + }, + ) + .expect("post::create: author save error"); + + let tags = form + .tags + .split(',') + .map(|t| t.trim()) + .filter(|t| !t.is_empty()) + .collect::<HashSet<_>>(); + for tag in tags { + Tag::insert( + &conn, + NewTag { + tag: tag.to_string(), + is_hashtag: false, + post_id: post.id, + }, + ) + .expect("post::create: tags save error"); + } + for hashtag in hashtags { + Tag::insert( + &conn, + NewTag { + tag: hashtag, + is_hashtag: true, + post_id: post.id, + }, + ) + .expect("post::create: hashtags save error"); + } + + if post.published { + for m in mentions { + Mention::from_activity( + &conn, + &Mention::build_activity(&conn, &m).expect("post::create: mention build error"), + post.id, + true, + true, + ) + .expect("post::create: mention save error"); + } + + let act = post + .create_activity(&conn) + .expect("posts::create: activity error"); + let dest = User::one_by_instance(&conn).expect("posts::create: dest error"); + let worker = &rockets.worker; + worker.execute(move || broadcast(&user, act, dest, CONFIG.proxy().cloned())); + + Timeline::add_to_all_timelines(&conn, &post, Kind::Original)?; + } + + Ok(Flash::success( + Redirect::to(uri!( + details: blog = blog_name, + slug = slug, + responding_to = _ + )), + i18n!(&rockets.intl.catalog, "Your article has been saved."), + ) + .into()) + } else { + let medias = Media::for_user(&conn, user.id).expect("posts::create: medias error"); + Ok(render!(posts::new( + &(&conn, &rockets).to_context(), + i18n!(rockets.intl.catalog, "New article"), + blog, + false, + &*form, + form.draft, + None, + errors, + medias, + cl.0 + )) + .into()) + } +} + +#[post("/~/<blog_name>/<slug>/delete")] +pub fn delete( + blog_name: String, + slug: String, + conn: DbConn, + rockets: PlumeRocket, + intl: I18n, +) -> Result<Flash<Redirect>, ErrorPage> { + let user = rockets.user.clone().unwrap(); + let post = Blog::find_by_fqn(&conn, &blog_name) + .and_then(|blog| Post::find_by_slug(&conn, &slug, blog.id)); + + if let Ok(post) = post { + if !post + .get_authors(&conn)? + .into_iter() + .any(|a| a.id == user.id) + { + return Ok(Flash::error( + Redirect::to(uri!( + details: blog = blog_name, + slug = slug, + responding_to = _ + )), + i18n!(intl.catalog, "You are not allowed to delete this article."), + )); + } + + let dest = User::one_by_instance(&conn)?; + let delete_activity = post.build_delete(&conn)?; + inbox( + &conn, + serde_json::to_value(&delete_activity).map_err(Error::from)?, + )?; + + let user_c = user.clone(); + rockets + .worker + .execute(move || broadcast(&user_c, delete_activity, dest, CONFIG.proxy().cloned())); + rockets + .worker + .execute_after(Duration::from_secs(10 * 60), move || { + user.rotate_keypair(&conn) + .expect("Failed to rotate keypair"); + }); + + Ok(Flash::success( + Redirect::to(uri!(super::blogs::details: name = blog_name, page = _)), + i18n!(intl.catalog, "Your article has been deleted."), + )) + } else { + Ok(Flash::error(Redirect::to( + uri!(super::blogs::details: name = blog_name, page = _), + ), i18n!(intl.catalog, "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?"))) + } +} + +#[get("/~/<blog_name>/<slug>/remote_interact")] +pub fn remote_interact( + conn: DbConn, + rockets: PlumeRocket, + blog_name: String, + slug: String, +) -> Result<Ructe, ErrorPage> { + let target = Blog::find_by_fqn(&conn, &blog_name) + .and_then(|blog| Post::find_by_slug(&conn, &slug, blog.id))?; + Ok(render!(posts::remote_interact( + &(&conn, &rockets).to_context(), + target, + super::session::LoginForm::default(), + ValidationErrors::default(), + RemoteForm::default(), + ValidationErrors::default() + ))) +} + +#[post("/~/<blog_name>/<slug>/remote_interact", data = "<remote>")] +pub fn remote_interact_post( + conn: DbConn, + rockets: PlumeRocket, + blog_name: String, + slug: String, + remote: LenientForm<RemoteForm>, +) -> Result<RespondOrRedirect, ErrorPage> { + let target = Blog::find_by_fqn(&conn, &blog_name) + .and_then(|blog| Post::find_by_slug(&conn, &slug, blog.id))?; + if let Some(uri) = User::fetch_remote_interact_uri(&remote.remote) + .ok() + .map(|uri| uri.replace("{uri}", &Uri::percent_encode(&target.ap_url))) + { + Ok(Redirect::to(uri).into()) + } else { + let mut errs = ValidationErrors::new(); + errs.add("remote", ValidationError { + code: Cow::from("invalid_remote"), + message: Some(Cow::from(i18n!(rockets.intl.catalog, "Couldn't obtain enough information about your account. Please make sure your username is correct."))), + params: HashMap::new(), + }); + //could not get your remote url? + Ok(render!(posts::remote_interact( + &(&conn, &rockets).to_context(), + target, + super::session::LoginForm::default(), + ValidationErrors::default(), + remote.clone(), + errs + )) + .into()) + } +} diff --git a/src/routes/reshares.rs b/src/routes/reshares.rs new file mode 100644 index 00000000000..b6d11c8a0a0 --- /dev/null +++ b/src/routes/reshares.rs @@ -0,0 +1,61 @@ +use rocket::response::{Flash, Redirect}; +use rocket_i18n::I18n; + +use crate::routes::errors::ErrorPage; +use crate::utils::requires_login; +use plume_common::activity_pub::broadcast; +use plume_models::{ + blogs::Blog, db_conn::DbConn, inbox::inbox, posts::Post, reshares::*, timeline::*, users::User, + Error, PlumeRocket, CONFIG, +}; + +#[post("/~/<blog>/<slug>/reshare")] +pub fn create( + blog: String, + slug: String, + user: User, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Redirect, ErrorPage> { + let b = Blog::find_by_fqn(&conn, &blog)?; + let post = Post::find_by_slug(&conn, &slug, b.id)?; + + if !user.has_reshared(&conn, &post)? { + let reshare = Reshare::insert(&conn, NewReshare::new(&post, &user))?; + reshare.notify(&conn)?; + + Timeline::add_to_all_timelines(&conn, &post, Kind::Reshare(&user))?; + + let dest = User::one_by_instance(&conn)?; + let act = reshare.to_activity(&conn)?; + rockets + .worker + .execute(move || broadcast(&user, act, dest, CONFIG.proxy().cloned())); + } else { + let reshare = Reshare::find_by_user_on_post(&conn, user.id, post.id)?; + let delete_act = reshare.build_undo(&conn)?; + inbox( + &conn, + serde_json::to_value(&delete_act).map_err(Error::from)?, + )?; + + let dest = User::one_by_instance(&conn)?; + rockets + .worker + .execute(move || broadcast(&user, delete_act, dest, CONFIG.proxy().cloned())); + } + + Ok(Redirect::to(uri!( + super::posts::details: blog = blog, + slug = slug, + responding_to = _ + ))) +} + +#[post("/~/<blog>/<slug>/reshare", rank = 1)] +pub fn create_auth(blog: String, slug: String, i18n: I18n) -> Flash<Redirect> { + requires_login( + &i18n!(i18n.catalog, "To reshare a post, you need to be logged in"), + uri!(create: blog = blog, slug = slug), + ) +} diff --git a/src/routes/search.rs b/src/routes/search.rs new file mode 100644 index 00000000000..33d8443f340 --- /dev/null +++ b/src/routes/search.rs @@ -0,0 +1,83 @@ +use chrono::offset::Utc; +use rocket::request::Form; + +use crate::routes::Page; +use crate::template_utils::{IntoContext, Ructe}; +use plume_models::{db_conn::DbConn, search::Query, PlumeRocket}; +use std::str::FromStr; + +#[derive(Default, FromForm)] +pub struct SearchQuery { + q: Option<String>, + title: Option<String>, + subtitle: Option<String>, + content: Option<String>, + instance: Option<String>, + author: Option<String>, + tag: Option<String>, + blog: Option<String>, + lang: Option<String>, + license: Option<String>, + after: Option<String>, + before: Option<String>, + page: Option<Page>, +} + +macro_rules! param_to_query { + ( $query:ident, $parsed_query:ident; normal: $($field:ident),*; date: $($date:ident),*) => { + $( + let mut rest = $query.$field.as_ref().map(String::as_str).unwrap_or_default(); + while !rest.is_empty() { + let (token, r) = Query::get_first_token(rest); + rest = r; + $parsed_query.$field(token, None); + } + )* + $( + if let Some(ref field) = $query.$date { + let mut rest = field.as_str(); + while !rest.is_empty() { + use chrono::naive::NaiveDate; + let (token, r) = Query::get_first_token(rest); + rest = r; + if let Ok(token) = NaiveDate::parse_from_str(token, "%Y-%m-%d") { + $parsed_query.$date(&token); + } + } + } + )* + } +} + +#[get("/search?<query..>")] +pub fn search(query: Option<Form<SearchQuery>>, conn: DbConn, rockets: PlumeRocket) -> Ructe { + let query = query.map(Form::into_inner).unwrap_or_default(); + let page = query.page.unwrap_or_default(); + let mut parsed_query = + Query::from_str(query.q.as_deref().unwrap_or_default()).unwrap_or_default(); + + param_to_query!(query, parsed_query; normal: title, subtitle, content, tag, + instance, author, blog, lang, license; + date: before, after); + + let str_query = parsed_query.to_string(); + + if str_query.is_empty() { + render!(search::index( + &(&conn, &rockets).to_context(), + &format!("{}", Utc::today().format("%Y-%m-d")) + )) + } else { + let res = rockets + .searcher + .search_document(&conn, parsed_query, page.limits()); + let next_page = if res.is_empty() { 0 } else { page.0 + 1 }; + render!(search::result( + &(&conn, &rockets).to_context(), + &str_query, + res, + page.0, + next_page + )) + } +} diff --git a/src/routes/session.rs b/src/routes/session.rs new file mode 100644 index 00000000000..1eab96934df --- /dev/null +++ b/src/routes/session.rs @@ -0,0 +1,248 @@ +use crate::routes::RespondOrRedirect; +use plume_models::lettre::Transport; +use rocket::http::ext::IntoOwned; +use rocket::{ + http::{uri::Uri, Cookie, Cookies, SameSite}, + request::LenientForm, + response::{Flash, Redirect}, + State, +}; +use rocket_i18n::I18n; +use std::{ + borrow::Cow, + sync::{Arc, Mutex}, + time::Instant, +}; +use tracing::warn; +use validator::{Validate, ValidationError, ValidationErrors}; + +use crate::mail::{build_mail, Mailer}; +use crate::template_utils::{IntoContext, Ructe}; +use plume_models::{ + db_conn::DbConn, + password_reset_requests::*, + users::{User, AUTH_COOKIE}, + Error, PlumeRocket, CONFIG, +}; + +#[get("/login?<m>")] +pub fn new(m: Option<String>, conn: DbConn, rockets: PlumeRocket) -> Ructe { + render!(session::login( + &(&conn, &rockets).to_context(), + m, + &LoginForm::default(), + ValidationErrors::default() + )) +} + +#[derive(Default, FromForm, Validate)] +pub struct LoginForm { + #[validate(length(min = 1, message = "We need an email, or a username to identify you"))] + pub email_or_name: String, + #[validate(length(min = 1, message = "Your password can't be empty"))] + pub password: String, +} + +#[post("/login", data = "<form>")] +pub fn create( + form: LenientForm<LoginForm>, + mut cookies: Cookies<'_>, + conn: DbConn, + rockets: PlumeRocket, +) -> RespondOrRedirect { + let mut errors = match form.validate() { + Ok(_) => ValidationErrors::new(), + Err(e) => e, + }; + let user = User::login(&conn, &form.email_or_name, &form.password); + let user_id = if let Ok(user) = user { + user.id.to_string() + } else { + let mut err = ValidationError::new("invalid_login"); + err.message = Some(Cow::from("Invalid username, or password")); + errors.add("email_or_name", err); + return render!(session::login( + &(&conn, &rockets).to_context(), + None, + &*form, + errors + )) + .into(); + }; + + cookies.add_private( + Cookie::build(AUTH_COOKIE, user_id) + .same_site(SameSite::Lax) + .finish(), + ); + let destination = rockets + .flash_msg + .clone() + .and_then( + |(name, msg)| { + if name == "callback" { + Some(msg) + } else { + None + } + }, + ) + .unwrap_or_else(|| "/".to_owned()); + + if let Ok(uri) = Uri::parse(&destination).map(IntoOwned::into_owned) { + Flash::success( + Redirect::to(uri), + i18n!(&rockets.intl.catalog, "You are now connected."), + ) + .into() + } else { + render!(session::login( + &(&conn, &rockets.intl.catalog, None, None), + None, + &*form, + errors + )) + .into() + } +} + +#[get("/logout")] +pub fn delete(mut cookies: Cookies<'_>, intl: I18n) -> Flash<Redirect> { + if let Some(cookie) = cookies.get_private(AUTH_COOKIE) { + cookies.remove_private(cookie); + } + Flash::success( + Redirect::to("/"), + i18n!(intl.catalog, "You are now logged off."), + ) +} + +#[derive(Clone)] +pub struct ResetRequest { + pub mail: String, + pub id: String, + pub creation_date: Instant, +} + +impl PartialEq for ResetRequest { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +#[get("/password-reset")] +pub fn password_reset_request_form(conn: DbConn, rockets: PlumeRocket) -> Ructe { + render!(session::password_reset_request( + &(&conn, &rockets).to_context(), + &ResetForm::default(), + ValidationErrors::default() + )) +} + +#[derive(FromForm, Validate, Default)] +pub struct ResetForm { + #[validate(email)] + pub email: String, +} + +#[post("/password-reset", data = "<form>")] +pub fn password_reset_request( + mail: State<'_, Arc<Mutex<Mailer>>>, + form: LenientForm<ResetForm>, + conn: DbConn, + rockets: PlumeRocket, +) -> Ructe { + if User::find_by_email(&conn, &form.email).is_ok() { + let token = PasswordResetRequest::insert(&conn, &form.email) + .expect("password_reset_request::insert: error"); + + let url = format!("https://{}/password-reset/{}", CONFIG.base_url, token); + if let Some(message) = build_mail( + form.email.clone(), + i18n!(rockets.intl.catalog, "Password reset"), + i18n!(rockets.intl.catalog, "Here is the link to reset your password: {0}"; url), + ) { + if let Some(ref mut mail) = *mail.lock().unwrap() { + mail.send(message.into()) + .map_err(|_| warn!("Couldn't send password reset email")) + .ok(); + } + } + } + render!(session::password_reset_request_ok( + &(&conn, &rockets).to_context() + )) +} + +#[get("/password-reset/<token>")] +pub fn password_reset_form( + token: String, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Ructe, Ructe> { + PasswordResetRequest::find_by_token(&conn, &token) + .map_err(|err| password_reset_error_response(err, &conn, &rockets))?; + + Ok(render!(session::password_reset( + &(&conn, &rockets).to_context(), + &NewPasswordForm::default(), + ValidationErrors::default() + ))) +} + +#[derive(FromForm, Default, Validate)] +#[validate(schema( + function = "passwords_match", + skip_on_field_errors = false, + message = "Passwords are not matching" +))] +pub struct NewPasswordForm { + pub password: String, + pub password_confirmation: String, +} + +fn passwords_match(form: &NewPasswordForm) -> Result<(), ValidationError> { + if form.password != form.password_confirmation { + Err(ValidationError::new("password_match")) + } else { + Ok(()) + } +} + +#[post("/password-reset/<token>", data = "<form>")] +pub fn password_reset( + token: String, + form: LenientForm<NewPasswordForm>, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Flash<Redirect>, Ructe> { + form.validate().map_err(|err| { + render!(session::password_reset( + &(&conn, &rockets).to_context(), + &form, + err + )) + })?; + + PasswordResetRequest::find_and_delete_by_token(&conn, &token) + .and_then(|request| User::find_by_email(&conn, &request.email)) + .and_then(|user| user.reset_password(&conn, &form.password)) + .map_err(|err| password_reset_error_response(err, &conn, &rockets))?; + + Ok(Flash::success( + Redirect::to(uri!(new: m = _)), + i18n!( + rockets.intl.catalog, + "Your password was successfully reset." + ), + )) +} + +fn password_reset_error_response(err: Error, conn: &DbConn, rockets: &PlumeRocket) -> Ructe { + match err { + Error::Expired => render!(session::password_reset_request_expired( + &(conn, rockets).to_context() + )), + _ => render!(errors::not_found(&(conn, rockets).to_context())), + } +} diff --git a/src/routes/tags.rs b/src/routes/tags.rs new file mode 100644 index 00000000000..eaab8b1c638 --- /dev/null +++ b/src/routes/tags.rs @@ -0,0 +1,21 @@ +use crate::routes::{errors::ErrorPage, Page}; +use crate::template_utils::{IntoContext, Ructe}; +use plume_models::{db_conn::DbConn, posts::Post, PlumeRocket}; + +#[get("/tag/<name>?<page>")] +pub fn tag( + name: String, + page: Option<Page>, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Ructe, ErrorPage> { + let page = page.unwrap_or_default(); + let posts = Post::list_by_tag(&conn, name.clone(), page.limits())?; + Ok(render!(tags::index( + &(&conn, &rockets).to_context(), + name.clone(), + posts, + page.0, + Page::total(Post::count_for_tag(&conn, name)? as i32) + ))) +} diff --git a/src/routes/timelines.rs b/src/routes/timelines.rs new file mode 100644 index 00000000000..a5c24ff7155 --- /dev/null +++ b/src/routes/timelines.rs @@ -0,0 +1,56 @@ +#![allow(dead_code)] + +use crate::routes::Page; +use crate::template_utils::IntoContext; +use crate::{routes::errors::ErrorPage, template_utils::Ructe}; +use plume_models::{db_conn::DbConn, timeline::*, PlumeRocket}; +use rocket::response::Redirect; + +#[get("/timeline/<id>?<page>")] +pub fn details( + id: i32, + conn: DbConn, + rockets: PlumeRocket, + page: Option<Page>, +) -> Result<Ructe, ErrorPage> { + let page = page.unwrap_or_default(); + let all_tl = Timeline::list_all_for_user(&conn, rockets.user.clone().map(|u| u.id))?; + let tl = Timeline::get(&conn, id)?; + let posts = tl.get_page(&conn, page.limits())?; + let total_posts = tl.count_posts(&conn)?; + Ok(render!(timelines::details( + &(&conn, &rockets).to_context(), + tl, + posts, + all_tl, + page.0, + Page::total(total_posts as i32) + ))) +} + +// TODO + +#[get("/timeline/new")] +pub fn new() -> Result<Ructe, ErrorPage> { + unimplemented!() +} + +#[post("/timeline/new")] +pub fn create() -> Result<Redirect, Ructe> { + unimplemented!() +} + +#[get("/timeline/<_id>/edit")] +pub fn edit(_id: i32) -> Result<Ructe, ErrorPage> { + unimplemented!() +} + +#[post("/timeline/<_id>/edit")] +pub fn update(_id: i32) -> Result<Redirect, Ructe> { + unimplemented!() +} + +#[post("/timeline/<_id>/delete")] +pub fn delete(_id: i32) -> Result<Redirect, ErrorPage> { + unimplemented!() +} diff --git a/src/routes/user.rs b/src/routes/user.rs new file mode 100644 index 00000000000..576fc3c6d5f --- /dev/null +++ b/src/routes/user.rs @@ -0,0 +1,593 @@ +use activitypub::collection::{OrderedCollection, OrderedCollectionPage}; +use diesel::SaveChangesDsl; +use rocket::{ + http::{uri::Uri, ContentType, Cookies}, + request::LenientForm, + response::{status, Content, Flash, Redirect}, +}; +use rocket_i18n::I18n; +use std::{borrow::Cow, collections::HashMap}; +use validator::{Validate, ValidationError, ValidationErrors}; + +use crate::inbox; +use crate::routes::{ + email_signups::EmailSignupForm, errors::ErrorPage, Page, RemoteForm, RespondOrRedirect, +}; +use crate::template_utils::{IntoContext, Ructe}; +use crate::utils::requires_login; +use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest, Id}; +use plume_common::utils::md_to_html; +use plume_models::{ + blogs::Blog, + db_conn::DbConn, + follows, + headers::Headers, + inbox::inbox as local_inbox, + instance::Instance, + medias::Media, + posts::Post, + reshares::Reshare, + safe_string::SafeString, + signups::{self, Strategy as SignupStrategy}, + users::*, + Error, PlumeRocket, CONFIG, +}; + +#[get("/me")] +pub fn me(user: Option<User>) -> RespondOrRedirect { + match user { + Some(user) => Redirect::to(uri!(details: name = user.username)).into(), + None => requires_login("", uri!(me)).into(), + } +} + +#[get("/@/<name>", rank = 2)] +pub fn details(name: String, rockets: PlumeRocket, conn: DbConn) -> Result<Ructe, ErrorPage> { + let user = User::find_by_fqn(&conn, &name)?; + let recents = Post::get_recents_for_author(&*conn, &user, 6)?; + let reshares = Reshare::get_recents_for_author(&*conn, &user, 6)?; + + if !user.get_instance(&*conn)?.local { + tracing::trace!("remote user found"); + user.remote_user_found(); // Doesn't block + } + + Ok(render!(users::details( + &(&conn, &rockets).to_context(), + user.clone(), + rockets + .user + .clone() + .and_then(|x| x.is_following(&*conn, user.id).ok()) + .unwrap_or(false), + user.instance_id != Instance::get_local()?.id, + user.get_instance(&*conn)?.public_domain, + recents, + reshares + .into_iter() + .filter_map(|r| r.get_post(&*conn).ok()) + .collect() + ))) +} + +#[get("/dashboard")] +pub fn dashboard(user: User, conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> { + let blogs = Blog::find_for_author(&conn, &user)?; + Ok(render!(users::dashboard( + &(&conn, &rockets).to_context(), + blogs, + Post::drafts_by_author(&conn, &user)? + ))) +} + +#[get("/dashboard", rank = 2)] +pub fn dashboard_auth(i18n: I18n) -> Flash<Redirect> { + requires_login( + &i18n!( + i18n.catalog, + "To access your dashboard, you need to be logged in" + ), + uri!(dashboard), + ) +} + +#[post("/@/<name>/follow")] +pub fn follow( + name: String, + user: User, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Flash<Redirect>, ErrorPage> { + let target = User::find_by_fqn(&conn, &name)?; + let message = if let Ok(follow) = follows::Follow::find(&conn, user.id, target.id) { + let delete_act = follow.build_undo(&conn)?; + local_inbox( + &conn, + serde_json::to_value(&delete_act).map_err(Error::from)?, + )?; + + let msg = i18n!(rockets.intl.catalog, "You are no longer following {}."; target.name()); + rockets + .worker + .execute(move || broadcast(&user, delete_act, vec![target], CONFIG.proxy().cloned())); + msg + } else { + let f = follows::Follow::insert( + &conn, + follows::NewFollow { + follower_id: user.id, + following_id: target.id, + ap_url: String::new(), + }, + )?; + f.notify(&conn)?; + + let act = f.to_activity(&conn)?; + let msg = i18n!(rockets.intl.catalog, "You are now following {}."; target.name()); + rockets + .worker + .execute(move || broadcast(&user, act, vec![target], CONFIG.proxy().cloned())); + msg + }; + Ok(Flash::success( + Redirect::to(uri!(details: name = name)), + message, + )) +} + +#[post("/@/<name>/follow", data = "<remote_form>", rank = 2)] +pub fn follow_not_connected( + conn: DbConn, + rockets: PlumeRocket, + name: String, + remote_form: Option<LenientForm<RemoteForm>>, + i18n: I18n, +) -> Result<RespondOrRedirect, ErrorPage> { + let target = User::find_by_fqn(&conn, &name)?; + if let Some(remote_form) = remote_form { + if let Some(uri) = User::fetch_remote_interact_uri(&remote_form) + .ok() + .and_then(|uri| { + Some(uri.replace( + "{uri}", + &Uri::percent_encode(&target.acct_authority(&conn).ok()?), + )) + }) + { + Ok(Redirect::to(uri).into()) + } else { + let mut err = ValidationErrors::default(); + err.add("remote", + ValidationError { + code: Cow::from("invalid_remote"), + message: Some(Cow::from(i18n!(&i18n.catalog, "Couldn't obtain enough information about your account. Please make sure your username is correct."))), + params: HashMap::new(), + }, + ); + Ok(Flash::new( + render!(users::follow_remote( + &(&conn, &rockets).to_context(), + target, + super::session::LoginForm::default(), + ValidationErrors::default(), + remote_form.clone(), + err + )), + "callback", + uri!(follow: name = name).to_string(), + ) + .into()) + } + } else { + Ok(Flash::new( + render!(users::follow_remote( + &(&conn, &rockets).to_context(), + target, + super::session::LoginForm::default(), + ValidationErrors::default(), + #[allow(clippy::map_clone)] + remote_form.map(|x| x.clone()).unwrap_or_default(), + ValidationErrors::default() + )), + "callback", + uri!(follow: name = name).to_string(), + ) + .into()) + } +} + +#[get("/@/<name>/follow?local", rank = 2)] +pub fn follow_auth(name: String, i18n: I18n) -> Flash<Redirect> { + requires_login( + &i18n!( + i18n.catalog, + "To subscribe to someone, you need to be logged in" + ), + uri!(follow: name = name), + ) +} + +#[get("/@/<name>/followers?<page>", rank = 2)] +pub fn followers( + name: String, + page: Option<Page>, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Ructe, ErrorPage> { + let page = page.unwrap_or_default(); + let user = User::find_by_fqn(&conn, &name)?; + let followers_count = user.count_followers(&conn)?; + + Ok(render!(users::followers( + &(&conn, &rockets).to_context(), + user.clone(), + rockets + .user + .clone() + .and_then(|x| x.is_following(&conn, user.id).ok()) + .unwrap_or(false), + user.instance_id != Instance::get_local()?.id, + user.get_instance(&conn)?.public_domain, + user.get_followers_page(&conn, page.limits())?, + page.0, + Page::total(followers_count as i32) + ))) +} + +#[get("/@/<name>/followed?<page>", rank = 2)] +pub fn followed( + name: String, + page: Option<Page>, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Ructe, ErrorPage> { + let page = page.unwrap_or_default(); + let user = User::find_by_fqn(&conn, &name)?; + let followed_count = user.count_followed(&conn)?; + + Ok(render!(users::followed( + &(&conn, &rockets).to_context(), + user.clone(), + rockets + .user + .clone() + .and_then(|x| x.is_following(&conn, user.id).ok()) + .unwrap_or(false), + user.instance_id != Instance::get_local()?.id, + user.get_instance(&conn)?.public_domain, + user.get_followed_page(&conn, page.limits())?, + page.0, + Page::total(followed_count as i32) + ))) +} + +#[get("/@/<name>", rank = 1)] +pub fn activity_details( + name: String, + conn: DbConn, + _ap: ApRequest, +) -> Option<ActivityStream<CustomPerson>> { + let user = User::find_by_fqn(&conn, &name).ok()?; + Some(ActivityStream::new(user.to_activity(&conn).ok()?)) +} + +#[get("/users/new")] +pub fn new(conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> { + use SignupStrategy::*; + + let rendered = match CONFIG.signup { + Password => render!(users::new( + &(&conn, &rockets).to_context(), + Instance::get_local()?.open_registrations, + &NewUserForm::default(), + ValidationErrors::default() + )), + Email => render!(email_signups::new( + &(&conn, &rockets).to_context(), + Instance::get_local()?.open_registrations, + &EmailSignupForm::default(), + ValidationErrors::default() + )), + }; + Ok(rendered) +} + +#[get("/@/<name>/edit")] +pub fn edit( + name: String, + user: User, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Ructe, ErrorPage> { + if user.username == name && !name.contains('@') { + Ok(render!(users::edit( + &(&conn, &rockets).to_context(), + UpdateUserForm { + display_name: user.display_name.clone(), + email: user.email.clone().unwrap_or_default(), + summary: user.summary.clone(), + theme: user.preferred_theme, + hide_custom_css: user.hide_custom_css, + }, + ValidationErrors::default() + ))) + } else { + Err(Error::Unauthorized.into()) + } +} + +#[get("/@/<name>/edit", rank = 2)] +pub fn edit_auth(name: String, i18n: I18n) -> Flash<Redirect> { + requires_login( + &i18n!( + i18n.catalog, + "To edit your profile, you need to be logged in" + ), + uri!(edit: name = name), + ) +} + +#[derive(FromForm)] +pub struct UpdateUserForm { + pub display_name: String, + pub email: String, + pub summary: String, + pub theme: Option<String>, + pub hide_custom_css: bool, +} + +#[allow(unused_variables)] +#[put("/@/<name>/edit", data = "<form>")] +pub fn update( + name: String, + conn: DbConn, + mut user: User, + form: LenientForm<UpdateUserForm>, + intl: I18n, +) -> Result<Flash<Redirect>, ErrorPage> { + user.display_name = form.display_name.clone(); + user.email = Some(form.email.clone()); + user.summary = form.summary.clone(); + user.summary_html = SafeString::new( + &md_to_html( + &form.summary, + None, + false, + Some(Media::get_media_processor(&conn, vec![&user])), + ) + .0, + ); + user.preferred_theme = form + .theme + .clone() + .and_then(|t| if t.is_empty() { None } else { Some(t) }); + user.hide_custom_css = form.hide_custom_css; + let _: User = user.save_changes(&*conn).map_err(Error::from)?; + + Ok(Flash::success( + Redirect::to(uri!(me)), + i18n!(intl.catalog, "Your profile has been updated."), + )) +} + +#[post("/@/<name>/delete")] +pub fn delete( + name: String, + user: User, + mut cookies: Cookies<'_>, + conn: DbConn, + rockets: PlumeRocket, +) -> Result<Flash<Redirect>, ErrorPage> { + let account = User::find_by_fqn(&conn, &name)?; + if user.id == account.id { + account.delete(&conn)?; + + let target = User::one_by_instance(&conn)?; + let delete_act = account.delete_activity(&conn)?; + rockets + .worker + .execute(move || broadcast(&account, delete_act, target, CONFIG.proxy().cloned())); + + if let Some(cookie) = cookies.get_private(AUTH_COOKIE) { + cookies.remove_private(cookie); + } + + Ok(Flash::success( + Redirect::to(uri!(super::instance::index)), + i18n!(rockets.intl.catalog, "Your account has been deleted."), + )) + } else { + Ok(Flash::error( + Redirect::to(uri!(edit: name = name)), + i18n!( + rockets.intl.catalog, + "You can't delete someone else's account." + ), + )) + } +} + +#[derive(Default, FromForm, Validate)] +#[validate(schema( + function = "passwords_match", + skip_on_field_errors = false, + message = "Passwords are not matching" +))] +pub struct NewUserForm { + #[validate( + length(min = 1, message = "Username can't be empty"), + custom( + function = "validate_username", + message = "User name is not allowed to contain any of < > & @ ' or \"" + ) + )] + pub username: String, + #[validate(email(message = "Invalid email"))] + pub email: String, + #[validate(length(min = 8, message = "Password should be at least 8 characters long"))] + pub password: String, + #[validate(length(min = 8, message = "Password should be at least 8 characters long"))] + pub password_confirmation: String, +} + +pub fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> { + if form.password != form.password_confirmation { + Err(ValidationError::new("password_match")) + } else { + Ok(()) + } +} + +pub fn validate_username(username: &str) -> Result<(), ValidationError> { + if username.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\n', '\t'][..]) { + Err(ValidationError::new("username_illegal_char")) + } else { + Ok(()) + } +} + +fn to_validation(x: Error) -> ValidationErrors { + let mut errors = ValidationErrors::new(); + if let Error::Blocklisted(show, msg) = x { + if show { + errors.add( + "email", + ValidationError { + code: Cow::from("blocklisted"), + message: Some(Cow::from(msg)), + params: HashMap::new(), + }, + ); + } + } + errors.add( + "", + ValidationError { + code: Cow::from("server_error"), + message: Some(Cow::from("An unknown error occured")), + params: HashMap::new(), + }, + ); + errors +} + +#[post("/users/new", data = "<form>")] +pub fn create( + form: LenientForm<NewUserForm>, + conn: DbConn, + rockets: PlumeRocket, + _enabled: signups::Password, +) -> Result<Flash<Redirect>, Ructe> { + if !Instance::get_local() + .map(|i| i.open_registrations) + .unwrap_or(true) + { + return Ok(Flash::error( + Redirect::to(uri!(new)), + i18n!( + rockets.intl.catalog, + "Registrations are closed on this instance." + ), + )); // Actually, it is an error + } + + let mut form = form.into_inner(); + form.username = form.username.trim().to_owned(); + form.email = form.email.trim().to_owned(); + form.validate() + .and_then(|_| { + NewUser::new_local( + &conn, + form.username.to_string(), + form.username.to_string(), + Role::Normal, + "", + form.email.to_string(), + Some(User::hash_pass(&form.password).map_err(to_validation)?), + ).map_err(to_validation)?; + Ok(Flash::success( + Redirect::to(uri!(super::session::new: m = _)), + i18n!( + rockets.intl.catalog, + "Your account has been created. Now you just need to log in, before you can use it." + ), + )) + }) + .map_err(|err| { + render!(users::new( + &(&conn, &rockets).to_context(), + Instance::get_local() + .map(|i| i.open_registrations) + .unwrap_or(true), + &form, + err + )) + }) +} + +#[get("/@/<name>/outbox")] +pub fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection>> { + let user = User::find_by_fqn(&conn, &name).ok()?; + user.outbox(&conn).ok() +} +#[get("/@/<name>/outbox?<page>")] +pub fn outbox_page( + name: String, + page: Page, + conn: DbConn, +) -> Option<ActivityStream<OrderedCollectionPage>> { + let user = User::find_by_fqn(&conn, &name).ok()?; + user.outbox_page(&conn, page.limits()).ok() +} +#[post("/@/<name>/inbox", data = "<data>")] +pub fn inbox( + name: String, + data: inbox::SignedJson<serde_json::Value>, + headers: Headers<'_>, + conn: DbConn, +) -> Result<String, status::BadRequest<&'static str>> { + User::find_by_fqn(&conn, &name).map_err(|_| status::BadRequest(Some("User not found")))?; + inbox::handle_incoming(conn, data, headers) +} + +#[get("/@/<name>/followers", rank = 1)] +pub fn ap_followers( + name: String, + conn: DbConn, + _ap: ApRequest, +) -> Option<ActivityStream<OrderedCollection>> { + let user = User::find_by_fqn(&conn, &name).ok()?; + let followers = user + .get_followers(&conn) + .ok()? + .into_iter() + .map(|f| Id::new(f.ap_url)) + .collect::<Vec<Id>>(); + + let mut coll = OrderedCollection::default(); + coll.object_props + .set_id_string(user.followers_endpoint) + .ok()?; + coll.collection_props + .set_total_items_u64(followers.len() as u64) + .ok()?; + coll.collection_props.set_items_link_vec(followers).ok()?; + Some(ActivityStream::new(coll)) +} + +#[get("/@/<name>/atom.xml")] +pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> { + let conn = &conn; + let author = User::find_by_fqn(conn, &name).ok()?; + let entries = Post::get_recents_for_author(conn, &author, 15).ok()?; + let uri = Instance::get_local() + .ok()? + .compute_box("@", &name, "atom.xml"); + let title = &author.display_name; + let default_updated = &author.creation_date; + let feed = super::build_atom_feed(entries, &uri, title, default_updated, conn); + Some(Content( + ContentType::new("application", "atom+xml"), + feed.to_string(), + )) +} diff --git a/src/routes/well_known.rs b/src/routes/well_known.rs new file mode 100644 index 00000000000..759d4a06e70 --- /dev/null +++ b/src/routes/well_known.rs @@ -0,0 +1,80 @@ +use rocket::http::ContentType; +use rocket::response::Content; +use webfinger::*; + +use plume_models::{ap_url, blogs::Blog, db_conn::DbConn, users::User, CONFIG}; + +#[get("/.well-known/nodeinfo")] +pub fn nodeinfo() -> Content<String> { + Content( + ContentType::new("application", "jrd+json"), + json!({ + "links": [ + { + "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0", + "href": ap_url(&format!("{domain}/nodeinfo/2.0", domain = CONFIG.base_url.as_str())) + }, + { + "rel": "http://nodeinfo.diaspora.software/ns/schema/2.1", + "href": ap_url(&format!("{domain}/nodeinfo/2.1", domain = CONFIG.base_url.as_str())) + } + ] + }) + .to_string(), + ) +} + +#[get("/.well-known/host-meta")] +pub fn host_meta() -> String { + format!( + r#" + <?xml version="1.0"?> + <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"> + <Link rel="lrdd" type="application/xrd+xml" template="{url}"/> + </XRD> + "#, + url = ap_url(&format!( + "{domain}/.well-known/webfinger?resource={{uri}}", + domain = CONFIG.base_url.as_str() + )) + ) +} + +struct WebfingerResolver; + +impl Resolver<DbConn> for WebfingerResolver { + fn instance_domain<'a>() -> &'a str { + CONFIG.base_url.as_str() + } + + fn find(prefix: Prefix, acct: String, conn: DbConn) -> Result<Webfinger, ResolverError> { + match prefix { + Prefix::Acct => User::find_by_fqn(&conn, &acct) + .and_then(|usr| usr.webfinger(&*conn)) + .or(Err(ResolverError::NotFound)), + Prefix::Group => Blog::find_by_fqn(&conn, &acct) + .and_then(|blog| blog.webfinger(&*conn)) + .or(Err(ResolverError::NotFound)), + Prefix::Custom(_) => Err(ResolverError::NotFound), + } + } +} + +#[get("/.well-known/webfinger?<resource>")] +pub fn webfinger(resource: String, conn: DbConn) -> Content<String> { + match WebfingerResolver::endpoint(resource, conn) + .and_then(|wf| serde_json::to_string(&wf).map_err(|_| ResolverError::NotFound)) + { + Ok(wf) => Content(ContentType::new("application", "jrd+json"), wf), + Err(err) => Content( + ContentType::new("text", "plain"), + String::from(match err { + ResolverError::InvalidResource => { + "Invalid resource. Make sure to request an acct: URI" + } + ResolverError::NotFound => "Requested resource was not found", + ResolverError::WrongDomain => "This is not the instance of the requested resource", + }), + ), + } +} diff --git a/src/template_utils.rs b/src/template_utils.rs new file mode 100644 index 00000000000..4c41c0346a8 --- /dev/null +++ b/src/template_utils.rs @@ -0,0 +1,387 @@ +use plume_models::{db_conn::DbConn, notifications::*, users::User, Connection, PlumeRocket}; + +use crate::templates::Html; +use gettext::Catalog; +use rocket::http::hyper::header::{ETag, EntityTag}; +use rocket::http::{Method, Status}; +use rocket::request::Request; +use rocket::response::{self, content::Html as HtmlCt, Responder, Response}; +use std::collections::{btree_map::BTreeMap, hash_map::DefaultHasher}; +use std::hash::Hasher; + +pub use plume_common::utils::escape; + +pub static CACHE_NAME: &str = env!("CACHE_ID"); + +pub type BaseContext<'a> = &'a ( + &'a Connection, + &'a Catalog, + Option<User>, + Option<(String, String)>, +); + +pub trait IntoContext { + fn to_context( + &self, + ) -> ( + &Connection, + &Catalog, + Option<User>, + Option<(String, String)>, + ); +} + +impl IntoContext for (&DbConn, &PlumeRocket) { + fn to_context( + &self, + ) -> ( + &Connection, + &Catalog, + Option<User>, + Option<(String, String)>, + ) { + ( + self.0, + &self.1.intl.catalog, + self.1.user.clone(), + self.1.flash_msg.clone(), + ) + } +} + +#[derive(Debug)] +pub struct Ructe(pub Vec<u8>); + +impl<'r> Responder<'r> for Ructe { + fn respond_to(self, r: &Request<'_>) -> response::Result<'r> { + //if method is not Get or page contain a form, no caching + if r.method() != Method::Get || self.0.windows(6).any(|w| w == b"<form ") { + return HtmlCt(self.0).respond_to(r); + } + let mut hasher = DefaultHasher::new(); + hasher.write(&self.0); + let etag = format!("{:x}", hasher.finish()); + if r.headers() + .get("If-None-Match") + // This check matches both weak and strong ETags + // NGINX (and maybe other software) sometimes sends ETags with a + // "W/" prefix, that we ignore here + .any(|s| s[1..s.len() - 1] == etag || s[3..s.len() - 1] == etag) + { + Response::build() + .status(Status::NotModified) + .header(ETag(EntityTag::strong(etag))) + .ok() + } else { + Response::build() + .merge(HtmlCt(self.0).respond_to(r)?) + .header(ETag(EntityTag::strong(etag))) + .ok() + } + } +} + +#[macro_export] +macro_rules! render { + ($group:tt :: $page:tt ( $( $param:expr ),* ) ) => { + { + use crate::templates; + + let mut res = vec![]; + templates::$group::$page( + &mut res, + $( + $param + ),* + ).unwrap(); + Ructe(res) + } + } +} + +pub fn translate_notification(ctx: BaseContext<'_>, notif: Notification) -> String { + let name = notif + .get_actor(ctx.0) + .map_or_else(|_| i18n!(ctx.1, "Someone"), |user| user.name()); + match notif.kind.as_ref() { + notification_kind::COMMENT => i18n!(ctx.1, "{0} commented on your article."; &name), + notification_kind::FOLLOW => i18n!(ctx.1, "{0} is subscribed to you."; &name), + notification_kind::LIKE => i18n!(ctx.1, "{0} liked your article."; &name), + notification_kind::MENTION => i18n!(ctx.1, "{0} mentioned you."; &name), + notification_kind::RESHARE => i18n!(ctx.1, "{0} boosted your article."; &name), + _ => unreachable!("translate_notification: Unknow type"), + } +} + +pub fn i18n_timeline_name(cat: &Catalog, tl: &str) -> String { + match tl { + "Your feed" => i18n!(cat, "Your feed"), + "Local feed" => i18n!(cat, "Local feed"), + "Federated feed" => i18n!(cat, "Federated feed"), + n => n.to_string(), + } +} + +pub enum Size { + Small, + Medium, +} + +impl Size { + fn as_str(&self) -> &'static str { + match self { + Size::Small => "small", + Size::Medium => "medium", + } + } +} + +pub fn avatar( + conn: &Connection, + user: &User, + size: Size, + pad: bool, + catalog: &Catalog, +) -> Html<String> { + let name = escape(&user.name()).to_string(); + Html(format!( + r#"<div class="avatar {size} {padded}" + style="background-image: url('{url}');" + title="{title}" + aria-label="{title}"></div> + <img class="hidden u-photo" src="{url}"/>"#, + size = size.as_str(), + padded = if pad { "padded" } else { "" }, + url = user.avatar_url(conn), + title = i18n!(catalog, "{0}'s avatar"; name), + )) +} + +pub fn tabs(links: &[(impl AsRef<str>, String, bool)]) -> Html<String> { + let mut res = String::from(r#"<div class="tabs">"#); + for (url, title, selected) in links { + res.push_str(r#"<a dir="auto" href=""#); + res.push_str(url.as_ref()); + if *selected { + res.push_str(r#"" class="selected">"#); + } else { + res.push_str("\">"); + } + res.push_str(title); + res.push_str("</a>"); + } + res.push_str("</div>"); + Html(res) +} + +pub fn paginate(catalog: &Catalog, page: i32, total: i32) -> Html<String> { + paginate_param(catalog, page, total, None) +} +pub fn paginate_param( + catalog: &Catalog, + page: i32, + total: i32, + param: Option<String>, +) -> Html<String> { + let mut res = String::new(); + let param = param + .map(|mut p| { + p.push('&'); + p + }) + .unwrap_or_default(); + res.push_str(r#"<div class="pagination" dir="auto">"#); + if page != 1 { + res.push_str( + format!( + r#"<a href="?{}page={}">{}</a>"#, + param, + page - 1, + i18n!(catalog, "Previous page") + ) + .as_str(), + ); + } + if page < total { + res.push_str( + format!( + r#"<a href="?{}page={}">{}</a>"#, + param, + page + 1, + i18n!(catalog, "Next page") + ) + .as_str(), + ); + } + res.push_str("</div>"); + Html(res) +} + +pub fn encode_query_param(param: &str) -> String { + param + .chars() + .map(|c| match c { + '+' => Ok("%2B"), + ' ' => Err('+'), + c => Err(c), + }) + .fold(String::new(), |mut s, r| { + match r { + Ok(r) => s.push_str(r), + Err(r) => s.push(r), + }; + s + }) +} + +#[macro_export] +macro_rules! icon { + ($name:expr) => { + Html(concat!( + r#"<svg class="feather"><use xlink:href="/static/images/feather-sprite.svg#"#, + $name, + "\"/></svg>" + )) + }; +} + +/// A builder type to generate `<input>` tags in a type-safe way. +/// +/// # Example +/// +/// This example uses all options, but you don't have to specify everything. +/// +/// ```rust +/// # let current_email = "foo@bar.baz"; +/// # let catalog = gettext::Catalog::parse("").unwrap(); +/// Input::new("mail", "Your email address") +/// .input_type("email") +/// .default(current_email) +/// .optional() +/// .details("We won't use it for advertising.") +/// .set_prop("class", "email-input") +/// .to_html(catalog); +/// ``` +pub struct Input { + /// The name of the input (`name` and `id` in HTML). + name: String, + /// The description of this field. + label: String, + /// The `type` of the input (`text`, `email`, `password`, etc). + input_type: String, + /// The default value for this input field. + default: Option<String>, + /// `true` if this field is not required (will add a little badge next to the label). + optional: bool, + /// A small message to display next to the label. + details: Option<String>, + /// Additional HTML properties. + props: BTreeMap<String, String>, + /// The error message to show next to this field. + error: Option<String>, +} + +impl Input { + /// Creates a new input with a given name. + pub fn new(name: impl ToString, label: impl ToString) -> Input { + Input { + name: name.to_string(), + label: label.to_string(), + input_type: "text".into(), + default: None, + optional: false, + details: None, + props: BTreeMap::new(), + error: None, + } + } + + /// Set the `type` of this input. + pub fn input_type(mut self, t: impl ToString) -> Input { + self.input_type = t.to_string(); + self + } + + /// Marks this field as optional. + pub fn optional(mut self) -> Input { + self.optional = true; + self + } + + /// Fills the input with a default value (useful for edition form, to show the current values). + pub fn default(mut self, val: impl ToString) -> Input { + self.default = Some(val.to_string()); + self + } + + /// Adds additional information next to the label. + pub fn details(mut self, text: impl ToString) -> Input { + self.details = Some(text.to_string()); + self + } + + /// Defines an additional HTML property. + /// + /// This method can be called multiple times for the same input. + pub fn set_prop(mut self, key: impl ToString, val: impl ToString) -> Input { + self.props.insert(key.to_string(), val.to_string()); + self + } + + /// Shows an error message + pub fn error(mut self, errs: &validator::ValidationErrors) -> Input { + if let Some(field_errs) = errs.clone().field_errors().get(self.name.as_str()) { + self.error = Some( + field_errs[0] + .message + .clone() + .unwrap_or_default() + .to_string(), + ); + } + self + } + + /// Returns the HTML markup for this field. + pub fn html(mut self, cat: &Catalog) -> Html<String> { + if !self.optional { + self = self.set_prop("required", true); + } + + Html(format!( + r#" + <label for="{name}" dir="auto"> + {label} + {optional} + {details} + </label> + {error} + <input type="{kind}" id="{name}" name="{name}" value="{val}" {props} dir="auto"/> + "#, + name = self.name, + label = self.label, + kind = self.input_type, + optional = if self.optional { + format!("<small>{}</small>", i18n!(cat, "Optional")) + } else { + String::new() + }, + details = self + .details + .map(|d| format!("<small>{}</small>", d)) + .unwrap_or_default(), + error = self + .error + .map(|e| format!(r#"<p class="error" dir="auto">{}</p>"#, e)) + .unwrap_or_default(), + val = escape(&self.default.unwrap_or_default()), + props = self + .props + .into_iter() + .fold(String::new(), |mut res, (key, val)| { + res.push_str(&format!("{}=\"{}\" ", key, val)); + res + }) + )) + } +} diff --git a/src/test_routes.rs b/src/test_routes.rs new file mode 100644 index 00000000000..a24e1c7c047 --- /dev/null +++ b/src/test_routes.rs @@ -0,0 +1,2 @@ +#[get("/health")] +pub fn health() {} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 00000000000..b204814c073 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,17 @@ +use rocket::{ + http::uri::Uri, + response::{Flash, Redirect}, +}; + +/** +* Redirects to the login page with a given message. +* +* Note that the message should be translated before passed to this function. +*/ +pub fn requires_login<T: Into<Uri<'static>>>(message: &str, url: T) -> Flash<Redirect> { + Flash::new( + Redirect::to(format!("/login?m={}", Uri::percent_encode(message))), + "callback", + url.into().to_string(), + ) +} diff --git a/templates/base.rs.html b/templates/base.rs.html new file mode 100644 index 00000000000..564c1f84f6d --- /dev/null +++ b/templates/base.rs.html @@ -0,0 +1,104 @@ +@use plume_models::CONFIG; +@use plume_models::instance::Instance; +@use std::path::Path; +@use crate::template_utils::*; +@use crate::routes::*; + +@(ctx: BaseContext, title: String, head: Content, header: Content, content: Content) + +<!DOCTYPE html> +<html class="@ctx.2.clone().and_then(|u| u.preferred_theme).unwrap_or_else(|| CONFIG.default_theme.clone())"> + <head> + <meta charset="utf-8" /> + <title>@title ⋅ @i18n!(ctx.1, "Plume") + + + + + + @:head() + + +
+ + +
+
+ @if let Some(ref message) = ctx.3 { +

@message.1

+ } +
+
+ @:content() +
+ + + + diff --git a/templates/blogs/details.rs.html b/templates/blogs/details.rs.html new file mode 100644 index 00000000000..8598cc06ce8 --- /dev/null +++ b/templates/blogs/details.rs.html @@ -0,0 +1,92 @@ +@use plume_models::blogs::Blog; +@use plume_models::instance::Instance; +@use plume_models::posts::Post; +@use plume_models::users::User; +@use std::path::Path; +@use crate::templates::{base, partials::post_card}; +@use crate::template_utils::*; +@use crate::routes::*; + +@(ctx: BaseContext, blog: Blog, authors: &[User], page: i32, n_pages: i32, posts: Vec) + +@:base(ctx, blog.title.clone(), { + + + + + + + + + + + + + + + + @if !ctx.2.clone().map(|u| u.hide_custom_css).unwrap_or(false) { + @if let Some(ref theme) = blog.theme { + + } + } +}, { + @blog.title +}, { + +
+ @if let Some(banner_url) = blog.banner_url(ctx.0) { +
+ + } +
+
+
+
+ + +

+ @blog.title + ~@blog.fqn +

+ + @if ctx.2.clone().and_then(|u| u.is_author_in(ctx.0, &blog).ok()).unwrap_or(false) { + @i18n!(ctx.1, "New article") + @i18n!(ctx.1, "Edit") + } +
+ +
+

+ @i18n!(ctx.1, "There's one author on this blog: ", "There are {0} authors on this blog: "; authors.len()) + @for (i, author) in authors.iter().enumerate() {@if i >= 1 {, } + @author.name()} +

+ @Html(blog.summary_html.clone()) +
+
+ +
+

+ @i18n!(ctx.1, "Latest articles") + @icon!("rss") +

+ @if posts.is_empty() { +

@i18n!(ctx.1, "No posts to see here yet.")

+ } +
+ @for article in posts { + @:post_card(ctx, article) + } +
+ @paginate(ctx.1, page, n_pages) +
+
+}) diff --git a/templates/blogs/edit.rs.html b/templates/blogs/edit.rs.html new file mode 100644 index 00000000000..b7806830682 --- /dev/null +++ b/templates/blogs/edit.rs.html @@ -0,0 +1,59 @@ +@use validator::ValidationErrors; +@use plume_models::blogs::Blog; +@use plume_models::instance::Instance; +@use plume_models::medias::Media; +@use crate::template_utils::*; +@use crate::templates::base; +@use crate::templates::partials::image_select; +@use crate::routes::blogs; +@use crate::routes::blogs::EditForm; +@use crate::routes::medias; + +@(ctx: BaseContext, blog: &Blog, medias: Vec, form: &EditForm, errors: ValidationErrors) + +@:base(ctx, i18n!(ctx.1, "Edit \"{}\""; &blog.title), {}, { + @blog.title +}, { +

@i18n!(ctx.1, "Edit \"{}\""; &blog.title)

+
+ + + + @(Input::new("title", i18n!(ctx.1, "Title")) + .default(&form.title) + .error(&errors) + .set_prop("minlenght", 1) + .html(ctx.1)) + + + + +

+ @i18n!(ctx.1, "You can upload images to your gallery, to use them as blog icons, or banners.") + @i18n!(ctx.1, "Upload images") +

+ + @:image_select(ctx, "icon", i18n!(ctx.1, "Blog icon"), true, medias.clone(), form.icon) + @:image_select(ctx, "banner", i18n!(ctx.1, "Blog banner"), true, medias, form.banner) + + @if let Ok(themes) = Instance::list_themes() { + + + } else { +

@i18n!(ctx.1, "Error while loading theme selector.")

+ } + + +
+ +

@i18n!(ctx.1, "Danger zone")

+

@i18n!(ctx.1, "Be very careful, any action taken here can't be reversed.")

+
+ +
+}) diff --git a/templates/blogs/new.rs.html b/templates/blogs/new.rs.html new file mode 100644 index 00000000000..5b98a7e8a75 --- /dev/null +++ b/templates/blogs/new.rs.html @@ -0,0 +1,19 @@ +@use validator::ValidationErrors; +@use crate::templates::base; +@use crate::template_utils::*; +@use crate::routes::blogs::NewBlogForm; +@use crate::routes::*; + +@(ctx: BaseContext, form: &NewBlogForm, errors: ValidationErrors) + +@:base(ctx, i18n!(ctx.1, "New Blog"), {}, {}, { +

@i18n!(ctx.1, "Create a blog")

+
+ @(Input::new("title", i18n!(ctx.1, "Title")) + .default(&form.title) + .error(&errors) + .set_prop("minlength", 1) + .html(ctx.1)) + +
+}) diff --git a/templates/email_signups/create.rs.html b/templates/email_signups/create.rs.html new file mode 100644 index 00000000000..41e7483fa4a --- /dev/null +++ b/templates/email_signups/create.rs.html @@ -0,0 +1,9 @@ +@use crate::template_utils::*; +@use crate::templates::base; + +@(ctx: BaseContext) + +@:base(ctx, i18n!(ctx.1, "Registration"), {}, {}, { +

@i18n!(ctx.1, "Check your inbox!")

+

@i18n!(ctx.1, "We sent a mail to the address you gave us, with a link for registration.")

+}) diff --git a/templates/email_signups/edit.rs.html b/templates/email_signups/edit.rs.html new file mode 100644 index 00000000000..f6beebb588f --- /dev/null +++ b/templates/email_signups/edit.rs.html @@ -0,0 +1,43 @@ +@use std::borrow::Cow; +@use validator::{ValidationErrors, ValidationErrorsKind}; +@use crate::templates::base; +@use crate::template_utils::*; +@use crate::routes::email_signups::NewUserForm; +@use crate::routes::*; + +@(ctx: BaseContext, enabled: bool, form: &NewUserForm, errors: ValidationErrors) + +@:base(ctx, i18n!(ctx.1, "Create your account"), {}, {}, { + @if enabled { +

@i18n!(ctx.1, "Create an account")

+
+ @if let Some(ValidationErrorsKind::Field(errs)) = errors.clone().errors().get("__all__") { +

@errs[0].message.as_ref().unwrap_or(&Cow::from("Unknown error"))

+ } + + @(Input::new("username", i18n!(ctx.1, "Username")) + .default(&form.username) + .error(&errors) + .set_prop("required", "") + .html(ctx.1)) + @(Input::new("password", i18n!(ctx.1, "Password")) + .default(&form.password) + .error(&errors) + .set_prop("minlength", 8) + .input_type("password") + .html(ctx.1)) + @(Input::new("password_confirmation", i18n!(ctx.1, "Password confirmation")) + .default(&form.password_confirmation) + .error(&errors) + .set_prop("minlength", 8) + .input_type("password") + .html(ctx.1)) + + + + +
+ } else { +

@i18n!(ctx.1, "Apologies, but registrations are closed on this particular instance. You can, however, find a different one.")

+ } +}) diff --git a/templates/email_signups/new.rs.html b/templates/email_signups/new.rs.html new file mode 100644 index 00000000000..dd04a8622d9 --- /dev/null +++ b/templates/email_signups/new.rs.html @@ -0,0 +1,39 @@ +@use std::borrow::Cow; +@use validator::{ValidationErrors, ValidationErrorsKind}; +@use crate::templates::base; +@use crate::template_utils::*; +@use crate::routes::email_signups::EmailSignupForm; +@use crate::routes::*; + +@(ctx: BaseContext, enabled: bool, form: &EmailSignupForm, errors: ValidationErrors) + +@:base(ctx, i18n!(ctx.1, "Create your account"), {}, {}, { + @if enabled { +

@i18n!(ctx.1, "Create an account")

+
+ @if let Some(ValidationErrorsKind::Field(errs)) = errors.clone().errors().get("__all__") { +

@errs[0].message.as_ref().unwrap_or(&Cow::from("Unknown error"))

+ } + + @(Input::new("email", i18n!(ctx.1, "Email")) + .input_type("email") + .default(&form.email) + .error(&errors) + .set_prop("required", "") + .html(ctx.1)) + + @(Input::new("email_confirmation", i18n!(ctx.1, "Email confirmation")) + .input_type("email") + .default(&form.email_confirmation) + .error(&errors) + .set_prop("required", "") + .html(ctx.1)) + +

@i18n!(ctx.1, "An email will be sent to provided email. You can continue signing-up via the email.")

+ + +
+ } else { +

@i18n!(ctx.1, "Apologies, but registrations are closed on this particular instance. You can, however, find a different one.")

+ } +}) diff --git a/templates/errors/base.rs.html b/templates/errors/base.rs.html new file mode 100644 index 00000000000..247a6000d23 --- /dev/null +++ b/templates/errors/base.rs.html @@ -0,0 +1,9 @@ +@use crate::templates::base as base_template; +@use crate::template_utils::*; + +@(ctx: BaseContext, error_message: String, error: Content) + +@:base_template(ctx, error_message.clone(), {}, {}, { + @:error() +

@error_message

+}) diff --git a/templates/errors/csrf.rs.html b/templates/errors/csrf.rs.html new file mode 100644 index 00000000000..a66a1e95cd2 --- /dev/null +++ b/templates/errors/csrf.rs.html @@ -0,0 +1,13 @@ +@use crate::templates::errors::base; +@use crate::template_utils::*; + +@(ctx: BaseContext) + +@:base(ctx, i18n!(ctx.1, "Invalid CSRF token"), { +

@i18n!(ctx.1, "Invalid CSRF token")

+

+ @i18n!(ctx.1, + "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it." + ) +

+}) diff --git a/templates/errors/not_authorized.rs.html b/templates/errors/not_authorized.rs.html new file mode 100644 index 00000000000..2efd299349b --- /dev/null +++ b/templates/errors/not_authorized.rs.html @@ -0,0 +1,8 @@ +@use crate::templates::errors::base; +@use crate::template_utils::*; + +@(ctx: BaseContext, error_message: String) + +@:base(ctx, error_message, { +

@i18n!(ctx.1, "You are not authorized.")

+}) diff --git a/templates/errors/not_found.rs.html b/templates/errors/not_found.rs.html new file mode 100644 index 00000000000..758d976e106 --- /dev/null +++ b/templates/errors/not_found.rs.html @@ -0,0 +1,9 @@ +@use crate::templates::errors::base; +@use crate::template_utils::*; + +@(ctx: BaseContext) + +@:base(ctx, i18n!(ctx.1, "Page not found"), { +

@i18n!(ctx.1, "We couldn't find this page.")

+

@i18n!(ctx.1, "The link that led you here may be broken.")

+}) diff --git a/templates/errors/server_error.rs.html b/templates/errors/server_error.rs.html new file mode 100644 index 00000000000..5e5ce46dc75 --- /dev/null +++ b/templates/errors/server_error.rs.html @@ -0,0 +1,9 @@ +@use crate::templates::errors::base; +@use crate::template_utils::*; + +@(ctx: BaseContext) + +@:base(ctx, i18n!(ctx.1, "Internal server error"), { +

@i18n!(ctx.1, "Something broke on our side.")

+

@i18n!(ctx.1, "Sorry about that. If you think this is a bug, please report it.")

+}) diff --git a/templates/errors/unprocessable_entity.rs.html b/templates/errors/unprocessable_entity.rs.html new file mode 100644 index 00000000000..8e0347ac843 --- /dev/null +++ b/templates/errors/unprocessable_entity.rs.html @@ -0,0 +1,9 @@ +@use crate::templates::errors::base; +@use crate::template_utils::*; + +@(ctx: BaseContext) + +@:base(ctx, "Unprocessable entity".to_string(), { +

@i18n!(ctx.1, "The content you sent can't be processed.")

+

@i18n!(ctx.1, "Maybe it was too long.")

+}) diff --git a/templates/instance/about.rs.html b/templates/instance/about.rs.html new file mode 100644 index 00000000000..180521d7295 --- /dev/null +++ b/templates/instance/about.rs.html @@ -0,0 +1,36 @@ +@use plume_models::{instance::Instance, users::User}; +@use crate::templates::base; +@use crate::template_utils::*; +@use crate::routes::*; + +@(ctx: BaseContext, instance: Instance, admin: User, n_users: i64, n_articles: i64, n_instances: i64) + +@:base(ctx, i18n!(ctx.1, "About {0}"; instance.name.clone()), {}, {}, { +

@i18n!(ctx.1, "About {0}"; instance.name)

+
+ @Html(instance.short_description_html) + +

@i18n!(ctx.1, "Runs Plume {0}"; env!("CARGO_PKG_VERSION"))

+
+ +
+ @Html(instance.long_description_html) +
+}) diff --git a/templates/instance/admin.rs.html b/templates/instance/admin.rs.html new file mode 100644 index 00000000000..7ff3750d2f7 --- /dev/null +++ b/templates/instance/admin.rs.html @@ -0,0 +1,46 @@ +@use plume_models::instance::Instance; +@use validator::ValidationErrors; +@use crate::templates::base; +@use crate::template_utils::*; +@use crate::routes::instance::InstanceSettingsForm; +@use crate::routes::*; + +@(ctx: BaseContext, instance: Instance, form: InstanceSettingsForm, errors: ValidationErrors) + +@:base(ctx, i18n!(ctx.1, "Administration of {0}"; instance.name.clone()), {}, {}, { +

@i18n!(ctx.1, "Administration")

+ + @tabs(&[ + (&uri!(instance::admin).to_string(), i18n!(ctx.1, "Configuration"), true), + (&uri!(instance::admin_instances: page = _).to_string(), i18n!(ctx.1, "Instances"), false), + (&uri!(instance::admin_users: page = _).to_string(), i18n!(ctx.1, "Users"), false), + (&uri!(instance::admin_email_blocklist: page=_).to_string(), i18n!(ctx.1, "Email blocklist"), false) + ]) + +
+ @(Input::new("name", i18n!(ctx.1, "Name")) + .default(&form.name) + .error(&errors) + .set_prop("minlength", 1) + .html(ctx.1)) + + + + + + + + + + @(Input::new("default_license", i18n!(ctx.1, "Default article license")) + .default(&form.default_license) + .error(&errors) + .set_prop("minlength", 1) + .html(ctx.1)) + + +
+}) diff --git a/templates/instance/admin_mod.rs.html b/templates/instance/admin_mod.rs.html new file mode 100644 index 00000000000..e215d7e1e35 --- /dev/null +++ b/templates/instance/admin_mod.rs.html @@ -0,0 +1,15 @@ +@use crate::templates::base; +@use crate::template_utils::*; +@use crate::routes::*; + +@(ctx: BaseContext) + +@:base(ctx, i18n!(ctx.1, "Moderation"), {}, {}, { +

@i18n!(ctx.1, "Moderation")

+ + @tabs(&[ + (&uri!(instance::admin).to_string(), i18n!(ctx.1, "Home"), true), + (&uri!(instance::admin_instances: page = _).to_string(), i18n!(ctx.1, "Instances"), false), + (&uri!(instance::admin_users: page = _).to_string(), i18n!(ctx.1, "Users"), false), + ]) +}) diff --git a/templates/instance/emailblocklist.rs.html b/templates/instance/emailblocklist.rs.html new file mode 100644 index 00000000000..fe9729fcc91 --- /dev/null +++ b/templates/instance/emailblocklist.rs.html @@ -0,0 +1,71 @@ +@use plume_models::blocklisted_emails::BlocklistedEmail; +@use crate::templates::base; +@use crate::template_utils::*; +@use crate::routes::*; + +@(ctx:BaseContext, emails: Vec, page:i32, n_pages:i32) + @:base(ctx, i18n!(ctx.1, "Blocklisted Emails"), {}, {}, { +

@i18n!(ctx.1,"Blocklisted Emails")

+ @tabs(&[ + (&uri!(instance::admin).to_string(), i18n!(ctx.1, "Configuration"), false), + (&uri!(instance::admin_instances: page = _).to_string(), i18n!(ctx.1, "Instances"), false), + (&uri!(instance::admin_users: page = _).to_string(), i18n!(ctx.1, "Users"), false), + (&uri!(instance::admin_email_blocklist:page=_).to_string(), i18n!(ctx.1, "Email blocklist"), true), + ]) +
+ @(Input::new("email_address", i18n!(ctx.1, "Email address")) + .details(i18n!(ctx.1, "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com")) + .set_prop("minlength", 1) + .html(ctx.1)) + @(Input::new("note", i18n!(ctx.1, "Note")).optional().html(ctx.1)) + + @(Input::new("notification_text", i18n!(ctx.1, "Blocklisting notification")) + .optional() + .details(i18n!(ctx.1, "The message to be shown when the user attempts to create an account with this email address")).html(ctx.1)) + +
+
+
+ @if emails.is_empty() { +

@i18n!(ctx.1, "There are no blocked emails on your instance")

+ } else { + + } +
+
+ @for email in emails { +
+ +

+ + @i18n!(ctx.1, "Email address:") + @email.email_address +

+

+ + @i18n!(ctx.1, "Blocklisted for:") + @email.note +

+ +

+ @if email.notify_user { + + @i18n!(ctx.1, "Will notify them on account creation with this message:") + + @email.notification_text + } else { + @i18n!(ctx.1, "The user will be silently prevented from making an account") + } +

+ +
+ } +
+
+ @paginate(ctx.1, page, n_pages) +}) diff --git a/templates/instance/index.rs.html b/templates/instance/index.rs.html new file mode 100644 index 00000000000..0361318e7e3 --- /dev/null +++ b/templates/instance/index.rs.html @@ -0,0 +1,41 @@ +@use plume_models::instance::Instance; +@use plume_models::posts::Post; +@use plume_models::timeline::Timeline; +@use crate::templates::{base, partials::*}; +@use crate::template_utils::*; +@use crate::routes::*; + +@(ctx: BaseContext, instance: Instance, n_users: i64, n_articles: i64, all_tl: Vec<(Timeline, Vec)>) + +@:base(ctx, instance.name.clone(), {}, {}, { +

@i18n!(ctx.1, "Welcome to {}"; instance.name.as_str())

+ + @tabs(&vec![(format!("{}", uri!(instance::index)), i18n!(ctx.1, "Latest articles"), true)] + .into_iter().chain(all_tl.clone() + .into_iter() + .map(|(tl, _)| { + let url = format!("{}", uri!(timelines::details: id = tl.id, page = _)); + (url, i18n_timeline_name(ctx.1, &tl.name), false) + }) + ).collect::>() + ) + + @for (tl, articles) in all_tl { + @if !articles.is_empty() { +
+

+ @i18n_timeline_name(ctx.1, &tl.name) + — + @i18n!(ctx.1, "View all") +

+
+ @for article in articles { + @:post_card(ctx, article) + } +
+
+ } + } + + @:instance_description(ctx, instance, n_users, n_articles) +}) diff --git a/templates/instance/list.rs.html b/templates/instance/list.rs.html new file mode 100644 index 00000000000..619689f7861 --- /dev/null +++ b/templates/instance/list.rs.html @@ -0,0 +1,34 @@ +@use plume_models::instance::Instance; +@use crate::templates::base; +@use crate::template_utils::*; +@use crate::routes::*; + +@(ctx: BaseContext, instance: Instance, instances: Vec, page: i32, n_pages: i32) + +@:base(ctx, i18n!(ctx.1, "Administration of {0}"; instance.name), {}, {}, { +

@i18n!(ctx.1, "Instances")

+ + @tabs(&[ + (&uri!(instance::admin).to_string(), i18n!(ctx.1, "Configuration"), false), + (&uri!(instance::admin_instances: page = _).to_string(), i18n!(ctx.1, "Instances"), true), + (&uri!(instance::admin_users: page = _).to_string(), i18n!(ctx.1, "Users"), false), + (&uri!(instance::admin_email_blocklist:page=_).to_string(), i18n!(ctx.1, "Email blocklist"), false), + ]) + +
+ @for instance in instances { +
+

+ @instance.name + @instance.public_domain +

+ @if !instance.local { +
+ +
+ } +
+ } +
+ @paginate(ctx.1, page, n_pages) +}) diff --git a/templates/instance/privacy.rs.html b/templates/instance/privacy.rs.html new file mode 100644 index 00000000000..0dd8a4b2964 --- /dev/null +++ b/templates/instance/privacy.rs.html @@ -0,0 +1,13 @@ +@use crate::templates::base; +@use crate::template_utils::*; + +@(ctx: BaseContext) + +@:base(ctx, i18n!(ctx.1, "Privacy policy"), {}, {}, { +

@i18n!(ctx.1, "Privacy policy")

+
+

@i18n!(ctx.1, "If you are browsing this site as a visitor, no data about you is collected.")

+

@i18n!(ctx.1, "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it.")

+

@i18n!(ctx.1, "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies.")

+
+}) diff --git a/templates/instance/users.rs.html b/templates/instance/users.rs.html new file mode 100644 index 00000000000..8bcf48e72e3 --- /dev/null +++ b/templates/instance/users.rs.html @@ -0,0 +1,50 @@ +@use plume_models::users::User; +@use crate::templates::base; +@use crate::template_utils::*; +@use crate::routes::*; + +@(ctx: BaseContext, users: Vec, page: i32, n_pages: i32) + +@:base(ctx, i18n!(ctx.1, "Users"), {}, {}, { +

@i18n!(ctx.1, "Users")

+ + @tabs(&[ + (&uri!(instance::admin).to_string(), i18n!(ctx.1, "Configuration"), false), + (&uri!(instance::admin_instances: page = _).to_string(), i18n!(ctx.1, "Instances"), false), + (&uri!(instance::admin_users: page = _).to_string(), i18n!(ctx.1, "Users"), true), + (&uri!(instance::admin_email_blocklist: page=_).to_string(), i18n!(ctx.1, "Email blocklist"), false) + ]) + +
+
+ + +
+
+ @for user in users { +
+ + @avatar(ctx.0, &user, Size::Small, false, ctx.1) +

+ @user.name() + @format!("@{}", user.username) +

+ @if user.is_admin() { +

@i18n!(ctx.1, "Admin")

+ } else { + @if user.is_moderator() { +

@i18n!(ctx.1, "Moderator")

+ } + } +
+ } +
+
+ @paginate(ctx.1, page, n_pages) +}) diff --git a/templates/medias/details.rs.html b/templates/medias/details.rs.html new file mode 100644 index 00000000000..0f5e894a04f --- /dev/null +++ b/templates/medias/details.rs.html @@ -0,0 +1,40 @@ +@use plume_models::medias::{Media, MediaCategory}; +@use plume_models::safe_string::SafeString; +@use crate::templates::base; +@use crate::template_utils::*; +@use crate::routes::*; + +@(ctx: BaseContext, media: Media) + +@:base(ctx, i18n!(ctx.1, "Media details"), {}, {}, { +

@i18n!(ctx.1, "Media details")

+
+ @i18n!(ctx.1, "Go back to the gallery") +
+ +
+
+ @Html(media.html().unwrap_or_else(|_| SafeString::new(""))) +
@media.alt_text
+
+
+

+ @i18n!(ctx.1, "Markdown syntax") + @i18n!(ctx.1, "Copy it into your articles, to insert this media:") +

+ @media.markdown().unwrap_or_else(|_| SafeString::new("")) +
+
+ +
+ @if media.category() == MediaCategory::Image { +
+ +
+ } + +
+ +
+
+}) diff --git a/templates/medias/index.rs.html b/templates/medias/index.rs.html new file mode 100644 index 00000000000..faa59771469 --- /dev/null +++ b/templates/medias/index.rs.html @@ -0,0 +1,43 @@ +@use plume_models::medias::*; +@use crate::templates::base; +@use crate::template_utils::*; +@use crate::routes::*; + +@(ctx: BaseContext, medias: Vec, page: i32, n_pages: i32) + +@:base(ctx, i18n!(ctx.1, "Your media"), {}, {}, { +

@i18n!(ctx.1, "Your media")

+ + + @if medias.is_empty() { +

@i18n!(ctx.1, "You don't have any media yet.")

+ } + +
+ @for media in medias { +
+
+
+

@media.alt_text

+ @if let Some(cw) = media.content_warning { +

@i18n!(ctx.1, "Content warning: {0}"; cw)

+ } +
+ +
+ } +
+ @paginate(ctx.1, page, n_pages) +}) diff --git a/templates/medias/new.rs.html b/templates/medias/new.rs.html new file mode 100644 index 00000000000..40bb6f8700d --- /dev/null +++ b/templates/medias/new.rs.html @@ -0,0 +1,26 @@ +@use crate::templates::base; +@use crate::template_utils::*; +@use crate::routes::*; + +@(ctx: BaseContext) + +@:base(ctx, i18n!(ctx.1, "Media upload"), {}, {}, { +

@i18n!(ctx.1, "Media upload")

+
+ @(Input::new("alt", i18n!(ctx.1, "Description")) + .details(i18n!(ctx.1, "Useful for visually impaired people, as well as licensing information")) + .set_prop("minlenght", 1) + .html(ctx.1)) + + @(Input::new("cw", i18n!(ctx.1, "Content warning")) + .details(i18n!(ctx.1, "Leave it empty, if none is needed")) + .optional() + .html(ctx.1)) + + @(Input::new("file", i18n!(ctx.1, "File")) + .input_type("file") + .html(ctx.1)) + + +
+}) diff --git a/templates/notifications/index.rs.html b/templates/notifications/index.rs.html new file mode 100644 index 00000000000..92d5e5387a0 --- /dev/null +++ b/templates/notifications/index.rs.html @@ -0,0 +1,33 @@ +@use plume_models::notifications::Notification; +@use crate::templates::base; +@use crate::template_utils::*; + +@(ctx: BaseContext, notifications: Vec, page: i32, n_pages: i32) + +@:base(ctx, i18n!(ctx.1, "Notifications"), {}, {}, { +

@i18n!(ctx.1, "Notifications")

+ +
+ @for notification in notifications { +
+ +
+

+ @if let Some(url) = notification.get_url(ctx.0) { + + @translate_notification(ctx, notification.clone()) + + } else { + @translate_notification(ctx, notification.clone()) + } +

+ @if let Some(post) = notification.get_post(ctx.0) { +

@post.title

+ } +
+

@notification.creation_date.format("%B %e, %H:%M")

+
+ } +
+ @paginate(ctx.1, page, n_pages) +}) diff --git a/templates/partials/comment.rs.html b/templates/partials/comment.rs.html new file mode 100644 index 00000000000..f9f78da398c --- /dev/null +++ b/templates/partials/comment.rs.html @@ -0,0 +1,50 @@ +@use plume_models::comments::CommentTree; +@use crate::template_utils::*; +@use crate::routes::*; + +@(ctx: BaseContext, comment_tree: &CommentTree, in_reply_to: Option<&str>, blog: &str, slug: &str) + +@if let Some(comm) = Some(&comment_tree.comment) { +@if let Ok(author) = comm.get_author(ctx.0) { +@* comment-@comm.id is used for link *@ +
+
+
+ + @avatar(ctx.0, &author, Size::Small, true, ctx.1) + @author.name() + @author.fqn + +

+ @if let Ok(post) = comm.get_post(ctx.0) { + @* comment-@comm.id is same to this div's id attribute *@ + @comm.creation_date.format("%B %e, %Y %H:%M") + } +

+ + @if let Some(ref in_reply_to) = in_reply_to { + + } +
+
+ @if comm.sensitive { +
+ @comm.spoiler_text + } + @Html(&comm.content) + @if comm.sensitive { +
+ } +
+ @i18n!(ctx.1, "Respond") + @if ctx.2.clone().map(|u| u.id == author.id).unwrap_or(false) { +
+ +
+ } +
+ @for res in &comment_tree.responses { + @:comment_html(ctx, res, comm.ap_url.as_deref(), blog, slug) + } +
+}} diff --git a/templates/partials/image_select.rs.html b/templates/partials/image_select.rs.html new file mode 100644 index 00000000000..53e7eee0c09 --- /dev/null +++ b/templates/partials/image_select.rs.html @@ -0,0 +1,25 @@ +@use plume_models::medias::*; +@use crate::template_utils::*; + +@(ctx: BaseContext, id: &str, title: String, optional: bool, medias: Vec, selected: Option) + + + diff --git a/templates/partials/instance_description.rs.html b/templates/partials/instance_description.rs.html new file mode 100644 index 00000000000..3553e0f478d --- /dev/null +++ b/templates/partials/instance_description.rs.html @@ -0,0 +1,32 @@ +@use plume_models::instance::Instance; +@use crate::template_utils::*; +@use crate::routes::*; + +@(ctx: BaseContext, instance: Instance, n_users: i64, n_articles: i64) + +
+
+

@i18n!(ctx.1, "What is Plume?")

+
+

@i18n!(ctx.1, "Plume is a decentralized blogging engine.")

+

@i18n!(ctx.1, "Authors can manage multiple blogs, each as its own website.")

+

@i18n!(ctx.1, "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon.")

+
+ @i18n!(ctx.1, "Create your account") +
+
+

@i18n!(ctx.1, "About {0}"; instance.name)

+
+ @Html(instance.short_description_html) +
+
+

@Html(i18n!(ctx.1, "Home to {0} people"; n_users))

+
+
+

@Html(i18n!(ctx.1, "Who wrote {0} articles"; n_articles))

+
+
+
+ @i18n!(ctx.1, "Read the detailed rules") +
+
diff --git a/templates/partials/post_card.rs.html b/templates/partials/post_card.rs.html new file mode 100644 index 00000000000..e91d8311937 --- /dev/null +++ b/templates/partials/post_card.rs.html @@ -0,0 +1,55 @@ +@use plume_models::posts::Post; +@use crate::template_utils::*; +@use crate::routes::*; + +@(ctx: BaseContext, article: Post) + +
+ @if article.cover_id.is_some() { + +
+
+ } +
+

+ + @article.title + +

+ @if ctx.2.clone().and_then(|u| article.is_author(ctx.0, u.id).ok()).unwrap_or(false) { + + } +
+
+

@article.subtitle

+
+
+
+ @Html(i18n!(ctx.1, "By {0}"; format!( + "{}", + uri!(user::details: name = &article.get_authors(ctx.0).unwrap_or_default()[0].fqn), + escape(&article.get_authors(ctx.0).unwrap_or_default()[0].name()) + ))) + @if article.published { + ⋅ @article.creation_date.format("%B %e, %Y") + } + ⋅ @article.get_blog(ctx.0).unwrap().title + ⋅ +
+ @if !article.published { +
⋅ @i18n!(ctx.1, "Draft")
+ } else { +
+ + ⋅ + + @icon!("repeat") @article.count_reshares(ctx.0).unwrap_or_default() + +
+ } +
+
diff --git a/templates/posts/details.rs.html b/templates/posts/details.rs.html new file mode 100644 index 00000000000..a6674748755 --- /dev/null +++ b/templates/posts/details.rs.html @@ -0,0 +1,195 @@ +@use plume_models::blogs::Blog; +@use plume_models::comments::{Comment, CommentTree}; +@use plume_models::posts::Post; +@use plume_models::tags::Tag; +@use plume_models::users::User; +@use std::path::Path; +@use validator::ValidationErrors; +@use crate::templates::{base, partials::comment}; +@use crate::template_utils::*; +@use crate::routes::comments::NewCommentForm; +@use crate::routes::*; + +@(ctx: BaseContext, article: Post, blog: Blog, comment_form: &NewCommentForm, comment_errors: ValidationErrors, tags: Vec, comments: Vec, previous_comment: Option, n_likes: i64, n_reshares: i64, has_liked: bool, has_reshared: bool, is_following: bool, author: User) + +@:base(ctx, article.title.clone(), { + + + @if article.cover_id.is_some() { + + } + + + + + @if !ctx.2.clone().map(|u| u.hide_custom_css).unwrap_or(false) { + @if let Some(ref theme) = blog.theme { + + } + } +}, { + @blog.title +}, { +
+
+
+

@article.title

+ +

@article.subtitle

+
+ @if article.cover_id.is_some() { +
+ + } +
+ +
+ @Html(&article.content) +
+ +
+@if ctx.2.clone().and_then(|u| article.is_author(ctx.0, u.id).ok()).unwrap_or(false) { + +} +}) diff --git a/templates/posts/new.rs.html b/templates/posts/new.rs.html new file mode 100644 index 00000000000..5debc7afbdd --- /dev/null +++ b/templates/posts/new.rs.html @@ -0,0 +1,81 @@ +@use plume_models::medias::*; +@use plume_models::blogs::Blog; +@use plume_models::posts::Post; +@use std::borrow::Cow; +@use validator::{ValidationErrors, ValidationErrorsKind}; +@use crate::templates::base; +@use crate::templates::partials::image_select; +@use crate::template_utils::*; +@use crate::routes::posts::NewPostForm; +@use crate::routes::*; + +@(ctx: BaseContext, title: String, blog: Blog, editing: bool, form: &NewPostForm, is_draft: bool, article: Option, errors: ValidationErrors, medias: Vec, content_len: u64) + +@:base(ctx, title.clone(), {}, {}, { +

@title

+ + @if let Some(article) = article { +
+ } else { + + } + @(Input::new("title", i18n!(ctx.1, "Title")) + .default(&form.title) + .error(&errors) + .html(ctx.1)) + @(Input::new("subtitle", i18n!(ctx.1, "Subtitle")) + .default(&form.subtitle) + .error(&errors) + .optional() + .html(ctx.1)) + + @if let Some(ValidationErrorsKind::Field(errs)) = errors.clone().errors().get("content") { + @format!(r#"

{}

"#, errs[0].message.clone().unwrap_or_else(|| Cow::from("Unknown error"))) + } + + + + @content_len +

+ @i18n!(ctx.1, "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them.") + @i18n!(ctx.1, "Upload media") +

+ + @(Input::new("tags", i18n!(ctx.1, "Tags, separated by commas")) + .default(&form.tags) + .error(&errors) + .optional() + .html(ctx.1)) + @(Input::new("license", i18n!(ctx.1, "License")) + .default(&form.license) + .error(&errors) + .optional() + .details("Leave it empty to reserve all rights") + .html(ctx.1)) + + @:image_select(ctx, "cover", i18n!(ctx.1, "Illustration"), true, medias, form.cover) + + @if is_draft { + + } + + @if editing { + + } else { + @if is_draft { + + } else { + + } + } +
+}) diff --git a/templates/posts/remote_interact.rs.html b/templates/posts/remote_interact.rs.html new file mode 100644 index 00000000000..77d1605e541 --- /dev/null +++ b/templates/posts/remote_interact.rs.html @@ -0,0 +1,14 @@ +@use plume_models::posts::Post; +@use validator::ValidationErrors; +@use crate::templates::remote_interact_base; +@use crate::templates::partials::post_card; +@use crate::template_utils::*; +@use crate::routes::RemoteForm; +@use crate::routes::session::LoginForm; + +@(ctx: BaseContext, post: Post, login_form: LoginForm, login_errs: ValidationErrors, remote_form: RemoteForm, remote_errs: ValidationErrors) + +@:remote_interact_base(ctx, i18n!(ctx.1, "Interact with {}"; post.title.clone()), i18n!(ctx.1, "Log in to interact"), i18n!(ctx.1, "Enter your full username to interact"), { +

@i18n!(ctx.1, "Interact with {}"; post.title.clone())

+ @:post_card(ctx, post) +}, login_form, login_errs, remote_form, remote_errs) diff --git a/templates/remote_interact_base.rs.html b/templates/remote_interact_base.rs.html new file mode 100644 index 00000000000..ddcf9dc5898 --- /dev/null +++ b/templates/remote_interact_base.rs.html @@ -0,0 +1,46 @@ +@use validator::ValidationErrors; +@use crate::templates::base; +@use crate::template_utils::*; +@use crate::routes::session::LoginForm; +@use crate::routes::RemoteForm; + +@(ctx: BaseContext, title: String, login_msg: String, remote_msg: String, header: Content, login_form: LoginForm, login_errs: ValidationErrors, remote_form: RemoteForm, remote_errs: ValidationErrors) + +@:base(ctx, title, {}, {}, { +
+
+ @:header() +
+ +
+
+

@i18n!(ctx.1, "I'm from this instance")

+

@login_msg

+ @(Input::new("email_or_name", i18n!(ctx.1, "Username, or email")) + .default(&login_form.email_or_name) + .error(&login_errs) + .set_prop("minlenght", 1) + .html(ctx.1)) + @(Input::new("password", i18n!(ctx.1, "Password")) + .default(login_form.password) + .error(&login_errs) + .set_prop("minlength", 1) + .input_type("password") + .html(ctx.1)) + +
+ +
+

@i18n!(ctx.1, "I'm from another instance")

+

@remote_msg

+ @(Input::new("remote", i18n!(ctx.1, "Username")) + .details("Example: user@plu.me") + .default(&remote_form.remote) + .error(&remote_errs) + .set_prop("minlenght", 1) + .html(ctx.1)) + +
+
+
+}) diff --git a/templates/search/index.rs.html b/templates/search/index.rs.html new file mode 100644 index 00000000000..c4fe7f7314b --- /dev/null +++ b/templates/search/index.rs.html @@ -0,0 +1,66 @@ +@use crate::templates::base; +@use crate::template_utils::*; + +@(ctx: BaseContext, now: &str) + +@:base(ctx, i18n!(ctx.1, "Search"), {}, {}, { +

@i18n!(ctx.1, "Search")

+
+ @(Input::new("q", "Your query") + .input_type("search") + .set_prop("style", "-webkit-appearance: none;") + .optional() + .html(ctx.1)) +
+ @i18n!(ctx.1, "Advanced search") + + @(Input::new("title", i18n!(ctx.1, "Article title matching these words")) + .set_prop("placeholder", i18n!(ctx.1, "Title")) + .optional() + .html(ctx.1)) + @(Input::new("subtitle", i18n!(ctx.1, "Subtitle matching these words")) + .set_prop("placeholder", i18n!(ctx.1, "Subtitle")) + .optional() + .html(ctx.1)) + @(Input::new("content", i18n!(ctx.1, "Content macthing these words")) + .set_prop("placeholder", i18n!(ctx.1, "Body content")) + .optional() + .html(ctx.1)) + @(Input::new("after", i18n!(ctx.1, "From this date")) + .input_type("date") + .set_prop("max", now) + .optional() + .html(ctx.1)) + @(Input::new("before", i18n!(ctx.1, "To this date")) + .input_type("date") + .set_prop("max", now) + .optional() + .html(ctx.1)) + @(Input::new("tag", i18n!(ctx.1, "Containing these tags")) + .set_prop("placeholder", i18n!(ctx.1, "Tags")) + .optional() + .html(ctx.1)) + @(Input::new("instance", i18n!(ctx.1, "Posted on one of these instances")) + .set_prop("placeholder", i18n!(ctx.1, "Instance domain")) + .optional() + .html(ctx.1)) + @(Input::new("author", i18n!(ctx.1, "Posted by one of these authors")) + .set_prop("placeholder", i18n!(ctx.1, "Author(s)")) + .optional() + .html(ctx.1)) + @(Input::new("blog", i18n!(ctx.1, "Posted on one of these blogs")) + .set_prop("placeholder", i18n!(ctx.1, "Blog title")) + .optional() + .html(ctx.1)) + @(Input::new("lang", i18n!(ctx.1, "Written in this language")) + .set_prop("placeholder", i18n!(ctx.1, "Language")) + .optional() + .html(ctx.1)) + @(Input::new("license", i18n!(ctx.1, "Published under this license")) + .set_prop("placeholder", i18n!(ctx.1, "Article license")) + .optional() + .html(ctx.1)) +
+ +
+}) diff --git a/templates/search/result.rs.html b/templates/search/result.rs.html new file mode 100644 index 00000000000..cd0998e9b54 --- /dev/null +++ b/templates/search/result.rs.html @@ -0,0 +1,27 @@ +@use plume_models::posts::Post; +@use crate::templates::{base, partials::post_card}; +@use crate::template_utils::*; + +@(ctx: BaseContext, query_str: &str, articles: Vec, page: i32, n_pages: i32) + +@:base(ctx, i18n!(ctx.1, "Search result(s) for \"{0}\""; query_str), {}, {}, { +

@i18n!(ctx.1, "Search result(s)")

+

@query_str

+ + @if articles.is_empty() { +
+ @if page == 1 { +

@i18n!(ctx.1, "No results for your query")

+ } else { +

@i18n!(ctx.1, "No more results for your query")

+ } +
+ } else { +
+ @for article in articles { + @:post_card(ctx, article) + } +
+ } + @paginate_param(ctx.1, page, n_pages, Some(format!("q={}", encode_query_param(query_str)))) +}) diff --git a/templates/session/login.rs.html b/templates/session/login.rs.html new file mode 100644 index 00000000000..a154d06edd9 --- /dev/null +++ b/templates/session/login.rs.html @@ -0,0 +1,29 @@ +@use validator::ValidationErrors; +@use crate::template_utils::*; +@use crate::templates::base; +@use crate::routes::session::LoginForm; +@use crate::routes::*; + +@(ctx: BaseContext, message: Option, form: &LoginForm, errors: ValidationErrors) + +@:base(ctx, i18n!(ctx.1, "Log in"), {}, {}, { +

@i18n!(ctx.1, "Log in")

+ @if let Some(message) = message { +

@message

+ } +
+ @(Input::new("email_or_name", i18n!(ctx.1, "Username, or email")) + .default(&form.email_or_name) + .error(&errors) + .set_prop("minlenght", 1) + .html(ctx.1)) + @(Input::new("password", i18n!(ctx.1, "Password")) + .default(&form.password) + .error(&errors) + .set_prop("minlenght", 8) + .input_type("password") + .html(ctx.1)) + +
+ Forgot your password? +}) diff --git a/templates/session/password_reset.rs.html b/templates/session/password_reset.rs.html new file mode 100644 index 00000000000..5d33f7c77d1 --- /dev/null +++ b/templates/session/password_reset.rs.html @@ -0,0 +1,26 @@ +@use validator::ValidationErrors; +@use crate::template_utils::*; +@use crate::templates::base; +@use crate::routes::session::NewPasswordForm; + +@(ctx: BaseContext, form: &NewPasswordForm, errors: ValidationErrors) + +@:base(ctx, i18n!(ctx.1, "Reset your password"), {}, {}, { +

@i18n!(ctx.1, "Reset your password")

+ +
+ @(Input::new("password", i18n!(ctx.1, "New password")) + .default(&form.password) + .error(&errors) + .set_prop("minlenght", 8) + .input_type("password") + .html(ctx.1)) + @(Input::new("password_confirmation", i18n!(ctx.1, "Confirmation")) + .default(&form.password_confirmation) + .error(&errors) + .set_prop("minlenght", 8) + .input_type("password") + .html(ctx.1)) + +
+}) diff --git a/templates/session/password_reset_request.rs.html b/templates/session/password_reset_request.rs.html new file mode 100644 index 00000000000..6ac9f88c302 --- /dev/null +++ b/templates/session/password_reset_request.rs.html @@ -0,0 +1,20 @@ +@use validator::ValidationErrors; +@use crate::template_utils::*; +@use crate::templates::base; +@use crate::routes::session::ResetForm; + +@(ctx: BaseContext, form: &ResetForm, errors: ValidationErrors) + +@:base(ctx, i18n!(ctx.1, "Reset your password"), {}, {}, { +

@i18n!(ctx.1, "Reset your password")

+ +
+ @(Input::new("email", i18n!(ctx.1, "Email")) + .default(&form.email) + .error(&errors) + .set_prop("minlenght", 1) + .input_type("email") + .html(ctx.1)) + +
+}) diff --git a/templates/session/password_reset_request_expired.rs.html b/templates/session/password_reset_request_expired.rs.html new file mode 100644 index 00000000000..aad1fd76a1f --- /dev/null +++ b/templates/session/password_reset_request_expired.rs.html @@ -0,0 +1,9 @@ +@use crate::template_utils::*; +@use crate::templates::base; + +@(ctx: BaseContext) + +@:base(ctx, i18n!(ctx.1, "Password reset"), {}, {}, { +

@i18n!(ctx.1, "This token has expired")

+

@i18n!(ctx.1, "Please start the process again by clicking here.")

+}) diff --git a/templates/session/password_reset_request_ok.rs.html b/templates/session/password_reset_request_ok.rs.html new file mode 100644 index 00000000000..c9d194ead23 --- /dev/null +++ b/templates/session/password_reset_request_ok.rs.html @@ -0,0 +1,9 @@ +@use crate::template_utils::*; +@use crate::templates::base; + +@(ctx: BaseContext) + +@:base(ctx, i18n!(ctx.1, "Password reset"), {}, {}, { +

@i18n!(ctx.1, "Check your inbox!")

+

@i18n!(ctx.1, "We sent a mail to the address you gave us, with a link to reset your password.")

+}) diff --git a/templates/tags/index.rs.html b/templates/tags/index.rs.html new file mode 100644 index 00000000000..46159b09779 --- /dev/null +++ b/templates/tags/index.rs.html @@ -0,0 +1,22 @@ +@use plume_models::posts::Post; +@use crate::templates::{base, partials::post_card}; +@use crate::template_utils::*; + +@(ctx: BaseContext, tag: String, articles: Vec, page: i32, n_pages: i32) + +@:base(ctx, i18n!(ctx.1, "Articles tagged \"{0}\""; &tag), {}, {}, { +

@i18n!(ctx.1, "Articles tagged \"{0}\""; &tag)

+ + @if !articles.is_empty() { +
+ @for article in articles { + @:post_card(ctx, article) + } +
+ } else { +
+

@i18n!(ctx.1, "There are currently no articles with such a tag")

+
+ } + @paginate(ctx.1, page, n_pages) +}) diff --git a/templates/timelines/details.rs.html b/templates/timelines/details.rs.html new file mode 100644 index 00000000000..916b4f764cd --- /dev/null +++ b/templates/timelines/details.rs.html @@ -0,0 +1,35 @@ +@use plume_models::posts::Post; +@use plume_models::timeline::Timeline; +@use crate::template_utils::*; +@use crate::templates::base; +@use crate::templates::partials::post_card; +@use crate::routes::*; + +@(ctx: BaseContext, tl: Timeline, articles: Vec, all_tl: Vec, page: i32, n_pages: i32) + +@:base(ctx, tl.name.clone(), {}, {}, { +
+

@i18n_timeline_name(ctx.1, &tl.name)

+
+ + @tabs(&vec![(format!("{}", uri!(instance::index)), i18n!(ctx.1, "Latest articles"), false)] + .into_iter().chain(all_tl + .into_iter() + .map(|t| { + let url = format!("{}", uri!(timelines::details: id = t.id, page = _)); + (url, i18n_timeline_name(ctx.1, &t.name), t.id == tl.id) + }) + ).collect::>() + ) + + @if !articles.is_empty() { +
+ @for article in articles { + @:post_card(ctx, article) + } +
+ } else { +

@i18n!(ctx.1, "Nothing to see here yet.")

+ } + @paginate(ctx.1, page, n_pages) +}) diff --git a/templates/users/dashboard.rs.html b/templates/users/dashboard.rs.html new file mode 100644 index 00000000000..698c1620f0d --- /dev/null +++ b/templates/users/dashboard.rs.html @@ -0,0 +1,49 @@ +@use plume_models::blogs::Blog; +@use plume_models::posts::Post; +@use crate::templates::{base, partials::post_card}; +@use crate::template_utils::*; +@use crate::routes::*; + +@(ctx: BaseContext, blogs: Vec, drafts: Vec) + +@:base(ctx, i18n!(ctx.1, "Your Dashboard"), {}, {}, { +

@i18n!(ctx.1, "Your Dashboard")

+ +
+

@i18n!(ctx.1, "Your Blogs")

+ @if blogs.is_empty() { +

@i18n!(ctx.1, "You don't have any blog yet. Create your own, or ask to join one.")

+ } +
+ @for blog in blogs { +
+ @if blog.banner_id.is_some() { + +
+
+
+ } +

@blog.title

+

@Html(blog.summary_html)

+
+ } +
+ @i18n!(ctx.1, "Start a new blog") +
+ + @if !drafts.is_empty() { +
+

@i18n!(ctx.1, "Your Drafts")

+
+ @for draft in drafts { + @:post_card(ctx, draft) + } +
+
+ } + +
+

@i18n!(ctx.1, "Your media")

+ @i18n!(ctx.1, "Go to your gallery") +
+}) diff --git a/templates/users/details.rs.html b/templates/users/details.rs.html new file mode 100644 index 00000000000..609e26f9e6a --- /dev/null +++ b/templates/users/details.rs.html @@ -0,0 +1,59 @@ +@use plume_models::instance::Instance; +@use plume_models::users::User; +@use plume_models::posts::Post; +@use crate::templates::{base, partials::post_card, users::header}; +@use crate::template_utils::*; +@use crate::routes::*; + +@(ctx: BaseContext, user: User, follows: bool, is_remote: bool, remote_url: String, recents: Vec, reshares: Vec) + +@:base(ctx, user.name(), { + + + + + + + + + + + + + + + +}, {}, { + @:header(ctx, &user, follows, is_remote, remote_url) + + @tabs(&[ + (&uri!(user::details: name = &user.fqn).to_string(), i18n!(ctx.1, "Articles"), true), + (&uri!(user::followers: name = &user.fqn, page = _).to_string(), i18n!(ctx.1, "Subscribers"), false), + (&uri!(user::followed: name = &user.fqn, page = _).to_string(), i18n!(ctx.1, "Subscriptions"), false) + ]) + + @if !recents.is_empty() { +
+

+ @i18n!(ctx.1, "Latest articles") + @icon!("rss") +

+
+ @for article in recents { + @:post_card(ctx, article) + } +
+
+ } + + @if !reshares.is_empty() { +
+

@i18n!(ctx.1, "Recently boosted")

+
+ @for article in reshares { + @:post_card(ctx, article) + } +
+
+ } +}) diff --git a/templates/users/edit.rs.html b/templates/users/edit.rs.html new file mode 100644 index 00000000000..bad49b42cdb --- /dev/null +++ b/templates/users/edit.rs.html @@ -0,0 +1,63 @@ +@use plume_models::instance::Instance; +@use validator::ValidationErrors; +@use crate::templates::base; +@use crate::template_utils::*; +@use crate::routes::user::UpdateUserForm; +@use crate::routes::*; + +@(ctx: BaseContext, form: UpdateUserForm, errors: ValidationErrors) + +@:base(ctx, i18n!(ctx.1, "Edit your account"), {}, {}, { + @if let Some(u) = ctx.2.clone() { +

@i18n!(ctx.1, "Your Profile")

+

+ @i18n!(ctx.1, "To change your avatar, upload it to your gallery and then select from there.") + @i18n!(ctx.1, "Upload an avatar") +

+
+ + + + @(Input::new("display_name", i18n!(ctx.1, "Display name")) + .default(&form.display_name) + .error(&errors) + .html(ctx.1)) + @(Input::new("email", i18n!(ctx.1, "Email")) + .default(&form.email) + .error(&errors) + .input_type("email") + .html(ctx.1)) + + + + @if let Ok(themes) = Instance::list_themes() { + + + } else { +

@i18n!(ctx.1, "Error while loading theme selector.")

+ } + + + + +
+ +

@i18n!(ctx.1, "Danger zone")

+

@i18n!(ctx.1, "Be very careful, any action taken here can't be cancelled.") + @if !u.is_admin() { +

+ +
+ } else { +

@i18n!(ctx.1, "Sorry, but as an admin, you can't leave your own instance.")

+ } + } +}) diff --git a/templates/users/follow_remote.rs.html b/templates/users/follow_remote.rs.html new file mode 100644 index 00000000000..7b291187b2e --- /dev/null +++ b/templates/users/follow_remote.rs.html @@ -0,0 +1,14 @@ +@use plume_models::users::User; +@use validator::ValidationErrors; +@use crate::templates::remote_interact_base; +@use crate::template_utils::*; +@use crate::routes::RemoteForm; +@use crate::routes::session::LoginForm; + +@(ctx: BaseContext, user: User, login_form: LoginForm, login_errs: ValidationErrors, remote_form: RemoteForm, remote_errs: ValidationErrors) + +@:remote_interact_base(ctx, i18n!(ctx.1, "Follow {}"; user.name()), i18n!(ctx.1, "Log in to follow"), i18n!(ctx.1, "Enter your full username handle to follow"), { +

@i18n!(ctx.1, "Follow {}"; user.name())

+ @avatar(ctx.0, &user, Size::Medium, false, ctx.1) + @@@user.fqn +}, login_form, login_errs, remote_form, remote_errs) diff --git a/templates/users/followed.rs.html b/templates/users/followed.rs.html new file mode 100644 index 00000000000..106d6003bc6 --- /dev/null +++ b/templates/users/followed.rs.html @@ -0,0 +1,26 @@ +@use plume_models::users::User; +@use crate::templates::{base, users::header}; +@use crate::template_utils::*; +@use crate::routes::*; + +@(ctx: BaseContext, user: User, follows: bool, is_remote: bool, remote_url: String, followed: Vec, page: i32, n_pages: i32) + +@:base(ctx, i18n!(ctx.1, "{0}'s subscriptions"; user.name()), {}, {}, { + @:header(ctx, &user, follows, is_remote, remote_url) + + @tabs(&[ + (&uri!(user::details: name = &user.fqn).to_string(), i18n!(ctx.1, "Articles"), false), + (&uri!(user::followers: name = &user.fqn, page = _).to_string(), i18n!(ctx.1, "Subscribers"), false), + (&uri!(user::followed: name = &user.fqn, page = _).to_string(), i18n!(ctx.1, "Subscriptions"), true) + ]) + +
+ @for follow in followed { +
+

@follow.name() @format!("@{}", &follow.fqn)

+

@Html(follow.summary_html)

+
+ } +
+ @paginate(ctx.1, page, n_pages) +}) diff --git a/templates/users/followers.rs.html b/templates/users/followers.rs.html new file mode 100644 index 00000000000..65202ad0d7b --- /dev/null +++ b/templates/users/followers.rs.html @@ -0,0 +1,26 @@ +@use plume_models::users::User; +@use crate::templates::{base, users::header}; +@use crate::template_utils::*; +@use crate::routes::*; + +@(ctx: BaseContext, user: User, follows: bool, is_remote: bool, remote_url: String, followers: Vec, page: i32, n_pages: i32) + +@:base(ctx, i18n!(ctx.1, "{0}'s subscribers"; user.name()), {}, {}, { + @:header(ctx, &user, follows, is_remote, remote_url) + + @tabs(&[ + (&uri!(user::details: name = &user.fqn).to_string(), i18n!(ctx.1, "Articles"), false), + (&uri!(user::followers: name = &user.fqn, page = _).to_string(), i18n!(ctx.1, "Subscribers"), true), + (&uri!(user::followed: name = &user.fqn, page = _).to_string(), i18n!(ctx.1, "Subscriptions"), false) + ]) + +
+ @for follower in followers { +
+

@follower.name() @format!("@{}", &follower.fqn)

+

@Html(follower.summary_html)

+
+ } +
+ @paginate(ctx.1, page, n_pages) +}) diff --git a/templates/users/header.rs.html b/templates/users/header.rs.html new file mode 100644 index 00000000000..7e4813b183e --- /dev/null +++ b/templates/users/header.rs.html @@ -0,0 +1,48 @@ +@use plume_models::users::User; +@use crate::template_utils::*; +@use crate::routes::*; + +@(ctx: BaseContext, user: &User, follows: bool, is_remote: bool, instance_url: String) + +
+
+
+ @avatar(ctx.0, user, Size::Medium, false, ctx.1) + +

+ @user.name() + @user.fqn +

+ +

+ @if user.is_admin() { + @i18n!(ctx.1, "Admin") + } + + @if ctx.2.clone().map(|u| u.id == user.id).unwrap_or(false) { + @i18n!(ctx.1, "It is you") + @i18n!(ctx.1, "Edit your profile") + } +

+
+ + @if is_remote { + @i18n!(ctx.1, "Open on {0}"; instance_url) + } else { + + } + + @if ctx.2.clone().map(|u| u.id != user.id).unwrap_or(true) { +
+ @if follows { + + } else { + + } +
+ } +
+
+ @Html(user.summary_html.clone()) +
+
diff --git a/templates/users/new.rs.html b/templates/users/new.rs.html new file mode 100644 index 00000000000..1bf91acd9d8 --- /dev/null +++ b/templates/users/new.rs.html @@ -0,0 +1,46 @@ +@use std::borrow::Cow; +@use validator::{ValidationErrors, ValidationErrorsKind}; +@use crate::templates::base; +@use crate::template_utils::*; +@use crate::routes::user::NewUserForm; +@use crate::routes::*; + +@(ctx: BaseContext, enabled: bool, form: &NewUserForm, errors: ValidationErrors) + +@:base(ctx, i18n!(ctx.1, "Create your account"), {}, {}, { + @if enabled { +

@i18n!(ctx.1, "Create an account")

+
+ @if let Some(ValidationErrorsKind::Field(errs)) = errors.clone().errors().get("__all__") { +

@errs[0].message.as_ref().unwrap_or(&Cow::from("Unknown error"))

+ } + + @(Input::new("username", i18n!(ctx.1, "Username")) + .default(&form.username) + .error(&errors) + .set_prop("minlength", 1) + .html(ctx.1)) + @(Input::new("email", i18n!(ctx.1, "Email")) + .default(&form.email) + .error(&errors) + .set_prop("minlength", 1) + .html(ctx.1)) + @(Input::new("password", i18n!(ctx.1, "Password")) + .default(&form.password) + .error(&errors) + .set_prop("minlength", 8) + .input_type("password") + .html(ctx.1)) + @(Input::new("password_confirmation", i18n!(ctx.1, "Password confirmation")) + .default(&form.password_confirmation) + .error(&errors) + .set_prop("minlength", 8) + .input_type("password") + .html(ctx.1)) + + +
+ } else { +

@i18n!(ctx.1, "Apologies, but registrations are closed on this particular instance. You can, however, find a different one.")

+ } +})