Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
2fe705a712 | ||
|
3a448e9e17 | ||
|
6e4def4cc5 | ||
|
34b8fd83c1 | ||
|
d5774078e0 | ||
|
a3623412f9 | ||
|
1ed60537cf | ||
|
037d670fb7 | ||
|
8b817d50c5 | ||
|
fa48060a94 | ||
|
b41e982daf | ||
|
2fcb449ed8 | ||
|
218bc54a5f | ||
|
64f0333497 | ||
|
8aa7a5780d | ||
|
17c398bcee | ||
|
76f1455372 | ||
|
1bcad6d7cd | ||
|
9a58d9bcb7 | ||
|
c2dcac4413 | ||
|
394273e866 | ||
|
1bcc70c174 |
@ -10,8 +10,8 @@ executors:
|
||||
type: boolean
|
||||
default: false
|
||||
docker:
|
||||
- image: plumeorg/plume-buildenv:v0.8.0
|
||||
- image: <<#parameters.postgres>>cimg/postgres:14.2<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
|
||||
- image: plumeorg/plume-buildenv:v0.4.0
|
||||
- image: <<#parameters.postgres>>circleci/postgres:9.6-alpine<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: plume
|
||||
@ -38,7 +38,7 @@ commands:
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v0-<< parameters.cache >>-{{ checksum "Cargo.lock" }}-{{ .Branch }}
|
||||
- v0-<< parameters.cache >>-{{ checksum "Cargo.lock" }}-main
|
||||
- v0-<< parameters.cache >>-{{ checksum "Cargo.lock" }}-master
|
||||
|
||||
cache:
|
||||
description: push cache
|
||||
@ -63,7 +63,6 @@ commands:
|
||||
type: boolean
|
||||
default: false
|
||||
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_with_coverage:
|
||||
@ -112,7 +111,6 @@ jobs:
|
||||
name: default
|
||||
steps:
|
||||
- restore_env
|
||||
- run: rustup component add rustfmt --toolchain nightly-2022-07-19-x86_64-unknown-linux-gnu
|
||||
- run: cargo fmt --all -- --check
|
||||
|
||||
clippy:
|
||||
@ -260,4 +258,4 @@ workflows:
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^main/
|
||||
- /^master/
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM rust:1
|
||||
FROM debian:buster-20210208
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
#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&&\
|
||||
echo "deb [trusted=yes] https://apt.fury.io/caddy/ /" \
|
||||
| tee -a /etc/apt/sources.list.d/caddy-fury.list &&\
|
||||
wget -qO - https://artifacts.crowdin.com/repo/GPG-KEY-crowdin | apt-key add - &&\
|
||||
echo "deb https://artifacts.crowdin.com/repo/deb/ /" > /etc/apt/sources.list.d/crowdin.list &&\
|
||||
apt update &&\
|
||||
apt install -y --no-install-recommends binutils-dev build-essential cmake curl gcc gettext git libcurl4-openssl-dev libdw-dev libelf-dev libiberty-dev libpq-dev libsqlite3-dev libssl-dev make openssl pkg-config postgresql postgresql-contrib python zlib1g-dev python3-dev python3-pip python3-setuptools zip unzip libclang-dev clang caddy crowdin3 &&\
|
||||
apt install -y --no-install-recommends binutils-dev build-essential cmake curl gcc gettext git libcurl4-openssl-dev libdw-dev libelf-dev libiberty-dev libpq-dev libsqlite3-dev libssl-dev make openssl pkg-config postgresql postgresql-contrib python zlib1g-dev python3-pip zip unzip libclang-dev clang caddy&&\
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
#stick rust environment
|
||||
COPY rust-toolchain ./
|
||||
RUN rustup component add rustfmt clippy
|
||||
#install and configure rust
|
||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2021-01-15 -y &&\
|
||||
rustup component add rustfmt clippy &&\
|
||||
rustup component add rust-std --target wasm32-unknown-unknown
|
||||
|
||||
#compile some deps
|
||||
RUN cargo install wasm-pack &&\
|
||||
cargo install grcov &&\
|
||||
strip /root/.cargo/bin/* &&\
|
||||
rm -fr ~/.cargo/registry
|
||||
|
||||
#set some compilation parametters
|
||||
@ -29,3 +29,11 @@ RUN pip3 install selenium
|
||||
|
||||
#configure caddy
|
||||
COPY Caddyfile /Caddyfile
|
||||
|
||||
#install crowdin
|
||||
RUN mkdir /crowdin && cd /crowdin &&\
|
||||
curl -O https://downloads.crowdin.com/cli/v2/crowdin-cli.zip &&\
|
||||
unzip crowdin-cli.zip && rm crowdin-cli.zip &&\
|
||||
cd * && mv crowdin-cli.jar /usr/local/bin && cd && rm -rf /crowdin &&\
|
||||
/bin/echo -e '#!/bin/sh\njava -jar /usr/local/bin/crowdin-cli.jar $@' > /usr/local/bin/crowdin &&\
|
||||
chmod +x /usr/local/bin/crowdin
|
||||
|
@ -1 +0,0 @@
|
||||
nightly-2022-07-19
|
@ -3,5 +3,3 @@ data
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
.env
|
||||
target
|
||||
data
|
||||
|
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@ -1,6 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: cargo
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
30
.github/workflows/deploy-docker-latest.yaml
vendored
30
.github/workflows/deploy-docker-latest.yaml
vendored
@ -1,30 +0,0 @@
|
||||
name: cd
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
-
|
||||
name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: true
|
||||
tags: plumeorg/plume:latest
|
36
.github/workflows/deploy-docker-tag.yaml
vendored
36
.github/workflows/deploy-docker-tag.yaml
vendored
@ -1,36 +0,0 @@
|
||||
name: cd
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*.*.*'
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: plumeorg/plume
|
||||
-
|
||||
name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -20,5 +20,3 @@ search_index
|
||||
__pycache__
|
||||
.vscode/
|
||||
*-journal
|
||||
.direnv/
|
||||
build.log*
|
||||
|
89
CHANGELOG.md
89
CHANGELOG.md
@ -6,99 +6,20 @@
|
||||
|
||||
### 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
|
||||
|
||||
### Fixed
|
||||
|
||||
- Malfunction while creating a blog post in Persian (#1116)
|
||||
- Email block list is ignored when email sign-up (#1122)
|
||||
- Bug that some Activity Sytreams properties are not parsed properly (#1129)
|
||||
- Allow empty avatar for remote users (#1129)
|
||||
- Percent encode blog FQN for federation interoperability (#1129)
|
||||
- The same to `preferredUsername` (#1129)
|
||||
- Deprecation warnings during build process(see rust crate updates)
|
||||
- Server error 500 creating new blog with white spaces inside title. Bug reported on https://git.joinplu.me/Plume/Plume/issues/1152
|
||||
- Show _Subscribe_ button in column format instead of row format in screen smaller than 600px. https://git.lainoa.eus/aitzol/Plume/commit/db8cc6e7e8351a5d74f7ce0399126e13493c62d9
|
||||
### To do
|
||||
|
||||
- Choose rdn via environment variables for LDAP simple bind
|
||||
|
||||
## [[0.7.2]] - 2022-05-11
|
||||
|
||||
### Added
|
||||
|
||||
- Basque language (#1013)
|
||||
- Unit tests for ActivityPub (#1021)
|
||||
- Move to action area after liking/boosting/commenting (#1074)
|
||||
|
||||
### Changed
|
||||
|
||||
- Bump Rust to nightly 2022-01-26 (#1015)
|
||||
- Remove "Latest articles" timeline (#1069)
|
||||
- Change order of timeline tabs (#1069, #1070, #1072)
|
||||
- Migrate ActivityPub-related crates from activitypub 0.1 to activitystreams 0.7 (#1022)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Add explanation of sign-up step at sign-up page when email sign-up mode (#1012)
|
||||
- Add NOT NULL constraint to email_blocklist table fields (#1016)
|
||||
- Don't fill empty content when switching rich editor (#1017)
|
||||
- Fix accept header (#1058)
|
||||
- Render 404 page instead of 500 when data is not found (#1062)
|
||||
- Reuse reqwest client on broadcasting (#1059)
|
||||
- Reduce broadcasting HTTP request at once to prevent them being timed out (#1068, #1071)
|
||||
- Some ActivityPub data (#1021)
|
||||
|
||||
## [[0.7.1]] - 2022-01-12
|
||||
|
||||
### Added
|
||||
|
||||
- Introduce environment variable `MAIL_PORT` (#980)
|
||||
- Introduce email sign-up feature (#636, #1002)
|
||||
|
||||
### Changed
|
||||
|
||||
- Some styling improvements (#976, #977, #978)
|
||||
- Respond with error status code when error (#1002)
|
||||
|
||||
### Fiexed
|
||||
|
||||
- Fix comment link (#974)
|
||||
- Fix a bug that prevents posting articles (#975)
|
||||
- Fix a bug that notification page doesn't show (#981)
|
||||
|
||||
## [[0.7.0]] - 2022-01-02
|
||||
|
||||
### Added
|
||||
|
||||
- Allow `dir` attributes for LtoR text in RtoL document (#860)
|
||||
- More translation languages (#862)
|
||||
- Proxy support (#829)
|
||||
- Riker a actor system library (#870)
|
||||
- (request-target) and Host header in HTTP Signature (#872)
|
||||
- Default log levels for RUST_LOG (#885, #886, #919)
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade some dependent crates (#858)
|
||||
- Use tracing crate (#868)
|
||||
- Update Rust version to nightly-2021-11-27 (#961)
|
||||
- Update Rust version to nightly-2021-01-15 (#878)
|
||||
- Upgrade Tantivy to 0.13.3 and lindera-tantivy to 0.7.1 (#878)
|
||||
- Run searcher on actor system (#870)
|
||||
- Extract a function to calculate posts' ap_url and share it with some places (#918)
|
||||
- Use article title as its slug instead of capitalizing and inserting hyphens (#920)
|
||||
- Sign GET requests to other instances (#957)
|
||||
|
||||
### Fixed
|
||||
|
||||
@ -109,9 +30,6 @@
|
||||
- Update post's ActivityPub id when published by update (#915)
|
||||
- Calculate media URI properly even when MEDIA_UPLOAD_DIRECTORY configured (#916)
|
||||
- Prevent duplicated posts in 'all' timeline (#917)
|
||||
- Draw side line for blockquote on start (#933)
|
||||
- Fix URIs of posts on Mastodon (#947)
|
||||
- Place edit link proper position (#956, #963, #964)
|
||||
|
||||
## [[0.6.0]] - 2020-12-29
|
||||
|
||||
@ -292,10 +210,7 @@
|
||||
- Ability to create multiple blogs
|
||||
|
||||
<!-- next-url -->
|
||||
[Unreleased]: https://github.com/Plume-org/Plume/compare/0.7.2...HEAD
|
||||
[[0.7.2]]: https://github.com/Plume-org/Plume/compare/0.7.1...0.7.2
|
||||
[[0.7.1]]: https://github.com/Plume-org/Plume/compare/0.7.0...0.7.1
|
||||
[[0.7.0]]: https://github.com/Plume-org/Plume/compare/0.6.0...0.7.0
|
||||
[Unreleased]: https://github.com/Plume-org/Plume/compare/0.6.0...HEAD
|
||||
[[0.6.0]]: https://github.com/Plume-org/Plume/compare/0.5.0...0.6.0
|
||||
[0.5.0]: https://github.com/Plume-org/Plume/compare/0.4.0-alpha-4...0.5.0
|
||||
[0.4.0]: https://github.com/Plume-org/Plume/compare/0.3.0-alpha-2...0.4.0-alpha-4
|
||||
|
3074
Cargo.lock
generated
3074
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
61
Cargo.toml
61
Cargo.toml
@ -1,33 +1,37 @@
|
||||
[package]
|
||||
authors = ["Plume contributors"]
|
||||
name = "plume"
|
||||
version = "0.7.3-dev-fork"
|
||||
repository = "https://git.lainoa.eus/aitzol/Plume"
|
||||
edition = "2021"
|
||||
version = "0.6.1-dev"
|
||||
repository = "https://github.com/Plume-org/Plume"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
atom_syndication = "0.12.0"
|
||||
activitypub = "0.1.3"
|
||||
askama_escape = "0.1"
|
||||
atom_syndication = "0.6"
|
||||
clap = "2.33"
|
||||
dotenv = "0.15.0"
|
||||
gettext = "0.4.0"
|
||||
gettext-macros = "0.6.1"
|
||||
gettext-utils = "0.1.0"
|
||||
guid-create = "0.2"
|
||||
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
||||
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||
guid-create = "0.1"
|
||||
lettre = "0.9.2"
|
||||
lettre_email = "0.9.2"
|
||||
num_cpus = "1.16.0"
|
||||
rocket = "0.4.11"
|
||||
rocket_contrib = { version = "0.4.11", features = ["json"] }
|
||||
rocket_i18n = "0.4.1"
|
||||
scheduled-thread-pool = "0.2.7"
|
||||
serde = "1.0.137"
|
||||
serde_json = "1.0.81"
|
||||
shrinkwraprs = "0.3.0"
|
||||
validator = { version = "0.15", features = ["derive"] }
|
||||
num_cpus = "1.10"
|
||||
rocket = "=0.4.6"
|
||||
rocket_contrib = { version = "=0.4.5", features = ["json"] }
|
||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||
rpassword = "4.0"
|
||||
scheduled-thread-pool = "0.2.2"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
shrinkwraprs = "0.2.1"
|
||||
validator = "0.8"
|
||||
validator_derive = "0.8"
|
||||
webfinger = "0.4.1"
|
||||
tracing = "0.1.35"
|
||||
tracing-subscriber = "0.3.10"
|
||||
tracing = "0.1.22"
|
||||
tracing-subscriber = "0.2.15"
|
||||
riker = "0.4.2"
|
||||
activitystreams = "=0.7.0-alpha.20"
|
||||
|
||||
[[bin]]
|
||||
name = "plume"
|
||||
@ -35,11 +39,11 @@ path = "src/main.rs"
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
version = "0.4.31"
|
||||
version = "0.4"
|
||||
|
||||
[dependencies.ctrlc]
|
||||
features = ["termination"]
|
||||
version = "3.2.2"
|
||||
version = "3.1.2"
|
||||
|
||||
[dependencies.diesel]
|
||||
features = ["r2d2", "chrono"]
|
||||
@ -48,7 +52,7 @@ version = "1.4.5"
|
||||
[dependencies.multipart]
|
||||
default-features = false
|
||||
features = ["server"]
|
||||
version = "0.18"
|
||||
version = "0.16"
|
||||
|
||||
[dependencies.plume-api]
|
||||
path = "plume-api"
|
||||
@ -60,21 +64,20 @@ path = "plume-common"
|
||||
path = "plume-models"
|
||||
|
||||
[dependencies.rocket_csrf]
|
||||
git = "https://git.joinplu.me/plume/rocket_csrf"
|
||||
rev = "0.1.2"
|
||||
git = "https://github.com/fdb-hiroshima/rocket_csrf"
|
||||
rev = "29910f2829e7e590a540da3804336577b48c7b31"
|
||||
|
||||
[build-dependencies]
|
||||
ructe = "0.15.0"
|
||||
rsass = "0.26"
|
||||
ructe = "0.13.0"
|
||||
rsass = "0.9"
|
||||
|
||||
[features]
|
||||
default = ["postgres", "s3"]
|
||||
default = ["postgres"]
|
||||
postgres = ["plume-models/postgres", "diesel/postgres"]
|
||||
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
|
||||
debug-mailer = []
|
||||
test = []
|
||||
search-lindera = ["plume-models/search-lindera"]
|
||||
s3 = ["plume-models/s3"]
|
||||
|
||||
[workspace]
|
||||
members = ["plume-api", "plume-cli", "plume-models", "plume-common", "plume-front", "plume-macro"]
|
||||
|
11
Dockerfile
11
Dockerfile
@ -1,4 +1,4 @@
|
||||
FROM rust:1 as builder
|
||||
FROM rust:1-buster as builder
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
@ -18,19 +18,22 @@ COPY script/wasm-deps.sh .
|
||||
RUN chmod a+x ./wasm-deps.sh && sleep 1 && ./wasm-deps.sh
|
||||
|
||||
WORKDIR /app
|
||||
COPY Cargo.toml Cargo.lock rust-toolchain ./
|
||||
RUN cargo install wasm-pack
|
||||
|
||||
COPY . .
|
||||
RUN cargo install wasm-pack
|
||||
|
||||
RUN chmod a+x ./script/plume-front.sh && sleep 1 && ./script/plume-front.sh
|
||||
RUN cargo install --path ./ --force --no-default-features --features postgres
|
||||
RUN cargo install --path plume-cli --force --no-default-features --features postgres
|
||||
RUN cargo clean
|
||||
|
||||
FROM debian:stable-slim
|
||||
FROM debian:buster-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
libpq5
|
||||
libpq5 \
|
||||
libssl1.1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
<h1 align="center">
|
||||
<img src="https://raw.githubusercontent.com/Plume-org/Plume/main/assets/icons/trwnh/feather/plumeFeather64.png" alt="Plume's logo">
|
||||
<img src="https://raw.githubusercontent.com/Plume-org/Plume/master/assets/icons/trwnh/feather/plumeFeather64.png" alt="Plume's logo">
|
||||
Plume
|
||||
</h1>
|
||||
<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://codecov.io/gh/Plume-org/Plume"><img src="https://codecov.io/gh/Plume-org/Plume/branch/main/graph/badge.svg" alt="Code coverage"></a>
|
||||
<a href="https://codecov.io/gh/Plume-org/Plume"><img src="https://codecov.io/gh/Plume-org/Plume/branch/master/graph/badge.svg" alt="Code coverage"></a>
|
||||
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/plume"><img src="https://d322cqt584bo4o.cloudfront.net/plume/localized.svg"></a>
|
||||
<a 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>
|
||||
@ -53,4 +53,3 @@ As we want the various spaces related to the project (GitHub, Matrix, Loomio, et
|
||||
|
||||
We provide various way to install Plume: from source, with pre-built binaries, with Docker or with YunoHost.
|
||||
For detailed explanations, please refer to [the documentation](https://docs.joinplu.me/installation/).
|
||||
|
||||
|
@ -228,7 +228,7 @@ main .article-meta {
|
||||
fill: currentColor;
|
||||
}
|
||||
.action.liked:hover svg.feather {
|
||||
background: transparentize($red, 0.75);
|
||||
background: transparentize($red, 0.75)
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
@ -252,7 +252,7 @@ main .article-meta {
|
||||
background: $primary;
|
||||
}
|
||||
.action.reshared:hover svg.feather {
|
||||
background: transparentize($primary, 0.75);
|
||||
background: transparentize($primary, 0.75)
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
@ -516,11 +516,4 @@ input:checked ~ .cw-container > .cw-text {
|
||||
main .article-meta > *, main .article-meta .comments, main .article-meta > .banner > * {
|
||||
margin: 0 5%;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
align-items: center;
|
||||
& > div:nth-child(2) {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -135,7 +135,6 @@ form.new-post {
|
||||
|
||||
.button + .button {
|
||||
margin-left: 1em;
|
||||
margin-inline-start: 1em;
|
||||
}
|
||||
|
||||
.split {
|
||||
|
@ -219,23 +219,15 @@ p.error {
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.cover-link {
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.cover {
|
||||
min-height: 10em;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
@ -244,14 +236,9 @@ p.error {
|
||||
font-family: $playfair;
|
||||
font-size: 1.75em;
|
||||
font-weight: normal;
|
||||
line-height: 1.10;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
line-height: 1.75;
|
||||
a {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-block-start: 0.5em;
|
||||
transition: color 0.1s ease-in;
|
||||
color: $text-color;
|
||||
|
||||
@ -260,8 +247,7 @@ p.error {
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-shrink: 0;
|
||||
text-align: end;
|
||||
float: right;
|
||||
|
||||
.button {
|
||||
margin-top: 0;
|
||||
@ -275,7 +261,7 @@ p.error {
|
||||
font-family: $lora;
|
||||
font-size: 1em;
|
||||
line-height: 1.25;
|
||||
text-align: initial;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
@ -479,10 +465,9 @@ figure {
|
||||
|
||||
/// Avatars
|
||||
.avatar {
|
||||
background-position: center !important;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
border-radius: 100%;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.small {
|
||||
width: 50px;
|
||||
@ -507,7 +492,6 @@ figure {
|
||||
margin: auto $horizontal-margin 2em;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
@ -577,6 +561,14 @@ figure {
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
& > div {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
main .article-meta .comments .comment {
|
||||
header {
|
||||
flex-direction: column;
|
||||
|
20
build.rs
20
build.rs
@ -41,9 +41,9 @@ fn main() {
|
||||
.expect("compile templates");
|
||||
|
||||
compile_themes().expect("Theme compilation error");
|
||||
recursive_copy(&Path::new("assets").join("icons"), Path::new("static"))
|
||||
recursive_copy(&Path::new("assets").join("icons"), &Path::new("static"))
|
||||
.expect("Couldn't copy icons");
|
||||
recursive_copy(&Path::new("assets").join("images"), Path::new("static"))
|
||||
recursive_copy(&Path::new("assets").join("images"), &Path::new("static"))
|
||||
.expect("Couldn't copy images");
|
||||
create_dir_all(&Path::new("static").join("media")).expect("Couldn't init media directory");
|
||||
|
||||
@ -97,12 +97,12 @@ fn compile_theme(path: &Path, out_dir: &Path) -> std::io::Result<()> {
|
||||
.components()
|
||||
.skip_while(|c| *c != Component::Normal(OsStr::new("themes")))
|
||||
.skip(1)
|
||||
.map(|c| {
|
||||
.filter_map(|c| {
|
||||
c.as_os_str()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
.split_once('.')
|
||||
.map_or(c.as_os_str().to_str().unwrap_or_default(), |x| x.0)
|
||||
.splitn(2, '.')
|
||||
.next()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("-");
|
||||
@ -120,14 +120,8 @@ fn compile_theme(path: &Path, out_dir: &Path) -> std::io::Result<()> {
|
||||
// compile the .scss/.sass file
|
||||
let mut out = File::create(out.join("theme.css"))?;
|
||||
out.write_all(
|
||||
&rsass::compile_scss_path(
|
||||
path,
|
||||
rsass::output::Format {
|
||||
style: rsass::output::Style::Compressed,
|
||||
..rsass::output::Format::default()
|
||||
},
|
||||
)
|
||||
.expect("SCSS compilation error"),
|
||||
&rsass::compile_scss_file(path, rsass::OutputStyle::Compressed)
|
||||
.expect("SCSS compilation error"),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
|
116
flake.lock
116
flake.lock
@ -1,116 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1683408522,
|
||||
"narHash": "sha256-9kcPh6Uxo17a3kK3XCHhcWiV1Yu1kYj22RHiymUhMkU=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "897876e4c484f1e8f92009fd11b7d988a121a4e7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1683857898,
|
||||
"narHash": "sha256-pyVY4UxM6zUX97g6bk6UyCbZGCWZb2Zykrne8YxacRA=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "4e7fba3f37f5e184ada0ef3cf1e4d8ef450f240b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
60
flake.nix
60
flake.nix
@ -1,60 +0,0 @@
|
||||
{
|
||||
description = "Developpment shell for Plume including nightly Rust compiler";
|
||||
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
inputs.rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, rust-overlay, ... }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
overlays = [ (import rust-overlay) ];
|
||||
pkgs = import nixpkgs { inherit system overlays; };
|
||||
inputs = with pkgs; [
|
||||
(rust-bin.nightly.latest.default.override {
|
||||
targets = [ "wasm32-unknown-unknown" ];
|
||||
})
|
||||
wasm-pack
|
||||
openssl
|
||||
pkg-config
|
||||
gettext
|
||||
postgresql
|
||||
sqlite
|
||||
];
|
||||
in {
|
||||
packages.default = pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "plume";
|
||||
version = "0.7.3-dev";
|
||||
|
||||
src = ./.;
|
||||
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
outputHashes = {
|
||||
"pulldown-cmark-0.8.0" = "sha256-lpfoRDuY3zJ3QmUqJ5k9OL0MEdGDpwmpJ+u5BCj2kIA=";
|
||||
"rocket_csrf-0.1.2" = "sha256-WywZfMiwZqTPfSDcAE7ivTSYSaFX+N9fjnRsLSLb9wE=";
|
||||
};
|
||||
};
|
||||
buildNoDefaultFeatures = true;
|
||||
buildFeatures = ["postgresql" "s3"];
|
||||
|
||||
nativeBuildInputs = inputs;
|
||||
|
||||
buildPhase = ''
|
||||
wasm-pack build --target web --release plume-front
|
||||
cargo build --no-default-features --features postgresql,s3 --path .
|
||||
cargo build --no-default-features --features postgresql,s3 --path plume-cli
|
||||
'';
|
||||
installPhase = ''
|
||||
cargo install --no-default-features --features postgresql,s3 --path . --target-dir $out
|
||||
cargo install --no-default-features --features postgresql,s3 --path plume-cli --target-dir $out
|
||||
'';
|
||||
};
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = inputs;
|
||||
};
|
||||
});
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
ALTER TABLE instances DROP COLUMN private_key;
|
||||
ALTER TABLE instances DROP COLUMN public_key;
|
@ -0,0 +1,2 @@
|
||||
ALTER TABLE instances ADD COLUMN private_key TEXT;
|
||||
ALTER TABLE instances ADD COLUMN public_key TEXT;
|
@ -1 +0,0 @@
|
||||
DROP TABLE email_signups;
|
@ -1,9 +0,0 @@
|
||||
CREATE TABLE email_signups (
|
||||
id SERIAL PRIMARY KEY,
|
||||
email VARCHAR NOT NULL,
|
||||
token VARCHAR NOT NULL,
|
||||
expiration_date TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX email_signups_token ON email_signups (token);
|
||||
CREATE UNIQUE INDEX email_signups_token_requests_email ON email_signups (email);
|
@ -1,4 +0,0 @@
|
||||
ALTER TABLE email_blocklist ALTER COLUMN notification_text DROP NOT NULL;
|
||||
ALTER TABLE email_blocklist ALTER COLUMN notify_user DROP NOT NULL;
|
||||
ALTER TABLE email_blocklist ALTER COLUMN note DROP NOT NULL;
|
||||
ALTER TABLE email_blocklist ALTER COLUMN email_address DROP NOT NULL;
|
@ -1,4 +0,0 @@
|
||||
ALTER TABLE email_blocklist ALTER COLUMN email_address SET NOT NULL;
|
||||
ALTER TABLE email_blocklist ALTER COLUMN note SET NOT NULL;
|
||||
ALTER TABLE email_blocklist ALTER COLUMN notify_user SET NOT NULL;
|
||||
ALTER TABLE email_blocklist ALTER COLUMN notification_text SET NOT NULL;
|
@ -0,0 +1,30 @@
|
||||
CREATE TABLE instances_old (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
public_domain VARCHAR NOT NULL,
|
||||
name VARCHAR NOT NULL,
|
||||
local BOOLEAN NOT NULL DEFAULT 'f',
|
||||
blocked BOOLEAN NOT NULL DEFAULT 'f',
|
||||
creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
open_registrations BOOLEAN NOT NULL DEFAULT 't',
|
||||
short_description TEXT NOT NULL DEFAULT '',
|
||||
long_description TEXT NOT NULL DEFAULT '',
|
||||
default_license TEXT NOT NULL DEFAULT 'CC-0',
|
||||
long_description_html VARCHAR NOT NULL DEFAULT '',
|
||||
short_description_html VARCHAR NOT NULL DEFAULT ''
|
||||
);
|
||||
INSERT INTO instances_old SELECT
|
||||
id,
|
||||
public_domain,
|
||||
name,
|
||||
local,
|
||||
blocked,
|
||||
creation_date,
|
||||
open_registrations,
|
||||
short_description,
|
||||
long_description,
|
||||
default_license,
|
||||
long_description_html,
|
||||
short_description_html
|
||||
FROM instances;
|
||||
DROP TABLE instances;
|
||||
ALTER TABLE instances_old RENAME TO instances;
|
@ -0,0 +1,2 @@
|
||||
ALTER TABLE instances ADD COLUMN private_key TEXT;
|
||||
ALTER TABLE instances ADD COLUMN public_key TEXT;
|
@ -1 +0,0 @@
|
||||
DROP TABLE email_signups;
|
@ -1,9 +0,0 @@
|
||||
CREATE TABLE email_signups (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
email VARCHAR NOT NULL,
|
||||
token VARCHAR NOT NULL,
|
||||
expiration_date TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX email_signups_token ON email_signups (token);
|
||||
CREATE UNIQUE INDEX email_signups_token_requests_email ON email_signups (email);
|
@ -1,9 +0,0 @@
|
||||
CREATE TABLE email_blocklist2(id INTEGER PRIMARY KEY,
|
||||
email_address TEXT UNIQUE,
|
||||
note TEXT,
|
||||
notify_user BOOLEAN DEFAULT FALSE,
|
||||
notification_text TEXT);
|
||||
|
||||
INSERT INTO email_blocklist2 SELECT * FROM email_blocklist;
|
||||
DROP TABLE email_blocklist;
|
||||
ALTER TABLE email_blocklist2 RENAME TO email_blocklist;
|
@ -1,9 +0,0 @@
|
||||
CREATE TABLE email_blocklist2(id INTEGER PRIMARY KEY,
|
||||
email_address TEXT UNIQUE NOT NULL,
|
||||
note TEXT NOT NULL,
|
||||
notify_user BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
notification_text TEXT NOT NULL);
|
||||
|
||||
INSERT INTO email_blocklist2 SELECT * FROM email_blocklist;
|
||||
DROP TABLE email_blocklist;
|
||||
ALTER TABLE email_blocklist2 RENAME TO email_blocklist;
|
@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "plume-api"
|
||||
version = "0.7.2"
|
||||
version = "0.6.1-dev"
|
||||
authors = ["Plume contributors"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0.137"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
|
@ -1,3 +1,2 @@
|
||||
pre-release-hook = ["cargo", "fmt"]
|
||||
pre-release-replacements = []
|
||||
release = false
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "plume-cli"
|
||||
version = "0.7.2"
|
||||
version = "0.6.1-dev"
|
||||
authors = ["Plume contributors"]
|
||||
edition = "2018"
|
||||
|
||||
@ -10,8 +10,8 @@ path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33"
|
||||
dotenv = "0.15"
|
||||
rpassword = "6.0.1"
|
||||
dotenv = "0.14"
|
||||
rpassword = "5.0.0"
|
||||
|
||||
[dependencies.diesel]
|
||||
features = ["r2d2", "chrono"]
|
||||
@ -24,4 +24,3 @@ path = "../plume-models"
|
||||
postgres = ["plume-models/postgres", "diesel/postgres"]
|
||||
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
|
||||
search-lindera = ["plume-models/search-lindera"]
|
||||
s3 = ["plume-models/s3"]
|
||||
|
@ -1,3 +1,2 @@
|
||||
pre-release-hook = ["cargo", "fmt"]
|
||||
pre-release-replacements = []
|
||||
release = false
|
||||
|
@ -1,6 +1,6 @@
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
|
||||
use plume_models::{instance::*, safe_string::SafeString, Connection};
|
||||
use plume_models::{instance::NewInstance, Connection};
|
||||
use std::env;
|
||||
|
||||
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
||||
@ -53,21 +53,5 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||
.unwrap_or_else(|| String::from("CC-BY-SA"));
|
||||
let open_reg = !args.is_present("private");
|
||||
|
||||
Instance::insert(
|
||||
conn,
|
||||
NewInstance {
|
||||
public_domain: domain,
|
||||
name,
|
||||
local: true,
|
||||
long_description: SafeString::new(""),
|
||||
short_description: SafeString::new(""),
|
||||
default_license: license,
|
||||
open_registrations: open_reg,
|
||||
short_description_html: String::new(),
|
||||
long_description_html: String::new(),
|
||||
},
|
||||
)
|
||||
.expect("Couldn't save instance");
|
||||
Instance::cache_local(conn);
|
||||
Instance::create_local_instance_user(conn).expect("Couldn't save local instance user");
|
||||
NewInstance::new_local(conn, domain, name, open_reg, license).expect("Couldn't save instance");
|
||||
}
|
||||
|
@ -1,262 +0,0 @@
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
|
||||
use plume_models::{blogs::Blog, instance::Instance, lists::*, users::User, Connection};
|
||||
|
||||
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("lists")
|
||||
.about("Manage lists")
|
||||
.subcommand(
|
||||
SubCommand::with_name("new")
|
||||
.arg(
|
||||
Arg::with_name("name")
|
||||
.short("n")
|
||||
.long("name")
|
||||
.takes_value(true)
|
||||
.help("The name of this list"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("type")
|
||||
.short("t")
|
||||
.long("type")
|
||||
.takes_value(true)
|
||||
.help(
|
||||
r#"The type of this list (one of "user", "blog", "word" or "prefix")"#,
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("user")
|
||||
.short("u")
|
||||
.long("user")
|
||||
.takes_value(true)
|
||||
.help("Username of whom this list is for. Empty for an instance list"),
|
||||
)
|
||||
.about("Create a new list"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("delete")
|
||||
.arg(
|
||||
Arg::with_name("name")
|
||||
.short("n")
|
||||
.long("name")
|
||||
.takes_value(true)
|
||||
.help("The name of the list to delete"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("user")
|
||||
.short("u")
|
||||
.long("user")
|
||||
.takes_value(true)
|
||||
.help("Username of whom this list was for. Empty for instance list"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("yes")
|
||||
.short("y")
|
||||
.long("yes")
|
||||
.help("Confirm the deletion"),
|
||||
)
|
||||
.about("Delete a list"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("add")
|
||||
.arg(
|
||||
Arg::with_name("name")
|
||||
.short("n")
|
||||
.long("name")
|
||||
.takes_value(true)
|
||||
.help("The name of the list to add an element to"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("user")
|
||||
.short("u")
|
||||
.long("user")
|
||||
.takes_value(true)
|
||||
.help("Username of whom this list is for. Empty for instance list"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("value")
|
||||
.short("v")
|
||||
.long("value")
|
||||
.takes_value(true)
|
||||
.help("The value to add"),
|
||||
)
|
||||
.about("Add element to a list"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("rm")
|
||||
.arg(
|
||||
Arg::with_name("name")
|
||||
.short("n")
|
||||
.long("name")
|
||||
.takes_value(true)
|
||||
.help("The name of the list to remove an element from"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("user")
|
||||
.short("u")
|
||||
.long("user")
|
||||
.takes_value(true)
|
||||
.help("Username of whom this list is for. Empty for instance list"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("value")
|
||||
.short("v")
|
||||
.long("value")
|
||||
.takes_value(true)
|
||||
.help("The value to remove"),
|
||||
)
|
||||
.about("Remove element from list"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||
let conn = conn;
|
||||
match args.subcommand() {
|
||||
("new", Some(x)) => new(x, conn),
|
||||
("delete", Some(x)) => delete(x, conn),
|
||||
("add", Some(x)) => add(x, conn),
|
||||
("rm", Some(x)) => rm(x, conn),
|
||||
("", None) => command().print_help().unwrap(),
|
||||
_ => println!("Unknown subcommand"),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_list_identifier(args: &ArgMatches<'_>) -> (String, Option<String>) {
|
||||
let name = args
|
||||
.value_of("name")
|
||||
.map(String::from)
|
||||
.expect("No name provided for the list");
|
||||
let user = args.value_of("user").map(String::from);
|
||||
(name, user)
|
||||
}
|
||||
|
||||
fn get_list_type(args: &ArgMatches<'_>) -> ListType {
|
||||
let typ = args
|
||||
.value_of("type")
|
||||
.map(String::from)
|
||||
.expect("No name type for the list");
|
||||
match typ.as_str() {
|
||||
"user" => ListType::User,
|
||||
"blog" => ListType::Blog,
|
||||
"word" => ListType::Word,
|
||||
"prefix" => ListType::Prefix,
|
||||
_ => panic!("Invalid list type: {}", typ),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_value(args: &ArgMatches<'_>) -> String {
|
||||
args.value_of("value")
|
||||
.map(String::from)
|
||||
.expect("No query provided")
|
||||
}
|
||||
|
||||
fn resolve_user(username: &str, conn: &Connection) -> User {
|
||||
let instance = Instance::get_local_uncached(conn).expect("Failed to load local instance");
|
||||
|
||||
User::find_by_name(conn, username, instance.id).expect("User not found")
|
||||
}
|
||||
|
||||
fn new(args: &ArgMatches<'_>, conn: &Connection) {
|
||||
let (name, user) = get_list_identifier(args);
|
||||
let typ = get_list_type(args);
|
||||
|
||||
let user = user.map(|user| resolve_user(&user, conn));
|
||||
|
||||
List::new(conn, &name, user.as_ref(), typ).expect("failed to create list");
|
||||
}
|
||||
|
||||
fn delete(args: &ArgMatches<'_>, conn: &Connection) {
|
||||
let (name, user) = get_list_identifier(args);
|
||||
|
||||
if !args.is_present("yes") {
|
||||
panic!("Warning, this operation is destructive. Add --yes to confirm you want to do it.")
|
||||
}
|
||||
|
||||
let user = user.map(|user| resolve_user(&user, conn));
|
||||
|
||||
let list =
|
||||
List::find_for_user_by_name(conn, user.map(|u| u.id), &name).expect("list not found");
|
||||
|
||||
list.delete(conn).expect("Failed to update list");
|
||||
}
|
||||
|
||||
fn add(args: &ArgMatches<'_>, conn: &Connection) {
|
||||
let (name, user) = get_list_identifier(args);
|
||||
let value = get_value(args);
|
||||
|
||||
let user = user.map(|user| resolve_user(&user, conn));
|
||||
|
||||
let list =
|
||||
List::find_for_user_by_name(conn, user.map(|u| u.id), &name).expect("list not found");
|
||||
|
||||
match list.kind() {
|
||||
ListType::Blog => {
|
||||
let blog_id = Blog::find_by_fqn(conn, &value).expect("unknown blog").id;
|
||||
if !list.contains_blog(conn, blog_id).unwrap() {
|
||||
list.add_blogs(conn, &[blog_id]).unwrap();
|
||||
}
|
||||
}
|
||||
ListType::User => {
|
||||
let user_id = User::find_by_fqn(conn, &value).expect("unknown user").id;
|
||||
if !list.contains_user(conn, user_id).unwrap() {
|
||||
list.add_users(conn, &[user_id]).unwrap();
|
||||
}
|
||||
}
|
||||
ListType::Word => {
|
||||
if !list.contains_word(conn, &value).unwrap() {
|
||||
list.add_words(conn, &[&value]).unwrap();
|
||||
}
|
||||
}
|
||||
ListType::Prefix => {
|
||||
if !list.contains_prefix(conn, &value).unwrap() {
|
||||
list.add_prefixes(conn, &[&value]).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rm(args: &ArgMatches<'_>, conn: &Connection) {
|
||||
let (name, user) = get_list_identifier(args);
|
||||
let value = get_value(args);
|
||||
|
||||
let user = user.map(|user| resolve_user(&user, conn));
|
||||
|
||||
let list =
|
||||
List::find_for_user_by_name(conn, user.map(|u| u.id), &name).expect("list not found");
|
||||
|
||||
match list.kind() {
|
||||
ListType::Blog => {
|
||||
let blog_id = Blog::find_by_fqn(conn, &value).expect("unknown blog").id;
|
||||
let mut blogs = list.list_blogs(conn).unwrap();
|
||||
if let Some(index) = blogs.iter().position(|b| b.id == blog_id) {
|
||||
blogs.swap_remove(index);
|
||||
let blogs = blogs.iter().map(|b| b.id).collect::<Vec<_>>();
|
||||
list.set_blogs(conn, &blogs).unwrap();
|
||||
}
|
||||
}
|
||||
ListType::User => {
|
||||
let user_id = User::find_by_fqn(conn, &value).expect("unknown user").id;
|
||||
let mut users = list.list_users(conn).unwrap();
|
||||
if let Some(index) = users.iter().position(|u| u.id == user_id) {
|
||||
users.swap_remove(index);
|
||||
let users = users.iter().map(|u| u.id).collect::<Vec<_>>();
|
||||
list.set_users(conn, &users).unwrap();
|
||||
}
|
||||
}
|
||||
ListType::Word => {
|
||||
let mut words = list.list_words(conn).unwrap();
|
||||
if let Some(index) = words.iter().position(|w| *w == value) {
|
||||
words.swap_remove(index);
|
||||
let words = words.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
list.set_words(conn, &words).unwrap();
|
||||
}
|
||||
}
|
||||
ListType::Prefix => {
|
||||
let mut prefixes = list.list_prefixes(conn).unwrap();
|
||||
if let Some(index) = prefixes.iter().position(|p| *p == value) {
|
||||
prefixes.swap_remove(index);
|
||||
let prefixes = prefixes.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
list.set_prefixes(conn, &prefixes).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,10 +4,8 @@ use plume_models::{instance::Instance, Connection as Conn, CONFIG};
|
||||
use std::io::{self, prelude::*};
|
||||
|
||||
mod instance;
|
||||
mod list;
|
||||
mod migration;
|
||||
mod search;
|
||||
mod timeline;
|
||||
mod users;
|
||||
|
||||
fn main() {
|
||||
@ -18,8 +16,6 @@ fn main() {
|
||||
.subcommand(instance::command())
|
||||
.subcommand(migration::command())
|
||||
.subcommand(search::command())
|
||||
.subcommand(timeline::command())
|
||||
.subcommand(list::command())
|
||||
.subcommand(users::command());
|
||||
let matches = app.clone().get_matches();
|
||||
|
||||
@ -29,7 +25,7 @@ fn main() {
|
||||
e => e.map(|_| ()).unwrap(),
|
||||
}
|
||||
let conn = Conn::establish(CONFIG.database_url.as_str());
|
||||
let _ = conn.as_ref().map(Instance::cache_local);
|
||||
let _ = conn.as_ref().map(|conn| Instance::cache_local(conn));
|
||||
|
||||
match matches.subcommand() {
|
||||
("instance", Some(args)) => {
|
||||
@ -41,10 +37,6 @@ fn main() {
|
||||
("search", Some(args)) => {
|
||||
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::run(args, &conn.expect("Couldn't connect to the database."))
|
||||
}
|
||||
|
@ -1,257 +0,0 @@
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
|
||||
use plume_models::{instance::Instance, posts::Post, timeline::*, users::*, Connection};
|
||||
|
||||
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("timeline")
|
||||
.about("Manage public timeline")
|
||||
.subcommand(
|
||||
SubCommand::with_name("new")
|
||||
.arg(
|
||||
Arg::with_name("name")
|
||||
.short("n")
|
||||
.long("name")
|
||||
.takes_value(true)
|
||||
.help("The name of this timeline"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("query")
|
||||
.short("q")
|
||||
.long("query")
|
||||
.takes_value(true)
|
||||
.help("The query posts in this timelines have to match"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("user")
|
||||
.short("u")
|
||||
.long("user")
|
||||
.takes_value(true)
|
||||
.help(
|
||||
"Username of whom this timeline is for. Empty for an instance timeline",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("preload-count")
|
||||
.short("p")
|
||||
.long("preload-count")
|
||||
.takes_value(true)
|
||||
.help("Number of posts to try to preload in this timeline at its creation"),
|
||||
)
|
||||
.about("Create a new timeline"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("delete")
|
||||
.arg(
|
||||
Arg::with_name("name")
|
||||
.short("n")
|
||||
.long("name")
|
||||
.takes_value(true)
|
||||
.help("The name of the timeline to delete"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("user")
|
||||
.short("u")
|
||||
.long("user")
|
||||
.takes_value(true)
|
||||
.help(
|
||||
"Username of whom this timeline was for. Empty for instance timeline",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("yes")
|
||||
.short("y")
|
||||
.long("yes")
|
||||
.help("Confirm the deletion"),
|
||||
)
|
||||
.about("Delete a timeline"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("edit")
|
||||
.arg(
|
||||
Arg::with_name("name")
|
||||
.short("n")
|
||||
.long("name")
|
||||
.takes_value(true)
|
||||
.help("The name of the timeline to edit"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("user")
|
||||
.short("u")
|
||||
.long("user")
|
||||
.takes_value(true)
|
||||
.help("Username of whom this timeline is for. Empty for instance timeline"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("query")
|
||||
.short("q")
|
||||
.long("query")
|
||||
.takes_value(true)
|
||||
.help("The query posts in this timelines have to match"),
|
||||
)
|
||||
.about("Edit the query of a timeline"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("repopulate")
|
||||
.arg(
|
||||
Arg::with_name("name")
|
||||
.short("n")
|
||||
.long("name")
|
||||
.takes_value(true)
|
||||
.help("The name of the timeline to repopulate"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("user")
|
||||
.short("u")
|
||||
.long("user")
|
||||
.takes_value(true)
|
||||
.help(
|
||||
"Username of whom this timeline was for. Empty for instance timeline",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("preload-count")
|
||||
.short("p")
|
||||
.long("preload-count")
|
||||
.takes_value(true)
|
||||
.help("Number of posts to try to preload in this timeline at its creation"),
|
||||
)
|
||||
.about("Repopulate a timeline. Run this after modifying a list the timeline depends on."),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||
let conn = conn;
|
||||
match args.subcommand() {
|
||||
("new", Some(x)) => new(x, conn),
|
||||
("edit", Some(x)) => edit(x, conn),
|
||||
("delete", Some(x)) => delete(x, conn),
|
||||
("repopulate", Some(x)) => repopulate(x, conn),
|
||||
("", None) => command().print_help().unwrap(),
|
||||
_ => println!("Unknown subcommand"),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_timeline_identifier(args: &ArgMatches<'_>) -> (String, Option<String>) {
|
||||
let name = args
|
||||
.value_of("name")
|
||||
.map(String::from)
|
||||
.expect("No name provided for the timeline");
|
||||
let user = args.value_of("user").map(String::from);
|
||||
(name, user)
|
||||
}
|
||||
|
||||
fn get_query(args: &ArgMatches<'_>) -> String {
|
||||
let query = args
|
||||
.value_of("query")
|
||||
.map(String::from)
|
||||
.expect("No query provided");
|
||||
|
||||
match TimelineQuery::parse(&query) {
|
||||
Ok(_) => (),
|
||||
Err(QueryError::SyntaxError(start, end, message)) => panic!(
|
||||
"Query parsing error between {} and {}: {}",
|
||||
start, end, message
|
||||
),
|
||||
Err(QueryError::UnexpectedEndOfQuery) => {
|
||||
panic!("Query parsing error: unexpected end of query")
|
||||
}
|
||||
Err(QueryError::RuntimeError(message)) => panic!("Query parsing error: {}", message),
|
||||
}
|
||||
|
||||
query
|
||||
}
|
||||
|
||||
fn get_preload_count(args: &ArgMatches<'_>) -> usize {
|
||||
args.value_of("preload-count")
|
||||
.map(|arg| arg.parse().expect("invalid preload-count"))
|
||||
.unwrap_or(plume_models::ITEMS_PER_PAGE as usize)
|
||||
}
|
||||
|
||||
fn resolve_user(username: &str, conn: &Connection) -> User {
|
||||
let instance = Instance::get_local_uncached(conn).expect("Failed to load local instance");
|
||||
|
||||
User::find_by_name(conn, username, instance.id).expect("User not found")
|
||||
}
|
||||
|
||||
fn preload(timeline: Timeline, count: usize, conn: &Connection) {
|
||||
timeline.remove_all_posts(conn).unwrap();
|
||||
|
||||
if count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut posts = Vec::with_capacity(count as usize);
|
||||
for post in Post::list_filtered(conn, None, None, None)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.rev()
|
||||
{
|
||||
if timeline.matches(conn, &post, Kind::Original).unwrap() {
|
||||
posts.push(post);
|
||||
if posts.len() >= count {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for post in posts.iter().rev() {
|
||||
timeline.add_post(conn, post).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn new(args: &ArgMatches<'_>, conn: &Connection) {
|
||||
let (name, user) = get_timeline_identifier(args);
|
||||
let query = get_query(args);
|
||||
let preload_count = get_preload_count(args);
|
||||
|
||||
let user = user.map(|user| resolve_user(&user, conn));
|
||||
|
||||
let timeline = if let Some(user) = user {
|
||||
Timeline::new_for_user(conn, user.id, name, query)
|
||||
} else {
|
||||
Timeline::new_for_instance(conn, name, query)
|
||||
}
|
||||
.expect("Failed to create new timeline");
|
||||
|
||||
preload(timeline, preload_count, conn);
|
||||
}
|
||||
|
||||
fn edit(args: &ArgMatches<'_>, conn: &Connection) {
|
||||
let (name, user) = get_timeline_identifier(args);
|
||||
let query = get_query(args);
|
||||
|
||||
let user = user.map(|user| resolve_user(&user, conn));
|
||||
|
||||
let mut timeline = Timeline::find_for_user_by_name(conn, user.map(|u| u.id), &name)
|
||||
.expect("timeline not found");
|
||||
|
||||
timeline.query = query;
|
||||
|
||||
timeline.update(conn).expect("Failed to update timeline");
|
||||
}
|
||||
|
||||
fn delete(args: &ArgMatches<'_>, conn: &Connection) {
|
||||
let (name, user) = get_timeline_identifier(args);
|
||||
|
||||
if !args.is_present("yes") {
|
||||
panic!("Warning, this operation is destructive. Add --yes to confirm you want to do it.")
|
||||
}
|
||||
|
||||
let user = user.map(|user| resolve_user(&user, conn));
|
||||
|
||||
let timeline = Timeline::find_for_user_by_name(conn, user.map(|u| u.id), &name)
|
||||
.expect("timeline not found");
|
||||
|
||||
timeline.delete(conn).expect("Failed to update timeline");
|
||||
}
|
||||
|
||||
fn repopulate(args: &ArgMatches<'_>, conn: &Connection) {
|
||||
let (name, user) = get_timeline_identifier(args);
|
||||
let preload_count = get_preload_count(args);
|
||||
|
||||
let user = user.map(|user| resolve_user(&user, conn));
|
||||
|
||||
let timeline = Timeline::find_for_user_by_name(conn, user.map(|u| u.id), &name)
|
||||
.expect("timeline not found");
|
||||
preload(timeline, preload_count, conn);
|
||||
}
|
@ -1,42 +1,35 @@
|
||||
[package]
|
||||
name = "plume-common"
|
||||
version = "0.7.2"
|
||||
version = "0.6.1-dev"
|
||||
authors = ["Plume contributors"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
activitypub = "0.1.1"
|
||||
activitystreams-derive = "0.1.1"
|
||||
activitystreams-traits = "0.1.0"
|
||||
array_tool = "1.0"
|
||||
base64 = "0.13"
|
||||
hex = "0.4"
|
||||
openssl = "0.10.40"
|
||||
rocket = "0.4.11"
|
||||
reqwest = { version = "0.11.11", features = ["blocking", "json", "socks"] }
|
||||
serde = "1.0.137"
|
||||
base64 = "0.10"
|
||||
heck = "0.3.0"
|
||||
hex = "0.3"
|
||||
hyper = "0.12.33"
|
||||
openssl = "0.10.22"
|
||||
rocket = "=0.4.6"
|
||||
reqwest = { version = "0.9", features = ["socks"] }
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0.81"
|
||||
serde_json = "1.0"
|
||||
shrinkwraprs = "0.3.0"
|
||||
syntect = "4.5.0"
|
||||
regex-syntax = { version = "0.6.26", default-features = false, features = ["unicode-perl"] }
|
||||
tracing = "0.1.35"
|
||||
askama_escape = "0.10.3"
|
||||
activitystreams = "=0.7.0-alpha.20"
|
||||
activitystreams-ext = "0.1.0-alpha.2"
|
||||
url = "2.2.2"
|
||||
flume = "0.10.13"
|
||||
tokio = { version = "1.19.2", features = ["full"] }
|
||||
futures = "0.3.25"
|
||||
tokio = "0.1.22"
|
||||
regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] }
|
||||
tracing = "0.1.22"
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
version = "0.4.31"
|
||||
version = "0.4"
|
||||
|
||||
[dependencies.pulldown-cmark]
|
||||
default-features = false
|
||||
git = "https://git.joinplu.me/Plume/pulldown-cmark"
|
||||
branch = "bidi-plume"
|
||||
|
||||
[dev-dependencies]
|
||||
assert-json-diff = "2.0.1"
|
||||
once_cell = "1.12.0"
|
||||
|
||||
[features]
|
||||
|
@ -1,3 +1,2 @@
|
||||
pre-release-hook = ["cargo", "fmt"]
|
||||
pre-release-replacements = []
|
||||
release = false
|
||||
|
@ -1,7 +1,10 @@
|
||||
use reqwest;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use super::{request, sign::Signer};
|
||||
use reqwest::{
|
||||
header::{HeaderValue, HOST},
|
||||
Url,
|
||||
};
|
||||
|
||||
/// Represents an ActivityPub inbox.
|
||||
///
|
||||
@ -10,51 +13,10 @@ use super::{request, sign::Signer};
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use activitystreams::{prelude::*, base::Base, actor::Person, activity::{Announce, Create}, object::Note, iri_string::types::IriString};
|
||||
/// # extern crate activitypub;
|
||||
/// # use activitypub::{actor::Person, activity::{Announce, Create}, object::Note};
|
||||
/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
|
||||
/// # use once_cell::sync::Lazy;
|
||||
/// # use plume_common::activity_pub::inbox::*;
|
||||
/// # use plume_common::activity_pub::sign::{gen_keypair, Error as SignError, Result as SignResult, Signer};
|
||||
/// #
|
||||
/// # static MY_SIGNER: Lazy<MySigner> = Lazy::new(|| MySigner::new());
|
||||
/// #
|
||||
/// # struct MySigner {
|
||||
/// # public_key: String,
|
||||
/// # private_key: String,
|
||||
/// # }
|
||||
/// #
|
||||
/// # impl MySigner {
|
||||
/// # fn new() -> Self {
|
||||
/// # let (pub_key, priv_key) = gen_keypair();
|
||||
/// # Self {
|
||||
/// # public_key: String::from_utf8(pub_key).unwrap(),
|
||||
/// # private_key: String::from_utf8(priv_key).unwrap(),
|
||||
/// # }
|
||||
/// # }
|
||||
/// # }
|
||||
/// #
|
||||
/// # impl Signer for MySigner {
|
||||
/// # fn get_key_id(&self) -> String {
|
||||
/// # "mysigner".into()
|
||||
/// # }
|
||||
/// #
|
||||
/// # fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> {
|
||||
/// # let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
|
||||
/// # .unwrap();
|
||||
/// # let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
|
||||
/// # signer.update(to_sign.as_bytes()).unwrap();
|
||||
/// # signer.sign_to_vec().map_err(|_| SignError())
|
||||
/// # }
|
||||
/// #
|
||||
/// # fn verify(&self, data: &str, signature: &[u8]) -> SignResult<bool> {
|
||||
/// # let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
|
||||
/// # .unwrap();
|
||||
/// # let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
|
||||
/// # verifier.update(data.as_bytes()).unwrap();
|
||||
/// # verifier.verify(&signature).map_err(|_| SignError())
|
||||
/// # }
|
||||
/// # }
|
||||
/// #
|
||||
/// # use plume_common::activity_pub::{inbox::*, sign::{gen_keypair, Error as SignatureError, Result as SignatureResult, Signer}};
|
||||
/// # struct User;
|
||||
/// # impl FromId<()> for User {
|
||||
/// # type Error = ();
|
||||
@ -67,10 +29,6 @@ use super::{request, sign::Signer};
|
||||
/// # fn from_activity(_: &(), obj: Person) -> Result<Self, Self::Error> {
|
||||
/// # Ok(User)
|
||||
/// # }
|
||||
/// #
|
||||
/// # fn get_sender() -> &'static dyn Signer {
|
||||
/// # &*MY_SIGNER
|
||||
/// # }
|
||||
/// # }
|
||||
/// # impl AsActor<&()> for User {
|
||||
/// # fn get_inbox_url(&self) -> String {
|
||||
@ -90,10 +48,6 @@ use super::{request, sign::Signer};
|
||||
/// # fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> {
|
||||
/// # Ok(Message)
|
||||
/// # }
|
||||
/// #
|
||||
/// # fn get_sender() -> &'static dyn Signer {
|
||||
/// # &*MY_SIGNER
|
||||
/// # }
|
||||
/// # }
|
||||
/// # impl AsObject<User, Create, &()> for Message {
|
||||
/// # type Error = ();
|
||||
@ -111,19 +65,55 @@ use super::{request, sign::Signer};
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// # }
|
||||
/// # struct MySigner {
|
||||
/// # public_key: String,
|
||||
/// # private_key: String,
|
||||
/// # }
|
||||
/// #
|
||||
/// # let mut person = Person::new();
|
||||
/// # person.set_id("https://test.ap/actor".parse::<IriString>().unwrap());
|
||||
/// # let mut act = Create::new(
|
||||
/// # Base::retract(person).unwrap().into_generic().unwrap(),
|
||||
/// # Base::retract(Note::new()).unwrap().into_generic().unwrap()
|
||||
/// # );
|
||||
/// # act.set_id("https://test.ap/activity".parse::<IriString>().unwrap());
|
||||
/// # impl MySigner {
|
||||
/// # fn new() -> Self {
|
||||
/// # let (pub_key, priv_key) = gen_keypair();
|
||||
/// # Self {
|
||||
/// # public_key: String::from_utf8(pub_key).unwrap(),
|
||||
/// # private_key: String::from_utf8(priv_key).unwrap(),
|
||||
/// # }
|
||||
/// # }
|
||||
/// # }
|
||||
/// #
|
||||
/// # impl Signer for MySigner {
|
||||
/// # fn get_key_id(&self) -> String {
|
||||
/// # "mysigner".into()
|
||||
/// # }
|
||||
/// #
|
||||
/// # fn sign(&self, to_sign: &str) -> SignatureResult<Vec<u8>> {
|
||||
/// # let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
|
||||
/// # .unwrap();
|
||||
/// # let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
|
||||
/// # signer.update(to_sign.as_bytes()).unwrap();
|
||||
/// # signer.sign_to_vec().map_err(|_| SignatureError())
|
||||
/// # }
|
||||
/// #
|
||||
/// # fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<bool> {
|
||||
/// # let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
|
||||
/// # .unwrap();
|
||||
/// # let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
|
||||
/// # verifier.update(data.as_bytes()).unwrap();
|
||||
/// # verifier.verify(&signature).map_err(|_| SignatureError())
|
||||
/// # }
|
||||
/// # }
|
||||
/// #
|
||||
/// # let mut act = Create::default();
|
||||
/// # act.object_props.set_id_string(String::from("https://test.ap/activity")).unwrap();
|
||||
/// # let mut person = Person::default();
|
||||
/// # person.object_props.set_id_string(String::from("https://test.ap/actor")).unwrap();
|
||||
/// # act.create_props.set_actor_object(person).unwrap();
|
||||
/// # act.create_props.set_object_object(Note::default()).unwrap();
|
||||
/// # let activity_json = serde_json::to_value(act).unwrap();
|
||||
/// #
|
||||
/// # let conn = ();
|
||||
/// # let sender = MySigner::new();
|
||||
/// #
|
||||
/// let result: Result<(), ()> = Inbox::handle(&conn, activity_json)
|
||||
/// let result: Result<(), ()> = Inbox::handle(&conn, &sender, activity_json)
|
||||
/// .with::<User, Announce, Message>(None)
|
||||
/// .with::<User, Create, Message>(None)
|
||||
/// .done();
|
||||
@ -137,9 +127,10 @@ where
|
||||
/// # Structure
|
||||
///
|
||||
/// - the context to be passed to each handler.
|
||||
/// - the sender actor to sign request
|
||||
/// - the activity
|
||||
/// - the reason it has not been handled yet
|
||||
NotHandled(&'a C, serde_json::Value, InboxError<E>),
|
||||
NotHandled(&'a C, &'a dyn Signer, serde_json::Value, InboxError<E>),
|
||||
|
||||
/// A matching handler have been found but failed
|
||||
///
|
||||
@ -192,39 +183,46 @@ where
|
||||
///
|
||||
/// - `ctx`: the context to pass to each handler
|
||||
/// - `json`: the JSON representation of the incoming activity
|
||||
pub fn handle(ctx: &'a C, json: serde_json::Value) -> Inbox<'a, C, E, R> {
|
||||
Inbox::NotHandled(ctx, json, InboxError::NoMatch)
|
||||
pub fn handle(
|
||||
ctx: &'a C,
|
||||
sender: &'a dyn Signer,
|
||||
json: serde_json::Value,
|
||||
) -> Inbox<'a, C, E, R> {
|
||||
Inbox::NotHandled(ctx, sender, json, InboxError::NoMatch)
|
||||
}
|
||||
|
||||
/// Registers an handler on this Inbox.
|
||||
pub fn with<A, V, M>(self, proxy: Option<&reqwest::Proxy>) -> Self
|
||||
pub fn with<A, V, M>(self, proxy: Option<&reqwest::Proxy>) -> Inbox<'a, C, E, R>
|
||||
where
|
||||
A: AsActor<&'a C> + FromId<C, Error = E>,
|
||||
V: activitystreams::markers::Activity + serde::de::DeserializeOwned,
|
||||
V: activitypub::Activity,
|
||||
M: AsObject<A, V, &'a C, Error = E> + FromId<C, Error = E>,
|
||||
M::Output: Into<R>,
|
||||
{
|
||||
if let Self::NotHandled(ctx, mut act, e) = self {
|
||||
if let Inbox::NotHandled(ctx, sender, mut act, e) = self {
|
||||
if serde_json::from_value::<V>(act.clone()).is_ok() {
|
||||
let act_clone = act.clone();
|
||||
let act_id = match act_clone["id"].as_str() {
|
||||
Some(x) => x,
|
||||
None => return Self::NotHandled(ctx, act, InboxError::InvalidID),
|
||||
None => return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidID),
|
||||
};
|
||||
|
||||
// Get the actor ID
|
||||
let actor_id = match get_id(act["actor"].clone()) {
|
||||
Some(x) => x,
|
||||
None => return Self::NotHandled(ctx, act, InboxError::InvalidActor(None)),
|
||||
None => {
|
||||
return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidActor(None))
|
||||
}
|
||||
};
|
||||
|
||||
if Self::is_spoofed_activity(&actor_id, &act) {
|
||||
return Self::NotHandled(ctx, act, InboxError::InvalidObject(None));
|
||||
return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidObject(None));
|
||||
}
|
||||
|
||||
// Transform this actor to a model (see FromId for details about the from_id function)
|
||||
let actor = match A::from_id(
|
||||
ctx,
|
||||
sender,
|
||||
&actor_id,
|
||||
serde_json::from_value(act["actor"].clone()).ok(),
|
||||
proxy,
|
||||
@ -235,17 +233,25 @@ where
|
||||
if let Some(json) = json {
|
||||
act["actor"] = json;
|
||||
}
|
||||
return Self::NotHandled(ctx, act, InboxError::InvalidActor(Some(e)));
|
||||
return Inbox::NotHandled(
|
||||
ctx,
|
||||
sender,
|
||||
act,
|
||||
InboxError::InvalidActor(Some(e)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Same logic for "object"
|
||||
let obj_id = match get_id(act["object"].clone()) {
|
||||
Some(x) => x,
|
||||
None => return Self::NotHandled(ctx, act, InboxError::InvalidObject(None)),
|
||||
None => {
|
||||
return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidObject(None))
|
||||
}
|
||||
};
|
||||
let obj = match M::from_id(
|
||||
ctx,
|
||||
sender,
|
||||
&obj_id,
|
||||
serde_json::from_value(act["object"].clone()).ok(),
|
||||
proxy,
|
||||
@ -255,19 +261,24 @@ where
|
||||
if let Some(json) = json {
|
||||
act["object"] = json;
|
||||
}
|
||||
return Self::NotHandled(ctx, act, InboxError::InvalidObject(Some(e)));
|
||||
return Inbox::NotHandled(
|
||||
ctx,
|
||||
sender,
|
||||
act,
|
||||
InboxError::InvalidObject(Some(e)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle the activity
|
||||
match obj.activity(ctx, actor, act_id) {
|
||||
Ok(res) => Self::Handled(res.into()),
|
||||
Err(e) => Self::Failed(e),
|
||||
match obj.activity(ctx, actor, &act_id) {
|
||||
Ok(res) => Inbox::Handled(res.into()),
|
||||
Err(e) => Inbox::Failed(e),
|
||||
}
|
||||
} else {
|
||||
// If the Activity type is not matching the expected one for
|
||||
// this handler, try with the next one.
|
||||
Self::NotHandled(ctx, act, e)
|
||||
Inbox::NotHandled(ctx, sender, act, e)
|
||||
}
|
||||
} else {
|
||||
self
|
||||
@ -278,7 +289,7 @@ where
|
||||
pub fn done(self) -> Result<R, E> {
|
||||
match self {
|
||||
Inbox::Handled(res) => Ok(res),
|
||||
Inbox::NotHandled(_, _, err) => Err(E::from(err)),
|
||||
Inbox::NotHandled(_, _, _, err) => Err(E::from(err)),
|
||||
Inbox::Failed(err) => Err(err),
|
||||
}
|
||||
}
|
||||
@ -333,7 +344,7 @@ pub trait FromId<C>: Sized {
|
||||
type Error: From<InboxError<Self::Error>> + Debug;
|
||||
|
||||
/// The ActivityPub object type representing Self
|
||||
type Object: activitystreams::markers::Object + serde::de::DeserializeOwned;
|
||||
type Object: activitypub::Object;
|
||||
|
||||
/// Tries to get an instance of `Self` from an ActivityPub ID.
|
||||
///
|
||||
@ -345,6 +356,7 @@ pub trait FromId<C>: Sized {
|
||||
/// If absent, the ID will be dereferenced.
|
||||
fn from_id(
|
||||
ctx: &C,
|
||||
sender: &dyn Signer,
|
||||
id: &str,
|
||||
object: Option<Self::Object>,
|
||||
proxy: Option<&reqwest::Proxy>,
|
||||
@ -353,7 +365,7 @@ pub trait FromId<C>: Sized {
|
||||
Ok(x) => Ok(x),
|
||||
_ => match object {
|
||||
Some(o) => Self::from_activity(ctx, o).map_err(|e| (None, e)),
|
||||
None => Self::from_activity(ctx, Self::deref(id, proxy.cloned())?)
|
||||
None => Self::from_activity(ctx, Self::deref(id, sender, proxy.cloned())?)
|
||||
.map_err(|e| (None, e)),
|
||||
},
|
||||
}
|
||||
@ -362,18 +374,42 @@ pub trait FromId<C>: Sized {
|
||||
/// Dereferences an ID
|
||||
fn deref(
|
||||
id: &str,
|
||||
sender: &dyn Signer,
|
||||
proxy: Option<reqwest::Proxy>,
|
||||
) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
|
||||
request::get(id, Self::get_sender(), proxy)
|
||||
.map_err(|_| (None, InboxError::DerefError))
|
||||
.and_then(|r| {
|
||||
let json: serde_json::Value = r
|
||||
.json()
|
||||
.map_err(|_| (None, InboxError::InvalidObject(None)))?;
|
||||
serde_json::from_value(json.clone())
|
||||
.map_err(|_| (Some(json), InboxError::InvalidObject(None)))
|
||||
})
|
||||
.map_err(|(json, e)| (json, e.into()))
|
||||
let mut headers = request::headers();
|
||||
let url = Url::parse(&id).map_err(|_| (None, InboxError::InvalidID.into()))?;
|
||||
if !url.has_host() {
|
||||
return Err((None, InboxError::InvalidID.into()));
|
||||
}
|
||||
let host_header_value = HeaderValue::from_str(&url.host_str().expect("Unreachable"))
|
||||
.map_err(|_| (None, InboxError::DerefError.into()))?;
|
||||
headers.insert(HOST, host_header_value);
|
||||
if let Some(proxy) = proxy {
|
||||
reqwest::ClientBuilder::new().proxy(proxy)
|
||||
} else {
|
||||
reqwest::ClientBuilder::new()
|
||||
}
|
||||
.connect_timeout(Some(std::time::Duration::from_secs(5)))
|
||||
.build()
|
||||
.map_err(|_| (None, InboxError::DerefError.into()))?
|
||||
.get(id)
|
||||
.headers(headers.clone())
|
||||
.header(
|
||||
"Signature",
|
||||
request::signature(sender, &headers, ("get", url.path(), url.query()))
|
||||
.map_err(|_| (None, InboxError::DerefError.into()))?,
|
||||
)
|
||||
.send()
|
||||
.map_err(|_| (None, InboxError::DerefError))
|
||||
.and_then(|mut r| {
|
||||
let json: serde_json::Value = r
|
||||
.json()
|
||||
.map_err(|_| (None, InboxError::InvalidObject(None)))?;
|
||||
serde_json::from_value(json.clone())
|
||||
.map_err(|_| (Some(json), InboxError::InvalidObject(None)))
|
||||
})
|
||||
.map_err(|(json, e)| (json, e.into()))
|
||||
}
|
||||
|
||||
/// Builds a `Self` from its ActivityPub representation
|
||||
@ -381,8 +417,6 @@ pub trait FromId<C>: Sized {
|
||||
|
||||
/// Tries to find a `Self` with a given ID (`id`), using `ctx` (a database)
|
||||
fn from_db(ctx: &C, id: &str) -> Result<Self, Self::Error>;
|
||||
|
||||
fn get_sender() -> &'static dyn Signer;
|
||||
}
|
||||
|
||||
/// Should be implemented by anything representing an ActivityPub actor.
|
||||
@ -418,51 +452,9 @@ pub trait AsActor<C> {
|
||||
/// representing the Note by a Message type, without any specific context.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use activitystreams::{prelude::*, activity::Create, actor::Person, object::Note};
|
||||
/// # extern crate activitypub;
|
||||
/// # use activitypub::{activity::Create, actor::Person, object::Note};
|
||||
/// # use plume_common::activity_pub::inbox::{AsActor, AsObject, FromId};
|
||||
/// # use plume_common::activity_pub::sign::{gen_keypair, Error as SignError, Result as SignResult, Signer};
|
||||
/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
|
||||
/// # use once_cell::sync::Lazy;
|
||||
/// #
|
||||
/// # static MY_SIGNER: Lazy<MySigner> = Lazy::new(|| MySigner::new());
|
||||
/// #
|
||||
/// # struct MySigner {
|
||||
/// # public_key: String,
|
||||
/// # private_key: String,
|
||||
/// # }
|
||||
/// #
|
||||
/// # impl MySigner {
|
||||
/// # fn new() -> Self {
|
||||
/// # let (pub_key, priv_key) = gen_keypair();
|
||||
/// # Self {
|
||||
/// # public_key: String::from_utf8(pub_key).unwrap(),
|
||||
/// # private_key: String::from_utf8(priv_key).unwrap(),
|
||||
/// # }
|
||||
/// # }
|
||||
/// # }
|
||||
/// #
|
||||
/// # impl Signer for MySigner {
|
||||
/// # fn get_key_id(&self) -> String {
|
||||
/// # "mysigner".into()
|
||||
/// # }
|
||||
/// #
|
||||
/// # fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> {
|
||||
/// # let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
|
||||
/// # .unwrap();
|
||||
/// # let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
|
||||
/// # signer.update(to_sign.as_bytes()).unwrap();
|
||||
/// # signer.sign_to_vec().map_err(|_| SignError())
|
||||
/// # }
|
||||
/// #
|
||||
/// # fn verify(&self, data: &str, signature: &[u8]) -> SignResult<bool> {
|
||||
/// # let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
|
||||
/// # .unwrap();
|
||||
/// # let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
|
||||
/// # verifier.update(data.as_bytes()).unwrap();
|
||||
/// # verifier.verify(&signature).map_err(|_| SignError())
|
||||
/// # }
|
||||
/// # }
|
||||
/// #
|
||||
/// # struct Account;
|
||||
/// # impl FromId<()> for Account {
|
||||
/// # type Error = ();
|
||||
@ -475,10 +467,6 @@ pub trait AsActor<C> {
|
||||
/// # fn from_activity(_: &(), obj: Person) -> Result<Self, Self::Error> {
|
||||
/// # Ok(Account)
|
||||
/// # }
|
||||
/// #
|
||||
/// # fn get_sender() -> &'static dyn Signer {
|
||||
/// # &*MY_SIGNER
|
||||
/// # }
|
||||
/// # }
|
||||
/// # impl AsActor<()> for Account {
|
||||
/// # fn get_inbox_url(&self) -> String {
|
||||
@ -500,14 +488,7 @@ pub trait AsActor<C> {
|
||||
/// }
|
||||
///
|
||||
/// fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> {
|
||||
/// Ok(Message {
|
||||
/// text: obj.content()
|
||||
/// .and_then(|content| content.to_owned().single_xsd_string()).ok_or(())?
|
||||
/// })
|
||||
/// }
|
||||
///
|
||||
/// fn get_sender() -> &'static dyn Signer {
|
||||
/// &*MY_SIGNER
|
||||
/// Ok(Message { text: obj.object_props.content_string().map_err(|_| ())? })
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
@ -523,7 +504,7 @@ pub trait AsActor<C> {
|
||||
/// ```
|
||||
pub trait AsObject<A, V, C>
|
||||
where
|
||||
V: activitystreams::markers::Activity,
|
||||
V: activitypub::Activity,
|
||||
{
|
||||
/// What kind of error is returned when something fails
|
||||
type Error;
|
||||
@ -547,74 +528,22 @@ where
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::sign::{gen_keypair, Error as SignatureError, Result as SignatureResult};
|
||||
use super::*;
|
||||
use crate::activity_pub::sign::{
|
||||
gen_keypair, Error as SignError, Result as SignResult, Signer,
|
||||
};
|
||||
use activitystreams::{
|
||||
activity::{Announce, Create, Delete, Like},
|
||||
actor::Person,
|
||||
base::Base,
|
||||
object::Note,
|
||||
prelude::*,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use activitypub::{activity::*, actor::Person, object::Note};
|
||||
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
|
||||
|
||||
static MY_SIGNER: Lazy<MySigner> = Lazy::new(MySigner::new);
|
||||
|
||||
struct MySigner {
|
||||
public_key: String,
|
||||
private_key: String,
|
||||
}
|
||||
|
||||
impl MySigner {
|
||||
fn new() -> Self {
|
||||
let (pub_key, priv_key) = gen_keypair();
|
||||
Self {
|
||||
public_key: String::from_utf8(pub_key).unwrap(),
|
||||
private_key: String::from_utf8(priv_key).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Signer for MySigner {
|
||||
fn get_key_id(&self) -> String {
|
||||
"mysigner".into()
|
||||
}
|
||||
|
||||
fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> {
|
||||
let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
|
||||
.unwrap();
|
||||
let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
|
||||
signer.update(to_sign.as_bytes()).unwrap();
|
||||
signer.sign_to_vec().map_err(|_| SignError())
|
||||
}
|
||||
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> SignResult<bool> {
|
||||
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
|
||||
.unwrap();
|
||||
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
|
||||
verifier.update(data.as_bytes()).unwrap();
|
||||
verifier.verify(signature).map_err(|_| SignError())
|
||||
}
|
||||
}
|
||||
|
||||
struct MyActor;
|
||||
impl FromId<()> for MyActor {
|
||||
type Error = ();
|
||||
type Object = Person;
|
||||
|
||||
fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> {
|
||||
Ok(Self)
|
||||
Ok(MyActor)
|
||||
}
|
||||
|
||||
fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> {
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
fn get_sender() -> &'static dyn Signer {
|
||||
&*MY_SIGNER
|
||||
Ok(MyActor)
|
||||
}
|
||||
}
|
||||
|
||||
@ -634,15 +563,11 @@ mod tests {
|
||||
type Object = Note;
|
||||
|
||||
fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> {
|
||||
Ok(Self)
|
||||
Ok(MyObject)
|
||||
}
|
||||
|
||||
fn from_activity(_: &(), _obj: Note) -> Result<Self, Self::Error> {
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
fn get_sender() -> &'static dyn Signer {
|
||||
&*MY_SIGNER
|
||||
Ok(MyObject)
|
||||
}
|
||||
}
|
||||
impl AsObject<MyActor, Create, &()> for MyObject {
|
||||
@ -686,22 +611,65 @@ mod tests {
|
||||
}
|
||||
|
||||
fn build_create() -> Create {
|
||||
let mut person = Person::new();
|
||||
person.set_id("https://test.ap/actor".parse().unwrap());
|
||||
let mut note = Note::new();
|
||||
note.set_id("https://test.ap/note".parse().unwrap());
|
||||
let mut act = Create::new(
|
||||
Base::retract(person).unwrap().into_generic().unwrap(),
|
||||
Base::retract(note).unwrap().into_generic().unwrap(),
|
||||
);
|
||||
act.set_id("https://test.ap/activity".parse().unwrap());
|
||||
let mut act = Create::default();
|
||||
act.object_props
|
||||
.set_id_string(String::from("https://test.ap/activity"))
|
||||
.unwrap();
|
||||
let mut person = Person::default();
|
||||
person
|
||||
.object_props
|
||||
.set_id_string(String::from("https://test.ap/actor"))
|
||||
.unwrap();
|
||||
act.create_props.set_actor_object(person).unwrap();
|
||||
let mut note = Note::default();
|
||||
note.object_props
|
||||
.set_id_string(String::from("https://test.ap/note"))
|
||||
.unwrap();
|
||||
act.create_props.set_object_object(note).unwrap();
|
||||
act
|
||||
}
|
||||
|
||||
struct MySigner {
|
||||
public_key: String,
|
||||
private_key: String,
|
||||
}
|
||||
|
||||
impl MySigner {
|
||||
fn new() -> Self {
|
||||
let (pub_key, priv_key) = gen_keypair();
|
||||
Self {
|
||||
public_key: String::from_utf8(pub_key).unwrap(),
|
||||
private_key: String::from_utf8(priv_key).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Signer for MySigner {
|
||||
fn get_key_id(&self) -> String {
|
||||
"mysigner".into()
|
||||
}
|
||||
|
||||
fn sign(&self, to_sign: &str) -> SignatureResult<Vec<u8>> {
|
||||
let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
|
||||
.unwrap();
|
||||
let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
|
||||
signer.update(to_sign.as_bytes()).unwrap();
|
||||
signer.sign_to_vec().map_err(|_| SignatureError())
|
||||
}
|
||||
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<bool> {
|
||||
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
|
||||
.unwrap();
|
||||
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
|
||||
verifier.update(data.as_bytes()).unwrap();
|
||||
verifier.verify(&signature).map_err(|_| SignatureError())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inbox_basic() {
|
||||
let act = serde_json::to_value(build_create()).unwrap();
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act)
|
||||
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act)
|
||||
.with::<MyActor, Create, MyObject>(None)
|
||||
.done();
|
||||
assert!(res.is_ok());
|
||||
@ -710,7 +678,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_inbox_multi_handlers() {
|
||||
let act = serde_json::to_value(build_create()).unwrap();
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act)
|
||||
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act)
|
||||
.with::<MyActor, Announce, MyObject>(None)
|
||||
.with::<MyActor, Delete, MyObject>(None)
|
||||
.with::<MyActor, Create, MyObject>(None)
|
||||
@ -723,7 +691,7 @@ mod tests {
|
||||
fn test_inbox_failure() {
|
||||
let act = serde_json::to_value(build_create()).unwrap();
|
||||
// Create is not handled by this inbox
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act)
|
||||
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act)
|
||||
.with::<MyActor, Announce, MyObject>(None)
|
||||
.with::<MyActor, Like, MyObject>(None)
|
||||
.done();
|
||||
@ -731,16 +699,6 @@ mod tests {
|
||||
}
|
||||
|
||||
struct FailingActor;
|
||||
impl AsActor<&()> for FailingActor {
|
||||
fn get_inbox_url(&self) -> String {
|
||||
String::from("https://test.ap/failing-actor/inbox")
|
||||
}
|
||||
|
||||
fn is_local(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl FromId<()> for FailingActor {
|
||||
type Error = ();
|
||||
type Object = Person;
|
||||
@ -749,12 +707,17 @@ mod tests {
|
||||
Err(())
|
||||
}
|
||||
|
||||
fn from_activity(_: &(), _obj: Self::Object) -> Result<Self, Self::Error> {
|
||||
fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
impl AsActor<&()> for FailingActor {
|
||||
fn get_inbox_url(&self) -> String {
|
||||
String::from("https://test.ap/failing-actor/inbox")
|
||||
}
|
||||
|
||||
fn get_sender() -> &'static dyn Signer {
|
||||
&*MY_SIGNER
|
||||
fn is_local(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@ -777,12 +740,12 @@ mod tests {
|
||||
fn test_inbox_actor_failure() {
|
||||
let act = serde_json::to_value(build_create()).unwrap();
|
||||
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
|
||||
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act.clone())
|
||||
.with::<FailingActor, Create, MyObject>(None)
|
||||
.done();
|
||||
assert!(res.is_err());
|
||||
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act)
|
||||
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act.clone())
|
||||
.with::<FailingActor, Create, MyObject>(None)
|
||||
.with::<MyActor, Create, MyObject>(None)
|
||||
.done();
|
||||
|
@ -1,27 +1,13 @@
|
||||
use activitystreams::{
|
||||
actor::{ApActor, Group, Person},
|
||||
base::{AnyBase, Base, Extends},
|
||||
iri_string::types::IriString,
|
||||
kind,
|
||||
markers::{self, Activity},
|
||||
object::{ApObject, Article, Object},
|
||||
primitives::{AnyString, OneOrMany},
|
||||
unparsed::UnparsedMutExt,
|
||||
};
|
||||
use activitystreams_ext::{Ext1, Ext2, UnparsedExtension};
|
||||
use activitypub::{Activity, Link, Object};
|
||||
use array_tool::vec::Uniq;
|
||||
use futures::future::join_all;
|
||||
use reqwest::{header::HeaderValue, ClientBuilder, RequestBuilder, Url};
|
||||
use reqwest::{header::HeaderValue, r#async::ClientBuilder, Url};
|
||||
use rocket::{
|
||||
http::Status,
|
||||
request::{FromRequest, Request},
|
||||
response::{Responder, Response},
|
||||
Outcome,
|
||||
};
|
||||
use tokio::{
|
||||
runtime,
|
||||
time::{sleep, Duration},
|
||||
};
|
||||
use tokio::prelude::*;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use self::sign::Signable;
|
||||
@ -38,8 +24,8 @@ pub const AP_CONTENT_TYPE: &str =
|
||||
|
||||
pub fn ap_accept_header() -> Vec<&'static str> {
|
||||
vec![
|
||||
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
|
||||
"application/ld+json;profile=\"https://www.w3.org/ns/activitystreams\"",
|
||||
"application/ld+json; profile=\"https://w3.org/ns/activitystreams\"",
|
||||
"application/ld+json;profile=\"https://w3.org/ns/activitystreams\"",
|
||||
"application/activity+json",
|
||||
"application/ld+json",
|
||||
]
|
||||
@ -77,7 +63,7 @@ impl<T> ActivityStream<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, O: serde::Serialize> Responder<'r> for ActivityStream<O> {
|
||||
impl<'r, O: Object> Responder<'r> for ActivityStream<O> {
|
||||
fn respond_to(self, request: &Request<'_>) -> Result<Response<'r>, Status> {
|
||||
let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?;
|
||||
json["@context"] = context();
|
||||
@ -101,16 +87,14 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApRequest {
|
||||
.map(|header| {
|
||||
header
|
||||
.split(',')
|
||||
.map(|ct| {
|
||||
match ct.trim() {
|
||||
.map(|ct| match ct.trim() {
|
||||
// bool for Forward: true if found a valid Content-Type for Plume first (HTML), false otherwise
|
||||
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
||||
| "application/ld+json;profile=\"https://www.w3.org/ns/activitystreams\""
|
||||
"application/ld+json; profile=\"https://w3.org/ns/activitystreams\""
|
||||
| "application/ld+json;profile=\"https://w3.org/ns/activitystreams\""
|
||||
| "application/activity+json"
|
||||
| "application/ld+json" => Outcome::Success(ApRequest),
|
||||
"text/html" => Outcome::Forward(true),
|
||||
_ => Outcome::Forward(false),
|
||||
}
|
||||
})
|
||||
.fold(Outcome::Forward(false), |out, ct| {
|
||||
if out.clone().forwarded().unwrap_or_else(|| out.is_success()) {
|
||||
@ -124,11 +108,10 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApRequest {
|
||||
.unwrap_or(Outcome::Forward(()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn broadcast<S, A, T, C>(sender: &S, act: A, to: Vec<T>, proxy: Option<reqwest::Proxy>)
|
||||
where
|
||||
S: sign::Signer,
|
||||
A: Activity + serde::Serialize,
|
||||
A: Activity,
|
||||
T: inbox::AsActor<C>,
|
||||
{
|
||||
let boxes = to
|
||||
@ -147,79 +130,59 @@ where
|
||||
.sign(sender)
|
||||
.expect("activity_pub::broadcast: signature error");
|
||||
|
||||
let client = if let Some(proxy) = proxy {
|
||||
ClientBuilder::new().proxy(proxy)
|
||||
} else {
|
||||
ClientBuilder::new()
|
||||
}
|
||||
.connect_timeout(std::time::Duration::from_secs(5))
|
||||
.build()
|
||||
.expect("Can't build client");
|
||||
let rt = runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
let mut rt = tokio::runtime::current_thread::Runtime::new()
|
||||
.expect("Error while initializing tokio runtime for federation");
|
||||
rt.block_on(async {
|
||||
// TODO: should be determined dependent on database connections because
|
||||
// after broadcasting, target instance sends request to this instance,
|
||||
// and Plume accesses database at that time.
|
||||
let capacity = 6;
|
||||
let (tx, rx) = flume::bounded::<RequestBuilder>(capacity);
|
||||
let mut handles = Vec::with_capacity(capacity);
|
||||
for _ in 0..capacity {
|
||||
let rx = rx.clone();
|
||||
let handle = rt.spawn(async move {
|
||||
while let Ok(request_builder) = rx.recv_async().await {
|
||||
// After broadcasting, target instance sends request to this instance.
|
||||
// Sleep here in order to reduce requests at once
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
let _ = request_builder
|
||||
.send()
|
||||
.await
|
||||
.map(move |r| {
|
||||
if r.status().is_success() {
|
||||
debug!("Successfully sent activity to inbox ({})", &r.url());
|
||||
} else {
|
||||
warn!("Error while sending to inbox ({:?})", &r)
|
||||
}
|
||||
debug!("Response: \"{:?}\"\n", r);
|
||||
})
|
||||
.map_err(|e| warn!("Error while sending to inbox ({:?})", e));
|
||||
}
|
||||
});
|
||||
handles.push(handle);
|
||||
for inbox in boxes {
|
||||
let body = signed.to_string();
|
||||
let mut headers = request::headers();
|
||||
let url = Url::parse(&inbox);
|
||||
if url.is_err() {
|
||||
warn!("Inbox is invalid URL: {:?}", &inbox);
|
||||
continue;
|
||||
}
|
||||
for inbox in boxes {
|
||||
let body = signed.to_string();
|
||||
let mut headers = request::headers();
|
||||
let url = Url::parse(&inbox);
|
||||
if url.is_err() {
|
||||
warn!("Inbox is invalid URL: {:?}", &inbox);
|
||||
continue;
|
||||
let url = url.unwrap();
|
||||
if !url.has_host() {
|
||||
warn!("Inbox doesn't have host: {:?}", &inbox);
|
||||
continue;
|
||||
};
|
||||
let host_header_value = HeaderValue::from_str(&url.host_str().expect("Unreachable"));
|
||||
if host_header_value.is_err() {
|
||||
warn!("Header value is invalid: {:?}", url.host_str());
|
||||
continue;
|
||||
}
|
||||
headers.insert("Host", host_header_value.unwrap());
|
||||
headers.insert("Digest", request::Digest::digest(&body));
|
||||
rt.spawn(
|
||||
if let Some(proxy) = proxy.clone() {
|
||||
ClientBuilder::new().proxy(proxy)
|
||||
} else {
|
||||
ClientBuilder::new()
|
||||
}
|
||||
let url = url.unwrap();
|
||||
if !url.has_host() {
|
||||
warn!("Inbox doesn't have host: {:?}", &inbox);
|
||||
continue;
|
||||
};
|
||||
let host_header_value = HeaderValue::from_str(url.host_str().expect("Unreachable"));
|
||||
if host_header_value.is_err() {
|
||||
warn!("Header value is invalid: {:?}", url.host_str());
|
||||
continue;
|
||||
}
|
||||
headers.insert("Host", host_header_value.unwrap());
|
||||
headers.insert("Digest", request::Digest::digest(&body));
|
||||
headers.insert(
|
||||
.connect_timeout(std::time::Duration::from_secs(5))
|
||||
.build()
|
||||
.expect("Can't build client")
|
||||
.post(&inbox)
|
||||
.headers(headers.clone())
|
||||
.header(
|
||||
"Signature",
|
||||
request::signature(sender, &headers, ("post", url.path(), url.query()))
|
||||
.expect("activity_pub::broadcast: request signature error"),
|
||||
);
|
||||
let request_builder = client.post(&inbox).headers(headers.clone()).body(body);
|
||||
let _ = tx.send_async(request_builder).await;
|
||||
}
|
||||
drop(tx);
|
||||
join_all(handles).await;
|
||||
});
|
||||
)
|
||||
.body(body)
|
||||
.send()
|
||||
.and_then(move |r| {
|
||||
if r.status().is_success() {
|
||||
debug!("Successfully sent activity to inbox ({})", &inbox);
|
||||
} else {
|
||||
warn!("Error while sending to inbox ({:?})", &r)
|
||||
}
|
||||
r.into_body().concat2()
|
||||
})
|
||||
.map(move |response| debug!("Response: \"{:?}\"\n", response))
|
||||
.map_err(|e| warn!("Error while sending to inbox ({:?})", e)),
|
||||
);
|
||||
}
|
||||
rt.run().unwrap();
|
||||
}
|
||||
|
||||
#[derive(Shrinkwrap, Clone, Serialize, Deserialize)]
|
||||
@ -241,193 +204,46 @@ pub trait IntoId {
|
||||
fn into_id(self) -> Id;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
impl Link for Id {}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApSignature {
|
||||
pub public_key: PublicKey,
|
||||
#[activitystreams(concrete(PublicKey), functional)]
|
||||
pub public_key: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PublicKey {
|
||||
pub id: IriString,
|
||||
pub owner: IriString,
|
||||
pub public_key_pem: String,
|
||||
#[activitystreams(concrete(String), functional)]
|
||||
pub id: Option<serde_json::Value>,
|
||||
|
||||
#[activitystreams(concrete(String), functional)]
|
||||
pub owner: Option<serde_json::Value>,
|
||||
|
||||
#[activitystreams(concrete(String), functional)]
|
||||
pub public_key_pem: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl<U> UnparsedExtension<U> for ApSignature
|
||||
where
|
||||
U: UnparsedMutExt,
|
||||
{
|
||||
type Error = serde_json::Error;
|
||||
#[derive(Clone, Debug, Default, UnitString)]
|
||||
#[activitystreams(Hashtag)]
|
||||
pub struct HashtagType;
|
||||
|
||||
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
|
||||
Ok(ApSignature {
|
||||
public_key: unparsed_mut.remove("publicKey")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
|
||||
unparsed_mut.insert("publicKey", self.public_key)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SourceProperty {
|
||||
pub source: Source,
|
||||
}
|
||||
|
||||
impl<U> UnparsedExtension<U> for SourceProperty
|
||||
where
|
||||
U: UnparsedMutExt,
|
||||
{
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
|
||||
Ok(SourceProperty {
|
||||
source: unparsed_mut.remove("source")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
|
||||
unparsed_mut.insert("source", self.source)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub type CustomPerson = Ext1<ApActor<Person>, ApSignature>;
|
||||
pub type CustomGroup = Ext2<ApActor<Group>, ApSignature, SourceProperty>;
|
||||
|
||||
kind!(HashtagType, Hashtag);
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Hashtag {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub href: Option<IriString>,
|
||||
#[serde(rename = "type")]
|
||||
kind: HashtagType,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<AnyString>,
|
||||
#[activitystreams(concrete(String), functional)]
|
||||
pub href: Option<serde_json::Value>,
|
||||
|
||||
#[serde(flatten)]
|
||||
inner: Object<HashtagType>,
|
||||
#[activitystreams(concrete(String), functional)]
|
||||
pub name: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl Hashtag {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
href: None,
|
||||
name: None,
|
||||
inner: Object::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extending(mut inner: Object<HashtagType>) -> Result<Self, serde_json::Error> {
|
||||
let href = inner.remove("href")?;
|
||||
let name = inner.remove("name")?;
|
||||
|
||||
Ok(Self { href, name, inner })
|
||||
}
|
||||
|
||||
pub fn retracting(self) -> Result<Object<HashtagType>, serde_json::Error> {
|
||||
let Self {
|
||||
href,
|
||||
name,
|
||||
mut inner,
|
||||
} = self;
|
||||
|
||||
inner.insert("href", href)?;
|
||||
inner.insert("name", name)?;
|
||||
Ok(inner)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AsHashtag: markers::Object {
|
||||
fn hashtag_ref(&self) -> &Hashtag;
|
||||
|
||||
fn hashtag_mut(&mut self) -> &mut Hashtag;
|
||||
}
|
||||
|
||||
pub trait HashtagExt: AsHashtag {
|
||||
fn href(&self) -> Option<&IriString> {
|
||||
self.hashtag_ref().href.as_ref()
|
||||
}
|
||||
|
||||
fn set_href<T>(&mut self, href: T) -> &mut Self
|
||||
where
|
||||
T: Into<IriString>,
|
||||
{
|
||||
self.hashtag_mut().href = Some(href.into());
|
||||
self
|
||||
}
|
||||
|
||||
fn take_href(&mut self) -> Option<IriString> {
|
||||
self.hashtag_mut().href.take()
|
||||
}
|
||||
|
||||
fn delete_href(&mut self) -> &mut Self {
|
||||
self.hashtag_mut().href = None;
|
||||
self
|
||||
}
|
||||
|
||||
fn name(&self) -> Option<&AnyString> {
|
||||
self.hashtag_ref().name.as_ref()
|
||||
}
|
||||
|
||||
fn set_name<T>(&mut self, name: T) -> &mut Self
|
||||
where
|
||||
T: Into<AnyString>,
|
||||
{
|
||||
self.hashtag_mut().name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
fn take_name(&mut self) -> Option<AnyString> {
|
||||
self.hashtag_mut().name.take()
|
||||
}
|
||||
|
||||
fn delete_name(&mut self) -> &mut Self {
|
||||
self.hashtag_mut().name = None;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Hashtag {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsHashtag for Hashtag {
|
||||
fn hashtag_ref(&self) -> &Self {
|
||||
self
|
||||
}
|
||||
|
||||
fn hashtag_mut(&mut self) -> &mut Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Extends<HashtagType> for Hashtag {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn extends(base: Base<HashtagType>) -> Result<Self, Self::Error> {
|
||||
let inner = Object::extends(base)?;
|
||||
Self::extending(inner)
|
||||
}
|
||||
|
||||
fn retracts(self) -> Result<Base<HashtagType>, Self::Error> {
|
||||
let inner = self.retracting()?;
|
||||
inner.retracts()
|
||||
}
|
||||
}
|
||||
|
||||
impl markers::Base for Hashtag {}
|
||||
impl markers::Object for Hashtag {}
|
||||
impl<T> HashtagExt for T where T: AsHashtag {}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Source {
|
||||
pub media_type: String,
|
||||
@ -435,366 +251,13 @@ pub struct Source {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl<U> UnparsedExtension<U> for Source
|
||||
where
|
||||
U: UnparsedMutExt,
|
||||
{
|
||||
type Error = serde_json::Error;
|
||||
impl Object for Source {}
|
||||
|
||||
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
|
||||
Ok(Source {
|
||||
content: unparsed_mut.remove("content")?,
|
||||
media_type: unparsed_mut.remove("mediaType")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
|
||||
unparsed_mut.insert("content", self.content)?;
|
||||
unparsed_mut.insert("mediaType", self.media_type)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Licensed {
|
||||
pub license: Option<String>,
|
||||
#[activitystreams(concrete(String), functional)]
|
||||
pub license: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl<U> UnparsedExtension<U> for Licensed
|
||||
where
|
||||
U: UnparsedMutExt,
|
||||
{
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
|
||||
Ok(Licensed {
|
||||
license: unparsed_mut.remove("license")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
|
||||
unparsed_mut.insert("license", self.license)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub type LicensedArticle = Ext1<ApObject<Article>, Licensed>;
|
||||
|
||||
pub trait ToAsString {
|
||||
fn to_as_string(&self) -> Option<String>;
|
||||
}
|
||||
|
||||
impl ToAsString for OneOrMany<&AnyString> {
|
||||
fn to_as_string(&self) -> Option<String> {
|
||||
self.as_as_str().map(|s| s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
trait AsAsStr {
|
||||
fn as_as_str(&self) -> Option<&str>;
|
||||
}
|
||||
|
||||
impl AsAsStr for OneOrMany<&AnyString> {
|
||||
fn as_as_str(&self) -> Option<&str> {
|
||||
self.iter().next().map(|prop| prop.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToAsUri {
|
||||
fn to_as_uri(&self) -> Option<String>;
|
||||
}
|
||||
|
||||
impl ToAsUri for OneOrMany<AnyBase> {
|
||||
fn to_as_uri(&self) -> Option<String> {
|
||||
self.iter()
|
||||
.next()
|
||||
.and_then(|prop| prop.as_xsd_any_uri().map(|uri| uri.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use activitystreams::{
|
||||
activity::{ActorAndObjectRef, Create},
|
||||
object::{kind::ArticleType, Image},
|
||||
prelude::{ApActorExt, BaseExt, ExtendsExt, ObjectExt},
|
||||
};
|
||||
use assert_json_diff::assert_json_eq;
|
||||
use serde_json::{from_str, json, to_value};
|
||||
|
||||
#[test]
|
||||
fn se_ap_signature() {
|
||||
let ap_signature = ApSignature {
|
||||
public_key: PublicKey {
|
||||
id: "https://example.com/pubkey".parse().unwrap(),
|
||||
owner: "https://example.com/owner".parse().unwrap(),
|
||||
public_key_pem: "pubKeyPem".into(),
|
||||
},
|
||||
};
|
||||
let expected = json!({
|
||||
"publicKey": {
|
||||
"id": "https://example.com/pubkey",
|
||||
"owner": "https://example.com/owner",
|
||||
"publicKeyPem": "pubKeyPem"
|
||||
}
|
||||
});
|
||||
assert_json_eq!(to_value(ap_signature).unwrap(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn de_ap_signature() {
|
||||
let value: ApSignature = from_str(
|
||||
r#"
|
||||
{
|
||||
"publicKey": {
|
||||
"id": "https://example.com/",
|
||||
"owner": "https://example.com/",
|
||||
"publicKeyPem": ""
|
||||
}
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let expected = ApSignature {
|
||||
public_key: PublicKey {
|
||||
id: "https://example.com/".parse().unwrap(),
|
||||
owner: "https://example.com/".parse().unwrap(),
|
||||
public_key_pem: "".into(),
|
||||
},
|
||||
};
|
||||
assert_eq!(value, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn se_custom_person() {
|
||||
let actor = ApActor::new("https://example.com/inbox".parse().unwrap(), Person::new());
|
||||
let person = CustomPerson::new(
|
||||
actor,
|
||||
ApSignature {
|
||||
public_key: PublicKey {
|
||||
id: "https://example.com/pubkey".parse().unwrap(),
|
||||
owner: "https://example.com/owner".parse().unwrap(),
|
||||
public_key_pem: "pubKeyPem".into(),
|
||||
},
|
||||
},
|
||||
);
|
||||
let expected = json!({
|
||||
"inbox": "https://example.com/inbox",
|
||||
"type": "Person",
|
||||
"publicKey": {
|
||||
"id": "https://example.com/pubkey",
|
||||
"owner": "https://example.com/owner",
|
||||
"publicKeyPem": "pubKeyPem"
|
||||
}
|
||||
});
|
||||
assert_eq!(to_value(person).unwrap(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn se_custom_group() {
|
||||
let group = CustomGroup::new(
|
||||
ApActor::new("https://example.com/inbox".parse().unwrap(), Group::new()),
|
||||
ApSignature {
|
||||
public_key: PublicKey {
|
||||
id: "https://example.com/pubkey".parse().unwrap(),
|
||||
owner: "https://example.com/owner".parse().unwrap(),
|
||||
public_key_pem: "pubKeyPem".into(),
|
||||
},
|
||||
},
|
||||
SourceProperty {
|
||||
source: Source {
|
||||
content: String::from("This is a *custom* group."),
|
||||
media_type: String::from("text/markdown"),
|
||||
},
|
||||
},
|
||||
);
|
||||
let expected = json!({
|
||||
"inbox": "https://example.com/inbox",
|
||||
"type": "Group",
|
||||
"publicKey": {
|
||||
"id": "https://example.com/pubkey",
|
||||
"owner": "https://example.com/owner",
|
||||
"publicKeyPem": "pubKeyPem"
|
||||
},
|
||||
"source": {
|
||||
"content": "This is a *custom* group.",
|
||||
"mediaType": "text/markdown"
|
||||
}
|
||||
});
|
||||
assert_eq!(to_value(group).unwrap(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn de_custom_group() {
|
||||
let value: CustomGroup = from_str(
|
||||
r#"
|
||||
{
|
||||
"icon": {
|
||||
"type": "Image"
|
||||
},
|
||||
"id": "https://plume01.localhost/~/Plume01%20Blog%202/",
|
||||
"image": {
|
||||
"type": "Image"
|
||||
},
|
||||
"inbox": "https://plume01.localhost/~/Plume01%20Blog%202/inbox",
|
||||
"name": "Plume01 Blog 2",
|
||||
"outbox": "https://plume01.localhost/~/Plume01%20Blog%202/outbox",
|
||||
"preferredUsername": "Plume01 Blog 2",
|
||||
"publicKey": {
|
||||
"id": "https://plume01.localhost/~/Plume01%20Blog%202/#main-key",
|
||||
"owner": "https://plume01.localhost/~/Plume01%20Blog%202/",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwPGtKkl/iMsNAyeVaJGz\noEz5PoNkjRnKK7G97MFvb4zw9zs5SpzWW7b/pKHa4dODcGDJXmkCJ1H5JWyguzN8\n2GNoFjtEOJHxEGwBHSYDsTmhuLNB0DKxMU2iu55g8iIiXhZiIW1FBNGs/Geaymvr\nh/TEtzdReN8wzloRR55kOVcU49xBkqx8cfDSk/lrrDLlpveHdqgaFnIvuw2vycK0\nxFzS3xlEUpzJk9kHxoR1uEAfZ+gCv26Sgo/HqOAhqSD5IU3QZC3kdkr/hwVqtr8U\nXGkGG6Mo1rgzhkYiCFkWrV2WoKkcEHD4nEzbgoZZ5MyuSoloxnyF3NiScqmqW+Yx\nkQIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"source": {
|
||||
"content": "",
|
||||
"mediaType": "text/markdown"
|
||||
},
|
||||
"summary": "",
|
||||
"type": "Group"
|
||||
}
|
||||
"#
|
||||
).unwrap();
|
||||
let mut expected = CustomGroup::new(
|
||||
ApActor::new("https://plume01.localhost/~/Plume01%20Blog%202/inbox".parse().unwrap(), Group::new()),
|
||||
ApSignature {
|
||||
public_key: PublicKey {
|
||||
id: "https://plume01.localhost/~/Plume01%20Blog%202/#main-key".parse().unwrap(),
|
||||
owner: "https://plume01.localhost/~/Plume01%20Blog%202/".parse().unwrap(),
|
||||
public_key_pem: "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwPGtKkl/iMsNAyeVaJGz\noEz5PoNkjRnKK7G97MFvb4zw9zs5SpzWW7b/pKHa4dODcGDJXmkCJ1H5JWyguzN8\n2GNoFjtEOJHxEGwBHSYDsTmhuLNB0DKxMU2iu55g8iIiXhZiIW1FBNGs/Geaymvr\nh/TEtzdReN8wzloRR55kOVcU49xBkqx8cfDSk/lrrDLlpveHdqgaFnIvuw2vycK0\nxFzS3xlEUpzJk9kHxoR1uEAfZ+gCv26Sgo/HqOAhqSD5IU3QZC3kdkr/hwVqtr8U\nXGkGG6Mo1rgzhkYiCFkWrV2WoKkcEHD4nEzbgoZZ5MyuSoloxnyF3NiScqmqW+Yx\nkQIDAQAB\n-----END PUBLIC KEY-----\n".into(),
|
||||
}
|
||||
},
|
||||
SourceProperty {
|
||||
source: Source {
|
||||
content: String::from(""),
|
||||
media_type: String::from("text/markdown")
|
||||
}
|
||||
}
|
||||
);
|
||||
expected.set_icon(Image::new().into_any_base().unwrap());
|
||||
expected.set_id(
|
||||
"https://plume01.localhost/~/Plume01%20Blog%202/"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
expected.set_image(Image::new().into_any_base().unwrap());
|
||||
expected.set_name("Plume01 Blog 2");
|
||||
expected.set_outbox(
|
||||
"https://plume01.localhost/~/Plume01%20Blog%202/outbox"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
expected.set_preferred_username("Plume01 Blog 2");
|
||||
expected.set_summary("");
|
||||
|
||||
assert_json_eq!(value, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn se_licensed_article() {
|
||||
let object = ApObject::new(Article::new());
|
||||
let licensed_article = LicensedArticle::new(
|
||||
object,
|
||||
Licensed {
|
||||
license: Some("CC-0".into()),
|
||||
},
|
||||
);
|
||||
let expected = json!({
|
||||
"type": "Article",
|
||||
"license": "CC-0",
|
||||
});
|
||||
assert_json_eq!(to_value(licensed_article).unwrap(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn de_licensed_article() {
|
||||
let value: LicensedArticle = from_str(
|
||||
r#"
|
||||
{
|
||||
"type": "Article",
|
||||
"id": "https://plu.me/~/Blog/my-article",
|
||||
"attributedTo": ["https://plu.me/@/Admin", "https://plu.me/~/Blog"],
|
||||
"content": "Hello.",
|
||||
"name": "My Article",
|
||||
"summary": "Bye.",
|
||||
"source": {
|
||||
"content": "Hello.",
|
||||
"mediaType": "text/markdown"
|
||||
},
|
||||
"published": "2014-12-12T12:12:12Z",
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"license": "CC-0"
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let expected = json!({
|
||||
"type": "Article",
|
||||
"id": "https://plu.me/~/Blog/my-article",
|
||||
"attributedTo": ["https://plu.me/@/Admin", "https://plu.me/~/Blog"],
|
||||
"content": "Hello.",
|
||||
"name": "My Article",
|
||||
"summary": "Bye.",
|
||||
"source": {
|
||||
"content": "Hello.",
|
||||
"mediaType": "text/markdown"
|
||||
},
|
||||
"published": "2014-12-12T12:12:12Z",
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"license": "CC-0"
|
||||
});
|
||||
|
||||
assert_eq!(to_value(value).unwrap(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn de_create_with_licensed_article() {
|
||||
let create: Create = from_str(
|
||||
r#"
|
||||
{
|
||||
"id": "https://plu.me/~/Blog/my-article",
|
||||
"type": "Create",
|
||||
"actor": "https://plu.me/@/Admin",
|
||||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"object": {
|
||||
"type": "Article",
|
||||
"id": "https://plu.me/~/Blog/my-article",
|
||||
"attributedTo": ["https://plu.me/@/Admin", "https://plu.me/~/Blog"],
|
||||
"content": "Hello.",
|
||||
"name": "My Article",
|
||||
"summary": "Bye.",
|
||||
"source": {
|
||||
"content": "Hello.",
|
||||
"mediaType": "text/markdown"
|
||||
},
|
||||
"published": "2014-12-12T12:12:12Z",
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"license": "CC-0"
|
||||
}
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let base = create.object_field_ref().as_single_base().unwrap();
|
||||
let any_base = AnyBase::from_base(base.clone());
|
||||
let value = any_base.extend::<LicensedArticle, ArticleType>().unwrap();
|
||||
let expected = json!({
|
||||
"type": "Article",
|
||||
"id": "https://plu.me/~/Blog/my-article",
|
||||
"attributedTo": ["https://plu.me/@/Admin", "https://plu.me/~/Blog"],
|
||||
"content": "Hello.",
|
||||
"name": "My Article",
|
||||
"summary": "Bye.",
|
||||
"source": {
|
||||
"content": "Hello.",
|
||||
"mediaType": "text/markdown"
|
||||
},
|
||||
"published": "2014-12-12T12:12:12Z",
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"license": "CC-0"
|
||||
});
|
||||
|
||||
assert_eq!(to_value(value).unwrap(), expected);
|
||||
}
|
||||
}
|
||||
impl Object for Licensed {}
|
||||
|
@ -1,12 +1,6 @@
|
||||
use chrono::{offset::Utc, DateTime};
|
||||
use openssl::hash::{Hasher, MessageDigest};
|
||||
use reqwest::{
|
||||
blocking::{ClientBuilder, Response},
|
||||
header::{
|
||||
HeaderMap, HeaderValue, InvalidHeaderValue, ACCEPT, CONTENT_TYPE, DATE, HOST, USER_AGENT,
|
||||
},
|
||||
Proxy, Url,
|
||||
};
|
||||
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, DATE, USER_AGENT};
|
||||
use std::ops::Deref;
|
||||
use std::time::SystemTime;
|
||||
use tracing::warn;
|
||||
@ -19,24 +13,6 @@ const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION"));
|
||||
#[derive(Debug)]
|
||||
pub struct Error();
|
||||
|
||||
impl From<url::ParseError> for Error {
|
||||
fn from(_err: url::ParseError) -> Self {
|
||||
Error()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InvalidHeaderValue> for Error {
|
||||
fn from(_err: InvalidHeaderValue) -> Self {
|
||||
Error()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
fn from(_err: reqwest::Error) -> Self {
|
||||
Error()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Digest(String);
|
||||
|
||||
impl Digest {
|
||||
@ -188,35 +164,12 @@ pub fn signature(
|
||||
)).map_err(|_| Error())
|
||||
}
|
||||
|
||||
pub fn get(url_str: &str, sender: &dyn Signer, proxy: Option<Proxy>) -> Result<Response, Error> {
|
||||
let mut headers = headers();
|
||||
let url = Url::parse(url_str)?;
|
||||
if !url.has_host() {
|
||||
return Err(Error());
|
||||
}
|
||||
let host_header_value = HeaderValue::from_str(url.host_str().expect("Unreachable"))?;
|
||||
headers.insert(HOST, host_header_value);
|
||||
if let Some(proxy) = proxy {
|
||||
ClientBuilder::new().proxy(proxy)
|
||||
} else {
|
||||
ClientBuilder::new()
|
||||
}
|
||||
.connect_timeout(Some(std::time::Duration::from_secs(5)))
|
||||
.build()?
|
||||
.get(url_str)
|
||||
.headers(headers.clone())
|
||||
.header(
|
||||
"Signature",
|
||||
signature(sender, &headers, ("get", url.path(), url.query()))?,
|
||||
)
|
||||
.send()
|
||||
.map_err(|_| Error())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::signature;
|
||||
use crate::activity_pub::sign::{gen_keypair, Error, Result, Signer};
|
||||
use crate::activity_pub::sign::{
|
||||
gen_keypair, Error as SignatureError, Result as SignatureResult, Signer,
|
||||
};
|
||||
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
|
||||
use reqwest::header::HeaderMap;
|
||||
|
||||
@ -240,20 +193,20 @@ mod tests {
|
||||
"mysigner".into()
|
||||
}
|
||||
|
||||
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
|
||||
fn sign(&self, to_sign: &str) -> SignatureResult<Vec<u8>> {
|
||||
let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
|
||||
.unwrap();
|
||||
let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
|
||||
signer.update(to_sign.as_bytes()).unwrap();
|
||||
signer.sign_to_vec().map_err(|_| Error())
|
||||
signer.sign_to_vec().map_err(|_| SignatureError())
|
||||
}
|
||||
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<bool> {
|
||||
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
|
||||
.unwrap();
|
||||
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
|
||||
verifier.update(data.as_bytes()).unwrap();
|
||||
verifier.verify(signature).map_err(|_| Error())
|
||||
verifier.verify(&signature).map_err(|_| SignatureError())
|
||||
}
|
||||
}
|
||||
|
||||
@ -262,7 +215,7 @@ mod tests {
|
||||
let signer = MySigner::new();
|
||||
let headers = HeaderMap::new();
|
||||
let result = signature(&signer, &headers, ("post", "/inbox", None)).unwrap();
|
||||
let fields: Vec<&str> = result.to_str().unwrap().split(',').collect();
|
||||
let fields: Vec<&str> = result.to_str().unwrap().split(",").collect();
|
||||
assert_eq!(r#"headers="(request-target)""#, fields[2]);
|
||||
let sign = &fields[3][11..(fields[3].len() - 1)];
|
||||
assert!(signer.verify("post /inbox", sign.as_bytes()).is_ok());
|
||||
|
@ -119,7 +119,7 @@ impl Signable for serde_json::Value {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum SignatureValidity {
|
||||
Invalid,
|
||||
ValidNoDigest,
|
||||
@ -187,7 +187,7 @@ pub fn verify_http_headers<S: Signer + ::std::fmt::Debug>(
|
||||
}
|
||||
let digest = all_headers.get_one("digest").unwrap_or("");
|
||||
let digest = request::Digest::from_header(digest);
|
||||
if !digest.map(|d| d.verify_header(data)).unwrap_or(false) {
|
||||
if !digest.map(|d| d.verify_header(&data)).unwrap_or(false) {
|
||||
// signature was valid, but body content does not match its digest
|
||||
return SignatureValidity::Invalid;
|
||||
}
|
||||
|
2
plume-common/src/lib.rs
Normal file → Executable file
2
plume-common/src/lib.rs
Normal file → Executable file
@ -1,5 +1,7 @@
|
||||
#![feature(associated_type_defaults)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate activitystreams_derive;
|
||||
#[macro_use]
|
||||
extern crate shrinkwraprs;
|
||||
#[macro_use]
|
||||
|
@ -1,7 +1,11 @@
|
||||
use heck::CamelCase;
|
||||
use openssl::rand::rand_bytes;
|
||||
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag};
|
||||
use regex_syntax::is_word_character;
|
||||
use rocket::http::uri::Uri;
|
||||
use rocket::{
|
||||
http::uri::Uri,
|
||||
response::{Flash, Redirect},
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
use syntect::html::{ClassStyle, ClassedHTMLGenerator};
|
||||
use syntect::parsing::SyntaxSet;
|
||||
@ -15,6 +19,14 @@ pub fn random_hex() -> String {
|
||||
.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_camel_case()
|
||||
.chars()
|
||||
.filter(|c| c.is_alphanumeric())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/**
|
||||
* Percent-encode characters which are not allowed in IRI path segments.
|
||||
*
|
||||
@ -68,6 +80,19 @@ pub fn iri_percent_encode_seg_char(c: char) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects to the login page with a given message.
|
||||
*
|
||||
* Note that the message should be translated before passed to this function.
|
||||
*/
|
||||
pub fn requires_login<T: Into<Uri<'static>>>(message: &str, url: T) -> Flash<Redirect> {
|
||||
Flash::new(
|
||||
Redirect::to(format!("/login?m={}", Uri::percent_encode(message))),
|
||||
"callback",
|
||||
url.into().to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum State {
|
||||
Mention,
|
||||
@ -116,13 +141,13 @@ fn highlight_code<'a>(
|
||||
unreachable!();
|
||||
};
|
||||
let syntax_set = SyntaxSet::load_defaults_newlines();
|
||||
let syntax = syntax_set.find_syntax_by_token(lang).unwrap_or_else(|| {
|
||||
let syntax = syntax_set.find_syntax_by_token(&lang).unwrap_or_else(|| {
|
||||
syntax_set
|
||||
.find_syntax_by_name(lang)
|
||||
.find_syntax_by_name(&lang)
|
||||
.unwrap_or_else(|| syntax_set.find_syntax_plain_text())
|
||||
});
|
||||
let mut html = ClassedHTMLGenerator::new_with_class_style(
|
||||
syntax,
|
||||
&syntax,
|
||||
&syntax_set,
|
||||
ClassStyle::Spaced,
|
||||
);
|
||||
@ -262,7 +287,7 @@ pub fn md_to_html<'a>(
|
||||
media_processor: Option<MediaProcessor<'a>>,
|
||||
) -> (String, HashSet<String>, HashSet<String>) {
|
||||
let base_url = if let Some(base_url) = base_url {
|
||||
format!("https://{}/", base_url)
|
||||
format!("//{}/", base_url)
|
||||
} else {
|
||||
"/".to_owned()
|
||||
};
|
||||
@ -309,15 +334,16 @@ pub fn md_to_html<'a>(
|
||||
text_acc.push(c)
|
||||
}
|
||||
let mention = text_acc;
|
||||
let short_mention = mention.splitn(1, '@').next().unwrap_or("");
|
||||
let link = Tag::Link(
|
||||
LinkType::Inline,
|
||||
format!("{}@/{}/", base_url, &mention).into(),
|
||||
mention.clone().into(),
|
||||
short_mention.to_owned().into(),
|
||||
);
|
||||
|
||||
mentions.push(mention.clone());
|
||||
events.push(Event::Start(link.clone()));
|
||||
events.push(Event::Text(format!("@{}", &mention).into()));
|
||||
events.push(Event::Text(format!("@{}", &short_mention).into()));
|
||||
events.push(Event::End(link));
|
||||
|
||||
(
|
||||
@ -441,10 +467,6 @@ pub fn md_to_html<'a>(
|
||||
(buf, mentions.collect(), hashtags.collect())
|
||||
}
|
||||
|
||||
pub fn escape(string: &str) -> askama_escape::Escaped<askama_escape::Html> {
|
||||
askama_escape::escape(string, askama_escape::Html)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -1,29 +1,26 @@
|
||||
[package]
|
||||
name = "plume-front"
|
||||
version = "0.7.2"
|
||||
version = "0.6.1-dev"
|
||||
authors = ["Plume contributors"]
|
||||
edition = "2018"
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
gettext = "0.4.0"
|
||||
gettext-macros = "0.6.1"
|
||||
gettext-utils = "0.1.0"
|
||||
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
||||
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||
lazy_static = "1.3"
|
||||
serde = "1.0.137"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
wasm-bindgen = "0.2.81"
|
||||
js-sys = "0.3.58"
|
||||
wasm-bindgen = "0.2.70"
|
||||
js-sys = "0.3.47"
|
||||
serde_derive = "1.0.123"
|
||||
console_error_panic_hook = "0.1.6"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.58"
|
||||
version = "0.3.47"
|
||||
features = [
|
||||
'console',
|
||||
'ClipboardEvent',
|
||||
|
@ -1,3 +1,2 @@
|
||||
pre-release-hook = ["cargo", "fmt"]
|
||||
pre-release-replacements = []
|
||||
release = false
|
||||
|
@ -54,6 +54,11 @@ pub enum EditorError {
|
||||
DOMError,
|
||||
}
|
||||
|
||||
impl From<std::option::NoneError> for EditorError {
|
||||
fn from(_: std::option::NoneError) -> Self {
|
||||
EditorError::NoneError
|
||||
}
|
||||
}
|
||||
const AUTOSAVE_DEBOUNCE_TIME: i32 = 5000;
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct AutosaveInformation {
|
||||
@ -193,7 +198,7 @@ fn clear_autosave() {
|
||||
.unwrap()
|
||||
.remove_item(&get_autosave_id())
|
||||
.unwrap();
|
||||
console::log_1(&format!("Saved to {}", &get_autosave_id()).into());
|
||||
console::log_1(&&format!("Saved to {}", &get_autosave_id()).into());
|
||||
}
|
||||
type TimeoutHandle = i32;
|
||||
lazy_static! {
|
||||
@ -361,9 +366,7 @@ fn init_editor() -> Result<(), EditorError> {
|
||||
return Ok(());
|
||||
}
|
||||
let old_ed = old_ed.unwrap();
|
||||
let old_title = document()
|
||||
.get_element_by_id("plume-editor-title")
|
||||
.ok_or(EditorError::NoneError)?;
|
||||
let old_title = document().get_element_by_id("plume-editor-title")?;
|
||||
old_ed
|
||||
.dyn_ref::<HtmlElement>()
|
||||
.unwrap()
|
||||
@ -397,9 +400,7 @@ fn init_editor() -> Result<(), EditorError> {
|
||||
content_val.clone(),
|
||||
false,
|
||||
)?;
|
||||
if !content_val.is_empty() {
|
||||
content.set_inner_html(&content_val);
|
||||
}
|
||||
content.set_inner_html(&content_val);
|
||||
|
||||
// character counter
|
||||
let character_counter = Closure::wrap(Box::new(mv!(content => move |_| {
|
||||
@ -433,8 +434,7 @@ fn init_editor() -> Result<(), EditorError> {
|
||||
bg.class_list().add_1("show").unwrap();
|
||||
})) as Box<dyn FnMut(MouseEvent)>);
|
||||
document()
|
||||
.get_element_by_id("publish")
|
||||
.ok_or(EditorError::NoneError)?
|
||||
.get_element_by_id("publish")?
|
||||
.add_event_listener_with_callback("click", show_popup.as_ref().unchecked_ref())
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
show_popup.forget();
|
||||
@ -528,14 +528,8 @@ fn init_popup(
|
||||
cover_label
|
||||
.set_attribute("for", "cover")
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
let cover = document
|
||||
.get_element_by_id("cover")
|
||||
.ok_or(EditorError::NoneError)?;
|
||||
cover
|
||||
.parent_element()
|
||||
.ok_or(EditorError::NoneError)?
|
||||
.remove_child(&cover)
|
||||
.ok();
|
||||
let cover = document.get_element_by_id("cover")?;
|
||||
cover.parent_element()?.remove_child(&cover).ok();
|
||||
popup
|
||||
.append_child(&cover_label)
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
@ -560,7 +554,7 @@ fn init_popup(
|
||||
draft.set_checked(draft_checkbox.checked());
|
||||
|
||||
draft_label
|
||||
.append_child(draft)
|
||||
.append_child(&draft)
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
draft_label
|
||||
.append_child(&document.create_text_node(&i18n!(CATALOG, "This is a draft")))
|
||||
@ -626,12 +620,11 @@ fn init_popup(
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
callback.forget();
|
||||
popup
|
||||
.append_child(button)
|
||||
.append_child(&button)
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
|
||||
document
|
||||
.body()
|
||||
.ok_or(EditorError::NoneError)?
|
||||
.body()?
|
||||
.append_child(&popup)
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
Ok(popup)
|
||||
@ -648,8 +641,7 @@ fn init_popup_bg() -> Result<Element, EditorError> {
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
|
||||
document()
|
||||
.body()
|
||||
.ok_or(EditorError::NoneError)?
|
||||
.body()?
|
||||
.append_child(&bg)
|
||||
.map_err(|_| EditorError::DOMError)?;
|
||||
let callback = Closure::wrap(Box::new(|_| close_popup()) as Box<dyn FnMut(MouseEvent)>);
|
||||
|
@ -1,5 +1,5 @@
|
||||
#![recursion_limit = "128"]
|
||||
#![feature(decl_macro, proc_macro_hygiene)]
|
||||
#![feature(decl_macro, proc_macro_hygiene, try_trait)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate gettext_macros;
|
||||
@ -23,7 +23,6 @@ init_i18n!(
|
||||
en,
|
||||
eo,
|
||||
es,
|
||||
eu,
|
||||
fa,
|
||||
fi,
|
||||
fr,
|
||||
@ -62,7 +61,7 @@ lazy_static! {
|
||||
static ref CATALOG: gettext::Catalog = {
|
||||
let catalogs = include_i18n!();
|
||||
let lang = window().unwrap().navigator().language().unwrap();
|
||||
let lang = lang.split_once('-').map_or("en", |x| x.0);
|
||||
let lang = lang.splitn(2, '-').next().unwrap_or("en");
|
||||
|
||||
let english_position = catalogs
|
||||
.iter()
|
||||
@ -86,7 +85,7 @@ pub fn main() -> Result<(), JsValue> {
|
||||
menu();
|
||||
search();
|
||||
editor::init()
|
||||
.map_err(|e| console::error_1(&format!("Editor error: {:?}", e).into()))
|
||||
.map_err(|e| console::error_1(&&format!("Editor error: {:?}", e).into()))
|
||||
.ok();
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "plume-macro"
|
||||
version = "0.7.2"
|
||||
version = "0.6.1-dev"
|
||||
authors = ["Trinity Pointard <trinity.pointard@insa-rennes.fr>"]
|
||||
edition = "2018"
|
||||
description = "Plume procedural macros"
|
||||
|
@ -1,3 +1,2 @@
|
||||
pre-release-hook = ["cargo", "fmt"]
|
||||
pre-release-replacements = []
|
||||
release = false
|
||||
|
@ -58,7 +58,7 @@ pub fn import_migrations(input: TokenStream) -> TokenStream {
|
||||
(name, up_sql, down_sql)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let migrations_name = migrations.iter().map(|m| &m.0);
|
||||
let migrations_name = migrations.iter().map(|m| &m.0).collect::<Vec<_>>();
|
||||
let migrations_up = migrations
|
||||
.iter()
|
||||
.map(|m| m.1.as_str())
|
||||
@ -103,7 +103,7 @@ fn file_to_migration(file: &str) -> TokenStream2 {
|
||||
acc.push('\n');
|
||||
}
|
||||
} else if let Some(acc_str) = line.strip_prefix("--#!") {
|
||||
acc.push_str(acc_str);
|
||||
acc.push_str(&acc_str);
|
||||
acc.push('\n');
|
||||
} else if line.starts_with("--") {
|
||||
continue;
|
||||
|
@ -1,47 +1,43 @@
|
||||
[package]
|
||||
name = "plume-models"
|
||||
version = "0.7.2"
|
||||
version = "0.6.1-dev"
|
||||
authors = ["Plume contributors"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
ammonia = "3.2.0"
|
||||
bcrypt = "0.12.1"
|
||||
guid-create = "0.2"
|
||||
itertools = "0.10.3"
|
||||
activitypub = "0.1.1"
|
||||
ammonia = "2.1.1"
|
||||
askama_escape = "0.1"
|
||||
bcrypt = "0.10.1"
|
||||
guid-create = "0.1"
|
||||
itertools = "0.8.0"
|
||||
lazy_static = "1.0"
|
||||
ldap3 = "0.11.1"
|
||||
ldap3 = "0.7.1"
|
||||
migrations_internals= "1.4.0"
|
||||
openssl = "0.10.40"
|
||||
rocket = "0.4.11"
|
||||
rocket_i18n = "0.4.1"
|
||||
reqwest = "0.11.11"
|
||||
scheduled-thread-pool = "0.2.7"
|
||||
serde = "1.0.137"
|
||||
rust-s3 = { version = "0.33.0", optional = true, features = ["blocking"] }
|
||||
openssl = "0.10.22"
|
||||
rocket = "=0.4.6"
|
||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||
reqwest = "0.9"
|
||||
scheduled-thread-pool = "0.2.2"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0.81"
|
||||
serde_json = "1.0"
|
||||
tantivy = "0.13.3"
|
||||
url = "2.1"
|
||||
walkdir = "2.2"
|
||||
webfinger = "0.4.1"
|
||||
whatlang = "0.16.2"
|
||||
shrinkwraprs = "0.3.0"
|
||||
diesel-derive-newtype = "1.0.0"
|
||||
glob = "0.3.1"
|
||||
whatlang = "0.11.1"
|
||||
shrinkwraprs = "0.2.1"
|
||||
diesel-derive-newtype = "0.1.2"
|
||||
glob = "0.3.0"
|
||||
lindera-tantivy = { version = "0.7.1", optional = true }
|
||||
tracing = "0.1.35"
|
||||
tracing = "0.1.22"
|
||||
riker = "0.4.2"
|
||||
once_cell = "1.12.0"
|
||||
lettre = "0.9.6"
|
||||
native-tls = "0.2.10"
|
||||
activitystreams = "=0.7.0-alpha.20"
|
||||
ahash = "=0.8.6"
|
||||
heck = "0.4.1"
|
||||
once_cell = "1.5.2"
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
version = "0.4.31"
|
||||
version = "0.4"
|
||||
|
||||
[dependencies.diesel]
|
||||
features = ["r2d2", "chrono"]
|
||||
@ -57,11 +53,9 @@ path = "../plume-common"
|
||||
path = "../plume-macro"
|
||||
|
||||
[dev-dependencies]
|
||||
assert-json-diff = "2.0.1"
|
||||
diesel_migrations = "1.3.0"
|
||||
|
||||
[features]
|
||||
postgres = ["diesel/postgres", "plume-macro/postgres" ]
|
||||
sqlite = ["diesel/sqlite", "plume-macro/sqlite" ]
|
||||
search-lindera = ["lindera-tantivy"]
|
||||
s3 = ["rust-s3"]
|
||||
|
@ -1,3 +1,2 @@
|
||||
pre-release-hook = ["cargo", "fmt"]
|
||||
pre-release-replacements = []
|
||||
release = false
|
||||
|
@ -5,7 +5,7 @@ use rocket::{
|
||||
Outcome,
|
||||
};
|
||||
|
||||
/// Wrapper around User to use as a request guard on pages exclusively reserved to admins.
|
||||
/// Wrapper around User to use as a request guard on pages reserved to admins.
|
||||
pub struct Admin(pub User);
|
||||
|
||||
impl<'a, 'r> FromRequest<'a, 'r> for Admin {
|
||||
@ -21,23 +21,6 @@ impl<'a, 'r> FromRequest<'a, 'r> for Admin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Same as `Admin` but it forwards to next guard if the user is not an admin.
|
||||
/// It's useful when there are multiple implementations of routes for admin and moderator.
|
||||
pub struct InclusiveAdmin(pub User);
|
||||
|
||||
impl<'a, 'r> FromRequest<'a, 'r> for InclusiveAdmin {
|
||||
type Error = ();
|
||||
|
||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<InclusiveAdmin, ()> {
|
||||
let user = request.guard::<User>()?;
|
||||
if user.is_admin() {
|
||||
Outcome::Success(InclusiveAdmin(user))
|
||||
} else {
|
||||
Outcome::Forward(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Same as `Admin` but for moderators.
|
||||
pub struct Moderator(pub User);
|
||||
|
||||
|
@ -86,24 +86,20 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApiToken {
|
||||
}
|
||||
|
||||
let mut parsed_header = headers[0].split(' ');
|
||||
let auth_type = parsed_header
|
||||
.next()
|
||||
.map_or_else::<rocket::Outcome<&str, _, ()>, _, _>(
|
||||
|| Outcome::Failure((Status::BadRequest, TokenError::NoType)),
|
||||
Outcome::Success,
|
||||
)?;
|
||||
let val = parsed_header
|
||||
.next()
|
||||
.map_or_else::<rocket::Outcome<&str, _, ()>, _, _>(
|
||||
|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)),
|
||||
Outcome::Success,
|
||||
)?;
|
||||
let auth_type = parsed_header.next().map_or_else(
|
||||
|| Outcome::Failure((Status::BadRequest, TokenError::NoType)),
|
||||
Outcome::Success,
|
||||
)?;
|
||||
let val = parsed_header.next().map_or_else(
|
||||
|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)),
|
||||
Outcome::Success,
|
||||
)?;
|
||||
|
||||
if auth_type == "Bearer" {
|
||||
let conn = request
|
||||
.guard::<DbConn>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ impl BlocklistedEmail {
|
||||
pub fn delete_entries(conn: &Connection, ids: Vec<i32>) -> Result<bool> {
|
||||
use diesel::delete;
|
||||
for i in ids {
|
||||
let be: BlocklistedEmail = BlocklistedEmail::find_by_id(conn, i)?;
|
||||
let be: BlocklistedEmail = BlocklistedEmail::find_by_id(&conn, i)?;
|
||||
delete(&be).execute(conn)?;
|
||||
}
|
||||
Ok(true)
|
||||
@ -126,9 +126,12 @@ pub(crate) mod tests {
|
||||
.id,
|
||||
various[1].id
|
||||
);
|
||||
assert!(BlocklistedEmail::matches_blocklist(&conn, no_match)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert_eq!(
|
||||
BlocklistedEmail::matches_blocklist(&conn, no_match)
|
||||
.unwrap()
|
||||
.is_none(),
|
||||
true
|
||||
);
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
@ -1,15 +1,12 @@
|
||||
use heck::ToUpperCamelCase;
|
||||
use crate::{
|
||||
instance::*, medias::Media, posts::Post, safe_string::SafeString, schema::blogs, users::User,
|
||||
Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE,
|
||||
ap_url, db_conn::DbConn, instance::*, medias::Media, posts::Post, safe_string::SafeString,
|
||||
schema::blogs, users::User, Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE,
|
||||
};
|
||||
use activitystreams::{
|
||||
actor::{ApActor, ApActorExt, AsApActor, Group},
|
||||
base::AnyBase,
|
||||
use activitypub::{
|
||||
actor::Group,
|
||||
collection::{OrderedCollection, OrderedCollectionPage},
|
||||
iri_string::types::IriString,
|
||||
object::{kind::ImageType, ApObject, Image, ObjectExt},
|
||||
prelude::*,
|
||||
object::Image,
|
||||
CustomObject,
|
||||
};
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::{self, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||
@ -19,17 +16,17 @@ use openssl::{
|
||||
rsa::Rsa,
|
||||
sign::{Signer, Verifier},
|
||||
};
|
||||
use plume_common::{
|
||||
activity_pub::{
|
||||
inbox::{AsActor, FromId},
|
||||
sign, ActivityStream, ApSignature, CustomGroup, Id, IntoId, PublicKey, Source,
|
||||
SourceProperty, ToAsString, ToAsUri,
|
||||
},
|
||||
utils::iri_percent_encode_seg,
|
||||
use plume_common::activity_pub::{
|
||||
inbox::{AsActor, FromId},
|
||||
sign::{self, Error as SignatureError, Result as SignatureResult},
|
||||
ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
|
||||
};
|
||||
use url::Url;
|
||||
use webfinger::*;
|
||||
|
||||
#[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)]
|
||||
pub type CustomGroup = CustomObject<ApSignature, Group>;
|
||||
|
||||
#[derive(Queryable, Identifiable, Clone, AsChangeset)]
|
||||
#[changeset_options(treat_none_as_null = "true")]
|
||||
pub struct Blog {
|
||||
pub id: i32,
|
||||
@ -87,13 +84,9 @@ impl Blog {
|
||||
|
||||
if inserted.fqn.is_empty() {
|
||||
if instance.local {
|
||||
inserted.fqn = iri_percent_encode_seg(&inserted.actor_id);
|
||||
inserted.fqn = inserted.actor_id.clone();
|
||||
} else {
|
||||
inserted.fqn = format!(
|
||||
"{}@{}",
|
||||
iri_percent_encode_seg(&inserted.actor_id),
|
||||
instance.public_domain
|
||||
);
|
||||
inserted.fqn = format!("{}@{}", inserted.actor_id, instance.public_domain);
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,19 +96,6 @@ impl Blog {
|
||||
find_by!(blogs, find_by_ap_url, ap_url as &str);
|
||||
find_by!(blogs, find_by_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> {
|
||||
Instance::get(conn, self.instance_id)
|
||||
}
|
||||
@ -152,10 +132,10 @@ impl Blog {
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn find_by_fqn(conn: &Connection, fqn: &str) -> Result<Blog> {
|
||||
pub fn find_by_fqn(conn: &DbConn, fqn: &str) -> Result<Blog> {
|
||||
let from_db = blogs::table
|
||||
.filter(blogs::fqn.eq(fqn))
|
||||
.first(conn)
|
||||
.first(&**conn)
|
||||
.optional()?;
|
||||
if let Some(from_db) = from_db {
|
||||
Ok(from_db)
|
||||
@ -164,7 +144,7 @@ impl Blog {
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_from_webfinger(conn: &Connection, acct: &str) -> Result<Blog> {
|
||||
fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result<Blog> {
|
||||
resolve_with_prefix(Prefix::Group, acct.to_owned(), true)?
|
||||
.links
|
||||
.into_iter()
|
||||
@ -173,7 +153,8 @@ impl Blog {
|
||||
.and_then(|l| {
|
||||
Blog::from_id(
|
||||
conn,
|
||||
&l.href.ok_or(Error::MissingApProperty)?,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&l.href?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
@ -182,120 +163,104 @@ impl Blog {
|
||||
}
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> {
|
||||
let mut blog = ApActor::new(self.inbox_url.parse()?, Group::new());
|
||||
blog.set_preferred_username(iri_percent_encode_seg(&self.actor_id));
|
||||
blog.set_name(self.title.clone());
|
||||
blog.set_outbox(self.outbox_url.parse()?);
|
||||
blog.set_summary(self.summary_html.to_string());
|
||||
let source = SourceProperty {
|
||||
source: Source {
|
||||
content: self.summary.clone(),
|
||||
media_type: String::from("text/markdown"),
|
||||
},
|
||||
};
|
||||
let mut blog = Group::default();
|
||||
blog.ap_actor_props
|
||||
.set_preferred_username_string(self.actor_id.clone())?;
|
||||
blog.object_props.set_name_string(self.title.clone())?;
|
||||
blog.ap_actor_props
|
||||
.set_outbox_string(self.outbox_url.clone())?;
|
||||
blog.ap_actor_props
|
||||
.set_inbox_string(self.inbox_url.clone())?;
|
||||
blog.object_props
|
||||
.set_summary_string(self.summary_html.to_string())?;
|
||||
blog.ap_object_props.set_source_object(Source {
|
||||
content: self.summary.clone(),
|
||||
media_type: String::from("text/markdown"),
|
||||
})?;
|
||||
|
||||
let mut icon = Image::new();
|
||||
let _ = self.icon_id.map(|id| {
|
||||
Media::get(conn, id).and_then(|m| {
|
||||
let _ = m
|
||||
.url()
|
||||
.and_then(|url| url.parse::<IriString>().map_err(|_| Error::Url))
|
||||
.map(|url| icon.set_url(url));
|
||||
icon.set_attributed_to(
|
||||
User::get(conn, m.owner_id)?
|
||||
.into_id()
|
||||
.parse::<IriString>()?,
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
blog.set_icon(icon.into_any_base()?);
|
||||
let mut icon = Image::default();
|
||||
icon.object_props.set_url_string(
|
||||
self.icon_id
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
|
||||
.unwrap_or_default(),
|
||||
)?;
|
||||
icon.object_props.set_attributed_to_link(
|
||||
self.icon_id
|
||||
.and_then(|id| {
|
||||
Media::get(conn, id)
|
||||
.and_then(|m| Ok(User::get(conn, m.owner_id)?.into_id()))
|
||||
.ok()
|
||||
})
|
||||
.unwrap_or_else(|| Id::new(String::new())),
|
||||
)?;
|
||||
blog.object_props.set_icon_object(icon)?;
|
||||
|
||||
let mut banner = Image::new();
|
||||
let _ = self.banner_id.map(|id| {
|
||||
Media::get(conn, id).and_then(|m| {
|
||||
let _ = m
|
||||
.url()
|
||||
.and_then(|url| url.parse::<IriString>().map_err(|_| Error::Url))
|
||||
.map(|url| banner.set_url(url));
|
||||
banner.set_attributed_to(
|
||||
User::get(conn, m.owner_id)?
|
||||
.into_id()
|
||||
.parse::<IriString>()?,
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
blog.set_image(banner.into_any_base()?);
|
||||
let mut banner = Image::default();
|
||||
banner.object_props.set_url_string(
|
||||
self.banner_id
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
|
||||
.unwrap_or_default(),
|
||||
)?;
|
||||
banner.object_props.set_attributed_to_link(
|
||||
self.banner_id
|
||||
.and_then(|id| {
|
||||
Media::get(conn, id)
|
||||
.and_then(|m| Ok(User::get(conn, m.owner_id)?.into_id()))
|
||||
.ok()
|
||||
})
|
||||
.unwrap_or_else(|| Id::new(String::new())),
|
||||
)?;
|
||||
blog.object_props.set_image_object(banner)?;
|
||||
|
||||
blog.set_id(self.ap_url.parse()?);
|
||||
blog.object_props.set_id_string(self.ap_url.clone())?;
|
||||
|
||||
let pub_key = PublicKey {
|
||||
id: format!("{}#main-key", self.ap_url).parse()?,
|
||||
owner: self.ap_url.parse()?,
|
||||
public_key_pem: self.public_key.clone(),
|
||||
};
|
||||
let ap_signature = ApSignature {
|
||||
public_key: pub_key,
|
||||
};
|
||||
let mut public_key = PublicKey::default();
|
||||
public_key.set_id_string(format!("{}#main-key", self.ap_url))?;
|
||||
public_key.set_owner_string(self.ap_url.clone())?;
|
||||
public_key.set_public_key_pem_string(self.public_key.clone())?;
|
||||
let mut ap_signature = ApSignature::default();
|
||||
ap_signature.set_public_key_publickey(public_key)?;
|
||||
|
||||
Ok(CustomGroup::new(blog, ap_signature, source))
|
||||
Ok(CustomGroup::new(blog, ap_signature))
|
||||
}
|
||||
|
||||
pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> {
|
||||
self.outbox_collection(conn).map(ActivityStream::new)
|
||||
}
|
||||
pub fn outbox_collection(&self, conn: &Connection) -> Result<OrderedCollection> {
|
||||
let acts = self.get_activities(conn);
|
||||
let acts = acts
|
||||
.iter()
|
||||
.filter_map(|value| AnyBase::from_arbitrary_json(value).ok())
|
||||
.collect::<Vec<AnyBase>>();
|
||||
let n_acts = acts.len();
|
||||
let mut coll = OrderedCollection::new();
|
||||
coll.set_many_items(acts);
|
||||
coll.set_total_items(n_acts as u64);
|
||||
coll.set_first(format!("{}?page=1", &self.outbox_url).parse::<IriString>()?);
|
||||
coll.set_last(
|
||||
format!(
|
||||
let mut coll = OrderedCollection::default();
|
||||
coll.collection_props.items = serde_json::to_value(self.get_activities(conn))?;
|
||||
coll.collection_props
|
||||
.set_total_items_u64(self.get_activities(conn).len() as u64)?;
|
||||
coll.collection_props
|
||||
.set_first_link(Id::new(ap_url(&format!("{}?page=1", &self.outbox_url))))?;
|
||||
coll.collection_props
|
||||
.set_last_link(Id::new(ap_url(&format!(
|
||||
"{}?page={}",
|
||||
&self.outbox_url,
|
||||
(n_acts as u64 + ITEMS_PER_PAGE as u64 - 1) as u64 / ITEMS_PER_PAGE as u64
|
||||
)
|
||||
.parse::<IriString>()?,
|
||||
);
|
||||
Ok(coll)
|
||||
(self.get_activities(conn).len() as u64 + ITEMS_PER_PAGE as u64 - 1) as u64
|
||||
/ ITEMS_PER_PAGE as u64
|
||||
))))?;
|
||||
Ok(ActivityStream::new(coll))
|
||||
}
|
||||
pub fn outbox_page(
|
||||
&self,
|
||||
conn: &Connection,
|
||||
(min, max): (i32, i32),
|
||||
) -> Result<ActivityStream<OrderedCollectionPage>> {
|
||||
self.outbox_collection_page(conn, (min, max))
|
||||
.map(ActivityStream::new)
|
||||
}
|
||||
pub fn outbox_collection_page(
|
||||
&self,
|
||||
conn: &Connection,
|
||||
(min, max): (i32, i32),
|
||||
) -> Result<OrderedCollectionPage> {
|
||||
let mut coll = OrderedCollectionPage::new();
|
||||
let acts = self.get_activity_page(conn, (min, max));
|
||||
let mut coll = OrderedCollectionPage::default();
|
||||
let acts = self.get_activity_page(&conn, (min, max));
|
||||
//This still doesn't do anything because the outbox
|
||||
//doesn't do anything yet
|
||||
coll.set_next(
|
||||
format!("{}?page={}", &self.outbox_url, min / ITEMS_PER_PAGE + 1)
|
||||
.parse::<IriString>()?,
|
||||
);
|
||||
coll.set_prev(
|
||||
format!("{}?page={}", &self.outbox_url, min / ITEMS_PER_PAGE - 1)
|
||||
.parse::<IriString>()?,
|
||||
);
|
||||
coll.set_many_items(
|
||||
acts.iter()
|
||||
.filter_map(|value| AnyBase::from_arbitrary_json(value).ok()),
|
||||
);
|
||||
Ok(coll)
|
||||
coll.collection_page_props.set_next_link(Id::new(&format!(
|
||||
"{}?page={}",
|
||||
&self.outbox_url,
|
||||
min / ITEMS_PER_PAGE + 1
|
||||
)))?;
|
||||
coll.collection_page_props.set_prev_link(Id::new(&format!(
|
||||
"{}?page={}",
|
||||
&self.outbox_url,
|
||||
min / ITEMS_PER_PAGE - 1
|
||||
)))?;
|
||||
coll.collection_props.items = serde_json::to_value(acts)?;
|
||||
Ok(ActivityStream::new(coll))
|
||||
}
|
||||
fn get_activities(&self, _conn: &Connection) -> Vec<serde_json::Value> {
|
||||
vec![]
|
||||
@ -310,10 +275,7 @@ impl Blog {
|
||||
|
||||
pub fn get_keypair(&self) -> Result<PKey<Private>> {
|
||||
PKey::from_rsa(Rsa::private_key_from_pem(
|
||||
self.private_key
|
||||
.clone()
|
||||
.ok_or(Error::MissingApProperty)?
|
||||
.as_ref(),
|
||||
self.private_key.clone()?.as_ref(),
|
||||
)?)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
@ -366,7 +328,7 @@ impl Blog {
|
||||
}
|
||||
|
||||
pub fn delete(&self, conn: &Connection) -> Result<()> {
|
||||
for post in Post::get_for_blog(conn, self)? {
|
||||
for post in Post::get_for_blog(conn, &self)? {
|
||||
post.delete(conn)?;
|
||||
}
|
||||
diesel::delete(self)
|
||||
@ -382,100 +344,18 @@ impl IntoId for Blog {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromId<Connection> for Blog {
|
||||
impl FromId<DbConn> for Blog {
|
||||
type Error = Error;
|
||||
type Object = CustomGroup;
|
||||
|
||||
fn from_db(conn: &Connection, id: &str) -> Result<Self> {
|
||||
Self::find_by_ap_url(conn, id)
|
||||
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
||||
Self::find_by_ap_url(&conn, id)
|
||||
}
|
||||
|
||||
fn from_activity(conn: &Connection, acct: CustomGroup) -> Result<Self> {
|
||||
let (name, outbox_url, inbox_url) = {
|
||||
let actor = acct.ap_actor_ref();
|
||||
let name = actor
|
||||
.preferred_username()
|
||||
.ok_or(Error::MissingApProperty)?
|
||||
.to_string();
|
||||
if name.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
|
||||
tracing::error!("preferredUsername includes invalid character(s): {}", &name);
|
||||
return Err(Error::InvalidValue);
|
||||
}
|
||||
(
|
||||
name,
|
||||
actor.outbox()?.ok_or(Error::MissingApProperty)?.to_string(),
|
||||
actor.inbox()?.to_string(),
|
||||
)
|
||||
};
|
||||
|
||||
let mut new_blog = NewBlog {
|
||||
actor_id: name.to_string(),
|
||||
outbox_url,
|
||||
inbox_url,
|
||||
public_key: acct.ext_one.public_key.public_key_pem.to_string(),
|
||||
private_key: None,
|
||||
theme: None,
|
||||
..NewBlog::default()
|
||||
};
|
||||
|
||||
let object = ApObject::new(acct.inner);
|
||||
new_blog.title = object
|
||||
.name()
|
||||
.and_then(|name| name.to_as_string())
|
||||
.unwrap_or(name);
|
||||
new_blog.summary_html = SafeString::new(
|
||||
&object
|
||||
.summary()
|
||||
.and_then(|summary| summary.to_as_string())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
let icon_id = object
|
||||
.icon()
|
||||
.and_then(|icons| {
|
||||
icons.iter().next().and_then(|icon| {
|
||||
let icon = icon.to_owned().extend::<Image, ImageType>().ok()??;
|
||||
let owner = icon.attributed_to()?.to_as_uri()?;
|
||||
Media::save_remote(
|
||||
conn,
|
||||
icon.url()?.to_as_uri()?,
|
||||
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
})
|
||||
.map(|m| m.id);
|
||||
new_blog.icon_id = icon_id;
|
||||
|
||||
let banner_id = object
|
||||
.image()
|
||||
.and_then(|banners| {
|
||||
banners.iter().next().and_then(|banner| {
|
||||
let banner = banner.to_owned().extend::<Image, ImageType>().ok()??;
|
||||
let owner = banner.attributed_to()?.to_as_uri()?;
|
||||
Media::save_remote(
|
||||
conn,
|
||||
banner.url()?.to_as_uri()?,
|
||||
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
})
|
||||
.map(|m| m.id);
|
||||
new_blog.banner_id = banner_id;
|
||||
|
||||
new_blog.summary = acct.ext_two.source.content;
|
||||
|
||||
let any_base = AnyBase::from_extended(object)?;
|
||||
let id = any_base.id().ok_or(Error::MissingApProperty)?;
|
||||
new_blog.ap_url = id.to_string();
|
||||
|
||||
let inst = id
|
||||
.authority_components()
|
||||
.ok_or(Error::Url)?
|
||||
.host()
|
||||
.to_string();
|
||||
let instance = Instance::find_by_domain(conn, &inst).or_else(|_| {
|
||||
fn from_activity(conn: &DbConn, acct: CustomGroup) -> Result<Self> {
|
||||
let url = Url::parse(&acct.object.object_props.id_string()?)?;
|
||||
let inst = url.host_str()?;
|
||||
let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
|
||||
Instance::insert(
|
||||
conn,
|
||||
NewInstance {
|
||||
@ -489,16 +369,94 @@ impl FromId<Connection> for Blog {
|
||||
open_registrations: true,
|
||||
short_description_html: String::new(),
|
||||
long_description_html: String::new(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
)
|
||||
})?;
|
||||
new_blog.instance_id = instance.id;
|
||||
let icon_id = acct
|
||||
.object
|
||||
.object_props
|
||||
.icon_image()
|
||||
.ok()
|
||||
.and_then(|icon| {
|
||||
let owner = icon.object_props.attributed_to_link::<Id>().ok()?;
|
||||
Media::save_remote(
|
||||
conn,
|
||||
icon.object_props.url_string().ok()?,
|
||||
&User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&owner,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.ok()?,
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
.map(|m| m.id);
|
||||
|
||||
Blog::insert(conn, new_blog)
|
||||
}
|
||||
let banner_id = acct
|
||||
.object
|
||||
.object_props
|
||||
.image_image()
|
||||
.ok()
|
||||
.and_then(|banner| {
|
||||
let owner = banner.object_props.attributed_to_link::<Id>().ok()?;
|
||||
Media::save_remote(
|
||||
conn,
|
||||
banner.object_props.url_string().ok()?,
|
||||
&User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&owner,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.ok()?,
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
.map(|m| m.id);
|
||||
|
||||
fn get_sender() -> &'static dyn sign::Signer {
|
||||
Instance::get_local_instance_user().expect("Failed to local instance user")
|
||||
let name = acct.object.ap_actor_props.preferred_username_string()?;
|
||||
if name.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
|
||||
return Err(Error::InvalidValue);
|
||||
}
|
||||
|
||||
Blog::insert(
|
||||
conn,
|
||||
NewBlog {
|
||||
actor_id: name.clone(),
|
||||
title: acct.object.object_props.name_string().unwrap_or(name),
|
||||
outbox_url: acct.object.ap_actor_props.outbox_string()?,
|
||||
inbox_url: acct.object.ap_actor_props.inbox_string()?,
|
||||
summary: acct
|
||||
.object
|
||||
.ap_object_props
|
||||
.source_object::<Source>()
|
||||
.map(|s| s.content)
|
||||
.unwrap_or_default(),
|
||||
instance_id: instance.id,
|
||||
ap_url: acct.object.object_props.id_string()?,
|
||||
public_key: acct
|
||||
.custom_props
|
||||
.public_key_publickey()?
|
||||
.public_key_pem_string()?,
|
||||
private_key: None,
|
||||
banner_id,
|
||||
icon_id,
|
||||
summary_html: SafeString::new(
|
||||
&acct
|
||||
.object
|
||||
.object_props
|
||||
.summary_string()
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
theme: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -523,18 +481,18 @@ impl sign::Signer for Blog {
|
||||
format!("{}#main-key", self.ap_url)
|
||||
}
|
||||
|
||||
fn sign(&self, to_sign: &str) -> sign::Result<Vec<u8>> {
|
||||
let key = self.get_keypair().map_err(|_| sign::Error())?;
|
||||
fn sign(&self, to_sign: &str) -> SignatureResult<Vec<u8>> {
|
||||
let key = self.get_keypair()?;
|
||||
let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
|
||||
signer.update(to_sign.as_bytes())?;
|
||||
signer.sign_to_vec().map_err(sign::Error::from)
|
||||
signer.sign_to_vec().map_err(SignatureError::from)
|
||||
}
|
||||
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> sign::Result<bool> {
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<bool> {
|
||||
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
|
||||
let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?;
|
||||
verifier.update(data.as_bytes())?;
|
||||
verifier.verify(signature).map_err(sign::Error::from)
|
||||
verifier.verify(&signature).map_err(SignatureError::from)
|
||||
}
|
||||
}
|
||||
|
||||
@ -565,14 +523,12 @@ pub(crate) mod tests {
|
||||
blog_authors::*, instance::tests as instance_tests, medias::NewMedia, tests::db,
|
||||
users::tests as usersTests, Connection as Conn,
|
||||
};
|
||||
use assert_json_diff::assert_json_eq;
|
||||
use diesel::Connection;
|
||||
use serde_json::to_value;
|
||||
|
||||
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Blog>) {
|
||||
instance_tests::fill_database(conn);
|
||||
let users = usersTests::fill_database(conn);
|
||||
let mut blog1 = Blog::insert(
|
||||
let blog1 = Blog::insert(
|
||||
conn,
|
||||
NewBlog::new_local(
|
||||
"BlogName".to_owned(),
|
||||
@ -645,41 +601,6 @@ pub(crate) mod tests {
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
blog1.icon_id = Some(
|
||||
Media::insert(
|
||||
conn,
|
||||
NewMedia {
|
||||
file_path: "aaa.png".into(),
|
||||
alt_text: String::new(),
|
||||
is_remote: false,
|
||||
remote_url: None,
|
||||
sensitive: false,
|
||||
content_warning: None,
|
||||
owner_id: users[0].id,
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
.id,
|
||||
);
|
||||
blog1.banner_id = Some(
|
||||
Media::insert(
|
||||
conn,
|
||||
NewMedia {
|
||||
file_path: "bbb.png".into(),
|
||||
alt_text: String::new(),
|
||||
is_remote: false,
|
||||
remote_url: None,
|
||||
sensitive: false,
|
||||
content_warning: None,
|
||||
owner_id: users[0].id,
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
.id,
|
||||
);
|
||||
let _: Blog = blog1.save_changes(conn).unwrap();
|
||||
|
||||
(users, vec![blog1, blog2, blog3])
|
||||
}
|
||||
|
||||
@ -687,10 +608,10 @@ pub(crate) mod tests {
|
||||
fn get_instance() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
fill_database(conn);
|
||||
fill_database(&conn);
|
||||
|
||||
let blog = Blog::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlog::new_local(
|
||||
"SomeName".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
@ -702,7 +623,7 @@ pub(crate) mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
blog.get_instance(conn).unwrap().id,
|
||||
blog.get_instance(&conn).unwrap().id,
|
||||
Instance::get_local().unwrap().id
|
||||
);
|
||||
// TODO add tests for remote instance
|
||||
@ -714,10 +635,10 @@ pub(crate) mod tests {
|
||||
fn authors() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (user, _) = fill_database(conn);
|
||||
let (user, _) = fill_database(&conn);
|
||||
|
||||
let b1 = Blog::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlog::new_local(
|
||||
"SomeName".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
@ -728,7 +649,7 @@ pub(crate) mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
let b2 = Blog::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlog::new_local(
|
||||
"Blog".to_owned(),
|
||||
"Blog".to_owned(),
|
||||
@ -741,7 +662,7 @@ pub(crate) mod tests {
|
||||
let blog = vec![b1, b2];
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlogAuthor {
|
||||
blog_id: blog[0].id,
|
||||
author_id: user[0].id,
|
||||
@ -751,7 +672,7 @@ pub(crate) mod tests {
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlogAuthor {
|
||||
blog_id: blog[0].id,
|
||||
author_id: user[1].id,
|
||||
@ -761,7 +682,7 @@ pub(crate) mod tests {
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlogAuthor {
|
||||
blog_id: blog[1].id,
|
||||
author_id: user[0].id,
|
||||
@ -771,39 +692,39 @@ pub(crate) mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert!(blog[0]
|
||||
.list_authors(conn)
|
||||
.list_authors(&conn)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[0].id));
|
||||
assert!(blog[0]
|
||||
.list_authors(conn)
|
||||
.list_authors(&conn)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[1].id));
|
||||
assert!(blog[1]
|
||||
.list_authors(conn)
|
||||
.list_authors(&conn)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[0].id));
|
||||
assert!(!blog[1]
|
||||
.list_authors(conn)
|
||||
.list_authors(&conn)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[1].id));
|
||||
|
||||
assert!(Blog::find_for_author(conn, &user[0])
|
||||
assert!(Blog::find_for_author(&conn, &user[0])
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[0].id));
|
||||
assert!(Blog::find_for_author(conn, &user[1])
|
||||
assert!(Blog::find_for_author(&conn, &user[1])
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[0].id));
|
||||
assert!(Blog::find_for_author(conn, &user[0])
|
||||
assert!(Blog::find_for_author(&conn, &user[0])
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[1].id));
|
||||
assert!(!Blog::find_for_author(conn, &user[1])
|
||||
assert!(!Blog::find_for_author(&conn, &user[1])
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[1].id));
|
||||
@ -815,10 +736,10 @@ pub(crate) mod tests {
|
||||
fn find_local() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
fill_database(conn);
|
||||
fill_database(&conn);
|
||||
|
||||
let blog = Blog::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlog::new_local(
|
||||
"SomeName".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
@ -829,7 +750,7 @@ pub(crate) mod tests {
|
||||
)
|
||||
.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(())
|
||||
})
|
||||
}
|
||||
@ -838,10 +759,10 @@ pub(crate) mod tests {
|
||||
fn get_fqn() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
fill_database(conn);
|
||||
fill_database(&conn);
|
||||
|
||||
let blog = Blog::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlog::new_local(
|
||||
"SomeName".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
@ -861,10 +782,10 @@ pub(crate) mod tests {
|
||||
fn delete() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (_, blogs) = fill_database(conn);
|
||||
let (_, blogs) = fill_database(&conn);
|
||||
|
||||
blogs[0].delete(conn).unwrap();
|
||||
assert!(Blog::get(conn, blogs[0].id).is_err());
|
||||
blogs[0].delete(&conn).unwrap();
|
||||
assert!(Blog::get(&conn, blogs[0].id).is_err());
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@ -873,10 +794,10 @@ pub(crate) mod tests {
|
||||
fn delete_via_user() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (user, _) = fill_database(conn);
|
||||
let (user, _) = fill_database(&conn);
|
||||
|
||||
let b1 = Blog::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlog::new_local(
|
||||
"SomeName".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
@ -887,7 +808,7 @@ pub(crate) mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
let b2 = Blog::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlog::new_local(
|
||||
"Blog".to_owned(),
|
||||
"Blog".to_owned(),
|
||||
@ -900,7 +821,7 @@ pub(crate) mod tests {
|
||||
let blog = vec![b1, b2];
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlogAuthor {
|
||||
blog_id: blog[0].id,
|
||||
author_id: user[0].id,
|
||||
@ -910,7 +831,7 @@ pub(crate) mod tests {
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlogAuthor {
|
||||
blog_id: blog[0].id,
|
||||
author_id: user[1].id,
|
||||
@ -920,7 +841,7 @@ pub(crate) mod tests {
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewBlogAuthor {
|
||||
blog_id: blog[1].id,
|
||||
author_id: user[0].id,
|
||||
@ -929,11 +850,11 @@ pub(crate) mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
user[0].delete(conn).unwrap();
|
||||
assert!(Blog::get(conn, blog[0].id).is_ok());
|
||||
assert!(Blog::get(conn, blog[1].id).is_err());
|
||||
user[1].delete(conn).unwrap();
|
||||
assert!(Blog::get(conn, blog[0].id).is_err());
|
||||
user[0].delete(&conn).unwrap();
|
||||
assert!(Blog::get(&conn, blog[0].id).is_ok());
|
||||
assert!(Blog::get(&conn, blog[1].id).is_err());
|
||||
user[1].delete(&conn).unwrap();
|
||||
assert!(Blog::get(&conn, blog[0].id).is_err());
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@ -942,10 +863,10 @@ pub(crate) mod tests {
|
||||
fn self_federation() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (users, mut blogs) = fill_database(conn);
|
||||
let (users, mut blogs) = fill_database(&conn);
|
||||
blogs[0].icon_id = Some(
|
||||
Media::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewMedia {
|
||||
file_path: "aaa.png".into(),
|
||||
alt_text: String::new(),
|
||||
@ -961,7 +882,7 @@ pub(crate) mod tests {
|
||||
);
|
||||
blogs[0].banner_id = Some(
|
||||
Media::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewMedia {
|
||||
file_path: "bbb.png".into(),
|
||||
alt_text: String::new(),
|
||||
@ -976,9 +897,10 @@ pub(crate) mod tests {
|
||||
.id,
|
||||
);
|
||||
let _: Blog = blogs[0].save_changes(&**conn).unwrap();
|
||||
let ap_repr = blogs[0].to_activity(conn).unwrap();
|
||||
blogs[0].delete(conn).unwrap();
|
||||
let blog = Blog::from_activity(conn, ap_repr).unwrap();
|
||||
|
||||
let ap_repr = blogs[0].to_activity(&conn).unwrap();
|
||||
blogs[0].delete(&conn).unwrap();
|
||||
let blog = Blog::from_activity(&conn, ap_repr).unwrap();
|
||||
|
||||
assert_eq!(blog.actor_id, blogs[0].actor_id);
|
||||
assert_eq!(blog.title, blogs[0].title);
|
||||
@ -990,96 +912,10 @@ pub(crate) mod tests {
|
||||
assert_eq!(blog.public_key, blogs[0].public_key);
|
||||
assert_eq!(blog.fqn, blogs[0].fqn);
|
||||
assert_eq!(blog.summary_html, blogs[0].summary_html);
|
||||
assert_eq!(blog.icon_url(conn), blogs[0].icon_url(conn));
|
||||
assert_eq!(blog.banner_url(conn), blogs[0].banner_url(conn));
|
||||
assert_eq!(blog.icon_url(&conn), blogs[0].icon_url(&conn));
|
||||
assert_eq!(blog.banner_url(&conn), blogs[0].banner_url(&conn));
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_activity() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, Error, _>(|| {
|
||||
let (_users, blogs) = fill_database(conn);
|
||||
let blog = &blogs[0];
|
||||
let act = blog.to_activity(conn)?;
|
||||
|
||||
let expected = json!({
|
||||
"icon": {
|
||||
"attributedTo": "https://plu.me/@/admin/",
|
||||
"type": "Image",
|
||||
"url": "https://plu.me/aaa.png"
|
||||
},
|
||||
"id": "https://plu.me/~/BlogName/",
|
||||
"image": {
|
||||
"attributedTo": "https://plu.me/@/admin/",
|
||||
"type": "Image",
|
||||
"url": "https://plu.me/bbb.png"
|
||||
},
|
||||
"inbox": "https://plu.me/~/BlogName/inbox",
|
||||
"name": "Blog name",
|
||||
"outbox": "https://plu.me/~/BlogName/outbox",
|
||||
"preferredUsername": "BlogName",
|
||||
"publicKey": {
|
||||
"id": "https://plu.me/~/BlogName/#main-key",
|
||||
"owner": "https://plu.me/~/BlogName/",
|
||||
"publicKeyPem": blog.public_key
|
||||
},
|
||||
"source": {
|
||||
"content": "This is a small blog",
|
||||
"mediaType": "text/markdown"
|
||||
},
|
||||
"summary": "",
|
||||
"type": "Group"
|
||||
});
|
||||
|
||||
assert_json_eq!(to_value(act)?, expected);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outbox_collection() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, Error, _>(|| {
|
||||
let (_users, blogs) = fill_database(conn);
|
||||
let blog = &blogs[0];
|
||||
let act = blog.outbox_collection(conn)?;
|
||||
|
||||
let expected = json!({
|
||||
"items": [],
|
||||
"totalItems": 0,
|
||||
"first": "https://plu.me/~/BlogName/outbox?page=1",
|
||||
"last": "https://plu.me/~/BlogName/outbox?page=0",
|
||||
"type": "OrderedCollection"
|
||||
});
|
||||
|
||||
assert_json_eq!(to_value(act)?, expected);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outbox_collection_page() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, Error, _>(|| {
|
||||
let (_users, blogs) = fill_database(conn);
|
||||
let blog = &blogs[0];
|
||||
let act = blog.outbox_collection_page(conn, (33, 36))?;
|
||||
|
||||
let expected = json!({
|
||||
"next": "https://plu.me/~/BlogName/outbox?page=3",
|
||||
"prev": "https://plu.me/~/BlogName/outbox?page=1",
|
||||
"items": [],
|
||||
"type": "OrderedCollectionPage"
|
||||
});
|
||||
|
||||
assert_json_eq!(to_value(act)?, expected);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
comment_seers::{CommentSeers, NewCommentSeers},
|
||||
db_conn::DbConn,
|
||||
instance::Instance,
|
||||
medias::Media,
|
||||
mentions::Mention,
|
||||
@ -10,23 +11,17 @@ use crate::{
|
||||
users::User,
|
||||
Connection, Error, Result, CONFIG,
|
||||
};
|
||||
use activitystreams::{
|
||||
use activitypub::{
|
||||
activity::{Create, Delete},
|
||||
base::{AnyBase, Base},
|
||||
iri_string::types::IriString,
|
||||
link::{self, kind::MentionType},
|
||||
link,
|
||||
object::{Note, Tombstone},
|
||||
prelude::*,
|
||||
primitives::OneOrMany,
|
||||
time::OffsetDateTime,
|
||||
};
|
||||
use chrono::{self, NaiveDateTime};
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||
use plume_common::{
|
||||
activity_pub::{
|
||||
inbox::{AsActor, AsObject, FromId},
|
||||
sign::Signer,
|
||||
IntoId, ToAsString, ToAsUri, PUBLIC_VISIBILITY,
|
||||
Id, IntoId, PUBLIC_VISIBILITY,
|
||||
},
|
||||
utils,
|
||||
};
|
||||
@ -63,7 +58,7 @@ impl Comment {
|
||||
insert!(comments, NewComment, |inserted, conn| {
|
||||
if inserted.ap_url.is_none() {
|
||||
inserted.ap_url = Some(format!(
|
||||
"{}/comment/{}",
|
||||
"{}comment/{}",
|
||||
inserted.get_post(conn)?.ap_url,
|
||||
inserted.id
|
||||
));
|
||||
@ -73,7 +68,6 @@ impl Comment {
|
||||
});
|
||||
get!(comments);
|
||||
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);
|
||||
|
||||
pub fn get_author(&self, conn: &Connection) -> Result<User> {
|
||||
@ -111,7 +105,7 @@ impl Comment {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<Note> {
|
||||
pub fn to_activity(&self, conn: &DbConn) -> Result<Note> {
|
||||
let author = User::get(conn, self.author_id)?;
|
||||
let (html, mentions, _hashtags) = utils::md_to_html(
|
||||
self.content.get().as_ref(),
|
||||
@ -120,59 +114,45 @@ impl Comment {
|
||||
Some(Media::get_media_processor(conn, vec![&author])),
|
||||
);
|
||||
|
||||
let mut note = Note::new();
|
||||
let to = vec![PUBLIC_VISIBILITY.parse::<IriString>()?];
|
||||
let mut note = Note::default();
|
||||
let to = vec![Id::new(PUBLIC_VISIBILITY.to_string())];
|
||||
|
||||
note.set_id(
|
||||
self.ap_url
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.parse::<IriString>()?,
|
||||
);
|
||||
note.set_summary(self.spoiler_text.clone());
|
||||
note.set_content(html);
|
||||
note.set_in_reply_to(self.in_response_to_id.map_or_else(
|
||||
|| Post::get(conn, self.post_id).map(|post| post.ap_url),
|
||||
|id| Comment::get(conn, id).map(|comment| comment.ap_url.unwrap_or_default()),
|
||||
)?);
|
||||
note.set_published(
|
||||
OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos_opt().unwrap().into())
|
||||
.expect("OffsetDateTime"),
|
||||
);
|
||||
note.set_attributed_to(author.into_id().parse::<IriString>()?);
|
||||
note.set_many_tos(to);
|
||||
note.set_many_tags(mentions.into_iter().filter_map(|m| {
|
||||
Mention::build_activity(conn, &m)
|
||||
.map(|mention| mention.into_any_base().expect("Can convert"))
|
||||
.ok()
|
||||
}));
|
||||
note.object_props
|
||||
.set_id_string(self.ap_url.clone().unwrap_or_default())?;
|
||||
note.object_props
|
||||
.set_summary_string(self.spoiler_text.clone())?;
|
||||
note.object_props.set_content_string(html)?;
|
||||
note.object_props
|
||||
.set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else(
|
||||
|| Ok(Post::get(conn, self.post_id)?.ap_url),
|
||||
|id| Ok(Comment::get(conn, id)?.ap_url.unwrap_or_default()) as Result<String>,
|
||||
)?))?;
|
||||
note.object_props
|
||||
.set_published_string(chrono::Utc::now().to_rfc3339())?;
|
||||
note.object_props.set_attributed_to_link(author.into_id())?;
|
||||
note.object_props.set_to_link_vec(to)?;
|
||||
note.object_props.set_tag_link_vec(
|
||||
mentions
|
||||
.into_iter()
|
||||
.filter_map(|m| Mention::build_activity(conn, &m).ok())
|
||||
.collect::<Vec<link::Mention>>(),
|
||||
)?;
|
||||
Ok(note)
|
||||
}
|
||||
|
||||
pub fn create_activity(&self, conn: &Connection) -> Result<Create> {
|
||||
let author = User::get(conn, self.author_id)?;
|
||||
pub fn create_activity(&self, conn: &DbConn) -> Result<Create> {
|
||||
let author = User::get(&conn, self.author_id)?;
|
||||
|
||||
let note = self.to_activity(conn)?;
|
||||
let note_clone = note.clone();
|
||||
|
||||
let mut act = Create::new(
|
||||
author.into_id().parse::<IriString>()?,
|
||||
Base::retract(note)?.into_generic()?,
|
||||
);
|
||||
act.set_id(
|
||||
format!(
|
||||
"{}/activity",
|
||||
self.ap_url.clone().ok_or(Error::MissingApProperty)?,
|
||||
)
|
||||
.parse::<IriString>()?,
|
||||
);
|
||||
act.set_many_tos(
|
||||
note_clone
|
||||
.to()
|
||||
.iter()
|
||||
.flat_map(|tos| tos.iter().map(|to| to.to_owned())),
|
||||
);
|
||||
act.set_many_ccs(vec![self.get_author(conn)?.followers_endpoint]);
|
||||
let mut act = Create::default();
|
||||
act.create_props.set_actor_link(author.into_id())?;
|
||||
act.create_props.set_object_object(note.clone())?;
|
||||
act.object_props
|
||||
.set_id_string(format!("{}/activity", self.ap_url.clone()?,))?;
|
||||
act.object_props
|
||||
.set_to_link_vec(note.object_props.to_link_vec::<Id>()?)?;
|
||||
act.object_props
|
||||
.set_cc_link_vec(vec![Id::new(self.get_author(&conn)?.followers_endpoint)])?;
|
||||
Ok(act)
|
||||
}
|
||||
|
||||
@ -197,140 +177,137 @@ impl Comment {
|
||||
}
|
||||
|
||||
pub fn build_delete(&self, conn: &Connection) -> Result<Delete> {
|
||||
let mut tombstone = Tombstone::new();
|
||||
tombstone.set_id(
|
||||
self.ap_url
|
||||
.as_ref()
|
||||
.ok_or(Error::MissingApProperty)?
|
||||
.parse::<IriString>()?,
|
||||
);
|
||||
let mut act = Delete::default();
|
||||
act.delete_props
|
||||
.set_actor_link(self.get_author(conn)?.into_id())?;
|
||||
|
||||
let mut act = Delete::new(
|
||||
self.get_author(conn)?.into_id().parse::<IriString>()?,
|
||||
Base::retract(tombstone)?.into_generic()?,
|
||||
);
|
||||
let mut tombstone = Tombstone::default();
|
||||
tombstone.object_props.set_id_string(self.ap_url.clone()?)?;
|
||||
act.delete_props.set_object_object(tombstone)?;
|
||||
|
||||
act.set_id(format!("{}#delete", self.ap_url.clone().unwrap()).parse::<IriString>()?);
|
||||
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
|
||||
act.object_props
|
||||
.set_id_string(format!("{}#delete", self.ap_url.clone().unwrap()))?;
|
||||
act.object_props
|
||||
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?;
|
||||
|
||||
Ok(act)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromId<Connection> for Comment {
|
||||
impl FromId<DbConn> for Comment {
|
||||
type Error = Error;
|
||||
type Object = Note;
|
||||
|
||||
fn from_db(conn: &Connection, id: &str) -> Result<Self> {
|
||||
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
||||
Self::find_by_ap_url(conn, id)
|
||||
}
|
||||
|
||||
fn from_activity(conn: &Connection, note: Note) -> Result<Self> {
|
||||
fn from_activity(conn: &DbConn, note: Note) -> Result<Self> {
|
||||
let comm = {
|
||||
let previous_url = note
|
||||
.in_reply_to()
|
||||
.ok_or(Error::MissingApProperty)?
|
||||
.iter()
|
||||
.next()
|
||||
.ok_or(Error::MissingApProperty)?
|
||||
.id()
|
||||
.ok_or(Error::MissingApProperty)?;
|
||||
let previous_comment = Comment::find_by_ap_url(conn, previous_url.as_str());
|
||||
let previous_url = note.object_props.in_reply_to.as_ref()?.as_str()?;
|
||||
let previous_comment = Comment::find_by_ap_url(conn, previous_url);
|
||||
|
||||
let is_public = |v: &Option<&OneOrMany<AnyBase>>| match v {
|
||||
Some(one_or_many) => one_or_many.iter().any(|any_base| {
|
||||
let id = any_base.id();
|
||||
id.is_some() && id.unwrap() == PUBLIC_VISIBILITY
|
||||
}),
|
||||
None => false,
|
||||
let is_public = |v: &Option<serde_json::Value>| match v
|
||||
.as_ref()
|
||||
.unwrap_or(&serde_json::Value::Null)
|
||||
{
|
||||
serde_json::Value::Array(v) => v
|
||||
.iter()
|
||||
.filter_map(serde_json::Value::as_str)
|
||||
.any(|s| s == PUBLIC_VISIBILITY),
|
||||
serde_json::Value::String(s) => s == PUBLIC_VISIBILITY,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let public_visibility = is_public(¬e.to())
|
||||
|| is_public(¬e.bto())
|
||||
|| is_public(¬e.cc())
|
||||
|| is_public(¬e.bcc());
|
||||
let public_visibility = is_public(¬e.object_props.to)
|
||||
|| is_public(¬e.object_props.bto)
|
||||
|| is_public(¬e.object_props.cc)
|
||||
|| is_public(¬e.object_props.bcc);
|
||||
|
||||
let summary = note.summary().and_then(|summary| summary.to_as_string());
|
||||
let sensitive = summary.is_some();
|
||||
let comm = Comment::insert(
|
||||
conn,
|
||||
NewComment {
|
||||
content: SafeString::new(
|
||||
¬e
|
||||
.content()
|
||||
.ok_or(Error::MissingApProperty)?
|
||||
.to_as_string()
|
||||
.ok_or(Error::InvalidValue)?,
|
||||
),
|
||||
spoiler_text: summary.unwrap_or_default(),
|
||||
ap_url: Some(
|
||||
note.id_unchecked()
|
||||
.ok_or(Error::MissingApProperty)?
|
||||
.to_string(),
|
||||
),
|
||||
content: SafeString::new(¬e.object_props.content_string()?),
|
||||
spoiler_text: note.object_props.summary_string().unwrap_or_default(),
|
||||
ap_url: note.object_props.id_string().ok(),
|
||||
in_response_to_id: previous_comment.iter().map(|c| c.id).next(),
|
||||
post_id: previous_comment.map(|c| c.post_id).or_else(|_| {
|
||||
Ok(Post::find_by_ap_url(conn, previous_url.as_str())?.id) as Result<i32>
|
||||
Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result<i32>
|
||||
})?,
|
||||
author_id: User::from_id(
|
||||
conn,
|
||||
¬e
|
||||
.attributed_to()
|
||||
.ok_or(Error::MissingApProperty)?
|
||||
.to_as_uri()
|
||||
.ok_or(Error::MissingApProperty)?,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
¬e.object_props.attributed_to_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
sensitive,
|
||||
sensitive: note.object_props.summary_string().is_ok(),
|
||||
public_visibility,
|
||||
},
|
||||
)?;
|
||||
|
||||
// save mentions
|
||||
if let Some(tags) = note.tag() {
|
||||
let author_url = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0].ap_url;
|
||||
for tag in tags.iter() {
|
||||
let m = tag.clone().extend::<link::Mention, MentionType>()?; // FIXME: Don't clone
|
||||
if m.is_none() {
|
||||
continue;
|
||||
}
|
||||
let m = m.unwrap();
|
||||
let not_author = m.href().ok_or(Error::MissingApProperty)? != author_url;
|
||||
let _ = Mention::from_activity(conn, &m, comm.id, false, not_author);
|
||||
if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() {
|
||||
for tag in tags {
|
||||
serde_json::from_value::<link::Mention>(tag)
|
||||
.map_err(Error::from)
|
||||
.and_then(|m| {
|
||||
let author = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0];
|
||||
let not_author = m.link_props.href_string()? != author.ap_url.clone();
|
||||
Mention::from_activity(conn, &m, comm.id, false, not_author)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
comm
|
||||
};
|
||||
|
||||
if !comm.public_visibility {
|
||||
let mut receiver_ids = HashSet::new();
|
||||
let mut receivers_id = |v: Option<&'_ OneOrMany<AnyBase>>| {
|
||||
if let Some(one_or_many) = v {
|
||||
for any_base in one_or_many.iter() {
|
||||
if let Some(id) = any_base.id() {
|
||||
receiver_ids.insert(id.to_string());
|
||||
}
|
||||
let receivers_ap_url = |v: Option<serde_json::Value>| {
|
||||
let filter = |e: serde_json::Value| {
|
||||
if let serde_json::Value::String(s) = e {
|
||||
Some(s)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
match v.unwrap_or(serde_json::Value::Null) {
|
||||
serde_json::Value::Array(v) => v,
|
||||
v => vec![v],
|
||||
}
|
||||
.into_iter()
|
||||
.filter_map(filter)
|
||||
};
|
||||
|
||||
receivers_id(note.to());
|
||||
receivers_id(note.cc());
|
||||
receivers_id(note.bto());
|
||||
receivers_id(note.bcc());
|
||||
let mut note = note;
|
||||
|
||||
let receivers_ap_url = receiver_ids
|
||||
let to = receivers_ap_url(note.object_props.to.take());
|
||||
let cc = receivers_ap_url(note.object_props.cc.take());
|
||||
let bto = receivers_ap_url(note.object_props.bto.take());
|
||||
let bcc = receivers_ap_url(note.object_props.bcc.take());
|
||||
|
||||
let receivers_ap_url = to
|
||||
.chain(cc)
|
||||
.chain(bto)
|
||||
.chain(bcc)
|
||||
.collect::<HashSet<_>>() // remove duplicates (don't do a query more than once)
|
||||
.into_iter()
|
||||
.flat_map(|v| {
|
||||
if let Ok(user) = User::from_id(conn, v.as_ref(), None, CONFIG.proxy()) {
|
||||
.map(|v| {
|
||||
if let Ok(user) = User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&v,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
) {
|
||||
vec![user]
|
||||
} else {
|
||||
vec![] // TODO try to fetch collection
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
.filter(|u| u.get_instance(conn).map(|i| i.local).unwrap_or(false))
|
||||
.collect::<HashSet<User>>(); //remove duplicates (prevent db error)
|
||||
|
||||
@ -348,27 +325,23 @@ impl FromId<Connection> for Comment {
|
||||
comm.notify(conn)?;
|
||||
Ok(comm)
|
||||
}
|
||||
|
||||
fn get_sender() -> &'static dyn Signer {
|
||||
Instance::get_local_instance_user().expect("Failed to local instance user")
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, Create, &Connection> for Comment {
|
||||
impl AsObject<User, Create, &DbConn> for Comment {
|
||||
type Error = Error;
|
||||
type Output = Self;
|
||||
|
||||
fn activity(self, _conn: &Connection, _actor: User, _id: &str) -> Result<Self> {
|
||||
fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result<Self> {
|
||||
// The actual creation takes place in the FromId impl
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, Delete, &Connection> for Comment {
|
||||
impl AsObject<User, Delete, &DbConn> for Comment {
|
||||
type Error = Error;
|
||||
type Output = ();
|
||||
|
||||
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> {
|
||||
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
||||
if self.author_id != actor.id {
|
||||
return Err(Error::Unauthorized);
|
||||
}
|
||||
@ -380,15 +353,15 @@ impl AsObject<User, Delete, &Connection> for Comment {
|
||||
m.delete(conn)?;
|
||||
}
|
||||
|
||||
for n in Notification::find_for_comment(conn, &self)? {
|
||||
n.delete(conn)?;
|
||||
for n in Notification::find_for_comment(&conn, &self)? {
|
||||
n.delete(&**conn)?;
|
||||
}
|
||||
|
||||
diesel::update(comments::table)
|
||||
.filter(comments::in_response_to_id.eq(self.id))
|
||||
.set(comments::in_response_to_id.eq(self.in_response_to_id))
|
||||
.execute(conn)?;
|
||||
diesel::delete(&self).execute(conn)?;
|
||||
.execute(&**conn)?;
|
||||
diesel::delete(&self).execute(&**conn)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -422,35 +395,10 @@ impl CommentTree {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::blogs::Blog;
|
||||
use crate::db_conn::DbConn;
|
||||
use crate::inbox::{inbox, tests::fill_database, InboxResult};
|
||||
use crate::safe_string::SafeString;
|
||||
use crate::tests::{db, format_datetime};
|
||||
use assert_json_diff::assert_json_eq;
|
||||
use crate::tests::db;
|
||||
use diesel::Connection;
|
||||
use serde_json::{json, to_value};
|
||||
|
||||
fn prepare_activity(conn: &DbConn) -> (Comment, Vec<Post>, Vec<User>, Vec<Blog>) {
|
||||
let (posts, users, blogs) = fill_database(conn);
|
||||
|
||||
let comment = Comment::insert(
|
||||
conn,
|
||||
NewComment {
|
||||
content: SafeString::new("My comment, mentioning to @user"),
|
||||
in_response_to_id: None,
|
||||
post_id: posts[0].id,
|
||||
author_id: users[0].id,
|
||||
ap_url: None,
|
||||
sensitive: true,
|
||||
spoiler_text: "My CW".into(),
|
||||
public_visibility: true,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
(comment, posts, users, blogs)
|
||||
}
|
||||
|
||||
// creates a post, get it's Create activity, delete the post,
|
||||
// "send" the Create to the inbox, and check it works
|
||||
@ -458,77 +406,30 @@ mod tests {
|
||||
fn self_federation() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (original_comm, posts, users, _blogs) = prepare_activity(conn);
|
||||
let act = original_comm.create_activity(conn).unwrap();
|
||||
let (posts, users, _) = fill_database(&conn);
|
||||
|
||||
assert_json_eq!(to_value(&act).unwrap(), json!({
|
||||
"actor": "https://plu.me/@/admin/",
|
||||
"cc": ["https://plu.me/@/admin/followers"],
|
||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}/activity", original_comm.id),
|
||||
"object": {
|
||||
"attributedTo": "https://plu.me/@/admin/",
|
||||
"content": r###"<p dir="auto">My comment, mentioning to <a href="https://plu.me/@/user/" title="user">@user</a></p>
|
||||
"###,
|
||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", original_comm.id),
|
||||
"inReplyTo": "https://plu.me/~/BlogName/testing",
|
||||
"published": format_datetime(&original_comm.creation_date),
|
||||
"summary": "My CW",
|
||||
"tag": [
|
||||
{
|
||||
"href": "https://plu.me/@/user/",
|
||||
"name": "@user",
|
||||
"type": "Mention"
|
||||
}
|
||||
],
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Note"
|
||||
},
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Create",
|
||||
}));
|
||||
|
||||
let reply = Comment::insert(
|
||||
let original_comm = Comment::insert(
|
||||
conn,
|
||||
NewComment {
|
||||
content: SafeString::new(""),
|
||||
in_response_to_id: Some(original_comm.id),
|
||||
content: SafeString::new("My comment"),
|
||||
in_response_to_id: None,
|
||||
post_id: posts[0].id,
|
||||
author_id: users[1].id,
|
||||
author_id: users[0].id,
|
||||
ap_url: None,
|
||||
sensitive: false,
|
||||
spoiler_text: "".into(),
|
||||
sensitive: true,
|
||||
spoiler_text: "My CW".into(),
|
||||
public_visibility: true,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let reply_act = reply.create_activity(conn).unwrap();
|
||||
|
||||
assert_json_eq!(to_value(&reply_act).unwrap(), json!({
|
||||
"actor": "https://plu.me/@/user/",
|
||||
"cc": ["https://plu.me/@/user/followers"],
|
||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}/activity", reply.id),
|
||||
"object": {
|
||||
"attributedTo": "https://plu.me/@/user/",
|
||||
"content": "",
|
||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", reply.id),
|
||||
"inReplyTo": format!("https://plu.me/~/BlogName/testing/comment/{}", original_comm.id),
|
||||
"published": format_datetime(&reply.creation_date),
|
||||
"summary": "",
|
||||
"tag": [],
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Note"
|
||||
},
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Create"
|
||||
}));
|
||||
|
||||
let act = original_comm.create_activity(&conn).unwrap();
|
||||
inbox(
|
||||
conn,
|
||||
serde_json::to_value(original_comm.build_delete(conn).unwrap()).unwrap(),
|
||||
&conn,
|
||||
serde_json::to_value(original_comm.build_delete(&conn).unwrap()).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
match inbox(conn, to_value(act).unwrap()).unwrap() {
|
||||
match inbox(&conn, serde_json::to_value(act).unwrap()).unwrap() {
|
||||
InboxResult::Commented(c) => {
|
||||
// TODO: one is HTML, the other markdown: assert_eq!(c.content, original_comm.content);
|
||||
assert_eq!(c.in_response_to_id, original_comm.in_response_to_id);
|
||||
@ -543,60 +444,4 @@ mod tests {
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_activity() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, Error, _>(|| {
|
||||
let (comment, _posts, _users, _blogs) = prepare_activity(&conn);
|
||||
let act = comment.to_activity(&conn)?;
|
||||
|
||||
let expected = json!({
|
||||
"attributedTo": "https://plu.me/@/admin/",
|
||||
"content": r###"<p dir="auto">My comment, mentioning to <a href="https://plu.me/@/user/" title="user">@user</a></p>
|
||||
"###,
|
||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", comment.id),
|
||||
"inReplyTo": "https://plu.me/~/BlogName/testing",
|
||||
"published": format_datetime(&comment.creation_date),
|
||||
"summary": "My CW",
|
||||
"tag": [
|
||||
{
|
||||
"href": "https://plu.me/@/user/",
|
||||
"name": "@user",
|
||||
"type": "Mention"
|
||||
}
|
||||
],
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Note"
|
||||
});
|
||||
|
||||
assert_json_eq!(to_value(act)?, expected);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_delete() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, Error, _>(|| {
|
||||
let (comment, _posts, _users, _blogs) = prepare_activity(&conn);
|
||||
let act = comment.build_delete(&conn)?;
|
||||
|
||||
let expected = json!({
|
||||
"actor": "https://plu.me/@/admin/",
|
||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}#delete", comment.id),
|
||||
"object": {
|
||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", comment.id),
|
||||
"type": "Tombstone"
|
||||
},
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Delete"
|
||||
});
|
||||
|
||||
assert_json_eq!(to_value(act)?, expected);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,9 @@
|
||||
use crate::search::TokenizerKind as SearchTokenizer;
|
||||
use crate::signups::Strategy as SignupStrategy;
|
||||
use crate::smtp::{SMTP_PORT, SUBMISSIONS_PORT, SUBMISSION_PORT};
|
||||
use rocket::config::Limits;
|
||||
use rocket::Config as RocketConfig;
|
||||
use std::collections::HashSet;
|
||||
use std::env::{self, var};
|
||||
|
||||
#[cfg(feature = "s3")]
|
||||
use s3::{Bucket, Region, creds::Credentials};
|
||||
|
||||
#[cfg(not(test))]
|
||||
const DB_NAME: &str = "plume";
|
||||
#[cfg(test)]
|
||||
@ -20,49 +15,37 @@ pub struct Config {
|
||||
pub db_name: &'static str,
|
||||
pub db_max_size: Option<u32>,
|
||||
pub db_min_idle: Option<u32>,
|
||||
pub signup: SignupStrategy,
|
||||
pub search_index: String,
|
||||
pub search_tokenizers: SearchTokenizerConfig,
|
||||
pub rocket: Result<RocketConfig, InvalidRocketConfig>,
|
||||
pub rocket: Result<RocketConfig, RocketError>,
|
||||
pub logo: LogoConfig,
|
||||
pub default_theme: String,
|
||||
pub media_directory: String,
|
||||
pub mail: Option<MailConfig>,
|
||||
pub ldap: Option<LdapConfig>,
|
||||
pub proxy: Option<ProxyConfig>,
|
||||
pub s3: Option<S3Config>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn proxy(&self) -> Option<&reqwest::Proxy> {
|
||||
self.proxy.as_ref().map(|p| &p.proxy)
|
||||
}
|
||||
}
|
||||
|
||||
fn string_to_bool(val: &str, name: &str) -> bool {
|
||||
match val {
|
||||
"1" | "true" | "TRUE" => true,
|
||||
"0" | "false" | "FALSE" => false,
|
||||
_ => panic!("Invalid configuration: {} is not boolean", name),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum InvalidRocketConfig {
|
||||
Env,
|
||||
Address,
|
||||
SecretKey,
|
||||
pub enum RocketError {
|
||||
InvalidEnv,
|
||||
InvalidAddress,
|
||||
InvalidSecretKey,
|
||||
}
|
||||
|
||||
fn get_rocket_config() -> Result<RocketConfig, InvalidRocketConfig> {
|
||||
let mut c = RocketConfig::active().map_err(|_| InvalidRocketConfig::Env)?;
|
||||
fn get_rocket_config() -> Result<RocketConfig, RocketError> {
|
||||
let mut c = RocketConfig::active().map_err(|_| RocketError::InvalidEnv)?;
|
||||
|
||||
let address = var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned());
|
||||
let port = var("ROCKET_PORT")
|
||||
.ok()
|
||||
.map(|s| s.parse::<u16>().unwrap())
|
||||
.unwrap_or(7878);
|
||||
let secret_key = var("ROCKET_SECRET_KEY").map_err(|_| InvalidRocketConfig::SecretKey)?;
|
||||
let secret_key = var("ROCKET_SECRET_KEY").map_err(|_| RocketError::InvalidSecretKey)?;
|
||||
let form_size = var("FORM_SIZE")
|
||||
.unwrap_or_else(|_| "128".to_owned())
|
||||
.parse::<u64>()
|
||||
@ -73,10 +56,10 @@ fn get_rocket_config() -> Result<RocketConfig, InvalidRocketConfig> {
|
||||
.unwrap();
|
||||
|
||||
c.set_address(address)
|
||||
.map_err(|_| InvalidRocketConfig::Address)?;
|
||||
.map_err(|_| RocketError::InvalidAddress)?;
|
||||
c.set_port(port);
|
||||
c.set_secret_key(secret_key)
|
||||
.map_err(|_| InvalidRocketConfig::SecretKey)?;
|
||||
.map_err(|_| RocketError::InvalidSecretKey)?;
|
||||
|
||||
c.set_limits(
|
||||
Limits::new()
|
||||
@ -172,7 +155,7 @@ impl Default for LogoConfig {
|
||||
.ok()
|
||||
.or_else(|| custom_main.clone());
|
||||
let other = if let Some(main) = custom_main.clone() {
|
||||
let ext = |path: &str| match path.rsplit_once('.').map(|x| x.1) {
|
||||
let ext = |path: &str| match path.rsplitn(2, '.').next() {
|
||||
Some("png") => Some("image/png".to_owned()),
|
||||
Some("jpg") | Some("jpeg") => Some("image/jpeg".to_owned()),
|
||||
Some("svg") => Some("image/svg+xml".to_owned()),
|
||||
@ -262,38 +245,12 @@ impl SearchTokenizerConfig {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MailConfig {
|
||||
pub server: String,
|
||||
pub port: u16,
|
||||
pub helo_name: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
fn get_mail_config() -> Option<MailConfig> {
|
||||
Some(MailConfig {
|
||||
server: env::var("MAIL_SERVER").ok()?,
|
||||
port: env::var("MAIL_PORT").map_or(SUBMISSIONS_PORT, |port| match port.as_str() {
|
||||
"smtp" => SMTP_PORT,
|
||||
"submissions" => SUBMISSIONS_PORT,
|
||||
"submission" => SUBMISSION_PORT,
|
||||
number => number
|
||||
.parse()
|
||||
.expect(r#"MAIL_PORT must be "smtp", "submissions", "submission" or an integer."#),
|
||||
}),
|
||||
helo_name: env::var("MAIL_HELO_NAME").unwrap_or_else(|_| "localhost".to_owned()),
|
||||
username: env::var("MAIL_USER").ok()?,
|
||||
password: env::var("MAIL_PASSWORD").ok()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub struct LdapConfig {
|
||||
pub addr: String,
|
||||
pub base_dn: String,
|
||||
pub tls: bool,
|
||||
pub user_name_attr: String,
|
||||
pub mail_attr: String,
|
||||
pub user: Option<(String, String)>,
|
||||
}
|
||||
|
||||
fn get_ldap_config() -> Option<LdapConfig> {
|
||||
@ -302,29 +259,23 @@ fn get_ldap_config() -> Option<LdapConfig> {
|
||||
match (addr, base_dn) {
|
||||
(Some(addr), Some(base_dn)) => {
|
||||
let tls = var("LDAP_TLS").unwrap_or_else(|_| "false".to_owned());
|
||||
let tls = string_to_bool(&tls, "LDAP_TLS");
|
||||
let tls = match tls.as_ref() {
|
||||
"1" | "true" | "TRUE" => true,
|
||||
"0" | "false" | "FALSE" => false,
|
||||
_ => panic!("Invalid LDAP configuration : tls"),
|
||||
};
|
||||
let user_name_attr = var("LDAP_USER_NAME_ATTR").unwrap_or_else(|_| "cn".to_owned());
|
||||
let mail_attr = var("LDAP_USER_MAIL_ATTR").unwrap_or_else(|_| "mail".to_owned());
|
||||
//2023-12-30
|
||||
let user = var("LDAP_USER").ok();
|
||||
let password = var("LDAP_PASSWORD").ok();
|
||||
let user = match (user, password) {
|
||||
(Some(user), Some(password)) => Some((user, password)),
|
||||
(None, None) => None,
|
||||
_ => panic!("Invalid LDAP configuration both or neither of LDAP_USER and LDAP_PASSWORD must be set")
|
||||
};
|
||||
//
|
||||
Some(LdapConfig {
|
||||
addr,
|
||||
base_dn,
|
||||
tls,
|
||||
user_name_attr,
|
||||
mail_attr,
|
||||
user,
|
||||
})
|
||||
}
|
||||
(None, None) => None,
|
||||
_ => {
|
||||
(_, _) => {
|
||||
panic!("Invalid LDAP configuration : both LDAP_ADDR and LDAP_BASE_DN must be set")
|
||||
}
|
||||
}
|
||||
@ -369,104 +320,6 @@ 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! {
|
||||
pub static ref CONFIG: Config = Config {
|
||||
base_url: var("BASE_URL").unwrap_or_else(|_| format!(
|
||||
@ -482,7 +335,6 @@ lazy_static! {
|
||||
s.parse::<u32>()
|
||||
.expect("Couldn't parse DB_MIN_IDLE into u32")
|
||||
)),
|
||||
signup: var("SIGNUP").map_or(SignupStrategy::default(), |s| s.parse().unwrap()),
|
||||
#[cfg(feature = "postgres")]
|
||||
database_url: var("DATABASE_URL")
|
||||
.unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME)),
|
||||
@ -495,9 +347,7 @@ lazy_static! {
|
||||
default_theme: var("DEFAULT_THEME").unwrap_or_else(|_| "default-light".to_owned()),
|
||||
media_directory: var("MEDIA_UPLOAD_DIRECTORY")
|
||||
.unwrap_or_else(|_| "static/media".to_owned()),
|
||||
mail: get_mail_config(),
|
||||
ldap: get_ldap_config(),
|
||||
proxy: get_proxy_config(),
|
||||
s3: get_s3_config(),
|
||||
};
|
||||
}
|
||||
|
@ -69,8 +69,7 @@ pub(crate) mod tests {
|
||||
impl CustomizeConnection<Connection, ConnError> for TestConnectionCustomizer {
|
||||
fn on_acquire(&self, conn: &mut Connection) -> Result<(), ConnError> {
|
||||
PragmaForeignKey.on_acquire(conn)?;
|
||||
conn.begin_test_transaction().unwrap();
|
||||
Ok(())
|
||||
Ok(conn.begin_test_transaction().unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,158 +0,0 @@
|
||||
use crate::{
|
||||
blocklisted_emails::BlocklistedEmail,
|
||||
db_conn::DbConn,
|
||||
schema::email_signups,
|
||||
users::{NewUser, Role, User},
|
||||
Error, Result,
|
||||
};
|
||||
use chrono::{offset::Utc, Duration, NaiveDateTime};
|
||||
use diesel::{
|
||||
Connection as _, ExpressionMethods, Identifiable, Insertable, QueryDsl, Queryable, RunQueryDsl,
|
||||
};
|
||||
use plume_common::utils::random_hex;
|
||||
use std::ops::Deref;
|
||||
|
||||
const TOKEN_VALIDITY_HOURS: i64 = 2;
|
||||
|
||||
#[repr(transparent)]
|
||||
pub struct Token(String);
|
||||
|
||||
impl From<String> for Token {
|
||||
fn from(string: String) -> Self {
|
||||
Token(string)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Token> for String {
|
||||
fn from(token: Token) -> Self {
|
||||
token.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Token {
|
||||
type Target = String;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Token {
|
||||
fn generate() -> Self {
|
||||
Self(random_hex())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Identifiable, Queryable)]
|
||||
pub struct EmailSignup {
|
||||
pub id: i32,
|
||||
pub email: String,
|
||||
pub token: String,
|
||||
pub expiration_date: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[table_name = "email_signups"]
|
||||
pub struct NewEmailSignup<'a> {
|
||||
pub email: &'a str,
|
||||
pub token: &'a str,
|
||||
pub expiration_date: NaiveDateTime,
|
||||
}
|
||||
|
||||
impl EmailSignup {
|
||||
pub fn start(conn: &DbConn, email: &str) -> Result<Token> {
|
||||
Self::ensure_email_not_blocked(conn, email)?;
|
||||
|
||||
conn.transaction(|| {
|
||||
Self::ensure_user_not_exist_by_email(conn, email)?;
|
||||
let _rows = Self::delete_existings_by_email(conn, email)?;
|
||||
let token = Token::generate();
|
||||
let expiration_date = Utc::now()
|
||||
.naive_utc()
|
||||
.checked_add_signed(Duration::hours(TOKEN_VALIDITY_HOURS))
|
||||
.expect("could not calculate expiration date");
|
||||
let new_signup = NewEmailSignup {
|
||||
email,
|
||||
token: &token,
|
||||
expiration_date,
|
||||
};
|
||||
let _rows = diesel::insert_into(email_signups::table)
|
||||
.values(new_signup)
|
||||
.execute(&**conn)?;
|
||||
|
||||
Ok(token)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn find_by_token(conn: &DbConn, token: Token) -> Result<Self> {
|
||||
let signup = email_signups::table
|
||||
.filter(email_signups::token.eq(token.as_str()))
|
||||
.first::<Self>(&**conn)
|
||||
.map_err(Error::from)?;
|
||||
Ok(signup)
|
||||
}
|
||||
|
||||
pub fn confirm(&self, conn: &DbConn) -> Result<()> {
|
||||
Self::ensure_email_not_blocked(conn, &self.email)?;
|
||||
|
||||
conn.transaction(|| {
|
||||
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
|
||||
if self.expired() {
|
||||
Self::delete_existings_by_email(conn, &self.email)?;
|
||||
return Err(Error::Expired);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn complete(&self, conn: &DbConn, username: String, password: String) -> Result<User> {
|
||||
Self::ensure_email_not_blocked(conn, &self.email)?;
|
||||
|
||||
conn.transaction(|| {
|
||||
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
|
||||
let user = NewUser::new_local(
|
||||
conn,
|
||||
username,
|
||||
"".to_string(),
|
||||
Role::Normal,
|
||||
"",
|
||||
self.email.clone(),
|
||||
Some(User::hash_pass(&password)?),
|
||||
)?;
|
||||
self.delete(conn)?;
|
||||
Ok(user)
|
||||
})
|
||||
}
|
||||
|
||||
fn delete(&self, conn: &DbConn) -> Result<()> {
|
||||
let _rows = diesel::delete(self).execute(&**conn).map_err(Error::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_email_not_blocked(conn: &DbConn, email: &str) -> Result<()> {
|
||||
if let Some(x) = BlocklistedEmail::matches_blocklist(conn, email)? {
|
||||
Err(Error::Blocklisted(x.notify_user, x.notification_text))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_user_not_exist_by_email(conn: &DbConn, email: &str) -> Result<()> {
|
||||
if User::email_used(conn, email)? {
|
||||
let _rows = Self::delete_existings_by_email(conn, email)?;
|
||||
return Err(Error::UserAlreadyExists);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_existings_by_email(conn: &DbConn, email: &str) -> Result<usize> {
|
||||
let existing_signups = email_signups::table.filter(email_signups::email.eq(email));
|
||||
diesel::delete(existing_signups)
|
||||
.execute(&**conn)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
fn expired(&self) -> bool {
|
||||
self.expiration_date < Utc::now().naive_utc()
|
||||
}
|
||||
}
|
@ -1,13 +1,8 @@
|
||||
use crate::{
|
||||
ap_url, instance::Instance, notifications::*, schema::follows, users::User, Connection, Error,
|
||||
Result, CONFIG,
|
||||
};
|
||||
use activitystreams::{
|
||||
activity::{Accept, ActorAndObjectRef, Follow as FollowAct, Undo},
|
||||
base::AnyBase,
|
||||
iri_string::types::IriString,
|
||||
prelude::*,
|
||||
ap_url, db_conn::DbConn, instance::Instance, notifications::*, schema::follows, users::User,
|
||||
Connection, Error, Result, CONFIG,
|
||||
};
|
||||
use activitypub::activity::{Accept, Follow as FollowAct, Undo};
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||
use plume_common::activity_pub::{
|
||||
broadcast,
|
||||
@ -58,13 +53,15 @@ impl Follow {
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<FollowAct> {
|
||||
let user = User::get(conn, self.follower_id)?;
|
||||
let target = User::get(conn, self.following_id)?;
|
||||
let target_id = target.ap_url.parse::<IriString>()?;
|
||||
|
||||
let mut act = FollowAct::new(user.ap_url.parse::<IriString>()?, target_id.clone());
|
||||
act.set_id(self.ap_url.parse::<IriString>()?);
|
||||
act.set_many_tos(vec![target_id]);
|
||||
act.set_many_ccs(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
|
||||
|
||||
let mut act = FollowAct::default();
|
||||
act.follow_props.set_actor_link::<Id>(user.into_id())?;
|
||||
act.follow_props
|
||||
.set_object_link::<Id>(target.clone().into_id())?;
|
||||
act.object_props.set_id_string(self.ap_url.clone())?;
|
||||
act.object_props.set_to_link_vec(vec![target.into_id()])?;
|
||||
act.object_props
|
||||
.set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
|
||||
Ok(act)
|
||||
}
|
||||
|
||||
@ -97,87 +94,82 @@ impl Follow {
|
||||
NewFollow {
|
||||
follower_id: from_id,
|
||||
following_id: target_id,
|
||||
ap_url: follow
|
||||
.object_field_ref()
|
||||
.as_single_id()
|
||||
.ok_or(Error::MissingApProperty)?
|
||||
.to_string(),
|
||||
ap_url: follow.object_props.id_string()?,
|
||||
},
|
||||
)?;
|
||||
res.notify(conn)?;
|
||||
|
||||
let accept = res.build_accept(from, target, follow)?;
|
||||
broadcast(target, accept, vec![from.clone()], CONFIG.proxy().cloned());
|
||||
let mut accept = Accept::default();
|
||||
let accept_id = ap_url(&format!(
|
||||
"{}/follow/{}/accept",
|
||||
CONFIG.base_url.as_str(),
|
||||
&res.id
|
||||
));
|
||||
accept.object_props.set_id_string(accept_id)?;
|
||||
accept
|
||||
.object_props
|
||||
.set_to_link_vec(vec![from.clone().into_id()])?;
|
||||
accept
|
||||
.object_props
|
||||
.set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
|
||||
accept
|
||||
.accept_props
|
||||
.set_actor_link::<Id>(target.clone().into_id())?;
|
||||
accept.accept_props.set_object_object(follow)?;
|
||||
broadcast(
|
||||
&*target,
|
||||
accept,
|
||||
vec![from.clone()],
|
||||
CONFIG.proxy().cloned(),
|
||||
);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn build_accept<A: Signer + IntoId + Clone, B: Clone + AsActor<T> + IntoId, T>(
|
||||
&self,
|
||||
from: &B,
|
||||
target: &A,
|
||||
follow: FollowAct,
|
||||
) -> Result<Accept> {
|
||||
let mut accept = Accept::new(
|
||||
target.clone().into_id().parse::<IriString>()?,
|
||||
AnyBase::from_extended(follow)?,
|
||||
);
|
||||
let accept_id = ap_url(&format!(
|
||||
"{}/follows/{}/accept",
|
||||
CONFIG.base_url.as_str(),
|
||||
self.id
|
||||
));
|
||||
accept.set_id(accept_id.parse::<IriString>()?);
|
||||
accept.set_many_tos(vec![from.clone().into_id().parse::<IriString>()?]);
|
||||
accept.set_many_ccs(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
|
||||
|
||||
Ok(accept)
|
||||
}
|
||||
|
||||
pub fn build_undo(&self, conn: &Connection) -> Result<Undo> {
|
||||
let mut undo = Undo::new(
|
||||
User::get(conn, self.follower_id)?
|
||||
.ap_url
|
||||
.parse::<IriString>()?,
|
||||
self.ap_url.parse::<IriString>()?,
|
||||
);
|
||||
undo.set_id(format!("{}/undo", self.ap_url).parse::<IriString>()?);
|
||||
undo.set_many_tos(vec![User::get(conn, self.following_id)?
|
||||
.ap_url
|
||||
.parse::<IriString>()?]);
|
||||
undo.set_many_ccs(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
|
||||
|
||||
let mut undo = Undo::default();
|
||||
undo.undo_props
|
||||
.set_actor_link(User::get(conn, self.follower_id)?.into_id())?;
|
||||
undo.object_props
|
||||
.set_id_string(format!("{}/undo", self.ap_url))?;
|
||||
undo.undo_props
|
||||
.set_object_link::<Id>(self.clone().into_id())?;
|
||||
undo.object_props
|
||||
.set_to_link_vec(vec![User::get(conn, self.following_id)?.into_id()])?;
|
||||
undo.object_props
|
||||
.set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
|
||||
Ok(undo)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, FollowAct, &Connection> for User {
|
||||
impl AsObject<User, FollowAct, &DbConn> for User {
|
||||
type Error = Error;
|
||||
type Output = Follow;
|
||||
|
||||
fn activity(self, conn: &Connection, actor: User, id: &str) -> Result<Follow> {
|
||||
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Follow> {
|
||||
// Mastodon (at least) requires the full Follow object when accepting it,
|
||||
// so we rebuilt it here
|
||||
let follow = FollowAct::new(actor.ap_url.parse::<IriString>()?, id.parse::<IriString>()?);
|
||||
let mut follow = FollowAct::default();
|
||||
follow.object_props.set_id_string(id.to_string())?;
|
||||
follow
|
||||
.follow_props
|
||||
.set_actor_link::<Id>(actor.clone().into_id())?;
|
||||
Follow::accept_follow(conn, &actor, &self, follow, actor.id, self.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromId<Connection> for Follow {
|
||||
impl FromId<DbConn> for Follow {
|
||||
type Error = Error;
|
||||
type Object = FollowAct;
|
||||
|
||||
fn from_db(conn: &Connection, id: &str) -> Result<Self> {
|
||||
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
||||
Follow::find_by_ap_url(conn, id)
|
||||
}
|
||||
|
||||
fn from_activity(conn: &Connection, follow: FollowAct) -> Result<Self> {
|
||||
fn from_activity(conn: &DbConn, follow: FollowAct) -> Result<Self> {
|
||||
let actor = User::from_id(
|
||||
conn,
|
||||
follow
|
||||
.actor_field_ref()
|
||||
.as_single_id()
|
||||
.ok_or(Error::MissingApProperty)?
|
||||
.as_str(),
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&follow.follow_props.actor_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
@ -185,35 +177,28 @@ impl FromId<Connection> for Follow {
|
||||
|
||||
let target = User::from_id(
|
||||
conn,
|
||||
follow
|
||||
.object_field_ref()
|
||||
.as_single_id()
|
||||
.ok_or(Error::MissingApProperty)?
|
||||
.as_str(),
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&follow.follow_props.object_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.map_err(|(_, e)| e)?;
|
||||
Follow::accept_follow(conn, &actor, &target, follow, actor.id, target.id)
|
||||
}
|
||||
|
||||
fn get_sender() -> &'static dyn Signer {
|
||||
Instance::get_local_instance_user().expect("Failed to local instance user")
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, Undo, &Connection> for Follow {
|
||||
impl AsObject<User, Undo, &DbConn> for Follow {
|
||||
type Error = Error;
|
||||
type Output = ();
|
||||
|
||||
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> {
|
||||
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
||||
let conn = conn;
|
||||
if self.follower_id == actor.id {
|
||||
diesel::delete(&self).execute(conn)?;
|
||||
diesel::delete(&self).execute(&**conn)?;
|
||||
|
||||
// delete associated notification if any
|
||||
if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
|
||||
diesel::delete(¬if).execute(conn)?;
|
||||
if let Ok(notif) = Notification::find(&conn, notification_kind::FOLLOW, self.id) {
|
||||
diesel::delete(¬if).execute(&**conn)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -232,31 +217,8 @@ impl IntoId for Follow {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
db_conn::DbConn, tests::db, users::tests as user_tests, users::tests::fill_database,
|
||||
};
|
||||
use assert_json_diff::assert_json_eq;
|
||||
use crate::{tests::db, users::tests as user_tests};
|
||||
use diesel::Connection;
|
||||
use serde_json::{json, to_value};
|
||||
|
||||
fn prepare_activity(conn: &DbConn) -> (Follow, User, User, Vec<User>) {
|
||||
let users = fill_database(conn);
|
||||
let following = &users[1];
|
||||
let follower = &users[2];
|
||||
let mut follow = Follow::insert(
|
||||
conn,
|
||||
NewFollow {
|
||||
follower_id: follower.id,
|
||||
following_id: following.id,
|
||||
ap_url: "".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
// following.ap_url = format!("https://plu.me/follows/{}", follow.id);
|
||||
follow.ap_url = format!("https://plu.me/follows/{}", follow.id);
|
||||
|
||||
(follow, following.to_owned(), follower.to_owned(), users)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_id() {
|
||||
@ -291,77 +253,4 @@ mod tests {
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_activity() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, Error, _>(|| {
|
||||
let (follow, _following, _follower, _users) = prepare_activity(&conn);
|
||||
let act = follow.to_activity(&conn)?;
|
||||
|
||||
let expected = json!({
|
||||
"actor": "https://plu.me/@/other/",
|
||||
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"id": format!("https://plu.me/follows/{}", follow.id),
|
||||
"object": "https://plu.me/@/user/",
|
||||
"to": ["https://plu.me/@/user/"],
|
||||
"type": "Follow"
|
||||
});
|
||||
|
||||
assert_json_eq!(to_value(act)?, expected);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_accept() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, Error, _>(|| {
|
||||
let (follow, following, follower, _users) = prepare_activity(&conn);
|
||||
let act = follow.build_accept(&follower, &following, follow.to_activity(&conn)?)?;
|
||||
|
||||
let expected = json!({
|
||||
"actor": "https://plu.me/@/user/",
|
||||
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"id": format!("https://127.0.0.1:7878/follows/{}/accept", follow.id),
|
||||
"object": {
|
||||
"actor": "https://plu.me/@/other/",
|
||||
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"id": format!("https://plu.me/follows/{}", follow.id),
|
||||
"object": "https://plu.me/@/user/",
|
||||
"to": ["https://plu.me/@/user/"],
|
||||
"type": "Follow"
|
||||
},
|
||||
"to": ["https://plu.me/@/other/"],
|
||||
"type": "Accept"
|
||||
});
|
||||
|
||||
assert_json_eq!(to_value(act)?, expected);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_undo() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, Error, _>(|| {
|
||||
let (follow, _following, _follower, _users) = prepare_activity(&conn);
|
||||
let act = follow.build_undo(&conn)?;
|
||||
|
||||
let expected = json!({
|
||||
"actor": "https://plu.me/@/other/",
|
||||
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"id": format!("https://plu.me/follows/{}/undo", follow.id),
|
||||
"object": format!("https://plu.me/follows/{}", follow.id),
|
||||
"to": ["https://plu.me/@/user/"],
|
||||
"type": "Undo"
|
||||
});
|
||||
|
||||
assert_json_eq!(to_value(act)?, expected);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,15 @@
|
||||
use activitystreams::activity::{Announce, Create, Delete, Follow, Like, Undo, Update};
|
||||
use activitypub::activity::*;
|
||||
|
||||
use crate::{
|
||||
comments::Comment,
|
||||
follows, likes,
|
||||
db_conn::DbConn,
|
||||
follows,
|
||||
instance::Instance,
|
||||
likes,
|
||||
posts::{Post, PostUpdate},
|
||||
reshares::Reshare,
|
||||
users::User,
|
||||
Connection, Error, CONFIG,
|
||||
Error, CONFIG,
|
||||
};
|
||||
use plume_common::activity_pub::inbox::Inbox;
|
||||
|
||||
@ -45,21 +48,25 @@ impl_into_inbox_result! {
|
||||
Reshare => Reshared
|
||||
}
|
||||
|
||||
pub fn inbox(conn: &Connection, act: serde_json::Value) -> Result<InboxResult, Error> {
|
||||
Inbox::handle(conn, act)
|
||||
.with::<User, Announce, Post>(CONFIG.proxy())
|
||||
.with::<User, Create, Comment>(CONFIG.proxy())
|
||||
.with::<User, Create, Post>(CONFIG.proxy())
|
||||
.with::<User, Delete, Comment>(CONFIG.proxy())
|
||||
.with::<User, Delete, Post>(CONFIG.proxy())
|
||||
.with::<User, Delete, User>(CONFIG.proxy())
|
||||
.with::<User, Follow, User>(CONFIG.proxy())
|
||||
.with::<User, Like, Post>(CONFIG.proxy())
|
||||
.with::<User, Undo, Reshare>(CONFIG.proxy())
|
||||
.with::<User, Undo, follows::Follow>(CONFIG.proxy())
|
||||
.with::<User, Undo, likes::Like>(CONFIG.proxy())
|
||||
.with::<User, Update, PostUpdate>(CONFIG.proxy())
|
||||
.done()
|
||||
pub fn inbox(conn: &DbConn, act: serde_json::Value) -> Result<InboxResult, Error> {
|
||||
Inbox::handle(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
act,
|
||||
)
|
||||
.with::<User, Announce, Post>(CONFIG.proxy())
|
||||
.with::<User, Create, Comment>(CONFIG.proxy())
|
||||
.with::<User, Create, Post>(CONFIG.proxy())
|
||||
.with::<User, Delete, Comment>(CONFIG.proxy())
|
||||
.with::<User, Delete, Post>(CONFIG.proxy())
|
||||
.with::<User, Delete, User>(CONFIG.proxy())
|
||||
.with::<User, Follow, User>(CONFIG.proxy())
|
||||
.with::<User, Like, Post>(CONFIG.proxy())
|
||||
.with::<User, Undo, Reshare>(CONFIG.proxy())
|
||||
.with::<User, Undo, follows::Follow>(CONFIG.proxy())
|
||||
.with::<User, Undo, likes::Like>(CONFIG.proxy())
|
||||
.with::<User, Update, PostUpdate>(CONFIG.proxy())
|
||||
.done()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -81,9 +88,9 @@ pub(crate) mod tests {
|
||||
use crate::post_authors::*;
|
||||
use crate::posts::*;
|
||||
|
||||
let (users, blogs) = blog_fill_db(conn);
|
||||
let (users, blogs) = blog_fill_db(&conn);
|
||||
let post = Post::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewPost {
|
||||
blog_id: blogs[0].id,
|
||||
slug: "testing".to_owned(),
|
||||
@ -93,15 +100,15 @@ pub(crate) mod tests {
|
||||
license: "WTFPL".to_owned(),
|
||||
creation_date: None,
|
||||
ap_url: format!("https://plu.me/~/{}/testing", blogs[0].actor_id),
|
||||
subtitle: "Bye".to_string(),
|
||||
source: "Hello".to_string(),
|
||||
subtitle: String::new(),
|
||||
source: String::new(),
|
||||
cover_id: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
PostAuthor::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewPostAuthor {
|
||||
post_id: post.id,
|
||||
author_id: users[0].id,
|
||||
@ -189,7 +196,7 @@ pub(crate) mod tests {
|
||||
});
|
||||
|
||||
assert!(matches!(
|
||||
super::inbox(&conn, act),
|
||||
super::inbox(&conn, act.clone()),
|
||||
Err(super::Error::Inbox(
|
||||
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
||||
))
|
||||
@ -220,7 +227,7 @@ pub(crate) mod tests {
|
||||
});
|
||||
|
||||
assert!(matches!(
|
||||
super::inbox(&conn, act),
|
||||
super::inbox(&conn, act.clone()),
|
||||
Err(super::Error::Inbox(
|
||||
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
||||
))
|
||||
@ -248,7 +255,7 @@ pub(crate) mod tests {
|
||||
});
|
||||
|
||||
assert!(matches!(
|
||||
super::inbox(&conn, act),
|
||||
super::inbox(&conn, act.clone()),
|
||||
Err(super::Error::Inbox(
|
||||
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
||||
))
|
||||
@ -267,7 +274,7 @@ pub(crate) mod tests {
|
||||
"actor": users[0].ap_url,
|
||||
"object": {
|
||||
"type": "Article",
|
||||
"id": "https://plu.me/~/BlogName/testing",
|
||||
"id": "https://plu.me/~/Blog/my-article",
|
||||
"attributedTo": [users[0].ap_url, blogs[0].ap_url],
|
||||
"content": "Hello.",
|
||||
"name": "My Article",
|
||||
@ -285,11 +292,11 @@ pub(crate) mod tests {
|
||||
match super::inbox(&conn, act).unwrap() {
|
||||
super::InboxResult::Post(p) => {
|
||||
assert!(p.is_author(&conn, users[0].id).unwrap());
|
||||
assert_eq!(p.source, "Hello".to_owned());
|
||||
assert_eq!(p.source, "Hello.".to_owned());
|
||||
assert_eq!(p.blog_id, blogs[0].id);
|
||||
assert_eq!(p.content, SafeString::new("Hello"));
|
||||
assert_eq!(p.subtitle, "Bye".to_owned());
|
||||
assert_eq!(p.title, "Testing".to_owned());
|
||||
assert_eq!(p.content, SafeString::new("Hello."));
|
||||
assert_eq!(p.subtitle, "Bye.".to_owned());
|
||||
assert_eq!(p.title, "My Article".to_owned());
|
||||
}
|
||||
_ => panic!("Unexpected result"),
|
||||
};
|
||||
@ -323,7 +330,7 @@ pub(crate) mod tests {
|
||||
});
|
||||
|
||||
assert!(matches!(
|
||||
super::inbox(&conn, act),
|
||||
super::inbox(&conn, act.clone()),
|
||||
Err(super::Error::Inbox(
|
||||
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
||||
))
|
||||
@ -361,7 +368,7 @@ pub(crate) mod tests {
|
||||
});
|
||||
|
||||
assert!(matches!(
|
||||
super::inbox(&conn, act),
|
||||
super::inbox(&conn, act.clone()),
|
||||
Err(super::Error::Inbox(
|
||||
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
||||
))
|
||||
@ -396,7 +403,7 @@ pub(crate) mod tests {
|
||||
});
|
||||
|
||||
assert!(matches!(
|
||||
super::inbox(&conn, act),
|
||||
super::inbox(&conn, act.clone()),
|
||||
Err(super::Error::Inbox(
|
||||
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
|
||||
))
|
||||
|
@ -3,14 +3,29 @@ use crate::{
|
||||
medias::Media,
|
||||
safe_string::SafeString,
|
||||
schema::{instances, users},
|
||||
users::{NewUser, Role, User},
|
||||
users::{Role, User},
|
||||
Connection, Error, Result,
|
||||
};
|
||||
use activitypub::{actor::Service, CustomObject};
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::{self, result::Error::NotFound, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use once_cell::sync::OnceCell;
|
||||
use plume_common::utils::{iri_percent_encode_seg, md_to_html};
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use openssl::{
|
||||
hash::MessageDigest,
|
||||
pkey::{PKey, Private},
|
||||
rsa::Rsa,
|
||||
sign,
|
||||
};
|
||||
use plume_common::{
|
||||
activity_pub::{
|
||||
sign::{gen_keypair, Error as SignatureError, Result as SignatureResult, Signer},
|
||||
ApSignature, PublicKey,
|
||||
},
|
||||
utils::md_to_html,
|
||||
};
|
||||
use std::sync::RwLock;
|
||||
use tracing::warn;
|
||||
|
||||
pub type CustomService = CustomObject<ApSignature, Service>;
|
||||
|
||||
#[derive(Clone, Identifiable, Queryable)]
|
||||
pub struct Instance {
|
||||
@ -26,6 +41,8 @@ pub struct Instance {
|
||||
pub default_license: String,
|
||||
pub long_description_html: SafeString,
|
||||
pub short_description_html: SafeString,
|
||||
pub private_key: Option<String>,
|
||||
pub public_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Insertable)]
|
||||
@ -40,15 +57,14 @@ pub struct NewInstance {
|
||||
pub default_license: String,
|
||||
pub long_description_html: String,
|
||||
pub short_description_html: String,
|
||||
pub private_key: Option<String>,
|
||||
pub public_key: Option<String>,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref LOCAL_INSTANCE: RwLock<Option<Instance>> = RwLock::new(None);
|
||||
}
|
||||
|
||||
const LOCAL_INSTANCE_USERNAME: &str = "__instance__";
|
||||
static LOCAL_INSTANCE_USER: OnceCell<User> = OnceCell::new();
|
||||
|
||||
impl Instance {
|
||||
pub fn set_local(self) {
|
||||
LOCAL_INSTANCE.write().unwrap().replace(self);
|
||||
@ -73,6 +89,13 @@ impl Instance {
|
||||
*LOCAL_INSTANCE.write().unwrap() = Instance::get_local_uncached(conn).ok();
|
||||
}
|
||||
|
||||
pub fn get_locals(conn: &Connection) -> Result<Vec<Instance>> {
|
||||
instances::table
|
||||
.filter(instances::local.eq(true))
|
||||
.load::<Instance>(conn)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn get_remotes(conn: &Connection) -> Result<Vec<Instance>> {
|
||||
instances::table
|
||||
.filter(instances::local.eq(false))
|
||||
@ -80,42 +103,6 @@ impl Instance {
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn create_local_instance_user(conn: &Connection) -> Result<User> {
|
||||
let instance = Instance::get_local()?;
|
||||
let email = format!("{}@{}", LOCAL_INSTANCE_USERNAME, &instance.public_domain);
|
||||
NewUser::new_local(
|
||||
conn,
|
||||
LOCAL_INSTANCE_USERNAME.into(),
|
||||
instance.public_domain,
|
||||
Role::Instance,
|
||||
"Local instance",
|
||||
email,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_local_instance_user() -> Option<&'static User> {
|
||||
LOCAL_INSTANCE_USER.get()
|
||||
}
|
||||
|
||||
pub fn get_local_instance_user_uncached(conn: &Connection) -> Result<User> {
|
||||
users::table
|
||||
.filter(users::role.eq(3))
|
||||
.first(conn)
|
||||
.or_else(|err| match err {
|
||||
NotFound => Self::create_local_instance_user(conn),
|
||||
_ => Err(Error::Db(err)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cache_local_instance_user(conn: &Connection) {
|
||||
let _ = LOCAL_INSTANCE_USER.get_or_init(|| {
|
||||
Self::get_local_instance_user_uncached(conn)
|
||||
.or_else(|_| Self::create_local_instance_user(conn))
|
||||
.expect("Failed to cache local instance user")
|
||||
});
|
||||
}
|
||||
|
||||
pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<Instance>> {
|
||||
instances::table
|
||||
.order(instances::public_domain.asc())
|
||||
@ -173,8 +160,8 @@ impl Instance {
|
||||
"{instance}/{prefix}/{name}/{box_name}",
|
||||
instance = self.public_domain,
|
||||
prefix = prefix,
|
||||
name = iri_percent_encode_seg(name),
|
||||
box_name = iri_percent_encode_seg(box_name)
|
||||
name = name,
|
||||
box_name = box_name
|
||||
))
|
||||
}
|
||||
|
||||
@ -278,6 +265,112 @@ impl Instance {
|
||||
})
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn set_keypair(&self, conn: &Connection) -> Result<()> {
|
||||
let (pub_key, priv_key) = gen_keypair();
|
||||
let private_key = String::from_utf8(priv_key).or(Err(Error::Signature))?;
|
||||
let public_key = String::from_utf8(pub_key).or(Err(Error::Signature))?;
|
||||
diesel::update(self)
|
||||
.set((
|
||||
instances::private_key.eq(Some(private_key)),
|
||||
instances::public_key.eq(Some(public_key)),
|
||||
))
|
||||
.execute(conn)
|
||||
.and(Ok(()))
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn get_keypair(&self) -> Result<PKey<Private>> {
|
||||
PKey::from_rsa(Rsa::private_key_from_pem(
|
||||
self.private_key.clone()?.as_ref(),
|
||||
)?)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// This is experimental and might change in the future.
|
||||
/// Currently "!" sign is used but it's not decided.
|
||||
pub fn ap_url(&self) -> String {
|
||||
ap_url(&format!(
|
||||
"{}/!/{}",
|
||||
Self::get_local().unwrap().public_domain,
|
||||
self.public_domain
|
||||
))
|
||||
}
|
||||
|
||||
pub fn to_activity(&self) -> Result<CustomService> {
|
||||
let mut actor = Service::default();
|
||||
let id = self.ap_url();
|
||||
actor.object_props.set_id_string(id.clone())?;
|
||||
actor.object_props.set_name_string(self.name.clone())?;
|
||||
|
||||
let mut ap_signature = ApSignature::default();
|
||||
if self.local {
|
||||
if let Some(pub_key) = self.public_key.clone() {
|
||||
let mut public_key = PublicKey::default();
|
||||
public_key.set_id_string(format!("{}#main-key", id))?;
|
||||
public_key.set_owner_string(id)?;
|
||||
public_key.set_public_key_pem_string(pub_key)?;
|
||||
ap_signature.set_public_key_publickey(public_key)?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(CustomService::new(actor, ap_signature))
|
||||
}
|
||||
}
|
||||
|
||||
impl NewInstance {
|
||||
pub fn new_local(
|
||||
conn: &Connection,
|
||||
public_domain: String,
|
||||
name: String,
|
||||
open_registrations: bool,
|
||||
default_license: String,
|
||||
) -> Result<Instance> {
|
||||
let (pub_key, priv_key) = gen_keypair();
|
||||
|
||||
Instance::insert(
|
||||
conn,
|
||||
NewInstance {
|
||||
public_domain,
|
||||
name,
|
||||
local: true,
|
||||
open_registrations,
|
||||
short_description: SafeString::new(""),
|
||||
long_description: SafeString::new(""),
|
||||
default_license,
|
||||
long_description_html: String::new(),
|
||||
short_description_html: String::new(),
|
||||
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
|
||||
public_key: Some(String::from_utf8(pub_key).or(Err(Error::Signature))?),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Signer for Instance {
|
||||
fn get_key_id(&self) -> String {
|
||||
format!("{}#main-key", self.ap_url())
|
||||
}
|
||||
|
||||
fn sign(&self, to_sign: &str) -> SignatureResult<Vec<u8>> {
|
||||
let key = self.get_keypair()?;
|
||||
let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?;
|
||||
signer.update(to_sign.as_bytes())?;
|
||||
signer.sign_to_vec().map_err(SignatureError::from)
|
||||
}
|
||||
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<bool> {
|
||||
if self.public_key.is_none() {
|
||||
warn!("missing public key for {}", self.public_domain);
|
||||
return Err(SignatureError());
|
||||
}
|
||||
let key = PKey::from_rsa(Rsa::public_key_from_pem(
|
||||
self.public_key.clone().unwrap().as_ref(),
|
||||
)?)?;
|
||||
let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?;
|
||||
verifier.update(data.as_bytes())?;
|
||||
verifier.verify(&signature).map_err(SignatureError::from)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -299,6 +392,8 @@ pub(crate) mod tests {
|
||||
name: "My instance".to_string(),
|
||||
open_registrations: true,
|
||||
public_domain: "plu.me".to_string(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
NewInstance {
|
||||
default_license: "WTFPL".to_string(),
|
||||
@ -310,6 +405,8 @@ pub(crate) mod tests {
|
||||
name: "An instance".to_string(),
|
||||
open_registrations: true,
|
||||
public_domain: "1plu.me".to_string(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
NewInstance {
|
||||
default_license: "CC-0".to_string(),
|
||||
@ -321,6 +418,8 @@ pub(crate) mod tests {
|
||||
name: "Someone instance".to_string(),
|
||||
open_registrations: false,
|
||||
public_domain: "2plu.me".to_string(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
NewInstance {
|
||||
default_license: "CC-0-BY-SA".to_string(),
|
||||
@ -332,6 +431,8 @@ pub(crate) mod tests {
|
||||
name: "Nice day".to_string(),
|
||||
open_registrations: true,
|
||||
public_domain: "3plu.me".to_string(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
@ -344,7 +445,6 @@ pub(crate) mod tests {
|
||||
})
|
||||
.collect();
|
||||
Instance::cache_local(conn);
|
||||
Instance::cache_local_instance_user(conn);
|
||||
res
|
||||
}
|
||||
|
||||
@ -523,7 +623,7 @@ pub(crate) mod tests {
|
||||
.unwrap();
|
||||
let inst = Instance::get(conn, inst.id).unwrap();
|
||||
assert_eq!(inst.name, "NewName".to_owned());
|
||||
assert!(!inst.open_registrations);
|
||||
assert_eq!(inst.open_registrations, false);
|
||||
assert_eq!(
|
||||
inst.long_description.get(),
|
||||
"[long_description](/with_link)"
|
||||
|
119
plume-models/src/lib.rs
Normal file → Executable file
119
plume-models/src/lib.rs
Normal file → Executable file
@ -1,3 +1,4 @@
|
||||
#![feature(try_trait)]
|
||||
#![feature(never_type)]
|
||||
#![feature(proc_macro_hygiene)]
|
||||
#![feature(box_patterns)]
|
||||
@ -16,11 +17,10 @@ extern crate serde_json;
|
||||
#[macro_use]
|
||||
extern crate tantivy;
|
||||
|
||||
use activitystreams::iri_string;
|
||||
pub use lettre;
|
||||
pub use lettre::smtp;
|
||||
use db_conn::DbPool;
|
||||
use instance::Instance;
|
||||
use once_cell::sync::Lazy;
|
||||
use plume_common::activity_pub::{inbox::InboxError, request, sign};
|
||||
use plume_common::activity_pub::{inbox::InboxError, sign};
|
||||
use posts::PostEvent;
|
||||
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
|
||||
use users::UserEvent;
|
||||
@ -68,9 +68,6 @@ pub enum Error {
|
||||
Url,
|
||||
Webfinger,
|
||||
Expired,
|
||||
UserAlreadyExists,
|
||||
#[cfg(feature = "s3")]
|
||||
S3(s3::error::S3Error),
|
||||
}
|
||||
|
||||
impl From<bcrypt::BcryptError> for Error {
|
||||
@ -97,14 +94,14 @@ impl From<diesel::result::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for Error {
|
||||
fn from(_: url::ParseError) -> Self {
|
||||
Error::Url
|
||||
impl From<std::option::NoneError> for Error {
|
||||
fn from(_: std::option::NoneError) -> Self {
|
||||
Error::NotFound
|
||||
}
|
||||
}
|
||||
|
||||
impl From<iri_string::validate::Error> for Error {
|
||||
fn from(_: iri_string::validate::Error) -> Self {
|
||||
impl From<url::ParseError> for Error {
|
||||
fn from(_: url::ParseError) -> Self {
|
||||
Error::Url
|
||||
}
|
||||
}
|
||||
@ -127,9 +124,12 @@ impl From<reqwest::header::InvalidHeaderValue> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<activitystreams::checked::CheckError> for Error {
|
||||
fn from(_: activitystreams::checked::CheckError) -> Error {
|
||||
Error::MissingApProperty
|
||||
impl From<activitypub::Error> for Error {
|
||||
fn from(err: activitypub::Error) -> Self {
|
||||
match err {
|
||||
activitypub::Error::NotFound => Error::MissingApProperty,
|
||||
_ => Error::SerDe,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,27 +166,20 @@ impl From<InboxError<Error>> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<request::Error> for Error {
|
||||
fn from(_err: request::Error) -> Error {
|
||||
Error::Request
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "s3")]
|
||||
impl From<s3::error::S3Error> for Error {
|
||||
fn from(err: s3::error::S3Error) -> Error {
|
||||
Error::S3(err)
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
impl From<Error> for sign::Error {
|
||||
fn from(_: Error) -> Self {
|
||||
Self()
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a function to a model, that returns the first
|
||||
/// matching row for a given list of fields.
|
||||
///
|
||||
/// Usage:
|
||||
///
|
||||
/// ```ignore
|
||||
/// ```rust
|
||||
/// impl Model {
|
||||
/// find_by!(model_table, name_of_the_function, field1 as String, field2 as i32);
|
||||
/// }
|
||||
@ -210,7 +203,7 @@ macro_rules! find_by {
|
||||
///
|
||||
/// Usage:
|
||||
///
|
||||
/// ```ignore
|
||||
/// ```rust
|
||||
/// impl Model {
|
||||
/// list_by!(model_table, name_of_the_function, field1 as String);
|
||||
/// }
|
||||
@ -234,7 +227,7 @@ macro_rules! list_by {
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// ```ignore
|
||||
/// ```rust
|
||||
/// impl Model {
|
||||
/// get!(model_table);
|
||||
/// }
|
||||
@ -257,7 +250,7 @@ macro_rules! get {
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// ```ignore
|
||||
/// ```rust
|
||||
/// impl Model {
|
||||
/// insert!(model_table, NewModelType);
|
||||
/// }
|
||||
@ -289,7 +282,7 @@ macro_rules! insert {
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// ```ignore
|
||||
/// ```rust
|
||||
/// impl Model {
|
||||
/// last!(model_table);
|
||||
/// }
|
||||
@ -316,38 +309,21 @@ pub fn ap_url(url: &str) -> String {
|
||||
format!("https://{}", url)
|
||||
}
|
||||
|
||||
pub trait SmtpNewWithAddr {
|
||||
fn new_with_addr(
|
||||
addr: (&str, u16),
|
||||
) -> std::result::Result<smtp::SmtpClient, smtp::error::Error>;
|
||||
pub fn migrate_data(dbpool: &DbPool) -> Result<()> {
|
||||
ensure_local_instance_keys(&dbpool.get().unwrap())
|
||||
}
|
||||
|
||||
impl SmtpNewWithAddr for smtp::SmtpClient {
|
||||
// Stolen from lettre::smtp::SmtpClient::new_simple()
|
||||
fn new_with_addr(addr: (&str, u16)) -> std::result::Result<Self, smtp::error::Error> {
|
||||
use native_tls::TlsConnector;
|
||||
use smtp::{
|
||||
client::net::{ClientTlsParameters, DEFAULT_TLS_PROTOCOLS},
|
||||
ClientSecurity, SmtpClient,
|
||||
};
|
||||
|
||||
let (domain, port) = addr;
|
||||
|
||||
let mut tls_builder = TlsConnector::builder();
|
||||
tls_builder.min_protocol_version(Some(DEFAULT_TLS_PROTOCOLS[0]));
|
||||
|
||||
let tls_parameters =
|
||||
ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap());
|
||||
|
||||
SmtpClient::new((domain, port), ClientSecurity::Wrapper(tls_parameters))
|
||||
fn ensure_local_instance_keys(conn: &Connection) -> Result<()> {
|
||||
for instance in Instance::get_locals(conn)? {
|
||||
instance.set_keypair(conn)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
mod tests {
|
||||
use crate::{db_conn, migrations::IMPORTED_MIGRATIONS, Connection as Conn, CONFIG};
|
||||
use chrono::{naive::NaiveDateTime, Datelike, Timelike};
|
||||
use diesel::r2d2::ConnectionManager;
|
||||
use plume_common::utils::random_hex;
|
||||
use std::env::temp_dir;
|
||||
@ -363,7 +339,7 @@ mod tests {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn db() -> db_conn::DbConn {
|
||||
pub fn db<'a>() -> db_conn::DbConn {
|
||||
db_conn::DbConn((*DB_POOL).get().unwrap())
|
||||
}
|
||||
|
||||
@ -380,33 +356,6 @@ mod tests {
|
||||
pool
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
pub(crate) fn format_datetime(dt: &NaiveDateTime) -> String {
|
||||
format!(
|
||||
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z",
|
||||
dt.year(),
|
||||
dt.month(),
|
||||
dt.day(),
|
||||
dt.hour(),
|
||||
dt.minute(),
|
||||
dt.second(),
|
||||
dt.timestamp_subsec_micros()
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub(crate) fn format_datetime(dt: &NaiveDateTime) -> String {
|
||||
format!(
|
||||
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
|
||||
dt.year(),
|
||||
dt.month(),
|
||||
dt.day(),
|
||||
dt.hour(),
|
||||
dt.minute(),
|
||||
dt.second()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod admin;
|
||||
@ -418,7 +367,6 @@ pub mod blogs;
|
||||
pub mod comment_seers;
|
||||
pub mod comments;
|
||||
pub mod db_conn;
|
||||
pub mod email_signups;
|
||||
pub mod follows;
|
||||
pub mod headers;
|
||||
pub mod inbox;
|
||||
@ -439,7 +387,6 @@ pub mod safe_string;
|
||||
#[allow(unused_imports)]
|
||||
pub mod schema;
|
||||
pub mod search;
|
||||
pub mod signups;
|
||||
pub mod tags;
|
||||
pub mod timeline;
|
||||
pub mod users;
|
||||
|
@ -1,19 +1,13 @@
|
||||
use crate::{
|
||||
instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*, users::User,
|
||||
Connection, Error, Result, CONFIG,
|
||||
};
|
||||
use activitystreams::{
|
||||
activity::{ActorAndObjectRef, Like as LikeAct, Undo},
|
||||
base::AnyBase,
|
||||
iri_string::types::IriString,
|
||||
prelude::*,
|
||||
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*,
|
||||
users::User, Connection, Error, Result, CONFIG,
|
||||
};
|
||||
use activitypub::activity;
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use plume_common::activity_pub::{
|
||||
inbox::{AsActor, AsObject, FromId},
|
||||
sign::Signer,
|
||||
PUBLIC_VISIBILITY,
|
||||
Id, IntoId, PUBLIC_VISIBILITY,
|
||||
};
|
||||
|
||||
#[derive(Clone, Queryable, Identifiable)]
|
||||
@ -39,16 +33,18 @@ impl Like {
|
||||
find_by!(likes, find_by_ap_url, ap_url as &str);
|
||||
find_by!(likes, find_by_user_on_post, user_id as i32, post_id as i32);
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<LikeAct> {
|
||||
let mut act = LikeAct::new(
|
||||
User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?,
|
||||
Post::get(conn, self.post_id)?.ap_url.parse::<IriString>()?,
|
||||
);
|
||||
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
|
||||
act.set_many_ccs(vec![User::get(conn, self.user_id)?
|
||||
.followers_endpoint
|
||||
.parse::<IriString>()?]);
|
||||
act.set_id(self.ap_url.parse::<IriString>()?);
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<activity::Like> {
|
||||
let mut act = activity::Like::default();
|
||||
act.like_props
|
||||
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
||||
act.like_props
|
||||
.set_object_link(Post::get(conn, self.post_id)?.into_id())?;
|
||||
act.object_props
|
||||
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
|
||||
act.object_props.set_cc_link_vec(vec![Id::new(
|
||||
User::get(conn, self.user_id)?.followers_endpoint,
|
||||
)])?;
|
||||
act.object_props.set_id_string(self.ap_url.clone())?;
|
||||
|
||||
Ok(act)
|
||||
}
|
||||
@ -70,26 +66,28 @@ impl Like {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn build_undo(&self, conn: &Connection) -> Result<Undo> {
|
||||
let mut act = Undo::new(
|
||||
User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?,
|
||||
AnyBase::from_extended(self.to_activity(conn)?)?,
|
||||
);
|
||||
act.set_id(format!("{}#delete", self.ap_url).parse::<IriString>()?);
|
||||
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
|
||||
act.set_many_ccs(vec![User::get(conn, self.user_id)?
|
||||
.followers_endpoint
|
||||
.parse::<IriString>()?]);
|
||||
pub fn build_undo(&self, conn: &Connection) -> Result<activity::Undo> {
|
||||
let mut act = activity::Undo::default();
|
||||
act.undo_props
|
||||
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
||||
act.undo_props.set_object_object(self.to_activity(conn)?)?;
|
||||
act.object_props
|
||||
.set_id_string(format!("{}#delete", self.ap_url))?;
|
||||
act.object_props
|
||||
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
|
||||
act.object_props.set_cc_link_vec(vec![Id::new(
|
||||
User::get(conn, self.user_id)?.followers_endpoint,
|
||||
)])?;
|
||||
|
||||
Ok(act)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, LikeAct, &Connection> for Post {
|
||||
impl AsObject<User, activity::Like, &DbConn> for Post {
|
||||
type Error = Error;
|
||||
type Output = Like;
|
||||
|
||||
fn activity(self, conn: &Connection, actor: User, id: &str) -> Result<Like> {
|
||||
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Like> {
|
||||
let res = Like::insert(
|
||||
conn,
|
||||
NewLike {
|
||||
@ -105,24 +103,22 @@ impl AsObject<User, LikeAct, &Connection> for Post {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromId<Connection> for Like {
|
||||
impl FromId<DbConn> for Like {
|
||||
type Error = Error;
|
||||
type Object = LikeAct;
|
||||
type Object = activity::Like;
|
||||
|
||||
fn from_db(conn: &Connection, id: &str) -> Result<Self> {
|
||||
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
||||
Like::find_by_ap_url(conn, id)
|
||||
}
|
||||
|
||||
fn from_activity(conn: &Connection, act: LikeAct) -> Result<Self> {
|
||||
fn from_activity(conn: &DbConn, act: activity::Like) -> Result<Self> {
|
||||
let res = Like::insert(
|
||||
conn,
|
||||
NewLike {
|
||||
post_id: Post::from_id(
|
||||
conn,
|
||||
act.object_field_ref()
|
||||
.as_single_id()
|
||||
.ok_or(Error::MissingApProperty)?
|
||||
.as_str(),
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&act.like_props.object_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
@ -130,41 +126,32 @@ impl FromId<Connection> for Like {
|
||||
.id,
|
||||
user_id: User::from_id(
|
||||
conn,
|
||||
act.actor_field_ref()
|
||||
.as_single_id()
|
||||
.ok_or(Error::MissingApProperty)?
|
||||
.as_str(),
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&act.like_props.actor_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
ap_url: act
|
||||
.id_unchecked()
|
||||
.ok_or(Error::MissingApProperty)?
|
||||
.to_string(),
|
||||
ap_url: act.object_props.id_string()?,
|
||||
},
|
||||
)?;
|
||||
res.notify(conn)?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn get_sender() -> &'static dyn Signer {
|
||||
Instance::get_local_instance_user().expect("Failed to local instance user")
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, Undo, &Connection> for Like {
|
||||
impl AsObject<User, activity::Undo, &DbConn> for Like {
|
||||
type Error = Error;
|
||||
type Output = ();
|
||||
|
||||
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> {
|
||||
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
||||
if actor.id == self.user_id {
|
||||
diesel::delete(&self).execute(conn)?;
|
||||
diesel::delete(&self).execute(&**conn)?;
|
||||
|
||||
// delete associated notification if any
|
||||
if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) {
|
||||
diesel::delete(¬if).execute(conn)?;
|
||||
if let Ok(notif) = Notification::find(&conn, notification_kind::LIKE, self.id) {
|
||||
diesel::delete(¬if).execute(&**conn)?;
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
@ -175,7 +162,8 @@ impl AsObject<User, Undo, &Connection> for Like {
|
||||
|
||||
impl NewLike {
|
||||
pub fn new(p: &Post, u: &User) -> Self {
|
||||
let ap_url = format!("{}like/{}", u.ap_url, p.ap_url);
|
||||
// TODO: this URL is not valid
|
||||
let ap_url = format!("{}/like/{}", u.ap_url, p.ap_url);
|
||||
NewLike {
|
||||
post_id: p.id,
|
||||
user_id: u.id,
|
||||
@ -183,67 +171,3 @@ impl NewLike {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::diesel::Connection;
|
||||
use crate::{inbox::tests::fill_database, tests::db};
|
||||
use assert_json_diff::assert_json_eq;
|
||||
use serde_json::{json, to_value};
|
||||
|
||||
#[test]
|
||||
fn to_activity() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, Error, _>(|| {
|
||||
let (posts, _users, _blogs) = fill_database(&conn);
|
||||
let post = &posts[0];
|
||||
let user = &post.get_authors(&conn)?[0];
|
||||
let like = Like::insert(&conn, NewLike::new(post, user))?;
|
||||
let act = like.to_activity(&conn).unwrap();
|
||||
|
||||
let expected = json!({
|
||||
"actor": "https://plu.me/@/admin/",
|
||||
"cc": ["https://plu.me/@/admin/followers"],
|
||||
"id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing",
|
||||
"object": "https://plu.me/~/BlogName/testing",
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Like",
|
||||
});
|
||||
assert_json_eq!(to_value(act)?, expected);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_undo() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, Error, _>(|| {
|
||||
let (posts, _users, _blogs) = fill_database(&conn);
|
||||
let post = &posts[0];
|
||||
let user = &post.get_authors(&conn)?[0];
|
||||
let like = Like::insert(&conn, NewLike::new(post, user))?;
|
||||
let act = like.build_undo(&conn)?;
|
||||
|
||||
let expected = json!({
|
||||
"actor": "https://plu.me/@/admin/",
|
||||
"cc": ["https://plu.me/@/admin/followers"],
|
||||
"id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing#delete",
|
||||
"object": {
|
||||
"actor": "https://plu.me/@/admin/",
|
||||
"cc": ["https://plu.me/@/admin/followers"],
|
||||
"id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing",
|
||||
"object": "https://plu.me/~/BlogName/testing",
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Like",
|
||||
},
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Undo",
|
||||
});
|
||||
assert_json_eq!(to_value(act)?, expected);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -143,7 +143,6 @@ macro_rules! func {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Queryable, Identifiable)]
|
||||
struct ListElem {
|
||||
pub id: i32,
|
||||
@ -297,28 +296,6 @@ impl List {
|
||||
.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_blogs, Blog, add_blogs}
|
||||
func! {set: set_words, Word, add_words}
|
||||
@ -435,7 +412,7 @@ mod tests {
|
||||
&List::find_for_user_by_name(conn, l1.user_id, &l1.name).unwrap(),
|
||||
);
|
||||
l_eq(
|
||||
&l1u,
|
||||
&&l1u,
|
||||
&List::find_for_user_by_name(conn, l1u.user_id, &l1u.name).unwrap(),
|
||||
);
|
||||
Ok(())
|
||||
|
@ -1,13 +1,14 @@
|
||||
use crate::{
|
||||
ap_url, instance::Instance, safe_string::SafeString, schema::medias, users::User, Connection,
|
||||
Error, Result, CONFIG,
|
||||
ap_url, db_conn::DbConn, instance::Instance, safe_string::SafeString, schema::medias,
|
||||
users::User, Connection, Error, Result, CONFIG,
|
||||
};
|
||||
use activitystreams::{object::Image, prelude::*};
|
||||
use activitypub::object::Image;
|
||||
use askama_escape::escape;
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use guid_create::GUID;
|
||||
use plume_common::{
|
||||
activity_pub::{inbox::FromId, request, ToAsString, ToAsUri},
|
||||
utils::{escape, MediaProcessor},
|
||||
activity_pub::{inbox::FromId, Id},
|
||||
utils::MediaProcessor,
|
||||
};
|
||||
use std::{
|
||||
fs::{self, DirBuilder},
|
||||
@ -16,9 +17,6 @@ use std::{
|
||||
use tracing::warn;
|
||||
use url::Url;
|
||||
|
||||
#[cfg(feature = "s3")]
|
||||
use crate::config::S3Config;
|
||||
|
||||
const REMOTE_MEDIA_DIRECTORY: &str = "remote";
|
||||
|
||||
#[derive(Clone, Identifiable, Queryable, AsChangeset)]
|
||||
@ -45,7 +43,7 @@ pub struct NewMedia {
|
||||
pub owner_id: i32,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
#[derive(PartialEq)]
|
||||
pub enum MediaCategory {
|
||||
Image,
|
||||
Audio,
|
||||
@ -106,9 +104,9 @@ impl Media {
|
||||
pub fn category(&self) -> MediaCategory {
|
||||
match &*self
|
||||
.file_path
|
||||
.rsplit_once('.')
|
||||
.map(|x| x.1)
|
||||
.unwrap_or("")
|
||||
.rsplitn(2, '.')
|
||||
.next()
|
||||
.expect("Media::category: extension error")
|
||||
.to_lowercase()
|
||||
{
|
||||
"png" | "jpg" | "jpeg" | "gif" | "svg" => MediaCategory::Image,
|
||||
@ -154,99 +152,26 @@ 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> {
|
||||
if self.is_remote {
|
||||
Ok(self.remote_url.clone().unwrap_or_default())
|
||||
} else {
|
||||
let relative_url = self.relative_url().unwrap_or_default();
|
||||
|
||||
#[cfg(feature="s3")]
|
||||
if CONFIG.s3.as_ref().map(|x| x.direct_download).unwrap_or(false) {
|
||||
let s3_url = match CONFIG.s3.as_ref().unwrap() {
|
||||
S3Config { alias: Some(alias), .. } => {
|
||||
format!("https://{}/{}", alias, relative_url)
|
||||
}
|
||||
S3Config { path_style: true, hostname, bucket, .. } => {
|
||||
format!("https://{}/{}/{}",
|
||||
hostname,
|
||||
bucket,
|
||||
relative_url
|
||||
)
|
||||
}
|
||||
S3Config { path_style: false, hostname, bucket, .. } => {
|
||||
format!("https://{}.{}/{}",
|
||||
bucket,
|
||||
hostname,
|
||||
relative_url
|
||||
)
|
||||
}
|
||||
};
|
||||
return Ok(s3_url);
|
||||
}
|
||||
|
||||
let file_path = self.file_path.replace(path::MAIN_SEPARATOR, "/").replacen(
|
||||
&CONFIG.media_directory,
|
||||
"static/media",
|
||||
1,
|
||||
); // "static/media" from plume::routs::plume_media_files()
|
||||
Ok(ap_url(&format!(
|
||||
"{}/{}",
|
||||
Instance::get_local()?.public_domain,
|
||||
relative_url
|
||||
&file_path
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(&self, conn: &Connection) -> Result<()> {
|
||||
if !self.is_remote {
|
||||
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)?)?;
|
||||
}
|
||||
fs::remove_file(self.file_path.as_str())?;
|
||||
}
|
||||
diesel::delete(self)
|
||||
.execute(conn)
|
||||
@ -282,75 +207,34 @@ impl Media {
|
||||
}
|
||||
|
||||
// TODO: merge with save_remote?
|
||||
pub fn from_activity(conn: &Connection, image: &Image) -> Result<Media> {
|
||||
let remote_url = image
|
||||
.url()
|
||||
.and_then(|url| url.to_as_uri())
|
||||
.ok_or(Error::MissingApProperty)?;
|
||||
pub fn from_activity(conn: &DbConn, image: &Image) -> Result<Media> {
|
||||
let remote_url = image.object_props.url_string().ok()?;
|
||||
let path = determine_mirror_file_path(&remote_url);
|
||||
let parent = path.parent()?;
|
||||
if !parent.is_dir() {
|
||||
DirBuilder::new().recursive(true).create(parent)?;
|
||||
}
|
||||
|
||||
let file_path = if CONFIG.s3.is_some() {
|
||||
#[cfg(not(feature="s3"))]
|
||||
unreachable!();
|
||||
|
||||
#[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
|
||||
}
|
||||
let mut dest = fs::File::create(path.clone()).ok()?;
|
||||
// TODO: conditional GET
|
||||
if let Some(proxy) = CONFIG.proxy() {
|
||||
reqwest::ClientBuilder::new().proxy(proxy.clone()).build()?
|
||||
} 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)?;
|
||||
}
|
||||
reqwest::Client::new()
|
||||
}
|
||||
.get(remote_url.as_str())
|
||||
.send()
|
||||
.ok()?
|
||||
.copy_to(&mut dest)
|
||||
.ok()?;
|
||||
|
||||
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)
|
||||
Media::find_by_file_path(conn, &path.to_str()?)
|
||||
.and_then(|mut media| {
|
||||
let mut updated = false;
|
||||
|
||||
let alt_text = image
|
||||
.content()
|
||||
.and_then(|content| content.to_as_string())
|
||||
.ok_or(Error::NotFound)?;
|
||||
let summary = image.summary().and_then(|summary| summary.to_as_string());
|
||||
let sensitive = summary.is_some();
|
||||
let content_warning = summary;
|
||||
let alt_text = image.object_props.content_string().ok()?;
|
||||
let sensitive = image.object_props.summary_string().is_ok();
|
||||
let content_warning = image.object_props.summary_string().ok();
|
||||
if media.alt_text != alt_text {
|
||||
media.alt_text = alt_text;
|
||||
updated = true;
|
||||
@ -372,30 +256,30 @@ impl Media {
|
||||
updated = true;
|
||||
}
|
||||
if updated {
|
||||
diesel::update(&media).set(&media).execute(conn)?;
|
||||
diesel::update(&media).set(&media).execute(&**conn)?;
|
||||
}
|
||||
Ok(media)
|
||||
})
|
||||
.or_else(|_| {
|
||||
let summary = image.summary().and_then(|summary| summary.to_as_string());
|
||||
Media::insert(
|
||||
conn,
|
||||
NewMedia {
|
||||
file_path,
|
||||
alt_text: image
|
||||
.content()
|
||||
.and_then(|content| content.to_as_string())
|
||||
.ok_or(Error::NotFound)?,
|
||||
file_path: path.to_str()?.to_string(),
|
||||
alt_text: image.object_props.content_string().ok()?,
|
||||
is_remote: false,
|
||||
remote_url: None,
|
||||
sensitive: summary.is_some(),
|
||||
content_warning: summary,
|
||||
sensitive: image.object_props.summary_string().is_ok(),
|
||||
content_warning: image.object_props.summary_string().ok(),
|
||||
owner_id: User::from_id(
|
||||
conn,
|
||||
&image
|
||||
.attributed_to()
|
||||
.and_then(|attributed_to| attributed_to.to_as_uri())
|
||||
.ok_or(Error::MissingApProperty)?,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
image
|
||||
.object_props
|
||||
.attributed_to_link_vec::<Id>()
|
||||
.ok()?
|
||||
.into_iter()
|
||||
.next()?
|
||||
.as_ref(),
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
@ -421,10 +305,12 @@ impl Media {
|
||||
}
|
||||
|
||||
fn determine_mirror_file_path(url: &str) -> PathBuf {
|
||||
let mut file_path = Path::new(&CONFIG.media_directory).join(REMOTE_MEDIA_DIRECTORY);
|
||||
|
||||
match Url::parse(url) {
|
||||
Ok(url) if url.has_host() => {
|
||||
let mut file_path = Path::new(&super::CONFIG.media_directory).join(REMOTE_MEDIA_DIRECTORY);
|
||||
Url::parse(url)
|
||||
.map(|url| {
|
||||
if !url.has_host() {
|
||||
return;
|
||||
}
|
||||
file_path.push(url.host_str().unwrap());
|
||||
for segment in url.path_segments().expect("FIXME") {
|
||||
file_path.push(segment);
|
||||
@ -432,54 +318,19 @@ fn determine_mirror_file_path(url: &str) -> PathBuf {
|
||||
// TODO: handle query
|
||||
// HINT: Use characters which must be percent-encoded in path as separator between path and query
|
||||
// HINT: handle extension
|
||||
}
|
||||
other => {
|
||||
if let Err(err) = other {
|
||||
warn!("Failed to parse url: {} {}", &url, err);
|
||||
} else {
|
||||
warn!("Error without a host: {}", &url);
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|err| {
|
||||
warn!("Failed to parse url: {} {}", &url, err);
|
||||
let ext = url
|
||||
.rsplit('.')
|
||||
.next()
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_else(|| String::from("png"));
|
||||
file_path.push(format!("{}.{}", GUID::rand(), ext));
|
||||
}
|
||||
}
|
||||
file_path.push(format!("{}.{}", GUID::rand().to_string(), ext));
|
||||
});
|
||||
file_path
|
||||
}
|
||||
|
||||
#[cfg(feature="s3")]
|
||||
fn determine_mirror_s3_path(url: &str) -> String {
|
||||
match Url::parse(url) {
|
||||
Ok(url) if url.has_host() => {
|
||||
format!("static/media/{}/{}/{}",
|
||||
REMOTE_MEDIA_DIRECTORY,
|
||||
url.host_str().unwrap(),
|
||||
url.path().trim_start_matches('/'),
|
||||
)
|
||||
}
|
||||
other => {
|
||||
if let Err(err) = other {
|
||||
warn!("Failed to parse url: {} {}", &url, err);
|
||||
} else {
|
||||
warn!("Error without a host: {}", &url);
|
||||
}
|
||||
let ext = url
|
||||
.rsplit('.')
|
||||
.next()
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_else(|| String::from("png"));
|
||||
format!("static/media/{}/{}.{}",
|
||||
REMOTE_MEDIA_DIRECTORY,
|
||||
GUID::rand(),
|
||||
ext,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
@ -490,7 +341,7 @@ pub(crate) mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Media>) {
|
||||
let mut wd = current_dir().unwrap();
|
||||
let mut wd = current_dir().unwrap().to_path_buf();
|
||||
while wd.pop() {
|
||||
if wd.join(".git").exists() {
|
||||
set_current_dir(wd).unwrap();
|
||||
@ -545,15 +396,7 @@ pub(crate) mod tests {
|
||||
pub(crate) fn clean(conn: &Conn) {
|
||||
//used to remove files generated by tests
|
||||
for media in Media::list_all_medias(conn).unwrap() {
|
||||
if let Some(err) = media.delete(conn).err() {
|
||||
match &err {
|
||||
Error::Io(e) => match e.kind() {
|
||||
std::io::ErrorKind::NotFound => (),
|
||||
_ => panic!("{:?}", err),
|
||||
},
|
||||
_ => panic!("{:?}", err),
|
||||
}
|
||||
}
|
||||
media.delete(conn).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@ -603,7 +446,7 @@ pub(crate) mod tests {
|
||||
let media = Media::insert(
|
||||
conn,
|
||||
NewMedia {
|
||||
file_path: path,
|
||||
file_path: path.clone(),
|
||||
alt_text: "alt message".to_owned(),
|
||||
is_remote: false,
|
||||
remote_url: None,
|
||||
|
@ -1,12 +1,8 @@
|
||||
use crate::{
|
||||
comments::Comment, notifications::*, posts::Post, schema::mentions, users::User, Connection,
|
||||
Error, Result,
|
||||
};
|
||||
use activitystreams::{
|
||||
base::BaseExt,
|
||||
iri_string::types::IriString,
|
||||
link::{self, LinkExt},
|
||||
comments::Comment, db_conn::DbConn, notifications::*, posts::Post, schema::mentions,
|
||||
users::User, Connection, Error, Result,
|
||||
};
|
||||
use activitypub::link;
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use plume_common::activity_pub::inbox::AsActor;
|
||||
|
||||
@ -51,28 +47,26 @@ impl Mention {
|
||||
|
||||
pub fn get_user(&self, conn: &Connection) -> Result<User> {
|
||||
match self.get_post(conn) {
|
||||
Ok(p) => Ok(p
|
||||
.get_authors(conn)?
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(Error::NotFound)?),
|
||||
Ok(p) => Ok(p.get_authors(conn)?.into_iter().next()?),
|
||||
Err(_) => self.get_comment(conn).and_then(|c| c.get_author(conn)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_activity(conn: &Connection, ment: &str) -> Result<link::Mention> {
|
||||
pub fn build_activity(conn: &DbConn, ment: &str) -> Result<link::Mention> {
|
||||
let user = User::find_by_fqn(conn, ment)?;
|
||||
let mut mention = link::Mention::new();
|
||||
mention.set_href(user.ap_url.parse::<IriString>()?);
|
||||
mention.set_name(format!("@{}", ment));
|
||||
let mut mention = link::Mention::default();
|
||||
mention.link_props.set_href_string(user.ap_url)?;
|
||||
mention.link_props.set_name_string(format!("@{}", ment))?;
|
||||
Ok(mention)
|
||||
}
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<link::Mention> {
|
||||
let user = self.get_mentioned(conn)?;
|
||||
let mut mention = link::Mention::new();
|
||||
mention.set_href(user.ap_url.parse::<IriString>()?);
|
||||
mention.set_name(format!("@{}", user.fqn));
|
||||
let mut mention = link::Mention::default();
|
||||
mention.link_props.set_href_string(user.ap_url.clone())?;
|
||||
mention
|
||||
.link_props
|
||||
.set_name_string(format!("@{}", user.fqn))?;
|
||||
Ok(mention)
|
||||
}
|
||||
|
||||
@ -83,8 +77,8 @@ impl Mention {
|
||||
in_post: bool,
|
||||
notify: bool,
|
||||
) -> Result<Self> {
|
||||
let ap_url = ment.href().ok_or(Error::NotFound)?.as_str();
|
||||
let mentioned = User::find_by_ap_url(conn, ap_url)?;
|
||||
let ap_url = ment.link_props.href_string().ok()?;
|
||||
let mentioned = User::find_by_ap_url(conn, &ap_url)?;
|
||||
|
||||
if in_post {
|
||||
Post::get(conn, inside).and_then(|post| {
|
||||
@ -147,62 +141,3 @@ impl Mention {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{inbox::tests::fill_database, tests::db, Error};
|
||||
use assert_json_diff::assert_json_eq;
|
||||
use diesel::Connection;
|
||||
use serde_json::{json, to_value};
|
||||
|
||||
#[test]
|
||||
fn build_activity() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, Error, _>(|| {
|
||||
let (_posts, users, _blogs) = fill_database(&conn);
|
||||
let user = &users[0];
|
||||
let name = &user.username;
|
||||
let act = Mention::build_activity(&conn, name)?;
|
||||
|
||||
let expected = json!({
|
||||
"href": "https://plu.me/@/admin/",
|
||||
"name": "@admin",
|
||||
"type": "Mention",
|
||||
});
|
||||
|
||||
assert_json_eq!(to_value(act)?, expected);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_activity() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, Error, _>(|| {
|
||||
let (posts, users, _blogs) = fill_database(&conn);
|
||||
let post = &posts[0];
|
||||
let user = &users[0];
|
||||
let mention = Mention::insert(
|
||||
&conn,
|
||||
NewMention {
|
||||
mentioned_id: user.id,
|
||||
post_id: Some(post.id),
|
||||
comment_id: None,
|
||||
},
|
||||
)?;
|
||||
let act = mention.to_activity(&conn)?;
|
||||
|
||||
let expected = json!({
|
||||
"href": "https://plu.me/@/admin/",
|
||||
"name": "@admin",
|
||||
"type": "Mention",
|
||||
});
|
||||
|
||||
assert_json_eq!(to_value(act)?, expected);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -105,8 +105,7 @@ impl ImportedMigrations {
|
||||
pub fn rerun_last_migration(&self, conn: &Connection, path: &Path) -> Result<()> {
|
||||
let latest_migration = conn.latest_run_migration_version()?;
|
||||
let id = latest_migration
|
||||
.and_then(|m| self.0.binary_search_by_key(&m.as_str(), |m| m.name).ok())
|
||||
.ok_or(Error::NotFound)?;
|
||||
.and_then(|m| self.0.binary_search_by_key(&m.as_str(), |m| m.name).ok())?;
|
||||
let migration = &self.0[id];
|
||||
conn.transaction(|| {
|
||||
migration.revert(conn, path)?;
|
||||
|
@ -61,7 +61,7 @@ impl PasswordResetRequest {
|
||||
}
|
||||
|
||||
pub fn find_and_delete_by_token(conn: &Connection, token: &str) -> Result<Self> {
|
||||
let request = Self::find_by_token(conn, token)?;
|
||||
let request = Self::find_by_token(&conn, &token)?;
|
||||
|
||||
let filter =
|
||||
password_reset_requests::table.filter(password_reset_requests::id.eq(request.id));
|
||||
@ -89,7 +89,7 @@ mod tests {
|
||||
let request = PasswordResetRequest::find_by_token(&conn, &token)
|
||||
.expect("couldn't retrieve request");
|
||||
|
||||
assert!(token.len() > 32);
|
||||
assert!(&token.len() > &32);
|
||||
assert_eq!(&request.email, &admin_email);
|
||||
|
||||
Ok(())
|
||||
@ -103,8 +103,8 @@ mod tests {
|
||||
user_tests::fill_database(&conn);
|
||||
let admin_email = "admin@example.com";
|
||||
|
||||
PasswordResetRequest::insert(&conn, admin_email).expect("couldn't insert new request");
|
||||
PasswordResetRequest::insert(&conn, admin_email)
|
||||
PasswordResetRequest::insert(&conn, &admin_email).expect("couldn't insert new request");
|
||||
PasswordResetRequest::insert(&conn, &admin_email)
|
||||
.expect("couldn't insert second request");
|
||||
|
||||
let count = password_reset_requests::table.count().get_result(&*conn);
|
||||
@ -132,7 +132,7 @@ mod tests {
|
||||
.execute(&*conn)
|
||||
.expect("could not insert request");
|
||||
|
||||
match PasswordResetRequest::find_by_token(&conn, token) {
|
||||
match PasswordResetRequest::find_by_token(&conn, &token) {
|
||||
Err(Error::Expired) => (),
|
||||
_ => panic!("Received unexpected result finding expired token"),
|
||||
}
|
||||
@ -148,7 +148,7 @@ mod tests {
|
||||
user_tests::fill_database(&conn);
|
||||
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");
|
||||
PasswordResetRequest::find_and_delete_by_token(&conn, &token)
|
||||
.expect("couldn't find and delete request");
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,16 +1,13 @@
|
||||
use crate::{
|
||||
db_conn::{DbConn, DbPool},
|
||||
follows,
|
||||
posts::Post,
|
||||
instance::Instance,
|
||||
posts::{LicensedArticle, Post},
|
||||
users::{User, UserEvent},
|
||||
ACTOR_SYS, CONFIG, USER_CHAN,
|
||||
};
|
||||
use activitystreams::{
|
||||
activity::{ActorAndObjectRef, Create},
|
||||
base::AnyBase,
|
||||
object::kind::ArticleType,
|
||||
};
|
||||
use plume_common::activity_pub::{inbox::FromId, LicensedArticle};
|
||||
use activitypub::activity::Create;
|
||||
use plume_common::activity_pub::inbox::FromId;
|
||||
use riker::actors::{Actor, ActorFactoryArgs, ActorRefFactory, Context, Sender, Subscribe, Tell};
|
||||
use std::sync::Arc;
|
||||
use tracing::{error, info, warn};
|
||||
@ -45,12 +42,6 @@ impl Actor for RemoteFetchActor {
|
||||
RemoteUserFound(user) => match self.conn.get() {
|
||||
Ok(conn) => {
|
||||
let conn = DbConn(conn);
|
||||
if user
|
||||
.get_instance(&conn)
|
||||
.map_or(false, |instance| instance.blocked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Don't call these functions in parallel
|
||||
// for the case database connections limit is too small
|
||||
fetch_and_cache_articles(&user, &conn);
|
||||
@ -78,17 +69,13 @@ fn fetch_and_cache_articles(user: &Arc<User>, conn: &DbConn) {
|
||||
match create_acts {
|
||||
Ok(create_acts) => {
|
||||
for create_act in create_acts {
|
||||
match create_act.object_field_ref().as_single_base().map(|base| {
|
||||
let any_base = AnyBase::from_base(base.clone()); // FIXME: Don't clone()
|
||||
any_base.extend::<LicensedArticle, ArticleType>()
|
||||
}) {
|
||||
Some(Ok(Some(article))) => {
|
||||
match create_act.create_props.object_object::<LicensedArticle>() {
|
||||
Ok(article) => {
|
||||
Post::from_activity(conn, article)
|
||||
.expect("Article from remote user couldn't be saved");
|
||||
info!("Fetched article from remote user");
|
||||
}
|
||||
Some(Err(e)) => warn!("Error while fetching articles in background: {:?}", e),
|
||||
_ => warn!("Error while fetching articles in background"),
|
||||
Err(e) => warn!("Error while fetching articles in background: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -103,7 +90,13 @@ fn fetch_and_cache_followers(user: &Arc<User>, conn: &DbConn) {
|
||||
match follower_ids {
|
||||
Ok(user_ids) => {
|
||||
for user_id in user_ids {
|
||||
let follower = User::from_id(conn, &user_id, None, CONFIG.proxy());
|
||||
let follower = User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&user_id,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
);
|
||||
match follower {
|
||||
Ok(follower) => {
|
||||
let inserted = follows::Follow::insert(
|
||||
|
@ -1,19 +1,13 @@
|
||||
use crate::{
|
||||
instance::Instance, notifications::*, posts::Post, schema::reshares, timeline::*, users::User,
|
||||
Connection, Error, Result, CONFIG,
|
||||
};
|
||||
use activitystreams::{
|
||||
activity::{ActorAndObjectRef, Announce, Undo},
|
||||
base::AnyBase,
|
||||
iri_string::types::IriString,
|
||||
prelude::*,
|
||||
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::reshares,
|
||||
timeline::*, users::User, Connection, Error, Result, CONFIG,
|
||||
};
|
||||
use activitypub::activity::{Announce, Undo};
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use plume_common::activity_pub::{
|
||||
inbox::{AsActor, AsObject, FromId},
|
||||
sign::Signer,
|
||||
PUBLIC_VISIBILITY,
|
||||
Id, IntoId, PUBLIC_VISIBILITY,
|
||||
};
|
||||
|
||||
#[derive(Clone, Queryable, Identifiable)]
|
||||
@ -66,16 +60,16 @@ impl Reshare {
|
||||
}
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<Announce> {
|
||||
let mut act = Announce::new(
|
||||
User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?,
|
||||
Post::get(conn, self.post_id)?.ap_url.parse::<IriString>()?,
|
||||
);
|
||||
act.set_id(self.ap_url.parse::<IriString>()?);
|
||||
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
|
||||
act.set_many_ccs(vec![self
|
||||
.get_user(conn)?
|
||||
.followers_endpoint
|
||||
.parse::<IriString>()?]);
|
||||
let mut act = Announce::default();
|
||||
act.announce_props
|
||||
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
||||
act.announce_props
|
||||
.set_object_link(Post::get(conn, self.post_id)?.into_id())?;
|
||||
act.object_props.set_id_string(self.ap_url.clone())?;
|
||||
act.object_props
|
||||
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
|
||||
act.object_props
|
||||
.set_cc_link_vec(vec![Id::new(self.get_user(conn)?.followers_endpoint)])?;
|
||||
|
||||
Ok(act)
|
||||
}
|
||||
@ -98,26 +92,26 @@ impl Reshare {
|
||||
}
|
||||
|
||||
pub fn build_undo(&self, conn: &Connection) -> Result<Undo> {
|
||||
let mut act = Undo::new(
|
||||
User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?,
|
||||
AnyBase::from_extended(self.to_activity(conn)?)?,
|
||||
);
|
||||
act.set_id(format!("{}#delete", self.ap_url).parse::<IriString>()?);
|
||||
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
|
||||
act.set_many_ccs(vec![self
|
||||
.get_user(conn)?
|
||||
.followers_endpoint
|
||||
.parse::<IriString>()?]);
|
||||
let mut act = Undo::default();
|
||||
act.undo_props
|
||||
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
||||
act.undo_props.set_object_object(self.to_activity(conn)?)?;
|
||||
act.object_props
|
||||
.set_id_string(format!("{}#delete", self.ap_url))?;
|
||||
act.object_props
|
||||
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
|
||||
act.object_props
|
||||
.set_cc_link_vec(vec![Id::new(self.get_user(conn)?.followers_endpoint)])?;
|
||||
|
||||
Ok(act)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, Announce, &Connection> for Post {
|
||||
impl AsObject<User, Announce, &DbConn> for Post {
|
||||
type Error = Error;
|
||||
type Output = Reshare;
|
||||
|
||||
fn activity(self, conn: &Connection, actor: User, id: &str) -> Result<Reshare> {
|
||||
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Reshare> {
|
||||
let conn = conn;
|
||||
let reshare = Reshare::insert(
|
||||
conn,
|
||||
@ -134,24 +128,22 @@ impl AsObject<User, Announce, &Connection> for Post {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromId<Connection> for Reshare {
|
||||
impl FromId<DbConn> for Reshare {
|
||||
type Error = Error;
|
||||
type Object = Announce;
|
||||
|
||||
fn from_db(conn: &Connection, id: &str) -> Result<Self> {
|
||||
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
||||
Reshare::find_by_ap_url(conn, id)
|
||||
}
|
||||
|
||||
fn from_activity(conn: &Connection, act: Announce) -> Result<Self> {
|
||||
fn from_activity(conn: &DbConn, act: Announce) -> Result<Self> {
|
||||
let res = Reshare::insert(
|
||||
conn,
|
||||
NewReshare {
|
||||
post_id: Post::from_id(
|
||||
conn,
|
||||
act.object_field_ref()
|
||||
.as_single_id()
|
||||
.ok_or(Error::MissingApProperty)?
|
||||
.as_str(),
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&act.announce_props.object_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
@ -159,41 +151,32 @@ impl FromId<Connection> for Reshare {
|
||||
.id,
|
||||
user_id: User::from_id(
|
||||
conn,
|
||||
act.actor_field_ref()
|
||||
.as_single_id()
|
||||
.ok_or(Error::MissingApProperty)?
|
||||
.as_str(),
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&act.announce_props.actor_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
ap_url: act
|
||||
.id_unchecked()
|
||||
.ok_or(Error::MissingApProperty)?
|
||||
.to_string(),
|
||||
ap_url: act.object_props.id_string()?,
|
||||
},
|
||||
)?;
|
||||
res.notify(conn)?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn get_sender() -> &'static dyn Signer {
|
||||
Instance::get_local_instance_user().expect("Failed to local instance user")
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, Undo, &Connection> for Reshare {
|
||||
impl AsObject<User, Undo, &DbConn> for Reshare {
|
||||
type Error = Error;
|
||||
type Output = ();
|
||||
|
||||
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> {
|
||||
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
||||
if actor.id == self.user_id {
|
||||
diesel::delete(&self).execute(conn)?;
|
||||
diesel::delete(&self).execute(&**conn)?;
|
||||
|
||||
// delete associated notification if any
|
||||
if let Ok(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) {
|
||||
diesel::delete(¬if).execute(conn)?;
|
||||
if let Ok(notif) = Notification::find(&conn, notification_kind::RESHARE, self.id) {
|
||||
diesel::delete(¬if).execute(&**conn)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -205,7 +188,7 @@ impl AsObject<User, Undo, &Connection> for Reshare {
|
||||
|
||||
impl NewReshare {
|
||||
pub fn new(p: &Post, u: &User) -> Self {
|
||||
let ap_url = format!("{}reshare/{}", u.ap_url, p.ap_url);
|
||||
let ap_url = format!("{}/reshare/{}", u.ap_url, p.ap_url);
|
||||
NewReshare {
|
||||
post_id: p.id,
|
||||
user_id: u.id,
|
||||
@ -213,67 +196,3 @@ impl NewReshare {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::diesel::Connection;
|
||||
use crate::{inbox::tests::fill_database, tests::db};
|
||||
use assert_json_diff::assert_json_eq;
|
||||
use serde_json::{json, to_value};
|
||||
|
||||
#[test]
|
||||
fn to_activity() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, Error, _>(|| {
|
||||
let (posts, _users, _blogs) = fill_database(&conn);
|
||||
let post = &posts[0];
|
||||
let user = &post.get_authors(&conn)?[0];
|
||||
let reshare = Reshare::insert(&conn, NewReshare::new(post, user))?;
|
||||
let act = reshare.to_activity(&conn).unwrap();
|
||||
|
||||
let expected = json!({
|
||||
"actor": "https://plu.me/@/admin/",
|
||||
"cc": ["https://plu.me/@/admin/followers"],
|
||||
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing",
|
||||
"object": "https://plu.me/~/BlogName/testing",
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Announce",
|
||||
});
|
||||
assert_json_eq!(to_value(act)?, expected);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_undo() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, Error, _>(|| {
|
||||
let (posts, _users, _blogs) = fill_database(&conn);
|
||||
let post = &posts[0];
|
||||
let user = &post.get_authors(&conn)?[0];
|
||||
let reshare = Reshare::insert(&conn, NewReshare::new(post, user))?;
|
||||
let act = reshare.build_undo(&conn)?;
|
||||
|
||||
let expected = json!({
|
||||
"actor": "https://plu.me/@/admin/",
|
||||
"cc": ["https://plu.me/@/admin/followers"],
|
||||
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing#delete",
|
||||
"object": {
|
||||
"actor": "https://plu.me/@/admin/",
|
||||
"cc": ["https://plu.me/@/admin/followers"],
|
||||
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing",
|
||||
"object": "https://plu.me/~/BlogName/testing",
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Announce"
|
||||
},
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Undo",
|
||||
});
|
||||
assert_json_eq!(to_value(act)?, expected);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ fn url_add_prefix(url: &str) -> Option<Cow<'_, str>> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, AsExpression, FromSqlRow, Default)]
|
||||
#[derive(Debug, Clone, PartialEq, AsExpression, FromSqlRow, Default)]
|
||||
#[sql_type = "Text"]
|
||||
pub struct SafeString {
|
||||
value: String,
|
||||
@ -102,7 +102,7 @@ pub struct SafeString {
|
||||
impl SafeString {
|
||||
pub fn new(value: &str) -> Self {
|
||||
SafeString {
|
||||
value: CLEAN.clean(value).to_string(),
|
||||
value: CLEAN.clean(&value).to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,26 +73,16 @@ table! {
|
||||
user_id -> Int4,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
email_blocklist (id) {
|
||||
email_blocklist(id){
|
||||
id -> Int4,
|
||||
email_address -> Text,
|
||||
email_address -> VarChar,
|
||||
note -> Text,
|
||||
notify_user -> Bool,
|
||||
notification_text -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
email_signups (id) {
|
||||
id -> Int4,
|
||||
email -> Varchar,
|
||||
token -> Varchar,
|
||||
expiration_date -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
follows (id) {
|
||||
id -> Int4,
|
||||
@ -116,6 +106,8 @@ table! {
|
||||
default_license -> Text,
|
||||
long_description_html -> Varchar,
|
||||
short_description_html -> Varchar,
|
||||
private_key -> Nullable<Text>,
|
||||
public_key -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
@ -316,8 +308,6 @@ allow_tables_to_appear_in_same_query!(
|
||||
blogs,
|
||||
comments,
|
||||
comment_seers,
|
||||
email_blocklist,
|
||||
email_signups,
|
||||
follows,
|
||||
instances,
|
||||
likes,
|
||||
|
@ -108,7 +108,7 @@ mod tests {
|
||||
|
||||
let searcher = Arc::new(get_searcher(&CONFIG.search_tokenizers));
|
||||
SearchActor::init(searcher.clone(), db_pool.clone());
|
||||
let conn = db_pool.get().unwrap();
|
||||
let conn = db_pool.clone().get().unwrap();
|
||||
|
||||
let title = random_hex()[..8].to_owned();
|
||||
let (_instance, _user, blog) = fill_database(&conn);
|
||||
@ -161,43 +161,43 @@ mod tests {
|
||||
long_description_html: "<p>Good morning</p>".to_string(),
|
||||
short_description: SafeString::new("Hello"),
|
||||
short_description_html: "<p>Hello</p>".to_string(),
|
||||
name: random_hex(),
|
||||
name: random_hex().to_string(),
|
||||
open_registrations: true,
|
||||
public_domain: random_hex(),
|
||||
public_domain: random_hex().to_string(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let user = User::insert(
|
||||
conn,
|
||||
NewUser {
|
||||
username: random_hex(),
|
||||
display_name: random_hex(),
|
||||
outbox_url: random_hex(),
|
||||
inbox_url: random_hex(),
|
||||
username: random_hex().to_string(),
|
||||
display_name: random_hex().to_string(),
|
||||
outbox_url: random_hex().to_string(),
|
||||
inbox_url: random_hex().to_string(),
|
||||
summary: "".to_string(),
|
||||
email: None,
|
||||
hashed_password: None,
|
||||
instance_id: instance.id,
|
||||
ap_url: random_hex(),
|
||||
ap_url: random_hex().to_string(),
|
||||
private_key: None,
|
||||
public_key: "".to_string(),
|
||||
shared_inbox_url: None,
|
||||
followers_endpoint: random_hex(),
|
||||
followers_endpoint: random_hex().to_string(),
|
||||
avatar_id: None,
|
||||
summary_html: SafeString::new(""),
|
||||
role: 0,
|
||||
fqn: random_hex(),
|
||||
fqn: random_hex().to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let blog = NewBlog {
|
||||
instance_id: instance.id,
|
||||
actor_id: random_hex(),
|
||||
ap_url: random_hex(),
|
||||
inbox_url: random_hex(),
|
||||
outbox_url: random_hex(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut blog = NewBlog::default();
|
||||
blog.instance_id = instance.id;
|
||||
blog.actor_id = random_hex().to_string();
|
||||
blog.ap_url = random_hex().to_string();
|
||||
blog.inbox_url = random_hex().to_string();
|
||||
blog.outbox_url = random_hex().to_string();
|
||||
let blog = Blog::insert(conn, blog).unwrap();
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
|
@ -154,7 +154,7 @@ pub(crate) mod tests {
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
searcher.add_document(conn, &post).unwrap();
|
||||
searcher.add_document(&conn, &post).unwrap();
|
||||
searcher.commit();
|
||||
assert_eq!(
|
||||
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 {
|
||||
$result.push_str(&format!("{}:{} ", stringify!($date), NaiveDate::from_num_days_from_ce_opt(*val as i32).unwrap().format("%Y-%m-%d")));
|
||||
$result.push_str(&format!("{}:{} ", stringify!($date), NaiveDate::from_num_days_from_ce(*val as i32).format("%Y-%m-%d")));
|
||||
}
|
||||
)*
|
||||
}
|
||||
@ -148,7 +148,7 @@ impl PlumeQuery {
|
||||
|
||||
/// Parse a query string into this Query
|
||||
pub fn parse_query(&mut self, query: &str) -> &mut Self {
|
||||
self.from_str_req(query.trim())
|
||||
self.from_str_req(&query.trim())
|
||||
}
|
||||
|
||||
/// Convert this Query to a Tantivy Query
|
||||
@ -180,16 +180,12 @@ impl PlumeQuery {
|
||||
|
||||
if self.before.is_some() || self.after.is_some() {
|
||||
// if at least one range bound is provided
|
||||
let after = self.after.unwrap_or_else(|| {
|
||||
i64::from(
|
||||
NaiveDate::from_ymd_opt(2000, 1, 1)
|
||||
.unwrap()
|
||||
.num_days_from_ce(),
|
||||
)
|
||||
});
|
||||
let after = self
|
||||
.after
|
||||
.unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce()));
|
||||
let before = self
|
||||
.before
|
||||
.unwrap_or_else(|| i64::from(Utc::now().date_naive().num_days_from_ce()));
|
||||
.unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce()));
|
||||
let field = Searcher::schema().get_field("creation_date").unwrap();
|
||||
let range =
|
||||
RangeQuery::new_i64_bounds(field, Bound::Included(after), Bound::Included(before));
|
||||
@ -206,20 +202,16 @@ impl PlumeQuery {
|
||||
pub fn before<D: Datelike>(&mut self, date: &D) -> &mut Self {
|
||||
let before = self
|
||||
.before
|
||||
.unwrap_or_else(|| i64::from(Utc::now().date_naive().num_days_from_ce()));
|
||||
.unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce()));
|
||||
self.before = Some(cmp::min(before, i64::from(date.num_days_from_ce())));
|
||||
self
|
||||
}
|
||||
|
||||
// documents older than the provided date will be ignored
|
||||
pub fn after<D: Datelike>(&mut self, date: &D) -> &mut Self {
|
||||
let after = self.after.unwrap_or_else(|| {
|
||||
i64::from(
|
||||
NaiveDate::from_ymd_opt(2000, 1, 1)
|
||||
.unwrap()
|
||||
.num_days_from_ce(),
|
||||
)
|
||||
});
|
||||
let after = self
|
||||
.after
|
||||
.unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce()));
|
||||
self.after = Some(cmp::max(after, i64::from(date.num_days_from_ce())));
|
||||
self
|
||||
}
|
||||
@ -368,7 +360,7 @@ impl std::str::FromStr for PlumeQuery {
|
||||
fn from_str(query: &str) -> Result<PlumeQuery, !> {
|
||||
let mut res: PlumeQuery = Default::default();
|
||||
|
||||
res.from_str_req(query.trim());
|
||||
res.from_str_req(&query.trim());
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ impl<'a> WhitespaceTokenStream<'a> {
|
||||
.filter(|&(_, ref c)| c.is_whitespace())
|
||||
.map(|(offset, _)| offset)
|
||||
.next()
|
||||
.unwrap_or(self.text.len())
|
||||
.unwrap_or_else(|| self.text.len())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,72 +0,0 @@
|
||||
use crate::CONFIG;
|
||||
use rocket::request::{FromRequest, Outcome, Request};
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub enum Strategy {
|
||||
Password,
|
||||
Email,
|
||||
}
|
||||
|
||||
impl Default for Strategy {
|
||||
fn default() -> Self {
|
||||
Self::Password
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Strategy {
|
||||
type Err = StrategyError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
use self::Strategy::*;
|
||||
|
||||
match s {
|
||||
"password" => Ok(Password),
|
||||
"email" => Ok(Email),
|
||||
s => Err(StrategyError::Unsupported(s.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum StrategyError {
|
||||
Unsupported(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for StrategyError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use self::StrategyError::*;
|
||||
|
||||
match self {
|
||||
// FIXME: Calc option strings from enum
|
||||
Unsupported(s) => write!(f, "Unsupported strategy: {}. Choose password or email", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for StrategyError {}
|
||||
|
||||
pub struct Password();
|
||||
pub struct Email();
|
||||
|
||||
impl<'a, 'r> FromRequest<'a, 'r> for Password {
|
||||
type Error = ();
|
||||
|
||||
fn from_request(_request: &'a Request<'r>) -> Outcome<Self, ()> {
|
||||
match matches!(CONFIG.signup, Strategy::Password) {
|
||||
true => Outcome::Success(Self()),
|
||||
false => Outcome::Forward(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'r> FromRequest<'a, 'r> for Email {
|
||||
type Error = ();
|
||||
|
||||
fn from_request(_request: &'a Request<'r>) -> Outcome<Self, ()> {
|
||||
match matches!(CONFIG.signup, Strategy::Email) {
|
||||
true => Outcome::Success(Self()),
|
||||
false => Outcome::Forward(()),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
use crate::{ap_url, instance::Instance, schema::tags, Connection, Error, Result};
|
||||
use activitystreams::iri_string::types::IriString;
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use plume_common::activity_pub::{Hashtag, HashtagExt};
|
||||
use plume_common::activity_pub::Hashtag;
|
||||
|
||||
#[derive(Clone, Identifiable, Queryable)]
|
||||
pub struct Tag {
|
||||
@ -26,16 +25,13 @@ impl Tag {
|
||||
list_by!(tags, for_post, post_id as i32);
|
||||
|
||||
pub fn to_activity(&self) -> Result<Hashtag> {
|
||||
let mut ht = Hashtag::new();
|
||||
ht.set_href(
|
||||
ap_url(&format!(
|
||||
"{}/tag/{}",
|
||||
Instance::get_local()?.public_domain,
|
||||
self.tag
|
||||
))
|
||||
.parse::<IriString>()?,
|
||||
);
|
||||
ht.set_name(self.tag.clone());
|
||||
let mut ht = Hashtag::default();
|
||||
ht.set_href_string(ap_url(&format!(
|
||||
"{}/tag/{}",
|
||||
Instance::get_local()?.public_domain,
|
||||
self.tag
|
||||
)))?;
|
||||
ht.set_name_string(self.tag.clone())?;
|
||||
Ok(ht)
|
||||
}
|
||||
|
||||
@ -48,7 +44,7 @@ impl Tag {
|
||||
Tag::insert(
|
||||
conn,
|
||||
NewTag {
|
||||
tag: tag.name().ok_or(Error::MissingApProperty)?.as_str().into(),
|
||||
tag: tag.name_string()?,
|
||||
is_hashtag,
|
||||
post_id: post,
|
||||
},
|
||||
@ -56,16 +52,13 @@ impl Tag {
|
||||
}
|
||||
|
||||
pub fn build_activity(tag: String) -> Result<Hashtag> {
|
||||
let mut ht = Hashtag::new();
|
||||
ht.set_href(
|
||||
ap_url(&format!(
|
||||
"{}/tag/{}",
|
||||
Instance::get_local()?.public_domain,
|
||||
tag
|
||||
))
|
||||
.parse::<IriString>()?,
|
||||
);
|
||||
ht.set_name(tag);
|
||||
let mut ht = Hashtag::default();
|
||||
ht.set_href_string(ap_url(&format!(
|
||||
"{}/tag/{}",
|
||||
Instance::get_local()?.public_domain,
|
||||
tag
|
||||
)))?;
|
||||
ht.set_name_string(tag)?;
|
||||
Ok(ht)
|
||||
}
|
||||
|
||||
@ -76,72 +69,3 @@ impl Tag {
|
||||
.map_err(Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tests::db;
|
||||
use crate::{diesel::Connection, inbox::tests::fill_database};
|
||||
use assert_json_diff::assert_json_eq;
|
||||
use serde_json::to_value;
|
||||
|
||||
#[test]
|
||||
fn from_activity() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, Error, _>(|| {
|
||||
let (posts, _users, _blogs) = fill_database(conn);
|
||||
let post_id = posts[0].id;
|
||||
let mut ht = Hashtag::new();
|
||||
ht.set_href(ap_url("https://plu.me/tag/a_tag").parse::<IriString>()?);
|
||||
ht.set_name("a_tag".to_string());
|
||||
let tag = Tag::from_activity(conn, &ht, post_id, true)?;
|
||||
|
||||
assert_eq!(&tag.tag, "a_tag");
|
||||
assert!(tag.is_hashtag);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_activity() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, Error, _>(|| {
|
||||
fill_database(conn);
|
||||
let tag = Tag {
|
||||
id: 0,
|
||||
tag: "a_tag".into(),
|
||||
is_hashtag: false,
|
||||
post_id: 0,
|
||||
};
|
||||
let act = tag.to_activity()?;
|
||||
let expected = json!({
|
||||
"href": "https://plu.me/tag/a_tag",
|
||||
"name": "a_tag",
|
||||
"type": "Hashtag"
|
||||
});
|
||||
|
||||
assert_json_eq!(to_value(&act)?, expected);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_activity() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, Error, _>(|| {
|
||||
fill_database(conn);
|
||||
let act = Tag::build_activity("a_tag".into())?;
|
||||
let expected = json!({
|
||||
"href": "https://plu.me/tag/a_tag",
|
||||
"name": "a_tag",
|
||||
"type": "Hashtag"
|
||||
});
|
||||
|
||||
assert_json_eq!(to_value(&act)?, expected);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,19 @@
|
||||
use crate::{
|
||||
db_conn::DbConn,
|
||||
lists::List,
|
||||
posts::Post,
|
||||
schema::{posts, timeline, timeline_definition},
|
||||
Connection, Error, Result,
|
||||
};
|
||||
use diesel::{self, BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use std::cmp::Ordering;
|
||||
use std::ops::Deref;
|
||||
|
||||
pub(crate) mod query;
|
||||
|
||||
pub use self::query::Kind;
|
||||
pub use self::query::{QueryError, TimelineQuery};
|
||||
use self::query::{QueryError, TimelineQuery};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Queryable, Identifiable, AsChangeset)]
|
||||
#[derive(Clone, Debug, PartialEq, Queryable, Identifiable, AsChangeset)]
|
||||
#[table_name = "timeline_definition"]
|
||||
pub struct Timeline {
|
||||
pub id: i32,
|
||||
@ -92,16 +92,6 @@ impl Timeline {
|
||||
.load::<Self>(conn)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
.map(|mut timelines| {
|
||||
timelines.sort_by(|t1, t2| {
|
||||
if t1.user_id.is_some() && t2.user_id.is_none() {
|
||||
Ordering::Less
|
||||
} else {
|
||||
t1.id.cmp(&t2.id)
|
||||
}
|
||||
});
|
||||
timelines
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_for_user(
|
||||
@ -219,10 +209,9 @@ impl Timeline {
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn add_to_all_timelines(conn: &Connection, post: &Post, kind: Kind<'_>) -> Result<()> {
|
||||
pub fn add_to_all_timelines(conn: &DbConn, post: &Post, kind: Kind<'_>) -> Result<()> {
|
||||
let timelines = timeline_definition::table
|
||||
//.load::<Self>(conn.deref())
|
||||
.load::<Self>(conn)
|
||||
.load::<Self>(conn.deref())
|
||||
.map_err(Error::from)?;
|
||||
|
||||
for t in timelines {
|
||||
@ -246,26 +235,7 @@ impl Timeline {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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> {
|
||||
pub fn matches(&self, conn: &DbConn, post: &Post, kind: Kind<'_>) -> Result<bool> {
|
||||
let query = TimelineQuery::parse(&self.query)?;
|
||||
query.matches(conn, self, post, kind)
|
||||
}
|
||||
@ -301,63 +271,73 @@ mod tests {
|
||||
fn test_timeline() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let users = userTests::fill_database(conn);
|
||||
let users = userTests::fill_database(&conn);
|
||||
|
||||
let mut tl1_u1 = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"my timeline".to_owned(),
|
||||
"all".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
List::new(conn, "languages I speak", Some(&users[1]), ListType::Prefix).unwrap();
|
||||
List::new(
|
||||
&conn,
|
||||
"languages I speak",
|
||||
Some(&users[1]),
|
||||
ListType::Prefix,
|
||||
)
|
||||
.unwrap();
|
||||
let tl2_u1 = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"another timeline".to_owned(),
|
||||
"followed".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
let tl1_u2 = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[1].id,
|
||||
"english posts".to_owned(),
|
||||
"lang in \"languages I speak\"".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
let tl1_instance = Timeline::new_for_instance(
|
||||
conn,
|
||||
&conn,
|
||||
"english posts".to_owned(),
|
||||
"license in [cc]".to_owned(),
|
||||
)
|
||||
.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!(
|
||||
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()
|
||||
);
|
||||
assert_eq!(
|
||||
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!(tl_u1.iter().any(|tl| *tl == tl1_u1));
|
||||
assert!(tl_u1.iter().any(|tl| *tl == tl2_u1));
|
||||
assert!(tl_u1.iter().fold(false, |res, tl| { res || *tl == tl1_u1 }));
|
||||
assert!(tl_u1.iter().fold(false, |res, tl| { res || *tl == tl2_u1 }));
|
||||
|
||||
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!(tl_instance.iter().any(|tl| *tl == tl1_instance));
|
||||
assert!(tl_instance
|
||||
.iter()
|
||||
.fold(false, |res, tl| { res || *tl == tl1_instance }));
|
||||
|
||||
tl1_u1.name = "My Super TL".to_owned();
|
||||
let new_tl1_u2 = tl1_u2.update(conn).unwrap();
|
||||
let 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!(tl_u2.iter().any(|tl| *tl == new_tl1_u2));
|
||||
assert!(tl_u2
|
||||
.iter()
|
||||
.fold(false, |res, tl| { res || *tl == new_tl1_u2 }));
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@ -367,48 +347,48 @@ mod tests {
|
||||
fn test_timeline_creation_error() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let users = userTests::fill_database(conn);
|
||||
let users = userTests::fill_database(&conn);
|
||||
|
||||
assert!(Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"my timeline".to_owned(),
|
||||
"invalid keyword".to_owned(),
|
||||
)
|
||||
.is_err());
|
||||
assert!(Timeline::new_for_instance(
|
||||
conn,
|
||||
&conn,
|
||||
"my timeline".to_owned(),
|
||||
"invalid keyword".to_owned(),
|
||||
)
|
||||
.is_err());
|
||||
|
||||
assert!(Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"my timeline".to_owned(),
|
||||
"author in non_existant_list".to_owned(),
|
||||
)
|
||||
.is_err());
|
||||
assert!(Timeline::new_for_instance(
|
||||
conn,
|
||||
&conn,
|
||||
"my timeline".to_owned(),
|
||||
"lang in dont-exist".to_owned(),
|
||||
)
|
||||
.is_err());
|
||||
|
||||
List::new(conn, "friends", Some(&users[0]), ListType::User).unwrap();
|
||||
List::new(conn, "idk", None, ListType::Blog).unwrap();
|
||||
List::new(&conn, "friends", Some(&users[0]), ListType::User).unwrap();
|
||||
List::new(&conn, "idk", None, ListType::Blog).unwrap();
|
||||
|
||||
assert!(Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"my timeline".to_owned(),
|
||||
"blog in friends".to_owned(),
|
||||
)
|
||||
.is_err());
|
||||
assert!(Timeline::new_for_instance(
|
||||
conn,
|
||||
&conn,
|
||||
"my timeline".to_owned(),
|
||||
"not author in idk".to_owned(),
|
||||
)
|
||||
@ -422,10 +402,10 @@ mod tests {
|
||||
fn test_simple_match() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (users, blogs) = blogTests::fill_database(conn);
|
||||
let (users, blogs) = blogTests::fill_database(&conn);
|
||||
|
||||
let gnu_tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"GNU timeline".to_owned(),
|
||||
"license in [AGPL, LGPL, GPL]".to_owned(),
|
||||
@ -433,7 +413,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let gnu_post = Post::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewPost {
|
||||
blog_id: blogs[0].id,
|
||||
slug: "slug".to_string(),
|
||||
@ -449,10 +429,10 @@ mod tests {
|
||||
},
|
||||
)
|
||||
.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(
|
||||
conn,
|
||||
&conn,
|
||||
NewPost {
|
||||
blog_id: blogs[0].id,
|
||||
slug: "slug2".to_string(),
|
||||
@ -469,7 +449,7 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!gnu_tl
|
||||
.matches(conn, &non_free_post, Kind::Original)
|
||||
.matches(&conn, &non_free_post, Kind::Original)
|
||||
.unwrap());
|
||||
|
||||
Ok(())
|
||||
@ -480,9 +460,9 @@ mod tests {
|
||||
fn test_complex_match() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (users, blogs) = blogTests::fill_database(conn);
|
||||
let (users, blogs) = blogTests::fill_database(&conn);
|
||||
Follow::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewFollow {
|
||||
follower_id: users[0].id,
|
||||
following_id: users[1].id,
|
||||
@ -492,11 +472,11 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let fav_blogs_list =
|
||||
List::new(conn, "fav_blogs", Some(&users[0]), ListType::Blog).unwrap();
|
||||
fav_blogs_list.add_blogs(conn, &[blogs[0].id]).unwrap();
|
||||
List::new(&conn, "fav_blogs", Some(&users[0]), ListType::Blog).unwrap();
|
||||
fav_blogs_list.add_blogs(&conn, &[blogs[0].id]).unwrap();
|
||||
|
||||
let my_tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"My timeline".to_owned(),
|
||||
"blog in fav_blogs and not has_cover or local and followed exclude likes"
|
||||
@ -505,7 +485,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let post = Post::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewPost {
|
||||
blog_id: blogs[0].id,
|
||||
slug: "about-linux".to_string(),
|
||||
@ -521,10 +501,10 @@ mod tests {
|
||||
},
|
||||
)
|
||||
.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(
|
||||
conn,
|
||||
&conn,
|
||||
NewPost {
|
||||
blog_id: blogs[1].id,
|
||||
slug: "about-linux-2".to_string(),
|
||||
@ -542,7 +522,7 @@ mod tests {
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!my_tl.matches(conn, &post, Kind::Like(&users[1])).unwrap());
|
||||
assert!(!my_tl.matches(&conn, &post, Kind::Like(&users[1])).unwrap());
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@ -552,17 +532,17 @@ mod tests {
|
||||
fn test_add_to_all_timelines() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (users, blogs) = blogTests::fill_database(conn);
|
||||
let (users, blogs) = blogTests::fill_database(&conn);
|
||||
|
||||
let gnu_tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"GNU timeline".to_owned(),
|
||||
"license in [AGPL, LGPL, GPL]".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
let non_gnu_tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"Stallman disapproved timeline".to_owned(),
|
||||
"not license in [AGPL, LGPL, GPL]".to_owned(),
|
||||
@ -570,7 +550,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let gnu_post = Post::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewPost {
|
||||
blog_id: blogs[0].id,
|
||||
slug: "slug".to_string(),
|
||||
@ -588,7 +568,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let non_free_post = Post::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewPost {
|
||||
blog_id: blogs[0].id,
|
||||
slug: "slug2".to_string(),
|
||||
@ -605,13 +585,13 @@ mod tests {
|
||||
)
|
||||
.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, &gnu_post, Kind::Original).unwrap();
|
||||
Timeline::add_to_all_timelines(&conn, &non_free_post, Kind::Original).unwrap();
|
||||
|
||||
let res = gnu_tl.get_latest(conn, 2).unwrap();
|
||||
let res = gnu_tl.get_latest(&conn, 2).unwrap();
|
||||
assert_eq!(res.len(), 1);
|
||||
assert_eq!(res[0].id, gnu_post.id);
|
||||
let res = non_gnu_tl.get_latest(conn, 2).unwrap();
|
||||
let res = non_gnu_tl.get_latest(&conn, 2).unwrap();
|
||||
assert_eq!(res.len(), 1);
|
||||
assert_eq!(res[0].id, non_free_post.id);
|
||||
|
||||
@ -623,10 +603,10 @@ mod tests {
|
||||
fn test_matches_lists_direct() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (users, blogs) = blogTests::fill_database(conn);
|
||||
let (users, blogs) = blogTests::fill_database(&conn);
|
||||
|
||||
let gnu_post = Post::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewPost {
|
||||
blog_id: blogs[0].id,
|
||||
slug: "slug".to_string(),
|
||||
@ -643,63 +623,63 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
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();
|
||||
PostAuthor::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewPostAuthor {
|
||||
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();
|
||||
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"blog timeline".to_owned(),
|
||||
format!("blog in [{}]", blogs[0].fqn),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"blog timeline".to_owned(),
|
||||
"blog in [no_one@nowhere]".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"author timeline".to_owned(),
|
||||
format!(
|
||||
"author in [{}]",
|
||||
blogs[0].list_authors(conn).unwrap()[0].fqn
|
||||
blogs[0].list_authors(&conn).unwrap()[0].fqn
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"author timeline".to_owned(),
|
||||
format!("author in [{}]", users[2].fqn),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
assert!(tl
|
||||
.matches(conn, &gnu_post, Kind::Reshare(&users[2]))
|
||||
.matches(&conn, &gnu_post, Kind::Reshare(&users[2]))
|
||||
.unwrap());
|
||||
assert!(!tl.matches(conn, &gnu_post, Kind::Like(&users[2])).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Like(&users[2])).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"author timeline".to_owned(),
|
||||
format!(
|
||||
@ -708,50 +688,50 @@ mod tests {
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
assert!(!tl
|
||||
.matches(conn, &gnu_post, Kind::Reshare(&users[2]))
|
||||
.matches(&conn, &gnu_post, Kind::Reshare(&users[2]))
|
||||
.unwrap());
|
||||
assert!(tl.matches(conn, &gnu_post, Kind::Like(&users[2])).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(tl.matches(&conn, &gnu_post, Kind::Like(&users[2])).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"tag timeline".to_owned(),
|
||||
"tags in [free]".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"tag timeline".to_owned(),
|
||||
"tags in [private]".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"english timeline".to_owned(),
|
||||
"lang in [en]".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"franco-italian timeline".to_owned(),
|
||||
"lang in [fr, it]".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@ -795,10 +775,10 @@ mod tests {
|
||||
fn test_matches_keyword() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let (users, blogs) = blogTests::fill_database(conn);
|
||||
let (users, blogs) = blogTests::fill_database(&conn);
|
||||
|
||||
let gnu_post = Post::insert(
|
||||
conn,
|
||||
&conn,
|
||||
NewPost {
|
||||
blog_id: blogs[0].id,
|
||||
slug: "slug".to_string(),
|
||||
@ -816,61 +796,61 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"Linux title".to_owned(),
|
||||
"title contains Linux".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"Microsoft title".to_owned(),
|
||||
"title contains Microsoft".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"Linux subtitle".to_owned(),
|
||||
"subtitle contains Stallman".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"Microsoft subtitle".to_owned(),
|
||||
"subtitle contains Nadella".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"Linux content".to_owned(),
|
||||
"content contains Linux".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
let tl = Timeline::new_for_user(
|
||||
conn,
|
||||
&conn,
|
||||
users[0].id,
|
||||
"Microsoft content".to_owned(),
|
||||
"subtitle contains Windows".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(conn).unwrap();
|
||||
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
|
||||
tl.delete(&conn).unwrap();
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
@ -1,25 +1,32 @@
|
||||
use crate::{
|
||||
blogs::Blog,
|
||||
db_conn::DbConn,
|
||||
lists::{self, ListType},
|
||||
posts::Post,
|
||||
tags::Tag,
|
||||
timeline::Timeline,
|
||||
users::User,
|
||||
Connection, Result,
|
||||
Result,
|
||||
};
|
||||
use plume_common::activity_pub::inbox::AsActor;
|
||||
use whatlang::{self, Lang};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum QueryError {
|
||||
SyntaxError(usize, usize, String),
|
||||
UnexpectedEndOfQuery,
|
||||
RuntimeError(String),
|
||||
}
|
||||
|
||||
impl From<std::option::NoneError> for QueryError {
|
||||
fn from(_: std::option::NoneError) -> Self {
|
||||
QueryError::UnexpectedEndOfQuery
|
||||
}
|
||||
}
|
||||
|
||||
pub type QueryResult<T> = std::result::Result<T, QueryError>;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Kind<'a> {
|
||||
Original,
|
||||
Reshare(&'a User),
|
||||
@ -154,7 +161,7 @@ enum TQ<'a> {
|
||||
impl<'a> TQ<'a> {
|
||||
fn matches(
|
||||
&self,
|
||||
conn: &Connection,
|
||||
conn: &DbConn,
|
||||
timeline: &Timeline,
|
||||
post: &Post,
|
||||
kind: Kind<'_>,
|
||||
@ -199,7 +206,7 @@ enum Arg<'a> {
|
||||
impl<'a> Arg<'a> {
|
||||
pub fn matches(
|
||||
&self,
|
||||
conn: &Connection,
|
||||
conn: &DbConn,
|
||||
timeline: &Timeline,
|
||||
post: &Post,
|
||||
kind: Kind<'_>,
|
||||
@ -224,7 +231,7 @@ enum WithList {
|
||||
impl WithList {
|
||||
pub fn matches(
|
||||
&self,
|
||||
conn: &Connection,
|
||||
conn: &DbConn,
|
||||
timeline: &Timeline,
|
||||
post: &Post,
|
||||
list: &List<'_>,
|
||||
@ -232,7 +239,7 @@ impl WithList {
|
||||
) -> Result<bool> {
|
||||
match list {
|
||||
List::List(name) => {
|
||||
let list = lists::List::find_for_user_by_name(conn, timeline.user_id, name)?;
|
||||
let list = lists::List::find_for_user_by_name(conn, timeline.user_id, &name)?;
|
||||
match (self, list.kind()) {
|
||||
(WithList::Blog, ListType::Blog) => list.contains_blog(conn, post.blog_id),
|
||||
(WithList::Author { boosts, likes }, ListType::User) => match kind {
|
||||
@ -291,7 +298,7 @@ impl WithList {
|
||||
WithList::Author { boosts, likes } => match kind {
|
||||
Kind::Original => Ok(list
|
||||
.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))),
|
||||
Kind::Reshare(u) => {
|
||||
if *boosts {
|
||||
@ -360,7 +367,7 @@ enum Bool {
|
||||
impl Bool {
|
||||
pub fn matches(
|
||||
&self,
|
||||
conn: &Connection,
|
||||
conn: &DbConn,
|
||||
timeline: &Timeline,
|
||||
post: &Post,
|
||||
kind: Kind<'_>,
|
||||
@ -407,7 +414,7 @@ enum List<'a> {
|
||||
|
||||
fn parse_s<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> {
|
||||
let mut res = Vec::new();
|
||||
let (left, token) = parse_a(stream)?;
|
||||
let (left, token) = parse_a(&stream)?;
|
||||
res.push(token);
|
||||
stream = left;
|
||||
while !stream.is_empty() {
|
||||
@ -429,7 +436,7 @@ fn parse_s<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
|
||||
|
||||
fn parse_a<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> {
|
||||
let mut res = Vec::new();
|
||||
let (left, token) = parse_b(stream)?;
|
||||
let (left, token) = parse_b(&stream)?;
|
||||
res.push(token);
|
||||
stream = left;
|
||||
while !stream.is_empty() {
|
||||
@ -456,7 +463,7 @@ fn parse_b<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<
|
||||
match left.get(0) {
|
||||
Some(Token::RParent(_)) => Ok((&left[1..], token)),
|
||||
Some(t) => t.get_error(Token::RParent(0)),
|
||||
None => Err(QueryError::UnexpectedEndOfQuery),
|
||||
None => None?,
|
||||
}
|
||||
}
|
||||
_ => parse_c(stream),
|
||||
@ -477,13 +484,9 @@ fn parse_c<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<
|
||||
}
|
||||
|
||||
fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Arg<'a>)> {
|
||||
match stream
|
||||
.get(0)
|
||||
.map(Token::get_text)
|
||||
.ok_or(QueryError::UnexpectedEndOfQuery)?
|
||||
{
|
||||
match stream.get(0).map(Token::get_text)? {
|
||||
s @ "blog" | s @ "author" | s @ "license" | s @ "tags" | s @ "lang" => {
|
||||
match stream.get(1).ok_or(QueryError::UnexpectedEndOfQuery)? {
|
||||
match stream.get(1)? {
|
||||
Token::Word(_, _, r#in) if r#in == &"in" => {
|
||||
let (mut left, list) = parse_l(&stream[2..])?;
|
||||
let kind = match s {
|
||||
@ -495,12 +498,7 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
|
||||
if *clude != "include" && *clude != "exclude" {
|
||||
break;
|
||||
}
|
||||
match (
|
||||
*clude,
|
||||
left.get(1)
|
||||
.map(Token::get_text)
|
||||
.ok_or(QueryError::UnexpectedEndOfQuery)?,
|
||||
) {
|
||||
match (*clude, left.get(1).map(Token::get_text)?) {
|
||||
("include", "reshares") | ("include", "reshare") => {
|
||||
boosts = true
|
||||
}
|
||||
@ -531,10 +529,7 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
|
||||
t => t.get_error(Token::Word(0, 0, "'in'")),
|
||||
}
|
||||
}
|
||||
s @ "title" | s @ "subtitle" | s @ "content" => match (
|
||||
stream.get(1).ok_or(QueryError::UnexpectedEndOfQuery)?,
|
||||
stream.get(2).ok_or(QueryError::UnexpectedEndOfQuery)?,
|
||||
) {
|
||||
s @ "title" | s @ "subtitle" | s @ "content" => match (stream.get(1)?, stream.get(2)?) {
|
||||
(Token::Word(_, _, contains), Token::Word(_, _, w)) if contains == &"contains" => Ok((
|
||||
&stream[3..],
|
||||
Arg::Contains(
|
||||
@ -560,13 +555,7 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
|
||||
if *clude != "include" && *clude != "exclude" {
|
||||
break;
|
||||
}
|
||||
match (
|
||||
*clude,
|
||||
stream
|
||||
.get(2)
|
||||
.map(Token::get_text)
|
||||
.ok_or(QueryError::UnexpectedEndOfQuery)?,
|
||||
) {
|
||||
match (*clude, stream.get(2).map(Token::get_text)?) {
|
||||
("include", "reshares") | ("include", "reshare") => boosts = true,
|
||||
("exclude", "reshares") | ("exclude", "reshare") => boosts = false,
|
||||
("include", "likes") | ("include", "like") => likes = true,
|
||||
@ -588,23 +577,20 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
|
||||
"all" => Ok((&stream[1..], Arg::Boolean(Bool::All))),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
_ => stream
|
||||
.get(0)
|
||||
.ok_or(QueryError::UnexpectedEndOfQuery)?
|
||||
.get_error(Token::Word(
|
||||
0,
|
||||
0,
|
||||
"one of 'blog', 'author', 'license', 'tags', 'lang', \
|
||||
_ => stream.get(0)?.get_error(Token::Word(
|
||||
0,
|
||||
0,
|
||||
"one of 'blog', 'author', 'license', 'tags', 'lang', \
|
||||
'title', 'subtitle', 'content', 'followed', 'has_cover', 'local' or 'all'",
|
||||
)),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_l<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], List<'a>)> {
|
||||
match stream.get(0).ok_or(QueryError::UnexpectedEndOfQuery)? {
|
||||
match stream.get(0)? {
|
||||
Token::LBracket(_) => {
|
||||
let (left, list) = parse_m(&stream[1..])?;
|
||||
match left.get(0).ok_or(QueryError::UnexpectedEndOfQuery)? {
|
||||
match left.get(0)? {
|
||||
Token::RBracket(_) => Ok((&left[1..], List::Array(list))),
|
||||
t => t.get_error(Token::Word(0, 0, "one of ']' or ','")),
|
||||
}
|
||||
@ -615,20 +601,16 @@ fn parse_l<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Lis
|
||||
}
|
||||
|
||||
fn parse_m<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Vec<&'a str>)> {
|
||||
let mut res: Vec<&str> = vec![
|
||||
match stream.get(0).ok_or(QueryError::UnexpectedEndOfQuery)? {
|
||||
Token::Word(_, _, w) => w,
|
||||
t => return t.get_error(Token::Word(0, 0, "any word")),
|
||||
},
|
||||
];
|
||||
let mut res: Vec<&str> = vec![match stream.get(0)? {
|
||||
Token::Word(_, _, w) => w,
|
||||
t => return t.get_error(Token::Word(0, 0, "any word")),
|
||||
}];
|
||||
stream = &stream[1..];
|
||||
while let Token::Comma(_) = stream[0] {
|
||||
res.push(
|
||||
match stream.get(1).ok_or(QueryError::UnexpectedEndOfQuery)? {
|
||||
Token::Word(_, _, w) => w,
|
||||
t => return t.get_error(Token::Word(0, 0, "any word")),
|
||||
},
|
||||
);
|
||||
res.push(match stream.get(1)? {
|
||||
Token::Word(_, _, w) => w,
|
||||
t => return t.get_error(Token::Word(0, 0, "any word")),
|
||||
});
|
||||
stream = &stream[2..];
|
||||
}
|
||||
|
||||
@ -653,7 +635,7 @@ impl<'a> TimelineQuery<'a> {
|
||||
|
||||
pub fn matches(
|
||||
&self,
|
||||
conn: &Connection,
|
||||
conn: &DbConn,
|
||||
timeline: &Timeline,
|
||||
post: &Post,
|
||||
kind: Kind<'_>,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2022-01-12 01:20\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Afrikaans\n"
|
||||
"Language: af_ZA\n"
|
||||
@ -17,47 +17,47 @@ msgstr ""
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:172
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:326
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:385
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:389
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:396
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:407
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:517
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:518
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:524
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:564
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:575
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2022-01-12 01:20\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Arabic\n"
|
||||
"Language: ar_SA\n"
|
||||
@ -17,47 +17,47 @@ msgstr ""
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:172
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:326
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "فتح محرر النصوص الغني"
|
||||
|
||||
# plume-front/src/editor.rs:385
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "العنوان"
|
||||
|
||||
# plume-front/src/editor.rs:389
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr "العنوان الثانوي أو الملخص"
|
||||
|
||||
# plume-front/src/editor.rs:396
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "اكتب مقالك هنا. ماركداون مُدَعَّم."
|
||||
|
||||
# plume-front/src/editor.rs:407
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "يتبقا {} حرفا تقريبا"
|
||||
|
||||
# plume-front/src/editor.rs:517
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "الوسوم"
|
||||
|
||||
# plume-front/src/editor.rs:518
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "الرخصة"
|
||||
|
||||
# plume-front/src/editor.rs:524
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr "الغلاف"
|
||||
|
||||
# plume-front/src/editor.rs:564
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "هذه مسودة"
|
||||
|
||||
# plume-front/src/editor.rs:575
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "نشر كتابا"
|
||||
|
||||
|
@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2022-01-12 01:20\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Bulgarian\n"
|
||||
"Language: bg_BG\n"
|
||||
@ -17,47 +17,47 @@ msgstr ""
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:172
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr "Искате ли да активирате локално автоматично запаметяване, последно редактирано в {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:326
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "Отворете редактора с богат текст"
|
||||
|
||||
# plume-front/src/editor.rs:385
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "Заглавие"
|
||||
|
||||
# plume-front/src/editor.rs:389
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr "Подзаглавие или резюме"
|
||||
|
||||
# plume-front/src/editor.rs:396
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "Напишете статията си тук. Поддържа се Markdown."
|
||||
|
||||
# plume-front/src/editor.rs:407
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "Остават {} знака вляво"
|
||||
|
||||
# plume-front/src/editor.rs:517
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "Етикети"
|
||||
|
||||
# plume-front/src/editor.rs:518
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "Лиценз"
|
||||
|
||||
# plume-front/src/editor.rs:524
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr "Основно изображение"
|
||||
|
||||
# plume-front/src/editor.rs:564
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "Това е проект"
|
||||
|
||||
# plume-front/src/editor.rs:575
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "Публикувай"
|
||||
|
||||
|
@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2022-01-12 01:20\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Catalan\n"
|
||||
"Language: ca_ES\n"
|
||||
@ -17,47 +17,47 @@ msgstr ""
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:172
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:326
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "Obre l’editor de text enriquit"
|
||||
|
||||
# plume-front/src/editor.rs:385
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "Títol"
|
||||
|
||||
# plume-front/src/editor.rs:389
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr "Subtítol o resum"
|
||||
|
||||
# plume-front/src/editor.rs:396
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "Escriviu el vostre article ací. Podeu fer servir el Markdown."
|
||||
|
||||
# plume-front/src/editor.rs:407
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "Queden uns {} caràcters"
|
||||
|
||||
# plume-front/src/editor.rs:517
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetes"
|
||||
|
||||
# plume-front/src/editor.rs:518
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "Llicència"
|
||||
|
||||
# plume-front/src/editor.rs:524
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr "Coberta"
|
||||
|
||||
# plume-front/src/editor.rs:564
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "Açò és un esborrany"
|
||||
|
||||
# plume-front/src/editor.rs:575
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "Publica"
|
||||
|
||||
|
@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2022-05-09 09:58\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Czech\n"
|
||||
"Language: cs_CZ\n"
|
||||
@ -17,47 +17,47 @@ msgstr ""
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:172
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:326
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "Otevřít editor formátovaného textu"
|
||||
|
||||
# plume-front/src/editor.rs:385
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "Nadpis"
|
||||
|
||||
# plume-front/src/editor.rs:389
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr "Podnadpis, nebo shrnutí"
|
||||
|
||||
# plume-front/src/editor.rs:396
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "Sem napište svůj článek. Markdown je podporován."
|
||||
|
||||
# plume-front/src/editor.rs:407
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "Zbývá kolem {} znaků"
|
||||
|
||||
# plume-front/src/editor.rs:517
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "Tagy"
|
||||
|
||||
# plume-front/src/editor.rs:518
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "Licence"
|
||||
|
||||
# plume-front/src/editor.rs:524
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr "Titulka"
|
||||
|
||||
# plume-front/src/editor.rs:564
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "Tohle je koncept"
|
||||
|
||||
# plume-front/src/editor.rs:575
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "Zveřejnit"
|
||||
|
||||
|
@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2022-01-12 01:20\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Danish\n"
|
||||
"Language: da_DK\n"
|
||||
@ -17,47 +17,47 @@ msgstr ""
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:172
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:326
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:385
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:389
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:396
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:407
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:517
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:518
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:524
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:564
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:575
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2022-01-26 13:16\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Language: de_DE\n"
|
||||
@ -17,47 +17,47 @@ msgstr ""
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:172
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr "Möchten Sie die lokale automatische Speicherung laden, die zuletzt um {} bearbeitet wurde?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:326
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr " Rich Text Editor (RTE) öffnen"
|
||||
|
||||
# plume-front/src/editor.rs:385
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "Titel"
|
||||
|
||||
# plume-front/src/editor.rs:389
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr "Untertitel oder Zusammenfassung"
|
||||
|
||||
# plume-front/src/editor.rs:396
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "Schreiben deinen Artikel hier. Markdown wird unterstützt."
|
||||
|
||||
# plume-front/src/editor.rs:407
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "Ungefähr {} Zeichen übrig"
|
||||
|
||||
# plume-front/src/editor.rs:517
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "Schlagwörter"
|
||||
|
||||
# plume-front/src/editor.rs:518
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "Lizenz"
|
||||
|
||||
# plume-front/src/editor.rs:524
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr "Einband"
|
||||
|
||||
# plume-front/src/editor.rs:564
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "Dies ist ein Entwurf"
|
||||
|
||||
# plume-front/src/editor.rs:575
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "Veröffentlichen"
|
||||
|
||||
|
@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2022-01-12 01:20\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Greek\n"
|
||||
"Language: el_GR\n"
|
||||
@ -17,47 +17,47 @@ msgstr ""
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:172
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:326
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:385
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:389
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:396
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:407
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:517
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:518
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:524
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:564
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:575
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2022-01-12 01:20\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
"Language: en_US\n"
|
||||
@ -17,47 +17,47 @@ msgstr ""
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:172
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:326
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:385
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:389
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:396
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:407
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:517
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:518
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:524
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:564
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:575
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2022-01-12 01:20\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Esperanto\n"
|
||||
"Language: eo_UY\n"
|
||||
@ -17,47 +17,47 @@ msgstr ""
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:172
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:326
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "Malfermi la riĉan redaktilon"
|
||||
|
||||
# plume-front/src/editor.rs:385
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "Titolo"
|
||||
|
||||
# plume-front/src/editor.rs:389
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:396
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "Verku vian artikolon ĉi tie. Markdown estas subtenita."
|
||||
|
||||
# plume-front/src/editor.rs:407
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "Proksimume {} signoj restantaj"
|
||||
|
||||
# plume-front/src/editor.rs:517
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "Etikedoj"
|
||||
|
||||
# plume-front/src/editor.rs:518
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "Permesilo"
|
||||
|
||||
# plume-front/src/editor.rs:524
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr "Kovro"
|
||||
|
||||
# plume-front/src/editor.rs:564
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "Malfinias"
|
||||
|
||||
# plume-front/src/editor.rs:575
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "Eldoni"
|
||||
|
||||
|
@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: plume\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
|
||||
"PO-Revision-Date: 2022-01-26 13:16\n"
|
||||
"PO-Revision-Date: 2020-12-19 09:56\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Spanish\n"
|
||||
"Language: es_ES\n"
|
||||
@ -17,47 +17,47 @@ msgstr ""
|
||||
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
|
||||
"X-Crowdin-File-ID: 12\n"
|
||||
|
||||
# plume-front/src/editor.rs:172
|
||||
# plume-front/src/editor.rs:189
|
||||
msgid "Do you want to load the local autosave last edited at {}?"
|
||||
msgstr "¿Quieres cargar el guardado automático local editado por última vez en {}?"
|
||||
msgstr ""
|
||||
|
||||
# plume-front/src/editor.rs:326
|
||||
# plume-front/src/editor.rs:282
|
||||
msgid "Open the rich text editor"
|
||||
msgstr "Abrir el editor de texto enriquecido"
|
||||
|
||||
# plume-front/src/editor.rs:385
|
||||
# plume-front/src/editor.rs:315
|
||||
msgid "Title"
|
||||
msgstr "Título"
|
||||
|
||||
# plume-front/src/editor.rs:389
|
||||
# plume-front/src/editor.rs:319
|
||||
msgid "Subtitle, or summary"
|
||||
msgstr "Subtítulo, o resumen"
|
||||
|
||||
# plume-front/src/editor.rs:396
|
||||
# plume-front/src/editor.rs:326
|
||||
msgid "Write your article here. Markdown is supported."
|
||||
msgstr "Escriba su artículo aquí. Puede utilizar Markdown."
|
||||
|
||||
# plume-front/src/editor.rs:407
|
||||
# plume-front/src/editor.rs:337
|
||||
msgid "Around {} characters left"
|
||||
msgstr "Quedan unos {} caracteres"
|
||||
|
||||
# plume-front/src/editor.rs:517
|
||||
# plume-front/src/editor.rs:414
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetas"
|
||||
|
||||
# plume-front/src/editor.rs:518
|
||||
# plume-front/src/editor.rs:415
|
||||
msgid "License"
|
||||
msgstr "Licencia"
|
||||
|
||||
# plume-front/src/editor.rs:524
|
||||
# plume-front/src/editor.rs:418
|
||||
msgid "Cover"
|
||||
msgstr "Cubierta"
|
||||
|
||||
# plume-front/src/editor.rs:564
|
||||
# plume-front/src/editor.rs:438
|
||||
msgid "This is a draft"
|
||||
msgstr "Esto es un borrador"
|
||||
|
||||
# plume-front/src/editor.rs:575
|
||||
# plume-front/src/editor.rs:445
|
||||
msgid "Publish"
|
||||
msgstr "Publicar"
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user