Compare commits
211 Commits
0.7.2
...
tables-style
| Author | SHA1 | Date | |
|---|---|---|---|
| bf1c593cd1 | |||
| 09d1a414aa | |||
| 1a0d30762d | |||
| f568fd35b7 | |||
| 5e320d2782 | |||
| d06b08786c | |||
| 6940574a3e | |||
| d3e09ce8ac | |||
| e4e996f3a7 | |||
| f5b1b6f1c7 | |||
| 5d2e653a3b | |||
| ebc9fbc88a | |||
| db8cc6e7e8 | |||
| 2d888d6aa5 | |||
| 78b290a0dc | |||
| 59e1123d35 | |||
| 56f6e46c71 | |||
| 304fb740d8 | |||
| 61e65a55ad | |||
| 3f93212424 | |||
| 20d77c22df | |||
| 24d3b289da | |||
| 20fa2cacf4 | |||
| 4e67eb8317 | |||
| 24c008b0de | |||
| 1cb9459a23 | |||
| 10e06737cf | |||
| 30a3cec87e | |||
| 54af93d8ff | |||
| 9425b44d08 | |||
| 487f296db5 | |||
| 8bdd481e0d | |||
| 19f18421bc | |||
| e1777e9071 | |||
| 613ccbcd94 | |||
| b9a09a2511 | |||
| 213628e400 | |||
| d6bb2bfb72 | |||
| 33bd290679 | |||
| 85ab5393fd | |||
| 98c73bb6df | |||
| 3e9d9a459f | |||
| a394c3f210 | |||
| a1a19e091a | |||
| ec030d500d | |||
| cfa74f84e7 | |||
| 97cbe7f446 | |||
| 7e4d081027 | |||
| 1e5ae92135 | |||
| 036ee6fac4 | |||
| 6028295748 | |||
| aa4cfd374d | |||
| 3303a4af84 | |||
| 37a136787b | |||
| 300ff37694 | |||
| c1d9d39dc1 | |||
| 93d6ee04d4 | |||
| ae7bf2e132 | |||
| 0020242571 | |||
| 4f796e788c | |||
| 3d192c1179 | |||
| 2f8d188d59 | |||
| 19766662f1 | |||
| 301aad3f73 | |||
| 92a8f8aa4c | |||
| 0c856a5252 | |||
| 2df6138ff1 | |||
| b2942f3f47 | |||
| 94f20c8fc2 | |||
| 5d48b93c8b | |||
| bbf2e00920 | |||
| c97361f5f4 | |||
| 7c799e8abf | |||
| d196e1dbd0 | |||
| 1679315322 | |||
| dd3a5f4a5b | |||
| 3580fb04fa | |||
| 699fdc30d9 | |||
| 704e9aa47f | |||
| d741238ccb | |||
| 9776374d17 | |||
| 2d10ddb9fa | |||
| e746a0b03f | |||
| 85cacf4239 | |||
| f138ae6ed9 | |||
| 399af4004a | |||
| d36f13e984 | |||
| 9a3699160d | |||
| 4103e7513d | |||
| ed9970b102 | |||
| afa875366e | |||
| 9696f04c64 | |||
| 40e1a1fc2c | |||
| ee1e553460 | |||
| d20ce6dd0b | |||
| 72f7909a42 | |||
| 4e1fb64868 | |||
| 85c1bfa300 | |||
| 172c78c41d | |||
| 3b08d5b485 | |||
| 832479a706 | |||
| 3b3148fa6b | |||
| b38d55f486 | |||
| 2804a490ed | |||
| 8c098def61 | |||
| e10ddb50c0 | |||
| 4df2c3e6f6 | |||
| 2f53fc78b6 | |||
| fded87654d | |||
| 08cd777f81 | |||
| 96b88353c5 | |||
| 302026feb9 | |||
| ba6d322da7 | |||
| 488563e9c1 | |||
| 130bb4c102 | |||
| 9368aebe70 | |||
| ca2843822e | |||
| bd91b4a346 | |||
| 35b951967d | |||
| 63d2cf91e9 | |||
| 263cf9e04f | |||
| 22ebecba67 | |||
| 903b48ed12 | |||
| a550291c85 | |||
| 47394fc620 | |||
| b180089b1b | |||
| a275aa5965 | |||
| 87edb2486c | |||
| 10617f3144 | |||
| 6654ad28b7 | |||
| 771d4325c2 | |||
| 1536a6d3f3 | |||
| 620726cc25 | |||
| 0eef7c0b89 | |||
| 321e40ea3f | |||
| a218b4ea4f | |||
| 9613ccd0c3 | |||
| 9493c1ad06 | |||
| e92ac1a13f | |||
| 1517b4d91e | |||
| 38cc4c043d | |||
| 05c1d727dc | |||
| 84645c7ed9 | |||
| 7c505bde7f | |||
| 9f543f1b6b | |||
| 0f7b882749 | |||
| f9f4375a40 | |||
| 12c2848cc7 | |||
| 4cfb3e2494 | |||
| 090b0a6f0d | |||
| 4502b77094 | |||
| 8f5a86206a | |||
| 340157f80d | |||
| 16b10695df | |||
| b8eb631aa3 | |||
| 5c9094fede | |||
| 4e2ca515ce | |||
| eccfbd3fbc | |||
| 8408342b5d | |||
| c47921bb25 | |||
| 03f470f04c | |||
| 5770c3b85b | |||
| c92f46b2c9 | |||
| 69eba69528 | |||
| c302d842e0 | |||
| f660220495 | |||
| 155df7bdf0 | |||
| 29055d1957 | |||
| d6ee49b880 | |||
| 248ed265c4 | |||
| a1f958ee7a | |||
| abf352b957 | |||
| 393f8e5e0c | |||
| 4dfe300ee3 | |||
| 65829094c9 | |||
| d5c3e6d6f0 | |||
| d702dd2fae | |||
| 0d855823c9 | |||
| 2dd33769d4 | |||
| 4ea9f6ecf1 | |||
| 3b0b6c4b0b | |||
| 4328fad5a3 | |||
| b26822c045 | |||
| f372282b04 | |||
| 145253ccbf | |||
| 485223a3dd | |||
| 7f75fa74e7 | |||
| 821fce1903 | |||
| ce484de61e | |||
| 35fb57718d | |||
| 35d12d7cae | |||
| 846154efe1 | |||
| 1ec7acbdfe | |||
| e384fdfcff | |||
| ed58e44d2e | |||
| f151dee339 | |||
| 61f25941e8 | |||
| 0628a14be6 | |||
| b46ae83377 | |||
| 70bc7f8edf | |||
| aff481b947 | |||
| 29ef73d307 | |||
| db205d0d9d | |||
| 57ab7edf23 | |||
| e4bd9d65cf | |||
| 17e4ddb32d | |||
| 4fd85b30f1 | |||
| 6148f29c66 | |||
| 1a3fad2d6a | |||
| 0945d3bc53 | |||
| 7f63d2a129 |
@@ -10,7 +10,7 @@ executors:
|
|||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
docker:
|
docker:
|
||||||
- image: plumeorg/plume-buildenv:v0.4.0
|
- image: plumeorg/plume-buildenv:v0.8.0
|
||||||
- image: <<#parameters.postgres>>cimg/postgres:14.2<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
|
- image: <<#parameters.postgres>>cimg/postgres:14.2<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
@@ -38,7 +38,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" }}-master
|
- v0-<< parameters.cache >>-{{ checksum "Cargo.lock" }}-main
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
description: push cache
|
description: push cache
|
||||||
@@ -63,6 +63,7 @@ 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:
|
||||||
@@ -111,6 +112,7 @@ 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:
|
||||||
@@ -258,4 +260,4 @@ workflows:
|
|||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
- /^master/
|
- /^main/
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM debian:buster-20210208
|
FROM rust:1
|
||||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||||
|
|
||||||
#install native/circleci/build dependancies
|
#install native/circleci/build dependancies
|
||||||
@@ -6,19 +6,19 @@ 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/ /" \
|
echo "deb [trusted=yes] https://apt.fury.io/caddy/ /" \
|
||||||
| tee -a /etc/apt/sources.list.d/caddy-fury.list &&\
|
| 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 update &&\
|
||||||
apt install -y --no-install-recommends binutils-dev build-essential cmake curl gcc gettext git libcurl4-openssl-dev libdw-dev libelf-dev libiberty-dev libpq-dev libsqlite3-dev libssl-dev make openssl pkg-config postgresql postgresql-contrib python zlib1g-dev python3-pip zip unzip libclang-dev clang caddy&&\
|
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/*
|
||||||
|
|
||||||
#install and configure rust
|
#stick rust environment
|
||||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2021-11-27 -y &&\
|
COPY rust-toolchain ./
|
||||||
rustup component add rustfmt clippy &&\
|
RUN 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 wasm-pack &&\
|
||||||
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
|
||||||
@@ -29,11 +29,3 @@ RUN pip3 install selenium
|
|||||||
|
|
||||||
#configure caddy
|
#configure caddy
|
||||||
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
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
nightly-2022-07-19
|
||||||
@@ -3,3 +3,5 @@ data
|
|||||||
Dockerfile
|
Dockerfile
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
.env
|
.env
|
||||||
|
target
|
||||||
|
data
|
||||||
|
|||||||
@@ -11,20 +11,20 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Set up QEMU
|
name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v2
|
||||||
-
|
-
|
||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
-
|
-
|
||||||
name: Login to DockerHub
|
name: Login to DockerHub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
-
|
-
|
||||||
name: Build and push
|
name: Build and push
|
||||||
id: docker_build
|
id: docker_build
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
tags: plumeorg/plume:latest
|
tags: plumeorg/plume:latest
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Set up QEMU
|
name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v2
|
||||||
-
|
-
|
||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
-
|
-
|
||||||
name: Docker meta
|
name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
@@ -23,14 +23,14 @@ jobs:
|
|||||||
images: plumeorg/plume
|
images: plumeorg/plume
|
||||||
-
|
-
|
||||||
name: Login to DockerHub
|
name: Login to DockerHub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
-
|
-
|
||||||
name: Build and push
|
name: Build and push
|
||||||
id: docker_build
|
id: docker_build
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
|||||||
@@ -20,3 +20,5 @@ search_index
|
|||||||
__pycache__
|
__pycache__
|
||||||
.vscode/
|
.vscode/
|
||||||
*-journal
|
*-journal
|
||||||
|
.direnv/
|
||||||
|
build.log*
|
||||||
|
|||||||
@@ -4,6 +4,39 @@
|
|||||||
|
|
||||||
## [Unreleased] - ReleaseDate
|
## [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
|
## [[0.7.2]] - 2022-05-11
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
Generated
+1862
-1147
File diff suppressed because it is too large
Load Diff
+20
-18
@@ -1,33 +1,34 @@
|
|||||||
[package]
|
[package]
|
||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
name = "plume"
|
name = "plume"
|
||||||
version = "0.7.2"
|
version = "0.7.3-dev-fork"
|
||||||
repository = "https://github.com/Plume-org/Plume"
|
repository = "https://git.lainoa.eus/aitzol/Plume"
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
atom_syndication = "0.11.0"
|
atom_syndication = "0.12.0"
|
||||||
clap = "2.33"
|
clap = "2.33"
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
gettext = "0.4.0"
|
gettext = "0.4.0"
|
||||||
gettext-macros = "0.6.1"
|
gettext-macros = "0.6.1"
|
||||||
gettext-utils = "0.1.0"
|
gettext-utils = "0.1.0"
|
||||||
guid-create = "0.2"
|
guid-create = "0.4.1"
|
||||||
|
conv = "0.3.3"
|
||||||
lettre_email = "0.9.2"
|
lettre_email = "0.9.2"
|
||||||
num_cpus = "1.10"
|
num_cpus = "1.16.0"
|
||||||
rocket = "0.4.6"
|
rocket = "0.4.11"
|
||||||
rocket_contrib = { version = "0.4.5", features = ["json"] }
|
rocket_contrib = { version = "0.4.11", features = ["json"] }
|
||||||
rocket_i18n = "0.4.1"
|
rocket_i18n = "0.4.1"
|
||||||
scheduled-thread-pool = "0.2.2"
|
scheduled-thread-pool = "0.2.7"
|
||||||
serde = "1.0"
|
serde = "1.0.137"
|
||||||
serde_json = "1.0.81"
|
serde_json = "1.0.81"
|
||||||
shrinkwraprs = "0.3.0"
|
shrinkwraprs = "0.3.0"
|
||||||
validator = { version = "0.15", features = ["derive"] }
|
validator = { version = "0.15", features = ["derive"] }
|
||||||
webfinger = "0.4.1"
|
webfinger = "0.4.1"
|
||||||
tracing = "0.1.34"
|
tracing = "0.1.35"
|
||||||
tracing-subscriber = "0.3.10"
|
tracing-subscriber = "0.3.10"
|
||||||
riker = "0.4.2"
|
riker = "0.4.2"
|
||||||
activitystreams = "0.7.0-alpha.18"
|
activitystreams = "=0.7.0-alpha.20"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "plume"
|
name = "plume"
|
||||||
@@ -35,7 +36,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
version = "0.4"
|
version = "0.4.31"
|
||||||
|
|
||||||
[dependencies.ctrlc]
|
[dependencies.ctrlc]
|
||||||
features = ["termination"]
|
features = ["termination"]
|
||||||
@@ -60,20 +61,21 @@ path = "plume-common"
|
|||||||
path = "plume-models"
|
path = "plume-models"
|
||||||
|
|
||||||
[dependencies.rocket_csrf]
|
[dependencies.rocket_csrf]
|
||||||
git = "https://github.com/fdb-hiroshima/rocket_csrf"
|
git = "https://git.joinplu.me/plume/rocket_csrf"
|
||||||
rev = "29910f2829e7e590a540da3804336577b48c7b31"
|
rev = "0.1.2"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
ructe = "0.14.0"
|
ructe = "0.15.0"
|
||||||
rsass = "0.24"
|
rsass = "0.26"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["postgres"]
|
default = ["postgres", "s3"]
|
||||||
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"]
|
||||||
|
|||||||
+6
-7
@@ -1,4 +1,4 @@
|
|||||||
FROM rust:1-buster as builder
|
FROM rust:latest 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 \
|
||||||
@@ -16,24 +16,23 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
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
|
||||||
|
RUN ln -s /usr/bin/python3 /usr/bin/python & \
|
||||||
|
ln -s /usr/bin/pip3 /usr/bin/pip
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY Cargo.toml Cargo.lock rust-toolchain ./
|
|
||||||
RUN cargo install wasm-pack
|
|
||||||
|
|
||||||
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:buster-slim
|
FROM debian:stable-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,10 +1,10 @@
|
|||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
<img src="https://raw.githubusercontent.com/Plume-org/Plume/master/assets/icons/trwnh/feather/plumeFeather64.png" alt="Plume's logo">
|
<img src="https://raw.githubusercontent.com/Plume-org/Plume/main/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/master/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/main/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>
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ main article {
|
|||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
border-inline-start: 5px solid $gray;
|
border-inline-start: 5px solid $lightpurple;
|
||||||
margin: 1em auto;
|
margin: 1em auto;
|
||||||
padding: 0em 2em;
|
padding: 0em 2em;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,7 +252,7 @@ main .article-meta {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ form.new-post {
|
|||||||
|
|
||||||
.button + .button {
|
.button + .button {
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
|
margin-inline-start: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.split {
|
.split {
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ body > header {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
&.right-nav {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 0.2em;
|
width: 0.2em;
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/* tables */
|
||||||
|
table, td, th, tr {
|
||||||
|
border: 1px dotted $lightpurple;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(even){background-color: $gray;}
|
||||||
@@ -12,3 +12,4 @@
|
|||||||
@import "header";
|
@import "header";
|
||||||
@import "article";
|
@import "article";
|
||||||
@import "forms";
|
@import "forms";
|
||||||
|
@import "tables";
|
||||||
|
|||||||
@@ -12,3 +12,4 @@
|
|||||||
@import "header";
|
@import "header";
|
||||||
@import "article";
|
@import "article";
|
||||||
@import "forms";
|
@import "forms";
|
||||||
|
@import "tables";
|
||||||
|
|||||||
Generated
+116
@@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -5,5 +5,5 @@ authors = ["Plume contributors"]
|
|||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = "1.0"
|
serde = "1.0.137"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
pre-release-hook = ["cargo", "fmt"]
|
pre-release-hook = ["cargo", "fmt"]
|
||||||
pre-release-replacements = []
|
pre-release-replacements = []
|
||||||
|
release = false
|
||||||
|
|||||||
@@ -24,3 +24,4 @@ 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,2 +1,3 @@
|
|||||||
pre-release-hook = ["cargo", "fmt"]
|
pre-release-hook = ["cargo", "fmt"]
|
||||||
pre-release-replacements = []
|
pre-release-replacements = []
|
||||||
|
release = false
|
||||||
|
|||||||
@@ -0,0 +1,262 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,10 @@ 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() {
|
||||||
@@ -16,6 +18,8 @@ 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();
|
||||||
|
|
||||||
@@ -37,6 +41,10 @@ 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."))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
+11
-12
@@ -7,29 +7,28 @@ edition = "2018"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
array_tool = "1.0"
|
array_tool = "1.0"
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
heck = "0.4.0"
|
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
openssl = "0.10.40"
|
openssl = "0.10.40"
|
||||||
rocket = "0.4.6"
|
rocket = "0.4.11"
|
||||||
reqwest = { version = "0.11.10", features = ["blocking", "json", "socks"] }
|
reqwest = { version = "0.11.11", features = ["blocking", "json", "socks"] }
|
||||||
serde = "1.0"
|
serde = "1.0.137"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_json = "1.0.81"
|
serde_json = "1.0.81"
|
||||||
shrinkwraprs = "0.3.0"
|
shrinkwraprs = "0.3.0"
|
||||||
syntect = "4.5.0"
|
syntect = "4.5.0"
|
||||||
regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] }
|
regex-syntax = { version = "0.6.26", default-features = false, features = ["unicode-perl"] }
|
||||||
tracing = "0.1.34"
|
tracing = "0.1.35"
|
||||||
askama_escape = "0.10.3"
|
askama_escape = "0.10.3"
|
||||||
activitystreams = "0.7.0-alpha.18"
|
activitystreams = "=0.7.0-alpha.20"
|
||||||
activitystreams-ext = "0.1.0-alpha.2"
|
activitystreams-ext = "0.1.0-alpha.2"
|
||||||
url = "2.2.2"
|
url = "2.2.2"
|
||||||
flume = "0.10.12"
|
flume = "0.10.13"
|
||||||
tokio = { version = "1.18.2", features = ["full"] }
|
tokio = { version = "1.19.2", features = ["full"] }
|
||||||
futures = "0.3.21"
|
futures = "0.3.25"
|
||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
version = "0.4"
|
version = "0.4.31"
|
||||||
|
|
||||||
[dependencies.pulldown-cmark]
|
[dependencies.pulldown-cmark]
|
||||||
default-features = false
|
default-features = false
|
||||||
@@ -38,6 +37,6 @@ branch = "bidi-plume"
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert-json-diff = "2.0.1"
|
assert-json-diff = "2.0.1"
|
||||||
once_cell = "1.10.0"
|
once_cell = "1.12.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
pre-release-hook = ["cargo", "fmt"]
|
pre-release-hook = ["cargo", "fmt"]
|
||||||
pre-release-replacements = []
|
pre-release-replacements = []
|
||||||
|
release = false
|
||||||
|
|||||||
@@ -561,7 +561,7 @@ mod tests {
|
|||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
|
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
|
||||||
|
|
||||||
static MY_SIGNER: Lazy<MySigner> = Lazy::new(|| MySigner::new());
|
static MY_SIGNER: Lazy<MySigner> = Lazy::new(MySigner::new);
|
||||||
|
|
||||||
struct MySigner {
|
struct MySigner {
|
||||||
public_key: String,
|
public_key: String,
|
||||||
@@ -596,7 +596,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
|
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
|
||||||
verifier.update(data.as_bytes()).unwrap();
|
verifier.update(data.as_bytes()).unwrap();
|
||||||
verifier.verify(&signature).map_err(|_| SignError())
|
verifier.verify(signature).map_err(|_| SignError())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -782,7 +782,7 @@ mod tests {
|
|||||||
.done();
|
.done();
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
|
|
||||||
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
|
let res: Result<(), ()> = Inbox::handle(&(), act)
|
||||||
.with::<FailingActor, Create, MyObject>(None)
|
.with::<FailingActor, Create, MyObject>(None)
|
||||||
.with::<MyActor, Create, MyObject>(None)
|
.with::<MyActor, Create, MyObject>(None)
|
||||||
.done();
|
.done();
|
||||||
|
|||||||
@@ -518,7 +518,8 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
activity::{ActorAndObjectRef, Create},
|
activity::{ActorAndObjectRef, Create},
|
||||||
object::kind::ArticleType,
|
object::{kind::ArticleType, Image},
|
||||||
|
prelude::{ApActorExt, BaseExt, ExtendsExt, ObjectExt},
|
||||||
};
|
};
|
||||||
use assert_json_diff::assert_json_eq;
|
use assert_json_diff::assert_json_eq;
|
||||||
use serde_json::{from_str, json, to_value};
|
use serde_json::{from_str, json, to_value};
|
||||||
@@ -592,7 +593,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn de_custom_group() {
|
fn se_custom_group() {
|
||||||
let group = CustomGroup::new(
|
let group = CustomGroup::new(
|
||||||
ApActor::new("https://example.com/inbox".parse().unwrap(), Group::new()),
|
ApActor::new("https://example.com/inbox".parse().unwrap(), Group::new()),
|
||||||
ApSignature {
|
ApSignature {
|
||||||
@@ -625,6 +626,71 @@ mod tests {
|
|||||||
assert_eq!(to_value(group).unwrap(), expected);
|
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]
|
#[test]
|
||||||
fn se_licensed_article() {
|
fn se_licensed_article() {
|
||||||
let object = ApObject::new(Article::new());
|
let object = ApObject::new(Article::new());
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
|
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
|
||||||
verifier.update(data.as_bytes()).unwrap();
|
verifier.update(data.as_bytes()).unwrap();
|
||||||
verifier.verify(&signature).map_err(|_| Error())
|
verifier.verify(signature).map_err(|_| Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,7 +262,7 @@ mod tests {
|
|||||||
let signer = MySigner::new();
|
let signer = MySigner::new();
|
||||||
let headers = HeaderMap::new();
|
let headers = HeaderMap::new();
|
||||||
let result = signature(&signer, &headers, ("post", "/inbox", None)).unwrap();
|
let result = signature(&signer, &headers, ("post", "/inbox", None)).unwrap();
|
||||||
let fields: Vec<&str> = result.to_str().unwrap().split(",").collect();
|
let fields: Vec<&str> = result.to_str().unwrap().split(',').collect();
|
||||||
assert_eq!(r#"headers="(request-target)""#, fields[2]);
|
assert_eq!(r#"headers="(request-target)""#, fields[2]);
|
||||||
let sign = &fields[3][11..(fields[3].len() - 1)];
|
let sign = &fields[3][11..(fields[3].len() - 1)];
|
||||||
assert!(signer.verify("post /inbox", sign.as_bytes()).is_ok());
|
assert!(signer.verify("post /inbox", sign.as_bytes()).is_ok());
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ impl Signable for serde_json::Value {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
pub enum SignatureValidity {
|
pub enum SignatureValidity {
|
||||||
Invalid,
|
Invalid,
|
||||||
ValidNoDigest,
|
ValidNoDigest,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use heck::ToUpperCamelCase;
|
|
||||||
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, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag};
|
||||||
use regex_syntax::is_word_character;
|
use regex_syntax::is_word_character;
|
||||||
@@ -16,14 +15,6 @@ 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
|
|
||||||
pub fn make_actor_id(name: &str) -> String {
|
|
||||||
name.to_upper_camel_case()
|
|
||||||
.chars()
|
|
||||||
.filter(|c| c.is_alphanumeric())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Percent-encode characters which are not allowed in IRI path segments.
|
* Percent-encode characters which are not allowed in IRI path segments.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
name = "plume-front"
|
name = "plume-front"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
|
|
||||||
|
[package.metadata.wasm-pack.profile.release]
|
||||||
|
wasm-opt = false
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
@@ -12,15 +15,15 @@ gettext = "0.4.0"
|
|||||||
gettext-macros = "0.6.1"
|
gettext-macros = "0.6.1"
|
||||||
gettext-utils = "0.1.0"
|
gettext-utils = "0.1.0"
|
||||||
lazy_static = "1.3"
|
lazy_static = "1.3"
|
||||||
serde = "1.0"
|
serde = "1.0.137"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
wasm-bindgen = "0.2.80"
|
wasm-bindgen = "0.2.99"
|
||||||
js-sys = "0.3.57"
|
js-sys = "0.3.76"
|
||||||
serde_derive = "1.0.123"
|
serde_derive = "1.0.123"
|
||||||
console_error_panic_hook = "0.1.6"
|
console_error_panic_hook = "0.1.6"
|
||||||
|
|
||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
version = "0.3.57"
|
version = "0.3.76"
|
||||||
features = [
|
features = [
|
||||||
'console',
|
'console',
|
||||||
'ClipboardEvent',
|
'ClipboardEvent',
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
pre-release-hook = ["cargo", "fmt"]
|
pre-release-hook = ["cargo", "fmt"]
|
||||||
pre-release-replacements = []
|
pre-release-replacements = []
|
||||||
|
release = false
|
||||||
|
|||||||
@@ -168,10 +168,14 @@ fn load_autosave() {
|
|||||||
.get(&get_autosave_id())
|
.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 {}?";
|
||||||
Date::new(&JsValue::from_f64(autosave_info.last_saved)).to_date_string().as_string().unwrap()
|
// next line shows 'unexpected token' error on docker image building
|
||||||
|
//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 let Ok(true) = window().unwrap().confirm_with_message(&message) {
|
||||||
set_value("editor-content", &autosave_info.contents);
|
set_value("editor-content", &autosave_info.contents);
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
pre-release-hook = ["cargo", "fmt"]
|
pre-release-hook = ["cargo", "fmt"]
|
||||||
pre-release-replacements = []
|
pre-release-replacements = []
|
||||||
|
release = false
|
||||||
|
|||||||
+17
-13
@@ -7,38 +7,41 @@ edition = "2018"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
ammonia = "3.2.0"
|
ammonia = "3.2.0"
|
||||||
bcrypt = "0.12.1"
|
bcrypt = "0.12.1"
|
||||||
guid-create = "0.2"
|
guid-create = "0.3.0"
|
||||||
itertools = "0.10.3"
|
itertools = "0.10.3"
|
||||||
lazy_static = "1.0"
|
lazy_static = "1.0"
|
||||||
ldap3 = "0.10.4"
|
ldap3 = "0.11.1"
|
||||||
migrations_internals= "1.4.0"
|
migrations_internals= "1.4.0"
|
||||||
openssl = "0.10.40"
|
openssl = "0.10.40"
|
||||||
rocket = "0.4.6"
|
rocket = "0.4.11"
|
||||||
rocket_i18n = "0.4.1"
|
rocket_i18n = "0.4.1"
|
||||||
reqwest = "0.11.10"
|
reqwest = "0.11.11"
|
||||||
scheduled-thread-pool = "0.2.2"
|
scheduled-thread-pool = "0.2.7"
|
||||||
serde = "1.0"
|
serde = "1.0.137"
|
||||||
|
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.81"
|
||||||
tantivy = "0.13.3"
|
tantivy = "0.13.3"
|
||||||
url = "2.1"
|
url = "2.1"
|
||||||
walkdir = "2.2"
|
walkdir = "2.2"
|
||||||
webfinger = "0.4.1"
|
webfinger = "0.4.1"
|
||||||
whatlang = "0.16.0"
|
whatlang = "0.16.2"
|
||||||
shrinkwraprs = "0.3.0"
|
shrinkwraprs = "0.3.0"
|
||||||
diesel-derive-newtype = "0.1.2"
|
diesel-derive-newtype = "1.0.0"
|
||||||
glob = "0.3.0"
|
glob = "0.3.1"
|
||||||
lindera-tantivy = { version = "0.7.1", optional = true }
|
lindera-tantivy = { version = "0.7.1", optional = true }
|
||||||
tracing = "0.1.34"
|
tracing = "0.1.35"
|
||||||
riker = "0.4.2"
|
riker = "0.4.2"
|
||||||
once_cell = "1.10.0"
|
once_cell = "1.12.0"
|
||||||
lettre = "0.9.6"
|
lettre = "0.9.6"
|
||||||
native-tls = "0.2.10"
|
native-tls = "0.2.10"
|
||||||
activitystreams = "0.7.0-alpha.18"
|
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"
|
version = "0.4.31"
|
||||||
|
|
||||||
[dependencies.diesel]
|
[dependencies.diesel]
|
||||||
features = ["r2d2", "chrono"]
|
features = ["r2d2", "chrono"]
|
||||||
@@ -61,3 +64,4 @@ diesel_migrations = "1.3.0"
|
|||||||
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,2 +1,3 @@
|
|||||||
pre-release-hook = ["cargo", "fmt"]
|
pre-release-hook = ["cargo", "fmt"]
|
||||||
pre-release-replacements = []
|
pre-release-replacements = []
|
||||||
|
release = false
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use rocket::{
|
|||||||
Outcome,
|
Outcome,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Wrapper around User to use as a request guard on pages reserved to admins.
|
/// Wrapper around User to use as a request guard on pages exclusively 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,6 +21,23 @@ 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);
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApiToken {
|
|||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,12 +126,9 @@ pub(crate) mod tests {
|
|||||||
.id,
|
.id,
|
||||||
various[1].id
|
various[1].id
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert!(BlocklistedEmail::matches_blocklist(&conn, no_match)
|
||||||
BlocklistedEmail::matches_blocklist(&conn, no_match)
|
.unwrap()
|
||||||
.unwrap()
|
.is_none());
|
||||||
.is_none(),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+83
-61
@@ -1,6 +1,7 @@
|
|||||||
|
use heck::ToUpperCamelCase;
|
||||||
use crate::{
|
use crate::{
|
||||||
db_conn::DbConn, instance::*, medias::Media, posts::Post, safe_string::SafeString,
|
instance::*, medias::Media, posts::Post, safe_string::SafeString, schema::blogs, users::User,
|
||||||
schema::blogs, users::User, Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE,
|
Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE,
|
||||||
};
|
};
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
actor::{ApActor, ApActorExt, AsApActor, Group},
|
actor::{ApActor, ApActorExt, AsApActor, Group},
|
||||||
@@ -18,10 +19,13 @@ use openssl::{
|
|||||||
rsa::Rsa,
|
rsa::Rsa,
|
||||||
sign::{Signer, Verifier},
|
sign::{Signer, Verifier},
|
||||||
};
|
};
|
||||||
use plume_common::activity_pub::{
|
use plume_common::{
|
||||||
inbox::{AsActor, FromId},
|
activity_pub::{
|
||||||
sign, ActivityStream, ApSignature, CustomGroup, Id, IntoId, PublicKey, Source, SourceProperty,
|
inbox::{AsActor, FromId},
|
||||||
ToAsString, ToAsUri,
|
sign, ActivityStream, ApSignature, CustomGroup, Id, IntoId, PublicKey, Source,
|
||||||
|
SourceProperty, ToAsString, ToAsUri,
|
||||||
|
},
|
||||||
|
utils::iri_percent_encode_seg,
|
||||||
};
|
};
|
||||||
use webfinger::*;
|
use webfinger::*;
|
||||||
|
|
||||||
@@ -83,9 +87,13 @@ impl Blog {
|
|||||||
|
|
||||||
if inserted.fqn.is_empty() {
|
if inserted.fqn.is_empty() {
|
||||||
if instance.local {
|
if instance.local {
|
||||||
inserted.fqn = inserted.actor_id.clone();
|
inserted.fqn = iri_percent_encode_seg(&inserted.actor_id);
|
||||||
} else {
|
} else {
|
||||||
inserted.fqn = format!("{}@{}", inserted.actor_id, instance.public_domain);
|
inserted.fqn = format!(
|
||||||
|
"{}@{}",
|
||||||
|
iri_percent_encode_seg(&inserted.actor_id),
|
||||||
|
instance.public_domain
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +103,19 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -131,10 +152,10 @@ impl Blog {
|
|||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_fqn(conn: &DbConn, fqn: &str) -> Result<Blog> {
|
pub fn find_by_fqn(conn: &Connection, 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(conn)
|
||||||
.optional()?;
|
.optional()?;
|
||||||
if let Some(from_db) = from_db {
|
if let Some(from_db) = from_db {
|
||||||
Ok(from_db)
|
Ok(from_db)
|
||||||
@@ -143,7 +164,7 @@ impl Blog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result<Blog> {
|
fn fetch_from_webfinger(conn: &Connection, 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()
|
||||||
@@ -162,7 +183,7 @@ impl Blog {
|
|||||||
|
|
||||||
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 = ApActor::new(self.inbox_url.parse()?, Group::new());
|
||||||
blog.set_preferred_username(self.actor_id.clone());
|
blog.set_preferred_username(iri_percent_encode_seg(&self.actor_id));
|
||||||
blog.set_name(self.title.clone());
|
blog.set_name(self.title.clone());
|
||||||
blog.set_outbox(self.outbox_url.parse()?);
|
blog.set_outbox(self.outbox_url.parse()?);
|
||||||
blog.set_summary(self.summary_html.to_string());
|
blog.set_summary(self.summary_html.to_string());
|
||||||
@@ -361,15 +382,15 @@ impl IntoId for Blog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromId<DbConn> for Blog {
|
impl FromId<Connection> for Blog {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = CustomGroup;
|
type Object = CustomGroup;
|
||||||
|
|
||||||
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
fn from_db(conn: &Connection, id: &str) -> Result<Self> {
|
||||||
Self::find_by_ap_url(conn, id)
|
Self::find_by_ap_url(conn, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_activity(conn: &DbConn, acct: CustomGroup) -> Result<Self> {
|
fn from_activity(conn: &Connection, acct: CustomGroup) -> Result<Self> {
|
||||||
let (name, outbox_url, inbox_url) = {
|
let (name, outbox_url, inbox_url) = {
|
||||||
let actor = acct.ap_actor_ref();
|
let actor = acct.ap_actor_ref();
|
||||||
let name = actor
|
let name = actor
|
||||||
@@ -377,6 +398,7 @@ impl FromId<DbConn> for Blog {
|
|||||||
.ok_or(Error::MissingApProperty)?
|
.ok_or(Error::MissingApProperty)?
|
||||||
.to_string();
|
.to_string();
|
||||||
if name.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
|
if name.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
|
||||||
|
tracing::error!("preferredUsername includes invalid character(s): {}", &name);
|
||||||
return Err(Error::InvalidValue);
|
return Err(Error::InvalidValue);
|
||||||
}
|
}
|
||||||
(
|
(
|
||||||
@@ -656,7 +678,7 @@ pub(crate) mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.id,
|
.id,
|
||||||
);
|
);
|
||||||
let _: Blog = blog1.save_changes(&*conn).unwrap();
|
let _: Blog = blog1.save_changes(conn).unwrap();
|
||||||
|
|
||||||
(users, vec![blog1, blog2, blog3])
|
(users, vec![blog1, blog2, blog3])
|
||||||
}
|
}
|
||||||
@@ -665,10 +687,10 @@ pub(crate) mod tests {
|
|||||||
fn get_instance() {
|
fn get_instance() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
fill_database(&conn);
|
fill_database(conn);
|
||||||
|
|
||||||
let blog = Blog::insert(
|
let blog = Blog::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"SomeName".to_owned(),
|
"SomeName".to_owned(),
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
@@ -680,7 +702,7 @@ pub(crate) mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
blog.get_instance(&conn).unwrap().id,
|
blog.get_instance(conn).unwrap().id,
|
||||||
Instance::get_local().unwrap().id
|
Instance::get_local().unwrap().id
|
||||||
);
|
);
|
||||||
// TODO add tests for remote instance
|
// TODO add tests for remote instance
|
||||||
@@ -692,10 +714,10 @@ pub(crate) mod tests {
|
|||||||
fn authors() {
|
fn authors() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let (user, _) = fill_database(&conn);
|
let (user, _) = fill_database(conn);
|
||||||
|
|
||||||
let b1 = Blog::insert(
|
let b1 = Blog::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"SomeName".to_owned(),
|
"SomeName".to_owned(),
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
@@ -706,7 +728,7 @@ pub(crate) mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let b2 = Blog::insert(
|
let b2 = Blog::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"Blog".to_owned(),
|
"Blog".to_owned(),
|
||||||
"Blog".to_owned(),
|
"Blog".to_owned(),
|
||||||
@@ -719,7 +741,7 @@ pub(crate) mod tests {
|
|||||||
let blog = vec![b1, b2];
|
let blog = vec![b1, b2];
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewBlogAuthor {
|
NewBlogAuthor {
|
||||||
blog_id: blog[0].id,
|
blog_id: blog[0].id,
|
||||||
author_id: user[0].id,
|
author_id: user[0].id,
|
||||||
@@ -729,7 +751,7 @@ pub(crate) mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewBlogAuthor {
|
NewBlogAuthor {
|
||||||
blog_id: blog[0].id,
|
blog_id: blog[0].id,
|
||||||
author_id: user[1].id,
|
author_id: user[1].id,
|
||||||
@@ -739,7 +761,7 @@ pub(crate) mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewBlogAuthor {
|
NewBlogAuthor {
|
||||||
blog_id: blog[1].id,
|
blog_id: blog[1].id,
|
||||||
author_id: user[0].id,
|
author_id: user[0].id,
|
||||||
@@ -749,39 +771,39 @@ pub(crate) mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(blog[0]
|
assert!(blog[0]
|
||||||
.list_authors(&conn)
|
.list_authors(conn)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|a| a.id == user[0].id));
|
.any(|a| a.id == user[0].id));
|
||||||
assert!(blog[0]
|
assert!(blog[0]
|
||||||
.list_authors(&conn)
|
.list_authors(conn)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|a| a.id == user[1].id));
|
.any(|a| a.id == user[1].id));
|
||||||
assert!(blog[1]
|
assert!(blog[1]
|
||||||
.list_authors(&conn)
|
.list_authors(conn)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|a| a.id == user[0].id));
|
.any(|a| a.id == user[0].id));
|
||||||
assert!(!blog[1]
|
assert!(!blog[1]
|
||||||
.list_authors(&conn)
|
.list_authors(conn)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|a| a.id == user[1].id));
|
.any(|a| a.id == user[1].id));
|
||||||
|
|
||||||
assert!(Blog::find_for_author(&conn, &user[0])
|
assert!(Blog::find_for_author(conn, &user[0])
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|b| b.id == blog[0].id));
|
.any(|b| b.id == blog[0].id));
|
||||||
assert!(Blog::find_for_author(&conn, &user[1])
|
assert!(Blog::find_for_author(conn, &user[1])
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|b| b.id == blog[0].id));
|
.any(|b| b.id == blog[0].id));
|
||||||
assert!(Blog::find_for_author(&conn, &user[0])
|
assert!(Blog::find_for_author(conn, &user[0])
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|b| b.id == blog[1].id));
|
.any(|b| b.id == blog[1].id));
|
||||||
assert!(!Blog::find_for_author(&conn, &user[1])
|
assert!(!Blog::find_for_author(conn, &user[1])
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|b| b.id == blog[1].id));
|
.any(|b| b.id == blog[1].id));
|
||||||
@@ -793,10 +815,10 @@ pub(crate) mod tests {
|
|||||||
fn find_local() {
|
fn find_local() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
fill_database(&conn);
|
fill_database(conn);
|
||||||
|
|
||||||
let blog = Blog::insert(
|
let blog = Blog::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"SomeName".to_owned(),
|
"SomeName".to_owned(),
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
@@ -807,7 +829,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(conn, "SomeName").unwrap().id, blog.id);
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -816,10 +838,10 @@ pub(crate) mod tests {
|
|||||||
fn get_fqn() {
|
fn get_fqn() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
fill_database(&conn);
|
fill_database(conn);
|
||||||
|
|
||||||
let blog = Blog::insert(
|
let blog = Blog::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"SomeName".to_owned(),
|
"SomeName".to_owned(),
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
@@ -839,10 +861,10 @@ pub(crate) mod tests {
|
|||||||
fn delete() {
|
fn delete() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let (_, blogs) = fill_database(&conn);
|
let (_, blogs) = fill_database(conn);
|
||||||
|
|
||||||
blogs[0].delete(&conn).unwrap();
|
blogs[0].delete(conn).unwrap();
|
||||||
assert!(Blog::get(&conn, blogs[0].id).is_err());
|
assert!(Blog::get(conn, blogs[0].id).is_err());
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -851,10 +873,10 @@ pub(crate) mod tests {
|
|||||||
fn delete_via_user() {
|
fn delete_via_user() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let (user, _) = fill_database(&conn);
|
let (user, _) = fill_database(conn);
|
||||||
|
|
||||||
let b1 = Blog::insert(
|
let b1 = Blog::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"SomeName".to_owned(),
|
"SomeName".to_owned(),
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
@@ -865,7 +887,7 @@ pub(crate) mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let b2 = Blog::insert(
|
let b2 = Blog::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"Blog".to_owned(),
|
"Blog".to_owned(),
|
||||||
"Blog".to_owned(),
|
"Blog".to_owned(),
|
||||||
@@ -878,7 +900,7 @@ pub(crate) mod tests {
|
|||||||
let blog = vec![b1, b2];
|
let blog = vec![b1, b2];
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewBlogAuthor {
|
NewBlogAuthor {
|
||||||
blog_id: blog[0].id,
|
blog_id: blog[0].id,
|
||||||
author_id: user[0].id,
|
author_id: user[0].id,
|
||||||
@@ -888,7 +910,7 @@ pub(crate) mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewBlogAuthor {
|
NewBlogAuthor {
|
||||||
blog_id: blog[0].id,
|
blog_id: blog[0].id,
|
||||||
author_id: user[1].id,
|
author_id: user[1].id,
|
||||||
@@ -898,7 +920,7 @@ pub(crate) mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewBlogAuthor {
|
NewBlogAuthor {
|
||||||
blog_id: blog[1].id,
|
blog_id: blog[1].id,
|
||||||
author_id: user[0].id,
|
author_id: user[0].id,
|
||||||
@@ -907,11 +929,11 @@ pub(crate) mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
user[0].delete(&conn).unwrap();
|
user[0].delete(conn).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).unwrap();
|
||||||
assert!(Blog::get(&conn, blog[0].id).is_err());
|
assert!(Blog::get(conn, blog[0].id).is_err());
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -920,10 +942,10 @@ pub(crate) mod tests {
|
|||||||
fn self_federation() {
|
fn self_federation() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let (users, mut blogs) = fill_database(&conn);
|
let (users, mut blogs) = fill_database(conn);
|
||||||
blogs[0].icon_id = Some(
|
blogs[0].icon_id = Some(
|
||||||
Media::insert(
|
Media::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewMedia {
|
NewMedia {
|
||||||
file_path: "aaa.png".into(),
|
file_path: "aaa.png".into(),
|
||||||
alt_text: String::new(),
|
alt_text: String::new(),
|
||||||
@@ -939,7 +961,7 @@ pub(crate) mod tests {
|
|||||||
);
|
);
|
||||||
blogs[0].banner_id = Some(
|
blogs[0].banner_id = Some(
|
||||||
Media::insert(
|
Media::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewMedia {
|
NewMedia {
|
||||||
file_path: "bbb.png".into(),
|
file_path: "bbb.png".into(),
|
||||||
alt_text: String::new(),
|
alt_text: String::new(),
|
||||||
@@ -954,9 +976,9 @@ pub(crate) mod tests {
|
|||||||
.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).unwrap();
|
||||||
let blog = Blog::from_activity(&conn, ap_repr).unwrap();
|
let blog = Blog::from_activity(conn, 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);
|
||||||
@@ -968,8 +990,8 @@ pub(crate) mod tests {
|
|||||||
assert_eq!(blog.public_key, blogs[0].public_key);
|
assert_eq!(blog.public_key, blogs[0].public_key);
|
||||||
assert_eq!(blog.fqn, blogs[0].fqn);
|
assert_eq!(blog.fqn, blogs[0].fqn);
|
||||||
assert_eq!(blog.summary_html, blogs[0].summary_html);
|
assert_eq!(blog.summary_html, blogs[0].summary_html);
|
||||||
assert_eq!(blog.icon_url(&conn), blogs[0].icon_url(&conn));
|
assert_eq!(blog.icon_url(conn), blogs[0].icon_url(conn));
|
||||||
assert_eq!(blog.banner_url(&conn), blogs[0].banner_url(&conn));
|
assert_eq!(blog.banner_url(conn), blogs[0].banner_url(conn));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -979,7 +1001,7 @@ pub(crate) mod tests {
|
|||||||
fn to_activity() {
|
fn to_activity() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, Error, _>(|| {
|
conn.test_transaction::<_, Error, _>(|| {
|
||||||
let (_users, blogs) = fill_database(&conn);
|
let (_users, blogs) = fill_database(conn);
|
||||||
let blog = &blogs[0];
|
let blog = &blogs[0];
|
||||||
let act = blog.to_activity(conn)?;
|
let act = blog.to_activity(conn)?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
comment_seers::{CommentSeers, NewCommentSeers},
|
comment_seers::{CommentSeers, NewCommentSeers},
|
||||||
db_conn::DbConn,
|
|
||||||
instance::Instance,
|
instance::Instance,
|
||||||
medias::Media,
|
medias::Media,
|
||||||
mentions::Mention,
|
mentions::Mention,
|
||||||
@@ -74,6 +73,7 @@ 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,7 +111,7 @@ impl Comment {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_activity(&self, conn: &DbConn) -> Result<Note> {
|
pub fn to_activity(&self, conn: &Connection) -> Result<Note> {
|
||||||
let author = User::get(conn, self.author_id)?;
|
let author = User::get(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(),
|
||||||
@@ -136,7 +136,7 @@ impl Comment {
|
|||||||
|id| Comment::get(conn, id).map(|comment| comment.ap_url.unwrap_or_default()),
|
|id| Comment::get(conn, id).map(|comment| comment.ap_url.unwrap_or_default()),
|
||||||
)?);
|
)?);
|
||||||
note.set_published(
|
note.set_published(
|
||||||
OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos().into())
|
OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos_opt().unwrap().into())
|
||||||
.expect("OffsetDateTime"),
|
.expect("OffsetDateTime"),
|
||||||
);
|
);
|
||||||
note.set_attributed_to(author.into_id().parse::<IriString>()?);
|
note.set_attributed_to(author.into_id().parse::<IriString>()?);
|
||||||
@@ -149,7 +149,7 @@ impl Comment {
|
|||||||
Ok(note)
|
Ok(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_activity(&self, conn: &DbConn) -> Result<Create> {
|
pub fn create_activity(&self, conn: &Connection) -> Result<Create> {
|
||||||
let author = User::get(conn, self.author_id)?;
|
let author = User::get(conn, self.author_id)?;
|
||||||
|
|
||||||
let note = self.to_activity(conn)?;
|
let note = self.to_activity(conn)?;
|
||||||
@@ -217,15 +217,15 @@ impl Comment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromId<DbConn> for Comment {
|
impl FromId<Connection> for Comment {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = Note;
|
type Object = Note;
|
||||||
|
|
||||||
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
fn from_db(conn: &Connection, id: &str) -> Result<Self> {
|
||||||
Self::find_by_ap_url(conn, id)
|
Self::find_by_ap_url(conn, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_activity(conn: &DbConn, note: Note) -> Result<Self> {
|
fn from_activity(conn: &Connection, note: Note) -> Result<Self> {
|
||||||
let comm = {
|
let comm = {
|
||||||
let previous_url = note
|
let previous_url = note
|
||||||
.in_reply_to()
|
.in_reply_to()
|
||||||
@@ -354,21 +354,21 @@ impl FromId<DbConn> for Comment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Create, &DbConn> for Comment {
|
impl AsObject<User, Create, &Connection> for Comment {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result<Self> {
|
fn activity(self, _conn: &Connection, _actor: User, _id: &str) -> Result<Self> {
|
||||||
// The actual creation takes place in the FromId impl
|
// The actual creation takes place in the FromId impl
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Delete, &DbConn> for Comment {
|
impl AsObject<User, Delete, &Connection> for Comment {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
|
||||||
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
fn activity(self, conn: &Connection, 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);
|
||||||
}
|
}
|
||||||
@@ -381,14 +381,14 @@ impl AsObject<User, Delete, &DbConn> for Comment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for n in Notification::find_for_comment(conn, &self)? {
|
for n in Notification::find_for_comment(conn, &self)? {
|
||||||
n.delete(&**conn)?;
|
n.delete(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(conn)?;
|
||||||
diesel::delete(&self).execute(&**conn)?;
|
diesel::delete(&self).execute(conn)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -423,6 +423,7 @@ impl CommentTree {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::blogs::Blog;
|
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::{db, format_datetime};
|
||||||
@@ -431,7 +432,7 @@ mod tests {
|
|||||||
use serde_json::{json, to_value};
|
use serde_json::{json, to_value};
|
||||||
|
|
||||||
fn prepare_activity(conn: &DbConn) -> (Comment, Vec<Post>, Vec<User>, Vec<Blog>) {
|
fn prepare_activity(conn: &DbConn) -> (Comment, Vec<Post>, Vec<User>, Vec<Blog>) {
|
||||||
let (posts, users, blogs) = fill_database(&conn);
|
let (posts, users, blogs) = fill_database(conn);
|
||||||
|
|
||||||
let comment = Comment::insert(
|
let comment = Comment::insert(
|
||||||
conn,
|
conn,
|
||||||
@@ -457,8 +458,8 @@ mod tests {
|
|||||||
fn self_federation() {
|
fn self_federation() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let (original_comm, posts, users, _blogs) = prepare_activity(&conn);
|
let (original_comm, posts, users, _blogs) = prepare_activity(conn);
|
||||||
let act = original_comm.create_activity(&conn).unwrap();
|
let act = original_comm.create_activity(conn).unwrap();
|
||||||
|
|
||||||
assert_json_eq!(to_value(&act).unwrap(), json!({
|
assert_json_eq!(to_value(&act).unwrap(), json!({
|
||||||
"actor": "https://plu.me/@/admin/",
|
"actor": "https://plu.me/@/admin/",
|
||||||
@@ -500,7 +501,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let reply_act = reply.create_activity(&conn).unwrap();
|
let reply_act = reply.create_activity(conn).unwrap();
|
||||||
|
|
||||||
assert_json_eq!(to_value(&reply_act).unwrap(), json!({
|
assert_json_eq!(to_value(&reply_act).unwrap(), json!({
|
||||||
"actor": "https://plu.me/@/user/",
|
"actor": "https://plu.me/@/user/",
|
||||||
@@ -522,12 +523,12 @@ mod tests {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
inbox(
|
inbox(
|
||||||
&conn,
|
conn,
|
||||||
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(conn, 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);
|
||||||
|
|||||||
+125
-6
@@ -6,6 +6,9 @@ use rocket::Config as RocketConfig;
|
|||||||
use std::collections::HashSet;
|
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)]
|
||||||
@@ -27,13 +30,23 @@ pub struct Config {
|
|||||||
pub mail: Option<MailConfig>,
|
pub mail: Option<MailConfig>,
|
||||||
pub ldap: Option<LdapConfig>,
|
pub ldap: Option<LdapConfig>,
|
||||||
pub proxy: Option<ProxyConfig>,
|
pub proxy: Option<ProxyConfig>,
|
||||||
|
pub s3: Option<S3Config>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn proxy(&self) -> Option<&reqwest::Proxy> {
|
pub fn proxy(&self) -> Option<&reqwest::Proxy> {
|
||||||
self.proxy.as_ref().map(|p| &p.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 InvalidRocketConfig {
|
||||||
Env,
|
Env,
|
||||||
@@ -280,6 +293,7 @@ pub struct LdapConfig {
|
|||||||
pub tls: bool,
|
pub tls: bool,
|
||||||
pub user_name_attr: String,
|
pub user_name_attr: String,
|
||||||
pub mail_attr: String,
|
pub mail_attr: String,
|
||||||
|
pub user: Option<(String, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_ldap_config() -> Option<LdapConfig> {
|
fn get_ldap_config() -> Option<LdapConfig> {
|
||||||
@@ -288,23 +302,29 @@ fn get_ldap_config() -> Option<LdapConfig> {
|
|||||||
match (addr, base_dn) {
|
match (addr, base_dn) {
|
||||||
(Some(addr), Some(base_dn)) => {
|
(Some(addr), Some(base_dn)) => {
|
||||||
let tls = var("LDAP_TLS").unwrap_or_else(|_| "false".to_owned());
|
let tls = var("LDAP_TLS").unwrap_or_else(|_| "false".to_owned());
|
||||||
let tls = match tls.as_ref() {
|
let tls = string_to_bool(&tls, "LDAP_TLS");
|
||||||
"1" | "true" | "TRUE" => true,
|
|
||||||
"0" | "false" | "FALSE" => false,
|
|
||||||
_ => panic!("Invalid LDAP configuration : tls"),
|
|
||||||
};
|
|
||||||
let user_name_attr = var("LDAP_USER_NAME_ATTR").unwrap_or_else(|_| "cn".to_owned());
|
let user_name_attr = var("LDAP_USER_NAME_ATTR").unwrap_or_else(|_| "cn".to_owned());
|
||||||
let mail_attr = var("LDAP_USER_MAIL_ATTR").unwrap_or_else(|_| "mail".to_owned());
|
let 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 {
|
Some(LdapConfig {
|
||||||
addr,
|
addr,
|
||||||
base_dn,
|
base_dn,
|
||||||
tls,
|
tls,
|
||||||
user_name_attr,
|
user_name_attr,
|
||||||
mail_attr,
|
mail_attr,
|
||||||
|
user,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
(None, None) => None,
|
(None, None) => None,
|
||||||
(_, _) => {
|
_ => {
|
||||||
panic!("Invalid LDAP configuration : both LDAP_ADDR and LDAP_BASE_DN must be set")
|
panic!("Invalid LDAP configuration : both LDAP_ADDR and LDAP_BASE_DN must be set")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,6 +369,104 @@ fn get_proxy_config() -> Option<ProxyConfig> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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!(
|
||||||
@@ -380,5 +498,6 @@ lazy_static! {
|
|||||||
mail: get_mail_config(),
|
mail: get_mail_config(),
|
||||||
ldap: get_ldap_config(),
|
ldap: get_ldap_config(),
|
||||||
proxy: get_proxy_config(),
|
proxy: get_proxy_config(),
|
||||||
|
s3: get_s3_config(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ 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)?;
|
||||||
Ok(conn.begin_test_transaction().unwrap())
|
conn.begin_test_transaction().unwrap();
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
|
blocklisted_emails::BlocklistedEmail,
|
||||||
db_conn::DbConn,
|
db_conn::DbConn,
|
||||||
schema::email_signups,
|
schema::email_signups,
|
||||||
users::{NewUser, Role, User},
|
users::{NewUser, Role, User},
|
||||||
@@ -60,6 +61,8 @@ pub struct NewEmailSignup<'a> {
|
|||||||
|
|
||||||
impl EmailSignup {
|
impl EmailSignup {
|
||||||
pub fn start(conn: &DbConn, email: &str) -> Result<Token> {
|
pub fn start(conn: &DbConn, email: &str) -> Result<Token> {
|
||||||
|
Self::ensure_email_not_blocked(conn, email)?;
|
||||||
|
|
||||||
conn.transaction(|| {
|
conn.transaction(|| {
|
||||||
Self::ensure_user_not_exist_by_email(conn, email)?;
|
Self::ensure_user_not_exist_by_email(conn, email)?;
|
||||||
let _rows = Self::delete_existings_by_email(conn, email)?;
|
let _rows = Self::delete_existings_by_email(conn, email)?;
|
||||||
@@ -90,6 +93,8 @@ impl EmailSignup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn confirm(&self, conn: &DbConn) -> Result<()> {
|
pub fn confirm(&self, conn: &DbConn) -> Result<()> {
|
||||||
|
Self::ensure_email_not_blocked(conn, &self.email)?;
|
||||||
|
|
||||||
conn.transaction(|| {
|
conn.transaction(|| {
|
||||||
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
|
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
|
||||||
if self.expired() {
|
if self.expired() {
|
||||||
@@ -101,6 +106,8 @@ impl EmailSignup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn complete(&self, conn: &DbConn, username: String, password: String) -> Result<User> {
|
pub fn complete(&self, conn: &DbConn, username: String, password: String) -> Result<User> {
|
||||||
|
Self::ensure_email_not_blocked(conn, &self.email)?;
|
||||||
|
|
||||||
conn.transaction(|| {
|
conn.transaction(|| {
|
||||||
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
|
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
|
||||||
let user = NewUser::new_local(
|
let user = NewUser::new_local(
|
||||||
@@ -122,6 +129,14 @@ impl EmailSignup {
|
|||||||
Ok(())
|
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<()> {
|
fn ensure_user_not_exist_by_email(conn: &DbConn, email: &str) -> Result<()> {
|
||||||
if User::email_used(conn, email)? {
|
if User::email_used(conn, email)? {
|
||||||
let _rows = Self::delete_existings_by_email(conn, email)?;
|
let _rows = Self::delete_existings_by_email(conn, email)?;
|
||||||
|
|||||||
+15
-18
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
ap_url, db_conn::DbConn, instance::Instance, notifications::*, schema::follows, users::User,
|
ap_url, instance::Instance, notifications::*, schema::follows, users::User, Connection, Error,
|
||||||
Connection, Error, Result, CONFIG,
|
Result, CONFIG,
|
||||||
};
|
};
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
activity::{Accept, ActorAndObjectRef, Follow as FollowAct, Undo},
|
activity::{Accept, ActorAndObjectRef, Follow as FollowAct, Undo},
|
||||||
@@ -107,12 +107,7 @@ impl Follow {
|
|||||||
res.notify(conn)?;
|
res.notify(conn)?;
|
||||||
|
|
||||||
let accept = res.build_accept(from, target, follow)?;
|
let accept = res.build_accept(from, target, follow)?;
|
||||||
broadcast(
|
broadcast(target, accept, vec![from.clone()], CONFIG.proxy().cloned());
|
||||||
&*target,
|
|
||||||
accept,
|
|
||||||
vec![from.clone()],
|
|
||||||
CONFIG.proxy().cloned(),
|
|
||||||
);
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,11 +150,11 @@ impl Follow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, FollowAct, &DbConn> for User {
|
impl AsObject<User, FollowAct, &Connection> for User {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = Follow;
|
type Output = Follow;
|
||||||
|
|
||||||
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Follow> {
|
fn activity(self, conn: &Connection, 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 follow = FollowAct::new(actor.ap_url.parse::<IriString>()?, id.parse::<IriString>()?);
|
||||||
@@ -167,15 +162,15 @@ impl AsObject<User, FollowAct, &DbConn> for User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromId<DbConn> for Follow {
|
impl FromId<Connection> for Follow {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = FollowAct;
|
type Object = FollowAct;
|
||||||
|
|
||||||
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
fn from_db(conn: &Connection, id: &str) -> Result<Self> {
|
||||||
Follow::find_by_ap_url(conn, id)
|
Follow::find_by_ap_url(conn, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_activity(conn: &DbConn, follow: FollowAct) -> Result<Self> {
|
fn from_activity(conn: &Connection, follow: FollowAct) -> Result<Self> {
|
||||||
let actor = User::from_id(
|
let actor = User::from_id(
|
||||||
conn,
|
conn,
|
||||||
follow
|
follow
|
||||||
@@ -207,18 +202,18 @@ impl FromId<DbConn> for Follow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Undo, &DbConn> for Follow {
|
impl AsObject<User, Undo, &Connection> for Follow {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
|
||||||
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> {
|
||||||
let conn = conn;
|
let conn = conn;
|
||||||
if self.follower_id == actor.id {
|
if self.follower_id == actor.id {
|
||||||
diesel::delete(&self).execute(&**conn)?;
|
diesel::delete(&self).execute(conn)?;
|
||||||
|
|
||||||
// delete associated notification if any
|
// delete associated notification if any
|
||||||
if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
|
if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
|
||||||
diesel::delete(¬if).execute(&**conn)?;
|
diesel::delete(¬if).execute(conn)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -237,7 +232,9 @@ impl IntoId for Follow {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{tests::db, users::tests as user_tests, users::tests::fill_database};
|
use crate::{
|
||||||
|
db_conn::DbConn, tests::db, users::tests as user_tests, users::tests::fill_database,
|
||||||
|
};
|
||||||
use assert_json_diff::assert_json_eq;
|
use assert_json_diff::assert_json_eq;
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
use serde_json::{json, to_value};
|
use serde_json::{json, to_value};
|
||||||
|
|||||||
+11
-12
@@ -2,12 +2,11 @@ use activitystreams::activity::{Announce, Create, Delete, Follow, Like, Undo, Up
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
comments::Comment,
|
comments::Comment,
|
||||||
db_conn::DbConn,
|
|
||||||
follows, likes,
|
follows, likes,
|
||||||
posts::{Post, PostUpdate},
|
posts::{Post, PostUpdate},
|
||||||
reshares::Reshare,
|
reshares::Reshare,
|
||||||
users::User,
|
users::User,
|
||||||
Error, CONFIG,
|
Connection, Error, CONFIG,
|
||||||
};
|
};
|
||||||
use plume_common::activity_pub::inbox::Inbox;
|
use plume_common::activity_pub::inbox::Inbox;
|
||||||
|
|
||||||
@@ -46,7 +45,7 @@ impl_into_inbox_result! {
|
|||||||
Reshare => Reshared
|
Reshare => Reshared
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn inbox(conn: &DbConn, act: serde_json::Value) -> Result<InboxResult, Error> {
|
pub fn inbox(conn: &Connection, act: serde_json::Value) -> Result<InboxResult, Error> {
|
||||||
Inbox::handle(conn, act)
|
Inbox::handle(conn, act)
|
||||||
.with::<User, Announce, Post>(CONFIG.proxy())
|
.with::<User, Announce, Post>(CONFIG.proxy())
|
||||||
.with::<User, Create, Comment>(CONFIG.proxy())
|
.with::<User, Create, Comment>(CONFIG.proxy())
|
||||||
@@ -82,9 +81,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(conn);
|
||||||
let post = Post::insert(
|
let post = Post::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewPost {
|
NewPost {
|
||||||
blog_id: blogs[0].id,
|
blog_id: blogs[0].id,
|
||||||
slug: "testing".to_owned(),
|
slug: "testing".to_owned(),
|
||||||
@@ -102,7 +101,7 @@ pub(crate) mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
PostAuthor::insert(
|
PostAuthor::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewPostAuthor {
|
NewPostAuthor {
|
||||||
post_id: post.id,
|
post_id: post.id,
|
||||||
author_id: users[0].id,
|
author_id: users[0].id,
|
||||||
@@ -190,7 +189,7 @@ pub(crate) mod tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
super::inbox(&conn, act.clone()),
|
super::inbox(&conn, act),
|
||||||
Err(super::Error::Inbox(
|
Err(super::Error::Inbox(
|
||||||
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
||||||
))
|
))
|
||||||
@@ -221,7 +220,7 @@ pub(crate) mod tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
super::inbox(&conn, act.clone()),
|
super::inbox(&conn, act),
|
||||||
Err(super::Error::Inbox(
|
Err(super::Error::Inbox(
|
||||||
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
||||||
))
|
))
|
||||||
@@ -249,7 +248,7 @@ pub(crate) mod tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
super::inbox(&conn, act.clone()),
|
super::inbox(&conn, act),
|
||||||
Err(super::Error::Inbox(
|
Err(super::Error::Inbox(
|
||||||
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
||||||
))
|
))
|
||||||
@@ -324,7 +323,7 @@ pub(crate) mod tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
super::inbox(&conn, act.clone()),
|
super::inbox(&conn, act),
|
||||||
Err(super::Error::Inbox(
|
Err(super::Error::Inbox(
|
||||||
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
||||||
))
|
))
|
||||||
@@ -362,7 +361,7 @@ pub(crate) mod tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
super::inbox(&conn, act.clone()),
|
super::inbox(&conn, act),
|
||||||
Err(super::Error::Inbox(
|
Err(super::Error::Inbox(
|
||||||
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
||||||
))
|
))
|
||||||
@@ -397,7 +396,7 @@ pub(crate) mod tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
super::inbox(&conn, act.clone()),
|
super::inbox(&conn, act),
|
||||||
Err(super::Error::Inbox(
|
Err(super::Error::Inbox(
|
||||||
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::{
|
|||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{self, result::Error::NotFound, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, result::Error::NotFound, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
use once_cell::sync::OnceCell;
|
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)]
|
||||||
@@ -173,8 +173,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 = name,
|
name = iri_percent_encode_seg(name),
|
||||||
box_name = box_name
|
box_name = iri_percent_encode_seg(box_name)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,7 +523,7 @@ pub(crate) mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let inst = Instance::get(conn, inst.id).unwrap();
|
let inst = Instance::get(conn, inst.id).unwrap();
|
||||||
assert_eq!(inst.name, "NewName".to_owned());
|
assert_eq!(inst.name, "NewName".to_owned());
|
||||||
assert_eq!(inst.open_registrations, false);
|
assert!(!inst.open_registrations);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inst.long_description.get(),
|
inst.long_description.get(),
|
||||||
"[long_description](/with_link)"
|
"[long_description](/with_link)"
|
||||||
|
|||||||
+15
-6
@@ -69,6 +69,8 @@ pub enum Error {
|
|||||||
Webfinger,
|
Webfinger,
|
||||||
Expired,
|
Expired,
|
||||||
UserAlreadyExists,
|
UserAlreadyExists,
|
||||||
|
#[cfg(feature = "s3")]
|
||||||
|
S3(s3::error::S3Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<bcrypt::BcryptError> for Error {
|
impl From<bcrypt::BcryptError> for Error {
|
||||||
@@ -170,6 +172,13 @@ impl From<request::Error> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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
|
||||||
@@ -177,7 +186,7 @@ pub type Result<T> = std::result::Result<T, Error>;
|
|||||||
///
|
///
|
||||||
/// Usage:
|
/// Usage:
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```ignore
|
||||||
/// 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);
|
||||||
/// }
|
/// }
|
||||||
@@ -201,7 +210,7 @@ macro_rules! find_by {
|
|||||||
///
|
///
|
||||||
/// Usage:
|
/// Usage:
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```ignore
|
||||||
/// 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);
|
||||||
/// }
|
/// }
|
||||||
@@ -225,7 +234,7 @@ macro_rules! list_by {
|
|||||||
///
|
///
|
||||||
/// # Usage
|
/// # Usage
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```ignore
|
||||||
/// impl Model {
|
/// impl Model {
|
||||||
/// get!(model_table);
|
/// get!(model_table);
|
||||||
/// }
|
/// }
|
||||||
@@ -248,7 +257,7 @@ macro_rules! get {
|
|||||||
///
|
///
|
||||||
/// # Usage
|
/// # Usage
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```ignore
|
||||||
/// impl Model {
|
/// impl Model {
|
||||||
/// insert!(model_table, NewModelType);
|
/// insert!(model_table, NewModelType);
|
||||||
/// }
|
/// }
|
||||||
@@ -280,7 +289,7 @@ macro_rules! insert {
|
|||||||
///
|
///
|
||||||
/// # Usage
|
/// # Usage
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```ignore
|
||||||
/// impl Model {
|
/// impl Model {
|
||||||
/// last!(model_table);
|
/// last!(model_table);
|
||||||
/// }
|
/// }
|
||||||
@@ -354,7 +363,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn db<'a>() -> db_conn::DbConn {
|
pub fn db() -> db_conn::DbConn {
|
||||||
db_conn::DbConn((*DB_POOL).get().unwrap())
|
db_conn::DbConn((*DB_POOL).get().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-14
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*,
|
instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*, users::User,
|
||||||
users::User, Connection, Error, Result, CONFIG,
|
Connection, Error, Result, CONFIG,
|
||||||
};
|
};
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
activity::{ActorAndObjectRef, Like as LikeAct, Undo},
|
activity::{ActorAndObjectRef, Like as LikeAct, Undo},
|
||||||
@@ -85,11 +85,11 @@ impl Like {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, LikeAct, &DbConn> for Post {
|
impl AsObject<User, LikeAct, &Connection> for Post {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = Like;
|
type Output = Like;
|
||||||
|
|
||||||
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Like> {
|
fn activity(self, conn: &Connection, actor: User, id: &str) -> Result<Like> {
|
||||||
let res = Like::insert(
|
let res = Like::insert(
|
||||||
conn,
|
conn,
|
||||||
NewLike {
|
NewLike {
|
||||||
@@ -105,15 +105,15 @@ impl AsObject<User, LikeAct, &DbConn> for Post {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromId<DbConn> for Like {
|
impl FromId<Connection> for Like {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = LikeAct;
|
type Object = LikeAct;
|
||||||
|
|
||||||
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
fn from_db(conn: &Connection, id: &str) -> Result<Self> {
|
||||||
Like::find_by_ap_url(conn, id)
|
Like::find_by_ap_url(conn, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_activity(conn: &DbConn, act: LikeAct) -> Result<Self> {
|
fn from_activity(conn: &Connection, act: LikeAct) -> Result<Self> {
|
||||||
let res = Like::insert(
|
let res = Like::insert(
|
||||||
conn,
|
conn,
|
||||||
NewLike {
|
NewLike {
|
||||||
@@ -154,17 +154,17 @@ impl FromId<DbConn> for Like {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Undo, &DbConn> for Like {
|
impl AsObject<User, Undo, &Connection> for Like {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
|
||||||
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> {
|
||||||
if actor.id == self.user_id {
|
if actor.id == self.user_id {
|
||||||
diesel::delete(&self).execute(&**conn)?;
|
diesel::delete(&self).execute(conn)?;
|
||||||
|
|
||||||
// delete associated notification if any
|
// delete associated notification if any
|
||||||
if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) {
|
if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) {
|
||||||
diesel::delete(¬if).execute(&**conn)?;
|
diesel::delete(¬if).execute(conn)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
@@ -199,7 +199,7 @@ mod tests {
|
|||||||
let (posts, _users, _blogs) = fill_database(&conn);
|
let (posts, _users, _blogs) = fill_database(&conn);
|
||||||
let post = &posts[0];
|
let post = &posts[0];
|
||||||
let user = &post.get_authors(&conn)?[0];
|
let user = &post.get_authors(&conn)?[0];
|
||||||
let like = Like::insert(&*conn, NewLike::new(post, user))?;
|
let like = Like::insert(&conn, NewLike::new(post, user))?;
|
||||||
let act = like.to_activity(&conn).unwrap();
|
let act = like.to_activity(&conn).unwrap();
|
||||||
|
|
||||||
let expected = json!({
|
let expected = json!({
|
||||||
@@ -223,8 +223,8 @@ mod tests {
|
|||||||
let (posts, _users, _blogs) = fill_database(&conn);
|
let (posts, _users, _blogs) = fill_database(&conn);
|
||||||
let post = &posts[0];
|
let post = &posts[0];
|
||||||
let user = &post.get_authors(&conn)?[0];
|
let user = &post.get_authors(&conn)?[0];
|
||||||
let like = Like::insert(&*conn, NewLike::new(post, user))?;
|
let like = Like::insert(&conn, NewLike::new(post, user))?;
|
||||||
let act = like.build_undo(&*conn)?;
|
let act = like.build_undo(&conn)?;
|
||||||
|
|
||||||
let expected = json!({
|
let expected = json!({
|
||||||
"actor": "https://plu.me/@/admin/",
|
"actor": "https://plu.me/@/admin/",
|
||||||
|
|||||||
@@ -297,6 +297,28 @@ 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}
|
||||||
@@ -413,7 +435,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(())
|
||||||
|
|||||||
+187
-40
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
ap_url, db_conn::DbConn, instance::Instance, safe_string::SafeString, schema::medias,
|
ap_url, instance::Instance, safe_string::SafeString, schema::medias, users::User, Connection,
|
||||||
users::User, Connection, Error, Result, CONFIG,
|
Error, Result, CONFIG,
|
||||||
};
|
};
|
||||||
use activitystreams::{object::Image, prelude::*};
|
use activitystreams::{object::Image, prelude::*};
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
@@ -16,6 +16,9 @@ use std::{
|
|||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
#[cfg(feature = "s3")]
|
||||||
|
use crate::config::S3Config;
|
||||||
|
|
||||||
const REMOTE_MEDIA_DIRECTORY: &str = "remote";
|
const REMOTE_MEDIA_DIRECTORY: &str = "remote";
|
||||||
|
|
||||||
#[derive(Clone, Identifiable, Queryable, AsChangeset)]
|
#[derive(Clone, Identifiable, Queryable, AsChangeset)]
|
||||||
@@ -42,7 +45,7 @@ pub struct NewMedia {
|
|||||||
pub owner_id: i32,
|
pub owner_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq, Eq)]
|
||||||
pub enum MediaCategory {
|
pub enum MediaCategory {
|
||||||
Image,
|
Image,
|
||||||
Audio,
|
Audio,
|
||||||
@@ -105,7 +108,7 @@ impl Media {
|
|||||||
.file_path
|
.file_path
|
||||||
.rsplit_once('.')
|
.rsplit_once('.')
|
||||||
.map(|x| x.1)
|
.map(|x| x.1)
|
||||||
.expect("Media::category: extension error")
|
.unwrap_or("")
|
||||||
.to_lowercase()
|
.to_lowercase()
|
||||||
{
|
{
|
||||||
"png" | "jpg" | "jpeg" | "gif" | "svg" => MediaCategory::Image,
|
"png" | "jpg" | "jpeg" | "gif" | "svg" => MediaCategory::Image,
|
||||||
@@ -151,26 +154,99 @@ 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 file_path = self.file_path.replace(path::MAIN_SEPARATOR, "/").replacen(
|
let relative_url = self.relative_url().unwrap_or_default();
|
||||||
&CONFIG.media_directory,
|
|
||||||
"static/media",
|
#[cfg(feature="s3")]
|
||||||
1,
|
if CONFIG.s3.as_ref().map(|x| x.direct_download).unwrap_or(false) {
|
||||||
); // "static/media" from plume::routs::plume_media_files()
|
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!(
|
||||||
"{}/{}",
|
"{}/{}",
|
||||||
Instance::get_local()?.public_domain,
|
Instance::get_local()?.public_domain,
|
||||||
&file_path
|
relative_url
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(&self, conn: &Connection) -> Result<()> {
|
pub fn delete(&self, conn: &Connection) -> Result<()> {
|
||||||
if !self.is_remote {
|
if !self.is_remote {
|
||||||
fs::remove_file(self.file_path.as_str())?;
|
if CONFIG.s3.is_some() {
|
||||||
|
#[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)
|
||||||
@@ -206,27 +282,65 @@ impl Media {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: merge with save_remote?
|
// TODO: merge with save_remote?
|
||||||
pub fn from_activity(conn: &DbConn, image: &Image) -> Result<Media> {
|
pub fn from_activity(conn: &Connection, image: &Image) -> Result<Media> {
|
||||||
let remote_url = image
|
let remote_url = image
|
||||||
.url()
|
.url()
|
||||||
.and_then(|url| url.to_as_uri())
|
.and_then(|url| url.to_as_uri())
|
||||||
.ok_or(Error::MissingApProperty)?;
|
.ok_or(Error::MissingApProperty)?;
|
||||||
let path = determine_mirror_file_path(&remote_url);
|
|
||||||
let parent = path.parent().ok_or(Error::InvalidValue)?;
|
|
||||||
if !parent.is_dir() {
|
|
||||||
DirBuilder::new().recursive(true).create(parent)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dest = fs::File::create(path.clone())?;
|
let file_path = if CONFIG.s3.is_some() {
|
||||||
// TODO: conditional GET
|
#[cfg(not(feature="s3"))]
|
||||||
request::get(
|
unreachable!();
|
||||||
remote_url.as_str(),
|
|
||||||
User::get_sender(),
|
|
||||||
CONFIG.proxy().cloned(),
|
|
||||||
)?
|
|
||||||
.copy_to(&mut dest)?;
|
|
||||||
|
|
||||||
Media::find_by_file_path(conn, path.to_str().ok_or(Error::InvalidValue)?)
|
#[cfg(feature = "s3")]
|
||||||
|
{
|
||||||
|
use rocket::http::ContentType;
|
||||||
|
|
||||||
|
let dest = determine_mirror_s3_path(&remote_url);
|
||||||
|
|
||||||
|
let media = request::get(
|
||||||
|
remote_url.as_str(),
|
||||||
|
User::get_sender(),
|
||||||
|
CONFIG.proxy().cloned(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let content_type = media
|
||||||
|
.headers()
|
||||||
|
.get(reqwest::header::CONTENT_TYPE)
|
||||||
|
.and_then(|x| x.to_str().ok())
|
||||||
|
.and_then(ContentType::parse_flexible)
|
||||||
|
.unwrap_or(ContentType::Binary);
|
||||||
|
|
||||||
|
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| {
|
.and_then(|mut media| {
|
||||||
let mut updated = false;
|
let mut updated = false;
|
||||||
|
|
||||||
@@ -258,7 +372,7 @@ impl Media {
|
|||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
if updated {
|
if updated {
|
||||||
diesel::update(&media).set(&media).execute(&**conn)?;
|
diesel::update(&media).set(&media).execute(conn)?;
|
||||||
}
|
}
|
||||||
Ok(media)
|
Ok(media)
|
||||||
})
|
})
|
||||||
@@ -267,7 +381,7 @@ impl Media {
|
|||||||
Media::insert(
|
Media::insert(
|
||||||
conn,
|
conn,
|
||||||
NewMedia {
|
NewMedia {
|
||||||
file_path: path.to_str().ok_or(Error::InvalidValue)?.to_string(),
|
file_path,
|
||||||
alt_text: image
|
alt_text: image
|
||||||
.content()
|
.content()
|
||||||
.and_then(|content| content.to_as_string())
|
.and_then(|content| content.to_as_string())
|
||||||
@@ -307,12 +421,10 @@ impl Media {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn determine_mirror_file_path(url: &str) -> PathBuf {
|
fn determine_mirror_file_path(url: &str) -> PathBuf {
|
||||||
let mut file_path = Path::new(&super::CONFIG.media_directory).join(REMOTE_MEDIA_DIRECTORY);
|
let mut file_path = Path::new(&CONFIG.media_directory).join(REMOTE_MEDIA_DIRECTORY);
|
||||||
Url::parse(url)
|
|
||||||
.map(|url| {
|
match Url::parse(url) {
|
||||||
if !url.has_host() {
|
Ok(url) if url.has_host() => {
|
||||||
return;
|
|
||||||
}
|
|
||||||
file_path.push(url.host_str().unwrap());
|
file_path.push(url.host_str().unwrap());
|
||||||
for segment in url.path_segments().expect("FIXME") {
|
for segment in url.path_segments().expect("FIXME") {
|
||||||
file_path.push(segment);
|
file_path.push(segment);
|
||||||
@@ -320,19 +432,54 @@ fn determine_mirror_file_path(url: &str) -> PathBuf {
|
|||||||
// TODO: handle query
|
// TODO: handle query
|
||||||
// HINT: Use characters which must be percent-encoded in path as separator between path and query
|
// HINT: Use characters which must be percent-encoded in path as separator between path and query
|
||||||
// HINT: handle extension
|
// HINT: handle extension
|
||||||
})
|
}
|
||||||
.unwrap_or_else(|err| {
|
other => {
|
||||||
warn!("Failed to parse url: {} {}", &url, err);
|
if let Err(err) = other {
|
||||||
|
warn!("Failed to parse url: {} {}", &url, err);
|
||||||
|
} else {
|
||||||
|
warn!("Error without a host: {}", &url);
|
||||||
|
}
|
||||||
let ext = url
|
let ext = url
|
||||||
.rsplit('.')
|
.rsplit('.')
|
||||||
.next()
|
.next()
|
||||||
.map(ToOwned::to_owned)
|
.map(ToOwned::to_owned)
|
||||||
.unwrap_or_else(|| String::from("png"));
|
.unwrap_or_else(|| String::from("png"));
|
||||||
file_path.push(format!("{}.{}", GUID::rand(), ext));
|
file_path.push(format!("{}.{}", GUID::rand(), ext));
|
||||||
});
|
}
|
||||||
|
}
|
||||||
file_path
|
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::*;
|
||||||
@@ -343,7 +490,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().to_path_buf();
|
let mut wd = current_dir().unwrap();
|
||||||
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();
|
||||||
@@ -456,7 +603,7 @@ pub(crate) mod tests {
|
|||||||
let media = Media::insert(
|
let media = Media::insert(
|
||||||
conn,
|
conn,
|
||||||
NewMedia {
|
NewMedia {
|
||||||
file_path: path.clone(),
|
file_path: path,
|
||||||
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,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
comments::Comment, db_conn::DbConn, notifications::*, posts::Post, schema::mentions,
|
comments::Comment, notifications::*, posts::Post, schema::mentions, users::User, Connection,
|
||||||
users::User, Connection, Error, Result,
|
Error, Result,
|
||||||
};
|
};
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
base::BaseExt,
|
base::BaseExt,
|
||||||
@@ -60,7 +60,7 @@ impl Mention {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_activity(conn: &DbConn, ment: &str) -> Result<link::Mention> {
|
pub fn build_activity(conn: &Connection, ment: &str) -> Result<link::Mention> {
|
||||||
let user = User::find_by_fqn(conn, ment)?;
|
let user = User::find_by_fqn(conn, ment)?;
|
||||||
let mut mention = link::Mention::new();
|
let mut mention = link::Mention::new();
|
||||||
mention.set_href(user.ap_url.parse::<IriString>()?);
|
mention.set_href(user.ap_url.parse::<IriString>()?);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
+37
-32
@@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
ap_url, blogs::Blog, db_conn::DbConn, instance::Instance, medias::Media, mentions::Mention,
|
ap_url, blogs::Blog, instance::Instance, medias::Media, mentions::Mention, post_authors::*,
|
||||||
post_authors::*, safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User,
|
safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User, Connection, Error,
|
||||||
Connection, Error, PostEvent::*, Result, CONFIG, POST_CHAN,
|
PostEvent::*, Result, CONFIG, POST_CHAN,
|
||||||
};
|
};
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
activity::{Create, Delete, Update},
|
activity::{Create, Delete, Update},
|
||||||
@@ -134,7 +134,7 @@ impl Post {
|
|||||||
.filter(posts::published.eq(true))
|
.filter(posts::published.eq(true))
|
||||||
.count()
|
.count()
|
||||||
.load(conn)?
|
.load(conn)?
|
||||||
.get(0)
|
.first()
|
||||||
.cloned()
|
.cloned()
|
||||||
.ok_or(Error::NotFound)
|
.ok_or(Error::NotFound)
|
||||||
}
|
}
|
||||||
@@ -255,7 +255,7 @@ impl Post {
|
|||||||
ap_url(&format!(
|
ap_url(&format!(
|
||||||
"{}/~/{}/{}/",
|
"{}/~/{}/{}/",
|
||||||
CONFIG.base_url,
|
CONFIG.base_url,
|
||||||
blog.fqn,
|
iri_percent_encode_seg(&blog.fqn),
|
||||||
iri_percent_encode_seg(slug)
|
iri_percent_encode_seg(slug)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -373,7 +373,7 @@ impl Post {
|
|||||||
}))?;
|
}))?;
|
||||||
article.set_source(source);
|
article.set_source(source);
|
||||||
article.set_published(
|
article.set_published(
|
||||||
OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos().into())
|
OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos_opt().unwrap().into())
|
||||||
.expect("OffsetDateTime"),
|
.expect("OffsetDateTime"),
|
||||||
);
|
);
|
||||||
article.set_summary(&*self.subtitle);
|
article.set_summary(&*self.subtitle);
|
||||||
@@ -465,7 +465,7 @@ impl Post {
|
|||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
for (m, id) in &mentions {
|
for (m, id) in &mentions {
|
||||||
if !old_user_mentioned.contains(id) {
|
if !old_user_mentioned.contains(id) {
|
||||||
Mention::from_activity(&*conn, m, self.id, true, true)?;
|
Mention::from_activity(conn, m, self.id, true, true)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,7 +488,7 @@ impl Post {
|
|||||||
.filter_map(|t| t.name.as_ref().map(|name| name.as_str().to_string()))
|
.filter_map(|t| t.name.as_ref().map(|name| name.as_str().to_string()))
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
let old_tags = Tag::for_post(&*conn, self.id)?;
|
let old_tags = Tag::for_post(conn, self.id)?;
|
||||||
let old_tags_name = old_tags
|
let old_tags_name = old_tags
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|tag| {
|
.filter_map(|tag| {
|
||||||
@@ -525,7 +525,7 @@ impl Post {
|
|||||||
.filter_map(|t| t.name.as_ref().map(|name| name.as_str().to_string()))
|
.filter_map(|t| t.name.as_ref().map(|name| name.as_str().to_string()))
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
let old_tags = Tag::for_post(&*conn, self.id)?;
|
let old_tags = Tag::for_post(conn, self.id)?;
|
||||||
let old_tags_name = old_tags
|
let old_tags_name = old_tags
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|tag| {
|
.filter_map(|tag| {
|
||||||
@@ -615,15 +615,15 @@ impl Post {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromId<DbConn> for Post {
|
impl FromId<Connection> for Post {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = LicensedArticle;
|
type Object = LicensedArticle;
|
||||||
|
|
||||||
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
fn from_db(conn: &Connection, id: &str) -> Result<Self> {
|
||||||
Self::find_by_ap_url(conn, id)
|
Self::find_by_ap_url(conn, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_activity(conn: &DbConn, article: LicensedArticle) -> Result<Self> {
|
fn from_activity(conn: &Connection, article: LicensedArticle) -> Result<Self> {
|
||||||
let license = article.ext_one.license.unwrap_or_default();
|
let license = article.ext_one.license.unwrap_or_default();
|
||||||
let article = article.inner;
|
let article = article.inner;
|
||||||
|
|
||||||
@@ -756,7 +756,11 @@ impl FromId<DbConn> for Post {
|
|||||||
let timestamp_secs = published.unix_timestamp();
|
let timestamp_secs = published.unix_timestamp();
|
||||||
let timestamp_nanos = published.unix_timestamp_nanos()
|
let timestamp_nanos = published.unix_timestamp_nanos()
|
||||||
- (timestamp_secs as i128) * 1000i128 * 1000i128 * 1000i128;
|
- (timestamp_secs as i128) * 1000i128 * 1000i128 * 1000i128;
|
||||||
NaiveDateTime::from_timestamp(timestamp_secs, timestamp_nanos as u32)
|
NaiveDateTime::from_timestamp_opt(
|
||||||
|
timestamp_secs,
|
||||||
|
timestamp_nanos as u32,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
}),
|
}),
|
||||||
subtitle: article
|
subtitle: article
|
||||||
.summary()
|
.summary()
|
||||||
@@ -817,21 +821,21 @@ impl FromId<DbConn> for Post {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Create, &DbConn> for Post {
|
impl AsObject<User, Create, &Connection> for Post {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result<Self::Output> {
|
fn activity(self, _conn: &Connection, _actor: User, _id: &str) -> Result<Self::Output> {
|
||||||
// TODO: check that _actor is actually one of the author?
|
// TODO: check that _actor is actually one of the author?
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Delete, &DbConn> for Post {
|
impl AsObject<User, Delete, &Connection> for Post {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
|
||||||
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<Self::Output> {
|
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<Self::Output> {
|
||||||
let can_delete = self
|
let can_delete = self
|
||||||
.get_authors(conn)?
|
.get_authors(conn)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -855,16 +859,16 @@ pub struct PostUpdate {
|
|||||||
pub tags: Option<serde_json::Value>,
|
pub tags: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromId<DbConn> for PostUpdate {
|
impl FromId<Connection> for PostUpdate {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = LicensedArticle;
|
type Object = LicensedArticle;
|
||||||
|
|
||||||
fn from_db(_: &DbConn, _: &str) -> Result<Self> {
|
fn from_db(_: &Connection, _: &str) -> Result<Self> {
|
||||||
// Always fail because we always want to deserialize the AP object
|
// Always fail because we always want to deserialize the AP object
|
||||||
Err(Error::NotFound)
|
Err(Error::NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_activity(conn: &DbConn, updated: Self::Object) -> Result<Self> {
|
fn from_activity(conn: &Connection, updated: Self::Object) -> Result<Self> {
|
||||||
let mut post_update = PostUpdate {
|
let mut post_update = PostUpdate {
|
||||||
ap_url: updated
|
ap_url: updated
|
||||||
.ap_object_ref()
|
.ap_object_ref()
|
||||||
@@ -919,11 +923,11 @@ impl FromId<DbConn> for PostUpdate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Update, &DbConn> for PostUpdate {
|
impl AsObject<User, Update, &Connection> for PostUpdate {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
|
||||||
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> {
|
||||||
let mut post =
|
let mut post =
|
||||||
Post::from_id(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?;
|
Post::from_id(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?;
|
||||||
|
|
||||||
@@ -1023,6 +1027,7 @@ impl From<PostEvent> for Arc<Post> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::db_conn::DbConn;
|
||||||
use crate::inbox::{inbox, tests::fill_database, InboxResult};
|
use crate::inbox::{inbox, tests::fill_database, InboxResult};
|
||||||
use crate::mentions::{Mention, NewMention};
|
use crate::mentions::{Mention, NewMention};
|
||||||
use crate::safe_string::SafeString;
|
use crate::safe_string::SafeString;
|
||||||
@@ -1036,7 +1041,7 @@ mod tests {
|
|||||||
let post = &posts[0];
|
let post = &posts[0];
|
||||||
let mentioned = &users[1];
|
let mentioned = &users[1];
|
||||||
let mention = Mention::insert(
|
let mention = Mention::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewMention {
|
NewMention {
|
||||||
mentioned_id: mentioned.id,
|
mentioned_id: mentioned.id,
|
||||||
post_id: Some(post.id),
|
post_id: Some(post.id),
|
||||||
@@ -1044,7 +1049,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
(post.to_owned(), mention.to_owned(), posts, users, blogs)
|
(post.to_owned(), mention, 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,
|
||||||
@@ -1053,9 +1058,9 @@ mod tests {
|
|||||||
fn self_federation() {
|
fn self_federation() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let (_, users, blogs) = fill_database(&conn);
|
let (_, users, blogs) = fill_database(conn);
|
||||||
let post = Post::insert(
|
let post = Post::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewPost {
|
NewPost {
|
||||||
blog_id: blogs[0].id,
|
blog_id: blogs[0].id,
|
||||||
slug: "yo".into(),
|
slug: "yo".into(),
|
||||||
@@ -1072,19 +1077,19 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
PostAuthor::insert(
|
PostAuthor::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewPostAuthor {
|
NewPostAuthor {
|
||||||
post_id: post.id,
|
post_id: post.id,
|
||||||
author_id: users[0].id,
|
author_id: users[0].id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let create = post.create_activity(&conn).unwrap();
|
let create = post.create_activity(conn).unwrap();
|
||||||
post.delete(&conn).unwrap();
|
post.delete(conn).unwrap();
|
||||||
|
|
||||||
match inbox(&conn, serde_json::to_value(create).unwrap()).unwrap() {
|
match inbox(conn, serde_json::to_value(create).unwrap()).unwrap() {
|
||||||
InboxResult::Post(p) => {
|
InboxResult::Post(p) => {
|
||||||
assert!(p.is_author(&conn, users[0].id).unwrap());
|
assert!(p.is_author(conn, users[0].id).unwrap());
|
||||||
assert_eq!(p.source, "Hello".to_owned());
|
assert_eq!(p.source, "Hello".to_owned());
|
||||||
assert_eq!(p.blog_id, blogs[0].id);
|
assert_eq!(p.blog_id, blogs[0].id);
|
||||||
assert_eq!(p.content, SafeString::new("Hello"));
|
assert_eq!(p.content, SafeString::new("Hello"));
|
||||||
@@ -1221,7 +1226,7 @@ mod tests {
|
|||||||
let actual = to_value(act)?;
|
let actual = to_value(act)?;
|
||||||
|
|
||||||
let id = actual["id"].to_string();
|
let id = actual["id"].to_string();
|
||||||
let (id_pre, id_post) = id.rsplit_once("-").unwrap();
|
let (id_pre, id_post) = id.rsplit_once('-').unwrap();
|
||||||
assert_eq!(post.ap_url, "https://plu.me/~/BlogName/testing");
|
assert_eq!(post.ap_url, "https://plu.me/~/BlogName/testing");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
id_pre,
|
id_pre,
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ impl Actor for RemoteFetchActor {
|
|||||||
RemoteUserFound(user) => match self.conn.get() {
|
RemoteUserFound(user) => match self.conn.get() {
|
||||||
Ok(conn) => {
|
Ok(conn) => {
|
||||||
let conn = DbConn(conn);
|
let conn = DbConn(conn);
|
||||||
|
if user
|
||||||
|
.get_instance(&conn)
|
||||||
|
.map_or(false, |instance| instance.blocked)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Don't call these functions in parallel
|
// Don't call these functions in parallel
|
||||||
// for the case database connections limit is too small
|
// for the case database connections limit is too small
|
||||||
fetch_and_cache_articles(&user, &conn);
|
fetch_and_cache_articles(&user, &conn);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::reshares,
|
instance::Instance, notifications::*, posts::Post, schema::reshares, timeline::*, users::User,
|
||||||
timeline::*, users::User, Connection, Error, Result, CONFIG,
|
Connection, Error, Result, CONFIG,
|
||||||
};
|
};
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
activity::{ActorAndObjectRef, Announce, Undo},
|
activity::{ActorAndObjectRef, Announce, Undo},
|
||||||
@@ -113,11 +113,11 @@ impl Reshare {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Announce, &DbConn> for Post {
|
impl AsObject<User, Announce, &Connection> for Post {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = Reshare;
|
type Output = Reshare;
|
||||||
|
|
||||||
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Reshare> {
|
fn activity(self, conn: &Connection, actor: User, id: &str) -> Result<Reshare> {
|
||||||
let conn = conn;
|
let conn = conn;
|
||||||
let reshare = Reshare::insert(
|
let reshare = Reshare::insert(
|
||||||
conn,
|
conn,
|
||||||
@@ -134,15 +134,15 @@ impl AsObject<User, Announce, &DbConn> for Post {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromId<DbConn> for Reshare {
|
impl FromId<Connection> for Reshare {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = Announce;
|
type Object = Announce;
|
||||||
|
|
||||||
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
fn from_db(conn: &Connection, id: &str) -> Result<Self> {
|
||||||
Reshare::find_by_ap_url(conn, id)
|
Reshare::find_by_ap_url(conn, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_activity(conn: &DbConn, act: Announce) -> Result<Self> {
|
fn from_activity(conn: &Connection, act: Announce) -> Result<Self> {
|
||||||
let res = Reshare::insert(
|
let res = Reshare::insert(
|
||||||
conn,
|
conn,
|
||||||
NewReshare {
|
NewReshare {
|
||||||
@@ -183,17 +183,17 @@ impl FromId<DbConn> for Reshare {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Undo, &DbConn> for Reshare {
|
impl AsObject<User, Undo, &Connection> for Reshare {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
|
||||||
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> {
|
||||||
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)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -229,7 +229,7 @@ mod test {
|
|||||||
let (posts, _users, _blogs) = fill_database(&conn);
|
let (posts, _users, _blogs) = fill_database(&conn);
|
||||||
let post = &posts[0];
|
let post = &posts[0];
|
||||||
let user = &post.get_authors(&conn)?[0];
|
let user = &post.get_authors(&conn)?[0];
|
||||||
let reshare = Reshare::insert(&*conn, NewReshare::new(post, user))?;
|
let reshare = Reshare::insert(&conn, NewReshare::new(post, user))?;
|
||||||
let act = reshare.to_activity(&conn).unwrap();
|
let act = reshare.to_activity(&conn).unwrap();
|
||||||
|
|
||||||
let expected = json!({
|
let expected = json!({
|
||||||
@@ -253,8 +253,8 @@ mod test {
|
|||||||
let (posts, _users, _blogs) = fill_database(&conn);
|
let (posts, _users, _blogs) = fill_database(&conn);
|
||||||
let post = &posts[0];
|
let post = &posts[0];
|
||||||
let user = &post.get_authors(&conn)?[0];
|
let user = &post.get_authors(&conn)?[0];
|
||||||
let reshare = Reshare::insert(&*conn, NewReshare::new(post, user))?;
|
let reshare = Reshare::insert(&conn, NewReshare::new(post, user))?;
|
||||||
let act = reshare.build_undo(&*conn)?;
|
let act = reshare.build_undo(&conn)?;
|
||||||
|
|
||||||
let expected = json!({
|
let expected = json!({
|
||||||
"actor": "https://plu.me/@/admin/",
|
"actor": "https://plu.me/@/admin/",
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ fn url_add_prefix(url: &str) -> Option<Cow<'_, str>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, AsExpression, FromSqlRow, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, AsExpression, FromSqlRow, Default)]
|
||||||
#[sql_type = "Text"]
|
#[sql_type = "Text"]
|
||||||
pub struct SafeString {
|
pub struct SafeString {
|
||||||
value: String,
|
value: String,
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ mod tests {
|
|||||||
|
|
||||||
let searcher = Arc::new(get_searcher(&CONFIG.search_tokenizers));
|
let searcher = Arc::new(get_searcher(&CONFIG.search_tokenizers));
|
||||||
SearchActor::init(searcher.clone(), db_pool.clone());
|
SearchActor::init(searcher.clone(), db_pool.clone());
|
||||||
let conn = db_pool.clone().get().unwrap();
|
let conn = db_pool.get().unwrap();
|
||||||
|
|
||||||
let title = random_hex()[..8].to_owned();
|
let title = random_hex()[..8].to_owned();
|
||||||
let (_instance, _user, blog) = fill_database(&conn);
|
let (_instance, _user, blog) = fill_database(&conn);
|
||||||
@@ -161,41 +161,43 @@ mod tests {
|
|||||||
long_description_html: "<p>Good morning</p>".to_string(),
|
long_description_html: "<p>Good morning</p>".to_string(),
|
||||||
short_description: SafeString::new("Hello"),
|
short_description: SafeString::new("Hello"),
|
||||||
short_description_html: "<p>Hello</p>".to_string(),
|
short_description_html: "<p>Hello</p>".to_string(),
|
||||||
name: random_hex().to_string(),
|
name: random_hex(),
|
||||||
open_registrations: true,
|
open_registrations: true,
|
||||||
public_domain: random_hex().to_string(),
|
public_domain: random_hex(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let user = User::insert(
|
let user = User::insert(
|
||||||
conn,
|
conn,
|
||||||
NewUser {
|
NewUser {
|
||||||
username: random_hex().to_string(),
|
username: random_hex(),
|
||||||
display_name: random_hex().to_string(),
|
display_name: random_hex(),
|
||||||
outbox_url: random_hex().to_string(),
|
outbox_url: random_hex(),
|
||||||
inbox_url: random_hex().to_string(),
|
inbox_url: random_hex(),
|
||||||
summary: "".to_string(),
|
summary: "".to_string(),
|
||||||
email: None,
|
email: None,
|
||||||
hashed_password: None,
|
hashed_password: None,
|
||||||
instance_id: instance.id,
|
instance_id: instance.id,
|
||||||
ap_url: random_hex().to_string(),
|
ap_url: random_hex(),
|
||||||
private_key: None,
|
private_key: None,
|
||||||
public_key: "".to_string(),
|
public_key: "".to_string(),
|
||||||
shared_inbox_url: None,
|
shared_inbox_url: None,
|
||||||
followers_endpoint: random_hex().to_string(),
|
followers_endpoint: random_hex(),
|
||||||
avatar_id: None,
|
avatar_id: None,
|
||||||
summary_html: SafeString::new(""),
|
summary_html: SafeString::new(""),
|
||||||
role: 0,
|
role: 0,
|
||||||
fqn: random_hex().to_string(),
|
fqn: random_hex(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mut blog = NewBlog::default();
|
let blog = NewBlog {
|
||||||
blog.instance_id = instance.id;
|
instance_id: instance.id,
|
||||||
blog.actor_id = random_hex().to_string();
|
actor_id: random_hex(),
|
||||||
blog.ap_url = random_hex().to_string();
|
ap_url: random_hex(),
|
||||||
blog.inbox_url = random_hex().to_string();
|
inbox_url: random_hex(),
|
||||||
blog.outbox_url = random_hex().to_string();
|
outbox_url: random_hex(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
let blog = Blog::insert(conn, blog).unwrap();
|
let blog = Blog::insert(conn, blog).unwrap();
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
conn,
|
conn,
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ pub(crate) mod tests {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
searcher.add_document(&conn, &post).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,
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ macro_rules! gen_to_string {
|
|||||||
)*
|
)*
|
||||||
$(
|
$(
|
||||||
for val in &$self.$date {
|
for val in &$self.$date {
|
||||||
$result.push_str(&format!("{}:{} ", stringify!($date), NaiveDate::from_num_days_from_ce(*val as i32).format("%Y-%m-%d")));
|
$result.push_str(&format!("{}:{} ", stringify!($date), NaiveDate::from_num_days_from_ce_opt(*val as i32).unwrap().format("%Y-%m-%d")));
|
||||||
}
|
}
|
||||||
)*
|
)*
|
||||||
}
|
}
|
||||||
@@ -180,12 +180,16 @@ impl PlumeQuery {
|
|||||||
|
|
||||||
if self.before.is_some() || self.after.is_some() {
|
if self.before.is_some() || self.after.is_some() {
|
||||||
// if at least one range bound is provided
|
// if at least one range bound is provided
|
||||||
let after = self
|
let after = self.after.unwrap_or_else(|| {
|
||||||
.after
|
i64::from(
|
||||||
.unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce()));
|
NaiveDate::from_ymd_opt(2000, 1, 1)
|
||||||
|
.unwrap()
|
||||||
|
.num_days_from_ce(),
|
||||||
|
)
|
||||||
|
});
|
||||||
let before = self
|
let before = self
|
||||||
.before
|
.before
|
||||||
.unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce()));
|
.unwrap_or_else(|| i64::from(Utc::now().date_naive().num_days_from_ce()));
|
||||||
let field = Searcher::schema().get_field("creation_date").unwrap();
|
let field = Searcher::schema().get_field("creation_date").unwrap();
|
||||||
let range =
|
let range =
|
||||||
RangeQuery::new_i64_bounds(field, Bound::Included(after), Bound::Included(before));
|
RangeQuery::new_i64_bounds(field, Bound::Included(after), Bound::Included(before));
|
||||||
@@ -202,16 +206,20 @@ impl PlumeQuery {
|
|||||||
pub fn before<D: Datelike>(&mut self, date: &D) -> &mut Self {
|
pub fn before<D: Datelike>(&mut self, date: &D) -> &mut Self {
|
||||||
let before = self
|
let before = self
|
||||||
.before
|
.before
|
||||||
.unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce()));
|
.unwrap_or_else(|| i64::from(Utc::now().date_naive().num_days_from_ce()));
|
||||||
self.before = Some(cmp::min(before, i64::from(date.num_days_from_ce())));
|
self.before = Some(cmp::min(before, i64::from(date.num_days_from_ce())));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// documents older than the provided date will be ignored
|
// documents older than the provided date will be ignored
|
||||||
pub fn after<D: Datelike>(&mut self, date: &D) -> &mut Self {
|
pub fn after<D: Datelike>(&mut self, date: &D) -> &mut Self {
|
||||||
let after = self
|
let after = self.after.unwrap_or_else(|| {
|
||||||
.after
|
i64::from(
|
||||||
.unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce()));
|
NaiveDate::from_ymd_opt(2000, 1, 1)
|
||||||
|
.unwrap()
|
||||||
|
.num_days_from_ce(),
|
||||||
|
)
|
||||||
|
});
|
||||||
self.after = Some(cmp::max(after, i64::from(date.num_days_from_ce())));
|
self.after = Some(cmp::max(after, i64::from(date.num_days_from_ce())));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ mod tests {
|
|||||||
let (posts, _users, _blogs) = fill_database(conn);
|
let (posts, _users, _blogs) = fill_database(conn);
|
||||||
let post_id = posts[0].id;
|
let post_id = posts[0].id;
|
||||||
let mut ht = Hashtag::new();
|
let mut ht = Hashtag::new();
|
||||||
ht.set_href(ap_url(&format!("https://plu.me/tag/a_tag")).parse::<IriString>()?);
|
ht.set_href(ap_url("https://plu.me/tag/a_tag").parse::<IriString>()?);
|
||||||
ht.set_name("a_tag".to_string());
|
ht.set_name("a_tag".to_string());
|
||||||
let tag = Tag::from_activity(conn, &ht, post_id, true)?;
|
let tag = Tag::from_activity(conn, &ht, post_id, true)?;
|
||||||
|
|
||||||
|
|||||||
+132
-123
@@ -1,5 +1,4 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
db_conn::DbConn,
|
|
||||||
lists::List,
|
lists::List,
|
||||||
posts::Post,
|
posts::Post,
|
||||||
schema::{posts, timeline, timeline_definition},
|
schema::{posts, timeline, timeline_definition},
|
||||||
@@ -12,9 +11,9 @@ use std::ops::Deref;
|
|||||||
pub(crate) mod query;
|
pub(crate) mod query;
|
||||||
|
|
||||||
pub use self::query::Kind;
|
pub use self::query::Kind;
|
||||||
use self::query::{QueryError, TimelineQuery};
|
pub use self::query::{QueryError, TimelineQuery};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Queryable, Identifiable, AsChangeset)]
|
#[derive(Clone, Debug, PartialEq, Eq, Queryable, Identifiable, AsChangeset)]
|
||||||
#[table_name = "timeline_definition"]
|
#[table_name = "timeline_definition"]
|
||||||
pub struct Timeline {
|
pub struct Timeline {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
@@ -220,9 +219,10 @@ impl Timeline {
|
|||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_to_all_timelines(conn: &DbConn, post: &Post, kind: Kind<'_>) -> Result<()> {
|
pub fn add_to_all_timelines(conn: &Connection, post: &Post, kind: Kind<'_>) -> Result<()> {
|
||||||
let timelines = timeline_definition::table
|
let timelines = timeline_definition::table
|
||||||
.load::<Self>(conn.deref())
|
//.load::<Self>(conn.deref())
|
||||||
|
.load::<Self>(conn)
|
||||||
.map_err(Error::from)?;
|
.map_err(Error::from)?;
|
||||||
|
|
||||||
for t in timelines {
|
for t in timelines {
|
||||||
@@ -246,7 +246,26 @@ impl Timeline {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn matches(&self, conn: &DbConn, post: &Post, kind: Kind<'_>) -> Result<bool> {
|
pub fn remove_post(&self, conn: &Connection, post: &Post) -> Result<bool> {
|
||||||
|
if self.includes_post(conn, post)? {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
diesel::delete(
|
||||||
|
timeline::table
|
||||||
|
.filter(timeline::timeline_id.eq(self.id))
|
||||||
|
.filter(timeline::post_id.eq(post.id)),
|
||||||
|
)
|
||||||
|
.execute(conn)?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_all_posts(&self, conn: &Connection) -> Result<u64> {
|
||||||
|
let count = diesel::delete(timeline::table.filter(timeline::timeline_id.eq(self.id)))
|
||||||
|
.execute(conn)?;
|
||||||
|
Ok(count as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn matches(&self, conn: &Connection, post: &Post, kind: Kind<'_>) -> Result<bool> {
|
||||||
let query = TimelineQuery::parse(&self.query)?;
|
let query = TimelineQuery::parse(&self.query)?;
|
||||||
query.matches(conn, self, post, kind)
|
query.matches(conn, self, post, kind)
|
||||||
}
|
}
|
||||||
@@ -282,73 +301,63 @@ mod tests {
|
|||||||
fn test_timeline() {
|
fn test_timeline() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let users = userTests::fill_database(&conn);
|
let users = userTests::fill_database(conn);
|
||||||
|
|
||||||
let mut tl1_u1 = Timeline::new_for_user(
|
let mut tl1_u1 = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"my timeline".to_owned(),
|
"my timeline".to_owned(),
|
||||||
"all".to_owned(),
|
"all".to_owned(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
List::new(
|
List::new(conn, "languages I speak", Some(&users[1]), ListType::Prefix).unwrap();
|
||||||
&conn,
|
|
||||||
"languages I speak",
|
|
||||||
Some(&users[1]),
|
|
||||||
ListType::Prefix,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let tl2_u1 = Timeline::new_for_user(
|
let tl2_u1 = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"another timeline".to_owned(),
|
"another timeline".to_owned(),
|
||||||
"followed".to_owned(),
|
"followed".to_owned(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let tl1_u2 = Timeline::new_for_user(
|
let tl1_u2 = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[1].id,
|
users[1].id,
|
||||||
"english posts".to_owned(),
|
"english posts".to_owned(),
|
||||||
"lang in \"languages I speak\"".to_owned(),
|
"lang in \"languages I speak\"".to_owned(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let tl1_instance = Timeline::new_for_instance(
|
let tl1_instance = Timeline::new_for_instance(
|
||||||
&conn,
|
conn,
|
||||||
"english posts".to_owned(),
|
"english posts".to_owned(),
|
||||||
"license in [cc]".to_owned(),
|
"license in [cc]".to_owned(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(tl1_u1, Timeline::get(&conn, tl1_u1.id).unwrap());
|
assert_eq!(tl1_u1, Timeline::get(conn, tl1_u1.id).unwrap());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tl2_u1,
|
tl2_u1,
|
||||||
Timeline::find_for_user_by_name(&conn, Some(users[0].id), "another timeline")
|
Timeline::find_for_user_by_name(conn, Some(users[0].id), "another timeline")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tl1_instance,
|
tl1_instance,
|
||||||
Timeline::find_for_user_by_name(&conn, None, "english posts").unwrap()
|
Timeline::find_for_user_by_name(conn, None, "english posts").unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
let tl_u1 = Timeline::list_for_user(&conn, Some(users[0].id)).unwrap();
|
let tl_u1 = Timeline::list_for_user(conn, Some(users[0].id)).unwrap();
|
||||||
assert_eq!(3, tl_u1.len()); // it is not 2 because there is a "Your feed" tl created for each user automatically
|
assert_eq!(3, tl_u1.len()); // it is not 2 because there is a "Your feed" tl created for each user automatically
|
||||||
assert!(tl_u1.iter().fold(false, |res, tl| { res || *tl == tl1_u1 }));
|
assert!(tl_u1.iter().any(|tl| *tl == tl1_u1));
|
||||||
assert!(tl_u1.iter().fold(false, |res, tl| { res || *tl == tl2_u1 }));
|
assert!(tl_u1.iter().any(|tl| *tl == tl2_u1));
|
||||||
|
|
||||||
let tl_instance = Timeline::list_for_user(&conn, None).unwrap();
|
let tl_instance = Timeline::list_for_user(conn, None).unwrap();
|
||||||
assert_eq!(3, tl_instance.len()); // there are also the local and federated feed by default
|
assert_eq!(3, tl_instance.len()); // there are also the local and federated feed by default
|
||||||
assert!(tl_instance
|
assert!(tl_instance.iter().any(|tl| *tl == tl1_instance));
|
||||||
.iter()
|
|
||||||
.fold(false, |res, tl| { res || *tl == tl1_instance }));
|
|
||||||
|
|
||||||
tl1_u1.name = "My Super TL".to_owned();
|
tl1_u1.name = "My Super TL".to_owned();
|
||||||
let new_tl1_u2 = tl1_u2.update(&conn).unwrap();
|
let new_tl1_u2 = tl1_u2.update(conn).unwrap();
|
||||||
|
|
||||||
let tl_u2 = Timeline::list_for_user(&conn, Some(users[1].id)).unwrap();
|
let tl_u2 = Timeline::list_for_user(conn, Some(users[1].id)).unwrap();
|
||||||
assert_eq!(2, tl_u2.len()); // same here
|
assert_eq!(2, tl_u2.len()); // same here
|
||||||
assert!(tl_u2
|
assert!(tl_u2.iter().any(|tl| *tl == new_tl1_u2));
|
||||||
.iter()
|
|
||||||
.fold(false, |res, tl| { res || *tl == new_tl1_u2 }));
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
@@ -358,48 +367,48 @@ mod tests {
|
|||||||
fn test_timeline_creation_error() {
|
fn test_timeline_creation_error() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let users = userTests::fill_database(&conn);
|
let users = userTests::fill_database(conn);
|
||||||
|
|
||||||
assert!(Timeline::new_for_user(
|
assert!(Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"my timeline".to_owned(),
|
"my timeline".to_owned(),
|
||||||
"invalid keyword".to_owned(),
|
"invalid keyword".to_owned(),
|
||||||
)
|
)
|
||||||
.is_err());
|
.is_err());
|
||||||
assert!(Timeline::new_for_instance(
|
assert!(Timeline::new_for_instance(
|
||||||
&conn,
|
conn,
|
||||||
"my timeline".to_owned(),
|
"my timeline".to_owned(),
|
||||||
"invalid keyword".to_owned(),
|
"invalid keyword".to_owned(),
|
||||||
)
|
)
|
||||||
.is_err());
|
.is_err());
|
||||||
|
|
||||||
assert!(Timeline::new_for_user(
|
assert!(Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"my timeline".to_owned(),
|
"my timeline".to_owned(),
|
||||||
"author in non_existant_list".to_owned(),
|
"author in non_existant_list".to_owned(),
|
||||||
)
|
)
|
||||||
.is_err());
|
.is_err());
|
||||||
assert!(Timeline::new_for_instance(
|
assert!(Timeline::new_for_instance(
|
||||||
&conn,
|
conn,
|
||||||
"my timeline".to_owned(),
|
"my timeline".to_owned(),
|
||||||
"lang in dont-exist".to_owned(),
|
"lang in dont-exist".to_owned(),
|
||||||
)
|
)
|
||||||
.is_err());
|
.is_err());
|
||||||
|
|
||||||
List::new(&conn, "friends", Some(&users[0]), ListType::User).unwrap();
|
List::new(conn, "friends", Some(&users[0]), ListType::User).unwrap();
|
||||||
List::new(&conn, "idk", None, ListType::Blog).unwrap();
|
List::new(conn, "idk", None, ListType::Blog).unwrap();
|
||||||
|
|
||||||
assert!(Timeline::new_for_user(
|
assert!(Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"my timeline".to_owned(),
|
"my timeline".to_owned(),
|
||||||
"blog in friends".to_owned(),
|
"blog in friends".to_owned(),
|
||||||
)
|
)
|
||||||
.is_err());
|
.is_err());
|
||||||
assert!(Timeline::new_for_instance(
|
assert!(Timeline::new_for_instance(
|
||||||
&conn,
|
conn,
|
||||||
"my timeline".to_owned(),
|
"my timeline".to_owned(),
|
||||||
"not author in idk".to_owned(),
|
"not author in idk".to_owned(),
|
||||||
)
|
)
|
||||||
@@ -413,10 +422,10 @@ mod tests {
|
|||||||
fn test_simple_match() {
|
fn test_simple_match() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let (users, blogs) = blogTests::fill_database(&conn);
|
let (users, blogs) = blogTests::fill_database(conn);
|
||||||
|
|
||||||
let gnu_tl = Timeline::new_for_user(
|
let gnu_tl = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"GNU timeline".to_owned(),
|
"GNU timeline".to_owned(),
|
||||||
"license in [AGPL, LGPL, GPL]".to_owned(),
|
"license in [AGPL, LGPL, GPL]".to_owned(),
|
||||||
@@ -424,7 +433,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let gnu_post = Post::insert(
|
let gnu_post = Post::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewPost {
|
NewPost {
|
||||||
blog_id: blogs[0].id,
|
blog_id: blogs[0].id,
|
||||||
slug: "slug".to_string(),
|
slug: "slug".to_string(),
|
||||||
@@ -440,10 +449,10 @@ mod tests {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(gnu_tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
assert!(gnu_tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||||
|
|
||||||
let non_free_post = Post::insert(
|
let non_free_post = Post::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewPost {
|
NewPost {
|
||||||
blog_id: blogs[0].id,
|
blog_id: blogs[0].id,
|
||||||
slug: "slug2".to_string(),
|
slug: "slug2".to_string(),
|
||||||
@@ -460,7 +469,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!gnu_tl
|
assert!(!gnu_tl
|
||||||
.matches(&conn, &non_free_post, Kind::Original)
|
.matches(conn, &non_free_post, Kind::Original)
|
||||||
.unwrap());
|
.unwrap());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -471,9 +480,9 @@ mod tests {
|
|||||||
fn test_complex_match() {
|
fn test_complex_match() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let (users, blogs) = blogTests::fill_database(&conn);
|
let (users, blogs) = blogTests::fill_database(conn);
|
||||||
Follow::insert(
|
Follow::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewFollow {
|
NewFollow {
|
||||||
follower_id: users[0].id,
|
follower_id: users[0].id,
|
||||||
following_id: users[1].id,
|
following_id: users[1].id,
|
||||||
@@ -483,11 +492,11 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let fav_blogs_list =
|
let fav_blogs_list =
|
||||||
List::new(&conn, "fav_blogs", Some(&users[0]), ListType::Blog).unwrap();
|
List::new(conn, "fav_blogs", Some(&users[0]), ListType::Blog).unwrap();
|
||||||
fav_blogs_list.add_blogs(&conn, &[blogs[0].id]).unwrap();
|
fav_blogs_list.add_blogs(conn, &[blogs[0].id]).unwrap();
|
||||||
|
|
||||||
let my_tl = Timeline::new_for_user(
|
let my_tl = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"My timeline".to_owned(),
|
"My timeline".to_owned(),
|
||||||
"blog in fav_blogs and not has_cover or local and followed exclude likes"
|
"blog in fav_blogs and not has_cover or local and followed exclude likes"
|
||||||
@@ -496,7 +505,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let post = Post::insert(
|
let post = Post::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewPost {
|
NewPost {
|
||||||
blog_id: blogs[0].id,
|
blog_id: blogs[0].id,
|
||||||
slug: "about-linux".to_string(),
|
slug: "about-linux".to_string(),
|
||||||
@@ -512,10 +521,10 @@ mod tests {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(my_tl.matches(&conn, &post, Kind::Original).unwrap()); // matches because of "blog in fav_blogs" (and there is no cover)
|
assert!(my_tl.matches(conn, &post, Kind::Original).unwrap()); // matches because of "blog in fav_blogs" (and there is no cover)
|
||||||
|
|
||||||
let post = Post::insert(
|
let post = Post::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewPost {
|
NewPost {
|
||||||
blog_id: blogs[1].id,
|
blog_id: blogs[1].id,
|
||||||
slug: "about-linux-2".to_string(),
|
slug: "about-linux-2".to_string(),
|
||||||
@@ -533,7 +542,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!my_tl.matches(&conn, &post, Kind::Like(&users[1])).unwrap());
|
assert!(!my_tl.matches(conn, &post, Kind::Like(&users[1])).unwrap());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
@@ -543,17 +552,17 @@ mod tests {
|
|||||||
fn test_add_to_all_timelines() {
|
fn test_add_to_all_timelines() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let (users, blogs) = blogTests::fill_database(&conn);
|
let (users, blogs) = blogTests::fill_database(conn);
|
||||||
|
|
||||||
let gnu_tl = Timeline::new_for_user(
|
let gnu_tl = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"GNU timeline".to_owned(),
|
"GNU timeline".to_owned(),
|
||||||
"license in [AGPL, LGPL, GPL]".to_owned(),
|
"license in [AGPL, LGPL, GPL]".to_owned(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let non_gnu_tl = Timeline::new_for_user(
|
let non_gnu_tl = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"Stallman disapproved timeline".to_owned(),
|
"Stallman disapproved timeline".to_owned(),
|
||||||
"not license in [AGPL, LGPL, GPL]".to_owned(),
|
"not license in [AGPL, LGPL, GPL]".to_owned(),
|
||||||
@@ -561,7 +570,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let gnu_post = Post::insert(
|
let gnu_post = Post::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewPost {
|
NewPost {
|
||||||
blog_id: blogs[0].id,
|
blog_id: blogs[0].id,
|
||||||
slug: "slug".to_string(),
|
slug: "slug".to_string(),
|
||||||
@@ -579,7 +588,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let non_free_post = Post::insert(
|
let non_free_post = Post::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewPost {
|
NewPost {
|
||||||
blog_id: blogs[0].id,
|
blog_id: blogs[0].id,
|
||||||
slug: "slug2".to_string(),
|
slug: "slug2".to_string(),
|
||||||
@@ -596,13 +605,13 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
Timeline::add_to_all_timelines(&conn, &gnu_post, Kind::Original).unwrap();
|
Timeline::add_to_all_timelines(conn, &gnu_post, Kind::Original).unwrap();
|
||||||
Timeline::add_to_all_timelines(&conn, &non_free_post, Kind::Original).unwrap();
|
Timeline::add_to_all_timelines(conn, &non_free_post, Kind::Original).unwrap();
|
||||||
|
|
||||||
let res = gnu_tl.get_latest(&conn, 2).unwrap();
|
let res = gnu_tl.get_latest(conn, 2).unwrap();
|
||||||
assert_eq!(res.len(), 1);
|
assert_eq!(res.len(), 1);
|
||||||
assert_eq!(res[0].id, gnu_post.id);
|
assert_eq!(res[0].id, gnu_post.id);
|
||||||
let res = non_gnu_tl.get_latest(&conn, 2).unwrap();
|
let res = non_gnu_tl.get_latest(conn, 2).unwrap();
|
||||||
assert_eq!(res.len(), 1);
|
assert_eq!(res.len(), 1);
|
||||||
assert_eq!(res[0].id, non_free_post.id);
|
assert_eq!(res[0].id, non_free_post.id);
|
||||||
|
|
||||||
@@ -614,10 +623,10 @@ mod tests {
|
|||||||
fn test_matches_lists_direct() {
|
fn test_matches_lists_direct() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let (users, blogs) = blogTests::fill_database(&conn);
|
let (users, blogs) = blogTests::fill_database(conn);
|
||||||
|
|
||||||
let gnu_post = Post::insert(
|
let gnu_post = Post::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewPost {
|
NewPost {
|
||||||
blog_id: blogs[0].id,
|
blog_id: blogs[0].id,
|
||||||
slug: "slug".to_string(),
|
slug: "slug".to_string(),
|
||||||
@@ -634,63 +643,63 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
gnu_post
|
gnu_post
|
||||||
.update_tags(&conn, vec![Tag::build_activity("free".to_owned()).unwrap()])
|
.update_tags(conn, vec![Tag::build_activity("free".to_owned()).unwrap()])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
PostAuthor::insert(
|
PostAuthor::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewPostAuthor {
|
NewPostAuthor {
|
||||||
post_id: gnu_post.id,
|
post_id: gnu_post.id,
|
||||||
author_id: blogs[0].list_authors(&conn).unwrap()[0].id,
|
author_id: blogs[0].list_authors(conn).unwrap()[0].id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let tl = Timeline::new_for_user(
|
let tl = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"blog timeline".to_owned(),
|
"blog timeline".to_owned(),
|
||||||
format!("blog in [{}]", blogs[0].fqn),
|
format!("blog in [{}]", blogs[0].fqn),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||||
tl.delete(&conn).unwrap();
|
tl.delete(conn).unwrap();
|
||||||
let tl = Timeline::new_for_user(
|
let tl = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"blog timeline".to_owned(),
|
"blog timeline".to_owned(),
|
||||||
"blog in [no_one@nowhere]".to_owned(),
|
"blog in [no_one@nowhere]".to_owned(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||||
tl.delete(&conn).unwrap();
|
tl.delete(conn).unwrap();
|
||||||
|
|
||||||
let tl = Timeline::new_for_user(
|
let tl = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"author timeline".to_owned(),
|
"author timeline".to_owned(),
|
||||||
format!(
|
format!(
|
||||||
"author in [{}]",
|
"author in [{}]",
|
||||||
blogs[0].list_authors(&conn).unwrap()[0].fqn
|
blogs[0].list_authors(conn).unwrap()[0].fqn
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||||
tl.delete(&conn).unwrap();
|
tl.delete(conn).unwrap();
|
||||||
let tl = Timeline::new_for_user(
|
let tl = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"author timeline".to_owned(),
|
"author timeline".to_owned(),
|
||||||
format!("author in [{}]", users[2].fqn),
|
format!("author in [{}]", users[2].fqn),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||||
assert!(tl
|
assert!(tl
|
||||||
.matches(&conn, &gnu_post, Kind::Reshare(&users[2]))
|
.matches(conn, &gnu_post, Kind::Reshare(&users[2]))
|
||||||
.unwrap());
|
.unwrap());
|
||||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Like(&users[2])).unwrap());
|
assert!(!tl.matches(conn, &gnu_post, Kind::Like(&users[2])).unwrap());
|
||||||
tl.delete(&conn).unwrap();
|
tl.delete(conn).unwrap();
|
||||||
let tl = Timeline::new_for_user(
|
let tl = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"author timeline".to_owned(),
|
"author timeline".to_owned(),
|
||||||
format!(
|
format!(
|
||||||
@@ -699,50 +708,50 @@ mod tests {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||||
assert!(!tl
|
assert!(!tl
|
||||||
.matches(&conn, &gnu_post, Kind::Reshare(&users[2]))
|
.matches(conn, &gnu_post, Kind::Reshare(&users[2]))
|
||||||
.unwrap());
|
.unwrap());
|
||||||
assert!(tl.matches(&conn, &gnu_post, Kind::Like(&users[2])).unwrap());
|
assert!(tl.matches(conn, &gnu_post, Kind::Like(&users[2])).unwrap());
|
||||||
tl.delete(&conn).unwrap();
|
tl.delete(conn).unwrap();
|
||||||
|
|
||||||
let tl = Timeline::new_for_user(
|
let tl = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"tag timeline".to_owned(),
|
"tag timeline".to_owned(),
|
||||||
"tags in [free]".to_owned(),
|
"tags in [free]".to_owned(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||||
tl.delete(&conn).unwrap();
|
tl.delete(conn).unwrap();
|
||||||
let tl = Timeline::new_for_user(
|
let tl = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"tag timeline".to_owned(),
|
"tag timeline".to_owned(),
|
||||||
"tags in [private]".to_owned(),
|
"tags in [private]".to_owned(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||||
tl.delete(&conn).unwrap();
|
tl.delete(conn).unwrap();
|
||||||
|
|
||||||
let tl = Timeline::new_for_user(
|
let tl = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"english timeline".to_owned(),
|
"english timeline".to_owned(),
|
||||||
"lang in [en]".to_owned(),
|
"lang in [en]".to_owned(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||||
tl.delete(&conn).unwrap();
|
tl.delete(conn).unwrap();
|
||||||
let tl = Timeline::new_for_user(
|
let tl = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"franco-italian timeline".to_owned(),
|
"franco-italian timeline".to_owned(),
|
||||||
"lang in [fr, it]".to_owned(),
|
"lang in [fr, it]".to_owned(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||||
tl.delete(&conn).unwrap();
|
tl.delete(conn).unwrap();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
@@ -786,10 +795,10 @@ mod tests {
|
|||||||
fn test_matches_keyword() {
|
fn test_matches_keyword() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let (users, blogs) = blogTests::fill_database(&conn);
|
let (users, blogs) = blogTests::fill_database(conn);
|
||||||
|
|
||||||
let gnu_post = Post::insert(
|
let gnu_post = Post::insert(
|
||||||
&conn,
|
conn,
|
||||||
NewPost {
|
NewPost {
|
||||||
blog_id: blogs[0].id,
|
blog_id: blogs[0].id,
|
||||||
slug: "slug".to_string(),
|
slug: "slug".to_string(),
|
||||||
@@ -807,61 +816,61 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let tl = Timeline::new_for_user(
|
let tl = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"Linux title".to_owned(),
|
"Linux title".to_owned(),
|
||||||
"title contains Linux".to_owned(),
|
"title contains Linux".to_owned(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||||
tl.delete(&conn).unwrap();
|
tl.delete(conn).unwrap();
|
||||||
let tl = Timeline::new_for_user(
|
let tl = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"Microsoft title".to_owned(),
|
"Microsoft title".to_owned(),
|
||||||
"title contains Microsoft".to_owned(),
|
"title contains Microsoft".to_owned(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||||
tl.delete(&conn).unwrap();
|
tl.delete(conn).unwrap();
|
||||||
|
|
||||||
let tl = Timeline::new_for_user(
|
let tl = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"Linux subtitle".to_owned(),
|
"Linux subtitle".to_owned(),
|
||||||
"subtitle contains Stallman".to_owned(),
|
"subtitle contains Stallman".to_owned(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||||
tl.delete(&conn).unwrap();
|
tl.delete(conn).unwrap();
|
||||||
let tl = Timeline::new_for_user(
|
let tl = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"Microsoft subtitle".to_owned(),
|
"Microsoft subtitle".to_owned(),
|
||||||
"subtitle contains Nadella".to_owned(),
|
"subtitle contains Nadella".to_owned(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||||
tl.delete(&conn).unwrap();
|
tl.delete(conn).unwrap();
|
||||||
|
|
||||||
let tl = Timeline::new_for_user(
|
let tl = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"Linux content".to_owned(),
|
"Linux content".to_owned(),
|
||||||
"content contains Linux".to_owned(),
|
"content contains Linux".to_owned(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||||
tl.delete(&conn).unwrap();
|
tl.delete(conn).unwrap();
|
||||||
let tl = Timeline::new_for_user(
|
let tl = Timeline::new_for_user(
|
||||||
&conn,
|
conn,
|
||||||
users[0].id,
|
users[0].id,
|
||||||
"Microsoft content".to_owned(),
|
"Microsoft content".to_owned(),
|
||||||
"subtitle contains Windows".to_owned(),
|
"subtitle contains Windows".to_owned(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||||
tl.delete(&conn).unwrap();
|
tl.delete(conn).unwrap();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
blogs::Blog,
|
blogs::Blog,
|
||||||
db_conn::DbConn,
|
|
||||||
lists::{self, ListType},
|
lists::{self, ListType},
|
||||||
posts::Post,
|
posts::Post,
|
||||||
tags::Tag,
|
tags::Tag,
|
||||||
timeline::Timeline,
|
timeline::Timeline,
|
||||||
users::User,
|
users::User,
|
||||||
Result,
|
Connection, Result,
|
||||||
};
|
};
|
||||||
use plume_common::activity_pub::inbox::AsActor;
|
use plume_common::activity_pub::inbox::AsActor;
|
||||||
use whatlang::{self, Lang};
|
use whatlang::{self, Lang};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum QueryError {
|
pub enum QueryError {
|
||||||
SyntaxError(usize, usize, String),
|
SyntaxError(usize, usize, String),
|
||||||
UnexpectedEndOfQuery,
|
UnexpectedEndOfQuery,
|
||||||
@@ -20,7 +19,7 @@ pub enum QueryError {
|
|||||||
|
|
||||||
pub type QueryResult<T> = std::result::Result<T, QueryError>;
|
pub type QueryResult<T> = std::result::Result<T, QueryError>;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum Kind<'a> {
|
pub enum Kind<'a> {
|
||||||
Original,
|
Original,
|
||||||
Reshare(&'a User),
|
Reshare(&'a User),
|
||||||
@@ -155,7 +154,7 @@ enum TQ<'a> {
|
|||||||
impl<'a> TQ<'a> {
|
impl<'a> TQ<'a> {
|
||||||
fn matches(
|
fn matches(
|
||||||
&self,
|
&self,
|
||||||
conn: &DbConn,
|
conn: &Connection,
|
||||||
timeline: &Timeline,
|
timeline: &Timeline,
|
||||||
post: &Post,
|
post: &Post,
|
||||||
kind: Kind<'_>,
|
kind: Kind<'_>,
|
||||||
@@ -200,7 +199,7 @@ enum Arg<'a> {
|
|||||||
impl<'a> Arg<'a> {
|
impl<'a> Arg<'a> {
|
||||||
pub fn matches(
|
pub fn matches(
|
||||||
&self,
|
&self,
|
||||||
conn: &DbConn,
|
conn: &Connection,
|
||||||
timeline: &Timeline,
|
timeline: &Timeline,
|
||||||
post: &Post,
|
post: &Post,
|
||||||
kind: Kind<'_>,
|
kind: Kind<'_>,
|
||||||
@@ -225,7 +224,7 @@ enum WithList {
|
|||||||
impl WithList {
|
impl WithList {
|
||||||
pub fn matches(
|
pub fn matches(
|
||||||
&self,
|
&self,
|
||||||
conn: &DbConn,
|
conn: &Connection,
|
||||||
timeline: &Timeline,
|
timeline: &Timeline,
|
||||||
post: &Post,
|
post: &Post,
|
||||||
list: &List<'_>,
|
list: &List<'_>,
|
||||||
@@ -292,7 +291,7 @@ impl WithList {
|
|||||||
WithList::Author { boosts, likes } => match kind {
|
WithList::Author { boosts, likes } => match kind {
|
||||||
Kind::Original => Ok(list
|
Kind::Original => Ok(list
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|a| User::find_by_fqn(&*conn, a).ok())
|
.filter_map(|a| User::find_by_fqn(conn, a).ok())
|
||||||
.any(|a| post.is_author(conn, a.id).unwrap_or(false))),
|
.any(|a| post.is_author(conn, a.id).unwrap_or(false))),
|
||||||
Kind::Reshare(u) => {
|
Kind::Reshare(u) => {
|
||||||
if *boosts {
|
if *boosts {
|
||||||
@@ -361,7 +360,7 @@ enum Bool {
|
|||||||
impl Bool {
|
impl Bool {
|
||||||
pub fn matches(
|
pub fn matches(
|
||||||
&self,
|
&self,
|
||||||
conn: &DbConn,
|
conn: &Connection,
|
||||||
timeline: &Timeline,
|
timeline: &Timeline,
|
||||||
post: &Post,
|
post: &Post,
|
||||||
kind: Kind<'_>,
|
kind: Kind<'_>,
|
||||||
@@ -654,7 +653,7 @@ impl<'a> TimelineQuery<'a> {
|
|||||||
|
|
||||||
pub fn matches(
|
pub fn matches(
|
||||||
&self,
|
&self,
|
||||||
conn: &DbConn,
|
conn: &Connection,
|
||||||
timeline: &Timeline,
|
timeline: &Timeline,
|
||||||
post: &Post,
|
post: &Post,
|
||||||
kind: Kind<'_>,
|
kind: Kind<'_>,
|
||||||
|
|||||||
+135
-71
@@ -1,8 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
ap_url, blocklisted_emails::BlocklistedEmail, blogs::Blog, db_conn::DbConn, follows::Follow,
|
ap_url, blocklisted_emails::BlocklistedEmail, blogs::Blog, comments::Comment, db_conn::DbConn,
|
||||||
instance::*, medias::Media, notifications::Notification, post_authors::PostAuthor, posts::Post,
|
follows::Follow, instance::*, medias::Media, notifications::Notification,
|
||||||
safe_string::SafeString, schema::users, timeline::Timeline, Connection, Error, Result,
|
post_authors::PostAuthor, posts::Post, safe_string::SafeString, schema::users,
|
||||||
UserEvent::*, CONFIG, ITEMS_PER_PAGE, USER_CHAN,
|
timeline::Timeline, Connection, Error, Result, UserEvent::*, CONFIG, ITEMS_PER_PAGE, USER_CHAN,
|
||||||
};
|
};
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
activity::Delete,
|
activity::Delete,
|
||||||
@@ -15,7 +15,10 @@ use activitystreams::{
|
|||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl};
|
use diesel::{
|
||||||
|
self, BelongingToDsl, BoolExpressionMethods, ExpressionMethods, OptionalExtension, QueryDsl,
|
||||||
|
RunQueryDsl, TextExpressionMethods,
|
||||||
|
};
|
||||||
use ldap3::{LdapConn, Scope, SearchEntry};
|
use ldap3::{LdapConn, Scope, SearchEntry};
|
||||||
use openssl::{
|
use openssl::{
|
||||||
hash::MessageDigest,
|
hash::MessageDigest,
|
||||||
@@ -165,6 +168,14 @@ impl User {
|
|||||||
notif.delete(conn)?
|
notif.delete(conn)?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for comment in Comment::list_by_author(conn, self.id)? {
|
||||||
|
let delete_activity = comment.build_delete(conn)?;
|
||||||
|
crate::inbox::inbox(
|
||||||
|
conn,
|
||||||
|
serde_json::to_value(&delete_activity).map_err(Error::from)?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
diesel::delete(self)
|
diesel::delete(self)
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
@@ -186,15 +197,16 @@ impl User {
|
|||||||
pub fn count_local(conn: &Connection) -> Result<i64> {
|
pub fn count_local(conn: &Connection) -> Result<i64> {
|
||||||
users::table
|
users::table
|
||||||
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
||||||
|
.filter(users::role.ne(Role::Instance as i32))
|
||||||
.count()
|
.count()
|
||||||
.get_result(&*conn)
|
.get_result(conn)
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_fqn(conn: &DbConn, fqn: &str) -> Result<User> {
|
pub fn find_by_fqn(conn: &Connection, fqn: &str) -> Result<User> {
|
||||||
let from_db = users::table
|
let from_db = users::table
|
||||||
.filter(users::fqn.eq(fqn))
|
.filter(users::fqn.eq(fqn))
|
||||||
.first(&**conn)
|
.first(conn)
|
||||||
.optional()?;
|
.optional()?;
|
||||||
if let Some(from_db) = from_db {
|
if let Some(from_db) = from_db {
|
||||||
Ok(from_db)
|
Ok(from_db)
|
||||||
@@ -203,6 +215,27 @@ impl User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn search_local_by_name(
|
||||||
|
conn: &Connection,
|
||||||
|
name: &str,
|
||||||
|
(min, max): (i32, i32),
|
||||||
|
) -> Result<Vec<User>> {
|
||||||
|
users::table
|
||||||
|
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
||||||
|
.filter(users::role.ne(Role::Instance as i32))
|
||||||
|
// TODO: use `ilike` instead of `like` for PostgreSQL
|
||||||
|
.filter(
|
||||||
|
users::username
|
||||||
|
.like(format!("%{}%", name))
|
||||||
|
.or(users::display_name.like(format!("%{}%", name))),
|
||||||
|
)
|
||||||
|
.order(users::username.asc())
|
||||||
|
.offset(min.into())
|
||||||
|
.limit((max - min).into())
|
||||||
|
.load::<User>(conn)
|
||||||
|
.map_err(Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Should create user record with normalized(lowercased) email
|
* TODO: Should create user record with normalized(lowercased) email
|
||||||
*/
|
*/
|
||||||
@@ -219,7 +252,7 @@ impl User {
|
|||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result<User> {
|
fn fetch_from_webfinger(conn: &Connection, acct: &str) -> Result<User> {
|
||||||
let link = resolve(acct.to_owned(), true)?
|
let link = resolve(acct.to_owned(), true)?
|
||||||
.links
|
.links
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -246,20 +279,7 @@ impl User {
|
|||||||
fn fetch(url: &str) -> Result<CustomPerson> {
|
fn fetch(url: &str) -> Result<CustomPerson> {
|
||||||
let res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?;
|
let res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?;
|
||||||
let text = &res.text()?;
|
let text = &res.text()?;
|
||||||
// without this workaround, publicKey is not correctly deserialized
|
let json = serde_json::from_str::<CustomPerson>(text)?;
|
||||||
let ap_sign = serde_json::from_str::<ApSignature>(text)?;
|
|
||||||
let person = serde_json::from_str::<Person>(text)?;
|
|
||||||
let json = CustomPerson::new(
|
|
||||||
ApActor::new(
|
|
||||||
person
|
|
||||||
.clone()
|
|
||||||
.id_unchecked()
|
|
||||||
.ok_or(Error::MissingApProperty)?
|
|
||||||
.to_owned(),
|
|
||||||
person,
|
|
||||||
),
|
|
||||||
ap_sign,
|
|
||||||
); // FIXME: Don't clone()
|
|
||||||
Ok(json)
|
Ok(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,23 +289,13 @@ impl User {
|
|||||||
|
|
||||||
pub fn refetch(&self, conn: &Connection) -> Result<()> {
|
pub fn refetch(&self, conn: &Connection) -> Result<()> {
|
||||||
User::fetch(&self.ap_url.clone()).and_then(|json| {
|
User::fetch(&self.ap_url.clone()).and_then(|json| {
|
||||||
let avatar = Media::save_remote(
|
let avatar = json
|
||||||
conn,
|
.icon()
|
||||||
json.ap_actor_ref()
|
.and_then(|icon| icon.iter().next())
|
||||||
.icon()
|
.and_then(|i| i.clone().extend::<Image, ImageType>().ok())
|
||||||
.ok_or(Error::MissingApProperty)? // FIXME: Fails when icon is not set
|
.and_then(|image| image)
|
||||||
.iter()
|
.and_then(|image| image.id_unchecked().map(|url| url.to_string()))
|
||||||
.next()
|
.and_then(|url| Media::save_remote(conn, url, self).ok());
|
||||||
.and_then(|i| {
|
|
||||||
i.clone()
|
|
||||||
.extend::<Image, ImageType>() // FIXME: Don't clone()
|
|
||||||
.ok()?
|
|
||||||
.and_then(|url| Some(url.id_unchecked()?.to_string()))
|
|
||||||
})
|
|
||||||
.ok_or(Error::MissingApProperty)?,
|
|
||||||
self,
|
|
||||||
)
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
let pub_key = &json.ext_one.public_key.public_key_pem;
|
let pub_key = &json.ext_one.public_key.public_key_pem;
|
||||||
diesel::update(self)
|
diesel::update(self)
|
||||||
@@ -332,6 +342,42 @@ impl User {
|
|||||||
bcrypt::hash(pass, 10).map_err(Error::from)
|
bcrypt::hash(pass, 10).map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [[ LDAP non anonymous bind copied from ....
|
||||||
|
fn ldap_preconn(ldap_conn: &mut LdapConn) -> Result<()> {
|
||||||
|
let ldap = CONFIG.ldap.as_ref().unwrap();
|
||||||
|
|
||||||
|
if let Some((user, password)) = ldap.user.as_ref() {
|
||||||
|
let bind = ldap_conn
|
||||||
|
.simple_bind(user, password)
|
||||||
|
.map_err(|_| Error::NotFound)?;
|
||||||
|
|
||||||
|
if bind.success().is_err() {
|
||||||
|
return Err(Error::NotFound);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_dn(ldap_conn: &mut LdapConn, fil: &str) -> Result<String> {
|
||||||
|
let mut cn: String = String::new();
|
||||||
|
let ldap = CONFIG.ldap.as_ref().unwrap();
|
||||||
|
let (rs, _res) = ldap_conn.search(
|
||||||
|
&ldap.base_dn,
|
||||||
|
Scope::Subtree,
|
||||||
|
fil,
|
||||||
|
vec!["cn"]
|
||||||
|
)
|
||||||
|
.map_err(|_| Error::NotFound)?
|
||||||
|
.success()
|
||||||
|
.map_err(|_| Error::NotFound)?;
|
||||||
|
for entry in rs {
|
||||||
|
println!("{:?}", SearchEntry::construct(entry.clone()).dn);
|
||||||
|
cn = SearchEntry::construct(entry).dn;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(cn.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
fn ldap_register(conn: &Connection, name: &str, password: &str) -> Result<User> {
|
fn ldap_register(conn: &Connection, name: &str, password: &str) -> Result<User> {
|
||||||
if CONFIG.ldap.is_none() {
|
if CONFIG.ldap.is_none() {
|
||||||
return Err(Error::NotFound);
|
return Err(Error::NotFound);
|
||||||
@@ -339,7 +385,12 @@ impl User {
|
|||||||
let ldap = CONFIG.ldap.as_ref().unwrap();
|
let ldap = CONFIG.ldap.as_ref().unwrap();
|
||||||
|
|
||||||
let mut ldap_conn = LdapConn::new(&ldap.addr).map_err(|_| Error::NotFound)?;
|
let mut ldap_conn = LdapConn::new(&ldap.addr).map_err(|_| Error::NotFound)?;
|
||||||
let ldap_name = format!("{}={},{}", ldap.user_name_attr, name, ldap.base_dn);
|
|
||||||
|
User::ldap_preconn(&mut ldap_conn)?;
|
||||||
|
|
||||||
|
let _filter = format!("(&(objectClass=*)(uid={}))", name).to_owned();
|
||||||
|
let ldap_name = User::search_dn(&mut ldap_conn, &_filter).unwrap();
|
||||||
|
//let ldap_name = format!("{}={},{}", ldap.user_name_attr, name, ldap.base_dn);
|
||||||
let bind = ldap_conn
|
let bind = ldap_conn
|
||||||
.simple_bind(&ldap_name, password)
|
.simple_bind(&ldap_name, password)
|
||||||
.map_err(|_| Error::NotFound)?;
|
.map_err(|_| Error::NotFound)?;
|
||||||
@@ -385,10 +436,18 @@ impl User {
|
|||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
if User::ldap_preconn(&mut conn).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
//
|
||||||
|
let _filter = format!("(&(objectClass=*)(uid={}))", &self.username).to_owned();
|
||||||
|
let name = User::search_dn(&mut conn, &_filter).unwrap();
|
||||||
|
/*
|
||||||
let name = format!(
|
let name = format!(
|
||||||
"{}={},{}",
|
"{}={},{}",
|
||||||
ldap.user_name_attr, &self.username, ldap.base_dn
|
ldap.user_name_attr, &self.username, ldap.base_dn
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
if let Ok(bind) = conn.simple_bind(&name, password) {
|
if let Ok(bind) = conn.simple_bind(&name, password) {
|
||||||
bind.success().is_ok()
|
bind.success().is_ok()
|
||||||
} else {
|
} else {
|
||||||
@@ -443,7 +502,7 @@ impl User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ... ldap-non-anon PR https://git.joinplu.me/Plume/Plume/src/branch/ldap-non-anon ]].
|
||||||
pub fn reset_password(&self, conn: &Connection, pass: &str) -> Result<()> {
|
pub fn reset_password(&self, conn: &Connection, pass: &str) -> Result<()> {
|
||||||
diesel::update(self)
|
diesel::update(self)
|
||||||
.set(users::hashed_password.eq(User::hash_pass(pass)?))
|
.set(users::hashed_password.eq(User::hash_pass(pass)?))
|
||||||
@@ -454,6 +513,7 @@ impl User {
|
|||||||
pub fn get_local_page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<User>> {
|
pub fn get_local_page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<User>> {
|
||||||
users::table
|
users::table
|
||||||
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
||||||
|
.filter(users::role.ne(Role::Instance as i32))
|
||||||
.order(users::username.asc())
|
.order(users::username.asc())
|
||||||
.offset(min.into())
|
.offset(min.into())
|
||||||
.limit((max - min).into())
|
.limit((max - min).into())
|
||||||
@@ -931,7 +991,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for User {
|
|||||||
.cookies()
|
.cookies()
|
||||||
.get_private(AUTH_COOKIE)
|
.get_private(AUTH_COOKIE)
|
||||||
.and_then(|cookie| cookie.value().parse().ok())
|
.and_then(|cookie| cookie.value().parse().ok())
|
||||||
.and_then(|id| User::get(&*conn, id).ok())
|
.and_then(|id| User::get(&conn, id).ok())
|
||||||
.or_forward(())
|
.or_forward(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -944,15 +1004,15 @@ impl IntoId for User {
|
|||||||
|
|
||||||
impl Eq for User {}
|
impl Eq for User {}
|
||||||
|
|
||||||
impl FromId<DbConn> for User {
|
impl FromId<Connection> for User {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = CustomPerson;
|
type Object = CustomPerson;
|
||||||
|
|
||||||
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
fn from_db(conn: &Connection, id: &str) -> Result<Self> {
|
||||||
Self::find_by_ap_url(conn, id)
|
Self::find_by_ap_url(conn, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_activity(conn: &DbConn, acct: CustomPerson) -> Result<Self> {
|
fn from_activity(conn: &Connection, acct: CustomPerson) -> Result<Self> {
|
||||||
let actor = acct.ap_actor_ref();
|
let actor = acct.ap_actor_ref();
|
||||||
let username = actor
|
let username = actor
|
||||||
.preferred_username()
|
.preferred_username()
|
||||||
@@ -960,6 +1020,10 @@ impl FromId<DbConn> for User {
|
|||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
if username.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
|
if username.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
|
||||||
|
tracing::error!(
|
||||||
|
"preferredUsername includes invalid character(s): {}",
|
||||||
|
&username
|
||||||
|
);
|
||||||
return Err(Error::InvalidValue);
|
return Err(Error::InvalidValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1049,7 +1113,7 @@ impl FromId<DbConn> for User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsActor<&DbConn> for User {
|
impl AsActor<&Connection> for User {
|
||||||
fn get_inbox_url(&self) -> String {
|
fn get_inbox_url(&self) -> String {
|
||||||
self.inbox_url.clone()
|
self.inbox_url.clone()
|
||||||
}
|
}
|
||||||
@@ -1065,11 +1129,11 @@ impl AsActor<&DbConn> for User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Delete, &DbConn> for User {
|
impl AsObject<User, Delete, &Connection> for User {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
|
||||||
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> {
|
||||||
if self.id == actor.id {
|
if self.id == actor.id {
|
||||||
self.delete(conn).map(|_| ())
|
self.delete(conn).map(|_| ())
|
||||||
} else {
|
} else {
|
||||||
@@ -1232,7 +1296,7 @@ pub(crate) mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
other.avatar_id = Some(avatar.id);
|
other.avatar_id = Some(avatar.id);
|
||||||
let other = other.save_changes::<User>(&*conn).unwrap();
|
let other = other.save_changes::<User>(conn).unwrap();
|
||||||
|
|
||||||
vec![admin, user, other]
|
vec![admin, user, other]
|
||||||
}
|
}
|
||||||
@@ -1335,11 +1399,11 @@ pub(crate) mod tests {
|
|||||||
fn delete() {
|
fn delete() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let inserted = fill_database(&conn);
|
let inserted = fill_database(conn);
|
||||||
|
|
||||||
assert!(User::get(&conn, inserted[0].id).is_ok());
|
assert!(User::get(conn, inserted[0].id).is_ok());
|
||||||
inserted[0].delete(&conn).unwrap();
|
inserted[0].delete(conn).unwrap();
|
||||||
assert!(User::get(&conn, inserted[0].id).is_err());
|
assert!(User::get(conn, inserted[0].id).is_err());
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1348,20 +1412,20 @@ pub(crate) mod tests {
|
|||||||
fn admin() {
|
fn admin() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let inserted = fill_database(&conn);
|
let inserted = fill_database(conn);
|
||||||
let local_inst = Instance::get_local().unwrap();
|
let local_inst = Instance::get_local().unwrap();
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while local_inst.has_admin(&conn).unwrap() {
|
while local_inst.has_admin(conn).unwrap() {
|
||||||
assert!(i < 100); //prevent from looping indefinitelly
|
assert!(i < 100); //prevent from looping indefinitelly
|
||||||
local_inst
|
local_inst
|
||||||
.main_admin(&conn)
|
.main_admin(conn)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set_role(&conn, Role::Normal)
|
.set_role(conn, Role::Normal)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
inserted[0].set_role(&conn, Role::Admin).unwrap();
|
inserted[0].set_role(conn, Role::Admin).unwrap();
|
||||||
assert_eq!(inserted[0].id, local_inst.main_admin(&conn).unwrap().id);
|
assert_eq!(inserted[0].id, local_inst.main_admin(conn).unwrap().id);
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1370,9 +1434,9 @@ pub(crate) mod tests {
|
|||||||
fn auth() {
|
fn auth() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
fill_database(&conn);
|
fill_database(conn);
|
||||||
let test_user = NewUser::new_local(
|
let test_user = NewUser::new_local(
|
||||||
&conn,
|
conn,
|
||||||
"test".to_owned(),
|
"test".to_owned(),
|
||||||
"test user".to_owned(),
|
"test user".to_owned(),
|
||||||
Role::Normal,
|
Role::Normal,
|
||||||
@@ -1383,10 +1447,10 @@ pub(crate) mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
User::login(&conn, "test", "test_password").unwrap().id,
|
User::login(conn, "test", "test_password").unwrap().id,
|
||||||
test_user.id
|
test_user.id
|
||||||
);
|
);
|
||||||
assert!(User::login(&conn, "test", "other_password").is_err());
|
assert!(User::login(conn, "test", "other_password").is_err());
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1395,26 +1459,26 @@ pub(crate) mod tests {
|
|||||||
fn get_local_page() {
|
fn get_local_page() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
fill_database(&conn);
|
fill_database(conn);
|
||||||
|
|
||||||
let page = User::get_local_page(&conn, (0, 2)).unwrap();
|
let page = User::get_local_page(conn, (0, 2)).unwrap();
|
||||||
assert_eq!(page.len(), 2);
|
assert_eq!(page.len(), 2);
|
||||||
assert!(page[0].username <= page[1].username);
|
assert!(page[0].username <= page[1].username);
|
||||||
|
|
||||||
let mut last_username = User::get_local_page(&conn, (0, 1)).unwrap()[0]
|
let mut last_username = User::get_local_page(conn, (0, 1)).unwrap()[0]
|
||||||
.username
|
.username
|
||||||
.clone();
|
.clone();
|
||||||
for i in 1..User::count_local(&conn).unwrap() as i32 {
|
for i in 1..User::count_local(conn).unwrap() as i32 {
|
||||||
let page = User::get_local_page(&conn, (i, i + 1)).unwrap();
|
let page = User::get_local_page(conn, (i, i + 1)).unwrap();
|
||||||
assert_eq!(page.len(), 1);
|
assert_eq!(page.len(), 1);
|
||||||
assert!(last_username <= page[0].username);
|
assert!(last_username <= page[0].username);
|
||||||
last_username = page[0].username.clone();
|
last_username = page[0].username.clone();
|
||||||
}
|
}
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
User::get_local_page(&conn, (0, User::count_local(&conn).unwrap() as i32 + 10))
|
User::get_local_page(conn, (0, User::count_local(conn).unwrap() as i32 + 10))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.len() as i64,
|
.len() as i64,
|
||||||
User::count_local(&conn).unwrap()
|
User::count_local(conn).unwrap()
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
msgid ""
|
|
||||||
|
msgid "Do you want to load the local autosave last edited at {}?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: plume-front\n"
|
\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
|
||||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
@@ -12,9 +12,6 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
|
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
|
||||||
|
|
||||||
msgid "Do you want to load the local autosave last edited at {}?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Open the rich text editor"
|
msgid "Open the rich text editor"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
-1016
File diff suppressed because it is too large
Load Diff
+613
-692
File diff suppressed because it is too large
Load Diff
+729
-641
File diff suppressed because it is too large
Load Diff
+716
-643
File diff suppressed because it is too large
Load Diff
+672
-662
File diff suppressed because it is too large
Load Diff
-1088
File diff suppressed because it is too large
Load Diff
-1016
File diff suppressed because it is too large
Load Diff
+740
-632
File diff suppressed because it is too large
Load Diff
-1016
File diff suppressed because it is too large
Load Diff
-1016
File diff suppressed because it is too large
Load Diff
+259
-860
File diff suppressed because it is too large
Load Diff
+739
-632
File diff suppressed because it is too large
Load Diff
+730
-632
File diff suppressed because it is too large
Load Diff
+732
-632
File diff suppressed because it is too large
Load Diff
+129
-918
File diff suppressed because it is too large
Load Diff
+741
-632
File diff suppressed because it is too large
Load Diff
+730
-632
File diff suppressed because it is too large
Load Diff
+2
-1023
File diff suppressed because it is too large
Load Diff
+333
-829
File diff suppressed because it is too large
Load Diff
+85
-970
File diff suppressed because it is too large
Load Diff
-1016
File diff suppressed because it is too large
Load Diff
+720
-643
File diff suppressed because it is too large
Load Diff
+721
-631
File diff suppressed because it is too large
Load Diff
-1013
File diff suppressed because it is too large
Load Diff
+415
-1003
File diff suppressed because it is too large
Load Diff
+717
-641
File diff suppressed because it is too large
Load Diff
+500
-744
File diff suppressed because it is too large
Load Diff
+727
-651
File diff suppressed because it is too large
Load Diff
@@ -11,945 +11,3 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
|
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
|
||||||
|
|
||||||
msgid "Someone"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "{0} commented on your article."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "{0} is subscribed to you."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "{0} liked your article."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "{0} mentioned you."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "{0} boosted your article."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Your feed"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Local feed"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Federated feed"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "{0}'s avatar"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Previous page"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Next page"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Optional"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "To create a new blog, you need to be logged in"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "A blog with the same name already exists."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Your blog was successfully created!"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Your blog was deleted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You are not allowed to delete this blog."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You are not allowed to edit this blog."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You can't use this media as a blog icon."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You can't use this media as a blog banner."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Your blog information have been updated."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Your comment has been posted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Your comment has been deleted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Registrations are closed on this instance."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "User registration"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Here is the link for registration: {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Your account has been created. Now you just need to log in, before you can use it."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Instance settings have been saved."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "{} has been unblocked."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "{} has been blocked."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Blocks deleted"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Email already blocked"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Email Blocked"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You can't change your own rights."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You are not allowed to take this action."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Done."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "To like a post, you need to be logged in"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Your media have been deleted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You are not allowed to delete this media."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Your avatar has been updated."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You are not allowed to use this media."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "To see your notifications, you need to be logged in"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "This post isn't published yet."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "To write a new post, you need to be logged in"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You are not an author of this blog."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "New post"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Edit {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You are not allowed to publish on this blog."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Your article has been updated."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Your article has been saved."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "New article"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You are not allowed to delete this article."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Your article has been deleted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Couldn't obtain enough information about your account. Please make sure your username is correct."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "To reshare a post, you need to be logged in"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You are now connected."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You are now logged off."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Password reset"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Here is the link to reset your password: {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Your password was successfully reset."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "To access your dashboard, you need to be logged in"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You are no longer following {}."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You are now following {}."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "To subscribe to someone, you need to be logged in"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "To edit your profile, you need to be logged in"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Your profile has been updated."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Your account has been deleted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You can't delete someone else's account."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Create your account"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Create an account"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Email"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Email confirmation"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "An email will be sent to provided email. You can continue signing-up via the email."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Registration"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Check your inbox!"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "We sent a mail to the address you gave us, with a link for registration."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Username"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Password"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Password confirmation"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Media upload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Description"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Useful for visually impaired people, as well as licensing information"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Content warning"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Leave it empty, if none is needed"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "File"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Send"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Your media"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Upload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You don't have any media yet."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Content warning: {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Delete"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Details"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Media details"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Go back to the gallery"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Markdown syntax"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Copy it into your articles, to insert this media:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use as an avatar"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Plume"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Menu"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Search"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Dashboard"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Notifications"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Log Out"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "My account"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Log In"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Register"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "About this instance"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Privacy policy"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Administration"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Documentation"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Source code"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Matrix room"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Admin"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "It is you"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Edit your profile"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Open on {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Unsubscribe"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Subscribe"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Follow {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Log in to follow"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enter your full username handle to follow"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "{0}'s subscribers"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Articles"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Subscribers"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Subscriptions"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "{0}'s subscriptions"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Your Dashboard"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Your Blogs"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You don't have any blog yet. Create your own, or ask to join one."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Start a new blog"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Your Drafts"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Go to your gallery"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Edit your account"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Your Profile"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "To change your avatar, upload it to your gallery and then select from there."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Upload an avatar"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Display name"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Summary"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Theme"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Default theme"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Error while loading theme selector."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Never load blogs custom themes"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Update account"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Danger zone"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Be very careful, any action taken here can't be cancelled."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Delete your account"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Sorry, but as an admin, you can't leave your own instance."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Latest articles"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Atom feed"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Recently boosted"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Articles tagged \"{0}\""
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "There are currently no articles with such a tag"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "The content you sent can't be processed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Maybe it was too long."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Internal server error"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Something broke on our side."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Sorry about that. If you think this is a bug, please report it."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Invalid CSRF token"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You are not authorized."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Page not found"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "We couldn't find this page."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "The link that led you here may be broken."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Users"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Configuration"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Instances"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Email blocklist"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Grant admin rights"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Revoke admin rights"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Grant moderator rights"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Revoke moderator rights"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Ban"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Run on selected users"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Moderator"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Moderation"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Home"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Administration of {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Unblock"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Block"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Name"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Allow anyone to register here"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Short description"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Markdown syntax is supported"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Long description"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Default article license"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Save these settings"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "If you are browsing this site as a visitor, no data about you is collected."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Blocklisted Emails"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Email address"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Note"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Notify the user?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Optional, shows a message to the user when they attempt to create an account with that address"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Blocklisting notification"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "The message to be shown when the user attempts to create an account with this email address"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Add blocklisted address"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "There are no blocked emails on your instance"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Delete selected emails"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Email address:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Blocklisted for:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Will notify them on account creation with this message:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "The user will be silently prevented from making an account"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Welcome to {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Nothing to see here yet."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "About {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Runs Plume {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Home to <em>{0}</em> people"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Who wrote <em>{0}</em> articles"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "And are connected to <em>{0}</em> other instances"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Administred by"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Interact with {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Log in to interact"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enter your full username to interact"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Publish"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Classic editor (any changes will be lost)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Title"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Subtitle"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Content"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Upload media"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Tags, separated by commas"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "License"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Illustration"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "This is a draft, don't publish it yet."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Update"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Update, or publish"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Publish your post"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Written by {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "All rights reserved."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "This article is under the {0} license."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "One like"
|
|
||||||
msgid_plural "{0} likes"
|
|
||||||
msgstr[0] ""
|
|
||||||
|
|
||||||
msgid "I don't like this anymore"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Add yours"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "One boost"
|
|
||||||
msgid_plural "{0} boosts"
|
|
||||||
msgstr[0] ""
|
|
||||||
|
|
||||||
msgid "I don't want to boost this anymore"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Boost"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Comments"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Your comment"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Submit comment"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "No comments yet. Be the first to react!"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Are you sure?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "This article is still a draft. Only you and other authors can see it."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Only you and other authors can edit this article."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Edit"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "I'm from this instance"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Username, or email"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Log in"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "I'm from another instance"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Continue to your instance"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Reset your password"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "New password"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Confirmation"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Update password"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "We sent a mail to the address you gave us, with a link to reset your password."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Send password reset link"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "This token has expired"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Please start the process again by clicking <a href=\"/password-reset\">here</a>."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "New Blog"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Create a blog"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Create blog"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Edit \"{}\""
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You can upload images to your gallery, to use them as blog icons, or banners."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Upload images"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Blog icon"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Blog banner"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Custom theme"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Update blog"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Be very careful, any action taken here can't be reversed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Are you sure that you want to permanently delete this blog?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Permanently delete this blog"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "{}'s icon"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "There's one author on this blog: "
|
|
||||||
msgid_plural "There are {0} authors on this blog: "
|
|
||||||
msgstr[0] ""
|
|
||||||
|
|
||||||
msgid "No posts to see here yet."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "None"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "No description"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Respond"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Delete this comment"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "What is Plume?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Plume is a decentralized blogging engine."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Authors can manage multiple blogs, each as its own website."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Read the detailed rules"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "By {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Draft"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Search result(s) for \"{0}\""
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Search result(s)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "No results for your query"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "No more results for your query"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Advanced search"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Article title matching these words"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Subtitle matching these words"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Content macthing these words"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Body content"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "From this date"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "To this date"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Containing these tags"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Tags"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Posted on one of these instances"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Instance domain"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Posted by one of these authors"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Author(s)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Posted on one of these blogs"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Blog title"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Written in this language"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Language"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Published under this license"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Article license"
|
|
||||||
msgstr ""
|
|
||||||
|
|||||||
+715
-641
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user