Compare commits

...

787 Commits

Author SHA1 Message Date
KitaitiMakoto 620726cc25 Merge pull request 'Update crates' (#1107) from update-crates into main
Reviewed-on: Plume/Plume#1107
2 years ago
Kitaiti Makoto 0eef7c0b89 Merge remote-tracking branch 'github/dependabot/cargo/web-sys-0.3.58' into update-crates 2 years ago
dependabot[bot] 321e40ea3f
Bump web-sys from 0.3.57 to 0.3.58
Bumps [web-sys](https://github.com/rustwasm/wasm-bindgen) from 0.3.57 to 0.3.58.
- [Release notes](https://github.com/rustwasm/wasm-bindgen/releases)
- [Changelog](https://github.com/rustwasm/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustwasm/wasm-bindgen/commits)

---
updated-dependencies:
- dependency-name: web-sys
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
dependabot[bot] a218b4ea4f
Bump reqwest from 0.11.10 to 0.11.11
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.11.10 to 0.11.11.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.11.10...v0.11.11)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto 9613ccd0c3 Merge pull request 'Fix Cargo.toml' (#1106) from fix-cargo into main
Reviewed-on: Plume/Plume#1106
2 years ago
Kitaiti Makoto 9493c1ad06 Fix Cargo.toml 2 years ago
KitaitiMakoto e92ac1a13f Merge pull request 'Update crates' (#1105) from update-crates into main
Reviewed-on: Plume/Plume#1105
2 years ago
Kitaiti Makoto 1517b4d91e Merge remote-tracking branch 'github/dependabot/cargo/js-sys-0.3.58' into update-crates 2 years ago
dependabot[bot] 38cc4c043d
Bump js-sys from 0.3.57 to 0.3.58
Bumps [js-sys](https://github.com/rustwasm/wasm-bindgen) from 0.3.57 to 0.3.58.
- [Release notes](https://github.com/rustwasm/wasm-bindgen/releases)
- [Changelog](https://github.com/rustwasm/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustwasm/wasm-bindgen/commits)

---
updated-dependencies:
- dependency-name: js-sys
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
dependabot[bot] 05c1d727dc
Bump wasm-bindgen from 0.2.80 to 0.2.81
Bumps [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) from 0.2.80 to 0.2.81.
- [Release notes](https://github.com/rustwasm/wasm-bindgen/releases)
- [Changelog](https://github.com/rustwasm/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustwasm/wasm-bindgen/commits)

---
updated-dependencies:
- dependency-name: wasm-bindgen
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto 84645c7ed9 Merge pull request 'Update crates' (#1103) from update-crates into main
Reviewed-on: Plume/Plume#1103
2 years ago
KitaitiMakoto 7c505bde7f Merge pull request 'Blog's header buttons margin fix in RTL' (#1093) from mskf1383/Plume:main into main
Reviewed-on: Plume/Plume#1093
2 years ago
Kitaiti Makoto 9f543f1b6b Merge remote-tracking branch 'github/dependabot/cargo/flume-0.10.13' into update-crates 2 years ago
Kitaiti Makoto 0f7b882749 Merge remote-tracking branch 'github/dependabot/cargo/tracing-0.1.35' into update-crates 2 years ago
Kitaiti Makoto f9f4375a40 Merge remote-tracking branch 'github/dependabot/cargo/tokio-1.19.2' into update-crates 2 years ago
dependabot[bot] 12c2848cc7
Bump flume from 0.10.12 to 0.10.13
Bumps [flume](https://github.com/zesterer/flume) from 0.10.12 to 0.10.13.
- [Release notes](https://github.com/zesterer/flume/releases)
- [Changelog](https://github.com/zesterer/flume/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zesterer/flume/commits)

---
updated-dependencies:
- dependency-name: flume
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
dependabot[bot] 4cfb3e2494
Bump tracing from 0.1.34 to 0.1.35
Bumps [tracing](https://github.com/tokio-rs/tracing) from 0.1.34 to 0.1.35.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-0.1.34...tracing-0.1.35)

---
updated-dependencies:
- dependency-name: tracing
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
dependabot[bot] 090b0a6f0d
Bump tokio from 1.18.2 to 1.19.2
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.18.2 to 1.19.2.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/commits)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
MohammadSaleh Kamyab 4502b77094 Failsafe 2 years ago
MohammadSaleh Kamyab 8f5a86206a Merge branch 'main' into main 2 years ago
dependabot[bot] 340157f80d
Bump scheduled-thread-pool from 0.2.5 to 0.2.6
Bumps [scheduled-thread-pool](https://github.com/sfackler/scheduled-thread-pool) from 0.2.5 to 0.2.6.
- [Release notes](https://github.com/sfackler/scheduled-thread-pool/releases)
- [Commits](https://github.com/sfackler/scheduled-thread-pool/compare/v0.2.5...v0.2.6)

---
updated-dependencies:
- dependency-name: scheduled-thread-pool
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto 16b10695df Merge pull request 'Bump rocket_contrib from 0.4.10 to 0.4.11' (#1101) from rocket_contrib-0.4.11 into main
Reviewed-on: Plume/Plume#1101
2 years ago
Kitaiti Makoto b8eb631aa3 Merge remote-tracking branch 'origin/main' into rocket_contrib-0.4.11 2 years ago
KitaitiMakoto 5c9094fede Merge pull request 'Bump rocket from 0.4.10 to 0.4.11' (#1100) from rocket-0.4.11 into main
Reviewed-on: Plume/Plume#1100
2 years ago
dependabot[bot] 4e2ca515ce
Bump rocket_contrib from 0.4.10 to 0.4.11
Bumps [rocket_contrib](https://github.com/SergioBenitez/Rocket) from 0.4.10 to 0.4.11.
- [Release notes](https://github.com/SergioBenitez/Rocket/releases)
- [Changelog](https://github.com/SergioBenitez/Rocket/blob/master/CHANGELOG.md)
- [Commits](https://github.com/SergioBenitez/Rocket/commits)

---
updated-dependencies:
- dependency-name: rocket_contrib
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
dependabot[bot] eccfbd3fbc
Bump rocket from 0.4.10 to 0.4.11
Bumps [rocket](https://github.com/SergioBenitez/Rocket) from 0.4.10 to 0.4.11.
- [Release notes](https://github.com/SergioBenitez/Rocket/releases)
- [Changelog](https://github.com/SergioBenitez/Rocket/blob/master/CHANGELOG.md)
- [Commits](https://github.com/SergioBenitez/Rocket/commits)

---
updated-dependencies:
- dependency-name: rocket
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto 8408342b5d Merge pull request 'Bump once_cell from 1.11.0 to 1.12.0' (#1099) from once_cell-1.12.0 into main
Reviewed-on: Plume/Plume#1099
2 years ago
dependabot[bot] c47921bb25
Bump once_cell from 1.11.0 to 1.12.0
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.11.0 to 1.12.0.
- [Release notes](https://github.com/matklad/once_cell/releases)
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.11.0...v1.12.0)

---
updated-dependencies:
- dependency-name: once_cell
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto 03f470f04c Merge pull request 'Bump regex-syntax from 0.6.25 to 0.6.26' (#1096) from regex-syntax-0.6.26 into main
Reviewed-on: Plume/Plume#1096
2 years ago
KitaitiMakoto 5770c3b85b Merge branch 'main' into regex-syntax-0.6.26 2 years ago
KitaitiMakoto c92f46b2c9 Merge pull request 'Bump once_cell from 1.10.0 to 1.11.0' (#1095) from once_cell-1.11.0 into main
Reviewed-on: Plume/Plume#1095
2 years ago
dependabot[bot] 69eba69528
Bump regex-syntax from 0.6.25 to 0.6.26
Bumps [regex-syntax](https://github.com/rust-lang/regex) from 0.6.25 to 0.6.26.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/commits)

---
updated-dependencies:
- dependency-name: regex-syntax
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
dependabot[bot] c302d842e0
Bump once_cell from 1.10.0 to 1.11.0
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.10.0 to 1.11.0.
- [Release notes](https://github.com/matklad/once_cell/releases)
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.10.0...v1.11.0)

---
updated-dependencies:
- dependency-name: once_cell
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto f660220495 Merge pull request 'Fix blog slug' (#1094) from fix-blog-slug into main
Reviewed-on: Plume/Plume#1094
2 years ago
KitaitiMakoto 155df7bdf0 Merge branch 'main' into fix-blog-slug 2 years ago
Kitaiti Makoto 29055d1957 Follow clippy 2 years ago
Kitaiti Makoto d6ee49b880 Update Cargo.lock 2 years ago
Kitaiti Makoto 248ed265c4 Remove unsed heck from dependencies 2 years ago
Kitaiti Makoto a1f958ee7a Remove unused import 2 years ago
Kitaiti Makoto abf352b957 Remove unused function 2 years ago
Kitaiti Makoto 393f8e5e0c Use Blog::slug() to determine blog's slug 2 years ago
Kitaiti Makoto 4dfe300ee3 Define Blog::slug() 2 years ago
Kitaiti Makoto 65829094c9 Add test for blog slug validation 2 years ago
MohammadSaleh Kamyab d5c3e6d6f0 Blog's header buttons margin fix in RTL 2 years ago
KitaitiMakoto d702dd2fae Merge pull request 'Bidirectional support for user page header' (#1092) from mskf1383/Plume:main into main
Reviewed-on: Plume/Plume#1092
2 years ago
MohammadSaleh Kamyab 0d855823c9 Bidirectional support for user page header 2 years ago
KitaitiMakoto 2dd33769d4 Merge pull request 'Bump rsass from 0.24.0 to 0.25.0' (#1091) from rsass-0.25.0 into main
Reviewed-on: Plume/Plume#1091
2 years ago
dependabot[bot] 4ea9f6ecf1
Bump rsass from 0.24.0 to 0.25.0
Bumps [rsass](https://github.com/kaj/rsass) from 0.24.0 to 0.25.0.
- [Release notes](https://github.com/kaj/rsass/releases)
- [Changelog](https://github.com/kaj/rsass/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kaj/rsass/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: rsass
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto 3b0b6c4b0b Merge pull request 'Fix .venv path in buildenv' (#1090) from venv-path into main
Reviewed-on: Plume/Plume#1090
2 years ago
Kitaiti Makoto 4328fad5a3 Don't load venv 2 years ago
Kitaiti Makoto b26822c045 Update buildenv image 2 years ago
Kitaiti Makoto f372282b04 Use apt package for setuptools instead of pyenv 2 years ago
Kitaiti Makoto 145253ccbf Fix .venv path in buildenv 2 years ago
KitaitiMakoto 485223a3dd Merge pull request 'Add fmt and clippy on CI' (#1089) from add-toolchain into main
Reviewed-on: Plume/Plume#1089
2 years ago
Kitaiti Makoto 7f75fa74e7 Add fmt and clippy on CI 2 years ago
KitaitiMakoto 821fce1903 Merge pull request 'Use rust-toolchain in buildenv' (#1088) from buildenv-rust-toolchain into main
Reviewed-on: Plume/Plume#1088
2 years ago
Kitaiti Makoto ce484de61e Bump buildenv image 2 years ago
Kitaiti Makoto 35fb57718d Add rust-toolchain into buildenv 2 years ago
KitaitiMakoto 35d12d7cae Merge pull request 'Activate venv on integration test' (#1087) from fix-test-env into main
Reviewed-on: Plume/Plume#1087
2 years ago
Kitaiti Makoto 846154efe1 Activate venv on integration test 2 years ago
KitaitiMakoto 1ec7acbdfe Merge pull request 'Update Crowdin enviroment' (#1086) from update-crowdin into main
Reviewed-on: Plume/Plume#1086
2 years ago
Kitaiti Makoto e384fdfcff Update buildenv image to v0.5.0 2 years ago
Kitaiti Makoto ed58e44d2e Use Python 3 to install Selenium 2 years ago
Kitaiti Makoto f151dee339 Don't strip in buildenv 2 years ago
Kitaiti Makoto 61f25941e8 Install crowdin CLI using apt in buildenv 2 years ago
Kitaiti Makoto 0628a14be6 Use Rust image for buildenv 2 years ago
KitaitiMakoto b46ae83377 Merge pull request 'Change default branch to main' (#1085) from default-branch into main
Reviewed-on: Plume/Plume#1085
2 years ago
Kitaiti Makoto 70bc7f8edf Change default branch to main 2 years ago
KitaitiMakoto aff481b947 Merge pull request 'Add 'My feed' to i18n timeline name' (#1084) from myfeed-translation into main
Reviewed-on: Plume/Plume#1084
2 years ago
Kitaiti Makoto 29ef73d307 Add 'My feed' to i18n timeline name 2 years ago
KitaitiMakoto db205d0d9d Merge pull request 'Bump ldap3 from 0.10.4 to 0.10.5' (#1083) from ldap3-0.10.5 into main
Reviewed-on: Plume/Plume#1083
2 years ago
dependabot[bot] 57ab7edf23
Bump ldap3 from 0.10.4 to 0.10.5
Bumps [ldap3](https://github.com/inejge/ldap3) from 0.10.4 to 0.10.5.
- [Release notes](https://github.com/inejge/ldap3/releases)
- [Changelog](https://github.com/inejge/ldap3/blob/master/CHANGELOG.md)
- [Commits](https://github.com/inejge/ldap3/compare/v0.10.4...v0.10.5)

---
updated-dependencies:
- dependency-name: ldap3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto e4bd9d65cf Merge pull request 'Bump diesel-derive-newtype from 0.1.2 to 1.0.0' (#1081) from diesel-derive-newtype-1.0.0 into main
Reviewed-on: Plume/Plume#1081
2 years ago
KitaitiMakoto 17e4ddb32d Merge branch 'main' into diesel-derive-newtype-1.0.0 2 years ago
KitaitiMakoto 4fd85b30f1 Merge pull request '(cargo-release) version v0.7.3-dev' (#1080) from v0.7.3-dev into main
Reviewed-on: Plume/Plume#1080
2 years ago
Kitaiti Makoto 6148f29c66 (cargo-release) version {{version}} 2 years ago
KitaitiMakoto 1a3fad2d6a Merge pull request 'Release v0.7.2' (#1079) from v0.7.2 into main
Reviewed-on: Plume/Plume#1079
2 years ago
Kitaiti Makoto 0945d3bc53 Set release to false for sub crates [skip ci] 2 years ago
Kitaiti Makoto 9a824f06c3 (cargo-release) version {{version}} 2 years ago
Kitaiti Makoto eec09d79fe Fix release.toml 2 years ago
dependabot[bot] 7f63d2a129
Bump diesel-derive-newtype from 0.1.2 to 1.0.0
Bumps [diesel-derive-newtype](https://github.com/quodlibetor/diesel-derive-newtype) from 0.1.2 to 1.0.0.
- [Release notes](https://github.com/quodlibetor/diesel-derive-newtype/releases)
- [Changelog](https://github.com/quodlibetor/diesel-derive-newtype/blob/main/CHANGELOG.md)
- [Commits](https://github.com/quodlibetor/diesel-derive-newtype/commits)

---
updated-dependencies:
- dependency-name: diesel-derive-newtype
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
Kitaiti Makoto 79b639c3e6 Update PO files 2 years ago
Kitaiti Makoto efef208f53 Update translations 2 years ago
KitaitiMakoto 27e0f755f6 Merge pull request 'Bump whatlang from 0.15.0 to 0.16.0' (#1078) from whatlang-0.16.0 into main
Reviewed-on: Plume/Plume#1078
2 years ago
KitaitiMakoto a9d7aae5d6 Merge branch 'main' into whatlang-0.16.0 2 years ago
KitaitiMakoto 42e584a363 Merge pull request 'Add blank line' (#1077) from tiny-change into main
Reviewed-on: Plume/Plume#1077
2 years ago
Kitaiti Makoto 8c37ea3ec3 Add blank line 2 years ago
dependabot[bot] 3c14fa0058 Bump whatlang from 0.15.0 to 0.16.0
Bumps [whatlang](https://github.com/greyblake/whatlang-rs) from 0.15.0 to 0.16.0.
- [Release notes](https://github.com/greyblake/whatlang-rs/releases)
- [Changelog](https://github.com/greyblake/whatlang-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/greyblake/whatlang-rs/compare/v0.15.0...v0.16.0)

---
updated-dependencies:
- dependency-name: whatlang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
dependabot[bot] ab94cca210
Bump tokio from 1.18.1 to 1.18.2
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.18.1 to 1.18.2.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.18.1...tokio-1.18.2)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
Kitaiti Makoto ea62388985 Update PO files 2 years ago
KitaitiMakoto a9219efee4 Merge pull request 'Move to action area after liking/boosting/commenting' (#1074) from action-id into main
Reviewed-on: Plume/Plume#1074
2 years ago
Kitaiti Makoto 776ed058c7 [skip ci]Add changelog 2 years ago
Kitaiti Makoto aa3e196b8f Make comment content required 2 years ago
Kitaiti Makoto 52cb7270a9 Set id attributes to action forms in post details page 2 years ago
KitaitiMakoto 66376afb36 Merge pull request 'Upgrade activitystreams to 0.7, again' (#1022) from ap07 into main
Reviewed-on: Plume/Plume#1022
2 years ago
Kitaiti Makoto 96860be1be Fix Follow::accept_follow() 2 years ago
KitaitiMakoto 3bf61efc34 Merge pull request '[skip ci]Update changelog' (#1073) from changelog into main
Reviewed-on: Plume/Plume#1073
2 years ago
KitaitiMakoto d95549f58b Merge pull request 'Move local feed before federated feed for non-logged-in users' (#1072) from timeline-order into main
Reviewed-on: Plume/Plume#1072
2 years ago
Kitaiti Makoto bf24e4878a Merge remote-tracking branch 'origin/main' into ap07 2 years ago
Kitaiti Makoto 9ae231fcef [skip ci]Update changelog 2 years ago
KitaitiMakoto c32acb2fcf Merge pull request 'Sleep between broadcasting' (#1071) from sleep-broadcasting into main
Reviewed-on: Plume/Plume#1071
2 years ago
Kitaiti Makoto 770c77ee81 Move local feed before federated feed for non-logged-in users 2 years ago
KitaitiMakoto aa3e4d7cf8 Merge branch 'main' into sleep-broadcasting 2 years ago
KitaitiMakoto 156a875f02 Merge pull request 'Move local timeline before federated timeline' (#1070) from timeline-order into main
Reviewed-on: Plume/Plume#1070
2 years ago
Kitaiti Makoto cfed02bbcf Merge remote-tracking branch 'origin/main' into ap07 2 years ago
Kitaiti Makoto 4d3db9af73 Sleep between broadcasting 2 years ago
Kitaiti Makoto c1c606bc86 [skip ci]Add changelog about timeline order change 2 years ago
Kitaiti Makoto f401949037 Move local timeline before federated timeline 2 years ago
Kitaiti Makoto e8dc0942e5 Merge remote-tracking branch 'origin/main' into ap07 2 years ago
Kitaiti Makoto 9fbafd8e79 Fix Follow object in accepting follow 2 years ago
KitaitiMakoto b9ea83a602 Merge pull request 'More personal timelines' (#1069) from timeline-order into main
Reviewed-on: Plume/Plume#1069
2 years ago
KitaitiMakoto 2ada5a83af Merge branch 'main' into timeline-order 2 years ago
KitaitiMakoto ec25599d1f Merge pull request 'Fixes #949 Fix time out error on broadcasting' (#1068) from fix-timeout into main
Reviewed-on: Plume/Plume#1068
2 years ago
KitaitiMakoto 57551610e2 Merge pull request 'Update CircleCI image' (#1066) from update-circleci-image into main
Reviewed-on: Plume/Plume#1066
2 years ago
Kitaiti Makoto 8948b7acc1 Center timeline tabs 2 years ago
Kitaiti Makoto ccf7ff2bc9 Remove Latest articles from timeline tabs 2 years ago
Kitaiti Makoto 39de967141 Show first timeline at home 2 years ago
Kitaiti Makoto 118cfd7166 Replace hard tabs with soft tabs 2 years ago
Kitaiti Makoto c2fd4ab3a5 Merge remote-tracking branch 'origin/main' into fix-timeout 2 years ago
Kitaiti Makoto 70b5bee00f Move My feed first in timelines 2 years ago
Kitaiti Makoto de605deb1e Don't unwrap 2 years ago
Kitaiti Makoto 116974f811 Add comment about broadcast capacity 2 years ago
Kitaiti Makoto c57f36ccca Merge branch 'fix-timeout' into ap07 2 years ago
Kitaiti Makoto 9def0355aa Reduce broadcast request connections 2 years ago
Kitaiti Makoto 4e833c2061 Follow clippy 2 years ago
Kitaiti Makoto 5871ed7301 Merge branch 'fix-timeout' into ap07 2 years ago
Kitaiti Makoto 97632fdbfe Broadcast asynchronously 2 years ago
Kitaiti Makoto 1f8da7e63d Install futures 2 years ago
Kitaiti Makoto 76ca7c1462 Add futures to plume-common's dependencies 2 years ago
Kitaiti Makoto f06f444a13 Update CircleCI image
See https://discuss.circleci.com/t/legacy-convenience-image-deprecation/41034
2 years ago
Kitaiti Makoto 10dfecf45c Merge remote-tracking branch 'origin/fix-timeout' into ap07 2 years ago
Kitaiti Makoto a7b899817a Run HTTP request in broadcast() on tokio runtime 2 years ago
Kitaiti Makoto e0258003b9 Install tokio and flume 2 years ago
Kitaiti Makoto 2326eb77cd Add tokio to plume-common's dependencies 2 years ago
Kitaiti Makoto 504d41d887 Add flume to plume-common's dependencies 2 years ago
KitaitiMakoto 5a7d5e8099 Merge pull request 'Bump serde_json from 1.0.80 to 1.0.81' (#1064) from serde_json-1.0.81 into main
Reviewed-on: Plume/Plume#1064
2 years ago
KitaitiMakoto 74a1daac8c Merge branch 'main' into serde_json-1.0.81 2 years ago
KitaitiMakoto 1f855601ea Merge pull request 'Bump openssl from 0.10.38 to 0.10.40' (#1063) from openssl-0.10.40 into main
Reviewed-on: Plume/Plume#1063
2 years ago
Kitaiti Makoto f22c4d5c78 Await in consumer 2 years ago
Kitaiti Makoto ce4b216722 Broadcast asynchronously 2 years ago
Kitaiti Makoto 9016995d92 Install tokio 2 years ago
dependabot[bot] 853a1db028
Bump serde_json from 1.0.80 to 1.0.81
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.80 to 1.0.81.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.80...v1.0.81)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
dependabot[bot] 712ee30a1f
Bump openssl from 0.10.38 to 0.10.40
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.38 to 0.10.40.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.38...openssl-v0.10.40)

---
updated-dependencies:
- dependency-name: openssl
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
Kitaiti Makoto 9e5f9255d1 Add tokio to plume-common's dependencies 2 years ago
Kitaiti Makoto 2e35441483 Follow reqwest change 2 years ago
Kitaiti Makoto 5c74f598d8 Update Cargo.lock 2 years ago
Kitaiti Makoto 5d711dc47c Upgrade reqwest to 0.11 2 years ago
KitaitiMakoto 9ae3057106 Merge pull request 'Fixes #1061 Render 404 when page not found' (#1062) from render-404 into main
Reviewed-on: Plume/Plume#1062
2 years ago
Kitaiti Makoto b7ea154e51 Render 404 when page not found 2 years ago
Kitaiti Makoto 692e6b1c82 Uninstall tokio 2 years ago
Kitaiti Makoto 528f1bac48 Remove tokio from dependencies 2 years ago
Kitaiti Makoto 35aa2374c4 Execute broadcast synchronously 2 years ago
Kitaiti Makoto 3eb7662aef Log inbox URI when broadcast() failed 2 years ago
Kitaiti Makoto de4fcaee93 Merge remote-tracking branch 'origin/main' into ap07 2 years ago
KitaitiMakoto 812fd3d956 Merge pull request 'Reuse reqwest client on broadcasting' (#1059) from fix-timeout into main
Reviewed-on: Plume/Plume#1059
2 years ago
KitaitiMakoto 5d3b480790 Merge branch 'main' into fix-timeout 2 years ago
KitaitiMakoto 2f1801acae Merge pull request 'Bump validator from 0.14.0 to 0.15.0' (#1060) from validator-0.15.0 into main
Reviewed-on: Plume/Plume#1060
2 years ago
Kitaiti Makoto 0404528908 Remove unnecessary clone of config 2 years ago
Kitaiti Makoto 4529b929d8 [skip ci]Add changelog 2 years ago
dependabot[bot] 889decc720
Bump validator from 0.14.0 to 0.15.0
Bumps [validator](https://github.com/Keats/validator) from 0.14.0 to 0.15.0.
- [Release notes](https://github.com/Keats/validator/releases)
- [Changelog](https://github.com/Keats/validator/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Keats/validator/compare/v0.14.0...v0.15.0)

---
updated-dependencies:
- dependency-name: validator
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
Kitaiti Makoto db0f1a3c46 Reuse reqwest client on broadcasting
See https://users.rust-lang.org/t/reqwest-http-client-fails-when-too-much-concurrency/55644/2
2 years ago
KitaitiMakoto ef57ef91f0 Merge pull request 'Fixes #1051 Fix accept header' (#1058) from activitystreams-content-type into main
Reviewed-on: Plume/Plume#1058
2 years ago
Kitaiti Makoto 073b72c9ed Add more fixes 2 years ago
Kitaiti Makoto 45a6744d4d [skip ci]Add changelog 2 years ago
Kitaiti Makoto 4d19861a25 Fix accept header 2 years ago
Kitaiti Makoto f5906cacf3 Restore missing logic for Media 2 years ago
Kitaiti Makoto 03ba77a577 Restore filter 2 years ago
Kitaiti Makoto 0fc7372781 Restore order of decl of boundary of broadcast() 2 years ago
Kitaiti Makoto 6c2846980a Merge remote-tracking branch 'origin/main' into ap07 2 years ago
Kitaiti Makoto 0685c59bf3 Add changelog 2 years ago
KitaitiMakoto 5f629195f8 Merge pull request 'Bump whatlang from 0.13.0 to 0.15.0' (#1057) from whatlang-0.15.0 into main
Reviewed-on: Plume/Plume#1057
2 years ago
KitaitiMakoto 13eeedb620 Merge branch 'main' into whatlang-0.15.0 2 years ago
KitaitiMakoto a076c132ca Merge pull request 'Bump serde from 1.0.136 to 1.0.137' (#1056) from serde-1.0.137 into main
Reviewed-on: Plume/Plume#1056
2 years ago
KitaitiMakoto a76e0dfe5b Merge branch 'main' into serde-1.0.137 2 years ago
KitaitiMakoto 8faac20977 Merge pull request 'Bump serde_json from 1.0.79 to 1.0.80' (#1055) from serde_json-1.0.80 into main
Reviewed-on: Plume/Plume#1055
2 years ago
Kitaiti Makoto b04edfa05e Follow clippy 2 years ago
Kitaiti Makoto 1f62bf27f8 Fix nest of source property for Post 2 years ago
Kitaiti Makoto 1e0d1fb97a Add test for CustomGroup 2 years ago
Kitaiti Makoto 19d30c12d1 Parse source property properly 2 years ago
Kitaiti Makoto 384474930c Add test for Create activity with licensed article 2 years ago
dependabot[bot] 4cb64e0a8c
Bump whatlang from 0.13.0 to 0.15.0
Bumps [whatlang](https://github.com/greyblake/whatlang-rs) from 0.13.0 to 0.15.0.
- [Release notes](https://github.com/greyblake/whatlang-rs/releases)
- [Changelog](https://github.com/greyblake/whatlang-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/greyblake/whatlang-rs/compare/v0.13.0...v0.15.0)

---
updated-dependencies:
- dependency-name: whatlang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
dependabot[bot] cd2a2df48d
Bump serde from 1.0.136 to 1.0.137
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.136 to 1.0.137.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.136...v1.0.137)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
dependabot[bot] cbf960500b
Bump serde_json from 1.0.79 to 1.0.80
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.79 to 1.0.80.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.79...v1.0.80)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
Kitaiti Makoto 3d434f1923 Remove old activitypub related crates 2 years ago
Kitaiti Makoto 52022fb597 Remove activitypub crate from plume-models 2 years ago
Kitaiti Makoto d96940c848 Don't implement activitypub::Link for Id 2 years ago
Kitaiti Makoto b17884681d Implement ap_followers using activitystreams 2 years ago
Kitaiti Makoto 78a001ac89 Remove trailing 07 in routes/posts.rs 2 years ago
Kitaiti Makoto 9b04fb96e6 Remote trailing 07 in inbox.rs 2 years ago
Kitaiti Makoto d23002b817 Remove trailing 07 from method name 2 years ago
Kitaiti Makoto 6282b98b03 Fix doc test 2 years ago
Kitaiti Makoto d75600ba14 Remove trailing 07 in activity_pub/inbox.rs 2 years ago
Kitaiti Makoto e6ea302319 Remove activitypub crate from Inbox test 2 years ago
Kitaiti Makoto df005a28f8 Rename: activity07() -> activity() 2 years ago
Kitaiti Makoto 15134eed60 Rename: get_sender07() -> get_sender() 2 years ago
Kitaiti Makoto 7dd56a71e3 Rename: from_db07() -> from_db() 2 years ago
Kitaiti Makoto 06d2f68ecd Rename: from_activity07() -> from_activity() 2 years ago
Kitaiti Makoto 9a640b3438 Rename: deref07() -> deref() 2 years ago
Kitaiti Makoto 9791607793 Rename: with07() -> with() 2 years ago
Kitaiti Makoto ccd3c8a3f2 Don't implement activitypub's Object for Source 2 years ago
Kitaiti Makoto 6bbadc78b0 Rename: Licensed07 -> Licensed 2 years ago
Kitaiti Makoto e1673787b4 Remove unused Licensed struct 2 years ago
Kitaiti Makoto 1dd176dd80 Rename: broadcast07() -> broadcast() 2 years ago
Kitaiti Makoto 5c59687cb8 Remove unused broadcast() 2 years ago
Kitaiti Makoto a24e3c46e6 Remove trailing 07 in posts.rs 2 years ago
Kitaiti Makoto 267fecba66 Remove unsed posts::LicensedArticle 2 years ago
Kitaiti Makoto fc99d2b7a0 Remove trailing 07 in remote_fetch_actor.rs 2 years ago
Kitaiti Makoto 1b32fa1e34 Remove unused Media::from_activity() 2 years ago
Kitaiti Makoto ce42524273 Remote trailng 07 from Note in comments.rs 2 years ago
Kitaiti Makoto 595fa05660 Use Follow::to_activity07() instead of to_activity() 2 years ago
Kitaiti Makoto f8a0dff526 Remove unused Follow::build_accept() 2 years ago
Kitaiti Makoto 06d216c7ed Remove unused Follow::accept_follow() 2 years ago
Kitaiti Makoto f44bca30f4 Use Follow::build_undo07() instead of build_undo() 2 years ago
Kitaiti Makoto e4180b3b38 Rename: ApSignature07 -> ApSignature 2 years ago
Kitaiti Makoto 992a482b96 Remove unused ApSignature type 2 years ago
Kitaiti Makoto 0ad845e0f7 Remove unused User::fetch_outbox() 2 years ago
Kitaiti Makoto ee97213c90 Remove trailing 07 from import of OrderedCollectionPage 2 years ago
Kitaiti Makoto 6d919da049 Remove duplicate import 2 years ago
Kitaiti Makoto 6c615d01ad Remove users::CustomPerson 2 years ago
Kitaiti Makoto f8870af9fe Remove trailing 07 from Hashtag 2 years ago
Kitaiti Makoto 2fe2505a01 Remove unused Hashtag 2 years ago
Kitaiti Makoto e41fa353e4 Use User::to_activity07() instead of to_activity() 2 years ago
Kitaiti Makoto effdc44943 Use User::delete_activity07() instead of delete_activity() 2 years ago
Kitaiti Makoto fd341bdb22 Use User::outbox07() instead of outbox() 2 years ago
Kitaiti Makoto 68c794c54b Use User::outbox_page07() instead of outbox_page() 2 years ago
Kitaiti Makoto 7b3b00be23 Remove unused Tag::from_activity() and to_activity() 2 years ago
Kitaiti Makoto 41ccacc5d3 Remove unused Mention::from_activity() 2 years ago
Kitaiti Makoto 4ef9350ce7 Remove unused Mention::to_activity() 2 years ago
Kitaiti Makoto 5d08ff6c3b Use Mention::build_activity07() instead of build_activity() 2 years ago
Kitaiti Makoto 01dca62ce5 Rename: Like07 -> LikeAct 2 years ago
Kitaiti Makoto 6ab1ecd57b Use Like::to_activity07() instead of to_activity() 2 years ago
Kitaiti Makoto b13444895f Use Like::build_undo07() instead of build_undo() 2 years ago
Kitaiti Makoto 771c157fe5 Use Comment::to_activity07() instead of to_activity() 2 years ago
Kitaiti Makoto b5e1076b0e Use Comment::build_delete07() instead of build_delete() 2 years ago
Kitaiti Makoto 6cc43c2420 Use Comment::create_activity07() instead of create_activity() 2 years ago
Kitaiti Makoto f365041a45 Use Reshare::to_activity07() instead of to_activity() 2 years ago
Kitaiti Makoto ae9c9262f7 Use Reshare::build_undo07() instead of build_undo() 2 years ago
Kitaiti Makoto 40ce515e6c Don't rename activitystreams' tokens to 07 2 years ago
Kitaiti Makoto bc96af7f5f Remove unused blogs::CustomGroup 2 years ago
Kitaiti Makoto 811c20c8fb Use Blog::to_activity07() instead of to_activity() 2 years ago
Kitaiti Makoto 4b4c22cf8a Use Blog::outbox_page07() instead of outbox_page() 2 years ago
Kitaiti Makoto 0524b0b153 Add Blog::outbox_page07() 2 years ago
Kitaiti Makoto cd6c57b9c5 Use Blog::outbox07() instead of outbox() 2 years ago
Kitaiti Makoto f608f7a4d6 Install activitystreams 2 years ago
Kitaiti Makoto 803680186b Add Blog::outbox07() 2 years ago
Kitaiti Makoto 68a01d5f9b Add activitystreams to Plume's dependencies 2 years ago
Kitaiti Makoto 5b3a472b66 Use Post::to_activity07() instead of to_activity() 2 years ago
Kitaiti Makoto 2a85f775e9 Use Post::build_delete07() instead of build_delete() 2 years ago
Kitaiti Makoto a958300a58 Use Post::update_hashtags07() instead of update_hashtags() 2 years ago
Kitaiti Makoto a8be31b177 Use Post::update_tags07() instead of update_tags() 2 years ago
Kitaiti Makoto 6cd68ab8b0 Use Post::update_activity07() instead of update_activity() 2 years ago
Kitaiti Makoto a589435f4f Use Post::create_activity07() instead of create_activity() 2 years ago
Kitaiti Makoto 39b49c707e Use Post::update_mentions07() instead of update_mentions() 2 years ago
Kitaiti Makoto c4bb1f771b Add test for Tag::from_activity07() 2 years ago
Kitaiti Makoto 28440271bb Rename: FromId::from_id07 -> from_id 2 years ago
Kitaiti Makoto 0ab7774e29 Rename: AsObject07 -> AsObject 2 years ago
Kitaiti Makoto 33afe9111e Remove AsObject 2 years ago
Kitaiti Makoto d8a2e1925f Rename FromId07 -> FromId 2 years ago
Kitaiti Makoto 2804f44a06 Rmove FromId 2 years ago
Kitaiti Makoto 2165c286ae Remove with() 2 years ago
Kitaiti Makoto 8cbf410faf Remove execute permission from plume-common/src/lib.rs 2 years ago
Kitaiti Makoto c521a81373 Make test follow LicensedArticle change 2 years ago
Kitaiti Makoto 7ade0550c9 Remove unused import 2 years ago
Kitaiti Makoto 41bc2d6949 Make LicensedArticle's license fieald optional 2 years ago
Kitaiti Makoto de6e9c0e2e Fix Post::from_activity07() 2 years ago
Kitaiti Makoto 38ebc9ea41 Modify test data for Post 2 years ago
Kitaiti Makoto 8f976be998 Implement AsObject07 for PostUpdate 2 years ago
Kitaiti Makoto e5a2850105 Implement FromId07 for PostUpdate 2 years ago
KitaitiMakoto 85727c6d4c Merge pull request 'Bump ldap3 from 0.10.3 to 0.10.4' (#1054) from ldap3-0.10.4 into main
Reviewed-on: Plume/Plume#1054
2 years ago
KitaitiMakoto 87247a23b3 Merge branch 'main' into ldap3-0.10.4 2 years ago
KitaitiMakoto 61785364e3 Merge pull request 'Bump ctrlc from 3.2.1 to 3.2.2' (#1053) from ctrlc-3.2.2 into main
Reviewed-on: Plume/Plume#1053
2 years ago
Kitaiti Makoto 76f688c967 Replace some Inbox::with with with07 2 years ago
Kitaiti Makoto 05df3b89a1 Fix Follow::activity07() 2 years ago
Kitaiti Makoto 4e42a34337 Replace some with() with with07() 2 years ago
Kitaiti Makoto 62372201e0 Fix inbox::tests::create_post() 2 years ago
Kitaiti Makoto 036913a828 Use id() for reply_tos 2 years ago
dependabot[bot] b2a889b9e4
Bump ldap3 from 0.10.3 to 0.10.4
Bumps [ldap3](https://github.com/inejge/ldap3) from 0.10.3 to 0.10.4.
- [Release notes](https://github.com/inejge/ldap3/releases)
- [Changelog](https://github.com/inejge/ldap3/blob/master/CHANGELOG.md)
- [Commits](https://github.com/inejge/ldap3/compare/v0.10.3...v0.10.4)

---
updated-dependencies:
- dependency-name: ldap3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
dependabot[bot] fae8338772
Bump ctrlc from 3.2.1 to 3.2.2
Bumps [ctrlc](https://github.com/Detegr/rust-ctrlc) from 3.2.1 to 3.2.2.
- [Release notes](https://github.com/Detegr/rust-ctrlc/releases)
- [Commits](https://github.com/Detegr/rust-ctrlc/compare/3.2.1...3.2.2)

---
updated-dependencies:
- dependency-name: ctrlc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
Kitaiti Makoto 79b5d9a690 Replace Inbox::with() with with07() 2 years ago
Kitaiti Makoto 3e54d10981 Implement AsObject07<User, Undo07, &DbConn> for Like 2 years ago
Kitaiti Makoto a1c3bfb646 Implement FromId07 for Like 2 years ago
Kitaiti Makoto b2528c21ff Implement AsObject07<User, Like07, &DbConn> for Post 2 years ago
Kitaiti Makoto fcc9e1d81b Implement Like::build_undo07() 2 years ago
Kitaiti Makoto 3093f713ef Add test for Like::build_undo07() 2 years ago
Kitaiti Makoto 4ea29d29a0 Implement Like::to_activity07() 2 years ago
Kitaiti Makoto 6b8d90d8b6 Add test for Like::to_activity07() 2 years ago
Kitaiti Makoto bd3e6a5a91 Replace some Inbox::with with with07 2 years ago
Kitaiti Makoto 46f4676efb Implement Reshare::build_undo07() 2 years ago
Kitaiti Makoto c814ac5681 Add test for Reshare::build_undo07() 2 years ago
Kitaiti Makoto 0887399048 Implement AsObject07<User, Undo07, &DbConn> for Reshare 2 years ago
Kitaiti Makoto f2a2bf2b23 Implement FromId07 for Reshare 2 years ago
Kitaiti Makoto e2702a187b Implement AsObject07<User, Announce07, &DbConn> for Post 2 years ago
Kitaiti Makoto d78a57ce47 Implement Reshare::to_ativity07() 2 years ago
Kitaiti Makoto 10acbdd41f Add test for Reshare::to_activity07() 2 years ago
Kitaiti Makoto 73009818f2 Implement AsObject07<User, Undo07, &DbConn> for Follow 2 years ago
Kitaiti Makoto fb5027becd Implement FromId07 for Follow 2 years ago
Kitaiti Makoto 86609b51fa Implement AsObject07<User, FollowAct07, &DbConn> for User 2 years ago
Kitaiti Makoto 44799e94fd Implement Follow::accept_follow07() 2 years ago
Kitaiti Makoto f14c307786 Remove unused type parameter from broadcast07() 2 years ago
Kitaiti Makoto 174624f5c1 Implement Follow::build_undo07() 2 years ago
Kitaiti Makoto 5f91345d69 Add test for Follow::build_undo07() 2 years ago
Kitaiti Makoto 9ca975113c Implement Follow::build_accept07() 2 years ago
Kitaiti Makoto 38a55857c6 Add test for Follow::build_accept07() 2 years ago
Kitaiti Makoto 9343d3a120 Implement Follow::to_activity07() 2 years ago
Kitaiti Makoto c5656971c9 Add test for Follow::to_activity07() 2 years ago
Kitaiti Makoto ed55b66253 Implement AsObject07 for Comment 2 years ago
Kitaiti Makoto 713ffb9506 Fix Comment::create_activity07() 2 years ago
Kitaiti Makoto 9969e844ca Add test for Comment self federation 2 years ago
Kitaiti Makoto 0c61dca9ca Follow clippy 2 years ago
Kitaiti Makoto 957725fbf8 impl FromId07<DbConn> for Comment 2 years ago
Kitaiti Makoto 1f6361a9a2 Fix Cargo.toml 2 years ago
Kitaiti Makoto cf870971d1 Add test for Comment::build_delete07() 2 years ago
Kitaiti Makoto 08ac7227b5 Implement Comment::builde_delete07() 2 years ago
Kitaiti Makoto 88eb61c320 Implement Comment::create_activity07() 2 years ago
Kitaiti Makoto 1c1dbd481a Add test for Comment::to_activity07() 2 years ago
Kitaiti Makoto 86b4f622ea Implement Comment::to_activity07() 2 years ago
Kitaiti Makoto f854bc5838 Add test for LicensedArticle deserialization 2 years ago
Kitaiti Makoto 489156f4a3 Add test for Post's self federation 2 years ago
Kitaiti Makoto 01e8b0bce8 Implement AsObject for Post 2 years ago
Kitaiti Makoto 9183d04e66 Fix Post::from_activity07() for borrow checker 2 years ago
Kitaiti Makoto 5e463e2cc9 Implement FromId07 for Post 2 years ago
Kitaiti Makoto 6e2bff10f7 Add test for Post::build_delete07() 2 years ago
Kitaiti Makoto 3e9d9a81b7 Implement Mention::build_delete07() 2 years ago
Kitaiti Makoto 98e0754976 Add test for Post::build_delete() 2 years ago
Kitaiti Makoto da7870eeba Implement Post::update_hashtags07() 2 years ago
Kitaiti Makoto c1562f3868 Implement Post::update_tags07() 2 years ago
Kitaiti Makoto e0390cb105 Implement Post::update_mentions07() 2 years ago
Kitaiti Makoto 32cd91cfb9 Implement Mention::from_activity07() 2 years ago
Kitaiti Makoto 991dfccf3b Add test for Post::update_activity07() 2 years ago
Kitaiti Makoto 16e012ba00 Implement Post::update_activity07() 2 years ago
Kitaiti Makoto 871618f45d Add test for Post::create_activity07() 2 years ago
Kitaiti Makoto 680d321a2e Implement Post::create_activity07() 2 years ago
Kitaiti Makoto c37ff54857 Fix Post::to_activity07() 2 years ago
Kitaiti Makoto d4018d61d4 Add test for Post::to_activity07() 2 years ago
Kitaiti Makoto 21a0059755 Follow clippy 2 years ago
Kitaiti Makoto 05f4c186f4 Fix test for LicensedArticle serialization 2 years ago
Kitaiti Makoto 53512a6167 Fix SourceProperty property 2 years ago
Kitaiti Makoto 7cf7700ef7 Implement Post::to_activity07() 2 years ago
Kitaiti Makoto 216855d3a7 Add SourceProperty to LicensedArticle 2 years ago
Kitaiti Makoto 23f273e5e8 Readd assert-json-diff 2 years ago
Kitaiti Makoto 70949fad02 Rename: ActorSource -> SourceProperty 2 years ago
Kitaiti Makoto 1f5ce8e504 Add test for Mention::build_activity07() and to_activity07() 2 years ago
Kitaiti Makoto 2316d36e03 Add test for Tag::from_activity07() 2 years ago
Kitaiti Makoto 2b1ddc71ac Implement Tag::to_activity07() and Tag::build_activity07() 2 years ago
Kitaiti Makoto b9dac1a21a Define Hashtag07 2 years ago
Kitaiti Makoto 95fb5a3c71 Implement Media::from_activity07() 2 years ago
Kitaiti Makoto 75b43a738f Follow clippy 2 years ago
Kitaiti Makoto 5bd467c4c1 Remove unnecessary records 2 years ago
Kitaiti Makoto 74d6dc5089 Implement Blog::to_activity07(), outbox_collection07() and outbox_collection_page07() 2 years ago
Kitaiti Makoto 994a4dbb2d Add source property to CustomGroup 2 years ago
Kitaiti Makoto 67996cc938 Add test for Blog::outbox_collection_page() 2 years ago
Kitaiti Makoto f5e776c4d7 Fix first and last link in Blog::outbox_collection() 2 years ago
Kitaiti Makoto e27fc47287 Extract Blog::outbox_collection_page() 2 years ago
Kitaiti Makoto 0ed91b89ff Add test for Blog::outbox_collection() 2 years ago
Kitaiti Makoto 00862790a1 Extract Blog::outbox_collection() 2 years ago
Kitaiti Makoto ab6f39c192 Add test for Blog::to_activity() 2 years ago
Kitaiti Makoto 4edc201c14 Implement FromId07 for Blog 2 years ago
Kitaiti Makoto a1a7acfe94 Use new activitystreams APIs 2 years ago
Kitaiti Makoto 6b5a1d2130 Use Base::retract() instead of into_any_base() on creating activity 2 years ago
Kitaiti Makoto 85e35fdb5d Update activitystreams 2 years ago
Kitaiti Makoto da9e13622c Use Inbox::with07() for User, Delete, User 2 years ago
Kitaiti Makoto 8f4dd8a57b Implement User::fetch_outbox07() 2 years ago
Kitaiti Makoto 78b0535063 Implement User::fetch_outbox_page07() 2 years ago
Kitaiti Makoto 6323c7aef8 Add test for User::outbox_page_collection07() 2 years ago
Kitaiti Makoto 7f0ad56d07 Implement User::outbox_collection_page07() 2 years ago
Kitaiti Makoto e7eea3901f Implement User::outbox_page07() 2 years ago
Kitaiti Makoto 0979471e54 Add test for User::delete_activity07() 2 years ago
Kitaiti Makoto 8d69051a61 Implement User::delete_activity07() 2 years ago
Kitaiti Makoto 55ca1345e1 Add test self_federation07() for User 2 years ago
Kitaiti Makoto ad951ca842 Add test for User::to_activity07() 2 years ago
Kitaiti Makoto cb8e2e9294 Implement User::to_activity() 2 years ago
Kitaiti Makoto 038d65acaa Implement User::outbox07() 2 years ago
Kitaiti Makoto e392a89526 Add test for User::outbox_collection07() 2 years ago
Kitaiti Makoto d62f51665b Implement User::outbox_collection07() 2 years ago
Kitaiti Makoto e42aa6fe8e Implement From<iri_string::validate::Error> for Error 2 years ago
Kitaiti Makoto ab126563f3 Implement AsObject07 for User 2 years ago
Kitaiti Makoto c1b9ebdae6 [REFACTORING]Reduce duplicated closure 2 years ago
Kitaiti Makoto d3e11c78d7 [REFACTORING]Use method chain instead of if clauses 2 years ago
Kitaiti Makoto 4ccfec8019 Use OneOrMany<AnyBase>::to_as_uri() 2 years ago
Kitaiti Makoto bb5157637d Implement OneOrMany<AnyBase>::to_as_uri() 2 years ago
Kitaiti Makoto 456df3e535 Use OneOrMany<&AnyString>::as_as_str() 2 years ago
Kitaiti Makoto f0112850fa Implement OneOrMany<&AnyString>::as_as_str() 2 years ago
Kitaiti Makoto a6a21d5dfa Rewrite to_as_string() using method chain instead of if expressions 2 years ago
Kitaiti Makoto 249fbbe891 Remove unused import 2 years ago
Kitaiti Makoto e925865767 Use &AnyString::as_as_str() 2 years ago
Kitaiti Makoto 28643fc2c2 Implement &AnyString::as_as_str() 2 years ago
Kitaiti Makoto 3db10a09bb Use OneOrMany<&AnyString>::to_as_string() 2 years ago
Kitaiti Makoto a80a95d471 Implement OneOrMany<&AnyString>::to_as_string() 2 years ago
Kitaiti Makoto e407d58ee9 Implement FromId07 for User 2 years ago
Kitaiti Makoto a6d839a766 Make fields of ApSignature07 and PublicKey07 public 2 years ago
Kitaiti Makoto f3b67ab6c9 WIP 2 years ago
Kitaiti Makoto 66f5628a27 Add suffix 07 to activitystreams 0.7 related methods 2 years ago
Kitaiti Makoto 4b3b5c1f40 Implement From<activitystreams::checked::CheckError> for Error 2 years ago
Kitaiti Makoto 3e687f3af0 Reduce type parameter from broadcast07 2 years ago
Kitaiti Makoto 119d3e4f6a [plume-common]Add tests for new ActivityPub functions 2 years ago
Kitaiti Makoto a21d66178e [plume-common]Implement ActivityPub related function using activitystreams 0.7 2 years ago
Kitaiti Makoto 52967f3e47 [plume-common]Implement ActivityPub-related code using activitystreams 0.7 2 years ago
Kitaiti Makoto 29439f9d02 Add tests for newly added ActivityPub-related structs 2 years ago
Kitaiti Makoto bc72a4c2d1 Install assert-json-diff 2 years ago
Kitaiti Makoto 3ded0e2166 Add assert-json-diff to plume-common's dependencies 2 years ago
Kitaiti Makoto 27c10e5e5c Install activitystreams-ext 2 years ago
Kitaiti Makoto 816aefe72a Add ActivityStreams Ext to plume-common dependencies 2 years ago
Kitaiti Makoto bff50f8e4c Add activitystreams 0.7 to plume-models dependencies 2 years ago
Kitaiti Makoto be1c22815b Install activitystreams 0.7 2 years ago
Kitaiti Makoto 08ab7ffd08 Add activitystreams 0.7 to plume-common dependencies 2 years ago
KitaitiMakoto 8709f6cf9f Merge pull request 'Bump tracing from 0.1.32 to 0.1.34' (#1050) from tracing-0.1.34 into main
Reviewed-on: Plume/Plume#1050
2 years ago
KitaitiMakoto 04cae95635 Merge branch 'main' into tracing-0.1.34 2 years ago
KitaitiMakoto 5d37b2534a Merge pull request 'Bump wasm-bindgen from 0.2.78 to 0.2.80' (#1049) from wasm-bindgen-0.2.80 into main
Reviewed-on: Plume/Plume#1049
2 years ago
dependabot[bot] eafafdaadf Bump wasm-bindgen from 0.2.78 to 0.2.80
Bumps [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) from 0.2.78 to 0.2.80.
- [Release notes](https://github.com/rustwasm/wasm-bindgen/releases)
- [Changelog](https://github.com/rustwasm/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustwasm/wasm-bindgen/compare/0.2.78...0.2.80)

---
updated-dependencies:
- dependency-name: wasm-bindgen
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto 6897b8fa58 Merge pull request 'Bump js-sys from 0.3.55 to 0.3.57' (#1048) from js-sys-0.3.57 into main
Reviewed-on: Plume/Plume#1048
2 years ago
dependabot[bot] 16d3279d72 Bump js-sys from 0.3.55 to 0.3.57
Bumps [js-sys](https://github.com/rustwasm/wasm-bindgen) from 0.3.55 to 0.3.57.
- [Release notes](https://github.com/rustwasm/wasm-bindgen/releases)
- [Changelog](https://github.com/rustwasm/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustwasm/wasm-bindgen/commits)

---
updated-dependencies:
- dependency-name: js-sys
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto 36c76c534d Merge pull request 'Bump web-sys from 0.3.55 to 0.3.57' (#1047) from web-sys-0.3.57 into main
Reviewed-on: Plume/Plume#1047
2 years ago
KitaitiMakoto 26f460be89 Merge branch 'main' into web-sys-0.3.57 2 years ago
KitaitiMakoto b9fb13104a Merge pull request 'Bump ammonia from 3.1.4 to 3.2.0' (#1046) from ammonia-3.2.0 into main
Reviewed-on: Plume/Plume#1046
2 years ago
dependabot[bot] 5cc411158f
Bump tracing from 0.1.32 to 0.1.34
Bumps [tracing](https://github.com/tokio-rs/tracing) from 0.1.32 to 0.1.34.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-0.1.32...tracing-0.1.34)

---
updated-dependencies:
- dependency-name: tracing
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
dependabot[bot] ca69c93531
Bump web-sys from 0.3.55 to 0.3.57
Bumps [web-sys](https://github.com/rustwasm/wasm-bindgen) from 0.3.55 to 0.3.57.
- [Release notes](https://github.com/rustwasm/wasm-bindgen/releases)
- [Changelog](https://github.com/rustwasm/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustwasm/wasm-bindgen/commits)

---
updated-dependencies:
- dependency-name: web-sys
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
dependabot[bot] 95cb7cc904
Bump ammonia from 3.1.4 to 3.2.0
Bumps [ammonia](https://github.com/rust-ammonia/ammonia) from 3.1.4 to 3.2.0.
- [Release notes](https://github.com/rust-ammonia/ammonia/releases)
- [Changelog](https://github.com/rust-ammonia/ammonia/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-ammonia/ammonia/compare/v3.1.4...v3.2.0)

---
updated-dependencies:
- dependency-name: ammonia
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto 0a62fa46aa Merge pull request 'Bump tracing-subscriber from 0.3.9 to 0.3.10' (#1045) from tracing-subscriber-0.3.10 into main
Reviewed-on: Plume/Plume#1045
2 years ago
KitaitiMakoto 2e60410969 Merge branch 'main' into tracing-subscriber-0.3.10 2 years ago
KitaitiMakoto a12d3a591b Merge pull request 'Bump ldap3 from 0.10.2 to 0.10.3' (#1044) from ldap3-0.10.3 into main
Reviewed-on: Plume/Plume#1044
2 years ago
dependabot[bot] 3cf7c67b6d
Bump tracing-subscriber from 0.3.9 to 0.3.10
Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.9 to 0.3.10.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.9...tracing-subscriber-0.3.10)

---
updated-dependencies:
- dependency-name: tracing-subscriber
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
dependabot[bot] 2cf79f31b7
Bump ldap3 from 0.10.2 to 0.10.3
Bumps [ldap3](https://github.com/inejge/ldap3) from 0.10.2 to 0.10.3.
- [Release notes](https://github.com/inejge/ldap3/releases)
- [Changelog](https://github.com/inejge/ldap3/blob/master/CHANGELOG.md)
- [Commits](https://github.com/inejge/ldap3/compare/v0.10.2...v0.10.3)

---
updated-dependencies:
- dependency-name: ldap3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto 24cf941303 Merge pull request 'Bump native-tls from 0.2.8 to 0.2.10' (#1043) from native-tls-0.2.10 into main
Reviewed-on: Plume/Plume#1043
2 years ago
dependabot[bot] 38cf9b5496
Bump native-tls from 0.2.8 to 0.2.10
Bumps [native-tls](https://github.com/sfackler/rust-native-tls) from 0.2.8 to 0.2.10.
- [Release notes](https://github.com/sfackler/rust-native-tls/releases)
- [Changelog](https://github.com/sfackler/rust-native-tls/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sfackler/rust-native-tls/compare/v0.2.8...v0.2.10)

---
updated-dependencies:
- dependency-name: native-tls
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto b9607b32ac Merge pull request 'Bump rsass from 0.23.4 to 0.24.0' (#1042) from rsass-0.24.0 into main
Reviewed-on: Plume/Plume#1042
2 years ago
KitaitiMakoto 3393da2560 Merge pull request 'Bump bcrypt from 0.12.0 to 0.12.1' (#1041) from bcrypt-0.12.1 into main
Reviewed-on: Plume/Plume#1041
2 years ago
dependabot[bot] 7566f94690
Bump rsass from 0.23.4 to 0.24.0
Bumps [rsass](https://github.com/kaj/rsass) from 0.23.4 to 0.24.0.
- [Release notes](https://github.com/kaj/rsass/releases)
- [Changelog](https://github.com/kaj/rsass/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kaj/rsass/compare/v0.23.4...v0.24.0)

---
updated-dependencies:
- dependency-name: rsass
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
dependabot[bot] ee8312fb57
Bump bcrypt from 0.12.0 to 0.12.1
Bumps [bcrypt](https://github.com/Keats/rust-bcrypt) from 0.12.0 to 0.12.1.
- [Release notes](https://github.com/Keats/rust-bcrypt/releases)
- [Commits](https://github.com/Keats/rust-bcrypt/compare/v0.12.0...v0.12.1)

---
updated-dependencies:
- dependency-name: bcrypt
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto 76219704f3 Merge pull request 'Bump rpassword from 5.0.1 to 6.0.1' (#1040) from rpassword-6.0.1 into main
Reviewed-on: Plume/Plume#1040
2 years ago
dependabot[bot] 2a43d4e88a
Bump rpassword from 5.0.1 to 6.0.1
Bumps [rpassword](https://github.com/conradkleinespel/rpassword) from 5.0.1 to 6.0.1.
- [Release notes](https://github.com/conradkleinespel/rpassword/releases)
- [Commits](https://github.com/conradkleinespel/rpassword/compare/v5.0.1...v6.0.1)

---
updated-dependencies:
- dependency-name: rpassword
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto f0ce073a37 Merge pull request 'Bump tracing from 0.1.31 to 0.1.32' (#1039) from tracing-0.1.32 into main
Reviewed-on: Plume/Plume#1039
2 years ago
dependabot[bot] 8047196394
Bump tracing from 0.1.31 to 0.1.32
Bumps [tracing](https://github.com/tokio-rs/tracing) from 0.1.31 to 0.1.32.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-0.1.31...tracing-0.1.32)

---
updated-dependencies:
- dependency-name: tracing
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto 3cf52b3985 Merge pull request 'Add test for Tag::from_activity()' (#1037) from tag-test into main
Reviewed-on: Plume/Plume#1037
2 years ago
KitaitiMakoto ac378e448b Merge pull request 'Bump once_cell from 1.9.0 to 1.10.0' (#1038) from once_cell-1.10.0 into main
Reviewed-on: Plume/Plume#1038
2 years ago
Kitaiti Makoto daae2038f8 Add test for Tag::from_activity() 2 years ago
dependabot[bot] a2356c6e59
Bump once_cell from 1.9.0 to 1.10.0
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/matklad/once_cell/releases)
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.9.0...v1.10.0)

---
updated-dependencies:
- dependency-name: once_cell
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto 44b91c6f07 Merge pull request 'Update crates' (#1036) from ldap3-0.10.2 into main
Reviewed-on: Plume/Plume#1036
2 years ago
Kitaiti Makoto 144565d13e Merge remote-tracking branch 'github/dependabot/cargo/bcrypt-0.12.0' into ldap3-0.10.2 2 years ago
dependabot[bot] 07fd66863d
Bump bcrypt from 0.11.0 to 0.12.0
Bumps [bcrypt](https://github.com/Keats/rust-bcrypt) from 0.11.0 to 0.12.0.
- [Release notes](https://github.com/Keats/rust-bcrypt/releases)
- [Commits](https://github.com/Keats/rust-bcrypt/compare/v0.11.0...v0.12.0)

---
updated-dependencies:
- dependency-name: bcrypt
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
dependabot[bot] 385a5f7c33
Bump ldap3 from 0.9.3 to 0.10.2
Bumps [ldap3](https://github.com/inejge/ldap3) from 0.9.3 to 0.10.2.
- [Release notes](https://github.com/inejge/ldap3/releases)
- [Changelog](https://github.com/inejge/ldap3/blob/master/CHANGELOG.md)
- [Commits](https://github.com/inejge/ldap3/compare/v0.9.3...v0.10.2)

---
updated-dependencies:
- dependency-name: ldap3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto 8c8c2edc66 Merge pull request 'Add tests for Tag' (#1035) from ap-tests into main
Reviewed-on: Plume/Plume#1035
2 years ago
Kitaiti Makoto f7e393bded Add tests for Tag 2 years ago
KitaitiMakoto 1df25e34b0 Merge pull request 'Update gettext-macros to 0.6.1' (#1033) from update-gettext-macros into main
Reviewed-on: Plume/Plume#1033
2 years ago
Kitaiti Makoto ed491bad21 Update gettext-macros to 0.6.1 2 years ago
KitaitiMakoto 306f2d5738 Merge pull request 'Bump bcrypt from 0.10.1 to 0.11.0' (#1032) from bcrypt-0.11.0 into main
Reviewed-on: Plume/Plume#1032
2 years ago
dependabot[bot] 2196cb95c0
Bump bcrypt from 0.10.1 to 0.11.0
Bumps [bcrypt](https://github.com/Keats/rust-bcrypt) from 0.10.1 to 0.11.0.
- [Release notes](https://github.com/Keats/rust-bcrypt/releases)
- [Commits](https://github.com/Keats/rust-bcrypt/compare/v0.10.1...v0.11.0)

---
updated-dependencies:
- dependency-name: bcrypt
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto b4d494a5c7 Merge pull request 'Bump tracing from 0.1.30 to 0.1.31' (#1030) from tracing-0.1.31 into main
Reviewed-on: Plume/Plume#1030
2 years ago
Kitaiti Makoto e8432f575e Merge remote-tracking branch 'origin/main' into tracing-0.1.31 2 years ago
KitaitiMakoto eb48723c08 Merge pull request 'Bump tracing-subscriber from 0.3.8 to 0.3.9' (#1028) from tracing-subscriber-0.3.9 into main
Reviewed-on: Plume/Plume#1028
2 years ago
dependabot[bot] 65168202b4
Bump tracing from 0.1.30 to 0.1.31
Bumps [tracing](https://github.com/tokio-rs/tracing) from 0.1.30 to 0.1.31.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-0.1.30...tracing-0.1.31)

---
updated-dependencies:
- dependency-name: tracing
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
dependabot[bot] dced3cf881
Bump tracing-subscriber from 0.3.8 to 0.3.9
Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.8 to 0.3.9.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.8...tracing-subscriber-0.3.9)

---
updated-dependencies:
- dependency-name: tracing-subscriber
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto 170fd6026c Merge pull request 'Bump ammonia from 3.1.3 to 3.1.4' (#1027) from update-crate into main
Reviewed-on: Plume/Plume#1027
2 years ago
dependabot[bot] 1ccaa817b3
Bump ammonia from 3.1.3 to 3.1.4
Bumps [ammonia](https://github.com/rust-ammonia/ammonia) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/rust-ammonia/ammonia/releases)
- [Changelog](https://github.com/rust-ammonia/ammonia/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-ammonia/ammonia/compare/v3.1.3...v3.1.4)

---
updated-dependencies:
- dependency-name: ammonia
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto 65ba083720 Merge pull request 'Switch gettext crate from GitHub to crates.io' (#1018) from gettext-cratesio into main
Reviewed-on: Plume/Plume#1018
2 years ago
Kitaiti Makoto dba902d262 Merge remote-tracking branch 'origin/main' into gettext-cratesio 2 years ago
Kitaiti Makoto d52c7a3afa Update gettext-macros and gettext-utils 2 years ago
KitaitiMakoto c63f88fb7f Merge pull request 'Update crates' (#1026) from update-crates into main
Reviewed-on: Plume/Plume#1026
2 years ago
Kitaiti Makoto 4412e0598f Follow API change of rocket_i18n 2 years ago
Kitaiti Makoto eb22c1168e Install rocket_i18n from crates.io 2 years ago
Kitaiti Makoto 917eda356d Use rocket_i18n on crates.io 2 years ago
Kitaiti Makoto bc6580bbdc Switch gettext crate from GitHub to crates.io 2 years ago
Kitaiti Makoto 920cf622c5 Merge remote-tracking branch 'github/dependabot/cargo/askama_escape-0.10.3' into update-crates 2 years ago
dependabot[bot] 13dcb193dc
Bump askama_escape from 0.10.2 to 0.10.3
Bumps [askama_escape](https://github.com/djc/askama) from 0.10.2 to 0.10.3.
- [Release notes](https://github.com/djc/askama/releases)
- [Commits](https://github.com/djc/askama/commits)

---
updated-dependencies:
- dependency-name: askama_escape
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
dependabot[bot] 3afb724fed
Bump serde_json from 1.0.78 to 1.0.79
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.78 to 1.0.79.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.78...v1.0.79)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto 9662936b44 Merge pull request 'Update crates' (#1023) from update-crates into main
Reviewed-on: Plume/Plume#1023
2 years ago
Kitaiti Makoto 4780472d48 Make Circle CI follow ructe change 2 years ago
Kitaiti Makoto 6f68c4504b Update Cargo.lock 2 years ago
Kitaiti Makoto 28e0cdfe63 Remove activitystreams from dependencies 2 years ago
Kitaiti Makoto a5003526c8 Follow clippy 2 years ago
Kitaiti Makoto ec3d78b509 Merge remote-tracking branches 'github/dependabot/cargo/ructe-0.14.0', 'github/dependabot/cargo/rsass-0.23.4' and 'github/dependabot/cargo/tracing-subscriber-0.3.8' into update-crates 2 years ago
dependabot[bot] 4205e38605
Bump rsass from 0.23.2 to 0.23.4
Bumps [rsass](https://github.com/kaj/rsass) from 0.23.2 to 0.23.4.
- [Release notes](https://github.com/kaj/rsass/releases)
- [Changelog](https://github.com/kaj/rsass/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kaj/rsass/compare/v0.23.2...v0.23.4)

---
updated-dependencies:
- dependency-name: rsass
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
dependabot[bot] 8438d48c71
Bump ructe from 0.13.4 to 0.14.0
Bumps [ructe](https://github.com/kaj/ructe) from 0.13.4 to 0.14.0.
- [Release notes](https://github.com/kaj/ructe/releases)
- [Changelog](https://github.com/kaj/ructe/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kaj/ructe/compare/v0.13.4...v0.14.0)

---
updated-dependencies:
- dependency-name: ructe
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
dependabot[bot] 52faf5996b
Bump tracing-subscriber from 0.3.7 to 0.3.8
Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.7 to 0.3.8.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.7...tracing-subscriber-0.3.8)

---
updated-dependencies:
- dependency-name: tracing-subscriber
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto 69eccc50a3 Merge pull request 'Add ActivityPub tests and a little fixes' (#1021) from ap-tests into main
Reviewed-on: Plume/Plume#1021
2 years ago
Kitaiti Makoto 54cbdb236f Add tests for Mention activity 2 years ago
Kitaiti Makoto 34c374de1a Attach icon field to User activity only whene it has avatar 2 years ago
Kitaiti Makoto 113722e4ba Add more ActivityPub tests for User 2 years ago
Kitaiti Makoto 3b429909f1 Extract User::outbox_collection_page() from outbox_collection() for testablity 2 years ago
Kitaiti Makoto f1cdf4552f Extract User::outbox_collection() from outbox() for testablity 2 years ago
Kitaiti Makoto 7d320e57da Don't make medias::tests::clean() panic when file not found 2 years ago
Kitaiti Makoto e1a598a459 Attach avater to sample user 2 years ago
Kitaiti Makoto 6107842303 Add tests for Comment::to_activity() and build_delete() 2 years ago
Kitaiti Makoto 65372d2018 Extract comments::tests::prepare_activity() 2 years ago
Kitaiti Makoto 4842385ca6 Add test about reply 2 years ago
Kitaiti Makoto 05f55fc1ca Add https scheme to mention URI in contents 2 years ago
Kitaiti Makoto e8153d4b42 Fix Comment::to_activity() 2 years ago
Kitaiti Makoto 2087a659f9 Add test to validate comment json 2 years ago
Kitaiti Makoto 1770336c11 Make format_datetime() crate public 2 years ago
Kitaiti Makoto 9c177f6286 Change format_datetime implementation according to feature 2 years ago
Kitaiti Makoto 93a2c6d99f Add tests for Follow::build_accept() and build_undo() 2 years ago
Kitaiti Makoto 5ef76873b7 Fix tests 2 years ago
Kitaiti Makoto b97c3fdb87 Extract Follow::build_accept 2 years ago
Kitaiti Makoto 64838ad864 Add test for Follow::to_activity() 2 years ago
Kitaiti Makoto 4df2ce5744 Add mention to test suite for Post activities 2 years ago
Kitaiti Makoto c1f42836d9 Fix variable names 2 years ago
Kitaiti Makoto 0cbc9438d4 Complete a slash to Post Create activity's ID 2 years ago
Kitaiti Makoto ca6cd534d8 Add tests for Post::to_activity(), create_activity() and update_activity() 2 years ago
Kitaiti Makoto f529e803ef Fix ap_url of Reshare 2 years ago
Kitaiti Makoto e5bc84badf Add tests for Reshare::to_activity and build_undo 2 years ago
Kitaiti Makoto e2077bed59 Add test for Like::build_undo 2 years ago
Kitaiti Makoto 9ab9d29efb Remove double slashes 2 years ago
Kitaiti Makoto 5373a674e1 Add test for Like::to_activity 2 years ago
Kitaiti Makoto bfaa2fafaf Install assert-json-diff 2 years ago
Kitaiti Makoto d4a13a13d4 Add assert-json-diff to dev dependencies of plume-common
% cargo add assert-json-diff -p plume-models --dev
2 years ago
Kitaiti Makoto 92c0368dd8 Install activitystreams 0.7.0 2 years ago
Kitaiti Makoto 80c0426768 Add activitystreams 0.7.0 to plume-models dependencies 2 years ago
Kitaiti Makoto 71b21289ab Install activitystreams 0.7.0 2 years ago
Kitaiti Makoto fa861ff314 Add activitystreams 0.7.0 to plume-common dependencies 2 years ago
KitaitiMakoto 400d2dee32 Merge pull request 'Update crates' (#1020) from update-crates into main
Reviewed-on: Plume/Plume#1020
2 years ago
Kitaiti Makoto 3993dda17d Merge remote-tracking branch 'github/dependabot/cargo/tracing-0.1.30' into update-crates 2 years ago
dependabot[bot] b1255efdcd
Bump tracing from 0.1.29 to 0.1.30
Bumps [tracing](https://github.com/tokio-rs/tracing) from 0.1.29 to 0.1.30.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-0.1.29...tracing-0.1.30)

---
updated-dependencies:
- dependency-name: tracing
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
dependabot[bot] 22036c6a94
Bump rsass from 0.23.0 to 0.23.2
Bumps [rsass](https://github.com/kaj/rsass) from 0.23.0 to 0.23.2.
- [Release notes](https://github.com/kaj/rsass/releases)
- [Changelog](https://github.com/kaj/rsass/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kaj/rsass/compare/v0.23.0...v0.23.2)

---
updated-dependencies:
- dependency-name: rsass
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 years ago
KitaitiMakoto f2df4b7d7d Merge pull request 'Don't fill empty content when switching rich editor' (#1017) from content-placeholder into main
Reviewed-on: Plume/Plume#1017
2 years ago
Kitaiti Makoto 7c57bf78a1 [skip ci]Add changelog 2 years ago
Kitaiti Makoto 8d898ff477 Don't fill empty content when switching rich editor 2 years ago
KitaitiMakoto a1045dbce9 Merge pull request 'Fixes #988 Fix email_blocklist schema' (#1016) from block_list-schema into main
Reviewed-on: Plume/Plume#1016
2 years ago
Kitaiti Makoto 23a07f3f7b [skip ci]Add changelog 2 years ago
Kitaiti Makoto 458d87fef1 Run migration 2 years ago
Kitaiti Makoto 82df86d09e Set null to email_blocklist table fields for SQLite 2 years ago
Kitaiti Makoto 858cad2995 Set null to email_blocklist table fields 2 years ago
Kitaiti Makoto c0483cf12e Generate migration files for adding NOT NULL constraints to email_blocklist table fields
% diesel migration generate add_not_null_constraint_to_email_blocklist
2 years ago
KitaitiMakoto 57a54cf016 Merge pull request 'Update Rust' (#1015) from bump-rust into main
Reviewed-on: Plume/Plume#1015
2 years ago
Kitaiti Makoto 325d8cde08 [skip ci]Add changelog about Rust bump 2 years ago
Kitaiti Makoto 9e2c76c3bc Satisfy clippy 2 years ago
Kitaiti Makoto 996b161c1e Satisfy clippy 2 years ago
Kitaiti Makoto 831ef88431 Update Rust 2 years ago
KitaitiMakoto 89517e5988 Merge pull request 'Update crates' (#1014) from update-crates into main
Reviewed-on: Plume/Plume#1014
2 years ago
Kitaiti Makoto 48dbcf75a9 Update crates 2 years ago
Kitaiti Makoto a56a9bc9c5 Add changelogs 2 years ago
KitaitiMakoto 918103fa29 Merge pull request 'Fix #1011 Add Basque' (#1013) from langs into main
Reviewed-on: Plume/Plume#1013
2 years ago
KitaitiMakoto c9b8f5a739 Merge pull request 'Fix #1009 Email Sign-up Explanation' (#1012) from email-signup-explanation into main
Reviewed-on: Plume/Plume#1012
2 years ago
Kitaiti Makoto d58ff36d80 Update po files 2 years ago
Kitaiti Makoto 00d647c0ad Add Basque po files 2 years ago
Kitaiti Makoto a27f196578 Add Basque 2 years ago
Kitaiti Makoto abe82b79ce Update pot file 2 years ago
Kitaiti Makoto 95230c3a23 Add explanation for email signup 2 years ago
Kitaiti Makoto eade69a12c Fix indentation 2 years ago
KitaitiMakoto 4f89e214ef Merge pull request 'Update crates' (#1010) from update-crates into main
Reviewed-on: Plume/Plume#1010
2 years ago
Kitaiti Makoto 2936679326 Update crates 2 years ago
KitaitiMakoto 18a67fe1b5 Merge pull request 'Update crates' (#1008) from update-crates into main
Reviewed-on: Plume/Plume#1008
2 years ago
Kitaiti Makoto ba29c8ef6f Follow atom_syndication API change 2 years ago
Kitaiti Makoto d253f1a020 Upgrade atom_syndication 2 years ago
Kitaiti Makoto 03060d6ee2 Update crates 2 years ago
KitaitiMakoto ac8ad3aae2 Merge pull request 'Add dependabot.yml' (#1007) from dependabot into main
Reviewed-on: Plume/Plume#1007
2 years ago
Kitaiti Makoto 14e294efed Add dependabot.yml 2 years ago
KitaitiMakoto ec3205b372 Merge pull request 'v0.7.1' (#1006) from v0.7.1 into main
Reviewed-on: Plume/Plume#1006
2 years ago
Kitaiti Makoto 45119d9a8c (cargo-release) version {{version}} 2 years ago
Kitaiti Makoto 1065078f75 Update translation files 2 years ago
Kitaiti Makoto 0ce904a985 Update translation files 2 years ago
Kitaiti Makoto 254eec8e6a Follow cargo-release update 2 years ago
KitaitiMakoto 0e4cb4f6e1 Merge pull request '[skip ci]Fix # of pull reuqest in changelog' (#1004) from fix-changelog into main
Reviewed-on: Plume/Plume#1004
2 years ago
Kitaiti Makoto 9b05ac90df [skip ci]Fix # of pull reuqest in changelog 2 years ago
KitaitiMakoto f28a7fa508 Merge pull request 'Add changelogs' (#1003) from changelog into main
Reviewed-on: Plume/Plume#1003
2 years ago
Kitaiti Makoto 65e95d8998 Add changelogs 2 years ago
KitaitiMakoto 808b8f8e98 Merge pull request 'Fix #1001 Deny access to disabled sign-up strategy' (#1002) from restrict-signup into main
Reviewed-on: Plume/Plume#1002
2 years ago
Kitaiti Makoto 43b46a8be4 Make email_signups::create return ErrorPage on error 2 years ago
Kitaiti Makoto 9bbfc71fc8 Fix registration openess condition mistake 2 years ago
Kitaiti Makoto 5d58b31f1c Remove unreachable code 2 years ago
Kitaiti Makoto e31a2238fb Respond with error status code when error 2 years ago
Kitaiti Makoto 7de37bc9b7 Hide password sign-up routings when it's disabled 2 years ago
Kitaiti Makoto 13f7734751 Hide email sign-up routings when it's disabled 2 years ago
Kitaiti Makoto b4395bce99 Implement request guard to detect enabled sign-up strategy 2 years ago
Kitaiti Makoto 7c82b08615 Use into() instead of explicitly wrapping return values 2 years ago
Kitaiti Makoto 6498dbfbb7 Reuse form values 2 years ago
Kitaiti Makoto 74254aed4a Move require_logins from plume-common to plume 2 years ago
KitaitiMakoto 8c48abf48e Merge pull request 'Update crates' (#997) from update-crates into main
Reviewed-on: Plume/Plume#997
2 years ago
Kitaiti Makoto 8958226604 Upgrade shrinkwraprs 2 years ago
Kitaiti Makoto 005a6db230 Update crates 2 years ago
KitaitiMakoto 4397abd8ab Merge pull request 'Add plume-front.pot' (#994) from front-po into main
Reviewed-on: Plume/Plume#994
2 years ago
Kitaiti Makoto e53882f555 Add plume-front.pot 2 years ago
KitaitiMakoto 5d5e61dfa1 Merge pull request 'Update crates' (#993) from update-crates into main
Reviewed-on: Plume/Plume#993
2 years ago
Kitaiti Makoto c5c6b70a89 Upgrade ldap3 2 years ago
Kitaiti Makoto 6778a0e943 Remove hyper from plume-common 2 years ago
Kitaiti Makoto 677e238c6d Follow API change of heck 2 years ago
Kitaiti Makoto b0bc2372fa Upgrade heck 2 years ago
Kitaiti Makoto 6a808c7cc5 Upgrade hex 2 years ago
Kitaiti Makoto d53543ccb1 Upgrade base64 2 years ago
Kitaiti Makoto 88d7d54601 Upgrade whatlang 2 years ago
Kitaiti Makoto 0f0c896887 Upgrade itertools 2 years ago
Kitaiti Makoto 65233c0a9a Upgrade ammonia 2 years ago
Kitaiti Makoto 32e1e4788f Upgrade shrinkwraprs 2 years ago
Kitaiti Makoto 181a78876b Remove askama_escape from dependencies of plume-models 2 years ago
Kitaiti Makoto 61d5446113 Use plume_common::escape() instead of askama_escape::escape() directly 2 years ago
Kitaiti Makoto c786569171 Define plume_common::escape() 2 years ago
Kitaiti Makoto d83a75e3f4 Add askama_escape to plume-common 2 years ago
Kitaiti Makoto a6f06559ea Remove rspassword from dependencies 2 years ago
Kitaiti Makoto 2084145dd3 Upgrade multipart 2 years ago
Kitaiti Makoto dd54058516 Upgrade guid-create 2 years ago
Kitaiti Makoto 4056a54d44 Upgrade tracing-subscriber 2 years ago
Kitaiti Makoto 191cd11741 Upgrade dotenv 2 years ago
Kitaiti Makoto 800e74da67 Follow API change of validator 2 years ago
Kitaiti Makoto 237da47950 Upgrade validator 2 years ago
Kitaiti Makoto ec12539fd0 Follow rsass API change 2 years ago
Kitaiti Makoto a537db559b Upgrade rsass 2 years ago
Kitaiti Makoto 2ba158df67 Update crates 2 years ago
KitaitiMakoto c0c066547f Merge pull request 'Update po files' (#991) from po into main
Reviewed-on: Plume/Plume#991
2 years ago
Kitaiti Makoto c3f59b14b9 Update po files 2 years ago
KitaitiMakoto 1d06a8f1ad Merge pull request 'Fixes #636 Email sign up feature' (#990) from mail-confirmation into main
Reviewed-on: Plume/Plume#990
2 years ago
Kitaiti Makoto efaf1295e9 Suppress clippy 2 years ago
Kitaiti Makoto 4bc9cf3ad1 [skip ci]Complete changelogs 2 years ago
Kitaiti Makoto 1e3851ea69 Execute SQLs for email_signups in transaction 2 years ago
Kitaiti Makoto b6d38536e3 Add email signup feature 2 years ago
Kitaiti Makoto 9b4c678aa9 Make signup token transparent 2 years ago
Kitaiti Makoto a65775d85b Implement EmailSignup 2 years ago
Kitaiti Makoto 192c7677c3 Run migration
% diesel migration run
3 years ago
Kitaiti Makoto 2a31a7b601 Define email_singups table 3 years ago
Kitaiti Makoto 355fd7cb1d Generate create_email_signups_table migration
% diesel migration generate create_email_signups_table
3 years ago
Kitaiti Makoto 40efd73dfc Add config for sign up strategy 3 years ago
KitaitiMakoto 31b144c76d Merge pull request 'Remove unnecessary prefix' (#986) from fix-tag into main
Reviewed-on: Plume/Plume#986
3 years ago
Kitaiti Makoto 31a46514cb Remove unnecessary prefix 3 years ago
KitaitiMakoto c8d906eb99 Merge pull request 'Quote version tag' (#985) from fix-action into main
Reviewed-on: Plume/Plume#985
3 years ago
Kitaiti Makoto 2895a1c819 Quote version tag 3 years ago
KitaitiMakoto a4a5d08662 Merge pull request 'deploy-tags' (#984) from deploy-tags into main
Reviewed-on: Plume/Plume#984
3 years ago
Kitaiti Makoto b97c9d2165 Deploy Docker images with tags 3 years ago
Kitaiti Makoto 5b7e8a69a5 Revert "Deploy tags"
This reverts commit d9a59f1b07.
3 years ago
KitaitiMakoto 9601e99e33 Merge pull request 'Deploy tags' (#983) from deploy-tags into main
Reviewed-on: Plume/Plume#983
3 years ago
Kitaiti Makoto d9a59f1b07 Deploy tags 3 years ago
KitaitiMakoto b6a6af906a Merge pull request '[skip ci]Add changelog abourt MAIL_PORT' (#982) from changelog into main
Reviewed-on: Plume/Plume#982
3 years ago
Kitaiti Makoto 2c4799ce27 [skip ci]Add changelog abourt MAIL_PORT 3 years ago
KitaitiMakoto b33b19849c Merge pull request 'Fix notification page error' (#981) from fix-notification-page into main
Reviewed-on: Plume/Plume#981
3 years ago
Kitaiti Makoto e398f36c57 Add changelog 3 years ago
Kitaiti Makoto ee6064eee8 Don't unwrap() 3 years ago
KitaitiMakoto 9d012c8f3c Merge pull request 'Closes #944 Mail server port configuration' (#980) from mail-server-port into main
Reviewed-on: Plume/Plume#980
3 years ago
Kitaiti Makoto 8888dbba0a Initialize SMTP client with port number 3 years ago
Kitaiti Makoto 6f8d5c1eb4 Add SmtpClient::new_with_addr() method 3 years ago
Kitaiti Makoto 62da4a3d5c Add native-tls to plume-models' dependencies 3 years ago
Kitaiti Makoto 5cfc8e71a5 Remove Lettre from plume module dependencies 3 years ago
Kitaiti Makoto a599760891 Use smtp module from plume_models instead of lettre directly 3 years ago
Kitaiti Makoto 00324f668f Add port field to MailConfig 3 years ago
Kitaiti Makoto d4549704b9 Install Lettre 3 years ago
Kitaiti Makoto 0836e3d693 Add Lettre to plume-models' dependencies 3 years ago
Kitaiti Makoto 0058c3053d Move mail config from plume::mail::mailer to plume_models::CONFIG 3 years ago
KitaitiMakoto 2a1a0a23a5 Merge pull request 'Move bottombar styles to _article.scss' (#978) from bottombar-styling into main
Reviewed-on: Plume/Plume#978
3 years ago
Kitaiti Makoto 5614e3bd59 Move bottombar styles to _article.scss 3 years ago
KitaitiMakoto acbda3cde1 Merge pull request 'Make bottom bar smaller in narrow window' (#977) from layout-post-control into main
Reviewed-on: Plume/Plume#977
3 years ago
Kitaiti Makoto 0755436458 Make bottom bar smaller in narrow window 3 years ago
KitaitiMakoto 3daf405ae2 Merge pull request 'Make blog cover clickable' (#976) from clickable-blog-image into main
Reviewed-on: Plume/Plume#976
3 years ago
Kitaiti Makoto 53dc3b0c03 Fix cover size of posts 3 years ago
Kitaiti Makoto 371dcc5091 Address blog title positoin in dashboard 3 years ago
Kitaiti Makoto 905fe54fa3 Make blog cover a link 3 years ago
Kitaiti Makoto 62c0827ff5 Remove needless whitespaces 3 years ago
KitaitiMakoto d6c65ce81a Merge pull request 'Fix #927 Ensure Post ap_url' (#975) from ensure-ap-url into main
Reviewed-on: Plume/Plume#975
3 years ago
Kitaiti Makoto 5532b4a4d7 Ensure Post ap_url 3 years ago
KitaitiMakoto 637bd3347b Merge pull request 'Fix #967 Fix comment link' (#974) from fix-comment-link into main
Reviewed-on: Plume/Plume#974
3 years ago
Kitaiti Makoto bac373a818 Fix comment link 3 years ago
Kitaiti Makoto f0e7ea5640 (cargo-release) version {{version}} 3 years ago
Kitaiti Makoto 4b981e0fad (cargo-release) version {{version}} 3 years ago
KitaitiMakoto 8fb9d861de Merge pull request '[skip ci]Give up to deploy tags' (#973) from giveup-tag into main
Reviewed-on: Plume/Plume#973
3 years ago
Kitaiti Makoto 199269ba3c [skip ci]Give up to deploy tags 3 years ago
KitaitiMakoto 0b9ec4c52c Merge pull request 'Use Git tag for Docker image tag' (#972) from deploy-tag into main
Reviewed-on: Plume/Plume#972
3 years ago
Kitaiti Makoto 0e51565cc8 Use Git tag for Docker image tag 3 years ago
KitaitiMakoto 0418d35b67 Merge pull request 'Fix a typo' (#971) from deploy-tag into main
Reviewed-on: Plume/Plume#971
3 years ago
Kitaiti Makoto bf9d25363b Fix a typo 3 years ago
KitaitiMakoto 84f00c57d1 Merge pull request 'Fix tag calculation' (#970) from deploy-tag into main
Reviewed-on: Plume/Plume#970
3 years ago
Kitaiti Makoto 7c1a5421fa Fix tag calculation 3 years ago
KitaitiMakoto 3815bfe980 Merge pull request '[skip ci]Deploy tags to Docker Hub' (#969) from deploy-tag into main
Reviewed-on: Plume/Plume#969
3 years ago
Kitaiti Makoto de448c3192 [skip ci]Deploy tags to Docker Hub 3 years ago
KitaitiMakoto 967e2dfde6 Merge pull request 'Add GitHub Action to deploy Docker image' (#968) from gh-action into main
Reviewed-on: Plume/Plume#968
3 years ago
Kitaiti Makoto dd3c1eac5f Add GitHub Action to deploy Docker image 3 years ago
KitaitiMakoto abc0a794c1 Merge pull request 'v0.7.0' (#923) from v0.7.0 into main
Reviewed-on: Plume/Plume#923
3 years ago
Kitaiti Makoto ef628aa498 (cargo-release) version {{version}} 3 years ago
Kitaiti Makoto 011bd9602d Update plume-buildenv Docker image to v0.4.0 3 years ago
Kitaiti Makoto b0745cfd82 Fix search string in release.toml 3 years ago
Kitaiti Makoto d1b1d9f507 Update translations 3 years ago
Kitaiti Makoto 742d545f1c Update po files 3 years ago
Kitaiti Makoto 67dec4df22 Merge remote-tracking branch 'origin/main' into v0.7.0 3 years ago
KitaitiMakoto 33f3bbb774 Merge pull request 'Proper scaling of the default image, when a user doesn't have custom avatar' (#965) from mareklach/Plume:proper_scaling_for_blank_user_avatar into main
Reviewed-on: Plume/Plume#965
3 years ago
mareklach 7c1df80695 Update 'assets/themes/default/_global.scss' 3 years ago
mareklach 2adbb6f74c Merge branch 'main' into proper_scaling_for_blank_user_avatar 3 years ago
mareklach 79c05e8381 Proper scaling of the default image, when a user doesn't have custom avatar 3 years ago
Kitaiti Makoto 31a1591043 Merge remote-tracking branch 'origin/main' into v0.7.0 3 years ago
KitaitiMakoto 0ede2ab3ab Merge pull request 'Fixes #936: Sign GET request' (#957) from sign-get into main
Reviewed-on: Plume/Plume#957
3 years ago
Kitaiti Makoto 43656d8e46 Create instance for test 3 years ago
Kitaiti Makoto b7cc2369a7 Allow clippy::needless_borrow on CI 3 years ago
Kitaiti Makoto 05a98175bf Assign unsed return value to underscore variable 3 years ago
Kitaiti Makoto 346a67fe1c Create local instance user on start 3 years ago
Kitaiti Makoto a22f5f2336 Add more changelogs 3 years ago
Kitaiti Makoto 55439990c6 Complete changelog 3 years ago
Kitaiti Makoto 3b0f28d061 Update po files 3 years ago
Kitaiti Makoto ac3acfb4ac Merge latest translations 3 years ago
Kitaiti Makoto d5256e9ffc Merge translations from Crowdin 3 years ago
Kitaiti Makoto 13be46445c Add changelog about signing GET requests 3 years ago
Kitaiti Makoto c67b702425 Cache LOCAL_INSTANCE_USER once 3 years ago
Kitaiti Makoto e01539ef16 Follow Clippy 3 years ago
Kitaiti Makoto de4380fd34 Cache local when creating local instance 3 years ago
Kitaiti Makoto 5651e11ba1 Add FIXME comment 3 years ago
KitaitiMakoto 5815602309 Merge pull request 'Don't shrink edit link' (#964) from edit-link-no-shrink into main
Reviewed-on: Plume/Plume#964
3 years ago
Kitaiti Makoto a0a69dfb22 Don't shrink edit link 3 years ago
Kitaiti Makoto d98132db80 Don't log unnecessary error 3 years ago
Kitaiti Makoto 25fe2ad802 Use request::get() instead of ClientBuilder 3 years ago
Kitaiti Makoto 48fab8ad2c Impl From for request::Error 3 years ago
Kitaiti Makoto a7d8d49faf Define request::get() function 3 years ago
Kitaiti Makoto 388acd6738 Remove needless reference sign 3 years ago
Kitaiti Makoto d3c035aa39 Remove needless code 3 years ago
Kitaiti Makoto 12a8d00f8e Cache local instance user for test 3 years ago
Kitaiti Makoto b9ea06a01a Don't stop even when caching local instance user 3 years ago
Kitaiti Makoto 1e67b3c13c Create local instance user on caching if it doesn't exist 3 years ago
Kitaiti Makoto 5a5c8bdac8 Make inbox test follow API change 3 years ago
Kitaiti Makoto 44f9d36df1 Make request test follow API change 3 years ago
Kitaiti Makoto 7d349c2de6 Install once_cell 3 years ago
Kitaiti Makoto 79715ec7c7 Add once_cell to dev-dependencies for plume-common 3 years ago
Kitaiti Makoto f4d7dfb261 Sign GET request to other instances 3 years ago
Kitaiti Makoto c525410062 Create local instance user on creating instance 3 years ago
Kitaiti Makoto af5b0b961b Extract Instance::create_local_instance_user() from get_local_instance_user_uncached() 3 years ago
Kitaiti Makoto 2f7a5cbf56 Cache local instance user on start 3 years ago
Kitaiti Makoto 1506802c20 Implement LOCAL_INSTANCE_USER and related methods 3 years ago
Kitaiti Makoto 897ea8e11e Change const name: LOCAL_INSTANCE_USER -> LOCAL_INSTANCE_USERNAME 3 years ago
Kitaiti Makoto 858806149a Use concrete Error for Signer 3 years ago
Kitaiti Makoto 0da9572627 Implement User::get_local_instance_user() 3 years ago
Kitaiti Makoto 94cc260803 Add Instance to users::Role 3 years ago
KitaitiMakoto 7198b06a33 Merge pull request 'Fix problem of PL #956' (#963) from mskf1383/Plume:main into main
Reviewed-on: Plume/Plume#963
Reviewed-by: KitaitiMakoto <kitaitimakoto@noreply@joinplu.me>
3 years ago
MohammadSaleh Kamyab 81006e1db8 Update 'assets/themes/default/_global.scss' 3 years ago
KitaitiMakoto 0f7094a70e Merge pull request 'Update crates' (#961) from update-crates into main
Reviewed-on: Plume/Plume#961
3 years ago
Kitaiti Makoto a0b661fffe Make template follow change of Rust 3 years ago
Kitaiti Makoto 4bef91f08b Follow clippy warnings 3 years ago
Kitaiti Makoto 20c17be124 Update Rust version 3 years ago
Kitaiti Makoto ae3344f318 Follow Rust and crates update 3 years ago
Kitaiti Makoto 9187e4dde9 Update crates 3 years ago
Kitaiti Makoto 89c185f819 Specify Rocket and serde_json version loosely 3 years ago
Kitaiti Makoto 09b9a37720 Update Rust version 3 years ago
KitaitiMakoto 12a8bfcf2d Merge pull request 'Fix article title overflowing on small screens' (#960) from mareklach/Plume:fix-article-title-overflow-spill into main
Reviewed-on: Plume/Plume#960
3 years ago
mareklach b2be00b125 Fix article title overflowing on small screens
Improves CSS scaling of the article title for mobile-sized screens, to prevent it spilling over.
3 years ago
KitaitiMakoto 914d394bd0 Merge pull request 'Stick serde_json versoin for plume-models' (#959) from stick-serde_json into main
Reviewed-on: Plume/Plume#959
3 years ago
Kitaiti Makoto 811db1be0e Stick serde_json versoin for plume-models 3 years ago
KitaitiMakoto cd81f042b9 Merge pull request 'Stick serde_json to < 1.0.70' (#958) from stick-serde_json into main
Reviewed-on: Plume/Plume#958
3 years ago
Kitaiti Makoto c6111fcd28 Stick serde_json to < 1.0.70 3 years ago
KitaitiMakoto 82ebdc023c Merge pull request 'Fix RTL problem in post card' (#956) from mskf1383/Plume:main into main
Reviewed-on: Plume/Plume#956
Reviewed-by: KitaitiMakoto <kitaitimakoto@noreply@joinplu.me>
3 years ago
MohammadSaleh Kamyab 7b110179a9 Update 'assets/themes/default/_global.scss' 3 years ago
MohammadSaleh Kamyab afd66ce7cb Update 'assets/themes/default/_global.scss' 3 years ago
MohammadSaleh Kamyab 9dac97045b Update 'templates/partials/post_card.rs.html' 3 years ago
KitaitiMakoto 188d4ac063 Merge pull request 'Post ap_url as valid IRI' (#947) from post-slug into main
Reviewed-on: Plume/Plume#947
3 years ago
Kitaiti Makoto 0fbefe2cdc Percent-encode Post slug on sending Activity to other instances 3 years ago
Kitaiti Makoto eedd5fe4e9 Define utility function to percent-encode for IRI 3 years ago
KitaitiMakoto 27a1a56223 Merge pull request 'Make Feature list more clear' (#946) from github-856 into main
Reviewed-on: Plume/Plume#946
3 years ago
Kitaiti Makoto 8e3322776a Merge remote-tracking branch 'origin/main' into github-856 3 years ago
KitaitiMakoto 3d4336c548 Merge pull request 'update some dependancies' (#945) from update-deps into main
Reviewed-on: Plume/Plume#945
Reviewed-by: trinity-1686a <trinity-1686a@noreply@joinplu.me>
3 years ago
Trinity Pointard 0c8c607aa4 update some dependancies 3 years ago
D5k H3h 60d6734fe2
Make Feature list more clear
Say it, when something is currently missing.
Otherwise, people will get confused.
3 years ago
KitaitiMakoto 8c372aa6fc Merge pull request 'Fixes #929 Don't stip shipped binaries' (#942) from dont-strip into main
Reviewed-on: Plume/Plume#942
3 years ago
Kitaiti Makoto d1a74ca8e6 Don't stip shipped binaries 3 years ago
KitaitiMakoto c374e0af4c Merge pull request 'Upgrade bcrypt create' (#941) from update-blowfish into main
Reviewed-on: Plume/Plume#941
3 years ago
Kitaiti Makoto 58e8569048 Install bcrypt 3 years ago
Kitaiti Makoto 83dbf2a945 Upgrade bcrypt 3 years ago
trinity-1686a d7d6d5f644 Merge pull request 'Draw side line for blockquote on start' (#933) from ahangarha/Plume:ahangarha-patch-1-blockquote-line into main
Reviewed-on: Plume/Plume#933
Reviewed-by: trinity-1686a <trinity-1686a@noreply@joinplu.me>
3 years ago
trinity-1686a 9aa9885a89 Merge pull request 'Specify rocket version' (#937) from zynnnn/Plume:fix-rocket-version into main
Reviewed-on: Plume/Plume#937
Reviewed-by: trinity-1686a <trinity-1686a@noreply@joinplu.me>
3 years ago
Trinity Pointard 7fe1e083e0 use newer pulldown-cmark 3 years ago
Tdxdxoz a946823554 Specify rocket version 3 years ago
ahangarha 3a1872c03e Draw side line for blockquote on start
By this change, the line beside blockquote would appear at start. It means if the text is in LTR language, it appears on left and if it is in RTL, the line appears on right.
3 years ago
KitaitiMakoto 5424f9110f Merge pull request 'Add changelog about change of slug' (#921) from changelog into main
Reviewed-on: Plume/Plume#921
3 years ago
Kitaiti Makoto a597816617 Add changelog about change of slug 3 years ago
KitaitiMakoto fa7a44f6bb Merge pull request 'Fix #326 and #721: Keep title in URI' (#920) from keep-title-in-uri into main
Reviewed-on: Plume/Plume#920
3 years ago
Kitaiti Makoto dfcdcc1833 Remove heck from plume and plume-models 3 years ago
Kitaiti Makoto 6345a57498 Don't modify article title for slug 3 years ago
Kitaiti Makoto 87457c0ed1 Define Post::slug() 3 years ago
KitaitiMakoto 859a1fd528 Merge pull request 'Set default RUST_LOG to info' (#919) from default-log-level into main
Reviewed-on: Plume/Plume#919
3 years ago
Kitaiti Makoto 5069aab584 Set default RUST_LOG to info 3 years ago
KitaitiMakoto 033fde38fe Merge pull request 'Extract Post::ap_url()' (#918) from ap_url into main
Reviewed-on: Plume/Plume#918
3 years ago
Kitaiti Makoto 7b2bab0f9d Extract Post::ap_url() 3 years ago
KitaitiMakoto a419ef5319 Merge pull request 'Prevent duplicated posts in 'all' timeline' (#917) from dup-timeline into main
Reviewed-on: Plume/Plume#917
3 years ago
Kitaiti Makoto f77dce9f12 Add changelog about fix to duplicated posts in all timeline 3 years ago
Kitaiti Makoto 4c2cd92f0d Prevent duplicated posts in 'all' timeline 3 years ago
KitaitiMakoto dd932e1f15 Merge pull request 'Calculate media URI properly even when MEDIA_UPLOAD_DIRECTORY configured' (#916) from media-directory into main
Reviewed-on: Plume/Plume#916
3 years ago
Kitaiti Makoto 4376810d96 Add changelog about fix of MEDIA_UPLOAD_DIRECTORY handling 3 years ago
Kitaiti Makoto c961f4751b Complete pull request number to changelog 3 years ago
Kitaiti Makoto 9e5ec0c9df Calculate media URI properly even when MEDIA_UPLOAD_DIRECTORY configured 3 years ago
KitaitiMakoto 112c034e27 Merge pull request 'Update Post.ap_url when published' (#915) from update-ap-url into main
Reviewed-on: Plume/Plume#915
3 years ago
Kitaiti Makoto 664a3ddeea Add changelog about update of ap_url 3 years ago
Kitaiti Makoto 2ffd357d95 Update Post.ap_url when published 3 years ago
KitaitiMakoto 2bc4a13964 Merge pull request 'Add migration to add index to medias.file_path for SQLite3' (#914) from upsert-sqlite into main
Reviewed-on: Plume/Plume#914
3 years ago
Kitaiti Makoto 58324945cc Add migration to add index to medias.file_path for SQLite3 3 years ago
KitaitiMakoto 16953ea907 Merge pull request 'Upsert posts and media when fetching remote posts' (#912) from upsert into main
Reviewed-on: Plume/Plume#912
3 years ago
Kitaiti Makoto d3c2dc8286 Add changelog on upsert 3 years ago
Kitaiti Makoto ebb0b45299 Create blog_authors only when needed 3 years ago
Kitaiti Makoto 702aa11ecf Don't drop path components of image URI 3 years ago
Kitaiti Makoto 33221d386e Upsert Post in from_activity() 3 years ago
Kitaiti Makoto 589c159eb9 Call Post::publish_updated() only for local posts 3 years ago
Kitaiti Makoto ea1f4d48d5 Upsert Media in from_activity() 3 years ago
Kitaiti Makoto 462c5a1d42 Define Media::find_by_file_path() 3 years ago
Kitaiti Makoto f90d7ddee3 Add SQL to add/drop index to/from medias.file_path 3 years ago
Kitaiti Makoto 175055cf9d Add migration to add index to medias.file_path
% diesel migration generate medias_index_file_path
3 years ago
KitaitiMakoto fe92d95f6c Merge pull request 'Make actors subscribe to channel once' (#913) from remote-fetch-once into main
Reviewed-on: Plume/Plume#913
3 years ago
Kitaiti Makoto 8e50d95a7a Fix code according to clippy 3 years ago
Kitaiti Makoto 9ed36b2aa3 Add changelog on actor subscription fix 3 years ago
Kitaiti Makoto 722165a734 Upgrade lexical-core crate 3 years ago
Kitaiti Makoto 74f99e2588 Upgrade Rust version 3 years ago
Kitaiti Makoto 77c08845b5 [BUG FIX]Make SearchActor subscribe to channel once 3 years ago
Kitaiti Makoto 2bfc26faf2 [BUG FIX]Make RemoveFetchActor subscribe to channel once 3 years ago

@ -10,8 +10,8 @@ executors:
type: boolean
default: false
docker:
- image: plumeorg/plume-buildenv:v0.4.0
- image: <<#parameters.postgres>>circleci/postgres:9.6-alpine<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
- image: plumeorg/plume-buildenv:v0.7.0
- image: <<#parameters.postgres>>cimg/postgres:14.2<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
environment:
POSTGRES_USER: postgres
POSTGRES_DB: plume
@ -38,7 +38,7 @@ commands:
- restore_cache:
keys:
- v0-<< parameters.cache >>-{{ checksum "Cargo.lock" }}-{{ .Branch }}
- v0-<< parameters.cache >>-{{ checksum "Cargo.lock" }}-master
- v0-<< parameters.cache >>-{{ checksum "Cargo.lock" }}-main
cache:
description: push cache
@ -63,6 +63,7 @@ commands:
type: boolean
default: false
steps:
- run: rustup component add clippy --toolchain nightly-2022-01-27-x86_64-unknown-linux-gnu
- run: cargo clippy <<^parameters.no_feature>>--no-default-features --features="${FEATURES}"<</parameters.no_feature>> --release -p <<parameters.package>> -- -D warnings
run_with_coverage:
@ -111,6 +112,7 @@ jobs:
name: default
steps:
- restore_env
- run: rustup component add rustfmt --toolchain nightly-2022-01-27-x86_64-unknown-linux-gnu
- run: cargo fmt --all -- --check
clippy:
@ -258,4 +260,4 @@ workflows:
filters:
branches:
only:
- /^master/
- /^main/

@ -1,4 +1,4 @@
FROM debian:buster-20210208
FROM rust:1-buster
ENV PATH="/root/.cargo/bin:${PATH}"
#install native/circleci/build dependancies
@ -6,19 +6,18 @@ RUN apt update &&\
apt install -y --no-install-recommends git ssh tar gzip ca-certificates default-jre&&\
echo "deb [trusted=yes] https://apt.fury.io/caddy/ /" \
| tee -a /etc/apt/sources.list.d/caddy-fury.list &&\
wget -qO - https://artifacts.crowdin.com/repo/GPG-KEY-crowdin | apt-key add - &&\
echo "deb https://artifacts.crowdin.com/repo/deb/ /" > /etc/apt/sources.list.d/crowdin.list &&\
apt update &&\
apt install -y --no-install-recommends binutils-dev build-essential cmake curl gcc gettext git libcurl4-openssl-dev libdw-dev libelf-dev libiberty-dev libpq-dev libsqlite3-dev libssl-dev make openssl pkg-config postgresql postgresql-contrib python zlib1g-dev python3-pip zip unzip libclang-dev clang caddy&&\
apt install -y --no-install-recommends binutils-dev build-essential cmake curl gcc gettext git libcurl4-openssl-dev libdw-dev libelf-dev libiberty-dev libpq-dev libsqlite3-dev libssl-dev make openssl pkg-config postgresql postgresql-contrib python zlib1g-dev python3-dev python3-pip python3-setuptools zip unzip libclang-dev clang caddy crowdin3 &&\
rm -rf /var/lib/apt/lists/*
#install and configure rust
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2021-01-15 -y &&\
rustup component add rustfmt clippy &&\
rustup component add rust-std --target wasm32-unknown-unknown
#stick rust environment
COPY rust-toolchain ./
#compile some deps
RUN cargo install wasm-pack &&\
cargo install grcov &&\
strip /root/.cargo/bin/* &&\
rm -fr ~/.cargo/registry
#set some compilation parametters
@ -29,11 +28,3 @@ RUN pip3 install selenium
#configure caddy
COPY Caddyfile /Caddyfile
#install crowdin
RUN mkdir /crowdin && cd /crowdin &&\
curl -O https://downloads.crowdin.com/cli/v2/crowdin-cli.zip &&\
unzip crowdin-cli.zip && rm crowdin-cli.zip &&\
cd * && mv crowdin-cli.jar /usr/local/bin && cd && rm -rf /crowdin &&\
/bin/echo -e '#!/bin/sh\njava -jar /usr/local/bin/crowdin-cli.jar $@' > /usr/local/bin/crowdin &&\
chmod +x /usr/local/bin/crowdin

@ -16,7 +16,7 @@ DATABASE_URL=postgres://plume:plume@localhost/plume
BASE_URL=plu.me
# Log level for each crate
RUST_LOG=warn,html5ever=warn,hyper=warn,tantivy=warn
RUST_LOG=info
# The secret key for private cookies and CSRF protection
# You can generate one with `openssl rand -base64 32`

@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: cargo
directory: /
schedule:
interval: daily

@ -0,0 +1,30 @@
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

@ -0,0 +1,36 @@
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 }}

@ -4,6 +4,52 @@
## [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)
@ -11,19 +57,31 @@
- 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-01-15 (#878)
- 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
@ -204,7 +262,10 @@
- Ability to create multiple blogs
<!-- next-url -->
[Unreleased]: https://github.com/Plume-org/Plume/compare/0.6.0...HEAD
[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

2905
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -1,38 +1,33 @@
[package]
authors = ["Plume contributors"]
name = "plume"
version = "0.6.1-dev"
version = "0.7.3-dev"
repository = "https://github.com/Plume-org/Plume"
edition = "2018"
[dependencies]
activitypub = "0.1.3"
askama_escape = "0.1"
atom_syndication = "0.6"
atom_syndication = "0.11.0"
clap = "2.33"
dotenv = "0.15.0"
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
guid-create = "0.1"
heck = "0.3.0"
lettre = "0.9.2"
gettext = "0.4.0"
gettext-macros = "0.6.1"
gettext-utils = "0.1.0"
guid-create = "0.2"
lettre_email = "0.9.2"
num_cpus = "1.10"
rocket = "0.4.6"
rocket_contrib = { version = "0.4.5", features = ["json"] }
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
rpassword = "4.0"
scheduled-thread-pool = "0.2.2"
rocket = "0.4.11"
rocket_contrib = { version = "0.4.11", features = ["json"] }
rocket_i18n = "0.4.1"
scheduled-thread-pool = "0.2.6"
serde = "1.0"
serde_json = "1.0"
shrinkwraprs = "0.2.1"
validator = "0.8"
validator_derive = "0.8"
serde_json = "1.0.81"
shrinkwraprs = "0.3.0"
validator = { version = "0.15", features = ["derive"] }
webfinger = "0.4.1"
tracing = "0.1.22"
tracing-subscriber = "0.2.15"
tracing = "0.1.35"
tracing-subscriber = "0.3.10"
riker = "0.4.2"
activitystreams = "0.7.0-alpha.18"
[[bin]]
name = "plume"
@ -44,7 +39,7 @@ version = "0.4"
[dependencies.ctrlc]
features = ["termination"]
version = "3.1.2"
version = "3.2.2"
[dependencies.diesel]
features = ["r2d2", "chrono"]
@ -53,7 +48,7 @@ version = "1.4.5"
[dependencies.multipart]
default-features = false
features = ["server"]
version = "0.16"
version = "0.18"
[dependencies.plume-api]
path = "plume-api"
@ -69,8 +64,8 @@ git = "https://github.com/fdb-hiroshima/rocket_csrf"
rev = "29910f2829e7e590a540da3804336577b48c7b31"
[build-dependencies]
ructe = "0.13.0"
rsass = "0.9"
ructe = "0.14.0"
rsass = "0.25"
[features]
default = ["postgres"]

@ -1,10 +1,10 @@
<h1 align="center">
<img src="https://raw.githubusercontent.com/Plume-org/Plume/master/assets/icons/trwnh/feather/plumeFeather64.png" alt="Plume's logo">
<img src="https://raw.githubusercontent.com/Plume-org/Plume/main/assets/icons/trwnh/feather/plumeFeather64.png" alt="Plume's logo">
Plume
</h1>
<p align="center">
<a href="https://github.com/Plume-org/Plume/"><img alt="CircleCI" src="https://img.shields.io/circleci/build/gh/Plume-org/Plume.svg"></a>
<a href="https://codecov.io/gh/Plume-org/Plume"><img src="https://codecov.io/gh/Plume-org/Plume/branch/master/graph/badge.svg" alt="Code coverage"></a>
<a href="https://codecov.io/gh/Plume-org/Plume"><img src="https://codecov.io/gh/Plume-org/Plume/branch/main/graph/badge.svg" alt="Code coverage"></a>
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/plume"><img src="https://d322cqt584bo4o.cloudfront.net/plume/localized.svg"></a>
<a href="https://hub.docker.com/r/plumeorg/plume"><img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/plumeorg/plume.svg"></a>
<a href="https://liberapay.com/Plume"><img alt="Liberapay patrons" src="https://img.shields.io/liberapay/patrons/Plume.svg"></a>
@ -30,7 +30,7 @@ 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.
- **Collaborative writing**: invite other people to your blogs, and write articles together. (Not implemented yet, but will be in 1.0)
## Get involved
@ -53,3 +53,4 @@ 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/).

@ -98,7 +98,7 @@ main article {
}
blockquote {
border-left: 5px solid $gray;
border-inline-start: 5px solid $gray;
margin: 1em auto;
padding: 0em 2em;
}
@ -516,4 +516,11 @@ input:checked ~ .cw-container > .cw-text {
main .article-meta > *, main .article-meta .comments, main .article-meta > .banner > * {
margin: 0 5%;
}
.bottom-bar {
align-items: center;
& > div:nth-child(2) {
margin: 0;
}
}
}

@ -135,6 +135,7 @@ form.new-post {
.button + .button {
margin-left: 1em;
margin-inline-start: 1em;
}
.split {

@ -219,15 +219,23 @@ p.error {
margin: 20px;
}
.cover-link {
margin: 0;
&:hover {
opacity: 0.9;
}
}
.cover {
min-height: 10em;
background-position: center;
background-size: cover;
margin: 0px;
}
&:hover {
opacity: 0.9;
}
header {
display: flex;
}
h3 {
@ -236,9 +244,14 @@ p.error {
font-family: $playfair;
font-size: 1.75em;
font-weight: normal;
line-height: 1.75;
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;
@ -247,7 +260,8 @@ p.error {
}
.controls {
float: right;
flex-shrink: 0;
text-align: end;
.button {
margin-top: 0;
@ -261,7 +275,7 @@ p.error {
font-family: $lora;
font-size: 1em;
line-height: 1.25;
text-align: left;
text-align: initial;
overflow: hidden;
}
}
@ -465,9 +479,10 @@ figure {
/// Avatars
.avatar {
background-position: center;
background-position: center !important;
background-size: cover;
border-radius: 100%;
flex-shrink: 0;
&.small {
width: 50px;
@ -492,6 +507,7 @@ figure {
margin: auto $horizontal-margin 2em;
overflow: auto;
display: flex;
justify-content: center;
a {
display: inline-block;
@ -561,14 +577,6 @@ figure {
}
}
.bottom-bar {
flex-direction: column;
align-items: center;
& > div {
margin: 0;
}
}
main .article-meta .comments .comment {
header {
flex-direction: column;

@ -41,9 +41,9 @@ fn main() {
.expect("compile templates");
compile_themes().expect("Theme compilation error");
recursive_copy(&Path::new("assets").join("icons"), &Path::new("static"))
recursive_copy(&Path::new("assets").join("icons"), Path::new("static"))
.expect("Couldn't copy icons");
recursive_copy(&Path::new("assets").join("images"), &Path::new("static"))
recursive_copy(&Path::new("assets").join("images"), Path::new("static"))
.expect("Couldn't copy images");
create_dir_all(&Path::new("static").join("media")).expect("Couldn't init media directory");
@ -97,12 +97,12 @@ fn compile_theme(path: &Path, out_dir: &Path) -> std::io::Result<()> {
.components()
.skip_while(|c| *c != Component::Normal(OsStr::new("themes")))
.skip(1)
.filter_map(|c| {
.map(|c| {
c.as_os_str()
.to_str()
.unwrap_or_default()
.splitn(2, '.')
.next()
.split_once('.')
.map_or(c.as_os_str().to_str().unwrap_or_default(), |x| x.0)
})
.collect::<Vec<_>>()
.join("-");
@ -120,8 +120,14 @@ 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_file(path, rsass::OutputStyle::Compressed)
.expect("SCSS compilation error"),
&rsass::compile_scss_path(
path,
rsass::output::Format {
style: rsass::output::Style::Compressed,
..rsass::output::Format::default()
},
)
.expect("SCSS compilation error"),
)?;
Ok(())

@ -0,0 +1 @@
CREATE INDEX medias_index_file_path ON medias (file_path);

@ -0,0 +1,9 @@
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);

@ -0,0 +1,4 @@
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;

@ -0,0 +1,4 @@
ALTER TABLE email_blocklist ALTER COLUMN email_address SET NOT NULL;
ALTER TABLE email_blocklist ALTER COLUMN note SET NOT NULL;
ALTER TABLE email_blocklist ALTER COLUMN notify_user SET NOT NULL;
ALTER TABLE email_blocklist ALTER COLUMN notification_text SET NOT NULL;

@ -0,0 +1 @@
CREATE INDEX medias_index_file_path ON medias (file_path);

@ -0,0 +1,9 @@
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);

@ -0,0 +1,9 @@
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;

@ -0,0 +1,9 @@
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,6 +1,6 @@
[package]
name = "plume-api"
version = "0.6.1-dev"
version = "0.7.2"
authors = ["Plume contributors"]
edition = "2018"

@ -1,2 +1,3 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
release = false

@ -1,6 +1,6 @@
[package]
name = "plume-cli"
version = "0.6.1-dev"
version = "0.7.2"
authors = ["Plume contributors"]
edition = "2018"
@ -10,8 +10,8 @@ path = "src/main.rs"
[dependencies]
clap = "2.33"
dotenv = "0.14"
rpassword = "5.0.0"
dotenv = "0.15"
rpassword = "6.0.1"
[dependencies.diesel]
features = ["r2d2", "chrono"]

@ -1,2 +1,3 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
release = false

@ -68,4 +68,6 @@ 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");
}

@ -25,7 +25,7 @@ fn main() {
e => e.map(|_| ()).unwrap(),
}
let conn = Conn::establish(CONFIG.database_url.as_str());
let _ = conn.as_ref().map(|conn| Instance::cache_local(conn));
let _ = conn.as_ref().map(Instance::cache_local);
match matches.subcommand() {
("instance", Some(args)) => {

@ -1,29 +1,30 @@
[package]
name = "plume-common"
version = "0.6.1-dev"
version = "0.7.2"
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.10"
heck = "0.3.0"
hex = "0.3"
hyper = "0.12.33"
openssl = "0.10.22"
rocket = "0.4.6"
reqwest = { version = "0.9", features = ["socks"] }
base64 = "0.13"
hex = "0.4"
openssl = "0.10.40"
rocket = "0.4.11"
reqwest = { version = "0.11.11", features = ["blocking", "json", "socks"] }
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
serde_json = "1.0.81"
shrinkwraprs = "0.3.0"
syntect = "4.5.0"
tokio = "0.1.22"
regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] }
tracing = "0.1.22"
regex-syntax = { version = "0.6.26", default-features = false, features = ["unicode-perl"] }
tracing = "0.1.35"
askama_escape = "0.10.3"
activitystreams = "0.7.0-alpha.18"
activitystreams-ext = "0.1.0-alpha.2"
url = "2.2.2"
flume = "0.10.13"
tokio = { version = "1.19.2", features = ["full"] }
futures = "0.3.21"
[dependencies.chrono]
features = ["serde"]
@ -33,3 +34,9 @@ version = "0.4"
default-features = false
git = "https://git.joinplu.me/Plume/pulldown-cmark"
branch = "bidi-plume"
[dev-dependencies]
assert-json-diff = "2.0.1"
once_cell = "1.12.0"
[features]

@ -1,2 +1,3 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
release = false

@ -1,6 +1,8 @@
use reqwest::header::{HeaderValue, ACCEPT};
use reqwest;
use std::fmt::Debug;
use super::{request, sign::Signer};
/// Represents an ActivityPub inbox.
///
/// It routes an incoming Activity through the registered handlers.
@ -8,9 +10,51 @@ use std::fmt::Debug;
/// # Example
///
/// ```rust
/// # extern crate activitypub;
/// # use activitypub::{actor::Person, activity::{Announce, Create}, object::Note};
/// # 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;
/// # 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 = ();
@ -23,6 +67,10 @@ use std::fmt::Debug;
/// # 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 {
@ -42,6 +90,10 @@ use std::fmt::Debug;
/// # 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 = ();
@ -60,12 +112,13 @@ use std::fmt::Debug;
/// # }
/// # }
/// #
/// # 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 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 activity_json = serde_json::to_value(act).unwrap();
/// #
/// # let conn = ();
@ -144,29 +197,29 @@ where
}
/// Registers an handler on this Inbox.
pub fn with<A, V, M>(self, proxy: Option<&reqwest::Proxy>) -> Inbox<'a, C, E, R>
pub fn with<A, V, M>(self, proxy: Option<&reqwest::Proxy>) -> Self
where
A: AsActor<&'a C> + FromId<C, Error = E>,
V: activitypub::Activity,
V: activitystreams::markers::Activity + serde::de::DeserializeOwned,
M: AsObject<A, V, &'a C, Error = E> + FromId<C, Error = E>,
M::Output: Into<R>,
{
if let Inbox::NotHandled(ctx, mut act, e) = self {
if let Self::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 Inbox::NotHandled(ctx, act, InboxError::InvalidID),
None => return Self::NotHandled(ctx, act, InboxError::InvalidID),
};
// Get the actor ID
let actor_id = match get_id(act["actor"].clone()) {
Some(x) => x,
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(None)),
None => return Self::NotHandled(ctx, act, InboxError::InvalidActor(None)),
};
if Self::is_spoofed_activity(&actor_id, &act) {
return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(None));
return Self::NotHandled(ctx, act, InboxError::InvalidObject(None));
}
// Transform this actor to a model (see FromId for details about the from_id function)
@ -182,14 +235,14 @@ where
if let Some(json) = json {
act["actor"] = json;
}
return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(Some(e)));
return Self::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 Inbox::NotHandled(ctx, act, InboxError::InvalidObject(None)),
None => return Self::NotHandled(ctx, act, InboxError::InvalidObject(None)),
};
let obj = match M::from_id(
ctx,
@ -202,19 +255,19 @@ where
if let Some(json) = json {
act["object"] = json;
}
return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(Some(e)));
return Self::NotHandled(ctx, act, InboxError::InvalidObject(Some(e)));
}
};
// Handle the activity
match obj.activity(ctx, actor, &act_id) {
Ok(res) => Inbox::Handled(res.into()),
Err(e) => Inbox::Failed(e),
match obj.activity(ctx, actor, act_id) {
Ok(res) => Self::Handled(res.into()),
Err(e) => Self::Failed(e),
}
} else {
// If the Activity type is not matching the expected one for
// this handler, try with the next one.
Inbox::NotHandled(ctx, act, e)
Self::NotHandled(ctx, act, e)
}
} else {
self
@ -280,7 +333,7 @@ pub trait FromId<C>: Sized {
type Error: From<InboxError<Self::Error>> + Debug;
/// The ActivityPub object type representing Self
type Object: activitypub::Object;
type Object: activitystreams::markers::Object + serde::de::DeserializeOwned;
/// Tries to get an instance of `Self` from an ActivityPub ID.
///
@ -311,35 +364,16 @@ pub trait FromId<C>: Sized {
id: &str,
proxy: Option<reqwest::Proxy>,
) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
if let Some(proxy) = proxy {
reqwest::ClientBuilder::new().proxy(proxy)
} else {
reqwest::ClientBuilder::new()
}
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()
.map_err(|_| (None, InboxError::DerefError.into()))?
.get(id)
.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(|mut r| {
let json: serde_json::Value = r
.json()
.map_err(|_| (None, InboxError::InvalidObject(None)))?;
serde_json::from_value(json.clone())
.map_err(|_| (Some(json), InboxError::InvalidObject(None)))
})
.map_err(|(json, e)| (json, e.into()))
request::get(id, Self::get_sender(), proxy)
.map_err(|_| (None, InboxError::DerefError))
.and_then(|r| {
let json: serde_json::Value = r
.json()
.map_err(|_| (None, InboxError::InvalidObject(None)))?;
serde_json::from_value(json.clone())
.map_err(|_| (Some(json), InboxError::InvalidObject(None)))
})
.map_err(|(json, e)| (json, e.into()))
}
/// Builds a `Self` from its ActivityPub representation
@ -347,6 +381,8 @@ 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.
@ -382,9 +418,51 @@ pub trait AsActor<C> {
/// representing the Note by a Message type, without any specific context.
///
/// ```rust
/// # extern crate activitypub;
/// # use activitypub::{activity::Create, actor::Person, object::Note};
/// # use activitystreams::{prelude::*, 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 = ();
@ -397,6 +475,10 @@ 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 {
@ -418,7 +500,14 @@ pub trait AsActor<C> {
/// }
///
/// fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> {
/// Ok(Message { text: obj.object_props.content_string().map_err(|_| ())? })
/// Ok(Message {
/// text: obj.content()
/// .and_then(|content| content.to_owned().single_xsd_string()).ok_or(())?
/// })
/// }
///
/// fn get_sender() -> &'static dyn Signer {
/// &*MY_SIGNER
/// }
/// }
///
@ -434,7 +523,7 @@ pub trait AsActor<C> {
/// ```
pub trait AsObject<A, V, C>
where
V: activitypub::Activity,
V: activitystreams::markers::Activity,
{
/// What kind of error is returned when something fails
type Error;
@ -459,7 +548,57 @@ where
#[cfg(test)]
mod tests {
use super::*;
use activitypub::{activity::*, actor::Person, object::Note};
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())
}
}
struct MyActor;
impl FromId<()> for MyActor {
@ -467,11 +606,15 @@ mod tests {
type Object = Person;
fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> {
Ok(MyActor)
Ok(Self)
}
fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> {
Ok(MyActor)
Ok(Self)
}
fn get_sender() -> &'static dyn Signer {
&*MY_SIGNER
}
}
@ -491,11 +634,15 @@ mod tests {
type Object = Note;
fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> {
Ok(MyObject)
Ok(Self)
}
fn from_activity(_: &(), _obj: Note) -> Result<Self, Self::Error> {
Ok(MyObject)
Ok(Self)
}
fn get_sender() -> &'static dyn Signer {
&*MY_SIGNER
}
}
impl AsObject<MyActor, Create, &()> for MyObject {
@ -539,21 +686,15 @@ mod tests {
}
fn build_create() -> Create {
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();
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());
act
}
@ -590,6 +731,16 @@ mod tests {
}
struct FailingActor;
impl AsActor<&()> for FailingActor {
fn get_inbox_url(&self) -> String {
String::from("https://test.ap/failing-actor/inbox")
}
fn is_local(&self) -> bool {
false
}
}
impl FromId<()> for FailingActor {
type Error = ();
type Object = Person;
@ -598,17 +749,12 @@ mod tests {
Err(())
}
fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> {
fn from_activity(_: &(), _obj: Self::Object) -> Result<Self, Self::Error> {
Err(())
}
}
impl AsActor<&()> for FailingActor {
fn get_inbox_url(&self) -> String {
String::from("https://test.ap/failing-actor/inbox")
}
fn is_local(&self) -> bool {
false
fn get_sender() -> &'static dyn Signer {
&*MY_SIGNER
}
}

@ -1,13 +1,27 @@
use activitypub::{Activity, Link, Object};
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 array_tool::vec::Uniq;
use reqwest::{header::HeaderValue, r#async::ClientBuilder, Url};
use futures::future::join_all;
use reqwest::{header::HeaderValue, ClientBuilder, RequestBuilder, Url};
use rocket::{
http::Status,
request::{FromRequest, Request},
response::{Responder, Response},
Outcome,
};
use tokio::prelude::*;
use tokio::{
runtime,
time::{sleep, Duration},
};
use tracing::{debug, warn};
use self::sign::Signable;
@ -24,8 +38,8 @@ pub const AP_CONTENT_TYPE: &str =
pub fn ap_accept_header() -> Vec<&'static str> {
vec![
"application/ld+json; profile=\"https://w3.org/ns/activitystreams\"",
"application/ld+json;profile=\"https://w3.org/ns/activitystreams\"",
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
"application/ld+json;profile=\"https://www.w3.org/ns/activitystreams\"",
"application/activity+json",
"application/ld+json",
]
@ -63,7 +77,7 @@ impl<T> ActivityStream<T> {
}
}
impl<'r, O: Object> Responder<'r> for ActivityStream<O> {
impl<'r, O: serde::Serialize> 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();
@ -87,14 +101,16 @@ 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://w3.org/ns/activitystreams\""
| "application/ld+json;profile=\"https://w3.org/ns/activitystreams\""
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
| "application/ld+json;profile=\"https://www.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()) {
@ -108,10 +124,11 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApRequest {
.unwrap_or(Outcome::Forward(()))
}
}
pub fn broadcast<S, A, T, C>(sender: &S, act: A, to: Vec<T>, proxy: Option<reqwest::Proxy>)
where
S: sign::Signer,
A: Activity,
A: Activity + serde::Serialize,
T: inbox::AsActor<C>,
{
let boxes = to
@ -130,59 +147,79 @@ where
.sign(sender)
.expect("activity_pub::broadcast: signature error");
let mut rt = tokio::runtime::current_thread::Runtime::new()
let client = if let Some(proxy) = proxy {
ClientBuilder::new().proxy(proxy)
} else {
ClientBuilder::new()
}
.connect_timeout(std::time::Duration::from_secs(5))
.build()
.expect("Can't build client");
let rt = runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Error while initializing tokio runtime for federation");
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;
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);
}
headers.insert("Host", host_header_value.unwrap());
headers.insert("Digest", request::Digest::digest(&body));
rt.spawn(
if let Some(proxy) = proxy.clone() {
ClientBuilder::new().proxy(proxy)
} else {
ClientBuilder::new()
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;
}
.connect_timeout(std::time::Duration::from_secs(5))
.build()
.expect("Can't build client")
.post(&inbox)
.headers(headers.clone())
.header(
headers.insert("Host", host_header_value.unwrap());
headers.insert("Digest", request::Digest::digest(&body));
headers.insert(
"Signature",
request::signature(sender, &headers, ("post", url.path(), url.query()))
.expect("activity_pub::broadcast: request signature error"),
)
.body(body)
.send()
.and_then(move |r| {
if r.status().is_success() {
debug!("Successfully sent activity to inbox ({})", &inbox);
} else {
warn!("Error while sending to inbox ({:?})", &r)
}
r.into_body().concat2()
})
.map(move |response| debug!("Response: \"{:?}\"\n", response))
.map_err(|e| warn!("Error while sending to inbox ({:?})", e)),
);
}
rt.run().unwrap();
);
let request_builder = client.post(&inbox).headers(headers.clone()).body(body);
let _ = tx.send_async(request_builder).await;
}
drop(tx);
join_all(handles).await;
});
}
#[derive(Shrinkwrap, Clone, Serialize, Deserialize)]
@ -204,46 +241,193 @@ pub trait IntoId {
fn into_id(self) -> Id;
}
impl Link for Id {}
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ApSignature {
#[activitystreams(concrete(PublicKey), functional)]
pub public_key: Option<serde_json::Value>,
pub public_key: PublicKey,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct PublicKey {
#[activitystreams(concrete(String), functional)]
pub id: Option<serde_json::Value>,
pub id: IriString,
pub owner: IriString,
pub public_key_pem: String,
}
#[activitystreams(concrete(String), functional)]
pub owner: Option<serde_json::Value>,
impl<U> UnparsedExtension<U> for ApSignature
where
U: UnparsedMutExt,
{
type Error = serde_json::Error;
#[activitystreams(concrete(String), functional)]
pub public_key_pem: Option<serde_json::Value>,
}
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
Ok(ApSignature {
public_key: unparsed_mut.remove("publicKey")?,
})
}
#[derive(Clone, Debug, Default, UnitString)]
#[activitystreams(Hashtag)]
pub struct HashtagType;
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
unparsed_mut.insert("publicKey", self.public_key)?;
Ok(())
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[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(rename = "type")]
kind: HashtagType,
#[serde(skip_serializing_if = "Option::is_none")]
pub href: Option<IriString>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<AnyString>,
#[serde(flatten)]
inner: Object<HashtagType>,
}
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
}
}
#[activitystreams(concrete(String), functional)]
pub href: Option<serde_json::Value>,
impl Extends<HashtagType> for Hashtag {
type Error = serde_json::Error;
#[activitystreams(concrete(String), functional)]
pub name: Option<serde_json::Value>,
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()
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
impl markers::Base for Hashtag {}
impl markers::Object for Hashtag {}
impl<T> HashtagExt for T where T: AsHashtag {}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Source {
pub media_type: String,
@ -251,13 +435,300 @@ pub struct Source {
pub content: String,
}
impl Object for Source {}
impl<U> UnparsedExtension<U> for Source
where
U: UnparsedMutExt,
{
type Error = serde_json::Error;
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
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)]
#[serde(rename_all = "camelCase")]
pub struct Licensed {
#[activitystreams(concrete(String), functional)]
pub license: Option<serde_json::Value>,
pub license: Option<String>,
}
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()))
}
}
impl Object for Licensed {}
#[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);
}
}

@ -1,6 +1,12 @@
use chrono::{offset::Utc, DateTime};
use openssl::hash::{Hasher, MessageDigest};
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, DATE, USER_AGENT};
use reqwest::{
blocking::{ClientBuilder, Response},
header::{
HeaderMap, HeaderValue, InvalidHeaderValue, ACCEPT, CONTENT_TYPE, DATE, HOST, USER_AGENT,
},
Proxy, Url,
};
use std::ops::Deref;
use std::time::SystemTime;
use tracing::warn;
@ -13,6 +19,24 @@ 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 {
@ -118,8 +142,8 @@ type Path<'a> = &'a str;
type Query<'a> = &'a str;
type RequestTarget<'a> = (Method<'a>, Path<'a>, Option<Query<'a>>);
pub fn signature<S: Signer>(
signer: &S,
pub fn signature(
signer: &dyn Signer,
headers: &HeaderMap,
request_target: RequestTarget,
) -> Result<HeaderValue, Error> {
@ -164,10 +188,35 @@ pub fn signature<S: Signer>(
)).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, Error};
use crate::activity_pub::sign::{gen_keypair, Signer};
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;
@ -187,13 +236,11 @@ mod tests {
}
impl Signer for MySigner {
type Error = Error;
fn get_key_id(&self) -> String {
"mysigner".into()
}
fn sign(&self, to_sign: &str) -> Result<Vec<u8>, Self::Error> {
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();
@ -201,7 +248,7 @@ mod tests {
signer.sign_to_vec().map_err(|_| Error())
}
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool, Self::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();

@ -19,20 +19,25 @@ pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
#[derive(Debug)]
pub struct Error();
pub type Result<T> = std::result::Result<T, Error>;
pub trait Signer {
type Error;
impl From<openssl::error::ErrorStack> for Error {
fn from(_: openssl::error::ErrorStack) -> Self {
Self()
}
}
pub trait Signer {
fn get_key_id(&self) -> String;
/// Sign some data with the signer keypair
fn sign(&self, to_sign: &str) -> Result<Vec<u8>, Self::Error>;
fn sign(&self, to_sign: &str) -> Result<Vec<u8>>;
/// Verify if the signature is valid
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool, Self::Error>;
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool>;
}
pub trait Signable {
fn sign<T>(&mut self, creator: &T) -> Result<&mut Self, Error>
fn sign<T>(&mut self, creator: &T) -> Result<&mut Self>
where
T: Signer;
fn verify<T>(self, creator: &T) -> bool
@ -46,7 +51,7 @@ pub trait Signable {
}
impl Signable for serde_json::Value {
fn sign<T: Signer>(&mut self, creator: &T) -> Result<&mut serde_json::Value, Error> {
fn sign<T: Signer>(&mut self, creator: &T) -> Result<&mut serde_json::Value> {
let creation_date = Utc::now().to_rfc3339();
let mut options = json!({
"type": "RsaSignature2017",
@ -182,7 +187,7 @@ pub fn verify_http_headers<S: Signer + ::std::fmt::Debug>(
}
let digest = all_headers.get_one("digest").unwrap_or("");
let digest = request::Digest::from_header(digest);
if !digest.map(|d| d.verify_header(&data)).unwrap_or(false) {
if !digest.map(|d| d.verify_header(data)).unwrap_or(false) {
// signature was valid, but body content does not match its digest
return SignatureValidity::Invalid;
}

@ -1,7 +1,5 @@
#![feature(associated_type_defaults)]
#[macro_use]
extern crate activitystreams_derive;
#[macro_use]
extern crate shrinkwraprs;
#[macro_use]

@ -1,11 +1,7 @@
use heck::CamelCase;
use openssl::rand::rand_bytes;
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag};
use regex_syntax::is_word_character;
use rocket::{
http::uri::Uri,
response::{Flash, Redirect},
};
use rocket::http::uri::Uri;
use std::collections::HashSet;
use syntect::html::{ClassStyle, ClassedHTMLGenerator};
use syntect::parsing::SyntaxSet;
@ -19,25 +15,57 @@ pub fn random_hex() -> String {
.fold(String::new(), |res, byte| format!("{}{:x}", res, byte))
}
/// Remove non alphanumeric characters and CamelCase a string
pub fn make_actor_id(name: &str) -> String {
name.to_camel_case()
.chars()
.filter(|c| c.is_alphanumeric())
.collect()
/**
* Percent-encode characters which are not allowed in IRI path segments.
*
* Intended to be used for generating Post ap_url.
*/
pub fn iri_percent_encode_seg(segment: &str) -> String {
segment.chars().map(iri_percent_encode_seg_char).collect()
}
/**
* Redirects to the login page with a given message.
*
* Note that the message should be translated before passed to this function.
*/
pub fn requires_login<T: Into<Uri<'static>>>(message: &str, url: T) -> Flash<Redirect> {
Flash::new(
Redirect::to(format!("/login?m={}", Uri::percent_encode(message))),
"callback",
url.into().to_string(),
)
pub fn iri_percent_encode_seg_char(c: char) -> String {
if c.is_alphanumeric() {
c.to_string()
} else {
match c {
'-'
| '.'
| '_'
| '~'
| '\u{A0}'..='\u{D7FF}'
| '\u{20000}'..='\u{2FFFD}'
| '\u{30000}'..='\u{3FFFD}'
| '\u{40000}'..='\u{4FFFD}'
| '\u{50000}'..='\u{5FFFD}'
| '\u{60000}'..='\u{6FFFD}'
| '\u{70000}'..='\u{7FFFD}'
| '\u{80000}'..='\u{8FFFD}'
| '\u{90000}'..='\u{9FFFD}'
| '\u{A0000}'..='\u{AFFFD}'
| '\u{B0000}'..='\u{BFFFD}'
| '\u{C0000}'..='\u{CFFFD}'
| '\u{D0000}'..='\u{DFFFD}'
| '\u{E0000}'..='\u{EFFFD}'
| '!'
| '$'
| '&'
| '\''
| '('
| ')'
| '*'
| '+'
| ','
| ';'
| '='
| ':'
| '@' => c.to_string(),
_ => {
let s = c.to_string();
Uri::percent_encode(&s).to_string()
}
}
}
}
#[derive(Debug)]
@ -88,13 +116,13 @@ fn highlight_code<'a>(
unreachable!();
};
let syntax_set = SyntaxSet::load_defaults_newlines();
let syntax = syntax_set.find_syntax_by_token(&lang).unwrap_or_else(|| {
let syntax = syntax_set.find_syntax_by_token(lang).unwrap_or_else(|| {
syntax_set
.find_syntax_by_name(&lang)
.find_syntax_by_name(lang)
.unwrap_or_else(|| syntax_set.find_syntax_plain_text())
});
let mut html = ClassedHTMLGenerator::new_with_class_style(
&syntax,
syntax,
&syntax_set,
ClassStyle::Spaced,
);
@ -234,7 +262,7 @@ pub fn md_to_html<'a>(
media_processor: Option<MediaProcessor<'a>>,
) -> (String, HashSet<String>, HashSet<String>) {
let base_url = if let Some(base_url) = base_url {
format!("//{}/", base_url)
format!("https://{}/", base_url)
} else {
"/".to_owned()
};
@ -281,16 +309,15 @@ pub fn md_to_html<'a>(
text_acc.push(c)
}
let mention = text_acc;
let short_mention = mention.splitn(1, '@').next().unwrap_or("");
let link = Tag::Link(
LinkType::Inline,
format!("{}@/{}/", base_url, &mention).into(),
short_mention.to_owned().into(),
mention.clone().into(),
);
mentions.push(mention.clone());
events.push(Event::Start(link.clone()));
events.push(Event::Text(format!("@{}", &short_mention).into()));
events.push(Event::Text(format!("@{}", &mention).into()));
events.push(Event::End(link));
(
@ -414,6 +441,10 @@ pub fn md_to_html<'a>(
(buf, mentions.collect(), hashtags.collect())
}
pub fn escape(string: &str) -> askama_escape::Escaped<askama_escape::Html> {
askama_escape::escape(string, askama_escape::Html)
}
#[cfg(test)]
mod tests {
use super::*;
@ -476,6 +507,20 @@ mod tests {
}
}
#[test]
fn test_iri_percent_encode_seg() {
assert_eq!(
&iri_percent_encode_seg("including whitespace"),
"including%20whitespace"
);
assert_eq!(&iri_percent_encode_seg("%20"), "%2520");
assert_eq!(&iri_percent_encode_seg("é"), "é");
assert_eq!(
&iri_percent_encode_seg("空白入り 日本語"),
"空白入り%20日本語"
);
}
#[test]
fn test_inline() {
assert_eq!(

@ -1,6 +1,6 @@
[package]
name = "plume-front"
version = "0.6.1-dev"
version = "0.7.2"
authors = ["Plume contributors"]
edition = "2018"
@ -8,19 +8,19 @@ edition = "2018"
crate-type = ["cdylib"]
[dependencies]
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" }
gettext = "0.4.0"
gettext-macros = "0.6.1"
gettext-utils = "0.1.0"
lazy_static = "1.3"
serde = "1.0"
serde_json = "1.0"
wasm-bindgen = "0.2.70"
js-sys = "0.3.47"
wasm-bindgen = "0.2.81"
js-sys = "0.3.58"
serde_derive = "1.0.123"
console_error_panic_hook = "0.1.6"
[dependencies.web-sys]
version = "0.3.47"
version = "0.3.58"
features = [
'console',
'ClipboardEvent',

@ -1,2 +1,3 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
release = false

@ -54,11 +54,6 @@ pub enum EditorError {
DOMError,
}
impl From<std::option::NoneError> for EditorError {
fn from(_: std::option::NoneError) -> Self {
EditorError::NoneError
}
}
const AUTOSAVE_DEBOUNCE_TIME: i32 = 5000;
#[derive(Serialize, Deserialize)]
struct AutosaveInformation {
@ -198,7 +193,7 @@ fn clear_autosave() {
.unwrap()
.remove_item(&get_autosave_id())
.unwrap();
console::log_1(&&format!("Saved to {}", &get_autosave_id()).into());
console::log_1(&format!("Saved to {}", &get_autosave_id()).into());
}
type TimeoutHandle = i32;
lazy_static! {
@ -366,7 +361,9 @@ fn init_editor() -> Result<(), EditorError> {
return Ok(());
}
let old_ed = old_ed.unwrap();
let old_title = document().get_element_by_id("plume-editor-title")?;
let old_title = document()
.get_element_by_id("plume-editor-title")
.ok_or(EditorError::NoneError)?;
old_ed
.dyn_ref::<HtmlElement>()
.unwrap()
@ -400,7 +397,9 @@ fn init_editor() -> Result<(), EditorError> {
content_val.clone(),
false,
)?;
content.set_inner_html(&content_val);
if !content_val.is_empty() {
content.set_inner_html(&content_val);
}
// character counter
let character_counter = Closure::wrap(Box::new(mv!(content => move |_| {
@ -434,7 +433,8 @@ fn init_editor() -> Result<(), EditorError> {
bg.class_list().add_1("show").unwrap();
})) as Box<dyn FnMut(MouseEvent)>);
document()
.get_element_by_id("publish")?
.get_element_by_id("publish")
.ok_or(EditorError::NoneError)?
.add_event_listener_with_callback("click", show_popup.as_ref().unchecked_ref())
.map_err(|_| EditorError::DOMError)?;
show_popup.forget();
@ -528,8 +528,14 @@ fn init_popup(
cover_label
.set_attribute("for", "cover")
.map_err(|_| EditorError::DOMError)?;
let cover = document.get_element_by_id("cover")?;
cover.parent_element()?.remove_child(&cover).ok();
let cover = document
.get_element_by_id("cover")
.ok_or(EditorError::NoneError)?;
cover
.parent_element()
.ok_or(EditorError::NoneError)?
.remove_child(&cover)
.ok();
popup
.append_child(&cover_label)
.map_err(|_| EditorError::DOMError)?;
@ -554,7 +560,7 @@ fn init_popup(
draft.set_checked(draft_checkbox.checked());
draft_label
.append_child(&draft)
.append_child(draft)
.map_err(|_| EditorError::DOMError)?;
draft_label
.append_child(&document.create_text_node(&i18n!(CATALOG, "This is a draft")))
@ -620,11 +626,12 @@ fn init_popup(
.map_err(|_| EditorError::DOMError)?;
callback.forget();
popup
.append_child(&button)
.append_child(button)
.map_err(|_| EditorError::DOMError)?;
document
.body()?
.body()
.ok_or(EditorError::NoneError)?
.append_child(&popup)
.map_err(|_| EditorError::DOMError)?;
Ok(popup)
@ -641,7 +648,8 @@ fn init_popup_bg() -> Result<Element, EditorError> {
.map_err(|_| EditorError::DOMError)?;
document()
.body()?
.body()
.ok_or(EditorError::NoneError)?
.append_child(&bg)
.map_err(|_| EditorError::DOMError)?;
let callback = Closure::wrap(Box::new(|_| close_popup()) as Box<dyn FnMut(MouseEvent)>);

@ -1,5 +1,5 @@
#![recursion_limit = "128"]
#![feature(decl_macro, proc_macro_hygiene, try_trait)]
#![feature(decl_macro, proc_macro_hygiene)]
#[macro_use]
extern crate gettext_macros;
@ -23,6 +23,7 @@ init_i18n!(
en,
eo,
es,
eu,
fa,
fi,
fr,
@ -61,7 +62,7 @@ lazy_static! {
static ref CATALOG: gettext::Catalog = {
let catalogs = include_i18n!();
let lang = window().unwrap().navigator().language().unwrap();
let lang = lang.splitn(2, '-').next().unwrap_or("en");
let lang = lang.split_once('-').map_or("en", |x| x.0);
let english_position = catalogs
.iter()
@ -85,7 +86,7 @@ pub fn main() -> Result<(), JsValue> {
menu();
search();
editor::init()
.map_err(|e| console::error_1(&&format!("Editor error: {:?}", e).into()))
.map_err(|e| console::error_1(&format!("Editor error: {:?}", e).into()))
.ok();
Ok(())
}

@ -1,6 +1,6 @@
[package]
name = "plume-macro"
version = "0.6.1-dev"
version = "0.7.2"
authors = ["Trinity Pointard <trinity.pointard@insa-rennes.fr>"]
edition = "2018"
description = "Plume procedural macros"

@ -1,2 +1,3 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
release = false

@ -58,7 +58,7 @@ pub fn import_migrations(input: TokenStream) -> TokenStream {
(name, up_sql, down_sql)
})
.collect::<Vec<_>>();
let migrations_name = migrations.iter().map(|m| &m.0).collect::<Vec<_>>();
let migrations_name = migrations.iter().map(|m| &m.0);
let migrations_up = migrations
.iter()
.map(|m| m.1.as_str())
@ -103,7 +103,7 @@ fn file_to_migration(file: &str) -> TokenStream2 {
acc.push('\n');
}
} else if let Some(acc_str) = line.strip_prefix("--#!") {
acc.push_str(&acc_str);
acc.push_str(acc_str);
acc.push('\n');
} else if line.starts_with("--") {
continue;

@ -1,40 +1,40 @@
[package]
name = "plume-models"
version = "0.6.1-dev"
version = "0.7.2"
authors = ["Plume contributors"]
edition = "2018"
[dependencies]
activitypub = "0.1.1"
ammonia = "2.1.1"
askama_escape = "0.1"
bcrypt = "0.5"
guid-create = "0.1"
heck = "0.3.0"
itertools = "0.8.0"
ammonia = "3.2.0"
bcrypt = "0.12.1"
guid-create = "0.2"
itertools = "0.10.3"
lazy_static = "1.0"
ldap3 = "0.7.1"
ldap3 = "0.10.5"
migrations_internals= "1.4.0"
openssl = "0.10.22"
rocket = "0.4.6"
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
reqwest = "0.9"
scheduled-thread-pool = "0.2.2"
openssl = "0.10.40"
rocket = "0.4.11"
rocket_i18n = "0.4.1"
reqwest = "0.11.11"
scheduled-thread-pool = "0.2.6"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
serde_json = "1.0.81"
tantivy = "0.13.3"
url = "2.1"
walkdir = "2.2"
webfinger = "0.4.1"
whatlang = "0.11.1"
shrinkwraprs = "0.2.1"
diesel-derive-newtype = "0.1.2"
whatlang = "0.16.0"
shrinkwraprs = "0.3.0"
diesel-derive-newtype = "1.0.0"
glob = "0.3.0"
lindera-tantivy = { version = "0.7.1", optional = true }
tracing = "0.1.22"
tracing = "0.1.35"
riker = "0.4.2"
once_cell = "1.5.2"
once_cell = "1.12.0"
lettre = "0.9.6"
native-tls = "0.2.10"
activitystreams = "0.7.0-alpha.18"
[dependencies.chrono]
features = ["serde"]
@ -54,6 +54,7 @@ path = "../plume-common"
path = "../plume-macro"
[dev-dependencies]
assert-json-diff = "2.0.1"
diesel_migrations = "1.3.0"
[features]

@ -1,2 +1,3 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
release = false

@ -86,14 +86,18 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApiToken {
}
let mut parsed_header = headers[0].split(' ');
let auth_type = parsed_header.next().map_or_else(
|| Outcome::Failure((Status::BadRequest, TokenError::NoType)),
Outcome::Success,
)?;
let val = parsed_header.next().map_or_else(
|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)),
Outcome::Success,
)?;
let auth_type = parsed_header
.next()
.map_or_else::<rocket::Outcome<&str, _, ()>, _, _>(
|| Outcome::Failure((Status::BadRequest, TokenError::NoType)),
Outcome::Success,
)?;
let val = parsed_header
.next()
.map_or_else::<rocket::Outcome<&str, _, ()>, _, _>(
|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)),
Outcome::Success,
)?;
if auth_type == "Bearer" {
let conn = request

@ -28,7 +28,7 @@ impl BlocklistedEmail {
pub fn delete_entries(conn: &Connection, ids: Vec<i32>) -> Result<bool> {
use diesel::delete;
for i in ids {
let be: BlocklistedEmail = BlocklistedEmail::find_by_id(&conn, i)?;
let be: BlocklistedEmail = BlocklistedEmail::find_by_id(conn, i)?;
delete(&be).execute(conn)?;
}
Ok(true)

@ -1,12 +1,14 @@
use crate::{
ap_url, db_conn::DbConn, instance::*, medias::Media, posts::Post, safe_string::SafeString,
db_conn::DbConn, instance::*, medias::Media, posts::Post, safe_string::SafeString,
schema::blogs, users::User, Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE,
};
use activitypub::{
actor::Group,
use activitystreams::{
actor::{ApActor, ApActorExt, AsApActor, Group},
base::AnyBase,
collection::{OrderedCollection, OrderedCollectionPage},
object::Image,
CustomObject,
iri_string::types::IriString,
object::{kind::ImageType, ApObject, Image, ObjectExt},
prelude::*,
};
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SaveChangesDsl};
@ -18,14 +20,12 @@ use openssl::{
};
use plume_common::activity_pub::{
inbox::{AsActor, FromId},
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
sign, ActivityStream, ApSignature, CustomGroup, Id, IntoId, PublicKey, Source, SourceProperty,
ToAsString, ToAsUri,
};
use url::Url;
use webfinger::*;
pub type CustomGroup = CustomObject<ApSignature, Group>;
#[derive(Queryable, Identifiable, Clone, AsChangeset)]
#[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)]
#[changeset_options(treat_none_as_null = "true")]
pub struct Blog {
pub id: i32,
@ -95,6 +95,10 @@ impl Blog {
find_by!(blogs, find_by_ap_url, ap_url as &str);
find_by!(blogs, find_by_name, actor_id as &str, instance_id as i32);
pub fn slug(title: &str) -> &str {
title
}
pub fn get_instance(&self, conn: &Connection) -> Result<Instance> {
Instance::get(conn, self.instance_id)
}
@ -149,108 +153,132 @@ impl Blog {
.into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
.ok_or(Error::Webfinger)
.and_then(|l| Blog::from_id(conn, &l.href?, None, CONFIG.proxy()).map_err(|(_, e)| e))
.and_then(|l| {
Blog::from_id(
conn,
&l.href.ok_or(Error::MissingApProperty)?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)
})
}
pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> {
let mut blog = Group::default();
blog.ap_actor_props
.set_preferred_username_string(self.actor_id.clone())?;
blog.object_props.set_name_string(self.title.clone())?;
blog.ap_actor_props
.set_outbox_string(self.outbox_url.clone())?;
blog.ap_actor_props
.set_inbox_string(self.inbox_url.clone())?;
blog.object_props
.set_summary_string(self.summary_html.to_string())?;
blog.ap_object_props.set_source_object(Source {
content: self.summary.clone(),
media_type: String::from("text/markdown"),
})?;
let mut icon = Image::default();
icon.object_props.set_url_string(
self.icon_id
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
.unwrap_or_default(),
)?;
icon.object_props.set_attributed_to_link(
self.icon_id
.and_then(|id| {
Media::get(conn, id)
.and_then(|m| Ok(User::get(conn, m.owner_id)?.into_id()))
.ok()
})
.unwrap_or_else(|| Id::new(String::new())),
)?;
blog.object_props.set_icon_object(icon)?;
let mut banner = Image::default();
banner.object_props.set_url_string(
self.banner_id
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
.unwrap_or_default(),
)?;
banner.object_props.set_attributed_to_link(
self.banner_id
.and_then(|id| {
Media::get(conn, id)
.and_then(|m| Ok(User::get(conn, m.owner_id)?.into_id()))
.ok()
})
.unwrap_or_else(|| Id::new(String::new())),
)?;
blog.object_props.set_image_object(banner)?;
let mut blog = ApActor::new(self.inbox_url.parse()?, Group::new());
blog.set_preferred_username(self.actor_id.clone());
blog.set_name(self.title.clone());
blog.set_outbox(self.outbox_url.parse()?);
blog.set_summary(self.summary_html.to_string());
let source = SourceProperty {
source: Source {
content: self.summary.clone(),
media_type: String::from("text/markdown"),
},
};
let mut icon = Image::new();
let _ = self.icon_id.map(|id| {
Media::get(conn, id).and_then(|m| {
let _ = m
.url()
.and_then(|url| url.parse::<IriString>().map_err(|_| Error::Url))
.map(|url| icon.set_url(url));
icon.set_attributed_to(
User::get(conn, m.owner_id)?
.into_id()
.parse::<IriString>()?,
);
Ok(())
})
});
blog.set_icon(icon.into_any_base()?);
let mut banner = Image::new();
let _ = self.banner_id.map(|id| {
Media::get(conn, id).and_then(|m| {
let _ = m
.url()
.and_then(|url| url.parse::<IriString>().map_err(|_| Error::Url))
.map(|url| banner.set_url(url));
banner.set_attributed_to(
User::get(conn, m.owner_id)?
.into_id()
.parse::<IriString>()?,
);
Ok(())
})
});
blog.set_image(banner.into_any_base()?);
blog.object_props.set_id_string(self.ap_url.clone())?;
blog.set_id(self.ap_url.parse()?);
let mut public_key = PublicKey::default();
public_key.set_id_string(format!("{}#main-key", self.ap_url))?;
public_key.set_owner_string(self.ap_url.clone())?;
public_key.set_public_key_pem_string(self.public_key.clone())?;
let mut ap_signature = ApSignature::default();
ap_signature.set_public_key_publickey(public_key)?;
let pub_key = PublicKey {
id: format!("{}#main-key", self.ap_url).parse()?,
owner: self.ap_url.parse()?,
public_key_pem: self.public_key.clone(),
};
let ap_signature = ApSignature {
public_key: pub_key,
};
Ok(CustomGroup::new(blog, ap_signature))
Ok(CustomGroup::new(blog, ap_signature, source))
}
pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> {
let mut coll = OrderedCollection::default();
coll.collection_props.items = serde_json::to_value(self.get_activities(conn))?;
coll.collection_props
.set_total_items_u64(self.get_activities(conn).len() as u64)?;
coll.collection_props
.set_first_link(Id::new(ap_url(&format!("{}?page=1", &self.outbox_url))))?;
coll.collection_props
.set_last_link(Id::new(ap_url(&format!(
self.outbox_collection(conn).map(ActivityStream::new)
}
pub fn outbox_collection(&self, conn: &Connection) -> Result<OrderedCollection> {
let acts = self.get_activities(conn);
let acts = acts
.iter()
.filter_map(|value| AnyBase::from_arbitrary_json(value).ok())
.collect::<Vec<AnyBase>>();
let n_acts = acts.len();
let mut coll = OrderedCollection::new();
coll.set_many_items(acts);
coll.set_total_items(n_acts as u64);
coll.set_first(format!("{}?page=1", &self.outbox_url).parse::<IriString>()?);
coll.set_last(
format!(
"{}?page={}",
&self.outbox_url,
(self.get_activities(conn).len() as u64 + ITEMS_PER_PAGE as u64 - 1) as u64
/ ITEMS_PER_PAGE as u64
))))?;
Ok(ActivityStream::new(coll))
(n_acts as u64 + ITEMS_PER_PAGE as u64 - 1) as u64 / ITEMS_PER_PAGE as u64
)
.parse::<IriString>()?,
);
Ok(coll)
}
pub fn outbox_page(
&self,
conn: &Connection,
(min, max): (i32, i32),
) -> Result<ActivityStream<OrderedCollectionPage>> {
let mut coll = OrderedCollectionPage::default();
let acts = self.get_activity_page(&conn, (min, max));
self.outbox_collection_page(conn, (min, max))
.map(ActivityStream::new)
}
pub fn outbox_collection_page(
&self,
conn: &Connection,
(min, max): (i32, i32),
) -> Result<OrderedCollectionPage> {
let mut coll = OrderedCollectionPage::new();
let acts = self.get_activity_page(conn, (min, max));
//This still doesn't do anything because the outbox
//doesn't do anything yet
coll.collection_page_props.set_next_link(Id::new(&format!(
"{}?page={}",
&self.outbox_url,
min / ITEMS_PER_PAGE + 1
)))?;
coll.collection_page_props.set_prev_link(Id::new(&format!(
"{}?page={}",
&self.outbox_url,
min / ITEMS_PER_PAGE - 1
)))?;
coll.collection_props.items = serde_json::to_value(acts)?;
Ok(ActivityStream::new(coll))
coll.set_next(
format!("{}?page={}", &self.outbox_url, min / ITEMS_PER_PAGE + 1)
.parse::<IriString>()?,
);
coll.set_prev(
format!("{}?page={}", &self.outbox_url, min / ITEMS_PER_PAGE - 1)
.parse::<IriString>()?,
);
coll.set_many_items(
acts.iter()
.filter_map(|value| AnyBase::from_arbitrary_json(value).ok()),
);
Ok(coll)
}
fn get_activities(&self, _conn: &Connection) -> Vec<serde_json::Value> {
vec![]
@ -265,7 +293,10 @@ impl Blog {
pub fn get_keypair(&self) -> Result<PKey<Private>> {
PKey::from_rsa(Rsa::private_key_from_pem(
self.private_key.clone()?.as_ref(),
self.private_key
.clone()
.ok_or(Error::MissingApProperty)?
.as_ref(),
)?)
.map_err(Error::from)
}
@ -318,7 +349,7 @@ impl Blog {
}
pub fn delete(&self, conn: &Connection) -> Result<()> {
for post in Post::get_for_blog(conn, &self)? {
for post in Post::get_for_blog(conn, self)? {
post.delete(conn)?;
}
diesel::delete(self)
@ -339,13 +370,94 @@ impl FromId<DbConn> for Blog {
type Object = CustomGroup;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
Self::find_by_ap_url(&conn, id)
Self::find_by_ap_url(conn, id)
}
fn from_activity(conn: &DbConn, acct: CustomGroup) -> Result<Self> {
let url = Url::parse(&acct.object.object_props.id_string()?)?;
let inst = url.host_str()?;
let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
let (name, outbox_url, inbox_url) = {
let actor = acct.ap_actor_ref();
let name = actor
.preferred_username()
.ok_or(Error::MissingApProperty)?
.to_string();
if name.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
return Err(Error::InvalidValue);
}
(
name,
actor.outbox()?.ok_or(Error::MissingApProperty)?.to_string(),
actor.inbox()?.to_string(),
)
};
let mut new_blog = NewBlog {
actor_id: name.to_string(),
outbox_url,
inbox_url,
public_key: acct.ext_one.public_key.public_key_pem.to_string(),
private_key: None,
theme: None,
..NewBlog::default()
};
let object = ApObject::new(acct.inner);
new_blog.title = object
.name()
.and_then(|name| name.to_as_string())
.unwrap_or(name);
new_blog.summary_html = SafeString::new(
&object
.summary()
.and_then(|summary| summary.to_as_string())
.unwrap_or_default(),
);
let icon_id = object
.icon()
.and_then(|icons| {
icons.iter().next().and_then(|icon| {
let icon = icon.to_owned().extend::<Image, ImageType>().ok()??;
let owner = icon.attributed_to()?.to_as_uri()?;
Media::save_remote(
conn,
icon.url()?.to_as_uri()?,
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
)
.ok()
})
})
.map(|m| m.id);
new_blog.icon_id = icon_id;
let banner_id = object
.image()
.and_then(|banners| {
banners.iter().next().and_then(|banner| {
let banner = banner.to_owned().extend::<Image, ImageType>().ok()??;
let owner = banner.attributed_to()?.to_as_uri()?;
Media::save_remote(
conn,
banner.url()?.to_as_uri()?,
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
)
.ok()
})
})
.map(|m| m.id);
new_blog.banner_id = banner_id;
new_blog.summary = acct.ext_two.source.content;
let any_base = AnyBase::from_extended(object)?;
let id = any_base.id().ok_or(Error::MissingApProperty)?;
new_blog.ap_url = id.to_string();
let inst = id
.authority_components()
.ok_or(Error::Url)?
.host()
.to_string();
let instance = Instance::find_by_domain(conn, &inst).or_else(|_| {
Instance::insert(
conn,
NewInstance {
@ -362,75 +474,13 @@ impl FromId<DbConn> for Blog {
},
)
})?;
let icon_id = acct
.object
.object_props
.icon_image()
.ok()
.and_then(|icon| {
let owner = icon.object_props.attributed_to_link::<Id>().ok()?;
Media::save_remote(
conn,
icon.object_props.url_string().ok()?,
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
)
.ok()
})
.map(|m| m.id);
let banner_id = acct
.object
.object_props
.image_image()
.ok()
.and_then(|banner| {
let owner = banner.object_props.attributed_to_link::<Id>().ok()?;
Media::save_remote(
conn,
banner.object_props.url_string().ok()?,
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
)
.ok()
})
.map(|m| m.id);
new_blog.instance_id = instance.id;
let name = acct.object.ap_actor_props.preferred_username_string()?;
if name.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
return Err(Error::InvalidValue);
}
Blog::insert(conn, new_blog)
}
Blog::insert(
conn,
NewBlog {
actor_id: name.clone(),
title: acct.object.object_props.name_string().unwrap_or(name),
outbox_url: acct.object.ap_actor_props.outbox_string()?,
inbox_url: acct.object.ap_actor_props.inbox_string()?,
summary: acct
.object
.ap_object_props
.source_object::<Source>()
.map(|s| s.content)
.unwrap_or_default(),
instance_id: instance.id,
ap_url: acct.object.object_props.id_string()?,
public_key: acct
.custom_props
.public_key_publickey()?
.public_key_pem_string()?,
private_key: None,
banner_id,
icon_id,
summary_html: SafeString::new(
&acct
.object
.object_props
.summary_string()
.unwrap_or_default(),
),
theme: None,
},
)
fn get_sender() -> &'static dyn sign::Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
@ -451,24 +501,22 @@ impl AsActor<&PlumeRocket> for Blog {
}
impl sign::Signer for Blog {
type Error = Error;
fn get_key_id(&self) -> String {
format!("{}#main-key", self.ap_url)
}
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
let key = self.get_keypair()?;
fn sign(&self, to_sign: &str) -> sign::Result<Vec<u8>> {
let key = self.get_keypair().map_err(|_| sign::Error())?;
let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
signer.update(to_sign.as_bytes())?;
signer.sign_to_vec().map_err(Error::from)
signer.sign_to_vec().map_err(sign::Error::from)
}
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
fn verify(&self, data: &str, signature: &[u8]) -> sign::Result<bool> {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?;
verifier.update(data.as_bytes())?;
verifier.verify(&signature).map_err(Error::from)
verifier.verify(signature).map_err(sign::Error::from)
}
}
@ -499,12 +547,14 @@ pub(crate) mod tests {
blog_authors::*, instance::tests as instance_tests, medias::NewMedia, tests::db,
users::tests as usersTests, Connection as Conn,
};
use assert_json_diff::assert_json_eq;
use diesel::Connection;
use serde_json::to_value;
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Blog>) {
instance_tests::fill_database(conn);
let users = usersTests::fill_database(conn);
let blog1 = Blog::insert(
let mut blog1 = Blog::insert(
conn,
NewBlog::new_local(
"BlogName".to_owned(),
@ -577,6 +627,41 @@ pub(crate) mod tests {
},
)
.unwrap();
blog1.icon_id = Some(
Media::insert(
conn,
NewMedia {
file_path: "aaa.png".into(),
alt_text: String::new(),
is_remote: false,
remote_url: None,
sensitive: false,
content_warning: None,
owner_id: users[0].id,
},
)
.unwrap()
.id,
);
blog1.banner_id = Some(
Media::insert(
conn,
NewMedia {
file_path: "bbb.png".into(),
alt_text: String::new(),
is_remote: false,
remote_url: None,
sensitive: false,
content_warning: None,
owner_id: users[0].id,
},
)
.unwrap()
.id,
);
let _: Blog = blog1.save_changes(&*conn).unwrap();
(users, vec![blog1, blog2, blog3])
}
@ -873,7 +958,6 @@ pub(crate) mod tests {
.id,
);
let _: Blog = blogs[0].save_changes(&**conn).unwrap();
let ap_repr = blogs[0].to_activity(&conn).unwrap();
blogs[0].delete(&conn).unwrap();
let blog = Blog::from_activity(&conn, ap_repr).unwrap();
@ -894,4 +978,90 @@ pub(crate) mod tests {
Ok(())
})
}
#[test]
fn to_activity() {
let conn = &db();
conn.test_transaction::<_, Error, _>(|| {
let (_users, blogs) = fill_database(&conn);
let blog = &blogs[0];
let act = blog.to_activity(conn)?;
let expected = json!({
"icon": {
"attributedTo": "https://plu.me/@/admin/",
"type": "Image",
"url": "https://plu.me/aaa.png"
},
"id": "https://plu.me/~/BlogName/",
"image": {
"attributedTo": "https://plu.me/@/admin/",
"type": "Image",
"url": "https://plu.me/bbb.png"
},
"inbox": "https://plu.me/~/BlogName/inbox",
"name": "Blog name",
"outbox": "https://plu.me/~/BlogName/outbox",
"preferredUsername": "BlogName",
"publicKey": {
"id": "https://plu.me/~/BlogName/#main-key",
"owner": "https://plu.me/~/BlogName/",
"publicKeyPem": blog.public_key
},
"source": {
"content": "This is a small blog",
"mediaType": "text/markdown"
},
"summary": "",
"type": "Group"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn outbox_collection() {
let conn = &db();
conn.test_transaction::<_, Error, _>(|| {
let (_users, blogs) = fill_database(conn);
let blog = &blogs[0];
let act = blog.outbox_collection(conn)?;
let expected = json!({
"items": [],
"totalItems": 0,
"first": "https://plu.me/~/BlogName/outbox?page=1",
"last": "https://plu.me/~/BlogName/outbox?page=0",
"type": "OrderedCollection"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn outbox_collection_page() {
let conn = &db();
conn.test_transaction::<_, Error, _>(|| {
let (_users, blogs) = fill_database(conn);
let blog = &blogs[0];
let act = blog.outbox_collection_page(conn, (33, 36))?;
let expected = json!({
"next": "https://plu.me/~/BlogName/outbox?page=3",
"prev": "https://plu.me/~/BlogName/outbox?page=1",
"items": [],
"type": "OrderedCollectionPage"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
}

@ -11,17 +11,23 @@ use crate::{
users::User,
Connection, Error, Result, CONFIG,
};
use activitypub::{
use activitystreams::{
activity::{Create, Delete},
link,
base::{AnyBase, Base},
iri_string::types::IriString,
link::{self, kind::MentionType},
object::{Note, Tombstone},
prelude::*,
primitives::OneOrMany,
time::OffsetDateTime,
};
use chrono::{self, NaiveDateTime};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use plume_common::{
activity_pub::{
inbox::{AsActor, AsObject, FromId},
Id, IntoId, PUBLIC_VISIBILITY,
sign::Signer,
IntoId, ToAsString, ToAsUri, PUBLIC_VISIBILITY,
},
utils,
};
@ -58,7 +64,7 @@ impl Comment {
insert!(comments, NewComment, |inserted, conn| {
if inserted.ap_url.is_none() {
inserted.ap_url = Some(format!(
"{}comment/{}",
"{}/comment/{}",
inserted.get_post(conn)?.ap_url,
inserted.id
));
@ -114,45 +120,59 @@ impl Comment {
Some(Media::get_media_processor(conn, vec![&author])),
);
let mut note = Note::default();
let to = vec![Id::new(PUBLIC_VISIBILITY.to_string())];
note.object_props
.set_id_string(self.ap_url.clone().unwrap_or_default())?;
note.object_props
.set_summary_string(self.spoiler_text.clone())?;
note.object_props.set_content_string(html)?;
note.object_props
.set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else(
|| Ok(Post::get(conn, self.post_id)?.ap_url),
|id| Ok(Comment::get(conn, id)?.ap_url.unwrap_or_default()) as Result<String>,
)?))?;
note.object_props
.set_published_string(chrono::Utc::now().to_rfc3339())?;
note.object_props.set_attributed_to_link(author.into_id())?;
note.object_props.set_to_link_vec(to)?;
note.object_props.set_tag_link_vec(
mentions
.into_iter()
.filter_map(|m| Mention::build_activity(conn, &m).ok())
.collect::<Vec<link::Mention>>(),
)?;
let mut note = Note::new();
let to = vec![PUBLIC_VISIBILITY.parse::<IriString>()?];
note.set_id(
self.ap_url
.clone()
.unwrap_or_default()
.parse::<IriString>()?,
);
note.set_summary(self.spoiler_text.clone());
note.set_content(html);
note.set_in_reply_to(self.in_response_to_id.map_or_else(
|| Post::get(conn, self.post_id).map(|post| post.ap_url),
|id| Comment::get(conn, id).map(|comment| comment.ap_url.unwrap_or_default()),
)?);
note.set_published(
OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos().into())
.expect("OffsetDateTime"),
);
note.set_attributed_to(author.into_id().parse::<IriString>()?);
note.set_many_tos(to);
note.set_many_tags(mentions.into_iter().filter_map(|m| {
Mention::build_activity(conn, &m)
.map(|mention| mention.into_any_base().expect("Can convert"))
.ok()
}));
Ok(note)
}
pub fn create_activity(&self, conn: &DbConn) -> Result<Create> {
let author = User::get(&conn, self.author_id)?;
let author = User::get(conn, self.author_id)?;
let note = self.to_activity(conn)?;
let mut act = Create::default();
act.create_props.set_actor_link(author.into_id())?;
act.create_props.set_object_object(note.clone())?;
act.object_props
.set_id_string(format!("{}/activity", self.ap_url.clone()?,))?;
act.object_props
.set_to_link_vec(note.object_props.to_link_vec::<Id>()?)?;
act.object_props
.set_cc_link_vec(vec![Id::new(self.get_author(&conn)?.followers_endpoint)])?;
let note_clone = note.clone();
let mut act = Create::new(
author.into_id().parse::<IriString>()?,
Base::retract(note)?.into_generic()?,
);
act.set_id(
format!(
"{}/activity",
self.ap_url.clone().ok_or(Error::MissingApProperty)?,
)
.parse::<IriString>()?,
);
act.set_many_tos(
note_clone
.to()
.iter()
.flat_map(|tos| tos.iter().map(|to| to.to_owned())),
);
act.set_many_ccs(vec![self.get_author(conn)?.followers_endpoint]);
Ok(act)
}
@ -177,18 +197,21 @@ impl Comment {
}
pub fn build_delete(&self, conn: &Connection) -> Result<Delete> {
let mut act = Delete::default();
act.delete_props
.set_actor_link(self.get_author(conn)?.into_id())?;
let mut tombstone = Tombstone::new();
tombstone.set_id(
self.ap_url
.as_ref()
.ok_or(Error::MissingApProperty)?
.parse::<IriString>()?,
);
let mut tombstone = Tombstone::default();
tombstone.object_props.set_id_string(self.ap_url.clone()?)?;
act.delete_props.set_object_object(tombstone)?;
let mut act = Delete::new(
self.get_author(conn)?.into_id().parse::<IriString>()?,
Base::retract(tombstone)?.into_generic()?,
);
act.object_props
.set_id_string(format!("{}#delete", self.ap_url.clone().unwrap()))?;
act.object_props
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?;
act.set_id(format!("{}#delete", self.ap_url.clone().unwrap()).parse::<IriString>()?);
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
Ok(act)
}
@ -204,105 +227,110 @@ impl FromId<DbConn> for Comment {
fn from_activity(conn: &DbConn, note: Note) -> Result<Self> {
let comm = {
let previous_url = note.object_props.in_reply_to.as_ref()?.as_str()?;
let previous_comment = Comment::find_by_ap_url(conn, previous_url);
let is_public = |v: &Option<serde_json::Value>| match v
.as_ref()
.unwrap_or(&serde_json::Value::Null)
{
serde_json::Value::Array(v) => v
.iter()
.filter_map(serde_json::Value::as_str)
.any(|s| s == PUBLIC_VISIBILITY),
serde_json::Value::String(s) => s == PUBLIC_VISIBILITY,
_ => false,
let previous_url = note
.in_reply_to()
.ok_or(Error::MissingApProperty)?
.iter()
.next()
.ok_or(Error::MissingApProperty)?
.id()
.ok_or(Error::MissingApProperty)?;
let previous_comment = Comment::find_by_ap_url(conn, previous_url.as_str());
let is_public = |v: &Option<&OneOrMany<AnyBase>>| match v {
Some(one_or_many) => one_or_many.iter().any(|any_base| {
let id = any_base.id();
id.is_some() && id.unwrap() == PUBLIC_VISIBILITY
}),
None => false,
};
let public_visibility = is_public(&note.object_props.to)
|| is_public(&note.object_props.bto)
|| is_public(&note.object_props.cc)
|| is_public(&note.object_props.bcc);
let public_visibility = is_public(&note.to())
|| is_public(&note.bto())
|| is_public(&note.cc())
|| is_public(&note.bcc());
let summary = note.summary().and_then(|summary| summary.to_as_string());
let sensitive = summary.is_some();
let comm = Comment::insert(
conn,
NewComment {
content: SafeString::new(&note.object_props.content_string()?),
spoiler_text: note.object_props.summary_string().unwrap_or_default(),
ap_url: note.object_props.id_string().ok(),
content: SafeString::new(
&note
.content()
.ok_or(Error::MissingApProperty)?
.to_as_string()
.ok_or(Error::InvalidValue)?,
),
spoiler_text: summary.unwrap_or_default(),
ap_url: Some(
note.id_unchecked()
.ok_or(Error::MissingApProperty)?
.to_string(),
),
in_response_to_id: previous_comment.iter().map(|c| c.id).next(),
post_id: previous_comment.map(|c| c.post_id).or_else(|_| {
Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result<i32>
Ok(Post::find_by_ap_url(conn, previous_url.as_str())?.id) as Result<i32>
})?,
author_id: User::from_id(
conn,
&note.object_props.attributed_to_link::<Id>()?,
&note
.attributed_to()
.ok_or(Error::MissingApProperty)?
.to_as_uri()
.ok_or(Error::MissingApProperty)?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?
.id,
sensitive: note.object_props.summary_string().is_ok(),
sensitive,
public_visibility,
},
)?;
// save mentions
if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() {
for tag in tags {
serde_json::from_value::<link::Mention>(tag)
.map_err(Error::from)
.and_then(|m| {
let author = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0];
let not_author = m.link_props.href_string()? != author.ap_url.clone();
Ok(Mention::from_activity(
conn, &m, comm.id, false, not_author,
)?)
})
.ok();
if let Some(tags) = note.tag() {
let author_url = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0].ap_url;
for tag in tags.iter() {
let m = tag.clone().extend::<link::Mention, MentionType>()?; // FIXME: Don't clone
if m.is_none() {
continue;
}
let m = m.unwrap();
let not_author = m.href().ok_or(Error::MissingApProperty)? != author_url;
let _ = Mention::from_activity(conn, &m, comm.id, false, not_author);
}
}
comm
};
if !comm.public_visibility {
let receivers_ap_url = |v: Option<serde_json::Value>| {
let filter = |e: serde_json::Value| {
if let serde_json::Value::String(s) = e {
Some(s)
} else {
None
let mut receiver_ids = HashSet::new();
let mut receivers_id = |v: Option<&'_ OneOrMany<AnyBase>>| {
if let Some(one_or_many) = v {
for any_base in one_or_many.iter() {
if let Some(id) = any_base.id() {
receiver_ids.insert(id.to_string());
}
}
};
match v.unwrap_or(serde_json::Value::Null) {
serde_json::Value::Array(v) => v,
v => vec![v],
}
.into_iter()
.filter_map(filter)
};
let mut note = note;
receivers_id(note.to());
receivers_id(note.cc());
receivers_id(note.bto());
receivers_id(note.bcc());
let to = receivers_ap_url(note.object_props.to.take());
let cc = receivers_ap_url(note.object_props.cc.take());
let bto = receivers_ap_url(note.object_props.bto.take());
let bcc = receivers_ap_url(note.object_props.bcc.take());
let receivers_ap_url = to
.chain(cc)
.chain(bto)
.chain(bcc)
.collect::<HashSet<_>>() // remove duplicates (don't do a query more than once)
let receivers_ap_url = receiver_ids
.into_iter()
.map(|v| {
if let Ok(user) = User::from_id(conn, &v, None, CONFIG.proxy()) {
.flat_map(|v| {
if let Ok(user) = User::from_id(conn, v.as_ref(), None, CONFIG.proxy()) {
vec![user]
} else {
vec![] // TODO try to fetch collection
}
})
.flatten()
.filter(|u| u.get_instance(conn).map(|i| i.local).unwrap_or(false))
.collect::<HashSet<User>>(); //remove duplicates (prevent db error)
@ -320,6 +348,10 @@ impl FromId<DbConn> for Comment {
comm.notify(conn)?;
Ok(comm)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl AsObject<User, Create, &DbConn> for Comment {
@ -348,7 +380,7 @@ impl AsObject<User, Delete, &DbConn> for Comment {
m.delete(conn)?;
}
for n in Notification::find_for_comment(&conn, &self)? {
for n in Notification::find_for_comment(conn, &self)? {
n.delete(&**conn)?;
}
@ -390,10 +422,34 @@ impl CommentTree {
#[cfg(test)]
mod tests {
use super::*;
use crate::blogs::Blog;
use crate::inbox::{inbox, tests::fill_database, InboxResult};
use crate::safe_string::SafeString;
use crate::tests::db;
use crate::tests::{db, format_datetime};
use assert_json_diff::assert_json_eq;
use diesel::Connection;
use serde_json::{json, to_value};
fn prepare_activity(conn: &DbConn) -> (Comment, Vec<Post>, Vec<User>, Vec<Blog>) {
let (posts, users, blogs) = fill_database(&conn);
let comment = Comment::insert(
conn,
NewComment {
content: SafeString::new("My comment, mentioning to @user"),
in_response_to_id: None,
post_id: posts[0].id,
author_id: users[0].id,
ap_url: None,
sensitive: true,
spoiler_text: "My CW".into(),
public_visibility: true,
},
)
.unwrap();
(comment, posts, users, blogs)
}
// creates a post, get it's Create activity, delete the post,
// "send" the Create to the inbox, and check it works
@ -401,30 +457,77 @@ mod tests {
fn self_federation() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (posts, users, _) = fill_database(&conn);
let (original_comm, posts, users, _blogs) = prepare_activity(&conn);
let act = original_comm.create_activity(&conn).unwrap();
let original_comm = Comment::insert(
assert_json_eq!(to_value(&act).unwrap(), json!({
"actor": "https://plu.me/@/admin/",
"cc": ["https://plu.me/@/admin/followers"],
"id": format!("https://plu.me/~/BlogName/testing/comment/{}/activity", original_comm.id),
"object": {
"attributedTo": "https://plu.me/@/admin/",
"content": r###"<p dir="auto">My comment, mentioning to <a href="https://plu.me/@/user/" title="user">@user</a></p>
"###,
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", original_comm.id),
"inReplyTo": "https://plu.me/~/BlogName/testing",
"published": format_datetime(&original_comm.creation_date),
"summary": "My CW",
"tag": [
{
"href": "https://plu.me/@/user/",
"name": "@user",
"type": "Mention"
}
],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Note"
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Create",
}));
let reply = Comment::insert(
conn,
NewComment {
content: SafeString::new("My comment"),
in_response_to_id: None,
content: SafeString::new(""),
in_response_to_id: Some(original_comm.id),
post_id: posts[0].id,
author_id: users[0].id,
author_id: users[1].id,
ap_url: None,
sensitive: true,
spoiler_text: "My CW".into(),
sensitive: false,
spoiler_text: "".into(),
public_visibility: true,
},
)
.unwrap();
let act = original_comm.create_activity(&conn).unwrap();
let reply_act = reply.create_activity(&conn).unwrap();
assert_json_eq!(to_value(&reply_act).unwrap(), json!({
"actor": "https://plu.me/@/user/",
"cc": ["https://plu.me/@/user/followers"],
"id": format!("https://plu.me/~/BlogName/testing/comment/{}/activity", reply.id),
"object": {
"attributedTo": "https://plu.me/@/user/",
"content": "",
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", reply.id),
"inReplyTo": format!("https://plu.me/~/BlogName/testing/comment/{}", original_comm.id),
"published": format_datetime(&reply.creation_date),
"summary": "",
"tag": [],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Note"
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Create"
}));
inbox(
&conn,
serde_json::to_value(original_comm.build_delete(&conn).unwrap()).unwrap(),
)
.unwrap();
match inbox(&conn, serde_json::to_value(act).unwrap()).unwrap() {
match inbox(&conn, to_value(act).unwrap()).unwrap() {
InboxResult::Commented(c) => {
// TODO: one is HTML, the other markdown: assert_eq!(c.content, original_comm.content);
assert_eq!(c.in_response_to_id, original_comm.in_response_to_id);
@ -439,4 +542,60 @@ mod tests {
Ok(())
})
}
#[test]
fn to_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (comment, _posts, _users, _blogs) = prepare_activity(&conn);
let act = comment.to_activity(&conn)?;
let expected = json!({
"attributedTo": "https://plu.me/@/admin/",
"content": r###"<p dir="auto">My comment, mentioning to <a href="https://plu.me/@/user/" title="user">@user</a></p>
"###,
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", comment.id),
"inReplyTo": "https://plu.me/~/BlogName/testing",
"published": format_datetime(&comment.creation_date),
"summary": "My CW",
"tag": [
{
"href": "https://plu.me/@/user/",
"name": "@user",
"type": "Mention"
}
],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Note"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn build_delete() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (comment, _posts, _users, _blogs) = prepare_activity(&conn);
let act = comment.build_delete(&conn)?;
let expected = json!({
"actor": "https://plu.me/@/admin/",
"id": format!("https://plu.me/~/BlogName/testing/comment/{}#delete", comment.id),
"object": {
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", comment.id),
"type": "Tombstone"
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Delete"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
}

@ -1,4 +1,6 @@
use crate::search::TokenizerKind as SearchTokenizer;
use crate::signups::Strategy as SignupStrategy;
use crate::smtp::{SMTP_PORT, SUBMISSIONS_PORT, SUBMISSION_PORT};
use rocket::config::Limits;
use rocket::Config as RocketConfig;
use std::collections::HashSet;
@ -15,12 +17,14 @@ pub struct Config {
pub db_name: &'static str,
pub db_max_size: Option<u32>,
pub db_min_idle: Option<u32>,
pub signup: SignupStrategy,
pub search_index: String,
pub search_tokenizers: SearchTokenizerConfig,
pub rocket: Result<RocketConfig, RocketError>,
pub rocket: Result<RocketConfig, InvalidRocketConfig>,
pub logo: LogoConfig,
pub default_theme: String,
pub media_directory: String,
pub mail: Option<MailConfig>,
pub ldap: Option<LdapConfig>,
pub proxy: Option<ProxyConfig>,
}
@ -31,21 +35,21 @@ impl Config {
}
#[derive(Debug, Clone)]
pub enum RocketError {
InvalidEnv,
InvalidAddress,
InvalidSecretKey,
pub enum InvalidRocketConfig {
Env,
Address,
SecretKey,
}
fn get_rocket_config() -> Result<RocketConfig, RocketError> {
let mut c = RocketConfig::active().map_err(|_| RocketError::InvalidEnv)?;
fn get_rocket_config() -> Result<RocketConfig, InvalidRocketConfig> {
let mut c = RocketConfig::active().map_err(|_| InvalidRocketConfig::Env)?;
let address = var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned());
let port = var("ROCKET_PORT")
.ok()
.map(|s| s.parse::<u16>().unwrap())
.unwrap_or(7878);
let secret_key = var("ROCKET_SECRET_KEY").map_err(|_| RocketError::InvalidSecretKey)?;
let secret_key = var("ROCKET_SECRET_KEY").map_err(|_| InvalidRocketConfig::SecretKey)?;
let form_size = var("FORM_SIZE")
.unwrap_or_else(|_| "128".to_owned())
.parse::<u64>()
@ -56,10 +60,10 @@ fn get_rocket_config() -> Result<RocketConfig, RocketError> {
.unwrap();
c.set_address(address)
.map_err(|_| RocketError::InvalidAddress)?;
.map_err(|_| InvalidRocketConfig::Address)?;
c.set_port(port);
c.set_secret_key(secret_key)
.map_err(|_| RocketError::InvalidSecretKey)?;
.map_err(|_| InvalidRocketConfig::SecretKey)?;
c.set_limits(
Limits::new()
@ -155,7 +159,7 @@ impl Default for LogoConfig {
.ok()
.or_else(|| custom_main.clone());
let other = if let Some(main) = custom_main.clone() {
let ext = |path: &str| match path.rsplitn(2, '.').next() {
let ext = |path: &str| match path.rsplit_once('.').map(|x| x.1) {
Some("png") => Some("image/png".to_owned()),
Some("jpg") | Some("jpeg") => Some("image/jpeg".to_owned()),
Some("svg") => Some("image/svg+xml".to_owned()),
@ -164,11 +168,8 @@ impl Default for LogoConfig {
};
let mut custom_icons = env::vars()
.filter_map(|(var, val)| {
if let Some(size) = var.strip_prefix("PLUME_LOGO_") {
Some((size.to_owned(), val))
} else {
None
}
var.strip_prefix("PLUME_LOGO_")
.map(|size| (size.to_owned(), val))
})
.filter_map(|(var, val)| var.parse::<u64>().ok().map(|var| (var, val)))
.map(|(dim, src)| Icon {
@ -248,6 +249,31 @@ impl SearchTokenizerConfig {
}
}
pub struct MailConfig {
pub server: String,
pub port: u16,
pub helo_name: String,
pub username: String,
pub password: String,
}
fn get_mail_config() -> Option<MailConfig> {
Some(MailConfig {
server: env::var("MAIL_SERVER").ok()?,
port: env::var("MAIL_PORT").map_or(SUBMISSIONS_PORT, |port| match port.as_str() {
"smtp" => SMTP_PORT,
"submissions" => SUBMISSIONS_PORT,
"submission" => SUBMISSION_PORT,
number => number
.parse()
.expect(r#"MAIL_PORT must be "smtp", "submissions", "submission" or an integer."#),
}),
helo_name: env::var("MAIL_HELO_NAME").unwrap_or_else(|_| "localhost".to_owned()),
username: env::var("MAIL_USER").ok()?,
password: env::var("MAIL_PASSWORD").ok()?,
})
}
pub struct LdapConfig {
pub addr: String,
pub base_dn: String,
@ -338,6 +364,7 @@ lazy_static! {
s.parse::<u32>()
.expect("Couldn't parse DB_MIN_IDLE into u32")
)),
signup: var("SIGNUP").map_or(SignupStrategy::default(), |s| s.parse().unwrap()),
#[cfg(feature = "postgres")]
database_url: var("DATABASE_URL")
.unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME)),
@ -350,6 +377,7 @@ lazy_static! {
default_theme: var("DEFAULT_THEME").unwrap_or_else(|_| "default-light".to_owned()),
media_directory: var("MEDIA_UPLOAD_DIRECTORY")
.unwrap_or_else(|_| "static/media".to_owned()),
mail: get_mail_config(),
ldap: get_ldap_config(),
proxy: get_proxy_config(),
};

@ -0,0 +1,143 @@
use crate::{
db_conn::DbConn,
schema::email_signups,
users::{NewUser, Role, User},
Error, Result,
};
use chrono::{offset::Utc, Duration, NaiveDateTime};
use diesel::{
Connection as _, ExpressionMethods, Identifiable, Insertable, QueryDsl, Queryable, RunQueryDsl,
};
use plume_common::utils::random_hex;
use std::ops::Deref;
const TOKEN_VALIDITY_HOURS: i64 = 2;
#[repr(transparent)]
pub struct Token(String);
impl From<String> for Token {
fn from(string: String) -> Self {
Token(string)
}
}
impl From<Token> for String {
fn from(token: Token) -> Self {
token.0
}
}
impl Deref for Token {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Token {
fn generate() -> Self {
Self(random_hex())
}
}
#[derive(Identifiable, Queryable)]
pub struct EmailSignup {
pub id: i32,
pub email: String,
pub token: String,
pub expiration_date: NaiveDateTime,
}
#[derive(Insertable)]
#[table_name = "email_signups"]
pub struct NewEmailSignup<'a> {
pub email: &'a str,
pub token: &'a str,
pub expiration_date: NaiveDateTime,
}
impl EmailSignup {
pub fn start(conn: &DbConn, email: &str) -> Result<Token> {
conn.transaction(|| {
Self::ensure_user_not_exist_by_email(conn, email)?;
let _rows = Self::delete_existings_by_email(conn, email)?;
let token = Token::generate();
let expiration_date = Utc::now()
.naive_utc()
.checked_add_signed(Duration::hours(TOKEN_VALIDITY_HOURS))
.expect("could not calculate expiration date");
let new_signup = NewEmailSignup {
email,
token: &token,
expiration_date,
};
let _rows = diesel::insert_into(email_signups::table)
.values(new_signup)
.execute(&**conn)?;
Ok(token)
})
}
pub fn find_by_token(conn: &DbConn, token: Token) -> Result<Self> {
let signup = email_signups::table
.filter(email_signups::token.eq(token.as_str()))
.first::<Self>(&**conn)
.map_err(Error::from)?;
Ok(signup)
}
pub fn confirm(&self, conn: &DbConn) -> Result<()> {
conn.transaction(|| {
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
if self.expired() {
Self::delete_existings_by_email(conn, &self.email)?;
return Err(Error::Expired);
}
Ok(())
})
}
pub fn complete(&self, conn: &DbConn, username: String, password: String) -> Result<User> {
conn.transaction(|| {
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
let user = NewUser::new_local(
conn,
username,
"".to_string(),
Role::Normal,
"",
self.email.clone(),
Some(User::hash_pass(&password)?),
)?;
self.delete(conn)?;
Ok(user)
})
}
fn delete(&self, conn: &DbConn) -> Result<()> {
let _rows = diesel::delete(self).execute(&**conn).map_err(Error::from)?;
Ok(())
}
fn ensure_user_not_exist_by_email(conn: &DbConn, email: &str) -> Result<()> {
if User::email_used(conn, email)? {
let _rows = Self::delete_existings_by_email(conn, email)?;
return Err(Error::UserAlreadyExists);
}
Ok(())
}
fn delete_existings_by_email(conn: &DbConn, email: &str) -> Result<usize> {
let existing_signups = email_signups::table.filter(email_signups::email.eq(email));
diesel::delete(existing_signups)
.execute(&**conn)
.map_err(Error::from)
}
fn expired(&self) -> bool {
self.expiration_date < Utc::now().naive_utc()
}
}

@ -1,8 +1,13 @@
use crate::{
ap_url, db_conn::DbConn, notifications::*, schema::follows, users::User, Connection, Error,
Result, CONFIG,
ap_url, db_conn::DbConn, instance::Instance, notifications::*, schema::follows, users::User,
Connection, Error, Result, CONFIG,
};
use activitystreams::{
activity::{Accept, ActorAndObjectRef, Follow as FollowAct, Undo},
base::AnyBase,
iri_string::types::IriString,
prelude::*,
};
use activitypub::activity::{Accept, Follow as FollowAct, Undo};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use plume_common::activity_pub::{
broadcast,
@ -53,15 +58,13 @@ impl Follow {
pub fn to_activity(&self, conn: &Connection) -> Result<FollowAct> {
let user = User::get(conn, self.follower_id)?;
let target = User::get(conn, self.following_id)?;
let target_id = target.ap_url.parse::<IriString>()?;
let mut act = FollowAct::new(user.ap_url.parse::<IriString>()?, target_id.clone());
act.set_id(self.ap_url.parse::<IriString>()?);
act.set_many_tos(vec![target_id]);
act.set_many_ccs(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
let mut act = FollowAct::default();
act.follow_props.set_actor_link::<Id>(user.into_id())?;
act.follow_props
.set_object_link::<Id>(target.clone().into_id())?;
act.object_props.set_id_string(self.ap_url.clone())?;
act.object_props.set_to_link_vec(vec![target.into_id()])?;
act.object_props
.set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
Ok(act)
}
@ -94,28 +97,16 @@ impl Follow {
NewFollow {
follower_id: from_id,
following_id: target_id,
ap_url: follow.object_props.id_string()?,
ap_url: follow
.object_field_ref()
.as_single_id()
.ok_or(Error::MissingApProperty)?
.to_string(),
},
)?;
res.notify(conn)?;
let mut accept = Accept::default();
let accept_id = ap_url(&format!(
"{}/follow/{}/accept",
CONFIG.base_url.as_str(),
&res.id
));
accept.object_props.set_id_string(accept_id)?;
accept
.object_props
.set_to_link_vec(vec![from.clone().into_id()])?;
accept
.object_props
.set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
accept
.accept_props
.set_actor_link::<Id>(target.clone().into_id())?;
accept.accept_props.set_object_object(follow)?;
let accept = res.build_accept(from, target, follow)?;
broadcast(
&*target,
accept,
@ -125,18 +116,41 @@ impl Follow {
Ok(res)
}
pub fn build_accept<A: Signer + IntoId + Clone, B: Clone + AsActor<T> + IntoId, T>(
&self,
from: &B,
target: &A,
follow: FollowAct,
) -> Result<Accept> {
let mut accept = Accept::new(
target.clone().into_id().parse::<IriString>()?,
AnyBase::from_extended(follow)?,
);
let accept_id = ap_url(&format!(
"{}/follows/{}/accept",
CONFIG.base_url.as_str(),
self.id
));
accept.set_id(accept_id.parse::<IriString>()?);
accept.set_many_tos(vec![from.clone().into_id().parse::<IriString>()?]);
accept.set_many_ccs(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
Ok(accept)
}
pub fn build_undo(&self, conn: &Connection) -> Result<Undo> {
let mut undo = Undo::default();
undo.undo_props
.set_actor_link(User::get(conn, self.follower_id)?.into_id())?;
undo.object_props
.set_id_string(format!("{}/undo", self.ap_url))?;
undo.undo_props
.set_object_link::<Id>(self.clone().into_id())?;
undo.object_props
.set_to_link_vec(vec![User::get(conn, self.following_id)?.into_id()])?;
undo.object_props
.set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
let mut undo = Undo::new(
User::get(conn, self.follower_id)?
.ap_url
.parse::<IriString>()?,
self.ap_url.parse::<IriString>()?,
);
undo.set_id(format!("{}/undo", self.ap_url).parse::<IriString>()?);
undo.set_many_tos(vec![User::get(conn, self.following_id)?
.ap_url
.parse::<IriString>()?]);
undo.set_many_ccs(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
Ok(undo)
}
}
@ -148,11 +162,7 @@ impl AsObject<User, FollowAct, &DbConn> for User {
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Follow> {
// Mastodon (at least) requires the full Follow object when accepting it,
// so we rebuilt it here
let mut follow = FollowAct::default();
follow.object_props.set_id_string(id.to_string())?;
follow
.follow_props
.set_actor_link::<Id>(actor.clone().into_id())?;
let follow = FollowAct::new(actor.ap_url.parse::<IriString>()?, id.parse::<IriString>()?);
Follow::accept_follow(conn, &actor, &self, follow, actor.id, self.id)
}
}
@ -168,7 +178,11 @@ impl FromId<DbConn> for Follow {
fn from_activity(conn: &DbConn, follow: FollowAct) -> Result<Self> {
let actor = User::from_id(
conn,
&follow.follow_props.actor_link::<Id>()?,
follow
.actor_field_ref()
.as_single_id()
.ok_or(Error::MissingApProperty)?
.as_str(),
None,
CONFIG.proxy(),
)
@ -176,13 +190,21 @@ impl FromId<DbConn> for Follow {
let target = User::from_id(
conn,
&follow.follow_props.object_link::<Id>()?,
follow
.object_field_ref()
.as_single_id()
.ok_or(Error::MissingApProperty)?
.as_str(),
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?;
Follow::accept_follow(conn, &actor, &target, follow, actor.id, target.id)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl AsObject<User, Undo, &DbConn> for Follow {
@ -195,7 +217,7 @@ impl AsObject<User, Undo, &DbConn> for Follow {
diesel::delete(&self).execute(&**conn)?;
// delete associated notification if any
if let Ok(notif) = Notification::find(&conn, notification_kind::FOLLOW, self.id) {
if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
diesel::delete(&notif).execute(&**conn)?;
}
@ -215,8 +237,29 @@ impl IntoId for Follow {
#[cfg(test)]
mod tests {
use super::*;
use crate::{tests::db, users::tests as user_tests};
use crate::{tests::db, users::tests as user_tests, users::tests::fill_database};
use assert_json_diff::assert_json_eq;
use diesel::Connection;
use serde_json::{json, to_value};
fn prepare_activity(conn: &DbConn) -> (Follow, User, User, Vec<User>) {
let users = fill_database(conn);
let following = &users[1];
let follower = &users[2];
let mut follow = Follow::insert(
conn,
NewFollow {
follower_id: follower.id,
following_id: following.id,
ap_url: "".into(),
},
)
.unwrap();
// following.ap_url = format!("https://plu.me/follows/{}", follow.id);
follow.ap_url = format!("https://plu.me/follows/{}", follow.id);
(follow, following.to_owned(), follower.to_owned(), users)
}
#[test]
fn test_id() {
@ -251,4 +294,77 @@ mod tests {
Ok(())
})
}
#[test]
fn to_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (follow, _following, _follower, _users) = prepare_activity(&conn);
let act = follow.to_activity(&conn)?;
let expected = json!({
"actor": "https://plu.me/@/other/",
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
"id": format!("https://plu.me/follows/{}", follow.id),
"object": "https://plu.me/@/user/",
"to": ["https://plu.me/@/user/"],
"type": "Follow"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn build_accept() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (follow, following, follower, _users) = prepare_activity(&conn);
let act = follow.build_accept(&follower, &following, follow.to_activity(&conn)?)?;
let expected = json!({
"actor": "https://plu.me/@/user/",
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
"id": format!("https://127.0.0.1:7878/follows/{}/accept", follow.id),
"object": {
"actor": "https://plu.me/@/other/",
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
"id": format!("https://plu.me/follows/{}", follow.id),
"object": "https://plu.me/@/user/",
"to": ["https://plu.me/@/user/"],
"type": "Follow"
},
"to": ["https://plu.me/@/other/"],
"type": "Accept"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn build_undo() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (follow, _following, _follower, _users) = prepare_activity(&conn);
let act = follow.build_undo(&conn)?;
let expected = json!({
"actor": "https://plu.me/@/other/",
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
"id": format!("https://plu.me/follows/{}/undo", follow.id),
"object": format!("https://plu.me/follows/{}", follow.id),
"to": ["https://plu.me/@/user/"],
"type": "Undo"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
}

@ -1,4 +1,4 @@
use activitypub::activity::*;
use activitystreams::activity::{Announce, Create, Delete, Follow, Like, Undo, Update};
use crate::{
comments::Comment,
@ -94,8 +94,8 @@ pub(crate) mod tests {
license: "WTFPL".to_owned(),
creation_date: None,
ap_url: format!("https://plu.me/~/{}/testing", blogs[0].actor_id),
subtitle: String::new(),
source: String::new(),
subtitle: "Bye".to_string(),
source: "Hello".to_string(),
cover_id: None,
},
)
@ -268,7 +268,7 @@ pub(crate) mod tests {
"actor": users[0].ap_url,
"object": {
"type": "Article",
"id": "https://plu.me/~/Blog/my-article",
"id": "https://plu.me/~/BlogName/testing",
"attributedTo": [users[0].ap_url, blogs[0].ap_url],
"content": "Hello.",
"name": "My Article",
@ -286,11 +286,11 @@ pub(crate) mod tests {
match super::inbox(&conn, act).unwrap() {
super::InboxResult::Post(p) => {
assert!(p.is_author(&conn, users[0].id).unwrap());
assert_eq!(p.source, "Hello.".to_owned());
assert_eq!(p.source, "Hello".to_owned());
assert_eq!(p.blog_id, blogs[0].id);
assert_eq!(p.content, SafeString::new("Hello."));
assert_eq!(p.subtitle, "Bye.".to_owned());
assert_eq!(p.title, "My Article".to_owned());
assert_eq!(p.content, SafeString::new("Hello"));
assert_eq!(p.subtitle, "Bye".to_owned());
assert_eq!(p.title, "Testing".to_owned());
}
_ => panic!("Unexpected result"),
};

@ -3,11 +3,12 @@ use crate::{
medias::Media,
safe_string::SafeString,
schema::{instances, users},
users::{Role, User},
users::{NewUser, Role, User},
Connection, Error, Result,
};
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use diesel::{self, result::Error::NotFound, ExpressionMethods, QueryDsl, RunQueryDsl};
use once_cell::sync::OnceCell;
use plume_common::utils::md_to_html;
use std::sync::RwLock;
@ -45,6 +46,9 @@ lazy_static! {
static ref LOCAL_INSTANCE: RwLock<Option<Instance>> = RwLock::new(None);
}
const LOCAL_INSTANCE_USERNAME: &str = "__instance__";
static LOCAL_INSTANCE_USER: OnceCell<User> = OnceCell::new();
impl Instance {
pub fn set_local(self) {
LOCAL_INSTANCE.write().unwrap().replace(self);
@ -76,6 +80,42 @@ impl Instance {
.map_err(Error::from)
}
pub fn create_local_instance_user(conn: &Connection) -> Result<User> {
let instance = Instance::get_local()?;
let email = format!("{}@{}", LOCAL_INSTANCE_USERNAME, &instance.public_domain);
NewUser::new_local(
conn,
LOCAL_INSTANCE_USERNAME.into(),
instance.public_domain,
Role::Instance,
"Local instance",
email,
None,
)
}
pub fn get_local_instance_user() -> Option<&'static User> {
LOCAL_INSTANCE_USER.get()
}
pub fn get_local_instance_user_uncached(conn: &Connection) -> Result<User> {
users::table
.filter(users::role.eq(3))
.first(conn)
.or_else(|err| match err {
NotFound => Self::create_local_instance_user(conn),
_ => Err(Error::Db(err)),
})
}
pub fn cache_local_instance_user(conn: &Connection) {
let _ = LOCAL_INSTANCE_USER.get_or_init(|| {
Self::get_local_instance_user_uncached(conn)
.or_else(|_| Self::create_local_instance_user(conn))
.expect("Failed to cache local instance user")
});
}
pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<Instance>> {
instances::table
.order(instances::public_domain.asc())
@ -304,6 +344,7 @@ pub(crate) mod tests {
})
.collect();
Instance::cache_local(conn);
Instance::cache_local_instance_user(conn);
res
}

@ -1,4 +1,3 @@
#![feature(try_trait)]
#![feature(never_type)]
#![feature(proc_macro_hygiene)]
#![feature(box_patterns)]
@ -17,8 +16,11 @@ extern crate serde_json;
#[macro_use]
extern crate tantivy;
use activitystreams::iri_string;
pub use lettre;
pub use lettre::smtp;
use once_cell::sync::Lazy;
use plume_common::activity_pub::inbox::InboxError;
use plume_common::activity_pub::{inbox::InboxError, request, sign};
use posts::PostEvent;
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
use users::UserEvent;
@ -66,6 +68,7 @@ pub enum Error {
Url,
Webfinger,
Expired,
UserAlreadyExists,
}
impl From<bcrypt::BcryptError> for Error {
@ -80,20 +83,26 @@ impl From<openssl::error::ErrorStack> for Error {
}
}
impl From<sign::Error> for Error {
fn from(_: sign::Error) -> Self {
Error::Signature
}
}
impl From<diesel::result::Error> for Error {
fn from(err: diesel::result::Error) -> Self {
Error::Db(err)
}
}
impl From<std::option::NoneError> for Error {
fn from(_: std::option::NoneError) -> Self {
Error::NotFound
impl From<url::ParseError> for Error {
fn from(_: url::ParseError) -> Self {
Error::Url
}
}
impl From<url::ParseError> for Error {
fn from(_: url::ParseError) -> Self {
impl From<iri_string::validate::Error> for Error {
fn from(_: iri_string::validate::Error) -> Self {
Error::Url
}
}
@ -116,12 +125,9 @@ impl From<reqwest::header::InvalidHeaderValue> for Error {
}
}
impl From<activitypub::Error> for Error {
fn from(err: activitypub::Error) -> Self {
match err {
activitypub::Error::NotFound => Error::MissingApProperty,
_ => Error::SerDe,
}
impl From<activitystreams::checked::CheckError> for Error {
fn from(_: activitystreams::checked::CheckError) -> Error {
Error::MissingApProperty
}
}
@ -158,6 +164,12 @@ impl From<InboxError<Error>> for Error {
}
}
impl From<request::Error> for Error {
fn from(_err: request::Error) -> Error {
Error::Request
}
}
pub type Result<T> = std::result::Result<T, Error>;
/// Adds a function to a model, that returns the first
@ -295,10 +307,38 @@ pub fn ap_url(url: &str) -> String {
format!("https://{}", url)
}
pub trait SmtpNewWithAddr {
fn new_with_addr(
addr: (&str, u16),
) -> std::result::Result<smtp::SmtpClient, smtp::error::Error>;
}
impl SmtpNewWithAddr for smtp::SmtpClient {
// Stolen from lettre::smtp::SmtpClient::new_simple()
fn new_with_addr(addr: (&str, u16)) -> std::result::Result<Self, smtp::error::Error> {
use native_tls::TlsConnector;
use smtp::{
client::net::{ClientTlsParameters, DEFAULT_TLS_PROTOCOLS},
ClientSecurity, SmtpClient,
};
let (domain, port) = addr;
let mut tls_builder = TlsConnector::builder();
tls_builder.min_protocol_version(Some(DEFAULT_TLS_PROTOCOLS[0]));
let tls_parameters =
ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap());
SmtpClient::new((domain, port), ClientSecurity::Wrapper(tls_parameters))
}
}
#[cfg(test)]
#[macro_use]
mod tests {
use crate::{db_conn, migrations::IMPORTED_MIGRATIONS, Connection as Conn, CONFIG};
use chrono::{naive::NaiveDateTime, Datelike, Timelike};
use diesel::r2d2::ConnectionManager;
use plume_common::utils::random_hex;
use std::env::temp_dir;
@ -331,6 +371,33 @@ mod tests {
pool
};
}
#[cfg(feature = "postgres")]
pub(crate) fn format_datetime(dt: &NaiveDateTime) -> String {
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z",
dt.year(),
dt.month(),
dt.day(),
dt.hour(),
dt.minute(),
dt.second(),
dt.timestamp_subsec_micros()
)
}
#[cfg(feature = "sqlite")]
pub(crate) fn format_datetime(dt: &NaiveDateTime) -> String {
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
dt.year(),
dt.month(),
dt.day(),
dt.hour(),
dt.minute(),
dt.second()
)
}
}
pub mod admin;
@ -342,6 +409,7 @@ pub mod blogs;
pub mod comment_seers;
pub mod comments;
pub mod db_conn;
pub mod email_signups;
pub mod follows;
pub mod headers;
pub mod inbox;
@ -362,6 +430,7 @@ pub mod safe_string;
#[allow(unused_imports)]
pub mod schema;
pub mod search;
pub mod signups;
pub mod tags;
pub mod timeline;
pub mod users;

@ -1,13 +1,19 @@
use crate::{
db_conn::DbConn, notifications::*, posts::Post, schema::likes, timeline::*, users::User,
Connection, Error, Result, CONFIG,
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*,
users::User, Connection, Error, Result, CONFIG,
};
use activitystreams::{
activity::{ActorAndObjectRef, Like as LikeAct, Undo},
base::AnyBase,
iri_string::types::IriString,
prelude::*,
};
use activitypub::activity;
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId},
Id, IntoId, PUBLIC_VISIBILITY,
sign::Signer,
PUBLIC_VISIBILITY,
};
#[derive(Clone, Queryable, Identifiable)]
@ -33,18 +39,16 @@ impl Like {
find_by!(likes, find_by_ap_url, ap_url as &str);
find_by!(likes, find_by_user_on_post, user_id as i32, post_id as i32);
pub fn to_activity(&self, conn: &Connection) -> Result<activity::Like> {
let mut act = activity::Like::default();
act.like_props
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
act.like_props
.set_object_link(Post::get(conn, self.post_id)?.into_id())?;
act.object_props
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
act.object_props.set_cc_link_vec(vec![Id::new(
User::get(conn, self.user_id)?.followers_endpoint,
)])?;
act.object_props.set_id_string(self.ap_url.clone())?;
pub fn to_activity(&self, conn: &Connection) -> Result<LikeAct> {
let mut act = LikeAct::new(
User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?,
Post::get(conn, self.post_id)?.ap_url.parse::<IriString>()?,
);
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
act.set_many_ccs(vec![User::get(conn, self.user_id)?
.followers_endpoint
.parse::<IriString>()?]);
act.set_id(self.ap_url.parse::<IriString>()?);
Ok(act)
}
@ -66,24 +70,22 @@ impl Like {
Ok(())
}
pub fn build_undo(&self, conn: &Connection) -> Result<activity::Undo> {
let mut act = activity::Undo::default();
act.undo_props
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
act.undo_props.set_object_object(self.to_activity(conn)?)?;
act.object_props
.set_id_string(format!("{}#delete", self.ap_url))?;
act.object_props
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
act.object_props.set_cc_link_vec(vec![Id::new(
User::get(conn, self.user_id)?.followers_endpoint,
)])?;
pub fn build_undo(&self, conn: &Connection) -> Result<Undo> {
let mut act = Undo::new(
User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?,
AnyBase::from_extended(self.to_activity(conn)?)?,
);
act.set_id(format!("{}#delete", self.ap_url).parse::<IriString>()?);
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
act.set_many_ccs(vec![User::get(conn, self.user_id)?
.followers_endpoint
.parse::<IriString>()?]);
Ok(act)
}
}
impl AsObject<User, activity::Like, &DbConn> for Post {
impl AsObject<User, LikeAct, &DbConn> for Post {
type Error = Error;
type Output = Like;
@ -105,19 +107,22 @@ impl AsObject<User, activity::Like, &DbConn> for Post {
impl FromId<DbConn> for Like {
type Error = Error;
type Object = activity::Like;
type Object = LikeAct;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
Like::find_by_ap_url(conn, id)
}
fn from_activity(conn: &DbConn, act: activity::Like) -> Result<Self> {
fn from_activity(conn: &DbConn, act: LikeAct) -> Result<Self> {
let res = Like::insert(
conn,
NewLike {
post_id: Post::from_id(
conn,
&act.like_props.object_link::<Id>()?,
act.object_field_ref()
.as_single_id()
.ok_or(Error::MissingApProperty)?
.as_str(),
None,
CONFIG.proxy(),
)
@ -125,21 +130,31 @@ impl FromId<DbConn> for Like {
.id,
user_id: User::from_id(
conn,
&act.like_props.actor_link::<Id>()?,
act.actor_field_ref()
.as_single_id()
.ok_or(Error::MissingApProperty)?
.as_str(),
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?
.id,
ap_url: act.object_props.id_string()?,
ap_url: act
.id_unchecked()
.ok_or(Error::MissingApProperty)?
.to_string(),
},
)?;
res.notify(conn)?;
Ok(res)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl AsObject<User, activity::Undo, &DbConn> for Like {
impl AsObject<User, Undo, &DbConn> for Like {
type Error = Error;
type Output = ();
@ -148,7 +163,7 @@ impl AsObject<User, activity::Undo, &DbConn> for Like {
diesel::delete(&self).execute(&**conn)?;
// delete associated notification if any
if let Ok(notif) = Notification::find(&conn, notification_kind::LIKE, self.id) {
if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) {
diesel::delete(&notif).execute(&**conn)?;
}
Ok(())
@ -160,8 +175,7 @@ impl AsObject<User, activity::Undo, &DbConn> for Like {
impl NewLike {
pub fn new(p: &Post, u: &User) -> Self {
// TODO: this URL is not valid
let ap_url = format!("{}/like/{}", u.ap_url, p.ap_url);
let ap_url = format!("{}like/{}", u.ap_url, p.ap_url);
NewLike {
post_id: p.id,
user_id: u.id,
@ -169,3 +183,67 @@ impl NewLike {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diesel::Connection;
use crate::{inbox::tests::fill_database, tests::db};
use assert_json_diff::assert_json_eq;
use serde_json::{json, to_value};
#[test]
fn to_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (posts, _users, _blogs) = fill_database(&conn);
let post = &posts[0];
let user = &post.get_authors(&conn)?[0];
let like = Like::insert(&*conn, NewLike::new(post, user))?;
let act = like.to_activity(&conn).unwrap();
let expected = json!({
"actor": "https://plu.me/@/admin/",
"cc": ["https://plu.me/@/admin/followers"],
"id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing",
"object": "https://plu.me/~/BlogName/testing",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Like",
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn build_undo() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (posts, _users, _blogs) = fill_database(&conn);
let post = &posts[0];
let user = &post.get_authors(&conn)?[0];
let like = Like::insert(&*conn, NewLike::new(post, user))?;
let act = like.build_undo(&*conn)?;
let expected = json!({
"actor": "https://plu.me/@/admin/",
"cc": ["https://plu.me/@/admin/followers"],
"id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing#delete",
"object": {
"actor": "https://plu.me/@/admin/",
"cc": ["https://plu.me/@/admin/followers"],
"id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing",
"object": "https://plu.me/~/BlogName/testing",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Like",
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Undo",
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
}

@ -143,6 +143,7 @@ macro_rules! func {
}
}
#[allow(dead_code)]
#[derive(Clone, Queryable, Identifiable)]
struct ListElem {
pub id: i32,
@ -285,7 +286,8 @@ impl List {
.select(list_elems::word)
.load::<Option<String>>(conn)
.map_err(Error::from)
.map(|r| r.into_iter().filter_map(|o| o).collect::<Vec<String>>())
// .map(|r| r.into_iter().filter_map(|o| o).collect::<Vec<String>>())
.map(|r| r.into_iter().flatten().collect::<Vec<String>>())
}
pub fn clear(&self, conn: &Connection) -> Result<()> {

@ -2,24 +2,23 @@ use crate::{
ap_url, db_conn::DbConn, instance::Instance, safe_string::SafeString, schema::medias,
users::User, Connection, Error, Result, CONFIG,
};
use activitypub::object::Image;
use askama_escape::escape;
use activitystreams::{object::Image, prelude::*};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use guid_create::GUID;
use plume_common::{
activity_pub::{inbox::FromId, Id},
utils::MediaProcessor,
activity_pub::{inbox::FromId, request, ToAsString, ToAsUri},
utils::{escape, MediaProcessor},
};
use std::{
fs::{self, DirBuilder},
path::{Path, PathBuf},
path::{self, Path, PathBuf},
};
use tracing::warn;
use url::Url;
const REMOTE_MEDIA_DIRECTORY: &str = "remote";
#[derive(Clone, Identifiable, Queryable)]
#[derive(Clone, Identifiable, Queryable, AsChangeset)]
pub struct Media {
pub id: i32,
pub file_path: String,
@ -65,6 +64,7 @@ impl MediaCategory {
impl Media {
insert!(medias, NewMedia);
get!(medias);
find_by!(medias, find_by_file_path, file_path as &str);
pub fn for_user(conn: &Connection, owner: i32) -> Result<Vec<Media>> {
medias::table
@ -103,8 +103,8 @@ impl Media {
pub fn category(&self) -> MediaCategory {
match &*self
.file_path
.rsplitn(2, '.')
.next()
.rsplit_once('.')
.map(|x| x.1)
.expect("Media::category: extension error")
.to_lowercase()
{
@ -155,12 +155,15 @@ impl Media {
if self.is_remote {
Ok(self.remote_url.clone().unwrap_or_default())
} else {
let p = Path::new(&self.file_path);
let filename: String = p.file_name().unwrap().to_str().unwrap().to_owned();
let file_path = self.file_path.replace(path::MAIN_SEPARATOR, "/").replacen(
&CONFIG.media_directory,
"static/media",
1,
); // "static/media" from plume::routs::plume_media_files()
Ok(ap_url(&format!(
"{}/static/media/{}",
"{}/{}",
Instance::get_local()?.public_domain,
&filename
&file_path
)))
}
}
@ -204,52 +207,89 @@ impl Media {
// TODO: merge with save_remote?
pub fn from_activity(conn: &DbConn, image: &Image) -> Result<Media> {
let remote_url = image.object_props.url_string().ok()?;
let remote_url = image
.url()
.and_then(|url| url.to_as_uri())
.ok_or(Error::MissingApProperty)?;
let path = determine_mirror_file_path(&remote_url);
let parent = path.parent()?;
let parent = path.parent().ok_or(Error::InvalidValue)?;
if !parent.is_dir() {
DirBuilder::new().recursive(true).create(parent)?;
}
let mut dest = fs::File::create(path.clone()).ok()?;
let mut dest = fs::File::create(path.clone())?;
// TODO: conditional GET
if let Some(proxy) = CONFIG.proxy() {
reqwest::ClientBuilder::new().proxy(proxy.clone()).build()?
} else {
reqwest::Client::new()
}
.get(remote_url.as_str())
.send()
.ok()?
.copy_to(&mut dest)
.ok()?;
// TODO: upsert
Media::insert(
conn,
NewMedia {
file_path: path.to_str()?.to_string(),
alt_text: image.object_props.content_string().ok()?,
is_remote: false,
remote_url: None,
sensitive: image.object_props.summary_string().is_ok(),
content_warning: image.object_props.summary_string().ok(),
owner_id: User::from_id(
request::get(
remote_url.as_str(),
User::get_sender(),
CONFIG.proxy().cloned(),
)?
.copy_to(&mut dest)?;
Media::find_by_file_path(conn, path.to_str().ok_or(Error::InvalidValue)?)
.and_then(|mut media| {
let mut updated = false;
let alt_text = image
.content()
.and_then(|content| content.to_as_string())
.ok_or(Error::NotFound)?;
let summary = image.summary().and_then(|summary| summary.to_as_string());
let sensitive = summary.is_some();
let content_warning = summary;
if media.alt_text != alt_text {
media.alt_text = alt_text;
updated = true;
}
if media.is_remote {
media.is_remote = false;
updated = true;
}
if media.remote_url.is_some() {
media.remote_url = None;
updated = true;
}
if media.sensitive != sensitive {
media.sensitive = sensitive;
updated = true;
}
if media.content_warning != content_warning {
media.content_warning = content_warning;
updated = true;
}
if updated {
diesel::update(&media).set(&media).execute(&**conn)?;
}
Ok(media)
})
.or_else(|_| {
let summary = image.summary().and_then(|summary| summary.to_as_string());
Media::insert(
conn,
image
.object_props
.attributed_to_link_vec::<Id>()
.ok()?
.into_iter()
.next()?
.as_ref(),
None,
CONFIG.proxy(),
NewMedia {
file_path: path.to_str().ok_or(Error::InvalidValue)?.to_string(),
alt_text: image
.content()
.and_then(|content| content.to_as_string())
.ok_or(Error::NotFound)?,
is_remote: false,
remote_url: None,
sensitive: summary.is_some(),
content_warning: summary,
owner_id: User::from_id(
conn,
&image
.attributed_to()
.and_then(|attributed_to| attributed_to.to_as_uri())
.ok_or(Error::MissingApProperty)?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?
.id,
},
)
.map_err(|(_, e)| e)?
.id,
},
)
})
}
pub fn get_media_processor<'a>(conn: &'a Connection, user: Vec<&User>) -> MediaProcessor<'a> {
@ -288,7 +328,7 @@ fn determine_mirror_file_path(url: &str) -> PathBuf {
.next()
.map(ToOwned::to_owned)
.unwrap_or_else(|| String::from("png"));
file_path.push(format!("{}.{}", GUID::rand().to_string(), ext));
file_path.push(format!("{}.{}", GUID::rand(), ext));
});
file_path
}
@ -358,7 +398,15 @@ pub(crate) mod tests {
pub(crate) fn clean(conn: &Conn) {
//used to remove files generated by tests
for media in Media::list_all_medias(conn).unwrap() {
media.delete(conn).unwrap();
if let Some(err) = media.delete(conn).err() {
match &err {
Error::Io(e) => match e.kind() {
std::io::ErrorKind::NotFound => (),
_ => panic!("{:?}", err),
},
_ => panic!("{:?}", err),
}
}
}
}

@ -2,7 +2,11 @@ use crate::{
comments::Comment, db_conn::DbConn, notifications::*, posts::Post, schema::mentions,
users::User, Connection, Error, Result,
};
use activitypub::link;
use activitystreams::{
base::BaseExt,
iri_string::types::IriString,
link::{self, LinkExt},
};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::inbox::AsActor;
@ -47,26 +51,28 @@ impl Mention {
pub fn get_user(&self, conn: &Connection) -> Result<User> {
match self.get_post(conn) {
Ok(p) => Ok(p.get_authors(conn)?.into_iter().next()?),
Ok(p) => Ok(p
.get_authors(conn)?
.into_iter()
.next()
.ok_or(Error::NotFound)?),
Err(_) => self.get_comment(conn).and_then(|c| c.get_author(conn)),
}
}
pub fn build_activity(conn: &DbConn, ment: &str) -> Result<link::Mention> {
let user = User::find_by_fqn(conn, ment)?;
let mut mention = link::Mention::default();
mention.link_props.set_href_string(user.ap_url)?;
mention.link_props.set_name_string(format!("@{}", ment))?;
let mut mention = link::Mention::new();
mention.set_href(user.ap_url.parse::<IriString>()?);
mention.set_name(format!("@{}", ment));
Ok(mention)
}
pub fn to_activity(&self, conn: &Connection) -> Result<link::Mention> {
let user = self.get_mentioned(conn)?;
let mut mention = link::Mention::default();
mention.link_props.set_href_string(user.ap_url.clone())?;
mention
.link_props
.set_name_string(format!("@{}", user.fqn))?;
let mut mention = link::Mention::new();
mention.set_href(user.ap_url.parse::<IriString>()?);
mention.set_name(format!("@{}", user.fqn));
Ok(mention)
}
@ -77,8 +83,8 @@ impl Mention {
in_post: bool,
notify: bool,
) -> Result<Self> {
let ap_url = ment.link_props.href_string().ok()?;
let mentioned = User::find_by_ap_url(conn, &ap_url)?;
let ap_url = ment.href().ok_or(Error::NotFound)?.as_str();
let mentioned = User::find_by_ap_url(conn, ap_url)?;
if in_post {
Post::get(conn, inside).and_then(|post| {
@ -141,3 +147,62 @@ impl Mention {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{inbox::tests::fill_database, tests::db, Error};
use assert_json_diff::assert_json_eq;
use diesel::Connection;
use serde_json::{json, to_value};
#[test]
fn build_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (_posts, users, _blogs) = fill_database(&conn);
let user = &users[0];
let name = &user.username;
let act = Mention::build_activity(&conn, name)?;
let expected = json!({
"href": "https://plu.me/@/admin/",
"name": "@admin",
"type": "Mention",
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn to_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (posts, users, _blogs) = fill_database(&conn);
let post = &posts[0];
let user = &users[0];
let mention = Mention::insert(
&conn,
NewMention {
mentioned_id: user.id,
post_id: Some(post.id),
comment_id: None,
},
)?;
let act = mention.to_activity(&conn)?;
let expected = json!({
"href": "https://plu.me/@/admin/",
"name": "@admin",
"type": "Mention",
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
}

@ -105,7 +105,8 @@ impl ImportedMigrations {
pub fn rerun_last_migration(&self, conn: &Connection, path: &Path) -> Result<()> {
let latest_migration = conn.latest_run_migration_version()?;
let id = latest_migration
.and_then(|m| self.0.binary_search_by_key(&m.as_str(), |m| m.name).ok())?;
.and_then(|m| self.0.binary_search_by_key(&m.as_str(), |m| m.name).ok())
.ok_or(Error::NotFound)?;
let migration = &self.0[id];
conn.transaction(|| {
migration.revert(conn, path)?;

@ -61,7 +61,7 @@ impl PasswordResetRequest {
}
pub fn find_and_delete_by_token(conn: &Connection, token: &str) -> Result<Self> {
let request = Self::find_by_token(&conn, &token)?;
let request = Self::find_by_token(conn, token)?;
let filter =
password_reset_requests::table.filter(password_reset_requests::id.eq(request.id));

@ -3,29 +3,31 @@ use crate::{
post_authors::*, safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User,
Connection, Error, PostEvent::*, Result, CONFIG, POST_CHAN,
};
use activitypub::{
use activitystreams::{
activity::{Create, Delete, Update},
link,
object::{Article, Image, Tombstone},
CustomObject,
base::{AnyBase, Base},
iri_string::types::IriString,
link::{self, kind::MentionType},
object::{kind::ImageType, ApObject, Article, AsApObject, Image, ObjectExt, Tombstone},
prelude::*,
time::OffsetDateTime,
};
use chrono::{NaiveDateTime, TimeZone, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use heck::KebabCase;
use chrono::{NaiveDateTime, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl};
use once_cell::sync::Lazy;
use plume_common::{
activity_pub::{
inbox::{AsObject, FromId},
Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILITY,
inbox::{AsActor, AsObject, FromId},
sign::Signer,
Hashtag, HashtagType, Id, IntoId, Licensed, LicensedArticle, ToAsString, ToAsUri,
PUBLIC_VISIBILITY,
},
utils::md_to_html,
utils::{iri_percent_encode_seg, md_to_html},
};
use riker::actors::{Publish, Tell};
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
pub type LicensedArticle = CustomObject<Licensed, Article>;
static BLOG_FQN_CACHE: Lazy<Mutex<HashMap<i32, String>>> = Lazy::new(|| Mutex::new(HashMap::new()));
#[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)]
@ -67,20 +69,15 @@ impl Post {
find_by!(posts, find_by_ap_url, ap_url as &str);
last!(posts);
pub fn insert(conn: &Connection, new: NewPost) -> Result<Self> {
pub fn insert(conn: &Connection, mut new: NewPost) -> Result<Self> {
if new.ap_url.is_empty() {
let blog = Blog::get(conn, new.blog_id)?;
new.ap_url = Self::ap_url(blog, &new.slug);
}
diesel::insert_into(posts::table)
.values(new)
.execute(conn)?;
let mut post = Self::last(conn)?;
if post.ap_url.is_empty() {
post.ap_url = ap_url(&format!(
"{}/~/{}/{}/",
CONFIG.base_url,
post.get_blog(conn)?.fqn,
post.slug
));
let _: Post = post.save_changes(conn)?;
}
let post = Self::last(conn)?;
if post.published {
post.publish_published();
@ -94,13 +91,16 @@ impl Post {
let post = Self::get(conn, self.id)?;
// TODO: Call publish_published() when newly published
if post.published {
self.publish_updated();
let blog = post.get_blog(conn);
if blog.is_ok() && blog.unwrap().is_local() {
self.publish_updated();
}
}
Ok(post)
}
pub fn delete(&self, conn: &Connection) -> Result<()> {
for m in Mention::list_for_post(&conn, self.id)? {
for m in Mention::list_for_post(conn, self.id)? {
m.delete(conn)?;
}
diesel::delete(self).execute(conn)?;
@ -251,6 +251,20 @@ impl Post {
.map_err(Error::from)
}
pub fn ap_url(blog: Blog, slug: &str) -> String {
ap_url(&format!(
"{}/~/{}/{}/",
CONFIG.base_url,
blog.fqn,
iri_percent_encode_seg(slug)
))
}
// It's better to calc slug in insert and update
pub fn slug(title: &str) -> &str {
title
}
pub fn get_authors(&self, conn: &Connection) -> Result<Vec<User>> {
use crate::schema::post_authors;
use crate::schema::users;
@ -341,92 +355,92 @@ impl Post {
.collect::<Vec<serde_json::Value>>();
mentions_json.append(&mut tags_json);
let mut article = Article::default();
article.object_props.set_name_string(self.title.clone())?;
article.object_props.set_id_string(self.ap_url.clone())?;
let mut article = ApObject::new(Article::new());
article.set_name(self.title.clone());
article.set_id(self.ap_url.parse::<IriString>()?);
let mut authors = self
.get_authors(conn)?
.into_iter()
.map(|x| Id::new(x.ap_url))
.collect::<Vec<Id>>();
authors.push(self.get_blog(conn)?.into_id()); // add the blog URL here too
article
.object_props
.set_attributed_to_link_vec::<Id>(authors)?;
article
.object_props
.set_content_string(self.content.get().clone())?;
article.ap_object_props.set_source_object(Source {
content: self.source.clone(),
media_type: String::from("text/markdown"),
})?;
article
.object_props
.set_published_utctime(Utc.from_utc_datetime(&self.creation_date))?;
article
.object_props
.set_summary_string(self.subtitle.clone())?;
article.object_props.tag = Some(json!(mentions_json));
.filter_map(|x| x.ap_url.parse::<IriString>().ok())
.collect::<Vec<IriString>>();
authors.push(self.get_blog(conn)?.ap_url.parse::<IriString>()?); // add the blog URL here too
article.set_many_attributed_tos(authors);
article.set_content(self.content.get().clone());
let source = AnyBase::from_arbitrary_json(serde_json::json!({
"content": self.source,
"mediaType": "text/markdown",
}))?;
article.set_source(source);
article.set_published(
OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos().into())
.expect("OffsetDateTime"),
);
article.set_summary(&*self.subtitle);
article.set_many_tags(
mentions_json
.iter()
.filter_map(|mention_json| AnyBase::from_arbitrary_json(mention_json).ok()),
);
if let Some(media_id) = self.cover_id {
let media = Media::get(conn, media_id)?;
let mut cover = Image::default();
cover.object_props.set_url_string(media.url()?)?;
let mut cover = Image::new();
cover.set_url(media.url()?);
if media.sensitive {
cover
.object_props
.set_summary_string(media.content_warning.unwrap_or_default())?;
cover.set_summary(media.content_warning.unwrap_or_default());
}
cover.object_props.set_content_string(media.alt_text)?;
cover
.object_props
.set_attributed_to_link_vec(vec![User::get(conn, media.owner_id)?.into_id()])?;
article.object_props.set_icon_object(cover)?;
cover.set_content(media.alt_text);
cover.set_many_attributed_tos(vec![User::get(conn, media.owner_id)?
.ap_url
.parse::<IriString>()?]);
article.set_icon(cover.into_any_base()?);
}
article.object_props.set_url_string(self.ap_url.clone())?;
article
.object_props
.set_to_link_vec::<Id>(to.into_iter().map(Id::new).collect())?;
article
.object_props
.set_cc_link_vec::<Id>(cc.into_iter().map(Id::new).collect())?;
let mut license = Licensed::default();
license.set_license_string(self.license.clone())?;
article.set_url(self.ap_url.parse::<IriString>()?);
article.set_many_tos(
to.into_iter()
.filter_map(|to| to.parse::<IriString>().ok())
.collect::<Vec<IriString>>(),
);
article.set_many_ccs(
cc.into_iter()
.filter_map(|cc| cc.parse::<IriString>().ok())
.collect::<Vec<IriString>>(),
);
let license = Licensed {
license: Some(self.license.clone()),
};
Ok(LicensedArticle::new(article, license))
}
pub fn create_activity(&self, conn: &Connection) -> Result<Create> {
let article = self.to_activity(conn)?;
let mut act = Create::default();
act.object_props
.set_id_string(format!("{}activity", self.ap_url))?;
act.object_props
.set_to_link_vec::<Id>(article.object.object_props.to_link_vec()?)?;
act.object_props
.set_cc_link_vec::<Id>(article.object.object_props.cc_link_vec()?)?;
act.create_props
.set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?;
act.create_props.set_object_object(article)?;
let to = article.to().ok_or(Error::MissingApProperty)?.clone();
let cc = article.cc().ok_or(Error::MissingApProperty)?.clone();
let mut act = Create::new(
self.get_authors(conn)?[0].ap_url.parse::<IriString>()?,
Base::retract(article)?.into_generic()?,
);
act.set_id(format!("{}/activity", self.ap_url).parse::<IriString>()?);
act.set_many_tos(to);
act.set_many_ccs(cc);
Ok(act)
}
pub fn update_activity(&self, conn: &Connection) -> Result<Update> {
let article = self.to_activity(conn)?;
let mut act = Update::default();
act.object_props.set_id_string(format!(
"{}/update-{}",
self.ap_url,
Utc::now().timestamp()
))?;
act.object_props
.set_to_link_vec::<Id>(article.object.object_props.to_link_vec()?)?;
act.object_props
.set_cc_link_vec::<Id>(article.object.object_props.cc_link_vec()?)?;
act.update_props
.set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?;
act.update_props.set_object_object(article)?;
let to = article.to().ok_or(Error::MissingApProperty)?.clone();
let cc = article.cc().ok_or(Error::MissingApProperty)?.clone();
let mut act = Update::new(
self.get_authors(conn)?[0].ap_url.parse::<IriString>()?,
Base::retract(article)?.into_generic()?,
);
act.set_id(
format!("{}/update-{}", self.ap_url, Utc::now().timestamp()).parse::<IriString>()?,
);
act.set_many_tos(to);
act.set_many_ccs(cc);
Ok(act)
}
@ -435,31 +449,23 @@ impl Post {
.into_iter()
.map(|m| {
(
m.link_props
.href_string()
.ok()
.and_then(|ap_url| User::find_by_ap_url(conn, &ap_url).ok())
m.href()
.and_then(|ap_url| User::find_by_ap_url(conn, ap_url.as_ref()).ok())
.map(|u| u.id),
m,
)
})
.filter_map(|(id, m)| {
if let Some(id) = id {
Some((m, id))
} else {
None
}
})
.filter_map(|(id, m)| id.map(|id| (m, id)))
.collect::<Vec<_>>();
let old_mentions = Mention::list_for_post(&conn, self.id)?;
let old_mentions = Mention::list_for_post(conn, self.id)?;
let old_user_mentioned = old_mentions
.iter()
.map(|m| m.mentioned_id)
.collect::<HashSet<_>>();
for (m, id) in &mentions {
if !old_user_mentioned.contains(&id) {
Mention::from_activity(&*conn, &m, self.id, true, true)?;
if !old_user_mentioned.contains(id) {
Mention::from_activity(&*conn, m, self.id, true, true)?;
}
}
@ -471,7 +477,7 @@ impl Post {
.iter()
.filter(|m| !new_mentions.contains(&m.mentioned_id))
{
m.delete(&conn)?;
m.delete(conn)?;
}
Ok(())
}
@ -479,7 +485,7 @@ impl Post {
pub fn update_tags(&self, conn: &Connection, tags: Vec<Hashtag>) -> Result<()> {
let tags_name = tags
.iter()
.filter_map(|t| t.name_string().ok())
.filter_map(|t| t.name.as_ref().map(|name| name.as_str().to_string()))
.collect::<HashSet<_>>();
let old_tags = Tag::for_post(&*conn, self.id)?;
@ -496,8 +502,9 @@ impl Post {
for t in tags {
if !t
.name_string()
.map(|n| old_tags_name.contains(&n))
.name
.as_ref()
.map(|n| old_tags_name.contains(n.as_str()))
.unwrap_or(true)
{
Tag::from_activity(conn, &t, self.id, false)?;
@ -515,7 +522,7 @@ impl Post {
pub fn update_hashtags(&self, conn: &Connection, tags: Vec<Hashtag>) -> Result<()> {
let tags_name = tags
.iter()
.filter_map(|t| t.name_string().ok())
.filter_map(|t| t.name.as_ref().map(|name| name.as_str().to_string()))
.collect::<HashSet<_>>();
let old_tags = Tag::for_post(&*conn, self.id)?;
@ -532,8 +539,9 @@ impl Post {
for t in tags {
if !t
.name_string()
.map(|n| old_tags_name.contains(&n))
.name
.as_ref()
.map(|n| old_tags_name.contains(n.as_str()))
.unwrap_or(true)
{
Tag::from_activity(conn, &t, self.id, true)?;
@ -560,18 +568,19 @@ impl Post {
}
pub fn build_delete(&self, conn: &Connection) -> Result<Delete> {
let mut act = Delete::default();
act.delete_props
.set_actor_link(self.get_authors(conn)?[0].clone().into_id())?;
let mut tombstone = Tombstone::default();
tombstone.object_props.set_id_string(self.ap_url.clone())?;
act.delete_props.set_object_object(tombstone)?;
act.object_props
.set_id_string(format!("{}#delete", self.ap_url))?;
act.object_props
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?;
let mut tombstone = Tombstone::new();
tombstone.set_id(self.ap_url.parse()?);
let mut act = Delete::new(
self.get_authors(conn)?[0]
.clone()
.into_id()
.parse::<IriString>()?,
Base::retract(tombstone)?.into_generic()?,
);
act.set_id(format!("{}#delete", self.ap_url).parse()?);
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
Ok(act)
}
@ -615,88 +624,184 @@ impl FromId<DbConn> for Post {
}
fn from_activity(conn: &DbConn, article: LicensedArticle) -> Result<Self> {
let conn = conn;
let license = article.custom_props.license_string().unwrap_or_default();
let article = article.object;
let license = article.ext_one.license.unwrap_or_default();
let article = article.inner;
let (blog, authors) = article
.object_props
.attributed_to_link_vec::<Id>()?
.into_iter()
.ap_object_ref()
.attributed_to()
.ok_or(Error::MissingApProperty)?
.iter()
.fold((None, vec![]), |(blog, mut authors), link| {
let url = link;
match User::from_id(conn, &url, None, CONFIG.proxy()) {
Ok(u) => {
authors.push(u);
(blog, authors)
if let Some(url) = link.id() {
match User::from_id(conn, url.as_str(), None, CONFIG.proxy()) {
Ok(u) => {
authors.push(u);
(blog, authors)
}
Err(_) => (
blog.or_else(|| {
Blog::from_id(conn, url.as_str(), None, CONFIG.proxy()).ok()
}),
authors,
),
}
Err(_) => (
blog.or_else(|| Blog::from_id(conn, &url, None, CONFIG.proxy()).ok()),
authors,
),
} else {
// logically, url possible to be an object without id proprty like {"type":"Person", "name":"Sally"} but we ignore the case
(blog, authors)
}
});
let cover = article
.object_props
.icon_object::<Image>()
let cover = article.icon().and_then(|icon| {
icon.iter().next().and_then(|img| {
let image = img.to_owned().extend::<Image, ImageType>().ok()??;
Media::from_activity(conn, &image).ok().map(|m| m.id)
})
});
let title = article
.name()
.and_then(|name| name.to_as_string())
.ok_or(Error::MissingApProperty)?;
let id = AnyBase::from_extended(article.clone()) // FIXME: Don't clone
.ok()
.and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id));
let title = article.object_props.name_string()?;
// TODO: upsert
let post = Post::insert(
conn,
NewPost {
blog_id: blog?.id,
slug: title.to_kebab_case(),
title,
content: SafeString::new(&article.object_props.content_string()?),
published: true,
license,
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
ap_url: article
.object_props
.url_string()
.or_else(|_| article.object_props.id_string())?,
creation_date: Some(article.object_props.published_utctime()?.naive_utc()),
subtitle: article.object_props.summary_string()?,
source: article.ap_object_props.source_object::<Source>()?.content,
cover_id: cover,
},
)?;
.ok_or(Error::MissingApProperty)?
.id()
.map(|id| id.to_string());
let ap_url = article
.url()
.and_then(|url| url.to_as_uri().or(id))
.ok_or(Error::MissingApProperty)?;
let source = article
.source()
.and_then(|s| {
serde_json::to_value(s).ok().and_then(|obj| {
if !obj.is_object() {
return None;
}
obj.get("content")
.and_then(|content| content.as_str().map(|c| c.to_string()))
})
})
.unwrap_or_default();
let post = Post::from_db(conn, &ap_url)
.and_then(|mut post| {
let mut updated = false;
let slug = Self::slug(&title);
let content = SafeString::new(
&article
.content()
.and_then(|content| content.to_as_string())
.ok_or(Error::MissingApProperty)?,
);
let subtitle = article
.summary()
.and_then(|summary| summary.to_as_string())
.ok_or(Error::MissingApProperty)?;
if post.slug != slug {
post.slug = slug.to_string();
updated = true;
}
if post.title != title {
post.title = title.clone();
updated = true;
}
if post.content != content {
post.content = content;
updated = true;
}
if post.license != license {
post.license = license.clone();
updated = true;
}
if post.subtitle != subtitle {
post.subtitle = subtitle;
updated = true;
}
if post.source != source {
post.source = source.clone();
updated = true;
}
if post.cover_id != cover {
post.cover_id = cover;
updated = true;
}
for author in authors {
PostAuthor::insert(
conn,
NewPostAuthor {
post_id: post.id,
author_id: author.id,
},
)?;
}
if updated {
post.update(conn)?;
}
Ok(post)
})
.or_else(|_| {
Post::insert(
conn,
NewPost {
blog_id: blog.ok_or(Error::NotFound)?.id,
slug: Self::slug(&title).to_string(),
title,
content: SafeString::new(
&article
.content()
.and_then(|content| content.to_as_string())
.ok_or(Error::MissingApProperty)?,
),
published: true,
license,
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
ap_url,
creation_date: article.published().map(|published| {
let timestamp_secs = published.unix_timestamp();
let timestamp_nanos = published.unix_timestamp_nanos()
- (timestamp_secs as i128) * 1000i128 * 1000i128 * 1000i128;
NaiveDateTime::from_timestamp(timestamp_secs, timestamp_nanos as u32)
}),
subtitle: article
.summary()
.and_then(|summary| summary.to_as_string())
.ok_or(Error::MissingApProperty)?,
source,
cover_id: cover,
},
)
.and_then(|post| {
for author in authors {
PostAuthor::insert(
conn,
NewPostAuthor {
post_id: post.id,
author_id: author.id,
},
)?;
}
Ok(post)
})
})?;
// save mentions and tags
let mut hashtags = md_to_html(&post.source, None, false, None)
.2
.into_iter()
.collect::<HashSet<_>>();
if let Some(serde_json::Value::Array(tags)) = article.object_props.tag {
for tag in tags {
serde_json::from_value::<link::Mention>(tag.clone())
.map(|m| Mention::from_activity(conn, &m, post.id, true, true))
if let Some(tags) = article.tag() {
for tag in tags.iter() {
tag.clone()
.extend::<link::Mention, MentionType>() // FIXME: Don't clone
.map(|mention| {
mention.map(|m| Mention::from_activity(conn, &m, post.id, true, true))
})
.ok();
serde_json::from_value::<Hashtag>(tag.clone())
.map_err(Error::from)
.and_then(|t| {
let tag_name = t.name_string()?;
Ok(Tag::from_activity(
conn,
&t,
post.id,
hashtags.remove(&tag_name),
))
tag.clone()
.extend::<Hashtag, HashtagType>() // FIXME: Don't clone
.map(|hashtag| {
hashtag.and_then(|t| {
let tag_name = t.name.clone()?.as_str().to_string();
Tag::from_activity(conn, &t, post.id, hashtags.remove(&tag_name)).ok()
})
})
.ok();
}
@ -706,13 +811,17 @@ impl FromId<DbConn> for Post {
Ok(post)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to get local instance user")
}
}
impl AsObject<User, Create, &DbConn> for Post {
type Error = Error;
type Output = Post;
type Output = Self;
fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result<Post> {
fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result<Self::Output> {
// TODO: check that _actor is actually one of the author?
Ok(self)
}
@ -722,7 +831,7 @@ impl AsObject<User, Delete, &DbConn> for Post {
type Error = Error;
type Output = ();
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<Self::Output> {
let can_delete = self
.get_authors(conn)?
.into_iter()
@ -755,27 +864,58 @@ impl FromId<DbConn> for PostUpdate {
Err(Error::NotFound)
}
fn from_activity(conn: &DbConn, updated: LicensedArticle) -> Result<Self> {
Ok(PostUpdate {
ap_url: updated.object.object_props.id_string()?,
title: updated.object.object_props.name_string().ok(),
subtitle: updated.object.object_props.summary_string().ok(),
content: updated.object.object_props.content_string().ok(),
cover: updated
.object
.object_props
.icon_object::<Image>()
.ok()
.and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id)),
source: updated
.object
.ap_object_props
.source_object::<Source>()
.ok()
.map(|x| x.content),
license: updated.custom_props.license_string().ok(),
tags: updated.object.object_props.tag,
})
fn from_activity(conn: &DbConn, updated: Self::Object) -> Result<Self> {
let mut post_update = PostUpdate {
ap_url: updated
.ap_object_ref()
.id_unchecked()
.ok_or(Error::MissingApProperty)?
.to_string(),
title: updated
.ap_object_ref()
.name()
.and_then(|name| name.to_as_string()),
subtitle: updated
.ap_object_ref()
.summary()
.and_then(|summary| summary.to_as_string()),
content: updated
.ap_object_ref()
.content()
.and_then(|content| content.to_as_string()),
cover: None,
source: updated.source().and_then(|s| {
serde_json::to_value(s).ok().and_then(|obj| {
if !obj.is_object() {
return None;
}
obj.get("content")
.and_then(|content| content.as_str().map(|c| c.to_string()))
})
}),
license: None,
tags: updated
.tag()
.and_then(|tags| serde_json::to_value(tags).ok()),
};
post_update.cover = updated.ap_object_ref().icon().and_then(|img| {
img.iter()
.next()
.and_then(|img| {
img.clone()
.extend::<Image, ImageType>()
.map(|img| img.and_then(|img| Media::from_activity(conn, &img).ok()))
.ok()
})
.and_then(|m| m.map(|m| m.id))
});
post_update.license = updated.ext_one.license;
Ok(post_update)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
@ -793,7 +933,7 @@ impl AsObject<User, Update, &DbConn> for PostUpdate {
}
if let Some(title) = self.title {
post.slug = title.to_kebab_case();
post.slug = Post::slug(&title).to_string();
post.title = title;
}
@ -831,8 +971,12 @@ impl AsObject<User, Update, &DbConn> for PostUpdate {
serde_json::from_value::<Hashtag>(tag.clone())
.map_err(Error::from)
.and_then(|t| {
let tag_name = t.name_string()?;
if txt_hashtags.remove(&tag_name) {
let tag_name = t.name.as_ref().ok_or(Error::MissingApProperty)?;
let tag_name_str = tag_name
.as_xsd_string()
.or_else(|| tag_name.as_rdf_lang_string().map(|rls| &*rls.value))
.ok_or(Error::MissingApProperty)?;
if txt_hashtags.remove(tag_name_str) {
hashtags.push(t);
} else {
tags.push(t);
@ -880,9 +1024,28 @@ impl From<PostEvent> for Arc<Post> {
mod tests {
use super::*;
use crate::inbox::{inbox, tests::fill_database, InboxResult};
use crate::mentions::{Mention, NewMention};
use crate::safe_string::SafeString;
use crate::tests::db;
use crate::tests::{db, format_datetime};
use assert_json_diff::assert_json_eq;
use diesel::Connection;
use serde_json::{json, to_value};
fn prepare_activity(conn: &DbConn) -> (Post, Mention, Vec<Post>, Vec<User>, Vec<Blog>) {
let (posts, users, blogs) = fill_database(conn);
let post = &posts[0];
let mentioned = &users[1];
let mention = Mention::insert(
&conn,
NewMention {
mentioned_id: mentioned.id,
post_id: Some(post.id),
comment_id: None,
},
)
.unwrap();
(post.to_owned(), mention.to_owned(), posts, users, blogs)
}
// creates a post, get it's Create activity, delete the post,
// "send" the Create to the inbox, and check it works
@ -935,45 +1098,177 @@ mod tests {
}
#[test]
fn licensed_article_serde() {
let mut article = Article::default();
article.object_props.set_id_string("Yo".into()).unwrap();
let mut license = Licensed::default();
license.set_license_string("WTFPL".into()).unwrap();
let full_article = LicensedArticle::new(article, license);
let json = serde_json::to_value(full_article).unwrap();
let article_from_json: LicensedArticle = serde_json::from_value(json).unwrap();
assert_eq!(
"Yo",
&article_from_json.object.object_props.id_string().unwrap()
);
assert_eq!(
"WTFPL",
&article_from_json.custom_props.license_string().unwrap()
);
fn to_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn);
let act = post.to_activity(&conn)?;
let expected = json!({
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"],
"cc": [],
"content": "Hello",
"id": "https://plu.me/~/BlogName/testing",
"license": "WTFPL",
"name": "Testing",
"published": format_datetime(&post.creation_date),
"source": {
"content": "Hello",
"mediaType": "text/markdown"
},
"summary": "Bye",
"tag": [
{
"href": "https://plu.me/@/user/",
"name": "@user",
"type": "Mention"
}
],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Article",
"url": "https://plu.me/~/BlogName/testing"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn licensed_article_deserialization() {
let json = 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": [plume_common::activity_pub::PUBLIC_VISIBILITY]
fn create_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn);
let act = post.create_activity(&conn)?;
let expected = json!({
"actor": "https://plu.me/@/admin/",
"cc": [],
"id": "https://plu.me/~/BlogName/testing/activity",
"object": {
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"],
"cc": [],
"content": "Hello",
"id": "https://plu.me/~/BlogName/testing",
"license": "WTFPL",
"name": "Testing",
"published": format_datetime(&post.creation_date),
"source": {
"content": "Hello",
"mediaType": "text/markdown"
},
"summary": "Bye",
"tag": [
{
"href": "https://plu.me/@/user/",
"name": "@user",
"type": "Mention"
}
],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Article",
"url": "https://plu.me/~/BlogName/testing"
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Create"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn update_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn);
let act = post.update_activity(&conn)?;
let expected = json!({
"actor": "https://plu.me/@/admin/",
"cc": [],
"id": "https://plu.me/~/BlogName/testing/update-",
"object": {
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"],
"cc": [],
"content": "Hello",
"id": "https://plu.me/~/BlogName/testing",
"license": "WTFPL",
"name": "Testing",
"published": format_datetime(&post.creation_date),
"source": {
"content": "Hello",
"mediaType": "text/markdown"
},
"summary": "Bye",
"tag": [
{
"href": "https://plu.me/@/user/",
"name": "@user",
"type": "Mention"
}
],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Article",
"url": "https://plu.me/~/BlogName/testing"
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Update"
});
let actual = to_value(act)?;
let id = actual["id"].to_string();
let (id_pre, id_post) = id.rsplit_once("-").unwrap();
assert_eq!(post.ap_url, "https://plu.me/~/BlogName/testing");
assert_eq!(
id_pre,
to_value("\"https://plu.me/~/BlogName/testing/update")
.unwrap()
.as_str()
.unwrap()
);
assert_eq!(id_post.len(), 11);
assert_eq!(
id_post.matches(char::is_numeric).collect::<String>().len(),
10
);
for (key, value) in actual.as_object().unwrap().into_iter() {
if key == "id" {
continue;
}
assert_json_eq!(value, expected.get(key).unwrap());
}
Ok(())
});
}
#[test]
fn build_delete() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn);
let act = post.build_delete(&conn)?;
let expected = json!({
"actor": "https://plu.me/@/admin/",
"id": "https://plu.me/~/BlogName/testing#delete",
"object": {
"id": "https://plu.me/~/BlogName/testing",
"type": "Tombstone"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"type": "Delete"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
let article: LicensedArticle = serde_json::from_value(json).unwrap();
assert_eq!(
"https://plu.me/~/Blog/my-article",
&article.object.object_props.id_string().unwrap()
);
}
}

@ -1,12 +1,16 @@
use crate::{
db_conn::{DbConn, DbPool},
follows,
posts::{LicensedArticle, Post},
posts::Post,
users::{User, UserEvent},
ACTOR_SYS, CONFIG, USER_CHAN,
};
use activitypub::activity::Create;
use plume_common::activity_pub::inbox::FromId;
use activitystreams::{
activity::{ActorAndObjectRef, Create},
base::AnyBase,
object::kind::ArticleType,
};
use plume_common::activity_pub::{inbox::FromId, LicensedArticle};
use riker::actors::{Actor, ActorFactoryArgs, ActorRefFactory, Context, Sender, Subscribe, Tell};
use std::sync::Arc;
use tracing::{error, info, warn};
@ -17,24 +21,22 @@ pub struct RemoteFetchActor {
impl RemoteFetchActor {
pub fn init(conn: DbPool) {
ACTOR_SYS
let actor = ACTOR_SYS
.actor_of_args::<RemoteFetchActor, _>("remote-fetch", conn)
.expect("Failed to initialize remote fetch actor");
}
}
impl Actor for RemoteFetchActor {
type Msg = UserEvent;
fn pre_start(&mut self, ctx: &Context<Self::Msg>) {
USER_CHAN.tell(
Subscribe {
actor: Box::new(ctx.myself()),
actor: Box::new(actor),
topic: "*".into(),
},
None,
)
}
}
impl Actor for RemoteFetchActor {
type Msg = UserEvent;
fn recv(&mut self, _ctx: &Context<Self::Msg>, msg: Self::Msg, _sender: Sender) {
use UserEvent::*;
@ -70,13 +72,17 @@ fn fetch_and_cache_articles(user: &Arc<User>, conn: &DbConn) {
match create_acts {
Ok(create_acts) => {
for create_act in create_acts {
match create_act.create_props.object_object::<LicensedArticle>() {
Ok(article) => {
match create_act.object_field_ref().as_single_base().map(|base| {
let any_base = AnyBase::from_base(base.clone()); // FIXME: Don't clone()
any_base.extend::<LicensedArticle, ArticleType>()
}) {
Some(Ok(Some(article))) => {
Post::from_activity(conn, article)
.expect("Article from remote user couldn't be saved");
info!("Fetched article from remote user");
}
Err(e) => warn!("Error while fetching articles in background: {:?}", e),
Some(Err(e)) => warn!("Error while fetching articles in background: {:?}", e),
_ => warn!("Error while fetching articles in background"),
}
}
}

@ -1,13 +1,19 @@
use crate::{
db_conn::DbConn, notifications::*, posts::Post, schema::reshares, timeline::*, users::User,
Connection, Error, Result, CONFIG,
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::reshares,
timeline::*, users::User, Connection, Error, Result, CONFIG,
};
use activitystreams::{
activity::{ActorAndObjectRef, Announce, Undo},
base::AnyBase,
iri_string::types::IriString,
prelude::*,
};
use activitypub::activity::{Announce, Undo};
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId},
Id, IntoId, PUBLIC_VISIBILITY,
sign::Signer,
PUBLIC_VISIBILITY,
};
#[derive(Clone, Queryable, Identifiable)]
@ -60,16 +66,16 @@ impl Reshare {
}
pub fn to_activity(&self, conn: &Connection) -> Result<Announce> {
let mut act = Announce::default();
act.announce_props
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
act.announce_props
.set_object_link(Post::get(conn, self.post_id)?.into_id())?;
act.object_props.set_id_string(self.ap_url.clone())?;
act.object_props
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
act.object_props
.set_cc_link_vec(vec![Id::new(self.get_user(conn)?.followers_endpoint)])?;
let mut act = Announce::new(
User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?,
Post::get(conn, self.post_id)?.ap_url.parse::<IriString>()?,
);
act.set_id(self.ap_url.parse::<IriString>()?);
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
act.set_many_ccs(vec![self
.get_user(conn)?
.followers_endpoint
.parse::<IriString>()?]);
Ok(act)
}
@ -92,16 +98,16 @@ impl Reshare {
}
pub fn build_undo(&self, conn: &Connection) -> Result<Undo> {
let mut act = Undo::default();
act.undo_props
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
act.undo_props.set_object_object(self.to_activity(conn)?)?;
act.object_props
.set_id_string(format!("{}#delete", self.ap_url))?;
act.object_props
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
act.object_props
.set_cc_link_vec(vec![Id::new(self.get_user(conn)?.followers_endpoint)])?;
let mut act = Undo::new(
User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?,
AnyBase::from_extended(self.to_activity(conn)?)?,
);
act.set_id(format!("{}#delete", self.ap_url).parse::<IriString>()?);
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
act.set_many_ccs(vec![self
.get_user(conn)?
.followers_endpoint
.parse::<IriString>()?]);
Ok(act)
}
@ -142,7 +148,10 @@ impl FromId<DbConn> for Reshare {
NewReshare {
post_id: Post::from_id(
conn,
&act.announce_props.object_link::<Id>()?,
act.object_field_ref()
.as_single_id()
.ok_or(Error::MissingApProperty)?
.as_str(),
None,
CONFIG.proxy(),
)
@ -150,18 +159,28 @@ impl FromId<DbConn> for Reshare {
.id,
user_id: User::from_id(
conn,
&act.announce_props.actor_link::<Id>()?,
act.actor_field_ref()
.as_single_id()
.ok_or(Error::MissingApProperty)?
.as_str(),
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?
.id,
ap_url: act.object_props.id_string()?,
ap_url: act
.id_unchecked()
.ok_or(Error::MissingApProperty)?
.to_string(),
},
)?;
res.notify(conn)?;
Ok(res)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl AsObject<User, Undo, &DbConn> for Reshare {
@ -173,7 +192,7 @@ impl AsObject<User, Undo, &DbConn> for Reshare {
diesel::delete(&self).execute(&**conn)?;
// delete associated notification if any
if let Ok(notif) = Notification::find(&conn, notification_kind::RESHARE, self.id) {
if let Ok(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) {
diesel::delete(&notif).execute(&**conn)?;
}
@ -186,7 +205,7 @@ impl AsObject<User, Undo, &DbConn> for Reshare {
impl NewReshare {
pub fn new(p: &Post, u: &User) -> Self {
let ap_url = format!("{}/reshare/{}", u.ap_url, p.ap_url);
let ap_url = format!("{}reshare/{}", u.ap_url, p.ap_url);
NewReshare {
post_id: p.id,
user_id: u.id,
@ -194,3 +213,67 @@ impl NewReshare {
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::diesel::Connection;
use crate::{inbox::tests::fill_database, tests::db};
use assert_json_diff::assert_json_eq;
use serde_json::{json, to_value};
#[test]
fn to_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (posts, _users, _blogs) = fill_database(&conn);
let post = &posts[0];
let user = &post.get_authors(&conn)?[0];
let reshare = Reshare::insert(&*conn, NewReshare::new(post, user))?;
let act = reshare.to_activity(&conn).unwrap();
let expected = json!({
"actor": "https://plu.me/@/admin/",
"cc": ["https://plu.me/@/admin/followers"],
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing",
"object": "https://plu.me/~/BlogName/testing",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Announce",
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn build_undo() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (posts, _users, _blogs) = fill_database(&conn);
let post = &posts[0];
let user = &post.get_authors(&conn)?[0];
let reshare = Reshare::insert(&*conn, NewReshare::new(post, user))?;
let act = reshare.build_undo(&*conn)?;
let expected = json!({
"actor": "https://plu.me/@/admin/",
"cc": ["https://plu.me/@/admin/followers"],
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing#delete",
"object": {
"actor": "https://plu.me/@/admin/",
"cc": ["https://plu.me/@/admin/followers"],
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing",
"object": "https://plu.me/~/BlogName/testing",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Announce"
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Undo",
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
}

@ -102,7 +102,7 @@ pub struct SafeString {
impl SafeString {
pub fn new(value: &str) -> Self {
SafeString {
value: CLEAN.clean(&value).to_string(),
value: CLEAN.clean(value).to_string(),
}
}
@ -156,7 +156,7 @@ impl<'de> Deserialize<'de> for SafeString {
where
D: Deserializer<'de>,
{
Ok(deserializer.deserialize_string(SafeStringVisitor)?)
deserializer.deserialize_string(SafeStringVisitor)
}
}

@ -73,16 +73,26 @@ table! {
user_id -> Int4,
}
}
table! {
email_blocklist(id){
email_blocklist (id) {
id -> Int4,
email_address -> VarChar,
email_address -> Text,
note -> Text,
notify_user -> Bool,
notification_text -> Text,
}
}
table! {
email_signups (id) {
id -> Int4,
email -> Varchar,
token -> Varchar,
expiration_date -> Timestamp,
}
}
table! {
follows (id) {
id -> Int4,
@ -306,6 +316,8 @@ allow_tables_to_appear_in_same_query!(
blogs,
comments,
comment_seers,
email_blocklist,
email_signups,
follows,
instances,
likes,

@ -13,24 +13,22 @@ pub struct SearchActor {
impl SearchActor {
pub fn init(searcher: Arc<Searcher>, conn: DbPool) {
ACTOR_SYS
let actor = ACTOR_SYS
.actor_of_args::<SearchActor, _>("search", (searcher, conn))
.expect("Failed to initialize searcher actor");
}
}
impl Actor for SearchActor {
type Msg = PostEvent;
fn pre_start(&mut self, ctx: &Context<Self::Msg>) {
POST_CHAN.tell(
Subscribe {
actor: Box::new(ctx.myself()),
actor: Box::new(actor),
topic: "*".into(),
},
None,
)
}
}
impl Actor for SearchActor {
type Msg = PostEvent;
fn recv(&mut self, _ctx: &Context<Self::Msg>, msg: Self::Msg, _sender: Sender) {
use PostEvent::*;

@ -148,7 +148,7 @@ impl PlumeQuery {
/// Parse a query string into this Query
pub fn parse_query(&mut self, query: &str) -> &mut Self {
self.from_str_req(&query.trim())
self.from_str_req(query.trim())
}
/// Convert this Query to a Tantivy Query
@ -360,7 +360,7 @@ impl std::str::FromStr for PlumeQuery {
fn from_str(query: &str) -> Result<PlumeQuery, !> {
let mut res: PlumeQuery = Default::default();
res.from_str_req(&query.trim());
res.from_str_req(query.trim());
Ok(res)
}
}

@ -57,7 +57,7 @@ impl<'a> WhitespaceTokenStream<'a> {
.filter(|&(_, ref c)| c.is_whitespace())
.map(|(offset, _)| offset)
.next()
.unwrap_or_else(|| self.text.len())
.unwrap_or(self.text.len())
}
}

@ -0,0 +1,72 @@
use crate::CONFIG;
use rocket::request::{FromRequest, Outcome, Request};
use std::fmt;
use std::str::FromStr;
pub enum Strategy {
Password,
Email,
}
impl Default for Strategy {
fn default() -> Self {
Self::Password
}
}
impl FromStr for Strategy {
type Err = StrategyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use self::Strategy::*;
match s {
"password" => Ok(Password),
"email" => Ok(Email),
s => Err(StrategyError::Unsupported(s.to_string())),
}
}
}
#[derive(Debug)]
pub enum StrategyError {
Unsupported(String),
}
impl fmt::Display for StrategyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use self::StrategyError::*;
match self {
// FIXME: Calc option strings from enum
Unsupported(s) => write!(f, "Unsupported strategy: {}. Choose password or email", s),
}
}
}
impl std::error::Error for StrategyError {}
pub struct Password();
pub struct Email();
impl<'a, 'r> FromRequest<'a, 'r> for Password {
type Error = ();
fn from_request(_request: &'a Request<'r>) -> Outcome<Self, ()> {
match matches!(CONFIG.signup, Strategy::Password) {
true => Outcome::Success(Self()),
false => Outcome::Forward(()),
}
}
}
impl<'a, 'r> FromRequest<'a, 'r> for Email {
type Error = ();
fn from_request(_request: &'a Request<'r>) -> Outcome<Self, ()> {
match matches!(CONFIG.signup, Strategy::Email) {
true => Outcome::Success(Self()),
false => Outcome::Forward(()),
}
}
}

@ -1,6 +1,7 @@
use crate::{ap_url, instance::Instance, schema::tags, Connection, Error, Result};
use activitystreams::iri_string::types::IriString;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::Hashtag;
use plume_common::activity_pub::{Hashtag, HashtagExt};
#[derive(Clone, Identifiable, Queryable)]
pub struct Tag {
@ -25,13 +26,16 @@ impl Tag {
list_by!(tags, for_post, post_id as i32);
pub fn to_activity(&self) -> Result<Hashtag> {
let mut ht = Hashtag::default();
ht.set_href_string(ap_url(&format!(
"{}/tag/{}",
Instance::get_local()?.public_domain,
self.tag
)))?;
ht.set_name_string(self.tag.clone())?;
let mut ht = Hashtag::new();
ht.set_href(
ap_url(&format!(
"{}/tag/{}",
Instance::get_local()?.public_domain,
self.tag
))
.parse::<IriString>()?,
);
ht.set_name(self.tag.clone());
Ok(ht)
}
@ -44,7 +48,7 @@ impl Tag {
Tag::insert(
conn,
NewTag {
tag: tag.name_string()?,
tag: tag.name().ok_or(Error::MissingApProperty)?.as_str().into(),
is_hashtag,
post_id: post,
},
@ -52,13 +56,16 @@ impl Tag {
}
pub fn build_activity(tag: String) -> Result<Hashtag> {
let mut ht = Hashtag::default();
ht.set_href_string(ap_url(&format!(
"{}/tag/{}",
Instance::get_local()?.public_domain,
tag
)))?;
ht.set_name_string(tag)?;
let mut ht = Hashtag::new();
ht.set_href(
ap_url(&format!(
"{}/tag/{}",
Instance::get_local()?.public_domain,
tag
))
.parse::<IriString>()?,
);
ht.set_name(tag);
Ok(ht)
}
@ -69,3 +76,72 @@ impl Tag {
.map_err(Error::from)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::db;
use crate::{diesel::Connection, inbox::tests::fill_database};
use assert_json_diff::assert_json_eq;
use serde_json::to_value;
#[test]
fn from_activity() {
let conn = &db();
conn.test_transaction::<_, Error, _>(|| {
let (posts, _users, _blogs) = fill_database(conn);
let post_id = posts[0].id;
let mut ht = Hashtag::new();
ht.set_href(ap_url(&format!("https://plu.me/tag/a_tag")).parse::<IriString>()?);
ht.set_name("a_tag".to_string());
let tag = Tag::from_activity(conn, &ht, post_id, true)?;
assert_eq!(&tag.tag, "a_tag");
assert!(tag.is_hashtag);
Ok(())
});
}
#[test]
fn to_activity() {
let conn = &db();
conn.test_transaction::<_, Error, _>(|| {
fill_database(conn);
let tag = Tag {
id: 0,
tag: "a_tag".into(),
is_hashtag: false,
post_id: 0,
};
let act = tag.to_activity()?;
let expected = json!({
"href": "https://plu.me/tag/a_tag",
"name": "a_tag",
"type": "Hashtag"
});
assert_json_eq!(to_value(&act)?, expected);
Ok(())
})
}
#[test]
fn build_activity() {
let conn = &db();
conn.test_transaction::<_, Error, _>(|| {
fill_database(conn);
let act = Tag::build_activity("a_tag".into())?;
let expected = json!({
"href": "https://plu.me/tag/a_tag",
"name": "a_tag",
"type": "Hashtag"
});
assert_json_eq!(to_value(&act)?, expected);
Ok(())
});
}
}

@ -6,6 +6,7 @@ use crate::{
Connection, Error, Result,
};
use diesel::{self, BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl};
use std::cmp::Ordering;
use std::ops::Deref;
pub(crate) mod query;
@ -92,6 +93,16 @@ impl Timeline {
.load::<Self>(conn)
.map_err(Error::from)
}
.map(|mut timelines| {
timelines.sort_by(|t1, t2| {
if t1.user_id.is_some() && t2.user_id.is_none() {
Ordering::Less
} else {
t1.id.cmp(&t2.id)
}
});
timelines
})
}
pub fn new_for_user(
@ -223,6 +234,9 @@ impl Timeline {
}
pub fn add_post(&self, conn: &Connection, post: &Post) -> Result<()> {
if self.includes_post(conn, post)? {
return Ok(());
}
diesel::insert_into(timeline::table)
.values(TimelineEntry {
post_id: post.id,
@ -236,6 +250,16 @@ impl Timeline {
let query = TimelineQuery::parse(&self.query)?;
query.matches(conn, self, post, kind)
}
fn includes_post(&self, conn: &Connection, post: &Post) -> Result<bool> {
diesel::dsl::select(diesel::dsl::exists(
timeline::table
.filter(timeline::timeline_id.eq(self.id))
.filter(timeline::post_id.eq(post.id)),
))
.get_result(conn)
.map_err(Error::from)
}
}
#[cfg(test)]

@ -18,12 +18,6 @@ pub enum QueryError {
RuntimeError(String),
}
impl From<std::option::NoneError> for QueryError {
fn from(_: std::option::NoneError) -> Self {
QueryError::UnexpectedEndOfQuery
}
}
pub type QueryResult<T> = std::result::Result<T, QueryError>;
#[derive(Debug, Clone, Copy, PartialEq)]
@ -239,7 +233,7 @@ impl WithList {
) -> Result<bool> {
match list {
List::List(name) => {
let list = lists::List::find_for_user_by_name(conn, timeline.user_id, &name)?;
let list = lists::List::find_for_user_by_name(conn, timeline.user_id, name)?;
match (self, list.kind()) {
(WithList::Blog, ListType::Blog) => list.contains_blog(conn, post.blog_id),
(WithList::Author { boosts, likes }, ListType::User) => match kind {
@ -414,7 +408,7 @@ enum List<'a> {
fn parse_s<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> {
let mut res = Vec::new();
let (left, token) = parse_a(&stream)?;
let (left, token) = parse_a(stream)?;
res.push(token);
stream = left;
while !stream.is_empty() {
@ -436,7 +430,7 @@ fn parse_s<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
fn parse_a<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> {
let mut res = Vec::new();
let (left, token) = parse_b(&stream)?;
let (left, token) = parse_b(stream)?;
res.push(token);
stream = left;
while !stream.is_empty() {
@ -463,7 +457,7 @@ fn parse_b<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<
match left.get(0) {
Some(Token::RParent(_)) => Ok((&left[1..], token)),
Some(t) => t.get_error(Token::RParent(0)),
None => None?,
None => Err(QueryError::UnexpectedEndOfQuery),
}
}
_ => parse_c(stream),
@ -484,9 +478,13 @@ fn parse_c<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<
}
fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Arg<'a>)> {
match stream.get(0).map(Token::get_text)? {
match stream
.get(0)
.map(Token::get_text)
.ok_or(QueryError::UnexpectedEndOfQuery)?
{
s @ "blog" | s @ "author" | s @ "license" | s @ "tags" | s @ "lang" => {
match stream.get(1)? {
match stream.get(1).ok_or(QueryError::UnexpectedEndOfQuery)? {
Token::Word(_, _, r#in) if r#in == &"in" => {
let (mut left, list) = parse_l(&stream[2..])?;
let kind = match s {
@ -498,7 +496,12 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
if *clude != "include" && *clude != "exclude" {
break;
}
match (*clude, left.get(1).map(Token::get_text)?) {
match (
*clude,
left.get(1)
.map(Token::get_text)
.ok_or(QueryError::UnexpectedEndOfQuery)?,
) {
("include", "reshares") | ("include", "reshare") => {
boosts = true
}
@ -529,7 +532,10 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
t => t.get_error(Token::Word(0, 0, "'in'")),
}
}
s @ "title" | s @ "subtitle" | s @ "content" => match (stream.get(1)?, stream.get(2)?) {
s @ "title" | s @ "subtitle" | s @ "content" => match (
stream.get(1).ok_or(QueryError::UnexpectedEndOfQuery)?,
stream.get(2).ok_or(QueryError::UnexpectedEndOfQuery)?,
) {
(Token::Word(_, _, contains), Token::Word(_, _, w)) if contains == &"contains" => Ok((
&stream[3..],
Arg::Contains(
@ -555,7 +561,13 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
if *clude != "include" && *clude != "exclude" {
break;
}
match (*clude, stream.get(2).map(Token::get_text)?) {
match (
*clude,
stream
.get(2)
.map(Token::get_text)
.ok_or(QueryError::UnexpectedEndOfQuery)?,
) {
("include", "reshares") | ("include", "reshare") => boosts = true,
("exclude", "reshares") | ("exclude", "reshare") => boosts = false,
("include", "likes") | ("include", "like") => likes = true,
@ -577,20 +589,23 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
"all" => Ok((&stream[1..], Arg::Boolean(Bool::All))),
_ => unreachable!(),
},
_ => stream.get(0)?.get_error(Token::Word(
0,
0,
"one of 'blog', 'author', 'license', 'tags', 'lang', \
_ => stream
.get(0)
.ok_or(QueryError::UnexpectedEndOfQuery)?
.get_error(Token::Word(
0,
0,
"one of 'blog', 'author', 'license', 'tags', 'lang', \
'title', 'subtitle', 'content', 'followed', 'has_cover', 'local' or 'all'",
)),
)),
}
}
fn parse_l<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], List<'a>)> {
match stream.get(0)? {
match stream.get(0).ok_or(QueryError::UnexpectedEndOfQuery)? {
Token::LBracket(_) => {
let (left, list) = parse_m(&stream[1..])?;
match left.get(0)? {
match left.get(0).ok_or(QueryError::UnexpectedEndOfQuery)? {
Token::RBracket(_) => Ok((&left[1..], List::Array(list))),
t => t.get_error(Token::Word(0, 0, "one of ']' or ','")),
}
@ -601,17 +616,20 @@ fn parse_l<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Lis
}
fn parse_m<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Vec<&'a str>)> {
let mut res: Vec<&str> = Vec::new();
res.push(match stream.get(0)? {
Token::Word(_, _, w) => w,
t => return t.get_error(Token::Word(0, 0, "any word")),
});
stream = &stream[1..];
while let Token::Comma(_) = stream[0] {
res.push(match stream.get(1)? {
let mut res: Vec<&str> = vec![
match stream.get(0).ok_or(QueryError::UnexpectedEndOfQuery)? {
Token::Word(_, _, w) => w,
t => return t.get_error(Token::Word(0, 0, "any word")),
});
},
];
stream = &stream[1..];
while let Token::Comma(_) = stream[0] {
res.push(
match stream.get(1).ok_or(QueryError::UnexpectedEndOfQuery)? {
Token::Word(_, _, w) => w,
t => return t.get_error(Token::Word(0, 0, "any word")),
},
);
stream = &stream[2..];
}

@ -4,12 +4,15 @@ use crate::{
safe_string::SafeString, schema::users, timeline::Timeline, Connection, Error, Result,
UserEvent::*, CONFIG, ITEMS_PER_PAGE, USER_CHAN,
};
use activitypub::{
use activitystreams::{
activity::Delete,
actor::Person,
actor::{ApActor, AsApActor, Endpoints, Person},
base::{AnyBase, Base},
collection::{OrderedCollection, OrderedCollectionPage},
object::{Image, Tombstone},
Activity, CustomObject, Endpoint,
iri_string::types::IriString,
markers::Activity,
object::{kind::ImageType, AsObject as _, Image, Tombstone},
prelude::*,
};
use chrono::{NaiveDateTime, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl};
@ -22,17 +25,14 @@ use openssl::{
};
use plume_common::{
activity_pub::{
ap_accept_header,
inbox::{AsActor, AsObject, FromId},
sign::{gen_keypair, Signer},
ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY,
request::get,
sign::{gen_keypair, Error as SignError, Result as SignResult, Signer},
ActivityStream, ApSignature, CustomPerson, Id, IntoId, PublicKey, ToAsString, ToAsUri,
PUBLIC_VISIBILITY,
},
utils,
};
use reqwest::{
header::{HeaderValue, ACCEPT},
ClientBuilder,
};
use riker::actors::{Publish, Tell};
use rocket::{
outcome::IntoOutcome,
@ -43,15 +43,13 @@ use std::{
hash::{Hash, Hasher},
sync::Arc,
};
use url::Url;
use webfinger::*;
pub type CustomPerson = CustomObject<ApSignature, Person>;
pub enum Role {
Admin = 0,
Moderator = 1,
Normal = 2,
Instance = 3,
}
#[derive(Queryable, Identifiable, Clone, Debug, AsChangeset)]
@ -78,6 +76,7 @@ pub struct User {
pub summary_html: SafeString,
/// 0 = admin
/// 1 = moderator
/// 3 = local instance
/// anything else = normal user
pub role: i32,
pub preferred_theme: Option<String>,
@ -204,13 +203,35 @@ impl User {
}
}
/**
* TODO: Should create user record with normalized(lowercased) email
*/
pub fn email_used(conn: &DbConn, email: &str) -> Result<bool> {
use diesel::dsl::{exists, select};
select(exists(
users::table
.filter(users::instance_id.eq(Instance::get_local()?.id))
.filter(users::email.eq(email))
.or_filter(users::email.eq(email.to_ascii_lowercase())),
))
.get_result(&**conn)
.map_err(Error::from)
}
fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result<User> {
let link = resolve(acct.to_owned(), true)?
.links
.into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
.ok_or(Error::Webfinger)?;
User::from_id(conn, link.href.as_ref()?, None, CONFIG.proxy()).map_err(|(_, e)| e)
User::from_id(
conn,
link.href.as_ref().ok_or(Error::Webfinger)?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)
}
pub fn fetch_remote_interact_uri(acct: &str) -> Result<String> {
@ -223,25 +244,22 @@ impl User {
}
fn fetch(url: &str) -> Result<CustomPerson> {
let mut res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()?
.get(url)
.header(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
)
.send()?;
let res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?;
let text = &res.text()?;
// without this workaround, publicKey is not correctly deserialized
let ap_sign = serde_json::from_str::<ApSignature>(text)?;
let mut json = serde_json::from_str::<CustomPerson>(text)?;
json.custom_props = ap_sign;
let person = serde_json::from_str::<Person>(text)?;
let json = CustomPerson::new(
ApActor::new(
person
.clone()
.id_unchecked()
.ok_or(Error::MissingApProperty)?
.to_owned(),
person,
),
ap_sign,
); // FIXME: Don't clone()
Ok(json)
}
@ -253,35 +271,56 @@ impl User {
User::fetch(&self.ap_url.clone()).and_then(|json| {
let avatar = Media::save_remote(
conn,
json.object
.object_props
.icon_image()?
.object_props
.url_string()?,
&self,
json.ap_actor_ref()
.icon()
.ok_or(Error::MissingApProperty)? // FIXME: Fails when icon is not set
.iter()
.next()
.and_then(|i| {
i.clone()
.extend::<Image, ImageType>() // FIXME: Don't clone()
.ok()?
.and_then(|url| Some(url.id_unchecked()?.to_string()))
})
.ok_or(Error::MissingApProperty)?,
self,
)
.ok();
let pub_key = &json.ext_one.public_key.public_key_pem;
diesel::update(self)
.set((
users::username.eq(json.object.ap_actor_props.preferred_username_string()?),
users::display_name.eq(json.object.object_props.name_string()?),
users::outbox_url.eq(json.object.ap_actor_props.outbox_string()?),
users::inbox_url.eq(json.object.ap_actor_props.inbox_string()?),
users::username.eq(json
.ap_actor_ref()
.preferred_username()
.ok_or(Error::MissingApProperty)?),
users::display_name.eq(json
.ap_actor_ref()
.name()
.ok_or(Error::MissingApProperty)?
.to_as_string()
.ok_or(Error::MissingApProperty)?),
users::outbox_url.eq(json
.ap_actor_ref()
.outbox()?
.ok_or(Error::MissingApProperty)?
.as_str()),
users::inbox_url.eq(json.ap_actor_ref().inbox()?.as_str()),
users::summary.eq(SafeString::new(
&json
.object
.object_props
.summary_string()
.ap_actor_ref()
.summary()
.and_then(|summary| summary.to_as_string())
.unwrap_or_default(),
)),
users::followers_endpoint.eq(json.object.ap_actor_props.followers_string()?),
users::followers_endpoint.eq(json
.ap_actor_ref()
.followers()?
.ok_or(Error::MissingApProperty)?
.as_str()),
users::avatar_id.eq(avatar.map(|a| a.id)),
users::last_fetched_date.eq(Utc::now().naive_utc()),
users::public_key.eq(json
.custom_props
.public_key_publickey()?
.public_key_pem_string()?),
users::public_key.eq(pub_key),
))
.execute(conn)
.map(|_| ())
@ -422,61 +461,63 @@ impl User {
.map_err(Error::from)
}
pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> {
let mut coll = OrderedCollection::default();
Ok(ActivityStream::new(self.outbox_collection(conn)?))
}
pub fn outbox_collection(&self, conn: &Connection) -> Result<OrderedCollection> {
let mut coll = OrderedCollection::new();
let first = &format!("{}?page=1", &self.outbox_url);
let last = &format!(
"{}?page={}",
&self.outbox_url,
self.get_activities_count(&conn) / i64::from(ITEMS_PER_PAGE) + 1
self.get_activities_count(conn) / i64::from(ITEMS_PER_PAGE) + 1
);
coll.collection_props.set_first_link(Id::new(first))?;
coll.collection_props.set_last_link(Id::new(last))?;
coll.collection_props
.set_total_items_u64(self.get_activities_count(&conn) as u64)?;
Ok(ActivityStream::new(coll))
coll.set_first(first.parse::<IriString>()?);
coll.set_last(last.parse::<IriString>()?);
coll.set_total_items(self.get_activities_count(conn) as u64);
Ok(coll)
}
pub fn outbox_page(
&self,
conn: &Connection,
(min, max): (i32, i32),
) -> Result<ActivityStream<OrderedCollectionPage>> {
Ok(ActivityStream::new(
self.outbox_collection_page(conn, (min, max))?,
))
}
pub fn outbox_collection_page(
&self,
conn: &Connection,
(min, max): (i32, i32),
) -> Result<OrderedCollectionPage> {
let acts = self.get_activities_page(conn, (min, max))?;
let n_acts = self.get_activities_count(&conn);
let mut coll = OrderedCollectionPage::default();
let n_acts = self.get_activities_count(conn);
let mut coll = OrderedCollectionPage::new();
if n_acts - i64::from(min) >= i64::from(ITEMS_PER_PAGE) {
coll.collection_page_props.set_next_link(Id::new(&format!(
"{}?page={}",
&self.outbox_url,
min / ITEMS_PER_PAGE + 2
)))?;
coll.set_next(
format!("{}?page={}", &self.outbox_url, min / ITEMS_PER_PAGE + 2)
.parse::<IriString>()?,
);
}
if min > 0 {
coll.collection_page_props.set_prev_link(Id::new(&format!(
"{}?page={}",
&self.outbox_url,
min / ITEMS_PER_PAGE
)))?;
coll.set_prev(
format!("{}?page={}", &self.outbox_url, min / ITEMS_PER_PAGE)
.parse::<IriString>()?,
);
}
coll.collection_props.items = serde_json::to_value(acts)?;
coll.collection_page_props
.set_part_of_link(Id::new(&self.outbox_url))?;
Ok(ActivityStream::new(coll))
}
fn fetch_outbox_page<T: Activity>(&self, url: &str) -> Result<(Vec<T>, Option<String>)> {
let mut res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()?
.get(url)
.header(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
)
.send()?;
coll.set_many_items(
acts.iter()
.filter_map(|value| AnyBase::from_arbitrary_json(value).ok()),
);
coll.set_part_of(self.outbox_url.parse::<IriString>()?);
Ok(coll)
}
pub fn fetch_outbox_page<T: Activity + serde::de::DeserializeOwned>(
&self,
url: &str,
) -> Result<(Vec<T>, Option<String>)> {
let res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?;
let text = &res.text()?;
let json: serde_json::Value = serde_json::from_str(text)?;
let items = json["items"]
@ -486,27 +527,16 @@ impl User {
.filter_map(|j| serde_json::from_value(j.clone()).ok())
.collect::<Vec<T>>();
let next = match json.get("next") {
Some(x) => Some(x.as_str().unwrap().to_owned()),
None => None,
};
let next = json.get("next").map(|x| x.as_str().unwrap().to_owned());
Ok((items, next))
}
pub fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> {
let mut res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()?
.get(&self.outbox_url[..])
.header(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
)
.send()?;
pub fn fetch_outbox<T: Activity + serde::de::DeserializeOwned>(&self) -> Result<Vec<T>> {
let res = get(
&self.outbox_url[..],
Self::get_sender(),
CONFIG.proxy().cloned(),
)?;
let text = &res.text()?;
let json: serde_json::Value = serde_json::from_str(text)?;
if let Some(first) = json.get("first") {
@ -516,7 +546,7 @@ impl User {
if page.is_empty() {
break;
}
items.extend(page.drain(..));
items.append(&mut page);
if let Some(n) = nxt {
if n == next {
break;
@ -538,20 +568,11 @@ impl User {
}
pub fn fetch_followers_ids(&self) -> Result<Vec<String>> {
let mut res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()?
.get(&self.followers_endpoint[..])
.header(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
)
.send()?;
let res = get(
&self.followers_endpoint[..],
Self::get_sender(),
CONFIG.proxy().cloned(),
)?;
let text = &res.text()?;
let json: serde_json::Value = serde_json::from_str(text)?;
Ok(json["items"]
@ -723,7 +744,7 @@ impl User {
pub fn get_keypair(&self) -> Result<PKey<Private>> {
PKey::from_rsa(Rsa::private_key_from_pem(
self.private_key.clone()?.as_ref(),
self.private_key.clone().ok_or(Error::Signature)?.as_ref(),
)?)
.map_err(Error::from)
}
@ -755,71 +776,58 @@ impl User {
}
pub fn to_activity(&self, conn: &Connection) -> Result<CustomPerson> {
let mut actor = Person::default();
actor.object_props.set_id_string(self.ap_url.clone())?;
actor
.object_props
.set_name_string(self.display_name.clone())?;
actor
.object_props
.set_summary_string(self.summary_html.get().clone())?;
actor.object_props.set_url_string(self.ap_url.clone())?;
actor
.ap_actor_props
.set_inbox_string(self.inbox_url.clone())?;
actor
.ap_actor_props
.set_outbox_string(self.outbox_url.clone())?;
actor
.ap_actor_props
.set_preferred_username_string(self.username.clone())?;
actor
.ap_actor_props
.set_followers_string(self.followers_endpoint.clone())?;
let mut actor = ApActor::new(self.inbox_url.parse()?, Person::new());
let ap_url = self.ap_url.parse::<IriString>()?;
actor.set_id(ap_url.clone());
actor.set_name(self.display_name.clone());
actor.set_summary(self.summary_html.get().clone());
actor.set_url(ap_url.clone());
actor.set_inbox(self.inbox_url.parse()?);
actor.set_outbox(self.outbox_url.parse()?);
actor.set_preferred_username(self.username.clone());
actor.set_followers(self.followers_endpoint.parse()?);
if let Some(shared_inbox_url) = self.shared_inbox_url.clone() {
let mut endpoints = Endpoint::default();
endpoints.set_shared_inbox_string(shared_inbox_url)?;
actor.ap_actor_props.set_endpoints_endpoint(endpoints)?;
let endpoints = Endpoints {
shared_inbox: Some(shared_inbox_url.parse::<IriString>()?),
..Endpoints::default()
};
actor.set_endpoints(endpoints);
}
let mut public_key = PublicKey::default();
public_key.set_id_string(format!("{}#main-key", self.ap_url))?;
public_key.set_owner_string(self.ap_url.clone())?;
public_key.set_public_key_pem_string(self.public_key.clone())?;
let mut ap_signature = ApSignature::default();
ap_signature.set_public_key_publickey(public_key)?;
let mut avatar = Image::default();
avatar.object_props.set_url_string(
self.avatar_id
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
.unwrap_or_default(),
)?;
actor.object_props.set_icon_object(avatar)?;
let pub_key = PublicKey {
id: format!("{}#main-key", self.ap_url).parse()?,
owner: ap_url,
public_key_pem: self.public_key.clone(),
};
let ap_signature = ApSignature {
public_key: pub_key,
};
if let Some(avatar_id) = self.avatar_id {
let mut avatar = Image::new();
avatar.set_url(Media::get(conn, avatar_id)?.url()?.parse::<IriString>()?);
actor.set_icon(avatar.into_any_base()?);
}
Ok(CustomPerson::new(actor, ap_signature))
}
pub fn delete_activity(&self, conn: &Connection) -> Result<Delete> {
let mut del = Delete::default();
let mut tombstone = Tombstone::default();
tombstone.object_props.set_id_string(self.ap_url.clone())?;
del.delete_props
.set_actor_link(Id::new(self.ap_url.clone()))?;
del.delete_props.set_object_object(tombstone)?;
del.object_props
.set_id_string(format!("{}#delete", self.ap_url))?;
del.object_props
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?;
del.object_props.set_cc_link_vec(
let mut tombstone = Tombstone::new();
tombstone.set_id(self.ap_url.parse()?);
let mut del = Delete::new(
self.ap_url.parse::<IriString>()?,
Base::retract(tombstone)?.into_generic()?,
);
del.set_id(format!("{}#delete", self.ap_url).parse()?);
del.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
del.set_many_ccs(
self.get_followers(conn)?
.into_iter()
.map(|f| Id::new(f.ap_url))
.collect(),
)?;
.filter_map(|f| f.ap_url.parse::<IriString>().ok()),
);
Ok(del)
}
@ -945,9 +953,60 @@ impl FromId<DbConn> for User {
}
fn from_activity(conn: &DbConn, acct: CustomPerson) -> Result<Self> {
let url = Url::parse(&acct.object.object_props.id_string()?)?;
let inst = url.host_str()?;
let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
let actor = acct.ap_actor_ref();
let username = actor
.preferred_username()
.ok_or(Error::MissingApProperty)?
.to_string();
if username.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
return Err(Error::InvalidValue);
}
let summary = acct
.object_ref()
.summary()
.and_then(|prop| prop.to_as_string())
.unwrap_or_default();
let mut new_user = NewUser {
display_name: acct
.object_ref()
.name()
.and_then(|prop| prop.to_as_string())
.unwrap_or_else(|| username.clone()),
username: username.clone(),
outbox_url: actor.outbox()?.ok_or(Error::MissingApProperty)?.to_string(),
inbox_url: actor.inbox()?.to_string(),
role: 2,
summary_html: SafeString::new(&summary),
summary,
public_key: acct.ext_one.public_key.public_key_pem.to_string(),
shared_inbox_url: actor
.endpoints()?
.and_then(|e| e.shared_inbox.map(|inbox| inbox.to_string())),
followers_endpoint: actor
.followers()?
.ok_or(Error::MissingApProperty)?
.to_string(),
..NewUser::default()
};
let avatar_id = acct.object_ref().icon().and_then(|icon| icon.to_as_uri());
let (ap_url, inst) = {
let any_base = acct.into_any_base()?;
let id = any_base.id().ok_or(Error::MissingApProperty)?;
(
id.to_string(),
id.authority_components()
.ok_or(Error::Url)?
.host()
.to_string(),
)
};
new_user.ap_url = ap_url;
let instance = Instance::find_by_domain(conn, &inst).or_else(|_| {
Instance::insert(
conn,
NewInstance {
@ -964,76 +1023,30 @@ impl FromId<DbConn> for User {
},
)
})?;
let username = acct.object.ap_actor_props.preferred_username_string()?;
if username.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
return Err(Error::InvalidValue);
}
let fqn = if instance.local {
username.clone()
new_user.instance_id = instance.id;
new_user.fqn = if instance.local {
username
} else {
format!("{}@{}", username, instance.public_domain)
};
let user = User::insert(
conn,
NewUser {
display_name: acct
.object
.object_props
.name_string()
.unwrap_or_else(|_| username.clone()),
username,
outbox_url: acct.object.ap_actor_props.outbox_string()?,
inbox_url: acct.object.ap_actor_props.inbox_string()?,
role: 2,
summary: acct
.object
.object_props
.summary_string()
.unwrap_or_default(),
summary_html: SafeString::new(
&acct
.object
.object_props
.summary_string()
.unwrap_or_default(),
),
email: None,
hashed_password: None,
instance_id: instance.id,
ap_url: acct.object.object_props.id_string()?,
public_key: acct
.custom_props
.public_key_publickey()?
.public_key_pem_string()?,
private_key: None,
shared_inbox_url: acct
.object
.ap_actor_props
.endpoints_endpoint()
.and_then(|e| e.shared_inbox_string())
.ok(),
followers_endpoint: acct.object.ap_actor_props.followers_string()?,
fqn,
avatar_id: None,
},
)?;
let user = User::insert(conn, new_user)?;
if let Some(avatar_id) = avatar_id {
let avatar = Media::save_remote(conn, avatar_id, &user);
if let Ok(icon) = acct.object.object_props.icon_image() {
if let Ok(url) = icon.object_props.url_string() {
let avatar = Media::save_remote(conn, url, &user);
if let Ok(avatar) = avatar {
user.set_avatar(conn, avatar.id)?;
if let Ok(avatar) = avatar {
if let Err(e) = user.set_avatar(conn, avatar.id) {
tracing::error!("{:?}", e);
}
}
}
Ok(user)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
}
impl AsActor<&DbConn> for User {
@ -1066,24 +1079,22 @@ impl AsObject<User, Delete, &DbConn> for User {
}
impl Signer for User {
type Error = Error;
fn get_key_id(&self) -> String {
format!("{}#main-key", self.ap_url)
}
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
let key = self.get_keypair()?;
fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> {
let key = self.get_keypair().map_err(|_| SignError())?;
let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?;
signer.update(to_sign.as_bytes())?;
signer.sign_to_vec().map_err(Error::from)
signer.sign_to_vec().map_err(SignError::from)
}
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
fn verify(&self, data: &str, signature: &[u8]) -> SignResult<bool> {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?;
verifier.update(data.as_bytes())?;
verifier.verify(&signature).map_err(Error::from)
verifier.verify(signature).map_err(SignError::from)
}
}
@ -1124,7 +1135,7 @@ impl NewUser {
display_name,
role: role as i32,
summary: summary.to_owned(),
summary_html: SafeString::new(&utils::md_to_html(&summary, None, false, None).0),
summary_html: SafeString::new(&utils::md_to_html(summary, None, false, None).0),
email: Some(email),
hashed_password: password,
instance_id: instance.id,
@ -1167,10 +1178,13 @@ pub(crate) mod tests {
use super::*;
use crate::{
instance::{tests as instance_tests, Instance},
medias::{Media, NewMedia},
tests::db,
Connection as Conn,
Connection as Conn, ITEMS_PER_PAGE,
};
use diesel::Connection;
use assert_json_diff::assert_json_eq;
use diesel::{Connection, SaveChangesDsl};
use serde_json::to_value;
pub(crate) fn fill_database(conn: &Conn) -> Vec<User> {
instance_tests::fill_database(conn);
@ -1194,7 +1208,7 @@ pub(crate) mod tests {
Some("invalid_user_password".to_owned()),
)
.unwrap();
let other = NewUser::new_local(
let mut other = NewUser::new_local(
conn,
"other".to_owned(),
"Another user".to_owned(),
@ -1204,9 +1218,73 @@ pub(crate) mod tests {
Some("invalid_other_password".to_owned()),
)
.unwrap();
let avatar = Media::insert(
conn,
NewMedia {
file_path: "static/media/example.png".into(),
alt_text: "Another user".into(),
is_remote: false,
remote_url: None,
sensitive: false,
content_warning: None,
owner_id: other.id,
},
)
.unwrap();
other.avatar_id = Some(avatar.id);
let other = other.save_changes::<User>(&*conn).unwrap();
vec![admin, user, other]
}
fn fill_pages(
conn: &DbConn,
) -> (
Vec<crate::posts::Post>,
Vec<crate::users::User>,
Vec<crate::blogs::Blog>,
) {
use crate::post_authors::NewPostAuthor;
use crate::posts::NewPost;
let (mut posts, users, blogs) = crate::inbox::tests::fill_database(conn);
let user = &users[0];
let blog = &blogs[0];
for i in 1..(ITEMS_PER_PAGE * 4 + 3) {
let title = format!("Post {}", i);
let content = format!("Content for post {}.", i);
let post = Post::insert(
conn,
NewPost {
blog_id: blog.id,
slug: title.clone(),
title: title.clone(),
content: SafeString::new(&content),
published: true,
license: "CC-0".into(),
creation_date: None,
ap_url: format!("{}/{}", blog.ap_url, title),
subtitle: "".into(),
source: content,
cover_id: None,
},
)
.unwrap();
PostAuthor::insert(
conn,
NewPostAuthor {
post_id: post.id,
author_id: user.id,
},
)
.unwrap();
posts.push(post);
}
(posts, users, blogs)
}
#[test]
fn find_by() {
let conn = db();
@ -1367,4 +1445,134 @@ pub(crate) mod tests {
Ok(())
});
}
#[test]
fn to_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let users = fill_database(&conn);
let user = &users[0];
let act = user.to_activity(&conn)?;
let expected = json!({
"endpoints": {
"sharedInbox": "https://plu.me/inbox"
},
"followers": "https://plu.me/@/admin/followers",
"id": "https://plu.me/@/admin/",
"inbox": "https://plu.me/@/admin/inbox",
"name": "The admin",
"outbox": "https://plu.me/@/admin/outbox",
"preferredUsername": "admin",
"publicKey": {
"id": "https://plu.me/@/admin/#main-key",
"owner": "https://plu.me/@/admin/",
"publicKeyPem": user.public_key,
},
"summary": "<p dir=\"auto\">Hello there, Im the admin</p>\n",
"type": "Person",
"url": "https://plu.me/@/admin/"
});
assert_json_eq!(to_value(act)?, expected);
let other = &users[2];
let other_act = other.to_activity(&conn)?;
let expected_other = json!({
"endpoints": {
"sharedInbox": "https://plu.me/inbox"
},
"followers": "https://plu.me/@/other/followers",
"icon": {
"url": "https://plu.me/static/media/example.png",
"type": "Image",
},
"id": "https://plu.me/@/other/",
"inbox": "https://plu.me/@/other/inbox",
"name": "Another user",
"outbox": "https://plu.me/@/other/outbox",
"preferredUsername": "other",
"publicKey": {
"id": "https://plu.me/@/other/#main-key",
"owner": "https://plu.me/@/other/",
"publicKeyPem": other.public_key,
},
"summary": "<p dir=\"auto\">Hello there, Im someone else</p>\n",
"type": "Person",
"url": "https://plu.me/@/other/"
});
assert_json_eq!(to_value(other_act)?, expected_other);
Ok(())
});
}
#[test]
fn delete_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let users = fill_database(&conn);
let user = &users[1];
let act = user.delete_activity(&conn)?;
let expected = json!({
"actor": "https://plu.me/@/user/",
"cc": [],
"id": "https://plu.me/@/user/#delete",
"object": {
"id": "https://plu.me/@/user/",
"type": "Tombstone",
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Delete",
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn outbox_collection() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (_pages, users, _blogs) = fill_pages(&conn);
let user = &users[0];
let act = user.outbox_collection(&conn)?;
let expected = json!({
"first": "https://plu.me/@/admin/outbox?page=1",
"last": "https://plu.me/@/admin/outbox?page=5",
"totalItems": 51,
"type": "OrderedCollection",
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn outbox_collection_page() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let users = fill_database(&conn);
let user = &users[0];
let act = user.outbox_collection_page(&conn, (33, 36))?;
let expected = json!({
"items": [],
"partOf": "https://plu.me/@/admin/outbox",
"prev": "https://plu.me/@/admin/outbox?page=2",
"type": "OrderedCollectionPage",
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
}

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Afrikaans\n"
"Language: af_ZA\n"
@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr ""
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr ""

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Arabic\n"
"Language: ar_SA\n"
@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "فتح محرر النصوص الغني"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "العنوان"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "العنوان الثانوي أو الملخص"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "اكتب مقالك هنا. ماركداون مُدَعَّم."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "يتبقا {} حرفا تقريبا"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "الوسوم"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "الرخصة"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "الغلاف"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "هذه مسودة"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "نشر كتابا"

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Bulgarian\n"
"Language: bg_BG\n"
@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
msgstr "Искате ли да активирате локално автоматично запаметяване, последно редактирано в {}?"
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Отворете редактора с богат текст"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Заглавие"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Подзаглавие или резюме"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Напишете статията си тук. Поддържа се Markdown."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Остават {} знака вляво"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Етикети"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Лиценз"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Основно изображение"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Това е проект"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Публикувай"

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Catalan\n"
"Language: ca_ES\n"
@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Obre leditor de text enriquit"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Títol"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Subtítol o resum"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Escriviu el vostre article ací. Podeu fer servir el Markdown."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Queden uns {} caràcters"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Etiquetes"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Llicència"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Coberta"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Açò és un esborrany"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Publica"

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-05-09 09:58\n"
"Last-Translator: \n"
"Language-Team: Czech\n"
"Language: cs_CZ\n"
@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Otevřít editor formátovaného textu"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Nadpis"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Podnadpis, nebo shrnutí"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Sem napište svůj článek. Markdown je podporován."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Zbývá kolem {} znaků"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Tagy"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Licence"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Titulka"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Tohle je koncept"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Zveřejnit"

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Danish\n"
"Language: da_DK\n"
@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr ""
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr ""

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-26 13:16\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
msgstr "Möchten Sie die lokale automatische Speicherung laden, die zuletzt um {} bearbeitet wurde?"
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr " Rich Text Editor (RTE) öffnen"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Titel"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Untertitel oder Zusammenfassung"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Schreiben deinen Artikel hier. Markdown wird unterstützt."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Ungefähr {} Zeichen übrig"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Schlagwörter"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Lizenz"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Einband"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Dies ist ein Entwurf"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Veröffentlichen"

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Greek\n"
"Language: el_GR\n"
@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr ""
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr ""

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr ""
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr ""

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Esperanto\n"
"Language: eo_UY\n"
@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Malfermi la riĉan redaktilon"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Titolo"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Verku vian artikolon ĉi tie. Markdown estas subtenita."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Proksimume {} signoj restantaj"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Etikedoj"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Permesilo"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Kovro"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Malfinias"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Eldoni"

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:56\n"
"PO-Revision-Date: 2022-01-26 13:16\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
msgstr "¿Quieres cargar el guardado automático local editado por última vez en {}?"
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Abrir el editor de texto enriquecido"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Título"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Subtítulo, o resumen"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Escriba su artículo aquí. Puede utilizar Markdown."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Quedan unos {} caracteres"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Etiquetas"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Licencia"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Cubierta"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Esto es un borrador"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Publicar"

@ -0,0 +1,63 @@
msgid ""
msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-05-09 09:58\n"
"Last-Translator: \n"
"Language-Team: Basque\n"
"Language: eu_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: eu\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr "{}(t)an automatikoki gordetako azken kopia lokala kargatu nahi al duzu?"
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Ireki testu-formatutzaile aberatsa"
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Izenburua"
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Azpititulua edo laburpena"
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Idatzi hemen testua. Markdown erabil dezakezu."
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "%{count} karaktere geratzen dira"
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Etiketak"
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Lizentzia"
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Azala"
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Zirriborro bat da"
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Argitaratu"

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:56\n"
"PO-Revision-Date: 2022-05-10 17:54\n"
"Last-Translator: \n"
"Language-Team: Persian\n"
"Language: fa_IR\n"
@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
msgstr "آیا می‌خواهید نسخهٔ ذخیره شدهٔ خودکار محلّی از آخرین ویرایش در {} را بار کنید؟"
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "باز کردن ویرایش‌گر غنی"
msgstr "گشودن ویرایشگر غنی"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "عنوان"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "زیرعنوان، یا چکیده"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "مقاله را اینجا بنویسید. از مارک‌داون پشتیبانی می‌شود."
msgstr "مقاله‌تان را اینجا بنویسید. از مارک‌داون پشتیبانی می‌شود."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "نزدیک به {} حرف باقی مانده است"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "برچسب‌ها"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "پروانه"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "تصویر شاخص"
msgstr "جلد"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "این، یک پیش‌نویس است"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "انتشار"

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Finnish\n"
"Language: fi_FI\n"
@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Avaa edistynyt tekstieditori"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Otsikko"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Alaotsikko tai tiivistelmä"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Kirjoita artikkelisi tähän. Markdown -kuvauskieli on tuettu."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "%{count} merkkiä jäljellä"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Tagit"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Lisenssi"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Kansi"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Tämä on luonnos"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Julkaise"

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-05-09 09:59\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
msgstr "Voulez vous charger la sauvegarde automatique locale, éditée la dernière fois à {}?"
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Ouvrir l'éditeur de texte avancé"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Titre"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Sous-titre ou résumé"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Écrivez votre article ici. Vous pouvez utiliser du Markdown."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Environ {} caractères restant"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Étiquettes"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Licence"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Illustration"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Ceci est un brouillon"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Publier"

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-26 13:16\n"
"Last-Translator: \n"
"Language-Team: Galician\n"
"Language: gl_ES\n"
@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
msgstr "Queres cargar a última copia gardada editada o {}?"
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Abre o editor de texto enriquecido"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Título"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Subtítulo, ou resumo"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Escribe aquí o teu artigo: podes utilizar Markdown."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Dispós de {} caracteres"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Etiquetas"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Licenza"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Portada"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Este é un borrador"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Publicar"

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Hebrew\n"
"Language: he_IL\n"
@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr ""
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr ""

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Hindi\n"
"Language: hi_IN\n"
@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "शीर्षक"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "अपना आर्टिकल या लेख यहाँ लिखें. Markdown उपलब्ध है."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "लगभग {} अक्षर बाकी हैं"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "टैग्स"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "लाइसेंस"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "पब्लिश करें"

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Croatian\n"
"Language: hr_HR\n"
@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Naslov"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Tagovi"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Licenca"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Objavi"

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Hungarian\n"
"Language: hu_HU\n"
@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr ""
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr ""
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr ""
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr ""
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr ""
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr ""
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr ""
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr ""
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr ""
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr ""

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2020-12-19 09:55\n"
"PO-Revision-Date: 2022-01-12 01:20\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:189
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
# plume-front/src/editor.rs:282
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Apri il compositore di testo avanzato"
# plume-front/src/editor.rs:315
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Titolo"
# plume-front/src/editor.rs:319
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Sottotitolo, o sommario"
# plume-front/src/editor.rs:326
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Scrivi qui il tuo articolo. È supportato il Markdown."
# plume-front/src/editor.rs:337
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "Circa {} caratteri rimasti"
# plume-front/src/editor.rs:414
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Etichette"
# plume-front/src/editor.rs:415
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Licenza"
# plume-front/src/editor.rs:418
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Copertina"
# plume-front/src/editor.rs:438
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Questa è una bozza"
# plume-front/src/editor.rs:445
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Pubblica"

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

Loading…
Cancel
Save