Compare commits

...

215 Commits

Author SHA1 Message Date
aitzol e2c9e8000a docker build fixes 2025-06-23 11:07:54 +02:00
aitzol 475271ca56 Merge pull request 'tables style' (#7) from tables-style into main
Reviewed-on: #7
2024-12-23 23:18:52 +01:00
aitzol bf1c593cd1 tables style 2024-12-23 22:41:52 +01:00
aitzol bf11857d3e Merge pull request 'tables-style' (#6) from tables-style into main
Reviewed-on: #6
2024-12-22 11:13:06 +01:00
aitzol 09d1a414aa cleanup 2024-12-22 10:35:58 +01:00
aitzol 1a0d30762d cleanup 2024-12-22 10:33:58 +01:00
aitzol f568fd35b7 dependencies update 2024-12-21 23:11:13 +01:00
aitzol 477d5c54d9 Merge pull request 'tables-style' (#5) from tables-style into main
Reviewed-on: #5
2024-12-19 22:53:59 +01:00
aitzol 5e320d2782 tables style 2024-12-19 22:35:37 +01:00
aitzol d06b08786c tables style 2024-12-19 20:23:43 +01:00
aitzol 6940574a3e blockquote style 2024-12-19 16:21:52 +01:00
aitzol d3e09ce8ac blockquote style 2024-12-19 16:18:45 +01:00
aitzol e4e996f3a7 Fix horizontal scroll 2024-12-18 15:59:13 +01:00
aitzol f5b1b6f1c7 Merge pull request 'Fix moderation issues #1156(#1157)' (#1) from fix-moderation-issues into main
Reviewed-on: #1
2024-08-16 09:58:04 +02:00
aitzol 5d2e653a3b Fix moderation issues 2024-08-16 08:02:28 +02:00
aitzol ebc9fbc88a changelog 2024-01-11 22:09:25 +01:00
aitzol db8cc6e7e8 css aldaketak pantaila txikirako 2024-01-11 22:01:44 +01:00
aitzol 2d888d6aa5 changelog 2024-01-06 17:57:57 +01:00
aitzol 78b290a0dc garbiketa 2024-01-06 17:50:15 +01:00
aitzol 59e1123d35 eguneraketa 2024-01-06 17:48:05 +01:00
aitzol 56f6e46c71 lehen commita 2024-01-05 18:50:06 +01:00
trinity-1686a 304fb740d8 Merge pull request 'Support for storing media on S3' (#1149) from lx/Plume:s3 into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1149
Reviewed-by: trinity-1686a <trinity-1686a@noreply@joinplu.me>
2023-06-21 18:18:37 +00:00
Alex Auvolat 61e65a55ad improve formatting 2023-05-15 12:35:39 +02:00
Alex Auvolat 3f93212424 fix plume-cli 2023-05-12 18:25:19 +02:00
Alex Auvolat 20d77c22df try (and fail) to build with Nix 2023-05-12 17:20:45 +02:00
Alex Auvolat 24d3b289da Properly handle Content-Type 2023-05-12 16:11:29 +02:00
Alex Auvolat 20fa2cacf4 Store replicated remote media on S3 if available 2023-05-12 15:54:44 +02:00
Alex Auvolat 4e67eb8317 Uniformize media path/URL handling and implement direct download from S3 backend 2023-05-12 15:40:36 +02:00
Alex Auvolat 24c008b0de Add support for uploading media files to S3 2023-05-12 13:24:36 +02:00
Alex Auvolat 1cb9459a23 Update S3 features and make S3 support optional 2023-05-12 12:35:11 +02:00
Alex Auvolat 10e06737cf Update rust-s3 dependency and move Cargo.toml dependencies 2023-05-12 12:12:32 +02:00
Alex Auvolat 30a3cec87e Add Nix development shell 2023-05-12 12:12:09 +02:00
trinity-1686a 54af93d8ff initial s3 support
probably incomplete
2023-05-12 11:30:31 +02:00
KitaitiMakoto 9425b44d08 Merge pull request 'FIX: #1145 Fix SCSS errors' (#1146) from scss-errors into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1146
2023-04-16 07:13:55 +00:00
Kitaiti Makoto 487f296db5 Fix Clippy warnings 2023-04-16 16:12:09 +09:00
Kitaiti Makoto 8bdd481e0d Fix SCSS errors 2023-04-16 16:10:54 +09:00
KitaitiMakoto 19f18421bc Merge pull request 'delete comments properly when deleting users' (#1144) from fix-delete-user into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1144
Reviewed-by: KitaitiMakoto <kitaitimakoto@noreply@joinplu.me>
2023-04-16 06:59:53 +00:00
trinity-1686a e1777e9071 delete comments properly when deleting users 2023-04-09 12:54:29 +02:00
KitaitiMakoto 613ccbcd94 Merge pull request 'Add user search form to admin panel' (#1143) from moderation-improvement into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1143
2023-03-21 10:29:33 +00:00
Kitaiti Makoto b9a09a2511 Follow pagination user list page change 2023-03-21 19:15:22 +09:00
Kitaiti Makoto 213628e400 Don't use LIKE query when username is empty for user search 2023-03-21 19:07:09 +09:00
Kitaiti Makoto d6bb2bfb72 Use unwrap_or_default() instead of unwrap_or("") 2023-03-21 18:49:52 +09:00
Kitaiti Makoto 33bd290679 Use DbConn in model tests 2023-03-20 01:21:39 +09:00
Kitaiti Makoto 85ab5393fd Set style to user search form 2023-03-20 01:03:09 +09:00
Kitaiti Makoto 98c73bb6df Add search form to user list page 2023-03-20 01:00:17 +09:00
Kitaiti Makoto 3e9d9a459f Enable admin_search_user route 2023-03-20 01:00:02 +09:00
Kitaiti Makoto a394c3f210 Define admin_search_user route 2023-03-20 00:59:46 +09:00
Kitaiti Makoto a1a19e091a Define User::search_local_by_name() method 2023-03-20 00:59:16 +09:00
Kitaiti Makoto ec030d500d Exclude instance user when counting local users 2023-03-20 00:19:42 +09:00
Kitaiti Makoto cfa74f84e7 Remove instance users from user list to show 2023-03-06 19:30:18 +09:00
trinity-1686a 97cbe7f446 Merge pull request 'allow timeline manipulation from plm' (#1113) from timeline-cli into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1113
Reviewed-by: KitaitiMakoto <kitaitimakoto@noreply@joinplu.me>
2023-02-26 15:56:16 +00:00
trinity-1686a 7e4d081027 Merge branch 'main' into timeline-cli 2023-02-26 16:48:40 +01:00
KitaitiMakoto 1e5ae92135 Merge pull request 'Update crates' (#1138) from update-crates into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1138
2023-01-10 15:53:07 +00:00
Kitaiti Makoto 036ee6fac4 Merge remote-tracking branch 'origin/main' into update-crates 2023-01-11 00:42:34 +09:00
dependabot[bot] 6028295748 Bump glob from 0.3.0 to 0.3.1
Bumps [glob](https://github.com/rust-lang/glob) from 0.3.0 to 0.3.1.
- [Release notes](https://github.com/rust-lang/glob/releases)
- [Commits](https://github.com/rust-lang/glob/compare/0.3.0...0.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-11 00:41:35 +09:00
dependabot[bot] aa4cfd374d Bump atom_syndication from 0.11.0 to 0.12.0
Bumps [atom_syndication](https://github.com/rust-syndication/atom) from 0.11.0 to 0.12.0.
- [Release notes](https://github.com/rust-syndication/atom/releases)
- [Changelog](https://github.com/rust-syndication/atom/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-syndication/atom/compare/0.11.0...0.12.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-11 00:41:35 +09:00
dependabot[bot] 3303a4af84 Bump ructe from 0.14.2 to 0.15.0
Bumps [ructe](https://github.com/kaj/ructe) from 0.14.2 to 0.15.0.
- [Release notes](https://github.com/kaj/ructe/releases)
- [Changelog](https://github.com/kaj/ructe/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kaj/ructe/compare/v0.14.2...v0.15.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Change blog title specification

Revert "Change blog title specification"

This reverts commit a362b2474fa32b0e937f59acb9edb68d462c0719.
2023-01-11 00:41:22 +09:00
KitaitiMakoto 37a136787b Merge pull request 'Update crates' (#1136) from update-crates into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1136
2023-01-08 20:07:16 +00:00
Kitaiti Makoto 300ff37694 Merge remote-tracking branch 'github/dependabot/cargo/rsass-0.26.0' into update-crates 2023-01-09 04:50:04 +09:00
dependabot[bot] c1d9d39dc1 Bump rsass from 0.25.2 to 0.26.0
Bumps [rsass](https://github.com/kaj/rsass) from 0.25.2 to 0.26.0.
- [Release notes](https://github.com/kaj/rsass/releases)
- [Changelog](https://github.com/kaj/rsass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/kaj/rsass/compare/v0.25.2...v0.26.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-06 19:06:33 +00:00
dependabot[bot] 93d6ee04d4 Bump ructe from 0.14.2 to 0.15.0
Bumps [ructe](https://github.com/kaj/ructe) from 0.14.2 to 0.15.0.
- [Release notes](https://github.com/kaj/ructe/releases)
- [Changelog](https://github.com/kaj/ructe/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kaj/ructe/compare/v0.14.2...v0.15.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-06 19:06:09 +00: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
trinity-1686a 35b951967d add a few help messages to the cli 2023-01-01 11:03:50 +01: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
trinity-1686a 771d4325c2 add plm command for list management 2022-12-17 17:51:51 +01:00
trinity-1686a 1536a6d3f3 allow timeline manipulation from plm 2022-12-16 22:51:14 +01: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
138 changed files with 19778 additions and 36470 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
+1
View File
@@ -0,0 +1 @@
use flake
+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 }}
+2
View File
@@ -20,3 +20,5 @@ search_index
__pycache__
.vscode/
*-journal
.direnv/
build.log*
+33
View File
@@ -4,6 +4,39 @@
## [Unreleased] - ReleaseDate
### Added
- Add 'My feed' to i18n timeline name (#1084)
- Bidirectional support for user page header (#1092)
- Add non anonymous bind to LDAP server, taken from https://git.joinplu.me/Plume/Plume/src/branch/ldap-non-anon PR
### Changed
- Use blog title as slug (#1094, #1126, #1127)
- Bump Rust to nightly 2022-07-19 (#1119)
- Force LDAP simple bind with *cn* rdn instead of *uid*
- Update rust-toolchain to nightly-2023-04-14
- Update chrono from 0.4.0 to 0.4.31
- Update scheduled-thread-pool from 0.2.6 to 0.2.7
- Change blockquote color to `lightpurple`
- Some colour has been added to the tables to make them more visible
### Fixed
- Malfunction while creating a blog post in Persian (#1116)
- Email block list is ignored when email sign-up (#1122)
- Bug that some Activity Sytreams properties are not parsed properly (#1129)
- Allow empty avatar for remote users (#1129)
- Percent encode blog FQN for federation interoperability (#1129)
- The same to `preferredUsername` (#1129)
- Deprecation warnings during build process(see rust crate updates)
- Server error 500 creating new blog with white spaces inside title. Bug reported on https://git.joinplu.me/Plume/Plume/issues/1152
- Show _Subscribe_ button in column format instead of row format in screen smaller than 600px. https://git.lainoa.eus/aitzol/Plume/commit/db8cc6e7e8351a5d74f7ce0399126e13493c62d9
### To do
- Choose rdn via environment variables for LDAP simple bind
## [[0.7.2]] - 2022-05-11
### Added
Generated
+1862 -1147
View File
File diff suppressed because it is too large Load Diff
+20 -18
View File
@@ -1,33 +1,34 @@
[package]
authors = ["Plume contributors"]
name = "plume"
version = "0.7.2"
repository = "https://github.com/Plume-org/Plume"
edition = "2018"
version = "0.7.3-dev-fork"
repository = "https://git.lainoa.eus/aitzol/Plume"
edition = "2021"
[dependencies]
atom_syndication = "0.11.0"
atom_syndication = "0.12.0"
clap = "2.33"
dotenv = "0.15.0"
gettext = "0.4.0"
gettext-macros = "0.6.1"
gettext-utils = "0.1.0"
guid-create = "0.2"
guid-create = "0.4.1"
conv = "0.3.3"
lettre_email = "0.9.2"
num_cpus = "1.10"
rocket = "0.4.6"
rocket_contrib = { version = "0.4.5", features = ["json"] }
num_cpus = "1.16.0"
rocket = "0.4.11"
rocket_contrib = { version = "0.4.11", features = ["json"] }
rocket_i18n = "0.4.1"
scheduled-thread-pool = "0.2.2"
serde = "1.0"
scheduled-thread-pool = "0.2.7"
serde = "1.0.137"
serde_json = "1.0.81"
shrinkwraprs = "0.3.0"
validator = { version = "0.15", features = ["derive"] }
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"
@@ -35,7 +36,7 @@ path = "src/main.rs"
[dependencies.chrono]
features = ["serde"]
version = "0.4"
version = "0.4.31"
[dependencies.ctrlc]
features = ["termination"]
@@ -60,20 +61,21 @@ 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"
ructe = "0.15.0"
rsass = "0.26"
[features]
default = ["postgres"]
default = ["postgres", "s3"]
postgres = ["plume-models/postgres", "diesel/postgres"]
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
debug-mailer = []
test = []
search-lindera = ["plume-models/search-lindera"]
s3 = ["plume-models/s3"]
[workspace]
members = ["plume-api", "plume-cli", "plume-models", "plume-common", "plume-front", "plume-macro"]
+7 -7
View File
@@ -1,4 +1,4 @@
FROM rust:1-buster as builder
FROM rust:latest AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
@@ -13,27 +13,27 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libssl-dev \
clang
RUN ln -s /usr/bin/python3 /usr/bin/python & \
ln -s /usr/bin/pip3 /usr/bin/pip
WORKDIR /scratch
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 \
libpq5 \
libssl1.1
libpq5
WORKDIR /app
+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>
+3 -3
View File
@@ -98,7 +98,7 @@ main article {
}
blockquote {
border-inline-start: 5px solid $gray;
border-inline-start: 5px solid $lightpurple;
margin: 1em auto;
padding: 0em 2em;
}
@@ -228,7 +228,7 @@ main .article-meta {
fill: currentColor;
}
.action.liked:hover svg.feather {
background: transparentize($red, 0.75)
background: transparentize($red, 0.75);
color: $red;
}
}
@@ -252,7 +252,7 @@ main .article-meta {
background: $primary;
}
.action.reshared:hover svg.feather {
background: transparentize($primary, 0.75)
background: transparentize($primary, 0.75);
color: $primary;
}
}
+1
View File
@@ -135,6 +135,7 @@ form.new-post {
.button + .button {
margin-left: 1em;
margin-inline-start: 1em;
}
.split {
+4
View File
@@ -37,6 +37,10 @@ body > header {
flex-direction: row;
align-items: center;
&.right-nav {
overflow-x: hidden;
}
hr {
height: 100%;
width: 0.2em;
+10
View File
@@ -0,0 +1,10 @@
/* tables */
table, td, th, tr {
border: 1px dotted $lightpurple;
}
table {
border-collapse: collapse;
}
tr:nth-child(even){background-color: $gray;}
+1
View File
@@ -12,3 +12,4 @@
@import "header";
@import "article";
@import "forms";
@import "tables";
+1
View File
@@ -12,3 +12,4 @@
@import "header";
@import "article";
@import "forms";
@import "tables";
Generated
+116
View File
@@ -0,0 +1,116 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1683408522,
"narHash": "sha256-9kcPh6Uxo17a3kK3XCHhcWiV1Yu1kYj22RHiymUhMkU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "897876e4c484f1e8f92009fd11b7d988a121a4e7",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1683857898,
"narHash": "sha256-pyVY4UxM6zUX97g6bk6UyCbZGCWZb2Zykrne8YxacRA=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "4e7fba3f37f5e184ada0ef3cf1e4d8ef450f240b",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+60
View File
@@ -0,0 +1,60 @@
{
description = "Developpment shell for Plume including nightly Rust compiler";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
inputs.rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, flake-utils, rust-overlay, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs { inherit system overlays; };
inputs = with pkgs; [
(rust-bin.nightly.latest.default.override {
targets = [ "wasm32-unknown-unknown" ];
})
wasm-pack
openssl
pkg-config
gettext
postgresql
sqlite
];
in {
packages.default = pkgs.rustPlatform.buildRustPackage {
pname = "plume";
version = "0.7.3-dev";
src = ./.;
cargoLock = {
lockFile = ./Cargo.lock;
outputHashes = {
"pulldown-cmark-0.8.0" = "sha256-lpfoRDuY3zJ3QmUqJ5k9OL0MEdGDpwmpJ+u5BCj2kIA=";
"rocket_csrf-0.1.2" = "sha256-WywZfMiwZqTPfSDcAE7ivTSYSaFX+N9fjnRsLSLb9wE=";
};
};
buildNoDefaultFeatures = true;
buildFeatures = ["postgresql" "s3"];
nativeBuildInputs = inputs;
buildPhase = ''
wasm-pack build --target web --release plume-front
cargo build --no-default-features --features postgresql,s3 --path .
cargo build --no-default-features --features postgresql,s3 --path plume-cli
'';
installPhase = ''
cargo install --no-default-features --features postgresql,s3 --path . --target-dir $out
cargo install --no-default-features --features postgresql,s3 --path plume-cli --target-dir $out
'';
};
devShells.default = pkgs.mkShell {
packages = inputs;
};
});
}
+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
@@ -24,3 +24,4 @@ path = "../plume-models"
postgres = ["plume-models/postgres", "diesel/postgres"]
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
search-lindera = ["plume-models/search-lindera"]
s3 = ["plume-models/s3"]
+1
View File
@@ -1,2 +1,3 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
release = false
+262
View File
@@ -0,0 +1,262 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use plume_models::{blogs::Blog, instance::Instance, lists::*, users::User, Connection};
pub fn command<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("lists")
.about("Manage lists")
.subcommand(
SubCommand::with_name("new")
.arg(
Arg::with_name("name")
.short("n")
.long("name")
.takes_value(true)
.help("The name of this list"),
)
.arg(
Arg::with_name("type")
.short("t")
.long("type")
.takes_value(true)
.help(
r#"The type of this list (one of "user", "blog", "word" or "prefix")"#,
),
)
.arg(
Arg::with_name("user")
.short("u")
.long("user")
.takes_value(true)
.help("Username of whom this list is for. Empty for an instance list"),
)
.about("Create a new list"),
)
.subcommand(
SubCommand::with_name("delete")
.arg(
Arg::with_name("name")
.short("n")
.long("name")
.takes_value(true)
.help("The name of the list to delete"),
)
.arg(
Arg::with_name("user")
.short("u")
.long("user")
.takes_value(true)
.help("Username of whom this list was for. Empty for instance list"),
)
.arg(
Arg::with_name("yes")
.short("y")
.long("yes")
.help("Confirm the deletion"),
)
.about("Delete a list"),
)
.subcommand(
SubCommand::with_name("add")
.arg(
Arg::with_name("name")
.short("n")
.long("name")
.takes_value(true)
.help("The name of the list to add an element to"),
)
.arg(
Arg::with_name("user")
.short("u")
.long("user")
.takes_value(true)
.help("Username of whom this list is for. Empty for instance list"),
)
.arg(
Arg::with_name("value")
.short("v")
.long("value")
.takes_value(true)
.help("The value to add"),
)
.about("Add element to a list"),
)
.subcommand(
SubCommand::with_name("rm")
.arg(
Arg::with_name("name")
.short("n")
.long("name")
.takes_value(true)
.help("The name of the list to remove an element from"),
)
.arg(
Arg::with_name("user")
.short("u")
.long("user")
.takes_value(true)
.help("Username of whom this list is for. Empty for instance list"),
)
.arg(
Arg::with_name("value")
.short("v")
.long("value")
.takes_value(true)
.help("The value to remove"),
)
.about("Remove element from list"),
)
}
pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) {
let conn = conn;
match args.subcommand() {
("new", Some(x)) => new(x, conn),
("delete", Some(x)) => delete(x, conn),
("add", Some(x)) => add(x, conn),
("rm", Some(x)) => rm(x, conn),
("", None) => command().print_help().unwrap(),
_ => println!("Unknown subcommand"),
}
}
fn get_list_identifier(args: &ArgMatches<'_>) -> (String, Option<String>) {
let name = args
.value_of("name")
.map(String::from)
.expect("No name provided for the list");
let user = args.value_of("user").map(String::from);
(name, user)
}
fn get_list_type(args: &ArgMatches<'_>) -> ListType {
let typ = args
.value_of("type")
.map(String::from)
.expect("No name type for the list");
match typ.as_str() {
"user" => ListType::User,
"blog" => ListType::Blog,
"word" => ListType::Word,
"prefix" => ListType::Prefix,
_ => panic!("Invalid list type: {}", typ),
}
}
fn get_value(args: &ArgMatches<'_>) -> String {
args.value_of("value")
.map(String::from)
.expect("No query provided")
}
fn resolve_user(username: &str, conn: &Connection) -> User {
let instance = Instance::get_local_uncached(conn).expect("Failed to load local instance");
User::find_by_name(conn, username, instance.id).expect("User not found")
}
fn new(args: &ArgMatches<'_>, conn: &Connection) {
let (name, user) = get_list_identifier(args);
let typ = get_list_type(args);
let user = user.map(|user| resolve_user(&user, conn));
List::new(conn, &name, user.as_ref(), typ).expect("failed to create list");
}
fn delete(args: &ArgMatches<'_>, conn: &Connection) {
let (name, user) = get_list_identifier(args);
if !args.is_present("yes") {
panic!("Warning, this operation is destructive. Add --yes to confirm you want to do it.")
}
let user = user.map(|user| resolve_user(&user, conn));
let list =
List::find_for_user_by_name(conn, user.map(|u| u.id), &name).expect("list not found");
list.delete(conn).expect("Failed to update list");
}
fn add(args: &ArgMatches<'_>, conn: &Connection) {
let (name, user) = get_list_identifier(args);
let value = get_value(args);
let user = user.map(|user| resolve_user(&user, conn));
let list =
List::find_for_user_by_name(conn, user.map(|u| u.id), &name).expect("list not found");
match list.kind() {
ListType::Blog => {
let blog_id = Blog::find_by_fqn(conn, &value).expect("unknown blog").id;
if !list.contains_blog(conn, blog_id).unwrap() {
list.add_blogs(conn, &[blog_id]).unwrap();
}
}
ListType::User => {
let user_id = User::find_by_fqn(conn, &value).expect("unknown user").id;
if !list.contains_user(conn, user_id).unwrap() {
list.add_users(conn, &[user_id]).unwrap();
}
}
ListType::Word => {
if !list.contains_word(conn, &value).unwrap() {
list.add_words(conn, &[&value]).unwrap();
}
}
ListType::Prefix => {
if !list.contains_prefix(conn, &value).unwrap() {
list.add_prefixes(conn, &[&value]).unwrap();
}
}
}
}
fn rm(args: &ArgMatches<'_>, conn: &Connection) {
let (name, user) = get_list_identifier(args);
let value = get_value(args);
let user = user.map(|user| resolve_user(&user, conn));
let list =
List::find_for_user_by_name(conn, user.map(|u| u.id), &name).expect("list not found");
match list.kind() {
ListType::Blog => {
let blog_id = Blog::find_by_fqn(conn, &value).expect("unknown blog").id;
let mut blogs = list.list_blogs(conn).unwrap();
if let Some(index) = blogs.iter().position(|b| b.id == blog_id) {
blogs.swap_remove(index);
let blogs = blogs.iter().map(|b| b.id).collect::<Vec<_>>();
list.set_blogs(conn, &blogs).unwrap();
}
}
ListType::User => {
let user_id = User::find_by_fqn(conn, &value).expect("unknown user").id;
let mut users = list.list_users(conn).unwrap();
if let Some(index) = users.iter().position(|u| u.id == user_id) {
users.swap_remove(index);
let users = users.iter().map(|u| u.id).collect::<Vec<_>>();
list.set_users(conn, &users).unwrap();
}
}
ListType::Word => {
let mut words = list.list_words(conn).unwrap();
if let Some(index) = words.iter().position(|w| *w == value) {
words.swap_remove(index);
let words = words.iter().map(String::as_str).collect::<Vec<_>>();
list.set_words(conn, &words).unwrap();
}
}
ListType::Prefix => {
let mut prefixes = list.list_prefixes(conn).unwrap();
if let Some(index) = prefixes.iter().position(|p| *p == value) {
prefixes.swap_remove(index);
let prefixes = prefixes.iter().map(String::as_str).collect::<Vec<_>>();
list.set_prefixes(conn, &prefixes).unwrap();
}
}
}
}
+8
View File
@@ -4,8 +4,10 @@ use plume_models::{instance::Instance, Connection as Conn, CONFIG};
use std::io::{self, prelude::*};
mod instance;
mod list;
mod migration;
mod search;
mod timeline;
mod users;
fn main() {
@@ -16,6 +18,8 @@ fn main() {
.subcommand(instance::command())
.subcommand(migration::command())
.subcommand(search::command())
.subcommand(timeline::command())
.subcommand(list::command())
.subcommand(users::command());
let matches = app.clone().get_matches();
@@ -37,6 +41,10 @@ fn main() {
("search", Some(args)) => {
search::run(args, &conn.expect("Couldn't connect to the database."))
}
("timeline", Some(args)) => {
timeline::run(args, &conn.expect("Couldn't connect to the database."))
}
("lists", Some(args)) => list::run(args, &conn.expect("Couldn't connect to the database.")),
("users", Some(args)) => {
users::run(args, &conn.expect("Couldn't connect to the database."))
}
+257
View File
@@ -0,0 +1,257 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use plume_models::{instance::Instance, posts::Post, timeline::*, users::*, Connection};
pub fn command<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("timeline")
.about("Manage public timeline")
.subcommand(
SubCommand::with_name("new")
.arg(
Arg::with_name("name")
.short("n")
.long("name")
.takes_value(true)
.help("The name of this timeline"),
)
.arg(
Arg::with_name("query")
.short("q")
.long("query")
.takes_value(true)
.help("The query posts in this timelines have to match"),
)
.arg(
Arg::with_name("user")
.short("u")
.long("user")
.takes_value(true)
.help(
"Username of whom this timeline is for. Empty for an instance timeline",
),
)
.arg(
Arg::with_name("preload-count")
.short("p")
.long("preload-count")
.takes_value(true)
.help("Number of posts to try to preload in this timeline at its creation"),
)
.about("Create a new timeline"),
)
.subcommand(
SubCommand::with_name("delete")
.arg(
Arg::with_name("name")
.short("n")
.long("name")
.takes_value(true)
.help("The name of the timeline to delete"),
)
.arg(
Arg::with_name("user")
.short("u")
.long("user")
.takes_value(true)
.help(
"Username of whom this timeline was for. Empty for instance timeline",
),
)
.arg(
Arg::with_name("yes")
.short("y")
.long("yes")
.help("Confirm the deletion"),
)
.about("Delete a timeline"),
)
.subcommand(
SubCommand::with_name("edit")
.arg(
Arg::with_name("name")
.short("n")
.long("name")
.takes_value(true)
.help("The name of the timeline to edit"),
)
.arg(
Arg::with_name("user")
.short("u")
.long("user")
.takes_value(true)
.help("Username of whom this timeline is for. Empty for instance timeline"),
)
.arg(
Arg::with_name("query")
.short("q")
.long("query")
.takes_value(true)
.help("The query posts in this timelines have to match"),
)
.about("Edit the query of a timeline"),
)
.subcommand(
SubCommand::with_name("repopulate")
.arg(
Arg::with_name("name")
.short("n")
.long("name")
.takes_value(true)
.help("The name of the timeline to repopulate"),
)
.arg(
Arg::with_name("user")
.short("u")
.long("user")
.takes_value(true)
.help(
"Username of whom this timeline was for. Empty for instance timeline",
),
)
.arg(
Arg::with_name("preload-count")
.short("p")
.long("preload-count")
.takes_value(true)
.help("Number of posts to try to preload in this timeline at its creation"),
)
.about("Repopulate a timeline. Run this after modifying a list the timeline depends on."),
)
}
pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) {
let conn = conn;
match args.subcommand() {
("new", Some(x)) => new(x, conn),
("edit", Some(x)) => edit(x, conn),
("delete", Some(x)) => delete(x, conn),
("repopulate", Some(x)) => repopulate(x, conn),
("", None) => command().print_help().unwrap(),
_ => println!("Unknown subcommand"),
}
}
fn get_timeline_identifier(args: &ArgMatches<'_>) -> (String, Option<String>) {
let name = args
.value_of("name")
.map(String::from)
.expect("No name provided for the timeline");
let user = args.value_of("user").map(String::from);
(name, user)
}
fn get_query(args: &ArgMatches<'_>) -> String {
let query = args
.value_of("query")
.map(String::from)
.expect("No query provided");
match TimelineQuery::parse(&query) {
Ok(_) => (),
Err(QueryError::SyntaxError(start, end, message)) => panic!(
"Query parsing error between {} and {}: {}",
start, end, message
),
Err(QueryError::UnexpectedEndOfQuery) => {
panic!("Query parsing error: unexpected end of query")
}
Err(QueryError::RuntimeError(message)) => panic!("Query parsing error: {}", message),
}
query
}
fn get_preload_count(args: &ArgMatches<'_>) -> usize {
args.value_of("preload-count")
.map(|arg| arg.parse().expect("invalid preload-count"))
.unwrap_or(plume_models::ITEMS_PER_PAGE as usize)
}
fn resolve_user(username: &str, conn: &Connection) -> User {
let instance = Instance::get_local_uncached(conn).expect("Failed to load local instance");
User::find_by_name(conn, username, instance.id).expect("User not found")
}
fn preload(timeline: Timeline, count: usize, conn: &Connection) {
timeline.remove_all_posts(conn).unwrap();
if count == 0 {
return;
}
let mut posts = Vec::with_capacity(count as usize);
for post in Post::list_filtered(conn, None, None, None)
.unwrap()
.into_iter()
.rev()
{
if timeline.matches(conn, &post, Kind::Original).unwrap() {
posts.push(post);
if posts.len() >= count {
break;
}
}
}
for post in posts.iter().rev() {
timeline.add_post(conn, post).unwrap();
}
}
fn new(args: &ArgMatches<'_>, conn: &Connection) {
let (name, user) = get_timeline_identifier(args);
let query = get_query(args);
let preload_count = get_preload_count(args);
let user = user.map(|user| resolve_user(&user, conn));
let timeline = if let Some(user) = user {
Timeline::new_for_user(conn, user.id, name, query)
} else {
Timeline::new_for_instance(conn, name, query)
}
.expect("Failed to create new timeline");
preload(timeline, preload_count, conn);
}
fn edit(args: &ArgMatches<'_>, conn: &Connection) {
let (name, user) = get_timeline_identifier(args);
let query = get_query(args);
let user = user.map(|user| resolve_user(&user, conn));
let mut timeline = Timeline::find_for_user_by_name(conn, user.map(|u| u.id), &name)
.expect("timeline not found");
timeline.query = query;
timeline.update(conn).expect("Failed to update timeline");
}
fn delete(args: &ArgMatches<'_>, conn: &Connection) {
let (name, user) = get_timeline_identifier(args);
if !args.is_present("yes") {
panic!("Warning, this operation is destructive. Add --yes to confirm you want to do it.")
}
let user = user.map(|user| resolve_user(&user, conn));
let timeline = Timeline::find_for_user_by_name(conn, user.map(|u| u.id), &name)
.expect("timeline not found");
timeline.delete(conn).expect("Failed to update timeline");
}
fn repopulate(args: &ArgMatches<'_>, conn: &Connection) {
let (name, user) = get_timeline_identifier(args);
let preload_count = get_preload_count(args);
let user = user.map(|user| resolve_user(&user, conn));
let timeline = Timeline::find_for_user_by_name(conn, user.map(|u| u.id), &name)
.expect("timeline not found");
preload(timeline, preload_count, conn);
}
+11 -12
View File
@@ -7,29 +7,28 @@ 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"
[dependencies.chrono]
features = ["serde"]
version = "0.4"
version = "0.4.31"
[dependencies.pulldown-cmark]
default-features = false
@@ -38,6 +37,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();
+68 -2
View File
@@ -518,7 +518,8 @@ 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};
@@ -592,7 +593,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 +626,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,
-9
View File
@@ -1,4 +1,3 @@
use heck::ToUpperCamelCase;
use openssl::rand::rand_bytes;
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag};
use regex_syntax::is_word_character;
@@ -16,14 +15,6 @@ 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.
*
+8 -5
View File
@@ -2,7 +2,10 @@
name = "plume-front"
version = "0.7.2"
authors = ["Plume contributors"]
edition = "2018"
edition = "2021"
[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.99"
js-sys = "0.3.76"
serde_derive = "1.0.123"
console_error_panic_hook = "0.1.6"
[dependencies.web-sys]
version = "0.3.57"
version = "0.3.76"
features = [
'console',
'ClipboardEvent',
+1
View File
@@ -1,2 +1,3 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
release = false
+5 -1
View File
@@ -168,10 +168,14 @@ fn load_autosave() {
.get(&get_autosave_id())
{
let autosave_info: AutosaveInformation = serde_json::from_str(&autosave_str).ok().unwrap();
let d = &JsValue::from_f64(autosave_info.last_saved);
let message = i18n!(
CATALOG,
"Do you want to load the local autosave last edited at {}?";
Date::new(&JsValue::from_f64(autosave_info.last_saved)).to_date_string().as_string().unwrap()
// next line shows 'unexpected token' error on docker image building
//Date::new(&JsValue::from_f64(autosave_info.last_saved)).to_date_string().as_string().unwrap()
Date::new(d).to_date_string().as_string().unwrap()
);
if let Ok(true) = window().unwrap().confirm_with_message(&message) {
set_value("editor-content", &autosave_info.contents);
+1
View File
@@ -1,2 +1,3 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
release = false
+17 -13
View File
@@ -7,38 +7,41 @@ edition = "2018"
[dependencies]
ammonia = "3.2.0"
bcrypt = "0.12.1"
guid-create = "0.2"
guid-create = "0.3.0"
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.7"
serde = "1.0.137"
rust-s3 = { version = "0.33.0", optional = true, features = ["blocking"] }
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"
glob = "0.3.0"
diesel-derive-newtype = "1.0.0"
glob = "0.3.1"
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"
ahash = "=0.8.11"
heck = "0.4.1"
[dependencies.chrono]
features = ["serde"]
version = "0.4"
version = "0.4.31"
[dependencies.diesel]
features = ["r2d2", "chrono"]
@@ -61,3 +64,4 @@ diesel_migrations = "1.3.0"
postgres = ["diesel/postgres", "plume-macro/postgres" ]
sqlite = ["diesel/sqlite", "plume-macro/sqlite" ]
search-lindera = ["lindera-tantivy"]
s3 = ["rust-s3"]
+1
View File
@@ -1,2 +1,3 @@
pre-release-hook = ["cargo", "fmt"]
pre-release-replacements = []
release = false
+18 -1
View File
@@ -5,7 +5,7 @@ use rocket::{
Outcome,
};
/// Wrapper around User to use as a request guard on pages reserved to admins.
/// Wrapper around User to use as a request guard on pages exclusively reserved to admins.
pub struct Admin(pub User);
impl<'a, 'r> FromRequest<'a, 'r> for Admin {
@@ -21,6 +21,23 @@ impl<'a, 'r> FromRequest<'a, 'r> for Admin {
}
}
/// Same as `Admin` but it forwards to next guard if the user is not an admin.
/// It's useful when there are multiple implementations of routes for admin and moderator.
pub struct InclusiveAdmin(pub User);
impl<'a, 'r> FromRequest<'a, 'r> for InclusiveAdmin {
type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<InclusiveAdmin, ()> {
let user = request.guard::<User>()?;
if user.is_admin() {
Outcome::Success(InclusiveAdmin(user))
} else {
Outcome::Forward(())
}
}
}
/// Same as `Admin` but for moderators.
pub struct Moderator(pub User);
+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(())
});
}
+83 -61
View File
@@ -1,6 +1,7 @@
use heck::ToUpperCamelCase;
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,
instance::*, medias::Media, posts::Post, safe_string::SafeString, schema::blogs, users::User,
Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE,
};
use activitystreams::{
actor::{ApActor, ApActorExt, AsApActor, Group},
@@ -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::*;
@@ -83,9 +87,13 @@ impl Blog {
if inserted.fqn.is_empty() {
if instance.local {
inserted.fqn = inserted.actor_id.clone();
inserted.fqn = iri_percent_encode_seg(&inserted.actor_id);
} else {
inserted.fqn = format!("{}@{}", inserted.actor_id, instance.public_domain);
inserted.fqn = format!(
"{}@{}",
iri_percent_encode_seg(&inserted.actor_id),
instance.public_domain
);
}
}
@@ -95,6 +103,19 @@ impl Blog {
find_by!(blogs, find_by_ap_url, ap_url as &str);
find_by!(blogs, find_by_name, actor_id as &str, instance_id as i32);
/// Remove non alphanumeric characters and CamelCase a string
pub fn slug(title: &str) -> String {
title.to_upper_camel_case()
.chars()
.filter(|c| c.is_alphanumeric())
.collect()
}
/*
pub fn slug(title: &str) -> &str {
title
}
*/
pub fn get_instance(&self, conn: &Connection) -> Result<Instance> {
Instance::get(conn, self.instance_id)
}
@@ -131,10 +152,10 @@ impl Blog {
.map_err(Error::from)
}
pub fn find_by_fqn(conn: &DbConn, fqn: &str) -> Result<Blog> {
pub fn find_by_fqn(conn: &Connection, fqn: &str) -> Result<Blog> {
let from_db = blogs::table
.filter(blogs::fqn.eq(fqn))
.first(&**conn)
.first(conn)
.optional()?;
if let Some(from_db) = from_db {
Ok(from_db)
@@ -143,7 +164,7 @@ impl Blog {
}
}
fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result<Blog> {
fn fetch_from_webfinger(conn: &Connection, acct: &str) -> Result<Blog> {
resolve_with_prefix(Prefix::Group, acct.to_owned(), true)?
.links
.into_iter()
@@ -162,7 +183,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(iri_percent_encode_seg(&self.actor_id));
blog.set_name(self.title.clone());
blog.set_outbox(self.outbox_url.parse()?);
blog.set_summary(self.summary_html.to_string());
@@ -361,15 +382,15 @@ impl IntoId for Blog {
}
}
impl FromId<DbConn> for Blog {
impl FromId<Connection> for Blog {
type Error = Error;
type Object = CustomGroup;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
fn from_db(conn: &Connection, id: &str) -> Result<Self> {
Self::find_by_ap_url(conn, id)
}
fn from_activity(conn: &DbConn, acct: CustomGroup) -> Result<Self> {
fn from_activity(conn: &Connection, acct: CustomGroup) -> Result<Self> {
let (name, outbox_url, inbox_url) = {
let actor = acct.ap_actor_ref();
let name = actor
@@ -377,6 +398,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);
}
(
@@ -656,7 +678,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,10 +687,10 @@ pub(crate) mod tests {
fn get_instance() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
fill_database(&conn);
fill_database(conn);
let blog = Blog::insert(
&conn,
conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some name".to_owned(),
@@ -680,7 +702,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,10 +714,10 @@ pub(crate) mod tests {
fn authors() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (user, _) = fill_database(&conn);
let (user, _) = fill_database(conn);
let b1 = Blog::insert(
&conn,
conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some name".to_owned(),
@@ -706,7 +728,7 @@ pub(crate) mod tests {
)
.unwrap();
let b2 = Blog::insert(
&conn,
conn,
NewBlog::new_local(
"Blog".to_owned(),
"Blog".to_owned(),
@@ -719,7 +741,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 +751,7 @@ pub(crate) mod tests {
.unwrap();
BlogAuthor::insert(
&conn,
conn,
NewBlogAuthor {
blog_id: blog[0].id,
author_id: user[1].id,
@@ -739,7 +761,7 @@ pub(crate) mod tests {
.unwrap();
BlogAuthor::insert(
&conn,
conn,
NewBlogAuthor {
blog_id: blog[1].id,
author_id: user[0].id,
@@ -749,39 +771,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,10 +815,10 @@ pub(crate) mod tests {
fn find_local() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
fill_database(&conn);
fill_database(conn);
let blog = Blog::insert(
&conn,
conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some name".to_owned(),
@@ -807,7 +829,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,10 +838,10 @@ pub(crate) mod tests {
fn get_fqn() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
fill_database(&conn);
fill_database(conn);
let blog = Blog::insert(
&conn,
conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some name".to_owned(),
@@ -839,10 +861,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,10 +873,10 @@ pub(crate) mod tests {
fn delete_via_user() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (user, _) = fill_database(&conn);
let (user, _) = fill_database(conn);
let b1 = Blog::insert(
&conn,
conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some name".to_owned(),
@@ -865,7 +887,7 @@ pub(crate) mod tests {
)
.unwrap();
let b2 = Blog::insert(
&conn,
conn,
NewBlog::new_local(
"Blog".to_owned(),
"Blog".to_owned(),
@@ -878,7 +900,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 +910,7 @@ pub(crate) mod tests {
.unwrap();
BlogAuthor::insert(
&conn,
conn,
NewBlogAuthor {
blog_id: blog[0].id,
author_id: user[1].id,
@@ -898,7 +920,7 @@ pub(crate) mod tests {
.unwrap();
BlogAuthor::insert(
&conn,
conn,
NewBlogAuthor {
blog_id: blog[1].id,
author_id: user[0].id,
@@ -907,11 +929,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 +942,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 +961,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 +976,9 @@ pub(crate) mod tests {
.id,
);
let _: Blog = blogs[0].save_changes(&**conn).unwrap();
let ap_repr = blogs[0].to_activity(&conn).unwrap();
blogs[0].delete(&conn).unwrap();
let blog = Blog::from_activity(&conn, ap_repr).unwrap();
let ap_repr = blogs[0].to_activity(conn).unwrap();
blogs[0].delete(conn).unwrap();
let blog = Blog::from_activity(conn, ap_repr).unwrap();
assert_eq!(blog.actor_id, blogs[0].actor_id);
assert_eq!(blog.title, blogs[0].title);
@@ -968,8 +990,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 +1001,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)?;
+22 -21
View File
@@ -1,6 +1,5 @@
use crate::{
comment_seers::{CommentSeers, NewCommentSeers},
db_conn::DbConn,
instance::Instance,
medias::Media,
mentions::Mention,
@@ -74,6 +73,7 @@ impl Comment {
});
get!(comments);
list_by!(comments, list_by_post, post_id as i32);
list_by!(comments, list_by_author, author_id as i32);
find_by!(comments, find_by_ap_url, ap_url as &str);
pub fn get_author(&self, conn: &Connection) -> Result<User> {
@@ -111,7 +111,7 @@ impl Comment {
.unwrap_or(false)
}
pub fn to_activity(&self, conn: &DbConn) -> Result<Note> {
pub fn to_activity(&self, conn: &Connection) -> Result<Note> {
let author = User::get(conn, self.author_id)?;
let (html, mentions, _hashtags) = utils::md_to_html(
self.content.get().as_ref(),
@@ -136,7 +136,7 @@ impl Comment {
|id| Comment::get(conn, id).map(|comment| comment.ap_url.unwrap_or_default()),
)?);
note.set_published(
OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos().into())
OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos_opt().unwrap().into())
.expect("OffsetDateTime"),
);
note.set_attributed_to(author.into_id().parse::<IriString>()?);
@@ -149,7 +149,7 @@ impl Comment {
Ok(note)
}
pub fn create_activity(&self, conn: &DbConn) -> Result<Create> {
pub fn create_activity(&self, conn: &Connection) -> Result<Create> {
let author = User::get(conn, self.author_id)?;
let note = self.to_activity(conn)?;
@@ -217,15 +217,15 @@ impl Comment {
}
}
impl FromId<DbConn> for Comment {
impl FromId<Connection> for Comment {
type Error = Error;
type Object = Note;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
fn from_db(conn: &Connection, id: &str) -> Result<Self> {
Self::find_by_ap_url(conn, id)
}
fn from_activity(conn: &DbConn, note: Note) -> Result<Self> {
fn from_activity(conn: &Connection, note: Note) -> Result<Self> {
let comm = {
let previous_url = note
.in_reply_to()
@@ -354,21 +354,21 @@ impl FromId<DbConn> for Comment {
}
}
impl AsObject<User, Create, &DbConn> for Comment {
impl AsObject<User, Create, &Connection> for Comment {
type Error = Error;
type Output = Self;
fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result<Self> {
fn activity(self, _conn: &Connection, _actor: User, _id: &str) -> Result<Self> {
// The actual creation takes place in the FromId impl
Ok(self)
}
}
impl AsObject<User, Delete, &DbConn> for Comment {
impl AsObject<User, Delete, &Connection> for Comment {
type Error = Error;
type Output = ();
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> {
if self.author_id != actor.id {
return Err(Error::Unauthorized);
}
@@ -381,14 +381,14 @@ 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)
.filter(comments::in_response_to_id.eq(self.id))
.set(comments::in_response_to_id.eq(self.in_response_to_id))
.execute(&**conn)?;
diesel::delete(&self).execute(&**conn)?;
.execute(conn)?;
diesel::delete(&self).execute(conn)?;
Ok(())
}
}
@@ -423,6 +423,7 @@ impl CommentTree {
mod tests {
use super::*;
use crate::blogs::Blog;
use crate::db_conn::DbConn;
use crate::inbox::{inbox, tests::fill_database, InboxResult};
use crate::safe_string::SafeString;
use crate::tests::{db, format_datetime};
@@ -431,7 +432,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,8 +458,8 @@ 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/",
@@ -500,7 +501,7 @@ 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/",
@@ -522,12 +523,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);
+125 -6
View File
@@ -6,6 +6,9 @@ use rocket::Config as RocketConfig;
use std::collections::HashSet;
use std::env::{self, var};
#[cfg(feature = "s3")]
use s3::{Bucket, Region, creds::Credentials};
#[cfg(not(test))]
const DB_NAME: &str = "plume";
#[cfg(test)]
@@ -27,13 +30,23 @@ pub struct Config {
pub mail: Option<MailConfig>,
pub ldap: Option<LdapConfig>,
pub proxy: Option<ProxyConfig>,
pub s3: Option<S3Config>,
}
impl Config {
pub fn proxy(&self) -> Option<&reqwest::Proxy> {
self.proxy.as_ref().map(|p| &p.proxy)
}
}
fn string_to_bool(val: &str, name: &str) -> bool {
match val {
"1" | "true" | "TRUE" => true,
"0" | "false" | "FALSE" => false,
_ => panic!("Invalid configuration: {} is not boolean", name),
}
}
#[derive(Debug, Clone)]
pub enum InvalidRocketConfig {
Env,
@@ -280,6 +293,7 @@ pub struct LdapConfig {
pub tls: bool,
pub user_name_attr: String,
pub mail_attr: String,
pub user: Option<(String, String)>,
}
fn get_ldap_config() -> Option<LdapConfig> {
@@ -288,23 +302,29 @@ fn get_ldap_config() -> Option<LdapConfig> {
match (addr, base_dn) {
(Some(addr), Some(base_dn)) => {
let tls = var("LDAP_TLS").unwrap_or_else(|_| "false".to_owned());
let tls = match tls.as_ref() {
"1" | "true" | "TRUE" => true,
"0" | "false" | "FALSE" => false,
_ => panic!("Invalid LDAP configuration : tls"),
};
let tls = string_to_bool(&tls, "LDAP_TLS");
let user_name_attr = var("LDAP_USER_NAME_ATTR").unwrap_or_else(|_| "cn".to_owned());
let mail_attr = var("LDAP_USER_MAIL_ATTR").unwrap_or_else(|_| "mail".to_owned());
//2023-12-30
let user = var("LDAP_USER").ok();
let password = var("LDAP_PASSWORD").ok();
let user = match (user, password) {
(Some(user), Some(password)) => Some((user, password)),
(None, None) => None,
_ => panic!("Invalid LDAP configuration both or neither of LDAP_USER and LDAP_PASSWORD must be set")
};
//
Some(LdapConfig {
addr,
base_dn,
tls,
user_name_attr,
mail_attr,
user,
})
}
(None, None) => None,
(_, _) => {
_ => {
panic!("Invalid LDAP configuration : both LDAP_ADDR and LDAP_BASE_DN must be set")
}
}
@@ -349,6 +369,104 @@ fn get_proxy_config() -> Option<ProxyConfig> {
})
}
pub struct S3Config {
pub bucket: String,
pub access_key_id: String,
pub access_key_secret: String,
// region? If not set, default to us-east-1
pub region: String,
// hostname for s3. If not set, default to $region.amazonaws.com
pub hostname: String,
// may be useful when using self hosted s3. Won't work with recent AWS buckets
pub path_style: bool,
// http or https
pub protocol: String,
// download directly from s3 to user, wihout going through Plume. Require public read on bucket
pub direct_download: bool,
// use this hostname for downloads, can be used with caching proxy in front of s3 (expected to
// be reachable through https)
pub alias: Option<String>,
}
impl S3Config {
#[cfg(feature = "s3")]
pub fn get_bucket(&self) -> Bucket {
let region = Region::Custom {
region: self.region.clone(),
endpoint: format!("{}://{}", self.protocol, self.hostname),
};
let credentials = Credentials {
access_key: Some(self.access_key_id.clone()),
secret_key: Some(self.access_key_secret.clone()),
security_token: None,
session_token: None,
expiration: None,
};
let bucket = Bucket::new(&self.bucket, region, credentials).unwrap();
if self.path_style {
bucket.with_path_style()
} else {
bucket
}
}
}
fn get_s3_config() -> Option<S3Config> {
let bucket = var("S3_BUCKET").ok();
let access_key_id = var("AWS_ACCESS_KEY_ID").ok();
let access_key_secret = var("AWS_SECRET_ACCESS_KEY").ok();
if bucket.is_none() && access_key_id.is_none() && access_key_secret.is_none() {
return None;
}
#[cfg(not(feature = "s3"))]
panic!("S3 support is not enabled in this build");
#[cfg(feature = "s3")]
{
if bucket.is_none() || access_key_id.is_none() || access_key_secret.is_none() {
panic!("Invalid S3 configuration: some required values are set, but not others");
}
let bucket = bucket.unwrap();
let access_key_id = access_key_id.unwrap();
let access_key_secret = access_key_secret.unwrap();
let region = var("S3_REGION").unwrap_or_else(|_| "us-east-1".to_owned());
let hostname = var("S3_HOSTNAME").unwrap_or_else(|_| format!("{}.amazonaws.com", region));
let protocol = var("S3_PROTOCOL").unwrap_or_else(|_| "https".to_owned());
if protocol != "http" && protocol != "https" {
panic!("Invalid S3 configuration: invalid protocol {}", protocol);
}
let path_style = var("S3_PATH_STYLE").unwrap_or_else(|_| "false".to_owned());
let path_style = string_to_bool(&path_style, "S3_PATH_STYLE");
let direct_download = var("S3_DIRECT_DOWNLOAD").unwrap_or_else(|_| "false".to_owned());
let direct_download = string_to_bool(&direct_download, "S3_DIRECT_DOWNLOAD");
let alias = var("S3_ALIAS_HOST").ok();
if direct_download && protocol == "http" && alias.is_none() {
panic!("S3 direct download is disabled because bucket is accessed through plain HTTP. Use HTTPS or set an alias hostname (S3_ALIAS_HOST).");
}
Some(S3Config {
bucket,
access_key_id,
access_key_secret,
region,
hostname,
protocol,
path_style,
direct_download,
alias,
})
}
}
lazy_static! {
pub static ref CONFIG: Config = Config {
base_url: var("BASE_URL").unwrap_or_else(|_| format!(
@@ -380,5 +498,6 @@ lazy_static! {
mail: get_mail_config(),
ldap: get_ldap_config(),
proxy: get_proxy_config(),
s3: get_s3_config(),
};
}
+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)?;
+15 -18
View File
@@ -1,6 +1,6 @@
use crate::{
ap_url, db_conn::DbConn, instance::Instance, notifications::*, schema::follows, users::User,
Connection, Error, Result, CONFIG,
ap_url, instance::Instance, notifications::*, schema::follows, users::User, Connection, Error,
Result, CONFIG,
};
use activitystreams::{
activity::{Accept, ActorAndObjectRef, Follow as FollowAct, Undo},
@@ -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)
}
@@ -155,11 +150,11 @@ impl Follow {
}
}
impl AsObject<User, FollowAct, &DbConn> for User {
impl AsObject<User, FollowAct, &Connection> for User {
type Error = Error;
type Output = Follow;
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Follow> {
fn activity(self, conn: &Connection, actor: User, id: &str) -> Result<Follow> {
// Mastodon (at least) requires the full Follow object when accepting it,
// so we rebuilt it here
let follow = FollowAct::new(actor.ap_url.parse::<IriString>()?, id.parse::<IriString>()?);
@@ -167,15 +162,15 @@ impl AsObject<User, FollowAct, &DbConn> for User {
}
}
impl FromId<DbConn> for Follow {
impl FromId<Connection> for Follow {
type Error = Error;
type Object = FollowAct;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
fn from_db(conn: &Connection, id: &str) -> Result<Self> {
Follow::find_by_ap_url(conn, id)
}
fn from_activity(conn: &DbConn, follow: FollowAct) -> Result<Self> {
fn from_activity(conn: &Connection, follow: FollowAct) -> Result<Self> {
let actor = User::from_id(
conn,
follow
@@ -207,18 +202,18 @@ impl FromId<DbConn> for Follow {
}
}
impl AsObject<User, Undo, &DbConn> for Follow {
impl AsObject<User, Undo, &Connection> for Follow {
type Error = Error;
type Output = ();
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> {
let conn = conn;
if self.follower_id == actor.id {
diesel::delete(&self).execute(&**conn)?;
diesel::delete(&self).execute(conn)?;
// delete associated notification if any
if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
diesel::delete(&notif).execute(&**conn)?;
diesel::delete(&notif).execute(conn)?;
}
Ok(())
@@ -237,7 +232,9 @@ impl IntoId for Follow {
#[cfg(test)]
mod tests {
use super::*;
use crate::{tests::db, users::tests as user_tests, users::tests::fill_database};
use crate::{
db_conn::DbConn, tests::db, users::tests as user_tests, users::tests::fill_database,
};
use assert_json_diff::assert_json_eq;
use diesel::Connection;
use serde_json::{json, to_value};
+11 -12
View File
@@ -2,12 +2,11 @@ use activitystreams::activity::{Announce, Create, Delete, Follow, Like, Undo, Up
use crate::{
comments::Comment,
db_conn::DbConn,
follows, likes,
posts::{Post, PostUpdate},
reshares::Reshare,
users::User,
Error, CONFIG,
Connection, Error, CONFIG,
};
use plume_common::activity_pub::inbox::Inbox;
@@ -46,7 +45,7 @@ impl_into_inbox_result! {
Reshare => Reshared
}
pub fn inbox(conn: &DbConn, act: serde_json::Value) -> Result<InboxResult, Error> {
pub fn inbox(conn: &Connection, act: serde_json::Value) -> Result<InboxResult, Error> {
Inbox::handle(conn, act)
.with::<User, Announce, Post>(CONFIG.proxy())
.with::<User, Create, Comment>(CONFIG.proxy())
@@ -82,9 +81,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 +101,7 @@ pub(crate) mod tests {
.unwrap();
PostAuthor::insert(
&conn,
conn,
NewPostAuthor {
post_id: post.id,
author_id: users[0].id,
@@ -190,7 +189,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 +220,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 +248,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(_),
))
@@ -324,7 +323,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 +361,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 +396,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(_),
))
+4 -4
View File
@@ -9,7 +9,7 @@ use crate::{
use chrono::NaiveDateTime;
use diesel::{self, result::Error::NotFound, ExpressionMethods, QueryDsl, RunQueryDsl};
use once_cell::sync::OnceCell;
use plume_common::utils::md_to_html;
use plume_common::utils::{iri_percent_encode_seg, md_to_html};
use std::sync::RwLock;
#[derive(Clone, Identifiable, Queryable)]
@@ -173,8 +173,8 @@ impl Instance {
"{instance}/{prefix}/{name}/{box_name}",
instance = self.public_domain,
prefix = prefix,
name = name,
box_name = box_name
name = iri_percent_encode_seg(name),
box_name = iri_percent_encode_seg(box_name)
))
}
@@ -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)"
+15 -6
View File
@@ -69,6 +69,8 @@ pub enum Error {
Webfinger,
Expired,
UserAlreadyExists,
#[cfg(feature = "s3")]
S3(s3::error::S3Error),
}
impl From<bcrypt::BcryptError> for Error {
@@ -170,6 +172,13 @@ impl From<request::Error> for Error {
}
}
#[cfg(feature = "s3")]
impl From<s3::error::S3Error> for Error {
fn from(err: s3::error::S3Error) -> Error {
Error::S3(err)
}
}
pub type Result<T> = std::result::Result<T, Error>;
/// Adds a function to a model, that returns the first
@@ -177,7 +186,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 +210,7 @@ macro_rules! find_by {
///
/// Usage:
///
/// ```rust
/// ```ignore
/// impl Model {
/// list_by!(model_table, name_of_the_function, field1 as String);
/// }
@@ -225,7 +234,7 @@ macro_rules! list_by {
///
/// # Usage
///
/// ```rust
/// ```ignore
/// impl Model {
/// get!(model_table);
/// }
@@ -248,7 +257,7 @@ macro_rules! get {
///
/// # Usage
///
/// ```rust
/// ```ignore
/// impl Model {
/// insert!(model_table, NewModelType);
/// }
@@ -280,7 +289,7 @@ macro_rules! insert {
///
/// # Usage
///
/// ```rust
/// ```ignore
/// impl Model {
/// last!(model_table);
/// }
@@ -354,7 +363,7 @@ mod tests {
};
}
pub fn db<'a>() -> db_conn::DbConn {
pub fn db() -> db_conn::DbConn {
db_conn::DbConn((*DB_POOL).get().unwrap())
}
+14 -14
View File
@@ -1,6 +1,6 @@
use crate::{
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*,
users::User, Connection, Error, Result, CONFIG,
instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*, users::User,
Connection, Error, Result, CONFIG,
};
use activitystreams::{
activity::{ActorAndObjectRef, Like as LikeAct, Undo},
@@ -85,11 +85,11 @@ impl Like {
}
}
impl AsObject<User, LikeAct, &DbConn> for Post {
impl AsObject<User, LikeAct, &Connection> for Post {
type Error = Error;
type Output = Like;
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Like> {
fn activity(self, conn: &Connection, actor: User, id: &str) -> Result<Like> {
let res = Like::insert(
conn,
NewLike {
@@ -105,15 +105,15 @@ impl AsObject<User, LikeAct, &DbConn> for Post {
}
}
impl FromId<DbConn> for Like {
impl FromId<Connection> for Like {
type Error = Error;
type Object = LikeAct;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
fn from_db(conn: &Connection, id: &str) -> Result<Self> {
Like::find_by_ap_url(conn, id)
}
fn from_activity(conn: &DbConn, act: LikeAct) -> Result<Self> {
fn from_activity(conn: &Connection, act: LikeAct) -> Result<Self> {
let res = Like::insert(
conn,
NewLike {
@@ -154,17 +154,17 @@ impl FromId<DbConn> for Like {
}
}
impl AsObject<User, Undo, &DbConn> for Like {
impl AsObject<User, Undo, &Connection> for Like {
type Error = Error;
type Output = ();
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> {
if actor.id == self.user_id {
diesel::delete(&self).execute(&**conn)?;
diesel::delete(&self).execute(conn)?;
// delete associated notification if any
if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) {
diesel::delete(&notif).execute(&**conn)?;
diesel::delete(&notif).execute(conn)?;
}
Ok(())
} else {
@@ -199,7 +199,7 @@ 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!({
@@ -223,8 +223,8 @@ 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/",
+23 -1
View File
@@ -297,6 +297,28 @@ impl List {
.map_err(Error::from)
}
pub fn delete(&self, conn: &Connection) -> Result<()> {
if let Some(user_id) = self.user_id {
diesel::delete(
lists::table
.filter(lists::user_id.eq(user_id))
.filter(lists::name.eq(&self.name)),
)
.execute(conn)
.map(|_| ())
.map_err(Error::from)
} else {
diesel::delete(
lists::table
.filter(lists::user_id.is_null())
.filter(lists::name.eq(&self.name)),
)
.execute(conn)
.map(|_| ())
.map_err(Error::from)
}
}
func! {set: set_users, User, add_users}
func! {set: set_blogs, Blog, add_blogs}
func! {set: set_words, Word, add_words}
@@ -413,7 +435,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(())
+187 -40
View File
@@ -1,6 +1,6 @@
use crate::{
ap_url, db_conn::DbConn, instance::Instance, safe_string::SafeString, schema::medias,
users::User, Connection, Error, Result, CONFIG,
ap_url, instance::Instance, safe_string::SafeString, schema::medias, users::User, Connection,
Error, Result, CONFIG,
};
use activitystreams::{object::Image, prelude::*};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
@@ -16,6 +16,9 @@ use std::{
use tracing::warn;
use url::Url;
#[cfg(feature = "s3")]
use crate::config::S3Config;
const REMOTE_MEDIA_DIRECTORY: &str = "remote";
#[derive(Clone, Identifiable, Queryable, AsChangeset)]
@@ -42,7 +45,7 @@ pub struct NewMedia {
pub owner_id: i32,
}
#[derive(PartialEq)]
#[derive(PartialEq, Eq)]
pub enum MediaCategory {
Image,
Audio,
@@ -105,7 +108,7 @@ impl Media {
.file_path
.rsplit_once('.')
.map(|x| x.1)
.expect("Media::category: extension error")
.unwrap_or("")
.to_lowercase()
{
"png" | "jpg" | "jpeg" | "gif" | "svg" => MediaCategory::Image,
@@ -151,26 +154,99 @@ impl Media {
})
}
/// Returns full file path for medias stored in the local media directory.
pub fn local_path(&self) -> Option<PathBuf> {
if self.file_path.is_empty() {
return None;
}
if CONFIG.s3.is_some() {
#[cfg(feature="s3")]
unreachable!("Called Media::local_path() but media are stored on S3");
#[cfg(not(feature="s3"))]
unreachable!();
}
let relative_path = self
.file_path
.trim_start_matches(&CONFIG.media_directory)
.trim_start_matches(path::MAIN_SEPARATOR)
.trim_start_matches("static/media/");
Some(Path::new(&CONFIG.media_directory).join(relative_path))
}
/// Returns the relative URL to access this file, which is also the key at which
/// it is stored in the S3 bucket if we are using S3 storage.
/// Does not start with a '/', it is of the form "static/media/<...>"
pub fn relative_url(&self) -> Option<String> {
if self.file_path.is_empty() {
return None;
}
let relative_path = self
.file_path
.trim_start_matches(&CONFIG.media_directory)
.replace(path::MAIN_SEPARATOR, "/");
let relative_path = relative_path
.trim_start_matches('/')
.trim_start_matches("static/media/");
Some(format!("static/media/{}", relative_path))
}
/// Returns a public URL through which this media file can be accessed
pub fn url(&self) -> Result<String> {
if self.is_remote {
Ok(self.remote_url.clone().unwrap_or_default())
} else {
let file_path = self.file_path.replace(path::MAIN_SEPARATOR, "/").replacen(
&CONFIG.media_directory,
"static/media",
1,
); // "static/media" from plume::routs::plume_media_files()
let relative_url = self.relative_url().unwrap_or_default();
#[cfg(feature="s3")]
if CONFIG.s3.as_ref().map(|x| x.direct_download).unwrap_or(false) {
let s3_url = match CONFIG.s3.as_ref().unwrap() {
S3Config { alias: Some(alias), .. } => {
format!("https://{}/{}", alias, relative_url)
}
S3Config { path_style: true, hostname, bucket, .. } => {
format!("https://{}/{}/{}",
hostname,
bucket,
relative_url
)
}
S3Config { path_style: false, hostname, bucket, .. } => {
format!("https://{}.{}/{}",
bucket,
hostname,
relative_url
)
}
};
return Ok(s3_url);
}
Ok(ap_url(&format!(
"{}/{}",
Instance::get_local()?.public_domain,
&file_path
relative_url
)))
}
}
pub fn delete(&self, conn: &Connection) -> Result<()> {
if !self.is_remote {
fs::remove_file(self.file_path.as_str())?;
if CONFIG.s3.is_some() {
#[cfg(not(feature="s3"))]
unreachable!();
#[cfg(feature = "s3")]
CONFIG.s3.as_ref().unwrap().get_bucket()
.delete_object_blocking(&self.relative_url().ok_or(Error::NotFound)?)?;
} else {
fs::remove_file(self.local_path().ok_or(Error::NotFound)?)?;
}
}
diesel::delete(self)
.execute(conn)
@@ -206,27 +282,65 @@ impl Media {
}
// TODO: merge with save_remote?
pub fn from_activity(conn: &DbConn, image: &Image) -> Result<Media> {
pub fn from_activity(conn: &Connection, image: &Image) -> Result<Media> {
let remote_url = image
.url()
.and_then(|url| url.to_as_uri())
.ok_or(Error::MissingApProperty)?;
let path = determine_mirror_file_path(&remote_url);
let parent = path.parent().ok_or(Error::InvalidValue)?;
if !parent.is_dir() {
DirBuilder::new().recursive(true).create(parent)?;
}
let mut dest = fs::File::create(path.clone())?;
// TODO: conditional GET
request::get(
remote_url.as_str(),
User::get_sender(),
CONFIG.proxy().cloned(),
)?
.copy_to(&mut dest)?;
let file_path = if CONFIG.s3.is_some() {
#[cfg(not(feature="s3"))]
unreachable!();
Media::find_by_file_path(conn, path.to_str().ok_or(Error::InvalidValue)?)
#[cfg(feature = "s3")]
{
use rocket::http::ContentType;
let dest = determine_mirror_s3_path(&remote_url);
let media = request::get(
remote_url.as_str(),
User::get_sender(),
CONFIG.proxy().cloned(),
)?;
let content_type = media
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|x| x.to_str().ok())
.and_then(ContentType::parse_flexible)
.unwrap_or(ContentType::Binary);
let bytes = media.bytes()?;
let bucket = CONFIG.s3.as_ref().unwrap().get_bucket();
bucket.put_object_with_content_type_blocking(
&dest,
&bytes,
&content_type.to_string()
)?;
dest
}
} else {
let path = determine_mirror_file_path(&remote_url);
let parent = path.parent().ok_or(Error::InvalidValue)?;
if !parent.is_dir() {
DirBuilder::new().recursive(true).create(parent)?;
}
let mut dest = fs::File::create(path.clone())?;
// TODO: conditional GET
request::get(
remote_url.as_str(),
User::get_sender(),
CONFIG.proxy().cloned(),
)?
.copy_to(&mut dest)?;
path.to_str().ok_or(Error::InvalidValue)?.to_string()
};
Media::find_by_file_path(conn, &file_path)
.and_then(|mut media| {
let mut updated = false;
@@ -258,7 +372,7 @@ impl Media {
updated = true;
}
if updated {
diesel::update(&media).set(&media).execute(&**conn)?;
diesel::update(&media).set(&media).execute(conn)?;
}
Ok(media)
})
@@ -267,7 +381,7 @@ impl Media {
Media::insert(
conn,
NewMedia {
file_path: path.to_str().ok_or(Error::InvalidValue)?.to_string(),
file_path,
alt_text: image
.content()
.and_then(|content| content.to_as_string())
@@ -307,12 +421,10 @@ impl Media {
}
fn determine_mirror_file_path(url: &str) -> PathBuf {
let mut file_path = Path::new(&super::CONFIG.media_directory).join(REMOTE_MEDIA_DIRECTORY);
Url::parse(url)
.map(|url| {
if !url.has_host() {
return;
}
let mut file_path = Path::new(&CONFIG.media_directory).join(REMOTE_MEDIA_DIRECTORY);
match Url::parse(url) {
Ok(url) if url.has_host() => {
file_path.push(url.host_str().unwrap());
for segment in url.path_segments().expect("FIXME") {
file_path.push(segment);
@@ -320,19 +432,54 @@ fn determine_mirror_file_path(url: &str) -> PathBuf {
// TODO: handle query
// HINT: Use characters which must be percent-encoded in path as separator between path and query
// HINT: handle extension
})
.unwrap_or_else(|err| {
warn!("Failed to parse url: {} {}", &url, err);
}
other => {
if let Err(err) = other {
warn!("Failed to parse url: {} {}", &url, err);
} else {
warn!("Error without a host: {}", &url);
}
let ext = url
.rsplit('.')
.next()
.map(ToOwned::to_owned)
.unwrap_or_else(|| String::from("png"));
file_path.push(format!("{}.{}", GUID::rand(), ext));
});
}
}
file_path
}
#[cfg(feature="s3")]
fn determine_mirror_s3_path(url: &str) -> String {
match Url::parse(url) {
Ok(url) if url.has_host() => {
format!("static/media/{}/{}/{}",
REMOTE_MEDIA_DIRECTORY,
url.host_str().unwrap(),
url.path().trim_start_matches('/'),
)
}
other => {
if let Err(err) = other {
warn!("Failed to parse url: {} {}", &url, err);
} else {
warn!("Error without a host: {}", &url);
}
let ext = url
.rsplit('.')
.next()
.map(ToOwned::to_owned)
.unwrap_or_else(|| String::from("png"));
format!("static/media/{}/{}.{}",
REMOTE_MEDIA_DIRECTORY,
GUID::rand(),
ext,
)
}
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
@@ -343,7 +490,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 +603,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,
+3 -3
View File
@@ -1,6 +1,6 @@
use crate::{
comments::Comment, db_conn::DbConn, notifications::*, posts::Post, schema::mentions,
users::User, Connection, Error, Result,
comments::Comment, notifications::*, posts::Post, schema::mentions, users::User, Connection,
Error, Result,
};
use activitystreams::{
base::BaseExt,
@@ -60,7 +60,7 @@ impl Mention {
}
}
pub fn build_activity(conn: &DbConn, ment: &str) -> Result<link::Mention> {
pub fn build_activity(conn: &Connection, ment: &str) -> Result<link::Mention> {
let user = User::find_by_fqn(conn, ment)?;
let mut mention = link::Mention::new();
mention.set_href(user.ap_url.parse::<IriString>()?);
+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");
+37 -32
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,
ap_url, blogs::Blog, instance::Instance, medias::Media, mentions::Mention, post_authors::*,
safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User, Connection, Error,
PostEvent::*, Result, CONFIG, POST_CHAN,
};
use activitystreams::{
activity::{Create, Delete, Update},
@@ -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,
iri_percent_encode_seg(&blog.fqn),
iri_percent_encode_seg(slug)
))
}
@@ -373,7 +373,7 @@ impl Post {
}))?;
article.set_source(source);
article.set_published(
OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos().into())
OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos_opt().unwrap().into())
.expect("OffsetDateTime"),
);
article.set_summary(&*self.subtitle);
@@ -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| {
@@ -615,15 +615,15 @@ impl Post {
}
}
impl FromId<DbConn> for Post {
impl FromId<Connection> for Post {
type Error = Error;
type Object = LicensedArticle;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
fn from_db(conn: &Connection, id: &str) -> Result<Self> {
Self::find_by_ap_url(conn, id)
}
fn from_activity(conn: &DbConn, article: LicensedArticle) -> Result<Self> {
fn from_activity(conn: &Connection, article: LicensedArticle) -> Result<Self> {
let license = article.ext_one.license.unwrap_or_default();
let article = article.inner;
@@ -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()
@@ -817,21 +821,21 @@ impl FromId<DbConn> for Post {
}
}
impl AsObject<User, Create, &DbConn> for Post {
impl AsObject<User, Create, &Connection> for Post {
type Error = Error;
type Output = Self;
fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result<Self::Output> {
fn activity(self, _conn: &Connection, _actor: User, _id: &str) -> Result<Self::Output> {
// TODO: check that _actor is actually one of the author?
Ok(self)
}
}
impl AsObject<User, Delete, &DbConn> for Post {
impl AsObject<User, Delete, &Connection> for Post {
type Error = Error;
type Output = ();
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<Self::Output> {
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<Self::Output> {
let can_delete = self
.get_authors(conn)?
.into_iter()
@@ -855,16 +859,16 @@ pub struct PostUpdate {
pub tags: Option<serde_json::Value>,
}
impl FromId<DbConn> for PostUpdate {
impl FromId<Connection> for PostUpdate {
type Error = Error;
type Object = LicensedArticle;
fn from_db(_: &DbConn, _: &str) -> Result<Self> {
fn from_db(_: &Connection, _: &str) -> Result<Self> {
// Always fail because we always want to deserialize the AP object
Err(Error::NotFound)
}
fn from_activity(conn: &DbConn, updated: Self::Object) -> Result<Self> {
fn from_activity(conn: &Connection, updated: Self::Object) -> Result<Self> {
let mut post_update = PostUpdate {
ap_url: updated
.ap_object_ref()
@@ -919,11 +923,11 @@ impl FromId<DbConn> for PostUpdate {
}
}
impl AsObject<User, Update, &DbConn> for PostUpdate {
impl AsObject<User, Update, &Connection> for PostUpdate {
type Error = Error;
type Output = ();
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> {
let mut post =
Post::from_id(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?;
@@ -1023,6 +1027,7 @@ impl From<PostEvent> for Arc<Post> {
#[cfg(test)]
mod tests {
use super::*;
use crate::db_conn::DbConn;
use crate::inbox::{inbox, tests::fill_database, InboxResult};
use crate::mentions::{Mention, NewMention};
use crate::safe_string::SafeString;
@@ -1036,7 +1041,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 +1049,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 +1058,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 +1077,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"));
@@ -1221,7 +1226,7 @@ mod tests {
let actual = to_value(act)?;
let id = actual["id"].to_string();
let (id_pre, id_post) = id.rsplit_once("-").unwrap();
let (id_pre, id_post) = id.rsplit_once('-').unwrap();
assert_eq!(post.ap_url, "https://plu.me/~/BlogName/testing");
assert_eq!(
id_pre,
+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);
+14 -14
View File
@@ -1,6 +1,6 @@
use crate::{
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::reshares,
timeline::*, users::User, Connection, Error, Result, CONFIG,
instance::Instance, notifications::*, posts::Post, schema::reshares, timeline::*, users::User,
Connection, Error, Result, CONFIG,
};
use activitystreams::{
activity::{ActorAndObjectRef, Announce, Undo},
@@ -113,11 +113,11 @@ impl Reshare {
}
}
impl AsObject<User, Announce, &DbConn> for Post {
impl AsObject<User, Announce, &Connection> for Post {
type Error = Error;
type Output = Reshare;
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Reshare> {
fn activity(self, conn: &Connection, actor: User, id: &str) -> Result<Reshare> {
let conn = conn;
let reshare = Reshare::insert(
conn,
@@ -134,15 +134,15 @@ impl AsObject<User, Announce, &DbConn> for Post {
}
}
impl FromId<DbConn> for Reshare {
impl FromId<Connection> for Reshare {
type Error = Error;
type Object = Announce;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
fn from_db(conn: &Connection, id: &str) -> Result<Self> {
Reshare::find_by_ap_url(conn, id)
}
fn from_activity(conn: &DbConn, act: Announce) -> Result<Self> {
fn from_activity(conn: &Connection, act: Announce) -> Result<Self> {
let res = Reshare::insert(
conn,
NewReshare {
@@ -183,17 +183,17 @@ impl FromId<DbConn> for Reshare {
}
}
impl AsObject<User, Undo, &DbConn> for Reshare {
impl AsObject<User, Undo, &Connection> for Reshare {
type Error = Error;
type Output = ();
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> {
if actor.id == self.user_id {
diesel::delete(&self).execute(&**conn)?;
diesel::delete(&self).execute(conn)?;
// delete associated notification if any
if let Ok(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) {
diesel::delete(&notif).execute(&**conn)?;
diesel::delete(&notif).execute(conn)?;
}
Ok(())
@@ -229,7 +229,7 @@ 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!({
@@ -253,8 +253,8 @@ 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/",
+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,
+18 -16
View File
@@ -108,7 +108,7 @@ mod tests {
let searcher = Arc::new(get_searcher(&CONFIG.search_tokenizers));
SearchActor::init(searcher.clone(), db_pool.clone());
let conn = db_pool.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 +161,43 @@ mod tests {
long_description_html: "<p>Good morning</p>".to_string(),
short_description: SafeString::new("Hello"),
short_description_html: "<p>Hello</p>".to_string(),
name: random_hex().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 blog = NewBlog {
instance_id: instance.id,
actor_id: random_hex(),
ap_url: random_hex(),
inbox_url: random_hex(),
outbox_url: random_hex(),
..Default::default()
};
let 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)?;
+132 -123
View File
@@ -1,5 +1,4 @@
use crate::{
db_conn::DbConn,
lists::List,
posts::Post,
schema::{posts, timeline, timeline_definition},
@@ -12,9 +11,9 @@ use std::ops::Deref;
pub(crate) mod query;
pub use self::query::Kind;
use self::query::{QueryError, TimelineQuery};
pub use self::query::{QueryError, TimelineQuery};
#[derive(Clone, Debug, PartialEq, Queryable, Identifiable, AsChangeset)]
#[derive(Clone, Debug, PartialEq, Eq, Queryable, Identifiable, AsChangeset)]
#[table_name = "timeline_definition"]
pub struct Timeline {
pub id: i32,
@@ -220,9 +219,10 @@ impl Timeline {
.map_err(Error::from)
}
pub fn add_to_all_timelines(conn: &DbConn, post: &Post, kind: Kind<'_>) -> Result<()> {
pub fn add_to_all_timelines(conn: &Connection, post: &Post, kind: Kind<'_>) -> Result<()> {
let timelines = timeline_definition::table
.load::<Self>(conn.deref())
//.load::<Self>(conn.deref())
.load::<Self>(conn)
.map_err(Error::from)?;
for t in timelines {
@@ -246,7 +246,26 @@ impl Timeline {
Ok(())
}
pub fn matches(&self, conn: &DbConn, post: &Post, kind: Kind<'_>) -> Result<bool> {
pub fn remove_post(&self, conn: &Connection, post: &Post) -> Result<bool> {
if self.includes_post(conn, post)? {
return Ok(false);
}
diesel::delete(
timeline::table
.filter(timeline::timeline_id.eq(self.id))
.filter(timeline::post_id.eq(post.id)),
)
.execute(conn)?;
Ok(true)
}
pub fn remove_all_posts(&self, conn: &Connection) -> Result<u64> {
let count = diesel::delete(timeline::table.filter(timeline::timeline_id.eq(self.id)))
.execute(conn)?;
Ok(count as u64)
}
pub fn matches(&self, conn: &Connection, post: &Post, kind: Kind<'_>) -> Result<bool> {
let query = TimelineQuery::parse(&self.query)?;
query.matches(conn, self, post, kind)
}
@@ -282,73 +301,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 +367,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 +422,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 +433,7 @@ mod tests {
.unwrap();
let gnu_post = Post::insert(
&conn,
conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug".to_string(),
@@ -440,10 +449,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 +469,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 +480,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 +492,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 +505,7 @@ mod tests {
.unwrap();
let post = Post::insert(
&conn,
conn,
NewPost {
blog_id: blogs[0].id,
slug: "about-linux".to_string(),
@@ -512,10 +521,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 +542,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 +552,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 +570,7 @@ mod tests {
.unwrap();
let gnu_post = Post::insert(
&conn,
conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug".to_string(),
@@ -579,7 +588,7 @@ mod tests {
.unwrap();
let non_free_post = Post::insert(
&conn,
conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug2".to_string(),
@@ -596,13 +605,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 +623,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 +643,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 +708,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 +795,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 +816,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(())
});
+9 -10
View File
@@ -1,17 +1,16 @@
use crate::{
blogs::Blog,
db_conn::DbConn,
lists::{self, ListType},
posts::Post,
tags::Tag,
timeline::Timeline,
users::User,
Result,
Connection, Result,
};
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 +19,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),
@@ -155,7 +154,7 @@ enum TQ<'a> {
impl<'a> TQ<'a> {
fn matches(
&self,
conn: &DbConn,
conn: &Connection,
timeline: &Timeline,
post: &Post,
kind: Kind<'_>,
@@ -200,7 +199,7 @@ enum Arg<'a> {
impl<'a> Arg<'a> {
pub fn matches(
&self,
conn: &DbConn,
conn: &Connection,
timeline: &Timeline,
post: &Post,
kind: Kind<'_>,
@@ -225,7 +224,7 @@ enum WithList {
impl WithList {
pub fn matches(
&self,
conn: &DbConn,
conn: &Connection,
timeline: &Timeline,
post: &Post,
list: &List<'_>,
@@ -292,7 +291,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 {
@@ -361,7 +360,7 @@ enum Bool {
impl Bool {
pub fn matches(
&self,
conn: &DbConn,
conn: &Connection,
timeline: &Timeline,
post: &Post,
kind: Kind<'_>,
@@ -654,7 +653,7 @@ impl<'a> TimelineQuery<'a> {
pub fn matches(
&self,
conn: &DbConn,
conn: &Connection,
timeline: &Timeline,
post: &Post,
kind: Kind<'_>,
+135 -71
View File
@@ -1,8 +1,8 @@
use crate::{
ap_url, blocklisted_emails::BlocklistedEmail, blogs::Blog, db_conn::DbConn, follows::Follow,
instance::*, medias::Media, notifications::Notification, post_authors::PostAuthor, posts::Post,
safe_string::SafeString, schema::users, timeline::Timeline, Connection, Error, Result,
UserEvent::*, CONFIG, ITEMS_PER_PAGE, USER_CHAN,
ap_url, blocklisted_emails::BlocklistedEmail, blogs::Blog, comments::Comment, db_conn::DbConn,
follows::Follow, instance::*, medias::Media, notifications::Notification,
post_authors::PostAuthor, posts::Post, safe_string::SafeString, schema::users,
timeline::Timeline, Connection, Error, Result, UserEvent::*, CONFIG, ITEMS_PER_PAGE, USER_CHAN,
};
use activitystreams::{
activity::Delete,
@@ -15,7 +15,10 @@ use activitystreams::{
prelude::*,
};
use chrono::{NaiveDateTime, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl};
use diesel::{
self, BelongingToDsl, BoolExpressionMethods, ExpressionMethods, OptionalExtension, QueryDsl,
RunQueryDsl, TextExpressionMethods,
};
use ldap3::{LdapConn, Scope, SearchEntry};
use openssl::{
hash::MessageDigest,
@@ -165,6 +168,14 @@ impl User {
notif.delete(conn)?
}
for comment in Comment::list_by_author(conn, self.id)? {
let delete_activity = comment.build_delete(conn)?;
crate::inbox::inbox(
conn,
serde_json::to_value(&delete_activity).map_err(Error::from)?,
)?;
}
diesel::delete(self)
.execute(conn)
.map(|_| ())
@@ -186,15 +197,16 @@ impl User {
pub fn count_local(conn: &Connection) -> Result<i64> {
users::table
.filter(users::instance_id.eq(Instance::get_local()?.id))
.filter(users::role.ne(Role::Instance as i32))
.count()
.get_result(&*conn)
.get_result(conn)
.map_err(Error::from)
}
pub fn find_by_fqn(conn: &DbConn, fqn: &str) -> Result<User> {
pub fn find_by_fqn(conn: &Connection, fqn: &str) -> Result<User> {
let from_db = users::table
.filter(users::fqn.eq(fqn))
.first(&**conn)
.first(conn)
.optional()?;
if let Some(from_db) = from_db {
Ok(from_db)
@@ -203,6 +215,27 @@ impl User {
}
}
pub fn search_local_by_name(
conn: &Connection,
name: &str,
(min, max): (i32, i32),
) -> Result<Vec<User>> {
users::table
.filter(users::instance_id.eq(Instance::get_local()?.id))
.filter(users::role.ne(Role::Instance as i32))
// TODO: use `ilike` instead of `like` for PostgreSQL
.filter(
users::username
.like(format!("%{}%", name))
.or(users::display_name.like(format!("%{}%", name))),
)
.order(users::username.asc())
.offset(min.into())
.limit((max - min).into())
.load::<User>(conn)
.map_err(Error::from)
}
/**
* TODO: Should create user record with normalized(lowercased) email
*/
@@ -219,7 +252,7 @@ impl User {
.map_err(Error::from)
}
fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result<User> {
fn fetch_from_webfinger(conn: &Connection, acct: &str) -> Result<User> {
let link = resolve(acct.to_owned(), true)?
.links
.into_iter()
@@ -246,20 +279,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 +289,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)
@@ -332,6 +342,42 @@ impl User {
bcrypt::hash(pass, 10).map_err(Error::from)
}
// [[ LDAP non anonymous bind copied from ....
fn ldap_preconn(ldap_conn: &mut LdapConn) -> Result<()> {
let ldap = CONFIG.ldap.as_ref().unwrap();
if let Some((user, password)) = ldap.user.as_ref() {
let bind = ldap_conn
.simple_bind(user, password)
.map_err(|_| Error::NotFound)?;
if bind.success().is_err() {
return Err(Error::NotFound);
}
}
Ok(())
}
fn search_dn(ldap_conn: &mut LdapConn, fil: &str) -> Result<String> {
let mut cn: String = String::new();
let ldap = CONFIG.ldap.as_ref().unwrap();
let (rs, _res) = ldap_conn.search(
&ldap.base_dn,
Scope::Subtree,
fil,
vec!["cn"]
)
.map_err(|_| Error::NotFound)?
.success()
.map_err(|_| Error::NotFound)?;
for entry in rs {
println!("{:?}", SearchEntry::construct(entry.clone()).dn);
cn = SearchEntry::construct(entry).dn;
}
Ok(cn.to_owned())
}
fn ldap_register(conn: &Connection, name: &str, password: &str) -> Result<User> {
if CONFIG.ldap.is_none() {
return Err(Error::NotFound);
@@ -339,7 +385,12 @@ impl User {
let ldap = CONFIG.ldap.as_ref().unwrap();
let mut ldap_conn = LdapConn::new(&ldap.addr).map_err(|_| Error::NotFound)?;
let ldap_name = format!("{}={},{}", ldap.user_name_attr, name, ldap.base_dn);
User::ldap_preconn(&mut ldap_conn)?;
let _filter = format!("(&(objectClass=*)(uid={}))", name).to_owned();
let ldap_name = User::search_dn(&mut ldap_conn, &_filter).unwrap();
//let ldap_name = format!("{}={},{}", ldap.user_name_attr, name, ldap.base_dn);
let bind = ldap_conn
.simple_bind(&ldap_name, password)
.map_err(|_| Error::NotFound)?;
@@ -385,10 +436,18 @@ impl User {
} else {
return false;
};
if User::ldap_preconn(&mut conn).is_err() {
return false;
}
//
let _filter = format!("(&(objectClass=*)(uid={}))", &self.username).to_owned();
let name = User::search_dn(&mut conn, &_filter).unwrap();
/*
let name = format!(
"{}={},{}",
ldap.user_name_attr, &self.username, ldap.base_dn
);
*/
if let Ok(bind) = conn.simple_bind(&name, password) {
bind.success().is_ok()
} else {
@@ -443,7 +502,7 @@ impl User {
}
}
}
// ... ldap-non-anon PR https://git.joinplu.me/Plume/Plume/src/branch/ldap-non-anon ]].
pub fn reset_password(&self, conn: &Connection, pass: &str) -> Result<()> {
diesel::update(self)
.set(users::hashed_password.eq(User::hash_pass(pass)?))
@@ -454,6 +513,7 @@ impl User {
pub fn get_local_page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<User>> {
users::table
.filter(users::instance_id.eq(Instance::get_local()?.id))
.filter(users::role.ne(Role::Instance as i32))
.order(users::username.asc())
.offset(min.into())
.limit((max - min).into())
@@ -931,7 +991,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(())
}
}
@@ -944,15 +1004,15 @@ impl IntoId for User {
impl Eq for User {}
impl FromId<DbConn> for User {
impl FromId<Connection> for User {
type Error = Error;
type Object = CustomPerson;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
fn from_db(conn: &Connection, id: &str) -> Result<Self> {
Self::find_by_ap_url(conn, id)
}
fn from_activity(conn: &DbConn, acct: CustomPerson) -> Result<Self> {
fn from_activity(conn: &Connection, acct: CustomPerson) -> Result<Self> {
let actor = acct.ap_actor_ref();
let username = actor
.preferred_username()
@@ -960,6 +1020,10 @@ impl FromId<DbConn> for User {
.to_string();
if username.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
tracing::error!(
"preferredUsername includes invalid character(s): {}",
&username
);
return Err(Error::InvalidValue);
}
@@ -1049,7 +1113,7 @@ impl FromId<DbConn> for User {
}
}
impl AsActor<&DbConn> for User {
impl AsActor<&Connection> for User {
fn get_inbox_url(&self) -> String {
self.inbox_url.clone()
}
@@ -1065,11 +1129,11 @@ impl AsActor<&DbConn> for User {
}
}
impl AsObject<User, Delete, &DbConn> for User {
impl AsObject<User, Delete, &Connection> for User {
type Error = Error;
type Output = ();
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> {
if self.id == actor.id {
self.delete(conn).map(|_| ())
} else {
@@ -1232,7 +1296,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 +1399,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 +1412,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 +1434,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 +1447,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 +1459,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(())
});
+3 -6
View File
@@ -1,7 +1,7 @@
msgid ""
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
"Project-Id-Version: plume-front\n"
"Report-Msgid-Bugs-To: \n"
\n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
@@ -12,9 +12,6 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
msgid "Do you want to load the local autosave last edited at {}?"
msgstr ""
msgid "Open the rich text editor"
msgstr ""
-1016
View File
File diff suppressed because it is too large Load Diff
+613 -692
View File
File diff suppressed because it is too large Load Diff
+729 -641
View File
File diff suppressed because it is too large Load Diff
+716 -643
View File
File diff suppressed because it is too large Load Diff
+672 -662
View File
File diff suppressed because it is too large Load Diff
-1088
View File
File diff suppressed because it is too large Load Diff
-1016
View File
File diff suppressed because it is too large Load Diff
+740 -632
View File
File diff suppressed because it is too large Load Diff
-1016
View File
File diff suppressed because it is too large Load Diff
-1016
View File
File diff suppressed because it is too large Load Diff
+259 -860
View File
File diff suppressed because it is too large Load Diff
+739 -632
View File
File diff suppressed because it is too large Load Diff
+730 -632
View File
File diff suppressed because it is too large Load Diff
+732 -632
View File
File diff suppressed because it is too large Load Diff
+129 -918
View File
File diff suppressed because it is too large Load Diff
+741 -632
View File
File diff suppressed because it is too large Load Diff
+730 -632
View File
File diff suppressed because it is too large Load Diff
+2 -1023
View File
File diff suppressed because it is too large Load Diff
+333 -829
View File
File diff suppressed because it is too large Load Diff
+85 -970
View File
File diff suppressed because it is too large Load Diff
-1016
View File
File diff suppressed because it is too large Load Diff
+720 -643
View File
File diff suppressed because it is too large Load Diff
+721 -631
View File
File diff suppressed because it is too large Load Diff
-1013
View File
File diff suppressed because it is too large Load Diff
+415 -1003
View File
File diff suppressed because it is too large Load Diff
+717 -641
View File
File diff suppressed because it is too large Load Diff
+500 -744
View File
File diff suppressed because it is too large Load Diff
+727 -651
View File
File diff suppressed because it is too large Load Diff
-942
View File
@@ -11,945 +11,3 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
msgid "Someone"
msgstr ""
msgid "{0} commented on your article."
msgstr ""
msgid "{0} is subscribed to you."
msgstr ""
msgid "{0} liked your article."
msgstr ""
msgid "{0} mentioned you."
msgstr ""
msgid "{0} boosted your article."
msgstr ""
msgid "Your feed"
msgstr ""
msgid "Local feed"
msgstr ""
msgid "Federated feed"
msgstr ""
msgid "{0}'s avatar"
msgstr ""
msgid "Previous page"
msgstr ""
msgid "Next page"
msgstr ""
msgid "Optional"
msgstr ""
msgid "To create a new blog, you need to be logged in"
msgstr ""
msgid "A blog with the same name already exists."
msgstr ""
msgid "Your blog was successfully created!"
msgstr ""
msgid "Your blog was deleted."
msgstr ""
msgid "You are not allowed to delete this blog."
msgstr ""
msgid "You are not allowed to edit this blog."
msgstr ""
msgid "You can't use this media as a blog icon."
msgstr ""
msgid "You can't use this media as a blog banner."
msgstr ""
msgid "Your blog information have been updated."
msgstr ""
msgid "Your comment has been posted."
msgstr ""
msgid "Your comment has been deleted."
msgstr ""
msgid "Registrations are closed on this instance."
msgstr ""
msgid "User registration"
msgstr ""
msgid "Here is the link for registration: {0}"
msgstr ""
msgid "Your account has been created. Now you just need to log in, before you can use it."
msgstr ""
msgid "Instance settings have been saved."
msgstr ""
msgid "{} has been unblocked."
msgstr ""
msgid "{} has been blocked."
msgstr ""
msgid "Blocks deleted"
msgstr ""
msgid "Email already blocked"
msgstr ""
msgid "Email Blocked"
msgstr ""
msgid "You can't change your own rights."
msgstr ""
msgid "You are not allowed to take this action."
msgstr ""
msgid "Done."
msgstr ""
msgid "To like a post, you need to be logged in"
msgstr ""
msgid "Your media have been deleted."
msgstr ""
msgid "You are not allowed to delete this media."
msgstr ""
msgid "Your avatar has been updated."
msgstr ""
msgid "You are not allowed to use this media."
msgstr ""
msgid "To see your notifications, you need to be logged in"
msgstr ""
msgid "This post isn't published yet."
msgstr ""
msgid "To write a new post, you need to be logged in"
msgstr ""
msgid "You are not an author of this blog."
msgstr ""
msgid "New post"
msgstr ""
msgid "Edit {0}"
msgstr ""
msgid "You are not allowed to publish on this blog."
msgstr ""
msgid "Your article has been updated."
msgstr ""
msgid "Your article has been saved."
msgstr ""
msgid "New article"
msgstr ""
msgid "You are not allowed to delete this article."
msgstr ""
msgid "Your article has been deleted."
msgstr ""
msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?"
msgstr ""
msgid "Couldn't obtain enough information about your account. Please make sure your username is correct."
msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr ""
msgid "You are now connected."
msgstr ""
msgid "You are now logged off."
msgstr ""
msgid "Password reset"
msgstr ""
msgid "Here is the link to reset your password: {0}"
msgstr ""
msgid "Your password was successfully reset."
msgstr ""
msgid "To access your dashboard, you need to be logged in"
msgstr ""
msgid "You are no longer following {}."
msgstr ""
msgid "You are now following {}."
msgstr ""
msgid "To subscribe to someone, you need to be logged in"
msgstr ""
msgid "To edit your profile, you need to be logged in"
msgstr ""
msgid "Your profile has been updated."
msgstr ""
msgid "Your account has been deleted."
msgstr ""
msgid "You can't delete someone else's account."
msgstr ""
msgid "Create your account"
msgstr ""
msgid "Create an account"
msgstr ""
msgid "Email"
msgstr ""
msgid "Email confirmation"
msgstr ""
msgid "An email will be sent to provided email. You can continue signing-up via the email."
msgstr ""
msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one."
msgstr ""
msgid "Registration"
msgstr ""
msgid "Check your inbox!"
msgstr ""
msgid "We sent a mail to the address you gave us, with a link for registration."
msgstr ""
msgid "Username"
msgstr ""
msgid "Password"
msgstr ""
msgid "Password confirmation"
msgstr ""
msgid "Media upload"
msgstr ""
msgid "Description"
msgstr ""
msgid "Useful for visually impaired people, as well as licensing information"
msgstr ""
msgid "Content warning"
msgstr ""
msgid "Leave it empty, if none is needed"
msgstr ""
msgid "File"
msgstr ""
msgid "Send"
msgstr ""
msgid "Your media"
msgstr ""
msgid "Upload"
msgstr ""
msgid "You don't have any media yet."
msgstr ""
msgid "Content warning: {0}"
msgstr ""
msgid "Delete"
msgstr ""
msgid "Details"
msgstr ""
msgid "Media details"
msgstr ""
msgid "Go back to the gallery"
msgstr ""
msgid "Markdown syntax"
msgstr ""
msgid "Copy it into your articles, to insert this media:"
msgstr ""
msgid "Use as an avatar"
msgstr ""
msgid "Plume"
msgstr ""
msgid "Menu"
msgstr ""
msgid "Search"
msgstr ""
msgid "Dashboard"
msgstr ""
msgid "Notifications"
msgstr ""
msgid "Log Out"
msgstr ""
msgid "My account"
msgstr ""
msgid "Log In"
msgstr ""
msgid "Register"
msgstr ""
msgid "About this instance"
msgstr ""
msgid "Privacy policy"
msgstr ""
msgid "Administration"
msgstr ""
msgid "Documentation"
msgstr ""
msgid "Source code"
msgstr ""
msgid "Matrix room"
msgstr ""
msgid "Admin"
msgstr ""
msgid "It is you"
msgstr ""
msgid "Edit your profile"
msgstr ""
msgid "Open on {0}"
msgstr ""
msgid "Unsubscribe"
msgstr ""
msgid "Subscribe"
msgstr ""
msgid "Follow {}"
msgstr ""
msgid "Log in to follow"
msgstr ""
msgid "Enter your full username handle to follow"
msgstr ""
msgid "{0}'s subscribers"
msgstr ""
msgid "Articles"
msgstr ""
msgid "Subscribers"
msgstr ""
msgid "Subscriptions"
msgstr ""
msgid "{0}'s subscriptions"
msgstr ""
msgid "Your Dashboard"
msgstr ""
msgid "Your Blogs"
msgstr ""
msgid "You don't have any blog yet. Create your own, or ask to join one."
msgstr ""
msgid "Start a new blog"
msgstr ""
msgid "Your Drafts"
msgstr ""
msgid "Go to your gallery"
msgstr ""
msgid "Edit your account"
msgstr ""
msgid "Your Profile"
msgstr ""
msgid "To change your avatar, upload it to your gallery and then select from there."
msgstr ""
msgid "Upload an avatar"
msgstr ""
msgid "Display name"
msgstr ""
msgid "Summary"
msgstr ""
msgid "Theme"
msgstr ""
msgid "Default theme"
msgstr ""
msgid "Error while loading theme selector."
msgstr ""
msgid "Never load blogs custom themes"
msgstr ""
msgid "Update account"
msgstr ""
msgid "Danger zone"
msgstr ""
msgid "Be very careful, any action taken here can't be cancelled."
msgstr ""
msgid "Delete your account"
msgstr ""
msgid "Sorry, but as an admin, you can't leave your own instance."
msgstr ""
msgid "Latest articles"
msgstr ""
msgid "Atom feed"
msgstr ""
msgid "Recently boosted"
msgstr ""
msgid "Articles tagged \"{0}\""
msgstr ""
msgid "There are currently no articles with such a tag"
msgstr ""
msgid "The content you sent can't be processed."
msgstr ""
msgid "Maybe it was too long."
msgstr ""
msgid "Internal server error"
msgstr ""
msgid "Something broke on our side."
msgstr ""
msgid "Sorry about that. If you think this is a bug, please report it."
msgstr ""
msgid "Invalid CSRF token"
msgstr ""
msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it."
msgstr ""
msgid "You are not authorized."
msgstr ""
msgid "Page not found"
msgstr ""
msgid "We couldn't find this page."
msgstr ""
msgid "The link that led you here may be broken."
msgstr ""
msgid "Users"
msgstr ""
msgid "Configuration"
msgstr ""
msgid "Instances"
msgstr ""
msgid "Email blocklist"
msgstr ""
msgid "Grant admin rights"
msgstr ""
msgid "Revoke admin rights"
msgstr ""
msgid "Grant moderator rights"
msgstr ""
msgid "Revoke moderator rights"
msgstr ""
msgid "Ban"
msgstr ""
msgid "Run on selected users"
msgstr ""
msgid "Moderator"
msgstr ""
msgid "Moderation"
msgstr ""
msgid "Home"
msgstr ""
msgid "Administration of {0}"
msgstr ""
msgid "Unblock"
msgstr ""
msgid "Block"
msgstr ""
msgid "Name"
msgstr ""
msgid "Allow anyone to register here"
msgstr ""
msgid "Short description"
msgstr ""
msgid "Markdown syntax is supported"
msgstr ""
msgid "Long description"
msgstr ""
msgid "Default article license"
msgstr ""
msgid "Save these settings"
msgstr ""
msgid "If you are browsing this site as a visitor, no data about you is collected."
msgstr ""
msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it."
msgstr ""
msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies."
msgstr ""
msgid "Blocklisted Emails"
msgstr ""
msgid "Email address"
msgstr ""
msgid "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com"
msgstr ""
msgid "Note"
msgstr ""
msgid "Notify the user?"
msgstr ""
msgid "Optional, shows a message to the user when they attempt to create an account with that address"
msgstr ""
msgid "Blocklisting notification"
msgstr ""
msgid "The message to be shown when the user attempts to create an account with this email address"
msgstr ""
msgid "Add blocklisted address"
msgstr ""
msgid "There are no blocked emails on your instance"
msgstr ""
msgid "Delete selected emails"
msgstr ""
msgid "Email address:"
msgstr ""
msgid "Blocklisted for:"
msgstr ""
msgid "Will notify them on account creation with this message:"
msgstr ""
msgid "The user will be silently prevented from making an account"
msgstr ""
msgid "Welcome to {}"
msgstr ""
msgid "Nothing to see here yet."
msgstr ""
msgid "About {0}"
msgstr ""
msgid "Runs Plume {0}"
msgstr ""
msgid "Home to <em>{0}</em> people"
msgstr ""
msgid "Who wrote <em>{0}</em> articles"
msgstr ""
msgid "And are connected to <em>{0}</em> other instances"
msgstr ""
msgid "Administred by"
msgstr ""
msgid "Interact with {}"
msgstr ""
msgid "Log in to interact"
msgstr ""
msgid "Enter your full username to interact"
msgstr ""
msgid "Publish"
msgstr ""
msgid "Classic editor (any changes will be lost)"
msgstr ""
msgid "Title"
msgstr ""
msgid "Subtitle"
msgstr ""
msgid "Content"
msgstr ""
msgid "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them."
msgstr ""
msgid "Upload media"
msgstr ""
msgid "Tags, separated by commas"
msgstr ""
msgid "License"
msgstr ""
msgid "Illustration"
msgstr ""
msgid "This is a draft, don't publish it yet."
msgstr ""
msgid "Update"
msgstr ""
msgid "Update, or publish"
msgstr ""
msgid "Publish your post"
msgstr ""
msgid "Written by {0}"
msgstr ""
msgid "All rights reserved."
msgstr ""
msgid "This article is under the {0} license."
msgstr ""
msgid "One like"
msgid_plural "{0} likes"
msgstr[0] ""
msgid "I don't like this anymore"
msgstr ""
msgid "Add yours"
msgstr ""
msgid "One boost"
msgid_plural "{0} boosts"
msgstr[0] ""
msgid "I don't want to boost this anymore"
msgstr ""
msgid "Boost"
msgstr ""
msgid "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article"
msgstr ""
msgid "Comments"
msgstr ""
msgid "Your comment"
msgstr ""
msgid "Submit comment"
msgstr ""
msgid "No comments yet. Be the first to react!"
msgstr ""
msgid "Are you sure?"
msgstr ""
msgid "This article is still a draft. Only you and other authors can see it."
msgstr ""
msgid "Only you and other authors can edit this article."
msgstr ""
msgid "Edit"
msgstr ""
msgid "I'm from this instance"
msgstr ""
msgid "Username, or email"
msgstr ""
msgid "Log in"
msgstr ""
msgid "I'm from another instance"
msgstr ""
msgid "Continue to your instance"
msgstr ""
msgid "Reset your password"
msgstr ""
msgid "New password"
msgstr ""
msgid "Confirmation"
msgstr ""
msgid "Update password"
msgstr ""
msgid "We sent a mail to the address you gave us, with a link to reset your password."
msgstr ""
msgid "Send password reset link"
msgstr ""
msgid "This token has expired"
msgstr ""
msgid "Please start the process again by clicking <a href=\"/password-reset\">here</a>."
msgstr ""
msgid "New Blog"
msgstr ""
msgid "Create a blog"
msgstr ""
msgid "Create blog"
msgstr ""
msgid "Edit \"{}\""
msgstr ""
msgid "You can upload images to your gallery, to use them as blog icons, or banners."
msgstr ""
msgid "Upload images"
msgstr ""
msgid "Blog icon"
msgstr ""
msgid "Blog banner"
msgstr ""
msgid "Custom theme"
msgstr ""
msgid "Update blog"
msgstr ""
msgid "Be very careful, any action taken here can't be reversed."
msgstr ""
msgid "Are you sure that you want to permanently delete this blog?"
msgstr ""
msgid "Permanently delete this blog"
msgstr ""
msgid "{}'s icon"
msgstr ""
msgid "There's one author on this blog: "
msgid_plural "There are {0} authors on this blog: "
msgstr[0] ""
msgid "No posts to see here yet."
msgstr ""
msgid "None"
msgstr ""
msgid "No description"
msgstr ""
msgid "Respond"
msgstr ""
msgid "Delete this comment"
msgstr ""
msgid "What is Plume?"
msgstr ""
msgid "Plume is a decentralized blogging engine."
msgstr ""
msgid "Authors can manage multiple blogs, each as its own website."
msgstr ""
msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon."
msgstr ""
msgid "Read the detailed rules"
msgstr ""
msgid "By {0}"
msgstr ""
msgid "Draft"
msgstr ""
msgid "Search result(s) for \"{0}\""
msgstr ""
msgid "Search result(s)"
msgstr ""
msgid "No results for your query"
msgstr ""
msgid "No more results for your query"
msgstr ""
msgid "Advanced search"
msgstr ""
msgid "Article title matching these words"
msgstr ""
msgid "Subtitle matching these words"
msgstr ""
msgid "Content macthing these words"
msgstr ""
msgid "Body content"
msgstr ""
msgid "From this date"
msgstr ""
msgid "To this date"
msgstr ""
msgid "Containing these tags"
msgstr ""
msgid "Tags"
msgstr ""
msgid "Posted on one of these instances"
msgstr ""
msgid "Instance domain"
msgstr ""
msgid "Posted by one of these authors"
msgstr ""
msgid "Author(s)"
msgstr ""
msgid "Posted on one of these blogs"
msgstr ""
msgid "Blog title"
msgstr ""
msgid "Written in this language"
msgstr ""
msgid "Language"
msgstr ""
msgid "Published under this license"
msgstr ""
msgid "Article license"
msgstr ""
+715 -641
View File
File diff suppressed because it is too large Load Diff

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