Compare commits

..

31 Commits

Author SHA1 Message Date
Ana Gelez 9ddba17d46 I guess it was localhost this time 2020-08-10 18:59:52 +02:00
Ana Gelez b04426a330 I want some logs please 2020-08-10 18:28:11 +02:00
Ana Gelez 925254983e Update Caddyfile for integration tests 2020-08-10 18:03:51 +02:00
Ana Gelez 4c6fb83793 Change another network name 2020-08-10 17:06:14 +02:00
Ana Gelez 7c456009be Fix Selenium address, remove volumes 2020-08-10 16:47:13 +02:00
Ana Gelez db916039db Fix syntax 2020-08-10 16:00:45 +02:00
Ana Gelez 06c625c686 Add a shared volume for WebDriver's socket 2020-08-10 15:59:06 +02:00
Ana Gelez f2203710cb Start selenium 2020-08-10 15:37:52 +02:00
Ana Gelez 3c1617c4f9 Add a dummy password 2020-08-10 14:40:36 +02:00
Ana Gelez 2388a5846d Fix PostgreSQL user 2020-08-10 14:17:59 +02:00
Ana Gelez b102534136 Set DATABASE_URL in integration tests 2020-08-10 13:03:33 +02:00
Ana Gelez 072e32da30 Try something to make integration-sqlite run 2020-08-10 12:57:17 +02:00
Ana Gelez 5ea3e73727 Update PostgreSQL image 2020-08-10 12:33:53 +02:00
Ana Gelez f340bd50c7 Passwordless PostgreSQL connections 2020-08-10 11:38:38 +02:00
Ana Gelez 3de6b46465 Start PostgreSQL and set BROWSER for integration tests 2020-08-10 11:15:02 +02:00
Ana Gelez 3c6d5de314 Unique artifact path 2020-07-26 20:37:09 +02:00
Ana Gelez 2a4b98dce4 test feature is only for plume, not plm 2020-07-26 19:47:47 +02:00
Ana Gelez d253fee523 Run tests one by one 2020-07-26 19:42:43 +02:00
Ana Gelez 07731d0b73 Forgot a space once again 2020-07-26 19:39:00 +02:00
Ana Gelez 15cbd17003 Try to save artifacts to Minio 2020-07-26 19:29:59 +02:00
Ana Gelez 5d3b3485fa Enable debugging of cache 2020-07-26 17:55:26 +02:00
Ana Gelez 8a2788bf6a Try another region 2020-07-26 17:49:15 +02:00
Ana Gelez ecbd64efb1 OK, it actually needs a region 2020-07-26 17:17:56 +02:00
Ana Gelez 9245320712 Forgot a space 2020-07-26 17:13:03 +02:00
Ana Gelez 7cf3a4b37c Let's try without a region 2020-07-26 17:11:00 +02:00
Ana Gelez 3ddd6d0254 Try to use Minio for caching 2020-07-26 17:07:41 +02:00
Ana Gelez 7edd0220b6 Dummy commit to retrigger the build 2020-07-26 16:28:03 +02:00
Ana Gelez b26e785277 Continue the Drone CI config 2020-07-15 12:40:11 +02:00
Ana Gelez b2829908f1 Fix a few errors in the config 2020-06-26 13:53:11 +02:00
Ana Gelez 60bb5b72f6 Just a dumb commit to retrigger a build, please squash it 2020-06-26 13:47:31 +02:00
Ana Gelez 9e0bbf81ed WIP: Drone CI config 2020-06-26 13:38:13 +02:00
192 changed files with 25018 additions and 35065 deletions
-10
View File
@@ -1,10 +0,0 @@
[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",
]
+9 -13
View File
@@ -10,7 +10,7 @@ executors:
type: boolean type: boolean
default: false default: false
docker: docker:
- image: plumeorg/plume-buildenv:v0.4.0 - image: plumeorg/plume-buildenv:v0.0.9
- image: <<#parameters.postgres>>circleci/postgres:9.6-alpine<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>> - image: <<#parameters.postgres>>circleci/postgres:9.6-alpine<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
environment: environment:
POSTGRES_USER: postgres POSTGRES_USER: postgres
@@ -21,7 +21,6 @@ executors:
RUST_TEST_THREADS: 1 RUST_TEST_THREADS: 1
FEATURES: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>> FEATURES: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
DATABASE_URL: <<#parameters.postgres>>postgres://postgres@localhost/plume<</parameters.postgres>><<^parameters.postgres>>plume.sqlite<</parameters.postgres>> DATABASE_URL: <<#parameters.postgres>>postgres://postgres@localhost/plume<</parameters.postgres>><<^parameters.postgres>>plume.sqlite<</parameters.postgres>>
ROCKET_SECRET_KEY: VN5xV1DN7XdpATadOCYcuGeR/dV0hHfgx9mx9TarLdM=
commands: commands:
@@ -72,7 +71,7 @@ commands:
type: string type: string
steps: steps:
- run: | - 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 RUSTFLAGS="-Zprofile -Zfewer-names -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zno-landing-pads -Clink-arg=-Xlinker -Clink-arg=--no-keep-memory -Clink-arg=-Xlinker -Clink-arg=--reduce-memory-overheads"
export CARGO_INCREMENTAL=0 export CARGO_INCREMENTAL=0
<< parameters.cmd >> << parameters.cmd >>
@@ -100,7 +99,7 @@ commands:
steps: steps:
- run: | - run: |
cmd="cargo build <<#parameters.release>>--release<</parameters.release>> --no-default-features --features="${FEATURES}" -p <<parameters.package>> -j" cmd="cargo build <<#parameters.release>>--release<</parameters.release>> --no-default-features --features="${FEATURES}" -p <<parameters.package>> -j"
for i in 16 4 2 1 1; do for i in 36 4 2 1 1; do
$cmd $i && exit 0 $cmd $i && exit 0
done done
exit 1 exit 1
@@ -144,14 +143,11 @@ jobs:
cache: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>> cache: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
- run_with_coverage: - run_with_coverage:
cmd: | 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" 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 for i in 36 4 2 1 1; do
$cmd $i && break $cmd $i && break
done done
cargo test --all --exclude plume-front --exclude plume-macro --no-default-features --features="${FEATURES}" -j1 cargo test --all --exclude plume-front --exclude plume-macro --no-default-features --features="${FEATURES}" -j1 -- --test-threads=1
- upload_coverage: - upload_coverage:
type: unit type: unit
- cache: - cache:
@@ -168,18 +164,18 @@ jobs:
steps: steps:
- restore_env: - restore_env:
cache: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>> cache: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
- run: wasm-pack build --target web --release plume-front - run: cargo web deploy -p plume-front
- run_with_coverage: - run_with_coverage:
cmd: | cmd: |
cmd="cargo install --debug --no-default-features --features="${FEATURES}",test --force --path . -j" cmd="cargo install --debug --no-default-features --features="${FEATURES}",test --force --path . -j"
for i in 16 4 2 1 1; do for i in 36 4 2 1 1; do
$cmd $i && exit 0 $cmd $i && exit 0
done done
exit 1 exit 1
- run_with_coverage: - run_with_coverage:
cmd: | cmd: |
cmd="cargo install --debug --no-default-features --features="${FEATURES}" --force --path plume-cli -j" cmd="cargo install --debug --no-default-features --features="${FEATURES}" --force --path plume-cli -j"
for i in 16 4 2 1 1; do for i in 36 4 2 1 1; do
$cmd $i && exit 0 $cmd $i && exit 0
done done
exit 1 exit 1
@@ -203,7 +199,7 @@ jobs:
steps: steps:
- restore_env: - restore_env:
cache: release-<<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>> cache: release-<<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
- run: wasm-pack build --target web --release plume-front - run: cargo web deploy -p plume-front --release
- build: - build:
package: plume package: plume
release: true release: true
+5 -2
View File
@@ -1,3 +1,6 @@
localhost { localhost:443 {
reverse_proxy localhost:7878 proxy / integration:7878 {
transparent
}
tls self_signed
} }
+6 -8
View File
@@ -1,22 +1,19 @@
FROM debian:buster-20210208 FROM debian:stretch-20190326
ENV PATH="/root/.cargo/bin:${PATH}" ENV PATH="/root/.cargo/bin:${PATH}"
#install native/circleci/build dependancies #install native/circleci/build dependancies
RUN apt update &&\ RUN apt update &&\
apt install -y --no-install-recommends git ssh tar gzip ca-certificates default-jre&&\ apt install -y --no-install-recommends git ssh tar gzip ca-certificates default-jre&&\
echo "deb [trusted=yes] https://apt.fury.io/caddy/ /" \ 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&&\
| 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/* rm -rf /var/lib/apt/lists/*
#install and configure rust #install and configure rust
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2021-01-15 -y &&\ RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2020-01-15 -y &&\
rustup component add rustfmt clippy &&\ rustup component add rustfmt clippy &&\
rustup component add rust-std --target wasm32-unknown-unknown rustup component add rust-std --target wasm32-unknown-unknown
#compile some deps #compile some deps
RUN cargo install wasm-pack &&\ RUN cargo install cargo-web &&\
cargo install grcov &&\ cargo install grcov &&\
strip /root/.cargo/bin/* &&\ strip /root/.cargo/bin/* &&\
rm -fr ~/.cargo/registry rm -fr ~/.cargo/registry
@@ -27,7 +24,8 @@ COPY cargo_config /root/.cargo/config
#install selenium for front end tests #install selenium for front end tests
RUN pip3 install selenium RUN pip3 install selenium
#configure caddy #install and configure caddy
RUN curl https://getcaddy.com | bash -s personal
COPY Caddyfile /Caddyfile COPY Caddyfile /Caddyfile
#install crowdin #install crowdin
+233
View File
@@ -0,0 +1,233 @@
// This is the CI config for Plume.
// It uses a Drone CI instance, on https://ci.joinplu.me
// First of all, we define a few useful constants
// This Docker image contains everything we need to build Plume.
// Its Dockerfile can be found at https://git.joinplu.me/plume/buildenv
local plumeEnv = "plumeorg/plume-buildenv:v0.2.0";
// Common cache config
local cacheConfig(name, extra) = {
name: name,
image: "meltwater/drone-cache:dev",
pull: true,
environment: {
AWS_ACCESS_KEY_ID: { from_secret: 'minio_key' },
AWS_SECRET_ACCESS_KEY: { from_secret: 'minio_secret' },
},
settings: extra + {
cache_key: 'v0-{{ checksum "Cargo.lock" }}-{{ .Commit.Branch }}',
archive_format: "gzip",
mount: [ "~/.cargo/", "./target" ],
bucket: 'cache',
path_style: true,
endpoints: "127.0.0.1:9000",
region: 'us-east-1',
debug: true,
},
volumes: [ { name: "cache", path: "/tmp/cache" } ]
};
// A pipeline step that restores the cache.
// The cache contains all the cargo build files.
// Thus, we don't have to download and compile all of our dependencies for each
// commit.
// This cache is only "deleted" when the contents of Cargo.lock changes.
//
// We use this plugin for caching: https://github.com/meltwater/drone-cache/
//
// Potential TODO: use one cache per pipeline, as we used to do when we were
// using CircleCI.
local restoreCache = cacheConfig("restore-cache", { restore: true });
// And a step that saves the cache.
local saveCache = cacheConfig("save-cache", { rebuild: true });
// This step starts a PostgreSQL database if the db parameter is "postgres",
// otherwise it does nothing.
local startDb(db) = if db == "postgres" then {
name: "start-db",
image: "postgres:12.3-alpine",
detach: true,
environment: {
POSTGRES_USER: "plume",
POSTGRES_DB: "plume",
POSTGRES_PASSWORD: "password",
}
} else {};
// A utility function to generate a new pipeline
local basePipeline(name, steps) = {
kind: "pipeline",
name: name,
type: "docker",
environment: {
RUST_TEST_THREADS: '1',
},
steps: steps
};
// And this function creates a pipeline with caching
local cachedPipeline(name, commands) = basePipeline(
name,
[
restoreCache,
{
name: name,
image: plumeEnv,
commands: commands,
},
saveCache
]
);
// This function creates a step to upload artifacts to Minio
local upload(name, source) = {
name: name,
image: 'plugins/s3',
settings: {
bucket: 'artifacts',
source: source,
target: '/${DRONE_BUILD_NUMBER}',
path_style: true,
endpoint: 'http://127.0.0.1:9000',
access_key: { from_secret: 'minio_key' },
secret_key: { from_secret: 'minio_secret' },
},
};
// Here starts the actual list of pipelines!
// PIPELINE 1: a pipeline that runs cargo fmt, and that fails if the style of
// the code is not standard.
local CargoFmt() = cachedPipeline(
"cargo-fmt",
[ "cargo fmt --all -- --check" ]
);
// PIPELINE 2: runs clippy, a tool that helps
// you writing idiomatic Rust.
// Helper function:
local cmd(db, pkg, features=true) = if features then
"cargo clippy --no-default-features --features " + db + " --release -p "
+ pkg + " -- -D warnings"
else
"cargo clippy --no-default-features --release -p "
+ pkg + " -- -D warnings";
// The actual pipeline:
local Clippy(db) = cachedPipeline(
"clippy-" + db,
[
cmd(db, "plume"),
cmd(db, "plume-cli"),
cmd(db, "plume-front", false)
]
);
// PIPELINE 3: runs unit tests
local Unit(db) = cachedPipeline(
"unit-" + db,
[
"cargo test --all --exclude plume-front --exclude plume-macro"
+ " --no-run --no-default-features --features=" + db
]
);
// PIPELINE 4: runs integration tests
// It installs a local instance an run integration test with Python scripts
// that use Selenium (located in scripts/browser_test).
local Integration(db) = basePipeline(
"integration-" + db,
[
restoreCache,
startDb(db),
{
name: 'selenium',
image: 'elgalu/selenium:latest',
detach: true,
},
{
name: "integration",
image: plumeEnv,
environment: {
BROWSER: "firefox",
DATABASE_URL: if db == "postgres" then "postgres://plume:password@start-db/plume" else "plume.db",
},
commands: [
// Install the front-end
"cargo web deploy -p plume-front",
// Install the server
'cargo install --debug --no-default-features --features="'
+ db + '",test --force --path .',
// Install plm
'cargo install --debug --no-default-features --features="'
+ db + '" --force --path plume-cli',
// Run the tests
"./script/run_browser_test.sh"
],
},
saveCache,
]
);
// PIPELINE 5: make a release build and save artifacts
//
// It should also deploy the SQlite build to a test instance
// located at https://pr-XXX.joinplu.me (but this system is not very
// stable, and often breaks).
local Release(db) = basePipeline(
"release-" + db,
[
restoreCache,
{
name: 'release-' + db,
image: plumeEnv,
commands: [
"cargo web deploy -p plume-front --release",
"cargo build --release --no-default-features --features=" + db + " -p plume",
"cargo build --release --no-default-features --features=" + db + " -p plume-cli",
"./script/generate_artifact.sh",
] + if db == "sqlite" then
[ "./script/upload_test_environment.sh" ] else
[]
},
upload('artifacts-' + db, '*.tar.gz'),
saveCache,
]
);
// PIPELINE 6: upload the new PO templates (.pot) to Crowdin
//
// TODO: run only on master
local PushTranslations() = basePipeline(
"push-translations",
[
{
name: "push-translations",
image: plumeEnv,
commands: [
"cargo build",
"crowdin upload -b master"
]
}
]
);
// And finally, the list of all our pipelines:
[
CargoFmt(),
Clippy("postgres"),
Clippy("sqlite"),
Unit("postgres"),
Unit("sqlite"),
Integration("postgres"),
Integration("sqlite"),
Release("postgres"),
Release("sqlite"),
PushTranslations()
]
-12
View File
@@ -15,9 +15,6 @@ DATABASE_URL=postgres://plume:plume@localhost/plume
# The domain of your instance # The domain of your instance
BASE_URL=plu.me BASE_URL=plu.me
# Log level for each crate
RUST_LOG=warn,html5ever=warn,hyper=warn,tantivy=warn
# The secret key for private cookies and CSRF protection # The secret key for private cookies and CSRF protection
# You can generate one with `openssl rand -base64 32` # You can generate one with `openssl rand -base64 32`
ROCKET_SECRET_KEY= ROCKET_SECRET_KEY=
@@ -48,12 +45,3 @@ ROCKET_ADDRESS=127.0.0.1
#PLUME_LOGO_192=icons/trwnh/paragraphs/plumeParagraphs192.png #PLUME_LOGO_192=icons/trwnh/paragraphs/plumeParagraphs192.png
#PLUME_LOGO_256=icons/trwnh/paragraphs/plumeParagraphs256.png #PLUME_LOGO_256=icons/trwnh/paragraphs/plumeParagraphs256.png
#PLUME_LOGO_512=icons/trwnh/paragraphs/plumeParagraphs512.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
-10
View File
@@ -7,16 +7,6 @@ assignees: ''
--- ---
<!--
We would appreciated if you report a bug at our Gitea instance's issue page:
https://git.joinplu.me/Plume/Plume/issues
You can login to the Gitea with your GitHub account.
We welcome to receive bug reports here, GitHub, too.
-->
<!-- Describe your bug, explaining how to reproduce it, and what was expected --> <!-- Describe your bug, explaining how to reproduce it, and what was expected -->
@@ -7,15 +7,6 @@ assignees: ''
--- ---
<!--
We would appreciated if you request a feature at our Gitea instance's issue page:
https://git.joinplu.me/Plume/Plume/issues
You can login to the Gitea with your GitHub account.
We welcome to receive feature requests here, GitHub, too.
-->
**Is your feature request related to a problem? Please describe.** **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 [...] A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
-7
View File
@@ -1,7 +0,0 @@
<!--
We would appreciated if you report a bug at our Gitea instance's pull request page:
https://git.joinplu.me/Plume/Plume/pulls
You can login to the Gitea with your GitHub account.
We welcome to receive pull requests here, GitHub, too.
-->
-2
View File
@@ -18,5 +18,3 @@ tags.*
search_index search_index
.buildconfig .buildconfig
__pycache__ __pycache__
.vscode/
*-journal
-212
View File
@@ -1,212 +0,0 @@
# Changelog
<!-- next-header -->
## [Unreleased] - ReleaseDate
### 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)
### Changed
- Upgrade some dependent crates (#858)
- Use tracing crate (#868)
- Update Rust version to nightly-2021-01-15 (#878)
- Upgrade Tantivy to 0.13.3 and lindera-tantivy to 0.7.1 (#878)
- Run searcher on actor system (#870)
### Fixed
- Percent-encode URI for remote_interact (#866, #857)
- Menu animation not opening on iOS (#876, #897)
## [[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
<!-- next-url -->
[Unreleased]: https://github.com/Plume-org/Plume/compare/0.6.0...HEAD
[[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
Generated
+2107 -2693
View File
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -1,7 +1,7 @@
[package] [package]
authors = ["Plume contributors"] authors = ["Plume contributors"]
name = "plume" name = "plume"
version = "0.6.1-dev" version = "0.4.0"
repository = "https://github.com/Plume-org/Plume" repository = "https://github.com/Plume-org/Plume"
edition = "2018" edition = "2018"
@@ -10,7 +10,8 @@ activitypub = "0.1.3"
askama_escape = "0.1" askama_escape = "0.1"
atom_syndication = "0.6" atom_syndication = "0.6"
clap = "2.33" clap = "2.33"
dotenv = "0.15.0" colored = "1.8"
dotenv = "0.14"
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" } gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" } gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" } gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
@@ -19,20 +20,19 @@ heck = "0.3.0"
lettre = "0.9.2" lettre = "0.9.2"
lettre_email = "0.9.2" lettre_email = "0.9.2"
num_cpus = "1.10" num_cpus = "1.10"
rocket = "0.4.6" rocket = "0.4.2"
rocket_contrib = { version = "0.4.5", features = ["json"] } rocket_contrib = { version = "0.4.2", features = ["json"] }
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" } rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
rpassword = "4.0" rpassword = "4.0"
scheduled-thread-pool = "0.2.2" scheduled-thread-pool = "0.2.2"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
serde_qs = "0.5"
shrinkwraprs = "0.2.1" shrinkwraprs = "0.2.1"
syntect = "3.3"
validator = "0.8" validator = "0.8"
validator_derive = "0.8" validator_derive = "0.8"
webfinger = "0.4.1" webfinger = "0.4.1"
tracing = "0.1.22"
tracing-subscriber = "0.2.15"
riker = "0.4.2"
[[bin]] [[bin]]
name = "plume" name = "plume"
@@ -48,7 +48,7 @@ version = "3.1.2"
[dependencies.diesel] [dependencies.diesel]
features = ["r2d2", "chrono"] features = ["r2d2", "chrono"]
version = "1.4.5" version = "*"
[dependencies.multipart] [dependencies.multipart]
default-features = false default-features = false
@@ -69,7 +69,7 @@ git = "https://github.com/fdb-hiroshima/rocket_csrf"
rev = "29910f2829e7e590a540da3804336577b48c7b31" rev = "29910f2829e7e590a540da3804336577b48c7b31"
[build-dependencies] [build-dependencies]
ructe = "0.13.0" ructe = "0.9.0"
rsass = "0.9" rsass = "0.9"
[features] [features]
+3 -3
View File
@@ -1,4 +1,4 @@
FROM rust:1-buster as builder FROM rust:1-stretch as builder
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \ ca-certificates \
@@ -19,7 +19,7 @@ RUN chmod a+x ./wasm-deps.sh && sleep 1 && ./wasm-deps.sh
WORKDIR /app WORKDIR /app
COPY Cargo.toml Cargo.lock rust-toolchain ./ COPY Cargo.toml Cargo.lock rust-toolchain ./
RUN cargo install wasm-pack RUN cargo install cargo-web
COPY . . COPY . .
@@ -28,7 +28,7 @@ RUN cargo install --path ./ --force --no-default-features --features postgres
RUN cargo install --path plume-cli --force --no-default-features --features postgres RUN cargo install --path plume-cli --force --no-default-features --features postgres
RUN cargo clean RUN cargo clean
FROM debian:buster-slim FROM debian:stretch-slim
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \ ca-certificates \
+3 -4
View File
@@ -1,4 +1,4 @@
FROM rust:1-buster FROM rust:1-stretch
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \ ca-certificates \
@@ -10,8 +10,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \ gcc \
make \ make \
openssl \ openssl \
libssl-dev\ libssl-dev
clang
WORKDIR /scratch WORKDIR /scratch
COPY script/wasm-deps.sh . COPY script/wasm-deps.sh .
@@ -20,7 +19,7 @@ RUN chmod a+x ./wasm-deps.sh && sleep 1 && ./wasm-deps.sh
WORKDIR /app WORKDIR /app
COPY Cargo.toml Cargo.lock rust-toolchain ./ COPY Cargo.toml Cargo.lock rust-toolchain ./
RUN cargo install diesel_cli --no-default-features --features postgres --version '=1.3.0' RUN cargo install diesel_cli --no-default-features --features postgres --version '=1.3.0'
RUN cargo install wasm-pack RUN cargo install cargo-web
COPY . . COPY . .
+1 -1
View File
@@ -34,7 +34,7 @@ rules and thematics, but they can all communicate with each other.
## Get involved ## 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 have regular news about the project, the best place is probably [our blog](https://fediverse.blog/~/PlumeDev), or our Matrix room: [`#plume:disroot.org`](https://riot.im/app/#/room/#plume:disroot.org).
If you want to contribute more, a good first step is to read [our contribution guides](https://docs.joinplu.me/contribute). We accept all kind of contribution: 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:
+1 -1
View File
@@ -1,3 +1,3 @@
* { * {
font-family: monospace; font-family: monospace;
} }
+89 -116
View File
@@ -40,7 +40,7 @@ main header.article {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-end; justify-content: end;
h1, .article-info { h1, .article-info {
text-align: center; text-align: center;
@@ -64,37 +64,37 @@ main header.article {
} }
main .article-info { main .article-info {
margin: 0 auto 3em; margin: 0 auto 3em;
font-size: 0.95em; font-size: 0.95em;
font-weight: 400; font-weight: 400;
.author, .author a { .author, .author a {
font-weight: 600; font-weight: 600;
} }
} }
/* The article itself */ /* The article itself */
main article { main article {
max-width: $article-width; max-width: $article-width;
margin: 2.5em auto; margin: 2.5em auto;
font-family: $lora; font-family: $lora;
font-size: 1.2em; font-size: 1.2em;
line-height: 1.7; line-height: 1.7;
a:hover { a:hover {
text-decoration: underline; text-decoration: underline;
} }
img { img {
display: block; display: block;
margin: 3em auto; margin: 3em auto;
max-width: 100%; max-width: 100%;
} }
pre { pre {
padding: 1em; padding: 1em;
background: $gray; background: $gray;
overflow: auto; overflow: auto;
} }
blockquote { blockquote {
@@ -126,7 +126,7 @@ main .article-meta {
> p { > p {
margin: 2em $horizontal-margin; margin: 2em $horizontal-margin;
font-size: 0.9em; font-size: 0.9em;
} }
/* Article Tags */ /* Article Tags */
@@ -157,15 +157,15 @@ main .article-meta {
/* Likes & Boosts */ /* Likes & Boosts */
.actions { .actions {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-around; justify-content: space-around;
} }
.likes, .reshares { .likes, .reshares {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 0.5em 0; padding: 0.5em 0;
p { p {
font-size: 1.5em; font-size: 1.5em;
@@ -175,34 +175,34 @@ main .article-meta {
.action { .action {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin: 0; margin: 0;
padding: 0; padding: 0;
background: none; background: none;
color: $text-color; color: $text-color;
border: none; border: none;
font-size: 1.1em; font-size: 1.1em;
cursor: pointer; cursor: pointer;
svg.feather { svg.feather {
transition: background 0.1s ease-in; transition: background 0.1s ease-in;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin: 0.5em 0; margin: 0.5em 0;
width: 2.5em; width: 2.5em;
height: 2.5em; height: 2.5em;
border-radius: 50%; border-radius: 50%;
} }
&.reshared, &.liked { &.reshared, &.liked {
svg.feather { svg.feather {
color: $background; color: $background;
font-weight: 900; font-weight: 900;
} }
} }
} }
@@ -213,14 +213,14 @@ main .article-meta {
.action svg.feather { .action svg.feather {
padding: 0.7em; padding: 0.7em;
box-sizing: border-box; box-sizing: border-box;
color: $red; color: $red;
fill: none; fill: none;
border: solid $red thin; border: solid $red thin;
} }
.action:hover svg.feather { .action:hover svg.feather {
background: transparentize($red, 0.85); background: transparentize($red, 0.85);
} }
.action.liked svg.feather { .action.liked svg.feather {
@@ -238,22 +238,22 @@ main .article-meta {
.action svg.feather { .action svg.feather {
padding: 0.7em; padding: 0.7em;
box-sizing: border-box; box-sizing: border-box;
color: $primary; color: $primary;
border: solid $primary thin; border: solid $primary thin;
font-weight: 600; font-weight: 600;
} }
.action:hover svg.feather { .action:hover svg.feather {
background: transparentize($primary, 0.85); background: transparentize($primary, 0.85);
} }
.action.reshared svg.feather { .action.reshared svg.feather {
background: $primary; background: $primary;
} }
.action.reshared:hover svg.feather { .action.reshared:hover svg.feather {
background: transparentize($primary, 0.75) background: transparentize($primary, 0.75)
color: $primary; color: $primary;
} }
} }
@@ -262,9 +262,9 @@ main .article-meta {
margin: 0 $horizontal-margin; margin: 0 $horizontal-margin;
h2 { h2 {
color: $primary; color: $primary;
font-size: 1.5em; font-size: 1.5em;
font-weight: 600; font-weight: 600;
} }
summary { summary {
@@ -279,16 +279,16 @@ main .article-meta {
// Respond & delete comment buttons // Respond & delete comment buttons
a.button, form.inline, form.inline input { a.button, form.inline, form.inline input {
padding: 0; padding: 0;
background: none; background: none;
color: $text-color; color: $text-color;
margin-right: 2em; margin-right: 2em;
font-family: $route159; font-family: $route159;
font-weight: normal; font-weight: normal;
&::before { &::before {
color: $primary; color: $primary;
padding-right: 0.5em; padding-right: 0.5em;
} }
&:hover { color: $primary; } &:hover { color: $primary; }
@@ -296,8 +296,8 @@ main .article-meta {
.comment { .comment {
margin: 1em 0; margin: 1em 0;
font-size: 1em; font-size: 1em;
border: none; border: none;
.content { .content {
background: $gray; background: $gray;
@@ -328,36 +328,36 @@ main .article-meta {
color: transparentize($text-color, 0.6); color: transparentize($text-color, 0.6);
} }
.author { .author {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
align-content: center; align-content: center;
* { * {
transition: all 0.1s ease-in; transition: all 0.1s ease-in;
} }
.display-name { .display-name {
color: $text-color; color: $text-color;
} }
&:hover { &:hover {
.display-name { color: $primary; } .display-name { color: $primary; }
small { opacity: 1; } small { opacity: 1; }
} }
} }
& > .comment { & > .comment {
padding-left: 2em; padding-left: 2em;
} }
.text { .text {
padding: 1.25em 0; padding: 1.25em 0;
font-family: $lora; font-family: $lora;
font-size: 1.1em; font-size: 1.1em;
line-height: 1.4; line-height: 1.4;
text-align: left; text-align: left;
} }
} }
} }
@@ -490,30 +490,3 @@ input:checked ~ .cw-container > .cw-text {
display: inline; 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%;
}
}
+48 -48
View File
@@ -1,27 +1,27 @@
label { label {
display: block; display: block;
margin: 2em auto .5em; margin: 2em auto .5em;
font-size: 1.2em; font-size: 1.2em;
} }
input, textarea, select { input, textarea, select {
transition: all 0.1s ease-in; transition: all 0.1s ease-in;
display: block; display: block;
width: 100%; width: 100%;
margin: auto; margin: auto;
padding: 1em; padding: 1em;
box-sizing: border-box; box-sizing: border-box;
-webkit-appearance: textarea; -webkit-appearance: textarea;
background: $form-input-background; background: $form-input-background;
color: $text-color; color: $text-color;
border: solid $form-input-border thin; border: solid $form-input-border thin;
font-size: 1.2em; font-size: 1.2em;
font-weight: 400; font-weight: 400;
&:focus { &:focus {
border-color: $primary; border-color: $primary;
} }
} }
form input[type="submit"] { form input[type="submit"] {
margin: 2em auto; margin: 2em auto;
@@ -29,18 +29,18 @@ form input[type="submit"] {
} }
textarea { textarea {
resize: vertical; resize: vertical;
overflow-y: scroll; overflow-y: scroll;
font-family: $lora; font-family: $lora;
font-size: 1.1em; font-size: 1.1em;
line-height: 1.5; line-height: 1.5;
} }
input[type="checkbox"] { input[type="checkbox"] {
display: inline; display: inline;
margin: initial; margin: initial;
min-width: initial; min-width: initial;
width: initial; width: initial;
-webkit-appearance: checkbox; -webkit-appearance: checkbox;
} }
@@ -71,31 +71,31 @@ form.inline {
} }
.button, .button:visited, input[type="submit"], input[type="submit"].button { .button, .button:visited, input[type="submit"], input[type="submit"].button {
transition: all 0.1s ease-in; transition: all 0.1s ease-in;
display: inline-block; display: inline-block;
-webkit-appearance: none; -webkit-appearance: none;
margin: 0.5em auto; margin: 0.5em auto;
padding: 0.75em 1em; padding: 0.75em 1em;
background: $primary; background: $primary;
color: $primary-text-color; color: $primary-text-color;
font-weight: bold; font-weight: bold;
border: none; border: none;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: transparentize($primary, 0.1); background: transparentize($primary, 0.1);
} }
&.destructive { &.destructive {
background: $red; background: $red;
&:hover { &:hover {
background: transparentize($red, 0.1); background: transparentize($red, 0.1);
} }
} }
&.secondary { &.secondary {
background: $gray; background: $gray;
@@ -115,20 +115,20 @@ input[type="submit"] {
form.new-post { form.new-post {
max-width: 60em; max-width: 60em;
.title { .title {
margin: 0 auto; margin: 0 auto;
padding: 0.75em 0; padding: 0.75em 0;
background: none; background: none;
border: none; border: none;
font-family: $playfair; font-family: $playfair;
font-size: 2em; font-size: 2em;
text-align: left; text-align: left;
} }
textarea { textarea {
min-height: 20em; min-height: 20em;
overflow-y: scroll; overflow-y: scroll;
resize: none; resize: none;
-webkit-appearance: textarea; -webkit-appearance: textarea;
} }
} }
+87 -109
View File
@@ -6,43 +6,43 @@ html {
} }
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
background: $background; background: $background;
color: $text-color; color: $text-color;
font-family: $route159; font-family: $route159;
::selection { ::selection {
background: transparentize($primary, 0.7); background: transparentize($primary, 0.7);
} }
::-moz-selection { ::-moz-selection {
background: transparentize($primary, 0.7); background: transparentize($primary, 0.7);
} }
} }
a, a:visited { a, a:visited {
color: $primary; color: $primary;
text-decoration: none; text-decoration: none;
} }
a::selection { a::selection {
color: $background; color: $background;
} }
a::-moz-selection { a::-moz-selection {
color: $background; color: $background;
} }
small { small {
margin-left: 1em; margin-left: 1em;
color: transparentize($text-color, 0.6); color: transparentize($text-color, 0.6);
font-size: 0.75em; font-size: 0.75em;
word-wrap: break-word; word-wrap: break-word;
word-break: break-all; word-break: break-all;
} }
.center { .center {
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
opacity: 0.6; opacity: 0.6;
padding: 5em; padding: 5em;
} }
.right { .right {
@@ -53,28 +53,28 @@ small {
} }
.spaced { .spaced {
margin: 4rem 0; margin: 4rem 0;
} }
.banner { .banner {
background: $gray; background: $gray;
padding-top: 2em; padding-top: 2em;
padding-bottom: 1em; padding-bottom: 1em;
margin: 3em 0px; margin: 3em 0px;
} }
.hidden { .hidden {
display: none; display: none;
appearance: none; appearance: none;
} }
/* Main */ /* Main */
body > main > *, .h-feed > * { body > main > *, .h-feed > * {
margin: 1em $horizontal-margin; margin: 1em $horizontal-margin;
} }
body > main > .h-entry, .h-feed { body > main > .h-entry, .h-feed {
margin: 0; margin: 0;
} }
body > main { body > main {
@@ -98,18 +98,18 @@ main {
margin-top: 1em; margin-top: 1em;
&.article { &.article {
margin: 1em auto 0.5em; margin: 1em auto 0.5em;
font-family: $playfair; font-family: $playfair;
font-size: 2.5em; font-size: 2.5em;
font-weight: normal; font-weight: normal;
} }
} }
h2 { h2 {
font-size: 1.75em; font-size: 1.75em;
font-weight: 300; font-weight: 300;
&.article { &.article {
font-size: 1.25em; font-size: 1.25em;
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
@@ -139,15 +139,15 @@ main {
/* Errors */ /* Errors */
p.error { p.error {
color: $red; color: $red;
font-weight: bold; font-weight: bold;
} }
/* User page */ /* User page */
.user h1 { .user h1 {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
margin: 0px; margin: 0px;
} }
@@ -156,14 +156,14 @@ p.error {
} }
.badge { .badge {
margin-right: 1em; margin-right: 1em;
padding: 0.35em 1em; padding: 0.35em 1em;
background: $background; background: $background;
color: $primary; color: $primary;
border: 1px solid $primary; border: 1px solid $primary;
font-size: 1rem; font-size: 1rem;
} }
.user-summary { .user-summary {
@@ -172,25 +172,23 @@ p.error {
/* Cards */ /* Cards */
.cards { .cards {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
padding: 0 5%; padding: 0 5%;
margin: 1rem 0 5rem; margin: 1rem 0 5rem;
} }
.card { .card {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; min-width: 20em;
min-height: 20em;
margin: 1em;
box-sizing: border-box;
min-width: 20em; background: $gray;
min-height: 20em;
margin: 1em;
box-sizing: border-box;
background: $gray;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -215,54 +213,38 @@ p.error {
} }
> * { > * {
margin: 20px; margin: 20px;
} }
.cover { .cover {
min-height: 10em; min-height: 10em;
background-position: center; background-position: center;
background-size: cover; background-size: cover;
margin: 0px; margin: 0px;
&:hover {
opacity: 0.9;
}
} }
h3 { h3 {
flex-grow: 1; margin: 0.75em 20px;
margin: 0; font-family: $playfair;
font-family: $playfair; font-size: 1.75em;
font-size: 1.75em; font-weight: normal;
font-weight: normal; a {
line-height: 1.75; transition: color 0.1s ease-in;
a { color: $text-color;
display: block;
transition: color 0.1s ease-in;
color: $text-color;
&:hover { color: $primary; } &:hover { color: $primary; }
} }
}
.controls {
float: right;
.button {
margin-top: 0;
margin-bottom: 0;
}
} }
main { main {
flex: 1; flex: 1;
font-family: $lora; font-family: $lora;
font-size: 1em; font-size: 1em;
line-height: 1.25; line-height: 1.25;
text-align: left; text-align: left;
overflow: hidden; overflow: hidden;
} }
} }
@@ -304,15 +286,15 @@ p.error {
/* Stats */ /* Stats */
.stats { .stats {
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
margin: 2em; margin: 2em;
> div { > div {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
p { p {
@@ -508,10 +490,6 @@ figure {
/// Small screens /// Small screens
@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {
body > main > *, .h-feed > * {
margin: 1em;
}
main .article-meta { main .article-meta {
> *, .comments { > *, .comments {
margin: 0 5%; margin: 0 5%;
@@ -557,7 +535,7 @@ figure {
margin: 0; margin: 0;
& > * { & > * {
max-width: 100% !important; max-width: 100%;
} }
} }
+56 -170
View File
@@ -3,8 +3,8 @@ body > header {
#content { #content {
display: flex; display: flex;
align-content: center; align-content: center;
justify-content: space-between; justify-content: space-between;
} }
nav#menu { nav#menu {
@@ -19,44 +19,44 @@ body > header {
a { a {
transform: skewX(15deg); transform: skewX(15deg);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 1.4em; width: 1.4em;
height: 1.4em; height: 1.4em;
margin: 0; margin: 0;
padding: 0; padding: 0;
color: $gray; color: $gray;
font-size: 1.33em; font-size: 1.33em;
} }
} }
nav { nav {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
hr { hr {
height: 100%; height: 100%;
width: 0.2em; width: 0.2em;
background: $primary; background: $primary;
border: none; border: none;
transform: skewX(-15deg); transform: skewX(-15deg);
} }
a { a {
display: flex; display: flex;
align-items: center; align-items: center;
position: relative; position: relative;
align-self: stretch; align-self: stretch;
margin: 0; margin: 0;
padding: 0 2em; padding: 0 2em;
font-size: 1em; font-size: 1em;
i { font-size: 1.2em; } i { font-size: 1.2em; }
&.title { &.title {
margin: 0; margin: 0;
text-align: center; text-align: center;
padding: 0.5em 1em; padding: 0.5em 1em;
font-size: 1.75em; font-size: 1.75em;
@@ -70,7 +70,7 @@ body > header {
margin: 0; margin: 0;
padding-left: 0.5em; padding-left: 0.5em;
} }
} }
} }
} }
} }
@@ -115,18 +115,6 @@ body > header {
opacity: 1; 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 { body > header {
flex-direction: column; flex-direction: column;
@@ -144,7 +132,7 @@ body > header {
} }
} }
body > header:focus-within #content, .show + #content { body > header:focus-within #content, #content.show {
position: fixed; position: fixed;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -205,133 +193,31 @@ body > header {
/* Only enable label animations on large screens */ /* Only enable label animations on large screens */
@media screen and (min-width: 600px) { @media screen and (min-width: 600px) {
header nav a { header nav a {
i { i {
transition: all 0.2s ease; transition: all 0.2s ease;
margin: 0; margin: 0;
} }
.mobile-label { .mobile-label {
transition: all 0.2s ease; transition: all 0.2s ease;
display: block; display: block;
position: absolute; position: absolute;
left: 50%; left: 50%;
transform: translateZ(0); transform: translate(-50%, 0);
opacity: 0; opacity: 0;
font-size: 0.9em; font-size: 0.9em;
white-space: nowrap; white-space: nowrap;
} }
img + .mobile-label { display: none; } img + .mobile-label { display: none; }
&:hover { &:hover {
i { margin-bottom: 0.75em; } i { margin-bottom: 0.75em; }
.mobile-label { .mobile-label {
opacity: 1; opacity: 1;
transform: translate(-50%, 80%); 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; }
}
}
}
} }
+15 -7
View File
@@ -1,3 +1,5 @@
use rsass;
use ructe::Ructe; use ructe::Ructe;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::{ffi::OsStr, fs::*, io::Write, path::*}; use std::{ffi::OsStr, fs::*, io::Write, path::*};
@@ -48,13 +50,19 @@ fn main() {
create_dir_all(&Path::new("static").join("media")).expect("Couldn't init media directory"); create_dir_all(&Path::new("static").join("media")).expect("Couldn't init media directory");
let cache_id = &compute_static_hash()[..8]; let cache_id = &compute_static_hash()[..8];
println!("cargo:rerun-if-changed=plume-front/pkg/plume_front_bg.wasm"); println!("cargo:rerun-if-changed=target/deploy/plume-front.wasm");
copy( copy("target/deploy/plume-front.wasm", "static/plume-front.wasm")
"plume-front/pkg/plume_front_bg.wasm", .and_then(|_| read_to_string("target/deploy/plume-front.js"))
"static/plume_front_bg.wasm", .and_then(|js| {
) write(
.and_then(|_| copy("plume-front/pkg/plume_front.js", "static/plume_front.js")) "static/plume-front.js",
.ok(); js.replace(
"\"plume-front.wasm\"",
&format!("\"/static/cached/{}/plume-front.wasm\"", cache_id),
),
)
})
.ok();
println!("cargo:rustc-env=CACHE_ID={}", cache_id) println!("cargo:rustc-env=CACHE_ID={}", cache_id)
} }
+2 -2
View File
@@ -1,5 +1,5 @@
"project_id": 352097 "project_identifier": "plume"
"api_token_env": "CROWDIN_API_KEY" "api_key_env": CROWDIN_API_KEY
preserve_hierarchy: true preserve_hierarchy: true
files: files:
- source: /po/plume/plume.pot - source: /po/plume/plume.pot
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "plume-api" name = "plume-api"
version = "0.6.1-dev" version = "0.4.0"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018" edition = "2018"
-2
View File
@@ -1,2 +0,0 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
+3 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "plume-cli" name = "plume-cli"
version = "0.6.1-dev" version = "0.4.0"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018" edition = "2018"
@@ -11,11 +11,11 @@ path = "src/main.rs"
[dependencies] [dependencies]
clap = "2.33" clap = "2.33"
dotenv = "0.14" dotenv = "0.14"
rpassword = "5.0.0" rpassword = "4.0"
[dependencies.diesel] [dependencies.diesel]
features = ["r2d2", "chrono"] features = ["r2d2", "chrono"]
version = "1.4.5" version = "*"
[dependencies.plume-models] [dependencies.plume-models]
path = "../plume-models" path = "../plume-models"
-2
View File
@@ -1,2 +0,0 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
+2
View File
@@ -1,3 +1,5 @@
use dotenv;
use clap::App; use clap::App;
use diesel::Connection; use diesel::Connection;
use plume_models::{instance::Instance, Connection as Conn, CONFIG}; use plume_models::{instance::Instance, Connection as Conn, CONFIG};
+1 -1
View File
@@ -106,7 +106,7 @@ fn refill<'a>(args: &ArgMatches<'a>, conn: &Connection, searcher: Option<Searche
searcher.commit(); searcher.commit();
} }
fn unlock(args: &ArgMatches) { fn unlock<'a>(args: &ArgMatches<'a>) {
let path = match args.value_of("path") { let path = match args.value_of("path") {
None => Path::new(&CONFIG.search_index), None => Path::new(&CONFIG.search_index),
Some(x) => Path::new(x), Some(x) => Path::new(x),
+2 -1
View File
@@ -1,6 +1,7 @@
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use plume_models::{instance::Instance, users::*, Connection}; use plume_models::{instance::Instance, users::*, Connection};
use rpassword;
use std::io::{self, Write}; use std::io::{self, Write};
pub fn command<'a, 'b>() -> App<'a, 'b> { pub fn command<'a, 'b>() -> App<'a, 'b> {
@@ -131,7 +132,7 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
role, role,
&bio, &bio,
email, email,
Some(User::hash_pass(&password).expect("Couldn't hash password")), User::hash_pass(&password).expect("Couldn't hash password"),
) )
.expect("Couldn't save new user"); .expect("Couldn't save new user");
} }
+6 -8
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "plume-common" name = "plume-common"
version = "0.6.1-dev" version = "0.4.0"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018" edition = "2018"
@@ -14,16 +14,15 @@ heck = "0.3.0"
hex = "0.3" hex = "0.3"
hyper = "0.12.33" hyper = "0.12.33"
openssl = "0.10.22" openssl = "0.10.22"
rocket = "0.4.6" rocket = "0.4.0"
reqwest = { version = "0.9", features = ["socks"] } reqwest = "0.9"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
serde_json = "1.0" serde_json = "1.0"
shrinkwraprs = "0.3.0" shrinkwraprs = "0.2.1"
syntect = "4.5.0" syntect = "3.3"
tokio = "0.1.22" tokio = "0.1.22"
regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] } regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] }
tracing = "0.1.22"
[dependencies.chrono] [dependencies.chrono]
features = ["serde"] features = ["serde"]
@@ -31,5 +30,4 @@ version = "0.4"
[dependencies.pulldown-cmark] [dependencies.pulldown-cmark]
default-features = false default-features = false
git = "https://git.joinplu.me/Plume/pulldown-cmark" version = "0.2.0"
branch = "bidi-plume"
-2
View File
@@ -1,2 +0,0 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
+39 -75
View File
@@ -71,8 +71,8 @@ use std::fmt::Debug;
/// # let conn = (); /// # let conn = ();
/// # /// #
/// let result: Result<(), ()> = Inbox::handle(&conn, activity_json) /// let result: Result<(), ()> = Inbox::handle(&conn, activity_json)
/// .with::<User, Announce, Message>(None) /// .with::<User, Announce, Message>()
/// .with::<User, Create, Message>(None) /// .with::<User, Create, Message>()
/// .done(); /// .done();
/// ``` /// ```
pub enum Inbox<'a, C, E, R> pub enum Inbox<'a, C, E, R>
@@ -144,7 +144,7 @@ where
} }
/// Registers an handler on this Inbox. /// Registers an handler on this Inbox.
pub fn with<A, V, M>(self, proxy: Option<&reqwest::Proxy>) -> Inbox<'a, C, E, R> pub fn with<A, V, M>(self) -> Inbox<'a, C, E, R>
where where
A: AsActor<&'a C> + FromId<C, Error = E>, A: AsActor<&'a C> + FromId<C, Error = E>,
V: activitypub::Activity, V: activitypub::Activity,
@@ -164,17 +164,11 @@ where
Some(x) => x, Some(x) => x,
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(None)), 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) // Transform this actor to a model (see FromId for details about the from_id function)
let actor = match A::from_id( let actor = match A::from_id(
ctx, ctx,
&actor_id, &actor_id,
serde_json::from_value(act["actor"].clone()).ok(), serde_json::from_value(act["actor"].clone()).ok(),
proxy,
) { ) {
Ok(a) => a, Ok(a) => a,
// If the actor was not found, go to the next handler // If the actor was not found, go to the next handler
@@ -195,7 +189,6 @@ where
ctx, ctx,
&obj_id, &obj_id,
serde_json::from_value(act["object"].clone()).ok(), serde_json::from_value(act["object"].clone()).ok(),
proxy,
) { ) {
Ok(o) => o, Ok(o) => o,
Err((json, e)) => { Err((json, e)) => {
@@ -229,26 +222,6 @@ where
Inbox::Failed(err) => Err(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. /// Get the ActivityPub ID of a JSON value.
@@ -294,52 +267,43 @@ pub trait FromId<C>: Sized {
ctx: &C, ctx: &C,
id: &str, id: &str,
object: Option<Self::Object>, object: Option<Self::Object>,
proxy: Option<&reqwest::Proxy>,
) -> Result<Self, (Option<serde_json::Value>, Self::Error)> { ) -> Result<Self, (Option<serde_json::Value>, Self::Error)> {
match Self::from_db(ctx, id) { match Self::from_db(ctx, id) {
Ok(x) => Ok(x), Ok(x) => Ok(x),
_ => match object { _ => match object {
Some(o) => Self::from_activity(ctx, o).map_err(|e| (None, e)), Some(o) => Self::from_activity(ctx, o).map_err(|e| (None, e)),
None => Self::from_activity(ctx, Self::deref(id, proxy.cloned())?) None => Self::from_activity(ctx, Self::deref(id)?).map_err(|e| (None, e)),
.map_err(|e| (None, e)),
}, },
} }
} }
/// Dereferences an ID /// Dereferences an ID
fn deref( fn deref(id: &str) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
id: &str, reqwest::ClientBuilder::new()
proxy: Option<reqwest::Proxy>, .connect_timeout(Some(std::time::Duration::from_secs(5)))
) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> { .build()
if let Some(proxy) = proxy { .map_err(|_| (None, InboxError::DerefError.into()))?
reqwest::ClientBuilder::new().proxy(proxy) .get(id)
} else { .header(
reqwest::ClientBuilder::new() ACCEPT,
} HeaderValue::from_str(
.connect_timeout(Some(std::time::Duration::from_secs(5))) &super::ap_accept_header()
.build() .into_iter()
.map_err(|_| (None, InboxError::DerefError.into()))? .collect::<Vec<_>>()
.get(id) .join(", "),
.header( )
ACCEPT, .map_err(|_| (None, InboxError::DerefError.into()))?,
HeaderValue::from_str(
&super::ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
) )
.map_err(|_| (None, InboxError::DerefError.into()))?, .send()
) .map_err(|_| (None, InboxError::DerefError))
.send() .and_then(|mut r| {
.map_err(|_| (None, InboxError::DerefError)) let json: serde_json::Value = r
.and_then(|mut r| { .json()
let json: serde_json::Value = r .map_err(|_| (None, InboxError::InvalidObject(None)))?;
.json() serde_json::from_value(json.clone())
.map_err(|_| (None, InboxError::InvalidObject(None)))?; .map_err(|_| (Some(json), InboxError::InvalidObject(None)))
serde_json::from_value(json.clone()) })
.map_err(|_| (Some(json), InboxError::InvalidObject(None))) .map_err(|(json, e)| (json, e.into()))
})
.map_err(|(json, e)| (json, e.into()))
} }
/// Builds a `Self` from its ActivityPub representation /// Builds a `Self` from its ActivityPub representation
@@ -561,7 +525,7 @@ mod tests {
fn test_inbox_basic() { fn test_inbox_basic() {
let act = serde_json::to_value(build_create()).unwrap(); let act = serde_json::to_value(build_create()).unwrap();
let res: Result<(), ()> = Inbox::handle(&(), act) let res: Result<(), ()> = Inbox::handle(&(), act)
.with::<MyActor, Create, MyObject>(None) .with::<MyActor, Create, MyObject>()
.done(); .done();
assert!(res.is_ok()); assert!(res.is_ok());
} }
@@ -570,10 +534,10 @@ mod tests {
fn test_inbox_multi_handlers() { fn test_inbox_multi_handlers() {
let act = serde_json::to_value(build_create()).unwrap(); let act = serde_json::to_value(build_create()).unwrap();
let res: Result<(), ()> = Inbox::handle(&(), act) let res: Result<(), ()> = Inbox::handle(&(), act)
.with::<MyActor, Announce, MyObject>(None) .with::<MyActor, Announce, MyObject>()
.with::<MyActor, Delete, MyObject>(None) .with::<MyActor, Delete, MyObject>()
.with::<MyActor, Create, MyObject>(None) .with::<MyActor, Create, MyObject>()
.with::<MyActor, Like, MyObject>(None) .with::<MyActor, Like, MyObject>()
.done(); .done();
assert!(res.is_ok()); assert!(res.is_ok());
} }
@@ -583,8 +547,8 @@ mod tests {
let act = serde_json::to_value(build_create()).unwrap(); let act = serde_json::to_value(build_create()).unwrap();
// Create is not handled by this inbox // Create is not handled by this inbox
let res: Result<(), ()> = Inbox::handle(&(), act) let res: Result<(), ()> = Inbox::handle(&(), act)
.with::<MyActor, Announce, MyObject>(None) .with::<MyActor, Announce, MyObject>()
.with::<MyActor, Like, MyObject>(None) .with::<MyActor, Like, MyObject>()
.done(); .done();
assert!(res.is_err()); assert!(res.is_err());
} }
@@ -632,13 +596,13 @@ mod tests {
let act = serde_json::to_value(build_create()).unwrap(); let act = serde_json::to_value(build_create()).unwrap();
let res: Result<(), ()> = Inbox::handle(&(), act.clone()) let res: Result<(), ()> = Inbox::handle(&(), act.clone())
.with::<FailingActor, Create, MyObject>(None) .with::<FailingActor, Create, MyObject>()
.done(); .done();
assert!(res.is_err()); assert!(res.is_err());
let res: Result<(), ()> = Inbox::handle(&(), act.clone()) let res: Result<(), ()> = Inbox::handle(&(), act.clone())
.with::<FailingActor, Create, MyObject>(None) .with::<FailingActor, Create, MyObject>()
.with::<MyActor, Create, MyObject>(None) .with::<MyActor, Create, MyObject>()
.done(); .done();
assert!(res.is_ok()); assert!(res.is_ok());
} }
+23 -46
View File
@@ -1,14 +1,14 @@
use activitypub::{Activity, Link, Object}; use activitypub::{Activity, Link, Object};
use array_tool::vec::Uniq; use array_tool::vec::Uniq;
use reqwest::{header::HeaderValue, r#async::ClientBuilder, Url}; use reqwest::r#async::ClientBuilder;
use rocket::{ use rocket::{
http::Status, http::Status,
request::{FromRequest, Request}, request::{FromRequest, Request},
response::{Responder, Response}, response::{Responder, Response},
Outcome, Outcome,
}; };
use serde_json;
use tokio::prelude::*; use tokio::prelude::*;
use tracing::{debug, warn};
use self::sign::Signable; use self::sign::Signable;
@@ -108,7 +108,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApRequest {
.unwrap_or(Outcome::Forward(())) .unwrap_or(Outcome::Forward(()))
} }
} }
pub fn broadcast<S, A, T, C>(sender: &S, act: A, to: Vec<T>, proxy: Option<reqwest::Proxy>) pub fn broadcast<S, A, T, C>(sender: &S, act: A, to: Vec<T>)
where where
S: sign::Signer, S: sign::Signer,
A: Activity, A: Activity,
@@ -132,54 +132,31 @@ where
let mut rt = tokio::runtime::current_thread::Runtime::new() let mut rt = tokio::runtime::current_thread::Runtime::new()
.expect("Error while initializing tokio runtime for federation"); .expect("Error while initializing tokio runtime for federation");
let client = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(5))
.build()
.expect("Can't build client");
for inbox in boxes { for inbox in boxes {
let body = signed.to_string(); let body = signed.to_string();
let mut headers = request::headers(); 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)); headers.insert("Digest", request::Digest::digest(&body));
rt.spawn( rt.spawn(
if let Some(proxy) = proxy.clone() { client
ClientBuilder::new().proxy(proxy) .post(&inbox)
} else { .headers(headers.clone())
ClientBuilder::new() .header(
} "Signature",
.connect_timeout(std::time::Duration::from_secs(5)) request::signature(sender, &headers)
.build() .expect("activity_pub::broadcast: request signature error"),
.expect("Can't build client") )
.post(&inbox) .body(body)
.headers(headers.clone()) .send()
.header( .and_then(|r| r.into_body().concat2())
"Signature", .map(move |response| {
request::signature(sender, &headers, ("post", url.path(), url.query())) println!("Successfully sent activity to inbox ({})", inbox);
.expect("activity_pub::broadcast: request signature error"), println!("Response: \"{:?}\"\n", response)
) })
.body(body) .map_err(|e| println!("Error while sending to inbox ({:?})", e)),
.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(); rt.run().unwrap();
+20 -101
View File
@@ -1,18 +1,15 @@
use base64;
use chrono::{offset::Utc, DateTime}; use chrono::{offset::Utc, DateTime};
use openssl::hash::{Hasher, MessageDigest}; use openssl::hash::{Hasher, MessageDigest};
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, DATE, USER_AGENT}; use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, DATE, USER_AGENT};
use std::ops::Deref; use std::ops::Deref;
use std::time::SystemTime; use std::time::SystemTime;
use tracing::warn;
use crate::activity_pub::sign::Signer; use crate::activity_pub::sign::Signer;
use crate::activity_pub::{ap_accept_header, AP_CONTENT_TYPE}; use crate::activity_pub::{ap_accept_header, AP_CONTENT_TYPE};
const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION")); const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION"));
#[derive(Debug)]
pub struct Error();
pub struct Digest(String); pub struct Digest(String);
impl Digest { impl Digest {
@@ -65,16 +62,16 @@ impl Digest {
base64::decode(&self.0[pos..]).expect("Digest::value: invalid encoding error") base64::decode(&self.0[pos..]).expect("Digest::value: invalid encoding error")
} }
pub fn from_header(dig: &str) -> Result<Self, Error> { pub fn from_header(dig: &str) -> Result<Self, ()> {
if let Some(pos) = dig.find('=') { if let Some(pos) = dig.find('=') {
let pos = pos + 1; let pos = pos + 1;
if base64::decode(&dig[pos..]).is_ok() { if base64::decode(&dig[pos..]).is_ok() {
Ok(Digest(dig.to_owned())) Ok(Digest(dig.to_owned()))
} else { } else {
Err(Error()) Err(())
} }
} else { } else {
Err(Error()) Err(())
} }
} }
@@ -113,47 +110,27 @@ pub fn headers() -> HeaderMap {
headers headers
} }
type Method<'a> = &'a str; pub fn signature<S: Signer>(signer: &S, headers: &HeaderMap) -> Result<HeaderValue, ()> {
type Path<'a> = &'a str; let signed_string = headers
type Query<'a> = &'a str;
type RequestTarget<'a> = (Method<'a>, Path<'a>, Option<Query<'a>>);
pub fn signature<S: Signer>(
signer: &S,
headers: &HeaderMap,
request_target: RequestTarget,
) -> Result<HeaderValue, Error> {
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() .iter()
.map(|(h, v)| format!("{}: {}", h, v)) .map(|(h, v)| {
format!(
"{}: {}",
h.as_str().to_lowercase(),
v.to_str()
.expect("request::signature: invalid header error")
)
})
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n"); .join("\n");
let signed_headers = headers_vec let signed_headers = headers
.iter() .iter()
.map(|(h, _)| h.as_ref()) .map(|(h, _)| h.as_str())
.collect::<Vec<&str>>() .collect::<Vec<&str>>()
.join(" "); .join(" ")
.to_lowercase();
let data = signer.sign(&signed_string).map_err(|_| Error())?; let data = signer.sign(&signed_string).map_err(|_| ())?;
let sign = base64::encode(&data); let sign = base64::encode(&data);
HeaderValue::from_str(&format!( HeaderValue::from_str(&format!(
@@ -161,63 +138,5 @@ pub fn signature<S: Signer>(
key_id = signer.get_key_id(), key_id = signer.get_key_id(),
signed_headers = signed_headers, signed_headers = signed_headers,
signature = sign signature = sign
)).map_err(|_| Error()) )).map_err(|_| ())
}
#[cfg(test)]
mod tests {
use super::{signature, Error};
use crate::activity_pub::sign::{gen_keypair, 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 {
type Error = Error;
fn get_key_id(&self) -> String {
"mysigner".into()
}
fn sign(&self, to_sign: &str) -> Result<Vec<u8>, Self::Error> {
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<bool, Self::Error> {
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());
}
} }
+6 -6
View File
@@ -1,7 +1,10 @@
use super::request; use super::request;
use base64;
use chrono::{naive::NaiveDateTime, DateTime, Duration, Utc}; use chrono::{naive::NaiveDateTime, DateTime, Duration, Utc};
use hex;
use openssl::{pkey::PKey, rsa::Rsa, sha::sha256}; use openssl::{pkey::PKey, rsa::Rsa, sha::sha256};
use rocket::http::HeaderMap; use rocket::http::HeaderMap;
use serde_json;
/// Returns (public key, private key) /// Returns (public key, private key)
pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) { pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
@@ -17,9 +20,6 @@ pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
) )
} }
#[derive(Debug)]
pub struct Error();
pub trait Signer { pub trait Signer {
type Error; type Error;
@@ -32,7 +32,7 @@ pub trait Signer {
} }
pub trait Signable { pub trait Signable {
fn sign<T>(&mut self, creator: &T) -> Result<&mut Self, Error> fn sign<T>(&mut self, creator: &T) -> Result<&mut Self, ()>
where where
T: Signer; T: Signer;
fn verify<T>(self, creator: &T) -> bool fn verify<T>(self, creator: &T) -> bool
@@ -46,7 +46,7 @@ pub trait Signable {
} }
impl Signable for serde_json::Value { impl Signable for serde_json::Value {
fn sign<T: Signer>(&mut self, creator: &T) -> Result<&mut serde_json::Value, Error> { fn sign<T: Signer>(&mut self, creator: &T) -> Result<&mut serde_json::Value, ()> {
let creation_date = Utc::now().to_rfc3339(); let creation_date = Utc::now().to_rfc3339();
let mut options = json!({ let mut options = json!({
"type": "RsaSignature2017", "type": "RsaSignature2017",
@@ -64,7 +64,7 @@ impl Signable for serde_json::Value {
let document_hash = Self::hash(&self.to_string()); let document_hash = Self::hash(&self.to_string());
let to_be_signed = options_hash + &document_hash; let to_be_signed = options_hash + &document_hash;
let signature = base64::encode(&creator.sign(&to_be_signed).map_err(|_| Error())?); let signature = base64::encode(&creator.sign(&to_be_signed).map_err(|_| ())?);
options["signatureValue"] = serde_json::Value::String(signature); options["signatureValue"] = serde_json::Value::String(signature);
self["signature"] = options; self["signature"] = options;
Executable → Regular
+3
View File
@@ -2,6 +2,9 @@
#[macro_use] #[macro_use]
extern crate activitystreams_derive; extern crate activitystreams_derive;
use activitystreams_traits;
use serde;
#[macro_use] #[macro_use]
extern crate shrinkwraprs; extern crate shrinkwraprs;
#[macro_use] #[macro_use]
+40 -60
View File
@@ -1,13 +1,14 @@
use heck::CamelCase; use heck::CamelCase;
use openssl::rand::rand_bytes; use openssl::rand::rand_bytes;
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag}; use pulldown_cmark::{html, Event, Options, Parser, Tag};
use regex_syntax::is_word_character; use regex_syntax::is_word_character;
use rocket::{ use rocket::{
http::uri::Uri, http::uri::Uri,
response::{Flash, Redirect}, response::{Flash, Redirect},
}; };
use std::borrow::Cow;
use std::collections::HashSet; use std::collections::HashSet;
use syntect::html::{ClassStyle, ClassedHTMLGenerator}; use syntect::html::ClassedHTMLGenerator;
use syntect::parsing::SyntaxSet; use syntect::parsing::SyntaxSet;
/// Generates an hexadecimal representation of 32 bytes of random data /// Generates an hexadecimal representation of 32 bytes of random data
@@ -50,61 +51,46 @@ enum State {
fn to_inline(tag: Tag<'_>) -> Tag<'_> { fn to_inline(tag: Tag<'_>) -> Tag<'_> {
match tag { match tag {
Tag::Heading(_) | Tag::Table(_) | Tag::TableHead | Tag::TableRow | Tag::TableCell => { Tag::Header(_) | Tag::Table(_) | Tag::TableHead | Tag::TableRow | Tag::TableCell => {
Tag::Paragraph Tag::Paragraph
} }
Tag::Image(typ, url, title) => Tag::Link(typ, url, title), Tag::Image(url, title) => Tag::Link(url, title),
t => t, t => t,
} }
} }
struct HighlighterContext { struct HighlighterContext {
content: Vec<String>, content: Vec<String>,
} }
#[allow(clippy::unnecessary_wraps)]
fn highlight_code<'a>( fn highlight_code<'a>(
context: &mut Option<HighlighterContext>, context: &mut Option<HighlighterContext>,
evt: Event<'a>, evt: Event<'a>,
) -> Option<Vec<Event<'a>>> { ) -> Option<Vec<Event<'a>>> {
match evt { match evt {
Event::Start(Tag::CodeBlock(kind)) => { Event::Start(Tag::CodeBlock(lang)) => {
match &kind { if lang.is_empty() {
CodeBlockKind::Fenced(lang) if !lang.is_empty() => { Some(vec![Event::Start(Tag::CodeBlock(lang))])
*context = Some(HighlighterContext { content: vec![] }); } else {
} *context = Some(HighlighterContext { content: vec![] });
_ => {} Some(vec![Event::Start(Tag::CodeBlock(lang))])
} }
Some(vec![Event::Start(Tag::CodeBlock(kind))])
} }
Event::End(Tag::CodeBlock(kind)) => { Event::End(Tag::CodeBlock(x)) => {
let mut result = vec![]; let mut result = vec![];
if let Some(ctx) = context.take() { 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_set = SyntaxSet::load_defaults_newlines();
let syntax = syntax_set.find_syntax_by_token(&lang).unwrap_or_else(|| { let syntax = syntax_set.find_syntax_by_token(&x).unwrap_or_else(|| {
syntax_set syntax_set
.find_syntax_by_name(&lang) .find_syntax_by_name(&x)
.unwrap_or_else(|| syntax_set.find_syntax_plain_text()) .unwrap_or_else(|| syntax_set.find_syntax_plain_text())
}); });
let mut html = ClassedHTMLGenerator::new_with_class_style( let mut html = ClassedHTMLGenerator::new(&syntax, &syntax_set);
&syntax,
&syntax_set,
ClassStyle::Spaced,
);
for line in ctx.content { for line in ctx.content {
html.parse_html_for_line_which_includes_newline(&line); html.parse_html_for_line(&line);
} }
let q = html.finalize(); let q = html.finalize();
result.push(Event::Html(q.into())); result.push(Event::Html(q.into()));
} }
result.push(Event::End(Tag::CodeBlock(kind))); result.push(Event::End(Tag::CodeBlock(x)));
*context = None; *context = None;
Some(result) Some(result)
} }
@@ -120,7 +106,6 @@ fn highlight_code<'a>(
_ => Some(vec![evt]), _ => Some(vec![evt]),
} }
} }
#[allow(clippy::unnecessary_wraps)]
fn flatten_text<'a>(state: &mut Option<String>, evt: Event<'a>) -> Option<Vec<Event<'a>>> { fn flatten_text<'a>(state: &mut Option<String>, evt: Event<'a>) -> Option<Vec<Event<'a>>> {
let (s, res) = match evt { let (s, res) = match evt {
Event::Text(txt) => match state.take() { Event::Text(txt) => match state.take() {
@@ -128,10 +113,10 @@ fn flatten_text<'a>(state: &mut Option<String>, evt: Event<'a>) -> Option<Vec<Ev
prev_txt.push_str(&txt); prev_txt.push_str(&txt);
(Some(prev_txt), vec![]) (Some(prev_txt), vec![])
} }
None => (Some(txt.into_string()), vec![]), None => (Some(txt.into_owned()), vec![]),
}, },
e => match state.take() { e => match state.take() {
Some(prev) => (None, vec![Event::Text(CowStr::Boxed(prev.into())), e]), Some(prev) => (None, vec![Event::Text(Cow::Owned(prev)), e]),
None => (None, vec![e]), None => (None, vec![e]),
}, },
}; };
@@ -139,7 +124,6 @@ fn flatten_text<'a>(state: &mut Option<String>, evt: Event<'a>) -> Option<Vec<Ev
Some(res) Some(res)
} }
#[allow(clippy::unnecessary_wraps)]
fn inline_tags<'a>( fn inline_tags<'a>(
(state, inline): &mut (Vec<Tag<'a>>, bool), (state, inline): &mut (Vec<Tag<'a>>, bool),
evt: Event<'a>, evt: Event<'a>,
@@ -172,45 +156,42 @@ fn process_image<'a, 'b>(
) -> Event<'a> { ) -> Event<'a> {
if let Some(ref processor) = *processor { if let Some(ref processor) = *processor {
match evt { match evt {
Event::Start(Tag::Image(typ, id, title)) => { Event::Start(Tag::Image(id, title)) => {
if let Some((url, cw)) = id.parse::<i32>().ok().and_then(processor.as_ref()) { if let Some((url, cw)) = id.parse::<i32>().ok().and_then(processor.as_ref()) {
if let (Some(cw), false) = (cw, inline) { if let (Some(cw), false) = (cw, inline) {
// there is a cw, and where are not inline // there is a cw, and where are not inline
Event::Html(CowStr::Boxed( Event::Html(Cow::Owned(format!(
format!( r#"<label for="postcontent-cw-{id}">
r#"<label for="postcontent-cw-{id}">
<input type="checkbox" id="postcontent-cw-{id}" checked="checked" class="cw-checkbox"> <input type="checkbox" id="postcontent-cw-{id}" checked="checked" class="cw-checkbox">
<span class="cw-container"> <span class="cw-container">
<span class="cw-text"> <span class="cw-text">
{cw} {cw}
</span> </span>
<img src="{url}" alt=""#, <img src="{url}" alt=""#,
id = random_hex(), id = random_hex(),
cw = cw, cw = cw,
url = url url = url
) )))
.into(),
))
} else { } else {
Event::Start(Tag::Image(typ, CowStr::Boxed(url.into()), title)) Event::Start(Tag::Image(Cow::Owned(url), title))
} }
} else { } else {
Event::Start(Tag::Image(typ, id, title)) Event::Start(Tag::Image(id, title))
} }
} }
Event::End(Tag::Image(typ, id, title)) => { Event::End(Tag::Image(id, title)) => {
if let Some((url, cw)) = id.parse::<i32>().ok().and_then(processor.as_ref()) { if let Some((url, cw)) = id.parse::<i32>().ok().and_then(processor.as_ref()) {
if inline || cw.is_none() { if inline || cw.is_none() {
Event::End(Tag::Image(typ, CowStr::Boxed(url.into()), title)) Event::End(Tag::Image(Cow::Owned(url), title))
} else { } else {
Event::Html(CowStr::Borrowed( Event::Html(Cow::Borrowed(
r#""/> r#""/>
</span> </span>
</label>"#, </label>"#,
)) ))
} }
} else { } else {
Event::End(Tag::Image(typ, id, title)) Event::End(Tag::Image(id, title))
} }
} }
e => e, e => e,
@@ -250,19 +231,19 @@ pub fn md_to_html<'a>(
// Ignore headings, images, and tables if inline = true // Ignore headings, images, and tables if inline = true
.scan((vec![], inline), inline_tags) .scan((vec![], inline), inline_tags)
.scan(&mut DocumentContext::default(), |ctx, evt| match evt { .scan(&mut DocumentContext::default(), |ctx, evt| match evt {
Event::Start(Tag::CodeBlock(_)) => { Event::Start(Tag::CodeBlock(_)) | Event::Start(Tag::Code) => {
ctx.in_code = true; ctx.in_code = true;
Some((vec![evt], vec![], vec![])) Some((vec![evt], vec![], vec![]))
} }
Event::End(Tag::CodeBlock(_)) => { Event::End(Tag::CodeBlock(_)) | Event::End(Tag::Code) => {
ctx.in_code = false; ctx.in_code = false;
Some((vec![evt], vec![], vec![])) Some((vec![evt], vec![], vec![]))
} }
Event::Start(Tag::Link(_, _, _)) => { Event::Start(Tag::Link(_, _)) => {
ctx.in_link = true; ctx.in_link = true;
Some((vec![evt], vec![], vec![])) Some((vec![evt], vec![], vec![]))
} }
Event::End(Tag::Link(_, _, _)) => { Event::End(Tag::Link(_, _)) => {
ctx.in_link = false; ctx.in_link = false;
Some((vec![evt], vec![], vec![])) Some((vec![evt], vec![], vec![]))
} }
@@ -283,7 +264,6 @@ pub fn md_to_html<'a>(
let mention = text_acc; let mention = text_acc;
let short_mention = mention.splitn(1, '@').next().unwrap_or(""); let short_mention = mention.splitn(1, '@').next().unwrap_or("");
let link = Tag::Link( let link = Tag::Link(
LinkType::Inline,
format!("{}@/{}/", base_url, &mention).into(), format!("{}@/{}/", base_url, &mention).into(),
short_mention.to_owned().into(), short_mention.to_owned().into(),
); );
@@ -314,8 +294,8 @@ pub fn md_to_html<'a>(
} }
let hashtag = text_acc; let hashtag = text_acc;
let link = Tag::Link( let link = Tag::Link(
LinkType::Inline, format!("{}tag/{}", base_url, &hashtag.to_camel_case())
format!("{}tag/{}", base_url, &hashtag).into(), .into(),
hashtag.to_owned().into(), hashtag.to_owned().into(),
); );
@@ -480,11 +460,11 @@ mod tests {
fn test_inline() { fn test_inline() {
assert_eq!( assert_eq!(
md_to_html("# Hello", None, false, None).0, md_to_html("# Hello", None, false, None).0,
String::from("<h1 dir=\"auto\">Hello</h1>\n") String::from("<h1>Hello</h1>\n")
); );
assert_eq!( assert_eq!(
md_to_html("# Hello", None, true, None).0, md_to_html("# Hello", None, true, None).0,
String::from("<p dir=\"auto\">Hello</p>\n") String::from("<p>Hello</p>\n")
); );
} }
} }
+3 -40
View File
@@ -1,52 +1,15 @@
[package] [package]
name = "plume-front" name = "plume-front"
version = "0.6.1-dev" version = "0.4.0"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018" edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies] [dependencies]
stdweb = "=0.4.18"
stdweb-internal-runtime = "=0.1.4"
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" } gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" } gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" } gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
lazy_static = "1.3" lazy_static = "1.3"
serde = "1.0" serde = "1.0"
serde_json = "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'
]
-2
View File
@@ -1,2 +0,0 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
+264 -430
View File
@@ -1,12 +1,10 @@
use crate::{document, CATALOG}; use crate::CATALOG;
use js_sys::{encode_uri_component, Date, RegExp}; use serde::{Deserialize, Serialize};
use serde_derive::{Deserialize, Serialize}; use serde_json;
use std::{convert::TryInto, sync::Mutex}; use std::sync::Mutex;
use wasm_bindgen::{prelude::*, JsCast, JsValue}; use stdweb::{
use web_sys::{ unstable::{TryFrom, TryInto},
console, window, ClipboardEvent, Element, Event, FocusEvent, HtmlAnchorElement, HtmlDocument, web::{event::*, html_element::*, *},
HtmlElement, HtmlFormElement, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement,
KeyboardEvent, MouseEvent, Node,
}; };
macro_rules! mv { macro_rules! mv {
@@ -20,29 +18,30 @@ macro_rules! mv {
fn get_elt_value(id: &'static str) -> String { fn get_elt_value(id: &'static str) -> String {
let elt = document().get_element_by_id(id).unwrap(); let elt = document().get_element_by_id(id).unwrap();
let inp: Option<&HtmlInputElement> = elt.dyn_ref(); let inp: Result<InputElement, _> = elt.clone().try_into();
let textarea: Option<&HtmlTextAreaElement> = elt.dyn_ref(); let textarea: Result<TextAreaElement, _> = elt.clone().try_into();
let select: Option<&HtmlSelectElement> = elt.dyn_ref(); let select: Result<SelectElement, _> = elt.try_into();
inp.map(|i| i.value()).unwrap_or_else(|| { inp.map(|i| i.raw_value()).unwrap_or_else(|_| {
textarea textarea
.map(|t| t.value()) .map(|t| t.value())
.unwrap_or_else(|| select.unwrap().value()) .unwrap_or_else(|_| select.unwrap().raw_value())
}) })
} }
fn set_value<S: AsRef<str>>(id: &'static str, val: S) { fn set_value<S: AsRef<str>>(id: &'static str, val: S) {
let elt = document().get_element_by_id(id).unwrap(); let elt = document().get_element_by_id(id).unwrap();
let inp: Option<&HtmlInputElement> = elt.dyn_ref(); let inp: Result<InputElement, _> = elt.clone().try_into();
let textarea: Option<&HtmlTextAreaElement> = elt.dyn_ref(); let textarea: Result<TextAreaElement, _> = elt.clone().try_into();
let select: Option<&HtmlSelectElement> = elt.dyn_ref(); let select: Result<SelectElement, _> = elt.try_into();
inp.map(|i| i.set_value(val.as_ref())).unwrap_or_else(|| { inp.map(|i| i.set_raw_value(val.as_ref()))
textarea .unwrap_or_else(|_| {
.map(|t| t.set_value(val.as_ref())) textarea
.unwrap_or_else(|| select.unwrap().set_value(val.as_ref())) .map(|t| t.set_value(val.as_ref()))
}) .unwrap_or_else(|_| select.unwrap().set_raw_value(val.as_ref()))
})
} }
fn no_return(evt: KeyboardEvent) { fn no_return(evt: KeyDownEvent) {
if evt.key() == "Enter" { if evt.key() == "Enter" {
evt.prevent_default(); evt.prevent_default();
} }
@@ -52,6 +51,7 @@ fn no_return(evt: KeyboardEvent) {
pub enum EditorError { pub enum EditorError {
NoneError, NoneError,
DOMError, DOMError,
TypeError,
} }
impl From<std::option::NoneError> for EditorError { impl From<std::option::NoneError> for EditorError {
@@ -59,7 +59,22 @@ impl From<std::option::NoneError> for EditorError {
EditorError::NoneError EditorError::NoneError
} }
} }
const AUTOSAVE_DEBOUNCE_TIME: i32 = 5000; impl From<stdweb::web::error::InvalidCharacterError> for EditorError {
fn from(_: stdweb::web::error::InvalidCharacterError) -> Self {
EditorError::DOMError
}
}
impl From<stdweb::private::TODO> for EditorError {
fn from(_: stdweb::private::TODO) -> Self {
EditorError::DOMError
}
}
impl From<stdweb::private::ConversionError> for EditorError {
fn from(_: stdweb::private::ConversionError) -> Self {
EditorError::TypeError
}
}
const AUTOSAVE_DEBOUNCE_TIME: u32 = 5000;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct AutosaveInformation { struct AutosaveInformation {
contents: String, contents: String,
@@ -70,16 +85,10 @@ struct AutosaveInformation {
tags: String, tags: String,
title: String, title: String,
} }
js_serializable!(AutosaveInformation);
fn is_basic_editor() -> bool { fn is_basic_editor() -> bool {
if let Some(basic_editor) = window() if let Some(basic_editor) = window().local_storage().get("basic-editor") {
.unwrap() basic_editor == "true"
.local_storage()
.unwrap()
.unwrap()
.get("basic-editor")
.unwrap()
{
&basic_editor == "true"
} else { } else {
false false
} }
@@ -88,58 +97,65 @@ fn get_title() -> String {
if is_basic_editor() { if is_basic_editor() {
get_elt_value("title") get_elt_value("title")
} else { } else {
document() let title_field = HtmlElement::try_from(
.query_selector("#plume-editor > h1") document()
.unwrap() .query_selector("#plume-editor > h1")
.unwrap() .ok()
.dyn_ref::<HtmlElement>() .unwrap()
.unwrap() .unwrap(),
.inner_text() )
.ok()
.unwrap();
title_field.inner_text()
} }
} }
fn get_autosave_id() -> String { fn get_autosave_id() -> String {
format!( format!(
"editor_contents={}", "editor_contents={}",
window().unwrap().location().pathname().unwrap() window().location().unwrap().pathname().unwrap()
) )
} }
fn get_editor_contents() -> String { fn get_editor_contents() -> String {
if is_basic_editor() { if is_basic_editor() {
get_elt_value("editor-content") get_elt_value("editor-content")
} else { } else {
let editor = document().query_selector("article").unwrap().unwrap(); let editor =
let child_nodes = editor.child_nodes(); HtmlElement::try_from(document().query_selector("article").ok().unwrap().unwrap())
let mut md = String::new(); .ok()
for i in 0..child_nodes.length() { .unwrap();
let ch = child_nodes.get(i).unwrap(); editor.child_nodes().iter().fold(String::new(), |md, ch| {
let to_append = match ch.node_type() { let to_append = match ch.node_type() {
Node::ELEMENT_NODE => { NodeType::Element => {
let elt = ch.dyn_ref::<Element>().unwrap(); if js! { return @{&ch}.tagName; } == "DIV" {
if elt.tag_name() == "DIV" { (js! { return @{&ch}.innerHTML; })
elt.inner_html() .try_into()
.unwrap_or_default()
} else { } else {
elt.outer_html() (js! { return @{&ch}.outerHTML; })
.try_into()
.unwrap_or_default()
} }
} }
Node::TEXT_NODE => ch.node_value().unwrap_or_default(), NodeType::Text => ch.node_value().unwrap_or_default(),
_ => unreachable!(), _ => unreachable!(),
}; };
md = format!("{}\n\n{}", md, to_append); format!("{}\n\n{}", md, to_append)
} })
md
} }
} }
fn get_subtitle() -> String { fn get_subtitle() -> String {
if is_basic_editor() { if is_basic_editor() {
get_elt_value("subtitle") get_elt_value("subtitle")
} else { } else {
document() let subtitle_element = HtmlElement::try_from(
.query_selector("#plume-editor > h2") document()
.unwrap() .query_selector("#plume-editor > h2")
.unwrap() .unwrap()
.dyn_ref::<HtmlElement>() .unwrap(),
.unwrap() )
.inner_text() .ok()
.unwrap();
subtitle_element.inner_text()
} }
} }
fn autosave() { fn autosave() {
@@ -154,31 +170,27 @@ fn autosave() {
}; };
let id = get_autosave_id(); let id = get_autosave_id();
match window() match window()
.unwrap()
.local_storage() .local_storage()
.unwrap() .insert(&id, &serde_json::to_string(&info).unwrap())
.unwrap()
.set(&id, &serde_json::to_string(&info).unwrap())
{ {
Ok(_) => {} Ok(_) => {}
_ => console::log_1(&"Autosave failed D:".into()), _ => console!(log, "Autosave failed D:"),
} }
} }
//This is only necessary until we go to stdweb 4.20 at least
fn confirm(message: &str) -> bool {
let result: bool = js! {return confirm(@{message});} == true;
result
}
fn load_autosave() { fn load_autosave() {
if let Ok(Some(autosave_str)) = window() if let Some(autosave_str) = window().local_storage().get(&get_autosave_id()) {
.unwrap()
.local_storage()
.unwrap()
.unwrap()
.get(&get_autosave_id())
{
let autosave_info: AutosaveInformation = serde_json::from_str(&autosave_str).ok().unwrap(); let autosave_info: AutosaveInformation = serde_json::from_str(&autosave_str).ok().unwrap();
let message = i18n!( let message = i18n!(
CATALOG, CATALOG,
"Do you want to load the local autosave last edited at {}?"; "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() Date::from_time(autosave_info.last_saved).to_date_string()
); );
if let Ok(true) = window().unwrap().confirm_with_message(&message) { if confirm(&message) {
set_value("editor-content", &autosave_info.contents); set_value("editor-content", &autosave_info.contents);
set_value("title", &autosave_info.title); set_value("title", &autosave_info.title);
set_value("subtitle", &autosave_info.subtitle); set_value("subtitle", &autosave_info.subtitle);
@@ -191,33 +203,18 @@ fn load_autosave() {
} }
} }
fn clear_autosave() { fn clear_autosave() {
window() window().local_storage().remove(&get_autosave_id());
.unwrap() console!(log, &format!("Saved to {}", &get_autosave_id()));
.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! { lazy_static! {
static ref AUTOSAVE_TIMEOUT: Mutex<Option<TimeoutHandle>> = Mutex::new(None); static ref AUTOSAVE_TIMEOUT: Mutex<Option<TimeoutHandle>> = Mutex::new(None);
} }
fn autosave_debounce() { fn autosave_debounce() {
let window = window().unwrap();
let timeout = &mut AUTOSAVE_TIMEOUT.lock().unwrap(); let timeout = &mut AUTOSAVE_TIMEOUT.lock().unwrap();
if let Some(timeout) = timeout.take() { if let Some(timeout) = timeout.take() {
window.clear_timeout_with_handle(timeout); timeout.clear();
} }
let callback = Closure::once(autosave); **timeout = Some(window().set_clearable_timeout(autosave, AUTOSAVE_DEBOUNCE_TIME));
**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( fn init_widget(
parent: &Element, parent: &Element,
@@ -226,33 +223,19 @@ fn init_widget(
content: String, content: String,
disable_return: bool, disable_return: bool,
) -> Result<HtmlElement, EditorError> { ) -> Result<HtmlElement, EditorError> {
let widget = placeholder( let widget = placeholder(make_editable(tag).try_into()?, &placeholder_text);
make_editable(tag).dyn_into::<HtmlElement>().unwrap(),
&placeholder_text,
);
if !content.is_empty() { if !content.is_empty() {
widget widget.dataset().insert("edited", "true")?;
.dataset()
.set("edited", "true")
.map_err(|_| EditorError::DOMError)?;
} }
widget widget.append_child(&document().create_text_node(&content));
.append_child(&document().create_text_node(&content))
.map_err(|_| EditorError::DOMError)?;
if disable_return { if disable_return {
let callback = Closure::wrap(Box::new(no_return) as Box<dyn FnMut(KeyboardEvent)>); widget.add_event_listener(no_return);
widget
.add_event_listener_with_callback("keydown", callback.as_ref().unchecked_ref())
.map_err(|_| EditorError::DOMError)?;
callback.forget();
} }
parent parent.append_child(&widget);
.append_child(&widget)
.map_err(|_| EditorError::DOMError)?;
// We need to do that to make sure the placeholder is correctly rendered // We need to do that to make sure the placeholder is correctly rendered
widget.focus().map_err(|_| EditorError::DOMError)?; widget.focus();
widget.blur().map_err(|_| EditorError::DOMError)?; widget.blur();
filter_paste(&widget); filter_paste(&widget);
@@ -261,88 +244,42 @@ fn init_widget(
fn filter_paste(elt: &HtmlElement) { fn filter_paste(elt: &HtmlElement) {
// Only insert text when pasting something // Only insert text when pasting something
let insert_text = Closure::wrap(Box::new(|evt: ClipboardEvent| { js! {
evt.prevent_default(); @{&elt}.addEventListener("paste", function (evt) {
if let Some(data) = evt.clipboard_data() { evt.preventDefault();
if let Ok(data) = data.get_data("text") { document.execCommand("insertText", false, evt.clipboardData.getData("text"));
document() });
.dyn_ref::<HtmlDocument>() };
.unwrap()
.exec_command_with_show_ui_and_value("insertText", false, &data)
.unwrap();
}
}
}) as Box<dyn FnMut(ClipboardEvent)>);
elt.add_event_listener_with_callback("paste", insert_text.as_ref().unchecked_ref())
.unwrap();
insert_text.forget();
} }
pub fn init() -> Result<(), EditorError> { pub fn init() -> Result<(), EditorError> {
if let Some(ed) = document().get_element_by_id("plume-fallback-editor") { if let Some(ed) = document().get_element_by_id("plume-fallback-editor") {
load_autosave(); load_autosave();
let callback = Closure::wrap(Box::new(|_| clear_autosave()) as Box<dyn FnMut(Event)>); ed.add_event_listener(|_: SubmitEvent| clear_autosave());
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 // Check if the user wants to use the basic editor
if window() if window()
.unwrap()
.local_storage() .local_storage()
.unwrap()
.unwrap()
.get("basic-editor") .get("basic-editor")
.map(|x| x.is_some() && x.unwrap() == "true") .map(|x| x == "true")
.unwrap_or(true) .unwrap_or(true)
{ {
if let Some(editor) = document().get_element_by_id("plume-fallback-editor") { if let Some(editor) = document().get_element_by_id("plume-fallback-editor") {
if let Ok(Some(title_label)) = document().query_selector("label[for=title]") { if let Ok(Some(title_label)) = document().query_selector("label[for=title]") {
let editor_button = document() let editor_button = document().create_element("a")?;
.create_element("a") js! { @{&editor_button}.href = "#"; }
.map_err(|_| EditorError::DOMError)?; editor_button.add_event_listener(|_: ClickEvent| {
editor_button window().local_storage().remove("basic-editor");
.dyn_ref::<HtmlAnchorElement>() window().history().go(0).ok(); // refresh
.unwrap() });
.set_href("#"); editor_button.append_child(
let disable_basic_editor = Closure::wrap(Box::new(|_| { &document().create_text_node(&i18n!(CATALOG, "Open the rich text editor")),
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<dyn FnMut(MouseEvent)>);
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<dyn FnMut(KeyboardEvent)>
); );
editor.insert_before(&editor_button, &title_label).ok();
document() document()
.get_element_by_id("editor-content") .get_element_by_id("editor-content")
.unwrap() .unwrap()
.add_event_listener_with_callback("keydown", callback.as_ref().unchecked_ref()) .add_event_listener(|_: KeyDownEvent| autosave_debounce());
.map_err(|_| EditorError::DOMError)?;
callback.forget();
} }
} }
@@ -355,30 +292,14 @@ pub fn init() -> Result<(), EditorError> {
fn init_editor() -> Result<(), EditorError> { fn init_editor() -> Result<(), EditorError> {
if let Some(ed) = document().get_element_by_id("plume-editor") { if let Some(ed) = document().get_element_by_id("plume-editor") {
// Show the editor // Show the editor
ed.dyn_ref::<HtmlElement>() js! { @{&ed}.style.display = "block"; };
.unwrap()
.style()
.set_property("display", "block")
.map_err(|_| EditorError::DOMError)?;
// And hide the HTML-only fallback // And hide the HTML-only fallback
let old_ed = document().get_element_by_id("plume-fallback-editor"); 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")?; let old_title = document().get_element_by_id("plume-editor-title")?;
old_ed js! {
.dyn_ref::<HtmlElement>() @{&old_ed}.style.display = "none";
.unwrap() @{&old_title}.style.display = "none";
.style() };
.set_property("display", "none")
.map_err(|_| EditorError::DOMError)?;
old_title
.dyn_ref::<HtmlElement>()
.unwrap()
.style()
.set_property("display", "none")
.map_err(|_| EditorError::DOMError)?;
// Get content from the old editor (when editing an article for instance) // Get content from the old editor (when editing an article for instance)
let title_val = get_elt_value("title"); let title_val = get_elt_value("title");
@@ -400,44 +321,35 @@ fn init_editor() -> Result<(), EditorError> {
content_val.clone(), content_val.clone(),
false, false,
)?; )?;
content.set_inner_html(&content_val); js! { @{&content}.innerHTML = @{content_val}; };
// character counter // character counter
let character_counter = Closure::wrap(Box::new(mv!(content => move |_| { content.add_event_listener(mv!(content => move |_: KeyDownEvent| {
let update_char_count = Closure::wrap(Box::new(mv!(content => move || { window().set_timeout(mv!(content => move || {
if let Some(e) = document().get_element_by_id("char-count") { if let Some(e) = document().get_element_by_id("char-count") {
let count = chars_left("#plume-fallback-editor", &content).unwrap_or_default(); let count = chars_left("#plume-fallback-editor", &content).unwrap_or_default();
let text = i18n!(CATALOG, "Around {} characters left"; count); let text = i18n!(CATALOG, "Around {} characters left"; count);
e.dyn_ref::<HtmlElement>().map(|e| { HtmlElement::try_from(e).map(|e| {
e.set_inner_text(&text); js!{@{e}.innerText = @{text}};
}).unwrap(); }).ok();
}; };
})) as Box<dyn FnMut()>); }), 0);
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(); autosave_debounce();
})) as Box<dyn FnMut(KeyboardEvent)>); }));
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 |_| { document().get_element_by_id("publish")?.add_event_listener(
let popup = document().get_element_by_id("publish-popup").or_else(|| mv!(title, subtitle, content, old_ed => move |_: ClickEvent| {
init_popup(&title, &subtitle, &content, &old_ed).ok() let popup = document().get_element_by_id("publish-popup").or_else(||
).unwrap(); init_popup(&title, &subtitle, &content, &old_ed).ok()
let bg = document().get_element_by_id("popup-bg").or_else(|| ).unwrap();
init_popup_bg().ok() let bg = document().get_element_by_id("popup-bg").or_else(||
).unwrap(); init_popup_bg().ok()
).unwrap();
popup.class_list().add_1("show").unwrap(); popup.class_list().add("show").unwrap();
bg.class_list().add_1("show").unwrap(); bg.class_list().add("show").unwrap();
})) as Box<dyn FnMut(MouseEvent)>); }),
document() );
.get_element_by_id("publish")?
.add_event_listener_with_callback("click", show_popup.as_ref().unchecked_ref())
.map_err(|_| EditorError::DOMError)?;
show_popup.forget();
show_errors(); show_errors();
setup_close_button(); setup_close_button();
@@ -447,47 +359,32 @@ fn init_editor() -> Result<(), EditorError> {
fn setup_close_button() { fn setup_close_button() {
if let Some(button) = document().get_element_by_id("close-editor") { if let Some(button) = document().get_element_by_id("close-editor") {
let close_editor = Closure::wrap(Box::new(|_| { button.add_event_listener(|_: ClickEvent| {
window() window()
.unwrap()
.local_storage() .local_storage()
.unwrap() .insert("basic-editor", "true")
.unwrap()
.set("basic-editor", "true")
.unwrap(); .unwrap();
window() window().history().go(0).unwrap(); // Refresh the page
.unwrap() });
.history()
.unwrap()
.go_with_delta(0)
.unwrap(); // Refresh the page
}) as Box<dyn FnMut(MouseEvent)>);
button
.add_event_listener_with_callback("click", close_editor.as_ref().unchecked_ref())
.unwrap();
close_editor.forget();
} }
} }
fn show_errors() { fn show_errors() {
let document = document(); if let Ok(Some(header)) = document().query_selector("header") {
if let Ok(Some(header)) = document.query_selector("header") { let list = document().create_element("header").unwrap();
let list = document.create_element("header").unwrap(); list.class_list().add("messages").unwrap();
list.class_list().add_1("messages").unwrap(); for error in document().query_selector_all("p.error").unwrap() {
let errors = document.query_selector_all("p.error").unwrap();
for i in 0..errors.length() {
let error = errors.get(i).unwrap();
error error
.parent_element() .parent_element()
.unwrap() .unwrap()
.remove_child(&error) .remove_child(&error)
.unwrap(); .unwrap();
let _ = list.append_child(&error); list.append_child(&error);
} }
header header
.parent_element() .parent_element()
.unwrap() .unwrap()
.insert_before(&list, header.next_sibling().as_ref()) .insert_before(&list, &header.next_sibling().unwrap())
.unwrap(); .unwrap();
} }
} }
@@ -498,17 +395,9 @@ fn init_popup(
content: &HtmlElement, content: &HtmlElement,
old_ed: &Element, old_ed: &Element,
) -> Result<Element, EditorError> { ) -> Result<Element, EditorError> {
let document = document(); let popup = document().create_element("div")?;
let popup = document popup.class_list().add("popup")?;
.create_element("div") popup.set_attribute("id", "publish-popup")?;
.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") let tags = get_elt_value("tags")
.split(',') .split(',')
@@ -516,157 +405,112 @@ fn init_popup(
.map(str::to_string) .map(str::to_string)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let license = get_elt_value("license"); let license = get_elt_value("license");
make_input(&i18n!(CATALOG, "Tags"), "popup-tags", &popup).set_value(&tags.join(", ")); make_input(&i18n!(CATALOG, "Tags"), "popup-tags", &popup).set_raw_value(&tags.join(", "));
make_input(&i18n!(CATALOG, "License"), "popup-license", &popup).set_value(&license); make_input(&i18n!(CATALOG, "License"), "popup-license", &popup).set_raw_value(&license);
let cover_label = document let cover_label = document().create_element("label")?;
.create_element("label") cover_label.append_child(&document().create_text_node(&i18n!(CATALOG, "Cover")));
.map_err(|_| EditorError::DOMError)?; cover_label.set_attribute("for", "cover")?;
cover_label let cover = document().get_element_by_id("cover")?;
.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")?;
cover.parent_element()?.remove_child(&cover).ok(); cover.parent_element()?.remove_child(&cover).ok();
popup popup.append_child(&cover_label);
.append_child(&cover_label) popup.append_child(&cover);
.map_err(|_| EditorError::DOMError)?;
popup
.append_child(&cover)
.map_err(|_| EditorError::DOMError)?;
if let Some(draft_checkbox) = document.get_element_by_id("draft") { if let Some(draft_checkbox) = document().get_element_by_id("draft") {
let draft_checkbox = draft_checkbox.dyn_ref::<HtmlInputElement>().unwrap(); let draft_label = document().create_element("label")?;
let draft_label = document draft_label.set_attribute("for", "popup-draft")?;
.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(); let draft = document().create_element("input").unwrap();
draft.set_id("popup-draft"); js! {
let draft = draft.dyn_ref::<HtmlInputElement>().unwrap(); @{&draft}.id = "popup-draft";
draft.set_name("popup-draft"); @{&draft}.name = "popup-draft";
draft.set_type("checkbox"); @{&draft}.type = "checkbox";
draft.set_checked(draft_checkbox.checked()); @{&draft}.checked = @{&draft_checkbox}.checked;
};
draft_label draft_label.append_child(&draft);
.append_child(&draft) draft_label.append_child(&document().create_text_node(&i18n!(CATALOG, "This is a draft")));
.map_err(|_| EditorError::DOMError)?; popup.append_child(&draft_label);
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 let button = document().create_element("input")?;
.create_element("input") js! {
.map_err(|_| EditorError::DOMError)?; @{&button}.type = "submit";
button @{&button}.value = @{i18n!(CATALOG, "Publish")};
.append_child(&document.create_text_node(&i18n!(CATALOG, "Publish"))) };
.map_err(|_| EditorError::DOMError)?; button.append_child(&document().create_text_node(&i18n!(CATALOG, "Publish")));
let button = button.dyn_ref::<HtmlInputElement>().unwrap(); button.add_event_listener(
button.set_type("submit"); mv!(title, subtitle, content, old_ed => move |_: ClickEvent| {
button.set_value(&i18n!(CATALOG, "Publish")); title.focus(); // Remove the placeholder before publishing
let callback = Closure::wrap(Box::new(mv!(title, subtitle, content, old_ed => move |_| { set_value("title", title.inner_text());
let document = self::document(); subtitle.focus();
title.focus().unwrap(); // Remove the placeholder before publishing set_value("subtitle", subtitle.inner_text());
set_value("title", title.inner_text()); content.focus();
subtitle.focus().unwrap(); set_value("editor-content", content.child_nodes().iter().fold(String::new(), |md, ch| {
set_value("subtitle", subtitle.inner_text()); let to_append = match ch.node_type() {
content.focus().unwrap(); NodeType::Element => {
let mut md = String::new(); if js!{ return @{&ch}.tagName; } == "DIV" {
let child_nodes = content.child_nodes(); (js!{ return @{&ch}.innerHTML; }).try_into().unwrap_or_default()
for i in 0..child_nodes.length() { } else {
let ch = child_nodes.get(i).unwrap(); (js!{ return @{&ch}.outerHTML; }).try_into().unwrap_or_default()
let to_append = match ch.node_type() { }
Node::ELEMENT_NODE => { },
let ch = ch.dyn_ref::<Element>().unwrap(); NodeType::Text => ch.node_value().unwrap_or_default(),
if ch.tag_name() == "DIV" { _ => unreachable!(),
ch.inner_html() };
} else { format!("{}\n\n{}", md, to_append)
ch.outer_html() }));
} set_value("tags", get_elt_value("popup-tags"));
}, if let Some(draft) = document().get_element_by_id("popup-draft") {
Node::TEXT_NODE => ch.node_value().unwrap_or_default(), js!{
_ => unreachable!(), document.getElementById("draft").checked = @{draft}.checked;
}; };
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::<HtmlInputElement>().unwrap();
let draft = draft.dyn_ref::<HtmlInputElement>().unwrap();
draft_checkbox.set_checked(draft.checked());
} }
} let cover = document().get_element_by_id("cover").unwrap();
let cover = document.get_element_by_id("cover").unwrap(); cover.parent_element().unwrap().remove_child(&cover).ok();
cover.parent_element().unwrap().remove_child(&cover).ok(); old_ed.append_child(&cover);
old_ed.append_child(&cover).unwrap(); set_value("license", get_elt_value("popup-license"));
set_value("license", get_elt_value("popup-license")); clear_autosave();
clear_autosave(); js! {
let old_ed = old_ed.dyn_ref::<HtmlFormElement>().unwrap(); @{&old_ed}.submit();
old_ed.submit().unwrap(); };
})) as Box<dyn FnMut(MouseEvent)>); }),
button );
.add_event_listener_with_callback("click", callback.as_ref().unchecked_ref()) popup.append_child(&button);
.map_err(|_| EditorError::DOMError)?;
callback.forget();
popup
.append_child(&button)
.map_err(|_| EditorError::DOMError)?;
document document().body()?.append_child(&popup);
.body()?
.append_child(&popup)
.map_err(|_| EditorError::DOMError)?;
Ok(popup) Ok(popup)
} }
fn init_popup_bg() -> Result<Element, EditorError> { fn init_popup_bg() -> Result<Element, EditorError> {
let bg = document() let bg = document().create_element("div")?;
.create_element("div") bg.class_list().add("popup-bg")?;
.map_err(|_| EditorError::DOMError)?; bg.set_attribute("id", "popup-bg")?;
bg.class_list()
.add_1("popup-bg")
.map_err(|_| EditorError::DOMError)?;
bg.set_attribute("id", "popup-bg")
.map_err(|_| EditorError::DOMError)?;
document() document().body()?.append_child(&bg);
.body()? bg.add_event_listener(|_: ClickEvent| close_popup());
.append_child(&bg)
.map_err(|_| EditorError::DOMError)?;
let callback = Closure::wrap(Box::new(|_| close_popup()) as Box<dyn FnMut(MouseEvent)>);
bg.add_event_listener_with_callback("click", callback.as_ref().unchecked_ref())
.unwrap();
callback.forget();
Ok(bg) Ok(bg)
} }
fn chars_left(selector: &str, content: &HtmlElement) -> Option<i32> { fn chars_left(selector: &str, content: &HtmlElement) -> Option<i32> {
match document().query_selector(selector) { match document().query_selector(selector) {
Ok(Some(form)) => form.dyn_ref::<HtmlElement>().and_then(|form| { Ok(Some(form)) => HtmlElement::try_from(form).ok().and_then(|form| {
if let Some(len) = form if let Some(len) = form
.get_attribute("content-size") .get_attribute("content-size")
.and_then(|s| s.parse::<i32>().ok()) .and_then(|s| s.parse::<i32>().ok())
{ {
(encode_uri_component(&content.inner_html()) (js! {
.replace("%20", "+") let x = encodeURIComponent(@{content}.innerHTML)
.replace("%0A", "%0D0A") .replace(/%20/g, "+")
.replace_by_pattern(&RegExp::new("[!'*()]", "g"), "XXX") .replace(/%0A/g, "%0D%0A")
.length() .replace(new RegExp("[!'*()]", "g"), "XXX") // replace exceptions of encodeURIComponent with placeholder
+ 2_u32) .length + 2;
.try_into() console.log(x);
.map(|c: i32| len - c) return x;
.ok() })
.try_into()
.map(|c: i32| len - c)
.ok()
} else { } else {
None None
} }
@@ -676,26 +520,26 @@ fn chars_left(selector: &str, content: &HtmlElement) -> Option<i32> {
} }
fn close_popup() { fn close_popup() {
let hide = |x: Element| x.class_list().remove_1("show"); let hide = |x: Element| x.class_list().remove("show");
document().get_element_by_id("publish-popup").map(hide); document().get_element_by_id("publish-popup").map(hide);
document().get_element_by_id("popup-bg").map(hide); document().get_element_by_id("popup-bg").map(hide);
} }
fn make_input(label_text: &str, name: &'static str, form: &Element) -> HtmlInputElement { fn make_input(label_text: &str, name: &'static str, form: &Element) -> InputElement {
let document = document(); let label = document().create_element("label").unwrap();
let label = document.create_element("label").unwrap(); label.append_child(&document().create_text_node(label_text));
label
.append_child(&document.create_text_node(label_text))
.unwrap();
label.set_attribute("for", name).unwrap(); label.set_attribute("for", name).unwrap();
let inp = document.create_element("input").unwrap(); let inp: InputElement = document()
let inp = inp.dyn_into::<HtmlInputElement>().unwrap(); .create_element("input")
.unwrap()
.try_into()
.unwrap();
inp.set_attribute("name", name).unwrap(); inp.set_attribute("name", name).unwrap();
inp.set_attribute("id", name).unwrap(); inp.set_attribute("id", name).unwrap();
form.append_child(&label).unwrap(); form.append_child(&label);
form.append_child(&inp).unwrap(); form.append_child(&inp);
inp inp
} }
@@ -709,46 +553,36 @@ fn make_editable(tag: &'static str) -> Element {
} }
fn placeholder(elt: HtmlElement, text: &str) -> HtmlElement { fn placeholder(elt: HtmlElement, text: &str) -> HtmlElement {
elt.dataset().set("placeholder", text).unwrap(); elt.dataset().insert("placeholder", text).unwrap();
elt.dataset().set("edited", "false").unwrap(); elt.dataset().insert("edited", "false").unwrap();
let callback = Closure::wrap(Box::new(mv!(elt => move |_: FocusEvent| { elt.add_event_listener(mv!(elt => move |_: FocusEvent| {
if elt.dataset().get("edited").unwrap().as_str() != "true" { if elt.dataset().get("edited").unwrap().as_str() != "true" {
clear_children(&elt); clear_children(&elt);
} }
})) as Box<dyn FnMut(FocusEvent)>); }));
elt.add_event_listener_with_callback("focus", callback.as_ref().unchecked_ref()) elt.add_event_listener(mv!(elt => move |_: BlurEvent| {
.unwrap();
callback.forget();
let callback = Closure::wrap(Box::new(mv!(elt => move |_: Event| {
if elt.dataset().get("edited").unwrap().as_str() != "true" { if elt.dataset().get("edited").unwrap().as_str() != "true" {
clear_children(&elt); clear_children(&elt);
let ph = document().create_element("span").expect("Couldn't create placeholder"); let ph = document().create_element("span").expect("Couldn't create placeholder");
ph.class_list().add_1("placeholder").expect("Couldn't add class"); ph.class_list().add("placeholder").expect("Couldn't add class");
ph.append_child(&document().create_text_node(&elt.dataset().get("placeholder").unwrap_or_default())).unwrap(); ph.append_child(&document().create_text_node(&elt.dataset().get("placeholder").unwrap_or_default()));
elt.append_child(&ph).unwrap(); elt.append_child(&ph);
} }
})) as Box<dyn FnMut(Event)>); }));
elt.add_event_listener_with_callback("blur", callback.as_ref().unchecked_ref()) elt.add_event_listener(mv!(elt => move |_: KeyUpEvent| {
.unwrap(); elt.dataset().insert("edited", if elt.inner_text().trim_matches('\n').is_empty() {
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" "false"
} else { } else {
"true" "true"
}).expect("Couldn't update edition state"); }).expect("Couldn't update edition state");
})) as Box<dyn FnMut(KeyboardEvent)>); }));
elt.add_event_listener_with_callback("keyup", callback.as_ref().unchecked_ref())
.unwrap();
callback.forget();
elt elt
} }
fn clear_children(elt: &HtmlElement) { fn clear_children(elt: &HtmlElement) {
let child_nodes = elt.child_nodes(); for child in elt.child_nodes() {
for _ in 0..child_nodes.length() { elt.remove_child(&child).unwrap();
elt.remove_child(&child_nodes.get(0).unwrap()).unwrap();
} }
} }
-174
View File
@@ -1,174 +0,0 @@
#![recursion_limit = "128"]
#![feature(decl_macro, proc_macro_hygiene, try_trait)]
#[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,
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.splitn(2, '-').next().unwrap_or("en");
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<dyn FnMut(TouchEvent)>);
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::<Element>()
.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<dyn FnMut(TouchEvent)>);
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::<HtmlInputElement>().unwrap();
if input.name().is_empty() {
input.set_name(&input.dyn_ref::<Element>().unwrap().id());
}
if !input.name().is_empty() && input.value().is_empty() {
input.set_name("");
}
}
})
.unwrap();
}) as Box<dyn FnMut(Event)>);
form.add_event_listener_with_callback("submit", normalize_query.as_ref().unchecked_ref())
.unwrap();
normalize_query.forget();
}
}
fn document() -> Document {
window().unwrap().document().unwrap()
}
+112
View File
@@ -0,0 +1,112 @@
#![recursion_limit = "128"]
#![feature(decl_macro, proc_macro_hygiene, try_trait)]
#[macro_use]
extern crate gettext_macros;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate stdweb;
use stdweb::web::{event::*, *};
init_i18n!(
"plume-front",
ar,
bg,
ca,
cs,
de,
en,
eo,
es,
fa,
fr,
gl,
hi,
hr,
it,
ja,
nb,
pl,
pt,
ro,
ru,
sr,
sk,
sv
);
mod editor;
compile_i18n!();
lazy_static! {
static ref CATALOG: gettext::Catalog = {
let catalogs = include_i18n!();
let lang = js! { return navigator.language }.into_string().unwrap();
let lang = lang.splitn(2, '-').next().unwrap_or("en");
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
};
}
fn main() {
menu();
search();
editor::init()
.map_err(|e| console!(error, format!("Editor error: {:?}", e)))
.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() {
if let Some(button) = document().get_element_by_id("menu") {
if let Some(menu) = document().get_element_by_id("content") {
button.add_event_listener(|_: TouchEnd| {
document()
.get_element_by_id("menu")
.map(|menu| menu.class_list().add("show"));
});
menu.add_event_listener(|_: TouchEnd| {
document()
.get_element_by_id("menu")
.map(|menu| menu.class_list().remove("show"));
});
}
}
}
/// Clear the URL of the search page before submitting request
fn search() {
if let Some(form) = document().get_element_by_id("form") {
form.add_event_listener(|_: SubmitEvent| {
document()
.query_selector_all("#form input")
.map(|inputs| {
for input in inputs {
js! {
if (@{&input}.name === "") {
@{&input}.name = @{&input}.id
}
if (@{&input}.name && !@{&input}.value) {
@{&input}.name = "";
}
}
}
})
.ok();
});
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "plume-macro" name = "plume-macro"
version = "0.6.1-dev" version = "0.4.0"
authors = ["Trinity Pointard <trinity.pointard@insa-rennes.fr>"] authors = ["Trinity Pointard <trinity.pointard@insa-rennes.fr>"]
edition = "2018" edition = "2018"
description = "Plume procedural macros" description = "Plume procedural macros"
-2
View File
@@ -1,2 +0,0 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
Executable → Regular
+4 -4
View File
@@ -89,12 +89,12 @@ fn file_to_migration(file: &str) -> TokenStream2 {
let mut actions = vec![]; let mut actions = vec![];
for line in file.lines() { for line in file.lines() {
if sql { if sql {
if let Some(acc_str) = line.strip_prefix("--#!") { if line.starts_with("--#!") {
if !acc.trim().is_empty() { if !acc.trim().is_empty() {
actions.push(quote!(Action::Sql(#acc))); actions.push(quote!(Action::Sql(#acc)));
} }
sql = false; sql = false;
acc = acc_str.to_string(); acc = line[4..].to_string();
acc.push('\n'); acc.push('\n');
} else if line.starts_with("--") { } else if line.starts_with("--") {
continue; continue;
@@ -102,8 +102,8 @@ fn file_to_migration(file: &str) -> TokenStream2 {
acc.push_str(line); acc.push_str(line);
acc.push('\n'); acc.push('\n');
} }
} else if let Some(acc_str) = line.strip_prefix("--#!") { } else if line.starts_with("--#!") {
acc.push_str(&acc_str); acc.push_str(&line[4..]);
acc.push('\n'); acc.push('\n');
} else if line.starts_with("--") { } else if line.starts_with("--") {
continue; continue;
+6 -10
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "plume-models" name = "plume-models"
version = "0.6.1-dev" version = "0.4.0"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018" edition = "2018"
@@ -13,28 +13,24 @@ guid-create = "0.1"
heck = "0.3.0" heck = "0.3.0"
itertools = "0.8.0" itertools = "0.8.0"
lazy_static = "1.0" lazy_static = "1.0"
ldap3 = "0.7.1"
migrations_internals= "1.4.0" migrations_internals= "1.4.0"
openssl = "0.10.22" openssl = "0.10.22"
rocket = "0.4.6" rocket = "0.4.0"
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" } rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
reqwest = "0.9" reqwest = "0.9"
scheduled-thread-pool = "0.2.2" scheduled-thread-pool = "0.2.2"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
serde_json = "1.0" serde_json = "1.0"
tantivy = "0.13.3" tantivy = "0.12.0"
url = "2.1" url = "2.1"
walkdir = "2.2" walkdir = "2.2"
webfinger = "0.4.1" webfinger = "0.4.1"
whatlang = "0.11.1" whatlang = "0.7.1"
shrinkwraprs = "0.2.1" shrinkwraprs = "0.2.1"
diesel-derive-newtype = "0.1.2" diesel-derive-newtype = "0.1.2"
glob = "0.3.0" glob = "0.3.0"
lindera-tantivy = { version = "0.7.1", optional = true } lindera-tantivy = { version = "0.1.2", optional = true }
tracing = "0.1.22"
riker = "0.4.2"
once_cell = "1.5.2"
[dependencies.chrono] [dependencies.chrono]
features = ["serde"] features = ["serde"]
@@ -42,7 +38,7 @@ version = "0.4"
[dependencies.diesel] [dependencies.diesel]
features = ["r2d2", "chrono"] features = ["r2d2", "chrono"]
version = "1.4.5" version = "1.4.2"
[dependencies.plume-api] [dependencies.plume-api]
path = "../plume-api" path = "../plume-api"
-2
View File
@@ -1,2 +0,0 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
+7 -6
View File
@@ -87,7 +87,7 @@ impl BlocklistedEmail {
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use super::*; use super::*;
use crate::{instance::tests as instance_tests, tests::db, Connection as Conn}; use crate::{instance::tests as instance_tests, tests::rockets, Connection as Conn};
use diesel::Connection; use diesel::Connection;
pub(crate) fn fill_database(conn: &Conn) -> Vec<BlocklistedEmail> { pub(crate) fn fill_database(conn: &Conn) -> Vec<BlocklistedEmail> {
@@ -106,28 +106,29 @@ pub(crate) mod tests {
} }
#[test] #[test]
fn test_match() { fn test_match() {
let conn = db(); let r = rockets();
let conn = &*r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let various = fill_database(&conn); let various = fill_database(conn);
let match1 = "user1@bad-actor.com"; let match1 = "user1@bad-actor.com";
let match2 = "spammer@lax-administration.com"; let match2 = "spammer@lax-administration.com";
let no_match = "happy-user@lax-administration.com"; let no_match = "happy-user@lax-administration.com";
assert_eq!( assert_eq!(
BlocklistedEmail::matches_blocklist(&conn, match1) BlocklistedEmail::matches_blocklist(conn, match1)
.unwrap() .unwrap()
.unwrap() .unwrap()
.id, .id,
various[0].id various[0].id
); );
assert_eq!( assert_eq!(
BlocklistedEmail::matches_blocklist(&conn, match2) BlocklistedEmail::matches_blocklist(conn, match2)
.unwrap() .unwrap()
.unwrap() .unwrap()
.id, .id,
various[1].id various[1].id
); );
assert_eq!( assert_eq!(
BlocklistedEmail::matches_blocklist(&conn, no_match) BlocklistedEmail::matches_blocklist(conn, no_match)
.unwrap() .unwrap()
.is_none(), .is_none(),
true true
+89 -77
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
ap_url, db_conn::DbConn, instance::*, medias::Media, posts::Post, safe_string::SafeString, ap_url, instance::*, medias::Media, posts::Post, safe_string::SafeString, schema::blogs,
schema::blogs, users::User, Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE, search::Searcher, users::User, Connection, Error, PlumeRocket, Result, ITEMS_PER_PAGE,
}; };
use activitypub::{ use activitypub::{
actor::Group, actor::Group,
@@ -20,6 +20,7 @@ use plume_common::activity_pub::{
inbox::{AsActor, FromId}, inbox::{AsActor, FromId},
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source, sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
}; };
use serde_json;
use url::Url; use url::Url;
use webfinger::*; use webfinger::*;
@@ -131,25 +132,25 @@ impl Blog {
.map_err(Error::from) .map_err(Error::from)
} }
pub fn find_by_fqn(conn: &DbConn, fqn: &str) -> Result<Blog> { pub fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<Blog> {
let from_db = blogs::table let from_db = blogs::table
.filter(blogs::fqn.eq(fqn)) .filter(blogs::fqn.eq(fqn))
.first(&**conn) .first(&*c.conn)
.optional()?; .optional()?;
if let Some(from_db) = from_db { if let Some(from_db) = from_db {
Ok(from_db) Ok(from_db)
} else { } else {
Blog::fetch_from_webfinger(conn, fqn) Blog::fetch_from_webfinger(c, fqn)
} }
} }
fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result<Blog> { fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<Blog> {
resolve_with_prefix(Prefix::Group, acct.to_owned(), true)? resolve_with_prefix(Prefix::Group, acct.to_owned(), true)?
.links .links
.into_iter() .into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json"))) .find(|l| l.mime_type == Some(String::from("application/activity+json")))
.ok_or(Error::Webfinger) .ok_or(Error::Webfinger)
.and_then(|l| Blog::from_id(conn, &l.href?, None, CONFIG.proxy()).map_err(|(_, e)| e)) .and_then(|l| Blog::from_id(c, &l.href?, None).map_err(|(_, e)| e))
} }
pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> { pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> {
@@ -216,16 +217,16 @@ impl Blog {
pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> { pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> {
let mut coll = OrderedCollection::default(); let mut coll = OrderedCollection::default();
coll.collection_props.items = serde_json::to_value(self.get_activities(conn))?; coll.collection_props.items = serde_json::to_value(self.get_activities(conn)?)?;
coll.collection_props coll.collection_props
.set_total_items_u64(self.get_activities(conn).len() as u64)?; .set_total_items_u64(self.get_activities(conn)?.len() as u64)?;
coll.collection_props coll.collection_props
.set_first_link(Id::new(ap_url(&format!("{}?page=1", &self.outbox_url))))?; .set_first_link(Id::new(ap_url(&format!("{}?page=1", &self.outbox_url))))?;
coll.collection_props coll.collection_props
.set_last_link(Id::new(ap_url(&format!( .set_last_link(Id::new(ap_url(&format!(
"{}?page={}", "{}?page={}",
&self.outbox_url, &self.outbox_url,
(self.get_activities(conn).len() as u64 + ITEMS_PER_PAGE as u64 - 1) as u64 (self.get_activities(conn)?.len() as u64 + ITEMS_PER_PAGE as u64 - 1) as u64
/ ITEMS_PER_PAGE as u64 / ITEMS_PER_PAGE as u64
))))?; ))))?;
Ok(ActivityStream::new(coll)) Ok(ActivityStream::new(coll))
@@ -236,7 +237,7 @@ impl Blog {
(min, max): (i32, i32), (min, max): (i32, i32),
) -> Result<ActivityStream<OrderedCollectionPage>> { ) -> Result<ActivityStream<OrderedCollectionPage>> {
let mut coll = OrderedCollectionPage::default(); let mut coll = OrderedCollectionPage::default();
let acts = self.get_activity_page(&conn, (min, max)); let acts = self.get_activity_page(&conn, (min, max))?;
//This still doesn't do anything because the outbox //This still doesn't do anything because the outbox
//doesn't do anything yet //doesn't do anything yet
coll.collection_page_props.set_next_link(Id::new(&format!( coll.collection_page_props.set_next_link(Id::new(&format!(
@@ -252,15 +253,15 @@ impl Blog {
coll.collection_props.items = serde_json::to_value(acts)?; coll.collection_props.items = serde_json::to_value(acts)?;
Ok(ActivityStream::new(coll)) Ok(ActivityStream::new(coll))
} }
fn get_activities(&self, _conn: &Connection) -> Vec<serde_json::Value> { fn get_activities(&self, _conn: &Connection) -> Result<Vec<serde_json::Value>> {
vec![] Ok(vec![])
} }
fn get_activity_page( fn get_activity_page(
&self, &self,
_conn: &Connection, _conn: &Connection,
(_min, _max): (i32, i32), (_min, _max): (i32, i32),
) -> Vec<serde_json::Value> { ) -> Result<Vec<serde_json::Value>> {
vec![] Ok(vec![])
} }
pub fn get_keypair(&self) -> Result<PKey<Private>> { pub fn get_keypair(&self) -> Result<PKey<Private>> {
@@ -317,9 +318,9 @@ impl Blog {
.and_then(|c| c.url().ok()) .and_then(|c| c.url().ok())
} }
pub fn delete(&self, conn: &Connection) -> Result<()> { pub fn delete(&self, conn: &Connection, searcher: &Searcher) -> Result<()> {
for post in Post::get_for_blog(conn, &self)? { for post in Post::get_for_blog(conn, &self)? {
post.delete(conn)?; post.delete(conn, searcher)?;
} }
diesel::delete(self) diesel::delete(self)
.execute(conn) .execute(conn)
@@ -334,20 +335,20 @@ impl IntoId for Blog {
} }
} }
impl FromId<DbConn> for Blog { impl FromId<PlumeRocket> for Blog {
type Error = Error; type Error = Error;
type Object = CustomGroup; type Object = CustomGroup;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> { fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Self::find_by_ap_url(&conn, id) Self::find_by_ap_url(&c.conn, id)
} }
fn from_activity(conn: &DbConn, acct: CustomGroup) -> Result<Self> { fn from_activity(c: &PlumeRocket, acct: CustomGroup) -> Result<Self> {
let url = Url::parse(&acct.object.object_props.id_string()?)?; let url = Url::parse(&acct.object.object_props.id_string()?)?;
let inst = url.host_str()?; let inst = url.host_str()?;
let instance = Instance::find_by_domain(conn, inst).or_else(|_| { let instance = Instance::find_by_domain(&c.conn, inst).or_else(|_| {
Instance::insert( Instance::insert(
conn, &c.conn,
NewInstance { NewInstance {
public_domain: inst.to_owned(), public_domain: inst.to_owned(),
name: inst.to_owned(), name: inst.to_owned(),
@@ -370,9 +371,9 @@ impl FromId<DbConn> for Blog {
.and_then(|icon| { .and_then(|icon| {
let owner = icon.object_props.attributed_to_link::<Id>().ok()?; let owner = icon.object_props.attributed_to_link::<Id>().ok()?;
Media::save_remote( Media::save_remote(
conn, &c.conn,
icon.object_props.url_string().ok()?, icon.object_props.url_string().ok()?,
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?, &User::from_id(c, &owner, None).ok()?,
) )
.ok() .ok()
}) })
@@ -386,9 +387,9 @@ impl FromId<DbConn> for Blog {
.and_then(|banner| { .and_then(|banner| {
let owner = banner.object_props.attributed_to_link::<Id>().ok()?; let owner = banner.object_props.attributed_to_link::<Id>().ok()?;
Media::save_remote( Media::save_remote(
conn, &c.conn,
banner.object_props.url_string().ok()?, banner.object_props.url_string().ok()?,
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?, &User::from_id(c, &owner, None).ok()?,
) )
.ok() .ok()
}) })
@@ -400,7 +401,7 @@ impl FromId<DbConn> for Blog {
} }
Blog::insert( Blog::insert(
conn, &c.conn,
NewBlog { NewBlog {
actor_id: name.clone(), actor_id: name.clone(),
title: acct.object.object_props.name_string().unwrap_or(name), title: acct.object.object_props.name_string().unwrap_or(name),
@@ -496,8 +497,14 @@ impl NewBlog {
pub(crate) mod tests { pub(crate) mod tests {
use super::*; use super::*;
use crate::{ use crate::{
blog_authors::*, instance::tests as instance_tests, medias::NewMedia, tests::db, blog_authors::*,
users::tests as usersTests, Connection as Conn, config::CONFIG,
instance::tests as instance_tests,
medias::NewMedia,
search::tests::get_searcher,
tests::{db, rockets},
users::tests as usersTests,
Connection as Conn,
}; };
use diesel::Connection; use diesel::Connection;
@@ -584,10 +591,10 @@ pub(crate) mod tests {
fn get_instance() { fn get_instance() {
let conn = &db(); let conn = &db();
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
fill_database(&conn); fill_database(conn);
let blog = Blog::insert( let blog = Blog::insert(
&conn, conn,
NewBlog::new_local( NewBlog::new_local(
"SomeName".to_owned(), "SomeName".to_owned(),
"Some name".to_owned(), "Some name".to_owned(),
@@ -599,7 +606,7 @@ pub(crate) mod tests {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
blog.get_instance(&conn).unwrap().id, blog.get_instance(conn).unwrap().id,
Instance::get_local().unwrap().id Instance::get_local().unwrap().id
); );
// TODO add tests for remote instance // TODO add tests for remote instance
@@ -611,10 +618,10 @@ pub(crate) mod tests {
fn authors() { fn authors() {
let conn = &db(); let conn = &db();
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (user, _) = fill_database(&conn); let (user, _) = fill_database(conn);
let b1 = Blog::insert( let b1 = Blog::insert(
&conn, conn,
NewBlog::new_local( NewBlog::new_local(
"SomeName".to_owned(), "SomeName".to_owned(),
"Some name".to_owned(), "Some name".to_owned(),
@@ -625,7 +632,7 @@ pub(crate) mod tests {
) )
.unwrap(); .unwrap();
let b2 = Blog::insert( let b2 = Blog::insert(
&conn, conn,
NewBlog::new_local( NewBlog::new_local(
"Blog".to_owned(), "Blog".to_owned(),
"Blog".to_owned(), "Blog".to_owned(),
@@ -638,7 +645,7 @@ pub(crate) mod tests {
let blog = vec![b1, b2]; let blog = vec![b1, b2];
BlogAuthor::insert( BlogAuthor::insert(
&conn, conn,
NewBlogAuthor { NewBlogAuthor {
blog_id: blog[0].id, blog_id: blog[0].id,
author_id: user[0].id, author_id: user[0].id,
@@ -648,7 +655,7 @@ pub(crate) mod tests {
.unwrap(); .unwrap();
BlogAuthor::insert( BlogAuthor::insert(
&conn, conn,
NewBlogAuthor { NewBlogAuthor {
blog_id: blog[0].id, blog_id: blog[0].id,
author_id: user[1].id, author_id: user[1].id,
@@ -658,7 +665,7 @@ pub(crate) mod tests {
.unwrap(); .unwrap();
BlogAuthor::insert( BlogAuthor::insert(
&conn, conn,
NewBlogAuthor { NewBlogAuthor {
blog_id: blog[1].id, blog_id: blog[1].id,
author_id: user[0].id, author_id: user[0].id,
@@ -668,39 +675,39 @@ pub(crate) mod tests {
.unwrap(); .unwrap();
assert!(blog[0] assert!(blog[0]
.list_authors(&conn) .list_authors(conn)
.unwrap() .unwrap()
.iter() .iter()
.any(|a| a.id == user[0].id)); .any(|a| a.id == user[0].id));
assert!(blog[0] assert!(blog[0]
.list_authors(&conn) .list_authors(conn)
.unwrap() .unwrap()
.iter() .iter()
.any(|a| a.id == user[1].id)); .any(|a| a.id == user[1].id));
assert!(blog[1] assert!(blog[1]
.list_authors(&conn) .list_authors(conn)
.unwrap() .unwrap()
.iter() .iter()
.any(|a| a.id == user[0].id)); .any(|a| a.id == user[0].id));
assert!(!blog[1] assert!(!blog[1]
.list_authors(&conn) .list_authors(conn)
.unwrap() .unwrap()
.iter() .iter()
.any(|a| a.id == user[1].id)); .any(|a| a.id == user[1].id));
assert!(Blog::find_for_author(&conn, &user[0]) assert!(Blog::find_for_author(conn, &user[0])
.unwrap() .unwrap()
.iter() .iter()
.any(|b| b.id == blog[0].id)); .any(|b| b.id == blog[0].id));
assert!(Blog::find_for_author(&conn, &user[1]) assert!(Blog::find_for_author(conn, &user[1])
.unwrap() .unwrap()
.iter() .iter()
.any(|b| b.id == blog[0].id)); .any(|b| b.id == blog[0].id));
assert!(Blog::find_for_author(&conn, &user[0]) assert!(Blog::find_for_author(conn, &user[0])
.unwrap() .unwrap()
.iter() .iter()
.any(|b| b.id == blog[1].id)); .any(|b| b.id == blog[1].id));
assert!(!Blog::find_for_author(&conn, &user[1]) assert!(!Blog::find_for_author(conn, &user[1])
.unwrap() .unwrap()
.iter() .iter()
.any(|b| b.id == blog[1].id)); .any(|b| b.id == blog[1].id));
@@ -710,12 +717,13 @@ pub(crate) mod tests {
#[test] #[test]
fn find_local() { fn find_local() {
let conn = &db(); let r = rockets();
let conn = &*r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
fill_database(&conn); fill_database(conn);
let blog = Blog::insert( let blog = Blog::insert(
&conn, conn,
NewBlog::new_local( NewBlog::new_local(
"SomeName".to_owned(), "SomeName".to_owned(),
"Some name".to_owned(), "Some name".to_owned(),
@@ -726,7 +734,7 @@ pub(crate) mod tests {
) )
.unwrap(); .unwrap();
assert_eq!(Blog::find_by_fqn(&conn, "SomeName").unwrap().id, blog.id); assert_eq!(Blog::find_by_fqn(&r, "SomeName").unwrap().id, blog.id);
Ok(()) Ok(())
}) })
} }
@@ -735,10 +743,10 @@ pub(crate) mod tests {
fn get_fqn() { fn get_fqn() {
let conn = &db(); let conn = &db();
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
fill_database(&conn); fill_database(conn);
let blog = Blog::insert( let blog = Blog::insert(
&conn, conn,
NewBlog::new_local( NewBlog::new_local(
"SomeName".to_owned(), "SomeName".to_owned(),
"Some name".to_owned(), "Some name".to_owned(),
@@ -758,10 +766,12 @@ pub(crate) mod tests {
fn delete() { fn delete() {
let conn = &db(); let conn = &db();
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (_, blogs) = fill_database(&conn); let (_, blogs) = fill_database(conn);
blogs[0].delete(&conn).unwrap(); blogs[0]
assert!(Blog::get(&conn, blogs[0].id).is_err()); .delete(conn, &get_searcher(&CONFIG.search_tokenizers))
.unwrap();
assert!(Blog::get(conn, blogs[0].id).is_err());
Ok(()) Ok(())
}) })
} }
@@ -770,10 +780,11 @@ pub(crate) mod tests {
fn delete_via_user() { fn delete_via_user() {
let conn = &db(); let conn = &db();
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (user, _) = fill_database(&conn); let searcher = get_searcher(&CONFIG.search_tokenizers);
let (user, _) = fill_database(conn);
let b1 = Blog::insert( let b1 = Blog::insert(
&conn, conn,
NewBlog::new_local( NewBlog::new_local(
"SomeName".to_owned(), "SomeName".to_owned(),
"Some name".to_owned(), "Some name".to_owned(),
@@ -784,7 +795,7 @@ pub(crate) mod tests {
) )
.unwrap(); .unwrap();
let b2 = Blog::insert( let b2 = Blog::insert(
&conn, conn,
NewBlog::new_local( NewBlog::new_local(
"Blog".to_owned(), "Blog".to_owned(),
"Blog".to_owned(), "Blog".to_owned(),
@@ -797,7 +808,7 @@ pub(crate) mod tests {
let blog = vec![b1, b2]; let blog = vec![b1, b2];
BlogAuthor::insert( BlogAuthor::insert(
&conn, conn,
NewBlogAuthor { NewBlogAuthor {
blog_id: blog[0].id, blog_id: blog[0].id,
author_id: user[0].id, author_id: user[0].id,
@@ -807,7 +818,7 @@ pub(crate) mod tests {
.unwrap(); .unwrap();
BlogAuthor::insert( BlogAuthor::insert(
&conn, conn,
NewBlogAuthor { NewBlogAuthor {
blog_id: blog[0].id, blog_id: blog[0].id,
author_id: user[1].id, author_id: user[1].id,
@@ -817,7 +828,7 @@ pub(crate) mod tests {
.unwrap(); .unwrap();
BlogAuthor::insert( BlogAuthor::insert(
&conn, conn,
NewBlogAuthor { NewBlogAuthor {
blog_id: blog[1].id, blog_id: blog[1].id,
author_id: user[0].id, author_id: user[0].id,
@@ -826,23 +837,24 @@ pub(crate) mod tests {
) )
.unwrap(); .unwrap();
user[0].delete(&conn).unwrap(); user[0].delete(conn, &searcher).unwrap();
assert!(Blog::get(&conn, blog[0].id).is_ok()); assert!(Blog::get(conn, blog[0].id).is_ok());
assert!(Blog::get(&conn, blog[1].id).is_err()); assert!(Blog::get(conn, blog[1].id).is_err());
user[1].delete(&conn).unwrap(); user[1].delete(conn, &searcher).unwrap();
assert!(Blog::get(&conn, blog[0].id).is_err()); assert!(Blog::get(conn, blog[0].id).is_err());
Ok(()) Ok(())
}) })
} }
#[test] #[test]
fn self_federation() { fn self_federation() {
let conn = &db(); let r = rockets();
let conn = &*r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (users, mut blogs) = fill_database(&conn); let (users, mut blogs) = fill_database(conn);
blogs[0].icon_id = Some( blogs[0].icon_id = Some(
Media::insert( Media::insert(
&conn, conn,
NewMedia { NewMedia {
file_path: "aaa.png".into(), file_path: "aaa.png".into(),
alt_text: String::new(), alt_text: String::new(),
@@ -858,7 +870,7 @@ pub(crate) mod tests {
); );
blogs[0].banner_id = Some( blogs[0].banner_id = Some(
Media::insert( Media::insert(
&conn, conn,
NewMedia { NewMedia {
file_path: "bbb.png".into(), file_path: "bbb.png".into(),
alt_text: String::new(), alt_text: String::new(),
@@ -872,11 +884,11 @@ pub(crate) mod tests {
.unwrap() .unwrap()
.id, .id,
); );
let _: Blog = blogs[0].save_changes(&**conn).unwrap(); let _: Blog = blogs[0].save_changes(conn).unwrap();
let ap_repr = blogs[0].to_activity(&conn).unwrap(); let ap_repr = blogs[0].to_activity(conn).unwrap();
blogs[0].delete(&conn).unwrap(); blogs[0].delete(conn, &*r.searcher).unwrap();
let blog = Blog::from_activity(&conn, ap_repr).unwrap(); let blog = Blog::from_activity(&r, ap_repr).unwrap();
assert_eq!(blog.actor_id, blogs[0].actor_id); assert_eq!(blog.actor_id, blogs[0].actor_id);
assert_eq!(blog.title, blogs[0].title); assert_eq!(blog.title, blogs[0].title);
@@ -888,8 +900,8 @@ pub(crate) mod tests {
assert_eq!(blog.public_key, blogs[0].public_key); assert_eq!(blog.public_key, blogs[0].public_key);
assert_eq!(blog.fqn, blogs[0].fqn); assert_eq!(blog.fqn, blogs[0].fqn);
assert_eq!(blog.summary_html, blogs[0].summary_html); assert_eq!(blog.summary_html, blogs[0].summary_html);
assert_eq!(blog.icon_url(&conn), blogs[0].icon_url(&conn)); assert_eq!(blog.icon_url(conn), blogs[0].icon_url(conn));
assert_eq!(blog.banner_url(&conn), blogs[0].banner_url(&conn)); assert_eq!(blog.banner_url(conn), blogs[0].banner_url(conn));
Ok(()) Ok(())
}) })
+39 -38
View File
@@ -1,6 +1,5 @@
use crate::{ use crate::{
comment_seers::{CommentSeers, NewCommentSeers}, comment_seers::{CommentSeers, NewCommentSeers},
db_conn::DbConn,
instance::Instance, instance::Instance,
medias::Media, medias::Media,
mentions::Mention, mentions::Mention,
@@ -9,7 +8,7 @@ use crate::{
safe_string::SafeString, safe_string::SafeString,
schema::comments, schema::comments,
users::User, users::User,
Connection, Error, Result, CONFIG, Connection, Error, PlumeRocket, Result,
}; };
use activitypub::{ use activitypub::{
activity::{Create, Delete}, activity::{Create, Delete},
@@ -25,6 +24,7 @@ use plume_common::{
}, },
utils, utils,
}; };
use serde_json;
use std::collections::HashSet; use std::collections::HashSet;
#[derive(Queryable, Identifiable, Clone, AsChangeset)] #[derive(Queryable, Identifiable, Clone, AsChangeset)]
@@ -105,13 +105,13 @@ impl Comment {
.unwrap_or(false) .unwrap_or(false)
} }
pub fn to_activity(&self, conn: &DbConn) -> Result<Note> { pub fn to_activity(&self, c: &PlumeRocket) -> Result<Note> {
let author = User::get(conn, self.author_id)?; let author = User::get(&c.conn, self.author_id)?;
let (html, mentions, _hashtags) = utils::md_to_html( let (html, mentions, _hashtags) = utils::md_to_html(
self.content.get().as_ref(), self.content.get().as_ref(),
Some(&Instance::get_local()?.public_domain), Some(&Instance::get_local()?.public_domain),
true, true,
Some(Media::get_media_processor(conn, vec![&author])), Some(Media::get_media_processor(&c.conn, vec![&author])),
); );
let mut note = Note::default(); let mut note = Note::default();
@@ -124,8 +124,8 @@ impl Comment {
note.object_props.set_content_string(html)?; note.object_props.set_content_string(html)?;
note.object_props note.object_props
.set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else( .set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else(
|| Ok(Post::get(conn, self.post_id)?.ap_url), || Ok(Post::get(&c.conn, self.post_id)?.ap_url),
|id| Ok(Comment::get(conn, id)?.ap_url.unwrap_or_default()) as Result<String>, |id| Ok(Comment::get(&c.conn, id)?.ap_url.unwrap_or_default()) as Result<String>,
)?))?; )?))?;
note.object_props note.object_props
.set_published_string(chrono::Utc::now().to_rfc3339())?; .set_published_string(chrono::Utc::now().to_rfc3339())?;
@@ -134,16 +134,16 @@ impl Comment {
note.object_props.set_tag_link_vec( note.object_props.set_tag_link_vec(
mentions mentions
.into_iter() .into_iter()
.filter_map(|m| Mention::build_activity(conn, &m).ok()) .filter_map(|m| Mention::build_activity(c, &m).ok())
.collect::<Vec<link::Mention>>(), .collect::<Vec<link::Mention>>(),
)?; )?;
Ok(note) Ok(note)
} }
pub fn create_activity(&self, conn: &DbConn) -> Result<Create> { pub fn create_activity(&self, c: &PlumeRocket) -> Result<Create> {
let author = User::get(&conn, self.author_id)?; let author = User::get(&c.conn, self.author_id)?;
let note = self.to_activity(conn)?; let note = self.to_activity(c)?;
let mut act = Create::default(); let mut act = Create::default();
act.create_props.set_actor_link(author.into_id())?; act.create_props.set_actor_link(author.into_id())?;
act.create_props.set_object_object(note.clone())?; act.create_props.set_object_object(note.clone())?;
@@ -152,7 +152,7 @@ impl Comment {
act.object_props act.object_props
.set_to_link_vec(note.object_props.to_link_vec::<Id>()?)?; .set_to_link_vec(note.object_props.to_link_vec::<Id>()?)?;
act.object_props act.object_props
.set_cc_link_vec(vec![Id::new(self.get_author(&conn)?.followers_endpoint)])?; .set_cc_link_vec(vec![Id::new(self.get_author(&c.conn)?.followers_endpoint)])?;
Ok(act) Ok(act)
} }
@@ -194,15 +194,16 @@ impl Comment {
} }
} }
impl FromId<DbConn> for Comment { impl FromId<PlumeRocket> for Comment {
type Error = Error; type Error = Error;
type Object = Note; type Object = Note;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> { fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Self::find_by_ap_url(conn, id) Self::find_by_ap_url(&c.conn, id)
} }
fn from_activity(conn: &DbConn, note: Note) -> Result<Self> { fn from_activity(c: &PlumeRocket, note: Note) -> Result<Self> {
let conn = &*c.conn;
let comm = { let comm = {
let previous_url = note.object_props.in_reply_to.as_ref()?.as_str()?; let previous_url = note.object_props.in_reply_to.as_ref()?.as_str()?;
let previous_comment = Comment::find_by_ap_url(conn, previous_url); let previous_comment = Comment::find_by_ap_url(conn, previous_url);
@@ -235,10 +236,9 @@ impl FromId<DbConn> for Comment {
Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result<i32> Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result<i32>
})?, })?,
author_id: User::from_id( author_id: User::from_id(
conn, c,
&note.object_props.attributed_to_link::<Id>()?, &note.object_props.attributed_to_link::<Id>()?,
None, None,
CONFIG.proxy(),
) )
.map_err(|(_, e)| e)? .map_err(|(_, e)| e)?
.id, .id,
@@ -296,7 +296,7 @@ impl FromId<DbConn> for Comment {
.collect::<HashSet<_>>() // remove duplicates (don't do a query more than once) .collect::<HashSet<_>>() // remove duplicates (don't do a query more than once)
.into_iter() .into_iter()
.map(|v| { .map(|v| {
if let Ok(user) = User::from_id(conn, &v, None, CONFIG.proxy()) { if let Ok(user) = User::from_id(c, &v, None) {
vec![user] vec![user]
} else { } else {
vec![] // TODO try to fetch collection vec![] // TODO try to fetch collection
@@ -322,41 +322,41 @@ impl FromId<DbConn> for Comment {
} }
} }
impl AsObject<User, Create, &DbConn> for Comment { impl AsObject<User, Create, &PlumeRocket> for Comment {
type Error = Error; type Error = Error;
type Output = Self; type Output = Self;
fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result<Self> { fn activity(self, _c: &PlumeRocket, _actor: User, _id: &str) -> Result<Self> {
// The actual creation takes place in the FromId impl // The actual creation takes place in the FromId impl
Ok(self) Ok(self)
} }
} }
impl AsObject<User, Delete, &DbConn> for Comment { impl AsObject<User, Delete, &PlumeRocket> for Comment {
type Error = Error; type Error = Error;
type Output = (); type Output = ();
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
if self.author_id != actor.id { if self.author_id != actor.id {
return Err(Error::Unauthorized); return Err(Error::Unauthorized);
} }
for m in Mention::list_for_comment(conn, self.id)? { for m in Mention::list_for_comment(&c.conn, self.id)? {
for n in Notification::find_for_mention(conn, &m)? { for n in Notification::find_for_mention(&c.conn, &m)? {
n.delete(conn)?; n.delete(&c.conn)?;
} }
m.delete(conn)?; m.delete(&c.conn)?;
} }
for n in Notification::find_for_comment(&conn, &self)? { for n in Notification::find_for_comment(&c.conn, &self)? {
n.delete(&**conn)?; n.delete(&c.conn)?;
} }
diesel::update(comments::table) diesel::update(comments::table)
.filter(comments::in_response_to_id.eq(self.id)) .filter(comments::in_response_to_id.eq(self.id))
.set(comments::in_response_to_id.eq(self.in_response_to_id)) .set(comments::in_response_to_id.eq(self.in_response_to_id))
.execute(&**conn)?; .execute(&*c.conn)?;
diesel::delete(&self).execute(&**conn)?; diesel::delete(&self).execute(&*c.conn)?;
Ok(()) Ok(())
} }
} }
@@ -392,16 +392,17 @@ mod tests {
use super::*; use super::*;
use crate::inbox::{inbox, tests::fill_database, InboxResult}; use crate::inbox::{inbox, tests::fill_database, InboxResult};
use crate::safe_string::SafeString; use crate::safe_string::SafeString;
use crate::tests::db; use crate::tests::rockets;
use diesel::Connection; use diesel::Connection;
// creates a post, get it's Create activity, delete the post, // creates a post, get it's Create activity, delete the post,
// "send" the Create to the inbox, and check it works // "send" the Create to the inbox, and check it works
#[test] #[test]
fn self_federation() { fn self_federation() {
let conn = &db(); let r = rockets();
let conn = &*r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&conn); let (posts, users, _) = fill_database(&r);
let original_comm = Comment::insert( let original_comm = Comment::insert(
conn, conn,
@@ -417,14 +418,14 @@ mod tests {
}, },
) )
.unwrap(); .unwrap();
let act = original_comm.create_activity(&conn).unwrap(); let act = original_comm.create_activity(&r).unwrap();
inbox( inbox(
&conn, &r,
serde_json::to_value(original_comm.build_delete(&conn).unwrap()).unwrap(), serde_json::to_value(original_comm.build_delete(conn).unwrap()).unwrap(),
) )
.unwrap(); .unwrap();
match inbox(&conn, serde_json::to_value(act).unwrap()).unwrap() { match inbox(&r, serde_json::to_value(act).unwrap()).unwrap() {
InboxResult::Commented(c) => { InboxResult::Commented(c) => {
// TODO: one is HTML, the other markdown: assert_eq!(c.content, original_comm.content); // 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.in_response_to_id, original_comm.in_response_to_id);
+2 -96
View File
@@ -1,7 +1,6 @@
use crate::search::TokenizerKind as SearchTokenizer; use crate::search::TokenizerKind as SearchTokenizer;
use rocket::config::Limits; use rocket::config::Limits;
use rocket::Config as RocketConfig; use rocket::Config as RocketConfig;
use std::collections::HashSet;
use std::env::{self, var}; use std::env::{self, var};
#[cfg(not(test))] #[cfg(not(test))]
@@ -21,13 +20,6 @@ pub struct Config {
pub logo: LogoConfig, pub logo: LogoConfig,
pub default_theme: String, pub default_theme: String,
pub media_directory: String, pub media_directory: String,
pub ldap: Option<LdapConfig>,
pub proxy: Option<ProxyConfig>,
}
impl Config {
pub fn proxy(&self) -> Option<&reqwest::Proxy> {
self.proxy.as_ref().map(|p| &p.proxy)
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -164,8 +156,8 @@ impl Default for LogoConfig {
}; };
let mut custom_icons = env::vars() let mut custom_icons = env::vars()
.filter_map(|(var, val)| { .filter_map(|(var, val)| {
if let Some(size) = var.strip_prefix("PLUME_LOGO_") { if var.starts_with("PLUME_LOGO_") {
Some((size.to_owned(), val)) Some((var[11..].to_owned(), val))
} else { } else {
None None
} }
@@ -248,90 +240,6 @@ impl SearchTokenizerConfig {
} }
} }
pub struct LdapConfig {
pub addr: String,
pub base_dn: String,
pub tls: bool,
pub user_name_attr: String,
pub mail_attr: String,
pub user: Option<(String, String)>,
}
fn get_ldap_config() -> Option<LdapConfig> {
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());
let user = var("LDAP_USER").ok();
let password = var("LDAP_PASSWORD").ok();
let user = match (user, password) {
(Some(user), Some(password)) => Some((user, password)),
(None, None) => None,
_ => panic!("Invalid LDAP configuration both or neither of LDAP_USER and LDAP_PASSWORD must be set")
};
Some(LdapConfig {
addr,
base_dn,
tls,
user_name_attr,
mail_attr,
user
})
}
(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<HashSet<String>>,
pub proxy: reqwest::Proxy,
}
fn get_proxy_config() -> Option<ProxyConfig> {
let url: reqwest::Url = var("PROXY_URL").ok()?.parse().expect("Invalid PROXY_URL");
let proxy_url = url.clone();
let only_domains: Option<HashSet<String>> = 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! { lazy_static! {
pub static ref CONFIG: Config = Config { pub static ref CONFIG: Config = Config {
base_url: var("BASE_URL").unwrap_or_else(|_| format!( base_url: var("BASE_URL").unwrap_or_else(|_| format!(
@@ -359,7 +267,5 @@ lazy_static! {
default_theme: var("DEFAULT_THEME").unwrap_or_else(|_| "default-light".to_owned()), default_theme: var("DEFAULT_THEME").unwrap_or_else(|_| "default-light".to_owned()),
media_directory: var("MEDIA_UPLOAD_DIRECTORY") media_directory: var("MEDIA_UPLOAD_DIRECTORY")
.unwrap_or_else(|_| "static/media".to_owned()), .unwrap_or_else(|_| "static/media".to_owned()),
ldap: get_ldap_config(),
proxy: get_proxy_config(),
}; };
} }
+21 -36
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
ap_url, db_conn::DbConn, notifications::*, schema::follows, users::User, Connection, Error, ap_url, notifications::*, schema::follows, users::User, Connection, Error, PlumeRocket, Result,
Result, CONFIG, CONFIG,
}; };
use activitypub::activity::{Accept, Follow as FollowAct, Undo}; use activitypub::activity::{Accept, Follow as FollowAct, Undo};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
@@ -116,12 +116,7 @@ impl Follow {
.accept_props .accept_props
.set_actor_link::<Id>(target.clone().into_id())?; .set_actor_link::<Id>(target.clone().into_id())?;
accept.accept_props.set_object_object(follow)?; accept.accept_props.set_object_object(follow)?;
broadcast( broadcast(&*target, accept, vec![from.clone()]);
&*target,
accept,
vec![from.clone()],
CONFIG.proxy().cloned(),
);
Ok(res) Ok(res)
} }
@@ -141,11 +136,11 @@ impl Follow {
} }
} }
impl AsObject<User, FollowAct, &DbConn> for User { impl AsObject<User, FollowAct, &PlumeRocket> for User {
type Error = Error; type Error = Error;
type Output = Follow; type Output = Follow;
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Follow> { fn activity(self, c: &PlumeRocket, actor: User, id: &str) -> Result<Follow> {
// Mastodon (at least) requires the full Follow object when accepting it, // Mastodon (at least) requires the full Follow object when accepting it,
// so we rebuilt it here // so we rebuilt it here
let mut follow = FollowAct::default(); let mut follow = FollowAct::default();
@@ -153,50 +148,40 @@ impl AsObject<User, FollowAct, &DbConn> for User {
follow follow
.follow_props .follow_props
.set_actor_link::<Id>(actor.clone().into_id())?; .set_actor_link::<Id>(actor.clone().into_id())?;
Follow::accept_follow(conn, &actor, &self, follow, actor.id, self.id) Follow::accept_follow(&c.conn, &actor, &self, follow, actor.id, self.id)
} }
} }
impl FromId<DbConn> for Follow { impl FromId<PlumeRocket> for Follow {
type Error = Error; type Error = Error;
type Object = FollowAct; type Object = FollowAct;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> { fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Follow::find_by_ap_url(conn, id) Follow::find_by_ap_url(&c.conn, id)
} }
fn from_activity(conn: &DbConn, follow: FollowAct) -> Result<Self> { fn from_activity(c: &PlumeRocket, follow: FollowAct) -> Result<Self> {
let actor = User::from_id( let actor =
conn, User::from_id(c, &follow.follow_props.actor_link::<Id>()?, None).map_err(|(_, e)| e)?;
&follow.follow_props.actor_link::<Id>()?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?;
let target = User::from_id( let target = User::from_id(c, &follow.follow_props.object_link::<Id>()?, None)
conn, .map_err(|(_, e)| e)?;
&follow.follow_props.object_link::<Id>()?, Follow::accept_follow(&c.conn, &actor, &target, follow, actor.id, target.id)
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?;
Follow::accept_follow(conn, &actor, &target, follow, actor.id, target.id)
} }
} }
impl AsObject<User, Undo, &DbConn> for Follow { impl AsObject<User, Undo, &PlumeRocket> for Follow {
type Error = Error; type Error = Error;
type Output = (); type Output = ();
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
let conn = conn; let conn = &*c.conn;
if self.follower_id == actor.id { if self.follower_id == actor.id {
diesel::delete(&self).execute(&**conn)?; diesel::delete(&self).execute(conn)?;
// delete associated notification if any // delete associated notification if any
if let Ok(notif) = Notification::find(&conn, notification_kind::FOLLOW, self.id) { if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
diesel::delete(&notif).execute(&**conn)?; diesel::delete(&notif).execute(conn)?;
} }
Ok(()) Ok(())
+83 -266
View File
@@ -1,13 +1,13 @@
use activitypub::activity::*; use activitypub::activity::*;
use serde_json;
use crate::{ use crate::{
comments::Comment, comments::Comment,
db_conn::DbConn,
follows, likes, follows, likes,
posts::{Post, PostUpdate}, posts::{Post, PostUpdate},
reshares::Reshare, reshares::Reshare,
users::User, users::User,
Error, CONFIG, Error, PlumeRocket,
}; };
use plume_common::activity_pub::inbox::Inbox; use plume_common::activity_pub::inbox::Inbox;
@@ -46,20 +46,20 @@ impl_into_inbox_result! {
Reshare => Reshared Reshare => Reshared
} }
pub fn inbox(conn: &DbConn, act: serde_json::Value) -> Result<InboxResult, Error> { pub fn inbox(ctx: &PlumeRocket, act: serde_json::Value) -> Result<InboxResult, Error> {
Inbox::handle(conn, act) Inbox::handle(ctx, act)
.with::<User, Announce, Post>(CONFIG.proxy()) .with::<User, Announce, Post>()
.with::<User, Create, Comment>(CONFIG.proxy()) .with::<User, Create, Comment>()
.with::<User, Create, Post>(CONFIG.proxy()) .with::<User, Create, Post>()
.with::<User, Delete, Comment>(CONFIG.proxy()) .with::<User, Delete, Comment>()
.with::<User, Delete, Post>(CONFIG.proxy()) .with::<User, Delete, Post>()
.with::<User, Delete, User>(CONFIG.proxy()) .with::<User, Delete, User>()
.with::<User, Follow, User>(CONFIG.proxy()) .with::<User, Follow, User>()
.with::<User, Like, Post>(CONFIG.proxy()) .with::<User, Like, Post>()
.with::<User, Undo, Reshare>(CONFIG.proxy()) .with::<User, Undo, Reshare>()
.with::<User, Undo, follows::Follow>(CONFIG.proxy()) .with::<User, Undo, follows::Follow>()
.with::<User, Undo, likes::Like>(CONFIG.proxy()) .with::<User, Undo, likes::Like>()
.with::<User, Update, PostUpdate>(CONFIG.proxy()) .with::<User, Update, PostUpdate>()
.done() .done()
} }
@@ -67,13 +67,13 @@ pub fn inbox(conn: &DbConn, act: serde_json::Value) -> Result<InboxResult, Error
pub(crate) mod tests { pub(crate) mod tests {
use super::InboxResult; use super::InboxResult;
use crate::blogs::tests::fill_database as blog_fill_db; use crate::blogs::tests::fill_database as blog_fill_db;
use crate::db_conn::DbConn;
use crate::safe_string::SafeString; use crate::safe_string::SafeString;
use crate::tests::db; use crate::tests::rockets;
use crate::PlumeRocket;
use diesel::Connection; use diesel::Connection;
pub fn fill_database( pub fn fill_database(
conn: &DbConn, rockets: &PlumeRocket,
) -> ( ) -> (
Vec<crate::posts::Post>, Vec<crate::posts::Post>,
Vec<crate::users::User>, Vec<crate::users::User>,
@@ -82,9 +82,9 @@ pub(crate) mod tests {
use crate::post_authors::*; use crate::post_authors::*;
use crate::posts::*; use crate::posts::*;
let (users, blogs) = blog_fill_db(&conn); let (users, blogs) = blog_fill_db(&rockets.conn);
let post = Post::insert( let post = Post::insert(
&conn, &rockets.conn,
NewPost { NewPost {
blog_id: blogs[0].id, blog_id: blogs[0].id,
slug: "testing".to_owned(), slug: "testing".to_owned(),
@@ -98,11 +98,12 @@ pub(crate) mod tests {
source: String::new(), source: String::new(),
cover_id: None, cover_id: None,
}, },
&rockets.searcher,
) )
.unwrap(); .unwrap();
PostAuthor::insert( PostAuthor::insert(
&conn, &rockets.conn,
NewPostAuthor { NewPostAuthor {
post_id: post.id, post_id: post.id,
author_id: users[0].id, author_id: users[0].id,
@@ -115,9 +116,10 @@ pub(crate) mod tests {
#[test] #[test]
fn announce_post() { fn announce_post() {
let conn = db(); let r = rockets();
let conn = &*r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&conn); let (posts, users, _) = fill_database(&r);
let act = json!({ let act = json!({
"id": "https://plu.me/announce/1", "id": "https://plu.me/announce/1",
"actor": users[0].ap_url, "actor": users[0].ap_url,
@@ -125,7 +127,7 @@ pub(crate) mod tests {
"type": "Announce", "type": "Announce",
}); });
match super::inbox(&conn, act).unwrap() { match super::inbox(&r, act).unwrap() {
super::InboxResult::Reshared(r) => { super::InboxResult::Reshared(r) => {
assert_eq!(r.post_id, posts[0].id); assert_eq!(r.post_id, posts[0].id);
assert_eq!(r.user_id, users[0].id); assert_eq!(r.user_id, users[0].id);
@@ -139,9 +141,10 @@ pub(crate) mod tests {
#[test] #[test]
fn create_comment() { fn create_comment() {
let conn = db(); let r = rockets();
let conn = &*r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&conn); let (posts, users, _) = fill_database(&r);
let act = json!({ let act = json!({
"id": "https://plu.me/comment/1/activity", "id": "https://plu.me/comment/1/activity",
"actor": users[0].ap_url, "actor": users[0].ap_url,
@@ -156,7 +159,7 @@ pub(crate) mod tests {
"type": "Create", "type": "Create",
}); });
match super::inbox(&conn, act).unwrap() { match super::inbox(&r, act).unwrap() {
super::InboxResult::Commented(c) => { super::InboxResult::Commented(c) => {
assert_eq!(c.author_id, users[0].id); assert_eq!(c.author_id, users[0].id);
assert_eq!(c.post_id, posts[0].id); assert_eq!(c.post_id, posts[0].id);
@@ -170,99 +173,12 @@ pub(crate) mod tests {
}); });
} }
#[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] #[test]
fn create_post() { fn create_post() {
let conn = db(); let r = rockets();
let conn = &*r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (_, users, blogs) = fill_database(&conn); let (_, users, blogs) = fill_database(&r);
let act = json!({ let act = json!({
"id": "https://plu.me/comment/1/activity", "id": "https://plu.me/comment/1/activity",
"actor": users[0].ap_url, "actor": users[0].ap_url,
@@ -283,9 +199,9 @@ pub(crate) mod tests {
"type": "Create", "type": "Create",
}); });
match super::inbox(&conn, act).unwrap() { match super::inbox(&r, act).unwrap() {
super::InboxResult::Post(p) => { super::InboxResult::Post(p) => {
assert!(p.is_author(&conn, users[0].id).unwrap()); assert!(p.is_author(conn, users[0].id).unwrap());
assert_eq!(p.source, "Hello.".to_owned()); assert_eq!(p.source, "Hello.".to_owned());
assert_eq!(p.blog_id, blogs[0].id); assert_eq!(p.blog_id, blogs[0].id);
assert_eq!(p.content, SafeString::new("Hello.")); assert_eq!(p.content, SafeString::new("Hello."));
@@ -298,123 +214,16 @@ pub(crate) mod tests {
}); });
} }
#[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] #[test]
fn delete_comment() { fn delete_comment() {
use crate::comments::*; use crate::comments::*;
let conn = db(); let r = rockets();
let conn = &*r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&conn); let (posts, users, _) = fill_database(&r);
Comment::insert( Comment::insert(
&conn, conn,
NewComment { NewComment {
content: SafeString::new("My comment"), content: SafeString::new("My comment"),
in_response_to_id: None, in_response_to_id: None,
@@ -434,7 +243,7 @@ pub(crate) mod tests {
"object": "https://plu.me/comment/1", "object": "https://plu.me/comment/1",
"type": "Delete", "type": "Delete",
}); });
assert!(super::inbox(&conn, fail_act).is_err()); assert!(super::inbox(&r, fail_act).is_err());
let ok_act = json!({ let ok_act = json!({
"id": "https://plu.me/comment/1/delete", "id": "https://plu.me/comment/1/delete",
@@ -442,16 +251,17 @@ pub(crate) mod tests {
"object": "https://plu.me/comment/1", "object": "https://plu.me/comment/1",
"type": "Delete", "type": "Delete",
}); });
assert!(super::inbox(&conn, ok_act).is_ok()); assert!(super::inbox(&r, ok_act).is_ok());
Ok(()) Ok(())
}) })
} }
#[test] #[test]
fn delete_post() { fn delete_post() {
let conn = db(); let r = rockets();
let conn = &*r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&conn); let (posts, users, _) = fill_database(&r);
let fail_act = json!({ let fail_act = json!({
"id": "https://plu.me/comment/1/delete", "id": "https://plu.me/comment/1/delete",
@@ -459,7 +269,7 @@ pub(crate) mod tests {
"object": posts[0].ap_url, "object": posts[0].ap_url,
"type": "Delete", "type": "Delete",
}); });
assert!(super::inbox(&conn, fail_act).is_err()); assert!(super::inbox(&r, fail_act).is_err());
let ok_act = json!({ let ok_act = json!({
"id": "https://plu.me/comment/1/delete", "id": "https://plu.me/comment/1/delete",
@@ -467,16 +277,17 @@ pub(crate) mod tests {
"object": posts[0].ap_url, "object": posts[0].ap_url,
"type": "Delete", "type": "Delete",
}); });
assert!(super::inbox(&conn, ok_act).is_ok()); assert!(super::inbox(&r, ok_act).is_ok());
Ok(()) Ok(())
}); });
} }
#[test] #[test]
fn delete_user() { fn delete_user() {
let conn = db(); let r = rockets();
let conn = &*r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (_, users, _) = fill_database(&conn); let (_, users, _) = fill_database(&r);
let fail_act = json!({ let fail_act = json!({
"id": "https://plu.me/@/Admin#delete", "id": "https://plu.me/@/Admin#delete",
@@ -484,7 +295,7 @@ pub(crate) mod tests {
"object": users[0].ap_url, "object": users[0].ap_url,
"type": "Delete", "type": "Delete",
}); });
assert!(super::inbox(&conn, fail_act).is_err()); assert!(super::inbox(&r, fail_act).is_err());
let ok_act = json!({ let ok_act = json!({
"id": "https://plu.me/@/Admin#delete", "id": "https://plu.me/@/Admin#delete",
@@ -492,8 +303,8 @@ pub(crate) mod tests {
"object": users[0].ap_url, "object": users[0].ap_url,
"type": "Delete", "type": "Delete",
}); });
assert!(super::inbox(&conn, ok_act).is_ok()); assert!(super::inbox(&r, ok_act).is_ok());
assert!(crate::users::User::get(&conn, users[0].id).is_err()); assert!(crate::users::User::get(conn, users[0].id).is_err());
Ok(()) Ok(())
}); });
@@ -501,9 +312,10 @@ pub(crate) mod tests {
#[test] #[test]
fn follow() { fn follow() {
let conn = db(); let r = rockets();
let conn = &*r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (_, users, _) = fill_database(&conn); let (_, users, _) = fill_database(&r);
let act = json!({ let act = json!({
"id": "https://plu.me/follow/1", "id": "https://plu.me/follow/1",
@@ -511,7 +323,7 @@ pub(crate) mod tests {
"object": users[1].ap_url, "object": users[1].ap_url,
"type": "Follow", "type": "Follow",
}); });
match super::inbox(&conn, act).unwrap() { match super::inbox(&r, act).unwrap() {
InboxResult::Followed(f) => { InboxResult::Followed(f) => {
assert_eq!(f.follower_id, users[0].id); assert_eq!(f.follower_id, users[0].id);
assert_eq!(f.following_id, users[1].id); assert_eq!(f.following_id, users[1].id);
@@ -525,9 +337,10 @@ pub(crate) mod tests {
#[test] #[test]
fn like() { fn like() {
let conn = db(); let r = rockets();
let conn = &*r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&conn); let (posts, users, _) = fill_database(&r);
let act = json!({ let act = json!({
"id": "https://plu.me/like/1", "id": "https://plu.me/like/1",
@@ -535,7 +348,7 @@ pub(crate) mod tests {
"object": posts[0].ap_url, "object": posts[0].ap_url,
"type": "Like", "type": "Like",
}); });
match super::inbox(&conn, act).unwrap() { match super::inbox(&r, act).unwrap() {
InboxResult::Liked(l) => { InboxResult::Liked(l) => {
assert_eq!(l.user_id, users[1].id); assert_eq!(l.user_id, users[1].id);
assert_eq!(l.post_id, posts[0].id); assert_eq!(l.post_id, posts[0].id);
@@ -551,12 +364,13 @@ pub(crate) mod tests {
fn undo_reshare() { fn undo_reshare() {
use crate::reshares::*; use crate::reshares::*;
let conn = db(); let r = rockets();
let conn = &*r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&conn); let (posts, users, _) = fill_database(&r);
let announce = Reshare::insert( let announce = Reshare::insert(
&conn, conn,
NewReshare { NewReshare {
post_id: posts[0].id, post_id: posts[0].id,
user_id: users[1].id, user_id: users[1].id,
@@ -571,7 +385,7 @@ pub(crate) mod tests {
"object": announce.ap_url, "object": announce.ap_url,
"type": "Undo", "type": "Undo",
}); });
assert!(super::inbox(&conn, fail_act).is_err()); assert!(super::inbox(&r, fail_act).is_err());
let ok_act = json!({ let ok_act = json!({
"id": "https://plu.me/undo/1", "id": "https://plu.me/undo/1",
@@ -579,7 +393,7 @@ pub(crate) mod tests {
"object": announce.ap_url, "object": announce.ap_url,
"type": "Undo", "type": "Undo",
}); });
assert!(super::inbox(&conn, ok_act).is_ok()); assert!(super::inbox(&r, ok_act).is_ok());
Ok(()) Ok(())
}); });
} }
@@ -588,12 +402,13 @@ pub(crate) mod tests {
fn undo_follow() { fn undo_follow() {
use crate::follows::*; use crate::follows::*;
let conn = db(); let r = rockets();
let conn = &*r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (_, users, _) = fill_database(&conn); let (_, users, _) = fill_database(&r);
let follow = Follow::insert( let follow = Follow::insert(
&conn, conn,
NewFollow { NewFollow {
follower_id: users[0].id, follower_id: users[0].id,
following_id: users[1].id, following_id: users[1].id,
@@ -608,7 +423,7 @@ pub(crate) mod tests {
"object": follow.ap_url, "object": follow.ap_url,
"type": "Undo", "type": "Undo",
}); });
assert!(super::inbox(&conn, fail_act).is_err()); assert!(super::inbox(&r, fail_act).is_err());
let ok_act = json!({ let ok_act = json!({
"id": "https://plu.me/undo/1", "id": "https://plu.me/undo/1",
@@ -616,7 +431,7 @@ pub(crate) mod tests {
"object": follow.ap_url, "object": follow.ap_url,
"type": "Undo", "type": "Undo",
}); });
assert!(super::inbox(&conn, ok_act).is_ok()); assert!(super::inbox(&r, ok_act).is_ok());
Ok(()) Ok(())
}); });
} }
@@ -625,12 +440,13 @@ pub(crate) mod tests {
fn undo_like() { fn undo_like() {
use crate::likes::*; use crate::likes::*;
let conn = db(); let r = rockets();
let conn = &*r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&conn); let (posts, users, _) = fill_database(&r);
let like = Like::insert( let like = Like::insert(
&conn, conn,
NewLike { NewLike {
post_id: posts[0].id, post_id: posts[0].id,
user_id: users[1].id, user_id: users[1].id,
@@ -645,7 +461,7 @@ pub(crate) mod tests {
"object": like.ap_url, "object": like.ap_url,
"type": "Undo", "type": "Undo",
}); });
assert!(super::inbox(&conn, fail_act).is_err()); assert!(super::inbox(&r, fail_act).is_err());
let ok_act = json!({ let ok_act = json!({
"id": "https://plu.me/undo/1", "id": "https://plu.me/undo/1",
@@ -653,16 +469,17 @@ pub(crate) mod tests {
"object": like.ap_url, "object": like.ap_url,
"type": "Undo", "type": "Undo",
}); });
assert!(super::inbox(&conn, ok_act).is_ok()); assert!(super::inbox(&r, ok_act).is_ok());
Ok(()) Ok(())
}); });
} }
#[test] #[test]
fn update_post() { fn update_post() {
let conn = db(); let r = rockets();
let conn = &*r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&conn); let (posts, users, _) = fill_database(&r);
let act = json!({ let act = json!({
"id": "https://plu.me/update/1", "id": "https://plu.me/update/1",
@@ -681,7 +498,7 @@ pub(crate) mod tests {
"type": "Update", "type": "Update",
}); });
super::inbox(&conn, act).unwrap(); super::inbox(&r, act).unwrap();
Ok(()) Ok(())
}); });
} }
+2 -5
View File
@@ -247,7 +247,6 @@ pub(crate) mod tests {
use diesel::Connection; use diesel::Connection;
pub(crate) fn fill_database(conn: &Conn) -> Vec<(NewInstance, Instance)> { pub(crate) fn fill_database(conn: &Conn) -> Vec<(NewInstance, Instance)> {
diesel::delete(instances::table).execute(conn).unwrap();
let res = vec![ let res = vec![
NewInstance { NewInstance {
default_license: "WTFPL".to_string(), default_license: "WTFPL".to_string(),
@@ -489,14 +488,12 @@ pub(crate) mod tests {
); );
assert_eq!( assert_eq!(
inst.long_description_html, inst.long_description_html,
SafeString::new( SafeString::new("<p><a href=\"/with_link\">long_description</a></p>\n")
"<p dir=\"auto\"><a href=\"/with_link\">long_description</a></p>\n"
)
); );
assert_eq!(inst.short_description.get(), "[short](#link)"); assert_eq!(inst.short_description.get(), "[short](#link)");
assert_eq!( assert_eq!(
inst.short_description_html, inst.short_description_html,
SafeString::new("<p dir=\"auto\"><a href=\"#link\">short</a></p>\n") SafeString::new("<p><a href=\"#link\">short</a></p>\n")
); );
assert_eq!(inst.default_license, "CC-BY-SAO".to_owned()); assert_eq!(inst.default_license, "CC-BY-SAO".to_owned());
Ok(()) Ok(())
Executable → Regular
+13 -20
View File
@@ -1,7 +1,6 @@
#![feature(try_trait)] #![feature(try_trait)]
#![feature(never_type)] #![feature(never_type)]
#![feature(proc_macro_hygiene)] #![feature(proc_macro_hygiene)]
#![feature(box_patterns)]
#[macro_use] #[macro_use]
extern crate diesel; extern crate diesel;
@@ -11,17 +10,14 @@ extern crate lazy_static;
extern crate plume_macro; extern crate plume_macro;
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;
#[macro_use]
extern crate serde_derive; extern crate serde_derive;
#[macro_use] #[macro_use]
extern crate serde_json; extern crate serde_json;
#[macro_use] #[macro_use]
extern crate tantivy; extern crate tantivy;
use once_cell::sync::Lazy;
use plume_common::activity_pub::inbox::InboxError; use plume_common::activity_pub::inbox::InboxError;
use posts::PostEvent;
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
use users::UserEvent;
#[cfg(not(any(feature = "sqlite", feature = "postgres")))] #[cfg(not(any(feature = "sqlite", feature = "postgres")))]
compile_error!("Either feature \"sqlite\" or \"postgres\" must be enabled for this crate."); compile_error!("Either feature \"sqlite\" or \"postgres\" must be enabled for this crate.");
@@ -34,19 +30,6 @@ pub type Connection = diesel::SqliteConnection;
#[cfg(all(not(feature = "sqlite"), feature = "postgres"))] #[cfg(all(not(feature = "sqlite"), feature = "postgres"))]
pub type Connection = diesel::PgConnection; pub type Connection = diesel::PgConnection;
pub(crate) static ACTOR_SYS: Lazy<ActorSystem> = Lazy::new(|| {
SystemBuilder::new()
.name("plume")
.create()
.expect("Failed to create actor system")
});
pub(crate) static USER_CHAN: Lazy<ChannelRef<UserEvent>> =
Lazy::new(|| channel("user_events", &*ACTOR_SYS).expect("Failed to create user channel"));
pub(crate) static POST_CHAN: Lazy<ChannelRef<PostEvent>> =
Lazy::new(|| channel("post_events", &*ACTOR_SYS).expect("Failed to create post channel"));
/// All the possible errors that can be encoutered in this crate /// All the possible errors that can be encoutered in this crate
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
@@ -298,10 +281,12 @@ pub fn ap_url(url: &str) -> String {
#[cfg(test)] #[cfg(test)]
#[macro_use] #[macro_use]
mod tests { mod tests {
use crate::{db_conn, migrations::IMPORTED_MIGRATIONS, Connection as Conn, CONFIG}; use crate::{db_conn, migrations::IMPORTED_MIGRATIONS, search, Connection as Conn, CONFIG};
use diesel::r2d2::ConnectionManager; use diesel::r2d2::ConnectionManager;
use plume_common::utils::random_hex; use plume_common::utils::random_hex;
use scheduled_thread_pool::ScheduledThreadPool;
use std::env::temp_dir; use std::env::temp_dir;
use std::sync::Arc;
#[macro_export] #[macro_export]
macro_rules! part_eq { macro_rules! part_eq {
@@ -331,6 +316,15 @@ mod tests {
pool pool
}; };
} }
pub fn rockets() -> super::PlumeRocket {
super::PlumeRocket {
conn: db_conn::DbConn((*DB_POOL).get().unwrap()),
searcher: Arc::new(search::tests::get_searcher(&CONFIG.search_tokenizers)),
worker: Arc::new(ScheduledThreadPool::new(2)),
user: None,
}
}
} }
pub mod admin; pub mod admin;
@@ -356,7 +350,6 @@ pub mod password_reset_requests;
pub mod plume_rocket; pub mod plume_rocket;
pub mod post_authors; pub mod post_authors;
pub mod posts; pub mod posts;
pub mod remote_fetch_actor;
pub mod reshares; pub mod reshares;
pub mod safe_string; pub mod safe_string;
#[allow(unused_imports)] #[allow(unused_imports)]
+25 -34
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
db_conn::DbConn, notifications::*, posts::Post, schema::likes, timeline::*, users::User, notifications::*, posts::Post, schema::likes, timeline::*, users::User, Connection, Error,
Connection, Error, Result, CONFIG, PlumeRocket, Result,
}; };
use activitypub::activity; use activitypub::activity;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
@@ -83,73 +83,64 @@ impl Like {
} }
} }
impl AsObject<User, activity::Like, &DbConn> for Post { impl AsObject<User, activity::Like, &PlumeRocket> for Post {
type Error = Error; type Error = Error;
type Output = Like; type Output = Like;
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Like> { fn activity(self, c: &PlumeRocket, actor: User, id: &str) -> Result<Like> {
let res = Like::insert( let res = Like::insert(
conn, &c.conn,
NewLike { NewLike {
post_id: self.id, post_id: self.id,
user_id: actor.id, user_id: actor.id,
ap_url: id.to_string(), ap_url: id.to_string(),
}, },
)?; )?;
res.notify(conn)?; res.notify(&c.conn)?;
Timeline::add_to_all_timelines(conn, &self, Kind::Like(&actor))?; Timeline::add_to_all_timelines(c, &self, Kind::Like(&actor))?;
Ok(res) Ok(res)
} }
} }
impl FromId<DbConn> for Like { impl FromId<PlumeRocket> for Like {
type Error = Error; type Error = Error;
type Object = activity::Like; type Object = activity::Like;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> { fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Like::find_by_ap_url(conn, id) Like::find_by_ap_url(&c.conn, id)
} }
fn from_activity(conn: &DbConn, act: activity::Like) -> Result<Self> { fn from_activity(c: &PlumeRocket, act: activity::Like) -> Result<Self> {
let res = Like::insert( let res = Like::insert(
conn, &c.conn,
NewLike { NewLike {
post_id: Post::from_id( post_id: Post::from_id(c, &act.like_props.object_link::<Id>()?, None)
conn, .map_err(|(_, e)| e)?
&act.like_props.object_link::<Id>()?, .id,
None, user_id: User::from_id(c, &act.like_props.actor_link::<Id>()?, None)
CONFIG.proxy(), .map_err(|(_, e)| e)?
) .id,
.map_err(|(_, e)| e)?
.id,
user_id: User::from_id(
conn,
&act.like_props.actor_link::<Id>()?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?
.id,
ap_url: act.object_props.id_string()?, ap_url: act.object_props.id_string()?,
}, },
)?; )?;
res.notify(conn)?; res.notify(&c.conn)?;
Ok(res) Ok(res)
} }
} }
impl AsObject<User, activity::Undo, &DbConn> for Like { impl AsObject<User, activity::Undo, &PlumeRocket> for Like {
type Error = Error; type Error = Error;
type Output = (); type Output = ();
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
let conn = &*c.conn;
if actor.id == self.user_id { if actor.id == self.user_id {
diesel::delete(&self).execute(&**conn)?; diesel::delete(&self).execute(conn)?;
// delete associated notification if any // delete associated notification if any
if let Ok(notif) = Notification::find(&conn, notification_kind::LIKE, self.id) { if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) {
diesel::delete(&notif).execute(&**conn)?; diesel::delete(&notif).execute(conn)?;
} }
Ok(()) Ok(())
} else { } else {
+9 -9
View File
@@ -30,9 +30,9 @@ impl TryFrom<i32> for ListType {
} }
} }
impl From<ListType> for i32 { impl Into<i32> for ListType {
fn from(list_type: ListType) -> Self { fn into(self) -> i32 {
match list_type { match self {
ListType::User => 0, ListType::User => 0,
ListType::Blog => 1, ListType::Blog => 1,
ListType::Word => 2, ListType::Word => 2,
@@ -246,22 +246,22 @@ impl List {
private::ListElem::prefix_in_list(conn, self, word) private::ListElem::prefix_in_list(conn, self, word)
} }
// Insert new users in a list /// Insert new users in a list
func! {add: add_users, User} func! {add: add_users, User}
// Insert new blogs in a list /// Insert new blogs in a list
func! {add: add_blogs, Blog} func! {add: add_blogs, Blog}
// Insert new words in a list /// Insert new words in a list
func! {add: add_words, Word} func! {add: add_words, Word}
// Insert new prefixes in a list /// Insert new prefixes in a list
func! {add: add_prefixes, Prefix} func! {add: add_prefixes, Prefix}
// Get all users in the list /// Get all users in the list
func! {list: list_users, User, users} func! {list: list_users, User, users}
// Get all blogs in the list /// Get all blogs in the list
func! {list: list_blogs, Blog, blogs} func! {list: list_blogs, Blog, blogs}
/// Get all words in the list /// Get all words in the list
+21 -57
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
ap_url, db_conn::DbConn, instance::Instance, safe_string::SafeString, schema::medias, ap_url, instance::Instance, safe_string::SafeString, schema::medias, users::User, Connection,
users::User, Connection, Error, Result, CONFIG, Error, PlumeRocket, Result,
}; };
use activitypub::object::Image; use activitypub::object::Image;
use askama_escape::escape; use askama_escape::escape;
@@ -10,14 +10,8 @@ use plume_common::{
activity_pub::{inbox::FromId, Id}, activity_pub::{inbox::FromId, Id},
utils::MediaProcessor, utils::MediaProcessor,
}; };
use std::{ use reqwest;
fs::{self, DirBuilder}, use std::{fs, path::Path};
path::{Path, PathBuf},
};
use tracing::warn;
use url::Url;
const REMOTE_MEDIA_DIRECTORY: &str = "remote";
#[derive(Clone, Identifiable, Queryable)] #[derive(Clone, Identifiable, Queryable)]
pub struct Media { pub struct Media {
@@ -203,28 +197,26 @@ impl Media {
} }
// TODO: merge with save_remote? // TODO: merge with save_remote?
pub fn from_activity(conn: &DbConn, image: &Image) -> Result<Media> { pub fn from_activity(c: &PlumeRocket, image: &Image) -> Result<Media> {
let conn = &*c.conn;
let remote_url = image.object_props.url_string().ok()?; let remote_url = image.object_props.url_string().ok()?;
let path = determine_mirror_file_path(&remote_url); let ext = remote_url
let parent = path.parent()?; .rsplit('.')
if !parent.is_dir() { .next()
DirBuilder::new().recursive(true).create(parent)?; .map(ToOwned::to_owned)
} .unwrap_or_else(|| String::from("png"));
let path = Path::new(&super::CONFIG.media_directory).join(format!(
"{}.{}",
GUID::rand().to_string(),
ext
));
let mut dest = fs::File::create(path.clone()).ok()?; let mut dest = fs::File::create(path.clone()).ok()?;
// TODO: conditional GET reqwest::get(remote_url.as_str())
if let Some(proxy) = CONFIG.proxy() { .ok()?
reqwest::ClientBuilder::new().proxy(proxy.clone()).build()? .copy_to(&mut dest)
} else { .ok()?;
reqwest::Client::new()
}
.get(remote_url.as_str())
.send()
.ok()?
.copy_to(&mut dest)
.ok()?;
// TODO: upsert
Media::insert( Media::insert(
conn, conn,
NewMedia { NewMedia {
@@ -235,7 +227,7 @@ impl Media {
sensitive: image.object_props.summary_string().is_ok(), sensitive: image.object_props.summary_string().is_ok(),
content_warning: image.object_props.summary_string().ok(), content_warning: image.object_props.summary_string().ok(),
owner_id: User::from_id( owner_id: User::from_id(
conn, c,
image image
.object_props .object_props
.attributed_to_link_vec::<Id>() .attributed_to_link_vec::<Id>()
@@ -244,7 +236,6 @@ impl Media {
.next()? .next()?
.as_ref(), .as_ref(),
None, None,
CONFIG.proxy(),
) )
.map_err(|(_, e)| e)? .map_err(|(_, e)| e)?
.id, .id,
@@ -266,33 +257,6 @@ impl Media {
} }
} }
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().to_string(), ext));
});
file_path
}
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use super::*; use super::*;
+4 -4
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
comments::Comment, db_conn::DbConn, notifications::*, posts::Post, schema::mentions, comments::Comment, notifications::*, posts::Post, schema::mentions, users::User, Connection,
users::User, Connection, Error, Result, Error, PlumeRocket, Result,
}; };
use activitypub::link; use activitypub::link;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
@@ -52,8 +52,8 @@ impl Mention {
} }
} }
pub fn build_activity(conn: &DbConn, ment: &str) -> Result<link::Mention> { pub fn build_activity(c: &PlumeRocket, ment: &str) -> Result<link::Mention> {
let user = User::find_by_fqn(conn, ment)?; let user = User::find_by_fqn(c, ment)?;
let mut mention = link::Mention::default(); let mut mention = link::Mention::default();
mention.link_props.set_href_string(user.ap_url)?; mention.link_props.set_href_string(user.ap_url)?;
mention.link_props.set_name_string(format!("@{}", ment))?; mention.link_props.set_name_string(format!("@{}", ment))?;
+2 -3
View File
@@ -2,7 +2,6 @@ use crate::{Connection, Error, Result};
use diesel::connection::{Connection as Conn, SimpleConnection}; use diesel::connection::{Connection as Conn, SimpleConnection};
use migrations_internals::{setup_database, MigrationConnection}; use migrations_internals::{setup_database, MigrationConnection};
use std::path::Path; use std::path::Path;
use tracing::info;
#[allow(dead_code)] //variants might not be constructed if not required by current migrations #[allow(dead_code)] //variants might not be constructed if not required by current migrations
enum Action { enum Action {
@@ -27,7 +26,7 @@ struct ComplexMigration {
impl ComplexMigration { impl ComplexMigration {
fn run(&self, conn: &Connection, path: &Path) -> Result<()> { fn run(&self, conn: &Connection, path: &Path) -> Result<()> {
info!("Running migration {}", self.name); println!("Running migration {}", self.name);
for step in self.up { for step in self.up {
step.run(conn, path)? step.run(conn, path)?
} }
@@ -35,7 +34,7 @@ impl ComplexMigration {
} }
fn revert(&self, conn: &Connection, path: &Path) -> Result<()> { fn revert(&self, conn: &Connection, path: &Path) -> Result<()> {
info!("Reverting migration {}", self.name); println!("Reverting migration {}", self.name);
for step in self.down { for step in self.down {
step.run(conn, path)? step.run(conn, path)?
} }
+8 -2
View File
@@ -2,7 +2,7 @@ pub use self::module::PlumeRocket;
#[cfg(not(test))] #[cfg(not(test))]
mod module { mod module {
use crate::{search, users}; use crate::{db_conn::DbConn, search, users};
use rocket::{ use rocket::{
request::{self, FlashMessage, FromRequest, Request}, request::{self, FlashMessage, FromRequest, Request},
Outcome, State, Outcome, State,
@@ -12,6 +12,7 @@ mod module {
/// Common context needed by most routes and operations on models /// Common context needed by most routes and operations on models
pub struct PlumeRocket { pub struct PlumeRocket {
pub conn: DbConn,
pub intl: rocket_i18n::I18n, pub intl: rocket_i18n::I18n,
pub user: Option<users::User>, pub user: Option<users::User>,
pub searcher: Arc<search::Searcher>, pub searcher: Arc<search::Searcher>,
@@ -23,12 +24,14 @@ mod module {
type Error = (); type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<PlumeRocket, ()> { fn from_request(request: &'a Request<'r>) -> request::Outcome<PlumeRocket, ()> {
let conn = request.guard::<DbConn>()?;
let intl = request.guard::<rocket_i18n::I18n>()?; let intl = request.guard::<rocket_i18n::I18n>()?;
let user = request.guard::<users::User>().succeeded(); let user = request.guard::<users::User>().succeeded();
let worker = request.guard::<'_, State<'_, Arc<ScheduledThreadPool>>>()?; let worker = request.guard::<'_, State<'_, Arc<ScheduledThreadPool>>>()?;
let searcher = request.guard::<'_, State<'_, Arc<search::Searcher>>>()?; let searcher = request.guard::<'_, State<'_, Arc<search::Searcher>>>()?;
let flash_msg = request.guard::<FlashMessage<'_, '_>>().succeeded(); let flash_msg = request.guard::<FlashMessage<'_, '_>>().succeeded();
Outcome::Success(PlumeRocket { Outcome::Success(PlumeRocket {
conn,
intl, intl,
user, user,
flash_msg: flash_msg.map(|f| (f.name().into(), f.msg().into())), flash_msg: flash_msg.map(|f| (f.name().into(), f.msg().into())),
@@ -41,7 +44,7 @@ mod module {
#[cfg(test)] #[cfg(test)]
mod module { mod module {
use crate::{search, users}; use crate::{db_conn::DbConn, search, users};
use rocket::{ use rocket::{
request::{self, FromRequest, Request}, request::{self, FromRequest, Request},
Outcome, State, Outcome, State,
@@ -51,6 +54,7 @@ mod module {
/// Common context needed by most routes and operations on models /// Common context needed by most routes and operations on models
pub struct PlumeRocket { pub struct PlumeRocket {
pub conn: DbConn,
pub user: Option<users::User>, pub user: Option<users::User>,
pub searcher: Arc<search::Searcher>, pub searcher: Arc<search::Searcher>,
pub worker: Arc<ScheduledThreadPool>, pub worker: Arc<ScheduledThreadPool>,
@@ -60,10 +64,12 @@ mod module {
type Error = (); type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<PlumeRocket, ()> { fn from_request(request: &'a Request<'r>) -> request::Outcome<PlumeRocket, ()> {
let conn = request.guard::<DbConn>()?;
let user = request.guard::<users::User>().succeeded(); let user = request.guard::<users::User>().succeeded();
let worker = request.guard::<'_, State<'_, Arc<ScheduledThreadPool>>>()?; let worker = request.guard::<'_, State<'_, Arc<ScheduledThreadPool>>>()?;
let searcher = request.guard::<'_, State<'_, Arc<search::Searcher>>>()?; let searcher = request.guard::<'_, State<'_, Arc<search::Searcher>>>()?;
Outcome::Success(PlumeRocket { Outcome::Success(PlumeRocket {
conn,
user, user,
worker: worker.clone(), worker: worker.clone(),
searcher: searcher.clone(), searcher: searcher.clone(),
+63 -135
View File
@@ -1,7 +1,7 @@
use crate::{ use crate::{
ap_url, blogs::Blog, db_conn::DbConn, instance::Instance, medias::Media, mentions::Mention, ap_url, blogs::Blog, instance::Instance, medias::Media, mentions::Mention, post_authors::*,
post_authors::*, safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User, safe_string::SafeString, schema::posts, search::Searcher, tags::*, timeline::*, users::User,
Connection, Error, PostEvent::*, Result, CONFIG, POST_CHAN, Connection, Error, PlumeRocket, Result, CONFIG,
}; };
use activitypub::{ use activitypub::{
activity::{Create, Delete, Update}, activity::{Create, Delete, Update},
@@ -11,8 +11,7 @@ use activitypub::{
}; };
use chrono::{NaiveDateTime, TimeZone, Utc}; use chrono::{NaiveDateTime, TimeZone, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use heck::KebabCase; use heck::{CamelCase, KebabCase};
use once_cell::sync::Lazy;
use plume_common::{ use plume_common::{
activity_pub::{ activity_pub::{
inbox::{AsObject, FromId}, inbox::{AsObject, FromId},
@@ -20,15 +19,12 @@ use plume_common::{
}, },
utils::md_to_html, utils::md_to_html,
}; };
use riker::actors::{Publish, Tell}; use serde_json;
use std::collections::{HashMap, HashSet}; use std::collections::HashSet;
use std::sync::{Arc, Mutex};
pub type LicensedArticle = CustomObject<Licensed, Article>; pub type LicensedArticle = CustomObject<Licensed, Article>;
static BLOG_FQN_CACHE: Lazy<Mutex<HashMap<i32, String>>> = Lazy::new(|| Mutex::new(HashMap::new())); #[derive(Queryable, Identifiable, Clone, AsChangeset)]
#[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)]
#[changeset_options(treat_none_as_null = "true")] #[changeset_options(treat_none_as_null = "true")]
pub struct Post { pub struct Post {
pub id: i32, pub id: i32,
@@ -67,7 +63,7 @@ impl Post {
find_by!(posts, find_by_ap_url, ap_url as &str); find_by!(posts, find_by_ap_url, ap_url as &str);
last!(posts); last!(posts);
pub fn insert(conn: &Connection, new: NewPost) -> Result<Self> { pub fn insert(conn: &Connection, new: NewPost, searcher: &Searcher) -> Result<Self> {
diesel::insert_into(posts::table) diesel::insert_into(posts::table)
.values(new) .values(new)
.execute(conn)?; .execute(conn)?;
@@ -82,29 +78,23 @@ impl Post {
let _: Post = post.save_changes(conn)?; let _: Post = post.save_changes(conn)?;
} }
if post.published { searcher.add_document(conn, &post)?;
post.publish_published();
}
Ok(post) Ok(post)
} }
pub fn update(&self, conn: &Connection) -> Result<Self> { pub fn update(&self, conn: &Connection, searcher: &Searcher) -> Result<Self> {
diesel::update(self).set(self).execute(conn)?; diesel::update(self).set(self).execute(conn)?;
let post = Self::get(conn, self.id)?; let post = Self::get(conn, self.id)?;
// TODO: Call publish_published() when newly published searcher.update_document(conn, &post)?;
if post.published {
self.publish_updated();
}
Ok(post) Ok(post)
} }
pub fn delete(&self, conn: &Connection) -> Result<()> { pub fn delete(&self, conn: &Connection, searcher: &Searcher) -> Result<()> {
for m in Mention::list_for_post(&conn, self.id)? { for m in Mention::list_for_post(&conn, self.id)? {
m.delete(conn)?; m.delete(conn)?;
} }
diesel::delete(self).execute(conn)?; diesel::delete(self).execute(conn)?;
self.publish_deleted(); searcher.delete_document(self);
Ok(()) Ok(())
} }
@@ -134,7 +124,8 @@ impl Post {
.filter(posts::published.eq(true)) .filter(posts::published.eq(true))
.count() .count()
.load(conn)? .load(conn)?
.get(0) .iter()
.next()
.cloned() .cloned()
.ok_or(Error::NotFound) .ok_or(Error::NotFound)
} }
@@ -278,24 +269,6 @@ impl Post {
.map_err(Error::from) .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<i64> { pub fn count_likes(&self, conn: &Connection) -> Result<i64> {
use crate::schema::likes; use crate::schema::likes;
likes::table likes::table
@@ -315,16 +288,17 @@ impl Post {
} }
pub fn get_receivers_urls(&self, conn: &Connection) -> Result<Vec<String>> { pub fn get_receivers_urls(&self, conn: &Connection) -> Result<Vec<String>> {
Ok(self let followers = self
.get_authors(conn)? .get_authors(conn)?
.into_iter() .into_iter()
.filter_map(|a| a.get_followers(conn).ok()) .filter_map(|a| a.get_followers(conn).ok())
.fold(vec![], |mut acc, f| { .collect::<Vec<Vec<User>>>();
for x in f { Ok(followers.into_iter().fold(vec![], |mut acc, f| {
acc.push(x.ap_url); for x in f {
} acc.push(x.ap_url);
acc }
})) acc
}))
} }
pub fn to_activity(&self, conn: &Connection) -> Result<LicensedArticle> { pub fn to_activity(&self, conn: &Connection) -> Result<LicensedArticle> {
@@ -574,48 +548,19 @@ impl Post {
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?; .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?;
Ok(act) 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<DbConn> for Post { impl FromId<PlumeRocket> for Post {
type Error = Error; type Error = Error;
type Object = LicensedArticle; type Object = LicensedArticle;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> { fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Self::find_by_ap_url(conn, id) Self::find_by_ap_url(&c.conn, id)
} }
fn from_activity(conn: &DbConn, article: LicensedArticle) -> Result<Self> { fn from_activity(c: &PlumeRocket, article: LicensedArticle) -> Result<Self> {
let conn = conn; let conn = &*c.conn;
let searcher = &c.searcher;
let license = article.custom_props.license_string().unwrap_or_default(); let license = article.custom_props.license_string().unwrap_or_default();
let article = article.object; let article = article.object;
@@ -625,15 +570,12 @@ impl FromId<DbConn> for Post {
.into_iter() .into_iter()
.fold((None, vec![]), |(blog, mut authors), link| { .fold((None, vec![]), |(blog, mut authors), link| {
let url = link; let url = link;
match User::from_id(conn, &url, None, CONFIG.proxy()) { match User::from_id(&c, &url, None) {
Ok(u) => { Ok(u) => {
authors.push(u); authors.push(u);
(blog, authors) (blog, authors)
} }
Err(_) => ( Err(_) => (blog.or_else(|| Blog::from_id(&c, &url, None).ok()), authors),
blog.or_else(|| Blog::from_id(conn, &url, None, CONFIG.proxy()).ok()),
authors,
),
} }
}); });
@@ -641,10 +583,9 @@ impl FromId<DbConn> for Post {
.object_props .object_props
.icon_object::<Image>() .icon_object::<Image>()
.ok() .ok()
.and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id)); .and_then(|img| Media::from_activity(&c, &img).ok().map(|m| m.id));
let title = article.object_props.name_string()?; let title = article.object_props.name_string()?;
// TODO: upsert
let post = Post::insert( let post = Post::insert(
conn, conn,
NewPost { NewPost {
@@ -664,6 +605,7 @@ impl FromId<DbConn> for Post {
source: article.ap_object_props.source_object::<Source>()?.content, source: article.ap_object_props.source_object::<Source>()?.content,
cover_id: cover, cover_id: cover,
}, },
searcher,
)?; )?;
for author in authors { for author in authors {
@@ -680,6 +622,7 @@ impl FromId<DbConn> for Post {
let mut hashtags = md_to_html(&post.source, None, false, None) let mut hashtags = md_to_html(&post.source, None, false, None)
.2 .2
.into_iter() .into_iter()
.map(|s| s.to_camel_case())
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
if let Some(serde_json::Value::Array(tags)) = article.object_props.tag { if let Some(serde_json::Value::Array(tags)) = article.object_props.tag {
for tag in tags { for tag in tags {
@@ -702,33 +645,33 @@ impl FromId<DbConn> for Post {
} }
} }
Timeline::add_to_all_timelines(conn, &post, Kind::Original)?; Timeline::add_to_all_timelines(c, &post, Kind::Original)?;
Ok(post) Ok(post)
} }
} }
impl AsObject<User, Create, &DbConn> for Post { impl AsObject<User, Create, &PlumeRocket> for Post {
type Error = Error; type Error = Error;
type Output = Post; type Output = Post;
fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result<Post> { fn activity(self, _c: &PlumeRocket, _actor: User, _id: &str) -> Result<Post> {
// TODO: check that _actor is actually one of the author? // TODO: check that _actor is actually one of the author?
Ok(self) Ok(self)
} }
} }
impl AsObject<User, Delete, &DbConn> for Post { impl AsObject<User, Delete, &PlumeRocket> for Post {
type Error = Error; type Error = Error;
type Output = (); type Output = ();
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
let can_delete = self let can_delete = self
.get_authors(conn)? .get_authors(&c.conn)?
.into_iter() .into_iter()
.any(|a| actor.id == a.id); .any(|a| actor.id == a.id);
if can_delete { if can_delete {
self.delete(conn).map(|_| ()) self.delete(&c.conn, &c.searcher).map(|_| ())
} else { } else {
Err(Error::Unauthorized) Err(Error::Unauthorized)
} }
@@ -746,16 +689,16 @@ pub struct PostUpdate {
pub tags: Option<serde_json::Value>, pub tags: Option<serde_json::Value>,
} }
impl FromId<DbConn> for PostUpdate { impl FromId<PlumeRocket> for PostUpdate {
type Error = Error; type Error = Error;
type Object = LicensedArticle; type Object = LicensedArticle;
fn from_db(_: &DbConn, _: &str) -> Result<Self> { fn from_db(_: &PlumeRocket, _: &str) -> Result<Self> {
// Always fail because we always want to deserialize the AP object // Always fail because we always want to deserialize the AP object
Err(Error::NotFound) Err(Error::NotFound)
} }
fn from_activity(conn: &DbConn, updated: LicensedArticle) -> Result<Self> { fn from_activity(c: &PlumeRocket, updated: LicensedArticle) -> Result<Self> {
Ok(PostUpdate { Ok(PostUpdate {
ap_url: updated.object.object_props.id_string()?, ap_url: updated.object.object_props.id_string()?,
title: updated.object.object_props.name_string().ok(), title: updated.object.object_props.name_string().ok(),
@@ -766,7 +709,7 @@ impl FromId<DbConn> for PostUpdate {
.object_props .object_props
.icon_object::<Image>() .icon_object::<Image>()
.ok() .ok()
.and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id)), .and_then(|img| Media::from_activity(&c, &img).ok().map(|m| m.id)),
source: updated source: updated
.object .object
.ap_object_props .ap_object_props
@@ -779,13 +722,14 @@ impl FromId<DbConn> for PostUpdate {
} }
} }
impl AsObject<User, Update, &DbConn> for PostUpdate { impl AsObject<User, Update, &PlumeRocket> for PostUpdate {
type Error = Error; type Error = Error;
type Output = (); type Output = ();
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
let mut post = let conn = &*c.conn;
Post::from_id(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?; let searcher = &c.searcher;
let mut post = Post::from_id(c, &self.ap_url, None).map_err(|(_, e)| e)?;
if !post.is_author(conn, actor.id)? { if !post.is_author(conn, actor.id)? {
// TODO: maybe the author was added in the meantime // TODO: maybe the author was added in the meantime
@@ -818,6 +762,7 @@ impl AsObject<User, Update, &DbConn> for PostUpdate {
let mut txt_hashtags = md_to_html(&post.source, None, false, None) let mut txt_hashtags = md_to_html(&post.source, None, false, None)
.2 .2
.into_iter() .into_iter()
.map(|s| s.to_camel_case())
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
if let Some(serde_json::Value::Array(mention_tags)) = self.tags { if let Some(serde_json::Value::Array(mention_tags)) = self.tags {
let mut mentions = vec![]; let mut mentions = vec![];
@@ -846,7 +791,7 @@ impl AsObject<User, Update, &DbConn> for PostUpdate {
post.update_hashtags(conn, hashtags)?; post.update_hashtags(conn, hashtags)?;
} }
post.update(conn)?; post.update(conn, searcher)?;
Ok(()) Ok(())
} }
} }
@@ -857,42 +802,24 @@ impl IntoId for Post {
} }
} }
#[derive(Clone, Debug)]
pub enum PostEvent {
PostPublished(Arc<Post>),
PostUpdated(Arc<Post>),
PostDeleted(Arc<Post>),
}
impl From<PostEvent> for Arc<Post> {
fn from(event: PostEvent) -> Self {
use PostEvent::*;
match event {
PostPublished(post) => post,
PostUpdated(post) => post,
PostDeleted(post) => post,
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::inbox::{inbox, tests::fill_database, InboxResult}; use crate::inbox::{inbox, tests::fill_database, InboxResult};
use crate::safe_string::SafeString; use crate::safe_string::SafeString;
use crate::tests::db; use crate::tests::rockets;
use diesel::Connection; use diesel::Connection;
// creates a post, get it's Create activity, delete the post, // creates a post, get it's Create activity, delete the post,
// "send" the Create to the inbox, and check it works // "send" the Create to the inbox, and check it works
#[test] #[test]
fn self_federation() { fn self_federation() {
let conn = &db(); let r = rockets();
let conn = &*r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (_, users, blogs) = fill_database(&conn); let (_, users, blogs) = fill_database(&r);
let post = Post::insert( let post = Post::insert(
&conn, conn,
NewPost { NewPost {
blog_id: blogs[0].id, blog_id: blogs[0].id,
slug: "yo".into(), slug: "yo".into(),
@@ -906,22 +833,23 @@ mod tests {
source: "Hello".into(), source: "Hello".into(),
cover_id: None, cover_id: None,
}, },
&r.searcher,
) )
.unwrap(); .unwrap();
PostAuthor::insert( PostAuthor::insert(
&conn, conn,
NewPostAuthor { NewPostAuthor {
post_id: post.id, post_id: post.id,
author_id: users[0].id, author_id: users[0].id,
}, },
) )
.unwrap(); .unwrap();
let create = post.create_activity(&conn).unwrap(); let create = post.create_activity(conn).unwrap();
post.delete(&conn).unwrap(); post.delete(conn, &r.searcher).unwrap();
match inbox(&conn, serde_json::to_value(create).unwrap()).unwrap() { match inbox(&r, serde_json::to_value(create).unwrap()).unwrap() {
InboxResult::Post(p) => { InboxResult::Post(p) => {
assert!(p.is_author(&conn, users[0].id).unwrap()); assert!(p.is_author(conn, users[0].id).unwrap());
assert_eq!(p.source, "Hello".to_owned()); assert_eq!(p.source, "Hello".to_owned());
assert_eq!(p.blog_id, blogs[0].id); assert_eq!(p.blog_id, blogs[0].id);
assert_eq!(p.content, SafeString::new("Hello")); assert_eq!(p.content, SafeString::new("Hello"));
-125
View File
@@ -1,125 +0,0 @@
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) {
ACTOR_SYS
.actor_of_args::<RemoteFetchActor, _>("remote-fetch", conn)
.expect("Failed to initialize remote fetch actor");
}
}
impl Actor for RemoteFetchActor {
type Msg = UserEvent;
fn pre_start(&mut self, ctx: &Context<Self::Msg>) {
USER_CHAN.tell(
Subscribe {
actor: Box::new(ctx.myself()),
topic: "*".into(),
},
None,
)
}
fn recv(&mut self, _ctx: &Context<Self::Msg>, 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<DbPool> for RemoteFetchActor {
fn create_args(conn: DbPool) -> Self {
Self { conn }
}
}
fn fetch_and_cache_articles(user: &Arc<User>, conn: &DbConn) {
let create_acts = user.fetch_outbox::<Create>();
match create_acts {
Ok(create_acts) => {
for create_act in create_acts {
match create_act.create_props.object_object::<LicensedArticle>() {
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<User>, 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<User>, conn: &DbConn) {
if user.refetch(conn).is_err() {
error!("Couldn't update user info: {:?}", user);
}
}
+23 -32
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
db_conn::DbConn, notifications::*, posts::Post, schema::reshares, timeline::*, users::User, notifications::*, posts::Post, schema::reshares, timeline::*, users::User, Connection, Error,
Connection, Error, Result, CONFIG, PlumeRocket, Result,
}; };
use activitypub::activity::{Announce, Undo}; use activitypub::activity::{Announce, Undo};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
@@ -107,12 +107,12 @@ impl Reshare {
} }
} }
impl AsObject<User, Announce, &DbConn> for Post { impl AsObject<User, Announce, &PlumeRocket> for Post {
type Error = Error; type Error = Error;
type Output = Reshare; type Output = Reshare;
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Reshare> { fn activity(self, c: &PlumeRocket, actor: User, id: &str) -> Result<Reshare> {
let conn = conn; let conn = &*c.conn;
let reshare = Reshare::insert( let reshare = Reshare::insert(
conn, conn,
NewReshare { NewReshare {
@@ -123,58 +123,49 @@ impl AsObject<User, Announce, &DbConn> for Post {
)?; )?;
reshare.notify(conn)?; reshare.notify(conn)?;
Timeline::add_to_all_timelines(conn, &self, Kind::Reshare(&actor))?; Timeline::add_to_all_timelines(c, &self, Kind::Reshare(&actor))?;
Ok(reshare) Ok(reshare)
} }
} }
impl FromId<DbConn> for Reshare { impl FromId<PlumeRocket> for Reshare {
type Error = Error; type Error = Error;
type Object = Announce; type Object = Announce;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> { fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Reshare::find_by_ap_url(conn, id) Reshare::find_by_ap_url(&c.conn, id)
} }
fn from_activity(conn: &DbConn, act: Announce) -> Result<Self> { fn from_activity(c: &PlumeRocket, act: Announce) -> Result<Self> {
let res = Reshare::insert( let res = Reshare::insert(
conn, &c.conn,
NewReshare { NewReshare {
post_id: Post::from_id( post_id: Post::from_id(c, &act.announce_props.object_link::<Id>()?, None)
conn, .map_err(|(_, e)| e)?
&act.announce_props.object_link::<Id>()?, .id,
None, user_id: User::from_id(c, &act.announce_props.actor_link::<Id>()?, None)
CONFIG.proxy(), .map_err(|(_, e)| e)?
) .id,
.map_err(|(_, e)| e)?
.id,
user_id: User::from_id(
conn,
&act.announce_props.actor_link::<Id>()?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?
.id,
ap_url: act.object_props.id_string()?, ap_url: act.object_props.id_string()?,
}, },
)?; )?;
res.notify(conn)?; res.notify(&c.conn)?;
Ok(res) Ok(res)
} }
} }
impl AsObject<User, Undo, &DbConn> for Reshare { impl AsObject<User, Undo, &PlumeRocket> for Reshare {
type Error = Error; type Error = Error;
type Output = (); type Output = ();
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
let conn = &*c.conn;
if actor.id == self.user_id { if actor.id == self.user_id {
diesel::delete(&self).execute(&**conn)?; diesel::delete(&self).execute(conn)?;
// delete associated notification if any // delete associated notification if any
if let Ok(notif) = Notification::find(&conn, notification_kind::RESHARE, self.id) { if let Ok(notif) = Notification::find(&conn, notification_kind::RESHARE, self.id) {
diesel::delete(&notif).execute(&**conn)?; diesel::delete(&notif).execute(conn)?;
} }
Ok(()) Ok(())
+2 -2
View File
@@ -11,13 +11,14 @@ use std::{
borrow::{Borrow, Cow}, borrow::{Borrow, Cow},
fmt::{self, Display}, fmt::{self, Display},
io::Write, io::Write,
iter,
ops::Deref, ops::Deref,
}; };
lazy_static! { lazy_static! {
static ref CLEAN: Builder<'static> = { static ref CLEAN: Builder<'static> = {
let mut b = Builder::new(); let mut b = Builder::new();
b.add_generic_attributes(&["id", "dir"]) b.add_generic_attributes(iter::once("id"))
.add_tags(&["iframe", "video", "audio", "label", "input"]) .add_tags(&["iframe", "video", "audio", "label", "input"])
.id_prefix(Some("postcontent-")) .id_prefix(Some("postcontent-"))
.url_relative(UrlRelative::Custom(Box::new(url_add_prefix))) .url_relative(UrlRelative::Custom(Box::new(url_add_prefix)))
@@ -81,7 +82,6 @@ lazy_static! {
}; };
} }
#[allow(clippy::unnecessary_wraps)]
fn url_add_prefix(url: &str) -> Option<Cow<'_, str>> { fn url_add_prefix(url: &str) -> Option<Cow<'_, str>> {
if url.starts_with('#') && !url.starts_with("#postcontent-") { if url.starts_with('#') && !url.starts_with("#postcontent-") {
//if start with an # //if start with an #
-216
View File
@@ -1,216 +0,0 @@
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<Searcher>,
conn: DbPool,
}
impl SearchActor {
pub fn init(searcher: Arc<Searcher>, conn: DbPool) {
ACTOR_SYS
.actor_of_args::<SearchActor, _>("search", (searcher, conn))
.expect("Failed to initialize searcher actor");
}
}
impl Actor for SearchActor {
type Msg = PostEvent;
fn pre_start(&mut self, ctx: &Context<Self::Msg>) {
POST_CHAN.tell(
Subscribe {
actor: Box::new(ctx.myself()),
topic: "*".into(),
},
None,
)
}
fn recv(&mut self, _ctx: &Context<Self::Msg>, 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<Searcher>, DbPool)> for SearchActor {
fn create_args((searcher, conn): (Arc<Searcher>, 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::<Conn>::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: "<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();
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()
}
}
+11 -10
View File
@@ -1,4 +1,3 @@
pub mod actor;
mod query; mod query;
mod searcher; mod searcher;
mod tokenizer; mod tokenizer;
@@ -8,7 +7,12 @@ pub use self::tokenizer::TokenizerKind;
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use super::{Query, Searcher}; use super::{Query, Searcher, TokenizerKind};
use diesel::Connection;
use plume_common::utils::random_hex;
use std::env::temp_dir;
use std::str::FromStr;
use crate::{ use crate::{
blogs::tests::fill_database, blogs::tests::fill_database,
config::SearchTokenizerConfig, config::SearchTokenizerConfig,
@@ -18,10 +22,6 @@ pub(crate) mod tests {
tests::db, tests::db,
CONFIG, 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 { pub(crate) fn get_searcher(tokenizers: &SearchTokenizerConfig) -> Searcher {
let dir = temp_dir().join(&format!("plume-test-{}", random_hex())); let dir = temp_dir().join(&format!("plume-test-{}", random_hex()));
@@ -144,6 +144,7 @@ pub(crate) mod tests {
source: "".to_owned(), source: "".to_owned(),
cover_id: None, cover_id: None,
}, },
&searcher,
) )
.unwrap(); .unwrap();
PostAuthor::insert( PostAuthor::insert(
@@ -154,7 +155,7 @@ pub(crate) mod tests {
}, },
) )
.unwrap(); .unwrap();
searcher.add_document(&conn, &post).unwrap();
searcher.commit(); searcher.commit();
assert_eq!( assert_eq!(
searcher.search_document(conn, Query::from_str(&title).unwrap(), (0, 1))[0].id, searcher.search_document(conn, Query::from_str(&title).unwrap(), (0, 1))[0].id,
@@ -163,8 +164,7 @@ pub(crate) mod tests {
let newtitle = random_hex()[..8].to_owned(); let newtitle = random_hex()[..8].to_owned();
post.title = newtitle.clone(); post.title = newtitle.clone();
post.update(conn).unwrap(); post.update(conn, &searcher).unwrap();
searcher.update_document(conn, &post).unwrap();
searcher.commit(); searcher.commit();
assert_eq!( assert_eq!(
searcher.search_document(conn, Query::from_str(&newtitle).unwrap(), (0, 1))[0].id, searcher.search_document(conn, Query::from_str(&newtitle).unwrap(), (0, 1))[0].id,
@@ -174,7 +174,7 @@ pub(crate) mod tests {
.search_document(conn, Query::from_str(&title).unwrap(), (0, 1)) .search_document(conn, Query::from_str(&title).unwrap(), (0, 1))
.is_empty()); .is_empty());
searcher.delete_document(&post); post.delete(conn, &searcher).unwrap();
searcher.commit(); searcher.commit();
assert!(searcher assert!(searcher
.search_document(conn, Query::from_str(&newtitle).unwrap(), (0, 1)) .search_document(conn, Query::from_str(&newtitle).unwrap(), (0, 1))
@@ -213,6 +213,7 @@ pub(crate) mod tests {
source: "".to_owned(), source: "".to_owned(),
cover_id: None, cover_id: None,
}, },
&searcher,
) )
.unwrap(); .unwrap();
+5 -72
View File
@@ -1,17 +1,15 @@
use crate::{ use crate::{
config::SearchTokenizerConfig, instance::Instance, posts::Post, schema::posts, config::SearchTokenizerConfig, instance::Instance, posts::Post, schema::posts,
search::query::PlumeQuery, tags::Tag, Connection, Error, Result, search::query::PlumeQuery, tags::Tag, Connection, Result,
}; };
use chrono::{Datelike, Utc}; use chrono::Datelike;
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use itertools::Itertools; use itertools::Itertools;
use std::fs; use std::{cmp, fs::create_dir_all, path::Path, sync::Mutex};
use std::{cmp, fs::create_dir_all, io, path::Path, sync::Mutex};
use tantivy::{ use tantivy::{
collector::TopDocs, directory::MmapDirectory, schema::*, Index, IndexReader, IndexWriter, collector::TopDocs, directory::MmapDirectory, schema::*, Index, IndexReader, IndexWriter,
ReloadPolicy, TantivyError, Term, ReloadPolicy, Term,
}; };
use tracing::warn;
use whatlang::{detect as detect_lang, Lang}; use whatlang::{detect as detect_lang, Lang};
#[derive(Debug)] #[derive(Debug)]
@@ -20,7 +18,6 @@ pub enum SearcherError {
WriteLockAcquisitionError, WriteLockAcquisitionError,
IndexOpeningError, IndexOpeningError,
IndexEditionError, IndexEditionError,
InvalidIndexDataError,
} }
pub struct Searcher { pub struct Searcher {
@@ -69,58 +66,6 @@ impl Searcher {
schema_builder.build() schema_builder.build()
} }
pub fn open_or_recreate(path: &dyn AsRef<Path>, 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<Path>, tokenizers: &SearchTokenizerConfig) -> Result<Self> { pub fn create(path: &dyn AsRef<Path>, tokenizers: &SearchTokenizerConfig) -> Result<Self> {
let schema = Self::schema(); let schema = Self::schema();
@@ -190,19 +135,7 @@ Then try to restart Plume
.reader_builder() .reader_builder()
.reload_policy(ReloadPolicy::Manual) .reload_policy(ReloadPolicy::Manual)
.try_into() .try_into()
.map_err(|e| { .map_err(|_| SearcherError::IndexCreationError)?,
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, index,
}) })
} }
+135 -134
View File
@@ -1,9 +1,8 @@
use crate::{ use crate::{
db_conn::DbConn,
lists::List, lists::List,
posts::Post, posts::Post,
schema::{posts, timeline, timeline_definition}, schema::{posts, timeline, timeline_definition},
Connection, Error, Result, Connection, Error, PlumeRocket, Result,
}; };
use diesel::{self, BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl};
use std::ops::Deref; use std::ops::Deref;
@@ -209,14 +208,14 @@ impl Timeline {
.map_err(Error::from) .map_err(Error::from)
} }
pub fn add_to_all_timelines(conn: &DbConn, post: &Post, kind: Kind<'_>) -> Result<()> { pub fn add_to_all_timelines(rocket: &PlumeRocket, post: &Post, kind: Kind<'_>) -> Result<()> {
let timelines = timeline_definition::table let timelines = timeline_definition::table
.load::<Self>(conn.deref()) .load::<Self>(rocket.conn.deref())
.map_err(Error::from)?; .map_err(Error::from)?;
for t in timelines { for t in timelines {
if t.matches(conn, post, kind)? { if t.matches(rocket, post, kind)? {
t.add_post(conn, post)?; t.add_post(&rocket.conn, post)?;
} }
} }
Ok(()) Ok(())
@@ -232,9 +231,9 @@ impl Timeline {
Ok(()) Ok(())
} }
pub fn matches(&self, conn: &DbConn, post: &Post, kind: Kind<'_>) -> Result<bool> { pub fn matches(&self, rocket: &PlumeRocket, post: &Post, kind: Kind<'_>) -> Result<bool> {
let query = TimelineQuery::parse(&self.query)?; let query = TimelineQuery::parse(&self.query)?;
query.matches(conn, self, post, kind) query.matches(rocket, self, post, kind)
} }
} }
@@ -249,7 +248,7 @@ mod tests {
posts::NewPost, posts::NewPost,
safe_string::SafeString, safe_string::SafeString,
tags::Tag, tags::Tag,
tests::db, tests::{db, rockets},
users::tests as userTests, users::tests as userTests,
}; };
use diesel::Connection; use diesel::Connection;
@@ -258,69 +257,63 @@ mod tests {
fn test_timeline() { fn test_timeline() {
let conn = &db(); let conn = &db();
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let users = userTests::fill_database(&conn); let users = userTests::fill_database(conn);
let mut tl1_u1 = Timeline::new_for_user( let mut tl1_u1 = Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"my timeline".to_owned(), "my timeline".to_owned(),
"all".to_owned(), "all".to_owned(),
) )
.unwrap(); .unwrap();
List::new( List::new(conn, "languages I speak", Some(&users[1]), ListType::Prefix).unwrap();
&conn,
"languages I speak",
Some(&users[1]),
ListType::Prefix,
)
.unwrap();
let tl2_u1 = Timeline::new_for_user( let tl2_u1 = Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"another timeline".to_owned(), "another timeline".to_owned(),
"followed".to_owned(), "followed".to_owned(),
) )
.unwrap(); .unwrap();
let tl1_u2 = Timeline::new_for_user( let tl1_u2 = Timeline::new_for_user(
&conn, conn,
users[1].id, users[1].id,
"english posts".to_owned(), "english posts".to_owned(),
"lang in \"languages I speak\"".to_owned(), "lang in \"languages I speak\"".to_owned(),
) )
.unwrap(); .unwrap();
let tl1_instance = Timeline::new_for_instance( let tl1_instance = Timeline::new_for_instance(
&conn, conn,
"english posts".to_owned(), "english posts".to_owned(),
"license in [cc]".to_owned(), "license in [cc]".to_owned(),
) )
.unwrap(); .unwrap();
assert_eq!(tl1_u1, Timeline::get(&conn, tl1_u1.id).unwrap()); assert_eq!(tl1_u1, Timeline::get(conn, tl1_u1.id).unwrap());
assert_eq!( assert_eq!(
tl2_u1, tl2_u1,
Timeline::find_for_user_by_name(&conn, Some(users[0].id), "another timeline") Timeline::find_for_user_by_name(conn, Some(users[0].id), "another timeline")
.unwrap() .unwrap()
); );
assert_eq!( assert_eq!(
tl1_instance, tl1_instance,
Timeline::find_for_user_by_name(&conn, None, "english posts").unwrap() Timeline::find_for_user_by_name(conn, None, "english posts").unwrap()
); );
let tl_u1 = Timeline::list_for_user(&conn, Some(users[0].id)).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_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 == tl1_u1 }));
assert!(tl_u1.iter().fold(false, |res, tl| { res || *tl == tl2_u1 })); assert!(tl_u1.iter().fold(false, |res, tl| { res || *tl == tl2_u1 }));
let tl_instance = Timeline::list_for_user(&conn, None).unwrap(); 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_eq!(3, tl_instance.len()); // there are also the local and federated feed by default
assert!(tl_instance assert!(tl_instance
.iter() .iter()
.fold(false, |res, tl| { res || *tl == tl1_instance })); .fold(false, |res, tl| { res || *tl == tl1_instance }));
tl1_u1.name = "My Super TL".to_owned(); tl1_u1.name = "My Super TL".to_owned();
let new_tl1_u2 = tl1_u2.update(&conn).unwrap(); let new_tl1_u2 = tl1_u2.update(conn).unwrap();
let tl_u2 = Timeline::list_for_user(&conn, Some(users[1].id)).unwrap(); let tl_u2 = Timeline::list_for_user(conn, Some(users[1].id)).unwrap();
assert_eq!(2, tl_u2.len()); // same here assert_eq!(2, tl_u2.len()); // same here
assert!(tl_u2 assert!(tl_u2
.iter() .iter()
@@ -334,48 +327,48 @@ mod tests {
fn test_timeline_creation_error() { fn test_timeline_creation_error() {
let conn = &db(); let conn = &db();
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let users = userTests::fill_database(&conn); let users = userTests::fill_database(conn);
assert!(Timeline::new_for_user( assert!(Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"my timeline".to_owned(), "my timeline".to_owned(),
"invalid keyword".to_owned(), "invalid keyword".to_owned(),
) )
.is_err()); .is_err());
assert!(Timeline::new_for_instance( assert!(Timeline::new_for_instance(
&conn, conn,
"my timeline".to_owned(), "my timeline".to_owned(),
"invalid keyword".to_owned(), "invalid keyword".to_owned(),
) )
.is_err()); .is_err());
assert!(Timeline::new_for_user( assert!(Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"my timeline".to_owned(), "my timeline".to_owned(),
"author in non_existant_list".to_owned(), "author in non_existant_list".to_owned(),
) )
.is_err()); .is_err());
assert!(Timeline::new_for_instance( assert!(Timeline::new_for_instance(
&conn, conn,
"my timeline".to_owned(), "my timeline".to_owned(),
"lang in dont-exist".to_owned(), "lang in dont-exist".to_owned(),
) )
.is_err()); .is_err());
List::new(&conn, "friends", Some(&users[0]), ListType::User).unwrap(); List::new(conn, "friends", Some(&users[0]), ListType::User).unwrap();
List::new(&conn, "idk", None, ListType::Blog).unwrap(); List::new(conn, "idk", None, ListType::Blog).unwrap();
assert!(Timeline::new_for_user( assert!(Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"my timeline".to_owned(), "my timeline".to_owned(),
"blog in friends".to_owned(), "blog in friends".to_owned(),
) )
.is_err()); .is_err());
assert!(Timeline::new_for_instance( assert!(Timeline::new_for_instance(
&conn, conn,
"my timeline".to_owned(), "my timeline".to_owned(),
"not author in idk".to_owned(), "not author in idk".to_owned(),
) )
@@ -387,12 +380,13 @@ mod tests {
#[test] #[test]
fn test_simple_match() { fn test_simple_match() {
let conn = &db(); let r = &rockets();
let conn = &r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(&conn); let (users, blogs) = blogTests::fill_database(conn);
let gnu_tl = Timeline::new_for_user( let gnu_tl = Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"GNU timeline".to_owned(), "GNU timeline".to_owned(),
"license in [AGPL, LGPL, GPL]".to_owned(), "license in [AGPL, LGPL, GPL]".to_owned(),
@@ -400,7 +394,7 @@ mod tests {
.unwrap(); .unwrap();
let gnu_post = Post::insert( let gnu_post = Post::insert(
&conn, conn,
NewPost { NewPost {
blog_id: blogs[0].id, blog_id: blogs[0].id,
slug: "slug".to_string(), slug: "slug".to_string(),
@@ -414,12 +408,13 @@ mod tests {
source: "you must say GNU/Linux, not Linux!!!".to_string(), source: "you must say GNU/Linux, not Linux!!!".to_string(),
cover_id: None, cover_id: None,
}, },
&r.searcher,
) )
.unwrap(); .unwrap();
assert!(gnu_tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); assert!(gnu_tl.matches(r, &gnu_post, Kind::Original).unwrap());
let non_free_post = Post::insert( let non_free_post = Post::insert(
&conn, conn,
NewPost { NewPost {
blog_id: blogs[0].id, blog_id: blogs[0].id,
slug: "slug2".to_string(), slug: "slug2".to_string(),
@@ -433,11 +428,10 @@ mod tests {
source: "so is Microsoft".to_string(), source: "so is Microsoft".to_string(),
cover_id: None, cover_id: None,
}, },
&r.searcher,
) )
.unwrap(); .unwrap();
assert!(!gnu_tl assert!(!gnu_tl.matches(r, &non_free_post, Kind::Original).unwrap());
.matches(&conn, &non_free_post, Kind::Original)
.unwrap());
Ok(()) Ok(())
}); });
@@ -445,11 +439,12 @@ mod tests {
#[test] #[test]
fn test_complex_match() { fn test_complex_match() {
let conn = &db(); let r = &rockets();
let conn = &r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(&conn); let (users, blogs) = blogTests::fill_database(conn);
Follow::insert( Follow::insert(
&conn, conn,
NewFollow { NewFollow {
follower_id: users[0].id, follower_id: users[0].id,
following_id: users[1].id, following_id: users[1].id,
@@ -459,11 +454,11 @@ mod tests {
.unwrap(); .unwrap();
let fav_blogs_list = let fav_blogs_list =
List::new(&conn, "fav_blogs", Some(&users[0]), ListType::Blog).unwrap(); List::new(conn, "fav_blogs", Some(&users[0]), ListType::Blog).unwrap();
fav_blogs_list.add_blogs(&conn, &[blogs[0].id]).unwrap(); fav_blogs_list.add_blogs(conn, &[blogs[0].id]).unwrap();
let my_tl = Timeline::new_for_user( let my_tl = Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"My timeline".to_owned(), "My timeline".to_owned(),
"blog in fav_blogs and not has_cover or local and followed exclude likes" "blog in fav_blogs and not has_cover or local and followed exclude likes"
@@ -472,7 +467,7 @@ mod tests {
.unwrap(); .unwrap();
let post = Post::insert( let post = Post::insert(
&conn, conn,
NewPost { NewPost {
blog_id: blogs[0].id, blog_id: blogs[0].id,
slug: "about-linux".to_string(), slug: "about-linux".to_string(),
@@ -486,12 +481,13 @@ mod tests {
subtitle: "".to_string(), subtitle: "".to_string(),
cover_id: None, cover_id: None,
}, },
&r.searcher,
) )
.unwrap(); .unwrap();
assert!(my_tl.matches(&conn, &post, Kind::Original).unwrap()); // matches because of "blog in fav_blogs" (and there is no cover) assert!(my_tl.matches(r, &post, Kind::Original).unwrap()); // matches because of "blog in fav_blogs" (and there is no cover)
let post = Post::insert( let post = Post::insert(
&conn, conn,
NewPost { NewPost {
blog_id: blogs[1].id, blog_id: blogs[1].id,
slug: "about-linux-2".to_string(), slug: "about-linux-2".to_string(),
@@ -507,9 +503,10 @@ mod tests {
subtitle: "".to_string(), subtitle: "".to_string(),
cover_id: None, cover_id: None,
}, },
&r.searcher,
) )
.unwrap(); .unwrap();
assert!(!my_tl.matches(&conn, &post, Kind::Like(&users[1])).unwrap()); assert!(!my_tl.matches(r, &post, Kind::Like(&users[1])).unwrap());
Ok(()) Ok(())
}); });
@@ -517,19 +514,20 @@ mod tests {
#[test] #[test]
fn test_add_to_all_timelines() { fn test_add_to_all_timelines() {
let conn = &db(); let r = &rockets();
let conn = &r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(&conn); let (users, blogs) = blogTests::fill_database(conn);
let gnu_tl = Timeline::new_for_user( let gnu_tl = Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"GNU timeline".to_owned(), "GNU timeline".to_owned(),
"license in [AGPL, LGPL, GPL]".to_owned(), "license in [AGPL, LGPL, GPL]".to_owned(),
) )
.unwrap(); .unwrap();
let non_gnu_tl = Timeline::new_for_user( let non_gnu_tl = Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"Stallman disapproved timeline".to_owned(), "Stallman disapproved timeline".to_owned(),
"not license in [AGPL, LGPL, GPL]".to_owned(), "not license in [AGPL, LGPL, GPL]".to_owned(),
@@ -537,7 +535,7 @@ mod tests {
.unwrap(); .unwrap();
let gnu_post = Post::insert( let gnu_post = Post::insert(
&conn, conn,
NewPost { NewPost {
blog_id: blogs[0].id, blog_id: blogs[0].id,
slug: "slug".to_string(), slug: "slug".to_string(),
@@ -551,11 +549,12 @@ mod tests {
source: "you must say GNU/Linux, not Linux!!!".to_string(), source: "you must say GNU/Linux, not Linux!!!".to_string(),
cover_id: None, cover_id: None,
}, },
&r.searcher,
) )
.unwrap(); .unwrap();
let non_free_post = Post::insert( let non_free_post = Post::insert(
&conn, conn,
NewPost { NewPost {
blog_id: blogs[0].id, blog_id: blogs[0].id,
slug: "slug2".to_string(), slug: "slug2".to_string(),
@@ -569,16 +568,17 @@ mod tests {
source: "so is Microsoft".to_string(), source: "so is Microsoft".to_string(),
cover_id: None, cover_id: None,
}, },
&r.searcher,
) )
.unwrap(); .unwrap();
Timeline::add_to_all_timelines(&conn, &gnu_post, Kind::Original).unwrap(); Timeline::add_to_all_timelines(r, &gnu_post, Kind::Original).unwrap();
Timeline::add_to_all_timelines(&conn, &non_free_post, Kind::Original).unwrap(); Timeline::add_to_all_timelines(r, &non_free_post, Kind::Original).unwrap();
let res = gnu_tl.get_latest(&conn, 2).unwrap(); let res = gnu_tl.get_latest(conn, 2).unwrap();
assert_eq!(res.len(), 1); assert_eq!(res.len(), 1);
assert_eq!(res[0].id, gnu_post.id); assert_eq!(res[0].id, gnu_post.id);
let res = non_gnu_tl.get_latest(&conn, 2).unwrap(); let res = non_gnu_tl.get_latest(conn, 2).unwrap();
assert_eq!(res.len(), 1); assert_eq!(res.len(), 1);
assert_eq!(res[0].id, non_free_post.id); assert_eq!(res[0].id, non_free_post.id);
@@ -588,12 +588,13 @@ mod tests {
#[test] #[test]
fn test_matches_lists_direct() { fn test_matches_lists_direct() {
let conn = &db(); let r = &rockets();
let conn = &r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(&conn); let (users, blogs) = blogTests::fill_database(conn);
let gnu_post = Post::insert( let gnu_post = Post::insert(
&conn, conn,
NewPost { NewPost {
blog_id: blogs[0].id, blog_id: blogs[0].id,
slug: "slug".to_string(), slug: "slug".to_string(),
@@ -607,66 +608,65 @@ mod tests {
source: "you must say GNU/Linux, not Linux!!!".to_string(), source: "you must say GNU/Linux, not Linux!!!".to_string(),
cover_id: None, cover_id: None,
}, },
&r.searcher,
) )
.unwrap(); .unwrap();
gnu_post gnu_post
.update_tags(&conn, vec![Tag::build_activity("free".to_owned()).unwrap()]) .update_tags(conn, vec![Tag::build_activity("free".to_owned()).unwrap()])
.unwrap(); .unwrap();
PostAuthor::insert( PostAuthor::insert(
&conn, conn,
NewPostAuthor { NewPostAuthor {
post_id: gnu_post.id, post_id: gnu_post.id,
author_id: blogs[0].list_authors(&conn).unwrap()[0].id, author_id: blogs[0].list_authors(conn).unwrap()[0].id,
}, },
) )
.unwrap(); .unwrap();
let tl = Timeline::new_for_user( let tl = Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"blog timeline".to_owned(), "blog timeline".to_owned(),
format!("blog in [{}]", blogs[0].fqn), format!("blog in [{}]", blogs[0].fqn),
) )
.unwrap(); .unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap(); tl.delete(conn).unwrap();
let tl = Timeline::new_for_user( let tl = Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"blog timeline".to_owned(), "blog timeline".to_owned(),
"blog in [no_one@nowhere]".to_owned(), "blog in [no_one@nowhere]".to_owned(),
) )
.unwrap(); .unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap(); tl.delete(conn).unwrap();
let tl = Timeline::new_for_user( let tl = Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"author timeline".to_owned(), "author timeline".to_owned(),
format!( format!(
"author in [{}]", "author in [{}]",
blogs[0].list_authors(&conn).unwrap()[0].fqn blogs[0].list_authors(conn).unwrap()[0].fqn
), ),
) )
.unwrap(); .unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap(); tl.delete(conn).unwrap();
let tl = Timeline::new_for_user( let tl = Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"author timeline".to_owned(), "author timeline".to_owned(),
format!("author in [{}]", users[2].fqn), format!("author in [{}]", users[2].fqn),
) )
.unwrap(); .unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
assert!(tl assert!(tl.matches(r, &gnu_post, Kind::Reshare(&users[2])).unwrap());
.matches(&conn, &gnu_post, Kind::Reshare(&users[2])) assert!(!tl.matches(r, &gnu_post, Kind::Like(&users[2])).unwrap());
.unwrap()); tl.delete(conn).unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Like(&users[2])).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user( let tl = Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"author timeline".to_owned(), "author timeline".to_owned(),
format!( format!(
@@ -675,50 +675,48 @@ mod tests {
), ),
) )
.unwrap(); .unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
assert!(!tl assert!(!tl.matches(r, &gnu_post, Kind::Reshare(&users[2])).unwrap());
.matches(&conn, &gnu_post, Kind::Reshare(&users[2])) assert!(tl.matches(r, &gnu_post, Kind::Like(&users[2])).unwrap());
.unwrap()); tl.delete(conn).unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Like(&users[2])).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user( let tl = Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"tag timeline".to_owned(), "tag timeline".to_owned(),
"tags in [free]".to_owned(), "tags in [free]".to_owned(),
) )
.unwrap(); .unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap(); tl.delete(conn).unwrap();
let tl = Timeline::new_for_user( let tl = Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"tag timeline".to_owned(), "tag timeline".to_owned(),
"tags in [private]".to_owned(), "tags in [private]".to_owned(),
) )
.unwrap(); .unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap(); tl.delete(conn).unwrap();
let tl = Timeline::new_for_user( let tl = Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"english timeline".to_owned(), "english timeline".to_owned(),
"lang in [en]".to_owned(), "lang in [en]".to_owned(),
) )
.unwrap(); .unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap(); tl.delete(conn).unwrap();
let tl = Timeline::new_for_user( let tl = Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"franco-italian timeline".to_owned(), "franco-italian timeline".to_owned(),
"lang in [fr, it]".to_owned(), "lang in [fr, it]".to_owned(),
) )
.unwrap(); .unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap(); tl.delete(conn).unwrap();
Ok(()) Ok(())
}); });
@@ -728,12 +726,12 @@ mod tests {
#[test] #[test]
fn test_matches_lists_saved() { fn test_matches_lists_saved() {
let r = &rockets(); let r = &rockets();
let conn = &db(); let conn = &r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(&conn); let (users, blogs) = blogTests::fill_database(conn);
let gnu_post = Post::insert( let gnu_post = Post::insert(
&conn, conn,
NewPost { NewPost {
blog_id: blogs[0].id, blog_id: blogs[0].id,
slug: "slug".to_string(), slug: "slug".to_string(),
@@ -747,10 +745,11 @@ mod tests {
source: "you must say GNU/Linux, not Linux!!!".to_string(), source: "you must say GNU/Linux, not Linux!!!".to_string(),
cover_id: None, cover_id: None,
}, },
&r.searcher,
) )
.unwrap(); .unwrap();
gnu_post.update_tags(&conn, vec![Tag::build_activity("free".to_owned()).unwrap()]).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(); PostAuthor::insert(conn, NewPostAuthor {post_id: gnu_post.id, author_id: blogs[0].list_authors(conn).unwrap()[0].id}).unwrap();
unimplemented!(); unimplemented!();
@@ -760,12 +759,13 @@ mod tests {
#[test] #[test]
fn test_matches_keyword() { fn test_matches_keyword() {
let conn = &db(); let r = &rockets();
let conn = &r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(&conn); let (users, blogs) = blogTests::fill_database(conn);
let gnu_post = Post::insert( let gnu_post = Post::insert(
&conn, conn,
NewPost { NewPost {
blog_id: blogs[0].id, blog_id: blogs[0].id,
slug: "slug".to_string(), slug: "slug".to_string(),
@@ -779,65 +779,66 @@ mod tests {
source: "you must say GNU/Linux, not Linux!!!".to_string(), source: "you must say GNU/Linux, not Linux!!!".to_string(),
cover_id: None, cover_id: None,
}, },
&r.searcher,
) )
.unwrap(); .unwrap();
let tl = Timeline::new_for_user( let tl = Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"Linux title".to_owned(), "Linux title".to_owned(),
"title contains Linux".to_owned(), "title contains Linux".to_owned(),
) )
.unwrap(); .unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap(); tl.delete(conn).unwrap();
let tl = Timeline::new_for_user( let tl = Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"Microsoft title".to_owned(), "Microsoft title".to_owned(),
"title contains Microsoft".to_owned(), "title contains Microsoft".to_owned(),
) )
.unwrap(); .unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap(); tl.delete(conn).unwrap();
let tl = Timeline::new_for_user( let tl = Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"Linux subtitle".to_owned(), "Linux subtitle".to_owned(),
"subtitle contains Stallman".to_owned(), "subtitle contains Stallman".to_owned(),
) )
.unwrap(); .unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap(); tl.delete(conn).unwrap();
let tl = Timeline::new_for_user( let tl = Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"Microsoft subtitle".to_owned(), "Microsoft subtitle".to_owned(),
"subtitle contains Nadella".to_owned(), "subtitle contains Nadella".to_owned(),
) )
.unwrap(); .unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap(); tl.delete(conn).unwrap();
let tl = Timeline::new_for_user( let tl = Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"Linux content".to_owned(), "Linux content".to_owned(),
"content contains Linux".to_owned(), "content contains Linux".to_owned(),
) )
.unwrap(); .unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap(); tl.delete(conn).unwrap();
let tl = Timeline::new_for_user( let tl = Timeline::new_for_user(
&conn, conn,
users[0].id, users[0].id,
"Microsoft content".to_owned(), "Microsoft content".to_owned(),
"subtitle contains Windows".to_owned(), "subtitle contains Windows".to_owned(),
) )
.unwrap(); .unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap()); assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap(); tl.delete(conn).unwrap();
Ok(()) Ok(())
}); });
+38 -32
View File
@@ -1,12 +1,11 @@
use crate::{ use crate::{
blogs::Blog, blogs::Blog,
db_conn::DbConn,
lists::{self, ListType}, lists::{self, ListType},
posts::Post, posts::Post,
tags::Tag, tags::Tag,
timeline::Timeline, timeline::Timeline,
users::User, users::User,
Result, PlumeRocket, Result,
}; };
use plume_common::activity_pub::inbox::AsActor; use plume_common::activity_pub::inbox::AsActor;
use whatlang::{self, Lang}; use whatlang::{self, Lang};
@@ -161,19 +160,19 @@ enum TQ<'a> {
impl<'a> TQ<'a> { impl<'a> TQ<'a> {
fn matches( fn matches(
&self, &self,
conn: &DbConn, rocket: &PlumeRocket,
timeline: &Timeline, timeline: &Timeline,
post: &Post, post: &Post,
kind: Kind<'_>, kind: Kind<'_>,
) -> Result<bool> { ) -> Result<bool> {
match self { match self {
TQ::Or(inner) => inner.iter().try_fold(false, |s, e| { TQ::Or(inner) => inner.iter().try_fold(false, |s, e| {
e.matches(conn, timeline, post, kind).map(|r| s || r) e.matches(rocket, timeline, post, kind).map(|r| s || r)
}), }),
TQ::And(inner) => inner.iter().try_fold(true, |s, e| { TQ::And(inner) => inner.iter().try_fold(true, |s, e| {
e.matches(conn, timeline, post, kind).map(|r| s && r) e.matches(rocket, timeline, post, kind).map(|r| s && r)
}), }),
TQ::Arg(inner, invert) => Ok(inner.matches(conn, timeline, post, kind)? ^ invert), TQ::Arg(inner, invert) => Ok(inner.matches(rocket, timeline, post, kind)? ^ invert),
} }
} }
@@ -206,15 +205,15 @@ enum Arg<'a> {
impl<'a> Arg<'a> { impl<'a> Arg<'a> {
pub fn matches( pub fn matches(
&self, &self,
conn: &DbConn, rocket: &PlumeRocket,
timeline: &Timeline, timeline: &Timeline,
post: &Post, post: &Post,
kind: Kind<'_>, kind: Kind<'_>,
) -> Result<bool> { ) -> Result<bool> {
match self { match self {
Arg::In(t, l) => t.matches(conn, timeline, post, l, kind), Arg::In(t, l) => t.matches(rocket, timeline, post, l, kind),
Arg::Contains(t, v) => t.matches(post, v), Arg::Contains(t, v) => t.matches(post, v),
Arg::Boolean(t) => t.matches(conn, timeline, post, kind), Arg::Boolean(t) => t.matches(rocket, timeline, post, kind),
} }
} }
} }
@@ -231,7 +230,7 @@ enum WithList {
impl WithList { impl WithList {
pub fn matches( pub fn matches(
&self, &self,
conn: &DbConn, rocket: &PlumeRocket,
timeline: &Timeline, timeline: &Timeline,
post: &Post, post: &Post,
list: &List<'_>, list: &List<'_>,
@@ -239,34 +238,39 @@ impl WithList {
) -> Result<bool> { ) -> Result<bool> {
match list { match list {
List::List(name) => { List::List(name) => {
let list = lists::List::find_for_user_by_name(conn, timeline.user_id, &name)?; let list =
lists::List::find_for_user_by_name(&rocket.conn, timeline.user_id, &name)?;
match (self, list.kind()) { match (self, list.kind()) {
(WithList::Blog, ListType::Blog) => list.contains_blog(conn, post.blog_id), (WithList::Blog, ListType::Blog) => {
list.contains_blog(&rocket.conn, post.blog_id)
}
(WithList::Author { boosts, likes }, ListType::User) => match kind { (WithList::Author { boosts, likes }, ListType::User) => match kind {
Kind::Original => Ok(list Kind::Original => Ok(list
.list_users(conn)? .list_users(&rocket.conn)?
.iter() .iter()
.any(|a| post.is_author(conn, a.id).unwrap_or(false))), .any(|a| post.is_author(&rocket.conn, a.id).unwrap_or(false))),
Kind::Reshare(u) => { Kind::Reshare(u) => {
if *boosts { if *boosts {
list.contains_user(conn, u.id) list.contains_user(&rocket.conn, u.id)
} else { } else {
Ok(false) Ok(false)
} }
} }
Kind::Like(u) => { Kind::Like(u) => {
if *likes { if *likes {
list.contains_user(conn, u.id) list.contains_user(&rocket.conn, u.id)
} else { } else {
Ok(false) Ok(false)
} }
} }
}, },
(WithList::License, ListType::Word) => list.contains_word(conn, &post.license), (WithList::License, ListType::Word) => {
list.contains_word(&rocket.conn, &post.license)
}
(WithList::Tags, ListType::Word) => { (WithList::Tags, ListType::Word) => {
let tags = Tag::for_post(conn, post.id)?; let tags = Tag::for_post(&rocket.conn, post.id)?;
Ok(list Ok(list
.list_words(conn)? .list_words(&rocket.conn)?
.iter() .iter()
.any(|s| tags.iter().any(|t| s == &t.tag))) .any(|s| tags.iter().any(|t| s == &t.tag)))
} }
@@ -281,7 +285,7 @@ impl WithList {
}) })
.unwrap_or(Lang::Eng) .unwrap_or(Lang::Eng)
.name(); .name();
list.contains_prefix(conn, lang) list.contains_prefix(&rocket.conn, lang)
} }
(_, _) => Err(QueryError::RuntimeError(format!( (_, _) => Err(QueryError::RuntimeError(format!(
"The list '{}' is of the wrong type for this usage", "The list '{}' is of the wrong type for this usage",
@@ -293,13 +297,13 @@ impl WithList {
List::Array(list) => match self { List::Array(list) => match self {
WithList::Blog => Ok(list WithList::Blog => Ok(list
.iter() .iter()
.filter_map(|b| Blog::find_by_fqn(conn, b).ok()) .filter_map(|b| Blog::find_by_fqn(rocket, b).ok())
.any(|b| b.id == post.blog_id)), .any(|b| b.id == post.blog_id)),
WithList::Author { boosts, likes } => match kind { WithList::Author { boosts, likes } => match kind {
Kind::Original => Ok(list Kind::Original => Ok(list
.iter() .iter()
.filter_map(|a| User::find_by_fqn(&*conn, a).ok()) .filter_map(|a| User::find_by_fqn(rocket, a).ok())
.any(|a| post.is_author(conn, a.id).unwrap_or(false))), .any(|a| post.is_author(&rocket.conn, a.id).unwrap_or(false))),
Kind::Reshare(u) => { Kind::Reshare(u) => {
if *boosts { if *boosts {
Ok(list.iter().any(|user| &u.fqn == user)) Ok(list.iter().any(|user| &u.fqn == user))
@@ -317,7 +321,7 @@ impl WithList {
}, },
WithList::License => Ok(list.iter().any(|s| s == &post.license)), WithList::License => Ok(list.iter().any(|s| s == &post.license)),
WithList::Tags => { WithList::Tags => {
let tags = Tag::for_post(conn, post.id)?; let tags = Tag::for_post(&rocket.conn, post.id)?;
Ok(list.iter().any(|s| tags.iter().any(|t| s == &t.tag))) Ok(list.iter().any(|s| tags.iter().any(|t| s == &t.tag)))
} }
WithList::Lang => { WithList::Lang => {
@@ -367,7 +371,7 @@ enum Bool {
impl Bool { impl Bool {
pub fn matches( pub fn matches(
&self, &self,
conn: &DbConn, rocket: &PlumeRocket,
timeline: &Timeline, timeline: &Timeline,
post: &Post, post: &Post,
kind: Kind<'_>, kind: Kind<'_>,
@@ -380,19 +384,21 @@ impl Bool {
let user = timeline.user_id.unwrap(); let user = timeline.user_id.unwrap();
match kind { match kind {
Kind::Original => post Kind::Original => post
.get_authors(conn)? .get_authors(&rocket.conn)?
.iter() .iter()
.try_fold(false, |s, a| a.is_followed_by(conn, user).map(|r| s || r)), .try_fold(false, |s, a| {
a.is_followed_by(&rocket.conn, user).map(|r| s || r)
}),
Kind::Reshare(u) => { Kind::Reshare(u) => {
if *boosts { if *boosts {
u.is_followed_by(conn, user) u.is_followed_by(&rocket.conn, user)
} else { } else {
Ok(false) Ok(false)
} }
} }
Kind::Like(u) => { Kind::Like(u) => {
if *likes { if *likes {
u.is_followed_by(conn, user) u.is_followed_by(&rocket.conn, user)
} else { } else {
Ok(false) Ok(false)
} }
@@ -400,7 +406,7 @@ impl Bool {
} }
} }
Bool::HasCover => Ok(post.cover_id.is_some()), Bool::HasCover => Ok(post.cover_id.is_some()),
Bool::Local => Ok(post.get_blog(conn)?.is_local() && kind == Kind::Original), Bool::Local => Ok(post.get_blog(&rocket.conn)?.is_local() && kind == Kind::Original),
Bool::All => Ok(kind == Kind::Original), Bool::All => Ok(kind == Kind::Original),
} }
} }
@@ -636,12 +642,12 @@ impl<'a> TimelineQuery<'a> {
pub fn matches( pub fn matches(
&self, &self,
conn: &DbConn, rocket: &PlumeRocket,
timeline: &Timeline, timeline: &Timeline,
post: &Post, post: &Post,
kind: Kind<'_>, kind: Kind<'_>,
) -> Result<bool> { ) -> Result<bool> {
self.0.matches(conn, timeline, post, kind) self.0.matches(rocket, timeline, post, kind)
} }
pub fn list_used_lists(&self) -> Vec<(String, ListType)> { pub fn list_used_lists(&self) -> Vec<(String, ListType)> {
+86 -244
View File
@@ -1,8 +1,8 @@
use crate::{ use crate::{
ap_url, blocklisted_emails::BlocklistedEmail, blogs::Blog, db_conn::DbConn, follows::Follow, ap_url, blocklisted_emails::BlocklistedEmail, blogs::Blog, db_conn::DbConn, follows::Follow,
instance::*, medias::Media, notifications::Notification, post_authors::PostAuthor, posts::Post, instance::*, medias::Media, notifications::Notification, post_authors::PostAuthor, posts::Post,
safe_string::SafeString, schema::users, timeline::Timeline, Connection, Error, Result, safe_string::SafeString, schema::users, search::Searcher, timeline::Timeline, Connection,
UserEvent::*, CONFIG, ITEMS_PER_PAGE, USER_CHAN, Error, PlumeRocket, Result, ITEMS_PER_PAGE,
}; };
use activitypub::{ use activitypub::{
activity::Delete, activity::Delete,
@@ -11,9 +11,9 @@ use activitypub::{
object::{Image, Tombstone}, object::{Image, Tombstone},
Activity, CustomObject, Endpoint, Activity, CustomObject, Endpoint,
}; };
use bcrypt;
use chrono::{NaiveDateTime, Utc}; use chrono::{NaiveDateTime, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl}; use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl};
use ldap3::{LdapConn, Scope, SearchEntry};
use openssl::{ use openssl::{
hash::MessageDigest, hash::MessageDigest,
pkey::{PKey, Private}, pkey::{PKey, Private},
@@ -33,15 +33,14 @@ use reqwest::{
header::{HeaderValue, ACCEPT}, header::{HeaderValue, ACCEPT},
ClientBuilder, ClientBuilder,
}; };
use riker::actors::{Publish, Tell};
use rocket::{ use rocket::{
outcome::IntoOutcome, outcome::IntoOutcome,
request::{self, FromRequest, Request}, request::{self, FromRequest, Request},
}; };
use serde_json;
use std::{ use std::{
cmp::PartialEq, cmp::PartialEq,
hash::{Hash, Hasher}, hash::{Hash, Hasher},
sync::Arc,
}; };
use url::Url; use url::Url;
use webfinger::*; use webfinger::*;
@@ -131,14 +130,14 @@ impl User {
.map_err(Error::from) .map_err(Error::from)
} }
pub fn delete(&self, conn: &Connection) -> Result<()> { pub fn delete(&self, conn: &Connection, searcher: &Searcher) -> Result<()> {
use crate::schema::post_authors; use crate::schema::post_authors;
for blog in Blog::find_for_author(conn, self)? for blog in Blog::find_for_author(conn, self)?
.iter() .iter()
.filter(|b| b.count_authors(conn).map(|c| c <= 1).unwrap_or(false)) .filter(|b| b.count_authors(conn).map(|c| c <= 1).unwrap_or(false))
{ {
blog.delete(conn)?; blog.delete(conn, searcher)?;
} }
// delete the posts if they is the only author // delete the posts if they is the only author
let all_their_posts_ids: Vec<i32> = post_authors::table let all_their_posts_ids: Vec<i32> = post_authors::table
@@ -158,7 +157,7 @@ impl User {
.unwrap_or(&0) .unwrap_or(&0)
> &0; > &0;
if !has_other_authors { if !has_other_authors {
Post::get(conn, post_id)?.delete(conn)?; Post::get(conn, post_id)?.delete(conn, searcher)?;
} }
} }
@@ -188,29 +187,29 @@ impl User {
users::table users::table
.filter(users::instance_id.eq(Instance::get_local()?.id)) .filter(users::instance_id.eq(Instance::get_local()?.id))
.count() .count()
.get_result(&*conn) .get_result(conn)
.map_err(Error::from) .map_err(Error::from)
} }
pub fn find_by_fqn(conn: &DbConn, fqn: &str) -> Result<User> { pub fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<User> {
let from_db = users::table let from_db = users::table
.filter(users::fqn.eq(fqn)) .filter(users::fqn.eq(fqn))
.first(&**conn) .first(&*c.conn)
.optional()?; .optional()?;
if let Some(from_db) = from_db { if let Some(from_db) = from_db {
Ok(from_db) Ok(from_db)
} else { } else {
User::fetch_from_webfinger(conn, fqn) User::fetch_from_webfinger(c, fqn)
} }
} }
fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result<User> { fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<User> {
let link = resolve(acct.to_owned(), true)? let link = resolve(acct.to_owned(), true)?
.links .links
.into_iter() .into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json"))) .find(|l| l.mime_type == Some(String::from("application/activity+json")))
.ok_or(Error::Webfinger)?; .ok_or(Error::Webfinger)?;
User::from_id(conn, link.href.as_ref()?, None, CONFIG.proxy()).map_err(|(_, e)| e) User::from_id(c, link.href.as_ref()?, None).map_err(|(_, e)| e)
} }
pub fn fetch_remote_interact_uri(acct: &str) -> Result<String> { pub fn fetch_remote_interact_uri(acct: &str) -> Result<String> {
@@ -245,8 +244,8 @@ impl User {
Ok(json) Ok(json)
} }
pub fn fetch_from_url(conn: &DbConn, url: &str) -> Result<User> { pub fn fetch_from_url(c: &PlumeRocket, url: &str) -> Result<User> {
User::fetch(url).and_then(|json| User::from_activity(conn, json)) User::fetch(url).and_then(|json| User::from_activity(c, json))
} }
pub fn refetch(&self, conn: &Connection) -> Result<()> { pub fn refetch(&self, conn: &Connection) -> Result<()> {
@@ -293,137 +292,11 @@ impl User {
bcrypt::hash(pass, 10).map_err(Error::from) bcrypt::hash(pass, 10).map_err(Error::from)
} }
fn ldap_preconn(ldap_conn: &mut LdapConn) -> Result<()> { pub fn auth(&self, pass: &str) -> bool {
let ldap = CONFIG.ldap.as_ref().unwrap(); self.hashed_password
.clone()
if let Some((user, password)) = ldap.user.as_ref() { .map(|hashed| bcrypt::verify(pass, hashed.as_ref()).unwrap_or(false))
let bind = ldap_conn .unwrap_or(false)
.simple_bind(user, password)
.map_err(|_| Error::NotFound)?;
if bind.success().is_err() {
return Err(Error::NotFound);
}
}
Ok(())
}
fn ldap_register(conn: &Connection, name: &str, password: &str) -> Result<User> {
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)?;
User::ldap_preconn(&mut ldap_conn)?;
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;
};
if User::ldap_preconn(&mut conn).is_err() {
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<User> {
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<()> { pub fn reset_password(&self, conn: &Connection, pass: &str) -> Result<()> {
@@ -853,7 +726,11 @@ impl User {
pub fn webfinger(&self, conn: &Connection) -> Result<Webfinger> { pub fn webfinger(&self, conn: &Connection) -> Result<Webfinger> {
Ok(Webfinger { Ok(Webfinger {
subject: format!("acct:{}", self.acct_authority(conn)?), subject: format!(
"acct:{}@{}",
self.username,
self.get_instance(conn)?.public_domain
),
aliases: vec![self.ap_url.clone()], aliases: vec![self.ap_url.clone()],
links: vec![ links: vec![
Link { Link {
@@ -891,14 +768,6 @@ impl User {
}) })
} }
pub fn acct_authority(&self, conn: &Connection) -> Result<String> {
Ok(format!(
"{}@{}",
self.username,
self.get_instance(conn)?.public_domain
))
}
pub fn set_avatar(&self, conn: &Connection, id: i32) -> Result<()> { pub fn set_avatar(&self, conn: &Connection, id: i32) -> Result<()> {
diesel::update(self) diesel::update(self)
.set(users::avatar_id.eq(id)) .set(users::avatar_id.eq(id))
@@ -918,21 +787,6 @@ impl User {
self.fqn.clone() 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 { impl<'a, 'r> FromRequest<'a, 'r> for User {
@@ -957,20 +811,20 @@ impl IntoId for User {
impl Eq for User {} impl Eq for User {}
impl FromId<DbConn> for User { impl FromId<PlumeRocket> for User {
type Error = Error; type Error = Error;
type Object = CustomPerson; type Object = CustomPerson;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> { fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Self::find_by_ap_url(conn, id) Self::find_by_ap_url(&c.conn, id)
} }
fn from_activity(conn: &DbConn, acct: CustomPerson) -> Result<Self> { fn from_activity(c: &PlumeRocket, acct: CustomPerson) -> Result<Self> {
let url = Url::parse(&acct.object.object_props.id_string()?)?; let url = Url::parse(&acct.object.object_props.id_string()?)?;
let inst = url.host_str()?; let inst = url.host_str()?;
let instance = Instance::find_by_domain(conn, inst).or_else(|_| { let instance = Instance::find_by_domain(&c.conn, inst).or_else(|_| {
Instance::insert( Instance::insert(
conn, &c.conn,
NewInstance { NewInstance {
name: inst.to_owned(), name: inst.to_owned(),
public_domain: inst.to_owned(), public_domain: inst.to_owned(),
@@ -999,7 +853,7 @@ impl FromId<DbConn> for User {
}; };
let user = User::insert( let user = User::insert(
conn, &c.conn,
NewUser { NewUser {
display_name: acct display_name: acct
.object .object
@@ -1045,10 +899,10 @@ impl FromId<DbConn> for User {
if let Ok(icon) = acct.object.object_props.icon_image() { if let Ok(icon) = acct.object.object_props.icon_image() {
if let Ok(url) = icon.object_props.url_string() { if let Ok(url) = icon.object_props.url_string() {
let avatar = Media::save_remote(conn, url, &user); let avatar = Media::save_remote(&c.conn, url, &user);
if let Ok(avatar) = avatar { if let Ok(avatar) = avatar {
user.set_avatar(conn, avatar.id)?; user.set_avatar(&c.conn, avatar.id)?;
} }
} }
} }
@@ -1057,7 +911,7 @@ impl FromId<DbConn> for User {
} }
} }
impl AsActor<&DbConn> for User { impl AsActor<&PlumeRocket> for User {
fn get_inbox_url(&self) -> String { fn get_inbox_url(&self) -> String {
self.inbox_url.clone() self.inbox_url.clone()
} }
@@ -1073,13 +927,13 @@ impl AsActor<&DbConn> for User {
} }
} }
impl AsObject<User, Delete, &DbConn> for User { impl AsObject<User, Delete, &PlumeRocket> for User {
type Error = Error; type Error = Error;
type Output = (); type Output = ();
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
if self.id == actor.id { if self.id == actor.id {
self.delete(conn).map(|_| ()) self.delete(&c.conn, &c.searcher).map(|_| ())
} else { } else {
Err(Error::Unauthorized) Err(Error::Unauthorized)
} }
@@ -1129,7 +983,7 @@ impl NewUser {
role: Role, role: Role,
summary: &str, summary: &str,
email: String, email: String,
password: Option<String>, password: String,
) -> Result<User> { ) -> Result<User> {
let (pub_key, priv_key) = gen_keypair(); let (pub_key, priv_key) = gen_keypair();
let instance = Instance::get_local()?; let instance = Instance::get_local()?;
@@ -1147,7 +1001,7 @@ impl NewUser {
summary: summary.to_owned(), summary: summary.to_owned(),
summary_html: SafeString::new(&utils::md_to_html(&summary, None, false, None).0), summary_html: SafeString::new(&utils::md_to_html(&summary, None, false, None).0),
email: Some(email), email: Some(email),
hashed_password: password, hashed_password: Some(password),
instance_id: instance.id, instance_id: instance.id,
public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?, public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?,
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?), private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
@@ -1168,27 +1022,14 @@ impl NewUser {
} }
} }
#[derive(Clone, Debug)]
pub enum UserEvent {
RemoteUserFound(Arc<User>),
}
impl From<UserEvent> for Arc<User> {
fn from(event: UserEvent) -> Self {
use UserEvent::*;
match event {
RemoteUserFound(user) => user,
}
}
}
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use super::*; use super::*;
use crate::{ use crate::{
config::CONFIG,
instance::{tests as instance_tests, Instance}, instance::{tests as instance_tests, Instance},
tests::db, search::tests::get_searcher,
tests::{db, rockets},
Connection as Conn, Connection as Conn,
}; };
use diesel::Connection; use diesel::Connection;
@@ -1202,7 +1043,7 @@ pub(crate) mod tests {
Role::Admin, Role::Admin,
"Hello there, I'm the admin", "Hello there, I'm the admin",
"admin@example.com".to_owned(), "admin@example.com".to_owned(),
Some("invalid_admin_password".to_owned()), "invalid_admin_password".to_owned(),
) )
.unwrap(); .unwrap();
let user = NewUser::new_local( let user = NewUser::new_local(
@@ -1212,7 +1053,7 @@ pub(crate) mod tests {
Role::Normal, Role::Normal,
"Hello there, I'm no one", "Hello there, I'm no one",
"user@example.com".to_owned(), "user@example.com".to_owned(),
Some("invalid_user_password".to_owned()), "invalid_user_password".to_owned(),
) )
.unwrap(); .unwrap();
let other = NewUser::new_local( let other = NewUser::new_local(
@@ -1222,7 +1063,7 @@ pub(crate) mod tests {
Role::Normal, Role::Normal,
"Hello there, I'm someone else", "Hello there, I'm someone else",
"other@example.com".to_owned(), "other@example.com".to_owned(),
Some("invalid_other_password".to_owned()), "invalid_other_password".to_owned(),
) )
.unwrap(); .unwrap();
vec![admin, user, other] vec![admin, user, other]
@@ -1230,37 +1071,38 @@ pub(crate) mod tests {
#[test] #[test]
fn find_by() { fn find_by() {
let conn = db(); let r = rockets();
let conn = &*r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
fill_database(&conn); fill_database(conn);
let test_user = NewUser::new_local( let test_user = NewUser::new_local(
&conn, conn,
"test".to_owned(), "test".to_owned(),
"test user".to_owned(), "test user".to_owned(),
Role::Normal, Role::Normal,
"Hello I'm a test", "Hello I'm a test",
"test@example.com".to_owned(), "test@example.com".to_owned(),
Some(User::hash_pass("test_password").unwrap()), User::hash_pass("test_password").unwrap(),
) )
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
test_user.id, test_user.id,
User::find_by_name(&conn, "test", Instance::get_local().unwrap().id) User::find_by_name(conn, "test", Instance::get_local().unwrap().id)
.unwrap() .unwrap()
.id .id
); );
assert_eq!( assert_eq!(
test_user.id, test_user.id,
User::find_by_fqn(&conn, &test_user.fqn).unwrap().id User::find_by_fqn(&r, &test_user.fqn).unwrap().id
); );
assert_eq!( assert_eq!(
test_user.id, test_user.id,
User::find_by_email(&conn, "test@example.com").unwrap().id User::find_by_email(conn, "test@example.com").unwrap().id
); );
assert_eq!( assert_eq!(
test_user.id, test_user.id,
User::find_by_ap_url( User::find_by_ap_url(
&conn, conn,
&format!( &format!(
"https://{}/@/{}/", "https://{}/@/{}/",
Instance::get_local().unwrap().public_domain, Instance::get_local().unwrap().public_domain,
@@ -1278,11 +1120,13 @@ pub(crate) mod tests {
fn delete() { fn delete() {
let conn = &db(); let conn = &db();
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let inserted = fill_database(&conn); let inserted = fill_database(conn);
assert!(User::get(&conn, inserted[0].id).is_ok()); assert!(User::get(conn, inserted[0].id).is_ok());
inserted[0].delete(&conn).unwrap(); inserted[0]
assert!(User::get(&conn, inserted[0].id).is_err()); .delete(conn, &get_searcher(&CONFIG.search_tokenizers))
.unwrap();
assert!(User::get(conn, inserted[0].id).is_err());
Ok(()) Ok(())
}); });
} }
@@ -1291,20 +1135,20 @@ pub(crate) mod tests {
fn admin() { fn admin() {
let conn = &db(); let conn = &db();
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let inserted = fill_database(&conn); let inserted = fill_database(conn);
let local_inst = Instance::get_local().unwrap(); let local_inst = Instance::get_local().unwrap();
let mut i = 0; let mut i = 0;
while local_inst.has_admin(&conn).unwrap() { while local_inst.has_admin(conn).unwrap() {
assert!(i < 100); //prevent from looping indefinitelly assert!(i < 100); //prevent from looping indefinitelly
local_inst local_inst
.main_admin(&conn) .main_admin(conn)
.unwrap() .unwrap()
.set_role(&conn, Role::Normal) .set_role(conn, Role::Normal)
.unwrap(); .unwrap();
i += 1; i += 1;
} }
inserted[0].set_role(&conn, Role::Admin).unwrap(); inserted[0].set_role(conn, Role::Admin).unwrap();
assert_eq!(inserted[0].id, local_inst.main_admin(&conn).unwrap().id); assert_eq!(inserted[0].id, local_inst.main_admin(conn).unwrap().id);
Ok(()) Ok(())
}); });
} }
@@ -1313,23 +1157,20 @@ pub(crate) mod tests {
fn auth() { fn auth() {
let conn = &db(); let conn = &db();
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
fill_database(&conn); fill_database(conn);
let test_user = NewUser::new_local( let test_user = NewUser::new_local(
&conn, conn,
"test".to_owned(), "test".to_owned(),
"test user".to_owned(), "test user".to_owned(),
Role::Normal, Role::Normal,
"Hello I'm a test", "Hello I'm a test",
"test@example.com".to_owned(), "test@example.com".to_owned(),
Some(User::hash_pass("test_password").unwrap()), User::hash_pass("test_password").unwrap(),
) )
.unwrap(); .unwrap();
assert_eq!( assert!(test_user.auth("test_password"));
User::login(&conn, "test", "test_password").unwrap().id, assert!(!test_user.auth("other_password"));
test_user.id
);
assert!(User::login(&conn, "test", "other_password").is_err());
Ok(()) Ok(())
}); });
} }
@@ -1338,26 +1179,26 @@ pub(crate) mod tests {
fn get_local_page() { fn get_local_page() {
let conn = &db(); let conn = &db();
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
fill_database(&conn); fill_database(conn);
let page = User::get_local_page(&conn, (0, 2)).unwrap(); let page = User::get_local_page(conn, (0, 2)).unwrap();
assert_eq!(page.len(), 2); assert_eq!(page.len(), 2);
assert!(page[0].username <= page[1].username); assert!(page[0].username <= page[1].username);
let mut last_username = User::get_local_page(&conn, (0, 1)).unwrap()[0] let mut last_username = User::get_local_page(conn, (0, 1)).unwrap()[0]
.username .username
.clone(); .clone();
for i in 1..User::count_local(&conn).unwrap() as i32 { for i in 1..User::count_local(conn).unwrap() as i32 {
let page = User::get_local_page(&conn, (i, i + 1)).unwrap(); let page = User::get_local_page(conn, (i, i + 1)).unwrap();
assert_eq!(page.len(), 1); assert_eq!(page.len(), 1);
assert!(last_username <= page[0].username); assert!(last_username <= page[0].username);
last_username = page[0].username.clone(); last_username = page[0].username.clone();
} }
assert_eq!( assert_eq!(
User::get_local_page(&conn, (0, User::count_local(&conn).unwrap() as i32 + 10)) User::get_local_page(conn, (0, User::count_local(conn).unwrap() as i32 + 10))
.unwrap() .unwrap()
.len() as i64, .len() as i64,
User::count_local(&conn).unwrap() User::count_local(conn).unwrap()
); );
Ok(()) Ok(())
}); });
@@ -1365,13 +1206,14 @@ pub(crate) mod tests {
#[test] #[test]
fn self_federation() { fn self_federation() {
let conn = db(); let r = rockets();
let conn = &*r.conn;
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let users = fill_database(&conn); let users = fill_database(conn);
let ap_repr = users[0].to_activity(&conn).unwrap(); let ap_repr = users[0].to_activity(conn).unwrap();
users[0].delete(&conn).unwrap(); users[0].delete(conn, &*r.searcher).unwrap();
let user = User::from_activity(&conn, ap_repr).unwrap(); let user = User::from_activity(&r, ap_repr).unwrap();
assert_eq!(user.username, users[0].username); assert_eq!(user.username, users[0].username);
assert_eq!(user.display_name, users[0].display_name); assert_eq!(user.display_name, users[0].display_name);
@@ -1382,7 +1224,7 @@ pub(crate) mod tests {
assert_eq!(user.public_key, users[0].public_key); assert_eq!(user.public_key, users[0].public_key);
assert_eq!(user.shared_inbox_url, users[0].shared_inbox_url); assert_eq!(user.shared_inbox_url, users[0].shared_inbox_url);
assert_eq!(user.followers_endpoint, users[0].followers_endpoint); assert_eq!(user.followers_endpoint, users[0].followers_endpoint);
assert_eq!(user.avatar_url(&conn), users[0].avatar_url(&conn)); assert_eq!(user.avatar_url(conn), users[0].avatar_url(conn));
assert_eq!(user.fqn, users[0].fqn); assert_eq!(user.fqn, users[0].fqn);
assert_eq!(user.summary_html, users[0].summary_html); assert_eq!(user.summary_html, users[0].summary_html);
Ok(()) Ok(())
+12 -18
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n" "PO-Revision-Date: 2019-12-16 21:06\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Afrikaans\n" "Language-Team: Afrikaans\n"
"Language: af_ZA\n" "Language: af_ZA\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: af\n" "X-Crowdin-Language: af\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+12 -18
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n" "PO-Revision-Date: 2019-12-16 21:06\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Arabic\n" "Language-Team: Arabic\n"
"Language: ar_SA\n" "Language: ar_SA\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\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" "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: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: ar\n" "X-Crowdin-Language: ar\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "فتح محرر النصوص الغني" msgstr "فتح محرر النصوص الغني"
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "العنوان" msgstr "العنوان"
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "العنوان الثانوي أو الملخص" msgstr "العنوان الثانوي أو الملخص"
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "اكتب مقالك هنا. ماركداون مُدَعَّم." msgstr "اكتب مقالك هنا. ماركداون مُدَعَّم."
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "يتبقا {} حرفا تقريبا" msgstr "يتبقا {} حرفا تقريبا"
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "الوسوم" msgstr "الوسوم"
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "الرخصة" msgstr "الرخصة"
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "الغلاف" msgstr "الغلاف"
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "هذه مسودة" msgstr "هذه مسودة"
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "نشر كتابا" msgstr "نشر كتابا"
+12 -18
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n" "PO-Revision-Date: 2019-12-16 21:06\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Bulgarian\n" "Language-Team: Bulgarian\n"
"Language: bg_BG\n" "Language: bg_BG\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: bg\n" "X-Crowdin-Language: bg\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Отворете редактора с богат текст" msgstr "Отворете редактора с богат текст"
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "Заглавие" msgstr "Заглавие"
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Подзаглавие или резюме" msgstr "Подзаглавие или резюме"
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Напишете статията си тук. Поддържа се Markdown." msgstr "Напишете статията си тук. Поддържа се Markdown."
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Остават {} знака вляво" msgstr "Остават {} знака вляво"
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "Етикети" msgstr "Етикети"
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "Лиценз" msgstr "Лиценз"
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "Основно изображение" msgstr "Основно изображение"
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "Това е проект" msgstr "Това е проект"
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "Публикувай" msgstr "Публикувай"
+12 -18
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n" "PO-Revision-Date: 2019-12-16 21:06\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Catalan\n" "Language-Team: Catalan\n"
"Language: ca_ES\n" "Language: ca_ES\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: ca\n" "X-Crowdin-Language: ca\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Obre leditor de text enriquit" msgstr "Obre leditor de text enriquit"
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "Títol" msgstr "Títol"
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Subtítol o resum" msgstr "Subtítol o resum"
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Escriviu el vostre article ací. Podeu fer servir el Markdown." msgstr "Escriviu el vostre article ací. Podeu fer servir el Markdown."
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Queden uns {} caràcters" msgstr "Queden uns {} caràcters"
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "Etiquetes" msgstr "Etiquetes"
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "Llicència" msgstr "Llicència"
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "Coberta" msgstr "Coberta"
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "Açò és un esborrany" msgstr "Açò és un esborrany"
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "Publica" msgstr "Publica"
+12 -18
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n" "PO-Revision-Date: 2019-12-16 21:06\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Czech\n" "Language-Team: Czech\n"
"Language: cs_CZ\n" "Language: cs_CZ\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n" "Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: cs\n" "X-Crowdin-Language: cs\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Otevřít editor formátovaného textu" msgstr "Otevřít editor formátovaného textu"
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "Nadpis" msgstr "Nadpis"
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Podnadpis, nebo shrnutí" msgstr "Podnadpis, nebo shrnutí"
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Sem napište svůj článek. Markdown je podporován." msgstr "Sem napište svůj článek. Markdown je podporován."
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Zbývá kolem {} znaků" msgstr "Zbývá kolem {} znaků"
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "Tagy" msgstr "Tagy"
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "Licence" msgstr "Licence"
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "Titulka" msgstr "Titulka"
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "Tohle je koncept" msgstr "Tohle je koncept"
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "Zveřejnit" msgstr "Zveřejnit"
+4 -8
View File
@@ -10,17 +10,12 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=6; plural=(n == 0) ? 0 : ((n == 1) ? 1 : ((n == 2) ? " "Plural-Forms: nplurals=6; plural=(n == 0) ? 0 : ((n == 1) ? 1 : ((n == 2) ? 2 : ((n == 3) ? 3 : ((n == 6) ? 4 : 5))));\n"
"2 : ((n == 3) ? 3 : ((n == 6) ? 4 : 5))));\n"
"X-Generator: crowdin.com\n" "X-Generator: crowdin.com\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Language: cy\n" "X-Crowdin-Language: cy\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\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 # plume-front/src/editor.rs:114
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
@@ -29,8 +24,8 @@ msgstr ""
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:147
msgid "Subtitle, or summary" msgid "Subtitle or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:154 # plume-front/src/editor.rs:154
@@ -60,3 +55,4 @@ msgstr ""
# plume-front/src/editor.rs:259 # plume-front/src/editor.rs:259
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+12 -18
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n" "PO-Revision-Date: 2019-12-16 21:06\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Danish\n" "Language-Team: Danish\n"
"Language: da_DK\n" "Language: da_DK\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: da\n" "X-Crowdin-Language: da\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+12 -18
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n" "PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: German\n" "Language-Team: German\n"
"Language: de_DE\n" "Language: de_DE\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: de\n" "X-Crowdin-Language: de\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr " Rich Text Editor (RTE) öffnen" msgstr " Rich Text Editor (RTE) öffnen"
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "Titel" msgstr "Titel"
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Untertitel oder Zusammenfassung" msgstr "Untertitel oder Zusammenfassung"
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Schreiben deinen Artikel hier. Markdown wird unterstützt." msgstr "Schreiben deinen Artikel hier. Markdown wird unterstützt."
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Ungefähr {} Zeichen übrig" msgstr "Ungefähr {} Zeichen übrig"
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "Schlagwörter" msgstr "Schlagwörter"
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "Lizenz" msgstr "Lizenz"
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "Einband" msgstr "Einband"
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "Dies ist ein Entwurf" msgstr "Dies ist ein Entwurf"
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "Veröffentlichen" msgstr "Veröffentlichen"
+12 -18
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n" "PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Greek\n" "Language-Team: Greek\n"
"Language: el_GR\n" "Language: el_GR\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: el\n" "X-Crowdin-Language: el\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+12 -18
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n" "PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: English\n" "Language-Team: English\n"
"Language: en_US\n" "Language: en_US\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: en\n" "X-Crowdin-Language: en\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+12 -18
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n" "PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Esperanto\n" "Language-Team: Esperanto\n"
"Language: eo_UY\n" "Language: eo_UY\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: eo\n" "X-Crowdin-Language: eo\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Malfermi la riĉan redaktilon" msgstr "Malfermi la riĉan redaktilon"
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "Titolo" msgstr "Titolo"
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Verku vian artikolon ĉi tie. Markdown estas subtenita." msgstr "Verku vian artikolon ĉi tie. Markdown estas subtenita."
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Proksimume {} signoj restantaj" msgstr "Proksimume {} signoj restantaj"
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "Etikedoj" msgstr "Etikedoj"
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "Permesilo" msgstr "Permesilo"
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "Kovro" msgstr "Kovro"
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "Malfinias" msgstr "Malfinias"
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "Eldoni" msgstr "Eldoni"
+13 -19
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:56\n" "PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Spanish\n" "Language-Team: Spanish\n"
"Language: es_ES\n" "Language: es_ES\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: es-ES\n" "X-Crowdin-Language: es-ES\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Abrir el editor de texto enriquecido" msgstr "Abrir el editor de texto enriquecido"
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "Título" msgstr "Título"
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Subtítulo, o resumen" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Escriba su artículo aquí. Puede utilizar Markdown." msgstr "Escriba su artículo aquí. Puede utilizar Markdown."
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Quedan unos {} caracteres" msgstr "Quedan unos {} caracteres"
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "Etiquetas" msgstr "Etiquetas"
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "Licencia" msgstr "Licencia"
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "Cubierta" msgstr "Cubierta"
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "Esto es un borrador" msgstr "Esto es un borrador"
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "Publicar" msgstr "Publicar"
+14 -20
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:56\n" "PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Persian\n" "Language-Team: Persian\n"
"Language: fa_IR\n" "Language: fa_IR\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: fa\n" "X-Crowdin-Language: fa\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "باز کردن ویرایش‌گر غنی" msgstr "باز کردن ویرایش‌گر غنی"
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "عنوان" msgstr "عنوان"
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "زیرعنوان، یا چکیده" msgstr "زیرعنوان، یا چکیده"
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "مقاله را اینجا بنویسید. از مارک‌داون پشتیبانی می‌شود." msgstr "مقاله را اینجا بنویسید. از مارک‌داون پشتیبانی می‌شود."
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "نزدیک به {} حرف باقی مانده است" msgstr "نزدیک به {} نویسه باقی مانده است"
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "برچسب‌ها" msgstr "برچسب‌ها"
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "پروانه" msgstr "پروانه"
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "تصویر شاخص" msgstr ""
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "این، یک پیش‌نویس است" msgstr "این، یک پیش‌نویس است"
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "انتشار" msgstr "انتشار"
+12 -18
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n" "PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Finnish\n" "Language-Team: Finnish\n"
"Language: fi_FI\n" "Language: fi_FI\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: fi\n" "X-Crowdin-Language: fi\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Avaa edistynyt tekstieditori" msgstr "Avaa edistynyt tekstieditori"
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "Otsikko" msgstr "Otsikko"
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Alaotsikko tai tiivistelmä" msgstr "Alaotsikko tai tiivistelmä"
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Kirjoita artikkelisi tähän. Markdown -kuvauskieli on tuettu." msgstr "Kirjoita artikkelisi tähän. Markdown -kuvauskieli on tuettu."
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "%{count} merkkiä jäljellä" msgstr "%{count} merkkiä jäljellä"
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "Tagit" msgstr "Tagit"
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "Lisenssi" msgstr "Lisenssi"
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "Kansi" msgstr "Kansi"
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "Tämä on luonnos" msgstr "Tämä on luonnos"
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "Julkaise" msgstr "Julkaise"
+13 -19
View File
@@ -3,61 +3,55 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n" "PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: French\n" "Language-Team: French\n"
"Language: fr_FR\n" "Language: fr_FR\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: fr\n" "X-Crowdin-Language: fr\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Ouvrir l'éditeur de texte avancé" msgstr "Ouvrir l'éditeur de texte avancé"
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "Titre" msgstr "Titre"
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Sous-titre ou résumé" msgstr "Sous-titre ou résumé"
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Écrivez votre article ici. Vous pouvez utiliser du Markdown." msgstr "Écrivez votre article ici. Vous pouvez utiliser du Markdown."
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Environ {} caractères restant" msgstr "Environ {} caractères restant"
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "Étiquettes" msgstr "Étiquettes"
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "Licence" msgstr "Licence"
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "Illustration" msgstr "Illustration"
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "Ceci est un brouillon" msgstr "Ceci est un brouillon"
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "Publier" msgstr "Publier"
+15 -21
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n" "PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Galician\n" "Language-Team: Galician\n"
"Language: gl_ES\n" "Language: gl_ES\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: gl\n" "X-Crowdin-Language: gl\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Abre o editor de texto enriquecido" msgstr "Abra o editor de texto enriquecido"
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "Título" msgstr "Título"
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Subtítulo, ou resumo" msgstr "Subtítulo, ou resumo"
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Escribe aquí o teu artigo: podes utilizar Markdown." msgstr "Escriba aquí o seu artigo: pode utilizar Markdown."
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Dispós de {} caracteres" msgstr "Dispón de {} caracteres restantes"
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "Etiquetas" msgstr "Etiquetas"
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "Licenza" msgstr "Licenza"
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "Portada" msgstr "Portada"
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "Este é un borrador" msgstr "Este é un borrador"
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "Publicar" msgstr "Publicar"
+12 -18
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n" "PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Hebrew\n" "Language-Team: Hebrew\n"
"Language: he_IL\n" "Language: he_IL\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\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" "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: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: he\n" "X-Crowdin-Language: he\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+12 -18
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n" "PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Hindi\n" "Language-Team: Hindi\n"
"Language: hi_IN\n" "Language: hi_IN\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: hi\n" "X-Crowdin-Language: hi\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "शीर्षक" msgstr "शीर्षक"
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "अपना आर्टिकल या लेख यहाँ लिखें. Markdown उपलब्ध है." msgstr "अपना आर्टिकल या लेख यहाँ लिखें. Markdown उपलब्ध है."
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "लगभग {} अक्षर बाकी हैं" msgstr "लगभग {} अक्षर बाकी हैं"
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "टैग्स" msgstr "टैग्स"
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "लाइसेंस" msgstr "लाइसेंस"
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "पब्लिश करें" msgstr "पब्लिश करें"
+12 -18
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n" "PO-Revision-Date: 2019-12-16 21:06\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Croatian\n" "Language-Team: Croatian\n"
"Language: hr_HR\n" "Language: hr_HR\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\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" "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: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: hr\n" "X-Crowdin-Language: hr\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "Naslov" msgstr "Naslov"
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "Tagovi" msgstr "Tagovi"
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "Licenca" msgstr "Licenca"
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "Objavi" msgstr "Objavi"
+12 -18
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n" "PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Hungarian\n" "Language-Team: Hungarian\n"
"Language: hu_HU\n" "Language: hu_HU\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: hu\n" "X-Crowdin-Language: hu\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+12 -18
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n" "PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Italian\n" "Language-Team: Italian\n"
"Language: it_IT\n" "Language: it_IT\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: it\n" "X-Crowdin-Language: it\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Apri il compositore di testo avanzato" msgstr "Apri il compositore di testo avanzato"
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "Titolo" msgstr "Titolo"
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Sottotitolo, o sommario" msgstr "Sottotitolo, o sommario"
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Scrivi qui il tuo articolo. È supportato il Markdown." msgstr "Scrivi qui il tuo articolo. È supportato il Markdown."
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Circa {} caratteri rimasti" msgstr "Circa {} caratteri rimasti"
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "Etichette" msgstr "Etichette"
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "Licenza" msgstr "Licenza"
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "Copertina" msgstr "Copertina"
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "Questa è una bozza" msgstr "Questa è una bozza"
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "Pubblica" msgstr "Pubblica"
+12 -18
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n" "PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Japanese\n" "Language-Team: Japanese\n"
"Language: ja_JP\n" "Language: ja_JP\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n" "Plural-Forms: nplurals=1; plural=0;\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: ja\n" "X-Crowdin-Language: ja\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "リッチテキストエディターを開く" msgstr "リッチテキストエディターを開く"
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "タイトル" msgstr "タイトル"
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "サブタイトル、または概要" msgstr "サブタイトル、または概要"
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "投稿をここに書きます。Markdown がサポートされています。" msgstr "投稿をここに書きます。Markdown がサポートされています。"
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "残り約 {} 文字" msgstr "残り約 {} 文字"
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "タグ" msgstr "タグ"
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "ライセンス" msgstr "ライセンス"
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "カバー" msgstr "カバー"
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "これは下書きです" msgstr "これは下書きです"
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "公開" msgstr "公開"
+12 -18
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:56\n" "PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Korean\n" "Language-Team: Korean\n"
"Language: ko_KR\n" "Language: ko_KR\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n" "Plural-Forms: nplurals=1; plural=0;\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: ko\n" "X-Crowdin-Language: ko\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
-4
View File
@@ -12,10 +12,6 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\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 # plume-front/src/editor.rs:115
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
+22 -28
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n" "PO-Revision-Date: 2019-12-16 21:06\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Dutch\n" "Language-Team: Dutch\n"
"Language: nl_NL\n" "Language: nl_NL\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: nl\n" "X-Crowdin-Language: nl\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:282 # plume-front/src/editor.rs:145
msgid "Open the rich text editor"
msgstr "Open de rich-text editor"
# plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Titel" msgstr ""
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Ondertitel of samenvatting" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Schrijf hier je artikel. Markdown wordt ondersteund." msgstr ""
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Ongeveer {} tekens over" msgstr ""
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "Tags" msgstr ""
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "Licentie" msgstr ""
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "Hoofdafbeelding" msgstr ""
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "Dit is een concept" msgstr ""
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "Publiceren" msgstr ""
+12 -18
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:56\n" "PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Norwegian\n" "Language-Team: Norwegian\n"
"Language: no_NO\n" "Language: no_NO\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: no\n" "X-Crowdin-Language: no\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+12 -18
View File
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:56\n" "PO-Revision-Date: 2019-12-16 21:07\n"
"Last-Translator: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Polish\n" "Language-Team: Polish\n"
"Language: pl_PL\n" "Language: pl_PL\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,52 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\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" "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: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: pl\n" "X-Crowdin-Language: pl\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Otwórz edytor tekstu sformatowanego" msgstr "Otwórz edytor tekstu sformatowanego"
# plume-front/src/editor.rs:315 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "Tytuł" msgstr "Tytuł"
# plume-front/src/editor.rs:319 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Podtytuł, lub podsumowanie" msgstr "Podtytuł, lub podsumowanie"
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Napisz swój artykuł tutaj. Markdown jest obsługiwany." msgstr "Napisz swój artykuł tutaj. Markdown jest obsługiwany."
# plume-front/src/editor.rs:337 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Pozostało w okolicy {} znaków" msgstr "Pozostało w okolicy {} znaków"
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "Tagi" msgstr "Tagi"
# plume-front/src/editor.rs:415 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "Licencja" msgstr "Licencja"
# plume-front/src/editor.rs:418 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "Okładka" msgstr "Okładka"
# plume-front/src/editor.rs:438 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "To jest szkic" msgstr "To jest szkic"
# plume-front/src/editor.rs:445 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "Publikuj" msgstr "Publikuj"
+10 -14
View File
@@ -12,46 +12,42 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
# plume-front/src/editor.rs:188 # plume-front/src/editor.rs:115
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:281
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:314 # plume-front/src/editor.rs:145
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:318 # plume-front/src/editor.rs:149
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:325 # plume-front/src/editor.rs:156
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:336 # plume-front/src/editor.rs:167
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:413 # plume-front/src/editor.rs:243
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:414 # plume-front/src/editor.rs:244
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:417 # plume-front/src/editor.rs:247
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:437 # plume-front/src/editor.rs:267
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:444 # plume-front/src/editor.rs:274
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""

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