Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ddba17d46 | ||
|
|
b04426a330 | ||
|
|
925254983e | ||
|
|
4c6fb83793 | ||
|
|
7c456009be | ||
|
|
db916039db | ||
|
|
06c625c686 | ||
|
|
f2203710cb | ||
|
|
3c1617c4f9 | ||
|
|
2388a5846d | ||
|
|
b102534136 | ||
|
|
072e32da30 | ||
|
|
5ea3e73727 | ||
|
|
f340bd50c7 | ||
|
|
3de6b46465 | ||
|
|
3c6d5de314 | ||
|
|
2a4b98dce4 | ||
|
|
d253fee523 | ||
|
|
07731d0b73 | ||
|
|
15cbd17003 | ||
|
|
5d3b3485fa | ||
|
|
8a2788bf6a | ||
|
|
ecbd64efb1 | ||
|
|
9245320712 | ||
|
|
7cf3a4b37c | ||
|
|
3ddd6d0254 | ||
|
|
7edd0220b6 | ||
|
|
b26e785277 | ||
|
|
b2829908f1 | ||
|
|
60bb5b72f6 | ||
|
|
9e0bbf81ed |
@ -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",
|
|
||||||
]
|
|
||||||
@ -10,8 +10,8 @@ executors:
|
|||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
docker:
|
docker:
|
||||||
- image: plumeorg/plume-buildenv:v0.8.0
|
- image: plumeorg/plume-buildenv:v0.0.9
|
||||||
- image: <<#parameters.postgres>>cimg/postgres:14.2<</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
|
||||||
POSTGRES_DB: plume
|
POSTGRES_DB: plume
|
||||||
@ -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:
|
||||||
@ -38,7 +37,7 @@ commands:
|
|||||||
- restore_cache:
|
- restore_cache:
|
||||||
keys:
|
keys:
|
||||||
- v0-<< parameters.cache >>-{{ checksum "Cargo.lock" }}-{{ .Branch }}
|
- v0-<< parameters.cache >>-{{ checksum "Cargo.lock" }}-{{ .Branch }}
|
||||||
- v0-<< parameters.cache >>-{{ checksum "Cargo.lock" }}-main
|
- v0-<< parameters.cache >>-{{ checksum "Cargo.lock" }}-master
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
description: push cache
|
description: push cache
|
||||||
@ -63,7 +62,6 @@ commands:
|
|||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
steps:
|
steps:
|
||||||
- run: rustup component add clippy --toolchain nightly-2022-07-19-x86_64-unknown-linux-gnu
|
|
||||||
- run: cargo clippy <<^parameters.no_feature>>--no-default-features --features="${FEATURES}"<</parameters.no_feature>> --release -p <<parameters.package>> -- -D warnings
|
- run: cargo clippy <<^parameters.no_feature>>--no-default-features --features="${FEATURES}"<</parameters.no_feature>> --release -p <<parameters.package>> -- -D warnings
|
||||||
|
|
||||||
run_with_coverage:
|
run_with_coverage:
|
||||||
@ -73,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 >>
|
||||||
|
|
||||||
@ -101,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
|
||||||
@ -112,7 +110,6 @@ jobs:
|
|||||||
name: default
|
name: default
|
||||||
steps:
|
steps:
|
||||||
- restore_env
|
- restore_env
|
||||||
- run: rustup component add rustfmt --toolchain nightly-2022-07-19-x86_64-unknown-linux-gnu
|
|
||||||
- run: cargo fmt --all -- --check
|
- run: cargo fmt --all -- --check
|
||||||
|
|
||||||
clippy:
|
clippy:
|
||||||
@ -146,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:
|
||||||
@ -170,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
|
||||||
@ -205,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
|
||||||
@ -260,4 +254,4 @@ workflows:
|
|||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
- /^main/
|
- /^master/
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
localhost {
|
localhost:443 {
|
||||||
reverse_proxy localhost:7878
|
proxy / integration:7878 {
|
||||||
|
transparent
|
||||||
|
}
|
||||||
|
tls self_signed
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,24 +1,21 @@
|
|||||||
FROM rust:1
|
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 &&\
|
|
||||||
wget -qO - https://artifacts.crowdin.com/repo/GPG-KEY-crowdin | apt-key add - &&\
|
|
||||||
echo "deb https://artifacts.crowdin.com/repo/deb/ /" > /etc/apt/sources.list.d/crowdin.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-dev python3-pip python3-setuptools zip unzip libclang-dev clang caddy crowdin3 &&\
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
#stick rust environment
|
#install and configure rust
|
||||||
COPY rust-toolchain ./
|
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2020-01-15 -y &&\
|
||||||
RUN rustup component add rustfmt clippy
|
rustup component add rustfmt clippy &&\
|
||||||
|
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/* &&\
|
||||||
rm -fr ~/.cargo/registry
|
rm -fr ~/.cargo/registry
|
||||||
|
|
||||||
#set some compilation parametters
|
#set some compilation parametters
|
||||||
@ -27,5 +24,14 @@ 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
|
||||||
|
RUN mkdir /crowdin && cd /crowdin &&\
|
||||||
|
curl -O https://downloads.crowdin.com/cli/v2/crowdin-cli.zip &&\
|
||||||
|
unzip crowdin-cli.zip && rm crowdin-cli.zip &&\
|
||||||
|
cd * && mv crowdin-cli.jar /usr/local/bin && cd && rm -rf /crowdin &&\
|
||||||
|
/bin/echo -e '#!/bin/sh\njava -jar /usr/local/bin/crowdin-cli.jar $@' > /usr/local/bin/crowdin &&\
|
||||||
|
chmod +x /usr/local/bin/crowdin
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
nightly-2022-07-19
|
|
||||||
@ -3,5 +3,3 @@ data
|
|||||||
Dockerfile
|
Dockerfile
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
.env
|
.env
|
||||||
target
|
|
||||||
data
|
|
||||||
|
|||||||
233
.drone.jsonnet
Normal file
233
.drone.jsonnet
Normal 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
.env.sample
12
.env.sample
@ -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=info
|
|
||||||
|
|
||||||
# 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
.github/ISSUE_TEMPLATE/bug_report.md
vendored
10
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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 -->
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
9
.github/ISSUE_TEMPLATE/feature_request.md
vendored
9
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -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 [...]
|
||||||
|
|
||||||
|
|||||||
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@ -1,6 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: cargo
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: daily
|
|
||||||
7
.github/pull_request_template.md
vendored
7
.github/pull_request_template.md
vendored
@ -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.
|
|
||||||
-->
|
|
||||||
30
.github/workflows/deploy-docker-latest.yaml
vendored
30
.github/workflows/deploy-docker-latest.yaml
vendored
@ -1,30 +0,0 @@
|
|||||||
name: cd
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'main'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
docker:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
-
|
|
||||||
name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v2
|
|
||||||
-
|
|
||||||
name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
-
|
|
||||||
name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
-
|
|
||||||
name: Build and push
|
|
||||||
id: docker_build
|
|
||||||
uses: docker/build-push-action@v3
|
|
||||||
with:
|
|
||||||
push: true
|
|
||||||
tags: plumeorg/plume:latest
|
|
||||||
36
.github/workflows/deploy-docker-tag.yaml
vendored
36
.github/workflows/deploy-docker-tag.yaml
vendored
@ -1,36 +0,0 @@
|
|||||||
name: cd
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- '*.*.*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
docker:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
-
|
|
||||||
name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v2
|
|
||||||
-
|
|
||||||
name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
-
|
|
||||||
name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v3
|
|
||||||
with:
|
|
||||||
images: plumeorg/plume
|
|
||||||
-
|
|
||||||
name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
-
|
|
||||||
name: Build and push
|
|
||||||
id: docker_build
|
|
||||||
uses: docker/build-push-action@v3
|
|
||||||
with:
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -18,7 +18,3 @@ tags.*
|
|||||||
search_index
|
search_index
|
||||||
.buildconfig
|
.buildconfig
|
||||||
__pycache__
|
__pycache__
|
||||||
.vscode/
|
|
||||||
*-journal
|
|
||||||
.direnv/
|
|
||||||
build.log*
|
|
||||||
|
|||||||
306
CHANGELOG.md
306
CHANGELOG.md
@ -1,306 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
<!-- next-header -->
|
|
||||||
|
|
||||||
## [Unreleased] - ReleaseDate
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Add 'My feed' to i18n timeline name (#1084)
|
|
||||||
- Bidirectional support for user page header (#1092)
|
|
||||||
- Add non anonymous bind to LDAP server, taken from https://git.joinplu.me/Plume/Plume/src/branch/ldap-non-anon PR
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Use blog title as slug (#1094, #1126, #1127)
|
|
||||||
- Bump Rust to nightly 2022-07-19 (#1119)
|
|
||||||
- Force LDAP simple bind with *cn* rdn instead of *uid*
|
|
||||||
- Update rust-toolchain to nightly-2023-04-14
|
|
||||||
- Update chrono from 0.4.0 to 0.4.31
|
|
||||||
- Update scheduled-thread-pool from 0.2.6 to 0.2.7
|
|
||||||
- Change blockquote color to `lightpurple`
|
|
||||||
- Some colour has been added to the tables to make them more visible
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Malfunction while creating a blog post in Persian (#1116)
|
|
||||||
- Email block list is ignored when email sign-up (#1122)
|
|
||||||
- Bug that some Activity Sytreams properties are not parsed properly (#1129)
|
|
||||||
- Allow empty avatar for remote users (#1129)
|
|
||||||
- Percent encode blog FQN for federation interoperability (#1129)
|
|
||||||
- The same to `preferredUsername` (#1129)
|
|
||||||
- Deprecation warnings during build process(see rust crate updates)
|
|
||||||
- Server error 500 creating new blog with white spaces inside title. Bug reported on https://git.joinplu.me/Plume/Plume/issues/1152
|
|
||||||
- Show _Subscribe_ button in column format instead of row format in screen smaller than 600px. https://git.lainoa.eus/aitzol/Plume/commit/db8cc6e7e8351a5d74f7ce0399126e13493c62d9
|
|
||||||
|
|
||||||
### To do
|
|
||||||
|
|
||||||
- Choose rdn via environment variables for LDAP simple bind
|
|
||||||
|
|
||||||
## [[0.7.2]] - 2022-05-11
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Basque language (#1013)
|
|
||||||
- Unit tests for ActivityPub (#1021)
|
|
||||||
- Move to action area after liking/boosting/commenting (#1074)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Bump Rust to nightly 2022-01-26 (#1015)
|
|
||||||
- Remove "Latest articles" timeline (#1069)
|
|
||||||
- Change order of timeline tabs (#1069, #1070, #1072)
|
|
||||||
- Migrate ActivityPub-related crates from activitypub 0.1 to activitystreams 0.7 (#1022)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Add explanation of sign-up step at sign-up page when email sign-up mode (#1012)
|
|
||||||
- Add NOT NULL constraint to email_blocklist table fields (#1016)
|
|
||||||
- Don't fill empty content when switching rich editor (#1017)
|
|
||||||
- Fix accept header (#1058)
|
|
||||||
- Render 404 page instead of 500 when data is not found (#1062)
|
|
||||||
- Reuse reqwest client on broadcasting (#1059)
|
|
||||||
- Reduce broadcasting HTTP request at once to prevent them being timed out (#1068, #1071)
|
|
||||||
- Some ActivityPub data (#1021)
|
|
||||||
|
|
||||||
## [[0.7.1]] - 2022-01-12
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Introduce environment variable `MAIL_PORT` (#980)
|
|
||||||
- Introduce email sign-up feature (#636, #1002)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Some styling improvements (#976, #977, #978)
|
|
||||||
- Respond with error status code when error (#1002)
|
|
||||||
|
|
||||||
### Fiexed
|
|
||||||
|
|
||||||
- Fix comment link (#974)
|
|
||||||
- Fix a bug that prevents posting articles (#975)
|
|
||||||
- Fix a bug that notification page doesn't show (#981)
|
|
||||||
|
|
||||||
## [[0.7.0]] - 2022-01-02
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Allow `dir` attributes for LtoR text in RtoL document (#860)
|
|
||||||
- More translation languages (#862)
|
|
||||||
- Proxy support (#829)
|
|
||||||
- Riker a actor system library (#870)
|
|
||||||
- (request-target) and Host header in HTTP Signature (#872)
|
|
||||||
- Default log levels for RUST_LOG (#885, #886, #919)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Upgrade some dependent crates (#858)
|
|
||||||
- Use tracing crate (#868)
|
|
||||||
- Update Rust version to nightly-2021-11-27 (#961)
|
|
||||||
- Upgrade Tantivy to 0.13.3 and lindera-tantivy to 0.7.1 (#878)
|
|
||||||
- Run searcher on actor system (#870)
|
|
||||||
- Extract a function to calculate posts' ap_url and share it with some places (#918)
|
|
||||||
- Use article title as its slug instead of capitalizing and inserting hyphens (#920)
|
|
||||||
- Sign GET requests to other instances (#957)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Percent-encode URI for remote_interact (#866, #857)
|
|
||||||
- Menu animation not opening on iOS (#876, #897)
|
|
||||||
- Make actors subscribe to channel once (#913)
|
|
||||||
- Upsert posts and media instead of trying to insert and fail (#912)
|
|
||||||
- Update post's ActivityPub id when published by update (#915)
|
|
||||||
- Calculate media URI properly even when MEDIA_UPLOAD_DIRECTORY configured (#916)
|
|
||||||
- Prevent duplicated posts in 'all' timeline (#917)
|
|
||||||
- Draw side line for blockquote on start (#933)
|
|
||||||
- Fix URIs of posts on Mastodon (#947)
|
|
||||||
- Place edit link proper position (#956, #963, #964)
|
|
||||||
|
|
||||||
## [[0.6.0]] - 2020-12-29
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Vazir font for better support of languages written in Arabic script (#787)
|
|
||||||
- Login via LDAP (#826)
|
|
||||||
- cargo-release (#835)
|
|
||||||
- Care about weak ETag header for better caching (#840)
|
|
||||||
- Support for right to left languages in post content (#853)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Bump Docker base images to Buster flavor (#797)
|
|
||||||
- Upgrade Rocket to 0.4.5 (#800)
|
|
||||||
- Keep tags as-is (#832)
|
|
||||||
- Update Docker image for testing (#838)
|
|
||||||
- Update Dockerfile.dev (#841)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Recreate search index if its format is outdated (#802)
|
|
||||||
- Make it possible to switch to rich text editor (#808)
|
|
||||||
- Fix margins for the mobile devices (#817)
|
|
||||||
- GPU acceleration for the mobile menu (#818)
|
|
||||||
- Natural title position for RtoL languages (#825)
|
|
||||||
- Remove link to unimplemented page (#827)
|
|
||||||
- Fix displaying not found page when submitting a duplicated blocklist email (#831)
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Validate spoofing of activity
|
|
||||||
|
|
||||||
## [0.5.0] - 2020-06-21
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Email blocklisting (#718)
|
|
||||||
- Syntax highlighting (#691)
|
|
||||||
- Persian localization (#782)
|
|
||||||
- Switchable tokenizer - enables Japanese full-text search (#776)
|
|
||||||
- Make database connections configurable by environment variables (#768)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Display likes and boost on post cards (#744)
|
|
||||||
- Rust 2018 (#726)
|
|
||||||
- Bump to LLVM to 9.0.0 to fix ARM builds (#737)
|
|
||||||
- Remove dependency on runtime-fmt (#773)
|
|
||||||
- Drop the -alpha suffix in release names, it is implied that Plume is not stable yet because of the 0 major version (Plume 1.0.0 will be the first stable release).
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix parsing of mentions inside a Markdown code block (be430c6)
|
|
||||||
- Fix RSS issues (#720)
|
|
||||||
- Fix Atom feed (#764)
|
|
||||||
- Fix default theme (#746)
|
|
||||||
- Fix shown password on remote interact pages (#741)
|
|
||||||
- Allow unicode hashtags (#757)
|
|
||||||
- Fix French grammar for for 0 (#760)
|
|
||||||
- Don't show boosts and likes for "all" and "local" in timelines (#781)
|
|
||||||
- Fix liking and boosting posts on remote instances (#762)
|
|
||||||
|
|
||||||
## [0.4.0] - 2019-12-23
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Add support for generic timeline (#525)
|
|
||||||
- Federate user deletion (#551)
|
|
||||||
- import migrations and don't require diesel_cli for admins (#555)
|
|
||||||
- Cache local instance (#572)
|
|
||||||
- Initial RTL support #575 (#577)
|
|
||||||
- Confirm deletion of blog (#602)
|
|
||||||
- Make a distinction between moderators and admins (#619)
|
|
||||||
- Theming (#624)
|
|
||||||
- Add clap to plume in order to print help and version (#631)
|
|
||||||
- Add Snapcraft metadata and install/maintenance hooks (#666)
|
|
||||||
- Add environmental variable to control path of media (#683)
|
|
||||||
- Add autosaving to the editor (#688)
|
|
||||||
- CI: Upload artifacts to pull request deploy environment (#539)
|
|
||||||
- CI: Upload artifact of wasm binary (#571)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Update follow_remote.rs.html grammar (#548)
|
|
||||||
- Add some feedback when performing some actions (#552)
|
|
||||||
- Theme update (#553)
|
|
||||||
- Remove the new index lock tantivy uses (#556)
|
|
||||||
- Reduce reqwest timeout to 5s (#557)
|
|
||||||
- Improve notification management (#561)
|
|
||||||
- Fix occurrences of 'have been' to 'has been' (#578) + Direct follow-up to #578 (#603)
|
|
||||||
- Store password reset requests in database (#610)
|
|
||||||
- Use futures and tokio to send activities (#620)
|
|
||||||
- Don't ignore dotenv errors (#630)
|
|
||||||
- Replace the input! macro with an Input builder (#646)
|
|
||||||
- Update default license (#659)
|
|
||||||
- Paginate the outbox responses. Fixes #669 (#681)
|
|
||||||
- Use the "classic" editor by default (#697)
|
|
||||||
- Fix issue #705 (#708)
|
|
||||||
- Make comments in styleshhets a bit clearer (#545)
|
|
||||||
- Rewrite circleci config (#558)
|
|
||||||
- Use openssl instead of sha256sum for build.rs (#568)
|
|
||||||
- Update dependencies (#574)
|
|
||||||
- Refactor code to use Shrinkwraprs and diesel-derive-newtype (#598)
|
|
||||||
- Add enum containing all successful route returns (#614)
|
|
||||||
- Update dependencies which depended on nix -- fixes arm32 builds (#615)
|
|
||||||
- Update some documents (#616)
|
|
||||||
- Update dependencies (#643)
|
|
||||||
- Make the comment syntax consistent across all CSS (#487)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Remove r (#535)
|
|
||||||
- Fix certain improper rendering of forms (#560)
|
|
||||||
- make hashtags work in profile summary (#562)
|
|
||||||
- Fix some federation issue (#573)
|
|
||||||
- Prevent comment form submit button distortion on iOS (#592)
|
|
||||||
- Update textarea overflow to scroll (#609)
|
|
||||||
- Fix arm builds (#612)
|
|
||||||
- Fix theme caching (#647)
|
|
||||||
- Fix issue #642, frontend not in English if the user language does not exist (#648)
|
|
||||||
- Don't index drafts (#656)
|
|
||||||
- Fill entirely user on creation (#657)
|
|
||||||
- Delete notification on user deletion (#658)
|
|
||||||
- Order media so that latest added are top (#660)
|
|
||||||
- Fix logo URL (#664)
|
|
||||||
- Snap: Ensure cargo-web doesn't erroneously adopt our workspace. (#667)
|
|
||||||
- Snap: Another fix for building (#668)
|
|
||||||
- Snap: Fix build for non-Tier-1 Rust platforms (#672)
|
|
||||||
- Don't split sentences for translations (#677)
|
|
||||||
- Escape href quotation marks (#678)
|
|
||||||
- Re-add empty strings in translation (#682)
|
|
||||||
- Make the search index creation during migration respect SEARCH_INDEX (#689)
|
|
||||||
- Fix the navigation menu not opening on touch (#690)
|
|
||||||
- Make search items optional (#693)
|
|
||||||
- Various snap fixes (#698)
|
|
||||||
- Fix #637 : Markdown footnotes (#700)
|
|
||||||
- Fix lettre (#706)
|
|
||||||
- CI: Fix Crowdin upload (#576)
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
- Remove the Canapi dependency (#540)
|
|
||||||
- Remove use of Rust in migrations (#704)
|
|
||||||
|
|
||||||
## [0.3.0] - 2019-04-19
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Cover for articles (#299, #387)
|
|
||||||
- Password reset (#448)
|
|
||||||
- New editor (#293, #458, #482, #483, #486, #530)
|
|
||||||
- Search (#324, #375, #445)
|
|
||||||
- Edit blogs (#460, #494, #497)
|
|
||||||
- Hashtags in articles (#283, #295)
|
|
||||||
- API endpoints (#245, #285, #307)
|
|
||||||
- A bunch of new translations! (#479, #501, #506, #510, #512, #514)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Federation improvements (#216, #217, #357, #364, #399, #443, #446, #455, #502, #519)
|
|
||||||
- Improved build process (#281, #374, #392, #402, #489, #498, #503, #511, #513, #515, #528)
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
- UI usability fixes (#370, #386, #401, #417, #418, #444, #452, #480, #516, #518, #522, #532)
|
|
||||||
|
|
||||||
## [0.2.0] - 2018-09-12
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Article publishing, or save as a draft
|
|
||||||
- Like, or boost an article
|
|
||||||
- Basic Markdown editor
|
|
||||||
- Federated commenting system
|
|
||||||
- User account creation
|
|
||||||
- Limited federation on other platforms and subscribing to users
|
|
||||||
- Ability to create multiple blogs
|
|
||||||
|
|
||||||
<!-- next-url -->
|
|
||||||
[Unreleased]: https://github.com/Plume-org/Plume/compare/0.7.2...HEAD
|
|
||||||
[[0.7.2]]: https://github.com/Plume-org/Plume/compare/0.7.1...0.7.2
|
|
||||||
[[0.7.1]]: https://github.com/Plume-org/Plume/compare/0.7.0...0.7.1
|
|
||||||
[[0.7.0]]: https://github.com/Plume-org/Plume/compare/0.6.0...0.7.0
|
|
||||||
[[0.6.0]]: https://github.com/Plume-org/Plume/compare/0.5.0...0.6.0
|
|
||||||
[0.5.0]: https://github.com/Plume-org/Plume/compare/0.4.0-alpha-4...0.5.0
|
|
||||||
[0.4.0]: https://github.com/Plume-org/Plume/compare/0.3.0-alpha-2...0.4.0-alpha-4
|
|
||||||
[0.3.0]: https://github.com/Plume-org/Plume/compare/0.2.0-alpha-1...0.3.0-alpha-2
|
|
||||||
[0.2.0]: https://github.com/Plume-org/Plume/releases/tag/0.2.0-alpha-1
|
|
||||||
6458
Cargo.lock
generated
6458
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
69
Cargo.toml
69
Cargo.toml
@ -1,34 +1,38 @@
|
|||||||
[package]
|
[package]
|
||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
name = "plume"
|
name = "plume"
|
||||||
version = "0.7.3-dev-fork"
|
version = "0.4.0"
|
||||||
repository = "https://git.lainoa.eus/aitzol/Plume"
|
repository = "https://github.com/Plume-org/Plume"
|
||||||
edition = "2021"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
atom_syndication = "0.12.0"
|
activitypub = "0.1.3"
|
||||||
|
askama_escape = "0.1"
|
||||||
|
atom_syndication = "0.6"
|
||||||
clap = "2.33"
|
clap = "2.33"
|
||||||
dotenv = "0.15.0"
|
colored = "1.8"
|
||||||
gettext = "0.4.0"
|
dotenv = "0.14"
|
||||||
gettext-macros = "0.6.1"
|
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
||||||
gettext-utils = "0.1.0"
|
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||||
guid-create = "0.4.1"
|
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||||
conv = "0.3.3"
|
guid-create = "0.1"
|
||||||
|
heck = "0.3.0"
|
||||||
|
lettre = "0.9.2"
|
||||||
lettre_email = "0.9.2"
|
lettre_email = "0.9.2"
|
||||||
num_cpus = "1.16.0"
|
num_cpus = "1.10"
|
||||||
rocket = "0.4.11"
|
rocket = "0.4.2"
|
||||||
rocket_contrib = { version = "0.4.11", features = ["json"] }
|
rocket_contrib = { version = "0.4.2", features = ["json"] }
|
||||||
rocket_i18n = "0.4.1"
|
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||||
scheduled-thread-pool = "0.2.7"
|
rpassword = "4.0"
|
||||||
serde = "1.0.137"
|
scheduled-thread-pool = "0.2.2"
|
||||||
serde_json = "1.0.81"
|
serde = "1.0"
|
||||||
shrinkwraprs = "0.3.0"
|
serde_json = "1.0"
|
||||||
validator = { version = "0.15", features = ["derive"] }
|
serde_qs = "0.5"
|
||||||
|
shrinkwraprs = "0.2.1"
|
||||||
|
syntect = "3.3"
|
||||||
|
validator = "0.8"
|
||||||
|
validator_derive = "0.8"
|
||||||
webfinger = "0.4.1"
|
webfinger = "0.4.1"
|
||||||
tracing = "0.1.35"
|
|
||||||
tracing-subscriber = "0.3.10"
|
|
||||||
riker = "0.4.2"
|
|
||||||
activitystreams = "=0.7.0-alpha.20"
|
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "plume"
|
name = "plume"
|
||||||
@ -36,20 +40,20 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
version = "0.4.31"
|
version = "0.4"
|
||||||
|
|
||||||
[dependencies.ctrlc]
|
[dependencies.ctrlc]
|
||||||
features = ["termination"]
|
features = ["termination"]
|
||||||
version = "3.2.2"
|
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
|
||||||
features = ["server"]
|
features = ["server"]
|
||||||
version = "0.18"
|
version = "0.16"
|
||||||
|
|
||||||
[dependencies.plume-api]
|
[dependencies.plume-api]
|
||||||
path = "plume-api"
|
path = "plume-api"
|
||||||
@ -61,21 +65,20 @@ path = "plume-common"
|
|||||||
path = "plume-models"
|
path = "plume-models"
|
||||||
|
|
||||||
[dependencies.rocket_csrf]
|
[dependencies.rocket_csrf]
|
||||||
git = "https://git.joinplu.me/plume/rocket_csrf"
|
git = "https://github.com/fdb-hiroshima/rocket_csrf"
|
||||||
rev = "0.1.2"
|
rev = "29910f2829e7e590a540da3804336577b48c7b31"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
ructe = "0.15.0"
|
ructe = "0.9.0"
|
||||||
rsass = "0.26"
|
rsass = "0.9"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["postgres", "s3"]
|
default = ["postgres"]
|
||||||
postgres = ["plume-models/postgres", "diesel/postgres"]
|
postgres = ["plume-models/postgres", "diesel/postgres"]
|
||||||
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
|
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
|
||||||
debug-mailer = []
|
debug-mailer = []
|
||||||
test = []
|
test = []
|
||||||
search-lindera = ["plume-models/search-lindera"]
|
search-lindera = ["plume-models/search-lindera"]
|
||||||
s3 = ["plume-models/s3"]
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["plume-api", "plume-cli", "plume-models", "plume-common", "plume-front", "plume-macro"]
|
members = ["plume-api", "plume-cli", "plume-models", "plume-common", "plume-front", "plume-macro"]
|
||||||
|
|||||||
14
Dockerfile
14
Dockerfile
@ -1,4 +1,4 @@
|
|||||||
FROM rust:latest 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 \
|
||||||
@ -13,27 +13,27 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
libssl-dev \
|
libssl-dev \
|
||||||
clang
|
clang
|
||||||
|
|
||||||
RUN ln -s /usr/bin/python3 /usr/bin/python & \
|
|
||||||
ln -s /usr/bin/pip3 /usr/bin/pip
|
|
||||||
|
|
||||||
WORKDIR /scratch
|
WORKDIR /scratch
|
||||||
COPY script/wasm-deps.sh .
|
COPY script/wasm-deps.sh .
|
||||||
RUN chmod a+x ./wasm-deps.sh && sleep 1 && ./wasm-deps.sh
|
RUN chmod a+x ./wasm-deps.sh && sleep 1 && ./wasm-deps.sh
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
COPY Cargo.toml Cargo.lock rust-toolchain ./
|
||||||
|
RUN cargo install cargo-web
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN cargo install wasm-pack
|
|
||||||
RUN chmod a+x ./script/plume-front.sh && sleep 1 && ./script/plume-front.sh
|
RUN chmod a+x ./script/plume-front.sh && sleep 1 && ./script/plume-front.sh
|
||||||
RUN cargo install --path ./ --force --no-default-features --features postgres
|
RUN cargo install --path ./ --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:stable-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 \
|
||||||
libpq5
|
libpq5 \
|
||||||
|
libssl1.1
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@ -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,10 +1,10 @@
|
|||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
<img src="https://raw.githubusercontent.com/Plume-org/Plume/main/assets/icons/trwnh/feather/plumeFeather64.png" alt="Plume's logo">
|
<img src="https://raw.githubusercontent.com/Plume-org/Plume/master/assets/icons/trwnh/feather/plumeFeather64.png" alt="Plume's logo">
|
||||||
Plume
|
Plume
|
||||||
</h1>
|
</h1>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/Plume-org/Plume/"><img alt="CircleCI" src="https://img.shields.io/circleci/build/gh/Plume-org/Plume.svg"></a>
|
<a href="https://github.com/Plume-org/Plume/"><img alt="CircleCI" src="https://img.shields.io/circleci/build/gh/Plume-org/Plume.svg"></a>
|
||||||
<a href="https://codecov.io/gh/Plume-org/Plume"><img src="https://codecov.io/gh/Plume-org/Plume/branch/main/graph/badge.svg" alt="Code coverage"></a>
|
<a href="https://codecov.io/gh/Plume-org/Plume"><img src="https://codecov.io/gh/Plume-org/Plume/branch/master/graph/badge.svg" alt="Code coverage"></a>
|
||||||
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/plume"><img src="https://d322cqt584bo4o.cloudfront.net/plume/localized.svg"></a>
|
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/plume"><img src="https://d322cqt584bo4o.cloudfront.net/plume/localized.svg"></a>
|
||||||
<a href="https://hub.docker.com/r/plumeorg/plume"><img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/plumeorg/plume.svg"></a>
|
<a href="https://hub.docker.com/r/plumeorg/plume"><img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/plumeorg/plume.svg"></a>
|
||||||
<a href="https://liberapay.com/Plume"><img alt="Liberapay patrons" src="https://img.shields.io/liberapay/patrons/Plume.svg"></a>
|
<a href="https://liberapay.com/Plume"><img alt="Liberapay patrons" src="https://img.shields.io/liberapay/patrons/Plume.svg"></a>
|
||||||
@ -30,11 +30,11 @@ A lot of features are still missing, but what is already here should be quite st
|
|||||||
- **Media management**: you can upload pictures to illustrate your articles, but also audio files if you host a podcast, and manage them all from Plume.
|
- **Media management**: you can upload pictures to illustrate your articles, but also audio files if you host a podcast, and manage them all from Plume.
|
||||||
- **Federation**: Plume is part of a network of interconnected websites called the Fediverse. Each of these websites (often called *instances*) have their own
|
- **Federation**: Plume is part of a network of interconnected websites called the Fediverse. Each of these websites (often called *instances*) have their own
|
||||||
rules and thematics, but they can all communicate with each other.
|
rules and thematics, but they can all communicate with each other.
|
||||||
- **Collaborative writing**: invite other people to your blogs, and write articles together. (Not implemented yet, but will be in 1.0)
|
- **Collaborative writing**: invite other people to your blogs, and write articles together.
|
||||||
|
|
||||||
## 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:
|
||||||
|
|
||||||
@ -53,4 +53,3 @@ As we want the various spaces related to the project (GitHub, Matrix, Loomio, et
|
|||||||
|
|
||||||
We provide various way to install Plume: from source, with pre-built binaries, with Docker or with YunoHost.
|
We provide various way to install Plume: from source, with pre-built binaries, with Docker or with YunoHost.
|
||||||
For detailed explanations, please refer to [the documentation](https://docs.joinplu.me/installation/).
|
For detailed explanations, please refer to [the documentation](https://docs.joinplu.me/installation/).
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
* {
|
* {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,41 +64,41 @@ 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 {
|
||||||
border-inline-start: 5px solid $lightpurple;
|
border-left: 5px solid $gray;
|
||||||
margin: 1em auto;
|
margin: 1em auto;
|
||||||
padding: 0em 2em;
|
padding: 0em 2em;
|
||||||
}
|
}
|
||||||
@ -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 {
|
||||||
@ -228,7 +228,7 @@ main .article-meta {
|
|||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
}
|
}
|
||||||
.action.liked:hover svg.feather {
|
.action.liked:hover svg.feather {
|
||||||
background: transparentize($red, 0.75);
|
background: transparentize($red, 0.75)
|
||||||
color: $red;
|
color: $red;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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,37 +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%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-bar {
|
|
||||||
align-items: center;
|
|
||||||
& > div:nth-child(2) {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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,27 +115,26 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button + .button {
|
.button + .button {
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
margin-inline-start: 1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.split {
|
.split {
|
||||||
|
|||||||
@ -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,68 +213,38 @@ p.error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cover-link {
|
.cover {
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cover {
|
|
||||||
min-height: 10em;
|
min-height: 10em;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.10;
|
transition: color 0.1s ease-in;
|
||||||
display: inline-block;
|
color: $text-color;
|
||||||
position: relative;
|
|
||||||
a {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding-block-start: 0.5em;
|
|
||||||
transition: color 0.1s ease-in;
|
|
||||||
color: $text-color;
|
|
||||||
|
|
||||||
&:hover { color: $primary; }
|
&:hover { color: $primary; }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
flex-shrink: 0;
|
|
||||||
text-align: end;
|
|
||||||
|
|
||||||
.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: initial;
|
text-align: left;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,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 {
|
||||||
@ -479,10 +447,9 @@ figure {
|
|||||||
|
|
||||||
/// Avatars
|
/// Avatars
|
||||||
.avatar {
|
.avatar {
|
||||||
background-position: center !important;
|
background-position: center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
&.small {
|
&.small {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
@ -507,7 +474,6 @@ figure {
|
|||||||
margin: auto $horizontal-margin 2em;
|
margin: auto $horizontal-margin 2em;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@ -524,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%;
|
||||||
@ -573,7 +535,15 @@ figure {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
max-width: 100% !important;
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
& > div {
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,48 +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;
|
||||||
|
|
||||||
&.right-nav {
|
hr {
|
||||||
overflow-x: hidden;
|
height: 100%;
|
||||||
}
|
width: 0.2em;
|
||||||
|
background: $primary;
|
||||||
hr {
|
border: none;
|
||||||
height: 100%;
|
transform: skewX(-15deg);
|
||||||
width: 0.2em;
|
|
||||||
background: $primary;
|
|
||||||
border: none;
|
|
||||||
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;
|
||||||
@ -74,7 +70,7 @@ body > header {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 0.5em;
|
padding-left: 0.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -119,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;
|
||||||
@ -148,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;
|
||||||
@ -209,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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
/* tables */
|
|
||||||
table, td, th, tr {
|
|
||||||
border: 1px dotted $lightpurple;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr:nth-child(even){background-color: $gray;}
|
|
||||||
@ -12,4 +12,3 @@
|
|||||||
@import "header";
|
@import "header";
|
||||||
@import "article";
|
@import "article";
|
||||||
@import "forms";
|
@import "forms";
|
||||||
@import "tables";
|
|
||||||
|
|||||||
@ -12,4 +12,3 @@
|
|||||||
@import "header";
|
@import "header";
|
||||||
@import "article";
|
@import "article";
|
||||||
@import "forms";
|
@import "forms";
|
||||||
@import "tables";
|
|
||||||
|
|||||||
42
build.rs
42
build.rs
@ -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::*};
|
||||||
@ -41,20 +43,26 @@ fn main() {
|
|||||||
.expect("compile templates");
|
.expect("compile templates");
|
||||||
|
|
||||||
compile_themes().expect("Theme compilation error");
|
compile_themes().expect("Theme compilation error");
|
||||||
recursive_copy(&Path::new("assets").join("icons"), Path::new("static"))
|
recursive_copy(&Path::new("assets").join("icons"), &Path::new("static"))
|
||||||
.expect("Couldn't copy icons");
|
.expect("Couldn't copy icons");
|
||||||
recursive_copy(&Path::new("assets").join("images"), Path::new("static"))
|
recursive_copy(&Path::new("assets").join("images"), &Path::new("static"))
|
||||||
.expect("Couldn't copy images");
|
.expect("Couldn't copy images");
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -97,12 +105,12 @@ fn compile_theme(path: &Path, out_dir: &Path) -> std::io::Result<()> {
|
|||||||
.components()
|
.components()
|
||||||
.skip_while(|c| *c != Component::Normal(OsStr::new("themes")))
|
.skip_while(|c| *c != Component::Normal(OsStr::new("themes")))
|
||||||
.skip(1)
|
.skip(1)
|
||||||
.map(|c| {
|
.filter_map(|c| {
|
||||||
c.as_os_str()
|
c.as_os_str()
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.split_once('.')
|
.splitn(2, '.')
|
||||||
.map_or(c.as_os_str().to_str().unwrap_or_default(), |x| x.0)
|
.next()
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("-");
|
.join("-");
|
||||||
@ -120,14 +128,8 @@ fn compile_theme(path: &Path, out_dir: &Path) -> std::io::Result<()> {
|
|||||||
// compile the .scss/.sass file
|
// compile the .scss/.sass file
|
||||||
let mut out = File::create(out.join("theme.css"))?;
|
let mut out = File::create(out.join("theme.css"))?;
|
||||||
out.write_all(
|
out.write_all(
|
||||||
&rsass::compile_scss_path(
|
&rsass::compile_scss_file(path, rsass::OutputStyle::Compressed)
|
||||||
path,
|
.expect("SCSS compilation error"),
|
||||||
rsass::output::Format {
|
|
||||||
style: rsass::output::Style::Compressed,
|
|
||||||
..rsass::output::Format::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("SCSS compilation error"),
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
116
flake.lock
generated
116
flake.lock
generated
@ -1,116 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681202837,
|
|
||||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-utils_2": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems_2"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681202837,
|
|
||||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1683408522,
|
|
||||||
"narHash": "sha256-9kcPh6Uxo17a3kK3XCHhcWiV1Yu1kYj22RHiymUhMkU=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "897876e4c484f1e8f92009fd11b7d988a121a4e7",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": "nixpkgs",
|
|
||||||
"rust-overlay": "rust-overlay"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rust-overlay": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": "flake-utils_2",
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1683857898,
|
|
||||||
"narHash": "sha256-pyVY4UxM6zUX97g6bk6UyCbZGCWZb2Zykrne8YxacRA=",
|
|
||||||
"owner": "oxalica",
|
|
||||||
"repo": "rust-overlay",
|
|
||||||
"rev": "4e7fba3f37f5e184ada0ef3cf1e4d8ef450f240b",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "oxalica",
|
|
||||||
"repo": "rust-overlay",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems_2": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
||||||
60
flake.nix
60
flake.nix
@ -1,60 +0,0 @@
|
|||||||
{
|
|
||||||
description = "Developpment shell for Plume including nightly Rust compiler";
|
|
||||||
|
|
||||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
|
||||||
inputs.rust-overlay = {
|
|
||||||
url = "github:oxalica/rust-overlay";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
|
||||||
|
|
||||||
outputs = { self, nixpkgs, flake-utils, rust-overlay, ... }:
|
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
|
||||||
let
|
|
||||||
overlays = [ (import rust-overlay) ];
|
|
||||||
pkgs = import nixpkgs { inherit system overlays; };
|
|
||||||
inputs = with pkgs; [
|
|
||||||
(rust-bin.nightly.latest.default.override {
|
|
||||||
targets = [ "wasm32-unknown-unknown" ];
|
|
||||||
})
|
|
||||||
wasm-pack
|
|
||||||
openssl
|
|
||||||
pkg-config
|
|
||||||
gettext
|
|
||||||
postgresql
|
|
||||||
sqlite
|
|
||||||
];
|
|
||||||
in {
|
|
||||||
packages.default = pkgs.rustPlatform.buildRustPackage {
|
|
||||||
pname = "plume";
|
|
||||||
version = "0.7.3-dev";
|
|
||||||
|
|
||||||
src = ./.;
|
|
||||||
|
|
||||||
cargoLock = {
|
|
||||||
lockFile = ./Cargo.lock;
|
|
||||||
outputHashes = {
|
|
||||||
"pulldown-cmark-0.8.0" = "sha256-lpfoRDuY3zJ3QmUqJ5k9OL0MEdGDpwmpJ+u5BCj2kIA=";
|
|
||||||
"rocket_csrf-0.1.2" = "sha256-WywZfMiwZqTPfSDcAE7ivTSYSaFX+N9fjnRsLSLb9wE=";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
buildNoDefaultFeatures = true;
|
|
||||||
buildFeatures = ["postgresql" "s3"];
|
|
||||||
|
|
||||||
nativeBuildInputs = inputs;
|
|
||||||
|
|
||||||
buildPhase = ''
|
|
||||||
wasm-pack build --target web --release plume-front
|
|
||||||
cargo build --no-default-features --features postgresql,s3 --path .
|
|
||||||
cargo build --no-default-features --features postgresql,s3 --path plume-cli
|
|
||||||
'';
|
|
||||||
installPhase = ''
|
|
||||||
cargo install --no-default-features --features postgresql,s3 --path . --target-dir $out
|
|
||||||
cargo install --no-default-features --features postgresql,s3 --path plume-cli --target-dir $out
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
devShells.default = pkgs.mkShell {
|
|
||||||
packages = inputs;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
DROP INDEX medias_index_file_path;
|
|
||||||
@ -1 +0,0 @@
|
|||||||
CREATE INDEX medias_index_file_path ON medias (file_path);
|
|
||||||
@ -1 +0,0 @@
|
|||||||
DROP TABLE email_signups;
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
CREATE TABLE email_signups (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
email VARCHAR NOT NULL,
|
|
||||||
token VARCHAR NOT NULL,
|
|
||||||
expiration_date TIMESTAMP NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX email_signups_token ON email_signups (token);
|
|
||||||
CREATE UNIQUE INDEX email_signups_token_requests_email ON email_signups (email);
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
ALTER TABLE email_blocklist ALTER COLUMN notification_text DROP NOT NULL;
|
|
||||||
ALTER TABLE email_blocklist ALTER COLUMN notify_user DROP NOT NULL;
|
|
||||||
ALTER TABLE email_blocklist ALTER COLUMN note DROP NOT NULL;
|
|
||||||
ALTER TABLE email_blocklist ALTER COLUMN email_address DROP NOT NULL;
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
ALTER TABLE email_blocklist ALTER COLUMN email_address SET NOT NULL;
|
|
||||||
ALTER TABLE email_blocklist ALTER COLUMN note SET NOT NULL;
|
|
||||||
ALTER TABLE email_blocklist ALTER COLUMN notify_user SET NOT NULL;
|
|
||||||
ALTER TABLE email_blocklist ALTER COLUMN notification_text SET NOT NULL;
|
|
||||||
@ -1 +0,0 @@
|
|||||||
DROP INDEX medias_index_file_path;
|
|
||||||
@ -1 +0,0 @@
|
|||||||
CREATE INDEX medias_index_file_path ON medias (file_path);
|
|
||||||
@ -1 +0,0 @@
|
|||||||
DROP TABLE email_signups;
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
CREATE TABLE email_signups (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
email VARCHAR NOT NULL,
|
|
||||||
token VARCHAR NOT NULL,
|
|
||||||
expiration_date TIMESTAMP NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX email_signups_token ON email_signups (token);
|
|
||||||
CREATE UNIQUE INDEX email_signups_token_requests_email ON email_signups (email);
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
CREATE TABLE email_blocklist2(id INTEGER PRIMARY KEY,
|
|
||||||
email_address TEXT UNIQUE,
|
|
||||||
note TEXT,
|
|
||||||
notify_user BOOLEAN DEFAULT FALSE,
|
|
||||||
notification_text TEXT);
|
|
||||||
|
|
||||||
INSERT INTO email_blocklist2 SELECT * FROM email_blocklist;
|
|
||||||
DROP TABLE email_blocklist;
|
|
||||||
ALTER TABLE email_blocklist2 RENAME TO email_blocklist;
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
CREATE TABLE email_blocklist2(id INTEGER PRIMARY KEY,
|
|
||||||
email_address TEXT UNIQUE NOT NULL,
|
|
||||||
note TEXT NOT NULL,
|
|
||||||
notify_user BOOLEAN DEFAULT FALSE NOT NULL,
|
|
||||||
notification_text TEXT NOT NULL);
|
|
||||||
|
|
||||||
INSERT INTO email_blocklist2 SELECT * FROM email_blocklist;
|
|
||||||
DROP TABLE email_blocklist;
|
|
||||||
ALTER TABLE email_blocklist2 RENAME TO email_blocklist;
|
|
||||||
@ -1,9 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "plume-api"
|
name = "plume-api"
|
||||||
version = "0.7.2"
|
version = "0.4.0"
|
||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = "1.0.137"
|
serde = "1.0"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
pre-release-hook = ["cargo", "fmt"]
|
|
||||||
pre-release-replacements = []
|
|
||||||
release = false
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "plume-cli"
|
name = "plume-cli"
|
||||||
version = "0.7.2"
|
version = "0.4.0"
|
||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
@ -10,12 +10,12 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = "2.33"
|
clap = "2.33"
|
||||||
dotenv = "0.15"
|
dotenv = "0.14"
|
||||||
rpassword = "6.0.1"
|
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"
|
||||||
@ -24,4 +24,3 @@ path = "../plume-models"
|
|||||||
postgres = ["plume-models/postgres", "diesel/postgres"]
|
postgres = ["plume-models/postgres", "diesel/postgres"]
|
||||||
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
|
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
|
||||||
search-lindera = ["plume-models/search-lindera"]
|
search-lindera = ["plume-models/search-lindera"]
|
||||||
s3 = ["plume-models/s3"]
|
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
pre-release-hook = ["cargo", "fmt"]
|
|
||||||
pre-release-replacements = []
|
|
||||||
release = false
|
|
||||||
@ -68,6 +68,4 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.expect("Couldn't save instance");
|
.expect("Couldn't save instance");
|
||||||
Instance::cache_local(conn);
|
|
||||||
Instance::create_local_instance_user(conn).expect("Couldn't save local instance user");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,262 +0,0 @@
|
|||||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
|
||||||
|
|
||||||
use plume_models::{blogs::Blog, instance::Instance, lists::*, users::User, Connection};
|
|
||||||
|
|
||||||
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
|
||||||
SubCommand::with_name("lists")
|
|
||||||
.about("Manage lists")
|
|
||||||
.subcommand(
|
|
||||||
SubCommand::with_name("new")
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("name")
|
|
||||||
.short("n")
|
|
||||||
.long("name")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The name of this list"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("type")
|
|
||||||
.short("t")
|
|
||||||
.long("type")
|
|
||||||
.takes_value(true)
|
|
||||||
.help(
|
|
||||||
r#"The type of this list (one of "user", "blog", "word" or "prefix")"#,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("user")
|
|
||||||
.short("u")
|
|
||||||
.long("user")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Username of whom this list is for. Empty for an instance list"),
|
|
||||||
)
|
|
||||||
.about("Create a new list"),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
SubCommand::with_name("delete")
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("name")
|
|
||||||
.short("n")
|
|
||||||
.long("name")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The name of the list to delete"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("user")
|
|
||||||
.short("u")
|
|
||||||
.long("user")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Username of whom this list was for. Empty for instance list"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("yes")
|
|
||||||
.short("y")
|
|
||||||
.long("yes")
|
|
||||||
.help("Confirm the deletion"),
|
|
||||||
)
|
|
||||||
.about("Delete a list"),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
SubCommand::with_name("add")
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("name")
|
|
||||||
.short("n")
|
|
||||||
.long("name")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The name of the list to add an element to"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("user")
|
|
||||||
.short("u")
|
|
||||||
.long("user")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Username of whom this list is for. Empty for instance list"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("value")
|
|
||||||
.short("v")
|
|
||||||
.long("value")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The value to add"),
|
|
||||||
)
|
|
||||||
.about("Add element to a list"),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
SubCommand::with_name("rm")
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("name")
|
|
||||||
.short("n")
|
|
||||||
.long("name")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The name of the list to remove an element from"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("user")
|
|
||||||
.short("u")
|
|
||||||
.long("user")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Username of whom this list is for. Empty for instance list"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("value")
|
|
||||||
.short("v")
|
|
||||||
.long("value")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The value to remove"),
|
|
||||||
)
|
|
||||||
.about("Remove element from list"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
|
||||||
let conn = conn;
|
|
||||||
match args.subcommand() {
|
|
||||||
("new", Some(x)) => new(x, conn),
|
|
||||||
("delete", Some(x)) => delete(x, conn),
|
|
||||||
("add", Some(x)) => add(x, conn),
|
|
||||||
("rm", Some(x)) => rm(x, conn),
|
|
||||||
("", None) => command().print_help().unwrap(),
|
|
||||||
_ => println!("Unknown subcommand"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_list_identifier(args: &ArgMatches<'_>) -> (String, Option<String>) {
|
|
||||||
let name = args
|
|
||||||
.value_of("name")
|
|
||||||
.map(String::from)
|
|
||||||
.expect("No name provided for the list");
|
|
||||||
let user = args.value_of("user").map(String::from);
|
|
||||||
(name, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_list_type(args: &ArgMatches<'_>) -> ListType {
|
|
||||||
let typ = args
|
|
||||||
.value_of("type")
|
|
||||||
.map(String::from)
|
|
||||||
.expect("No name type for the list");
|
|
||||||
match typ.as_str() {
|
|
||||||
"user" => ListType::User,
|
|
||||||
"blog" => ListType::Blog,
|
|
||||||
"word" => ListType::Word,
|
|
||||||
"prefix" => ListType::Prefix,
|
|
||||||
_ => panic!("Invalid list type: {}", typ),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_value(args: &ArgMatches<'_>) -> String {
|
|
||||||
args.value_of("value")
|
|
||||||
.map(String::from)
|
|
||||||
.expect("No query provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_user(username: &str, conn: &Connection) -> User {
|
|
||||||
let instance = Instance::get_local_uncached(conn).expect("Failed to load local instance");
|
|
||||||
|
|
||||||
User::find_by_name(conn, username, instance.id).expect("User not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new(args: &ArgMatches<'_>, conn: &Connection) {
|
|
||||||
let (name, user) = get_list_identifier(args);
|
|
||||||
let typ = get_list_type(args);
|
|
||||||
|
|
||||||
let user = user.map(|user| resolve_user(&user, conn));
|
|
||||||
|
|
||||||
List::new(conn, &name, user.as_ref(), typ).expect("failed to create list");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete(args: &ArgMatches<'_>, conn: &Connection) {
|
|
||||||
let (name, user) = get_list_identifier(args);
|
|
||||||
|
|
||||||
if !args.is_present("yes") {
|
|
||||||
panic!("Warning, this operation is destructive. Add --yes to confirm you want to do it.")
|
|
||||||
}
|
|
||||||
|
|
||||||
let user = user.map(|user| resolve_user(&user, conn));
|
|
||||||
|
|
||||||
let list =
|
|
||||||
List::find_for_user_by_name(conn, user.map(|u| u.id), &name).expect("list not found");
|
|
||||||
|
|
||||||
list.delete(conn).expect("Failed to update list");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add(args: &ArgMatches<'_>, conn: &Connection) {
|
|
||||||
let (name, user) = get_list_identifier(args);
|
|
||||||
let value = get_value(args);
|
|
||||||
|
|
||||||
let user = user.map(|user| resolve_user(&user, conn));
|
|
||||||
|
|
||||||
let list =
|
|
||||||
List::find_for_user_by_name(conn, user.map(|u| u.id), &name).expect("list not found");
|
|
||||||
|
|
||||||
match list.kind() {
|
|
||||||
ListType::Blog => {
|
|
||||||
let blog_id = Blog::find_by_fqn(conn, &value).expect("unknown blog").id;
|
|
||||||
if !list.contains_blog(conn, blog_id).unwrap() {
|
|
||||||
list.add_blogs(conn, &[blog_id]).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ListType::User => {
|
|
||||||
let user_id = User::find_by_fqn(conn, &value).expect("unknown user").id;
|
|
||||||
if !list.contains_user(conn, user_id).unwrap() {
|
|
||||||
list.add_users(conn, &[user_id]).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ListType::Word => {
|
|
||||||
if !list.contains_word(conn, &value).unwrap() {
|
|
||||||
list.add_words(conn, &[&value]).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ListType::Prefix => {
|
|
||||||
if !list.contains_prefix(conn, &value).unwrap() {
|
|
||||||
list.add_prefixes(conn, &[&value]).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rm(args: &ArgMatches<'_>, conn: &Connection) {
|
|
||||||
let (name, user) = get_list_identifier(args);
|
|
||||||
let value = get_value(args);
|
|
||||||
|
|
||||||
let user = user.map(|user| resolve_user(&user, conn));
|
|
||||||
|
|
||||||
let list =
|
|
||||||
List::find_for_user_by_name(conn, user.map(|u| u.id), &name).expect("list not found");
|
|
||||||
|
|
||||||
match list.kind() {
|
|
||||||
ListType::Blog => {
|
|
||||||
let blog_id = Blog::find_by_fqn(conn, &value).expect("unknown blog").id;
|
|
||||||
let mut blogs = list.list_blogs(conn).unwrap();
|
|
||||||
if let Some(index) = blogs.iter().position(|b| b.id == blog_id) {
|
|
||||||
blogs.swap_remove(index);
|
|
||||||
let blogs = blogs.iter().map(|b| b.id).collect::<Vec<_>>();
|
|
||||||
list.set_blogs(conn, &blogs).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ListType::User => {
|
|
||||||
let user_id = User::find_by_fqn(conn, &value).expect("unknown user").id;
|
|
||||||
let mut users = list.list_users(conn).unwrap();
|
|
||||||
if let Some(index) = users.iter().position(|u| u.id == user_id) {
|
|
||||||
users.swap_remove(index);
|
|
||||||
let users = users.iter().map(|u| u.id).collect::<Vec<_>>();
|
|
||||||
list.set_users(conn, &users).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ListType::Word => {
|
|
||||||
let mut words = list.list_words(conn).unwrap();
|
|
||||||
if let Some(index) = words.iter().position(|w| *w == value) {
|
|
||||||
words.swap_remove(index);
|
|
||||||
let words = words.iter().map(String::as_str).collect::<Vec<_>>();
|
|
||||||
list.set_words(conn, &words).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ListType::Prefix => {
|
|
||||||
let mut prefixes = list.list_prefixes(conn).unwrap();
|
|
||||||
if let Some(index) = prefixes.iter().position(|p| *p == value) {
|
|
||||||
prefixes.swap_remove(index);
|
|
||||||
let prefixes = prefixes.iter().map(String::as_str).collect::<Vec<_>>();
|
|
||||||
list.set_prefixes(conn, &prefixes).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,13 +1,13 @@
|
|||||||
|
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};
|
||||||
use std::io::{self, prelude::*};
|
use std::io::{self, prelude::*};
|
||||||
|
|
||||||
mod instance;
|
mod instance;
|
||||||
mod list;
|
|
||||||
mod migration;
|
mod migration;
|
||||||
mod search;
|
mod search;
|
||||||
mod timeline;
|
|
||||||
mod users;
|
mod users;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@ -18,8 +18,6 @@ fn main() {
|
|||||||
.subcommand(instance::command())
|
.subcommand(instance::command())
|
||||||
.subcommand(migration::command())
|
.subcommand(migration::command())
|
||||||
.subcommand(search::command())
|
.subcommand(search::command())
|
||||||
.subcommand(timeline::command())
|
|
||||||
.subcommand(list::command())
|
|
||||||
.subcommand(users::command());
|
.subcommand(users::command());
|
||||||
let matches = app.clone().get_matches();
|
let matches = app.clone().get_matches();
|
||||||
|
|
||||||
@ -29,7 +27,7 @@ fn main() {
|
|||||||
e => e.map(|_| ()).unwrap(),
|
e => e.map(|_| ()).unwrap(),
|
||||||
}
|
}
|
||||||
let conn = Conn::establish(CONFIG.database_url.as_str());
|
let conn = Conn::establish(CONFIG.database_url.as_str());
|
||||||
let _ = conn.as_ref().map(Instance::cache_local);
|
let _ = conn.as_ref().map(|conn| Instance::cache_local(conn));
|
||||||
|
|
||||||
match matches.subcommand() {
|
match matches.subcommand() {
|
||||||
("instance", Some(args)) => {
|
("instance", Some(args)) => {
|
||||||
@ -41,10 +39,6 @@ fn main() {
|
|||||||
("search", Some(args)) => {
|
("search", Some(args)) => {
|
||||||
search::run(args, &conn.expect("Couldn't connect to the database."))
|
search::run(args, &conn.expect("Couldn't connect to the database."))
|
||||||
}
|
}
|
||||||
("timeline", Some(args)) => {
|
|
||||||
timeline::run(args, &conn.expect("Couldn't connect to the database."))
|
|
||||||
}
|
|
||||||
("lists", Some(args)) => list::run(args, &conn.expect("Couldn't connect to the database.")),
|
|
||||||
("users", Some(args)) => {
|
("users", Some(args)) => {
|
||||||
users::run(args, &conn.expect("Couldn't connect to the database."))
|
users::run(args, &conn.expect("Couldn't connect to the database."))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -1,257 +0,0 @@
|
|||||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
|
||||||
|
|
||||||
use plume_models::{instance::Instance, posts::Post, timeline::*, users::*, Connection};
|
|
||||||
|
|
||||||
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
|
||||||
SubCommand::with_name("timeline")
|
|
||||||
.about("Manage public timeline")
|
|
||||||
.subcommand(
|
|
||||||
SubCommand::with_name("new")
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("name")
|
|
||||||
.short("n")
|
|
||||||
.long("name")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The name of this timeline"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("query")
|
|
||||||
.short("q")
|
|
||||||
.long("query")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The query posts in this timelines have to match"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("user")
|
|
||||||
.short("u")
|
|
||||||
.long("user")
|
|
||||||
.takes_value(true)
|
|
||||||
.help(
|
|
||||||
"Username of whom this timeline is for. Empty for an instance timeline",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("preload-count")
|
|
||||||
.short("p")
|
|
||||||
.long("preload-count")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Number of posts to try to preload in this timeline at its creation"),
|
|
||||||
)
|
|
||||||
.about("Create a new timeline"),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
SubCommand::with_name("delete")
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("name")
|
|
||||||
.short("n")
|
|
||||||
.long("name")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The name of the timeline to delete"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("user")
|
|
||||||
.short("u")
|
|
||||||
.long("user")
|
|
||||||
.takes_value(true)
|
|
||||||
.help(
|
|
||||||
"Username of whom this timeline was for. Empty for instance timeline",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("yes")
|
|
||||||
.short("y")
|
|
||||||
.long("yes")
|
|
||||||
.help("Confirm the deletion"),
|
|
||||||
)
|
|
||||||
.about("Delete a timeline"),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
SubCommand::with_name("edit")
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("name")
|
|
||||||
.short("n")
|
|
||||||
.long("name")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The name of the timeline to edit"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("user")
|
|
||||||
.short("u")
|
|
||||||
.long("user")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Username of whom this timeline is for. Empty for instance timeline"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("query")
|
|
||||||
.short("q")
|
|
||||||
.long("query")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The query posts in this timelines have to match"),
|
|
||||||
)
|
|
||||||
.about("Edit the query of a timeline"),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
SubCommand::with_name("repopulate")
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("name")
|
|
||||||
.short("n")
|
|
||||||
.long("name")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The name of the timeline to repopulate"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("user")
|
|
||||||
.short("u")
|
|
||||||
.long("user")
|
|
||||||
.takes_value(true)
|
|
||||||
.help(
|
|
||||||
"Username of whom this timeline was for. Empty for instance timeline",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("preload-count")
|
|
||||||
.short("p")
|
|
||||||
.long("preload-count")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Number of posts to try to preload in this timeline at its creation"),
|
|
||||||
)
|
|
||||||
.about("Repopulate a timeline. Run this after modifying a list the timeline depends on."),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
|
||||||
let conn = conn;
|
|
||||||
match args.subcommand() {
|
|
||||||
("new", Some(x)) => new(x, conn),
|
|
||||||
("edit", Some(x)) => edit(x, conn),
|
|
||||||
("delete", Some(x)) => delete(x, conn),
|
|
||||||
("repopulate", Some(x)) => repopulate(x, conn),
|
|
||||||
("", None) => command().print_help().unwrap(),
|
|
||||||
_ => println!("Unknown subcommand"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_timeline_identifier(args: &ArgMatches<'_>) -> (String, Option<String>) {
|
|
||||||
let name = args
|
|
||||||
.value_of("name")
|
|
||||||
.map(String::from)
|
|
||||||
.expect("No name provided for the timeline");
|
|
||||||
let user = args.value_of("user").map(String::from);
|
|
||||||
(name, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_query(args: &ArgMatches<'_>) -> String {
|
|
||||||
let query = args
|
|
||||||
.value_of("query")
|
|
||||||
.map(String::from)
|
|
||||||
.expect("No query provided");
|
|
||||||
|
|
||||||
match TimelineQuery::parse(&query) {
|
|
||||||
Ok(_) => (),
|
|
||||||
Err(QueryError::SyntaxError(start, end, message)) => panic!(
|
|
||||||
"Query parsing error between {} and {}: {}",
|
|
||||||
start, end, message
|
|
||||||
),
|
|
||||||
Err(QueryError::UnexpectedEndOfQuery) => {
|
|
||||||
panic!("Query parsing error: unexpected end of query")
|
|
||||||
}
|
|
||||||
Err(QueryError::RuntimeError(message)) => panic!("Query parsing error: {}", message),
|
|
||||||
}
|
|
||||||
|
|
||||||
query
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_preload_count(args: &ArgMatches<'_>) -> usize {
|
|
||||||
args.value_of("preload-count")
|
|
||||||
.map(|arg| arg.parse().expect("invalid preload-count"))
|
|
||||||
.unwrap_or(plume_models::ITEMS_PER_PAGE as usize)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_user(username: &str, conn: &Connection) -> User {
|
|
||||||
let instance = Instance::get_local_uncached(conn).expect("Failed to load local instance");
|
|
||||||
|
|
||||||
User::find_by_name(conn, username, instance.id).expect("User not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn preload(timeline: Timeline, count: usize, conn: &Connection) {
|
|
||||||
timeline.remove_all_posts(conn).unwrap();
|
|
||||||
|
|
||||||
if count == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut posts = Vec::with_capacity(count as usize);
|
|
||||||
for post in Post::list_filtered(conn, None, None, None)
|
|
||||||
.unwrap()
|
|
||||||
.into_iter()
|
|
||||||
.rev()
|
|
||||||
{
|
|
||||||
if timeline.matches(conn, &post, Kind::Original).unwrap() {
|
|
||||||
posts.push(post);
|
|
||||||
if posts.len() >= count {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for post in posts.iter().rev() {
|
|
||||||
timeline.add_post(conn, post).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new(args: &ArgMatches<'_>, conn: &Connection) {
|
|
||||||
let (name, user) = get_timeline_identifier(args);
|
|
||||||
let query = get_query(args);
|
|
||||||
let preload_count = get_preload_count(args);
|
|
||||||
|
|
||||||
let user = user.map(|user| resolve_user(&user, conn));
|
|
||||||
|
|
||||||
let timeline = if let Some(user) = user {
|
|
||||||
Timeline::new_for_user(conn, user.id, name, query)
|
|
||||||
} else {
|
|
||||||
Timeline::new_for_instance(conn, name, query)
|
|
||||||
}
|
|
||||||
.expect("Failed to create new timeline");
|
|
||||||
|
|
||||||
preload(timeline, preload_count, conn);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn edit(args: &ArgMatches<'_>, conn: &Connection) {
|
|
||||||
let (name, user) = get_timeline_identifier(args);
|
|
||||||
let query = get_query(args);
|
|
||||||
|
|
||||||
let user = user.map(|user| resolve_user(&user, conn));
|
|
||||||
|
|
||||||
let mut timeline = Timeline::find_for_user_by_name(conn, user.map(|u| u.id), &name)
|
|
||||||
.expect("timeline not found");
|
|
||||||
|
|
||||||
timeline.query = query;
|
|
||||||
|
|
||||||
timeline.update(conn).expect("Failed to update timeline");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete(args: &ArgMatches<'_>, conn: &Connection) {
|
|
||||||
let (name, user) = get_timeline_identifier(args);
|
|
||||||
|
|
||||||
if !args.is_present("yes") {
|
|
||||||
panic!("Warning, this operation is destructive. Add --yes to confirm you want to do it.")
|
|
||||||
}
|
|
||||||
|
|
||||||
let user = user.map(|user| resolve_user(&user, conn));
|
|
||||||
|
|
||||||
let timeline = Timeline::find_for_user_by_name(conn, user.map(|u| u.id), &name)
|
|
||||||
.expect("timeline not found");
|
|
||||||
|
|
||||||
timeline.delete(conn).expect("Failed to update timeline");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn repopulate(args: &ArgMatches<'_>, conn: &Connection) {
|
|
||||||
let (name, user) = get_timeline_identifier(args);
|
|
||||||
let preload_count = get_preload_count(args);
|
|
||||||
|
|
||||||
let user = user.map(|user| resolve_user(&user, conn));
|
|
||||||
|
|
||||||
let timeline = Timeline::find_for_user_by_name(conn, user.map(|u| u.id), &name)
|
|
||||||
.expect("timeline not found");
|
|
||||||
preload(timeline, preload_count, conn);
|
|
||||||
}
|
|
||||||
@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,42 +1,33 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "plume-common"
|
name = "plume-common"
|
||||||
version = "0.7.2"
|
version = "0.4.0"
|
||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
activitypub = "0.1.1"
|
||||||
|
activitystreams-derive = "0.1.1"
|
||||||
|
activitystreams-traits = "0.1.0"
|
||||||
array_tool = "1.0"
|
array_tool = "1.0"
|
||||||
base64 = "0.13"
|
base64 = "0.10"
|
||||||
hex = "0.4"
|
heck = "0.3.0"
|
||||||
openssl = "0.10.40"
|
hex = "0.3"
|
||||||
rocket = "0.4.11"
|
hyper = "0.12.33"
|
||||||
reqwest = { version = "0.11.11", features = ["blocking", "json", "socks"] }
|
openssl = "0.10.22"
|
||||||
serde = "1.0.137"
|
rocket = "0.4.0"
|
||||||
|
reqwest = "0.9"
|
||||||
|
serde = "1.0"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_json = "1.0.81"
|
serde_json = "1.0"
|
||||||
shrinkwraprs = "0.3.0"
|
shrinkwraprs = "0.2.1"
|
||||||
syntect = "4.5.0"
|
syntect = "3.3"
|
||||||
regex-syntax = { version = "0.6.26", default-features = false, features = ["unicode-perl"] }
|
tokio = "0.1.22"
|
||||||
tracing = "0.1.35"
|
regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] }
|
||||||
askama_escape = "0.10.3"
|
|
||||||
activitystreams = "=0.7.0-alpha.20"
|
|
||||||
activitystreams-ext = "0.1.0-alpha.2"
|
|
||||||
url = "2.2.2"
|
|
||||||
flume = "0.10.13"
|
|
||||||
tokio = { version = "1.19.2", features = ["full"] }
|
|
||||||
futures = "0.3.25"
|
|
||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
version = "0.4.31"
|
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"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
assert-json-diff = "2.0.1"
|
|
||||||
once_cell = "1.12.0"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
pre-release-hook = ["cargo", "fmt"]
|
|
||||||
pre-release-replacements = []
|
|
||||||
release = false
|
|
||||||
@ -1,8 +1,6 @@
|
|||||||
use reqwest;
|
use reqwest::header::{HeaderValue, ACCEPT};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use super::{request, sign::Signer};
|
|
||||||
|
|
||||||
/// Represents an ActivityPub inbox.
|
/// Represents an ActivityPub inbox.
|
||||||
///
|
///
|
||||||
/// It routes an incoming Activity through the registered handlers.
|
/// It routes an incoming Activity through the registered handlers.
|
||||||
@ -10,51 +8,9 @@ use super::{request, sign::Signer};
|
|||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use activitystreams::{prelude::*, base::Base, actor::Person, activity::{Announce, Create}, object::Note, iri_string::types::IriString};
|
/// # extern crate activitypub;
|
||||||
/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
|
/// # use activitypub::{actor::Person, activity::{Announce, Create}, object::Note};
|
||||||
/// # use once_cell::sync::Lazy;
|
|
||||||
/// # use plume_common::activity_pub::inbox::*;
|
/// # use plume_common::activity_pub::inbox::*;
|
||||||
/// # use plume_common::activity_pub::sign::{gen_keypair, Error as SignError, Result as SignResult, Signer};
|
|
||||||
/// #
|
|
||||||
/// # static MY_SIGNER: Lazy<MySigner> = Lazy::new(|| MySigner::new());
|
|
||||||
/// #
|
|
||||||
/// # struct MySigner {
|
|
||||||
/// # public_key: String,
|
|
||||||
/// # private_key: String,
|
|
||||||
/// # }
|
|
||||||
/// #
|
|
||||||
/// # impl MySigner {
|
|
||||||
/// # fn new() -> Self {
|
|
||||||
/// # let (pub_key, priv_key) = gen_keypair();
|
|
||||||
/// # Self {
|
|
||||||
/// # public_key: String::from_utf8(pub_key).unwrap(),
|
|
||||||
/// # private_key: String::from_utf8(priv_key).unwrap(),
|
|
||||||
/// # }
|
|
||||||
/// # }
|
|
||||||
/// # }
|
|
||||||
/// #
|
|
||||||
/// # impl Signer for MySigner {
|
|
||||||
/// # fn get_key_id(&self) -> String {
|
|
||||||
/// # "mysigner".into()
|
|
||||||
/// # }
|
|
||||||
/// #
|
|
||||||
/// # fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> {
|
|
||||||
/// # let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
|
|
||||||
/// # .unwrap();
|
|
||||||
/// # let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
|
|
||||||
/// # signer.update(to_sign.as_bytes()).unwrap();
|
|
||||||
/// # signer.sign_to_vec().map_err(|_| SignError())
|
|
||||||
/// # }
|
|
||||||
/// #
|
|
||||||
/// # fn verify(&self, data: &str, signature: &[u8]) -> SignResult<bool> {
|
|
||||||
/// # let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
|
|
||||||
/// # .unwrap();
|
|
||||||
/// # let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
|
|
||||||
/// # verifier.update(data.as_bytes()).unwrap();
|
|
||||||
/// # verifier.verify(&signature).map_err(|_| SignError())
|
|
||||||
/// # }
|
|
||||||
/// # }
|
|
||||||
/// #
|
|
||||||
/// # struct User;
|
/// # struct User;
|
||||||
/// # impl FromId<()> for User {
|
/// # impl FromId<()> for User {
|
||||||
/// # type Error = ();
|
/// # type Error = ();
|
||||||
@ -67,10 +23,6 @@ use super::{request, sign::Signer};
|
|||||||
/// # fn from_activity(_: &(), obj: Person) -> Result<Self, Self::Error> {
|
/// # fn from_activity(_: &(), obj: Person) -> Result<Self, Self::Error> {
|
||||||
/// # Ok(User)
|
/// # Ok(User)
|
||||||
/// # }
|
/// # }
|
||||||
/// #
|
|
||||||
/// # fn get_sender() -> &'static dyn Signer {
|
|
||||||
/// # &*MY_SIGNER
|
|
||||||
/// # }
|
|
||||||
/// # }
|
/// # }
|
||||||
/// # impl AsActor<&()> for User {
|
/// # impl AsActor<&()> for User {
|
||||||
/// # fn get_inbox_url(&self) -> String {
|
/// # fn get_inbox_url(&self) -> String {
|
||||||
@ -90,10 +42,6 @@ use super::{request, sign::Signer};
|
|||||||
/// # fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> {
|
/// # fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> {
|
||||||
/// # Ok(Message)
|
/// # Ok(Message)
|
||||||
/// # }
|
/// # }
|
||||||
/// #
|
|
||||||
/// # fn get_sender() -> &'static dyn Signer {
|
|
||||||
/// # &*MY_SIGNER
|
|
||||||
/// # }
|
|
||||||
/// # }
|
/// # }
|
||||||
/// # impl AsObject<User, Create, &()> for Message {
|
/// # impl AsObject<User, Create, &()> for Message {
|
||||||
/// # type Error = ();
|
/// # type Error = ();
|
||||||
@ -112,20 +60,19 @@ use super::{request, sign::Signer};
|
|||||||
/// # }
|
/// # }
|
||||||
/// # }
|
/// # }
|
||||||
/// #
|
/// #
|
||||||
/// # let mut person = Person::new();
|
/// # let mut act = Create::default();
|
||||||
/// # person.set_id("https://test.ap/actor".parse::<IriString>().unwrap());
|
/// # act.object_props.set_id_string(String::from("https://test.ap/activity")).unwrap();
|
||||||
/// # let mut act = Create::new(
|
/// # let mut person = Person::default();
|
||||||
/// # Base::retract(person).unwrap().into_generic().unwrap(),
|
/// # person.object_props.set_id_string(String::from("https://test.ap/actor")).unwrap();
|
||||||
/// # Base::retract(Note::new()).unwrap().into_generic().unwrap()
|
/// # act.create_props.set_actor_object(person).unwrap();
|
||||||
/// # );
|
/// # act.create_props.set_object_object(Note::default()).unwrap();
|
||||||
/// # act.set_id("https://test.ap/activity".parse::<IriString>().unwrap());
|
|
||||||
/// # let activity_json = serde_json::to_value(act).unwrap();
|
/// # let activity_json = serde_json::to_value(act).unwrap();
|
||||||
/// #
|
/// #
|
||||||
/// # 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>
|
||||||
@ -197,37 +144,31 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Registers an handler on this Inbox.
|
/// Registers an handler on this Inbox.
|
||||||
pub fn with<A, V, M>(self, proxy: Option<&reqwest::Proxy>) -> Self
|
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: activitystreams::markers::Activity + serde::de::DeserializeOwned,
|
V: activitypub::Activity,
|
||||||
M: AsObject<A, V, &'a C, Error = E> + FromId<C, Error = E>,
|
M: AsObject<A, V, &'a C, Error = E> + FromId<C, Error = E>,
|
||||||
M::Output: Into<R>,
|
M::Output: Into<R>,
|
||||||
{
|
{
|
||||||
if let Self::NotHandled(ctx, mut act, e) = self {
|
if let Inbox::NotHandled(ctx, mut act, e) = self {
|
||||||
if serde_json::from_value::<V>(act.clone()).is_ok() {
|
if serde_json::from_value::<V>(act.clone()).is_ok() {
|
||||||
let act_clone = act.clone();
|
let act_clone = act.clone();
|
||||||
let act_id = match act_clone["id"].as_str() {
|
let act_id = match act_clone["id"].as_str() {
|
||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
None => return Self::NotHandled(ctx, act, InboxError::InvalidID),
|
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidID),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the actor ID
|
// Get the actor ID
|
||||||
let actor_id = match get_id(act["actor"].clone()) {
|
let actor_id = match get_id(act["actor"].clone()) {
|
||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
None => return Self::NotHandled(ctx, act, InboxError::InvalidActor(None)),
|
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(None)),
|
||||||
};
|
};
|
||||||
|
|
||||||
if Self::is_spoofed_activity(&actor_id, &act) {
|
|
||||||
return Self::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
|
||||||
@ -235,39 +176,38 @@ where
|
|||||||
if let Some(json) = json {
|
if let Some(json) = json {
|
||||||
act["actor"] = json;
|
act["actor"] = json;
|
||||||
}
|
}
|
||||||
return Self::NotHandled(ctx, act, InboxError::InvalidActor(Some(e)));
|
return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(Some(e)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Same logic for "object"
|
// Same logic for "object"
|
||||||
let obj_id = match get_id(act["object"].clone()) {
|
let obj_id = match get_id(act["object"].clone()) {
|
||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
None => return Self::NotHandled(ctx, act, InboxError::InvalidObject(None)),
|
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(None)),
|
||||||
};
|
};
|
||||||
let obj = match M::from_id(
|
let obj = match M::from_id(
|
||||||
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)) => {
|
||||||
if let Some(json) = json {
|
if let Some(json) = json {
|
||||||
act["object"] = json;
|
act["object"] = json;
|
||||||
}
|
}
|
||||||
return Self::NotHandled(ctx, act, InboxError::InvalidObject(Some(e)));
|
return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(Some(e)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle the activity
|
// Handle the activity
|
||||||
match obj.activity(ctx, actor, act_id) {
|
match obj.activity(ctx, actor, &act_id) {
|
||||||
Ok(res) => Self::Handled(res.into()),
|
Ok(res) => Inbox::Handled(res.into()),
|
||||||
Err(e) => Self::Failed(e),
|
Err(e) => Inbox::Failed(e),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If the Activity type is not matching the expected one for
|
// If the Activity type is not matching the expected one for
|
||||||
// this handler, try with the next one.
|
// this handler, try with the next one.
|
||||||
Self::NotHandled(ctx, act, e)
|
Inbox::NotHandled(ctx, act, e)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self
|
self
|
||||||
@ -282,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.
|
||||||
@ -333,7 +253,7 @@ pub trait FromId<C>: Sized {
|
|||||||
type Error: From<InboxError<Self::Error>> + Debug;
|
type Error: From<InboxError<Self::Error>> + Debug;
|
||||||
|
|
||||||
/// The ActivityPub object type representing Self
|
/// The ActivityPub object type representing Self
|
||||||
type Object: activitystreams::markers::Object + serde::de::DeserializeOwned;
|
type Object: activitypub::Object;
|
||||||
|
|
||||||
/// Tries to get an instance of `Self` from an ActivityPub ID.
|
/// Tries to get an instance of `Self` from an ActivityPub ID.
|
||||||
///
|
///
|
||||||
@ -347,26 +267,36 @@ 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()
|
||||||
request::get(id, Self::get_sender(), proxy)
|
.map_err(|_| (None, InboxError::DerefError.into()))?
|
||||||
|
.get(id)
|
||||||
|
.header(
|
||||||
|
ACCEPT,
|
||||||
|
HeaderValue::from_str(
|
||||||
|
&super::ap_accept_header()
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", "),
|
||||||
|
)
|
||||||
|
.map_err(|_| (None, InboxError::DerefError.into()))?,
|
||||||
|
)
|
||||||
|
.send()
|
||||||
.map_err(|_| (None, InboxError::DerefError))
|
.map_err(|_| (None, InboxError::DerefError))
|
||||||
.and_then(|r| {
|
.and_then(|mut r| {
|
||||||
let json: serde_json::Value = r
|
let json: serde_json::Value = r
|
||||||
.json()
|
.json()
|
||||||
.map_err(|_| (None, InboxError::InvalidObject(None)))?;
|
.map_err(|_| (None, InboxError::InvalidObject(None)))?;
|
||||||
@ -381,8 +311,6 @@ pub trait FromId<C>: Sized {
|
|||||||
|
|
||||||
/// Tries to find a `Self` with a given ID (`id`), using `ctx` (a database)
|
/// Tries to find a `Self` with a given ID (`id`), using `ctx` (a database)
|
||||||
fn from_db(ctx: &C, id: &str) -> Result<Self, Self::Error>;
|
fn from_db(ctx: &C, id: &str) -> Result<Self, Self::Error>;
|
||||||
|
|
||||||
fn get_sender() -> &'static dyn Signer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Should be implemented by anything representing an ActivityPub actor.
|
/// Should be implemented by anything representing an ActivityPub actor.
|
||||||
@ -418,51 +346,9 @@ pub trait AsActor<C> {
|
|||||||
/// representing the Note by a Message type, without any specific context.
|
/// representing the Note by a Message type, without any specific context.
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use activitystreams::{prelude::*, activity::Create, actor::Person, object::Note};
|
/// # extern crate activitypub;
|
||||||
|
/// # use activitypub::{activity::Create, actor::Person, object::Note};
|
||||||
/// # use plume_common::activity_pub::inbox::{AsActor, AsObject, FromId};
|
/// # use plume_common::activity_pub::inbox::{AsActor, AsObject, FromId};
|
||||||
/// # use plume_common::activity_pub::sign::{gen_keypair, Error as SignError, Result as SignResult, Signer};
|
|
||||||
/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
|
|
||||||
/// # use once_cell::sync::Lazy;
|
|
||||||
/// #
|
|
||||||
/// # static MY_SIGNER: Lazy<MySigner> = Lazy::new(|| MySigner::new());
|
|
||||||
/// #
|
|
||||||
/// # struct MySigner {
|
|
||||||
/// # public_key: String,
|
|
||||||
/// # private_key: String,
|
|
||||||
/// # }
|
|
||||||
/// #
|
|
||||||
/// # impl MySigner {
|
|
||||||
/// # fn new() -> Self {
|
|
||||||
/// # let (pub_key, priv_key) = gen_keypair();
|
|
||||||
/// # Self {
|
|
||||||
/// # public_key: String::from_utf8(pub_key).unwrap(),
|
|
||||||
/// # private_key: String::from_utf8(priv_key).unwrap(),
|
|
||||||
/// # }
|
|
||||||
/// # }
|
|
||||||
/// # }
|
|
||||||
/// #
|
|
||||||
/// # impl Signer for MySigner {
|
|
||||||
/// # fn get_key_id(&self) -> String {
|
|
||||||
/// # "mysigner".into()
|
|
||||||
/// # }
|
|
||||||
/// #
|
|
||||||
/// # fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> {
|
|
||||||
/// # let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
|
|
||||||
/// # .unwrap();
|
|
||||||
/// # let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
|
|
||||||
/// # signer.update(to_sign.as_bytes()).unwrap();
|
|
||||||
/// # signer.sign_to_vec().map_err(|_| SignError())
|
|
||||||
/// # }
|
|
||||||
/// #
|
|
||||||
/// # fn verify(&self, data: &str, signature: &[u8]) -> SignResult<bool> {
|
|
||||||
/// # let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
|
|
||||||
/// # .unwrap();
|
|
||||||
/// # let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
|
|
||||||
/// # verifier.update(data.as_bytes()).unwrap();
|
|
||||||
/// # verifier.verify(&signature).map_err(|_| SignError())
|
|
||||||
/// # }
|
|
||||||
/// # }
|
|
||||||
/// #
|
|
||||||
/// # struct Account;
|
/// # struct Account;
|
||||||
/// # impl FromId<()> for Account {
|
/// # impl FromId<()> for Account {
|
||||||
/// # type Error = ();
|
/// # type Error = ();
|
||||||
@ -475,10 +361,6 @@ pub trait AsActor<C> {
|
|||||||
/// # fn from_activity(_: &(), obj: Person) -> Result<Self, Self::Error> {
|
/// # fn from_activity(_: &(), obj: Person) -> Result<Self, Self::Error> {
|
||||||
/// # Ok(Account)
|
/// # Ok(Account)
|
||||||
/// # }
|
/// # }
|
||||||
/// #
|
|
||||||
/// # fn get_sender() -> &'static dyn Signer {
|
|
||||||
/// # &*MY_SIGNER
|
|
||||||
/// # }
|
|
||||||
/// # }
|
/// # }
|
||||||
/// # impl AsActor<()> for Account {
|
/// # impl AsActor<()> for Account {
|
||||||
/// # fn get_inbox_url(&self) -> String {
|
/// # fn get_inbox_url(&self) -> String {
|
||||||
@ -500,14 +382,7 @@ pub trait AsActor<C> {
|
|||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> {
|
/// fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> {
|
||||||
/// Ok(Message {
|
/// Ok(Message { text: obj.object_props.content_string().map_err(|_| ())? })
|
||||||
/// text: obj.content()
|
|
||||||
/// .and_then(|content| content.to_owned().single_xsd_string()).ok_or(())?
|
|
||||||
/// })
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// fn get_sender() -> &'static dyn Signer {
|
|
||||||
/// &*MY_SIGNER
|
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
@ -523,7 +398,7 @@ pub trait AsActor<C> {
|
|||||||
/// ```
|
/// ```
|
||||||
pub trait AsObject<A, V, C>
|
pub trait AsObject<A, V, C>
|
||||||
where
|
where
|
||||||
V: activitystreams::markers::Activity,
|
V: activitypub::Activity,
|
||||||
{
|
{
|
||||||
/// What kind of error is returned when something fails
|
/// What kind of error is returned when something fails
|
||||||
type Error;
|
type Error;
|
||||||
@ -548,57 +423,7 @@ where
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::activity_pub::sign::{
|
use activitypub::{activity::*, actor::Person, object::Note};
|
||||||
gen_keypair, Error as SignError, Result as SignResult, Signer,
|
|
||||||
};
|
|
||||||
use activitystreams::{
|
|
||||||
activity::{Announce, Create, Delete, Like},
|
|
||||||
actor::Person,
|
|
||||||
base::Base,
|
|
||||||
object::Note,
|
|
||||||
prelude::*,
|
|
||||||
};
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
|
|
||||||
|
|
||||||
static MY_SIGNER: Lazy<MySigner> = Lazy::new(MySigner::new);
|
|
||||||
|
|
||||||
struct MySigner {
|
|
||||||
public_key: String,
|
|
||||||
private_key: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MySigner {
|
|
||||||
fn new() -> Self {
|
|
||||||
let (pub_key, priv_key) = gen_keypair();
|
|
||||||
Self {
|
|
||||||
public_key: String::from_utf8(pub_key).unwrap(),
|
|
||||||
private_key: String::from_utf8(priv_key).unwrap(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Signer for MySigner {
|
|
||||||
fn get_key_id(&self) -> String {
|
|
||||||
"mysigner".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> {
|
|
||||||
let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
|
|
||||||
.unwrap();
|
|
||||||
let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
|
|
||||||
signer.update(to_sign.as_bytes()).unwrap();
|
|
||||||
signer.sign_to_vec().map_err(|_| SignError())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify(&self, data: &str, signature: &[u8]) -> SignResult<bool> {
|
|
||||||
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
|
|
||||||
.unwrap();
|
|
||||||
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
|
|
||||||
verifier.update(data.as_bytes()).unwrap();
|
|
||||||
verifier.verify(signature).map_err(|_| SignError())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MyActor;
|
struct MyActor;
|
||||||
impl FromId<()> for MyActor {
|
impl FromId<()> for MyActor {
|
||||||
@ -606,15 +431,11 @@ mod tests {
|
|||||||
type Object = Person;
|
type Object = Person;
|
||||||
|
|
||||||
fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> {
|
fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> {
|
||||||
Ok(Self)
|
Ok(MyActor)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> {
|
fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> {
|
||||||
Ok(Self)
|
Ok(MyActor)
|
||||||
}
|
|
||||||
|
|
||||||
fn get_sender() -> &'static dyn Signer {
|
|
||||||
&*MY_SIGNER
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -634,15 +455,11 @@ mod tests {
|
|||||||
type Object = Note;
|
type Object = Note;
|
||||||
|
|
||||||
fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> {
|
fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> {
|
||||||
Ok(Self)
|
Ok(MyObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_activity(_: &(), _obj: Note) -> Result<Self, Self::Error> {
|
fn from_activity(_: &(), _obj: Note) -> Result<Self, Self::Error> {
|
||||||
Ok(Self)
|
Ok(MyObject)
|
||||||
}
|
|
||||||
|
|
||||||
fn get_sender() -> &'static dyn Signer {
|
|
||||||
&*MY_SIGNER
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl AsObject<MyActor, Create, &()> for MyObject {
|
impl AsObject<MyActor, Create, &()> for MyObject {
|
||||||
@ -686,15 +503,21 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_create() -> Create {
|
fn build_create() -> Create {
|
||||||
let mut person = Person::new();
|
let mut act = Create::default();
|
||||||
person.set_id("https://test.ap/actor".parse().unwrap());
|
act.object_props
|
||||||
let mut note = Note::new();
|
.set_id_string(String::from("https://test.ap/activity"))
|
||||||
note.set_id("https://test.ap/note".parse().unwrap());
|
.unwrap();
|
||||||
let mut act = Create::new(
|
let mut person = Person::default();
|
||||||
Base::retract(person).unwrap().into_generic().unwrap(),
|
person
|
||||||
Base::retract(note).unwrap().into_generic().unwrap(),
|
.object_props
|
||||||
);
|
.set_id_string(String::from("https://test.ap/actor"))
|
||||||
act.set_id("https://test.ap/activity".parse().unwrap());
|
.unwrap();
|
||||||
|
act.create_props.set_actor_object(person).unwrap();
|
||||||
|
let mut note = Note::default();
|
||||||
|
note.object_props
|
||||||
|
.set_id_string(String::from("https://test.ap/note"))
|
||||||
|
.unwrap();
|
||||||
|
act.create_props.set_object_object(note).unwrap();
|
||||||
act
|
act
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -702,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());
|
||||||
}
|
}
|
||||||
@ -711,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());
|
||||||
}
|
}
|
||||||
@ -724,23 +547,13 @@ 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());
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FailingActor;
|
struct FailingActor;
|
||||||
impl AsActor<&()> for FailingActor {
|
|
||||||
fn get_inbox_url(&self) -> String {
|
|
||||||
String::from("https://test.ap/failing-actor/inbox")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_local(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromId<()> for FailingActor {
|
impl FromId<()> for FailingActor {
|
||||||
type Error = ();
|
type Error = ();
|
||||||
type Object = Person;
|
type Object = Person;
|
||||||
@ -749,12 +562,17 @@ mod tests {
|
|||||||
Err(())
|
Err(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_activity(_: &(), _obj: Self::Object) -> Result<Self, Self::Error> {
|
fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> {
|
||||||
Err(())
|
Err(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
impl AsActor<&()> for FailingActor {
|
||||||
|
fn get_inbox_url(&self) -> String {
|
||||||
|
String::from("https://test.ap/failing-actor/inbox")
|
||||||
|
}
|
||||||
|
|
||||||
fn get_sender() -> &'static dyn Signer {
|
fn is_local(&self) -> bool {
|
||||||
&*MY_SIGNER
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -778,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)
|
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());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,28 +1,14 @@
|
|||||||
use activitystreams::{
|
use activitypub::{Activity, Link, Object};
|
||||||
actor::{ApActor, Group, Person},
|
|
||||||
base::{AnyBase, Base, Extends},
|
|
||||||
iri_string::types::IriString,
|
|
||||||
kind,
|
|
||||||
markers::{self, Activity},
|
|
||||||
object::{ApObject, Article, Object},
|
|
||||||
primitives::{AnyString, OneOrMany},
|
|
||||||
unparsed::UnparsedMutExt,
|
|
||||||
};
|
|
||||||
use activitystreams_ext::{Ext1, Ext2, UnparsedExtension};
|
|
||||||
use array_tool::vec::Uniq;
|
use array_tool::vec::Uniq;
|
||||||
use futures::future::join_all;
|
use reqwest::r#async::ClientBuilder;
|
||||||
use reqwest::{header::HeaderValue, ClientBuilder, RequestBuilder, Url};
|
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::Status,
|
http::Status,
|
||||||
request::{FromRequest, Request},
|
request::{FromRequest, Request},
|
||||||
response::{Responder, Response},
|
response::{Responder, Response},
|
||||||
Outcome,
|
Outcome,
|
||||||
};
|
};
|
||||||
use tokio::{
|
use serde_json;
|
||||||
runtime,
|
use tokio::prelude::*;
|
||||||
time::{sleep, Duration},
|
|
||||||
};
|
|
||||||
use tracing::{debug, warn};
|
|
||||||
|
|
||||||
use self::sign::Signable;
|
use self::sign::Signable;
|
||||||
|
|
||||||
@ -38,8 +24,8 @@ pub const AP_CONTENT_TYPE: &str =
|
|||||||
|
|
||||||
pub fn ap_accept_header() -> Vec<&'static str> {
|
pub fn ap_accept_header() -> Vec<&'static str> {
|
||||||
vec![
|
vec![
|
||||||
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
|
"application/ld+json; profile=\"https://w3.org/ns/activitystreams\"",
|
||||||
"application/ld+json;profile=\"https://www.w3.org/ns/activitystreams\"",
|
"application/ld+json;profile=\"https://w3.org/ns/activitystreams\"",
|
||||||
"application/activity+json",
|
"application/activity+json",
|
||||||
"application/ld+json",
|
"application/ld+json",
|
||||||
]
|
]
|
||||||
@ -77,7 +63,7 @@ impl<T> ActivityStream<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'r, O: serde::Serialize> Responder<'r> for ActivityStream<O> {
|
impl<'r, O: Object> Responder<'r> for ActivityStream<O> {
|
||||||
fn respond_to(self, request: &Request<'_>) -> Result<Response<'r>, Status> {
|
fn respond_to(self, request: &Request<'_>) -> Result<Response<'r>, Status> {
|
||||||
let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?;
|
let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?;
|
||||||
json["@context"] = context();
|
json["@context"] = context();
|
||||||
@ -101,16 +87,14 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApRequest {
|
|||||||
.map(|header| {
|
.map(|header| {
|
||||||
header
|
header
|
||||||
.split(',')
|
.split(',')
|
||||||
.map(|ct| {
|
.map(|ct| match ct.trim() {
|
||||||
match ct.trim() {
|
|
||||||
// bool for Forward: true if found a valid Content-Type for Plume first (HTML), false otherwise
|
// bool for Forward: true if found a valid Content-Type for Plume first (HTML), false otherwise
|
||||||
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
"application/ld+json; profile=\"https://w3.org/ns/activitystreams\""
|
||||||
| "application/ld+json;profile=\"https://www.w3.org/ns/activitystreams\""
|
| "application/ld+json;profile=\"https://w3.org/ns/activitystreams\""
|
||||||
| "application/activity+json"
|
| "application/activity+json"
|
||||||
| "application/ld+json" => Outcome::Success(ApRequest),
|
| "application/ld+json" => Outcome::Success(ApRequest),
|
||||||
"text/html" => Outcome::Forward(true),
|
"text/html" => Outcome::Forward(true),
|
||||||
_ => Outcome::Forward(false),
|
_ => Outcome::Forward(false),
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.fold(Outcome::Forward(false), |out, ct| {
|
.fold(Outcome::Forward(false), |out, ct| {
|
||||||
if out.clone().forwarded().unwrap_or_else(|| out.is_success()) {
|
if out.clone().forwarded().unwrap_or_else(|| out.is_success()) {
|
||||||
@ -124,11 +108,10 @@ 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>)
|
||||||
pub fn broadcast<S, A, T, C>(sender: &S, act: A, to: Vec<T>, proxy: Option<reqwest::Proxy>)
|
|
||||||
where
|
where
|
||||||
S: sign::Signer,
|
S: sign::Signer,
|
||||||
A: Activity + serde::Serialize,
|
A: Activity,
|
||||||
T: inbox::AsActor<C>,
|
T: inbox::AsActor<C>,
|
||||||
{
|
{
|
||||||
let boxes = to
|
let boxes = to
|
||||||
@ -147,79 +130,36 @@ where
|
|||||||
.sign(sender)
|
.sign(sender)
|
||||||
.expect("activity_pub::broadcast: signature error");
|
.expect("activity_pub::broadcast: signature error");
|
||||||
|
|
||||||
let client = if let Some(proxy) = proxy {
|
let mut rt = tokio::runtime::current_thread::Runtime::new()
|
||||||
ClientBuilder::new().proxy(proxy)
|
|
||||||
} else {
|
|
||||||
ClientBuilder::new()
|
|
||||||
}
|
|
||||||
.connect_timeout(std::time::Duration::from_secs(5))
|
|
||||||
.build()
|
|
||||||
.expect("Can't build client");
|
|
||||||
let rt = runtime::Builder::new_current_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()
|
|
||||||
.expect("Error while initializing tokio runtime for federation");
|
.expect("Error while initializing tokio runtime for federation");
|
||||||
rt.block_on(async {
|
let client = ClientBuilder::new()
|
||||||
// TODO: should be determined dependent on database connections because
|
.connect_timeout(std::time::Duration::from_secs(5))
|
||||||
// after broadcasting, target instance sends request to this instance,
|
.build()
|
||||||
// and Plume accesses database at that time.
|
.expect("Can't build client");
|
||||||
let capacity = 6;
|
for inbox in boxes {
|
||||||
let (tx, rx) = flume::bounded::<RequestBuilder>(capacity);
|
let body = signed.to_string();
|
||||||
let mut handles = Vec::with_capacity(capacity);
|
let mut headers = request::headers();
|
||||||
for _ in 0..capacity {
|
headers.insert("Digest", request::Digest::digest(&body));
|
||||||
let rx = rx.clone();
|
rt.spawn(
|
||||||
let handle = rt.spawn(async move {
|
client
|
||||||
while let Ok(request_builder) = rx.recv_async().await {
|
.post(&inbox)
|
||||||
// After broadcasting, target instance sends request to this instance.
|
.headers(headers.clone())
|
||||||
// Sleep here in order to reduce requests at once
|
.header(
|
||||||
sleep(Duration::from_millis(500)).await;
|
"Signature",
|
||||||
let _ = request_builder
|
request::signature(sender, &headers)
|
||||||
.send()
|
.expect("activity_pub::broadcast: request signature error"),
|
||||||
.await
|
)
|
||||||
.map(move |r| {
|
.body(body)
|
||||||
if r.status().is_success() {
|
.send()
|
||||||
debug!("Successfully sent activity to inbox ({})", &r.url());
|
.and_then(|r| r.into_body().concat2())
|
||||||
} else {
|
.map(move |response| {
|
||||||
warn!("Error while sending to inbox ({:?})", &r)
|
println!("Successfully sent activity to inbox ({})", inbox);
|
||||||
}
|
println!("Response: \"{:?}\"\n", response)
|
||||||
debug!("Response: \"{:?}\"\n", r);
|
})
|
||||||
})
|
.map_err(|e| println!("Error while sending to inbox ({:?})", e)),
|
||||||
.map_err(|e| warn!("Error while sending to inbox ({:?})", e));
|
);
|
||||||
}
|
}
|
||||||
});
|
rt.run().unwrap();
|
||||||
handles.push(handle);
|
|
||||||
}
|
|
||||||
for inbox in boxes {
|
|
||||||
let body = signed.to_string();
|
|
||||||
let mut headers = request::headers();
|
|
||||||
let url = Url::parse(&inbox);
|
|
||||||
if url.is_err() {
|
|
||||||
warn!("Inbox is invalid URL: {:?}", &inbox);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let url = url.unwrap();
|
|
||||||
if !url.has_host() {
|
|
||||||
warn!("Inbox doesn't have host: {:?}", &inbox);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let host_header_value = HeaderValue::from_str(url.host_str().expect("Unreachable"));
|
|
||||||
if host_header_value.is_err() {
|
|
||||||
warn!("Header value is invalid: {:?}", url.host_str());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
headers.insert("Host", host_header_value.unwrap());
|
|
||||||
headers.insert("Digest", request::Digest::digest(&body));
|
|
||||||
headers.insert(
|
|
||||||
"Signature",
|
|
||||||
request::signature(sender, &headers, ("post", url.path(), url.query()))
|
|
||||||
.expect("activity_pub::broadcast: request signature error"),
|
|
||||||
);
|
|
||||||
let request_builder = client.post(&inbox).headers(headers.clone()).body(body);
|
|
||||||
let _ = tx.send_async(request_builder).await;
|
|
||||||
}
|
|
||||||
drop(tx);
|
|
||||||
join_all(handles).await;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Shrinkwrap, Clone, Serialize, Deserialize)]
|
#[derive(Shrinkwrap, Clone, Serialize, Deserialize)]
|
||||||
@ -241,193 +181,46 @@ pub trait IntoId {
|
|||||||
fn into_id(self) -> Id;
|
fn into_id(self) -> Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
impl Link for Id {}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ApSignature {
|
pub struct ApSignature {
|
||||||
pub public_key: PublicKey,
|
#[activitystreams(concrete(PublicKey), functional)]
|
||||||
|
pub public_key: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct PublicKey {
|
pub struct PublicKey {
|
||||||
pub id: IriString,
|
#[activitystreams(concrete(String), functional)]
|
||||||
pub owner: IriString,
|
pub id: Option<serde_json::Value>,
|
||||||
pub public_key_pem: String,
|
|
||||||
|
#[activitystreams(concrete(String), functional)]
|
||||||
|
pub owner: Option<serde_json::Value>,
|
||||||
|
|
||||||
|
#[activitystreams(concrete(String), functional)]
|
||||||
|
pub public_key_pem: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<U> UnparsedExtension<U> for ApSignature
|
#[derive(Clone, Debug, Default, UnitString)]
|
||||||
where
|
#[activitystreams(Hashtag)]
|
||||||
U: UnparsedMutExt,
|
pub struct HashtagType;
|
||||||
{
|
|
||||||
type Error = serde_json::Error;
|
|
||||||
|
|
||||||
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
|
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
|
||||||
Ok(ApSignature {
|
|
||||||
public_key: unparsed_mut.remove("publicKey")?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
|
|
||||||
unparsed_mut.insert("publicKey", self.public_key)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SourceProperty {
|
|
||||||
pub source: Source,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<U> UnparsedExtension<U> for SourceProperty
|
|
||||||
where
|
|
||||||
U: UnparsedMutExt,
|
|
||||||
{
|
|
||||||
type Error = serde_json::Error;
|
|
||||||
|
|
||||||
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
|
|
||||||
Ok(SourceProperty {
|
|
||||||
source: unparsed_mut.remove("source")?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
|
|
||||||
unparsed_mut.insert("source", self.source)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type CustomPerson = Ext1<ApActor<Person>, ApSignature>;
|
|
||||||
pub type CustomGroup = Ext2<ApActor<Group>, ApSignature, SourceProperty>;
|
|
||||||
|
|
||||||
kind!(HashtagType, Hashtag);
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub struct Hashtag {
|
pub struct Hashtag {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "type")]
|
||||||
pub href: Option<IriString>,
|
kind: HashtagType,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[activitystreams(concrete(String), functional)]
|
||||||
pub name: Option<AnyString>,
|
pub href: Option<serde_json::Value>,
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[activitystreams(concrete(String), functional)]
|
||||||
inner: Object<HashtagType>,
|
pub name: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hashtag {
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
href: None,
|
|
||||||
name: None,
|
|
||||||
inner: Object::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn extending(mut inner: Object<HashtagType>) -> Result<Self, serde_json::Error> {
|
|
||||||
let href = inner.remove("href")?;
|
|
||||||
let name = inner.remove("name")?;
|
|
||||||
|
|
||||||
Ok(Self { href, name, inner })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn retracting(self) -> Result<Object<HashtagType>, serde_json::Error> {
|
|
||||||
let Self {
|
|
||||||
href,
|
|
||||||
name,
|
|
||||||
mut inner,
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
inner.insert("href", href)?;
|
|
||||||
inner.insert("name", name)?;
|
|
||||||
Ok(inner)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait AsHashtag: markers::Object {
|
|
||||||
fn hashtag_ref(&self) -> &Hashtag;
|
|
||||||
|
|
||||||
fn hashtag_mut(&mut self) -> &mut Hashtag;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait HashtagExt: AsHashtag {
|
|
||||||
fn href(&self) -> Option<&IriString> {
|
|
||||||
self.hashtag_ref().href.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_href<T>(&mut self, href: T) -> &mut Self
|
|
||||||
where
|
|
||||||
T: Into<IriString>,
|
|
||||||
{
|
|
||||||
self.hashtag_mut().href = Some(href.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn take_href(&mut self) -> Option<IriString> {
|
|
||||||
self.hashtag_mut().href.take()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_href(&mut self) -> &mut Self {
|
|
||||||
self.hashtag_mut().href = None;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> Option<&AnyString> {
|
|
||||||
self.hashtag_ref().name.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_name<T>(&mut self, name: T) -> &mut Self
|
|
||||||
where
|
|
||||||
T: Into<AnyString>,
|
|
||||||
{
|
|
||||||
self.hashtag_mut().name = Some(name.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn take_name(&mut self) -> Option<AnyString> {
|
|
||||||
self.hashtag_mut().name.take()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_name(&mut self) -> &mut Self {
|
|
||||||
self.hashtag_mut().name = None;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Hashtag {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsHashtag for Hashtag {
|
|
||||||
fn hashtag_ref(&self) -> &Self {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hashtag_mut(&mut self) -> &mut Self {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Extends<HashtagType> for Hashtag {
|
|
||||||
type Error = serde_json::Error;
|
|
||||||
|
|
||||||
fn extends(base: Base<HashtagType>) -> Result<Self, Self::Error> {
|
|
||||||
let inner = Object::extends(base)?;
|
|
||||||
Self::extending(inner)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn retracts(self) -> Result<Base<HashtagType>, Self::Error> {
|
|
||||||
let inner = self.retracting()?;
|
|
||||||
inner.retracts()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl markers::Base for Hashtag {}
|
|
||||||
impl markers::Object for Hashtag {}
|
|
||||||
impl<T> HashtagExt for T where T: AsHashtag {}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Source {
|
pub struct Source {
|
||||||
pub media_type: String,
|
pub media_type: String,
|
||||||
@ -435,366 +228,13 @@ pub struct Source {
|
|||||||
pub content: String,
|
pub content: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<U> UnparsedExtension<U> for Source
|
impl Object for Source {}
|
||||||
where
|
|
||||||
U: UnparsedMutExt,
|
|
||||||
{
|
|
||||||
type Error = serde_json::Error;
|
|
||||||
|
|
||||||
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
|
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
|
||||||
Ok(Source {
|
|
||||||
content: unparsed_mut.remove("content")?,
|
|
||||||
media_type: unparsed_mut.remove("mediaType")?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
|
|
||||||
unparsed_mut.insert("content", self.content)?;
|
|
||||||
unparsed_mut.insert("mediaType", self.media_type)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Licensed {
|
pub struct Licensed {
|
||||||
pub license: Option<String>,
|
#[activitystreams(concrete(String), functional)]
|
||||||
|
pub license: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<U> UnparsedExtension<U> for Licensed
|
impl Object for Licensed {}
|
||||||
where
|
|
||||||
U: UnparsedMutExt,
|
|
||||||
{
|
|
||||||
type Error = serde_json::Error;
|
|
||||||
|
|
||||||
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
|
|
||||||
Ok(Licensed {
|
|
||||||
license: unparsed_mut.remove("license")?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
|
|
||||||
unparsed_mut.insert("license", self.license)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type LicensedArticle = Ext1<ApObject<Article>, Licensed>;
|
|
||||||
|
|
||||||
pub trait ToAsString {
|
|
||||||
fn to_as_string(&self) -> Option<String>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToAsString for OneOrMany<&AnyString> {
|
|
||||||
fn to_as_string(&self) -> Option<String> {
|
|
||||||
self.as_as_str().map(|s| s.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trait AsAsStr {
|
|
||||||
fn as_as_str(&self) -> Option<&str>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsAsStr for OneOrMany<&AnyString> {
|
|
||||||
fn as_as_str(&self) -> Option<&str> {
|
|
||||||
self.iter().next().map(|prop| prop.as_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ToAsUri {
|
|
||||||
fn to_as_uri(&self) -> Option<String>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToAsUri for OneOrMany<AnyBase> {
|
|
||||||
fn to_as_uri(&self) -> Option<String> {
|
|
||||||
self.iter()
|
|
||||||
.next()
|
|
||||||
.and_then(|prop| prop.as_xsd_any_uri().map(|uri| uri.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use activitystreams::{
|
|
||||||
activity::{ActorAndObjectRef, Create},
|
|
||||||
object::{kind::ArticleType, Image},
|
|
||||||
prelude::{ApActorExt, BaseExt, ExtendsExt, ObjectExt},
|
|
||||||
};
|
|
||||||
use assert_json_diff::assert_json_eq;
|
|
||||||
use serde_json::{from_str, json, to_value};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn se_ap_signature() {
|
|
||||||
let ap_signature = ApSignature {
|
|
||||||
public_key: PublicKey {
|
|
||||||
id: "https://example.com/pubkey".parse().unwrap(),
|
|
||||||
owner: "https://example.com/owner".parse().unwrap(),
|
|
||||||
public_key_pem: "pubKeyPem".into(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let expected = json!({
|
|
||||||
"publicKey": {
|
|
||||||
"id": "https://example.com/pubkey",
|
|
||||||
"owner": "https://example.com/owner",
|
|
||||||
"publicKeyPem": "pubKeyPem"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
assert_json_eq!(to_value(ap_signature).unwrap(), expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn de_ap_signature() {
|
|
||||||
let value: ApSignature = from_str(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"publicKey": {
|
|
||||||
"id": "https://example.com/",
|
|
||||||
"owner": "https://example.com/",
|
|
||||||
"publicKeyPem": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let expected = ApSignature {
|
|
||||||
public_key: PublicKey {
|
|
||||||
id: "https://example.com/".parse().unwrap(),
|
|
||||||
owner: "https://example.com/".parse().unwrap(),
|
|
||||||
public_key_pem: "".into(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
assert_eq!(value, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn se_custom_person() {
|
|
||||||
let actor = ApActor::new("https://example.com/inbox".parse().unwrap(), Person::new());
|
|
||||||
let person = CustomPerson::new(
|
|
||||||
actor,
|
|
||||||
ApSignature {
|
|
||||||
public_key: PublicKey {
|
|
||||||
id: "https://example.com/pubkey".parse().unwrap(),
|
|
||||||
owner: "https://example.com/owner".parse().unwrap(),
|
|
||||||
public_key_pem: "pubKeyPem".into(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
let expected = json!({
|
|
||||||
"inbox": "https://example.com/inbox",
|
|
||||||
"type": "Person",
|
|
||||||
"publicKey": {
|
|
||||||
"id": "https://example.com/pubkey",
|
|
||||||
"owner": "https://example.com/owner",
|
|
||||||
"publicKeyPem": "pubKeyPem"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
assert_eq!(to_value(person).unwrap(), expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn se_custom_group() {
|
|
||||||
let group = CustomGroup::new(
|
|
||||||
ApActor::new("https://example.com/inbox".parse().unwrap(), Group::new()),
|
|
||||||
ApSignature {
|
|
||||||
public_key: PublicKey {
|
|
||||||
id: "https://example.com/pubkey".parse().unwrap(),
|
|
||||||
owner: "https://example.com/owner".parse().unwrap(),
|
|
||||||
public_key_pem: "pubKeyPem".into(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
SourceProperty {
|
|
||||||
source: Source {
|
|
||||||
content: String::from("This is a *custom* group."),
|
|
||||||
media_type: String::from("text/markdown"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
let expected = json!({
|
|
||||||
"inbox": "https://example.com/inbox",
|
|
||||||
"type": "Group",
|
|
||||||
"publicKey": {
|
|
||||||
"id": "https://example.com/pubkey",
|
|
||||||
"owner": "https://example.com/owner",
|
|
||||||
"publicKeyPem": "pubKeyPem"
|
|
||||||
},
|
|
||||||
"source": {
|
|
||||||
"content": "This is a *custom* group.",
|
|
||||||
"mediaType": "text/markdown"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
assert_eq!(to_value(group).unwrap(), expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn de_custom_group() {
|
|
||||||
let value: CustomGroup = from_str(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"icon": {
|
|
||||||
"type": "Image"
|
|
||||||
},
|
|
||||||
"id": "https://plume01.localhost/~/Plume01%20Blog%202/",
|
|
||||||
"image": {
|
|
||||||
"type": "Image"
|
|
||||||
},
|
|
||||||
"inbox": "https://plume01.localhost/~/Plume01%20Blog%202/inbox",
|
|
||||||
"name": "Plume01 Blog 2",
|
|
||||||
"outbox": "https://plume01.localhost/~/Plume01%20Blog%202/outbox",
|
|
||||||
"preferredUsername": "Plume01 Blog 2",
|
|
||||||
"publicKey": {
|
|
||||||
"id": "https://plume01.localhost/~/Plume01%20Blog%202/#main-key",
|
|
||||||
"owner": "https://plume01.localhost/~/Plume01%20Blog%202/",
|
|
||||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwPGtKkl/iMsNAyeVaJGz\noEz5PoNkjRnKK7G97MFvb4zw9zs5SpzWW7b/pKHa4dODcGDJXmkCJ1H5JWyguzN8\n2GNoFjtEOJHxEGwBHSYDsTmhuLNB0DKxMU2iu55g8iIiXhZiIW1FBNGs/Geaymvr\nh/TEtzdReN8wzloRR55kOVcU49xBkqx8cfDSk/lrrDLlpveHdqgaFnIvuw2vycK0\nxFzS3xlEUpzJk9kHxoR1uEAfZ+gCv26Sgo/HqOAhqSD5IU3QZC3kdkr/hwVqtr8U\nXGkGG6Mo1rgzhkYiCFkWrV2WoKkcEHD4nEzbgoZZ5MyuSoloxnyF3NiScqmqW+Yx\nkQIDAQAB\n-----END PUBLIC KEY-----\n"
|
|
||||||
},
|
|
||||||
"source": {
|
|
||||||
"content": "",
|
|
||||||
"mediaType": "text/markdown"
|
|
||||||
},
|
|
||||||
"summary": "",
|
|
||||||
"type": "Group"
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
).unwrap();
|
|
||||||
let mut expected = CustomGroup::new(
|
|
||||||
ApActor::new("https://plume01.localhost/~/Plume01%20Blog%202/inbox".parse().unwrap(), Group::new()),
|
|
||||||
ApSignature {
|
|
||||||
public_key: PublicKey {
|
|
||||||
id: "https://plume01.localhost/~/Plume01%20Blog%202/#main-key".parse().unwrap(),
|
|
||||||
owner: "https://plume01.localhost/~/Plume01%20Blog%202/".parse().unwrap(),
|
|
||||||
public_key_pem: "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwPGtKkl/iMsNAyeVaJGz\noEz5PoNkjRnKK7G97MFvb4zw9zs5SpzWW7b/pKHa4dODcGDJXmkCJ1H5JWyguzN8\n2GNoFjtEOJHxEGwBHSYDsTmhuLNB0DKxMU2iu55g8iIiXhZiIW1FBNGs/Geaymvr\nh/TEtzdReN8wzloRR55kOVcU49xBkqx8cfDSk/lrrDLlpveHdqgaFnIvuw2vycK0\nxFzS3xlEUpzJk9kHxoR1uEAfZ+gCv26Sgo/HqOAhqSD5IU3QZC3kdkr/hwVqtr8U\nXGkGG6Mo1rgzhkYiCFkWrV2WoKkcEHD4nEzbgoZZ5MyuSoloxnyF3NiScqmqW+Yx\nkQIDAQAB\n-----END PUBLIC KEY-----\n".into(),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SourceProperty {
|
|
||||||
source: Source {
|
|
||||||
content: String::from(""),
|
|
||||||
media_type: String::from("text/markdown")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
expected.set_icon(Image::new().into_any_base().unwrap());
|
|
||||||
expected.set_id(
|
|
||||||
"https://plume01.localhost/~/Plume01%20Blog%202/"
|
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
expected.set_image(Image::new().into_any_base().unwrap());
|
|
||||||
expected.set_name("Plume01 Blog 2");
|
|
||||||
expected.set_outbox(
|
|
||||||
"https://plume01.localhost/~/Plume01%20Blog%202/outbox"
|
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
expected.set_preferred_username("Plume01 Blog 2");
|
|
||||||
expected.set_summary("");
|
|
||||||
|
|
||||||
assert_json_eq!(value, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn se_licensed_article() {
|
|
||||||
let object = ApObject::new(Article::new());
|
|
||||||
let licensed_article = LicensedArticle::new(
|
|
||||||
object,
|
|
||||||
Licensed {
|
|
||||||
license: Some("CC-0".into()),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
let expected = json!({
|
|
||||||
"type": "Article",
|
|
||||||
"license": "CC-0",
|
|
||||||
});
|
|
||||||
assert_json_eq!(to_value(licensed_article).unwrap(), expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn de_licensed_article() {
|
|
||||||
let value: LicensedArticle = from_str(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"type": "Article",
|
|
||||||
"id": "https://plu.me/~/Blog/my-article",
|
|
||||||
"attributedTo": ["https://plu.me/@/Admin", "https://plu.me/~/Blog"],
|
|
||||||
"content": "Hello.",
|
|
||||||
"name": "My Article",
|
|
||||||
"summary": "Bye.",
|
|
||||||
"source": {
|
|
||||||
"content": "Hello.",
|
|
||||||
"mediaType": "text/markdown"
|
|
||||||
},
|
|
||||||
"published": "2014-12-12T12:12:12Z",
|
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
||||||
"license": "CC-0"
|
|
||||||
}
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let expected = json!({
|
|
||||||
"type": "Article",
|
|
||||||
"id": "https://plu.me/~/Blog/my-article",
|
|
||||||
"attributedTo": ["https://plu.me/@/Admin", "https://plu.me/~/Blog"],
|
|
||||||
"content": "Hello.",
|
|
||||||
"name": "My Article",
|
|
||||||
"summary": "Bye.",
|
|
||||||
"source": {
|
|
||||||
"content": "Hello.",
|
|
||||||
"mediaType": "text/markdown"
|
|
||||||
},
|
|
||||||
"published": "2014-12-12T12:12:12Z",
|
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
||||||
"license": "CC-0"
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(to_value(value).unwrap(), expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn de_create_with_licensed_article() {
|
|
||||||
let create: Create = from_str(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"id": "https://plu.me/~/Blog/my-article",
|
|
||||||
"type": "Create",
|
|
||||||
"actor": "https://plu.me/@/Admin",
|
|
||||||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
|
||||||
"object": {
|
|
||||||
"type": "Article",
|
|
||||||
"id": "https://plu.me/~/Blog/my-article",
|
|
||||||
"attributedTo": ["https://plu.me/@/Admin", "https://plu.me/~/Blog"],
|
|
||||||
"content": "Hello.",
|
|
||||||
"name": "My Article",
|
|
||||||
"summary": "Bye.",
|
|
||||||
"source": {
|
|
||||||
"content": "Hello.",
|
|
||||||
"mediaType": "text/markdown"
|
|
||||||
},
|
|
||||||
"published": "2014-12-12T12:12:12Z",
|
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
||||||
"license": "CC-0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let base = create.object_field_ref().as_single_base().unwrap();
|
|
||||||
let any_base = AnyBase::from_base(base.clone());
|
|
||||||
let value = any_base.extend::<LicensedArticle, ArticleType>().unwrap();
|
|
||||||
let expected = json!({
|
|
||||||
"type": "Article",
|
|
||||||
"id": "https://plu.me/~/Blog/my-article",
|
|
||||||
"attributedTo": ["https://plu.me/@/Admin", "https://plu.me/~/Blog"],
|
|
||||||
"content": "Hello.",
|
|
||||||
"name": "My Article",
|
|
||||||
"summary": "Bye.",
|
|
||||||
"source": {
|
|
||||||
"content": "Hello.",
|
|
||||||
"mediaType": "text/markdown"
|
|
||||||
},
|
|
||||||
"published": "2014-12-12T12:12:12Z",
|
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
||||||
"license": "CC-0"
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(to_value(value).unwrap(), expected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,42 +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::{
|
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, DATE, USER_AGENT};
|
||||||
blocking::{ClientBuilder, Response},
|
|
||||||
header::{
|
|
||||||
HeaderMap, HeaderValue, InvalidHeaderValue, ACCEPT, CONTENT_TYPE, DATE, HOST, USER_AGENT,
|
|
||||||
},
|
|
||||||
Proxy, Url,
|
|
||||||
};
|
|
||||||
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();
|
|
||||||
|
|
||||||
impl From<url::ParseError> for Error {
|
|
||||||
fn from(_err: url::ParseError) -> Self {
|
|
||||||
Error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<InvalidHeaderValue> for Error {
|
|
||||||
fn from(_err: InvalidHeaderValue) -> Self {
|
|
||||||
Error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<reqwest::Error> for Error {
|
|
||||||
fn from(_err: reqwest::Error) -> Self {
|
|
||||||
Error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Digest(String);
|
pub struct Digest(String);
|
||||||
|
|
||||||
impl Digest {
|
impl Digest {
|
||||||
@ -89,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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,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(
|
|
||||||
signer: &dyn Signer,
|
|
||||||
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!(
|
||||||
@ -185,86 +138,5 @@ pub fn signature(
|
|||||||
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(|_| ())
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(url_str: &str, sender: &dyn Signer, proxy: Option<Proxy>) -> Result<Response, Error> {
|
|
||||||
let mut headers = headers();
|
|
||||||
let url = Url::parse(url_str)?;
|
|
||||||
if !url.has_host() {
|
|
||||||
return Err(Error());
|
|
||||||
}
|
|
||||||
let host_header_value = HeaderValue::from_str(url.host_str().expect("Unreachable"))?;
|
|
||||||
headers.insert(HOST, host_header_value);
|
|
||||||
if let Some(proxy) = proxy {
|
|
||||||
ClientBuilder::new().proxy(proxy)
|
|
||||||
} else {
|
|
||||||
ClientBuilder::new()
|
|
||||||
}
|
|
||||||
.connect_timeout(Some(std::time::Duration::from_secs(5)))
|
|
||||||
.build()?
|
|
||||||
.get(url_str)
|
|
||||||
.headers(headers.clone())
|
|
||||||
.header(
|
|
||||||
"Signature",
|
|
||||||
signature(sender, &headers, ("get", url.path(), url.query()))?,
|
|
||||||
)
|
|
||||||
.send()
|
|
||||||
.map_err(|_| Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::signature;
|
|
||||||
use crate::activity_pub::sign::{gen_keypair, Error, Result, Signer};
|
|
||||||
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
|
|
||||||
use reqwest::header::HeaderMap;
|
|
||||||
|
|
||||||
struct MySigner {
|
|
||||||
public_key: String,
|
|
||||||
private_key: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MySigner {
|
|
||||||
fn new() -> Self {
|
|
||||||
let (pub_key, priv_key) = gen_keypair();
|
|
||||||
Self {
|
|
||||||
public_key: String::from_utf8(pub_key).unwrap(),
|
|
||||||
private_key: String::from_utf8(priv_key).unwrap(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Signer for MySigner {
|
|
||||||
fn get_key_id(&self) -> String {
|
|
||||||
"mysigner".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
|
|
||||||
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> {
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,27 +20,19 @@ pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Error();
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
|
||||||
|
|
||||||
impl From<openssl::error::ErrorStack> for Error {
|
|
||||||
fn from(_: openssl::error::ErrorStack) -> Self {
|
|
||||||
Self()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Signer {
|
pub trait Signer {
|
||||||
|
type Error;
|
||||||
|
|
||||||
fn get_key_id(&self) -> String;
|
fn get_key_id(&self) -> String;
|
||||||
|
|
||||||
/// Sign some data with the signer keypair
|
/// Sign some data with the signer keypair
|
||||||
fn sign(&self, to_sign: &str) -> Result<Vec<u8>>;
|
fn sign(&self, to_sign: &str) -> Result<Vec<u8>, Self::Error>;
|
||||||
/// Verify if the signature is valid
|
/// Verify if the signature is valid
|
||||||
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool>;
|
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool, Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Signable {
|
pub trait Signable {
|
||||||
fn sign<T>(&mut self, creator: &T) -> Result<&mut Self>
|
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
|
||||||
@ -51,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> {
|
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",
|
||||||
@ -69,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;
|
||||||
@ -119,7 +114,7 @@ impl Signable for serde_json::Value {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
pub enum SignatureValidity {
|
pub enum SignatureValidity {
|
||||||
Invalid,
|
Invalid,
|
||||||
ValidNoDigest,
|
ValidNoDigest,
|
||||||
@ -187,7 +182,7 @@ pub fn verify_http_headers<S: Signer + ::std::fmt::Debug>(
|
|||||||
}
|
}
|
||||||
let digest = all_headers.get_one("digest").unwrap_or("");
|
let digest = all_headers.get_one("digest").unwrap_or("");
|
||||||
let digest = request::Digest::from_header(digest);
|
let digest = request::Digest::from_header(digest);
|
||||||
if !digest.map(|d| d.verify_header(data)).unwrap_or(false) {
|
if !digest.map(|d| d.verify_header(&data)).unwrap_or(false) {
|
||||||
// signature was valid, but body content does not match its digest
|
// signature was valid, but body content does not match its digest
|
||||||
return SignatureValidity::Invalid;
|
return SignatureValidity::Invalid;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
#![feature(associated_type_defaults)]
|
#![feature(associated_type_defaults)]
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate activitystreams_derive;
|
||||||
|
use activitystreams_traits;
|
||||||
|
|
||||||
|
use serde;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate shrinkwraprs;
|
extern crate shrinkwraprs;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
|
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::http::uri::Uri;
|
use rocket::{
|
||||||
|
http::uri::Uri,
|
||||||
|
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
|
||||||
@ -15,57 +20,25 @@ pub fn random_hex() -> String {
|
|||||||
.fold(String::new(), |res, byte| format!("{}{:x}", res, byte))
|
.fold(String::new(), |res, byte| format!("{}{:x}", res, byte))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Remove non alphanumeric characters and CamelCase a string
|
||||||
* Percent-encode characters which are not allowed in IRI path segments.
|
pub fn make_actor_id(name: &str) -> String {
|
||||||
*
|
name.to_camel_case()
|
||||||
* Intended to be used for generating Post ap_url.
|
.chars()
|
||||||
*/
|
.filter(|c| c.is_alphanumeric())
|
||||||
pub fn iri_percent_encode_seg(segment: &str) -> String {
|
.collect()
|
||||||
segment.chars().map(iri_percent_encode_seg_char).collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iri_percent_encode_seg_char(c: char) -> String {
|
/**
|
||||||
if c.is_alphanumeric() {
|
* Redirects to the login page with a given message.
|
||||||
c.to_string()
|
*
|
||||||
} else {
|
* Note that the message should be translated before passed to this function.
|
||||||
match c {
|
*/
|
||||||
'-'
|
pub fn requires_login<T: Into<Uri<'static>>>(message: &str, url: T) -> Flash<Redirect> {
|
||||||
| '.'
|
Flash::new(
|
||||||
| '_'
|
Redirect::to(format!("/login?m={}", Uri::percent_encode(message))),
|
||||||
| '~'
|
"callback",
|
||||||
| '\u{A0}'..='\u{D7FF}'
|
url.into().to_string(),
|
||||||
| '\u{20000}'..='\u{2FFFD}'
|
)
|
||||||
| '\u{30000}'..='\u{3FFFD}'
|
|
||||||
| '\u{40000}'..='\u{4FFFD}'
|
|
||||||
| '\u{50000}'..='\u{5FFFD}'
|
|
||||||
| '\u{60000}'..='\u{6FFFD}'
|
|
||||||
| '\u{70000}'..='\u{7FFFD}'
|
|
||||||
| '\u{80000}'..='\u{8FFFD}'
|
|
||||||
| '\u{90000}'..='\u{9FFFD}'
|
|
||||||
| '\u{A0000}'..='\u{AFFFD}'
|
|
||||||
| '\u{B0000}'..='\u{BFFFD}'
|
|
||||||
| '\u{C0000}'..='\u{CFFFD}'
|
|
||||||
| '\u{D0000}'..='\u{DFFFD}'
|
|
||||||
| '\u{E0000}'..='\u{EFFFD}'
|
|
||||||
| '!'
|
|
||||||
| '$'
|
|
||||||
| '&'
|
|
||||||
| '\''
|
|
||||||
| '('
|
|
||||||
| ')'
|
|
||||||
| '*'
|
|
||||||
| '+'
|
|
||||||
| ','
|
|
||||||
| ';'
|
|
||||||
| '='
|
|
||||||
| ':'
|
|
||||||
| '@' => c.to_string(),
|
|
||||||
_ => {
|
|
||||||
let s = c.to_string();
|
|
||||||
Uri::percent_encode(&s).to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -78,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)
|
||||||
}
|
}
|
||||||
@ -148,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() {
|
||||||
@ -156,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]),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -167,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>,
|
||||||
@ -200,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,
|
||||||
@ -262,7 +215,7 @@ pub fn md_to_html<'a>(
|
|||||||
media_processor: Option<MediaProcessor<'a>>,
|
media_processor: Option<MediaProcessor<'a>>,
|
||||||
) -> (String, HashSet<String>, HashSet<String>) {
|
) -> (String, HashSet<String>, HashSet<String>) {
|
||||||
let base_url = if let Some(base_url) = base_url {
|
let base_url = if let Some(base_url) = base_url {
|
||||||
format!("https://{}/", base_url)
|
format!("//{}/", base_url)
|
||||||
} else {
|
} else {
|
||||||
"/".to_owned()
|
"/".to_owned()
|
||||||
};
|
};
|
||||||
@ -278,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![]))
|
||||||
}
|
}
|
||||||
@ -309,15 +262,15 @@ pub fn md_to_html<'a>(
|
|||||||
text_acc.push(c)
|
text_acc.push(c)
|
||||||
}
|
}
|
||||||
let mention = text_acc;
|
let mention = text_acc;
|
||||||
|
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(),
|
||||||
mention.clone().into(),
|
short_mention.to_owned().into(),
|
||||||
);
|
);
|
||||||
|
|
||||||
mentions.push(mention.clone());
|
mentions.push(mention.clone());
|
||||||
events.push(Event::Start(link.clone()));
|
events.push(Event::Start(link.clone()));
|
||||||
events.push(Event::Text(format!("@{}", &mention).into()));
|
events.push(Event::Text(format!("@{}", &short_mention).into()));
|
||||||
events.push(Event::End(link));
|
events.push(Event::End(link));
|
||||||
|
|
||||||
(
|
(
|
||||||
@ -341,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(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -441,10 +394,6 @@ pub fn md_to_html<'a>(
|
|||||||
(buf, mentions.collect(), hashtags.collect())
|
(buf, mentions.collect(), hashtags.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn escape(string: &str) -> askama_escape::Escaped<askama_escape::Html> {
|
|
||||||
askama_escape::escape(string, askama_escape::Html)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -507,29 +456,15 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_iri_percent_encode_seg() {
|
|
||||||
assert_eq!(
|
|
||||||
&iri_percent_encode_seg("including whitespace"),
|
|
||||||
"including%20whitespace"
|
|
||||||
);
|
|
||||||
assert_eq!(&iri_percent_encode_seg("%20"), "%2520");
|
|
||||||
assert_eq!(&iri_percent_encode_seg("é"), "é");
|
|
||||||
assert_eq!(
|
|
||||||
&iri_percent_encode_seg("空白入り 日本語"),
|
|
||||||
"空白入り%20日本語"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
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")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,55 +1,15 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "plume-front"
|
name = "plume-front"
|
||||||
version = "0.7.2"
|
version = "0.4.0"
|
||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
edition = "2021"
|
edition = "2018"
|
||||||
|
|
||||||
[package.metadata.wasm-pack.profile.release]
|
|
||||||
wasm-opt = false
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
crate-type = ["cdylib"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gettext = "0.4.0"
|
stdweb = "=0.4.18"
|
||||||
gettext-macros = "0.6.1"
|
stdweb-internal-runtime = "=0.1.4"
|
||||||
gettext-utils = "0.1.0"
|
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
||||||
|
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||||
|
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||||
lazy_static = "1.3"
|
lazy_static = "1.3"
|
||||||
serde = "1.0.137"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
wasm-bindgen = "0.2.99"
|
|
||||||
js-sys = "0.3.76"
|
|
||||||
serde_derive = "1.0.123"
|
|
||||||
console_error_panic_hook = "0.1.6"
|
|
||||||
|
|
||||||
[dependencies.web-sys]
|
|
||||||
version = "0.3.76"
|
|
||||||
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'
|
|
||||||
]
|
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
pre-release-hook = ["cargo", "fmt"]
|
|
||||||
pre-release-replacements = []
|
|
||||||
release = false
|
|
||||||
@ -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,9 +51,30 @@ fn no_return(evt: KeyboardEvent) {
|
|||||||
pub enum EditorError {
|
pub enum EditorError {
|
||||||
NoneError,
|
NoneError,
|
||||||
DOMError,
|
DOMError,
|
||||||
|
TypeError,
|
||||||
}
|
}
|
||||||
|
|
||||||
const AUTOSAVE_DEBOUNCE_TIME: i32 = 5000;
|
impl From<std::option::NoneError> for EditorError {
|
||||||
|
fn from(_: std::option::NoneError) -> Self {
|
||||||
|
EditorError::NoneError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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,
|
||||||
@ -65,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
|
||||||
}
|
}
|
||||||
@ -83,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() {
|
||||||
@ -149,35 +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 d = &JsValue::from_f64(autosave_info.last_saved);
|
|
||||||
|
|
||||||
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 {}?";
|
||||||
// next line shows 'unexpected token' error on docker image building
|
Date::from_time(autosave_info.last_saved).to_date_string()
|
||||||
//Date::new(&JsValue::from_f64(autosave_info.last_saved)).to_date_string().as_string().unwrap()
|
|
||||||
Date::new(d).to_date_string().as_string().unwrap()
|
|
||||||
);
|
);
|
||||||
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);
|
||||||
@ -190,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,
|
||||||
@ -225,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);
|
||||||
|
|
||||||
@ -260,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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,32 +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() {
|
let old_title = document().get_element_by_id("plume-editor-title")?;
|
||||||
return Ok(());
|
js! {
|
||||||
}
|
@{&old_ed}.style.display = "none";
|
||||||
let old_ed = old_ed.unwrap();
|
@{&old_title}.style.display = "none";
|
||||||
let old_title = document()
|
};
|
||||||
.get_element_by_id("plume-editor-title")
|
|
||||||
.ok_or(EditorError::NoneError)?;
|
|
||||||
old_ed
|
|
||||||
.dyn_ref::<HtmlElement>()
|
|
||||||
.unwrap()
|
|
||||||
.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");
|
||||||
@ -401,47 +321,35 @@ fn init_editor() -> Result<(), EditorError> {
|
|||||||
content_val.clone(),
|
content_val.clone(),
|
||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
if !content_val.is_empty() {
|
js! { @{&content}.innerHTML = @{content_val}; };
|
||||||
content.set_inner_html(&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")
|
|
||||||
.ok_or(EditorError::NoneError)?
|
|
||||||
.add_event_listener_with_callback("click", show_popup.as_ref().unchecked_ref())
|
|
||||||
.map_err(|_| EditorError::DOMError)?;
|
|
||||||
show_popup.forget();
|
|
||||||
|
|
||||||
show_errors();
|
show_errors();
|
||||||
setup_close_button();
|
setup_close_button();
|
||||||
@ -451,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -502,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(',')
|
||||||
@ -520,165 +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")))
|
cover.parent_element()?.remove_child(&cover).ok();
|
||||||
.map_err(|_| EditorError::DOMError)?;
|
popup.append_child(&cover_label);
|
||||||
cover_label
|
popup.append_child(&cover);
|
||||||
.set_attribute("for", "cover")
|
|
||||||
.map_err(|_| EditorError::DOMError)?;
|
|
||||||
let cover = document
|
|
||||||
.get_element_by_id("cover")
|
|
||||||
.ok_or(EditorError::NoneError)?;
|
|
||||||
cover
|
|
||||||
.parent_element()
|
|
||||||
.ok_or(EditorError::NoneError)?
|
|
||||||
.remove_child(&cover)
|
|
||||||
.ok();
|
|
||||||
popup
|
|
||||||
.append_child(&cover_label)
|
|
||||||
.map_err(|_| EditorError::DOMError)?;
|
|
||||||
popup
|
|
||||||
.append_child(&cover)
|
|
||||||
.map_err(|_| EditorError::DOMError)?;
|
|
||||||
|
|
||||||
if let Some(draft_checkbox) = document.get_element_by_id("draft") {
|
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()
|
|
||||||
.ok_or(EditorError::NoneError)?
|
|
||||||
.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());
|
||||||
.ok_or(EditorError::NoneError)?
|
|
||||||
.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
|
||||||
}
|
}
|
||||||
@ -688,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -721,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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,175 +0,0 @@
|
|||||||
#![recursion_limit = "128"]
|
|
||||||
#![feature(decl_macro, proc_macro_hygiene)]
|
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
extern crate gettext_macros;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate lazy_static;
|
|
||||||
|
|
||||||
use wasm_bindgen::{prelude::*, JsCast};
|
|
||||||
use web_sys::{console, window, Document, Element, Event, HtmlInputElement, TouchEvent};
|
|
||||||
|
|
||||||
init_i18n!(
|
|
||||||
"plume-front",
|
|
||||||
af,
|
|
||||||
ar,
|
|
||||||
bg,
|
|
||||||
ca,
|
|
||||||
cs,
|
|
||||||
cy,
|
|
||||||
da,
|
|
||||||
de,
|
|
||||||
el,
|
|
||||||
en,
|
|
||||||
eo,
|
|
||||||
es,
|
|
||||||
eu,
|
|
||||||
fa,
|
|
||||||
fi,
|
|
||||||
fr,
|
|
||||||
gl,
|
|
||||||
he,
|
|
||||||
hi,
|
|
||||||
hr,
|
|
||||||
hu,
|
|
||||||
it,
|
|
||||||
ja,
|
|
||||||
ko,
|
|
||||||
nb,
|
|
||||||
nl,
|
|
||||||
no,
|
|
||||||
pl,
|
|
||||||
pt,
|
|
||||||
ro,
|
|
||||||
ru,
|
|
||||||
sat,
|
|
||||||
si,
|
|
||||||
sk,
|
|
||||||
sl,
|
|
||||||
sr,
|
|
||||||
sv,
|
|
||||||
tr,
|
|
||||||
uk,
|
|
||||||
vi,
|
|
||||||
zh
|
|
||||||
);
|
|
||||||
|
|
||||||
mod editor;
|
|
||||||
|
|
||||||
compile_i18n!();
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref CATALOG: gettext::Catalog = {
|
|
||||||
let catalogs = include_i18n!();
|
|
||||||
let lang = window().unwrap().navigator().language().unwrap();
|
|
||||||
let lang = lang.split_once('-').map_or("en", |x| x.0);
|
|
||||||
|
|
||||||
let english_position = catalogs
|
|
||||||
.iter()
|
|
||||||
.position(|(language_code, _)| *language_code == "en")
|
|
||||||
.unwrap();
|
|
||||||
catalogs
|
|
||||||
.iter()
|
|
||||||
.find(|(l, _)| l == &lang)
|
|
||||||
.unwrap_or(&catalogs[english_position])
|
|
||||||
.clone()
|
|
||||||
.1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(start)]
|
|
||||||
pub fn main() -> Result<(), JsValue> {
|
|
||||||
extern crate console_error_panic_hook;
|
|
||||||
use std::panic;
|
|
||||||
panic::set_hook(Box::new(console_error_panic_hook::hook));
|
|
||||||
|
|
||||||
menu();
|
|
||||||
search();
|
|
||||||
editor::init()
|
|
||||||
.map_err(|e| console::error_1(&format!("Editor error: {:?}", e).into()))
|
|
||||||
.ok();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Toggle menu on mobile devices
|
|
||||||
///
|
|
||||||
/// It should normally be working fine even without this code
|
|
||||||
/// But :focus-within is not yet supported by Webkit/Blink
|
|
||||||
fn menu() {
|
|
||||||
let document = document();
|
|
||||||
if let Ok(Some(button)) = document.query_selector("#menu a") {
|
|
||||||
if let Some(menu) = document.get_element_by_id("content") {
|
|
||||||
let show_menu = Closure::wrap(Box::new(|_: TouchEvent| {
|
|
||||||
self::document()
|
|
||||||
.get_element_by_id("menu")
|
|
||||||
.map(|menu| {
|
|
||||||
menu.set_attribute("aria-expanded", "true")
|
|
||||||
.map(|_| menu.class_list().add_1("show"))
|
|
||||||
})
|
|
||||||
.unwrap()
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
}) as Box<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
plume-front/src/main.rs
Normal file
112
plume-front/src/main.rs
Normal 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,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "plume-macro"
|
name = "plume-macro"
|
||||||
version = "0.7.2"
|
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"
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
pre-release-hook = ["cargo", "fmt"]
|
|
||||||
pre-release-replacements = []
|
|
||||||
release = false
|
|
||||||
10
plume-macro/src/lib.rs
Executable file → Normal file
10
plume-macro/src/lib.rs
Executable file → Normal file
@ -58,7 +58,7 @@ pub fn import_migrations(input: TokenStream) -> TokenStream {
|
|||||||
(name, up_sql, down_sql)
|
(name, up_sql, down_sql)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let migrations_name = migrations.iter().map(|m| &m.0);
|
let migrations_name = migrations.iter().map(|m| &m.0).collect::<Vec<_>>();
|
||||||
let migrations_up = migrations
|
let migrations_up = migrations
|
||||||
.iter()
|
.iter()
|
||||||
.map(|m| m.1.as_str())
|
.map(|m| m.1.as_str())
|
||||||
@ -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;
|
||||||
|
|||||||
@ -1,51 +1,44 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "plume-models"
|
name = "plume-models"
|
||||||
version = "0.7.2"
|
version = "0.4.0"
|
||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ammonia = "3.2.0"
|
activitypub = "0.1.1"
|
||||||
bcrypt = "0.12.1"
|
ammonia = "2.1.1"
|
||||||
guid-create = "0.3.0"
|
askama_escape = "0.1"
|
||||||
itertools = "0.10.3"
|
bcrypt = "0.5"
|
||||||
|
guid-create = "0.1"
|
||||||
|
heck = "0.3.0"
|
||||||
|
itertools = "0.8.0"
|
||||||
lazy_static = "1.0"
|
lazy_static = "1.0"
|
||||||
ldap3 = "0.11.1"
|
|
||||||
migrations_internals= "1.4.0"
|
migrations_internals= "1.4.0"
|
||||||
openssl = "0.10.40"
|
openssl = "0.10.22"
|
||||||
rocket = "0.4.11"
|
rocket = "0.4.0"
|
||||||
rocket_i18n = "0.4.1"
|
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||||
reqwest = "0.11.11"
|
reqwest = "0.9"
|
||||||
scheduled-thread-pool = "0.2.7"
|
scheduled-thread-pool = "0.2.2"
|
||||||
serde = "1.0.137"
|
serde = "1.0"
|
||||||
rust-s3 = { version = "0.33.0", optional = true, features = ["blocking"] }
|
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_json = "1.0.81"
|
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.16.2"
|
whatlang = "0.7.1"
|
||||||
shrinkwraprs = "0.3.0"
|
shrinkwraprs = "0.2.1"
|
||||||
diesel-derive-newtype = "1.0.0"
|
diesel-derive-newtype = "0.1.2"
|
||||||
glob = "0.3.1"
|
glob = "0.3.0"
|
||||||
lindera-tantivy = { version = "0.7.1", optional = true }
|
lindera-tantivy = { version = "0.1.2", optional = true }
|
||||||
tracing = "0.1.35"
|
|
||||||
riker = "0.4.2"
|
|
||||||
once_cell = "1.12.0"
|
|
||||||
lettre = "0.9.6"
|
|
||||||
native-tls = "0.2.10"
|
|
||||||
activitystreams = "=0.7.0-alpha.20"
|
|
||||||
ahash = "=0.8.11"
|
|
||||||
heck = "0.4.1"
|
|
||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
version = "0.4.31"
|
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"
|
||||||
@ -57,11 +50,9 @@ path = "../plume-common"
|
|||||||
path = "../plume-macro"
|
path = "../plume-macro"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert-json-diff = "2.0.1"
|
|
||||||
diesel_migrations = "1.3.0"
|
diesel_migrations = "1.3.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
postgres = ["diesel/postgres", "plume-macro/postgres" ]
|
postgres = ["diesel/postgres", "plume-macro/postgres" ]
|
||||||
sqlite = ["diesel/sqlite", "plume-macro/sqlite" ]
|
sqlite = ["diesel/sqlite", "plume-macro/sqlite" ]
|
||||||
search-lindera = ["lindera-tantivy"]
|
search-lindera = ["lindera-tantivy"]
|
||||||
s3 = ["rust-s3"]
|
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
pre-release-hook = ["cargo", "fmt"]
|
|
||||||
pre-release-replacements = []
|
|
||||||
release = false
|
|
||||||
@ -5,7 +5,7 @@ use rocket::{
|
|||||||
Outcome,
|
Outcome,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Wrapper around User to use as a request guard on pages exclusively reserved to admins.
|
/// Wrapper around User to use as a request guard on pages reserved to admins.
|
||||||
pub struct Admin(pub User);
|
pub struct Admin(pub User);
|
||||||
|
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for Admin {
|
impl<'a, 'r> FromRequest<'a, 'r> for Admin {
|
||||||
@ -21,23 +21,6 @@ impl<'a, 'r> FromRequest<'a, 'r> for Admin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Same as `Admin` but it forwards to next guard if the user is not an admin.
|
|
||||||
/// It's useful when there are multiple implementations of routes for admin and moderator.
|
|
||||||
pub struct InclusiveAdmin(pub User);
|
|
||||||
|
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for InclusiveAdmin {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<InclusiveAdmin, ()> {
|
|
||||||
let user = request.guard::<User>()?;
|
|
||||||
if user.is_admin() {
|
|
||||||
Outcome::Success(InclusiveAdmin(user))
|
|
||||||
} else {
|
|
||||||
Outcome::Forward(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Same as `Admin` but for moderators.
|
/// Same as `Admin` but for moderators.
|
||||||
pub struct Moderator(pub User);
|
pub struct Moderator(pub User);
|
||||||
|
|
||||||
|
|||||||
@ -86,24 +86,20 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApiToken {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut parsed_header = headers[0].split(' ');
|
let mut parsed_header = headers[0].split(' ');
|
||||||
let auth_type = parsed_header
|
let auth_type = parsed_header.next().map_or_else(
|
||||||
.next()
|
|| Outcome::Failure((Status::BadRequest, TokenError::NoType)),
|
||||||
.map_or_else::<rocket::Outcome<&str, _, ()>, _, _>(
|
Outcome::Success,
|
||||||
|| Outcome::Failure((Status::BadRequest, TokenError::NoType)),
|
)?;
|
||||||
Outcome::Success,
|
let val = parsed_header.next().map_or_else(
|
||||||
)?;
|
|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)),
|
||||||
let val = parsed_header
|
Outcome::Success,
|
||||||
.next()
|
)?;
|
||||||
.map_or_else::<rocket::Outcome<&str, _, ()>, _, _>(
|
|
||||||
|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)),
|
|
||||||
Outcome::Success,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if auth_type == "Bearer" {
|
if auth_type == "Bearer" {
|
||||||
let conn = request
|
let conn = request
|
||||||
.guard::<DbConn>()
|
.guard::<DbConn>()
|
||||||
.map_failure(|_| (Status::InternalServerError, TokenError::DbError))?;
|
.map_failure(|_| (Status::InternalServerError, TokenError::DbError))?;
|
||||||
if let Ok(token) = ApiToken::find_by_value(&conn, val) {
|
if let Ok(token) = ApiToken::find_by_value(&*conn, val) {
|
||||||
return Outcome::Success(token);
|
return Outcome::Success(token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,7 +28,7 @@ impl BlocklistedEmail {
|
|||||||
pub fn delete_entries(conn: &Connection, ids: Vec<i32>) -> Result<bool> {
|
pub fn delete_entries(conn: &Connection, ids: Vec<i32>) -> Result<bool> {
|
||||||
use diesel::delete;
|
use diesel::delete;
|
||||||
for i in ids {
|
for i in ids {
|
||||||
let be: BlocklistedEmail = BlocklistedEmail::find_by_id(conn, i)?;
|
let be: BlocklistedEmail = BlocklistedEmail::find_by_id(&conn, i)?;
|
||||||
delete(&be).execute(conn)?;
|
delete(&be).execute(conn)?;
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok(true)
|
||||||
@ -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,29 +106,33 @@ 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!(BlocklistedEmail::matches_blocklist(&conn, no_match)
|
assert_eq!(
|
||||||
.unwrap()
|
BlocklistedEmail::matches_blocklist(conn, no_match)
|
||||||
.is_none());
|
.unwrap()
|
||||||
|
.is_none(),
|
||||||
|
true
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,12 @@
|
|||||||
use heck::ToUpperCamelCase;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
instance::*, medias::Media, posts::Post, safe_string::SafeString, schema::blogs, users::User,
|
ap_url, instance::*, medias::Media, posts::Post, safe_string::SafeString, schema::blogs,
|
||||||
Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE,
|
search::Searcher, users::User, Connection, Error, PlumeRocket, Result, ITEMS_PER_PAGE,
|
||||||
};
|
};
|
||||||
use activitystreams::{
|
use activitypub::{
|
||||||
actor::{ApActor, ApActorExt, AsApActor, Group},
|
actor::Group,
|
||||||
base::AnyBase,
|
|
||||||
collection::{OrderedCollection, OrderedCollectionPage},
|
collection::{OrderedCollection, OrderedCollectionPage},
|
||||||
iri_string::types::IriString,
|
object::Image,
|
||||||
object::{kind::ImageType, ApObject, Image, ObjectExt},
|
CustomObject,
|
||||||
prelude::*,
|
|
||||||
};
|
};
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{self, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
use diesel::{self, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||||
@ -19,17 +16,17 @@ use openssl::{
|
|||||||
rsa::Rsa,
|
rsa::Rsa,
|
||||||
sign::{Signer, Verifier},
|
sign::{Signer, Verifier},
|
||||||
};
|
};
|
||||||
use plume_common::{
|
use plume_common::activity_pub::{
|
||||||
activity_pub::{
|
inbox::{AsActor, FromId},
|
||||||
inbox::{AsActor, FromId},
|
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
|
||||||
sign, ActivityStream, ApSignature, CustomGroup, Id, IntoId, PublicKey, Source,
|
|
||||||
SourceProperty, ToAsString, ToAsUri,
|
|
||||||
},
|
|
||||||
utils::iri_percent_encode_seg,
|
|
||||||
};
|
};
|
||||||
|
use serde_json;
|
||||||
|
use url::Url;
|
||||||
use webfinger::*;
|
use webfinger::*;
|
||||||
|
|
||||||
#[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)]
|
pub type CustomGroup = CustomObject<ApSignature, Group>;
|
||||||
|
|
||||||
|
#[derive(Queryable, Identifiable, Clone, AsChangeset)]
|
||||||
#[changeset_options(treat_none_as_null = "true")]
|
#[changeset_options(treat_none_as_null = "true")]
|
||||||
pub struct Blog {
|
pub struct Blog {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
@ -87,13 +84,9 @@ impl Blog {
|
|||||||
|
|
||||||
if inserted.fqn.is_empty() {
|
if inserted.fqn.is_empty() {
|
||||||
if instance.local {
|
if instance.local {
|
||||||
inserted.fqn = iri_percent_encode_seg(&inserted.actor_id);
|
inserted.fqn = inserted.actor_id.clone();
|
||||||
} else {
|
} else {
|
||||||
inserted.fqn = format!(
|
inserted.fqn = format!("{}@{}", inserted.actor_id, instance.public_domain);
|
||||||
"{}@{}",
|
|
||||||
iri_percent_encode_seg(&inserted.actor_id),
|
|
||||||
instance.public_domain
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,19 +96,6 @@ impl Blog {
|
|||||||
find_by!(blogs, find_by_ap_url, ap_url as &str);
|
find_by!(blogs, find_by_ap_url, ap_url as &str);
|
||||||
find_by!(blogs, find_by_name, actor_id as &str, instance_id as i32);
|
find_by!(blogs, find_by_name, actor_id as &str, instance_id as i32);
|
||||||
|
|
||||||
/// Remove non alphanumeric characters and CamelCase a string
|
|
||||||
pub fn slug(title: &str) -> String {
|
|
||||||
title.to_upper_camel_case()
|
|
||||||
.chars()
|
|
||||||
.filter(|c| c.is_alphanumeric())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
pub fn slug(title: &str) -> &str {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
pub fn get_instance(&self, conn: &Connection) -> Result<Instance> {
|
pub fn get_instance(&self, conn: &Connection) -> Result<Instance> {
|
||||||
Instance::get(conn, self.instance_id)
|
Instance::get(conn, self.instance_id)
|
||||||
}
|
}
|
||||||
@ -152,168 +132,141 @@ impl Blog {
|
|||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_fqn(conn: &Connection, 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: &Connection, 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| {
|
.and_then(|l| Blog::from_id(c, &l.href?, None).map_err(|(_, e)| e))
|
||||||
Blog::from_id(
|
|
||||||
conn,
|
|
||||||
&l.href.ok_or(Error::MissingApProperty)?,
|
|
||||||
None,
|
|
||||||
CONFIG.proxy(),
|
|
||||||
)
|
|
||||||
.map_err(|(_, e)| e)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> {
|
pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> {
|
||||||
let mut blog = ApActor::new(self.inbox_url.parse()?, Group::new());
|
let mut blog = Group::default();
|
||||||
blog.set_preferred_username(iri_percent_encode_seg(&self.actor_id));
|
blog.ap_actor_props
|
||||||
blog.set_name(self.title.clone());
|
.set_preferred_username_string(self.actor_id.clone())?;
|
||||||
blog.set_outbox(self.outbox_url.parse()?);
|
blog.object_props.set_name_string(self.title.clone())?;
|
||||||
blog.set_summary(self.summary_html.to_string());
|
blog.ap_actor_props
|
||||||
let source = SourceProperty {
|
.set_outbox_string(self.outbox_url.clone())?;
|
||||||
source: Source {
|
blog.ap_actor_props
|
||||||
content: self.summary.clone(),
|
.set_inbox_string(self.inbox_url.clone())?;
|
||||||
media_type: String::from("text/markdown"),
|
blog.object_props
|
||||||
},
|
.set_summary_string(self.summary_html.to_string())?;
|
||||||
};
|
blog.ap_object_props.set_source_object(Source {
|
||||||
|
content: self.summary.clone(),
|
||||||
|
media_type: String::from("text/markdown"),
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut icon = Image::new();
|
let mut icon = Image::default();
|
||||||
let _ = self.icon_id.map(|id| {
|
icon.object_props.set_url_string(
|
||||||
Media::get(conn, id).and_then(|m| {
|
self.icon_id
|
||||||
let _ = m
|
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
|
||||||
.url()
|
.unwrap_or_default(),
|
||||||
.and_then(|url| url.parse::<IriString>().map_err(|_| Error::Url))
|
)?;
|
||||||
.map(|url| icon.set_url(url));
|
icon.object_props.set_attributed_to_link(
|
||||||
icon.set_attributed_to(
|
self.icon_id
|
||||||
User::get(conn, m.owner_id)?
|
.and_then(|id| {
|
||||||
.into_id()
|
Media::get(conn, id)
|
||||||
.parse::<IriString>()?,
|
.and_then(|m| Ok(User::get(conn, m.owner_id)?.into_id()))
|
||||||
);
|
.ok()
|
||||||
Ok(())
|
})
|
||||||
})
|
.unwrap_or_else(|| Id::new(String::new())),
|
||||||
});
|
)?;
|
||||||
blog.set_icon(icon.into_any_base()?);
|
blog.object_props.set_icon_object(icon)?;
|
||||||
|
|
||||||
let mut banner = Image::new();
|
let mut banner = Image::default();
|
||||||
let _ = self.banner_id.map(|id| {
|
banner.object_props.set_url_string(
|
||||||
Media::get(conn, id).and_then(|m| {
|
self.banner_id
|
||||||
let _ = m
|
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
|
||||||
.url()
|
.unwrap_or_default(),
|
||||||
.and_then(|url| url.parse::<IriString>().map_err(|_| Error::Url))
|
)?;
|
||||||
.map(|url| banner.set_url(url));
|
banner.object_props.set_attributed_to_link(
|
||||||
banner.set_attributed_to(
|
self.banner_id
|
||||||
User::get(conn, m.owner_id)?
|
.and_then(|id| {
|
||||||
.into_id()
|
Media::get(conn, id)
|
||||||
.parse::<IriString>()?,
|
.and_then(|m| Ok(User::get(conn, m.owner_id)?.into_id()))
|
||||||
);
|
.ok()
|
||||||
Ok(())
|
})
|
||||||
})
|
.unwrap_or_else(|| Id::new(String::new())),
|
||||||
});
|
)?;
|
||||||
blog.set_image(banner.into_any_base()?);
|
blog.object_props.set_image_object(banner)?;
|
||||||
|
|
||||||
blog.set_id(self.ap_url.parse()?);
|
blog.object_props.set_id_string(self.ap_url.clone())?;
|
||||||
|
|
||||||
let pub_key = PublicKey {
|
let mut public_key = PublicKey::default();
|
||||||
id: format!("{}#main-key", self.ap_url).parse()?,
|
public_key.set_id_string(format!("{}#main-key", self.ap_url))?;
|
||||||
owner: self.ap_url.parse()?,
|
public_key.set_owner_string(self.ap_url.clone())?;
|
||||||
public_key_pem: self.public_key.clone(),
|
public_key.set_public_key_pem_string(self.public_key.clone())?;
|
||||||
};
|
let mut ap_signature = ApSignature::default();
|
||||||
let ap_signature = ApSignature {
|
ap_signature.set_public_key_publickey(public_key)?;
|
||||||
public_key: pub_key,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(CustomGroup::new(blog, ap_signature, source))
|
Ok(CustomGroup::new(blog, ap_signature))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> {
|
pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> {
|
||||||
self.outbox_collection(conn).map(ActivityStream::new)
|
let mut coll = OrderedCollection::default();
|
||||||
}
|
coll.collection_props.items = serde_json::to_value(self.get_activities(conn)?)?;
|
||||||
pub fn outbox_collection(&self, conn: &Connection) -> Result<OrderedCollection> {
|
coll.collection_props
|
||||||
let acts = self.get_activities(conn);
|
.set_total_items_u64(self.get_activities(conn)?.len() as u64)?;
|
||||||
let acts = acts
|
coll.collection_props
|
||||||
.iter()
|
.set_first_link(Id::new(ap_url(&format!("{}?page=1", &self.outbox_url))))?;
|
||||||
.filter_map(|value| AnyBase::from_arbitrary_json(value).ok())
|
coll.collection_props
|
||||||
.collect::<Vec<AnyBase>>();
|
.set_last_link(Id::new(ap_url(&format!(
|
||||||
let n_acts = acts.len();
|
|
||||||
let mut coll = OrderedCollection::new();
|
|
||||||
coll.set_many_items(acts);
|
|
||||||
coll.set_total_items(n_acts as u64);
|
|
||||||
coll.set_first(format!("{}?page=1", &self.outbox_url).parse::<IriString>()?);
|
|
||||||
coll.set_last(
|
|
||||||
format!(
|
|
||||||
"{}?page={}",
|
"{}?page={}",
|
||||||
&self.outbox_url,
|
&self.outbox_url,
|
||||||
(n_acts as u64 + ITEMS_PER_PAGE as u64 - 1) as u64 / ITEMS_PER_PAGE as u64
|
(self.get_activities(conn)?.len() as u64 + ITEMS_PER_PAGE as u64 - 1) as u64
|
||||||
)
|
/ ITEMS_PER_PAGE as u64
|
||||||
.parse::<IriString>()?,
|
))))?;
|
||||||
);
|
Ok(ActivityStream::new(coll))
|
||||||
Ok(coll)
|
|
||||||
}
|
}
|
||||||
pub fn outbox_page(
|
pub fn outbox_page(
|
||||||
&self,
|
&self,
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
(min, max): (i32, i32),
|
(min, max): (i32, i32),
|
||||||
) -> Result<ActivityStream<OrderedCollectionPage>> {
|
) -> Result<ActivityStream<OrderedCollectionPage>> {
|
||||||
self.outbox_collection_page(conn, (min, max))
|
let mut coll = OrderedCollectionPage::default();
|
||||||
.map(ActivityStream::new)
|
let acts = self.get_activity_page(&conn, (min, max))?;
|
||||||
}
|
|
||||||
pub fn outbox_collection_page(
|
|
||||||
&self,
|
|
||||||
conn: &Connection,
|
|
||||||
(min, max): (i32, i32),
|
|
||||||
) -> Result<OrderedCollectionPage> {
|
|
||||||
let mut coll = OrderedCollectionPage::new();
|
|
||||||
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.set_next(
|
coll.collection_page_props.set_next_link(Id::new(&format!(
|
||||||
format!("{}?page={}", &self.outbox_url, min / ITEMS_PER_PAGE + 1)
|
"{}?page={}",
|
||||||
.parse::<IriString>()?,
|
&self.outbox_url,
|
||||||
);
|
min / ITEMS_PER_PAGE + 1
|
||||||
coll.set_prev(
|
)))?;
|
||||||
format!("{}?page={}", &self.outbox_url, min / ITEMS_PER_PAGE - 1)
|
coll.collection_page_props.set_prev_link(Id::new(&format!(
|
||||||
.parse::<IriString>()?,
|
"{}?page={}",
|
||||||
);
|
&self.outbox_url,
|
||||||
coll.set_many_items(
|
min / ITEMS_PER_PAGE - 1
|
||||||
acts.iter()
|
)))?;
|
||||||
.filter_map(|value| AnyBase::from_arbitrary_json(value).ok()),
|
coll.collection_props.items = serde_json::to_value(acts)?;
|
||||||
);
|
Ok(ActivityStream::new(coll))
|
||||||
Ok(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>> {
|
||||||
PKey::from_rsa(Rsa::private_key_from_pem(
|
PKey::from_rsa(Rsa::private_key_from_pem(
|
||||||
self.private_key
|
self.private_key.clone()?.as_ref(),
|
||||||
.clone()
|
|
||||||
.ok_or(Error::MissingApProperty)?
|
|
||||||
.as_ref(),
|
|
||||||
)?)
|
)?)
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
@ -365,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)
|
||||||
@ -382,102 +335,20 @@ impl IntoId for Blog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromId<Connection> for Blog {
|
impl FromId<PlumeRocket> for Blog {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = CustomGroup;
|
type Object = CustomGroup;
|
||||||
|
|
||||||
fn from_db(conn: &Connection, 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: &Connection, acct: CustomGroup) -> Result<Self> {
|
fn from_activity(c: &PlumeRocket, acct: CustomGroup) -> Result<Self> {
|
||||||
let (name, outbox_url, inbox_url) = {
|
let url = Url::parse(&acct.object.object_props.id_string()?)?;
|
||||||
let actor = acct.ap_actor_ref();
|
let inst = url.host_str()?;
|
||||||
let name = actor
|
let instance = Instance::find_by_domain(&c.conn, inst).or_else(|_| {
|
||||||
.preferred_username()
|
|
||||||
.ok_or(Error::MissingApProperty)?
|
|
||||||
.to_string();
|
|
||||||
if name.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
|
|
||||||
tracing::error!("preferredUsername includes invalid character(s): {}", &name);
|
|
||||||
return Err(Error::InvalidValue);
|
|
||||||
}
|
|
||||||
(
|
|
||||||
name,
|
|
||||||
actor.outbox()?.ok_or(Error::MissingApProperty)?.to_string(),
|
|
||||||
actor.inbox()?.to_string(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut new_blog = NewBlog {
|
|
||||||
actor_id: name.to_string(),
|
|
||||||
outbox_url,
|
|
||||||
inbox_url,
|
|
||||||
public_key: acct.ext_one.public_key.public_key_pem.to_string(),
|
|
||||||
private_key: None,
|
|
||||||
theme: None,
|
|
||||||
..NewBlog::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let object = ApObject::new(acct.inner);
|
|
||||||
new_blog.title = object
|
|
||||||
.name()
|
|
||||||
.and_then(|name| name.to_as_string())
|
|
||||||
.unwrap_or(name);
|
|
||||||
new_blog.summary_html = SafeString::new(
|
|
||||||
&object
|
|
||||||
.summary()
|
|
||||||
.and_then(|summary| summary.to_as_string())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let icon_id = object
|
|
||||||
.icon()
|
|
||||||
.and_then(|icons| {
|
|
||||||
icons.iter().next().and_then(|icon| {
|
|
||||||
let icon = icon.to_owned().extend::<Image, ImageType>().ok()??;
|
|
||||||
let owner = icon.attributed_to()?.to_as_uri()?;
|
|
||||||
Media::save_remote(
|
|
||||||
conn,
|
|
||||||
icon.url()?.to_as_uri()?,
|
|
||||||
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
|
|
||||||
)
|
|
||||||
.ok()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.map(|m| m.id);
|
|
||||||
new_blog.icon_id = icon_id;
|
|
||||||
|
|
||||||
let banner_id = object
|
|
||||||
.image()
|
|
||||||
.and_then(|banners| {
|
|
||||||
banners.iter().next().and_then(|banner| {
|
|
||||||
let banner = banner.to_owned().extend::<Image, ImageType>().ok()??;
|
|
||||||
let owner = banner.attributed_to()?.to_as_uri()?;
|
|
||||||
Media::save_remote(
|
|
||||||
conn,
|
|
||||||
banner.url()?.to_as_uri()?,
|
|
||||||
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
|
|
||||||
)
|
|
||||||
.ok()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.map(|m| m.id);
|
|
||||||
new_blog.banner_id = banner_id;
|
|
||||||
|
|
||||||
new_blog.summary = acct.ext_two.source.content;
|
|
||||||
|
|
||||||
let any_base = AnyBase::from_extended(object)?;
|
|
||||||
let id = any_base.id().ok_or(Error::MissingApProperty)?;
|
|
||||||
new_blog.ap_url = id.to_string();
|
|
||||||
|
|
||||||
let inst = id
|
|
||||||
.authority_components()
|
|
||||||
.ok_or(Error::Url)?
|
|
||||||
.host()
|
|
||||||
.to_string();
|
|
||||||
let instance = Instance::find_by_domain(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(),
|
||||||
@ -492,13 +363,75 @@ impl FromId<Connection> for Blog {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
new_blog.instance_id = instance.id;
|
let icon_id = acct
|
||||||
|
.object
|
||||||
|
.object_props
|
||||||
|
.icon_image()
|
||||||
|
.ok()
|
||||||
|
.and_then(|icon| {
|
||||||
|
let owner = icon.object_props.attributed_to_link::<Id>().ok()?;
|
||||||
|
Media::save_remote(
|
||||||
|
&c.conn,
|
||||||
|
icon.object_props.url_string().ok()?,
|
||||||
|
&User::from_id(c, &owner, None).ok()?,
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.map(|m| m.id);
|
||||||
|
|
||||||
Blog::insert(conn, new_blog)
|
let banner_id = acct
|
||||||
}
|
.object
|
||||||
|
.object_props
|
||||||
|
.image_image()
|
||||||
|
.ok()
|
||||||
|
.and_then(|banner| {
|
||||||
|
let owner = banner.object_props.attributed_to_link::<Id>().ok()?;
|
||||||
|
Media::save_remote(
|
||||||
|
&c.conn,
|
||||||
|
banner.object_props.url_string().ok()?,
|
||||||
|
&User::from_id(c, &owner, None).ok()?,
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.map(|m| m.id);
|
||||||
|
|
||||||
fn get_sender() -> &'static dyn sign::Signer {
|
let name = acct.object.ap_actor_props.preferred_username_string()?;
|
||||||
Instance::get_local_instance_user().expect("Failed to local instance user")
|
if name.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
|
||||||
|
return Err(Error::InvalidValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
Blog::insert(
|
||||||
|
&c.conn,
|
||||||
|
NewBlog {
|
||||||
|
actor_id: name.clone(),
|
||||||
|
title: acct.object.object_props.name_string().unwrap_or(name),
|
||||||
|
outbox_url: acct.object.ap_actor_props.outbox_string()?,
|
||||||
|
inbox_url: acct.object.ap_actor_props.inbox_string()?,
|
||||||
|
summary: acct
|
||||||
|
.object
|
||||||
|
.ap_object_props
|
||||||
|
.source_object::<Source>()
|
||||||
|
.map(|s| s.content)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
instance_id: instance.id,
|
||||||
|
ap_url: acct.object.object_props.id_string()?,
|
||||||
|
public_key: acct
|
||||||
|
.custom_props
|
||||||
|
.public_key_publickey()?
|
||||||
|
.public_key_pem_string()?,
|
||||||
|
private_key: None,
|
||||||
|
banner_id,
|
||||||
|
icon_id,
|
||||||
|
summary_html: SafeString::new(
|
||||||
|
&acct
|
||||||
|
.object
|
||||||
|
.object_props
|
||||||
|
.summary_string()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
),
|
||||||
|
theme: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -519,22 +452,24 @@ impl AsActor<&PlumeRocket> for Blog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl sign::Signer for Blog {
|
impl sign::Signer for Blog {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
fn get_key_id(&self) -> String {
|
fn get_key_id(&self) -> String {
|
||||||
format!("{}#main-key", self.ap_url)
|
format!("{}#main-key", self.ap_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign(&self, to_sign: &str) -> sign::Result<Vec<u8>> {
|
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
|
||||||
let key = self.get_keypair().map_err(|_| sign::Error())?;
|
let key = self.get_keypair()?;
|
||||||
let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
|
let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
|
||||||
signer.update(to_sign.as_bytes())?;
|
signer.update(to_sign.as_bytes())?;
|
||||||
signer.sign_to_vec().map_err(sign::Error::from)
|
signer.sign_to_vec().map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify(&self, data: &str, signature: &[u8]) -> sign::Result<bool> {
|
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
|
||||||
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
|
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
|
||||||
let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?;
|
let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?;
|
||||||
verifier.update(data.as_bytes())?;
|
verifier.update(data.as_bytes())?;
|
||||||
verifier.verify(signature).map_err(sign::Error::from)
|
verifier.verify(&signature).map_err(Error::from)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -562,17 +497,21 @@ 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 assert_json_diff::assert_json_eq;
|
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
use serde_json::to_value;
|
|
||||||
|
|
||||||
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Blog>) {
|
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Blog>) {
|
||||||
instance_tests::fill_database(conn);
|
instance_tests::fill_database(conn);
|
||||||
let users = usersTests::fill_database(conn);
|
let users = usersTests::fill_database(conn);
|
||||||
let mut blog1 = Blog::insert(
|
let blog1 = Blog::insert(
|
||||||
conn,
|
conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"BlogName".to_owned(),
|
"BlogName".to_owned(),
|
||||||
@ -645,41 +584,6 @@ pub(crate) mod tests {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
blog1.icon_id = Some(
|
|
||||||
Media::insert(
|
|
||||||
conn,
|
|
||||||
NewMedia {
|
|
||||||
file_path: "aaa.png".into(),
|
|
||||||
alt_text: String::new(),
|
|
||||||
is_remote: false,
|
|
||||||
remote_url: None,
|
|
||||||
sensitive: false,
|
|
||||||
content_warning: None,
|
|
||||||
owner_id: users[0].id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.id,
|
|
||||||
);
|
|
||||||
blog1.banner_id = Some(
|
|
||||||
Media::insert(
|
|
||||||
conn,
|
|
||||||
NewMedia {
|
|
||||||
file_path: "bbb.png".into(),
|
|
||||||
alt_text: String::new(),
|
|
||||||
is_remote: false,
|
|
||||||
remote_url: None,
|
|
||||||
sensitive: false,
|
|
||||||
content_warning: None,
|
|
||||||
owner_id: users[0].id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.id,
|
|
||||||
);
|
|
||||||
let _: Blog = blog1.save_changes(conn).unwrap();
|
|
||||||
|
|
||||||
(users, vec![blog1, blog2, blog3])
|
(users, vec![blog1, blog2, blog3])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -813,7 +717,8 @@ 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);
|
||||||
|
|
||||||
@ -829,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(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -863,7 +768,9 @@ pub(crate) mod tests {
|
|||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let (_, blogs) = fill_database(conn);
|
let (_, blogs) = fill_database(conn);
|
||||||
|
|
||||||
blogs[0].delete(conn).unwrap();
|
blogs[0]
|
||||||
|
.delete(conn, &get_searcher(&CONFIG.search_tokenizers))
|
||||||
|
.unwrap();
|
||||||
assert!(Blog::get(conn, blogs[0].id).is_err());
|
assert!(Blog::get(conn, blogs[0].id).is_err());
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@ -873,6 +780,7 @@ 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 searcher = get_searcher(&CONFIG.search_tokenizers);
|
||||||
let (user, _) = fill_database(conn);
|
let (user, _) = fill_database(conn);
|
||||||
|
|
||||||
let b1 = Blog::insert(
|
let b1 = Blog::insert(
|
||||||
@ -929,10 +837,10 @@ 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(())
|
||||||
})
|
})
|
||||||
@ -940,7 +848,8 @@ 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, mut blogs) = fill_database(conn);
|
let (users, mut blogs) = fill_database(conn);
|
||||||
blogs[0].icon_id = Some(
|
blogs[0].icon_id = Some(
|
||||||
@ -975,10 +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);
|
||||||
@ -996,90 +906,4 @@ pub(crate) mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn to_activity() {
|
|
||||||
let conn = &db();
|
|
||||||
conn.test_transaction::<_, Error, _>(|| {
|
|
||||||
let (_users, blogs) = fill_database(conn);
|
|
||||||
let blog = &blogs[0];
|
|
||||||
let act = blog.to_activity(conn)?;
|
|
||||||
|
|
||||||
let expected = json!({
|
|
||||||
"icon": {
|
|
||||||
"attributedTo": "https://plu.me/@/admin/",
|
|
||||||
"type": "Image",
|
|
||||||
"url": "https://plu.me/aaa.png"
|
|
||||||
},
|
|
||||||
"id": "https://plu.me/~/BlogName/",
|
|
||||||
"image": {
|
|
||||||
"attributedTo": "https://plu.me/@/admin/",
|
|
||||||
"type": "Image",
|
|
||||||
"url": "https://plu.me/bbb.png"
|
|
||||||
},
|
|
||||||
"inbox": "https://plu.me/~/BlogName/inbox",
|
|
||||||
"name": "Blog name",
|
|
||||||
"outbox": "https://plu.me/~/BlogName/outbox",
|
|
||||||
"preferredUsername": "BlogName",
|
|
||||||
"publicKey": {
|
|
||||||
"id": "https://plu.me/~/BlogName/#main-key",
|
|
||||||
"owner": "https://plu.me/~/BlogName/",
|
|
||||||
"publicKeyPem": blog.public_key
|
|
||||||
},
|
|
||||||
"source": {
|
|
||||||
"content": "This is a small blog",
|
|
||||||
"mediaType": "text/markdown"
|
|
||||||
},
|
|
||||||
"summary": "",
|
|
||||||
"type": "Group"
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_json_eq!(to_value(act)?, expected);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn outbox_collection() {
|
|
||||||
let conn = &db();
|
|
||||||
conn.test_transaction::<_, Error, _>(|| {
|
|
||||||
let (_users, blogs) = fill_database(conn);
|
|
||||||
let blog = &blogs[0];
|
|
||||||
let act = blog.outbox_collection(conn)?;
|
|
||||||
|
|
||||||
let expected = json!({
|
|
||||||
"items": [],
|
|
||||||
"totalItems": 0,
|
|
||||||
"first": "https://plu.me/~/BlogName/outbox?page=1",
|
|
||||||
"last": "https://plu.me/~/BlogName/outbox?page=0",
|
|
||||||
"type": "OrderedCollection"
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_json_eq!(to_value(act)?, expected);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn outbox_collection_page() {
|
|
||||||
let conn = &db();
|
|
||||||
conn.test_transaction::<_, Error, _>(|| {
|
|
||||||
let (_users, blogs) = fill_database(conn);
|
|
||||||
let blog = &blogs[0];
|
|
||||||
let act = blog.outbox_collection_page(conn, (33, 36))?;
|
|
||||||
|
|
||||||
let expected = json!({
|
|
||||||
"next": "https://plu.me/~/BlogName/outbox?page=3",
|
|
||||||
"prev": "https://plu.me/~/BlogName/outbox?page=1",
|
|
||||||
"items": [],
|
|
||||||
"type": "OrderedCollectionPage"
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_json_eq!(to_value(act)?, expected);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,28 +8,23 @@ 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 activitystreams::{
|
use activitypub::{
|
||||||
activity::{Create, Delete},
|
activity::{Create, Delete},
|
||||||
base::{AnyBase, Base},
|
link,
|
||||||
iri_string::types::IriString,
|
|
||||||
link::{self, kind::MentionType},
|
|
||||||
object::{Note, Tombstone},
|
object::{Note, Tombstone},
|
||||||
prelude::*,
|
|
||||||
primitives::OneOrMany,
|
|
||||||
time::OffsetDateTime,
|
|
||||||
};
|
};
|
||||||
use chrono::{self, NaiveDateTime};
|
use chrono::{self, NaiveDateTime};
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||||
use plume_common::{
|
use plume_common::{
|
||||||
activity_pub::{
|
activity_pub::{
|
||||||
inbox::{AsActor, AsObject, FromId},
|
inbox::{AsActor, AsObject, FromId},
|
||||||
sign::Signer,
|
Id, IntoId, PUBLIC_VISIBILITY,
|
||||||
IntoId, ToAsString, ToAsUri, PUBLIC_VISIBILITY,
|
|
||||||
},
|
},
|
||||||
utils,
|
utils,
|
||||||
};
|
};
|
||||||
|
use serde_json;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
#[derive(Queryable, Identifiable, Clone, AsChangeset)]
|
#[derive(Queryable, Identifiable, Clone, AsChangeset)]
|
||||||
@ -63,7 +58,7 @@ impl Comment {
|
|||||||
insert!(comments, NewComment, |inserted, conn| {
|
insert!(comments, NewComment, |inserted, conn| {
|
||||||
if inserted.ap_url.is_none() {
|
if inserted.ap_url.is_none() {
|
||||||
inserted.ap_url = Some(format!(
|
inserted.ap_url = Some(format!(
|
||||||
"{}/comment/{}",
|
"{}comment/{}",
|
||||||
inserted.get_post(conn)?.ap_url,
|
inserted.get_post(conn)?.ap_url,
|
||||||
inserted.id
|
inserted.id
|
||||||
));
|
));
|
||||||
@ -73,7 +68,6 @@ impl Comment {
|
|||||||
});
|
});
|
||||||
get!(comments);
|
get!(comments);
|
||||||
list_by!(comments, list_by_post, post_id as i32);
|
list_by!(comments, list_by_post, post_id as i32);
|
||||||
list_by!(comments, list_by_author, author_id as i32);
|
|
||||||
find_by!(comments, find_by_ap_url, ap_url as &str);
|
find_by!(comments, find_by_ap_url, ap_url as &str);
|
||||||
|
|
||||||
pub fn get_author(&self, conn: &Connection) -> Result<User> {
|
pub fn get_author(&self, conn: &Connection) -> Result<User> {
|
||||||
@ -111,68 +105,54 @@ impl Comment {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_activity(&self, conn: &Connection) -> 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::new();
|
let mut note = Note::default();
|
||||||
let to = vec![PUBLIC_VISIBILITY.parse::<IriString>()?];
|
let to = vec![Id::new(PUBLIC_VISIBILITY.to_string())];
|
||||||
|
|
||||||
note.set_id(
|
note.object_props
|
||||||
self.ap_url
|
.set_id_string(self.ap_url.clone().unwrap_or_default())?;
|
||||||
.clone()
|
note.object_props
|
||||||
.unwrap_or_default()
|
.set_summary_string(self.spoiler_text.clone())?;
|
||||||
.parse::<IriString>()?,
|
note.object_props.set_content_string(html)?;
|
||||||
);
|
note.object_props
|
||||||
note.set_summary(self.spoiler_text.clone());
|
.set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else(
|
||||||
note.set_content(html);
|
|| Ok(Post::get(&c.conn, self.post_id)?.ap_url),
|
||||||
note.set_in_reply_to(self.in_response_to_id.map_or_else(
|
|id| Ok(Comment::get(&c.conn, id)?.ap_url.unwrap_or_default()) as Result<String>,
|
||||||
|| Post::get(conn, self.post_id).map(|post| post.ap_url),
|
)?))?;
|
||||||
|id| Comment::get(conn, id).map(|comment| comment.ap_url.unwrap_or_default()),
|
note.object_props
|
||||||
)?);
|
.set_published_string(chrono::Utc::now().to_rfc3339())?;
|
||||||
note.set_published(
|
note.object_props.set_attributed_to_link(author.into_id())?;
|
||||||
OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos_opt().unwrap().into())
|
note.object_props.set_to_link_vec(to)?;
|
||||||
.expect("OffsetDateTime"),
|
note.object_props.set_tag_link_vec(
|
||||||
);
|
mentions
|
||||||
note.set_attributed_to(author.into_id().parse::<IriString>()?);
|
.into_iter()
|
||||||
note.set_many_tos(to);
|
.filter_map(|m| Mention::build_activity(c, &m).ok())
|
||||||
note.set_many_tags(mentions.into_iter().filter_map(|m| {
|
.collect::<Vec<link::Mention>>(),
|
||||||
Mention::build_activity(conn, &m)
|
)?;
|
||||||
.map(|mention| mention.into_any_base().expect("Can convert"))
|
|
||||||
.ok()
|
|
||||||
}));
|
|
||||||
Ok(note)
|
Ok(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_activity(&self, conn: &Connection) -> 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 note_clone = note.clone();
|
let mut act = Create::default();
|
||||||
|
act.create_props.set_actor_link(author.into_id())?;
|
||||||
let mut act = Create::new(
|
act.create_props.set_object_object(note.clone())?;
|
||||||
author.into_id().parse::<IriString>()?,
|
act.object_props
|
||||||
Base::retract(note)?.into_generic()?,
|
.set_id_string(format!("{}/activity", self.ap_url.clone()?,))?;
|
||||||
);
|
act.object_props
|
||||||
act.set_id(
|
.set_to_link_vec(note.object_props.to_link_vec::<Id>()?)?;
|
||||||
format!(
|
act.object_props
|
||||||
"{}/activity",
|
.set_cc_link_vec(vec![Id::new(self.get_author(&c.conn)?.followers_endpoint)])?;
|
||||||
self.ap_url.clone().ok_or(Error::MissingApProperty)?,
|
|
||||||
)
|
|
||||||
.parse::<IriString>()?,
|
|
||||||
);
|
|
||||||
act.set_many_tos(
|
|
||||||
note_clone
|
|
||||||
.to()
|
|
||||||
.iter()
|
|
||||||
.flat_map(|tos| tos.iter().map(|to| to.to_owned())),
|
|
||||||
);
|
|
||||||
act.set_many_ccs(vec![self.get_author(conn)?.followers_endpoint]);
|
|
||||||
Ok(act)
|
Ok(act)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,140 +177,132 @@ impl Comment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_delete(&self, conn: &Connection) -> Result<Delete> {
|
pub fn build_delete(&self, conn: &Connection) -> Result<Delete> {
|
||||||
let mut tombstone = Tombstone::new();
|
let mut act = Delete::default();
|
||||||
tombstone.set_id(
|
act.delete_props
|
||||||
self.ap_url
|
.set_actor_link(self.get_author(conn)?.into_id())?;
|
||||||
.as_ref()
|
|
||||||
.ok_or(Error::MissingApProperty)?
|
|
||||||
.parse::<IriString>()?,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut act = Delete::new(
|
let mut tombstone = Tombstone::default();
|
||||||
self.get_author(conn)?.into_id().parse::<IriString>()?,
|
tombstone.object_props.set_id_string(self.ap_url.clone()?)?;
|
||||||
Base::retract(tombstone)?.into_generic()?,
|
act.delete_props.set_object_object(tombstone)?;
|
||||||
);
|
|
||||||
|
|
||||||
act.set_id(format!("{}#delete", self.ap_url.clone().unwrap()).parse::<IriString>()?);
|
act.object_props
|
||||||
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
|
.set_id_string(format!("{}#delete", self.ap_url.clone().unwrap()))?;
|
||||||
|
act.object_props
|
||||||
|
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?;
|
||||||
|
|
||||||
Ok(act)
|
Ok(act)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromId<Connection> for Comment {
|
impl FromId<PlumeRocket> for Comment {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = Note;
|
type Object = Note;
|
||||||
|
|
||||||
fn from_db(conn: &Connection, 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: &Connection, 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
|
let previous_url = note.object_props.in_reply_to.as_ref()?.as_str()?;
|
||||||
.in_reply_to()
|
let previous_comment = Comment::find_by_ap_url(conn, previous_url);
|
||||||
.ok_or(Error::MissingApProperty)?
|
|
||||||
.iter()
|
|
||||||
.next()
|
|
||||||
.ok_or(Error::MissingApProperty)?
|
|
||||||
.id()
|
|
||||||
.ok_or(Error::MissingApProperty)?;
|
|
||||||
let previous_comment = Comment::find_by_ap_url(conn, previous_url.as_str());
|
|
||||||
|
|
||||||
let is_public = |v: &Option<&OneOrMany<AnyBase>>| match v {
|
let is_public = |v: &Option<serde_json::Value>| match v
|
||||||
Some(one_or_many) => one_or_many.iter().any(|any_base| {
|
.as_ref()
|
||||||
let id = any_base.id();
|
.unwrap_or(&serde_json::Value::Null)
|
||||||
id.is_some() && id.unwrap() == PUBLIC_VISIBILITY
|
{
|
||||||
}),
|
serde_json::Value::Array(v) => v
|
||||||
None => false,
|
.iter()
|
||||||
|
.filter_map(serde_json::Value::as_str)
|
||||||
|
.any(|s| s == PUBLIC_VISIBILITY),
|
||||||
|
serde_json::Value::String(s) => s == PUBLIC_VISIBILITY,
|
||||||
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let public_visibility = is_public(¬e.to())
|
let public_visibility = is_public(¬e.object_props.to)
|
||||||
|| is_public(¬e.bto())
|
|| is_public(¬e.object_props.bto)
|
||||||
|| is_public(¬e.cc())
|
|| is_public(¬e.object_props.cc)
|
||||||
|| is_public(¬e.bcc());
|
|| is_public(¬e.object_props.bcc);
|
||||||
|
|
||||||
let summary = note.summary().and_then(|summary| summary.to_as_string());
|
|
||||||
let sensitive = summary.is_some();
|
|
||||||
let comm = Comment::insert(
|
let comm = Comment::insert(
|
||||||
conn,
|
conn,
|
||||||
NewComment {
|
NewComment {
|
||||||
content: SafeString::new(
|
content: SafeString::new(¬e.object_props.content_string()?),
|
||||||
¬e
|
spoiler_text: note.object_props.summary_string().unwrap_or_default(),
|
||||||
.content()
|
ap_url: note.object_props.id_string().ok(),
|
||||||
.ok_or(Error::MissingApProperty)?
|
|
||||||
.to_as_string()
|
|
||||||
.ok_or(Error::InvalidValue)?,
|
|
||||||
),
|
|
||||||
spoiler_text: summary.unwrap_or_default(),
|
|
||||||
ap_url: Some(
|
|
||||||
note.id_unchecked()
|
|
||||||
.ok_or(Error::MissingApProperty)?
|
|
||||||
.to_string(),
|
|
||||||
),
|
|
||||||
in_response_to_id: previous_comment.iter().map(|c| c.id).next(),
|
in_response_to_id: previous_comment.iter().map(|c| c.id).next(),
|
||||||
post_id: previous_comment.map(|c| c.post_id).or_else(|_| {
|
post_id: previous_comment.map(|c| c.post_id).or_else(|_| {
|
||||||
Ok(Post::find_by_ap_url(conn, previous_url.as_str())?.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,
|
||||||
¬e
|
¬e.object_props.attributed_to_link::<Id>()?,
|
||||||
.attributed_to()
|
|
||||||
.ok_or(Error::MissingApProperty)?
|
|
||||||
.to_as_uri()
|
|
||||||
.ok_or(Error::MissingApProperty)?,
|
|
||||||
None,
|
None,
|
||||||
CONFIG.proxy(),
|
|
||||||
)
|
)
|
||||||
.map_err(|(_, e)| e)?
|
.map_err(|(_, e)| e)?
|
||||||
.id,
|
.id,
|
||||||
sensitive,
|
sensitive: note.object_props.summary_string().is_ok(),
|
||||||
public_visibility,
|
public_visibility,
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// save mentions
|
// save mentions
|
||||||
if let Some(tags) = note.tag() {
|
if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() {
|
||||||
let author_url = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0].ap_url;
|
for tag in tags {
|
||||||
for tag in tags.iter() {
|
serde_json::from_value::<link::Mention>(tag)
|
||||||
let m = tag.clone().extend::<link::Mention, MentionType>()?; // FIXME: Don't clone
|
.map_err(Error::from)
|
||||||
if m.is_none() {
|
.and_then(|m| {
|
||||||
continue;
|
let author = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0];
|
||||||
}
|
let not_author = m.link_props.href_string()? != author.ap_url.clone();
|
||||||
let m = m.unwrap();
|
Ok(Mention::from_activity(
|
||||||
let not_author = m.href().ok_or(Error::MissingApProperty)? != author_url;
|
conn, &m, comm.id, false, not_author,
|
||||||
let _ = Mention::from_activity(conn, &m, comm.id, false, not_author);
|
)?)
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
comm
|
comm
|
||||||
};
|
};
|
||||||
|
|
||||||
if !comm.public_visibility {
|
if !comm.public_visibility {
|
||||||
let mut receiver_ids = HashSet::new();
|
let receivers_ap_url = |v: Option<serde_json::Value>| {
|
||||||
let mut receivers_id = |v: Option<&'_ OneOrMany<AnyBase>>| {
|
let filter = |e: serde_json::Value| {
|
||||||
if let Some(one_or_many) = v {
|
if let serde_json::Value::String(s) = e {
|
||||||
for any_base in one_or_many.iter() {
|
Some(s)
|
||||||
if let Some(id) = any_base.id() {
|
} else {
|
||||||
receiver_ids.insert(id.to_string());
|
None
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
match v.unwrap_or(serde_json::Value::Null) {
|
||||||
|
serde_json::Value::Array(v) => v,
|
||||||
|
v => vec![v],
|
||||||
}
|
}
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(filter)
|
||||||
};
|
};
|
||||||
|
|
||||||
receivers_id(note.to());
|
let mut note = note;
|
||||||
receivers_id(note.cc());
|
|
||||||
receivers_id(note.bto());
|
|
||||||
receivers_id(note.bcc());
|
|
||||||
|
|
||||||
let receivers_ap_url = receiver_ids
|
let to = receivers_ap_url(note.object_props.to.take());
|
||||||
|
let cc = receivers_ap_url(note.object_props.cc.take());
|
||||||
|
let bto = receivers_ap_url(note.object_props.bto.take());
|
||||||
|
let bcc = receivers_ap_url(note.object_props.bcc.take());
|
||||||
|
|
||||||
|
let receivers_ap_url = to
|
||||||
|
.chain(cc)
|
||||||
|
.chain(bto)
|
||||||
|
.chain(bcc)
|
||||||
|
.collect::<HashSet<_>>() // remove duplicates (don't do a query more than once)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|v| {
|
.map(|v| {
|
||||||
if let Ok(user) = User::from_id(conn, v.as_ref(), 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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.flatten()
|
||||||
.filter(|u| u.get_instance(conn).map(|i| i.local).unwrap_or(false))
|
.filter(|u| u.get_instance(conn).map(|i| i.local).unwrap_or(false))
|
||||||
.collect::<HashSet<User>>(); //remove duplicates (prevent db error)
|
.collect::<HashSet<User>>(); //remove duplicates (prevent db error)
|
||||||
|
|
||||||
@ -348,47 +320,43 @@ impl FromId<Connection> for Comment {
|
|||||||
comm.notify(conn)?;
|
comm.notify(conn)?;
|
||||||
Ok(comm)
|
Ok(comm)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_sender() -> &'static dyn Signer {
|
|
||||||
Instance::get_local_instance_user().expect("Failed to local instance user")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Create, &Connection> 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: &Connection, _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, &Connection> for Comment {
|
impl AsObject<User, Delete, &PlumeRocket> for Comment {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
|
||||||
fn activity(self, conn: &Connection, 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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -422,113 +390,42 @@ impl CommentTree {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::blogs::Blog;
|
|
||||||
use crate::db_conn::DbConn;
|
|
||||||
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, format_datetime};
|
use crate::tests::rockets;
|
||||||
use assert_json_diff::assert_json_eq;
|
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
use serde_json::{json, to_value};
|
|
||||||
|
|
||||||
fn prepare_activity(conn: &DbConn) -> (Comment, Vec<Post>, Vec<User>, Vec<Blog>) {
|
|
||||||
let (posts, users, blogs) = fill_database(conn);
|
|
||||||
|
|
||||||
let comment = Comment::insert(
|
|
||||||
conn,
|
|
||||||
NewComment {
|
|
||||||
content: SafeString::new("My comment, mentioning to @user"),
|
|
||||||
in_response_to_id: None,
|
|
||||||
post_id: posts[0].id,
|
|
||||||
author_id: users[0].id,
|
|
||||||
ap_url: None,
|
|
||||||
sensitive: true,
|
|
||||||
spoiler_text: "My CW".into(),
|
|
||||||
public_visibility: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
(comment, posts, users, blogs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// creates a post, get it's Create activity, delete the post,
|
// 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 (original_comm, posts, users, _blogs) = prepare_activity(conn);
|
let (posts, users, _) = fill_database(&r);
|
||||||
let act = original_comm.create_activity(conn).unwrap();
|
|
||||||
|
|
||||||
assert_json_eq!(to_value(&act).unwrap(), json!({
|
let original_comm = Comment::insert(
|
||||||
"actor": "https://plu.me/@/admin/",
|
|
||||||
"cc": ["https://plu.me/@/admin/followers"],
|
|
||||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}/activity", original_comm.id),
|
|
||||||
"object": {
|
|
||||||
"attributedTo": "https://plu.me/@/admin/",
|
|
||||||
"content": r###"<p dir="auto">My comment, mentioning to <a href="https://plu.me/@/user/" title="user">@user</a></p>
|
|
||||||
"###,
|
|
||||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", original_comm.id),
|
|
||||||
"inReplyTo": "https://plu.me/~/BlogName/testing",
|
|
||||||
"published": format_datetime(&original_comm.creation_date),
|
|
||||||
"summary": "My CW",
|
|
||||||
"tag": [
|
|
||||||
{
|
|
||||||
"href": "https://plu.me/@/user/",
|
|
||||||
"name": "@user",
|
|
||||||
"type": "Mention"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
||||||
"type": "Note"
|
|
||||||
},
|
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
||||||
"type": "Create",
|
|
||||||
}));
|
|
||||||
|
|
||||||
let reply = Comment::insert(
|
|
||||||
conn,
|
conn,
|
||||||
NewComment {
|
NewComment {
|
||||||
content: SafeString::new(""),
|
content: SafeString::new("My comment"),
|
||||||
in_response_to_id: Some(original_comm.id),
|
in_response_to_id: None,
|
||||||
post_id: posts[0].id,
|
post_id: posts[0].id,
|
||||||
author_id: users[1].id,
|
author_id: users[0].id,
|
||||||
ap_url: None,
|
ap_url: None,
|
||||||
sensitive: false,
|
sensitive: true,
|
||||||
spoiler_text: "".into(),
|
spoiler_text: "My CW".into(),
|
||||||
public_visibility: true,
|
public_visibility: true,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let reply_act = reply.create_activity(conn).unwrap();
|
let act = original_comm.create_activity(&r).unwrap();
|
||||||
|
|
||||||
assert_json_eq!(to_value(&reply_act).unwrap(), json!({
|
|
||||||
"actor": "https://plu.me/@/user/",
|
|
||||||
"cc": ["https://plu.me/@/user/followers"],
|
|
||||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}/activity", reply.id),
|
|
||||||
"object": {
|
|
||||||
"attributedTo": "https://plu.me/@/user/",
|
|
||||||
"content": "",
|
|
||||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", reply.id),
|
|
||||||
"inReplyTo": format!("https://plu.me/~/BlogName/testing/comment/{}", original_comm.id),
|
|
||||||
"published": format_datetime(&reply.creation_date),
|
|
||||||
"summary": "",
|
|
||||||
"tag": [],
|
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
||||||
"type": "Note"
|
|
||||||
},
|
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
||||||
"type": "Create"
|
|
||||||
}));
|
|
||||||
|
|
||||||
inbox(
|
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, 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);
|
||||||
@ -543,60 +440,4 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn to_activity() {
|
|
||||||
let conn = db();
|
|
||||||
conn.test_transaction::<_, Error, _>(|| {
|
|
||||||
let (comment, _posts, _users, _blogs) = prepare_activity(&conn);
|
|
||||||
let act = comment.to_activity(&conn)?;
|
|
||||||
|
|
||||||
let expected = json!({
|
|
||||||
"attributedTo": "https://plu.me/@/admin/",
|
|
||||||
"content": r###"<p dir="auto">My comment, mentioning to <a href="https://plu.me/@/user/" title="user">@user</a></p>
|
|
||||||
"###,
|
|
||||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", comment.id),
|
|
||||||
"inReplyTo": "https://plu.me/~/BlogName/testing",
|
|
||||||
"published": format_datetime(&comment.creation_date),
|
|
||||||
"summary": "My CW",
|
|
||||||
"tag": [
|
|
||||||
{
|
|
||||||
"href": "https://plu.me/@/user/",
|
|
||||||
"name": "@user",
|
|
||||||
"type": "Mention"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
||||||
"type": "Note"
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_json_eq!(to_value(act)?, expected);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn build_delete() {
|
|
||||||
let conn = db();
|
|
||||||
conn.test_transaction::<_, Error, _>(|| {
|
|
||||||
let (comment, _posts, _users, _blogs) = prepare_activity(&conn);
|
|
||||||
let act = comment.build_delete(&conn)?;
|
|
||||||
|
|
||||||
let expected = json!({
|
|
||||||
"actor": "https://plu.me/@/admin/",
|
|
||||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}#delete", comment.id),
|
|
||||||
"object": {
|
|
||||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", comment.id),
|
|
||||||
"type": "Tombstone"
|
|
||||||
},
|
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
||||||
"type": "Delete"
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_json_eq!(to_value(act)?, expected);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,8 @@
|
|||||||
use crate::search::TokenizerKind as SearchTokenizer;
|
use crate::search::TokenizerKind as SearchTokenizer;
|
||||||
use crate::signups::Strategy as SignupStrategy;
|
|
||||||
use crate::smtp::{SMTP_PORT, SUBMISSIONS_PORT, SUBMISSION_PORT};
|
|
||||||
use rocket::config::Limits;
|
use rocket::config::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(feature = "s3")]
|
|
||||||
use s3::{Bucket, Region, creds::Credentials};
|
|
||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
const DB_NAME: &str = "plume";
|
const DB_NAME: &str = "plume";
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -20,49 +14,30 @@ pub struct Config {
|
|||||||
pub db_name: &'static str,
|
pub db_name: &'static str,
|
||||||
pub db_max_size: Option<u32>,
|
pub db_max_size: Option<u32>,
|
||||||
pub db_min_idle: Option<u32>,
|
pub db_min_idle: Option<u32>,
|
||||||
pub signup: SignupStrategy,
|
|
||||||
pub search_index: String,
|
pub search_index: String,
|
||||||
pub search_tokenizers: SearchTokenizerConfig,
|
pub search_tokenizers: SearchTokenizerConfig,
|
||||||
pub rocket: Result<RocketConfig, InvalidRocketConfig>,
|
pub rocket: Result<RocketConfig, RocketError>,
|
||||||
pub logo: LogoConfig,
|
pub logo: LogoConfig,
|
||||||
pub default_theme: String,
|
pub default_theme: String,
|
||||||
pub media_directory: String,
|
pub media_directory: String,
|
||||||
pub mail: Option<MailConfig>,
|
|
||||||
pub ldap: Option<LdapConfig>,
|
|
||||||
pub proxy: Option<ProxyConfig>,
|
|
||||||
pub s3: Option<S3Config>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub fn proxy(&self) -> Option<&reqwest::Proxy> {
|
|
||||||
self.proxy.as_ref().map(|p| &p.proxy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn string_to_bool(val: &str, name: &str) -> bool {
|
|
||||||
match val {
|
|
||||||
"1" | "true" | "TRUE" => true,
|
|
||||||
"0" | "false" | "FALSE" => false,
|
|
||||||
_ => panic!("Invalid configuration: {} is not boolean", name),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum InvalidRocketConfig {
|
pub enum RocketError {
|
||||||
Env,
|
InvalidEnv,
|
||||||
Address,
|
InvalidAddress,
|
||||||
SecretKey,
|
InvalidSecretKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_rocket_config() -> Result<RocketConfig, InvalidRocketConfig> {
|
fn get_rocket_config() -> Result<RocketConfig, RocketError> {
|
||||||
let mut c = RocketConfig::active().map_err(|_| InvalidRocketConfig::Env)?;
|
let mut c = RocketConfig::active().map_err(|_| RocketError::InvalidEnv)?;
|
||||||
|
|
||||||
let address = var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned());
|
let address = var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned());
|
||||||
let port = var("ROCKET_PORT")
|
let port = var("ROCKET_PORT")
|
||||||
.ok()
|
.ok()
|
||||||
.map(|s| s.parse::<u16>().unwrap())
|
.map(|s| s.parse::<u16>().unwrap())
|
||||||
.unwrap_or(7878);
|
.unwrap_or(7878);
|
||||||
let secret_key = var("ROCKET_SECRET_KEY").map_err(|_| InvalidRocketConfig::SecretKey)?;
|
let secret_key = var("ROCKET_SECRET_KEY").map_err(|_| RocketError::InvalidSecretKey)?;
|
||||||
let form_size = var("FORM_SIZE")
|
let form_size = var("FORM_SIZE")
|
||||||
.unwrap_or_else(|_| "128".to_owned())
|
.unwrap_or_else(|_| "128".to_owned())
|
||||||
.parse::<u64>()
|
.parse::<u64>()
|
||||||
@ -73,10 +48,10 @@ fn get_rocket_config() -> Result<RocketConfig, InvalidRocketConfig> {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
c.set_address(address)
|
c.set_address(address)
|
||||||
.map_err(|_| InvalidRocketConfig::Address)?;
|
.map_err(|_| RocketError::InvalidAddress)?;
|
||||||
c.set_port(port);
|
c.set_port(port);
|
||||||
c.set_secret_key(secret_key)
|
c.set_secret_key(secret_key)
|
||||||
.map_err(|_| InvalidRocketConfig::SecretKey)?;
|
.map_err(|_| RocketError::InvalidSecretKey)?;
|
||||||
|
|
||||||
c.set_limits(
|
c.set_limits(
|
||||||
Limits::new()
|
Limits::new()
|
||||||
@ -172,7 +147,7 @@ impl Default for LogoConfig {
|
|||||||
.ok()
|
.ok()
|
||||||
.or_else(|| custom_main.clone());
|
.or_else(|| custom_main.clone());
|
||||||
let other = if let Some(main) = custom_main.clone() {
|
let other = if let Some(main) = custom_main.clone() {
|
||||||
let ext = |path: &str| match path.rsplit_once('.').map(|x| x.1) {
|
let ext = |path: &str| match path.rsplitn(2, '.').next() {
|
||||||
Some("png") => Some("image/png".to_owned()),
|
Some("png") => Some("image/png".to_owned()),
|
||||||
Some("jpg") | Some("jpeg") => Some("image/jpeg".to_owned()),
|
Some("jpg") | Some("jpeg") => Some("image/jpeg".to_owned()),
|
||||||
Some("svg") => Some("image/svg+xml".to_owned()),
|
Some("svg") => Some("image/svg+xml".to_owned()),
|
||||||
@ -181,8 +156,11 @@ impl Default for LogoConfig {
|
|||||||
};
|
};
|
||||||
let mut custom_icons = env::vars()
|
let mut custom_icons = env::vars()
|
||||||
.filter_map(|(var, val)| {
|
.filter_map(|(var, val)| {
|
||||||
var.strip_prefix("PLUME_LOGO_")
|
if var.starts_with("PLUME_LOGO_") {
|
||||||
.map(|size| (size.to_owned(), val))
|
Some((var[11..].to_owned(), val))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.filter_map(|(var, val)| var.parse::<u64>().ok().map(|var| (var, val)))
|
.filter_map(|(var, val)| var.parse::<u64>().ok().map(|var| (var, val)))
|
||||||
.map(|(dim, src)| Icon {
|
.map(|(dim, src)| Icon {
|
||||||
@ -262,211 +240,6 @@ impl SearchTokenizerConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MailConfig {
|
|
||||||
pub server: String,
|
|
||||||
pub port: u16,
|
|
||||||
pub helo_name: String,
|
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_mail_config() -> Option<MailConfig> {
|
|
||||||
Some(MailConfig {
|
|
||||||
server: env::var("MAIL_SERVER").ok()?,
|
|
||||||
port: env::var("MAIL_PORT").map_or(SUBMISSIONS_PORT, |port| match port.as_str() {
|
|
||||||
"smtp" => SMTP_PORT,
|
|
||||||
"submissions" => SUBMISSIONS_PORT,
|
|
||||||
"submission" => SUBMISSION_PORT,
|
|
||||||
number => number
|
|
||||||
.parse()
|
|
||||||
.expect(r#"MAIL_PORT must be "smtp", "submissions", "submission" or an integer."#),
|
|
||||||
}),
|
|
||||||
helo_name: env::var("MAIL_HELO_NAME").unwrap_or_else(|_| "localhost".to_owned()),
|
|
||||||
username: env::var("MAIL_USER").ok()?,
|
|
||||||
password: env::var("MAIL_PASSWORD").ok()?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LdapConfig {
|
|
||||||
pub addr: String,
|
|
||||||
pub base_dn: String,
|
|
||||||
pub tls: bool,
|
|
||||||
pub user_name_attr: String,
|
|
||||||
pub mail_attr: String,
|
|
||||||
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 = string_to_bool(&tls, "LDAP_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());
|
|
||||||
//2023-12-30
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct S3Config {
|
|
||||||
pub bucket: String,
|
|
||||||
pub access_key_id: String,
|
|
||||||
pub access_key_secret: String,
|
|
||||||
|
|
||||||
// region? If not set, default to us-east-1
|
|
||||||
pub region: String,
|
|
||||||
// hostname for s3. If not set, default to $region.amazonaws.com
|
|
||||||
pub hostname: String,
|
|
||||||
// may be useful when using self hosted s3. Won't work with recent AWS buckets
|
|
||||||
pub path_style: bool,
|
|
||||||
// http or https
|
|
||||||
pub protocol: String,
|
|
||||||
|
|
||||||
// download directly from s3 to user, wihout going through Plume. Require public read on bucket
|
|
||||||
pub direct_download: bool,
|
|
||||||
// use this hostname for downloads, can be used with caching proxy in front of s3 (expected to
|
|
||||||
// be reachable through https)
|
|
||||||
pub alias: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl S3Config {
|
|
||||||
#[cfg(feature = "s3")]
|
|
||||||
pub fn get_bucket(&self) -> Bucket {
|
|
||||||
let region = Region::Custom {
|
|
||||||
region: self.region.clone(),
|
|
||||||
endpoint: format!("{}://{}", self.protocol, self.hostname),
|
|
||||||
};
|
|
||||||
let credentials = Credentials {
|
|
||||||
access_key: Some(self.access_key_id.clone()),
|
|
||||||
secret_key: Some(self.access_key_secret.clone()),
|
|
||||||
security_token: None,
|
|
||||||
session_token: None,
|
|
||||||
expiration: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let bucket = Bucket::new(&self.bucket, region, credentials).unwrap();
|
|
||||||
if self.path_style {
|
|
||||||
bucket.with_path_style()
|
|
||||||
} else {
|
|
||||||
bucket
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_s3_config() -> Option<S3Config> {
|
|
||||||
let bucket = var("S3_BUCKET").ok();
|
|
||||||
let access_key_id = var("AWS_ACCESS_KEY_ID").ok();
|
|
||||||
let access_key_secret = var("AWS_SECRET_ACCESS_KEY").ok();
|
|
||||||
if bucket.is_none() && access_key_id.is_none() && access_key_secret.is_none() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "s3"))]
|
|
||||||
panic!("S3 support is not enabled in this build");
|
|
||||||
|
|
||||||
#[cfg(feature = "s3")]
|
|
||||||
{
|
|
||||||
if bucket.is_none() || access_key_id.is_none() || access_key_secret.is_none() {
|
|
||||||
panic!("Invalid S3 configuration: some required values are set, but not others");
|
|
||||||
}
|
|
||||||
let bucket = bucket.unwrap();
|
|
||||||
let access_key_id = access_key_id.unwrap();
|
|
||||||
let access_key_secret = access_key_secret.unwrap();
|
|
||||||
|
|
||||||
let region = var("S3_REGION").unwrap_or_else(|_| "us-east-1".to_owned());
|
|
||||||
let hostname = var("S3_HOSTNAME").unwrap_or_else(|_| format!("{}.amazonaws.com", region));
|
|
||||||
|
|
||||||
let protocol = var("S3_PROTOCOL").unwrap_or_else(|_| "https".to_owned());
|
|
||||||
if protocol != "http" && protocol != "https" {
|
|
||||||
panic!("Invalid S3 configuration: invalid protocol {}", protocol);
|
|
||||||
}
|
|
||||||
|
|
||||||
let path_style = var("S3_PATH_STYLE").unwrap_or_else(|_| "false".to_owned());
|
|
||||||
let path_style = string_to_bool(&path_style, "S3_PATH_STYLE");
|
|
||||||
let direct_download = var("S3_DIRECT_DOWNLOAD").unwrap_or_else(|_| "false".to_owned());
|
|
||||||
let direct_download = string_to_bool(&direct_download, "S3_DIRECT_DOWNLOAD");
|
|
||||||
|
|
||||||
let alias = var("S3_ALIAS_HOST").ok();
|
|
||||||
|
|
||||||
if direct_download && protocol == "http" && alias.is_none() {
|
|
||||||
panic!("S3 direct download is disabled because bucket is accessed through plain HTTP. Use HTTPS or set an alias hostname (S3_ALIAS_HOST).");
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(S3Config {
|
|
||||||
bucket,
|
|
||||||
access_key_id,
|
|
||||||
access_key_secret,
|
|
||||||
region,
|
|
||||||
hostname,
|
|
||||||
protocol,
|
|
||||||
path_style,
|
|
||||||
direct_download,
|
|
||||||
alias,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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!(
|
||||||
@ -482,7 +255,6 @@ lazy_static! {
|
|||||||
s.parse::<u32>()
|
s.parse::<u32>()
|
||||||
.expect("Couldn't parse DB_MIN_IDLE into u32")
|
.expect("Couldn't parse DB_MIN_IDLE into u32")
|
||||||
)),
|
)),
|
||||||
signup: var("SIGNUP").map_or(SignupStrategy::default(), |s| s.parse().unwrap()),
|
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
database_url: var("DATABASE_URL")
|
database_url: var("DATABASE_URL")
|
||||||
.unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME)),
|
.unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME)),
|
||||||
@ -495,9 +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()),
|
||||||
mail: get_mail_config(),
|
|
||||||
ldap: get_ldap_config(),
|
|
||||||
proxy: get_proxy_config(),
|
|
||||||
s3: get_s3_config(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,8 +69,7 @@ pub(crate) mod tests {
|
|||||||
impl CustomizeConnection<Connection, ConnError> for TestConnectionCustomizer {
|
impl CustomizeConnection<Connection, ConnError> for TestConnectionCustomizer {
|
||||||
fn on_acquire(&self, conn: &mut Connection) -> Result<(), ConnError> {
|
fn on_acquire(&self, conn: &mut Connection) -> Result<(), ConnError> {
|
||||||
PragmaForeignKey.on_acquire(conn)?;
|
PragmaForeignKey.on_acquire(conn)?;
|
||||||
conn.begin_test_transaction().unwrap();
|
Ok(conn.begin_test_transaction().unwrap())
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,158 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
blocklisted_emails::BlocklistedEmail,
|
|
||||||
db_conn::DbConn,
|
|
||||||
schema::email_signups,
|
|
||||||
users::{NewUser, Role, User},
|
|
||||||
Error, Result,
|
|
||||||
};
|
|
||||||
use chrono::{offset::Utc, Duration, NaiveDateTime};
|
|
||||||
use diesel::{
|
|
||||||
Connection as _, ExpressionMethods, Identifiable, Insertable, QueryDsl, Queryable, RunQueryDsl,
|
|
||||||
};
|
|
||||||
use plume_common::utils::random_hex;
|
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
const TOKEN_VALIDITY_HOURS: i64 = 2;
|
|
||||||
|
|
||||||
#[repr(transparent)]
|
|
||||||
pub struct Token(String);
|
|
||||||
|
|
||||||
impl From<String> for Token {
|
|
||||||
fn from(string: String) -> Self {
|
|
||||||
Token(string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Token> for String {
|
|
||||||
fn from(token: Token) -> Self {
|
|
||||||
token.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for Token {
|
|
||||||
type Target = String;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Token {
|
|
||||||
fn generate() -> Self {
|
|
||||||
Self(random_hex())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Identifiable, Queryable)]
|
|
||||||
pub struct EmailSignup {
|
|
||||||
pub id: i32,
|
|
||||||
pub email: String,
|
|
||||||
pub token: String,
|
|
||||||
pub expiration_date: NaiveDateTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Insertable)]
|
|
||||||
#[table_name = "email_signups"]
|
|
||||||
pub struct NewEmailSignup<'a> {
|
|
||||||
pub email: &'a str,
|
|
||||||
pub token: &'a str,
|
|
||||||
pub expiration_date: NaiveDateTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EmailSignup {
|
|
||||||
pub fn start(conn: &DbConn, email: &str) -> Result<Token> {
|
|
||||||
Self::ensure_email_not_blocked(conn, email)?;
|
|
||||||
|
|
||||||
conn.transaction(|| {
|
|
||||||
Self::ensure_user_not_exist_by_email(conn, email)?;
|
|
||||||
let _rows = Self::delete_existings_by_email(conn, email)?;
|
|
||||||
let token = Token::generate();
|
|
||||||
let expiration_date = Utc::now()
|
|
||||||
.naive_utc()
|
|
||||||
.checked_add_signed(Duration::hours(TOKEN_VALIDITY_HOURS))
|
|
||||||
.expect("could not calculate expiration date");
|
|
||||||
let new_signup = NewEmailSignup {
|
|
||||||
email,
|
|
||||||
token: &token,
|
|
||||||
expiration_date,
|
|
||||||
};
|
|
||||||
let _rows = diesel::insert_into(email_signups::table)
|
|
||||||
.values(new_signup)
|
|
||||||
.execute(&**conn)?;
|
|
||||||
|
|
||||||
Ok(token)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn find_by_token(conn: &DbConn, token: Token) -> Result<Self> {
|
|
||||||
let signup = email_signups::table
|
|
||||||
.filter(email_signups::token.eq(token.as_str()))
|
|
||||||
.first::<Self>(&**conn)
|
|
||||||
.map_err(Error::from)?;
|
|
||||||
Ok(signup)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn confirm(&self, conn: &DbConn) -> Result<()> {
|
|
||||||
Self::ensure_email_not_blocked(conn, &self.email)?;
|
|
||||||
|
|
||||||
conn.transaction(|| {
|
|
||||||
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
|
|
||||||
if self.expired() {
|
|
||||||
Self::delete_existings_by_email(conn, &self.email)?;
|
|
||||||
return Err(Error::Expired);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn complete(&self, conn: &DbConn, username: String, password: String) -> Result<User> {
|
|
||||||
Self::ensure_email_not_blocked(conn, &self.email)?;
|
|
||||||
|
|
||||||
conn.transaction(|| {
|
|
||||||
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
|
|
||||||
let user = NewUser::new_local(
|
|
||||||
conn,
|
|
||||||
username,
|
|
||||||
"".to_string(),
|
|
||||||
Role::Normal,
|
|
||||||
"",
|
|
||||||
self.email.clone(),
|
|
||||||
Some(User::hash_pass(&password)?),
|
|
||||||
)?;
|
|
||||||
self.delete(conn)?;
|
|
||||||
Ok(user)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete(&self, conn: &DbConn) -> Result<()> {
|
|
||||||
let _rows = diesel::delete(self).execute(&**conn).map_err(Error::from)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensure_email_not_blocked(conn: &DbConn, email: &str) -> Result<()> {
|
|
||||||
if let Some(x) = BlocklistedEmail::matches_blocklist(conn, email)? {
|
|
||||||
Err(Error::Blocklisted(x.notify_user, x.notification_text))
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensure_user_not_exist_by_email(conn: &DbConn, email: &str) -> Result<()> {
|
|
||||||
if User::email_used(conn, email)? {
|
|
||||||
let _rows = Self::delete_existings_by_email(conn, email)?;
|
|
||||||
return Err(Error::UserAlreadyExists);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_existings_by_email(conn: &DbConn, email: &str) -> Result<usize> {
|
|
||||||
let existing_signups = email_signups::table.filter(email_signups::email.eq(email));
|
|
||||||
diesel::delete(existing_signups)
|
|
||||||
.execute(&**conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expired(&self) -> bool {
|
|
||||||
self.expiration_date < Utc::now().naive_utc()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,13 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
ap_url, instance::Instance, notifications::*, schema::follows, users::User, Connection, Error,
|
ap_url, notifications::*, schema::follows, users::User, Connection, Error, PlumeRocket, Result,
|
||||||
Result, CONFIG,
|
CONFIG,
|
||||||
};
|
|
||||||
use activitystreams::{
|
|
||||||
activity::{Accept, ActorAndObjectRef, Follow as FollowAct, Undo},
|
|
||||||
base::AnyBase,
|
|
||||||
iri_string::types::IriString,
|
|
||||||
prelude::*,
|
|
||||||
};
|
};
|
||||||
|
use activitypub::activity::{Accept, Follow as FollowAct, Undo};
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||||
use plume_common::activity_pub::{
|
use plume_common::activity_pub::{
|
||||||
broadcast,
|
broadcast,
|
||||||
@ -58,13 +53,15 @@ impl Follow {
|
|||||||
pub fn to_activity(&self, conn: &Connection) -> Result<FollowAct> {
|
pub fn to_activity(&self, conn: &Connection) -> Result<FollowAct> {
|
||||||
let user = User::get(conn, self.follower_id)?;
|
let user = User::get(conn, self.follower_id)?;
|
||||||
let target = User::get(conn, self.following_id)?;
|
let target = User::get(conn, self.following_id)?;
|
||||||
let target_id = target.ap_url.parse::<IriString>()?;
|
|
||||||
|
|
||||||
let mut act = FollowAct::new(user.ap_url.parse::<IriString>()?, target_id.clone());
|
|
||||||
act.set_id(self.ap_url.parse::<IriString>()?);
|
|
||||||
act.set_many_tos(vec![target_id]);
|
|
||||||
act.set_many_ccs(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
|
|
||||||
|
|
||||||
|
let mut act = FollowAct::default();
|
||||||
|
act.follow_props.set_actor_link::<Id>(user.into_id())?;
|
||||||
|
act.follow_props
|
||||||
|
.set_object_link::<Id>(target.clone().into_id())?;
|
||||||
|
act.object_props.set_id_string(self.ap_url.clone())?;
|
||||||
|
act.object_props.set_to_link_vec(vec![target.into_id()])?;
|
||||||
|
act.object_props
|
||||||
|
.set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
|
||||||
Ok(act)
|
Ok(act)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,117 +94,88 @@ impl Follow {
|
|||||||
NewFollow {
|
NewFollow {
|
||||||
follower_id: from_id,
|
follower_id: from_id,
|
||||||
following_id: target_id,
|
following_id: target_id,
|
||||||
ap_url: follow
|
ap_url: follow.object_props.id_string()?,
|
||||||
.object_field_ref()
|
|
||||||
.as_single_id()
|
|
||||||
.ok_or(Error::MissingApProperty)?
|
|
||||||
.to_string(),
|
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
res.notify(conn)?;
|
res.notify(conn)?;
|
||||||
|
|
||||||
let accept = res.build_accept(from, target, follow)?;
|
let mut accept = Accept::default();
|
||||||
broadcast(target, accept, vec![from.clone()], CONFIG.proxy().cloned());
|
let accept_id = ap_url(&format!(
|
||||||
|
"{}/follow/{}/accept",
|
||||||
|
CONFIG.base_url.as_str(),
|
||||||
|
&res.id
|
||||||
|
));
|
||||||
|
accept.object_props.set_id_string(accept_id)?;
|
||||||
|
accept
|
||||||
|
.object_props
|
||||||
|
.set_to_link_vec(vec![from.clone().into_id()])?;
|
||||||
|
accept
|
||||||
|
.object_props
|
||||||
|
.set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
|
||||||
|
accept
|
||||||
|
.accept_props
|
||||||
|
.set_actor_link::<Id>(target.clone().into_id())?;
|
||||||
|
accept.accept_props.set_object_object(follow)?;
|
||||||
|
broadcast(&*target, accept, vec![from.clone()]);
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_accept<A: Signer + IntoId + Clone, B: Clone + AsActor<T> + IntoId, T>(
|
|
||||||
&self,
|
|
||||||
from: &B,
|
|
||||||
target: &A,
|
|
||||||
follow: FollowAct,
|
|
||||||
) -> Result<Accept> {
|
|
||||||
let mut accept = Accept::new(
|
|
||||||
target.clone().into_id().parse::<IriString>()?,
|
|
||||||
AnyBase::from_extended(follow)?,
|
|
||||||
);
|
|
||||||
let accept_id = ap_url(&format!(
|
|
||||||
"{}/follows/{}/accept",
|
|
||||||
CONFIG.base_url.as_str(),
|
|
||||||
self.id
|
|
||||||
));
|
|
||||||
accept.set_id(accept_id.parse::<IriString>()?);
|
|
||||||
accept.set_many_tos(vec![from.clone().into_id().parse::<IriString>()?]);
|
|
||||||
accept.set_many_ccs(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
|
|
||||||
|
|
||||||
Ok(accept)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_undo(&self, conn: &Connection) -> Result<Undo> {
|
pub fn build_undo(&self, conn: &Connection) -> Result<Undo> {
|
||||||
let mut undo = Undo::new(
|
let mut undo = Undo::default();
|
||||||
User::get(conn, self.follower_id)?
|
undo.undo_props
|
||||||
.ap_url
|
.set_actor_link(User::get(conn, self.follower_id)?.into_id())?;
|
||||||
.parse::<IriString>()?,
|
undo.object_props
|
||||||
self.ap_url.parse::<IriString>()?,
|
.set_id_string(format!("{}/undo", self.ap_url))?;
|
||||||
);
|
undo.undo_props
|
||||||
undo.set_id(format!("{}/undo", self.ap_url).parse::<IriString>()?);
|
.set_object_link::<Id>(self.clone().into_id())?;
|
||||||
undo.set_many_tos(vec![User::get(conn, self.following_id)?
|
undo.object_props
|
||||||
.ap_url
|
.set_to_link_vec(vec![User::get(conn, self.following_id)?.into_id()])?;
|
||||||
.parse::<IriString>()?]);
|
undo.object_props
|
||||||
undo.set_many_ccs(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
|
.set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
|
||||||
|
|
||||||
Ok(undo)
|
Ok(undo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, FollowAct, &Connection> 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: &Connection, 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 follow = FollowAct::new(actor.ap_url.parse::<IriString>()?, id.parse::<IriString>()?);
|
let mut follow = FollowAct::default();
|
||||||
Follow::accept_follow(conn, &actor, &self, follow, actor.id, self.id)
|
follow.object_props.set_id_string(id.to_string())?;
|
||||||
|
follow
|
||||||
|
.follow_props
|
||||||
|
.set_actor_link::<Id>(actor.clone().into_id())?;
|
||||||
|
Follow::accept_follow(&c.conn, &actor, &self, follow, actor.id, self.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromId<Connection> for Follow {
|
impl FromId<PlumeRocket> for Follow {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = FollowAct;
|
type Object = FollowAct;
|
||||||
|
|
||||||
fn from_db(conn: &Connection, 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: &Connection, 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
|
|
||||||
.actor_field_ref()
|
|
||||||
.as_single_id()
|
|
||||||
.ok_or(Error::MissingApProperty)?
|
|
||||||
.as_str(),
|
|
||||||
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::accept_follow(&c.conn, &actor, &target, follow, actor.id, target.id)
|
||||||
.object_field_ref()
|
|
||||||
.as_single_id()
|
|
||||||
.ok_or(Error::MissingApProperty)?
|
|
||||||
.as_str(),
|
|
||||||
None,
|
|
||||||
CONFIG.proxy(),
|
|
||||||
)
|
|
||||||
.map_err(|(_, e)| e)?;
|
|
||||||
Follow::accept_follow(conn, &actor, &target, follow, actor.id, target.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_sender() -> &'static dyn Signer {
|
|
||||||
Instance::get_local_instance_user().expect("Failed to local instance user")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Undo, &Connection> for Follow {
|
impl AsObject<User, Undo, &PlumeRocket> for Follow {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
|
||||||
fn activity(self, conn: &Connection, 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)?;
|
||||||
|
|
||||||
@ -232,31 +200,8 @@ impl IntoId for Follow {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{tests::db, users::tests as user_tests};
|
||||||
db_conn::DbConn, tests::db, users::tests as user_tests, users::tests::fill_database,
|
|
||||||
};
|
|
||||||
use assert_json_diff::assert_json_eq;
|
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
use serde_json::{json, to_value};
|
|
||||||
|
|
||||||
fn prepare_activity(conn: &DbConn) -> (Follow, User, User, Vec<User>) {
|
|
||||||
let users = fill_database(conn);
|
|
||||||
let following = &users[1];
|
|
||||||
let follower = &users[2];
|
|
||||||
let mut follow = Follow::insert(
|
|
||||||
conn,
|
|
||||||
NewFollow {
|
|
||||||
follower_id: follower.id,
|
|
||||||
following_id: following.id,
|
|
||||||
ap_url: "".into(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
// following.ap_url = format!("https://plu.me/follows/{}", follow.id);
|
|
||||||
follow.ap_url = format!("https://plu.me/follows/{}", follow.id);
|
|
||||||
|
|
||||||
(follow, following.to_owned(), follower.to_owned(), users)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_id() {
|
fn test_id() {
|
||||||
@ -291,77 +236,4 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn to_activity() {
|
|
||||||
let conn = db();
|
|
||||||
conn.test_transaction::<_, Error, _>(|| {
|
|
||||||
let (follow, _following, _follower, _users) = prepare_activity(&conn);
|
|
||||||
let act = follow.to_activity(&conn)?;
|
|
||||||
|
|
||||||
let expected = json!({
|
|
||||||
"actor": "https://plu.me/@/other/",
|
|
||||||
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
||||||
"id": format!("https://plu.me/follows/{}", follow.id),
|
|
||||||
"object": "https://plu.me/@/user/",
|
|
||||||
"to": ["https://plu.me/@/user/"],
|
|
||||||
"type": "Follow"
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_json_eq!(to_value(act)?, expected);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn build_accept() {
|
|
||||||
let conn = db();
|
|
||||||
conn.test_transaction::<_, Error, _>(|| {
|
|
||||||
let (follow, following, follower, _users) = prepare_activity(&conn);
|
|
||||||
let act = follow.build_accept(&follower, &following, follow.to_activity(&conn)?)?;
|
|
||||||
|
|
||||||
let expected = json!({
|
|
||||||
"actor": "https://plu.me/@/user/",
|
|
||||||
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
||||||
"id": format!("https://127.0.0.1:7878/follows/{}/accept", follow.id),
|
|
||||||
"object": {
|
|
||||||
"actor": "https://plu.me/@/other/",
|
|
||||||
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
||||||
"id": format!("https://plu.me/follows/{}", follow.id),
|
|
||||||
"object": "https://plu.me/@/user/",
|
|
||||||
"to": ["https://plu.me/@/user/"],
|
|
||||||
"type": "Follow"
|
|
||||||
},
|
|
||||||
"to": ["https://plu.me/@/other/"],
|
|
||||||
"type": "Accept"
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_json_eq!(to_value(act)?, expected);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn build_undo() {
|
|
||||||
let conn = db();
|
|
||||||
conn.test_transaction::<_, Error, _>(|| {
|
|
||||||
let (follow, _following, _follower, _users) = prepare_activity(&conn);
|
|
||||||
let act = follow.build_undo(&conn)?;
|
|
||||||
|
|
||||||
let expected = json!({
|
|
||||||
"actor": "https://plu.me/@/other/",
|
|
||||||
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
||||||
"id": format!("https://plu.me/follows/{}/undo", follow.id),
|
|
||||||
"object": format!("https://plu.me/follows/{}", follow.id),
|
|
||||||
"to": ["https://plu.me/@/user/"],
|
|
||||||
"type": "Undo"
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_json_eq!(to_value(act)?, expected);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
use activitystreams::activity::{Announce, Create, Delete, Follow, Like, Undo, Update};
|
use activitypub::activity::*;
|
||||||
|
use serde_json;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
comments::Comment,
|
comments::Comment,
|
||||||
@ -6,7 +7,7 @@ use crate::{
|
|||||||
posts::{Post, PostUpdate},
|
posts::{Post, PostUpdate},
|
||||||
reshares::Reshare,
|
reshares::Reshare,
|
||||||
users::User,
|
users::User,
|
||||||
Connection, Error, CONFIG,
|
Error, PlumeRocket,
|
||||||
};
|
};
|
||||||
use plume_common::activity_pub::inbox::Inbox;
|
use plume_common::activity_pub::inbox::Inbox;
|
||||||
|
|
||||||
@ -45,20 +46,20 @@ impl_into_inbox_result! {
|
|||||||
Reshare => Reshared
|
Reshare => Reshared
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn inbox(conn: &Connection, 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,13 +67,13 @@ pub fn inbox(conn: &Connection, act: serde_json::Value) -> Result<InboxResult, E
|
|||||||
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>,
|
||||||
@ -81,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(),
|
||||||
@ -93,15 +94,16 @@ pub(crate) mod tests {
|
|||||||
license: "WTFPL".to_owned(),
|
license: "WTFPL".to_owned(),
|
||||||
creation_date: None,
|
creation_date: None,
|
||||||
ap_url: format!("https://plu.me/~/{}/testing", blogs[0].actor_id),
|
ap_url: format!("https://plu.me/~/{}/testing", blogs[0].actor_id),
|
||||||
subtitle: "Bye".to_string(),
|
subtitle: String::new(),
|
||||||
source: "Hello".to_string(),
|
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,
|
||||||
@ -114,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,
|
||||||
@ -124,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);
|
||||||
@ -138,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,
|
||||||
@ -155,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);
|
||||||
@ -169,105 +173,18 @@ 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),
|
|
||||||
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),
|
|
||||||
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),
|
|
||||||
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,
|
||||||
"object": {
|
"object": {
|
||||||
"type": "Article",
|
"type": "Article",
|
||||||
"id": "https://plu.me/~/BlogName/testing",
|
"id": "https://plu.me/~/Blog/my-article",
|
||||||
"attributedTo": [users[0].ap_url, blogs[0].ap_url],
|
"attributedTo": [users[0].ap_url, blogs[0].ap_url],
|
||||||
"content": "Hello.",
|
"content": "Hello.",
|
||||||
"name": "My Article",
|
"name": "My Article",
|
||||||
@ -282,14 +199,14 @@ 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."));
|
||||||
assert_eq!(p.subtitle, "Bye".to_owned());
|
assert_eq!(p.subtitle, "Bye.".to_owned());
|
||||||
assert_eq!(p.title, "Testing".to_owned());
|
assert_eq!(p.title, "My Article".to_owned());
|
||||||
}
|
}
|
||||||
_ => panic!("Unexpected result"),
|
_ => panic!("Unexpected result"),
|
||||||
};
|
};
|
||||||
@ -297,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),
|
|
||||||
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),
|
|
||||||
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),
|
|
||||||
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,
|
||||||
@ -433,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",
|
||||||
@ -441,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",
|
||||||
@ -458,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",
|
||||||
@ -466,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",
|
||||||
@ -483,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",
|
||||||
@ -491,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(())
|
||||||
});
|
});
|
||||||
@ -500,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",
|
||||||
@ -510,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);
|
||||||
@ -524,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",
|
||||||
@ -534,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);
|
||||||
@ -550,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,
|
||||||
@ -570,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",
|
||||||
@ -578,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(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -587,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,
|
||||||
@ -607,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",
|
||||||
@ -615,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(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -624,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,
|
||||||
@ -644,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",
|
||||||
@ -652,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",
|
||||||
@ -680,7 +498,7 @@ pub(crate) mod tests {
|
|||||||
"type": "Update",
|
"type": "Update",
|
||||||
});
|
});
|
||||||
|
|
||||||
super::inbox(&conn, act).unwrap();
|
super::inbox(&r, act).unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,13 +3,12 @@ use crate::{
|
|||||||
medias::Media,
|
medias::Media,
|
||||||
safe_string::SafeString,
|
safe_string::SafeString,
|
||||||
schema::{instances, users},
|
schema::{instances, users},
|
||||||
users::{NewUser, Role, User},
|
users::{Role, User},
|
||||||
Connection, Error, Result,
|
Connection, Error, Result,
|
||||||
};
|
};
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{self, result::Error::NotFound, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
use once_cell::sync::OnceCell;
|
use plume_common::utils::md_to_html;
|
||||||
use plume_common::utils::{iri_percent_encode_seg, md_to_html};
|
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
#[derive(Clone, Identifiable, Queryable)]
|
#[derive(Clone, Identifiable, Queryable)]
|
||||||
@ -46,9 +45,6 @@ lazy_static! {
|
|||||||
static ref LOCAL_INSTANCE: RwLock<Option<Instance>> = RwLock::new(None);
|
static ref LOCAL_INSTANCE: RwLock<Option<Instance>> = RwLock::new(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
const LOCAL_INSTANCE_USERNAME: &str = "__instance__";
|
|
||||||
static LOCAL_INSTANCE_USER: OnceCell<User> = OnceCell::new();
|
|
||||||
|
|
||||||
impl Instance {
|
impl Instance {
|
||||||
pub fn set_local(self) {
|
pub fn set_local(self) {
|
||||||
LOCAL_INSTANCE.write().unwrap().replace(self);
|
LOCAL_INSTANCE.write().unwrap().replace(self);
|
||||||
@ -80,42 +76,6 @@ impl Instance {
|
|||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_local_instance_user(conn: &Connection) -> Result<User> {
|
|
||||||
let instance = Instance::get_local()?;
|
|
||||||
let email = format!("{}@{}", LOCAL_INSTANCE_USERNAME, &instance.public_domain);
|
|
||||||
NewUser::new_local(
|
|
||||||
conn,
|
|
||||||
LOCAL_INSTANCE_USERNAME.into(),
|
|
||||||
instance.public_domain,
|
|
||||||
Role::Instance,
|
|
||||||
"Local instance",
|
|
||||||
email,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_local_instance_user() -> Option<&'static User> {
|
|
||||||
LOCAL_INSTANCE_USER.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_local_instance_user_uncached(conn: &Connection) -> Result<User> {
|
|
||||||
users::table
|
|
||||||
.filter(users::role.eq(3))
|
|
||||||
.first(conn)
|
|
||||||
.or_else(|err| match err {
|
|
||||||
NotFound => Self::create_local_instance_user(conn),
|
|
||||||
_ => Err(Error::Db(err)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cache_local_instance_user(conn: &Connection) {
|
|
||||||
let _ = LOCAL_INSTANCE_USER.get_or_init(|| {
|
|
||||||
Self::get_local_instance_user_uncached(conn)
|
|
||||||
.or_else(|_| Self::create_local_instance_user(conn))
|
|
||||||
.expect("Failed to cache local instance user")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<Instance>> {
|
pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<Instance>> {
|
||||||
instances::table
|
instances::table
|
||||||
.order(instances::public_domain.asc())
|
.order(instances::public_domain.asc())
|
||||||
@ -173,8 +133,8 @@ impl Instance {
|
|||||||
"{instance}/{prefix}/{name}/{box_name}",
|
"{instance}/{prefix}/{name}/{box_name}",
|
||||||
instance = self.public_domain,
|
instance = self.public_domain,
|
||||||
prefix = prefix,
|
prefix = prefix,
|
||||||
name = iri_percent_encode_seg(name),
|
name = name,
|
||||||
box_name = iri_percent_encode_seg(box_name)
|
box_name = box_name
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,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"
|
"[long_description](/with_link)"
|
||||||
);
|
);
|
||||||
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(())
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
#![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;
|
||||||
@ -10,20 +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 activitystreams::iri_string;
|
use plume_common::activity_pub::inbox::InboxError;
|
||||||
pub use lettre;
|
|
||||||
pub use lettre::smtp;
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use plume_common::activity_pub::{inbox::InboxError, request, sign};
|
|
||||||
use posts::PostEvent;
|
|
||||||
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
|
|
||||||
use users::UserEvent;
|
|
||||||
|
|
||||||
#[cfg(not(any(feature = "sqlite", feature = "postgres")))]
|
#[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.");
|
||||||
@ -36,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 {
|
||||||
@ -68,9 +49,6 @@ pub enum Error {
|
|||||||
Url,
|
Url,
|
||||||
Webfinger,
|
Webfinger,
|
||||||
Expired,
|
Expired,
|
||||||
UserAlreadyExists,
|
|
||||||
#[cfg(feature = "s3")]
|
|
||||||
S3(s3::error::S3Error),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<bcrypt::BcryptError> for Error {
|
impl From<bcrypt::BcryptError> for Error {
|
||||||
@ -85,26 +63,20 @@ impl From<openssl::error::ErrorStack> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<sign::Error> for Error {
|
|
||||||
fn from(_: sign::Error) -> Self {
|
|
||||||
Error::Signature
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<diesel::result::Error> for Error {
|
impl From<diesel::result::Error> for Error {
|
||||||
fn from(err: diesel::result::Error) -> Self {
|
fn from(err: diesel::result::Error) -> Self {
|
||||||
Error::Db(err)
|
Error::Db(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<url::ParseError> for Error {
|
impl From<std::option::NoneError> for Error {
|
||||||
fn from(_: url::ParseError) -> Self {
|
fn from(_: std::option::NoneError) -> Self {
|
||||||
Error::Url
|
Error::NotFound
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<iri_string::validate::Error> for Error {
|
impl From<url::ParseError> for Error {
|
||||||
fn from(_: iri_string::validate::Error) -> Self {
|
fn from(_: url::ParseError) -> Self {
|
||||||
Error::Url
|
Error::Url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -127,9 +99,12 @@ impl From<reqwest::header::InvalidHeaderValue> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<activitystreams::checked::CheckError> for Error {
|
impl From<activitypub::Error> for Error {
|
||||||
fn from(_: activitystreams::checked::CheckError) -> Error {
|
fn from(err: activitypub::Error) -> Self {
|
||||||
Error::MissingApProperty
|
match err {
|
||||||
|
activitypub::Error::NotFound => Error::MissingApProperty,
|
||||||
|
_ => Error::SerDe,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,19 +141,6 @@ impl From<InboxError<Error>> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<request::Error> for Error {
|
|
||||||
fn from(_err: request::Error) -> Error {
|
|
||||||
Error::Request
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "s3")]
|
|
||||||
impl From<s3::error::S3Error> for Error {
|
|
||||||
fn from(err: s3::error::S3Error) -> Error {
|
|
||||||
Error::S3(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
/// Adds a function to a model, that returns the first
|
/// Adds a function to a model, that returns the first
|
||||||
@ -186,7 +148,7 @@ pub type Result<T> = std::result::Result<T, Error>;
|
|||||||
///
|
///
|
||||||
/// Usage:
|
/// Usage:
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```rust
|
||||||
/// impl Model {
|
/// impl Model {
|
||||||
/// find_by!(model_table, name_of_the_function, field1 as String, field2 as i32);
|
/// find_by!(model_table, name_of_the_function, field1 as String, field2 as i32);
|
||||||
/// }
|
/// }
|
||||||
@ -210,7 +172,7 @@ macro_rules! find_by {
|
|||||||
///
|
///
|
||||||
/// Usage:
|
/// Usage:
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```rust
|
||||||
/// impl Model {
|
/// impl Model {
|
||||||
/// list_by!(model_table, name_of_the_function, field1 as String);
|
/// list_by!(model_table, name_of_the_function, field1 as String);
|
||||||
/// }
|
/// }
|
||||||
@ -234,7 +196,7 @@ macro_rules! list_by {
|
|||||||
///
|
///
|
||||||
/// # Usage
|
/// # Usage
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```rust
|
||||||
/// impl Model {
|
/// impl Model {
|
||||||
/// get!(model_table);
|
/// get!(model_table);
|
||||||
/// }
|
/// }
|
||||||
@ -257,7 +219,7 @@ macro_rules! get {
|
|||||||
///
|
///
|
||||||
/// # Usage
|
/// # Usage
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```rust
|
||||||
/// impl Model {
|
/// impl Model {
|
||||||
/// insert!(model_table, NewModelType);
|
/// insert!(model_table, NewModelType);
|
||||||
/// }
|
/// }
|
||||||
@ -289,7 +251,7 @@ macro_rules! insert {
|
|||||||
///
|
///
|
||||||
/// # Usage
|
/// # Usage
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```rust
|
||||||
/// impl Model {
|
/// impl Model {
|
||||||
/// last!(model_table);
|
/// last!(model_table);
|
||||||
/// }
|
/// }
|
||||||
@ -316,41 +278,15 @@ pub fn ap_url(url: &str) -> String {
|
|||||||
format!("https://{}", url)
|
format!("https://{}", url)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait SmtpNewWithAddr {
|
|
||||||
fn new_with_addr(
|
|
||||||
addr: (&str, u16),
|
|
||||||
) -> std::result::Result<smtp::SmtpClient, smtp::error::Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SmtpNewWithAddr for smtp::SmtpClient {
|
|
||||||
// Stolen from lettre::smtp::SmtpClient::new_simple()
|
|
||||||
fn new_with_addr(addr: (&str, u16)) -> std::result::Result<Self, smtp::error::Error> {
|
|
||||||
use native_tls::TlsConnector;
|
|
||||||
use smtp::{
|
|
||||||
client::net::{ClientTlsParameters, DEFAULT_TLS_PROTOCOLS},
|
|
||||||
ClientSecurity, SmtpClient,
|
|
||||||
};
|
|
||||||
|
|
||||||
let (domain, port) = addr;
|
|
||||||
|
|
||||||
let mut tls_builder = TlsConnector::builder();
|
|
||||||
tls_builder.min_protocol_version(Some(DEFAULT_TLS_PROTOCOLS[0]));
|
|
||||||
|
|
||||||
let tls_parameters =
|
|
||||||
ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap());
|
|
||||||
|
|
||||||
SmtpClient::new((domain, port), ClientSecurity::Wrapper(tls_parameters))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[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 chrono::{naive::NaiveDateTime, Datelike, Timelike};
|
|
||||||
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 {
|
||||||
@ -363,7 +299,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn db() -> db_conn::DbConn {
|
pub fn db<'a>() -> db_conn::DbConn {
|
||||||
db_conn::DbConn((*DB_POOL).get().unwrap())
|
db_conn::DbConn((*DB_POOL).get().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -381,31 +317,13 @@ mod tests {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "postgres")]
|
pub fn rockets() -> super::PlumeRocket {
|
||||||
pub(crate) fn format_datetime(dt: &NaiveDateTime) -> String {
|
super::PlumeRocket {
|
||||||
format!(
|
conn: db_conn::DbConn((*DB_POOL).get().unwrap()),
|
||||||
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z",
|
searcher: Arc::new(search::tests::get_searcher(&CONFIG.search_tokenizers)),
|
||||||
dt.year(),
|
worker: Arc::new(ScheduledThreadPool::new(2)),
|
||||||
dt.month(),
|
user: None,
|
||||||
dt.day(),
|
}
|
||||||
dt.hour(),
|
|
||||||
dt.minute(),
|
|
||||||
dt.second(),
|
|
||||||
dt.timestamp_subsec_micros()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "sqlite")]
|
|
||||||
pub(crate) fn format_datetime(dt: &NaiveDateTime) -> String {
|
|
||||||
format!(
|
|
||||||
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
|
|
||||||
dt.year(),
|
|
||||||
dt.month(),
|
|
||||||
dt.day(),
|
|
||||||
dt.hour(),
|
|
||||||
dt.minute(),
|
|
||||||
dt.second()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -418,7 +336,6 @@ pub mod blogs;
|
|||||||
pub mod comment_seers;
|
pub mod comment_seers;
|
||||||
pub mod comments;
|
pub mod comments;
|
||||||
pub mod db_conn;
|
pub mod db_conn;
|
||||||
pub mod email_signups;
|
|
||||||
pub mod follows;
|
pub mod follows;
|
||||||
pub mod headers;
|
pub mod headers;
|
||||||
pub mod inbox;
|
pub mod inbox;
|
||||||
@ -433,13 +350,11 @@ 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)]
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod signups;
|
|
||||||
pub mod tags;
|
pub mod tags;
|
||||||
pub mod timeline;
|
pub mod timeline;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
|
|||||||
@ -1,19 +1,13 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
instance::Instance, 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 activitystreams::{
|
|
||||||
activity::{ActorAndObjectRef, Like as LikeAct, Undo},
|
|
||||||
base::AnyBase,
|
|
||||||
iri_string::types::IriString,
|
|
||||||
prelude::*,
|
|
||||||
};
|
};
|
||||||
|
use activitypub::activity;
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
use plume_common::activity_pub::{
|
use plume_common::activity_pub::{
|
||||||
inbox::{AsActor, AsObject, FromId},
|
inbox::{AsActor, AsObject, FromId},
|
||||||
sign::Signer,
|
Id, IntoId, PUBLIC_VISIBILITY,
|
||||||
PUBLIC_VISIBILITY,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Identifiable)]
|
#[derive(Clone, Queryable, Identifiable)]
|
||||||
@ -39,16 +33,18 @@ impl Like {
|
|||||||
find_by!(likes, find_by_ap_url, ap_url as &str);
|
find_by!(likes, find_by_ap_url, ap_url as &str);
|
||||||
find_by!(likes, find_by_user_on_post, user_id as i32, post_id as i32);
|
find_by!(likes, find_by_user_on_post, user_id as i32, post_id as i32);
|
||||||
|
|
||||||
pub fn to_activity(&self, conn: &Connection) -> Result<LikeAct> {
|
pub fn to_activity(&self, conn: &Connection) -> Result<activity::Like> {
|
||||||
let mut act = LikeAct::new(
|
let mut act = activity::Like::default();
|
||||||
User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?,
|
act.like_props
|
||||||
Post::get(conn, self.post_id)?.ap_url.parse::<IriString>()?,
|
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
||||||
);
|
act.like_props
|
||||||
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
|
.set_object_link(Post::get(conn, self.post_id)?.into_id())?;
|
||||||
act.set_many_ccs(vec![User::get(conn, self.user_id)?
|
act.object_props
|
||||||
.followers_endpoint
|
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
|
||||||
.parse::<IriString>()?]);
|
act.object_props.set_cc_link_vec(vec![Id::new(
|
||||||
act.set_id(self.ap_url.parse::<IriString>()?);
|
User::get(conn, self.user_id)?.followers_endpoint,
|
||||||
|
)])?;
|
||||||
|
act.object_props.set_id_string(self.ap_url.clone())?;
|
||||||
|
|
||||||
Ok(act)
|
Ok(act)
|
||||||
}
|
}
|
||||||
@ -70,95 +66,75 @@ impl Like {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_undo(&self, conn: &Connection) -> Result<Undo> {
|
pub fn build_undo(&self, conn: &Connection) -> Result<activity::Undo> {
|
||||||
let mut act = Undo::new(
|
let mut act = activity::Undo::default();
|
||||||
User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?,
|
act.undo_props
|
||||||
AnyBase::from_extended(self.to_activity(conn)?)?,
|
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
||||||
);
|
act.undo_props.set_object_object(self.to_activity(conn)?)?;
|
||||||
act.set_id(format!("{}#delete", self.ap_url).parse::<IriString>()?);
|
act.object_props
|
||||||
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
|
.set_id_string(format!("{}#delete", self.ap_url))?;
|
||||||
act.set_many_ccs(vec![User::get(conn, self.user_id)?
|
act.object_props
|
||||||
.followers_endpoint
|
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
|
||||||
.parse::<IriString>()?]);
|
act.object_props.set_cc_link_vec(vec![Id::new(
|
||||||
|
User::get(conn, self.user_id)?.followers_endpoint,
|
||||||
|
)])?;
|
||||||
|
|
||||||
Ok(act)
|
Ok(act)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, LikeAct, &Connection> 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: &Connection, 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<Connection> for Like {
|
impl FromId<PlumeRocket> for Like {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = LikeAct;
|
type Object = activity::Like;
|
||||||
|
|
||||||
fn from_db(conn: &Connection, 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: &Connection, act: LikeAct) -> 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.object_field_ref()
|
.id,
|
||||||
.as_single_id()
|
user_id: User::from_id(c, &act.like_props.actor_link::<Id>()?, None)
|
||||||
.ok_or(Error::MissingApProperty)?
|
.map_err(|(_, e)| e)?
|
||||||
.as_str(),
|
.id,
|
||||||
None,
|
ap_url: act.object_props.id_string()?,
|
||||||
CONFIG.proxy(),
|
|
||||||
)
|
|
||||||
.map_err(|(_, e)| e)?
|
|
||||||
.id,
|
|
||||||
user_id: User::from_id(
|
|
||||||
conn,
|
|
||||||
act.actor_field_ref()
|
|
||||||
.as_single_id()
|
|
||||||
.ok_or(Error::MissingApProperty)?
|
|
||||||
.as_str(),
|
|
||||||
None,
|
|
||||||
CONFIG.proxy(),
|
|
||||||
)
|
|
||||||
.map_err(|(_, e)| e)?
|
|
||||||
.id,
|
|
||||||
ap_url: act
|
|
||||||
.id_unchecked()
|
|
||||||
.ok_or(Error::MissingApProperty)?
|
|
||||||
.to_string(),
|
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
res.notify(conn)?;
|
res.notify(&c.conn)?;
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_sender() -> &'static dyn Signer {
|
|
||||||
Instance::get_local_instance_user().expect("Failed to local instance user")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Undo, &Connection> for Like {
|
impl AsObject<User, activity::Undo, &PlumeRocket> for Like {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
|
||||||
fn activity(self, conn: &Connection, 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)?;
|
||||||
|
|
||||||
@ -175,7 +151,8 @@ impl AsObject<User, Undo, &Connection> for Like {
|
|||||||
|
|
||||||
impl NewLike {
|
impl NewLike {
|
||||||
pub fn new(p: &Post, u: &User) -> Self {
|
pub fn new(p: &Post, u: &User) -> Self {
|
||||||
let ap_url = format!("{}like/{}", u.ap_url, p.ap_url);
|
// TODO: this URL is not valid
|
||||||
|
let ap_url = format!("{}/like/{}", u.ap_url, p.ap_url);
|
||||||
NewLike {
|
NewLike {
|
||||||
post_id: p.id,
|
post_id: p.id,
|
||||||
user_id: u.id,
|
user_id: u.id,
|
||||||
@ -183,67 +160,3 @@ impl NewLike {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::diesel::Connection;
|
|
||||||
use crate::{inbox::tests::fill_database, tests::db};
|
|
||||||
use assert_json_diff::assert_json_eq;
|
|
||||||
use serde_json::{json, to_value};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn to_activity() {
|
|
||||||
let conn = db();
|
|
||||||
conn.test_transaction::<_, Error, _>(|| {
|
|
||||||
let (posts, _users, _blogs) = fill_database(&conn);
|
|
||||||
let post = &posts[0];
|
|
||||||
let user = &post.get_authors(&conn)?[0];
|
|
||||||
let like = Like::insert(&conn, NewLike::new(post, user))?;
|
|
||||||
let act = like.to_activity(&conn).unwrap();
|
|
||||||
|
|
||||||
let expected = json!({
|
|
||||||
"actor": "https://plu.me/@/admin/",
|
|
||||||
"cc": ["https://plu.me/@/admin/followers"],
|
|
||||||
"id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing",
|
|
||||||
"object": "https://plu.me/~/BlogName/testing",
|
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
||||||
"type": "Like",
|
|
||||||
});
|
|
||||||
assert_json_eq!(to_value(act)?, expected);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn build_undo() {
|
|
||||||
let conn = db();
|
|
||||||
conn.test_transaction::<_, Error, _>(|| {
|
|
||||||
let (posts, _users, _blogs) = fill_database(&conn);
|
|
||||||
let post = &posts[0];
|
|
||||||
let user = &post.get_authors(&conn)?[0];
|
|
||||||
let like = Like::insert(&conn, NewLike::new(post, user))?;
|
|
||||||
let act = like.build_undo(&conn)?;
|
|
||||||
|
|
||||||
let expected = json!({
|
|
||||||
"actor": "https://plu.me/@/admin/",
|
|
||||||
"cc": ["https://plu.me/@/admin/followers"],
|
|
||||||
"id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing#delete",
|
|
||||||
"object": {
|
|
||||||
"actor": "https://plu.me/@/admin/",
|
|
||||||
"cc": ["https://plu.me/@/admin/followers"],
|
|
||||||
"id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing",
|
|
||||||
"object": "https://plu.me/~/BlogName/testing",
|
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
||||||
"type": "Like",
|
|
||||||
},
|
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
||||||
"type": "Undo",
|
|
||||||
});
|
|
||||||
assert_json_eq!(to_value(act)?, expected);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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,
|
||||||
@ -143,7 +143,6 @@ macro_rules! func {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Clone, Queryable, Identifiable)]
|
#[derive(Clone, Queryable, Identifiable)]
|
||||||
struct ListElem {
|
struct ListElem {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
@ -247,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
|
||||||
@ -286,8 +285,7 @@ impl List {
|
|||||||
.select(list_elems::word)
|
.select(list_elems::word)
|
||||||
.load::<Option<String>>(conn)
|
.load::<Option<String>>(conn)
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
// .map(|r| r.into_iter().filter_map(|o| o).collect::<Vec<String>>())
|
.map(|r| r.into_iter().filter_map(|o| o).collect::<Vec<String>>())
|
||||||
.map(|r| r.into_iter().flatten().collect::<Vec<String>>())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear(&self, conn: &Connection) -> Result<()> {
|
pub fn clear(&self, conn: &Connection) -> Result<()> {
|
||||||
@ -297,28 +295,6 @@ impl List {
|
|||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(&self, conn: &Connection) -> Result<()> {
|
|
||||||
if let Some(user_id) = self.user_id {
|
|
||||||
diesel::delete(
|
|
||||||
lists::table
|
|
||||||
.filter(lists::user_id.eq(user_id))
|
|
||||||
.filter(lists::name.eq(&self.name)),
|
|
||||||
)
|
|
||||||
.execute(conn)
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(Error::from)
|
|
||||||
} else {
|
|
||||||
diesel::delete(
|
|
||||||
lists::table
|
|
||||||
.filter(lists::user_id.is_null())
|
|
||||||
.filter(lists::name.eq(&self.name)),
|
|
||||||
)
|
|
||||||
.execute(conn)
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func! {set: set_users, User, add_users}
|
func! {set: set_users, User, add_users}
|
||||||
func! {set: set_blogs, Blog, add_blogs}
|
func! {set: set_blogs, Blog, add_blogs}
|
||||||
func! {set: set_words, Word, add_words}
|
func! {set: set_words, Word, add_words}
|
||||||
@ -435,7 +411,7 @@ mod tests {
|
|||||||
&List::find_for_user_by_name(conn, l1.user_id, &l1.name).unwrap(),
|
&List::find_for_user_by_name(conn, l1.user_id, &l1.name).unwrap(),
|
||||||
);
|
);
|
||||||
l_eq(
|
l_eq(
|
||||||
&l1u,
|
&&l1u,
|
||||||
&List::find_for_user_by_name(conn, l1u.user_id, &l1u.name).unwrap(),
|
&List::find_for_user_by_name(conn, l1u.user_id, &l1u.name).unwrap(),
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@ -1,27 +1,19 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
ap_url, instance::Instance, safe_string::SafeString, schema::medias, users::User, Connection,
|
ap_url, instance::Instance, safe_string::SafeString, schema::medias, users::User, Connection,
|
||||||
Error, Result, CONFIG,
|
Error, PlumeRocket, Result,
|
||||||
};
|
};
|
||||||
use activitystreams::{object::Image, prelude::*};
|
use activitypub::object::Image;
|
||||||
|
use askama_escape::escape;
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
use guid_create::GUID;
|
use guid_create::GUID;
|
||||||
use plume_common::{
|
use plume_common::{
|
||||||
activity_pub::{inbox::FromId, request, ToAsString, ToAsUri},
|
activity_pub::{inbox::FromId, Id},
|
||||||
utils::{escape, MediaProcessor},
|
utils::MediaProcessor,
|
||||||
};
|
};
|
||||||
use std::{
|
use reqwest;
|
||||||
fs::{self, DirBuilder},
|
use std::{fs, path::Path};
|
||||||
path::{self, Path, PathBuf},
|
|
||||||
};
|
|
||||||
use tracing::warn;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
#[cfg(feature = "s3")]
|
#[derive(Clone, Identifiable, Queryable)]
|
||||||
use crate::config::S3Config;
|
|
||||||
|
|
||||||
const REMOTE_MEDIA_DIRECTORY: &str = "remote";
|
|
||||||
|
|
||||||
#[derive(Clone, Identifiable, Queryable, AsChangeset)]
|
|
||||||
pub struct Media {
|
pub struct Media {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub file_path: String,
|
pub file_path: String,
|
||||||
@ -45,7 +37,7 @@ pub struct NewMedia {
|
|||||||
pub owner_id: i32,
|
pub owner_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq)]
|
#[derive(PartialEq)]
|
||||||
pub enum MediaCategory {
|
pub enum MediaCategory {
|
||||||
Image,
|
Image,
|
||||||
Audio,
|
Audio,
|
||||||
@ -67,7 +59,6 @@ impl MediaCategory {
|
|||||||
impl Media {
|
impl Media {
|
||||||
insert!(medias, NewMedia);
|
insert!(medias, NewMedia);
|
||||||
get!(medias);
|
get!(medias);
|
||||||
find_by!(medias, find_by_file_path, file_path as &str);
|
|
||||||
|
|
||||||
pub fn for_user(conn: &Connection, owner: i32) -> Result<Vec<Media>> {
|
pub fn for_user(conn: &Connection, owner: i32) -> Result<Vec<Media>> {
|
||||||
medias::table
|
medias::table
|
||||||
@ -106,9 +97,9 @@ impl Media {
|
|||||||
pub fn category(&self) -> MediaCategory {
|
pub fn category(&self) -> MediaCategory {
|
||||||
match &*self
|
match &*self
|
||||||
.file_path
|
.file_path
|
||||||
.rsplit_once('.')
|
.rsplitn(2, '.')
|
||||||
.map(|x| x.1)
|
.next()
|
||||||
.unwrap_or("")
|
.expect("Media::category: extension error")
|
||||||
.to_lowercase()
|
.to_lowercase()
|
||||||
{
|
{
|
||||||
"png" | "jpg" | "jpeg" | "gif" | "svg" => MediaCategory::Image,
|
"png" | "jpg" | "jpeg" | "gif" | "svg" => MediaCategory::Image,
|
||||||
@ -154,99 +145,23 @@ impl Media {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns full file path for medias stored in the local media directory.
|
|
||||||
pub fn local_path(&self) -> Option<PathBuf> {
|
|
||||||
if self.file_path.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
if CONFIG.s3.is_some() {
|
|
||||||
#[cfg(feature="s3")]
|
|
||||||
unreachable!("Called Media::local_path() but media are stored on S3");
|
|
||||||
#[cfg(not(feature="s3"))]
|
|
||||||
unreachable!();
|
|
||||||
}
|
|
||||||
|
|
||||||
let relative_path = self
|
|
||||||
.file_path
|
|
||||||
.trim_start_matches(&CONFIG.media_directory)
|
|
||||||
.trim_start_matches(path::MAIN_SEPARATOR)
|
|
||||||
.trim_start_matches("static/media/");
|
|
||||||
|
|
||||||
Some(Path::new(&CONFIG.media_directory).join(relative_path))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the relative URL to access this file, which is also the key at which
|
|
||||||
/// it is stored in the S3 bucket if we are using S3 storage.
|
|
||||||
/// Does not start with a '/', it is of the form "static/media/<...>"
|
|
||||||
pub fn relative_url(&self) -> Option<String> {
|
|
||||||
if self.file_path.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let relative_path = self
|
|
||||||
.file_path
|
|
||||||
.trim_start_matches(&CONFIG.media_directory)
|
|
||||||
.replace(path::MAIN_SEPARATOR, "/");
|
|
||||||
|
|
||||||
let relative_path = relative_path
|
|
||||||
.trim_start_matches('/')
|
|
||||||
.trim_start_matches("static/media/");
|
|
||||||
|
|
||||||
Some(format!("static/media/{}", relative_path))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a public URL through which this media file can be accessed
|
|
||||||
pub fn url(&self) -> Result<String> {
|
pub fn url(&self) -> Result<String> {
|
||||||
if self.is_remote {
|
if self.is_remote {
|
||||||
Ok(self.remote_url.clone().unwrap_or_default())
|
Ok(self.remote_url.clone().unwrap_or_default())
|
||||||
} else {
|
} else {
|
||||||
let relative_url = self.relative_url().unwrap_or_default();
|
let p = Path::new(&self.file_path);
|
||||||
|
let filename: String = p.file_name().unwrap().to_str().unwrap().to_owned();
|
||||||
#[cfg(feature="s3")]
|
|
||||||
if CONFIG.s3.as_ref().map(|x| x.direct_download).unwrap_or(false) {
|
|
||||||
let s3_url = match CONFIG.s3.as_ref().unwrap() {
|
|
||||||
S3Config { alias: Some(alias), .. } => {
|
|
||||||
format!("https://{}/{}", alias, relative_url)
|
|
||||||
}
|
|
||||||
S3Config { path_style: true, hostname, bucket, .. } => {
|
|
||||||
format!("https://{}/{}/{}",
|
|
||||||
hostname,
|
|
||||||
bucket,
|
|
||||||
relative_url
|
|
||||||
)
|
|
||||||
}
|
|
||||||
S3Config { path_style: false, hostname, bucket, .. } => {
|
|
||||||
format!("https://{}.{}/{}",
|
|
||||||
bucket,
|
|
||||||
hostname,
|
|
||||||
relative_url
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return Ok(s3_url);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ap_url(&format!(
|
Ok(ap_url(&format!(
|
||||||
"{}/{}",
|
"{}/static/media/{}",
|
||||||
Instance::get_local()?.public_domain,
|
Instance::get_local()?.public_domain,
|
||||||
relative_url
|
&filename
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(&self, conn: &Connection) -> Result<()> {
|
pub fn delete(&self, conn: &Connection) -> Result<()> {
|
||||||
if !self.is_remote {
|
if !self.is_remote {
|
||||||
if CONFIG.s3.is_some() {
|
fs::remove_file(self.file_path.as_str())?;
|
||||||
#[cfg(not(feature="s3"))]
|
|
||||||
unreachable!();
|
|
||||||
|
|
||||||
#[cfg(feature = "s3")]
|
|
||||||
CONFIG.s3.as_ref().unwrap().get_bucket()
|
|
||||||
.delete_object_blocking(&self.relative_url().ok_or(Error::NotFound)?)?;
|
|
||||||
} else {
|
|
||||||
fs::remove_file(self.local_path().ok_or(Error::NotFound)?)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
diesel::delete(self)
|
diesel::delete(self)
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
@ -282,128 +197,50 @@ impl Media {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: merge with save_remote?
|
// TODO: merge with save_remote?
|
||||||
pub fn from_activity(conn: &Connection, image: &Image) -> Result<Media> {
|
pub fn from_activity(c: &PlumeRocket, image: &Image) -> Result<Media> {
|
||||||
let remote_url = image
|
let conn = &*c.conn;
|
||||||
.url()
|
let remote_url = image.object_props.url_string().ok()?;
|
||||||
.and_then(|url| url.to_as_uri())
|
let ext = remote_url
|
||||||
.ok_or(Error::MissingApProperty)?;
|
.rsplit('.')
|
||||||
|
.next()
|
||||||
|
.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 file_path = if CONFIG.s3.is_some() {
|
let mut dest = fs::File::create(path.clone()).ok()?;
|
||||||
#[cfg(not(feature="s3"))]
|
reqwest::get(remote_url.as_str())
|
||||||
unreachable!();
|
.ok()?
|
||||||
|
.copy_to(&mut dest)
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
#[cfg(feature = "s3")]
|
Media::insert(
|
||||||
{
|
conn,
|
||||||
use rocket::http::ContentType;
|
NewMedia {
|
||||||
|
file_path: path.to_str()?.to_string(),
|
||||||
let dest = determine_mirror_s3_path(&remote_url);
|
alt_text: image.object_props.content_string().ok()?,
|
||||||
|
is_remote: false,
|
||||||
let media = request::get(
|
remote_url: None,
|
||||||
remote_url.as_str(),
|
sensitive: image.object_props.summary_string().is_ok(),
|
||||||
User::get_sender(),
|
content_warning: image.object_props.summary_string().ok(),
|
||||||
CONFIG.proxy().cloned(),
|
owner_id: User::from_id(
|
||||||
)?;
|
c,
|
||||||
|
image
|
||||||
let content_type = media
|
.object_props
|
||||||
.headers()
|
.attributed_to_link_vec::<Id>()
|
||||||
.get(reqwest::header::CONTENT_TYPE)
|
.ok()?
|
||||||
.and_then(|x| x.to_str().ok())
|
.into_iter()
|
||||||
.and_then(ContentType::parse_flexible)
|
.next()?
|
||||||
.unwrap_or(ContentType::Binary);
|
.as_ref(),
|
||||||
|
None,
|
||||||
let bytes = media.bytes()?;
|
|
||||||
|
|
||||||
let bucket = CONFIG.s3.as_ref().unwrap().get_bucket();
|
|
||||||
bucket.put_object_with_content_type_blocking(
|
|
||||||
&dest,
|
|
||||||
&bytes,
|
|
||||||
&content_type.to_string()
|
|
||||||
)?;
|
|
||||||
|
|
||||||
dest
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let path = determine_mirror_file_path(&remote_url);
|
|
||||||
let parent = path.parent().ok_or(Error::InvalidValue)?;
|
|
||||||
if !parent.is_dir() {
|
|
||||||
DirBuilder::new().recursive(true).create(parent)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dest = fs::File::create(path.clone())?;
|
|
||||||
// TODO: conditional GET
|
|
||||||
request::get(
|
|
||||||
remote_url.as_str(),
|
|
||||||
User::get_sender(),
|
|
||||||
CONFIG.proxy().cloned(),
|
|
||||||
)?
|
|
||||||
.copy_to(&mut dest)?;
|
|
||||||
path.to_str().ok_or(Error::InvalidValue)?.to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
Media::find_by_file_path(conn, &file_path)
|
|
||||||
.and_then(|mut media| {
|
|
||||||
let mut updated = false;
|
|
||||||
|
|
||||||
let alt_text = image
|
|
||||||
.content()
|
|
||||||
.and_then(|content| content.to_as_string())
|
|
||||||
.ok_or(Error::NotFound)?;
|
|
||||||
let summary = image.summary().and_then(|summary| summary.to_as_string());
|
|
||||||
let sensitive = summary.is_some();
|
|
||||||
let content_warning = summary;
|
|
||||||
if media.alt_text != alt_text {
|
|
||||||
media.alt_text = alt_text;
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
if media.is_remote {
|
|
||||||
media.is_remote = false;
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
if media.remote_url.is_some() {
|
|
||||||
media.remote_url = None;
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
if media.sensitive != sensitive {
|
|
||||||
media.sensitive = sensitive;
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
if media.content_warning != content_warning {
|
|
||||||
media.content_warning = content_warning;
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
if updated {
|
|
||||||
diesel::update(&media).set(&media).execute(conn)?;
|
|
||||||
}
|
|
||||||
Ok(media)
|
|
||||||
})
|
|
||||||
.or_else(|_| {
|
|
||||||
let summary = image.summary().and_then(|summary| summary.to_as_string());
|
|
||||||
Media::insert(
|
|
||||||
conn,
|
|
||||||
NewMedia {
|
|
||||||
file_path,
|
|
||||||
alt_text: image
|
|
||||||
.content()
|
|
||||||
.and_then(|content| content.to_as_string())
|
|
||||||
.ok_or(Error::NotFound)?,
|
|
||||||
is_remote: false,
|
|
||||||
remote_url: None,
|
|
||||||
sensitive: summary.is_some(),
|
|
||||||
content_warning: summary,
|
|
||||||
owner_id: User::from_id(
|
|
||||||
conn,
|
|
||||||
&image
|
|
||||||
.attributed_to()
|
|
||||||
.and_then(|attributed_to| attributed_to.to_as_uri())
|
|
||||||
.ok_or(Error::MissingApProperty)?,
|
|
||||||
None,
|
|
||||||
CONFIG.proxy(),
|
|
||||||
)
|
|
||||||
.map_err(|(_, e)| e)?
|
|
||||||
.id,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
})
|
.map_err(|(_, e)| e)?
|
||||||
|
.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_media_processor<'a>(conn: &'a Connection, user: Vec<&User>) -> MediaProcessor<'a> {
|
pub fn get_media_processor<'a>(conn: &'a Connection, user: Vec<&User>) -> MediaProcessor<'a> {
|
||||||
@ -420,66 +257,6 @@ impl Media {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn determine_mirror_file_path(url: &str) -> PathBuf {
|
|
||||||
let mut file_path = Path::new(&CONFIG.media_directory).join(REMOTE_MEDIA_DIRECTORY);
|
|
||||||
|
|
||||||
match Url::parse(url) {
|
|
||||||
Ok(url) if url.has_host() => {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
other => {
|
|
||||||
if let Err(err) = other {
|
|
||||||
warn!("Failed to parse url: {} {}", &url, err);
|
|
||||||
} else {
|
|
||||||
warn!("Error without a host: {}", &url);
|
|
||||||
}
|
|
||||||
let ext = url
|
|
||||||
.rsplit('.')
|
|
||||||
.next()
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.unwrap_or_else(|| String::from("png"));
|
|
||||||
file_path.push(format!("{}.{}", GUID::rand(), ext));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
file_path
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature="s3")]
|
|
||||||
fn determine_mirror_s3_path(url: &str) -> String {
|
|
||||||
match Url::parse(url) {
|
|
||||||
Ok(url) if url.has_host() => {
|
|
||||||
format!("static/media/{}/{}/{}",
|
|
||||||
REMOTE_MEDIA_DIRECTORY,
|
|
||||||
url.host_str().unwrap(),
|
|
||||||
url.path().trim_start_matches('/'),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
other => {
|
|
||||||
if let Err(err) = other {
|
|
||||||
warn!("Failed to parse url: {} {}", &url, err);
|
|
||||||
} else {
|
|
||||||
warn!("Error without a host: {}", &url);
|
|
||||||
}
|
|
||||||
let ext = url
|
|
||||||
.rsplit('.')
|
|
||||||
.next()
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.unwrap_or_else(|| String::from("png"));
|
|
||||||
format!("static/media/{}/{}.{}",
|
|
||||||
REMOTE_MEDIA_DIRECTORY,
|
|
||||||
GUID::rand(),
|
|
||||||
ext,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -490,7 +267,7 @@ pub(crate) mod tests {
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Media>) {
|
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Media>) {
|
||||||
let mut wd = current_dir().unwrap();
|
let mut wd = current_dir().unwrap().to_path_buf();
|
||||||
while wd.pop() {
|
while wd.pop() {
|
||||||
if wd.join(".git").exists() {
|
if wd.join(".git").exists() {
|
||||||
set_current_dir(wd).unwrap();
|
set_current_dir(wd).unwrap();
|
||||||
@ -545,15 +322,7 @@ pub(crate) mod tests {
|
|||||||
pub(crate) fn clean(conn: &Conn) {
|
pub(crate) fn clean(conn: &Conn) {
|
||||||
//used to remove files generated by tests
|
//used to remove files generated by tests
|
||||||
for media in Media::list_all_medias(conn).unwrap() {
|
for media in Media::list_all_medias(conn).unwrap() {
|
||||||
if let Some(err) = media.delete(conn).err() {
|
media.delete(conn).unwrap();
|
||||||
match &err {
|
|
||||||
Error::Io(e) => match e.kind() {
|
|
||||||
std::io::ErrorKind::NotFound => (),
|
|
||||||
_ => panic!("{:?}", err),
|
|
||||||
},
|
|
||||||
_ => panic!("{:?}", err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -603,7 +372,7 @@ pub(crate) mod tests {
|
|||||||
let media = Media::insert(
|
let media = Media::insert(
|
||||||
conn,
|
conn,
|
||||||
NewMedia {
|
NewMedia {
|
||||||
file_path: path,
|
file_path: path.clone(),
|
||||||
alt_text: "alt message".to_owned(),
|
alt_text: "alt message".to_owned(),
|
||||||
is_remote: false,
|
is_remote: false,
|
||||||
remote_url: None,
|
remote_url: None,
|
||||||
|
|||||||
@ -1,12 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
comments::Comment, notifications::*, posts::Post, schema::mentions, users::User, Connection,
|
comments::Comment, notifications::*, posts::Post, schema::mentions, users::User, Connection,
|
||||||
Error, Result,
|
Error, PlumeRocket, Result,
|
||||||
};
|
|
||||||
use activitystreams::{
|
|
||||||
base::BaseExt,
|
|
||||||
iri_string::types::IriString,
|
|
||||||
link::{self, LinkExt},
|
|
||||||
};
|
};
|
||||||
|
use activitypub::link;
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
use plume_common::activity_pub::inbox::AsActor;
|
use plume_common::activity_pub::inbox::AsActor;
|
||||||
|
|
||||||
@ -51,28 +47,26 @@ impl Mention {
|
|||||||
|
|
||||||
pub fn get_user(&self, conn: &Connection) -> Result<User> {
|
pub fn get_user(&self, conn: &Connection) -> Result<User> {
|
||||||
match self.get_post(conn) {
|
match self.get_post(conn) {
|
||||||
Ok(p) => Ok(p
|
Ok(p) => Ok(p.get_authors(conn)?.into_iter().next()?),
|
||||||
.get_authors(conn)?
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
.ok_or(Error::NotFound)?),
|
|
||||||
Err(_) => self.get_comment(conn).and_then(|c| c.get_author(conn)),
|
Err(_) => self.get_comment(conn).and_then(|c| c.get_author(conn)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_activity(conn: &Connection, 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::new();
|
let mut mention = link::Mention::default();
|
||||||
mention.set_href(user.ap_url.parse::<IriString>()?);
|
mention.link_props.set_href_string(user.ap_url)?;
|
||||||
mention.set_name(format!("@{}", ment));
|
mention.link_props.set_name_string(format!("@{}", ment))?;
|
||||||
Ok(mention)
|
Ok(mention)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_activity(&self, conn: &Connection) -> Result<link::Mention> {
|
pub fn to_activity(&self, conn: &Connection) -> Result<link::Mention> {
|
||||||
let user = self.get_mentioned(conn)?;
|
let user = self.get_mentioned(conn)?;
|
||||||
let mut mention = link::Mention::new();
|
let mut mention = link::Mention::default();
|
||||||
mention.set_href(user.ap_url.parse::<IriString>()?);
|
mention.link_props.set_href_string(user.ap_url.clone())?;
|
||||||
mention.set_name(format!("@{}", user.fqn));
|
mention
|
||||||
|
.link_props
|
||||||
|
.set_name_string(format!("@{}", user.fqn))?;
|
||||||
Ok(mention)
|
Ok(mention)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,8 +77,8 @@ impl Mention {
|
|||||||
in_post: bool,
|
in_post: bool,
|
||||||
notify: bool,
|
notify: bool,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let ap_url = ment.href().ok_or(Error::NotFound)?.as_str();
|
let ap_url = ment.link_props.href_string().ok()?;
|
||||||
let mentioned = User::find_by_ap_url(conn, ap_url)?;
|
let mentioned = User::find_by_ap_url(conn, &ap_url)?;
|
||||||
|
|
||||||
if in_post {
|
if in_post {
|
||||||
Post::get(conn, inside).and_then(|post| {
|
Post::get(conn, inside).and_then(|post| {
|
||||||
@ -147,62 +141,3 @@ impl Mention {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::{inbox::tests::fill_database, tests::db, Error};
|
|
||||||
use assert_json_diff::assert_json_eq;
|
|
||||||
use diesel::Connection;
|
|
||||||
use serde_json::{json, to_value};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn build_activity() {
|
|
||||||
let conn = db();
|
|
||||||
conn.test_transaction::<_, Error, _>(|| {
|
|
||||||
let (_posts, users, _blogs) = fill_database(&conn);
|
|
||||||
let user = &users[0];
|
|
||||||
let name = &user.username;
|
|
||||||
let act = Mention::build_activity(&conn, name)?;
|
|
||||||
|
|
||||||
let expected = json!({
|
|
||||||
"href": "https://plu.me/@/admin/",
|
|
||||||
"name": "@admin",
|
|
||||||
"type": "Mention",
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_json_eq!(to_value(act)?, expected);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn to_activity() {
|
|
||||||
let conn = db();
|
|
||||||
conn.test_transaction::<_, Error, _>(|| {
|
|
||||||
let (posts, users, _blogs) = fill_database(&conn);
|
|
||||||
let post = &posts[0];
|
|
||||||
let user = &users[0];
|
|
||||||
let mention = Mention::insert(
|
|
||||||
&conn,
|
|
||||||
NewMention {
|
|
||||||
mentioned_id: user.id,
|
|
||||||
post_id: Some(post.id),
|
|
||||||
comment_id: None,
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
let act = mention.to_activity(&conn)?;
|
|
||||||
|
|
||||||
let expected = json!({
|
|
||||||
"href": "https://plu.me/@/admin/",
|
|
||||||
"name": "@admin",
|
|
||||||
"type": "Mention",
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_json_eq!(to_value(act)?, expected);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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)?
|
||||||
}
|
}
|
||||||
@ -105,8 +104,7 @@ impl ImportedMigrations {
|
|||||||
pub fn rerun_last_migration(&self, conn: &Connection, path: &Path) -> Result<()> {
|
pub fn rerun_last_migration(&self, conn: &Connection, path: &Path) -> Result<()> {
|
||||||
let latest_migration = conn.latest_run_migration_version()?;
|
let latest_migration = conn.latest_run_migration_version()?;
|
||||||
let id = latest_migration
|
let id = latest_migration
|
||||||
.and_then(|m| self.0.binary_search_by_key(&m.as_str(), |m| m.name).ok())
|
.and_then(|m| self.0.binary_search_by_key(&m.as_str(), |m| m.name).ok())?;
|
||||||
.ok_or(Error::NotFound)?;
|
|
||||||
let migration = &self.0[id];
|
let migration = &self.0[id];
|
||||||
conn.transaction(|| {
|
conn.transaction(|| {
|
||||||
migration.revert(conn, path)?;
|
migration.revert(conn, path)?;
|
||||||
|
|||||||
@ -61,7 +61,7 @@ impl PasswordResetRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_and_delete_by_token(conn: &Connection, token: &str) -> Result<Self> {
|
pub fn find_and_delete_by_token(conn: &Connection, token: &str) -> Result<Self> {
|
||||||
let request = Self::find_by_token(conn, token)?;
|
let request = Self::find_by_token(&conn, &token)?;
|
||||||
|
|
||||||
let filter =
|
let filter =
|
||||||
password_reset_requests::table.filter(password_reset_requests::id.eq(request.id));
|
password_reset_requests::table.filter(password_reset_requests::id.eq(request.id));
|
||||||
@ -89,7 +89,7 @@ mod tests {
|
|||||||
let request = PasswordResetRequest::find_by_token(&conn, &token)
|
let request = PasswordResetRequest::find_by_token(&conn, &token)
|
||||||
.expect("couldn't retrieve request");
|
.expect("couldn't retrieve request");
|
||||||
|
|
||||||
assert!(token.len() > 32);
|
assert!(&token.len() > &32);
|
||||||
assert_eq!(&request.email, &admin_email);
|
assert_eq!(&request.email, &admin_email);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -103,8 +103,8 @@ mod tests {
|
|||||||
user_tests::fill_database(&conn);
|
user_tests::fill_database(&conn);
|
||||||
let admin_email = "admin@example.com";
|
let admin_email = "admin@example.com";
|
||||||
|
|
||||||
PasswordResetRequest::insert(&conn, admin_email).expect("couldn't insert new request");
|
PasswordResetRequest::insert(&conn, &admin_email).expect("couldn't insert new request");
|
||||||
PasswordResetRequest::insert(&conn, admin_email)
|
PasswordResetRequest::insert(&conn, &admin_email)
|
||||||
.expect("couldn't insert second request");
|
.expect("couldn't insert second request");
|
||||||
|
|
||||||
let count = password_reset_requests::table.count().get_result(&*conn);
|
let count = password_reset_requests::table.count().get_result(&*conn);
|
||||||
@ -132,7 +132,7 @@ mod tests {
|
|||||||
.execute(&*conn)
|
.execute(&*conn)
|
||||||
.expect("could not insert request");
|
.expect("could not insert request");
|
||||||
|
|
||||||
match PasswordResetRequest::find_by_token(&conn, token) {
|
match PasswordResetRequest::find_by_token(&conn, &token) {
|
||||||
Err(Error::Expired) => (),
|
Err(Error::Expired) => (),
|
||||||
_ => panic!("Received unexpected result finding expired token"),
|
_ => panic!("Received unexpected result finding expired token"),
|
||||||
}
|
}
|
||||||
@ -148,7 +148,7 @@ mod tests {
|
|||||||
user_tests::fill_database(&conn);
|
user_tests::fill_database(&conn);
|
||||||
let admin_email = "admin@example.com";
|
let admin_email = "admin@example.com";
|
||||||
|
|
||||||
let token = PasswordResetRequest::insert(&conn, admin_email)
|
let token = PasswordResetRequest::insert(&conn, &admin_email)
|
||||||
.expect("couldn't insert new request");
|
.expect("couldn't insert new request");
|
||||||
PasswordResetRequest::find_and_delete_by_token(&conn, &token)
|
PasswordResetRequest::find_and_delete_by_token(&conn, &token)
|
||||||
.expect("couldn't find and delete request");
|
.expect("couldn't find and delete request");
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,137 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
db_conn::{DbConn, DbPool},
|
|
||||||
follows,
|
|
||||||
posts::Post,
|
|
||||||
users::{User, UserEvent},
|
|
||||||
ACTOR_SYS, CONFIG, USER_CHAN,
|
|
||||||
};
|
|
||||||
use activitystreams::{
|
|
||||||
activity::{ActorAndObjectRef, Create},
|
|
||||||
base::AnyBase,
|
|
||||||
object::kind::ArticleType,
|
|
||||||
};
|
|
||||||
use plume_common::activity_pub::{inbox::FromId, LicensedArticle};
|
|
||||||
use riker::actors::{Actor, ActorFactoryArgs, ActorRefFactory, Context, Sender, Subscribe, Tell};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tracing::{error, info, warn};
|
|
||||||
|
|
||||||
pub struct RemoteFetchActor {
|
|
||||||
conn: DbPool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RemoteFetchActor {
|
|
||||||
pub fn init(conn: DbPool) {
|
|
||||||
let actor = ACTOR_SYS
|
|
||||||
.actor_of_args::<RemoteFetchActor, _>("remote-fetch", conn)
|
|
||||||
.expect("Failed to initialize remote fetch actor");
|
|
||||||
|
|
||||||
USER_CHAN.tell(
|
|
||||||
Subscribe {
|
|
||||||
actor: Box::new(actor),
|
|
||||||
topic: "*".into(),
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Actor for RemoteFetchActor {
|
|
||||||
type Msg = UserEvent;
|
|
||||||
|
|
||||||
fn recv(&mut self, _ctx: &Context<Self::Msg>, msg: Self::Msg, _sender: Sender) {
|
|
||||||
use UserEvent::*;
|
|
||||||
|
|
||||||
match msg {
|
|
||||||
RemoteUserFound(user) => match self.conn.get() {
|
|
||||||
Ok(conn) => {
|
|
||||||
let conn = DbConn(conn);
|
|
||||||
if user
|
|
||||||
.get_instance(&conn)
|
|
||||||
.map_or(false, |instance| instance.blocked)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 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.object_field_ref().as_single_base().map(|base| {
|
|
||||||
let any_base = AnyBase::from_base(base.clone()); // FIXME: Don't clone()
|
|
||||||
any_base.extend::<LicensedArticle, ArticleType>()
|
|
||||||
}) {
|
|
||||||
Some(Ok(Some(article))) => {
|
|
||||||
Post::from_activity(conn, article)
|
|
||||||
.expect("Article from remote user couldn't be saved");
|
|
||||||
info!("Fetched article from remote user");
|
|
||||||
}
|
|
||||||
Some(Err(e)) => warn!("Error while fetching articles in background: {:?}", e),
|
|
||||||
_ => warn!("Error while fetching articles in background"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +1,13 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
instance::Instance, 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 activitystreams::{
|
|
||||||
activity::{ActorAndObjectRef, Announce, Undo},
|
|
||||||
base::AnyBase,
|
|
||||||
iri_string::types::IriString,
|
|
||||||
prelude::*,
|
|
||||||
};
|
};
|
||||||
|
use activitypub::activity::{Announce, Undo};
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
use plume_common::activity_pub::{
|
use plume_common::activity_pub::{
|
||||||
inbox::{AsActor, AsObject, FromId},
|
inbox::{AsActor, AsObject, FromId},
|
||||||
sign::Signer,
|
Id, IntoId, PUBLIC_VISIBILITY,
|
||||||
PUBLIC_VISIBILITY,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Identifiable)]
|
#[derive(Clone, Queryable, Identifiable)]
|
||||||
@ -66,16 +60,16 @@ impl Reshare {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_activity(&self, conn: &Connection) -> Result<Announce> {
|
pub fn to_activity(&self, conn: &Connection) -> Result<Announce> {
|
||||||
let mut act = Announce::new(
|
let mut act = Announce::default();
|
||||||
User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?,
|
act.announce_props
|
||||||
Post::get(conn, self.post_id)?.ap_url.parse::<IriString>()?,
|
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
||||||
);
|
act.announce_props
|
||||||
act.set_id(self.ap_url.parse::<IriString>()?);
|
.set_object_link(Post::get(conn, self.post_id)?.into_id())?;
|
||||||
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
|
act.object_props.set_id_string(self.ap_url.clone())?;
|
||||||
act.set_many_ccs(vec![self
|
act.object_props
|
||||||
.get_user(conn)?
|
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
|
||||||
.followers_endpoint
|
act.object_props
|
||||||
.parse::<IriString>()?]);
|
.set_cc_link_vec(vec![Id::new(self.get_user(conn)?.followers_endpoint)])?;
|
||||||
|
|
||||||
Ok(act)
|
Ok(act)
|
||||||
}
|
}
|
||||||
@ -98,27 +92,27 @@ impl Reshare {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_undo(&self, conn: &Connection) -> Result<Undo> {
|
pub fn build_undo(&self, conn: &Connection) -> Result<Undo> {
|
||||||
let mut act = Undo::new(
|
let mut act = Undo::default();
|
||||||
User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?,
|
act.undo_props
|
||||||
AnyBase::from_extended(self.to_activity(conn)?)?,
|
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
||||||
);
|
act.undo_props.set_object_object(self.to_activity(conn)?)?;
|
||||||
act.set_id(format!("{}#delete", self.ap_url).parse::<IriString>()?);
|
act.object_props
|
||||||
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
|
.set_id_string(format!("{}#delete", self.ap_url))?;
|
||||||
act.set_many_ccs(vec![self
|
act.object_props
|
||||||
.get_user(conn)?
|
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
|
||||||
.followers_endpoint
|
act.object_props
|
||||||
.parse::<IriString>()?]);
|
.set_cc_link_vec(vec![Id::new(self.get_user(conn)?.followers_endpoint)])?;
|
||||||
|
|
||||||
Ok(act)
|
Ok(act)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Announce, &Connection> 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: &Connection, 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 {
|
||||||
@ -129,70 +123,48 @@ impl AsObject<User, Announce, &Connection> 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<Connection> for Reshare {
|
impl FromId<PlumeRocket> for Reshare {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = Announce;
|
type Object = Announce;
|
||||||
|
|
||||||
fn from_db(conn: &Connection, 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: &Connection, 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.object_field_ref()
|
.id,
|
||||||
.as_single_id()
|
user_id: User::from_id(c, &act.announce_props.actor_link::<Id>()?, None)
|
||||||
.ok_or(Error::MissingApProperty)?
|
.map_err(|(_, e)| e)?
|
||||||
.as_str(),
|
.id,
|
||||||
None,
|
ap_url: act.object_props.id_string()?,
|
||||||
CONFIG.proxy(),
|
|
||||||
)
|
|
||||||
.map_err(|(_, e)| e)?
|
|
||||||
.id,
|
|
||||||
user_id: User::from_id(
|
|
||||||
conn,
|
|
||||||
act.actor_field_ref()
|
|
||||||
.as_single_id()
|
|
||||||
.ok_or(Error::MissingApProperty)?
|
|
||||||
.as_str(),
|
|
||||||
None,
|
|
||||||
CONFIG.proxy(),
|
|
||||||
)
|
|
||||||
.map_err(|(_, e)| e)?
|
|
||||||
.id,
|
|
||||||
ap_url: act
|
|
||||||
.id_unchecked()
|
|
||||||
.ok_or(Error::MissingApProperty)?
|
|
||||||
.to_string(),
|
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
res.notify(conn)?;
|
res.notify(&c.conn)?;
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_sender() -> &'static dyn Signer {
|
|
||||||
Instance::get_local_instance_user().expect("Failed to local instance user")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Undo, &Connection> for Reshare {
|
impl AsObject<User, Undo, &PlumeRocket> for Reshare {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
|
||||||
fn activity(self, conn: &Connection, 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(¬if).execute(conn)?;
|
diesel::delete(¬if).execute(conn)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,7 +177,7 @@ impl AsObject<User, Undo, &Connection> for Reshare {
|
|||||||
|
|
||||||
impl NewReshare {
|
impl NewReshare {
|
||||||
pub fn new(p: &Post, u: &User) -> Self {
|
pub fn new(p: &Post, u: &User) -> Self {
|
||||||
let ap_url = format!("{}reshare/{}", u.ap_url, p.ap_url);
|
let ap_url = format!("{}/reshare/{}", u.ap_url, p.ap_url);
|
||||||
NewReshare {
|
NewReshare {
|
||||||
post_id: p.id,
|
post_id: p.id,
|
||||||
user_id: u.id,
|
user_id: u.id,
|
||||||
@ -213,67 +185,3 @@ impl NewReshare {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
use crate::diesel::Connection;
|
|
||||||
use crate::{inbox::tests::fill_database, tests::db};
|
|
||||||
use assert_json_diff::assert_json_eq;
|
|
||||||
use serde_json::{json, to_value};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn to_activity() {
|
|
||||||
let conn = db();
|
|
||||||
conn.test_transaction::<_, Error, _>(|| {
|
|
||||||
let (posts, _users, _blogs) = fill_database(&conn);
|
|
||||||
let post = &posts[0];
|
|
||||||
let user = &post.get_authors(&conn)?[0];
|
|
||||||
let reshare = Reshare::insert(&conn, NewReshare::new(post, user))?;
|
|
||||||
let act = reshare.to_activity(&conn).unwrap();
|
|
||||||
|
|
||||||
let expected = json!({
|
|
||||||
"actor": "https://plu.me/@/admin/",
|
|
||||||
"cc": ["https://plu.me/@/admin/followers"],
|
|
||||||
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing",
|
|
||||||
"object": "https://plu.me/~/BlogName/testing",
|
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
||||||
"type": "Announce",
|
|
||||||
});
|
|
||||||
assert_json_eq!(to_value(act)?, expected);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn build_undo() {
|
|
||||||
let conn = db();
|
|
||||||
conn.test_transaction::<_, Error, _>(|| {
|
|
||||||
let (posts, _users, _blogs) = fill_database(&conn);
|
|
||||||
let post = &posts[0];
|
|
||||||
let user = &post.get_authors(&conn)?[0];
|
|
||||||
let reshare = Reshare::insert(&conn, NewReshare::new(post, user))?;
|
|
||||||
let act = reshare.build_undo(&conn)?;
|
|
||||||
|
|
||||||
let expected = json!({
|
|
||||||
"actor": "https://plu.me/@/admin/",
|
|
||||||
"cc": ["https://plu.me/@/admin/followers"],
|
|
||||||
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing#delete",
|
|
||||||
"object": {
|
|
||||||
"actor": "https://plu.me/@/admin/",
|
|
||||||
"cc": ["https://plu.me/@/admin/followers"],
|
|
||||||
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing",
|
|
||||||
"object": "https://plu.me/~/BlogName/testing",
|
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
||||||
"type": "Announce"
|
|
||||||
},
|
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
||||||
"type": "Undo",
|
|
||||||
});
|
|
||||||
assert_json_eq!(to_value(act)?, expected);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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 #
|
||||||
@ -93,7 +93,7 @@ fn url_add_prefix(url: &str) -> Option<Cow<'_, str>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, AsExpression, FromSqlRow, Default)]
|
#[derive(Debug, Clone, PartialEq, AsExpression, FromSqlRow, Default)]
|
||||||
#[sql_type = "Text"]
|
#[sql_type = "Text"]
|
||||||
pub struct SafeString {
|
pub struct SafeString {
|
||||||
value: String,
|
value: String,
|
||||||
@ -102,7 +102,7 @@ pub struct SafeString {
|
|||||||
impl SafeString {
|
impl SafeString {
|
||||||
pub fn new(value: &str) -> Self {
|
pub fn new(value: &str) -> Self {
|
||||||
SafeString {
|
SafeString {
|
||||||
value: CLEAN.clean(value).to_string(),
|
value: CLEAN.clean(&value).to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,7 +156,7 @@ impl<'de> Deserialize<'de> for SafeString {
|
|||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
{
|
{
|
||||||
deserializer.deserialize_string(SafeStringVisitor)
|
Ok(deserializer.deserialize_string(SafeStringVisitor)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -73,26 +73,16 @@ table! {
|
|||||||
user_id -> Int4,
|
user_id -> Int4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
email_blocklist (id) {
|
email_blocklist(id){
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
email_address -> Text,
|
email_address -> VarChar,
|
||||||
note -> Text,
|
note -> Text,
|
||||||
notify_user -> Bool,
|
notify_user -> Bool,
|
||||||
notification_text -> Text,
|
notification_text -> Text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
|
||||||
email_signups (id) {
|
|
||||||
id -> Int4,
|
|
||||||
email -> Varchar,
|
|
||||||
token -> Varchar,
|
|
||||||
expiration_date -> Timestamp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
follows (id) {
|
follows (id) {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
@ -316,8 +306,6 @@ allow_tables_to_appear_in_same_query!(
|
|||||||
blogs,
|
blogs,
|
||||||
comments,
|
comments,
|
||||||
comment_seers,
|
comment_seers,
|
||||||
email_blocklist,
|
|
||||||
email_signups,
|
|
||||||
follows,
|
follows,
|
||||||
instances,
|
instances,
|
||||||
likes,
|
likes,
|
||||||
|
|||||||
@ -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) {
|
|
||||||
let actor = ACTOR_SYS
|
|
||||||
.actor_of_args::<SearchActor, _>("search", (searcher, conn))
|
|
||||||
.expect("Failed to initialize searcher actor");
|
|
||||||
|
|
||||||
POST_CHAN.tell(
|
|
||||||
Subscribe {
|
|
||||||
actor: Box::new(actor),
|
|
||||||
topic: "*".into(),
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Actor for SearchActor {
|
|
||||||
type Msg = PostEvent;
|
|
||||||
|
|
||||||
fn recv(&mut self, _ctx: &Context<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.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(),
|
|
||||||
open_registrations: true,
|
|
||||||
public_domain: random_hex(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let user = User::insert(
|
|
||||||
conn,
|
|
||||||
NewUser {
|
|
||||||
username: random_hex(),
|
|
||||||
display_name: random_hex(),
|
|
||||||
outbox_url: random_hex(),
|
|
||||||
inbox_url: random_hex(),
|
|
||||||
summary: "".to_string(),
|
|
||||||
email: None,
|
|
||||||
hashed_password: None,
|
|
||||||
instance_id: instance.id,
|
|
||||||
ap_url: random_hex(),
|
|
||||||
private_key: None,
|
|
||||||
public_key: "".to_string(),
|
|
||||||
shared_inbox_url: None,
|
|
||||||
followers_endpoint: random_hex(),
|
|
||||||
avatar_id: None,
|
|
||||||
summary_html: SafeString::new(""),
|
|
||||||
role: 0,
|
|
||||||
fqn: random_hex(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let blog = NewBlog {
|
|
||||||
instance_id: instance.id,
|
|
||||||
actor_id: random_hex(),
|
|
||||||
ap_url: random_hex(),
|
|
||||||
inbox_url: random_hex(),
|
|
||||||
outbox_url: random_hex(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user