Compare commits

..

2 Commits

Author SHA1 Message Date
Violet white 26867ca6be Retrieve signing user from database instead of expecting it to materialize from thin air 2020-01-14 13:56:51 -05:00
Violet white 2b4e802914 Work towards authorized fetch 2020-01-12 22:09:56 -05:00
324 changed files with 27268 additions and 42564 deletions
-10
View File
@@ -1,10 +0,0 @@
[target.wasm32-unknown-unknown]
# required for clippy
rustflags = [
"--cfg", "web_sys_unstable_apis",
]
[target.x86_64-unknown-linux-gnu]
rustflags = [
"--cfg", "web_sys_unstable_apis",
]
+10 -15
View File
@@ -10,8 +10,8 @@ executors:
type: boolean
default: false
docker:
- image: plumeorg/plume-buildenv:v0.4.0
- image: <<#parameters.postgres>>cimg/postgres:14.2<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
- image: plumeorg/plume-buildenv:v0.0.8
- image: <<#parameters.postgres>>circleci/postgres:9.6-alpine<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
environment:
POSTGRES_USER: postgres
POSTGRES_DB: plume
@@ -21,7 +21,6 @@ executors:
RUST_TEST_THREADS: 1
FEATURES: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
DATABASE_URL: <<#parameters.postgres>>postgres://postgres@localhost/plume<</parameters.postgres>><<^parameters.postgres>>plume.sqlite<</parameters.postgres>>
ROCKET_SECRET_KEY: VN5xV1DN7XdpATadOCYcuGeR/dV0hHfgx9mx9TarLdM=
commands:
@@ -72,7 +71,7 @@ commands:
type: string
steps:
- run: |
export RUSTFLAGS="-Zprofile -Zfewer-names -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Clink-arg=-Xlinker -Clink-arg=--no-keep-memory -Clink-arg=-Xlinker -Clink-arg=--reduce-memory-overheads"
export RUSTFLAGS="-Zprofile -Zfewer-names -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zno-landing-pads -Clink-arg=-Xlinker -Clink-arg=--no-keep-memory -Clink-arg=-Xlinker -Clink-arg=--reduce-memory-overheads"
export CARGO_INCREMENTAL=0
<< parameters.cmd >>
@@ -100,7 +99,7 @@ commands:
steps:
- run: |
cmd="cargo build <<#parameters.release>>--release<</parameters.release>> --no-default-features --features="${FEATURES}" -p <<parameters.package>> -j"
for i in 16 4 2 1 1; do
for i in 36 4 2 1 1; do
$cmd $i && exit 0
done
exit 1
@@ -144,14 +143,11 @@ jobs:
cache: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
- run_with_coverage:
cmd: |
cargo build -p plume-cli --no-default-features --features=${FEATURES} -j 4
./target/debug/plm migration run
./target/debug/plm search init
cmd="cargo test --all --exclude plume-front --exclude plume-macro --no-run --no-default-features --features=${FEATURES} -j"
for i in 16 4 2 1 1; do
for i in 36 4 2 1 1; do
$cmd $i && break
done
cargo test --all --exclude plume-front --exclude plume-macro --no-default-features --features="${FEATURES}" -j1
cargo test --all --exclude plume-front --exclude plume-macro --no-default-features --features="${FEATURES}" -j1 -- --test-threads=1
- upload_coverage:
type: unit
- cache:
@@ -168,18 +164,18 @@ jobs:
steps:
- restore_env:
cache: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
- run: wasm-pack build --target web --release plume-front
- run: cargo web deploy -p plume-front
- run_with_coverage:
cmd: |
cmd="cargo install --debug --no-default-features --features="${FEATURES}",test --force --path . -j"
for i in 16 4 2 1 1; do
for i in 36 4 2 1 1; do
$cmd $i && exit 0
done
exit 1
- run_with_coverage:
cmd: |
cmd="cargo install --debug --no-default-features --features="${FEATURES}" --force --path plume-cli -j"
for i in 16 4 2 1 1; do
for i in 36 4 2 1 1; do
$cmd $i && exit 0
done
exit 1
@@ -203,7 +199,7 @@ jobs:
steps:
- restore_env:
cache: release-<<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
- run: wasm-pack build --target web --release plume-front
- run: cargo web deploy -p plume-front --release
- build:
package: plume
release: true
@@ -230,7 +226,6 @@ jobs:
steps:
- restore_env:
cache: none
- run: cargo build
- run: crowdin upload -b master
workflows:
+5 -2
View File
@@ -1,3 +1,6 @@
localhost {
reverse_proxy localhost:7878
localhost:443 {
proxy / localhost:7878 {
transparent
}
tls self_signed
}
+6 -8
View File
@@ -1,22 +1,19 @@
FROM debian:buster-20210208
FROM debian:stretch-20190326
ENV PATH="/root/.cargo/bin:${PATH}"
#install native/circleci/build dependancies
RUN apt update &&\
apt install -y --no-install-recommends git ssh tar gzip ca-certificates default-jre&&\
echo "deb [trusted=yes] https://apt.fury.io/caddy/ /" \
| tee -a /etc/apt/sources.list.d/caddy-fury.list &&\
apt update &&\
apt install -y --no-install-recommends binutils-dev build-essential cmake curl gcc gettext git libcurl4-openssl-dev libdw-dev libelf-dev libiberty-dev libpq-dev libsqlite3-dev libssl-dev make openssl pkg-config postgresql postgresql-contrib python zlib1g-dev python3-pip zip unzip libclang-dev clang caddy&&\
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&&\
rm -rf /var/lib/apt/lists/*
#install and configure rust
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2021-11-27 -y &&\
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2019-03-23 -y &&\
rustup component add rustfmt clippy &&\
rustup component add rust-std --target wasm32-unknown-unknown
#compile some deps
RUN cargo install wasm-pack &&\
RUN cargo install cargo-web &&\
cargo install grcov &&\
strip /root/.cargo/bin/* &&\
rm -fr ~/.cargo/registry
@@ -27,7 +24,8 @@ COPY cargo_config /root/.cargo/config
#install selenium for front end tests
RUN pip3 install selenium
#configure caddy
#install and configure caddy
RUN curl https://getcaddy.com | bash -s personal
COPY Caddyfile /Caddyfile
#install crowdin
-12
View File
@@ -15,9 +15,6 @@ DATABASE_URL=postgres://plume:plume@localhost/plume
# The domain of your instance
BASE_URL=plu.me
# Log level for each crate
RUST_LOG=info
# The secret key for private cookies and CSRF protection
# You can generate one with `openssl rand -base64 32`
ROCKET_SECRET_KEY=
@@ -48,12 +45,3 @@ ROCKET_ADDRESS=127.0.0.1
#PLUME_LOGO_192=icons/trwnh/paragraphs/plumeParagraphs192.png
#PLUME_LOGO_256=icons/trwnh/paragraphs/plumeParagraphs256.png
#PLUME_LOGO_512=icons/trwnh/paragraphs/plumeParagraphs512.png
## LDAP CONFIG ##
# the object that will be bound is "${USER_NAME_ATTR}=${username},${BASE_DN}"
#LDAP_ADDR=ldap://127.0.0.1:1389
#LDAP_BASE_DN="ou=users,dc=your-org,dc=eu"
#LDAP_USER_NAME_ATTR=cn
#LDAP_USER_MAIL_ATTR=mail
#LDAP_TLS=false
-10
View File
@@ -7,16 +7,6 @@ assignees: ''
---
<!--
We would appreciated if you report a bug at our Gitea instance's issue page:
https://git.joinplu.me/Plume/Plume/issues
You can login to the Gitea with your GitHub account.
We welcome to receive bug reports here, GitHub, too.
-->
<!-- Describe your bug, explaining how to reproduce it, and what was expected -->
@@ -7,15 +7,6 @@ assignees: ''
---
<!--
We would appreciated if you request a feature at our Gitea instance's issue page:
https://git.joinplu.me/Plume/Plume/issues
You can login to the Gitea with your GitHub account.
We welcome to receive feature requests here, GitHub, too.
-->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
-6
View File
@@ -1,6 +0,0 @@
version: 2
updates:
- package-ecosystem: cargo
directory: /
schedule:
interval: daily
-7
View File
@@ -1,7 +0,0 @@
<!--
We would appreciated if you report a bug at our Gitea instance's pull request page:
https://git.joinplu.me/Plume/Plume/pulls
You can login to the Gitea with your GitHub account.
We welcome to receive pull requests here, GitHub, too.
-->
@@ -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@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
tags: plumeorg/plume:latest
-36
View File
@@ -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@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: plumeorg/plume
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
-2
View File
@@ -18,5 +18,3 @@ tags.*
search_index
.buildconfig
__pycache__
.vscode/
*-journal
-273
View File
@@ -1,273 +0,0 @@
# Changelog
<!-- next-header -->
## [Unreleased] - ReleaseDate
## [[0.7.2]] - 2022-05-11
### Added
- Basque language (#1013)
- Unit tests for ActivityPub (#1021)
- Move to action area after liking/boosting/commenting (#1074)
### Changed
- Bump Rust to nightly 2022-01-26 (#1015)
- Remove "Latest articles" timeline (#1069)
- Change order of timeline tabs (#1069, #1070, #1072)
- Migrate ActivityPub-related crates from activitypub 0.1 to activitystreams 0.7 (#1022)
### Fixed
- Add explanation of sign-up step at sign-up page when email sign-up mode (#1012)
- Add NOT NULL constraint to email_blocklist table fields (#1016)
- Don't fill empty content when switching rich editor (#1017)
- Fix accept header (#1058)
- Render 404 page instead of 500 when data is not found (#1062)
- Reuse reqwest client on broadcasting (#1059)
- Reduce broadcasting HTTP request at once to prevent them being timed out (#1068, #1071)
- Some ActivityPub data (#1021)
## [[0.7.1]] - 2022-01-12
### Added
- Introduce environment variable `MAIL_PORT` (#980)
- Introduce email sign-up feature (#636, #1002)
### Changed
- Some styling improvements (#976, #977, #978)
- Respond with error status code when error (#1002)
### Fiexed
- Fix comment link (#974)
- Fix a bug that prevents posting articles (#975)
- Fix a bug that notification page doesn't show (#981)
## [[0.7.0]] - 2022-01-02
### Added
- Allow `dir` attributes for LtoR text in RtoL document (#860)
- More translation languages (#862)
- Proxy support (#829)
- Riker a actor system library (#870)
- (request-target) and Host header in HTTP Signature (#872)
- Default log levels for RUST_LOG (#885, #886, #919)
### Changed
- Upgrade some dependent crates (#858)
- Use tracing crate (#868)
- Update Rust version to nightly-2021-11-27 (#961)
- Upgrade Tantivy to 0.13.3 and lindera-tantivy to 0.7.1 (#878)
- Run searcher on actor system (#870)
- Extract a function to calculate posts' ap_url and share it with some places (#918)
- Use article title as its slug instead of capitalizing and inserting hyphens (#920)
- Sign GET requests to other instances (#957)
### Fixed
- Percent-encode URI for remote_interact (#866, #857)
- Menu animation not opening on iOS (#876, #897)
- Make actors subscribe to channel once (#913)
- Upsert posts and media instead of trying to insert and fail (#912)
- Update post's ActivityPub id when published by update (#915)
- Calculate media URI properly even when MEDIA_UPLOAD_DIRECTORY configured (#916)
- Prevent duplicated posts in 'all' timeline (#917)
- Draw side line for blockquote on start (#933)
- Fix URIs of posts on Mastodon (#947)
- Place edit link proper position (#956, #963, #964)
## [[0.6.0]] - 2020-12-29
### Added
- Vazir font for better support of languages written in Arabic script (#787)
- Login via LDAP (#826)
- cargo-release (#835)
- Care about weak ETag header for better caching (#840)
- Support for right to left languages in post content (#853)
### Changed
- Bump Docker base images to Buster flavor (#797)
- Upgrade Rocket to 0.4.5 (#800)
- Keep tags as-is (#832)
- Update Docker image for testing (#838)
- Update Dockerfile.dev (#841)
### Fixed
- Recreate search index if its format is outdated (#802)
- Make it possible to switch to rich text editor (#808)
- Fix margins for the mobile devices (#817)
- GPU acceleration for the mobile menu (#818)
- Natural title position for RtoL languages (#825)
- Remove link to unimplemented page (#827)
- Fix displaying not found page when submitting a duplicated blocklist email (#831)
### Security
- Validate spoofing of activity
## [0.5.0] - 2020-06-21
### Added
- Email blocklisting (#718)
- Syntax highlighting (#691)
- Persian localization (#782)
- Switchable tokenizer - enables Japanese full-text search (#776)
- Make database connections configurable by environment variables (#768)
### Changed
- Display likes and boost on post cards (#744)
- Rust 2018 (#726)
- Bump to LLVM to 9.0.0 to fix ARM builds (#737)
- Remove dependency on runtime-fmt (#773)
- Drop the -alpha suffix in release names, it is implied that Plume is not stable yet because of the 0 major version (Plume 1.0.0 will be the first stable release).
### Fixed
- Fix parsing of mentions inside a Markdown code block (be430c6)
- Fix RSS issues (#720)
- Fix Atom feed (#764)
- Fix default theme (#746)
- Fix shown password on remote interact pages (#741)
- Allow unicode hashtags (#757)
- Fix French grammar for for 0 (#760)
- Don't show boosts and likes for "all" and "local" in timelines (#781)
- Fix liking and boosting posts on remote instances (#762)
## [0.4.0] - 2019-12-23
### Added
- Add support for generic timeline (#525)
- Federate user deletion (#551)
- import migrations and don't require diesel_cli for admins (#555)
- Cache local instance (#572)
- Initial RTL support #575 (#577)
- Confirm deletion of blog (#602)
- Make a distinction between moderators and admins (#619)
- Theming (#624)
- Add clap to plume in order to print help and version (#631)
- Add Snapcraft metadata and install/maintenance hooks (#666)
- Add environmental variable to control path of media (#683)
- Add autosaving to the editor (#688)
- CI: Upload artifacts to pull request deploy environment (#539)
- CI: Upload artifact of wasm binary (#571)
### Changed
- Update follow_remote.rs.html grammar (#548)
- Add some feedback when performing some actions (#552)
- Theme update (#553)
- Remove the new index lock tantivy uses (#556)
- Reduce reqwest timeout to 5s (#557)
- Improve notification management (#561)
- Fix occurrences of 'have been' to 'has been' (#578) + Direct follow-up to #578 (#603)
- Store password reset requests in database (#610)
- Use futures and tokio to send activities (#620)
- Don't ignore dotenv errors (#630)
- Replace the input! macro with an Input builder (#646)
- Update default license (#659)
- Paginate the outbox responses. Fixes #669 (#681)
- Use the "classic" editor by default (#697)
- Fix issue #705 (#708)
- Make comments in styleshhets a bit clearer (#545)
- Rewrite circleci config (#558)
- Use openssl instead of sha256sum for build.rs (#568)
- Update dependencies (#574)
- Refactor code to use Shrinkwraprs and diesel-derive-newtype (#598)
- Add enum containing all successful route returns (#614)
- Update dependencies which depended on nix -- fixes arm32 builds (#615)
- Update some documents (#616)
- Update dependencies (#643)
- Make the comment syntax consistent across all CSS (#487)
### Fixed
- Remove r (#535)
- Fix certain improper rendering of forms (#560)
- make hashtags work in profile summary (#562)
- Fix some federation issue (#573)
- Prevent comment form submit button distortion on iOS (#592)
- Update textarea overflow to scroll (#609)
- Fix arm builds (#612)
- Fix theme caching (#647)
- Fix issue #642, frontend not in English if the user language does not exist (#648)
- Don't index drafts (#656)
- Fill entirely user on creation (#657)
- Delete notification on user deletion (#658)
- Order media so that latest added are top (#660)
- Fix logo URL (#664)
- Snap: Ensure cargo-web doesn't erroneously adopt our workspace. (#667)
- Snap: Another fix for building (#668)
- Snap: Fix build for non-Tier-1 Rust platforms (#672)
- Don't split sentences for translations (#677)
- Escape href quotation marks (#678)
- Re-add empty strings in translation (#682)
- Make the search index creation during migration respect SEARCH_INDEX (#689)
- Fix the navigation menu not opening on touch (#690)
- Make search items optional (#693)
- Various snap fixes (#698)
- Fix #637 : Markdown footnotes (#700)
- Fix lettre (#706)
- CI: Fix Crowdin upload (#576)
### Removed
- Remove the Canapi dependency (#540)
- Remove use of Rust in migrations (#704)
## [0.3.0] - 2019-04-19
### Added
- Cover for articles (#299, #387)
- Password reset (#448)
- New editor (#293, #458, #482, #483, #486, #530)
- Search (#324, #375, #445)
- Edit blogs (#460, #494, #497)
- Hashtags in articles (#283, #295)
- API endpoints (#245, #285, #307)
- A bunch of new translations! (#479, #501, #506, #510, #512, #514)
### Changed
- Federation improvements (#216, #217, #357, #364, #399, #443, #446, #455, #502, #519)
- Improved build process (#281, #374, #392, #402, #489, #498, #503, #511, #513, #515, #528)
### Fixes
- UI usability fixes (#370, #386, #401, #417, #418, #444, #452, #480, #516, #518, #522, #532)
## [0.2.0] - 2018-09-12
### Added
- Article publishing, or save as a draft
- Like, or boost an article
- Basic Markdown editor
- Federated commenting system
- User account creation
- Limited federation on other platforms and subscribing to users
- Ability to create multiple blogs
<!-- next-url -->
[Unreleased]: https://github.com/Plume-org/Plume/compare/0.7.2...HEAD
[[0.7.2]]: https://github.com/Plume-org/Plume/compare/0.7.1...0.7.2
[[0.7.1]]: https://github.com/Plume-org/Plume/compare/0.7.0...0.7.1
[[0.7.0]]: https://github.com/Plume-org/Plume/compare/0.6.0...0.7.0
[[0.6.0]]: https://github.com/Plume-org/Plume/compare/0.5.0...0.6.0
[0.5.0]: https://github.com/Plume-org/Plume/compare/0.4.0-alpha-4...0.5.0
[0.4.0]: https://github.com/Plume-org/Plume/compare/0.3.0-alpha-2...0.4.0-alpha-4
[0.3.0]: https://github.com/Plume-org/Plume/compare/0.2.0-alpha-1...0.3.0-alpha-2
[0.2.0]: https://github.com/Plume-org/Plume/releases/tag/0.2.0-alpha-1
Generated
+2187 -3585
View File
File diff suppressed because it is too large Load Diff
+29 -25
View File
@@ -1,33 +1,38 @@
[package]
authors = ["Plume contributors"]
name = "plume"
version = "0.7.2"
version = "0.4.0"
repository = "https://github.com/Plume-org/Plume"
edition = "2018"
[dependencies]
atom_syndication = "0.11.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"
colored = "1.8"
dotenv = "0.14"
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"
heck = "0.3.0"
lettre = "0.9.2"
lettre_email = "0.9.2"
num_cpus = "1.10"
rocket = "0.4.6"
rocket_contrib = { version = "0.4.5", features = ["json"] }
rocket_i18n = "0.4.1"
rocket = "0.4.0"
rocket_contrib = { version = "0.4.0", features = ["json"] }
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
rpassword = "4.0"
runtime-fmt = "0.3.0"
scheduled-thread-pool = "0.2.2"
serde = "1.0"
serde_json = "1.0.81"
shrinkwraprs = "0.3.0"
validator = { version = "0.15", features = ["derive"] }
serde_json = "1.0"
serde_qs = "0.5"
shrinkwraprs = "0.2.1"
syntect = "3.3"
validator = "0.8"
validator_derive = "0.8"
webfinger = "0.4.1"
tracing = "0.1.34"
tracing-subscriber = "0.3.10"
riker = "0.4.2"
activitystreams = "0.7.0-alpha.18"
[[bin]]
name = "plume"
@@ -39,16 +44,16 @@ version = "0.4"
[dependencies.ctrlc]
features = ["termination"]
version = "3.2.2"
version = "3.1.2"
[dependencies.diesel]
features = ["r2d2", "chrono"]
version = "1.4.5"
version = "*"
[dependencies.multipart]
default-features = false
features = ["server"]
version = "0.18"
version = "0.16"
[dependencies.plume-api]
path = "plume-api"
@@ -61,11 +66,11 @@ path = "plume-models"
[dependencies.rocket_csrf]
git = "https://github.com/fdb-hiroshima/rocket_csrf"
rev = "29910f2829e7e590a540da3804336577b48c7b31"
rev = "4a72ea2ec716cb0b26188fb00bccf2ef7d1e031c"
[build-dependencies]
ructe = "0.14.0"
rsass = "0.24"
ructe = "0.6.2"
rsass = "0.9"
[features]
default = ["postgres"]
@@ -73,7 +78,6 @@ postgres = ["plume-models/postgres", "diesel/postgres"]
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
debug-mailer = []
test = []
search-lindera = ["plume-models/search-lindera"]
[workspace]
members = ["plume-api", "plume-cli", "plume-models", "plume-common", "plume-front", "plume-macro"]
+4 -5
View File
@@ -1,4 +1,4 @@
FROM rust:1-buster as builder
FROM rust:1-stretch as builder
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
@@ -10,8 +10,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
make \
openssl \
libssl-dev \
clang
libssl-dev
WORKDIR /scratch
COPY script/wasm-deps.sh .
@@ -19,7 +18,7 @@ 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
RUN cargo install cargo-web
COPY . .
@@ -28,7 +27,7 @@ RUN cargo install --path ./ --force --no-default-features --features postgres
RUN cargo install --path plume-cli --force --no-default-features --features postgres
RUN cargo clean
FROM debian:buster-slim
FROM debian:stretch-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
+3 -4
View File
@@ -1,4 +1,4 @@
FROM rust:1-buster
FROM rust:1-stretch
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
@@ -10,8 +10,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
make \
openssl \
libssl-dev\
clang
libssl-dev
WORKDIR /scratch
COPY script/wasm-deps.sh .
@@ -20,7 +19,7 @@ RUN chmod a+x ./wasm-deps.sh && sleep 1 && ./wasm-deps.sh
WORKDIR /app
COPY Cargo.toml Cargo.lock rust-toolchain ./
RUN cargo install diesel_cli --no-default-features --features postgres --version '=1.3.0'
RUN cargo install wasm-pack
RUN cargo install cargo-web
COPY . .
+2 -3
View File
@@ -30,11 +30,11 @@ A lot of features are still missing, but what is already here should be quite st
- **Media management**: you can upload pictures to illustrate your articles, but also audio files if you host a podcast, and manage them all from Plume.
- **Federation**: Plume is part of a network of interconnected websites called the Fediverse. Each of these websites (often called *instances*) have their own
rules and thematics, but they can all communicate with each other.
- **Collaborative writing**: invite other people to your blogs, and write articles together. (Not implemented yet, but will be in 1.0)
- **Collaborative writing**: invite other people to your blogs, and write articles together.
## Get involved
If you want to have regular news about the project, the best place is probably [our blog](https://fediverse.blog/~/PlumeDev), or our Matrix room: [`#plume-blog:matrix.org`](https://matrix.to/#/#plume-blog:matrix.org).
If you want to have regular news about the project, the best place is probably [our blog](https://fediverse.blog/~/PlumeDev), or our Matrix room: [`#plume:disroot.org`](https://riot.im/app/#/room/#plume:disroot.org).
If you want to contribute more, a good first step is to read [our contribution guides](https://docs.joinplu.me/contribute). We accept all kind of contribution:
@@ -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/).
+2 -36
View File
@@ -40,7 +40,7 @@ main header.article {
display: flex;
flex-direction: column;
justify-content: flex-end;
justify-content: end;
h1, .article-info {
text-align: center;
@@ -98,7 +98,7 @@ main article {
}
blockquote {
border-inline-start: 5px solid $gray;
border-left: 5px solid $gray;
margin: 1em auto;
padding: 0em 2em;
}
@@ -490,37 +490,3 @@ input:checked ~ .cw-container > .cw-text {
display: inline;
}
}
// Small screens
@media screen and (max-width: 600px) {
#plume-editor header {
flex-direction: column-reverse;
button {
flex: 0 0 0;
}
}
.popup {
top: 10vh;
bottom: 10vh;
left: 1vw;
right: 1vw;
}
main article {
margin: 2.5em .5em;
max-width: none;
}
main .article-meta > *, main .article-meta .comments, main .article-meta > .banner > * {
margin: 0 5%;
}
.bottom-bar {
align-items: center;
& > div:nth-child(2) {
margin: 0;
}
}
}
+97 -61
View File
@@ -183,8 +183,6 @@ p.error {
display: flex;
flex-direction: column;
position: relative;
min-width: 20em;
min-height: 20em;
margin: 1em;
@@ -194,39 +192,10 @@ p.error {
text-overflow: ellipsis;
footer.authors {
div {
float: left;
margin-right: 0.25em;
}
.likes { color: $red; }
.reshares { color: $primary; }
span.likes, span.resahres {
font-family: "Route159",serif;
font-size: 1em;
}
svg.feather {
width: 0.85em;
height: 0.85em;
}
}
> * {
margin: 20px;
}
.cover-link {
margin: 0;
&:hover {
opacity: 0.9;
}
}
.cover {
min-height: 10em;
background-position: center;
@@ -234,24 +203,12 @@ p.error {
margin: 0px;
}
header {
display: flex;
}
h3 {
flex-grow: 1;
margin: 0;
margin: 0.75em 20px;
font-family: $playfair;
font-size: 1.75em;
font-weight: normal;
line-height: 1.10;
display: inline-block;
position: relative;
a {
display: block;
width: 100%;
height: 100%;
padding-block-start: 0.5em;
transition: color 0.1s ease-in;
color: $text-color;
@@ -259,23 +216,13 @@ p.error {
}
}
.controls {
flex-shrink: 0;
text-align: end;
.button {
margin-top: 0;
margin-bottom: 0;
}
}
main {
flex: 1;
font-family: $lora;
font-size: 1em;
line-height: 1.25;
text-align: initial;
text-align: left;
overflow: hidden;
}
}
@@ -479,10 +426,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 +453,6 @@ figure {
margin: auto $horizontal-margin 2em;
overflow: auto;
display: flex;
justify-content: center;
a {
display: inline-block;
@@ -524,8 +469,91 @@ figure {
/// Small screens
@media screen and (max-width: 600px) {
body > main > *, .h-feed > * {
margin: 1em;
@keyframes menuOpening {
from {
transform: scaleX(0);
transform-origin: left;
opacity: 0;
}
to {
transform: scaleX(1);
transform-origin: left;
opacity: 1;
}
}
body > header {
flex-direction: column;
nav#menu {
display: inline-flex;
z-index: 21;
}
#content {
display: none;
appearance: none;
text-align: center;
z-index: 20;
}
}
body > header:focus-within #content, #content.show {
position: fixed;
display: flex;
flex-direction: column;
justify-content: flex-start;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
animation: 0.2s menuOpening;
&::before {
content: "";
position: absolute;
transform: skewX(-10deg);
top: 0;
left: -20%;
width: 100%;
height: 100%;
z-index: -10;
background: $primary;
}
> nav {
flex-direction: column;
align-items: flex-start;
a {
display: flex;
flex-direction: row;
align-items: center;
margin: 0;
padding: 1rem 1.5rem;
color: $background;
font-size: 1.4em;
font-weight: 300;
&.title { font-size: 1.8em; }
> *:first-child { width: 3rem; }
> img:first-child { height: 3rem; }
> *:last-child { margin-left: 1rem; }
> nav hr {
display: block;
margin: 0;
width: 100%;
border: solid $background 0.1rem;
}
.mobile-label { display: initial; }
}
}
}
main .article-meta {
@@ -573,7 +601,15 @@ figure {
margin: 0;
& > * {
max-width: 100% !important;
max-width: 100%;
}
}
.bottom-bar {
flex-direction: column;
align-items: center;
& > div {
margin: 0;
}
}
+1 -205
View File
@@ -101,108 +101,6 @@ body > header {
}
}
/// Small screens
@media screen and (max-width: 600px) {
@keyframes menuOpening {
from {
transform: scaleX(0);
transform-origin: left;
opacity: 0;
}
to {
transform: scaleX(1);
transform-origin: left;
opacity: 1;
}
}
@-webkit-keyframes menuOpening {
from {
-webkit-transform: scaleX(0);
transform-origin: left;
opacity: 0;
}
to {
-webkit-transform: scaleX(1);
transform-origin: left;
opacity: 1;
}
}
body > header {
flex-direction: column;
nav#menu {
display: inline-flex;
z-index: 21;
}
#content {
display: none;
appearance: none;
text-align: center;
z-index: 20;
}
}
body > header:focus-within #content, .show + #content {
position: fixed;
display: flex;
flex-direction: column;
justify-content: flex-start;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
animation: 0.2s menuOpening;
&::before {
content: "";
position: absolute;
transform: skewX(-10deg);
top: 0;
left: -20%;
width: 100%;
height: 100%;
z-index: -10;
background: $primary;
}
> nav {
flex-direction: column;
align-items: flex-start;
a {
display: flex;
flex-direction: row;
align-items: center;
margin: 0;
padding: 1rem 1.5rem;
color: $background;
font-size: 1.4em;
font-weight: 300;
&.title { font-size: 1.8em; }
> *:first-child { width: 3rem; }
> img:first-child { height: 3rem; }
> *:last-child { margin-left: 1rem; }
> nav hr {
display: block;
margin: 0;
width: 100%;
border: solid $background 0.1rem;
}
.mobile-label { display: initial; }
}
}
}
}
/* Only enable label animations on large screens */
@media screen and (min-width: 600px) {
header nav a {
@@ -216,7 +114,7 @@ body > header {
display: block;
position: absolute;
left: 50%;
transform: translateZ(0);
transform: translate(-50%, 0);
opacity: 0;
font-size: 0.9em;
white-space: nowrap;
@@ -233,105 +131,3 @@ body > header {
}
}
}
// Small screens
@media screen and (max-width: 600px) {
@keyframes menuOpening {
from {
transform: scaleX(0);
transform-origin: left;
opacity: 0;
}
to {
transform: scaleX(1);
transform-origin: left;
opacity: 1;
}
}
@-webkit-keyframes menuOpening {
from {
-webkit-transform: scaleX(0);
transform-origin: left;
opacity: 0;
}
to {
-webkit-transform: scaleX(1);
transform-origin: left;
opacity: 1;
}
}
body > header {
flex-direction: column;
nav#menu {
display: inline-flex;
z-index: 21;
}
#content {
display: none;
appearance: none;
text-align: center;
z-index: 20;
}
}
body > header:focus-within #content, .show + #content {
position: fixed;
display: flex;
flex-direction: column;
justify-content: flex-start;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
animation: 0.2s menuOpening;
&::before {
content: "";
position: absolute;
transform: skewX(-10deg);
top: 0;
left: -20%;
width: 100%;
height: 100%;
z-index: -10;
background: $primary;
}
> nav {
flex-direction: column;
align-items: flex-start;
a {
display: flex;
flex-direction: row;
align-items: center;
margin: 0;
padding: 1rem 1.5rem;
color: $background;
font-size: 1.4em;
font-weight: 300;
&.title { font-size: 1.8em; }
> *:first-child { width: 3rem; }
> img:first-child { height: 3rem; }
> *:last-child { margin-left: 1rem; }
> nav hr {
display: block;
margin: 0;
width: 100%;
border: solid $background 0.1rem;
}
.mobile-label { display: initial; }
}
}
}
}
+7 -7
View File
@@ -1,10 +1,10 @@
/* Color Scheme */
$gray: #f3f3f3;
$gray: #F3F3F3;
$black: #242424;
$white: #f8f8f8;
$purple: #7765e3;
$white: #F8F8F8;
$purple: #7765E3;
$lightpurple: #c2bbee;
$red: #e92f2f;
$red: #E92F2F;
$yellow: #ffe347;
$green: #23f0c7;
@@ -24,9 +24,9 @@ $margin: 0 $horizontal-margin;
/* Fonts */
$route159: "Shabnam", "Route159", serif;
$playfair: "Vazir", "Playfair Display", serif;
$lora: "Vazir", "Lora", serif;
$route159: "Route159", serif;
$playfair: "Playfair Display", serif;
$lora: "Lora", serif;
//Code Highlighting
$code-keyword-color: #45244a;
+9 -11
View File
@@ -1,14 +1,12 @@
/* color palette: https://coolors.co/23f0c7-ef767a-7765e3-6457a6-ffe347 */
@import url("./feather.css");
@import url("./fonts/Route159/Route159.css");
@import url("./fonts/Lora/Lora.css");
@import url("./fonts/Playfair_Display/PlayfairDisplay.css");
@import url("./fonts/Vazir_WOL/Vazir_WOL.css");
@import url("./fonts/Shabnam_WOL/Shabnam_WOL.css");
@import url('./feather.css');
@import url('./fonts/Route159/Route159.css');
@import url('./fonts/Lora/Lora.css');
@import url('./fonts/Playfair_Display/PlayfairDisplay.css');
@import "dark_variables";
@import "global";
@import "header";
@import "article";
@import "forms";
@import 'dark_variables';
@import 'global';
@import 'header';
@import 'article';
@import 'forms';
@@ -1,94 +0,0 @@
Copyright (c) 2015, Saber Rastikerdar (saber.rastikerdar@gmail.com),
Glyphs and data from Roboto font are licensed under the Apache License, Version 2.0.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -1,49 +0,0 @@
@font-face {
font-family: Shabnam;
src: url("Shabnam-WOL.eot");
src: url("Shabnam-WOL.eot?#iefix") format("embedded-opentype"),
url("Shabnam-WOL.woff2") format("woff2"),
url("Shabnam-WOL.woff") format("woff"),
url("Shabnam-WOL.ttf") format("truetype");
font-weight: normal;
}
@font-face {
font-family: Shabnam;
src: url("Shabnam-Bold-WOL.eot");
src: url("Shabnam-Bold-WOL.eot?#iefix") format("embedded-opentype"),
url("Shabnam-Bold-WOL.woff2") format("woff2"),
url("Shabnam-Bold-WOL.woff") format("woff"),
url("Shabnam-Bold-WOL.ttf") format("truetype");
font-weight: bold;
}
@font-face {
font-family: Shabnam;
src: url("Shabnam-Thin-WOL.eot");
src: url("Shabnam-Thin-WOL.eot?#iefix") format("embedded-opentype"),
url("Shabnam-Thin-WOL.woff2") format("woff2"),
url("Shabnam-Thin-WOL.woff") format("woff"),
url("Shabnam-Thin-WOL.ttf") format("truetype");
font-weight: 100;
}
@font-face {
font-family: Shabnam;
src: url("Shabnam-Light-WOL.eot");
src: url("Shabnam-Light-WOL.eot?#iefix") format("embedded-opentype"),
url("Shabnam-Light-WOL.woff2") format("woff2"),
url("Shabnam-Light-WOL.woff") format("woff"),
url("Shabnam-Light-WOL.ttf") format("truetype");
font-weight: 300;
}
@font-face {
font-family: Shabnam;
src: url("Shabnam-Medium-WOL.eot");
src: url("Shabnam-Medium-WOL.eot?#iefix") format("embedded-opentype"),
url("Shabnam-Medium-WOL.woff2") format("woff2"),
url("Shabnam-Medium-WOL.woff") format("woff"),
url("Shabnam-Medium-WOL.ttf") format("truetype");
font-weight: 500;
}
@@ -1,51 +0,0 @@
Changes by Saber Rastikerdar (saber.rastikerdar@gmail.com) are in public domain.
Glyphs and data from Roboto font are licensed under the Apache License, Version 2.0.
Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
Bitstream Vera Fonts Copyright
------------------------------
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is
a trademark of Bitstream, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of the fonts accompanying this license ("Fonts") and associated
documentation files (the "Font Software"), to reproduce and distribute the
Font Software, including without limitation the rights to use, copy, merge,
publish, distribute, and/or sell copies of the Font Software, and to permit
persons to whom the Font Software is furnished to do so, subject to the
following conditions:
The above copyright and trademark notices and this permission notice shall
be included in all copies of one or more of the Font Software typefaces.
The Font Software may be modified, altered, or added to, and in particular
the designs of glyphs or characters in the Fonts may be modified and
additional glyphs or characters may be added to the Fonts, only if the fonts
are renamed to names not containing either the words "Bitstream" or the word
"Vera".
This License becomes null and void to the extent applicable to Fonts or Font
Software that has been modified and is distributed under the "Bitstream
Vera" names.
The Font Software may be sold as part of a larger software package but no
copy of one or more of the Font Software typefaces may be sold by itself.
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
FONT SOFTWARE.
Except as contained in this notice, the names of Gnome, the Gnome
Foundation, and Bitstream Inc., shall not be used in advertising or
otherwise to promote the sale, use or other dealings in this Font Software
without prior written authorization from the Gnome Foundation or Bitstream
Inc., respectively. For further information, contact: fonts at gnome dot
org.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,65 +0,0 @@
@font-face {
font-family: Vazir;
src: url('Vazir-WOL.eot');
src: url('Vazir-WOL.eot?#iefix') format('embedded-opentype'),
url('Vazir-WOL.woff2') format('woff2'),
url('Vazir-WOL.woff') format('woff'),
url('Vazir-WOL.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: Vazir;
src: url('Vazir-Bold-WOL.eot');
src: url('Vazir-Bold-WOL.eot?#iefix') format('embedded-opentype'),
url('Vazir-Bold-WOL.woff2') format('woff2'),
url('Vazir-Bold-WOL.woff') format('woff'),
url('Vazir-Bold-WOL.ttf') format('truetype');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: Vazir;
src: url('Vazir-Black-WOL.eot');
src: url('Vazir-Black-WOL.eot?#iefix') format('embedded-opentype'),
url('Vazir-Black-WOL.woff2') format('woff2'),
url('Vazir-Black-WOL.woff') format('woff'),
url('Vazir-Black-WOL.ttf') format('truetype');
font-weight: 900;
font-style: normal;
}
@font-face {
font-family: Vazir;
src: url('Vazir-Medium-WOL.eot');
src: url('Vazir-Medium-WOL.eot?#iefix') format('embedded-opentype'),
url('Vazir-Medium-WOL.woff2') format('woff2'),
url('Vazir-Medium-WOL.woff') format('woff'),
url('Vazir-Medium-WOL.ttf') format('truetype');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: Vazir;
src: url('Vazir-Light-WOL.eot');
src: url('Vazir-Light-WOL.eot?#iefix') format('embedded-opentype'),
url('Vazir-Light-WOL.woff2') format('woff2'),
url('Vazir-Light-WOL.woff') format('woff'),
url('Vazir-Light-WOL.ttf') format('truetype');
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: Vazir;
src: url('Vazir-Thin-WOL.eot');
src: url('Vazir-Thin-WOL.eot?#iefix') format('embedded-opentype'),
url('Vazir-Thin-WOL.woff2') format('woff2'),
url('Vazir-Thin-WOL.woff') format('woff'),
url('Vazir-Thin-WOL.ttf') format('truetype');
font-weight: 100;
font-style: normal;
}
+9 -11
View File
@@ -1,14 +1,12 @@
/* color palette: https://coolors.co/23f0c7-ef767a-7765e3-6457a6-ffe347 */
@import url("./feather.css");
@import url("./fonts/Route159/Route159.css");
@import url("./fonts/Lora/Lora.css");
@import url("./fonts/Playfair_Display/PlayfairDisplay.css");
@import url("./fonts/Vazir_WOL/Vazir_WOL.css");
@import url("./fonts/Shabnam_WOL/Shabnam_WOL.css");
@import url('./feather.css');
@import url('./fonts/Route159/Route159.css');
@import url('./fonts/Lora/Lora.css');
@import url('./fonts/Playfair_Display/PlayfairDisplay.css');
@import "variables";
@import "global";
@import "header";
@import "article";
@import "forms";
@import 'variables';
@import 'global';
@import 'header';
@import 'article';
@import 'forms';
+20 -18
View File
@@ -1,6 +1,8 @@
extern crate rsass;
extern crate ructe;
use ructe::Ructe;
use std::process::{Command, Stdio};
use std::{ffi::OsStr, fs::*, io::Write, path::*};
use std::{env, ffi::OsStr, fs::*, io::Write, path::*};
fn compute_static_hash() -> String {
//"find static/ -type f ! -path 'static/media/*' | sort | xargs stat -c'%n %Y' | openssl dgst -r"
@@ -41,19 +43,25 @@ 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");
let cache_id = &compute_static_hash()[..8];
println!("cargo:rerun-if-changed=plume-front/pkg/plume_front_bg.wasm");
copy(
"plume-front/pkg/plume_front_bg.wasm",
"static/plume_front_bg.wasm",
println!("cargo:rerun-if-changed=target/deploy/plume-front.wasm");
copy("target/deploy/plume-front.wasm", "static/plume-front.wasm")
.and_then(|_| read_to_string("target/deploy/plume-front.js"))
.and_then(|js| {
write(
"static/plume-front.js",
js.replace(
"\"plume-front.wasm\"",
&format!("\"/static/cached/{}/plume-front.wasm\"", cache_id),
),
)
.and_then(|_| copy("plume-front/pkg/plume_front.js", "static/plume_front.js"))
})
.ok();
println!("cargo:rustc-env=CACHE_ID={}", cache_id)
@@ -97,12 +105,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,13 +128,7 @@ 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()
},
)
&rsass::compile_scss_file(path, rsass::OutputStyle::Compressed)
.expect("SCSS compilation error"),
)?;
+2 -2
View File
@@ -1,5 +1,5 @@
"project_id": 352097
"api_token_env": "CROWDIN_API_KEY"
"project_identifier": "plume"
"api_key_env": CROWDIN_API_KEY
preserve_hierarchy: true
files:
- source: /po/plume/plume.pot
@@ -1 +0,0 @@
DROP INDEX medias_index_file_path;
@@ -1 +0,0 @@
CREATE INDEX medias_index_file_path ON medias (file_path);
@@ -1 +0,0 @@
DROP TABLE email_signups;
@@ -1,9 +0,0 @@
CREATE TABLE email_signups (
id SERIAL PRIMARY KEY,
email VARCHAR NOT NULL,
token VARCHAR NOT NULL,
expiration_date TIMESTAMP NOT NULL
);
CREATE INDEX email_signups_token ON email_signups (token);
CREATE UNIQUE INDEX email_signups_token_requests_email ON email_signups (email);
@@ -1,4 +0,0 @@
ALTER TABLE email_blocklist ALTER COLUMN notification_text DROP NOT NULL;
ALTER TABLE email_blocklist ALTER COLUMN notify_user DROP NOT NULL;
ALTER TABLE email_blocklist ALTER COLUMN note DROP NOT NULL;
ALTER TABLE email_blocklist ALTER COLUMN email_address DROP NOT NULL;
@@ -1,4 +0,0 @@
ALTER TABLE email_blocklist ALTER COLUMN email_address SET NOT NULL;
ALTER TABLE email_blocklist ALTER COLUMN note SET NOT NULL;
ALTER TABLE email_blocklist ALTER COLUMN notify_user SET NOT NULL;
ALTER TABLE email_blocklist ALTER COLUMN notification_text SET NOT NULL;
@@ -1 +0,0 @@
DROP INDEX medias_index_file_path;
@@ -1 +0,0 @@
CREATE INDEX medias_index_file_path ON medias (file_path);
@@ -1 +0,0 @@
DROP TABLE email_signups;
@@ -1,9 +0,0 @@
CREATE TABLE email_signups (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
email VARCHAR NOT NULL,
token VARCHAR NOT NULL,
expiration_date TIMESTAMP NOT NULL
);
CREATE INDEX email_signups_token ON email_signups (token);
CREATE UNIQUE INDEX email_signups_token_requests_email ON email_signups (email);
@@ -1,9 +0,0 @@
CREATE TABLE email_blocklist2(id INTEGER PRIMARY KEY,
email_address TEXT UNIQUE,
note TEXT,
notify_user BOOLEAN DEFAULT FALSE,
notification_text TEXT);
INSERT INTO email_blocklist2 SELECT * FROM email_blocklist;
DROP TABLE email_blocklist;
ALTER TABLE email_blocklist2 RENAME TO email_blocklist;
@@ -1,9 +0,0 @@
CREATE TABLE email_blocklist2(id INTEGER PRIMARY KEY,
email_address TEXT UNIQUE NOT NULL,
note TEXT NOT NULL,
notify_user BOOLEAN DEFAULT FALSE NOT NULL,
notification_text TEXT NOT NULL);
INSERT INTO email_blocklist2 SELECT * FROM email_blocklist;
DROP TABLE email_blocklist;
ALTER TABLE email_blocklist2 RENAME TO email_blocklist;
+1 -2
View File
@@ -1,8 +1,7 @@
[package]
name = "plume-api"
version = "0.7.2"
version = "0.4.0"
authors = ["Plume contributors"]
edition = "2018"
[dependencies]
serde = "1.0"
-2
View File
@@ -1,2 +0,0 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
+1
View File
@@ -1,3 +1,4 @@
extern crate serde;
#[macro_use]
extern crate serde_derive;
+4 -6
View File
@@ -1,8 +1,7 @@
[package]
name = "plume-cli"
version = "0.7.2"
version = "0.4.0"
authors = ["Plume contributors"]
edition = "2018"
[[bin]]
name = "plm"
@@ -10,12 +9,12 @@ path = "src/main.rs"
[dependencies]
clap = "2.33"
dotenv = "0.15"
rpassword = "6.0.1"
dotenv = "0.14"
rpassword = "4.0"
[dependencies.diesel]
features = ["r2d2", "chrono"]
version = "1.4.5"
version = "*"
[dependencies.plume-models]
path = "../plume-models"
@@ -23,4 +22,3 @@ path = "../plume-models"
[features]
postgres = ["plume-models/postgres", "diesel/postgres"]
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
search-lindera = ["plume-models/search-lindera"]
-2
View File
@@ -1,2 +0,0 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
-2
View File
@@ -68,6 +68,4 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
},
)
.expect("Couldn't save instance");
Instance::cache_local(conn);
Instance::create_local_instance_user(conn).expect("Couldn't save local instance user");
}
+7 -1
View File
@@ -1,3 +1,9 @@
extern crate clap;
extern crate diesel;
extern crate dotenv;
extern crate plume_models;
extern crate rpassword;
use clap::App;
use diesel::Connection;
use plume_models::{instance::Instance, Connection as Conn, CONFIG};
@@ -25,7 +31,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)) => {
+3 -4
View File
@@ -82,7 +82,7 @@ fn init<'a>(args: &ArgMatches<'a>, conn: &Connection) {
}
};
if can_do || force {
let searcher = Searcher::create(&path, &CONFIG.search_tokenizers).unwrap();
let searcher = Searcher::create(&path).unwrap();
refill(args, conn, Some(searcher));
} else {
eprintln!(
@@ -98,15 +98,14 @@ fn refill<'a>(args: &ArgMatches<'a>, conn: &Connection, searcher: Option<Searche
Some(path) => Path::new(path).join("search_index"),
None => Path::new(&CONFIG.search_index).to_path_buf(),
};
let searcher =
searcher.unwrap_or_else(|| Searcher::open(&path, &CONFIG.search_tokenizers).unwrap());
let searcher = searcher.unwrap_or_else(|| Searcher::open(&path).unwrap());
searcher.fill(conn).expect("Couldn't import post");
println!("Commiting result");
searcher.commit();
}
fn unlock(args: &ArgMatches) {
fn unlock<'a>(args: &ArgMatches<'a>) {
let path = match args.value_of("path") {
None => Path::new(&CONFIG.search_index),
Some(x) => Path::new(x),
+2 -1
View File
@@ -1,6 +1,7 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use plume_models::{instance::Instance, users::*, Connection};
use rpassword;
use std::io::{self, Write};
pub fn command<'a, 'b>() -> App<'a, 'b> {
@@ -131,7 +132,7 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
role,
&bio,
email,
Some(User::hash_pass(&password).expect("Couldn't hash password")),
User::hash_pass(&password).expect("Couldn't hash password"),
)
.expect("Couldn't save new user");
}
+16 -28
View File
@@ -1,31 +1,26 @@
[package]
name = "plume-common"
version = "0.7.2"
version = "0.4.0"
authors = ["Plume contributors"]
edition = "2018"
[dependencies]
activitypub = "0.1.1"
activitystreams-derive = "0.1.1"
activitystreams-traits = "0.1.0"
array_tool = "1.0"
base64 = "0.13"
heck = "0.4.0"
hex = "0.4"
openssl = "0.10.40"
rocket = "0.4.6"
reqwest = { version = "0.11.10", features = ["blocking", "json", "socks"] }
base64 = "0.10"
heck = "0.3.0"
hex = "0.3"
hyper = "0.12.33"
openssl = "0.10.22"
rocket = "0.4.0"
reqwest = "0.9"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0.81"
shrinkwraprs = "0.3.0"
syntect = "4.5.0"
regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] }
tracing = "0.1.34"
askama_escape = "0.10.3"
activitystreams = "0.7.0-alpha.18"
activitystreams-ext = "0.1.0-alpha.2"
url = "2.2.2"
flume = "0.10.12"
tokio = { version = "1.18.2", features = ["full"] }
futures = "0.3.21"
serde_json = "1.0"
shrinkwraprs = "0.2.1"
syntect = "3.3"
tokio = "0.1.22"
[dependencies.chrono]
features = ["serde"]
@@ -33,11 +28,4 @@ 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.10.0"
[features]
version = "0.2.0"
-2
View File
@@ -1,2 +0,0 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
+85 -267
View File
@@ -1,8 +1,6 @@
use reqwest;
use reqwest::header::{HeaderValue, ACCEPT};
use std::fmt::Debug;
use super::{request, sign::Signer};
/// Represents an ActivityPub inbox.
///
/// It routes an incoming Activity through the registered handlers.
@@ -10,51 +8,9 @@ use super::{request, sign::Signer};
/// # Example
///
/// ```rust
/// # use activitystreams::{prelude::*, base::Base, actor::Person, activity::{Announce, Create}, object::Note, iri_string::types::IriString};
/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
/// # use once_cell::sync::Lazy;
/// # extern crate activitypub;
/// # use activitypub::{actor::Person, activity::{Announce, Create}, object::Note};
/// # use plume_common::activity_pub::inbox::*;
/// # use plume_common::activity_pub::sign::{gen_keypair, Error as SignError, Result as SignResult, Signer};
/// #
/// # static MY_SIGNER: Lazy<MySigner> = Lazy::new(|| MySigner::new());
/// #
/// # struct MySigner {
/// # public_key: String,
/// # private_key: String,
/// # }
/// #
/// # impl MySigner {
/// # fn new() -> Self {
/// # let (pub_key, priv_key) = gen_keypair();
/// # Self {
/// # public_key: String::from_utf8(pub_key).unwrap(),
/// # private_key: String::from_utf8(priv_key).unwrap(),
/// # }
/// # }
/// # }
/// #
/// # impl Signer for MySigner {
/// # fn get_key_id(&self) -> String {
/// # "mysigner".into()
/// # }
/// #
/// # fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> {
/// # let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
/// # .unwrap();
/// # let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
/// # signer.update(to_sign.as_bytes()).unwrap();
/// # signer.sign_to_vec().map_err(|_| SignError())
/// # }
/// #
/// # fn verify(&self, data: &str, signature: &[u8]) -> SignResult<bool> {
/// # let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
/// # .unwrap();
/// # let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
/// # verifier.update(data.as_bytes()).unwrap();
/// # verifier.verify(&signature).map_err(|_| SignError())
/// # }
/// # }
/// #
/// # struct User;
/// # impl FromId<()> for User {
/// # type Error = ();
@@ -67,10 +23,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 +42,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 = ();
@@ -112,20 +60,19 @@ use super::{request, sign::Signer};
/// # }
/// # }
/// #
/// # 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());
/// # let mut act = Create::default();
/// # act.object_props.set_id_string(String::from("https://test.ap/activity")).unwrap();
/// # let mut person = Person::default();
/// # person.object_props.set_id_string(String::from("https://test.ap/actor")).unwrap();
/// # act.create_props.set_actor_object(person).unwrap();
/// # act.create_props.set_object_object(Note::default()).unwrap();
/// # let activity_json = serde_json::to_value(act).unwrap();
/// #
/// # let conn = ();
/// #
/// let result: Result<(), ()> = Inbox::handle(&conn, activity_json)
/// .with::<User, Announce, Message>(None)
/// .with::<User, Create, Message>(None)
/// .with::<User, Announce, Message>()
/// .with::<User, Create, Message>()
/// .done();
/// ```
pub enum Inbox<'a, C, E, R>
@@ -197,37 +144,31 @@ where
}
/// Registers an handler on this Inbox.
pub fn with<A, V, M>(self, proxy: Option<&reqwest::Proxy>) -> Self
pub fn with<A, V, M>(self) -> Inbox<'a, C, E, R>
where
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, 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, 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, act, InboxError::InvalidActor(None)),
};
if Self::is_spoofed_activity(&actor_id, &act) {
return Self::NotHandled(ctx, act, InboxError::InvalidObject(None));
}
// Transform this actor to a model (see FromId for details about the from_id function)
let actor = match A::from_id(
ctx,
&actor_id,
serde_json::from_value(act["actor"].clone()).ok(),
proxy,
) {
Ok(a) => a,
// If the actor was not found, go to the next handler
@@ -235,39 +176,38 @@ where
if let Some(json) = json {
act["actor"] = json;
}
return Self::NotHandled(ctx, act, InboxError::InvalidActor(Some(e)));
return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(Some(e)));
}
};
// Same logic for "object"
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, act, InboxError::InvalidObject(None)),
};
let obj = match M::from_id(
ctx,
&obj_id,
serde_json::from_value(act["object"].clone()).ok(),
proxy,
) {
Ok(o) => o,
Err((json, e)) => {
if let Some(json) = json {
act["object"] = json;
}
return Self::NotHandled(ctx, act, InboxError::InvalidObject(Some(e)));
return Inbox::NotHandled(ctx, 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, act, e)
}
} else {
self
@@ -282,26 +222,6 @@ where
Inbox::Failed(err) => Err(err),
}
}
fn is_spoofed_activity(actor_id: &str, act: &serde_json::Value) -> bool {
use serde_json::Value::{Array, Object, String};
let attributed_to = act["object"].get("attributedTo");
if attributed_to.is_none() {
return false;
}
let attributed_to = attributed_to.unwrap();
match attributed_to {
Array(v) => v.iter().all(|i| match i {
String(s) => s != actor_id,
Object(obj) => obj.get("id").map_or(true, |s| s != actor_id),
_ => false,
}),
String(s) => s != actor_id,
Object(obj) => obj.get("id").map_or(true, |s| s != actor_id),
_ => false,
}
}
}
/// Get the ActivityPub ID of a JSON value.
@@ -333,7 +253,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.
///
@@ -347,26 +267,36 @@ pub trait FromId<C>: Sized {
ctx: &C,
id: &str,
object: Option<Self::Object>,
proxy: Option<&reqwest::Proxy>,
) -> Result<Self, (Option<serde_json::Value>, Self::Error)> {
match Self::from_db(ctx, id) {
Ok(x) => Ok(x),
_ => match object {
Some(o) => Self::from_activity(ctx, o).map_err(|e| (None, e)),
None => Self::from_activity(ctx, Self::deref(id, proxy.cloned())?)
.map_err(|e| (None, e)),
None => Self::from_activity(ctx, Self::deref(id)?).map_err(|e| (None, e)),
},
}
}
/// Dereferences an ID
fn deref(
id: &str,
proxy: Option<reqwest::Proxy>,
) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
request::get(id, Self::get_sender(), proxy)
fn deref(id: &str) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
reqwest::ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()
.map_err(|_| (None, InboxError::DerefError.into()))?
.get(id)
.header(
ACCEPT,
HeaderValue::from_str(
&super::ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)
.map_err(|_| (None, InboxError::DerefError.into()))?,
)
.send()
.map_err(|_| (None, InboxError::DerefError))
.and_then(|r| {
.and_then(|mut r| {
let json: serde_json::Value = r
.json()
.map_err(|_| (None, InboxError::InvalidObject(None)))?;
@@ -381,8 +311,6 @@ pub trait FromId<C>: Sized {
/// Tries to find a `Self` with a given ID (`id`), using `ctx` (a database)
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 +346,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 +361,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 +382,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 +398,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;
@@ -548,57 +423,7 @@ where
#[cfg(test)]
mod tests {
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 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())
}
}
use activitypub::{activity::*, actor::Person, object::Note};
struct MyActor;
impl FromId<()> for MyActor {
@@ -606,15 +431,11 @@ mod tests {
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 +455,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,15 +503,21 @@ 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
}
@@ -702,7 +525,7 @@ mod tests {
fn test_inbox_basic() {
let act = serde_json::to_value(build_create()).unwrap();
let res: Result<(), ()> = Inbox::handle(&(), act)
.with::<MyActor, Create, MyObject>(None)
.with::<MyActor, Create, MyObject>()
.done();
assert!(res.is_ok());
}
@@ -711,10 +534,10 @@ mod tests {
fn test_inbox_multi_handlers() {
let act = serde_json::to_value(build_create()).unwrap();
let res: Result<(), ()> = Inbox::handle(&(), act)
.with::<MyActor, Announce, MyObject>(None)
.with::<MyActor, Delete, MyObject>(None)
.with::<MyActor, Create, MyObject>(None)
.with::<MyActor, Like, MyObject>(None)
.with::<MyActor, Announce, MyObject>()
.with::<MyActor, Delete, MyObject>()
.with::<MyActor, Create, MyObject>()
.with::<MyActor, Like, MyObject>()
.done();
assert!(res.is_ok());
}
@@ -724,23 +547,13 @@ mod tests {
let act = serde_json::to_value(build_create()).unwrap();
// Create is not handled by this inbox
let res: Result<(), ()> = Inbox::handle(&(), act)
.with::<MyActor, Announce, MyObject>(None)
.with::<MyActor, Like, MyObject>(None)
.with::<MyActor, Announce, MyObject>()
.with::<MyActor, Like, MyObject>()
.done();
assert!(res.is_err());
}
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 +562,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
}
}
@@ -778,13 +596,13 @@ mod tests {
let act = serde_json::to_value(build_create()).unwrap();
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
.with::<FailingActor, Create, MyObject>(None)
.with::<FailingActor, Create, MyObject>()
.done();
assert!(res.is_err());
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
.with::<FailingActor, Create, MyObject>(None)
.with::<MyActor, Create, MyObject>(None)
.with::<FailingActor, Create, MyObject>()
.with::<MyActor, Create, MyObject>()
.done();
assert!(res.is_ok());
}
+62 -556
View File
@@ -1,28 +1,14 @@
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::r#async::ClientBuilder;
use rocket::{
http::Status,
request::{FromRequest, Request},
response::{Responder, Response},
Outcome,
};
use tokio::{
runtime,
time::{sleep, Duration},
};
use tracing::{debug, warn};
use serde_json;
use tokio::prelude::*;
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,8 +63,8 @@ impl<T> ActivityStream<T> {
}
}
impl<'r, O: serde::Serialize> Responder<'r> for ActivityStream<O> {
fn respond_to(self, request: &Request<'_>) -> Result<Response<'r>, Status> {
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();
serde_json::to_string(&json).respond_to(request).map(|r| {
@@ -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>)
pub fn broadcast<S, A, T, C>(sender: &S, act: A, to: Vec<T>)
where
S: sign::Signer,
A: Activity + serde::Serialize,
A: Activity,
T: inbox::AsActor<C>,
{
let boxes = to
@@ -147,79 +130,36 @@ where
.sign(sender)
.expect("activity_pub::broadcast: signature error");
let client = if let Some(proxy) = proxy {
ClientBuilder::new().proxy(proxy)
} else {
ClientBuilder::new()
}
let mut rt = tokio::runtime::current_thread::Runtime::new()
.expect("Error while initializing tokio runtime for federation");
let client = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(5))
.build()
.expect("Can't build client");
let rt = runtime::Builder::new_current_thread()
.enable_all()
.build()
.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;
}
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(
rt.spawn(
client
.post(&inbox)
.headers(headers.clone())
.header(
"Signature",
request::signature(sender, &headers, ("post", url.path(), url.query()))
request::signature(sender, &headers)
.expect("activity_pub::broadcast: request signature error"),
)
.body(body)
.send()
.and_then(|r| r.into_body().concat2())
.map(move |response| {
println!("Successfully sent activity to inbox ({})", inbox);
println!("Response: \"{:?}\"\n", response)
})
.map_err(|e| println!("Error while sending to inbox ({:?})", e)),
);
let request_builder = client.post(&inbox).headers(headers.clone()).body(body);
let _ = tx.send_async(request_builder).await;
}
drop(tx);
join_all(handles).await;
});
rt.run().unwrap();
}
#[derive(Shrinkwrap, Clone, Serialize, Deserialize)]
@@ -241,193 +181,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,300 +228,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,
};
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 de_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 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 {}
+23 -151
View File
@@ -1,42 +1,15 @@
use base64;
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;
use crate::activity_pub::sign::Signer;
use crate::activity_pub::{ap_accept_header, AP_CONTENT_TYPE};
use activity_pub::sign::Signer;
use activity_pub::{ap_accept_header, AP_CONTENT_TYPE};
const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION"));
#[derive(Debug)]
pub struct Error();
impl From<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 {
@@ -89,16 +62,16 @@ impl Digest {
base64::decode(&self.0[pos..]).expect("Digest::value: invalid encoding error")
}
pub fn from_header(dig: &str) -> Result<Self, Error> {
pub fn from_header(dig: &str) -> Result<Self, ()> {
if let Some(pos) = dig.find('=') {
let pos = pos + 1;
if base64::decode(&dig[pos..]).is_ok() {
Ok(Digest(dig.to_owned()))
} else {
Err(Error())
Err(())
}
} else {
Err(Error())
Err(())
}
}
@@ -137,47 +110,27 @@ pub fn headers() -> HeaderMap {
headers
}
type Method<'a> = &'a str;
type Path<'a> = &'a str;
type Query<'a> = &'a str;
type RequestTarget<'a> = (Method<'a>, Path<'a>, Option<Query<'a>>);
pub fn signature(
signer: &dyn Signer,
headers: &HeaderMap,
request_target: RequestTarget,
) -> Result<HeaderValue, Error> {
let (method, path, query) = request_target;
let origin_form = if let Some(query) = query {
format!("{}?{}", path, query)
} else {
path.to_string()
};
let mut headers_vec = Vec::with_capacity(headers.len());
for (h, v) in headers.iter() {
let v = v.to_str();
if v.is_err() {
warn!("invalid header error: {:?}", v.unwrap_err());
return Err(Error());
}
headers_vec.push((h.as_str().to_lowercase(), v.expect("Unreachable")));
}
let request_target = format!("{} {}", method.to_lowercase(), origin_form);
headers_vec.push(("(request-target)".to_string(), &request_target));
let signed_string = headers_vec
pub fn signature<S: Signer>(signer: &S, headers: &HeaderMap) -> Result<HeaderValue, ()> {
let signed_string = headers
.iter()
.map(|(h, v)| format!("{}: {}", h, v))
.map(|(h, v)| {
format!(
"{}: {}",
h.as_str().to_lowercase(),
v.to_str()
.expect("request::signature: invalid header error")
)
})
.collect::<Vec<String>>()
.join("\n");
let signed_headers = headers_vec
let signed_headers = headers
.iter()
.map(|(h, _)| h.as_ref())
.map(|(h, _)| h.as_str())
.collect::<Vec<&str>>()
.join(" ");
.join(" ")
.to_lowercase();
let data = signer.sign(&signed_string).map_err(|_| Error())?;
let data = signer.sign(&signed_string).map_err(|_| ())?;
let sign = base64::encode(&data);
HeaderValue::from_str(&format!(
@@ -185,86 +138,5 @@ pub fn signature(
key_id = signer.get_key_id(),
signed_headers = signed_headers,
signature = sign
)).map_err(|_| Error())
}
pub fn get(url_str: &str, sender: &dyn Signer, proxy: Option<Proxy>) -> Result<Response, Error> {
let mut headers = headers();
let url = Url::parse(url_str)?;
if !url.has_host() {
return Err(Error());
}
let host_header_value = HeaderValue::from_str(url.host_str().expect("Unreachable"))?;
headers.insert(HOST, host_header_value);
if let Some(proxy) = proxy {
ClientBuilder::new().proxy(proxy)
} else {
ClientBuilder::new()
}
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()?
.get(url_str)
.headers(headers.clone())
.header(
"Signature",
signature(sender, &headers, ("get", url.path(), url.query()))?,
)
.send()
.map_err(|_| Error())
}
#[cfg(test)]
mod tests {
use super::signature;
use crate::activity_pub::sign::{gen_keypair, Error, Result, Signer};
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
use reqwest::header::HeaderMap;
struct MySigner {
public_key: String,
private_key: String,
}
impl MySigner {
fn new() -> Self {
let (pub_key, priv_key) = gen_keypair();
Self {
public_key: String::from_utf8(pub_key).unwrap(),
private_key: String::from_utf8(priv_key).unwrap(),
}
}
}
impl Signer for MySigner {
fn get_key_id(&self) -> String {
"mysigner".into()
}
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
.unwrap();
let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
signer.update(to_sign.as_bytes()).unwrap();
signer.sign_to_vec().map_err(|_| Error())
}
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
.unwrap();
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
verifier.update(data.as_bytes()).unwrap();
verifier.verify(&signature).map_err(|_| Error())
}
}
#[test]
fn test_signature_request_target() {
let signer = MySigner::new();
let headers = HeaderMap::new();
let result = signature(&signer, &headers, ("post", "/inbox", None)).unwrap();
let fields: Vec<&str> = result.to_str().unwrap().split(",").collect();
assert_eq!(r#"headers="(request-target)""#, fields[2]);
let sign = &fields[3][11..(fields[3].len() - 1)];
assert!(signer.verify("post /inbox", sign.as_bytes()).is_ok());
}
)).map_err(|_| ())
}

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