Compare commits

...

189 Commits

Author SHA1 Message Date
Kitaiti Makoto 9bd8f5272f Define From<PreferredUsernameError> for Error 2023-01-15 08:57:08 +09:00
Kitaiti Makoto 39cd4f830d Remove anyhow from plume-common dependencies 2023-01-15 08:52:18 +09:00
Kitaiti Makoto cd9cb311c7 Define PreferredUsernameError and use it 2023-01-15 08:51:38 +09:00
Kitaiti Makoto 83ed168f9c Add thiserror to plume-common dependencies 2023-01-15 08:38:55 +09:00
Kitaiti Makoto 83c628d490 Remove heck from plume-common dependencies 2023-01-15 08:14:58 +09:00
Kitaiti Makoto badff3f3cb Remove unused make_fqn() 2023-01-15 08:14:58 +09:00
Kitaiti Makoto ba00e36884 use Fqn::make_local() instead of make_fqn() 2023-01-15 08:14:58 +09:00
Kitaiti Makoto 5ee84427bf Add functions to make FQN to Fqn 2023-01-15 08:14:58 +09:00
Kitaiti Makoto f203dddae5 Add heck to plume-models dependencies 2023-01-15 08:14:58 +09:00
Kitaiti Makoto ba1eac9482 Add test for blog title 2023-01-15 08:14:56 +09:00
Kitaiti Makoto 3dad83b179 Set up Rocket for testing environment 2023-01-15 06:58:01 +09:00
Kitaiti Makoto 4eab51b159 Make Config a argument for init_rocket() 2023-01-15 06:20:54 +09:00
Kitaiti Makoto abf0b28fd4 Move set up from init_rocket() to main() 2023-01-15 06:07:24 +09:00
Kitaiti Makoto 115b5b31a4 Follow Blog's API change 2023-01-14 03:40:25 +09:00
Kitaiti Makoto 5a03fd7340 Make Blog::fqn Fqn 2023-01-14 03:23:45 +09:00
Kitaiti Makoto e75449410f Add test for Fqn 2023-01-14 03:22:42 +09:00
Kitaiti Makoto c9bb31b8f5 Format 2023-01-14 03:22:21 +09:00
Kitaiti Makoto 0c2eaf0f1b Define Fqn struct 2023-01-14 03:22:18 +09:00
Kitaiti Makoto 71824aa524 Define PreferredUsername struct 2023-01-13 20:19:36 +09:00
Kitaiti Makoto fc848a8d53 Allow ASCII and numeric only for fqn 2023-01-10 23:49:37 +09:00
Kitaiti Makoto 53cdd8198b Add anyhow to dependencies 2023-01-09 19:42:54 +09:00
Kitaiti Makoto 08f4dac3d3 Set blog title in test fixture 2023-01-09 19:14:20 +09:00
Kitaiti Makoto 18a9ed5504 Extract setup() and teardown() from blogs test 2023-01-09 17:46:54 +09:00
Kitaiti Makoto 631359c3f7 Create local instance in create_models() function 2023-01-09 17:05:55 +09:00
Kitaiti Makoto 3111fa0735 Run migration 2023-01-09 16:19:30 +09:00
Kitaiti Makoto 890c9a0da4 Add migration for SQLite 2023-01-09 16:19:05 +09:00
Kitaiti Makoto 22b03710be Implement migration for PostgreSQL 2023-01-09 16:13:12 +09:00
Kitaiti Makoto e3609f7863 Generate migration to add unique constraint to ActivityPub related fields
% diesel migration generate add_unique_constraint_to_activity_pub_related_fields
2023-01-09 16:10:23 +09:00
Kitaiti Makoto 0714d2d010 Use User.fqn for user activity for consistency with blog 2023-01-09 15:39:58 +09:00
Kitaiti Makoto 5bd084eff7 Make test data follow blog test data change 2023-01-09 07:29:01 +09:00
Kitaiti Makoto f369fa9b25 Fix blog title conversion for ActivityPub 2023-01-09 07:22:17 +09:00
Kitaiti Makoto 8afcc1511e Define make_fqn() 2023-01-09 06:55:49 +09:00
Kitaiti Makoto ce89faef84 Install heck 2023-01-09 06:54:38 +09:00
Kitaiti Makoto e18b6e78f2 Add heck to plume-common's dependencies 2023-01-09 06:54:30 +09:00
Kitaiti Makoto 31e817385d Percent-encode slug in Blog::slug() 2023-01-09 06:18:58 +09:00
Kitaiti Makoto af7ed450e2 Fix iri_percent_encode_seg() to encode some missing characters 2023-01-09 06:15:29 +09:00
Kitaiti Makoto 55a5a64b1a Fix valid slug spec 2023-01-09 06:15:29 +09:00
Kitaiti Makoto 08b7d100fd Change blog title specification 2023-01-09 06:15:23 +09:00
KitaitiMakoto ae7bf2e132 Merge pull request 'Update crates' (#1134) from update-crates into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1134
2023-01-06 14:57:17 +00:00
Kitaiti Makoto 0020242571 Format 2023-01-06 23:40:05 +09:00
Kitaiti Makoto 4f796e788c Clippy 2023-01-06 23:38:38 +09:00
Kitaiti Makoto 3d192c1179 Merge remote-tracking branches 'github/dependabot/cargo/whatlang-0.16.2', 'github/dependabot/cargo/ldap3-0.11.1' and 'github/dependabot/cargo/futures-0.3.25' into update-crates 2023-01-06 23:12:47 +09:00
dependabot[bot] 2f8d188d59 Bump futures from 0.3.21 to 0.3.25
Bumps [futures](https://github.com/rust-lang/futures-rs) from 0.3.21 to 0.3.25.
- [Release notes](https://github.com/rust-lang/futures-rs/releases)
- [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.21...0.3.25)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-05 19:07:12 +00:00
dependabot[bot] 19766662f1 Bump ldap3 from 0.10.6 to 0.11.1
Bumps [ldap3](https://github.com/inejge/ldap3) from 0.10.6 to 0.11.1.
- [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.6...v0.11.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-05 19:06:52 +00:00
dependabot[bot] 301aad3f73 Bump whatlang from 0.16.0 to 0.16.2
Bumps [whatlang](https://github.com/greyblake/whatlang-rs) from 0.16.0 to 0.16.2.
- [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.16.0...v0.16.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-05 19:06:39 +00:00
KitaitiMakoto 92a8f8aa4c Merge pull request 'Update crates' (#1131) from update-crates into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1131
2023-01-05 16:35:11 +00:00
Kitaiti Makoto 0c856a5252 Update Ructe 2023-01-06 01:11:46 +09:00
Kitaiti Makoto 2df6138ff1 Fix caddy run option 2023-01-06 00:59:00 +09:00
Kitaiti Makoto b2942f3f47 Update crates 2023-01-06 00:42:29 +09:00
Kitaiti Makoto 94f20c8fc2 Update rustfmt and clippy on Ci 2023-01-06 00:42:20 +09:00
Kitaiti Makoto 5d48b93c8b Update Docker image for CI 2023-01-06 00:27:57 +09:00
Kitaiti Makoto bbf2e00920 Install updated crates 2023-01-06 00:04:53 +09:00
Kitaiti Makoto c97361f5f4 Update crates 2023-01-06 00:04:45 +09:00
KitaitiMakoto 7c799e8abf Merge pull request 'Add changelogs' (#1130) from changelog into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1130
2023-01-05 14:47:52 +00:00
Kitaiti Makoto d196e1dbd0 Add changelogs [skip ci] 2023-01-05 23:46:50 +09:00
Kitaiti Makoto 1679315322 Clippy 2023-01-05 05:08:53 +09:00
KitaitiMakoto dd3a5f4a5b Merge pull request 'Fixes #1128 Some ActivityPub related fixes' (#1129) from ap-fixes into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1129
2023-01-04 19:27:50 +00:00
Kitaiti Makoto 3580fb04fa Format 2023-01-05 04:14:25 +09:00
Kitaiti Makoto 699fdc30d9 Make preferredUsername of blogs valid 2023-01-05 04:13:52 +09:00
Kitaiti Makoto 704e9aa47f Make blogs.fqn valid 2023-01-05 04:13:40 +09:00
Kitaiti Makoto d741238ccb Revert "Rejectd illegal characters from blog name"
This reverts commit 9776374d17.
2023-01-05 03:11:20 +09:00
Kitaiti Makoto 9776374d17 Rejectd illegal characters from blog name 2023-01-05 02:49:48 +09:00
Kitaiti Makoto 2d10ddb9fa Clippy 2023-01-05 02:33:29 +09:00
Kitaiti Makoto e746a0b03f Add error log for invalid preferredUsername 2023-01-05 02:25:58 +09:00
Kitaiti Makoto 85cacf4239 Format 2023-01-05 02:23:50 +09:00
Kitaiti Makoto f138ae6ed9 Allow empty avatar for remote users 2023-01-05 02:20:57 +09:00
Kitaiti Makoto 399af4004a Build CustomPerson from source string at once 2023-01-05 02:20:25 +09:00
Kitaiti Makoto d36f13e984 Add test for deserializing CustomGroup 2023-01-05 02:19:10 +09:00
Kitaiti Makoto 9a3699160d Fix test name 2023-01-05 01:16:13 +09:00
KitaitiMakoto 4103e7513d Merge pull request 'Fix #966: Don't retrieve user info from blocked instances' (#1120) from block-instance-user into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1120
2023-01-04 16:11:34 +00:00
KitaitiMakoto ed9970b102 Merge pull request 'Percent encode to create ActivityPub URI' (#1127) from blog-slug into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1127
2023-01-04 09:55:34 +00:00
Kitaiti Makoto afa875366e Percent encode to create ActivityPub URI 2023-01-04 18:54:38 +09:00
KitaitiMakoto 9696f04c64 Merge pull request 'Fix #1125 Fix a bug about blog title and AP URL' (#1126) from blog-slug into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1126
2023-01-04 09:02:06 +00:00
Kitaiti Makoto 40e1a1fc2c Don't encode whole AP ID 2023-01-04 17:49:16 +09:00
Kitaiti Makoto ee1e553460 Fix a but about blog title and AP URL 2023-01-03 22:09:43 +09:00
KitaitiMakoto d20ce6dd0b Merge pull request 'Downgrade Docker image to run on CI' (#1124) from downgrade-ci-docker into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1124
2023-01-03 10:16:01 +00:00
Kitaiti Makoto 72f7909a42 Downgrade Docker image to run on CI 2023-01-03 19:15:32 +09:00
KitaitiMakoto 4e1fb64868 Merge pull request 'Update CI environment' (#1123) from clippy-on-ci into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1123
2023-01-03 09:30:54 +00:00
KitaitiMakoto 85c1bfa300 Merge pull request 'Fix #1121: Check email block list when email sign-up' (#1122) from block-on-email-signup into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1122
2023-01-03 09:11:20 +00:00
Kitaiti Makoto 172c78c41d Update Dockerfile for CI 2023-01-03 18:10:18 +09:00
Kitaiti Makoto 3b08d5b485 Update .dockerignore 2023-01-03 18:05:25 +09:00
Kitaiti Makoto 832479a706 Extract EmailSingup::ensure_email_not_blocked() 2023-01-03 17:49:15 +09:00
Kitaiti Makoto 3b3148fa6b Clippy 2023-01-03 17:42:26 +09:00
Kitaiti Makoto b38d55f486 Check email block list when email sign-up 2023-01-03 17:41:25 +09:00
Kitaiti Makoto 2804a490ed Don't retrieve user info from blocked instances 2023-01-03 03:23:50 +09:00
KitaitiMakoto 8c098def61 Merge pull request 'Update Rust' (#1119) from update-rust into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1119
2023-01-02 18:22:29 +00:00
Kitaiti Makoto e10ddb50c0 Ignore sum doc tests 2023-01-03 03:22:05 +09:00
Kitaiti Makoto 4df2c3e6f6 Clippy 2023-01-03 02:53:12 +09:00
Kitaiti Makoto 2f53fc78b6 Remove unnecessary trick 2023-01-03 02:41:51 +09:00
Kitaiti Makoto fded87654d Update Rust 2023-01-03 02:34:28 +09:00
Kitaiti Makoto 08cd777f81 Update pear_codegen 2023-01-03 02:34:22 +09:00
KitaitiMakoto 96b88353c5 Merge pull request 'docker-management' (#1118) from docker-management into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1118
2023-01-02 17:31:36 +00:00
Kitaiti Makoto 302026feb9 Update Rust on building Docker image [skip ci] 2023-01-03 02:30:43 +09:00
Kitaiti Makoto ba6d322da7 Set wasm-opt = false 2023-01-03 02:29:48 +09:00
KitaitiMakoto 488563e9c1 Merge pull request 'Update GitHub actions' (#1117) from docker-management into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1117
2023-01-02 16:13:28 +00:00
Kitaiti Makoto 130bb4c102 Update GitHub actions 2023-01-03 01:10:11 +09:00
KitaitiMakoto 9368aebe70 Merge pull request 'Fix #924 Update rocket_csrf' (#1116) from update-rocket_csrf into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1116
2023-01-02 15:29:34 +00:00
Kitaiti Makoto ca2843822e Install rocket_csrf 0.1.2 2023-01-03 00:27:05 +09:00
Kitaiti Makoto bd91b4a346 Update rocket_csrf to 0.1.2 2023-01-03 00:26:31 +09:00
KitaitiMakoto 63d2cf91e9 Merge pull request 'Update crates' (#1115) from update-rocket_csrf into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1115
2023-01-01 00:10:29 +00:00
Kitaiti Makoto 263cf9e04f Stick activitystreams for all packages 2023-01-01 08:52:08 +09:00
Kitaiti Makoto 22ebecba67 Stick serde for all packages 2023-01-01 08:11:14 +09:00
Kitaiti Makoto 903b48ed12 Stick activitystreams version 2023-01-01 07:54:08 +09:00
Kitaiti Makoto a550291c85 Stick activitystreams version 2023-01-01 07:41:54 +09:00
Kitaiti Makoto 47394fc620 Stick serde 2023-01-01 07:24:32 +09:00
Kitaiti Makoto b180089b1b Update ldap3 2023-01-01 07:10:31 +09:00
Kitaiti Makoto a275aa5965 Stick serde version 2023-01-01 07:10:09 +09:00
Kitaiti Makoto 87edb2486c Update openssl 2023-01-01 06:36:30 +09:00
Kitaiti Makoto 10617f3144 Install rocket_csrf 2023-01-01 06:23:25 +09:00
Kitaiti Makoto 6654ad28b7 Update rocket_csrf to 0.1.1 2023-01-01 06:23:18 +09:00
KitaitiMakoto 620726cc25 Merge pull request 'Update crates' (#1107) from update-crates into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1107
2022-06-16 09:12:07 +00:00
Kitaiti Makoto 0eef7c0b89 Merge remote-tracking branch 'github/dependabot/cargo/web-sys-0.3.58' into update-crates 2022-06-16 15:58:57 +09:00
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>
2022-06-15 19:27:28 +00:00
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>
2022-06-15 19:26:39 +00:00
KitaitiMakoto 9613ccd0c3 Merge pull request 'Fix Cargo.toml' (#1106) from fix-cargo into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1106
2022-06-15 04:52:21 +00:00
Kitaiti Makoto 9493c1ad06 Fix Cargo.toml 2022-06-15 13:51:30 +09:00
KitaitiMakoto e92ac1a13f Merge pull request 'Update crates' (#1105) from update-crates into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1105
2022-06-15 04:47:53 +00:00
Kitaiti Makoto 1517b4d91e Merge remote-tracking branch 'github/dependabot/cargo/js-sys-0.3.58' into update-crates 2022-06-15 13:46:55 +09:00
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>
2022-06-14 19:21:44 +00:00
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>
2022-06-14 19:21:16 +00:00
KitaitiMakoto 84645c7ed9 Merge pull request 'Update crates' (#1103) from update-crates into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1103
2022-06-12 11:54:55 +00:00
KitaitiMakoto 7c505bde7f Merge pull request 'Blog's header buttons margin fix in RTL' (#1093) from mskf1383/Plume:main into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1093
2022-06-12 11:30:08 +00:00
Kitaiti Makoto 9f543f1b6b Merge remote-tracking branch 'github/dependabot/cargo/flume-0.10.13' into update-crates 2022-06-12 20:26:19 +09:00
Kitaiti Makoto 0f7b882749 Merge remote-tracking branch 'github/dependabot/cargo/tracing-0.1.35' into update-crates 2022-06-12 20:25:10 +09:00
Kitaiti Makoto f9f4375a40 Merge remote-tracking branch 'github/dependabot/cargo/tokio-1.19.2' into update-crates 2022-06-12 20:24:58 +09:00
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>
2022-06-10 19:31:34 +00:00
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>
2022-06-08 19:30:58 +00:00
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>
2022-06-06 19:47:34 +00:00
MohammadSaleh Kamyab 4502b77094 Failsafe 2022-06-06 05:39:21 +00:00
MohammadSaleh Kamyab 8f5a86206a Merge branch 'main' into main 2022-06-01 09:26:34 +00:00
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>
2022-05-30 19:35:18 +00:00
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: https://git.joinplu.me/Plume/Plume/pulls/1101
2022-05-27 02:08:27 +00:00
Kitaiti Makoto b8eb631aa3 Merge remote-tracking branch 'origin/main' into rocket_contrib-0.4.11 2022-05-27 11:07:59 +09:00
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: https://git.joinplu.me/Plume/Plume/pulls/1100
2022-05-27 02:03:58 +00:00
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>
2022-05-26 19:33:42 +00:00
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>
2022-05-26 19:33:24 +00:00
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: https://git.joinplu.me/Plume/Plume/pulls/1099
2022-05-24 05:48:38 +00:00
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>
2022-05-23 19:24:07 +00:00
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: https://git.joinplu.me/Plume/Plume/pulls/1096
2022-05-21 06:35:34 +00:00
KitaitiMakoto 5770c3b85b Merge branch 'main' into regex-syntax-0.6.26 2022-05-21 06:35:19 +00:00
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: https://git.joinplu.me/Plume/Plume/pulls/1095
2022-05-21 06:34:41 +00:00
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>
2022-05-20 19:26:36 +00:00
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>
2022-05-19 19:36:52 +00:00
KitaitiMakoto f660220495 Merge pull request 'Fix blog slug' (#1094) from fix-blog-slug into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1094
2022-05-19 17:29:34 +00:00
KitaitiMakoto 155df7bdf0 Merge branch 'main' into fix-blog-slug 2022-05-19 17:28:56 +00:00
Kitaiti Makoto 29055d1957 Follow clippy 2022-05-20 02:13:23 +09:00
Kitaiti Makoto d6ee49b880 Update Cargo.lock 2022-05-20 01:55:12 +09:00
Kitaiti Makoto 248ed265c4 Remove unsed heck from dependencies 2022-05-20 01:53:16 +09:00
Kitaiti Makoto a1f958ee7a Remove unused import 2022-05-20 01:52:26 +09:00
Kitaiti Makoto abf352b957 Remove unused function 2022-05-20 01:51:53 +09:00
Kitaiti Makoto 393f8e5e0c Use Blog::slug() to determine blog's slug 2022-05-20 01:51:16 +09:00
Kitaiti Makoto 4dfe300ee3 Define Blog::slug() 2022-05-20 01:50:51 +09:00
Kitaiti Makoto 65829094c9 Add test for blog slug validation 2022-05-20 01:40:51 +09:00
MohammadSaleh Kamyab d5c3e6d6f0 Blog's header buttons margin fix in RTL 2022-05-19 15:47:56 +00:00
KitaitiMakoto d702dd2fae Merge pull request 'Bidirectional support for user page header' (#1092) from mskf1383/Plume:main into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1092
2022-05-19 15:38:36 +00:00
MohammadSaleh Kamyab 0d855823c9 Bidirectional support for user page header 2022-05-19 15:26:44 +00:00
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: https://git.joinplu.me/Plume/Plume/pulls/1091
2022-05-19 01:29:12 +00:00
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>
2022-05-18 19:31:26 +00:00
KitaitiMakoto 3b0b6c4b0b Merge pull request 'Fix .venv path in buildenv' (#1090) from venv-path into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1090
2022-05-17 17:39:10 +00:00
Kitaiti Makoto 4328fad5a3 Don't load venv 2022-05-18 02:15:15 +09:00
Kitaiti Makoto b26822c045 Update buildenv image 2022-05-16 12:59:05 +09:00
Kitaiti Makoto f372282b04 Use apt package for setuptools instead of pyenv 2022-05-16 12:20:06 +09:00
Kitaiti Makoto 145253ccbf Fix .venv path in buildenv 2022-05-15 11:41:50 +09:00
KitaitiMakoto 485223a3dd Merge pull request 'Add fmt and clippy on CI' (#1089) from add-toolchain into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1089
2022-05-15 02:22:29 +00:00
Kitaiti Makoto 7f75fa74e7 Add fmt and clippy on CI 2022-05-15 11:21:07 +09:00
KitaitiMakoto 821fce1903 Merge pull request 'Use rust-toolchain in buildenv' (#1088) from buildenv-rust-toolchain into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1088
2022-05-15 02:16:39 +00:00
Kitaiti Makoto ce484de61e Bump buildenv image 2022-05-15 11:15:05 +09:00
Kitaiti Makoto 35fb57718d Add rust-toolchain into buildenv 2022-05-15 06:46:18 +09:00
KitaitiMakoto 35d12d7cae Merge pull request 'Activate venv on integration test' (#1087) from fix-test-env into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1087
2022-05-14 21:34:37 +00:00
Kitaiti Makoto 846154efe1 Activate venv on integration test 2022-05-15 06:34:09 +09:00
KitaitiMakoto 1ec7acbdfe Merge pull request 'Update Crowdin enviroment' (#1086) from update-crowdin into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1086
2022-05-14 21:14:15 +00:00
Kitaiti Makoto e384fdfcff Update buildenv image to v0.5.0 2022-05-15 05:56:31 +09:00
Kitaiti Makoto ed58e44d2e Use Python 3 to install Selenium 2022-05-15 05:45:25 +09:00
Kitaiti Makoto f151dee339 Don't strip in buildenv 2022-05-15 05:13:09 +09:00
Kitaiti Makoto 61f25941e8 Install crowdin CLI using apt in buildenv 2022-05-15 05:13:09 +09:00
Kitaiti Makoto 0628a14be6 Use Rust image for buildenv 2022-05-15 05:13:03 +09:00
KitaitiMakoto b46ae83377 Merge pull request 'Change default branch to main' (#1085) from default-branch into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1085
2022-05-14 18:57:21 +00:00
Kitaiti Makoto 70bc7f8edf Change default branch to main 2022-05-15 03:55:33 +09:00
KitaitiMakoto aff481b947 Merge pull request 'Add 'My feed' to i18n timeline name' (#1084) from myfeed-translation into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1084
2022-05-14 18:35:35 +00:00
Kitaiti Makoto 29ef73d307 Add 'My feed' to i18n timeline name 2022-05-15 03:32:45 +09:00
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: https://git.joinplu.me/Plume/Plume/pulls/1083
2022-05-12 23:19:47 +00:00
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>
2022-05-12 19:26:54 +00:00
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: https://git.joinplu.me/Plume/Plume/pulls/1081
2022-05-11 21:56:27 +00:00
KitaitiMakoto 17e4ddb32d Merge branch 'main' into diesel-derive-newtype-1.0.0 2022-05-11 21:56:09 +00:00
KitaitiMakoto 4fd85b30f1 Merge pull request '(cargo-release) version v0.7.3-dev' (#1080) from v0.7.3-dev into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1080
2022-05-11 21:33:43 +00:00
Kitaiti Makoto 6148f29c66 (cargo-release) version {{version}} 2022-05-12 06:30:25 +09:00
KitaitiMakoto 1a3fad2d6a Merge pull request 'Release v0.7.2' (#1079) from v0.7.2 into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1079
2022-05-11 19:50:40 +00:00
Kitaiti Makoto 0945d3bc53 Set release to false for sub crates [skip ci] 2022-05-12 04:35:05 +09:00
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>
2022-05-11 19:24:14 +00:00
79 changed files with 2066 additions and 1370 deletions
+5 -3
View File
@@ -10,7 +10,7 @@ executors:
type: boolean
default: false
docker:
- image: plumeorg/plume-buildenv:v0.4.0
- image: plumeorg/plume-buildenv:v0.8.0
- image: <<#parameters.postgres>>cimg/postgres:14.2<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
environment:
POSTGRES_USER: postgres
@@ -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-07-19-x86_64-unknown-linux-gnu
- run: cargo clippy <<^parameters.no_feature>>--no-default-features --features="${FEATURES}"<</parameters.no_feature>> --release -p <<parameters.package>> -- -D warnings
run_with_coverage:
@@ -111,6 +112,7 @@ jobs:
name: default
steps:
- restore_env
- run: rustup component add rustfmt --toolchain nightly-2022-07-19-x86_64-unknown-linux-gnu
- run: cargo fmt --all -- --check
clippy:
@@ -258,4 +260,4 @@ workflows:
filters:
branches:
only:
- /^master/
- /^main/
+7 -15
View File
@@ -1,4 +1,4 @@
FROM debian:buster-20210208
FROM rust:1
ENV PATH="/root/.cargo/bin:${PATH}"
#install native/circleci/build dependancies
@@ -6,19 +6,19 @@ RUN apt update &&\
apt install -y --no-install-recommends git ssh tar gzip ca-certificates default-jre&&\
echo "deb [trusted=yes] https://apt.fury.io/caddy/ /" \
| tee -a /etc/apt/sources.list.d/caddy-fury.list &&\
wget -qO - https://artifacts.crowdin.com/repo/GPG-KEY-crowdin | apt-key add - &&\
echo "deb https://artifacts.crowdin.com/repo/deb/ /" > /etc/apt/sources.list.d/crowdin.list &&\
apt update &&\
apt install -y --no-install-recommends binutils-dev build-essential cmake curl gcc gettext git libcurl4-openssl-dev libdw-dev libelf-dev libiberty-dev libpq-dev libsqlite3-dev libssl-dev make openssl pkg-config postgresql postgresql-contrib python zlib1g-dev python3-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-11-27 -y &&\
rustup component add rustfmt clippy &&\
rustup component add rust-std --target wasm32-unknown-unknown
#stick rust environment
COPY rust-toolchain ./
RUN rustup component add rustfmt clippy
#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 +29,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
@@ -0,0 +1 @@
nightly-2022-07-19
+2
View File
@@ -3,3 +3,5 @@ data
Dockerfile
docker-compose.yml
.env
target
data
+4 -4
View File
@@ -11,20 +11,20 @@ jobs:
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
-
name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
id: docker_build
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
push: true
tags: plumeorg/plume:latest
+4 -4
View File
@@ -11,10 +11,10 @@ jobs:
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
-
name: Docker meta
id: meta
@@ -23,14 +23,14 @@ jobs:
images: plumeorg/plume
-
name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
id: docker_build
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
+19
View File
@@ -4,6 +4,25 @@
## [Unreleased] - ReleaseDate
### Added
- Add 'My feed' to i18n timeline name (#1084)
- Bidirectional support for user page header (#1092)
### Changed
- Use blog title as slug (#1094, #1126, #1127)
- Bump Rust to nightly 2022-07-19 (#1119)
### Fixed
- Malfunction while creating a blog post in Persian (#1116)
- Email block list is ignored when email sign-up (#1122)
- Bug that some Activity Sytreams properties are not parsed properly (#1129)
- Allow empty avatar for remote users (#1129)
- Percent encode blog FQN for federation interoperability (#1129)
- The same to `preferredUsername` (#1129)
## [[0.7.2]] - 2022-05-11
### Added
Generated
+853 -670
View File
File diff suppressed because it is too large Load Diff
+10 -10
View File
@@ -1,7 +1,7 @@
[package]
authors = ["Plume contributors"]
name = "plume"
version = "0.7.2"
version = "0.7.3-dev"
repository = "https://github.com/Plume-org/Plume"
edition = "2018"
@@ -15,19 +15,19 @@ 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 = "0.4.11"
rocket_contrib = { version = "0.4.11", features = ["json"] }
rocket_i18n = "0.4.1"
scheduled-thread-pool = "0.2.2"
serde = "1.0"
scheduled-thread-pool = "0.2.6"
serde = "1.0.137"
serde_json = "1.0.81"
shrinkwraprs = "0.3.0"
validator = { version = "0.15", features = ["derive"] }
webfinger = "0.4.1"
tracing = "0.1.34"
tracing = "0.1.35"
tracing-subscriber = "0.3.10"
riker = "0.4.2"
activitystreams = "0.7.0-alpha.18"
activitystreams = "=0.7.0-alpha.20"
[[bin]]
name = "plume"
@@ -60,12 +60,12 @@ path = "plume-common"
path = "plume-models"
[dependencies.rocket_csrf]
git = "https://github.com/fdb-hiroshima/rocket_csrf"
rev = "29910f2829e7e590a540da3804336577b48c7b31"
git = "https://git.joinplu.me/plume/rocket_csrf"
rev = "0.1.2"
[build-dependencies]
ructe = "0.14.0"
rsass = "0.24"
rsass = "0.25"
[features]
default = ["postgres"]
+3 -5
View File
@@ -1,4 +1,4 @@
FROM rust:1-buster as builder
FROM rust:1 as builder
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
@@ -18,17 +18,15 @@ COPY script/wasm-deps.sh .
RUN chmod a+x ./wasm-deps.sh && sleep 1 && ./wasm-deps.sh
WORKDIR /app
COPY Cargo.toml Cargo.lock rust-toolchain ./
RUN cargo install wasm-pack
COPY . .
RUN cargo install wasm-pack
RUN chmod a+x ./script/plume-front.sh && sleep 1 && ./script/plume-front.sh
RUN cargo install --path ./ --force --no-default-features --features postgres
RUN cargo install --path plume-cli --force --no-default-features --features postgres
RUN cargo clean
FROM debian:buster-slim
FROM debian:stable-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
+2 -2
View File
@@ -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>
+1
View File
@@ -135,6 +135,7 @@ form.new-post {
.button + .button {
margin-left: 1em;
margin-inline-start: 1em;
}
.split {
@@ -0,0 +1,3 @@
ALTER TABLE users DROP CONSTRAINT users_fqn;
ALTER TABLE blogs DROP CONSTRAINT blogs_actor_id;
ALTER TABLE blogs DROP CONSTRAINT blogs_fqn;
@@ -0,0 +1,3 @@
ALTER TABLE users ADD CONSTRAINT users_fqn UNIQUE (fqn);
ALTER TABLE blogs ADD CONSTRAINT blogs_actor_id UNIQUE (actor_id);
ALTER TABLE blogs ADD CONSTRAINT blogs_fqn UNIQUE (fqn);
@@ -0,0 +1,3 @@
DROP INDEX users_fqn;
DROP INDEX blogs_actor_id;
DROP INDEX blogs_fqn;
@@ -0,0 +1,3 @@
CREATE UNIQUE INDEX users_fqn ON users (fqn);
CREATE UNIQUE INDEX blogs_actor_id ON blogs (actor_id);
CREATE UNIQUE INDEX blogs_fqn ON blogs (fqn);
+1 -1
View File
@@ -5,5 +5,5 @@ authors = ["Plume contributors"]
edition = "2018"
[dependencies]
serde = "1.0"
serde = "1.0.137"
serde_derive = "1.0"
+1
View File
@@ -1,2 +1,3 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
release = false
+1
View File
@@ -1,2 +1,3 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
release = false
+11 -11
View File
@@ -7,25 +7,25 @@ edition = "2018"
[dependencies]
array_tool = "1.0"
base64 = "0.13"
heck = "0.4.0"
hex = "0.4"
openssl = "0.10.40"
rocket = "0.4.6"
reqwest = { version = "0.11.10", features = ["blocking", "json", "socks"] }
serde = "1.0"
rocket = "0.4.11"
reqwest = { version = "0.11.11", features = ["blocking", "json", "socks"] }
serde = "1.0.137"
serde_derive = "1.0"
serde_json = "1.0.81"
shrinkwraprs = "0.3.0"
syntect = "4.5.0"
regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] }
tracing = "0.1.34"
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 = "=0.7.0-alpha.20"
activitystreams-ext = "0.1.0-alpha.2"
url = "2.2.2"
flume = "0.10.12"
tokio = { version = "1.18.2", features = ["full"] }
futures = "0.3.21"
flume = "0.10.13"
tokio = { version = "1.19.2", features = ["full"] }
futures = "0.3.25"
thiserror = "1.0.38"
[dependencies.chrono]
features = ["serde"]
@@ -38,6 +38,6 @@ branch = "bidi-plume"
[dev-dependencies]
assert-json-diff = "2.0.1"
once_cell = "1.10.0"
once_cell = "1.12.0"
[features]
+1
View File
@@ -1,2 +1,3 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
release = false
+3 -3
View File
@@ -561,7 +561,7 @@ mod tests {
use once_cell::sync::Lazy;
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
static MY_SIGNER: Lazy<MySigner> = Lazy::new(|| MySigner::new());
static MY_SIGNER: Lazy<MySigner> = Lazy::new(MySigner::new);
struct MySigner {
public_key: String,
@@ -596,7 +596,7 @@ mod tests {
.unwrap();
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
verifier.update(data.as_bytes()).unwrap();
verifier.verify(&signature).map_err(|_| SignError())
verifier.verify(signature).map_err(|_| SignError())
}
}
@@ -782,7 +782,7 @@ mod tests {
.done();
assert!(res.is_err());
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
let res: Result<(), ()> = Inbox::handle(&(), act)
.with::<FailingActor, Create, MyObject>(None)
.with::<MyActor, Create, MyObject>(None)
.done();
+193 -2
View File
@@ -18,6 +18,11 @@ use rocket::{
response::{Responder, Response},
Outcome,
};
use std::{
convert::{TryFrom, TryInto},
fmt,
str::FromStr,
};
use tokio::{
runtime,
time::{sleep, Duration},
@@ -241,6 +246,97 @@ pub trait IntoId {
fn into_id(self) -> Id;
}
#[derive(thiserror::Error, Debug)]
pub enum PreferredUsernameError {
#[error("preferredUsername must be longer than 2 characters")]
TooShort,
#[error("Invaliad character at {character:?}: {position:?}")]
InvalidCharacter { character: char, position: usize },
}
#[repr(transparent)]
#[derive(Shrinkwrap, PartialEq, Eq, Clone, Serialize, Deserialize, Debug)]
pub struct PreferredUsername(String);
// Mastodon allows only /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i for `preferredUsername`
impl PreferredUsername {
fn validate(name: &str) -> std::result::Result<(), PreferredUsernameError> {
let len = name.len();
if len < 3 {
return Err(PreferredUsernameError::TooShort);
}
match name.chars().enumerate().find(|(pos, c)| {
if pos == &0 || pos == &(len - 1) {
c != &'_' && !c.is_ascii_alphanumeric()
} else {
match c {
'_' | '\\' | '.' | '-' => false,
_ => !c.is_ascii_alphanumeric(),
}
}
}) {
Some((pos, c)) => Err(PreferredUsernameError::InvalidCharacter {
character: c,
position: pos,
}),
None => Ok(()),
}
}
/// # Safety
///
/// The given string must be match against /\A[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?\z/i in Ruby's RegExp which is required by Mastodon.
pub unsafe fn new_unchecked(name: String) -> Self {
Self(name)
}
pub fn new(name: String) -> std::result::Result<Self, PreferredUsernameError> {
Self::validate(&name).map(|_| unsafe { Self::new_unchecked(name) })
}
}
impl fmt::Display for PreferredUsername {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl TryFrom<String> for PreferredUsername {
type Error = PreferredUsernameError;
fn try_from(name: String) -> std::result::Result<Self, Self::Error> {
Self::new(name)
}
}
impl TryFrom<&str> for PreferredUsername {
type Error = PreferredUsernameError;
fn try_from(name: &str) -> std::result::Result<Self, Self::Error> {
Self::new(name.to_owned())
}
}
impl From<PreferredUsername> for String {
fn from(preferred_username: PreferredUsername) -> Self {
preferred_username.0
}
}
impl AsRef<str> for PreferredUsername {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl FromStr for PreferredUsername {
type Err = PreferredUsernameError;
fn from_str(name: &str) -> std::result::Result<Self, Self::Err> {
name.try_into()
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ApSignature {
@@ -518,11 +614,41 @@ mod tests {
use super::*;
use activitystreams::{
activity::{ActorAndObjectRef, Create},
object::kind::ArticleType,
object::{kind::ArticleType, Image},
prelude::{ApActorExt, BaseExt, ExtendsExt, ObjectExt},
};
use assert_json_diff::assert_json_eq;
use serde_json::{from_str, json, to_value};
#[test]
fn preferred_username() {
assert!(PreferredUsername::new("".into()).is_err());
assert!(PreferredUsername::new("a".into()).is_err());
assert!(PreferredUsername::new("ab".into()).is_err());
assert_eq!(
"abc",
PreferredUsername::new("abc".into()).unwrap().as_str()
);
assert_eq!(
"abcd",
PreferredUsername::new("abcd".into()).unwrap().as_str()
);
assert!(PreferredUsername::new("abc-".into()).is_err());
assert!(PreferredUsername::new("日本語".into()).is_err());
assert_eq!("abc", "abc".parse::<PreferredUsername>().unwrap().as_str());
assert!("abc-".parse::<PreferredUsername>().is_err());
assert_eq!(
PreferredUsername::new("admin".into()).unwrap(),
PreferredUsername("admin".into())
);
}
#[test]
fn prefferred_username_to_string() {
let pu = PreferredUsername::new("admin".into()).unwrap();
assert_eq!("admin".to_string(), pu.to_string());
}
#[test]
fn se_ap_signature() {
let ap_signature = ApSignature {
@@ -592,7 +718,7 @@ mod tests {
}
#[test]
fn de_custom_group() {
fn se_custom_group() {
let group = CustomGroup::new(
ApActor::new("https://example.com/inbox".parse().unwrap(), Group::new()),
ApSignature {
@@ -625,6 +751,71 @@ mod tests {
assert_eq!(to_value(group).unwrap(), expected);
}
#[test]
fn de_custom_group() {
let value: CustomGroup = from_str(
r#"
{
"icon": {
"type": "Image"
},
"id": "https://plume01.localhost/~/Plume01%20Blog%202/",
"image": {
"type": "Image"
},
"inbox": "https://plume01.localhost/~/Plume01%20Blog%202/inbox",
"name": "Plume01 Blog 2",
"outbox": "https://plume01.localhost/~/Plume01%20Blog%202/outbox",
"preferredUsername": "Plume01 Blog 2",
"publicKey": {
"id": "https://plume01.localhost/~/Plume01%20Blog%202/#main-key",
"owner": "https://plume01.localhost/~/Plume01%20Blog%202/",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwPGtKkl/iMsNAyeVaJGz\noEz5PoNkjRnKK7G97MFvb4zw9zs5SpzWW7b/pKHa4dODcGDJXmkCJ1H5JWyguzN8\n2GNoFjtEOJHxEGwBHSYDsTmhuLNB0DKxMU2iu55g8iIiXhZiIW1FBNGs/Geaymvr\nh/TEtzdReN8wzloRR55kOVcU49xBkqx8cfDSk/lrrDLlpveHdqgaFnIvuw2vycK0\nxFzS3xlEUpzJk9kHxoR1uEAfZ+gCv26Sgo/HqOAhqSD5IU3QZC3kdkr/hwVqtr8U\nXGkGG6Mo1rgzhkYiCFkWrV2WoKkcEHD4nEzbgoZZ5MyuSoloxnyF3NiScqmqW+Yx\nkQIDAQAB\n-----END PUBLIC KEY-----\n"
},
"source": {
"content": "",
"mediaType": "text/markdown"
},
"summary": "",
"type": "Group"
}
"#
).unwrap();
let mut expected = CustomGroup::new(
ApActor::new("https://plume01.localhost/~/Plume01%20Blog%202/inbox".parse().unwrap(), Group::new()),
ApSignature {
public_key: PublicKey {
id: "https://plume01.localhost/~/Plume01%20Blog%202/#main-key".parse().unwrap(),
owner: "https://plume01.localhost/~/Plume01%20Blog%202/".parse().unwrap(),
public_key_pem: "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwPGtKkl/iMsNAyeVaJGz\noEz5PoNkjRnKK7G97MFvb4zw9zs5SpzWW7b/pKHa4dODcGDJXmkCJ1H5JWyguzN8\n2GNoFjtEOJHxEGwBHSYDsTmhuLNB0DKxMU2iu55g8iIiXhZiIW1FBNGs/Geaymvr\nh/TEtzdReN8wzloRR55kOVcU49xBkqx8cfDSk/lrrDLlpveHdqgaFnIvuw2vycK0\nxFzS3xlEUpzJk9kHxoR1uEAfZ+gCv26Sgo/HqOAhqSD5IU3QZC3kdkr/hwVqtr8U\nXGkGG6Mo1rgzhkYiCFkWrV2WoKkcEHD4nEzbgoZZ5MyuSoloxnyF3NiScqmqW+Yx\nkQIDAQAB\n-----END PUBLIC KEY-----\n".into(),
}
},
SourceProperty {
source: Source {
content: String::from(""),
media_type: String::from("text/markdown")
}
}
);
expected.set_icon(Image::new().into_any_base().unwrap());
expected.set_id(
"https://plume01.localhost/~/Plume01%20Blog%202/"
.parse()
.unwrap(),
);
expected.set_image(Image::new().into_any_base().unwrap());
expected.set_name("Plume01 Blog 2");
expected.set_outbox(
"https://plume01.localhost/~/Plume01%20Blog%202/outbox"
.parse()
.unwrap(),
);
expected.set_preferred_username("Plume01 Blog 2");
expected.set_summary("");
assert_json_eq!(value, expected);
}
#[test]
fn se_licensed_article() {
let object = ApObject::new(Article::new());
+2 -2
View File
@@ -253,7 +253,7 @@ mod tests {
.unwrap();
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
verifier.update(data.as_bytes()).unwrap();
verifier.verify(&signature).map_err(|_| Error())
verifier.verify(signature).map_err(|_| Error())
}
}
@@ -262,7 +262,7 @@ mod tests {
let signer = MySigner::new();
let headers = HeaderMap::new();
let result = signature(&signer, &headers, ("post", "/inbox", None)).unwrap();
let fields: Vec<&str> = result.to_str().unwrap().split(",").collect();
let fields: Vec<&str> = result.to_str().unwrap().split(',').collect();
assert_eq!(r#"headers="(request-target)""#, fields[2]);
let sign = &fields[3][11..(fields[3].len() - 1)];
assert!(signer.verify("post /inbox", sign.as_bytes()).is_ok());
+1 -1
View File
@@ -119,7 +119,7 @@ impl Signable for serde_json::Value {
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum SignatureValidity {
Invalid,
ValidNoDigest,
+2 -55
View File
@@ -1,8 +1,7 @@
use heck::ToUpperCamelCase;
use activitystreams::iri_string::percent_encode::PercentEncodedForIri;
use openssl::rand::rand_bytes;
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag};
use regex_syntax::is_word_character;
use rocket::http::uri::Uri;
use std::collections::HashSet;
use syntect::html::{ClassStyle, ClassedHTMLGenerator};
use syntect::parsing::SyntaxSet;
@@ -16,65 +15,13 @@ 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_upper_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()
}
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()
}
}
}
PercentEncodedForIri::from_path_segment(segment).to_string()
}
#[derive(Debug)]
+7 -4
View File
@@ -4,6 +4,9 @@ version = "0.7.2"
authors = ["Plume contributors"]
edition = "2018"
[package.metadata.wasm-pack.profile.release]
wasm-opt = false
[lib]
crate-type = ["cdylib"]
@@ -12,15 +15,15 @@ gettext = "0.4.0"
gettext-macros = "0.6.1"
gettext-utils = "0.1.0"
lazy_static = "1.3"
serde = "1.0"
serde = "1.0.137"
serde_json = "1.0"
wasm-bindgen = "0.2.80"
js-sys = "0.3.57"
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.57"
version = "0.3.58"
features = [
'console',
'ClipboardEvent',
+1
View File
@@ -1,2 +1,3 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
release = false
+1
View File
@@ -1,2 +1,3 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
release = false
+11 -10
View File
@@ -10,31 +10,32 @@ bcrypt = "0.12.1"
guid-create = "0.2"
itertools = "0.10.3"
lazy_static = "1.0"
ldap3 = "0.10.4"
ldap3 = "0.11.1"
migrations_internals= "1.4.0"
openssl = "0.10.40"
rocket = "0.4.6"
rocket = "0.4.11"
rocket_i18n = "0.4.1"
reqwest = "0.11.10"
scheduled-thread-pool = "0.2.2"
serde = "1.0"
reqwest = "0.11.11"
scheduled-thread-pool = "0.2.6"
serde = "1.0.137"
serde_derive = "1.0"
serde_json = "1.0.81"
tantivy = "0.13.3"
url = "2.1"
walkdir = "2.2"
webfinger = "0.4.1"
whatlang = "0.16.0"
whatlang = "0.16.2"
shrinkwraprs = "0.3.0"
diesel-derive-newtype = "0.1.2"
diesel-derive-newtype = "1.0.0"
glob = "0.3.0"
lindera-tantivy = { version = "0.7.1", optional = true }
tracing = "0.1.34"
tracing = "0.1.35"
riker = "0.4.2"
once_cell = "1.10.0"
once_cell = "1.12.0"
lettre = "0.9.6"
native-tls = "0.2.10"
activitystreams = "0.7.0-alpha.18"
activitystreams = "=0.7.0-alpha.20"
heck = "0.4.0"
[dependencies.chrono]
features = ["serde"]
+1
View File
@@ -1,2 +1,3 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
release = false
+1 -1
View File
@@ -103,7 +103,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApiToken {
let conn = request
.guard::<DbConn>()
.map_failure(|_| (Status::InternalServerError, TokenError::DbError))?;
if let Ok(token) = ApiToken::find_by_value(&*conn, val) {
if let Ok(token) = ApiToken::find_by_value(&conn, val) {
return Outcome::Success(token);
}
}
+3 -6
View File
@@ -126,12 +126,9 @@ pub(crate) mod tests {
.id,
various[1].id
);
assert_eq!(
BlocklistedEmail::matches_blocklist(&conn, no_match)
.unwrap()
.is_none(),
true
);
assert!(BlocklistedEmail::matches_blocklist(&conn, no_match)
.unwrap()
.is_none());
Ok(())
});
}
+132 -97
View File
@@ -1,6 +1,7 @@
use crate::{
db_conn::DbConn, instance::*, medias::Media, posts::Post, safe_string::SafeString,
schema::blogs, users::User, Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE,
schema::blogs, users::User, Connection, Error, Fqn, PlumeRocket, Result, CONFIG,
ITEMS_PER_PAGE,
};
use activitystreams::{
actor::{ApActor, ApActorExt, AsApActor, Group},
@@ -18,10 +19,13 @@ use openssl::{
rsa::Rsa,
sign::{Signer, Verifier},
};
use plume_common::activity_pub::{
inbox::{AsActor, FromId},
sign, ActivityStream, ApSignature, CustomGroup, Id, IntoId, PublicKey, Source, SourceProperty,
ToAsString, ToAsUri,
use plume_common::{
activity_pub::{
inbox::{AsActor, FromId},
sign, ActivityStream, ApSignature, CustomGroup, Id, IntoId, PublicKey, Source,
SourceProperty, ToAsString, ToAsUri,
},
utils::iri_percent_encode_seg,
};
use webfinger::*;
@@ -39,14 +43,14 @@ pub struct Blog {
pub ap_url: String,
pub private_key: Option<String>,
pub public_key: String,
pub fqn: String,
pub fqn: Fqn,
pub summary_html: SafeString,
pub icon_id: Option<i32>,
pub banner_id: Option<i32>,
pub theme: Option<String>,
}
#[derive(Default, Insertable)]
#[derive(Insertable)]
#[table_name = "blogs"]
pub struct NewBlog {
pub actor_id: String,
@@ -58,6 +62,7 @@ pub struct NewBlog {
pub ap_url: String,
pub private_key: Option<String>,
pub public_key: String,
pub fqn: Fqn,
pub summary_html: SafeString,
pub icon_id: Option<i32>,
pub banner_id: Option<i32>,
@@ -81,11 +86,13 @@ impl Blog {
inserted.ap_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "");
}
if inserted.fqn.is_empty() {
if inserted.fqn.to_string().is_empty() {
// This might not enough for some titles such as all-Japanese title,
// but better than doing nothing.
if instance.local {
inserted.fqn = inserted.actor_id.clone();
inserted.fqn = Fqn::make_local(&inserted.title)?;
} else {
inserted.fqn = format!("{}@{}", inserted.actor_id, instance.public_domain);
inserted.fqn = Fqn::make_remote(&inserted.title, instance.public_domain)?;
}
}
@@ -95,6 +102,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) -> String {
iri_percent_encode_seg(title)
}
pub fn get_instance(&self, conn: &Connection) -> Result<Instance> {
Instance::get(conn, self.instance_id)
}
@@ -162,7 +173,7 @@ impl Blog {
pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> {
let mut blog = ApActor::new(self.inbox_url.parse()?, Group::new());
blog.set_preferred_username(self.actor_id.clone());
blog.set_preferred_username(self.fqn.to_string());
blog.set_name(self.title.clone());
blog.set_outbox(self.outbox_url.parse()?);
blog.set_summary(self.summary_html.to_string());
@@ -377,6 +388,7 @@ impl FromId<DbConn> for Blog {
.ok_or(Error::MissingApProperty)?
.to_string();
if name.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
tracing::error!("preferredUsername includes invalid character(s): {}", &name);
return Err(Error::InvalidValue);
}
(
@@ -386,22 +398,18 @@ impl FromId<DbConn> for Blog {
)
};
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 actor_id = iri_percent_encode_seg(
&acct
.name()
.and_then(|name| name.to_as_string())
.ok_or(Error::MissingApProperty)?,
);
let object = ApObject::new(acct.inner);
new_blog.title = object
let title = object
.name()
.and_then(|name| name.to_as_string())
.unwrap_or(name);
new_blog.summary_html = SafeString::new(
.unwrap_or(name.clone());
let summary_html = SafeString::new(
&object
.summary()
.and_then(|summary| summary.to_as_string())
@@ -423,7 +431,6 @@ impl FromId<DbConn> for Blog {
})
})
.map(|m| m.id);
new_blog.icon_id = icon_id;
let banner_id = object
.image()
@@ -440,13 +447,12 @@ impl FromId<DbConn> for Blog {
})
})
.map(|m| m.id);
new_blog.banner_id = banner_id;
new_blog.summary = acct.ext_two.source.content;
let 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 ap_url = id.to_string();
let inst = id
.authority_components()
@@ -470,7 +476,29 @@ impl FromId<DbConn> for Blog {
},
)
})?;
new_blog.instance_id = instance.id;
let instance_id = instance.id;
let fqn = if instance.local {
Fqn::new_local(name)?
} else {
Fqn::new_remote(name, instance.public_domain)?
};
let new_blog = NewBlog {
actor_id,
outbox_url,
inbox_url,
fqn,
public_key: acct.ext_one.public_key.public_key_pem.to_string(),
private_key: None,
theme: None,
title,
summary,
ap_url,
summary_html,
icon_id,
banner_id,
instance_id,
};
Blog::insert(conn, new_blog)
}
@@ -526,12 +554,19 @@ impl NewBlog {
let (pub_key, priv_key) = sign::gen_keypair();
Ok(NewBlog {
actor_id,
fqn: Fqn::make_local(&title)?,
title,
summary,
instance_id,
public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?,
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
..NewBlog::default()
outbox_url: Default::default(),
inbox_url: Default::default(),
ap_url: Default::default(),
summary_html: Default::default(),
icon_id: Default::default(),
banner_id: Default::default(),
theme: Default::default(),
})
}
}
@@ -553,8 +588,8 @@ pub(crate) mod tests {
let mut blog1 = Blog::insert(
conn,
NewBlog::new_local(
"BlogName".to_owned(),
"Blog name".to_owned(),
"Blog%20Name".to_owned(),
"Blog Name".to_owned(),
"This is a small blog".to_owned(),
Instance::get_local().unwrap().id,
)
@@ -564,7 +599,7 @@ pub(crate) mod tests {
let blog2 = Blog::insert(
conn,
NewBlog::new_local(
"MyBlog".to_owned(),
"My%20Blog".to_owned(),
"My blog".to_owned(),
"Welcome to my blog".to_owned(),
Instance::get_local().unwrap().id,
@@ -575,7 +610,7 @@ pub(crate) mod tests {
let blog3 = Blog::insert(
conn,
NewBlog::new_local(
"WhyILikePlume".to_owned(),
"Why%20I%20Like%20Plume".to_owned(),
"Why I like Plume".to_owned(),
"In this blog I will explay you why I like Plume so much".to_owned(),
Instance::get_local().unwrap().id,
@@ -656,7 +691,7 @@ pub(crate) mod tests {
.unwrap()
.id,
);
let _: Blog = blog1.save_changes(&*conn).unwrap();
let _: Blog = blog1.save_changes(conn).unwrap();
(users, vec![blog1, blog2, blog3])
}
@@ -665,12 +700,12 @@ pub(crate) mod tests {
fn get_instance() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
fill_database(&conn);
fill_database(conn);
let blog = Blog::insert(
&conn,
conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some%20Name".to_owned(),
"Some name".to_owned(),
"This is some blog".to_owned(),
Instance::get_local().unwrap().id,
@@ -680,7 +715,7 @@ pub(crate) mod tests {
.unwrap();
assert_eq!(
blog.get_instance(&conn).unwrap().id,
blog.get_instance(conn).unwrap().id,
Instance::get_local().unwrap().id
);
// TODO add tests for remote instance
@@ -692,12 +727,12 @@ pub(crate) mod tests {
fn authors() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (user, _) = fill_database(&conn);
let (user, _) = fill_database(conn);
let b1 = Blog::insert(
&conn,
conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some%20Name".to_owned(),
"Some name".to_owned(),
"This is some blog".to_owned(),
Instance::get_local().unwrap().id,
@@ -706,7 +741,7 @@ pub(crate) mod tests {
)
.unwrap();
let b2 = Blog::insert(
&conn,
conn,
NewBlog::new_local(
"Blog".to_owned(),
"Blog".to_owned(),
@@ -719,7 +754,7 @@ pub(crate) mod tests {
let blog = vec![b1, b2];
BlogAuthor::insert(
&conn,
conn,
NewBlogAuthor {
blog_id: blog[0].id,
author_id: user[0].id,
@@ -729,7 +764,7 @@ pub(crate) mod tests {
.unwrap();
BlogAuthor::insert(
&conn,
conn,
NewBlogAuthor {
blog_id: blog[0].id,
author_id: user[1].id,
@@ -739,7 +774,7 @@ pub(crate) mod tests {
.unwrap();
BlogAuthor::insert(
&conn,
conn,
NewBlogAuthor {
blog_id: blog[1].id,
author_id: user[0].id,
@@ -749,39 +784,39 @@ pub(crate) mod tests {
.unwrap();
assert!(blog[0]
.list_authors(&conn)
.list_authors(conn)
.unwrap()
.iter()
.any(|a| a.id == user[0].id));
assert!(blog[0]
.list_authors(&conn)
.list_authors(conn)
.unwrap()
.iter()
.any(|a| a.id == user[1].id));
assert!(blog[1]
.list_authors(&conn)
.list_authors(conn)
.unwrap()
.iter()
.any(|a| a.id == user[0].id));
assert!(!blog[1]
.list_authors(&conn)
.list_authors(conn)
.unwrap()
.iter()
.any(|a| a.id == user[1].id));
assert!(Blog::find_for_author(&conn, &user[0])
assert!(Blog::find_for_author(conn, &user[0])
.unwrap()
.iter()
.any(|b| b.id == blog[0].id));
assert!(Blog::find_for_author(&conn, &user[1])
assert!(Blog::find_for_author(conn, &user[1])
.unwrap()
.iter()
.any(|b| b.id == blog[0].id));
assert!(Blog::find_for_author(&conn, &user[0])
assert!(Blog::find_for_author(conn, &user[0])
.unwrap()
.iter()
.any(|b| b.id == blog[1].id));
assert!(!Blog::find_for_author(&conn, &user[1])
assert!(!Blog::find_for_author(conn, &user[1])
.unwrap()
.iter()
.any(|b| b.id == blog[1].id));
@@ -793,12 +828,12 @@ pub(crate) mod tests {
fn find_local() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
fill_database(&conn);
fill_database(conn);
let blog = Blog::insert(
&conn,
conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some%20Name".to_owned(),
"Some name".to_owned(),
"This is some blog".to_owned(),
Instance::get_local().unwrap().id,
@@ -807,7 +842,7 @@ pub(crate) mod tests {
)
.unwrap();
assert_eq!(Blog::find_by_fqn(&conn, "SomeName").unwrap().id, blog.id);
assert_eq!(Blog::find_by_fqn(conn, "SomeName").unwrap().id, blog.id);
Ok(())
})
}
@@ -816,12 +851,12 @@ pub(crate) mod tests {
fn get_fqn() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
fill_database(&conn);
fill_database(conn);
let blog = Blog::insert(
&conn,
let _ = Blog::insert(
conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some%20Name".to_owned(),
"Some name".to_owned(),
"This is some blog".to_owned(),
Instance::get_local().unwrap().id,
@@ -830,7 +865,6 @@ pub(crate) mod tests {
)
.unwrap();
assert_eq!(blog.fqn, "SomeName");
Ok(())
})
}
@@ -839,10 +873,10 @@ pub(crate) mod tests {
fn delete() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (_, blogs) = fill_database(&conn);
let (_, blogs) = fill_database(conn);
blogs[0].delete(&conn).unwrap();
assert!(Blog::get(&conn, blogs[0].id).is_err());
blogs[0].delete(conn).unwrap();
assert!(Blog::get(conn, blogs[0].id).is_err());
Ok(())
})
}
@@ -851,12 +885,12 @@ pub(crate) mod tests {
fn delete_via_user() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (user, _) = fill_database(&conn);
let (user, _) = fill_database(conn);
let b1 = Blog::insert(
&conn,
conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some%20Name".to_owned(),
"Some name".to_owned(),
"This is some blog".to_owned(),
Instance::get_local().unwrap().id,
@@ -865,7 +899,7 @@ pub(crate) mod tests {
)
.unwrap();
let b2 = Blog::insert(
&conn,
conn,
NewBlog::new_local(
"Blog".to_owned(),
"Blog".to_owned(),
@@ -878,7 +912,7 @@ pub(crate) mod tests {
let blog = vec![b1, b2];
BlogAuthor::insert(
&conn,
conn,
NewBlogAuthor {
blog_id: blog[0].id,
author_id: user[0].id,
@@ -888,7 +922,7 @@ pub(crate) mod tests {
.unwrap();
BlogAuthor::insert(
&conn,
conn,
NewBlogAuthor {
blog_id: blog[0].id,
author_id: user[1].id,
@@ -898,7 +932,7 @@ pub(crate) mod tests {
.unwrap();
BlogAuthor::insert(
&conn,
conn,
NewBlogAuthor {
blog_id: blog[1].id,
author_id: user[0].id,
@@ -907,11 +941,11 @@ pub(crate) mod tests {
)
.unwrap();
user[0].delete(&conn).unwrap();
assert!(Blog::get(&conn, blog[0].id).is_ok());
assert!(Blog::get(&conn, blog[1].id).is_err());
user[1].delete(&conn).unwrap();
assert!(Blog::get(&conn, blog[0].id).is_err());
user[0].delete(conn).unwrap();
assert!(Blog::get(conn, blog[0].id).is_ok());
assert!(Blog::get(conn, blog[1].id).is_err());
user[1].delete(conn).unwrap();
assert!(Blog::get(conn, blog[0].id).is_err());
Ok(())
})
}
@@ -920,10 +954,10 @@ pub(crate) mod tests {
fn self_federation() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (users, mut blogs) = fill_database(&conn);
let (users, mut blogs) = fill_database(conn);
blogs[0].icon_id = Some(
Media::insert(
&conn,
conn,
NewMedia {
file_path: "aaa.png".into(),
alt_text: String::new(),
@@ -939,7 +973,7 @@ pub(crate) mod tests {
);
blogs[0].banner_id = Some(
Media::insert(
&conn,
conn,
NewMedia {
file_path: "bbb.png".into(),
alt_text: String::new(),
@@ -954,9 +988,10 @@ pub(crate) mod tests {
.id,
);
let _: Blog = blogs[0].save_changes(&**conn).unwrap();
let ap_repr = blogs[0].to_activity(&conn).unwrap();
blogs[0].delete(&conn).unwrap();
let blog = Blog::from_activity(&conn, ap_repr).unwrap();
let ap_repr = blogs[0].to_activity(conn).unwrap();
blogs[0].delete(conn).unwrap();
eprintln!("{:#?}", &ap_repr);
let blog = Blog::from_activity(conn, ap_repr).unwrap();
assert_eq!(blog.actor_id, blogs[0].actor_id);
assert_eq!(blog.title, blogs[0].title);
@@ -968,8 +1003,8 @@ pub(crate) mod tests {
assert_eq!(blog.public_key, blogs[0].public_key);
assert_eq!(blog.fqn, blogs[0].fqn);
assert_eq!(blog.summary_html, blogs[0].summary_html);
assert_eq!(blog.icon_url(&conn), blogs[0].icon_url(&conn));
assert_eq!(blog.banner_url(&conn), blogs[0].banner_url(&conn));
assert_eq!(blog.icon_url(conn), blogs[0].icon_url(conn));
assert_eq!(blog.banner_url(conn), blogs[0].banner_url(conn));
Ok(())
})
@@ -979,7 +1014,7 @@ pub(crate) mod tests {
fn to_activity() {
let conn = &db();
conn.test_transaction::<_, Error, _>(|| {
let (_users, blogs) = fill_database(&conn);
let (_users, blogs) = fill_database(conn);
let blog = &blogs[0];
let act = blog.to_activity(conn)?;
@@ -989,19 +1024,19 @@ pub(crate) mod tests {
"type": "Image",
"url": "https://plu.me/aaa.png"
},
"id": "https://plu.me/~/BlogName/",
"id": "https://plu.me/~/Blog%20Name/",
"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",
"inbox": "https://plu.me/~/Blog%20Name/inbox",
"name": "Blog Name",
"outbox": "https://plu.me/~/Blog%20Name/outbox",
"preferredUsername": "BlogName",
"publicKey": {
"id": "https://plu.me/~/BlogName/#main-key",
"owner": "https://plu.me/~/BlogName/",
"id": "https://plu.me/~/Blog%20Name/#main-key",
"owner": "https://plu.me/~/Blog%20Name/",
"publicKeyPem": blog.public_key
},
"source": {
@@ -1029,8 +1064,8 @@ pub(crate) mod tests {
let expected = json!({
"items": [],
"totalItems": 0,
"first": "https://plu.me/~/BlogName/outbox?page=1",
"last": "https://plu.me/~/BlogName/outbox?page=0",
"first": "https://plu.me/~/Blog%20Name/outbox?page=1",
"last": "https://plu.me/~/Blog%20Name/outbox?page=0",
"type": "OrderedCollection"
});
@@ -1049,8 +1084,8 @@ pub(crate) mod tests {
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",
"next": "https://plu.me/~/Blog%20Name/outbox?page=3",
"prev": "https://plu.me/~/Blog%20Name/outbox?page=1",
"items": [],
"type": "OrderedCollectionPage"
});
+18 -18
View File
@@ -381,7 +381,7 @@ impl AsObject<User, Delete, &DbConn> for Comment {
}
for n in Notification::find_for_comment(conn, &self)? {
n.delete(&**conn)?;
n.delete(conn)?;
}
diesel::update(comments::table)
@@ -431,7 +431,7 @@ mod tests {
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 (posts, users, blogs) = fill_database(conn);
let comment = Comment::insert(
conn,
@@ -457,19 +457,19 @@ mod tests {
fn self_federation() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (original_comm, posts, users, _blogs) = prepare_activity(&conn);
let act = original_comm.create_activity(&conn).unwrap();
let (original_comm, posts, users, _blogs) = prepare_activity(conn);
let act = original_comm.create_activity(conn).unwrap();
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),
"id": format!("https://plu.me/~/Blog%20Name/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",
"id": format!("https://plu.me/~/Blog%20Name/testing/comment/{}", original_comm.id),
"inReplyTo": "https://plu.me/~/Blog%20Name/testing",
"published": format_datetime(&original_comm.creation_date),
"summary": "My CW",
"tag": [
@@ -500,17 +500,17 @@ mod tests {
},
)
.unwrap();
let reply_act = reply.create_activity(&conn).unwrap();
let reply_act = reply.create_activity(conn).unwrap();
assert_json_eq!(to_value(&reply_act).unwrap(), json!({
"actor": "https://plu.me/@/user/",
"cc": ["https://plu.me/@/user/followers"],
"id": format!("https://plu.me/~/BlogName/testing/comment/{}/activity", reply.id),
"id": format!("https://plu.me/~/Blog%20Name/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),
"id": format!("https://plu.me/~/Blog%20Name/testing/comment/{}", reply.id),
"inReplyTo": format!("https://plu.me/~/Blog%20Name/testing/comment/{}", original_comm.id),
"published": format_datetime(&reply.creation_date),
"summary": "",
"tag": [],
@@ -522,12 +522,12 @@ mod tests {
}));
inbox(
&conn,
serde_json::to_value(original_comm.build_delete(&conn).unwrap()).unwrap(),
conn,
serde_json::to_value(original_comm.build_delete(conn).unwrap()).unwrap(),
)
.unwrap();
match inbox(&conn, to_value(act).unwrap()).unwrap() {
match inbox(conn, 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);
@@ -554,8 +554,8 @@ mod tests {
"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",
"id": format!("https://plu.me/~/Blog%20Name/testing/comment/{}", comment.id),
"inReplyTo": "https://plu.me/~/Blog%20Name/testing",
"published": format_datetime(&comment.creation_date),
"summary": "My CW",
"tag": [
@@ -584,9 +584,9 @@ mod tests {
let expected = json!({
"actor": "https://plu.me/@/admin/",
"id": format!("https://plu.me/~/BlogName/testing/comment/{}#delete", comment.id),
"id": format!("https://plu.me/~/Blog%20Name/testing/comment/{}#delete", comment.id),
"object": {
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", comment.id),
"id": format!("https://plu.me/~/Blog%20Name/testing/comment/{}", comment.id),
"type": "Tombstone"
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
+1 -1
View File
@@ -41,7 +41,7 @@ pub enum InvalidRocketConfig {
SecretKey,
}
fn get_rocket_config() -> Result<RocketConfig, InvalidRocketConfig> {
pub 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());
+2 -1
View File
@@ -69,7 +69,8 @@ pub(crate) mod tests {
impl CustomizeConnection<Connection, ConnError> for TestConnectionCustomizer {
fn on_acquire(&self, conn: &mut Connection) -> Result<(), ConnError> {
PragmaForeignKey.on_acquire(conn)?;
Ok(conn.begin_test_transaction().unwrap())
conn.begin_test_transaction().unwrap();
Ok(())
}
}
}
+15
View File
@@ -1,4 +1,5 @@
use crate::{
blocklisted_emails::BlocklistedEmail,
db_conn::DbConn,
schema::email_signups,
users::{NewUser, Role, User},
@@ -60,6 +61,8 @@ pub struct NewEmailSignup<'a> {
impl EmailSignup {
pub fn start(conn: &DbConn, email: &str) -> Result<Token> {
Self::ensure_email_not_blocked(conn, email)?;
conn.transaction(|| {
Self::ensure_user_not_exist_by_email(conn, email)?;
let _rows = Self::delete_existings_by_email(conn, email)?;
@@ -90,6 +93,8 @@ impl EmailSignup {
}
pub fn confirm(&self, conn: &DbConn) -> Result<()> {
Self::ensure_email_not_blocked(conn, &self.email)?;
conn.transaction(|| {
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
if self.expired() {
@@ -101,6 +106,8 @@ impl EmailSignup {
}
pub fn complete(&self, conn: &DbConn, username: String, password: String) -> Result<User> {
Self::ensure_email_not_blocked(conn, &self.email)?;
conn.transaction(|| {
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
let user = NewUser::new_local(
@@ -122,6 +129,14 @@ impl EmailSignup {
Ok(())
}
fn ensure_email_not_blocked(conn: &DbConn, email: &str) -> Result<()> {
if let Some(x) = BlocklistedEmail::matches_blocklist(conn, email)? {
Err(Error::Blocklisted(x.notify_user, x.notification_text))
} else {
Ok(())
}
}
fn ensure_user_not_exist_by_email(conn: &DbConn, email: &str) -> Result<()> {
if User::email_used(conn, email)? {
let _rows = Self::delete_existings_by_email(conn, email)?;
+1 -6
View File
@@ -107,12 +107,7 @@ impl Follow {
res.notify(conn)?;
let accept = res.build_accept(from, target, follow)?;
broadcast(
&*target,
accept,
vec![from.clone()],
CONFIG.proxy().cloned(),
);
broadcast(target, accept, vec![from.clone()], CONFIG.proxy().cloned());
Ok(res)
}
+10 -10
View File
@@ -82,9 +82,9 @@ pub(crate) mod tests {
use crate::post_authors::*;
use crate::posts::*;
let (users, blogs) = blog_fill_db(&conn);
let (users, blogs) = blog_fill_db(conn);
let post = Post::insert(
&conn,
conn,
NewPost {
blog_id: blogs[0].id,
slug: "testing".to_owned(),
@@ -102,7 +102,7 @@ pub(crate) mod tests {
.unwrap();
PostAuthor::insert(
&conn,
conn,
NewPostAuthor {
post_id: post.id,
author_id: users[0].id,
@@ -190,7 +190,7 @@ pub(crate) mod tests {
});
assert!(matches!(
super::inbox(&conn, act.clone()),
super::inbox(&conn, act),
Err(super::Error::Inbox(
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
))
@@ -221,7 +221,7 @@ pub(crate) mod tests {
});
assert!(matches!(
super::inbox(&conn, act.clone()),
super::inbox(&conn, act),
Err(super::Error::Inbox(
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
))
@@ -249,7 +249,7 @@ pub(crate) mod tests {
});
assert!(matches!(
super::inbox(&conn, act.clone()),
super::inbox(&conn, act),
Err(super::Error::Inbox(
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
))
@@ -268,7 +268,7 @@ pub(crate) mod tests {
"actor": users[0].ap_url,
"object": {
"type": "Article",
"id": "https://plu.me/~/BlogName/testing",
"id": "https://plu.me/~/Blog%20Name/testing",
"attributedTo": [users[0].ap_url, blogs[0].ap_url],
"content": "Hello.",
"name": "My Article",
@@ -324,7 +324,7 @@ pub(crate) mod tests {
});
assert!(matches!(
super::inbox(&conn, act.clone()),
super::inbox(&conn, act),
Err(super::Error::Inbox(
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
))
@@ -362,7 +362,7 @@ pub(crate) mod tests {
});
assert!(matches!(
super::inbox(&conn, act.clone()),
super::inbox(&conn, act),
Err(super::Error::Inbox(
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
))
@@ -397,7 +397,7 @@ pub(crate) mod tests {
});
assert!(matches!(
super::inbox(&conn, act.clone()),
super::inbox(&conn, act),
Err(super::Error::Inbox(
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
))
+1 -1
View File
@@ -523,7 +523,7 @@ pub(crate) mod tests {
.unwrap();
let inst = Instance::get(conn, inst.id).unwrap();
assert_eq!(inst.name, "NewName".to_owned());
assert_eq!(inst.open_registrations, false);
assert!(!inst.open_registrations);
assert_eq!(
inst.long_description.get(),
"[long_description](/with_link)"
+123 -9
View File
@@ -17,12 +17,18 @@ extern crate serde_json;
extern crate tantivy;
use activitystreams::iri_string;
use diesel::backend::Backend;
use diesel::sql_types::Text;
use diesel::types::{FromSql, ToSql};
use heck::ToUpperCamelCase;
pub use lettre;
pub use lettre::smtp;
use once_cell::sync::Lazy;
use plume_common::activity_pub::{inbox::InboxError, request, sign};
use plume_common::activity_pub::PreferredUsernameError;
use plume_common::activity_pub::{inbox::InboxError, request, sign, PreferredUsername};
use posts::PostEvent;
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
use std::{fmt, io::Write, string::ToString};
use users::UserEvent;
#[cfg(not(any(feature = "sqlite", feature = "postgres")))]
@@ -170,6 +176,13 @@ impl From<request::Error> for Error {
}
}
impl From<PreferredUsernameError> for Error {
fn from(err: PreferredUsernameError) -> Error {
tracing::trace!("{:?}", err);
Error::InvalidValue
}
}
pub type Result<T> = std::result::Result<T, Error>;
/// Adds a function to a model, that returns the first
@@ -177,7 +190,7 @@ pub type Result<T> = std::result::Result<T, Error>;
///
/// Usage:
///
/// ```rust
/// ```ignore
/// impl Model {
/// find_by!(model_table, name_of_the_function, field1 as String, field2 as i32);
/// }
@@ -201,7 +214,7 @@ macro_rules! find_by {
///
/// Usage:
///
/// ```rust
/// ```ignore
/// impl Model {
/// list_by!(model_table, name_of_the_function, field1 as String);
/// }
@@ -225,7 +238,7 @@ macro_rules! list_by {
///
/// # Usage
///
/// ```rust
/// ```ignore
/// impl Model {
/// get!(model_table);
/// }
@@ -248,7 +261,7 @@ macro_rules! get {
///
/// # Usage
///
/// ```rust
/// ```ignore
/// impl Model {
/// insert!(model_table, NewModelType);
/// }
@@ -280,7 +293,7 @@ macro_rules! insert {
///
/// # Usage
///
/// ```rust
/// ```ignore
/// impl Model {
/// last!(model_table);
/// }
@@ -301,7 +314,7 @@ macro_rules! last {
}
mod config;
pub use config::CONFIG;
pub use config::{get_rocket_config, Config, SearchTokenizerConfig, CONFIG};
pub fn ap_url(url: &str) -> String {
format!("https://{}", url)
@@ -334,10 +347,97 @@ impl SmtpNewWithAddr for smtp::SmtpClient {
}
}
#[derive(AsExpression, PartialEq, Eq, Clone, FromSqlRow, Debug)]
#[sql_type = "Text"]
pub enum Fqn {
Local(PreferredUsername),
Remote(PreferredUsername, String),
}
impl Fqn {
pub fn new_local(username: String) -> Result<Self> {
Ok(Self::Local(
PreferredUsername::new(username)?,
))
}
pub fn new_remote(username: String, domain: String) -> Result<Self> {
Ok(Self::Remote(
PreferredUsername::new(username)?,
domain,
))
}
pub fn make_local_string(base: &str) -> String {
base.to_upper_camel_case()
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.collect()
}
pub fn make_local(base: &str) -> Result<Self> {
Self::new_local(Self::make_local_string(base))
}
pub fn make_remote(base: &str, domain: String) -> Result<Self> {
Self::new_remote(Self::make_local_string(base), domain)
}
}
impl From<&Fqn> for String {
fn from(fqn: &Fqn) -> Self {
match fqn {
Fqn::Local(username) => username.to_string(),
Fqn::Remote(username, domain) => format!("{}@{}", username, domain),
}
}
}
impl fmt::Display for Fqn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
String::from(self).fmt(f)
}
}
impl<DB> ToSql<Text, DB> for Fqn
where
DB: diesel::backend::Backend,
{
fn to_sql<W: Write>(
&self,
out: &mut diesel::serialize::Output<W, DB>,
) -> diesel::serialize::Result {
let fqn = match self {
Self::Local(username) => username.to_string(),
Self::Remote(username, domain) => format!("{}@{}", username, domain),
};
ToSql::<Text, DB>::to_sql::<W>(&fqn, out)
}
}
impl<DB> FromSql<Text, DB> for Fqn
where
DB: diesel::backend::Backend,
String: FromSql<Text, DB>,
{
/// We use PreferredUsername::new_unchecked() because, even if bytes is invalid as `preferredUsername`,
/// we need return some value.
fn from_sql(bytes: Option<&<DB as Backend>::RawValue>) -> diesel::deserialize::Result<Self> {
let value = <String as FromSql<Text, DB>>::from_sql(bytes)?;
Ok(match value.rsplit_once('@') {
None => Self::Local(unsafe { PreferredUsername::new_unchecked(value) }),
Some((username, domain)) => Self::Remote(
unsafe { PreferredUsername::new_unchecked(username.into()) },
domain.into(),
),
})
}
}
#[cfg(test)]
#[macro_use]
mod tests {
use crate::{db_conn, migrations::IMPORTED_MIGRATIONS, Connection as Conn, CONFIG};
use crate::{db_conn, migrations::IMPORTED_MIGRATIONS, Connection as Conn, CONFIG, Fqn};
use chrono::{naive::NaiveDateTime, Datelike, Timelike};
use diesel::r2d2::ConnectionManager;
use plume_common::utils::random_hex;
@@ -354,7 +454,7 @@ mod tests {
};
}
pub fn db<'a>() -> db_conn::DbConn {
pub fn db() -> db_conn::DbConn {
db_conn::DbConn((*DB_POOL).get().unwrap())
}
@@ -398,6 +498,20 @@ mod tests {
dt.second()
)
}
#[test]
fn fqn() {
assert_eq!(
Fqn::new_local("admin".to_string()).unwrap(),
Fqn::new_local("admin".to_string()).unwrap()
);
assert!(Fqn::new_local("admin".to_string()).is_ok());
let fqn = Fqn::new_local("admin".to_string()).unwrap();
assert_eq!("admin".to_string(), String::from(&fqn));
let fqn = Fqn::new_local("admin".to_string()).unwrap();
assert_eq!("admin".to_string(), ToString::to_string(&fqn));
assert_eq!("admin".to_string(), fqn.to_string());
}
}
pub mod admin;
+8 -8
View File
@@ -199,14 +199,14 @@ mod tests {
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 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",
"id": "https://plu.me/@/admin/like/https://plu.me/~/Blog%20Name/testing",
"object": "https://plu.me/~/Blog%20Name/testing",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Like",
});
@@ -223,18 +223,18 @@ mod tests {
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 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",
"id": "https://plu.me/@/admin/like/https://plu.me/~/Blog%20Name/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",
"id": "https://plu.me/@/admin/like/https://plu.me/~/Blog%20Name/testing",
"object": "https://plu.me/~/Blog%20Name/testing",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Like",
},
+1 -1
View File
@@ -413,7 +413,7 @@ mod tests {
&List::find_for_user_by_name(conn, l1.user_id, &l1.name).unwrap(),
);
l_eq(
&&l1u,
&l1u,
&List::find_for_user_by_name(conn, l1u.user_id, &l1u.name).unwrap(),
);
Ok(())
+3 -3
View File
@@ -42,7 +42,7 @@ pub struct NewMedia {
pub owner_id: i32,
}
#[derive(PartialEq)]
#[derive(PartialEq, Eq)]
pub enum MediaCategory {
Image,
Audio,
@@ -343,7 +343,7 @@ pub(crate) mod tests {
use std::path::Path;
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Media>) {
let mut wd = current_dir().unwrap().to_path_buf();
let mut wd = current_dir().unwrap();
while wd.pop() {
if wd.join(".git").exists() {
set_current_dir(wd).unwrap();
@@ -456,7 +456,7 @@ pub(crate) mod tests {
let media = Media::insert(
conn,
NewMedia {
file_path: path.clone(),
file_path: path,
alt_text: "alt message".to_owned(),
is_remote: false,
remote_url: None,
+5 -5
View File
@@ -89,7 +89,7 @@ mod tests {
let request = PasswordResetRequest::find_by_token(&conn, &token)
.expect("couldn't retrieve request");
assert!(&token.len() > &32);
assert!(token.len() > 32);
assert_eq!(&request.email, &admin_email);
Ok(())
@@ -103,8 +103,8 @@ mod tests {
user_tests::fill_database(&conn);
let admin_email = "admin@example.com";
PasswordResetRequest::insert(&conn, &admin_email).expect("couldn't insert new request");
PasswordResetRequest::insert(&conn, &admin_email)
PasswordResetRequest::insert(&conn, admin_email).expect("couldn't insert new request");
PasswordResetRequest::insert(&conn, admin_email)
.expect("couldn't insert second request");
let count = password_reset_requests::table.count().get_result(&*conn);
@@ -132,7 +132,7 @@ mod tests {
.execute(&*conn)
.expect("could not insert request");
match PasswordResetRequest::find_by_token(&conn, &token) {
match PasswordResetRequest::find_by_token(&conn, token) {
Err(Error::Expired) => (),
_ => panic!("Received unexpected result finding expired token"),
}
@@ -148,7 +148,7 @@ mod tests {
user_tests::fill_database(&conn);
let admin_email = "admin@example.com";
let token = PasswordResetRequest::insert(&conn, &admin_email)
let token = PasswordResetRequest::insert(&conn, admin_email)
.expect("couldn't insert new request");
PasswordResetRequest::find_and_delete_by_token(&conn, &token)
.expect("couldn't find and delete request");
+39 -35
View File
@@ -1,7 +1,7 @@
use crate::{
ap_url, blogs::Blog, db_conn::DbConn, instance::Instance, medias::Media, mentions::Mention,
post_authors::*, safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User,
Connection, Error, PostEvent::*, Result, CONFIG, POST_CHAN,
Connection, Error, Fqn, PostEvent::*, Result, CONFIG, POST_CHAN,
};
use activitystreams::{
activity::{Create, Delete, Update},
@@ -28,7 +28,7 @@ use riker::actors::{Publish, Tell};
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
static BLOG_FQN_CACHE: Lazy<Mutex<HashMap<i32, String>>> = Lazy::new(|| Mutex::new(HashMap::new()));
static BLOG_FQN_CACHE: Lazy<Mutex<HashMap<i32, Fqn>>> = Lazy::new(|| Mutex::new(HashMap::new()));
#[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)]
#[changeset_options(treat_none_as_null = "true")]
@@ -134,7 +134,7 @@ impl Post {
.filter(posts::published.eq(true))
.count()
.load(conn)?
.get(0)
.first()
.cloned()
.ok_or(Error::NotFound)
}
@@ -255,7 +255,7 @@ impl Post {
ap_url(&format!(
"{}/~/{}/{}/",
CONFIG.base_url,
blog.fqn,
&blog.fqn,
iri_percent_encode_seg(slug)
))
}
@@ -298,9 +298,9 @@ impl Post {
/// This caches query result. The best way to cache query result is holding it in `Post`s field
/// but Diesel doesn't allow it currently.
/// If sometime Diesel allow it, this method should be removed.
pub fn get_blog_fqn(&self, conn: &Connection) -> String {
pub fn get_blog_fqn(&self, conn: &Connection) -> Fqn {
if let Some(blog_fqn) = BLOG_FQN_CACHE.lock().unwrap().get(&self.blog_id) {
return blog_fqn.to_string();
return blog_fqn.to_owned();
}
let blog_fqn = self.get_blog(conn).unwrap().fqn;
BLOG_FQN_CACHE
@@ -465,7 +465,7 @@ impl Post {
.collect::<HashSet<_>>();
for (m, id) in &mentions {
if !old_user_mentioned.contains(id) {
Mention::from_activity(&*conn, m, self.id, true, true)?;
Mention::from_activity(conn, m, self.id, true, true)?;
}
}
@@ -488,7 +488,7 @@ impl Post {
.filter_map(|t| t.name.as_ref().map(|name| name.as_str().to_string()))
.collect::<HashSet<_>>();
let old_tags = Tag::for_post(&*conn, self.id)?;
let old_tags = Tag::for_post(conn, self.id)?;
let old_tags_name = old_tags
.iter()
.filter_map(|tag| {
@@ -525,7 +525,7 @@ impl Post {
.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)?;
let old_tags = Tag::for_post(conn, self.id)?;
let old_tags_name = old_tags
.iter()
.filter_map(|tag| {
@@ -756,7 +756,11 @@ impl FromId<DbConn> for Post {
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)
NaiveDateTime::from_timestamp_opt(
timestamp_secs,
timestamp_nanos as u32,
)
.unwrap()
}),
subtitle: article
.summary()
@@ -1036,7 +1040,7 @@ mod tests {
let post = &posts[0];
let mentioned = &users[1];
let mention = Mention::insert(
&conn,
conn,
NewMention {
mentioned_id: mentioned.id,
post_id: Some(post.id),
@@ -1044,7 +1048,7 @@ mod tests {
},
)
.unwrap();
(post.to_owned(), mention.to_owned(), posts, users, blogs)
(post.to_owned(), mention, posts, users, blogs)
}
// creates a post, get it's Create activity, delete the post,
@@ -1053,9 +1057,9 @@ mod tests {
fn self_federation() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (_, users, blogs) = fill_database(&conn);
let (_, users, blogs) = fill_database(conn);
let post = Post::insert(
&conn,
conn,
NewPost {
blog_id: blogs[0].id,
slug: "yo".into(),
@@ -1072,19 +1076,19 @@ mod tests {
)
.unwrap();
PostAuthor::insert(
&conn,
conn,
NewPostAuthor {
post_id: post.id,
author_id: users[0].id,
},
)
.unwrap();
let create = post.create_activity(&conn).unwrap();
post.delete(&conn).unwrap();
let create = post.create_activity(conn).unwrap();
post.delete(conn).unwrap();
match inbox(&conn, serde_json::to_value(create).unwrap()).unwrap() {
match inbox(conn, serde_json::to_value(create).unwrap()).unwrap() {
InboxResult::Post(p) => {
assert!(p.is_author(&conn, users[0].id).unwrap());
assert!(p.is_author(conn, users[0].id).unwrap());
assert_eq!(p.source, "Hello".to_owned());
assert_eq!(p.blog_id, blogs[0].id);
assert_eq!(p.content, SafeString::new("Hello"));
@@ -1105,10 +1109,10 @@ mod tests {
let act = post.to_activity(&conn)?;
let expected = json!({
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"],
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/Blog%20Name/"],
"cc": [],
"content": "Hello",
"id": "https://plu.me/~/BlogName/testing",
"id": "https://plu.me/~/Blog%20Name/testing",
"license": "WTFPL",
"name": "Testing",
"published": format_datetime(&post.creation_date),
@@ -1126,7 +1130,7 @@ mod tests {
],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Article",
"url": "https://plu.me/~/BlogName/testing"
"url": "https://plu.me/~/Blog%20Name/testing"
});
assert_json_eq!(to_value(act)?, expected);
@@ -1145,12 +1149,12 @@ mod tests {
let expected = json!({
"actor": "https://plu.me/@/admin/",
"cc": [],
"id": "https://plu.me/~/BlogName/testing/activity",
"id": "https://plu.me/~/Blog%20Name/testing/activity",
"object": {
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"],
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/Blog%20Name/"],
"cc": [],
"content": "Hello",
"id": "https://plu.me/~/BlogName/testing",
"id": "https://plu.me/~/Blog%20Name/testing",
"license": "WTFPL",
"name": "Testing",
"published": format_datetime(&post.creation_date),
@@ -1168,7 +1172,7 @@ mod tests {
],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Article",
"url": "https://plu.me/~/BlogName/testing"
"url": "https://plu.me/~/Blog%20Name/testing"
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Create"
@@ -1190,12 +1194,12 @@ mod tests {
let expected = json!({
"actor": "https://plu.me/@/admin/",
"cc": [],
"id": "https://plu.me/~/BlogName/testing/update-",
"id": "https://plu.me/~/Blog%20Name/testing/update-",
"object": {
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"],
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/Blog%20Name/"],
"cc": [],
"content": "Hello",
"id": "https://plu.me/~/BlogName/testing",
"id": "https://plu.me/~/Blog%20Name/testing",
"license": "WTFPL",
"name": "Testing",
"published": format_datetime(&post.creation_date),
@@ -1213,7 +1217,7 @@ mod tests {
],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Article",
"url": "https://plu.me/~/BlogName/testing"
"url": "https://plu.me/~/Blog%20Name/testing"
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Update"
@@ -1221,11 +1225,11 @@ mod tests {
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");
let (id_pre, id_post) = id.rsplit_once('-').unwrap();
assert_eq!(post.ap_url, "https://plu.me/~/Blog%20Name/testing");
assert_eq!(
id_pre,
to_value("\"https://plu.me/~/BlogName/testing/update")
to_value("\"https://plu.me/~/Blog%20Name/testing/update")
.unwrap()
.as_str()
.unwrap()
@@ -1255,9 +1259,9 @@ mod tests {
let expected = json!({
"actor": "https://plu.me/@/admin/",
"id": "https://plu.me/~/BlogName/testing#delete",
"id": "https://plu.me/~/Blog%20Name/testing#delete",
"object": {
"id": "https://plu.me/~/BlogName/testing",
"id": "https://plu.me/~/Blog%20Name/testing",
"type": "Tombstone"
},
"to": [
+6
View File
@@ -45,6 +45,12 @@ impl Actor for RemoteFetchActor {
RemoteUserFound(user) => match self.conn.get() {
Ok(conn) => {
let conn = DbConn(conn);
if user
.get_instance(&conn)
.map_or(false, |instance| instance.blocked)
{
return;
}
// Don't call these functions in parallel
// for the case database connections limit is too small
fetch_and_cache_articles(&user, &conn);
+8 -8
View File
@@ -229,14 +229,14 @@ mod test {
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 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",
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/Blog%20Name/testing",
"object": "https://plu.me/~/Blog%20Name/testing",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Announce",
});
@@ -253,18 +253,18 @@ mod test {
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 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",
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/Blog%20Name/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",
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/Blog%20Name/testing",
"object": "https://plu.me/~/Blog%20Name/testing",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Announce"
},
+1 -1
View File
@@ -93,7 +93,7 @@ fn url_add_prefix(url: &str) -> Option<Cow<'_, str>> {
}
}
#[derive(Debug, Clone, PartialEq, AsExpression, FromSqlRow, Default)]
#[derive(Debug, Clone, PartialEq, Eq, AsExpression, FromSqlRow, Default)]
#[sql_type = "Text"]
pub struct SafeString {
value: String,
+9 -9
View File
@@ -51,6 +51,14 @@ table! {
}
}
table! {
comment_seers (id) {
id -> Int4,
comment_id -> Int4,
user_id -> Int4,
}
}
table! {
comments (id) {
id -> Int4,
@@ -66,14 +74,6 @@ table! {
}
}
table! {
comment_seers (id) {
id -> Int4,
comment_id -> Int4,
user_id -> Int4,
}
}
table! {
email_blocklist (id) {
id -> Int4,
@@ -314,8 +314,8 @@ allow_tables_to_appear_in_same_query!(
apps,
blog_authors,
blogs,
comments,
comment_seers,
comments,
email_blocklist,
email_signups,
follows,
+28 -16
View File
@@ -77,6 +77,7 @@ impl ActorFactoryArgs<(Arc<Searcher>, DbPool)> for SearchActor {
#[cfg(test)]
mod tests {
use crate::diesel::Connection;
use crate::Fqn;
use crate::{
blog_authors::{BlogAuthor, NewBlogAuthor},
blogs::{Blog, NewBlog},
@@ -108,7 +109,7 @@ mod tests {
let searcher = Arc::new(get_searcher(&CONFIG.search_tokenizers));
SearchActor::init(searcher.clone(), db_pool.clone());
let conn = db_pool.clone().get().unwrap();
let conn = db_pool.get().unwrap();
let title = random_hex()[..8].to_owned();
let (_instance, _user, blog) = fill_database(&conn);
@@ -161,41 +162,52 @@ mod tests {
long_description_html: "<p>Good morning</p>".to_string(),
short_description: SafeString::new("Hello"),
short_description_html: "<p>Hello</p>".to_string(),
name: random_hex().to_string(),
name: random_hex(),
open_registrations: true,
public_domain: random_hex().to_string(),
public_domain: random_hex(),
},
)
.unwrap();
let user = User::insert(
conn,
NewUser {
username: random_hex().to_string(),
display_name: random_hex().to_string(),
outbox_url: random_hex().to_string(),
inbox_url: random_hex().to_string(),
username: random_hex(),
display_name: random_hex(),
outbox_url: random_hex(),
inbox_url: random_hex(),
summary: "".to_string(),
email: None,
hashed_password: None,
instance_id: instance.id,
ap_url: random_hex().to_string(),
ap_url: random_hex(),
private_key: None,
public_key: "".to_string(),
shared_inbox_url: None,
followers_endpoint: random_hex().to_string(),
followers_endpoint: random_hex(),
avatar_id: None,
summary_html: SafeString::new(""),
role: 0,
fqn: random_hex().to_string(),
fqn: random_hex(),
},
)
.unwrap();
let mut blog = NewBlog::default();
blog.instance_id = instance.id;
blog.actor_id = random_hex().to_string();
blog.ap_url = random_hex().to_string();
blog.inbox_url = random_hex().to_string();
blog.outbox_url = random_hex().to_string();
let title = random_hex();
let blog = NewBlog {
instance_id: instance.id,
actor_id: random_hex(),
ap_url: random_hex(),
inbox_url: random_hex(),
outbox_url: random_hex(),
fqn: Fqn::make_local(&title).unwrap(),
title,
summary: Default::default(),
summary_html: Default::default(),
private_key: Default::default(),
public_key: Default::default(),
icon_id: Default::default(),
banner_id: Default::default(),
theme: Default::default(),
};
let blog = Blog::insert(conn, blog).unwrap();
BlogAuthor::insert(
conn,
+1 -1
View File
@@ -154,7 +154,7 @@ pub(crate) mod tests {
},
)
.unwrap();
searcher.add_document(&conn, &post).unwrap();
searcher.add_document(conn, &post).unwrap();
searcher.commit();
assert_eq!(
searcher.search_document(conn, Query::from_str(&title).unwrap(), (0, 1))[0].id,
+17 -9
View File
@@ -94,7 +94,7 @@ macro_rules! gen_to_string {
)*
$(
for val in &$self.$date {
$result.push_str(&format!("{}:{} ", stringify!($date), NaiveDate::from_num_days_from_ce(*val as i32).format("%Y-%m-%d")));
$result.push_str(&format!("{}:{} ", stringify!($date), NaiveDate::from_num_days_from_ce_opt(*val as i32).unwrap().format("%Y-%m-%d")));
}
)*
}
@@ -180,12 +180,16 @@ impl PlumeQuery {
if self.before.is_some() || self.after.is_some() {
// if at least one range bound is provided
let after = self
.after
.unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce()));
let after = self.after.unwrap_or_else(|| {
i64::from(
NaiveDate::from_ymd_opt(2000, 1, 1)
.unwrap()
.num_days_from_ce(),
)
});
let before = self
.before
.unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce()));
.unwrap_or_else(|| i64::from(Utc::now().date_naive().num_days_from_ce()));
let field = Searcher::schema().get_field("creation_date").unwrap();
let range =
RangeQuery::new_i64_bounds(field, Bound::Included(after), Bound::Included(before));
@@ -202,16 +206,20 @@ impl PlumeQuery {
pub fn before<D: Datelike>(&mut self, date: &D) -> &mut Self {
let before = self
.before
.unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce()));
.unwrap_or_else(|| i64::from(Utc::now().date_naive().num_days_from_ce()));
self.before = Some(cmp::min(before, i64::from(date.num_days_from_ce())));
self
}
// documents older than the provided date will be ignored
pub fn after<D: Datelike>(&mut self, date: &D) -> &mut Self {
let after = self
.after
.unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce()));
let after = self.after.unwrap_or_else(|| {
i64::from(
NaiveDate::from_ymd_opt(2000, 1, 1)
.unwrap()
.num_days_from_ce(),
)
});
self.after = Some(cmp::max(after, i64::from(date.num_days_from_ce())));
self
}
+1 -1
View File
@@ -92,7 +92,7 @@ mod tests {
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_href(ap_url("https://plu.me/tag/a_tag").parse::<IriString>()?);
ht.set_name("a_tag".to_string());
let tag = Tag::from_activity(conn, &ht, post_id, true)?;
+108 -118
View File
@@ -14,7 +14,7 @@ pub(crate) mod query;
pub use self::query::Kind;
use self::query::{QueryError, TimelineQuery};
#[derive(Clone, Debug, PartialEq, Queryable, Identifiable, AsChangeset)]
#[derive(Clone, Debug, PartialEq, Eq, Queryable, Identifiable, AsChangeset)]
#[table_name = "timeline_definition"]
pub struct Timeline {
pub id: i32,
@@ -282,73 +282,63 @@ mod tests {
fn test_timeline() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let users = userTests::fill_database(&conn);
let users = userTests::fill_database(conn);
let mut tl1_u1 = Timeline::new_for_user(
&conn,
conn,
users[0].id,
"my timeline".to_owned(),
"all".to_owned(),
)
.unwrap();
List::new(
&conn,
"languages I speak",
Some(&users[1]),
ListType::Prefix,
)
.unwrap();
List::new(conn, "languages I speak", Some(&users[1]), ListType::Prefix).unwrap();
let tl2_u1 = Timeline::new_for_user(
&conn,
conn,
users[0].id,
"another timeline".to_owned(),
"followed".to_owned(),
)
.unwrap();
let tl1_u2 = Timeline::new_for_user(
&conn,
conn,
users[1].id,
"english posts".to_owned(),
"lang in \"languages I speak\"".to_owned(),
)
.unwrap();
let tl1_instance = Timeline::new_for_instance(
&conn,
conn,
"english posts".to_owned(),
"license in [cc]".to_owned(),
)
.unwrap();
assert_eq!(tl1_u1, Timeline::get(&conn, tl1_u1.id).unwrap());
assert_eq!(tl1_u1, Timeline::get(conn, tl1_u1.id).unwrap());
assert_eq!(
tl2_u1,
Timeline::find_for_user_by_name(&conn, Some(users[0].id), "another timeline")
Timeline::find_for_user_by_name(conn, Some(users[0].id), "another timeline")
.unwrap()
);
assert_eq!(
tl1_instance,
Timeline::find_for_user_by_name(&conn, None, "english posts").unwrap()
Timeline::find_for_user_by_name(conn, None, "english posts").unwrap()
);
let tl_u1 = Timeline::list_for_user(&conn, Some(users[0].id)).unwrap();
let tl_u1 = Timeline::list_for_user(conn, Some(users[0].id)).unwrap();
assert_eq!(3, tl_u1.len()); // it is not 2 because there is a "Your feed" tl created for each user automatically
assert!(tl_u1.iter().fold(false, |res, tl| { res || *tl == tl1_u1 }));
assert!(tl_u1.iter().fold(false, |res, tl| { res || *tl == tl2_u1 }));
assert!(tl_u1.iter().any(|tl| *tl == tl1_u1));
assert!(tl_u1.iter().any(|tl| *tl == tl2_u1));
let tl_instance = Timeline::list_for_user(&conn, None).unwrap();
let tl_instance = Timeline::list_for_user(conn, None).unwrap();
assert_eq!(3, tl_instance.len()); // there are also the local and federated feed by default
assert!(tl_instance
.iter()
.fold(false, |res, tl| { res || *tl == tl1_instance }));
assert!(tl_instance.iter().any(|tl| *tl == tl1_instance));
tl1_u1.name = "My Super TL".to_owned();
let new_tl1_u2 = tl1_u2.update(&conn).unwrap();
let new_tl1_u2 = tl1_u2.update(conn).unwrap();
let tl_u2 = Timeline::list_for_user(&conn, Some(users[1].id)).unwrap();
let tl_u2 = Timeline::list_for_user(conn, Some(users[1].id)).unwrap();
assert_eq!(2, tl_u2.len()); // same here
assert!(tl_u2
.iter()
.fold(false, |res, tl| { res || *tl == new_tl1_u2 }));
assert!(tl_u2.iter().any(|tl| *tl == new_tl1_u2));
Ok(())
});
@@ -358,48 +348,48 @@ mod tests {
fn test_timeline_creation_error() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let users = userTests::fill_database(&conn);
let users = userTests::fill_database(conn);
assert!(Timeline::new_for_user(
&conn,
conn,
users[0].id,
"my timeline".to_owned(),
"invalid keyword".to_owned(),
)
.is_err());
assert!(Timeline::new_for_instance(
&conn,
conn,
"my timeline".to_owned(),
"invalid keyword".to_owned(),
)
.is_err());
assert!(Timeline::new_for_user(
&conn,
conn,
users[0].id,
"my timeline".to_owned(),
"author in non_existant_list".to_owned(),
)
.is_err());
assert!(Timeline::new_for_instance(
&conn,
conn,
"my timeline".to_owned(),
"lang in dont-exist".to_owned(),
)
.is_err());
List::new(&conn, "friends", Some(&users[0]), ListType::User).unwrap();
List::new(&conn, "idk", None, ListType::Blog).unwrap();
List::new(conn, "friends", Some(&users[0]), ListType::User).unwrap();
List::new(conn, "idk", None, ListType::Blog).unwrap();
assert!(Timeline::new_for_user(
&conn,
conn,
users[0].id,
"my timeline".to_owned(),
"blog in friends".to_owned(),
)
.is_err());
assert!(Timeline::new_for_instance(
&conn,
conn,
"my timeline".to_owned(),
"not author in idk".to_owned(),
)
@@ -413,10 +403,10 @@ mod tests {
fn test_simple_match() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(&conn);
let (users, blogs) = blogTests::fill_database(conn);
let gnu_tl = Timeline::new_for_user(
&conn,
conn,
users[0].id,
"GNU timeline".to_owned(),
"license in [AGPL, LGPL, GPL]".to_owned(),
@@ -424,7 +414,7 @@ mod tests {
.unwrap();
let gnu_post = Post::insert(
&conn,
conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug".to_string(),
@@ -440,10 +430,10 @@ mod tests {
},
)
.unwrap();
assert!(gnu_tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
assert!(gnu_tl.matches(conn, &gnu_post, Kind::Original).unwrap());
let non_free_post = Post::insert(
&conn,
conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug2".to_string(),
@@ -460,7 +450,7 @@ mod tests {
)
.unwrap();
assert!(!gnu_tl
.matches(&conn, &non_free_post, Kind::Original)
.matches(conn, &non_free_post, Kind::Original)
.unwrap());
Ok(())
@@ -471,9 +461,9 @@ mod tests {
fn test_complex_match() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(&conn);
let (users, blogs) = blogTests::fill_database(conn);
Follow::insert(
&conn,
conn,
NewFollow {
follower_id: users[0].id,
following_id: users[1].id,
@@ -483,11 +473,11 @@ mod tests {
.unwrap();
let fav_blogs_list =
List::new(&conn, "fav_blogs", Some(&users[0]), ListType::Blog).unwrap();
fav_blogs_list.add_blogs(&conn, &[blogs[0].id]).unwrap();
List::new(conn, "fav_blogs", Some(&users[0]), ListType::Blog).unwrap();
fav_blogs_list.add_blogs(conn, &[blogs[0].id]).unwrap();
let my_tl = Timeline::new_for_user(
&conn,
conn,
users[0].id,
"My timeline".to_owned(),
"blog in fav_blogs and not has_cover or local and followed exclude likes"
@@ -496,7 +486,7 @@ mod tests {
.unwrap();
let post = Post::insert(
&conn,
conn,
NewPost {
blog_id: blogs[0].id,
slug: "about-linux".to_string(),
@@ -512,10 +502,10 @@ mod tests {
},
)
.unwrap();
assert!(my_tl.matches(&conn, &post, Kind::Original).unwrap()); // matches because of "blog in fav_blogs" (and there is no cover)
assert!(my_tl.matches(conn, &post, Kind::Original).unwrap()); // matches because of "blog in fav_blogs" (and there is no cover)
let post = Post::insert(
&conn,
conn,
NewPost {
blog_id: blogs[1].id,
slug: "about-linux-2".to_string(),
@@ -533,7 +523,7 @@ mod tests {
},
)
.unwrap();
assert!(!my_tl.matches(&conn, &post, Kind::Like(&users[1])).unwrap());
assert!(!my_tl.matches(conn, &post, Kind::Like(&users[1])).unwrap());
Ok(())
});
@@ -543,17 +533,17 @@ mod tests {
fn test_add_to_all_timelines() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(&conn);
let (users, blogs) = blogTests::fill_database(conn);
let gnu_tl = Timeline::new_for_user(
&conn,
conn,
users[0].id,
"GNU timeline".to_owned(),
"license in [AGPL, LGPL, GPL]".to_owned(),
)
.unwrap();
let non_gnu_tl = Timeline::new_for_user(
&conn,
conn,
users[0].id,
"Stallman disapproved timeline".to_owned(),
"not license in [AGPL, LGPL, GPL]".to_owned(),
@@ -561,7 +551,7 @@ mod tests {
.unwrap();
let gnu_post = Post::insert(
&conn,
conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug".to_string(),
@@ -579,7 +569,7 @@ mod tests {
.unwrap();
let non_free_post = Post::insert(
&conn,
conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug2".to_string(),
@@ -596,13 +586,13 @@ mod tests {
)
.unwrap();
Timeline::add_to_all_timelines(&conn, &gnu_post, Kind::Original).unwrap();
Timeline::add_to_all_timelines(&conn, &non_free_post, Kind::Original).unwrap();
Timeline::add_to_all_timelines(conn, &gnu_post, Kind::Original).unwrap();
Timeline::add_to_all_timelines(conn, &non_free_post, Kind::Original).unwrap();
let res = gnu_tl.get_latest(&conn, 2).unwrap();
let res = gnu_tl.get_latest(conn, 2).unwrap();
assert_eq!(res.len(), 1);
assert_eq!(res[0].id, gnu_post.id);
let res = non_gnu_tl.get_latest(&conn, 2).unwrap();
let res = non_gnu_tl.get_latest(conn, 2).unwrap();
assert_eq!(res.len(), 1);
assert_eq!(res[0].id, non_free_post.id);
@@ -614,10 +604,10 @@ mod tests {
fn test_matches_lists_direct() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(&conn);
let (users, blogs) = blogTests::fill_database(conn);
let gnu_post = Post::insert(
&conn,
conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug".to_string(),
@@ -634,63 +624,63 @@ mod tests {
)
.unwrap();
gnu_post
.update_tags(&conn, vec![Tag::build_activity("free".to_owned()).unwrap()])
.update_tags(conn, vec![Tag::build_activity("free".to_owned()).unwrap()])
.unwrap();
PostAuthor::insert(
&conn,
conn,
NewPostAuthor {
post_id: gnu_post.id,
author_id: blogs[0].list_authors(&conn).unwrap()[0].id,
author_id: blogs[0].list_authors(conn).unwrap()[0].id,
},
)
.unwrap();
let tl = Timeline::new_for_user(
&conn,
conn,
users[0].id,
"blog timeline".to_owned(),
format!("blog in [{}]", blogs[0].fqn),
)
.unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
let tl = Timeline::new_for_user(
&conn,
conn,
users[0].id,
"blog timeline".to_owned(),
"blog in [no_one@nowhere]".to_owned(),
)
.unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
let tl = Timeline::new_for_user(
&conn,
conn,
users[0].id,
"author timeline".to_owned(),
format!(
"author in [{}]",
blogs[0].list_authors(&conn).unwrap()[0].fqn
blogs[0].list_authors(conn).unwrap()[0].fqn
),
)
.unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
let tl = Timeline::new_for_user(
&conn,
conn,
users[0].id,
"author timeline".to_owned(),
format!("author in [{}]", users[2].fqn),
)
.unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
assert!(tl
.matches(&conn, &gnu_post, Kind::Reshare(&users[2]))
.matches(conn, &gnu_post, Kind::Reshare(&users[2]))
.unwrap());
assert!(!tl.matches(&conn, &gnu_post, Kind::Like(&users[2])).unwrap());
tl.delete(&conn).unwrap();
assert!(!tl.matches(conn, &gnu_post, Kind::Like(&users[2])).unwrap());
tl.delete(conn).unwrap();
let tl = Timeline::new_for_user(
&conn,
conn,
users[0].id,
"author timeline".to_owned(),
format!(
@@ -699,50 +689,50 @@ mod tests {
),
)
.unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
assert!(!tl
.matches(&conn, &gnu_post, Kind::Reshare(&users[2]))
.matches(conn, &gnu_post, Kind::Reshare(&users[2]))
.unwrap());
assert!(tl.matches(&conn, &gnu_post, Kind::Like(&users[2])).unwrap());
tl.delete(&conn).unwrap();
assert!(tl.matches(conn, &gnu_post, Kind::Like(&users[2])).unwrap());
tl.delete(conn).unwrap();
let tl = Timeline::new_for_user(
&conn,
conn,
users[0].id,
"tag timeline".to_owned(),
"tags in [free]".to_owned(),
)
.unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
let tl = Timeline::new_for_user(
&conn,
conn,
users[0].id,
"tag timeline".to_owned(),
"tags in [private]".to_owned(),
)
.unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
let tl = Timeline::new_for_user(
&conn,
conn,
users[0].id,
"english timeline".to_owned(),
"lang in [en]".to_owned(),
)
.unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
let tl = Timeline::new_for_user(
&conn,
conn,
users[0].id,
"franco-italian timeline".to_owned(),
"lang in [fr, it]".to_owned(),
)
.unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
Ok(())
});
@@ -786,10 +776,10 @@ mod tests {
fn test_matches_keyword() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(&conn);
let (users, blogs) = blogTests::fill_database(conn);
let gnu_post = Post::insert(
&conn,
conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug".to_string(),
@@ -807,61 +797,61 @@ mod tests {
.unwrap();
let tl = Timeline::new_for_user(
&conn,
conn,
users[0].id,
"Linux title".to_owned(),
"title contains Linux".to_owned(),
)
.unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
let tl = Timeline::new_for_user(
&conn,
conn,
users[0].id,
"Microsoft title".to_owned(),
"title contains Microsoft".to_owned(),
)
.unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
let tl = Timeline::new_for_user(
&conn,
conn,
users[0].id,
"Linux subtitle".to_owned(),
"subtitle contains Stallman".to_owned(),
)
.unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
let tl = Timeline::new_for_user(
&conn,
conn,
users[0].id,
"Microsoft subtitle".to_owned(),
"subtitle contains Nadella".to_owned(),
)
.unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
let tl = Timeline::new_for_user(
&conn,
conn,
users[0].id,
"Linux content".to_owned(),
"content contains Linux".to_owned(),
)
.unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
let tl = Timeline::new_for_user(
&conn,
conn,
users[0].id,
"Microsoft content".to_owned(),
"subtitle contains Windows".to_owned(),
)
.unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
Ok(())
});
+3 -3
View File
@@ -11,7 +11,7 @@ use crate::{
use plume_common::activity_pub::inbox::AsActor;
use whatlang::{self, Lang};
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QueryError {
SyntaxError(usize, usize, String),
UnexpectedEndOfQuery,
@@ -20,7 +20,7 @@ pub enum QueryError {
pub type QueryResult<T> = std::result::Result<T, QueryError>;
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kind<'a> {
Original,
Reshare(&'a User),
@@ -292,7 +292,7 @@ impl WithList {
WithList::Author { boosts, likes } => match kind {
Kind::Original => Ok(list
.iter()
.filter_map(|a| User::find_by_fqn(&*conn, a).ok())
.filter_map(|a| User::find_by_fqn(conn, a).ok())
.any(|a| post.is_author(conn, a.id).unwrap_or(false))),
Kind::Reshare(u) => {
if *boosts {
+38 -57
View File
@@ -187,7 +187,7 @@ impl User {
users::table
.filter(users::instance_id.eq(Instance::get_local()?.id))
.count()
.get_result(&*conn)
.get_result(conn)
.map_err(Error::from)
}
@@ -246,20 +246,7 @@ impl User {
fn fetch(url: &str) -> Result<CustomPerson> {
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 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()
let json = serde_json::from_str::<CustomPerson>(text)?;
Ok(json)
}
@@ -269,23 +256,13 @@ impl User {
pub fn refetch(&self, conn: &Connection) -> Result<()> {
User::fetch(&self.ap_url.clone()).and_then(|json| {
let avatar = Media::save_remote(
conn,
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 avatar = json
.icon()
.and_then(|icon| icon.iter().next())
.and_then(|i| i.clone().extend::<Image, ImageType>().ok())
.and_then(|image| image)
.and_then(|image| image.id_unchecked().map(|url| url.to_string()))
.and_then(|url| Media::save_remote(conn, url, self).ok());
let pub_key = &json.ext_one.public_key.public_key_pem;
diesel::update(self)
@@ -435,7 +412,7 @@ impl User {
}
// if no user was found, and we were unable to auto-register from ldap
// fake-verify a password, and return an error.
let other = User::get(&*conn, 1)
let other = User::get(conn, 1)
.expect("No user is registered")
.hashed_password;
other.map(|pass| bcrypt::verify(password, &pass));
@@ -784,7 +761,7 @@ impl User {
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_preferred_username(&self.fqn);
actor.set_followers(self.followers_endpoint.parse()?);
if let Some(shared_inbox_url) = self.shared_inbox_url.clone() {
@@ -931,7 +908,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for User {
.cookies()
.get_private(AUTH_COOKIE)
.and_then(|cookie| cookie.value().parse().ok())
.and_then(|id| User::get(&*conn, id).ok())
.and_then(|id| User::get(&conn, id).ok())
.or_forward(())
}
}
@@ -960,6 +937,10 @@ impl FromId<DbConn> for User {
.to_string();
if username.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
tracing::error!(
"preferredUsername includes invalid character(s): {}",
&username
);
return Err(Error::InvalidValue);
}
@@ -1232,7 +1213,7 @@ pub(crate) mod tests {
)
.unwrap();
other.avatar_id = Some(avatar.id);
let other = other.save_changes::<User>(&*conn).unwrap();
let other = other.save_changes::<User>(conn).unwrap();
vec![admin, user, other]
}
@@ -1335,11 +1316,11 @@ pub(crate) mod tests {
fn delete() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let inserted = fill_database(&conn);
let inserted = fill_database(conn);
assert!(User::get(&conn, inserted[0].id).is_ok());
inserted[0].delete(&conn).unwrap();
assert!(User::get(&conn, inserted[0].id).is_err());
assert!(User::get(conn, inserted[0].id).is_ok());
inserted[0].delete(conn).unwrap();
assert!(User::get(conn, inserted[0].id).is_err());
Ok(())
});
}
@@ -1348,20 +1329,20 @@ pub(crate) mod tests {
fn admin() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let inserted = fill_database(&conn);
let inserted = fill_database(conn);
let local_inst = Instance::get_local().unwrap();
let mut i = 0;
while local_inst.has_admin(&conn).unwrap() {
while local_inst.has_admin(conn).unwrap() {
assert!(i < 100); //prevent from looping indefinitelly
local_inst
.main_admin(&conn)
.main_admin(conn)
.unwrap()
.set_role(&conn, Role::Normal)
.set_role(conn, Role::Normal)
.unwrap();
i += 1;
}
inserted[0].set_role(&conn, Role::Admin).unwrap();
assert_eq!(inserted[0].id, local_inst.main_admin(&conn).unwrap().id);
inserted[0].set_role(conn, Role::Admin).unwrap();
assert_eq!(inserted[0].id, local_inst.main_admin(conn).unwrap().id);
Ok(())
});
}
@@ -1370,9 +1351,9 @@ pub(crate) mod tests {
fn auth() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
fill_database(&conn);
fill_database(conn);
let test_user = NewUser::new_local(
&conn,
conn,
"test".to_owned(),
"test user".to_owned(),
Role::Normal,
@@ -1383,10 +1364,10 @@ pub(crate) mod tests {
.unwrap();
assert_eq!(
User::login(&conn, "test", "test_password").unwrap().id,
User::login(conn, "test", "test_password").unwrap().id,
test_user.id
);
assert!(User::login(&conn, "test", "other_password").is_err());
assert!(User::login(conn, "test", "other_password").is_err());
Ok(())
});
}
@@ -1395,26 +1376,26 @@ pub(crate) mod tests {
fn get_local_page() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
fill_database(&conn);
fill_database(conn);
let page = User::get_local_page(&conn, (0, 2)).unwrap();
let page = User::get_local_page(conn, (0, 2)).unwrap();
assert_eq!(page.len(), 2);
assert!(page[0].username <= page[1].username);
let mut last_username = User::get_local_page(&conn, (0, 1)).unwrap()[0]
let mut last_username = User::get_local_page(conn, (0, 1)).unwrap()[0]
.username
.clone();
for i in 1..User::count_local(&conn).unwrap() as i32 {
let page = User::get_local_page(&conn, (i, i + 1)).unwrap();
for i in 1..User::count_local(conn).unwrap() as i32 {
let page = User::get_local_page(conn, (i, i + 1)).unwrap();
assert_eq!(page.len(), 1);
assert!(last_username <= page[0].username);
last_username = page[0].username.clone();
}
assert_eq!(
User::get_local_page(&conn, (0, User::count_local(&conn).unwrap() as i32 + 10))
User::get_local_page(conn, (0, User::count_local(conn).unwrap() as i32 + 10))
.unwrap()
.len() as i64,
User::count_local(&conn).unwrap()
User::count_local(conn).unwrap()
);
Ok(())
});
+1 -1
View File
@@ -1 +1 @@
nightly-2022-01-27
nightly-2022-07-19
+1 -1
View File
@@ -3,4 +3,4 @@ set -euo pipefail
version="$1"
docker run --rm -v $PWD:/repo -v $PWD/pkg:/pkg -v $PWD/script/prebuild.sh:/prebuild.sh plumeorg/plume-buildenv:v0.4.0 /prebuild.sh "$version" /repo /prebuild /pkg
docker run --rm -v $PWD:/repo -v $PWD/pkg:/pkg -v $PWD/script/prebuild.sh:/prebuild.sh plumeorg/plume-buildenv:v0.7.0 /prebuild.sh "$version" /repo /prebuild /pkg
+1 -1
View File
@@ -10,7 +10,7 @@ plm instance new -d plume-test.local -n plume-test
plm users new -n admin -N 'Admin' -e 'email@exemple.com' -p 'password'
plume &
caddy run -config /Caddyfile &
caddy run --config /Caddyfile &
until curl http://localhost:7878/test/health -f; do sleep 1; done 2>/dev/null >/dev/null
+1 -1
View File
@@ -10,7 +10,7 @@ pub fn create(conn: DbConn, data: Json<NewAppData>) -> Api<App> {
let client_id = random_hex();
let client_secret = random_hex();
let app = App::insert(
&*conn,
&conn,
NewApp {
name: data.name.clone(),
client_id,
+32 -31
View File
@@ -16,7 +16,7 @@ use plume_models::{
migrations::IMPORTED_MIGRATIONS,
remote_fetch_actor::RemoteFetchActor,
search::{actor::SearchActor, Searcher as UnmanagedSearcher},
Connection, CONFIG,
Config, Connection, CONFIG,
};
use rocket_csrf::CsrfFairingBuilder;
use scheduled_thread_pool::ScheduledThreadPool;
@@ -47,12 +47,12 @@ include!(concat!(env!("OUT_DIR"), "/templates.rs"));
compile_i18n!();
/// Initializes a database pool.
fn init_pool() -> Option<DbPool> {
fn init_pool(config: &Config) -> Option<DbPool> {
let manager = ConnectionManager::<Connection>::new(CONFIG.database_url.as_str());
let mut builder = DbPool::builder()
.connection_customizer(Box::new(PragmaForeignKey))
.min_idle(CONFIG.db_min_idle);
if let Some(max_size) = CONFIG.db_max_size {
.min_idle(config.db_min_idle);
if let Some(max_size) = config.db_max_size {
builder = builder.max_size(max_size);
};
let pool = builder.build(manager).ok()?;
@@ -63,28 +63,8 @@ fn init_pool() -> Option<DbPool> {
Some(pool)
}
pub(crate) fn init_rocket() -> rocket::Rocket {
match dotenv::dotenv() {
Ok(path) => eprintln!("Configuration read from {}", path.display()),
Err(ref e) if e.not_found() => eprintln!("no .env was found"),
e => e.map(|_| ()).unwrap(),
}
tracing_subscriber::fmt::init();
App::new("Plume")
.bin_name("plume")
.version(env!("CARGO_PKG_VERSION"))
.about("Plume backend server")
.after_help(
r#"
The plume command should be run inside the directory
containing the `.env` configuration file and `static` directory.
See https://docs.joinplu.me/installation/config
and https://docs.joinplu.me/installation/init for more info.
"#,
)
.get_matches();
let dbpool = init_pool().expect("main: database pool initialization error");
pub(crate) fn init_rocket(config: &Config) -> rocket::Rocket {
let dbpool = init_pool(config).expect("main: database pool initialization error");
if IMPORTED_MIGRATIONS
.is_pending(&dbpool.get().unwrap())
.unwrap_or(true)
@@ -104,8 +84,8 @@ Then try to restart Plume.
let workpool = ScheduledThreadPool::with_name("worker {}", num_cpus::get());
// we want a fast exit here, so
let searcher = Arc::new(UnmanagedSearcher::open_or_recreate(
&CONFIG.search_index,
&CONFIG.search_tokenizers,
&config.search_index,
&config.search_tokenizers,
));
RemoteFetchActor::init(dbpool.clone());
SearchActor::init(searcher.clone(), dbpool.clone());
@@ -125,12 +105,12 @@ Then try to restart Plume.
.expect("Error setting Ctrl-c handler");
let mail = mail::init();
if mail.is_none() && CONFIG.rocket.as_ref().unwrap().environment.is_prod() {
if mail.is_none() && config.rocket.as_ref().unwrap().environment.is_prod() {
warn!("Warning: the email server is not configured (or not completely).");
warn!("Please refer to the documentation to see how to configure it.");
}
rocket::custom(CONFIG.rocket.clone().unwrap())
rocket::custom(config.rocket.clone().unwrap())
.mount(
"/",
routes![
@@ -280,7 +260,28 @@ Then try to restart Plume.
}
fn main() {
let rocket = init_rocket();
match dotenv::dotenv() {
Ok(path) => eprintln!("Configuration read from {}", path.display()),
Err(ref e) if e.not_found() => eprintln!("no .env was found"),
e => e.map(|_| ()).unwrap(),
}
tracing_subscriber::fmt::init();
App::new("Plume")
.bin_name("plume")
.version(env!("CARGO_PKG_VERSION"))
.about("Plume backend server")
.after_help(
r#"
The plume command should be run inside the directory
containing the `.env` configuration file and `static` directory.
See https://docs.joinplu.me/installation/config
and https://docs.joinplu.me/installation/init for more info.
"#,
)
.get_matches();
let rocket = init_rocket(&CONFIG);
#[cfg(feature = "test")]
let rocket = rocket.mount("/test", routes![test_routes::health,]);
+154 -49
View File
@@ -1,4 +1,7 @@
use activitystreams::collection::{OrderedCollection, OrderedCollectionPage};
use activitystreams::{
collection::{OrderedCollection, OrderedCollectionPage},
iri_string::{spec::IriSpec, validate::iri_reference},
};
use diesel::SaveChangesDsl;
use rocket::{
http::ContentType,
@@ -79,8 +82,8 @@ pub struct NewBlogForm {
}
fn valid_slug(title: &str) -> Result<(), ValidationError> {
let slug = utils::make_actor_id(title);
if slug.is_empty() {
let slug = Blog::slug(title);
if slug.is_empty() || iri_reference::<IriSpec>(&slug).is_err() {
Err(ValidationError::new("empty_slug"))
} else {
Ok(())
@@ -93,7 +96,7 @@ pub fn create(
conn: DbConn,
rockets: PlumeRocket,
) -> RespondOrRedirect {
let slug = utils::make_actor_id(&form.title);
let slug = Blog::slug(&form.title);
let intl = &rockets.intl.catalog;
let user = rockets.user.clone().unwrap();
@@ -144,7 +147,7 @@ pub fn create(
.expect("blog::create: author error");
Flash::success(
Redirect::to(uri!(details: name = slug, page = _)),
Redirect::to(uri!(details: name = &slug, page = _)),
&i18n!(intl, "Your blog was successfully created!"),
)
.into()
@@ -160,7 +163,7 @@ pub fn delete(name: String, conn: DbConn, rockets: PlumeRocket) -> RespondOrRedi
.and_then(|u| u.is_author_in(&conn, &blog).ok())
.unwrap_or(false)
{
blog.delete(&*conn).expect("blog::expect: deletion error");
blog.delete(&conn).expect("blog::expect: deletion error");
Flash::success(
Redirect::to(uri!(super::instance::index)),
i18n!(rockets.intl.catalog, "Your blog was deleted."),
@@ -364,7 +367,7 @@ pub fn outbox_page(
#[get("/~/<name>/atom.xml")]
pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
let blog = Blog::find_by_fqn(&conn, &name).ok()?;
let entries = Post::get_recents_for_blog(&*conn, &blog, 15).ok()?;
let entries = Post::get_recents_for_blog(&conn, &blog, 15).ok()?;
let uri = Instance::get_local()
.ok()?
.compute_box("~", &name, "atom.xml");
@@ -379,6 +382,9 @@ pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
#[cfg(test)]
mod tests {
use std::env::var;
use super::valid_slug;
use crate::init_rocket;
use diesel::Connection;
use plume_common::utils::random_hex;
@@ -386,62 +392,107 @@ mod tests {
blog_authors::{BlogAuthor, NewBlogAuthor},
blogs::{Blog, NewBlog},
db_conn::{DbConn, DbPool},
get_rocket_config,
instance::{Instance, NewInstance},
post_authors::{NewPostAuthor, PostAuthor},
posts::{NewPost, Post},
safe_string::SafeString,
search::Searcher,
users::{NewUser, User, AUTH_COOKIE},
Connection as Conn, CONFIG,
Config, Fqn, SearchTokenizerConfig,
};
use rocket::{
http::{Cookie, Cookies, SameSite},
http::{ContentType, Cookie, Cookies, SameSite, Status},
local::{Client, LocalRequest},
};
#[test]
fn edit_link_within_post_card() {
let conn = Conn::establish(CONFIG.database_url.as_str()).unwrap();
Instance::insert(
&conn,
NewInstance {
public_domain: "example.org".to_string(),
name: "Plume".to_string(),
local: true,
long_description: SafeString::new(""),
short_description: SafeString::new(""),
default_license: "CC-BY-SA".to_string(),
open_registrations: true,
short_description_html: String::new(),
long_description_html: String::new(),
},
)
.unwrap();
let rocket = init_rocket();
type Models = (Instance, User, Blog, Post);
fn setup() -> (Client, Models) {
dotenv::from_path(".env.test").unwrap();
let config = Config {
base_url: var("BASE_URL").unwrap(),
db_name: "plume",
db_max_size: None,
db_min_idle: None,
signup: Default::default(),
database_url: var("DATABASE_URL").unwrap(),
search_index: format!("/tmp/plume_test-{}", random_hex()),
search_tokenizers: SearchTokenizerConfig::init(),
rocket: get_rocket_config(),
logo: Default::default(),
default_theme: Default::default(),
media_directory: format!("/tmp/plume_test-{}", random_hex()),
mail: None,
ldap: None,
proxy: None,
};
let _ = Searcher::create(&config.search_index, &config.search_tokenizers).unwrap();
let rocket = init_rocket(&config);
let client = Client::new(rocket).expect("valid rocket instance");
let dbpool = client.rocket().state::<DbPool>().unwrap();
let conn = &DbConn(dbpool.get().unwrap());
let (_instance, user, blog, post) = create_models(conn);
(client, create_models(conn))
}
let blog_path = uri!(super::activity_details: name = &blog.fqn).to_string();
fn teardown((client, (instance, user, _blog, _post)): (&Client, Models)) {
let rocket = client.rocket();
let dbpool = rocket.state::<DbPool>().unwrap();
let conn = &DbConn(dbpool.get().unwrap());
user.delete(conn).unwrap();
let _ = diesel::delete(&instance);
}
#[test]
fn edit_link_within_post_card() {
let (client, (instance, user, blog, post)) = setup();
let blog_path = uri!(super::activity_details: name = &blog.fqn.to_string()).to_string();
let edit_link = uri!(
super::super::posts::edit: blog = &blog.fqn,
super::super::posts::edit: blog = &blog.fqn.to_string(),
slug = &post.slug
)
.to_string();
let mut response = client.get(&blog_path).dispatch();
let body = response.body_string().unwrap();
assert!(!body.contains(&edit_link));
let body_not_contain_edit_link = !body.contains(&edit_link);
let request = client.get(&blog_path);
login(&request, &user);
let mut response = request.dispatch();
let body = response.body_string().unwrap();
assert!(body.contains(&edit_link));
eprintln!("{:?}", &blog.fqn);
let body_contains_edit_lnk = body.contains(&edit_link);
teardown((&client, (instance, user, blog, post)));
assert!(body_not_contain_edit_link);
assert!(body_contains_edit_lnk);
}
fn create_models(conn: &DbConn) -> (Instance, User, Blog, Post) {
fn create_models(conn: &DbConn) -> Models {
Instance::find_by_domain(conn, "example.org").unwrap_or_else(|_| {
Instance::insert(
conn,
NewInstance {
public_domain: "example.org".to_string(),
name: "Plume".to_string(),
local: true,
long_description: SafeString::new(""),
short_description: SafeString::new(""),
default_license: "CC-BY-SA".to_string(),
open_registrations: true,
short_description_html: String::new(),
long_description_html: String::new(),
},
)
.unwrap()
});
conn.transaction::<(Instance, User, Blog, Post), diesel::result::Error, _>(|| {
let instance = Instance::get_local().unwrap_or_else(|_| {
let instance = Instance::insert(
@@ -453,29 +504,43 @@ mod tests {
long_description_html: "<p>Good morning</p>".to_string(),
short_description: SafeString::new("Hello"),
short_description_html: "<p>Hello</p>".to_string(),
name: random_hex().to_string(),
name: random_hex(),
open_registrations: true,
public_domain: random_hex().to_string(),
public_domain: random_hex(),
},
)
.unwrap();
Instance::cache_local(conn);
instance
});
let mut user = NewUser::default();
user.instance_id = instance.id;
user.username = random_hex().to_string();
user.ap_url = random_hex().to_string();
user.inbox_url = random_hex().to_string();
user.outbox_url = random_hex().to_string();
user.followers_endpoint = random_hex().to_string();
let user = NewUser {
instance_id: instance.id,
username: random_hex(),
ap_url: random_hex(),
inbox_url: random_hex(),
outbox_url: random_hex(),
followers_endpoint: random_hex(),
fqn: random_hex(),
..Default::default()
};
let user = User::insert(conn, user).unwrap();
let mut blog = NewBlog::default();
blog.instance_id = instance.id;
blog.actor_id = random_hex().to_string();
blog.ap_url = random_hex().to_string();
blog.inbox_url = random_hex().to_string();
blog.outbox_url = random_hex().to_string();
let title = random_hex();
let blog = NewBlog {
instance_id: instance.id,
fqn: Fqn::make_local(&title).unwrap(),
title,
actor_id: random_hex(),
ap_url: random_hex(),
inbox_url: random_hex(),
outbox_url: random_hex(),
summary: Default::default(),
summary_html: Default::default(),
public_key: Default::default(),
private_key: Default::default(),
icon_id: Default::default(),
banner_id: Default::default(),
theme: Default::default(),
};
let blog = Blog::insert(conn, blog).unwrap();
BlogAuthor::insert(
conn,
@@ -524,4 +589,44 @@ mod tests {
.finish(),
);
}
#[test]
fn test_valid_slug() {
assert!(valid_slug("Blog Title").is_ok());
assert!(valid_slug("ブログ タイトル").is_ok());
}
#[test]
fn create_blog_with_same_title_twice() {
let (client, (instance, user, blog, post)) = setup();
let new_path = uri!(super::new).to_string();
let request = client.get(new_path);
login(&request, &user);
let mut response = request.dispatch();
let body = response.body_string().unwrap();
let prefix = r#"<input type="hidden" name="csrf-token" value=""#;
let pos = body.find(prefix).unwrap();
let token = body[pos + prefix.len()..pos + prefix.len() + 123].to_string();
let create_path = uri!(super::create).to_string();
let response = client
.post(&create_path)
.body(format!("title=My%20Blog&csrf-token={}", &token))
.header(ContentType::Form)
.dispatch();
let first_attempt = response;
let response = client
.post(&create_path)
.body(format!("title=My%20Blog&csrf-token={}", &token))
.header(ContentType::Form)
.dispatch();
let second_attempt = response;
teardown((&client, (instance, user, blog, post)));
assert_eq!(first_attempt.status(), Status::SeeOther);
assert_eq!(second_attempt.status(), Status::SeeOther);
}
}
+1 -1
View File
@@ -125,7 +125,7 @@ pub fn create(
user.has_reshared(&conn, &post)
.expect("comments::create: reshared error"),
user.is_following(
&*conn,
&conn,
post.get_authors(&conn)
.expect("comments::create: authors error")[0]
.id
+79 -6
View File
@@ -3,6 +3,7 @@ use crate::{
routes::{errors::ErrorPage, RespondOrRedirect},
template_utils::{IntoContext, Ructe},
};
use plume_models::{
db_conn::DbConn, email_signups::EmailSignup, instance::Instance, lettre::Transport, signups,
Error, PlumeRocket, CONFIG,
@@ -13,7 +14,11 @@ use rocket::{
response::{Flash, Redirect},
State,
};
use std::sync::{Arc, Mutex};
use std::{
borrow::Cow,
collections::HashMap,
sync::{Arc, Mutex},
};
use tracing::warn;
use validator::{Validate, ValidationError, ValidationErrors};
@@ -105,6 +110,26 @@ pub fn create(
render!(email_signups::create(&(&conn, &rockets).to_context())).into()
}
Error::NotFound => render!(errors::not_found(&(&conn, &rockets).to_context())).into(),
Error::Blocklisted(show, msg) => {
let mut errors = ValidationErrors::new();
if *show {
errors.add(
"email",
ValidationError {
code: Cow::from("blocklisted"),
message: Some(Cow::from(msg.clone())),
params: HashMap::new(),
},
);
}
render!(email_signups::new(
&(&conn, &rockets).to_context(),
registration_open,
&form,
errors
))
.into()
}
_ => render!(errors::not_found(&(&conn, &rockets).to_context())).into(), // FIXME
});
}
@@ -153,6 +178,28 @@ pub fn show(
)))
} // TODO: Flash and redirect
Error::NotFound => return Err(Error::NotFound.into()),
Error::Blocklisted(show, msg) => {
let mut errors = ValidationErrors::new();
if show {
errors.add(
"email",
ValidationError {
code: Cow::from("blocklisted"),
message: Some(Cow::from(msg)),
params: HashMap::new(),
},
);
}
return Ok(render!(email_signups::new(
&(&conn, &rockets).to_context(),
Instance::get_local()?.open_registrations,
&EmailSignupForm {
email: signup.email.clone(),
email_confirmation: signup.email
},
errors
)));
}
_ => return Err(Error::NotFound.into()), // FIXME
}
}
@@ -207,12 +254,38 @@ pub fn signup(
err
))));
}
let _user = signup
.complete(&conn, form.username.clone(), form.password.clone())
.map_err(|e| {
let user = signup.complete(&conn, form.username.clone(), form.password.clone());
match user {
Err(Error::Blocklisted(show, msg)) => {
let instance = Instance::get_local().map_err(|_| Status::UnprocessableEntity)?;
let mut errors = ValidationErrors::new();
if show {
errors.add(
"email",
ValidationError {
code: Cow::from("blocklisted"),
message: Some(Cow::from(msg)),
params: HashMap::new(),
},
);
}
return Ok(render!(email_signups::new(
&(&conn, &rockets).to_context(),
instance.open_registrations,
&EmailSignupForm {
email: signup.email.clone(),
email_confirmation: signup.email
},
errors
))
.into());
}
Err(e) => {
warn!("{:?}", e);
Status::UnprocessableEntity
})?;
return Err(Status::UnprocessableEntity);
}
_ => {}
}
Ok(FlashRedirect(Flash::success(
Redirect::to(uri!(super::session::new: m = _)),
i18n!(
+7 -7
View File
@@ -105,7 +105,7 @@ pub fn update_settings(
Instance::get_local().expect("instance::update_settings: local instance error");
instance
.update(
&*conn,
&conn,
form.name.clone(),
form.open_registrations,
form.short_description.clone(),
@@ -366,8 +366,8 @@ pub fn edit_users(
}
fn ban(id: i32, conn: &Connection, worker: &ScheduledThreadPool) -> Result<(), ErrorPage> {
let u = User::get(&*conn, id)?;
u.delete(&*conn)?;
let u = User::get(conn, id)?;
u.delete(conn)?;
if Instance::get_local()
.map(|i| u.instance_id == i.id)
.unwrap_or(false)
@@ -382,8 +382,8 @@ fn ban(id: i32, conn: &Connection, worker: &ScheduledThreadPool) -> Result<(), E
},
)
.unwrap();
let target = User::one_by_instance(&*conn)?;
let delete_act = u.delete_activity(&*conn)?;
let target = User::one_by_instance(conn)?;
let delete_act = u.delete_activity(conn)?;
worker.execute(move || broadcast(&u, delete_act, target, CONFIG.proxy().cloned()));
}
@@ -407,7 +407,7 @@ pub fn interact(conn: DbConn, user: Option<User>, target: String) -> Option<Redi
if let Ok(post) = Post::from_id(&conn, &target, None, CONFIG.proxy()) {
return Some(Redirect::to(uri!(
super::posts::details: blog = post.get_blog(&conn).expect("Can't retrieve blog").fqn,
super::posts::details: blog = post.get_blog(&conn).expect("Can't retrieve blog").fqn.to_string(),
slug = &post.slug,
responding_to = _
)));
@@ -418,7 +418,7 @@ pub fn interact(conn: DbConn, user: Option<User>, target: String) -> Option<Redi
let post = comment.get_post(&conn).expect("Can't retrieve post");
return Some(Redirect::to(uri!(
super::posts::details: blog =
post.get_blog(&conn).expect("Can't retrieve blog").fqn,
post.get_blog(&conn).expect("Can't retrieve blog").fqn.to_string(),
slug = &post.slug,
responding_to = comment.id
)));
+5 -5
View File
@@ -20,14 +20,14 @@ pub fn create(
let b = Blog::find_by_fqn(&conn, &blog)?;
let post = Post::find_by_slug(&conn, &slug, b.id)?;
if !user.has_liked(&*conn, &post)? {
let like = likes::Like::insert(&*conn, likes::NewLike::new(&post, &user))?;
like.notify(&*conn)?;
if !user.has_liked(&conn, &post)? {
let like = likes::Like::insert(&conn, likes::NewLike::new(&post, &user))?;
like.notify(&conn)?;
Timeline::add_to_all_timelines(&conn, &post, Kind::Like(&user))?;
let dest = User::one_by_instance(&*conn)?;
let act = like.to_activity(&*conn)?;
let dest = User::one_by_instance(&conn)?;
let act = like.to_activity(&conn)?;
rockets
.worker
.execute(move || broadcast(&user, act, dest, CONFIG.proxy().cloned()));
+4 -4
View File
@@ -145,9 +145,9 @@ pub fn details(
#[post("/medias/<id>/delete")]
pub fn delete(id: i32, user: User, conn: DbConn, intl: I18n) -> Result<Flash<Redirect>, ErrorPage> {
let media = Media::get(&*conn, id)?;
let media = Media::get(&conn, id)?;
if media.owner_id == user.id {
media.delete(&*conn)?;
media.delete(&conn)?;
Ok(Flash::success(
Redirect::to(uri!(list: page = _)),
i18n!(intl.catalog, "Your media have been deleted."),
@@ -167,9 +167,9 @@ pub fn set_avatar(
conn: DbConn,
intl: I18n,
) -> Result<Flash<Redirect>, ErrorPage> {
let media = Media::get(&*conn, id)?;
let media = Media::get(&conn, id)?;
if media.owner_id == user.id {
user.set_avatar(&*conn, media.id)?;
user.set_avatar(&conn, media.id)?;
Ok(Flash::success(
Redirect::to(uri!(details: id = id)),
i18n!(intl.catalog, "Your avatar has been updated."),
+1 -1
View File
@@ -165,7 +165,7 @@ fn post_to_atom(post: Post, conn: &Connection) -> Entry {
.build(),
)
.authors(
post.get_authors(&*conn)
post.get_authors(conn)
.expect("Atom feed: author error")
.into_iter()
.map(|a| {
+1 -1
View File
@@ -65,7 +65,7 @@ pub fn search(query: Option<Form<SearchQuery>>, conn: DbConn, rockets: PlumeRock
if str_query.is_empty() {
render!(search::index(
&(&conn, &rockets).to_context(),
&format!("{}", Utc::today().format("%Y-%m-d"))
&format!("{}", Utc::now().date_naive().format("%Y-%m-d"))
))
} else {
let res = rockets
+6 -6
View File
@@ -48,10 +48,10 @@ pub fn me(user: Option<User>) -> RespondOrRedirect {
#[get("/@/<name>", rank = 2)]
pub fn details(name: String, rockets: PlumeRocket, conn: DbConn) -> Result<Ructe, ErrorPage> {
let user = User::find_by_fqn(&conn, &name)?;
let recents = Post::get_recents_for_author(&*conn, &user, 6)?;
let reshares = Reshare::get_recents_for_author(&*conn, &user, 6)?;
let recents = Post::get_recents_for_author(&conn, &user, 6)?;
let reshares = Reshare::get_recents_for_author(&conn, &user, 6)?;
if !user.get_instance(&*conn)?.local {
if !user.get_instance(&conn)?.local {
tracing::trace!("remote user found");
user.remote_user_found(); // Doesn't block
}
@@ -62,14 +62,14 @@ pub fn details(name: String, rockets: PlumeRocket, conn: DbConn) -> Result<Ructe
rockets
.user
.clone()
.and_then(|x| x.is_following(&*conn, user.id).ok())
.and_then(|x| x.is_following(&conn, user.id).ok())
.unwrap_or(false),
user.instance_id != Instance::get_local()?.id,
user.get_instance(&*conn)?.public_domain,
user.get_instance(&conn)?.public_domain,
recents,
reshares
.into_iter()
.filter_map(|r| r.get_post(&*conn).ok())
.filter_map(|r| r.get_post(&conn).ok())
.collect()
)))
}
+2 -2
View File
@@ -50,10 +50,10 @@ impl Resolver<DbConn> for WebfingerResolver {
fn find(prefix: Prefix, acct: String, conn: DbConn) -> Result<Webfinger, ResolverError> {
match prefix {
Prefix::Acct => User::find_by_fqn(&conn, &acct)
.and_then(|usr| usr.webfinger(&*conn))
.and_then(|usr| usr.webfinger(&conn))
.or(Err(ResolverError::NotFound)),
Prefix::Group => Blog::find_by_fqn(&conn, &acct)
.and_then(|blog| blog.webfinger(&*conn))
.and_then(|blog| blog.webfinger(&conn))
.or(Err(ResolverError::NotFound)),
Prefix::Custom(_) => Err(ResolverError::NotFound),
}
+2 -1
View File
@@ -85,7 +85,7 @@ impl<'r> Responder<'r> for Ructe {
macro_rules! render {
($group:tt :: $page:tt ( $( $param:expr ),* ) ) => {
{
use crate::templates;
use $crate::templates;
let mut res = vec![];
templates::$group::$page(
@@ -116,6 +116,7 @@ pub fn translate_notification(ctx: BaseContext<'_>, notif: Notification) -> Stri
pub fn i18n_timeline_name(cat: &Catalog, tl: &str) -> String {
match tl {
"Your feed" => i18n!(cat, "Your feed"),
"My feed" => i18n!(cat, "My feed"),
"Local feed" => i18n!(cat, "Local feed"),
"Federated feed" => i18n!(cat, "Federated feed"),
n => n.to_string(),
+5 -5
View File
@@ -22,7 +22,7 @@
<meta content="@blog.summary_html" property="og:description" />
<meta content="@blog.icon_url(ctx.0)" property="og:image" />
<link href='@Instance::get_local().unwrap().compute_box("~", &blog.fqn, "atom.xml")' rel='alternate' type='application/atom+xml'>
<link href='@Instance::get_local().unwrap().compute_box("~", &blog.fqn.to_string(), "atom.xml")' rel='alternate' type='application/atom+xml'>
<link href='@blog.ap_url' rel='alternate' type='application/activity+json'>
<link href='@blog.ap_url' rel='canonical'>
@if !ctx.2.clone().map(|u| u.hide_custom_css).unwrap_or(false) {
@@ -31,7 +31,7 @@
}
}
}, {
<a href="@uri!(blogs::details: name = &blog.fqn, page = _)" dir="auto">@blog.title</a>
<a href="@uri!(blogs::details: name = &blog.fqn.to_string(), page = _)" dir="auto">@blog.title</a>
}, {
<div class="hidden">
@for author in authors {
@@ -58,8 +58,8 @@
</h1>
@if ctx.2.clone().and_then(|u| u.is_author_in(ctx.0, &blog).ok()).unwrap_or(false) {
<a href="@uri!(posts::new: blog = &blog.fqn)" class="button" dir="auto">@i18n!(ctx.1, "New article")</a>
<a href="@uri!(blogs::edit: name = &blog.fqn)" class="button" dir="auto">@i18n!(ctx.1, "Edit")</a>
<a href="@uri!(posts::new: blog = &blog.fqn.to_string())" class="button" dir="auto">@i18n!(ctx.1, "New article")</a>
<a href="@uri!(blogs::edit: name = &blog.fqn.to_string())" class="button" dir="auto">@i18n!(ctx.1, "Edit")</a>
}
</div>
@@ -76,7 +76,7 @@
<section>
<h2 dir="auto">
@i18n!(ctx.1, "Latest articles")
<small><a href="@uri!(blogs::atom_feed: name = &blog.fqn)" title="Atom feed">@icon!("rss")</a></small>
<small><a href="@uri!(blogs::atom_feed: name = &blog.fqn.to_string())" title="Atom feed">@icon!("rss")</a></small>
</h2>
@if posts.is_empty() {
<p dir="auto">@i18n!(ctx.1, "No posts to see here yet.")</p>
+3 -3
View File
@@ -12,10 +12,10 @@
@(ctx: BaseContext, blog: &Blog, medias: Vec<Media>, form: &EditForm, errors: ValidationErrors)
@:base(ctx, i18n!(ctx.1, "Edit \"{}\""; &blog.title), {}, {
<a href="@uri!(blogs::details: name = &blog.fqn, page = _)">@blog.title</a>
<a href="@uri!(blogs::details: name = &blog.fqn.to_string(), page = _)">@blog.title</a>
}, {
<h1>@i18n!(ctx.1, "Edit \"{}\""; &blog.title)</h1>
<form method="post" action="@uri!(blogs::update: name = &blog.fqn)">
<form method="post" action="@uri!(blogs::update: name = &blog.fqn.to_string())">
<!-- Rocket hack to use various HTTP methods -->
<input type=hidden name="_method" value="put">
@@ -53,7 +53,7 @@
<h2>@i18n!(ctx.1, "Danger zone")</h2>
<p>@i18n!(ctx.1, "Be very careful, any action taken here can't be reversed.")</p>
<form method="post" action="@uri!(blogs::delete: name = &blog.fqn)" onsubmit="return confirm('@i18n!(ctx.1, "Are you sure that you want to permanently delete this blog?")')">
<form method="post" action="@uri!(blogs::delete: name = &blog.fqn.to_string())" onsubmit="return confirm('@i18n!(ctx.1, "Are you sure that you want to permanently delete this blog?")')">
<input type="submit" class="inline-block button destructive" value="@i18n!(ctx.1, "Permanently delete this blog")">
</form>
})
+4 -4
View File
@@ -6,19 +6,19 @@
<div class="card h-entry">
@if article.cover_id.is_some() {
<a class="cover-link" href="@uri!(posts::details: blog = article.get_blog_fqn(ctx.0), slug = &article.slug, responding_to = _)">
<a class="cover-link" href="@uri!(posts::details: blog = article.get_blog_fqn(ctx.0).to_string(), slug = &article.slug, responding_to = _)">
<div class="cover" style="background-image: url('@Html(article.cover_url(ctx.0).unwrap_or_default())')"></div>
</a>
}
<header dir="auto">
<h3 class="p-name">
<a class="u-url" href="@uri!(posts::details: blog = article.get_blog_fqn(ctx.0), slug = &article.slug, responding_to = _)">
<a class="u-url" href="@uri!(posts::details: blog = article.get_blog_fqn(ctx.0).to_string(), slug = &article.slug, responding_to = _)">
@article.title
</a>
</h3>
@if ctx.2.clone().and_then(|u| article.is_author(ctx.0, u.id).ok()).unwrap_or(false) {
<div class="controls">
<a class="button" href="@uri!(posts::edit: blog = &article.get_blog_fqn(ctx.0), slug = &article.slug)">@i18n!(ctx.1, "Edit")</a>
<a class="button" href="@uri!(posts::edit: blog = &article.get_blog_fqn(ctx.0).to_string(), slug = &article.slug)">@i18n!(ctx.1, "Edit")</a>
</div>
}
</header>
@@ -35,7 +35,7 @@
@if article.published {
<span class="dt-published" datetime="@article.creation_date.format("%F %T")">@article.creation_date.format("%B %e, %Y")</span>
}
<a href="@uri!(blogs::details: name = &article.get_blog_fqn(ctx.0), page = _)">@article.get_blog(ctx.0).unwrap().title</a>
<a href="@uri!(blogs::details: name = &article.get_blog_fqn(ctx.0).to_string(), page = _)">@article.get_blog(ctx.0).unwrap().title</a>
</div>
@if !article.published {
+12 -12
View File
@@ -18,7 +18,7 @@
@if article.cover_id.is_some() {
<meta property="og:image" content="@Html(article.cover_url(ctx.0).unwrap_or_default())"/>
}
<meta property="og:url" content="@uri!(posts::details: blog = &blog.fqn, slug = &article.slug, responding_to = _)"/>
<meta property="og:url" content="@uri!(posts::details: blog = &blog.fqn.to_string(), slug = &article.slug, responding_to = _)"/>
<meta property="og:description" content="@article.subtitle"/>
<link rel="canonical" href="@article.ap_url"/>
@@ -28,7 +28,7 @@
}
}
}, {
<a href="@uri!(blogs::details: name = &blog.fqn, page = _)">@blog.title</a>
<a href="@uri!(blogs::details: name = &blog.fqn.to_string(), page = _)">@blog.title</a>
}, {
<div class="h-entry">
<header
@@ -78,7 +78,7 @@
</section>
@if ctx.2.is_some() {
<section class="actions">
<form id="likes" class="likes" action="@uri!(likes::create: blog = &blog.fqn, slug = &article.slug)#likes" method="POST">
<form id="likes" class="likes" action="@uri!(likes::create: blog = &blog.fqn.to_string(), slug = &article.slug)#likes" method="POST">
<p aria-label="@i18n!(ctx.1, "One like", "{0} likes"; n_likes)" title="@i18n!(ctx.1, "One like", "{0} likes"; n_likes)">
@n_likes
</p>
@@ -89,7 +89,7 @@
<button type="submit" class="action">@icon!("heart") @i18n!(ctx.1, "Add yours")</button>
}
</form>
<form id="reshares" class="reshares" action="@uri!(reshares::create: blog = &blog.fqn, slug = &article.slug)#reshares" method="POST">
<form id="reshares" class="reshares" action="@uri!(reshares::create: blog = &blog.fqn.to_string(), slug = &article.slug)#reshares" method="POST">
<p aria-label="@i18n!(ctx.1, "One boost", "{0} boosts"; n_reshares)" title="@i18n!(ctx.1, "One boost", "{0} boosts"; n_reshares)">
@n_reshares
</p>
@@ -104,7 +104,7 @@
} else {
<p class="center">@Html(i18n!(ctx.1, "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article";
format!("<a href='{}'>", escape(&uri!(session::new: m = _).to_string())), "</a>",
format!("<a href='{}'>", escape(&uri!(posts::remote_interact: blog_name = &blog.fqn, slug = &article.slug).to_string())), "</a>"
format!("<a href='{}'>", escape(&uri!(posts::remote_interact: blog_name = &blog.fqn.to_string(), slug = &article.slug).to_string())), "</a>"
))
</p>
<section class="actions">
@@ -112,14 +112,14 @@
<p aria-label="@i18n!(ctx.1, "One like", "{0} likes"; n_likes)" title="@i18n!(ctx.1, "One like", "{0} likes"; n_likes)">
@n_likes
</p>
<a href="@uri!(posts::remote_interact: blog_name = &blog.fqn, slug = &article.slug)" class="action">@icon!("heart") @i18n!(ctx.1, "Add yours")</a>
<a href="@uri!(posts::remote_interact: blog_name = &blog.fqn.to_string(), slug = &article.slug)" class="action">@icon!("heart") @i18n!(ctx.1, "Add yours")</a>
</div>
<div id="reshares" class="reshares">
<p aria-label="@i18n!(ctx.1, "One boost", "{0} boost"; n_reshares)" title="@i18n!(ctx.1, "One boost", "{0} boosts"; n_reshares)">
@n_reshares
</p>
<a href="@uri!(posts::remote_interact: blog_name = &blog.fqn, slug = &article.slug)" class="action">@icon!("repeat") @i18n!(ctx.1, "Boost")</a>
<a href="@uri!(posts::remote_interact: blog_name = &blog.fqn.to_string(), slug = &article.slug)" class="action">@icon!("repeat") @i18n!(ctx.1, "Boost")</a>
</div>
</section>
}
@@ -144,7 +144,7 @@
<h2>@i18n!(ctx.1, "Comments")</h2>
@if ctx.2.is_some() {
<form method="post" action="@uri!(comments::create: blog_name = &blog.fqn, slug = &article.slug)#comments">
<form method="post" action="@uri!(comments::create: blog_name = &blog.fqn.to_string(), slug = &article.slug)#comments">
@(Input::new("warning", i18n!(ctx.1, "Content warning"))
.default(&comment_form.warning)
.error(&comment_errors)
@@ -162,7 +162,7 @@
@if !comments.is_empty() {
@for comm in comments {
@:comment(ctx, &comm, Some(&article.ap_url), &blog.fqn, &article.slug)
@:comment(ctx, &comm, Some(&article.ap_url), &blog.fqn.to_string(), &article.slug)
}
} else {
<p class="center" dir="auto">@i18n!(ctx.1, "No comments yet. Be the first to react!")</p>
@@ -173,7 +173,7 @@
@if ctx.2.clone().and_then(|u| article.is_author(ctx.0, u.id).ok()).unwrap_or(false) {
<aside class="bottom-bar">
<div>
<form class="inline" method="post" action="@uri!(posts::delete: blog_name = &blog.fqn, slug = &article.slug)">
<form class="inline" method="post" action="@uri!(posts::delete: blog_name = &blog.fqn.to_string(), slug = &article.slug)">
<input class="button destructive" onclick="return confirm('@i18n!(ctx.1, "Are you sure?")')" type="submit" value="@i18n!(ctx.1, "Delete")">
</form>
</div>
@@ -186,9 +186,9 @@
</div>
<div>
@if !article.published {
<a class="button secondary" href="@uri!(posts::edit: blog = &blog.fqn, slug = &article.slug)">@i18n!(ctx.1, "Publish")</a>
<a class="button secondary" href="@uri!(posts::edit: blog = &blog.fqn.to_string(), slug = &article.slug)">@i18n!(ctx.1, "Publish")</a>
}
<a class="button" href="@uri!(posts::edit: blog = &blog.fqn, slug = &article.slug)">@i18n!(ctx.1, "Edit")</a>
<a class="button" href="@uri!(posts::edit: blog = &blog.fqn.to_string(), slug = &article.slug)">@i18n!(ctx.1, "Edit")</a>
</div>
</aside>
}
+1 -1
View File
@@ -6,7 +6,7 @@
<div class="h-card">
<div class="user">
<div class="flex wrap">
<div class="flex wrap" dir="auto">
@avatar(ctx.0, user, Size::Medium, false, ctx.1)
<h1 class="grow flex vertical">